blob: 2838c5606c92ef495d9cf87dc1a6f79f57fd4e7d [file]
/*
* Copyright 2026 Rive
*
* GM test for the Rive 2D RenderCanvas → Ore "imported canvas mirror"
* boundary. See dev/ore_canvas_import_invariant.md for the architecture.
*
* 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)
#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)
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)
#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 = *m_ore.oreContext;
m_ore.beginFrame();
// 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();
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)