| /* |
| * Copyright 2025 Rive |
| */ |
| |
| // GM test for Ore MRT (Multiple Render Targets). Renders a fullscreen quad |
| // with a shader that outputs different colors to 3 render targets, then |
| // composites all 3 side-by-side. Verifies: MRT render passes, multiple |
| // ColorAttachment, makeTexture with renderTarget, makeTextureView, |
| // and independent per-target output. |
| |
| #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) |
| #include "rive/renderer/render_canvas.hpp" |
| #include "rive/renderer/ore/ore_bind_group.hpp" |
| #include "rive/renderer/ore/ore_buffer.hpp" |
| #include "rive/renderer/ore/ore_texture.hpp" |
| #include "rive/renderer/ore/ore_sampler.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) |
| using namespace rive::ore; |
| #endif |
| |
| class OreMrtGM : public GM |
| { |
| public: |
| // 3 targets side-by-side, each 128x128 = 384x128 total. |
| OreMrtGM() : GM(384, 128) {} |
| |
| ColorInt clearColor() const override { return 0xff000000; } |
| |
| void onDraw(rive::Renderer* originalRenderer) override |
| { |
| auto renderContext = TestingWindow::Get()->renderContext(); |
| if (!renderContext || !m_ore.ensureContext(renderContext)) |
| return; |
| |
| #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) |
| auto& ctx = *renderContext->getOreContext(); |
| constexpr uint32_t kSize = 128; |
| |
| // Create 3 render target textures. |
| rcp<ore::Texture> targets[3]; |
| rcp<ore::TextureView> targetViews[3]; |
| for (int i = 0; i < 3; ++i) |
| { |
| TextureDesc td{}; |
| td.width = kSize; |
| td.height = kSize; |
| td.format = TextureFormat::rgba8unorm; |
| td.renderTarget = true; |
| td.label = "mrt_target"; |
| targets[i] = ctx.makeTexture(td); |
| |
| TextureViewDesc tvd{}; |
| tvd.texture = targets[i].get(); |
| targetViews[i] = ctx.makeTextureView(tvd); |
| } |
| |
| // Load MRT shader from the pre-compiled RSTB. |
| auto mrtShader = ore_gm::loadShader(ctx, ore_gm::kMrt); |
| if (!mrtShader.vsModule) |
| return; |
| |
| PipelineDesc pipeDesc{}; |
| pipeDesc.vertexModule = mrtShader.vsModule.get(); |
| pipeDesc.fragmentModule = mrtShader.psModule.get(); |
| pipeDesc.vertexEntryPoint = mrtShader.vsEntryPoint; |
| pipeDesc.fragmentEntryPoint = mrtShader.fsEntryPoint; |
| pipeDesc.vertexBufferCount = 0; // No vertex buffers, use vertex_id. |
| pipeDesc.topology = PrimitiveTopology::triangleList; |
| pipeDesc.colorTargets[0].format = TextureFormat::rgba8unorm; |
| pipeDesc.colorTargets[1].format = TextureFormat::rgba8unorm; |
| pipeDesc.colorTargets[2].format = TextureFormat::rgba8unorm; |
| pipeDesc.colorCount = 3; |
| pipeDesc.depthStencil.depthCompare = CompareFunction::always; |
| pipeDesc.depthStencil.depthWriteEnabled = false; |
| pipeDesc.label = "mrt_pipeline"; |
| auto mrtPipeline = ctx.makePipeline(pipeDesc); |
| |
| // Render to all 3 targets in a single MRT pass. |
| m_ore.beginFrame(renderContext); |
| |
| RenderPassDesc rpDesc{}; |
| for (int i = 0; i < 3; ++i) |
| { |
| rpDesc.colorAttachments[i].view = targetViews[i].get(); |
| rpDesc.colorAttachments[i].loadOp = LoadOp::clear; |
| rpDesc.colorAttachments[i].storeOp = StoreOp::store; |
| rpDesc.colorAttachments[i].clearColor = {0, 0, 0, 1}; |
| } |
| rpDesc.colorCount = 3; |
| rpDesc.label = "mrt_pass"; |
| |
| auto pass = ctx.beginRenderPass(rpDesc); |
| pass->setPipeline(mrtPipeline.get()); |
| pass->setViewport(0, 0, kSize, kSize); |
| pass->draw(3); // Fullscreen triangle, no VBO needed. |
| pass->finish(); |
| |
| // Now blit each target to a RenderCanvas for compositing. |
| // Load blit shader from the pre-compiled RSTB. |
| auto blitShader = ore_gm::loadShader(ctx, ore_gm::kMrtBlit); |
| if (!blitShader.vsModule) |
| return; |
| |
| auto blitLayout1 = |
| ore_gm::makeLayoutFromShader(ctx, blitShader.vsModule.get(), 1); |
| auto blitLayout2 = |
| ore_gm::makeLayoutFromShader(ctx, blitShader.vsModule.get(), 2); |
| BindGroupLayout* blitLayouts[] = {nullptr, |
| blitLayout1.get(), |
| blitLayout2.get()}; |
| |
| PipelineDesc blitPipeDesc{}; |
| blitPipeDesc.vertexModule = blitShader.vsModule.get(); |
| blitPipeDesc.fragmentModule = blitShader.psModule.get(); |
| blitPipeDesc.vertexEntryPoint = blitShader.vsEntryPoint; |
| blitPipeDesc.fragmentEntryPoint = blitShader.fsEntryPoint; |
| blitPipeDesc.vertexBufferCount = 0; |
| blitPipeDesc.topology = PrimitiveTopology::triangleList; |
| blitPipeDesc.colorTargets[0].format = TextureFormat::rgba8unorm; |
| blitPipeDesc.colorCount = 1; |
| blitPipeDesc.depthStencil.depthCompare = CompareFunction::always; |
| blitPipeDesc.depthStencil.depthWriteEnabled = false; |
| blitPipeDesc.bindGroupLayouts = blitLayouts; |
| blitPipeDesc.bindGroupLayoutCount = 3; |
| blitPipeDesc.label = "blit_pipeline"; |
| auto blitPipeline = ctx.makePipeline(blitPipeDesc); |
| |
| SamplerDesc sampDesc{}; |
| sampDesc.minFilter = Filter::nearest; |
| sampDesc.magFilter = Filter::nearest; |
| sampDesc.label = "blit_sampler"; |
| auto sampler = ctx.makeSampler(sampDesc); |
| |
| // Create a wide canvas (384x128) and blit each target side-by-side. |
| auto canvas = renderContext->makeRenderCanvas(384, kSize); |
| if (!canvas) |
| return; |
| auto canvasView = ctx.wrapCanvasTexture(canvas.get()); |
| |
| RenderPassDesc blitPassDesc{}; |
| blitPassDesc.colorAttachments[0].view = canvasView.get(); |
| blitPassDesc.colorAttachments[0].loadOp = LoadOp::clear; |
| blitPassDesc.colorAttachments[0].storeOp = StoreOp::store; |
| blitPassDesc.colorAttachments[0].clearColor = {0, 0, 0, 1}; |
| blitPassDesc.colorCount = 1; |
| blitPassDesc.label = "blit_pass"; |
| |
| // Create sampler bind group (shared across all blits). |
| BindGroupDesc::SampEntry blitSampEntry{0, sampler.get()}; |
| BindGroupDesc blitSampBGDesc{}; |
| blitSampBGDesc.layout = blitLayout2.get(); |
| blitSampBGDesc.samplers = &blitSampEntry; |
| blitSampBGDesc.samplerCount = 1; |
| auto blitSampBG = ctx.makeBindGroup(blitSampBGDesc); |
| |
| // Blit all 3 targets side-by-side using viewport offsets. |
| for (int i = 0; i < 3; ++i) |
| { |
| if (i > 0) |
| { |
| // Subsequent blits load (don't clear) the canvas. |
| blitPassDesc.colorAttachments[0].loadOp = LoadOp::load; |
| } |
| |
| // Create texture bind group per target. |
| BindGroupDesc::TexEntry blitTexEntry{0, targetViews[i].get()}; |
| BindGroupDesc blitTexBGDesc{}; |
| blitTexBGDesc.layout = blitLayout1.get(); |
| blitTexBGDesc.textures = &blitTexEntry; |
| blitTexBGDesc.textureCount = 1; |
| auto blitTexBG = ctx.makeBindGroup(blitTexBGDesc); |
| |
| auto blitPass = ctx.beginRenderPass(blitPassDesc); |
| blitPass->setPipeline(blitPipeline.get()); |
| blitPass->setBindGroup(1, blitTexBG.get()); |
| blitPass->setBindGroup(2, blitSampBG.get()); |
| blitPass->setViewport(i * kSize, 0, kSize, kSize); |
| blitPass->draw(3); |
| blitPass->finish(); |
| } |
| |
| m_ore.endFrame(renderContext); |
| ore_gm::invalidateGLStateAfterOre(renderContext); |
| |
| // Composite the canvas into the main framebuffer via the original |
| // renderer. No frame break needed — Ore rendered to a separate canvas |
| // texture, not the main render target. |
| originalRenderer->save(); |
| originalRenderer->drawImage(canvas->renderImage(), |
| {.filter = ImageFilter::nearest}, |
| BlendMode::srcOver, |
| 1); |
| originalRenderer->restore(); |
| #endif |
| } |
| |
| private: |
| ore_gm::OreGMContext m_ore; |
| }; |
| |
| GMREGISTER(ore_mrt, return new OreMrtGM()) |