Chapter 4 · ticket #369
The shadow mask's normals
Key idea. Two passes that must agree on a sample's triangle disagreed because only one of them read a flat input. The depth passes wrote one triangle's normals into an edge sample that the lit pass then shaded as its neighbour.
The passes that matter here, in order, with shadow rays and culling on:
- culling phase one
- depth pre-pass
- depth pyramid
- culling phase two
- phase two's depth pass
- shadow mask pass
- lit pass
Since #350, the depth passes, marked orange above, write the Surface normals with a fragment shader, fs_surface_normals. The shadow mask pass reads them to decide, per light, whether to hide it with no ray, trace a ray from outside the surface, or trace one from inside. The lit pass then tests each sample at depth Equal, shades the fragments that pass and reads the mask's bits.
fs_surface_normals read no flat input. The lit pass's fs_main reads the flat Material ID to find its Material. So on the M4 Pro the depth passes' fragments could merge and the lit pass's never did. At a sample beside a shared edge, the normals could come from triangle A while the lit pass shaded triangle B there, at the same depth, so B passed the Equal test. Across a crease, A may face away from a light that B faces, and the mask hides that light where the shading says lit.
How culling exposed it
Which fragments merge depends on what the draw holds. With culling on, the depth passes draw the two culling phases' Instances in two draws, while the lit pass draws them all in one. With culling off, phase two is empty, and the depth pre-pass makes the same draw as the lit pass. The depth passes' fragments could merge in both modes, so in both, some edge samples held a neighbour's normals. But the two modes merged different pairs, so their images differed at a few pixels, and that difference is what a test could see. A pixel wrong the same way in both modes was invisible to that comparison; the four Goldens the fix changed (chapter 5) show where the culled image had been wrong.
That is how the defect was found. Culling must not change the image, so tests/goldens.rs compares town culled and unculled at eight frames. With #350's normals, six of those frames differed by 1 to 6 pixels at 640×360, by up to 69 steps, and PR #366 tolerated up to 8, as HARDWARE_UNCULLED_PIXELS, filing #369 to bring it back to 0. Probes on #366's branch narrowed it down. A lit pass that wrote a hash of each fragment's depth and normal matched in both modes, and constant surface normals matched too. Neither depth pass's tie rule changed a single pixel. Later, #347's Meshlet culling test made helmet differ by 23 pixels, and by none with shadow rays off.