-
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
Preserve depth buffer between fill-extrusion layers + optimize render order #5101
Conversation
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.
Sharing a depth buffer between the 3D layers is an elegant solution to #4134 -- kudos to @kkaefer for the idea and @lbud for the implementation.
I'd like to see some refactoring of the draw process to integrate this more cleanly. It seems like it would make the most sense to treat the 3D pass as a full-fledged third rendering pass, preceding the existing opaque and translucent passes, and allowing any layer type to render during it, so that fill extrusions aren't so much of a special case.
Concretely, what that would mean is:
- Introduce an enum type
export type RenderPass = '3d' | 'opaque' | 'translucent'
, replaceisOpaquePass: boolean
withrenderPass: RenderPass
- Merge
draw_fill_extrusion_texture.js
intodraw_fill_extrusion.js
, and do the appropriate thing for the current pass. (BTW, iffill-extrusion-opacity
is 1, can it copy the texture over in the opaque pass?) - Refactor/clean up
render
,renderPass
,render3d
-- I'd suggest just inlining the latter two intorender
. On the native side, I did something similar in Refactor Painter away mapbox-gl-native#9541, and though it makes for a longrender
method, I think it's clearer in the end because it eliminates the complex looping and conditional logic based on which render pass it is.
The benefits would be:
- Future 3d layer types could hook into the 3d pass
- It would get us back to a single draw function per layer type
- Less special-case code for extrusions in
Painter
itself
I'm leaving a couple of smaller comments inline, but ☝️ is the big one.
src/style/style.js
Outdated
@@ -231,6 +233,7 @@ class Style extends Evented { | |||
const layers = deref(this.stylesheet.layers); | |||
|
|||
this._order = layers.map((layer) => layer.id); | |||
this._order3D = layers.filter((layer) => layer.type === 'fill-extrusion').map((layer) => layer.id); |
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.
Could we compute this from _order
when needed, rather than adding additional state?
array.emplaceBack(1, 1); | ||
const buffer = Buffer.fromStructArray(array, Buffer.BufferType.VERTEX); | ||
|
||
const vao = new VertexArrayObject(); |
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 see that this isn't new code, but is there a way to avoid creating a new VAO for each render?
src/render/painter.js
Outdated
if (this.viewportTexture) { | ||
this.gl.deleteTexture(this.viewportTexture); | ||
this.viewportTexture = null; | ||
for (let i = 0; i < this.viewportTextures.length; i++) { |
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.
Use a for ... of
loop here.
1af1936
to
31f6369
Compare
src/render/draw_circle.js
Outdated
@@ -11,7 +11,8 @@ import type TileCoord from '../source/tile_coord'; | |||
module.exports = drawCircles; | |||
|
|||
function drawCircles(painter: Painter, sourceCache: SourceCache, layer: CircleStyleLayer, coords: Array<TileCoord>) { | |||
if (painter.isOpaquePass) return; | |||
const pass = 'translucent'; | |||
if (painter.renderPass !== pass) return; |
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.
In places (x4) where there's no conditionals involved in computing pass
, just compare directly against the string literal.
let sourceCache; | ||
let coords = []; | ||
|
||
for (let i = 0; i < layerIds.length; i++) { |
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.
Are there ways we could simplify the control flow in the body of this loop? For example, move checks such as layer.paint['fill-extrusion-opacity'] === 0
into has3DPass
and set this._prerenderedTextures[layer.id] = null
up front for every layer? Or even eliminate has3Dpass
, have the layers without a 3D pass simply do nothing, and have the 3D layers do the texture setup inside their render
functions? Not sure what exactly is kosher performance-wise, but this code seems like it could be simpler.
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.
have the 3D layers do the texture setup inside their
render
functions
Conceptually I don't prefer this — as it is, the render
functions are mostly just responsible for rendering whatever they have to render to whatever target is bound, and the painter is responsible for managing render targets and switching between them. Especially if we add more types with 3d
passes I'd prefer to have the render target management for all 3D types happen in the painter's 3D pass loop. I did some simplification in 677c261.
src/render/painter.js
Outdated
// texture and clear the depth buffer a few lines down. | ||
} | ||
|
||
let renderTarget = this.viewportTextures.pop(); |
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 looks like this.viewportTextures
will always be an empty array. Am I missing somewhere where textures get released back (say when the layer is removed from the style)?
Instead of having a this._prerenderedTextures
map keyed on layer ID, could fill extrusion layers have a prerenderedTexture
property?
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.
Am I missing somewhere where textures get released back (say when the layer is removed from the style)?
Ah, I forgot this — added in 0651dcf.
In order for fill extrusion layers to have a prerenderedTexture
property, we'd have to make fbos/textures members of style layers, right? Which feels weird…
src/render/painter.js
Outdated
|
||
let sourceCache; | ||
let coords = []; | ||
if (layer.source !== (sourceCache && sourceCache.id)) { |
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 know this bit of code is duplicated in similar form in all three of the passes -- not ideal, but okay for now IMO. Hopefully as part of #4875 we can bring over the system in native where the coordinates are precomputed once for all layers ahead of time.)
const vao = new VertexArrayObject(); | ||
vao.bind(gl, program, buffer); | ||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | ||
} |
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.
Nit: missing newline
src/render/render_texture.js
Outdated
|
||
attachToFramebuffer() { | ||
const gl = this.gl; | ||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0); |
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.
Ideally, we'd be using one framebuffer for each layer (with different color attachments, and the same depth attachment), rather than detaching/reattaching textures to the framebuffer. The reason for this is that it's expensive to "validate" that a framebuffer has correctly bound attachments. It looks like we don't do that yet in JS; in Native, we're running glCheckFramebufferStatus
. Therefore, the API in Native is modeled so that you can't change the attachments of a framebuffer after creation.
http://www.songho.ca/opengl/gl_fbo.html claims that the opposite of what I said is true:
Framebuffer object (FBO) provides an efficient switching mechanism; detach the previous framebuffer-attachable image from a FBO, and attach a new framebuffer-attachable image to the FBO. Switching framebuffer-attachable images is much faster than switching between FBOs.
However, further research suggests that this is outdated information, and no longer true for modern GPUs:
-
Switching an entire FBO is more efficient than switching individual surfaces one at a time.
-
For optimal performance, [...] attachments should not be added or removed once the FBO has been created.
-
Do not create a single FBO and then swap out attachments on it. This causes lots of validation in the driver, which in turn leads to poor performance.
ARM/Mali also has a very good article on framebuffer handling and how it interacts with buffer swapping that also confirms the behavior we've seen of PowerVR GPUs which prompted mapbox/mapbox-gl-native#9286
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.
👀 interesting — thank you for the links @kkaefer! 🛠
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.
@kkaefer I implemented this in 0651dcf. We still have to detach/reattach the depth renderbuffer between framebuffers, so I do wonder about performance gains — before there was one framebuffer with an always-attached renderbuffer and we switched texture attachments, and now there are many framebuffers with their own texture but we switch renderbuffer attachments, so I assume they still do validation every time we attach the depth rbo… 🤔
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.
Hmm, why are we attaching the depth renderbuffer for every render? Can we leave the same renderbuffer attached to multiple framebuffers?
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.
Oh, interesting — okay, I guess I had conceptually assumed that since gl.framebufferRenderbuffer
attaches a renderbuffer to a framebuffer, that attachment was unique to the bound framebuffer. It seems instead based on what you're implying (and manual testing confirms this) that that attaches a renderbuffer to whatever is bound at the gl.FRAMEBUFFER
attachment point at the time of the render?
To me this doesn't read super clearly either way —
attach a renderbuffer object to a framebuffer object
glFramebufferRenderbuffer attaches the renderbuffer specified by renderbuffer as one of the logical buffers of the currently bound framebuffer object
but it does seem to work when I only attach the depth RBO on the first fill-extrusion draw ( 1143dc4 )…
Spoke too soon above — this appeared to work for two layers, but without reattaching the renderbuffer with framebufferRenderbuffer
per fbo, subsequent fbos are only reading from the depth rbo but not writing to them. So as soon as a third layer is introduced you can see how this goes wonky, reading depths from the first but not the second previously rendered layer:
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.
hmm, what could work is upon creation of a framebuffer, attach both the depth RBO + color texture to the FBO, with every FBO having a different texture, but the same depth RBO?
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.
Right — this is how it currently works 👍
1143dc4
to
bb51e2f
Compare
src/render/render_texture.js
Outdated
this.vao = new VertexArrayObject(); | ||
} | ||
|
||
attachRenderbuffer(depthRbo: WebGLRenderbuffer) { |
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.
The name of this method and detachRenderBuffer
is a bit misleading. Can we make DEPTH_ATTACHMENT
a parameter, or change to attachDepthBuffer
/detachDepthBuffer
?
bb51e2f
to
4642796
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.
Your assessments sound reasonable to me. One last question and I think this is ready to go.
src/render/painter.js
Outdated
} | ||
|
||
if (this.depthRboAttached) gl.bindRenderbuffer(gl.RENDERBUFFER, null); |
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.
_setup3DRenderbuffer
always unbinds the renderbuffer, so is this necessary?
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.
Given that we're binding/unbinding framebuffers, I believe we shouldn't be unbinding renderbuffers manually, since this state is part of the FBO.
…er layers + preserve depth buffer between extrusion layers
6060264
to
4bbef4e
Compare
Port of mapbox/mapbox-gl-js#5101: adds a new render pass `Pass3D` before any other rendering wherein we render layers with 3D passes (fill-extrusion layers) to offscreen framebuffers, sharing a depth renderbuffer between those layers in order to render 3D space correctly. Those framebuffers are saved on the RenderLayers and copied back to the map during the translucent pass. Rendering to offscreen framebuffers before we do any clear + draw means we can avoid expensive framebuffer restores.
I have a problem with the new render order. I have a base map at low detail and an overlay with high detail. With this change all low detail buildings are drawn on top and thru the high detail map. Is it possible to add an option to select how the extrusion layer interacts with other extrusions? Like: use z-axis drawing order, use layer drawing order, draw all extrusions. For me its not logical anymore how the drawing works now. In the past I used an UNION ALL to combine extrusions to have a correct drawing, now everything is mixed up. |
This PR changes the way we render fill-extrusion layers:
Before + after (the blue layer is on top of the green layer in the render stack):
Technically this is a "breaking" rendering change⚠️ , though I think we can all agree that it was already broken…
One odd quirk of this PR is the changes made in b155913: on my live maps, this worked whether clearing the depth buffer before binding or rendering any textures, or after rendering and unbinding all textures. However, all fill-extrusion tests were failing to render any fill-extrusions at this point. I'm not sure why clearing the depth buffer only once a texture (color target) is bound fixes the issue for the render tests, because it seems to me the depth clearing should work once the depth renderbuffer is attached whether or not there is a render target bound… 🤷♀️
Launch Checklist