| /* | |
| * Copyright 2026 Rive | |
| * | |
| * GM test / Phase 4 witness for the post-review fix sweep | |
| * (`/Users/luigi/Projects/plan/Ore/Post-Review Fix Plan.md` Phase 4 | |
| * — locks in the MSAA-resolve fixes from Phase 3 Vulkan + Metal). | |
| * | |
| * Renders the same triangle as `ore_triangle` but with `sampleCount=4` | |
| * MSAA, resolving into the canvas texture. Without working MSAA | |
| * resolve the canvas keeps the clear color (the MSAA buffer is | |
| * rendered but never blitted out) and the GM paints uniform grey. | |
| * With working resolve the triangle is anti-aliased — the diagonal | |
| * edges show smoothed-color pixels rather than hard transitions. | |
| * | |
| * Locks in: | |
| * - Vulkan `pResolveAttachments` wiring (commit `201873659e`): | |
| * pre-fix `subpass.pResolveAttachments` was always null and the | |
| * `resolveTarget` was silently ignored on every Vulkan pipeline. | |
| * - Metal `StoreAndMultisampleResolve` mapping (`8e1cbc9e19`): | |
| * pre-fix any color attachment with `resolveTarget` set | |
| * unconditionally collapsed `storeAction` to | |
| * `MultisampleResolve`, which is fine for this test (we use | |
| * `StoreOp::discard`) but the new branch covers the | |
| * store-and-resolve case the prior code dropped. | |
| */ | |
| #include "gm.hpp" | |
| #include "gmutils.hpp" | |
| #include "ore_gm_helper.hpp" | |
| #if defined(ORE_BACKEND_METAL) || defined(ORE_BACKEND_D3D11) || \ | |
| defined(ORE_BACKEND_D3D12) || defined(ORE_BACKEND_GL) || \ | |
| defined(ORE_BACKEND_WGPU) || defined(ORE_BACKEND_VK) || \ | |
| defined(ORE_BACKEND_RHI) | |
| #include "rive/renderer/render_canvas.hpp" | |
| #include "rive/renderer/ore/ore_buffer.hpp" | |
| #include "rive/renderer/ore/ore_texture.hpp" | |
| #include "rive/renderer/ore/ore_shader_module.hpp" | |
| #include "rive/renderer/ore/ore_pipeline.hpp" | |
| #include "rive/renderer/ore/ore_render_pass.hpp" | |
| #endif | |
| using namespace rivegm; | |
| using namespace rive; | |
| using namespace rive::gpu; | |
| #if defined(ORE_BACKEND_METAL) || defined(ORE_BACKEND_D3D11) || \ | |
| defined(ORE_BACKEND_D3D12) || defined(ORE_BACKEND_GL) || \ | |
| defined(ORE_BACKEND_WGPU) || defined(ORE_BACKEND_VK) || \ | |
| defined(ORE_BACKEND_RHI) | |
| using namespace rive::ore; | |
| #endif | |
| #if defined(ORE_BACKEND_METAL) || defined(ORE_BACKEND_D3D11) || \ | |
| defined(ORE_BACKEND_D3D12) || defined(ORE_BACKEND_GL) || \ | |
| defined(ORE_BACKEND_WGPU) || defined(ORE_BACKEND_VK) || \ | |
| defined(ORE_BACKEND_RHI) | |
| #define ORE_MSAA_RESOLVE_ACTIVE | |
| struct MsaaVertex | |
| { | |
| float x, y; | |
| float r, g, b, a; | |
| }; | |
| static const MsaaVertex kMsaaVertices[] = { | |
| // x y r g b a | |
| {0.0f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f}, // top — red | |
| {-0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f}, // bottom-left — green | |
| {0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 1.0f}, // bottom-right — blue | |
| }; | |
| #endif | |
| class OreMsaaResolveGM : public GM | |
| { | |
| public: | |
| OreMsaaResolveGM() : GM(256, 256) {} | |
| ColorInt clearColor() const override { return 0xff000000; } | |
| void onDraw(rive::Renderer* originalRenderer) override | |
| { | |
| auto renderContext = TestingWindow::Get()->renderContext(); | |
| if (!renderContext || !m_ore.ensureContext(renderContext)) | |
| return; | |
| #ifdef ORE_MSAA_RESOLVE_ACTIVE | |
| auto& ctx = *renderContext->getOreContext(); | |
| // Backends that don't advertise multi-sample support skip the GM — | |
| // no golden produced, mismatch surfaces as a missing-image diff | |
| // rather than a corrupt one. | |
| if (ctx.features().maxSamples < 4) | |
| { | |
| fprintf(stderr, | |
| "[ore_msaa_resolve] skipped: maxSamples=%u < 4\n", | |
| ctx.features().maxSamples); | |
| return; | |
| } | |
| auto canvas = renderContext->makeRenderCanvas(256, 256); | |
| if (!canvas) | |
| { | |
| fprintf(stderr, "[ore_msaa_resolve] makeRenderCanvas failed\n"); | |
| return; | |
| } | |
| // Resolve target — the canvas's single-sample backing texture. | |
| auto resolveView = ctx.wrapCanvasTexture(canvas.get()); | |
| if (!resolveView) | |
| { | |
| fprintf(stderr, | |
| "[ore_msaa_resolve] wrapCanvasTexture failed: %s\n", | |
| ctx.lastError().c_str()); | |
| return; | |
| } | |
| // MSAA color attachment — separate 4× sample texture. | |
| TextureDesc msaaDesc{}; | |
| msaaDesc.width = 256; | |
| msaaDesc.height = 256; | |
| msaaDesc.format = resolveView->texture()->format(); | |
| msaaDesc.type = TextureType::texture2D; | |
| msaaDesc.numMipmaps = 1; | |
| msaaDesc.sampleCount = 4; | |
| msaaDesc.renderTarget = true; | |
| msaaDesc.label = "ore_msaa_resolve_msaa_color"; | |
| auto msaaColor = ctx.makeTexture(msaaDesc); | |
| if (!msaaColor) | |
| { | |
| fprintf(stderr, | |
| "[ore_msaa_resolve] msaaColor makeTexture (sampleCount=4) " | |
| "failed: %s\n", | |
| ctx.lastError().c_str()); | |
| return; | |
| } | |
| TextureViewDesc msaaViewDesc{}; | |
| msaaViewDesc.texture = msaaColor.get(); | |
| msaaViewDesc.dimension = TextureViewDimension::texture2D; | |
| msaaViewDesc.baseMipLevel = 0; | |
| msaaViewDesc.mipCount = 1; | |
| msaaViewDesc.baseLayer = 0; | |
| msaaViewDesc.layerCount = 1; | |
| auto msaaView = ctx.makeTextureView(msaaViewDesc); | |
| if (!msaaView) | |
| { | |
| fprintf(stderr, | |
| "[ore_msaa_resolve] msaaView makeTextureView failed: %s\n", | |
| ctx.lastError().c_str()); | |
| return; | |
| } | |
| // Vertex buffer. | |
| BufferDesc vbDesc{}; | |
| vbDesc.usage = BufferUsage::vertex; | |
| vbDesc.size = sizeof(kMsaaVertices); | |
| vbDesc.data = kMsaaVertices; | |
| vbDesc.label = "ore_msaa_resolve_vbo"; | |
| auto vbo = ctx.makeBuffer(vbDesc); | |
| auto shader = ore_gm::loadShader(ctx, ore_gm::kTriangle); | |
| if (!shader.vsModule) | |
| { | |
| fprintf(stderr, | |
| "[ore_msaa_resolve] loadShader failed: %s\n", | |
| ctx.lastError().c_str()); | |
| return; | |
| } | |
| VertexAttribute attrs[] = { | |
| {offsetof(MsaaVertex, x), 0, VertexFormat::float2}, | |
| {offsetof(MsaaVertex, r), 1, VertexFormat::float4}, | |
| }; | |
| VertexBufferLayout vertexLayout{}; | |
| vertexLayout.stride = sizeof(MsaaVertex); | |
| vertexLayout.stepMode = VertexStepMode::vertex; | |
| vertexLayout.attributes = attrs; | |
| vertexLayout.attributeCount = 2; | |
| PipelineDesc pipeDesc{}; | |
| pipeDesc.vertexModule = shader.vsModule.get(); | |
| pipeDesc.fragmentModule = shader.psModule.get(); | |
| pipeDesc.vertexEntryPoint = shader.vsEntryPoint; | |
| pipeDesc.fragmentEntryPoint = shader.fsEntryPoint; | |
| pipeDesc.vertexBuffers = &vertexLayout; | |
| pipeDesc.vertexBufferCount = 1; | |
| pipeDesc.topology = PrimitiveTopology::triangleList; | |
| pipeDesc.colorTargets[0].format = msaaDesc.format; | |
| pipeDesc.colorCount = 1; | |
| pipeDesc.depthStencil.depthCompare = CompareFunction::always; | |
| pipeDesc.depthStencil.depthWriteEnabled = false; | |
| // The whole point of this GM — a 4×-MSAA pipeline whose render | |
| // pass declares a resolve target. | |
| pipeDesc.sampleCount = 4; | |
| pipeDesc.label = "ore_msaa_resolve_pipeline"; | |
| auto pipeline = ctx.makePipeline(pipeDesc); | |
| if (!pipeline) | |
| { | |
| fprintf(stderr, | |
| "[ore_msaa_resolve] pipeline creation failed: %s\n", | |
| ctx.lastError().c_str()); | |
| return; | |
| } | |
| m_ore.beginFrame(renderContext); | |
| ColorAttachment ca{}; | |
| ca.view = msaaView.get(); | |
| ca.resolveTarget = resolveView.get(); | |
| ca.loadOp = LoadOp::clear; | |
| // We don't need the MSAA buffer after resolve — discard saves | |
| // memory bandwidth on tilers. | |
| ca.storeOp = StoreOp::discard; | |
| ca.clearColor = {0.1f, 0.1f, 0.1f, 1.0f}; | |
| RenderPassDesc rpDesc{}; | |
| rpDesc.colorAttachments[0] = ca; | |
| rpDesc.colorCount = 1; | |
| rpDesc.label = "ore_msaa_resolve_pass"; | |
| auto pass = ctx.beginRenderPass(rpDesc); | |
| pass->setPipeline(pipeline.get()); | |
| pass->setVertexBuffer(0, vbo.get()); | |
| pass->setViewport(0, 0, 256, 256); | |
| pass->draw(3); | |
| pass->finish(); | |
| m_ore.endFrame(renderContext); | |
| ore_gm::invalidateGLStateAfterOre(renderContext); | |
| originalRenderer->save(); | |
| originalRenderer->drawImage(canvas->renderImage(), | |
| {.filter = ImageFilter::nearest}, | |
| BlendMode::srcOver, | |
| 1); | |
| originalRenderer->restore(); | |
| #endif | |
| } | |
| private: | |
| ore_gm::OreGMContext m_ore; | |
| }; | |
| GMREGISTER(ore_msaa_resolve, return new OreMsaaResolveGM()) |