Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Preserve depth buffer between 3D layers + optimize render order #9931

Merged
merged 14 commits into from
Sep 21, 2017

Conversation

lbud
Copy link
Contributor

@lbud lbud commented Sep 7, 2017

Port of mapbox/mapbox-gl-js#5101:

Also fixes #9978 (I think)…

@lbud
Copy link
Contributor Author

lbud commented Sep 7, 2017

😳 looks like all the various ways the different CI runs were failing were all because of d965338, so this is ready for review. A few things I want to point out:

One thing I want to flag is that I removed a few of the methods I added in the original fill-extrusion PR, as they're no longer needed here:

  • Context::createFramebuffer(const Texture&, const Renderbuffer<RenderbufferType::DepthComponent>&) (here) (the DepthComponent RBO type was added for extrusions)
  • Removed the optional OffscreenTextureAttachment type parameter from the OffscreenTexture constructor (here) / (here)

While these could be breaking changes if anything externally was using these APIs, I don't imagine they were being used anywhere other than fill-extrusion layers — do you think it's fine to remove these or do they need to be preserved just in case?

@lbud lbud changed the title [not ready] Preserve depth buffer between 3D layers + optimize render order Preserve depth buffer between 3D layers + optimize render order Sep 7, 2017
entry.second->startRender(parameters);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drawing the stencil mask before the 3D pass doesn't seem necessary since we're not using it, right? Drawing the clipping mask into the stencil buffer before means that we're drawing to the FBO before the clear step, which means that the OpenGL implementation has to restore the previous frame's stencil buffer (and it might potentially produce inaccurate clipping masks).

Copy link
Contributor Author

@lbud lbud Sep 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right — I didn't dig into startRender fully, but it looks like it both generates clipping IDs (I don't know how this works) and uploads buffers. (Without moving this step before drawing, the fill-extrusion primitive drawing runs into EXC_BAD_ACCESS here: )
image
And then like I said I know nothing about how the stencil drawing works but it at least reads like the actual rendering of stencil masks is still done in a separate step:

// - CLIPPING MASKS ----------------------------------------------------------------------------
// Draws the clipping masks to the stencil buffer.
{
MBGL_DEBUG_GROUP(parameters.context, "clipping masks");
static const style::FillPaintProperties::PossiblyEvaluated properties {};
static const FillProgram::PaintPropertyBinders paintAttibuteData(properties, 0);
for (const auto& clipID : parameters.clipIDGenerator.getClipIDs()) {

If that's the case, maybe copying the descriptions of this (comments + debug group) exactly is misleading and should be updated to sound more like "upload buffers + generate clipping IDs." Or does uploading and generating clipping IDs need to be separated?

Copy link
Contributor

@kkaefer kkaefer Sep 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it looks like the code in the (old) CLIPPING MASKS section does a few things: startRender collects the clipping IDs (CPU code only, no OpenGL involved here), and uploads buffers (OpenGL is involved here), then proceeds to actually render the clipping mask. I think we can split this up so we have the following code flow:

  • Bind
  • Call upload/startRender
  • 3D pass
  • Clear

Looking at the current code, this is already how it's set up. I assumed that startRender would actually be drawing, but that turned out to be false.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I consolidated this group with the "upload" group above it in 9e786c1.

if (!imagePosA || !imagePosB) {
return;
renderTexture->bind();
renderTexture->attachRenderbuffer(*parameters.staticData.depthRenderbuffer);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of first creating the texture and FBO (inside OffscreenTexture), validating + binding, then attaching the framebuffer (which requires revalidation), we should pass the depthRenderbuffer as an (optional) argument to OffscreenTexture construction so that we can internally call context.createFramebuffer(const Texture&, const Renderbuffer<RenderbufferType::DepthStencil>&) instead of exposing an additional bindRenderbuffer.

Additionally, it looks like the depth renderbuffer is attached on every frame (which requires revalidation on every frame). Moving it to the constructor would solve that problem as well.

class OffscreenTexture {
public:
OffscreenTexture(gl::Context&,
Size size = { 256, 256 },
OffscreenTextureAttachment type = OffscreenTextureAttachment::None);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per above, instead of passing in a type here, we could optionally pass in a gl::Renderbuffer<gl::RenderbufferType::DepthComponent> reference (or rather have two constructors, one with and one without).

if (!parameters.staticData.depthRenderbuffer ||
parameters.staticData.depthRenderbuffer->size != size) {
parameters.staticData.depthRenderbuffer =
parameters.context.createRenderbuffer<gl::RenderbufferType::DepthComponent>(size);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before creating any objects here, we should first check if there are even extrusion layers in the stylesheet. Otherwise, all of those resources + backend binding won't be necessary.

using type = std::integral_constant<RenderbufferType, renderbufferType>;
Size size;
gl::UniqueRenderbuffer renderbuffer;
UniqueRenderbuffer renderbuffer;
bool dirty;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please document what "dirty" means in this context? (or alternatively write convenience/wrapper functions around clearing the renderbuffer).

@kkaefer
Copy link
Contributor

kkaefer commented Sep 8, 2017

While these could be breaking changes if anything externally was using these APIs, I don't imagine they were being used anywhere other than fill-extrusion layers — do you think it's fine to remove these or do they need to be preserved just in case?

gl::Context is internal only, so we don't need to worry about breaking APIs.

Lauren Budorick added 2 commits September 8, 2017 13:16
* Add convenience methods to clarify use of renderbuffer::dirty
* Add OffscreenTexture constructor with rbo ref param so that initial framebuffer creation/binding can bind depth rbo at the same time
// depth rbo between them.
if (parameters.staticData.has3D) {
MBGL_DEBUG_GROUP(parameters.context, "3d");
parameters.backend.bind();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this call isn't necessary. Binding the backend means that the default framebuffer is going to be bound and that its viewport will be set accordingly. However, in the first call to RenderFillExtrusionLayer::render(), we're calling renderTexture->bind();, which binds the OffscreenTexture's framebuffer.

parameters.backend.bind();
parameters.pass = RenderPass::Pass3D;

const auto size = parameters.context.viewport.getCurrentValue().size;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parameters.context contains the value of the current OpenGL state (unless it is dirty). Instead of using this value, we should use the canonical value from parameters.state.getSize(), since the OpenGL state's value could be dirty or set to some other value if we don't bind the backend first (see above).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kkaefer interesting — when I change this to parameters.state.getSize() here (renderbuffer creation) and in framebuffer/texture creation

if (parameters.staticData.has3D) {
MBGL_DEBUG_GROUP(parameters.context, "3d");
parameters.backend.bind();
parameters.pass = RenderPass::Pass3D;
const auto size = parameters.context.viewport.getCurrentValue().size;
if (!parameters.staticData.depthRenderbuffer ||
parameters.staticData.depthRenderbuffer->size != size) {
parameters.staticData.depthRenderbuffer =
parameters.context.createRenderbuffer<gl::RenderbufferType::DepthComponent>(size);
}
parameters.staticData.depthRenderbuffer->shouldClear(true);

if (parameters.pass == RenderPass::Pass3D) {
const auto size = parameters.context.viewport.getCurrentValue().size;
if (!renderTexture || renderTexture->getSize() != size) {
renderTexture = OffscreenTexture(parameters.context, size, *parameters.staticData.depthRenderbuffer);
}

on my iPhone, it appears you're right that this value is different, but that the dimensions of parameters.state.getSize() are half those of the others (non-retina). If I change them, fill-extrusion layers degrade to non-retina quality…

checkFramebuffer();
return { depthTarget.size, std::move(fbo) };
return { color.size, std::move(fbo) };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only formatting/variable names changed here (except for size equality check); can we avoid this change please?

@kkaefer kkaefer added Core The cross-platform C++ core, aka mbgl rendering labels Sep 11, 2017
@lbud
Copy link
Contributor Author

lbud commented Sep 11, 2017

@kkaefer (commenting here rather than inline as they're getting collapsed and hard to find):

In the 3D pass (permalinking HEAD~1 here):

// - 3D PASS -------------------------------------------------------------------------------------
// Renders any 3D layers bottom-to-top to unique FBOs with texture attachments, but share the same
// depth rbo between them.
if (parameters.staticData.has3D) {
MBGL_DEBUG_GROUP(parameters.context, "3d");
parameters.backend.bind();
parameters.pass = RenderPass::Pass3D;
const auto size = parameters.context.viewport.getCurrentValue().size;
if (!parameters.staticData.depthRenderbuffer ||
parameters.staticData.depthRenderbuffer->size != size) {
parameters.staticData.depthRenderbuffer =
parameters.context.createRenderbuffer<gl::RenderbufferType::DepthComponent>(size);
}
parameters.staticData.depthRenderbuffer->shouldClear(true);
uint32_t i = static_cast<uint32_t>(order.size()) - 1;
for (auto it = order.begin(); it != order.end(); ++it, --i) {
parameters.currentLayer = i;
if (it->layer.hasRenderPass(parameters.pass)) {
MBGL_DEBUG_GROUP(parameters.context, it->layer.getID());
it->layer.render(parameters, it->source);
}
}
// The main backend/framebuffer will be rebound in the clear step.
}

  • In live rendering, changing parameters.context.viewport.getCurrentValue().size to parameters.state.getSize() causes non-retina (degraded) rendering for fill-extrusion layers
  • In headless rendering, failing to bind the backend before calling parameters.context.viewport.getCurrentValue().size throws an error because the viewport is uninitialized, so size = {0, 0}. On the other hand using parameters.state.getSize() without binding the backend allows all tests to pass.

🤔

@kkaefer
Copy link
Contributor

kkaefer commented Sep 13, 2017

In live rendering, changing parameters.context.viewport.getCurrentValue().size to parameters.state.getSize() causes non-retina (degraded) rendering for fill-extrusion layers

Good point. We have two different sizes; the canonical size, and the actual framebuffer size. In order to render at the correct resolution, we need the framebuffer size. In your initial code, you obtained that from parameters.context.viewport.getCurrentValue().size, but as you noted, that only works when setting the backend (which also updates this value). However, this is a bad idea since we bind the screen first just to unbind and bind the extrusions FBO in the next step. What we'd need here is a way to obtain the viewport size without actually calling bind. Since the only place that knows about the correct viewport size is the Backend, we'd need to add a call to backend that allows retrieving the size without going through OpenGL state.

@lbud
Copy link
Contributor Author

lbud commented Sep 19, 2017

I think this is ready — @kkaefer if you agree then I'll ping a few mobile folks for 👀 on the small platform-specific changes (getFramebufferSize) here.

@lbud
Copy link
Contributor Author

lbud commented Sep 20, 2017

@tobrun or @Guardiola31337 — can I bug one of you for a quick glance at the Android files changed here? It's pretty straightforward but I have zero Android experience/devices to test on.

@lbud
Copy link
Contributor Author

lbud commented Sep 20, 2017

@brunoabinader can I bug you for a quick glance at the Qt changes here? They're pretty straightforward but I haven't tested them on any Qt device 😬

@boundsj
Copy link
Contributor

boundsj commented Sep 20, 2017

Tagging @fabian-guerra for iOS.

@lbud I think the changes in MGLMapView are small. Do you need @fabian-guerra to help test on device?

@lbud
Copy link
Contributor Author

lbud commented Sep 20, 2017

Thanks @boundsj! I tested on my own 🍎 devices so I feel comfortable with the ios/macos changes functionally.

Copy link
Member

@tobrun tobrun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested on 3 Android devices, didn't notice any regressions.

Copy link
Member

@brunoabinader brunoabinader left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working great on Qt! 🚢

auto fbo = createFramebuffer();
bindFramebuffer = fbo;
// TODO: revert this; just for debugging on CI:
GLuint depthInt = depthTarget.renderbuffer;
Log::Info(Event::General, "bound framebuffer: %d; renderbuffer name: %d", bindFramebuffer, depthInt);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got this on Qt app's stdout ⚠️

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops 🙈 good catch, thanks @brunoabinader!

Copy link
Contributor

@fabian-guerra fabian-guerra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iOS/macOS code looks good to me. 👍🏼

@lbud lbud merged commit a9ddf5b into master Sep 21, 2017
@lbud lbud deleted the preserve-depth branch September 21, 2017 21:26
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Core The cross-platform C++ core, aka mbgl rendering
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Shuffling causes fill-extrusion-color/property-function to fail Optimize Extrusion drawing
6 participants