blob: 43e35399bc730f488828835a5f8aea5744d46528 [file]
/*
* Copyright 2026 Rive
*
* Phase E witness: ONE BindGroupLayout used by TWO pipelines + ONE BindGroup
* shared across them. Proves the layout/pipeline/bindgroup decoupling is
* actually decoupled — pre-Phase-E this scenario was impossible because
* BindGroups were tied to a specific Pipeline.
*
* Setup:
* - One shader (`binding_witness`) with two UBOs at @group(0) @binding(0,7).
* - One BindGroupLayout derived from the shader, shared across both
* pipelines.
* - Two pipelines that differ only in color-write mask — one writes RGBA,
* the other writes only the red channel. Both reference the SAME layout.
* - One BindGroup with the two UBOs (low.r=0.3, high.g=0.6) against the
* shared layout.
*
* Renders:
* - Left half (64×128): full-RGBA pipeline → olive (0.3, 0.6, 0, 1).
* - Right half (64×128): red-only pipeline → red (0.3, 0, 0, 0) on top of
* the cleared-black background → dark red.
*
* Pre-Phase-E: not expressible (single BindGroup couldn't be used with two
* pipelines). Post-Phase-E: works because both pipelines share the same
* layout, and the BindGroup conforms to that one layout.
*/
#include "gm.hpp"
#include "gmutils.hpp"
#include "ore_gm_helper.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/ore/ore_buffer.hpp"
#include "rive/renderer/ore/ore_bind_group.hpp"
#include "rive/renderer/ore/ore_bind_group_layout.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_LAYOUT_REUSE_ACTIVE
#endif
class OreLayoutReuseGM : public GM
{
public:
OreLayoutReuseGM() : GM(128, 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;
#ifdef ORE_LAYOUT_REUSE_ACTIVE
auto& ctx = *m_ore.oreContext;
struct Uniforms
{
float r, g, b, a;
};
static const Uniforms kLow = {0.3f, 0.0f, 0.0f, 0.0f};
static const Uniforms kHigh = {0.0f, 0.6f, 0.0f, 0.0f};
BufferDesc lowDesc{};
lowDesc.usage = BufferUsage::uniform;
lowDesc.size = sizeof(Uniforms);
lowDesc.data = &kLow;
lowDesc.label = "reuse_ubo_low";
auto lowBuf = ctx.makeBuffer(lowDesc);
BufferDesc highDesc{};
highDesc.usage = BufferUsage::uniform;
highDesc.size = sizeof(Uniforms);
highDesc.data = &kHigh;
highDesc.label = "reuse_ubo_high";
auto highBuf = ctx.makeBuffer(highDesc);
auto shader = ore_gm::loadShader(ctx, ore_gm::kBindingWitness);
if (!shader.vsModule)
return;
// ── ONE layout, used by both pipelines. ──
auto sharedLayout =
ore_gm::makeLayoutFromShader(ctx, shader.vsModule.get(), 0);
if (!sharedLayout)
return;
BindGroupLayout* layouts[] = {sharedLayout.get()};
// Pipeline A — full RGBA write.
PipelineDesc pipeADesc{};
pipeADesc.vertexModule = shader.vsModule.get();
pipeADesc.fragmentModule = shader.psModule.get();
pipeADesc.vertexEntryPoint = shader.vsEntryPoint;
pipeADesc.fragmentEntryPoint = shader.fsEntryPoint;
pipeADesc.vertexBufferCount = 0;
pipeADesc.topology = PrimitiveTopology::triangleList;
pipeADesc.colorTargets[0].format = TextureFormat::rgba8unorm;
pipeADesc.colorTargets[0].writeMask = ColorWriteMask::all;
pipeADesc.colorCount = 1;
pipeADesc.depthStencil.depthCompare = CompareFunction::always;
pipeADesc.depthStencil.depthWriteEnabled = false;
pipeADesc.bindGroupLayouts = layouts;
pipeADesc.bindGroupLayoutCount = 1;
pipeADesc.label = "layout_reuse_pipeline_full";
auto pipeA = ctx.makePipeline(pipeADesc);
if (!pipeA)
{
fprintf(stderr,
"[ore_layout_reuse] pipeA failed: %s\n",
ctx.lastError().c_str());
return;
}
// Pipeline B — red-only write mask. Same layout, different state.
PipelineDesc pipeBDesc = pipeADesc;
pipeBDesc.colorTargets[0].writeMask = ColorWriteMask::red;
pipeBDesc.label = "layout_reuse_pipeline_red";
auto pipeB = ctx.makePipeline(pipeBDesc);
if (!pipeB)
{
fprintf(stderr,
"[ore_layout_reuse] pipeB failed: %s\n",
ctx.lastError().c_str());
return;
}
// ── ONE BindGroup, conformed to the shared layout. Used with both
// pipelines below. Pre-Phase-E this required two BindGroups (one
// per pipeline). Now: one suffices.
BindGroupDesc::UBOEntry uboEntries[2]{};
uboEntries[0].slot = 0;
uboEntries[0].buffer = lowBuf.get();
uboEntries[0].size = sizeof(Uniforms);
uboEntries[1].slot = 7;
uboEntries[1].buffer = highBuf.get();
uboEntries[1].size = sizeof(Uniforms);
BindGroupDesc bgDesc{};
bgDesc.layout = sharedLayout.get();
bgDesc.ubos = uboEntries;
bgDesc.uboCount = 2;
bgDesc.label = "layout_reuse_bg";
auto sharedBG = ctx.makeBindGroup(bgDesc);
if (!sharedBG)
return;
auto canvas = renderContext->makeRenderCanvas(128, 128);
if (!canvas)
return;
auto canvasTarget = ctx.wrapCanvasTexture(canvas.get());
if (!canvasTarget)
return;
m_ore.beginFrame();
ColorAttachment ca{};
ca.view = canvasTarget.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_layout_reuse_pass";
auto pass = ctx.beginRenderPass(rpDesc);
// Left half: pipe A (full RGBA) — should produce olive (0.3, 0.6, 0,
// 1).
pass.setPipeline(pipeA.get());
pass.setBindGroup(0, sharedBG.get());
pass.setViewport(0, 0, 64, 128);
pass.draw(3);
// Right half: pipe B (red-only mask) — same shared bind group.
// Output: dark red (0.3, 0, 0, 1).
pass.setPipeline(pipeB.get());
pass.setBindGroup(0, sharedBG.get());
pass.setViewport(64, 0, 64, 128);
pass.draw(3);
pass.finish();
m_ore.endFrame();
ore_gm::invalidateGLStateAfterOre(renderContext);
originalRenderer->save();
originalRenderer->drawImage(canvas->renderImage(),
{.filter = ImageFilter::nearest},
BlendMode::srcOver,
1);
originalRenderer->restore();
#endif // ORE_LAYOUT_REUSE_ACTIVE
}
private:
ore_gm::OreGMContext m_ore;
};
GMREGISTER(ore_layout_reuse, return new OreLayoutReuseGM)