From 47d58aa484bc9e386a5871f445a8f90f4cb2f268 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Fri, 29 Jul 2022 16:22:22 -0700 Subject: [PATCH] Metal: implement MSAA stencil buffers (#5834) --- filament/backend/src/metal/MetalHandles.mm | 45 ++++++++++- filament/backend/test/test_StencilBuffer.cpp | 84 ++++++++++++++++++++ 2 files changed, 126 insertions(+), 3 deletions(-) diff --git a/filament/backend/src/metal/MetalHandles.mm b/filament/backend/src/metal/MetalHandles.mm index 43fd2d4e48..56de08feff 100644 --- a/filament/backend/src/metal/MetalHandles.mm +++ b/filament/backend/src/metal/MetalHandles.mm @@ -850,9 +850,26 @@ MetalRenderTarget::MetalRenderTarget(MetalContext* context, uint32_t width, uint if (stencilAttachment) { stencil = stencilAttachment; + ASSERT_PRECONDITION(stencil.getSampleCount() <= samples, + "MetalRenderTarget was initialized with a MSAA STENCIL texture, but sample count is %d.", + samples); + + auto t = stencil.metalTexture; + const auto twidth = std::max(1u, t->width >> stencil.level); + const auto theight = std::max(1u, t->height >> stencil.level); + tmin = { math::min(tmin.x, twidth), math::min(tmin.y, theight) }; + tmax = { math::max(tmax.x, twidth), math::max(tmax.y, theight) }; attachmentCount++; - // TODO: stencil MSAA + // If we were given a single-sampled texture but the samples parameter is > 1, we create + // a multisampled sidecar texture and do a resolve automatically. + if (samples > 1 && stencil.getSampleCount() == 1) { + auto& sidecar = stencil.metalTexture->msaaSidecar; + if (!sidecar) { + sidecar = createMultisampledTexture(context->device, stencil.getPixelFormat(), + stencil.metalTexture->width, stencil.metalTexture->height, samples); + } + } } // Verify that all attachments have the same non-zero dimensions. @@ -921,8 +938,8 @@ void MetalRenderTarget::setUpRenderPassAttachments(MTLRenderPassDescriptor* desc descriptor.depthAttachment.clearDepth = params.clearDepth; } - const bool automaticResolve = samples > 1 && depthAttachment.getSampleCount() == 1; - if (automaticResolve) { + const bool depthAutomaticResolve = samples > 1 && depthAttachment.getSampleCount() == 1; + if (depthAutomaticResolve) { // We're rendering into our temporary MSAA texture and doing an automatic resolve. // We should not be attempting to load anything into the MSAA texture. assert_invariant(descriptor.depthAttachment.loadAction != MTLLoadActionLoad); @@ -952,6 +969,28 @@ void MetalRenderTarget::setUpRenderPassAttachments(MTLRenderPassDescriptor* desc descriptor.stencilAttachment.storeAction = getStoreAction(params, TargetBufferFlags::STENCIL); descriptor.stencilAttachment.clearStencil = params.clearStencil; } + + const bool stencilAutomaticResolve = samples > 1 && stencilAttachment.getSampleCount() == 1; + if (stencilAutomaticResolve) { + // We're rendering into our temporary MSAA texture and doing an automatic resolve. + // We should not be attempting to load anything into the MSAA texture. + assert_invariant(descriptor.stencilAttachment.loadAction != MTLLoadActionLoad); + assert_invariant(!defaultRenderTarget); + + id sidecar = stencilAttachment.getMSAASidecarTexture(); + assert_invariant(sidecar); + + descriptor.stencilAttachment.texture = sidecar; + descriptor.stencilAttachment.level = 0; + descriptor.stencilAttachment.slice = 0; + const bool discard = any(discardFlags & TargetBufferFlags::STENCIL); + if (!discard) { + descriptor.stencilAttachment.resolveTexture = stencilAttachment.getTexture(); + descriptor.stencilAttachment.resolveLevel = stencilAttachment.level; + descriptor.stencilAttachment.resolveSlice = stencilAttachment.layer; + descriptor.stencilAttachment.storeAction = MTLStoreActionMultisampleResolve; + } + } } MetalRenderTarget::Attachment MetalRenderTarget::getDrawColorAttachment(size_t index) { diff --git a/filament/backend/test/test_StencilBuffer.cpp b/filament/backend/test/test_StencilBuffer.cpp index 5a950db52e..713db0c04b 100644 --- a/filament/backend/test/test_StencilBuffer.cpp +++ b/filament/backend/test/test_StencilBuffer.cpp @@ -189,4 +189,88 @@ TEST_F(BasicStencilBufferTest, DepthAndStencilBuffer) { api.destroyRenderTarget(renderTarget); } +TEST_F(BasicStencilBufferTest, StencilBufferMSAA) { + auto& api = getDriverApi(); + + // Create two textures: a single-sampled color and a MSAA stencil texture. + // We also create two RenderTargets, one for each pass: + // Pass 0: Render a triangle only into the MSAA stencil buffer. + // Pass 1: Render a triangle into (an auto-created) MSAA color buffer using the stencil test. + // Performs an auto-resolve on the color. + auto colorTexture = api.createTexture(SamplerType::SAMPLER_2D, 1, + TextureFormat::RGBA8, 1, 512, 512, 1, TextureUsage::COLOR_ATTACHMENT | TextureUsage::SAMPLEABLE); + auto depthStencilTextureMSAA = api.createTexture(SamplerType::SAMPLER_2D, 1, + TextureFormat::DEPTH24_STENCIL8, 4, 512, 512, 1, TextureUsage::STENCIL_ATTACHMENT | TextureUsage::DEPTH_ATTACHMENT); + auto renderTarget0 = getDriverApi().createRenderTarget( + TargetBufferFlags::DEPTH_AND_STENCIL, 512, 512, 4, + {{}}, {depthStencilTextureMSAA}, {depthStencilTextureMSAA}); + auto renderTarget1 = getDriverApi().createRenderTarget( + TargetBufferFlags::COLOR0 | TargetBufferFlags::DEPTH_AND_STENCIL, 512, 512, 4, + {{colorTexture}}, {depthStencilTextureMSAA}, {depthStencilTextureMSAA}); + + api.startCapture(0); + + // We'll be using a triangle as geometry. + TrianglePrimitive smallTriangle(api); + static filament::math::float2 vertices[3] = { + { -0.5, -0.5 }, + { 0.5, -0.5 }, + { -0.5, 0.5 } + }; + smallTriangle.updateVertices(vertices); + TrianglePrimitive triangle(api); + + // Step 1: Clear the stencil buffer to all zeroes. + // Render a small triangle only to the stencil buffer, increasing the stencil buffer to 1. + RenderPassParams params = {}; + params.flags.clear = TargetBufferFlags::STENCIL; + params.viewport = {0, 0, 512, 512}; + params.clearStencil = 0u; + params.flags.discardStart = TargetBufferFlags::ALL; + params.flags.discardEnd = TargetBufferFlags::NONE; + + PipelineState ps = {}; + ps.program = program; + ps.rasterState.colorWrite = false; + ps.rasterState.depthWrite = false; + ps.rasterState.stencilWrite = true; + ps.rasterState.stencilOpDepthStencilPass = StencilOperation::INCR; + + api.makeCurrent(swapChain, swapChain); + api.beginFrame(0, 0); + + api.beginRenderPass(renderTarget0, params); + api.draw(ps, smallTriangle.getRenderPrimitive(), 1); + api.endRenderPass(); + + // Step 2: Render a larger triangle with the stencil test enabled. + params.flags.clear = TargetBufferFlags::COLOR0; + params.flags.discardStart = TargetBufferFlags::COLOR0; + params.flags.discardEnd = TargetBufferFlags::STENCIL; + params.clearColor = math::float4(0.0f, 0.0f, 1.0f, 1.0f); + ps.rasterState.colorWrite = true; + ps.rasterState.stencilWrite = false; + ps.rasterState.stencilOpDepthStencilPass = StencilOperation::KEEP; + ps.rasterState.stencilFunc = RasterState::StencilFunction::E; + ps.rasterState.stencilRef = 0u; + + api.beginRenderPass(renderTarget1, params); + api.draw(ps, triangle.getRenderPrimitive(), 1); + api.endRenderPass(); + + api.commit(swapChain); + api.stopCapture(0); + api.endFrame(0); + + readPixelsAndAssertHash("StencilBufferAutoResolve", 512, 512, renderTarget1, 0xC7E34C43, true); + + flushAndWait(); + getDriver().purge(); + + api.destroyTexture(colorTexture); + api.destroyTexture(depthStencilTextureMSAA); + api.destroyRenderTarget(renderTarget0); + api.destroyRenderTarget(renderTarget1); +} + } // namespace test \ No newline at end of file