| /* | |
| * Copyright 2026 Rive | |
| * | |
| * GM test for the Rive 2D RenderCanvas → Ore "imported canvas mirror" | |
| * boundary. | |
| * | |
| * What this verifies: | |
| * 1. We render an asymmetric pattern into a Rive 2D RenderCanvas via PLS: | |
| * - top quarter: bright green | |
| * - bottom quarter: bright red | |
| * - middle: dark grey | |
| * The asymmetry is the whole point — if the Y axis is wrong anywhere | |
| * in the chain, green and red swap. | |
| * | |
| * 2. We import the source canvas's GPU texture into Ore via | |
| * RenderContextGLImpl::getCanvasImportMirror() (GL/WebGL only): | |
| * - On Metal/D3D/Vulkan/WebGPU the code is compiled out and we | |
| * sample the source texture directly. | |
| * - On GL/WebGL the GL impl returns a Y-flipped companion texture. | |
| * A subsequent flush() of the source RenderCanvas will hardware | |
| * blit the source into the companion with Y reversed. | |
| * | |
| * 3. We bind the chosen texture (mirror or source) into an Ore pipeline | |
| * using the same image_view WGSL shader the ore_image_view GM uses | |
| * (id = ore_gm::kImageView). The WGSL is authored with V=0 = visual | |
| * top of the texture — i.e. the WGSL author's natural convention. | |
| * | |
| * 4. We render that pipeline into a separate Ore-managed RenderCanvas | |
| * (the "destination"), and composite it into the main framebuffer | |
| * via the original Rive renderer. | |
| * | |
| * On a correctly working backend, the destination should show: | |
| * green at the top, dark grey in the middle, red at the bottom. | |
| * If the import-mirror is broken on GL the colors invert: red on top, | |
| * green at the bottom. If the WGSL UV convention is misinterpreted in | |
| * the shader the same inversion happens. | |
| * | |
| * The GM intentionally uses NO `1.0 - in.uv.y` workarounds in WGSL — the | |
| * whole point is to verify that the cross-backend invariant | |
| * "WGSL top of image at memory row 0" is preserved end-to-end. | |
| */ | |
| #include "gm.hpp" | |
| #include "gmutils.hpp" | |
| #include "ore_gm_helper.hpp" | |
| #include "rive/renderer/rive_renderer.hpp" | |
| #include "common/testing_window.hpp" | |
| #if defined(ORE_BACKEND_METAL) || defined(ORE_BACKEND_GL) || \ | |
| defined(ORE_BACKEND_VK) || defined(ORE_BACKEND_WGPU) || \ | |
| defined(ORE_BACKEND_D3D11) || defined(ORE_BACKEND_D3D12) || \ | |
| defined(ORE_BACKEND_RHI) | |
| #include "rive/renderer/render_canvas.hpp" | |
| #include "rive/renderer/render_context.hpp" | |
| #include "rive/renderer/rive_render_image.hpp" | |
| #if defined(ORE_BACKEND_GL) | |
| #include "rive/renderer/gl/render_context_gl_impl.hpp" | |
| #endif | |
| #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_bind_group.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_GL) || \ | |
| defined(ORE_BACKEND_VK) || defined(ORE_BACKEND_WGPU) || \ | |
| defined(ORE_BACKEND_D3D11) || defined(ORE_BACKEND_D3D12) || \ | |
| defined(ORE_BACKEND_RHI) | |
| using namespace rive::ore; | |
| #endif | |
| #if defined(ORE_BACKEND_METAL) || defined(ORE_BACKEND_GL) || \ | |
| defined(ORE_BACKEND_VK) || defined(ORE_BACKEND_WGPU) || \ | |
| defined(ORE_BACKEND_D3D11) || defined(ORE_BACKEND_D3D12) || \ | |
| defined(ORE_BACKEND_RHI) | |
| #define ORE_CANVAS_IMPORT_ACTIVE | |
| #endif | |
| class OreCanvasImportGM : public GM | |
| { | |
| public: | |
| OreCanvasImportGM() : GM(256, 256) {} | |
| ColorInt clearColor() const override { return 0xff000000; } // black | |
| void onDraw(rive::Renderer* originalRenderer) override | |
| { | |
| auto renderContext = TestingWindow::Get()->renderContext(); | |
| if (!renderContext || !m_ore.ensureContext(renderContext)) | |
| return; | |
| #ifdef ORE_CANVAS_IMPORT_ACTIVE | |
| constexpr uint32_t kSize = 256; | |
| // ── 1. Create the source Rive 2D RenderCanvas and render an | |
| // asymmetric Y pattern into it via Rive PLS. ── | |
| // | |
| // Use the same intercept-and-resume pattern as render_canvas.cpp: | |
| // we hijack the active frame, end it, render into the canvas via | |
| // its own beginFrame/flush cycle, then resume the original frame | |
| // for the final composite. | |
| auto sourceCanvas = renderContext->makeRenderCanvas(kSize, kSize); | |
| if (!sourceCanvas) | |
| return; | |
| // Allocate the GL canvas-import mirror up front, *before* the | |
| // source canvas is flushed. The blit from source → mirror fires | |
| // from RenderContextGLImpl::flush() only when hasMirror == true, | |
| // so the mirror must exist at flush time. Lua gets this ordering | |
| // for free because :view() is called during bind-group setup | |
| // before canvas:endFrame(); the C++ GM has to do it explicitly. | |
| rcp<RiveRenderImage> mirrorImage; | |
| #if defined(ORE_BACKEND_GL) | |
| if (renderContext->platformFeatures().framebufferBottomUp) | |
| { | |
| gpu::Texture* sourceTexEarly = | |
| sourceCanvas->renderImage()->getTexture(); | |
| if (sourceTexEarly != nullptr) | |
| { | |
| auto* glImpl = | |
| renderContext->static_impl_cast<gpu::RenderContextGLImpl>(); | |
| mirrorImage = | |
| glImpl->getCanvasImportMirror(sourceTexEarly, kSize, kSize); | |
| } | |
| } | |
| #endif // ORE_BACKEND_GL | |
| auto originalFrameDescriptor = renderContext->frameDescriptor(); | |
| TestingWindow::Get()->flushPLSContext(); | |
| { | |
| auto canvasFD = originalFrameDescriptor; | |
| canvasFD.renderTargetWidth = kSize; | |
| canvasFD.renderTargetHeight = kSize; | |
| canvasFD.loadAction = gpu::LoadAction::clear; | |
| canvasFD.clearColor = 0xff202020; // dark grey | |
| renderContext->beginFrame(std::move(canvasFD)); | |
| RiveRenderer renderer(renderContext); | |
| // Top quarter: bright green rectangle. (Rive pixel space: | |
| // y=0 is the visual top.) | |
| { | |
| Paint green(0xff00ff00); | |
| PathBuilder p; | |
| p.moveTo(0, 0); | |
| p.lineTo(kSize, 0); | |
| p.lineTo(kSize, kSize / 4.f); | |
| p.lineTo(0, kSize / 4.f); | |
| p.close(); | |
| renderer.drawPath(p.detach(), green); | |
| } | |
| // Bottom quarter: bright red rectangle. | |
| { | |
| Paint red(0xffff0000); | |
| PathBuilder p; | |
| p.moveTo(0, kSize * 3.f / 4.f); | |
| p.lineTo(kSize, kSize * 3.f / 4.f); | |
| p.lineTo(kSize, kSize); | |
| p.lineTo(0, kSize); | |
| p.close(); | |
| renderer.drawPath(p.detach(), red); | |
| } | |
| TestingWindow::Get()->flushPLSContext(sourceCanvas->renderTarget()); | |
| } | |
| // Resume the main frame so the final composite at the bottom of | |
| // this method has a valid frame to draw into. | |
| { | |
| auto mainFD = originalFrameDescriptor; | |
| mainFD.loadAction = gpu::LoadAction::preserveRenderTarget; | |
| renderContext->beginFrame(std::move(mainFD)); | |
| } | |
| // ── 2. Import the source canvas as an Ore-sampleable texture. ── | |
| // | |
| // On GL, the mirror was allocated up front (see top of this | |
| // method) and the blit from source → mirror fired during the | |
| // flushPLSContext(sourceCanvas) call above. The mirror is now | |
| // in sync with the source and ready to sample. | |
| gpu::Texture* sourceTex = sourceCanvas->renderImage()->getTexture(); | |
| if (sourceTex == nullptr) | |
| return; | |
| gpu::Texture* texToWrap = sourceTex; | |
| if (mirrorImage != nullptr) | |
| { | |
| auto* mirrorRive = | |
| lite_rtti_cast<RiveRenderImage*>(mirrorImage.get()); | |
| if (mirrorRive != nullptr && mirrorRive->getTexture() != nullptr) | |
| { | |
| texToWrap = mirrorRive->getTexture(); | |
| } | |
| } | |
| // ── 3. Open the Ore frame. On Vulkan this connects Ore to the | |
| // host's current command buffer so the texture we wrap below | |
| // — and the draw that samples it — land in the same | |
| // submission as Rive's writes into the source canvas. | |
| auto& ctx = *renderContext->getOreContext(); | |
| m_ore.beginFrame(renderContext); | |
| // Wrap the chosen texture as an ore::TextureView. On Vulkan this | |
| // also emits a layout barrier (GENERAL → SHADER_READ_ONLY_OPTIMAL) | |
| // in the current CB and updates Rive's lastAccess tracking. | |
| auto sampledView = ctx.wrapRiveTexture(texToWrap, kSize, kSize); | |
| if (!sampledView) | |
| return; | |
| // ── 4. Build the Ore pipeline (image_view shader = id 2). ── | |
| auto shader = ore_gm::loadShader(ctx, ore_gm::kImageView); | |
| if (!shader.vsModule) | |
| return; | |
| SamplerDesc sampDesc{}; | |
| sampDesc.minFilter = Filter::nearest; | |
| sampDesc.magFilter = Filter::nearest; | |
| sampDesc.label = "ore_canvas_import_sampler"; | |
| auto sampler = ctx.makeSampler(sampDesc); | |
| // group 0 is empty for this shader; layouts at 1 and 2 are | |
| // textures + samplers respectively. | |
| rcp<BindGroupLayout> emptyLayout; // optional placeholder | |
| auto layout1 = | |
| ore_gm::makeLayoutFromShader(ctx, shader.vsModule.get(), 1); | |
| auto layout2 = | |
| ore_gm::makeLayoutFromShader(ctx, shader.vsModule.get(), 2); | |
| BindGroupLayout* layouts[] = {nullptr, layout1.get(), layout2.get()}; | |
| PipelineDesc pipeDesc{}; | |
| pipeDesc.vertexModule = shader.vsModule.get(); | |
| pipeDesc.fragmentModule = shader.psModule.get(); | |
| pipeDesc.vertexEntryPoint = shader.vsEntryPoint; | |
| pipeDesc.fragmentEntryPoint = shader.fsEntryPoint; | |
| pipeDesc.vertexBufferCount = 0; | |
| pipeDesc.topology = PrimitiveTopology::triangleList; | |
| pipeDesc.colorTargets[0].format = TextureFormat::rgba8unorm; | |
| pipeDesc.colorCount = 1; | |
| pipeDesc.depthStencil.depthCompare = CompareFunction::always; | |
| pipeDesc.depthStencil.depthWriteEnabled = false; | |
| pipeDesc.bindGroupLayouts = layouts; | |
| pipeDesc.bindGroupLayoutCount = 3; | |
| pipeDesc.label = "ore_canvas_import_pipeline"; | |
| auto pipeline = ctx.makePipeline(pipeDesc); | |
| if (!pipeline) | |
| return; | |
| BindGroupDesc texBGDesc{}; | |
| texBGDesc.layout = layout1.get(); | |
| BindGroupDesc::TexEntry texEntry{}; | |
| texEntry.slot = 0; | |
| texEntry.view = sampledView.get(); | |
| texBGDesc.textures = &texEntry; | |
| texBGDesc.textureCount = 1; | |
| auto texBG = ctx.makeBindGroup(texBGDesc); | |
| BindGroupDesc sampBGDesc{}; | |
| sampBGDesc.layout = layout2.get(); | |
| BindGroupDesc::SampEntry sampEntry{}; | |
| sampEntry.slot = 0; | |
| sampEntry.sampler = sampler.get(); | |
| sampBGDesc.samplers = &sampEntry; | |
| sampBGDesc.samplerCount = 1; | |
| auto sampBG = ctx.makeBindGroup(sampBGDesc); | |
| // ── 5. Render the Ore pass into a destination canvas. ── | |
| auto destCanvas = renderContext->makeRenderCanvas(kSize, kSize); | |
| if (!destCanvas) | |
| return; | |
| auto destView = ctx.wrapCanvasTexture(destCanvas.get()); | |
| if (!destView) | |
| return; | |
| ColorAttachment ca{}; | |
| ca.view = destView.get(); | |
| ca.loadOp = LoadOp::clear; | |
| ca.storeOp = StoreOp::store; | |
| ca.clearColor = {0, 0, 0, 1}; | |
| RenderPassDesc rpDesc{}; | |
| rpDesc.colorAttachments[0] = ca; | |
| rpDesc.colorCount = 1; | |
| rpDesc.label = "ore_canvas_import_pass"; | |
| auto pass = ctx.beginRenderPass(rpDesc); | |
| pass->setPipeline(pipeline.get()); | |
| pass->setBindGroup(1, texBG.get()); | |
| pass->setBindGroup(2, sampBG.get()); | |
| pass->setViewport(0, 0, kSize, kSize); | |
| pass->draw(6); // fullscreen quad | |
| pass->finish(); | |
| m_ore.endFrame(renderContext); | |
| ore_gm::invalidateGLStateAfterOre(renderContext); | |
| // ── 6. Composite the Ore canvas into the main framebuffer. ── | |
| originalRenderer->save(); | |
| originalRenderer->drawImage(destCanvas->renderImage(), | |
| {.filter = ImageFilter::nearest}, | |
| BlendMode::srcOver, | |
| 1); | |
| originalRenderer->restore(); | |
| #endif // ORE_CANVAS_IMPORT_ACTIVE | |
| } | |
| private: | |
| ore_gm::OreGMContext m_ore; | |
| }; | |
| GMREGISTER(ore_canvas_import, return new OreCanvasImportGM) |