blob: 19a2168021f9939e53c628da875aa79b98c5d92a [file]
/*
* Copyright 2026 Rive
*
* GM test / regression witness for BindingMap's Sampler vs
* ComparisonSampler kind collapse (commit 7168f5e0e3).
*
* Pre-fix: every flatten-backend's `makeBindGroup` called
* `BindingMap::lookup(..., ResourceKind::Sampler, ...)` for every
* `SampEntry`. The allocator stored WGSL `sampler_comparison`
* declarations as `ResourceKind::ComparisonSampler`, so the strict-
* kind lookup returned `kAbsent`, mapSlot fell back to the raw
* `@binding`, and the sampler landed at the wrong backend slot. On
* Metal this manifested as a driver SEGV inside
* `drawIndexedPrimitives` because the shader sampled an unbound
* `[[sampler(N)]]`.
*
* Post-fix: `BindingMap::lookup` treats Sampler and ComparisonSampler
* as interchangeable. The sampler lands at the allocator's chosen
* slot, the shader reads it, and the draw succeeds.
*
* Shape of the test:
* 1. Create a 1x1 depth24plusStencil8 texture with aspect=depthOnly
* view (mirrors SpinningCube.luau's shadow-map usage shape).
* 2. Fullscreen triangle samples the depth texture via a
* `sampler_comparison` whose `compare=always`. Per WebGPU spec,
* `textureSampleCompare` returns 1.0 unconditionally regardless
* of the (uninitialised) stored depth, so this GM is a pure
* bind-slot / layout regression test — not a depth-compare
* correctness test.
* 3. UBO tint = green. Correct output = solid green.
* Pre-7168f5e0e3 = Metal driver crash.
*
* ── Twin GM ──────────────────────────────────────────────────────────
*
* `ore_binding_shadow_sampler_d32.cpp` is a one-variable twin of this
* GM — same shader, same bind layout, same compare=always, same
* `aspect=depthOnly` — but uses `depth32float` instead of
* `depth24plusStencil8`. That shape mirrors `SpinningCube.luau`'s
* shipping shadow map exactly, so the twin doubles as a real-content
* safety check. Keeping both in the suite lets anyone narrow future
* adapter-specific discrepancies in the shadow-sampler path in five
* minutes:
*
* Both green → shadow-sampler path healthy.
* D24S8 black → something specific to combined depth-stencil +
* aspect=depthOnly broke (Ore-side BGL emission for
* the depth-aspect view, or driver mishandling of
* Depth24PlusStencil8 sampling). SpinningCube-style
* `depth32f`+depthOnly content is SAFE.
* D32 black → pure-depth shadow sampling regressed independently;
* SpinningCube IS affected.
* Both black → comparison-sampler / texture_depth binding itself
* broken (BGL sampleType emission, WGSL→GLSL compile,
* or compare-mode sampler state) — broadest impact.
*/
#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_sampler.hpp"
#include "rive/renderer/ore/ore_texture.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_SHADOW_SAMPLER_ACTIVE
#endif
class OreBindingShadowSamplerGM : public GM
{
public:
OreBindingShadowSamplerGM() : 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_SHADOW_SAMPLER_ACTIVE
auto& ctx = *m_ore.oreContext;
// UBO: tint (green) + compare reference (0.25).
struct Uniforms
{
float tint[4];
float ref[4];
};
static const Uniforms kU = {
{0.0f, 1.0f, 0.0f, 1.0f},
{0.25f, 0.0f, 0.0f, 0.0f},
};
BufferDesc uboDesc{};
uboDesc.usage = BufferUsage::uniform;
uboDesc.size = sizeof(Uniforms);
uboDesc.data = &kU;
uboDesc.label = "shadow_sampler_ubo";
auto uboBuf = ctx.makeBuffer(uboDesc);
// 1x1 white texture for the regular (non-compare) sample path.
static const uint8_t kWhite[4] = {0xFF, 0xFF, 0xFF, 0xFF};
TextureDesc colorDesc{};
colorDesc.width = 1;
colorDesc.height = 1;
colorDesc.format = TextureFormat::rgba8unorm;
colorDesc.type = TextureType::texture2D;
colorDesc.numMipmaps = 1;
colorDesc.sampleCount = 1;
colorDesc.label = "shadow_sampler_color";
auto colorTex = ctx.makeTexture(colorDesc);
if (!colorTex)
return;
TextureDataDesc colorUpload{};
colorUpload.data = kWhite;
colorUpload.width = 1;
colorUpload.height = 1;
colorUpload.bytesPerRow = 4;
colorUpload.rowsPerImage = 1;
TextureViewDesc colorViewDesc{};
colorViewDesc.texture = colorTex.get();
colorViewDesc.dimension = TextureViewDimension::texture2D;
colorViewDesc.baseMipLevel = 0;
colorViewDesc.mipCount = 1;
colorViewDesc.baseLayer = 0;
colorViewDesc.layerCount = 1;
auto colorView = ctx.makeTextureView(colorViewDesc);
// depth24plusStencil8 texture with aspect=depthOnly view —
// mirrors SpinningCube.luau's `self.shadowDepthTex:view({
// aspect = 'depthOnly' })` pattern. renderTarget=true is
// required; we don't write into it (compare=always).
TextureDesc depthDesc{};
depthDesc.width = 1;
depthDesc.height = 1;
depthDesc.format = TextureFormat::depth24plusStencil8;
depthDesc.type = TextureType::texture2D;
depthDesc.renderTarget = true;
depthDesc.numMipmaps = 1;
depthDesc.sampleCount = 1;
depthDesc.label = "shadow_sampler_depth";
auto depthTex = ctx.makeTexture(depthDesc);
if (!depthTex)
return;
TextureViewDesc depthViewDesc{};
depthViewDesc.texture = depthTex.get();
depthViewDesc.dimension = TextureViewDimension::texture2D;
depthViewDesc.baseMipLevel = 0;
depthViewDesc.mipCount = 1;
depthViewDesc.baseLayer = 0;
depthViewDesc.layerCount = 1;
depthViewDesc.aspect = TextureAspect::depthOnly;
auto depthView = ctx.makeTextureView(depthViewDesc);
SamplerDesc sampDesc{};
sampDesc.minFilter = Filter::nearest;
sampDesc.magFilter = Filter::nearest;
sampDesc.label = "shadow_sampler_tex";
auto texSampler = ctx.makeSampler(sampDesc);
// Comparison sampler: compare=always. `textureSampleCompare`
// returns 1.0 regardless of stored depth. Using `always` side-
// steps any stored-value initialization concerns — this GM is
// a bind-slot regression test, not a depth-compare correctness
// test.
SamplerDesc compareDesc{};
compareDesc.minFilter = Filter::nearest;
compareDesc.magFilter = Filter::nearest;
compareDesc.compare = CompareFunction::always;
compareDesc.label = "shadow_sampler_compare";
auto compareSampler = ctx.makeSampler(compareDesc);
// Shader from the RSTB.
auto shader = ore_gm::loadShader(ctx, ore_gm::kShadowSamplerWitness);
if (!shader.vsModule)
return;
auto layout0 =
ore_gm::makeLayoutFromShader(ctx, shader.vsModule.get(), 0);
auto layout1 =
ore_gm::makeLayoutFromShader(ctx, shader.vsModule.get(), 1);
BindGroupLayout* layouts[] = {layout0.get(), layout1.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 = 2;
pipeDesc.label = "ore_binding_shadow_sampler_pipeline";
auto pipeline = ctx.makePipeline(pipeDesc);
if (!pipeline)
{
fprintf(stderr,
"[ore_binding_shadow_sampler] pipeline creation failed: "
"%s\n",
ctx.lastError().c_str());
return;
}
// Mirror torus_shader.wgsl's exact bind group split:
// group 0 = UBO alone.
// group 1 = {texSampler, texColor, shadowSampler, shadowMap}.
BindGroupDesc::UBOEntry ubo0[1]{};
ubo0[0].slot = 0;
ubo0[0].buffer = uboBuf.get();
ubo0[0].offset = 0;
ubo0[0].size = sizeof(Uniforms);
BindGroupDesc bg0Desc{};
bg0Desc.layout = layout0.get();
bg0Desc.ubos = ubo0;
bg0Desc.uboCount = 1;
bg0Desc.label = "shadow_sampler_bg_uniforms";
auto bg0 = ctx.makeBindGroup(bg0Desc);
if (!bg0)
return;
BindGroupDesc::TexEntry tex1[2]{};
tex1[0].slot = 1;
tex1[0].view = colorView.get();
tex1[1].slot = 3;
tex1[1].view = depthView.get();
BindGroupDesc::SampEntry samp1[2]{};
samp1[0].slot = 0;
samp1[0].sampler = texSampler.get();
samp1[1].slot = 2;
samp1[1].sampler = compareSampler.get();
BindGroupDesc bg1Desc{};
bg1Desc.layout = layout1.get();
bg1Desc.textures = tex1;
bg1Desc.textureCount = 2;
bg1Desc.samplers = samp1;
bg1Desc.samplerCount = 2;
bg1Desc.label = "shadow_sampler_bg_textures";
auto bg1 = ctx.makeBindGroup(bg1Desc);
if (!bg1)
return;
auto canvas = renderContext->makeRenderCanvas(128, 128);
if (!canvas)
return;
auto canvasTarget = ctx.wrapCanvasTexture(canvas.get());
if (!canvasTarget)
return;
m_ore.beginFrame();
colorTex->upload(colorUpload);
// Single pass: bind both groups + draw a fullscreen triangle.
// With compare=always the sample_compare result is 1.0 so the
// fragment output is tint (green) × white × 1 = green.
RenderPassDesc rpDesc{};
rpDesc.colorAttachments[0].view = canvasTarget.get();
rpDesc.colorAttachments[0].loadOp = LoadOp::clear;
rpDesc.colorAttachments[0].storeOp = StoreOp::store;
rpDesc.colorAttachments[0].clearColor = {0, 0, 0, 1};
rpDesc.colorCount = 1;
rpDesc.label = "ore_binding_shadow_sampler_pass";
auto pass = ctx.beginRenderPass(rpDesc);
pass.setPipeline(pipeline.get());
pass.setBindGroup(0, bg0.get());
pass.setBindGroup(1, bg1.get());
pass.setViewport(0, 0, 128, 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_BINDING_SHADOW_SAMPLER_ACTIVE
}
private:
ore_gm::OreGMContext m_ore;
};
GMREGISTER(ore_binding_shadow_sampler, return new OreBindingShadowSamplerGM)