-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Always preserve layer draw order when terrain is enabled #10258
Conversation
…er cache - Revise terrain render loop to be more explicit and easier to follow from the painter render passes: - Remove any painter modifications from render() function - Extract terrain draw depth to reduce branching and add explicit call from painter.render() - Make it explicit that all rendering of terrain happens in the translucent render pass - Update terrain.render() to work as follow: - In render cache mode: . Draw all layers until reaching first non-draped layer . Switch to interleaved render mode (not cached) for the remainder of the layer stack - In non render cache mode: . Draw all layers in interleaved render mode - Add debug/10186 sample code for simple reproduction case
- Add mapbox/satellite-streets-v11 style - Add back mistakenly removed fill-extrusion layer
5929701
to
de39709
Compare
It's great that you're looking into optimizing and improving the render cache! As you mentioned the chosen approach for grouping all drapeable layers into a single big batch was made purely from the rendering performance point of view. Rendering artefacts of this implementation are being masked by enabling the cache only during user interaction (which might not be very effective). I believe this approach is the right way to pursue especially if the drop in cache efficiency is minimal compared to the current implementation. I have few questions though:
|
If draw order is preserved always, do we need I'm concerned about performance. Used this for benchmark (3440 x 1440 external display on MacBook Pro (15-inch, 2018), Radeon Pro 560X 4 GB). main branch (forceDrapeFirst = true): 60 FPS (this is used when panning map) I think this is a way to go. However don't think we can enable it until panning is 60 FPS. Unless we want to force layers reorder until reaching user defined layer always, we can try address this by having precomputed heuristics about style layers that are safe to be reordered (safe as in not producing artifacts) for each style. What style is used in before/after videos? |
Thanks for testing @astojilj, what happens is that streets-v11 has a symbol layer named 'tunnel-oneway-arrow-blue' that only appears at very high zooms (so that this layer becomes interleaved at high zooms which breaks the assumption about the draped layers being nicely packed..). The render cache efficiency becomes ~30% at these zooms, this went through my testing due to not considering hidden layers in the cache efficiency, so thanks for finding that! (which is also what @mpulkki-mapbox has found in 1.)
Yes due to the above it might be worth investigating that option as well. So I guess we're left with these alternative approaches for draw order correctness (during interactive/moving frames): (1) Cache each renderable batch of sequential draped While this approach is: (4) Cache draped layers until reaching first non draped I think (2) is worth looking at, as this issue is most immediate and strongly visible for user defined layers that are not part of the style (programmatically added). We can investigate (1) and (3) otherwise.
That's a good idea but I wonder how hard it would become to assess which artifacts are strong enough to reject the layer from being reordered, unless there is some kind of user defined control for it.
The example from the video is from https://docs.mapbox.com/mapbox-gl-js/example/visualize-population-density/, but we really have that issue for any layer user-defined, while style layer that are in-place have minimal visual impact (cul-de-sac example, one way arrow...). |
@astojilj @mpulkki-mapbox here are some more details about what approach (1) or (3) may look like, it's a list of cacheable draped layer batches (for the set of styles streets-v11, satellite-streets-v11, light-v10, dark-v10, outdoors-v11, mapbox/cjerxnqt3cgvp2rmyuxbeqme7, mapbox-map-design/ckhqrdj3q0q1y19ko8t70qtqu, mapbox-map-design/ckhqrf2tz0dt119ny6azh975y, mapbox-map-design/ckhqrbxlc1awj19svtb92m0bd):
It is not immediately obvious that we would benefit to go with (1) for correctness of draw order, as some of these styles may still require a lot of batches. This also does not look promising for (3) as well, as the largest batch may not be large large enough to warrant caching when the style is really interleaved. For (4) here's the updated render cache efficiency from this PR which confirms only rendering the first batch isn't possible for performance, it's now inclusive of hidden layers that may usually appear at high zooms (higher is better):
For (2) Always drape first until reaching user defined layers, I wonder if this warrants minimal control, what do you think of extending addLayer like so if we were to choose that approach:
Having this flag defaulting to true (current behavior), and otherwise prevents this layer to be moved/reordered during caching phases, allowing user defined layers draw order to be honored, while not damaging performance for other in-place style layers. |
@karimnaaji I'm a little surprised there are so many layers breaking the draping in strict order btw. Is this something that is needed? Or should we also look into the styles and see if this can be resolved? |
Yes I believe streets and outdoors are the worst case scenario at z+17 from the styles I have tested, some non-draped layers become interleaved at these zooms due to the hidden property being dependent on the zoom, at low zoom these layers have a nice ordering of sequential draped then non-draped.
The major use case this is needed for is user defined layers. For basemap styles, we've worked with map design to make a few styles nicely ordered and optimized for use with the render cache drape first approach in interactive mode (3D Terrain RGB Outdoors, 3D Terrain RGB Outdoors, 3D Terrain Satellite). While it's possible to fix it in the style, it has two main disadvantages:
re: approach (1) and (3): I prototyped both of these approaches (reminder that it's Cache each renderable batch of sequential draped and Cache largest renderable batch of sequential draped). Caching each renderable batch is better than only caching the largest one, and while this fixes draw order issue, it does not come close to main performance in both cases but is better than this PR. On a very large viewport (Intel Iris Plus Graphics 655), streets-v11 z+17 (one of the worst case):
@mpulkki-mapbox , @astojilj , @ivovandongen Considering the above I'd like to discuss more proposition (2) Always drape first until reaching user defined layers, for programmatic styles add a user defined flag controlling whether or not to drape first. There might be other options that improves draw order that I have not considered, so please let me know! |
cc @ansis @mourner @arindam1993 for feedback on the potential solution targeting user defined layers (especially concerns around style reloading/diffing). |
This may not be a popular opinion, but I'm always in favor of correctness and determinism even if there is a slight performance impact. I think (1) (multiple fbo's for contiguous sets of drapeable layers) is the way to go, the performance impact is the least, and the path to optimizing it in a style is very clear. We can totally provide warning messages with exact layer-ids and recommend pushing them to the top. I also don't think we should distinct programmatically added layers from initially defined layers, we should always treat the style as a single declarative thing. And in a use case like studio there isn't really a distinction between whats user defined and whats not. |
It is good to address here approach for existing styles as well as discussing set of rules to integrate to Studio for later (as @arindam1993 mentioned) terrain specific styles. I might overemphasize the importance of use case "User new to Mapbox, wants to have high performance terrain map". So, I'll focus on existing vanilla styles with one user defined layer.
In this case, I was referring to no visual artifacts, order correct results. You might have understood it as acceptable artifacts. Heuristics would cover this knowledge for existing styles: E.g. tunnel symbols (tunnel-oneway-arrow-blue, tunnel-oneway-arrow-white) come with significant performance penalty here as we have theoretically have drape-immediate-drape-immediate-drape framebuffer switches and expensive GPU load store. In streets style (no translate) can tunnel symbols be safely reordered to be after road lines and together with road symbols? I have never seen tunnel symbol under road lines or road symbols (collision check doesnt allow it) - could it happen with vanilla styles? Same screenshot: beforementioned turning-feature-outline we can resolve (drape symbol while preventing cut on border issue). Next performance issue: We have a lot of breaks of drape-immediate sequences here and golf, road and bridges don't appear in the same place on map. Seems safe to group symbols together. cc @mapbox/map-design-studio Performance cost is subjective. For some cases 30 FPS is acceptable, for others only 90+. This is also directly related to CPU consumption/battery life/how warm is device when using map/ too. We must have option to enable 90+ FPS while keeping user defined layers rendered correctly.
Seems complicated to handle it with layer granularity. |
Definitely worth looking more at this option, something that may come up is making higher use of texture space. As we would be caching draped batches instead of all draped for a single tile (tile <=> 1 framebuffer in main, whereas we would have tile <=> number of batch framebuffers). We're already quite tight on framebuffer memory on mobile so we'd have to be careful on that.
Yes, that's how I understood this, this makes sense to approach it as you proposed. Especially if we provide some sort of warning to determine best placement.
Thinking more about that I agree, I like the option you proposed MapOptions.optimizeForTerrain:
👍 |
We can also use one large framebuffer woth each stack tiled inside it, this would take away the cost of switching framebuffers constantly. @mpulkki-mapbox aren't you doing something like this in native? |
Minor syntax tweak
This should be addressed in separate PR, needs to take into account video source type and feature state changes in order to invalidate the render cache.
@astojilj @arindam1993 @mpulkki-mapbox , updated the PR as per discussion and suggestions to have optimizeForTerrain map option as well as method (1). The optimization method is now explicit through this option instead of depending on the map state, and is set to cache sequential batches of draped layers while using draw order priority. Other work from native will help improve batching performance in draw order priority:
|
@@ -177,11 +178,87 @@ | |||
] | |||
} | |||
}); | |||
|
|||
var highlightLayer = { |
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.
Added terrain-debug demo with a new custom layer for manual testing and debugging.
@@ -0,0 +1,131 @@ | |||
<!DOCTYPE html> |
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 a simple example with very few layers to test draw ordering.
@@ -174,6 +174,8 @@ function drawTerrainRaster(painter: Painter, terrain: Terrain, sourceCache: Sour | |||
} | |||
|
|||
function drawTerrainDepth(painter: Painter, terrain: Terrain, sourceCache: SourceCache, tileIDs: Array<OverscaledTileID>) { | |||
assert(painter.renderPass === 'offscreen'); |
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.
Moved depth drawing in offscreen pass to reduce switching framebuffer from main as suggested by @astojilj .
@@ -380,7 +398,8 @@ export class Terrain extends Elevation { | |||
} | |||
|
|||
const options = this.painter.options; | |||
this.drapeFirst = (options.zooming || options.moving || options.rotating || !!this.forceDrapeFirst) && !this._invalidateRenderCache; | |||
this.renderCached = (options.zooming || options.moving || options.rotating || !!this.forceRenderCached) && !this._invalidateRenderCache; |
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 remove check against options.zooming || options.moving || options.rotating. I opted to leave as is in this PR, invalidation of cache will need to happen on video update event and feature state to make that work properly. I'll create a follow-up ticket for this.
@@ -580,34 +598,17 @@ export class Terrain extends Elevation { | |||
program.setTerrainUniformValues(context, uniforms); | |||
} | |||
|
|||
// If terrain handles layer rendering (rasterize it), return true. | |||
renderLayer(layer: StyleLayer, _?: SourceCache): boolean { |
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.
Removed this method and replaced it with batched rendering in the main render loop, the reason is to make the render commands and drawn layer range more explicit while reducing potential branching.
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, this is much clearer 💯
style.on('data', this._onStyleDataEvent.bind(this)); | ||
style.on('neworder', this._checkRenderCacheEfficiency.bind(this)); |
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.
Do we really need this neworder
event? I think its fine to not have it
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 would also prefer without, but this event is only necessary when order changes after setting the style in terrain, I'll look into ways to bypass that but we might have to keep this one.
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.
Just one major nit with.hasLayerType
looping over all layers constantly, other than that changes looking v good @karimnaaji !
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.
LGTM, nice work!
Terrain currently may not preserve draw order, it is a side-effect of an optimization that caches per-tile draped renders on interactive frames. See below for a visual representation of the issue.
Before / After
bug.mov
fix.mov
Terrain render cache & performance
The render cache optimization caches the result from offscreen framebuffer render during interactive frames. This is done only while there is user interaction or camera movement as animation changes are hardly noticeable during such frames. In cached mode, it renders all possible draped results in a single tile texture and reuses the result on subsequent frames. For interactive frames, it is done by batching all draped layers in a single texture and then drawing all other non-draped layers, while non-interactive frames preserve draw order by always interleaving and not caching results. As mentioned in #10186, it is critical for terrain performance as it highly reduces the number of draw call overhead.
Approach
During a frame, this PR proposes to change the terrain render cache to drape all layers in a cached result only until reaching the end of the batch, instead of grouping them always. Later in the frame, it then simply switches to regular draped/undraped interleaved mode.
This does not impact performance for base styles, as an observation we can make is most styles have draped layers at the bottom of the stack, and non-draped layers with a higher draw order (this is tested in all mapbox basemap styles by calculating the render cache efficiency which answers the question: How many layers are appropriately placed for the render cache to be optimal? this results in a 100% cache efficiency for all mapbox base maps and no performance regression in such situations).
Programmatically added draped layers are usually not as numerous as basemap draped layers such that the performance impact of not caching them remains minimal, for the tradeoff of correctness in layer ordering.
An alternative approach would be to allocate each possibly renderable batch in between undraped layers for a render cache entry. It does not seem worthwhile due to the observations above and the complexity involved with allowing multiple cache entries per tile.
Launch Checklist
@mapbox/map-design-team
@mapbox/static-apis
if this PR includes style spec API or visual changesmapbox-gl-js
changelog:<changelog>Improve terrain support by always preserving layer draw order</changelog>