New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Decode tile features to dart:ui
and render in tile space
#12
Conversation
This is really cool. Thanks for the PR! Great to see the performance metrics. Have you looked into whether this will work with isolates? I had avoided any What are your plans with this PR? There are a few things that could use some attention. e.g. CI build is failing and there is at least one spot that looks unfinished ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work!
final double pixelsPerTileUnit; | ||
|
||
@override | ||
LabelSpace get labelSpace => _context.labelSpace; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's unfortunate that we need all of this boilerplate so that we can pass around pixelsPerTileUnilt
. Would it make sense to split out a separate class, e.g. PixelSpaceMapper
that encapsulates the new logic, and make it a member of Context
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
} | ||
|
||
bool isPathWithinTileClip(Path path) { | ||
return tileClip.overlaps(rectFromTileToPixels(path.getBounds())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will get called quite a lot while rendering. How about having a pixelTileClip
member that is converted from tile to pixels once, then we can check overlaps
directly without converting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
lib/src/features/line_renderer.dart
Outdated
logger, | ||
); | ||
|
||
final effectivePaint = style.linePaint!.paint(evaluationContext); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we could use ?.
here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
final lines = feature.lines; | ||
logger.log(() => 'rendering linestring symbol'); | ||
// What if the feature has multiple paths? | ||
final path = feature.paths.first; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the plan here? Will we ever have multiple paths?
Thinking on how LabelSpace
works, this might have the same effect if the first line ends up placing the label (since labels can only appear once in LabelSpace
) but if the first path ends up with a collision with some other label, then another path might cause the label to be rendered.
I'm not sure if this ever worked before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad. I can just do this in the same the previous version did.
Do you remember why you implemented _findMiddleMetric
they way you did? Why not just iterate throuh all PathMetric
s and use the first one that can be used. I could not find anything on the ordering of the result of Path.computeMetrics
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But yes I found instances of symbol linestrings with multiple lines.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
return; | ||
} | ||
|
||
final textAbbr = TextAbbreviator().abbreviate(text); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to spell things out in full to avoid creating a barrier to others reading the code. We should go wth text
or textAbbreviation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
|
||
logger.log(() => 'rendering symbol points'); | ||
|
||
final textApprox = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same re: abbreviations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
With this approach decoding the feature geometry does not work in normal isolates that are spawned by the UI isolate. If decoding in a different isolate is still necessary flutter_isolate could be a path. The raw vector tiles and feature properties could still be decoded in a normal isolate.
I think CI is failing because it uses a new version of Flutter which has a few new analyzer rules. Might be best to fix that first and rebase. |
I couldn’t find docs on it either. I did it this way initially to try it
out. The results were good enough so I didn't come back to it.
…On Mon, Feb 7, 2022 at 7:56 AM Gabriel Terwesten ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In lib/src/features/symbol_line_renderer.dart
<#12 (comment)>
:
> final textPaint = style.textPaint;
final textLayout = style.textLayout;
if (textPaint == null || textLayout == null) {
logger.warn(() => 'line symbol does not have a text paint or layout');
return;
}
- final lines = feature.lines;
- logger.log(() => 'rendering linestring symbol');
+ // What if the feature has multiple paths?
+ final path = feature.paths.first;
My bad. I can just do this in the same the previous version did.
Do you remember why you implemented _findMiddleMetric they way you did?
Why not just iterate throuh all PathMetrics and use the first one that
can be used. I could not find anything on the ordering of the result of
Path.computeMetrics.
—
Reply to this email directly, view it on GitHub
<#12 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AEKDQ7QMK25WBEG3H626AM3UZ7TRJANCNFSM5NXH7KAQ>
.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
You are receiving this because you commented.Message ID:
***@***.***
com>
|
I have addressed most of your comments but still have to add some tests. |
4ca0d46
to
af504e3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really great. Thanks for adding tests.
I've given it a try with my latest flutter-vector-map-tiles experiment and so far the results look good subjectively, and in the profiler.
The implication of this change is that TileFactory
and tileset preprocessing must occur on the UI thread. This will potentially affect latency of tile loading, and it means that we will need to do some expensive work on the UI thread.
Tile loading latency occurs anyways, so a small addition in delay there probably won't matter. It could contribute to flashing when one tile is unloaded and others are not yet loaded. Previously I mitigated this issue by rendering the previous tiles until the new ones are loaded, but ran into a memory issue that caused crashes due to excessive RSS growth. We can continue to explore that path. Another option to mitigate this is improving how the map background is rendered.
As for performing expensive work on the UI thread, a couple of options come to mind that might help to mitigate. We could be careful about when to schedule that work, for example if it's done one at a time in between frames it may not be problematic. Alternatively we could look at running with ui bindings on an isolate, but I'd prefer to avoid that if possible.
Have you had any further ideas about potential impact of this change?
One minor issue below, see comment inline.
|
||
final layer = layers.first; | ||
|
||
context.tileSpaceMapper = TileSpaceMapper( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a reason we can't pass the TileSpaceMapper
in the constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to avoid making tileSpaceMapper
nullable because when it is actually used it is never null
. We have to set it in the ThemeLayer
because before that, we don't know what extent the layer tile has we are rendering. A tileset could have tiles with different extents.
In a next step, we could refactor
Carefully scheduling when to decode the geometry and when to paint new tiles would be my first choice as well. |
That's an excellent idea. If decoding were done on-demand (i.e. lazy), it could reduce the number of features that need to be decoded depending on the theme and whether it renders all of the features in the tile (e.g. with minzoom/maxzoom) Do you know if it's possible to pass a null reference to a dart:ui object to an isolate? If so, we could decode geometry lazily without needing anything else special in the model. Alternatively, we could do the same with So we could end up with something like this: class TileFeature {
final TileFeatureType type;
final Map<String, dynamic> properties;
List<int>? _geometry;
List<dynamic>? _paths;
List<dynamic>? _points;
TileFeature({
required this.type,
required this.properties,
List<int> geometry
}) : _geometry = geometry;
get List<dynamic> paths {
if (_paths == null) {
_paths = decodePaths(_geometry!);
_geometry = null;
}
return _paths!;
}
...
} What do you think? I'm reluctant to merge this PR as-is since just yet since it locks us into a specific threading model that would be hard to undo. To move this forward I think we either need to improve it so that we can use isolates, or some concrete metrics related to its use in a map so that we can understand the impact of this change and a plan to address any shortcomings. What do you think about taking the next step on this PR? |
Based on this issue (flutter/flutter#10647 (comment)) I think that importing and using types from
I didn't even think about that, but that's a nice side effect :) I'll go ahead and refactor |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is fantastic. Great that it was so easy to make it workable with isolates.
To make it work correctly with flutter maps this commit is needed, otherwise we end up attempting to pass ui objects to an isolate.
See a minor question below.
lib/src/model/tile_model.dart
Outdated
} | ||
List<Offset> get points { | ||
if (type != TileFeatureType.point) { | ||
throw Exception('Feature does not have points'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why Exception
here and StateError
below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. They should both be StateError
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
nice work, thanks for the contribution! |
Did you mean to squash when merging? Right now all the fixup commits are in the main branch :) |
Oops :D
I've force-pushed the branch with a single commit, so you may need to reset
your local if you've already pulled from main
Thanks,
David
…On Thu, Feb 10, 2022 at 7:57 AM Gabriel Terwesten ***@***.***> wrote:
Did you mean to squash when merging? Right now all the fixup commits are
in the main branch :)
—
Reply to this email directly, view it on GitHub
<#12 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AEKDQ7X47BVJN2DRVT5M6CLU2PN47ANCNFSM5NXH7KAQ>
.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
You are receiving this because you modified the open/close state.Message
ID: ***@***.***
com>
|
With this change, tile feature geometry is decoded from the raw vector tile geometry to
Path
andOffset
. This means thatPath
s andOffset
s don't have to be constructed during each render pass. Also, no itermediate lists need to be created during decoding.The decoded geometry is in tile space. By rendering in tile space the projection to screen space is performed by the render engine, which can take advantage of GPU acceleration.
Before this change:
After this change: