| name | indirect-draw-first-instance |
| description | When draw_indexed_indirect produces only one visible object despite multiple groups having instance_count>0, suspect a wgpu bug where first_instance is ignored — all groups read instance 0 from a shared SSBO. |
| source | auto-skill |
| extracted_at | 2026-06-16T09:45:32.632Z |
Indirect Draw first_instance Workaround
Symptom
Only one object renders on screen when multiple draw_indexed_indirect calls
should produce distinct geometry. The indirect draw buffer shows correct
instance_count > 0 for all expected groups (verified via GPU readback or tap),
but visually only the first group appears. The depth buffer shows one contiguous
blob at a single depth value.
Root Cause
The vendored wgpu used by Titan does NOT respect the first_instance field in
the indirect draw struct (DrawIndexedIndirect). All draw calls read instance
data starting at index 0 from the shared SSBO, regardless of the
first_instance value in the buffer.
With reverse-Z depth test (Greater), the first group to draw wins; subsequent
groups render at the same world-space position and fail the depth comparison.
Confirmation
Hardcode the instance index in the vertex shader to isolate the issue:
// Diagnostic: force all groups to read instance 0
let base = 0u * 8u;
// Then try instance 1 and observe the blob move
let base = 1u * 8u;
If hardcoding instance 0 produces the same image as the original code (and
instance 1 moves the blob), first_instance is being ignored.
Workaround
Replace draw_indexed_indirect with draw_indexed using explicit instance
ranges from the draw group's cumulative instance count:
let mut running_base: u32 = 0;
for (group_idx, group) in groups.iter().enumerate() {
let base = running_base;
let count = group.instances.len() as u32;
running_base += count;
if count == 0 { continue; }
rp.draw_indexed(0..group.mesh.index_count, 0, base..(base + count));
}
The WGSL shader uses instance_idx * 8u as before — instance_idx now starts
at base (the first value of the instance range) and correctly indexes the
shared SSBO.
Tradeoff
GPU occlusion culling (HZB compute pass zeroing instance_count in the indirect
buffer) is disabled with this workaround — draw_indexed reads the CPU-side
instance count, not the GPU-modified buffer. The HZB build and cull compute pass
can still run for bench verification purposes, but the occlusion result does not
affect the draw.
To restore GPU-driven occlusion:
- Fix wgpu to respect
first_instance, OR
- Use per-group instance vertex buffers (not a shared SSBO) so
first_instance
can remain 0 for all groups (the architecture at commit 602426de).
Right Way / Wrong Way
| Right Way | Wrong Way |
|---|
Use draw_indexed with explicit instance ranges as a workaround | Use draw_indexed_indirect with shared SSBO — first_instance is silently ignored |
Hardcode instance_idx in the shader to confirm the hypothesis | Assume the indirect buffer or instance data is corrupted |
| Keep per-group instance buffers if GPU-driven culling is required | Try push constants — vendored wgpu doesn't support them |