blob: 558e06514edaa81bdb2a8b6feab384d4365ff17b [file]
/*
* Copyright 2026 Rive
*
* GM test / Phase 0 witness for the post-review fix sweep
* (`/Users/luigi/Projects/plan/Ore/Post-Review Fix Plan.md` Phase 0
* + Phase 1).
*
* This shader has the vertex stage sample a 1×1 green texture and
* forward the sample as a varying. The fragment stage multiplies by a
* UBO tint:
*
* @group(0) @binding(0) var<uniform> u: Uniforms; // FS-side
* @group(0) @binding(1) var t: texture_2d<f32>; // VS-side sample
* @group(0) @binding(2) var s: sampler; // VS-side sample
*
* With texture = (0, 1, 0, 1) green and tint = (1, 1, 1, 1) white,
* correct output is pure green.
*
* Expected behavior on `ore_merge_squashed` today (PRE-FIX):
* - Metal: BLACK. `mtlSetBindGroup` (`ore_render_pass_metal.mm:139-146`)
* only emits `setFragmentTexture:` / `setFragmentSamplerState:` —
* never the vertex-stage equivalents. The VS sees an unbound texture
* and reads zeros; varying reaches FS as (0, 0, 0, 0); tint × 0 = 0.
* - Vulkan / D3D11 / D3D12 / GL / WGPU: GREEN. Their `makeBindGroup`
* also looks up `Stage::VS` only, but the v1 allocator stamps
* `[slot, slot, slot]` so VS happens to bind the right slot. This GM
* locks the post-fix invariant in so a future per-stage allocator
* can't regress them.
*
* Expected behavior POST-FIX (Phase 1 of the fix plan):
* - Every backend renders green.
*/
#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_shader_module.hpp"
#include "rive/renderer/ore/ore_pipeline.hpp"
#include "rive/renderer/ore/ore_render_pass.hpp"
#include "rive/renderer/ore/ore_texture.hpp"
#include "rive/renderer/ore/ore_sampler.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_BINDING_VS_TEXTURE_ACTIVE
#endif
class OreBindingVSTextureGM : public GM
{
public:
OreBindingVSTextureGM() : 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_BINDING_VS_TEXTURE_ACTIVE
auto& ctx = *m_ore.oreContext;
// ── UBO: white tint. Multiplier passes the VS sample through
// unchanged on a correct bind. If FS-stage UBO binding regresses,
// the shader multiplies by zero and the frame goes black anyway —
// but the same-color failure mode is shared with the VS-texture
// bug, so we rely on the rest of the matrix (mixed_kind etc.) to
// disambiguate.
struct Uniforms
{
float r, g, b, a;
};
static const Uniforms kTint = {1.0f, 1.0f, 1.0f, 1.0f};
BufferDesc uboDesc{};
uboDesc.usage = BufferUsage::uniform;
uboDesc.size = sizeof(Uniforms);
uboDesc.data = &kTint;
uboDesc.label = "vs_texture_ubo";
auto uboBuf = ctx.makeBuffer(uboDesc);
// ── 1×1 green texture. The VS samples this; if the vertex-stage
// texture binding is missing, the VS reads zeros and forwards
// black to the FS regardless of the UBO tint.
static const uint8_t kGreen[4] = {0x00, 0xFF, 0x00, 0xFF};
TextureDesc texDesc{};
texDesc.width = 1;
texDesc.height = 1;
texDesc.format = TextureFormat::rgba8unorm;
texDesc.type = TextureType::texture2D;
texDesc.numMipmaps = 1;
texDesc.sampleCount = 1;
texDesc.label = "vs_texture_texture";
auto oreTex = ctx.makeTexture(texDesc);
if (!oreTex)
return;
TextureDataDesc uploadDesc{};
uploadDesc.data = kGreen;
uploadDesc.width = 1;
uploadDesc.height = 1;
uploadDesc.bytesPerRow = 4;
uploadDesc.rowsPerImage = 1;
TextureViewDesc tvDesc{};
tvDesc.texture = oreTex.get();
tvDesc.dimension = TextureViewDimension::texture2D;
tvDesc.baseMipLevel = 0;
tvDesc.mipCount = 1;
tvDesc.baseLayer = 0;
tvDesc.layerCount = 1;
auto texView = ctx.makeTextureView(tvDesc);
if (!texView)
return;
SamplerDesc sampDesc{};
sampDesc.minFilter = Filter::nearest;
sampDesc.magFilter = Filter::nearest;
sampDesc.label = "vs_texture_sampler";
auto sampler = ctx.makeSampler(sampDesc);
// ── Shader from the RSTB ──
auto shader = ore_gm::loadShader(ctx, ore_gm::kVSTextureWitness);
if (!shader.vsModule)
return;
auto layout0 =
ore_gm::makeLayoutFromShader(ctx, shader.vsModule.get(), 0);
BindGroupLayout* layouts[] = {layout0.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 = 1;
pipeDesc.label = "ore_binding_vs_texture_pipeline";
auto pipeline = ctx.makePipeline(pipeDesc);
if (!pipeline)
{
fprintf(stderr,
"[ore_binding_vs_texture] pipeline creation failed: %s\n",
ctx.lastError().c_str());
return;
}
// ── Single BindGroup at groupIndex=0 holding all three kinds.
BindGroupDesc::UBOEntry uboEntries[1]{};
uboEntries[0].slot = 0;
uboEntries[0].buffer = uboBuf.get();
uboEntries[0].offset = 0;
uboEntries[0].size = sizeof(Uniforms);
BindGroupDesc::TexEntry texEntries[1]{};
texEntries[0].slot = 1;
texEntries[0].view = texView.get();
BindGroupDesc::SampEntry sampEntries[1]{};
sampEntries[0].slot = 2;
sampEntries[0].sampler = sampler.get();
BindGroupDesc bgDesc{};
bgDesc.layout = layout0.get();
bgDesc.ubos = uboEntries;
bgDesc.uboCount = 1;
bgDesc.textures = texEntries;
bgDesc.textureCount = 1;
bgDesc.samplers = sampEntries;
bgDesc.samplerCount = 1;
bgDesc.label = "vs_texture_bg";
auto bg = ctx.makeBindGroup(bgDesc);
if (!bg)
return;
// ── Render into a RenderCanvas ──
auto canvas = renderContext->makeRenderCanvas(128, 128);
if (!canvas)
return;
auto canvasTarget = ctx.wrapCanvasTexture(canvas.get());
if (!canvasTarget)
return;
m_ore.beginFrame();
oreTex->upload(uploadDesc);
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_binding_vs_texture_pass";
auto pass = ctx.beginRenderPass(rpDesc);
pass.setPipeline(pipeline.get());
pass.setBindGroup(0, bg.get());
pass.setViewport(0, 0, 128, 128);
pass.draw(3); // fullscreen triangle
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_BINDING_VS_TEXTURE_ACTIVE
}
private:
ore_gm::OreGMContext m_ore;
};
GMREGISTER(ore_binding_vs_texture, return new OreBindingVSTextureGM)