name: wgsl-compute-comparison-sampling description: textureSampleCompare is fragment-stage-only — compute shaders must use textureSampleCompareLevel with 4 params (no LOD, no offset). Also: bind group layouts must match pipeline stage visibility. When adding passes to shared RONs, all benches inherit them. source: auto-skill extracted_at: '2026-06-16T01:35:34.704Z'
WGSL Compute Comparison Sampling + Pass Inheritance
Two distinct traps found during scene culling integration (2026-06-15).
Trap 1: textureSampleCompare in compute shaders
textureSampleCompare(t, s, coords, depth_ref) is fragment-stage-only — it uses
implicit LOD which requires fragment derivative hardware. Compute shaders must use
the explicit-LOD variant:
// WRONG — fragment-only, will fail naga validation:
return textureSampleCompare(vsm_physical_pool, vsm_sampler, phys_uv, depth_ref);
// CORRECT — compute-safe, implicit LOD 0:
return textureSampleCompareLevel(vsm_physical_pool, vsm_sampler, phys_uv, depth_ref);
Why: WGSL § texture builtins: comparison sampling with implicit LOD
(textureSampleCompare) requires fragment stage. The Level variant takes an
explicit LOD that the return type supports, but with 4 params it defaults to
level 0. Do NOT add a 5th 0i param — WGSL interprets the 5th param as a
vec2<i32> offset for texture_sample_2d, not as the LOD, and naga rejects
it with "Sample offset constant doesn't match the image dimension."
Verification: textureSampleCompare → textureSampleCompareLevel in all
.wgsl files under crates/. The commit hook at
.qwen/local-scripts/rendering_naming_guard.py does not catch this — it
blocks proprietary names, not WGSL API misuse. A manual grep at review time is
the only gate.
Trap 2: Bind group layout stage visibility mismatch
When creating a simplified pipeline (e.g. depth-only prepass) that reuses a bind group from the main rendering pipeline, the bind group's BGL must match the pipeline's BGL stage visibility:
// WRONG — frame_bind_group() was created with VERTEX|FRAGMENT visibility,
// but the depth prepass pipeline expects VERTEX-only at group 0:
rp.set_bind_group(0, self.viewport.frame_bind_group(), &[]);
// CORRECT — create a dedicated camera BG matching the pipeline's BGL:
let cam_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX, // match the pipeline
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
label: Some("depth_prepass camera bgl (per-frame)"),
});
Why: wgpu validates BGL compatibility at set_bind_group time. A BGL with
VERTEX|FRAGMENT is not compatible with one that has only VERTEX, even if
both have the same binding count and types.
Trap 3: Shared RON pass inheritance
When new passes are added to shared RON configs (pipeline_deferred_stratum_mid.ron,
etc.), ALL benches that use those configs inherit the passes. If a bench doesn't
populate the pass-specific data structures (e.g. SceneCullData), the dispatch
arm must gracefully fall through:
// Required pattern for every new pass dispatch arm:
if let Some(ref data) = self.pass_specific_data {
// GPU-driven path
} else {
// Fallback: warn once and skip, or use legacy path
self.warn_once_unimplemented_pass("PassName (data not populated)", "boot");
continue;
}
Verification checklist when adding new passes:
- Run
cargo run -p titan-render-bench-probe -- --capture-fps 60 --capture-start 0 --capture-end 1to smoke-test the probe bench - If it crashes, the new pass is interfering with an existing bench
- Add fallback path in the dispatch arm