Depth bounds on an M1: test against a copy of the depth buffer
The D3D12 depth bounds test, which only Apple's newest GPUs have natively, is now emulated on an M1 Mac by reading a copy of the depth buffer in the pixel shader.
What changed
The depth bounds test discards a pixel when the depth already stored there is outside a range (min, max). It doesn't depend on the depth of the thing being drawn. Engines use it to limit light and shadow passes to the pixels whose geometry is at the light's depth. Metal has it only on the Apple10 GPU family, and this M1 Max (Apple7) doesn't have it. The layer now reports DepthBoundsTestSupported and emulates the test.
Before a draw with bounds, the layer copies the depth attachment on a compute encoder. A patch to SPIRV-Cross (0034) then makes the fragment function (Metal's pixel shader) read the stored depth from that copy first and discard the pixel when it's outside the range. With multisampling it builds a mask of the samples in range and hands Metal only those. Consecutive bounds draws share one copy until something writes depth. Pipelines without a pixel shader get a generated one.
The interesting part
The occlusion query was the tricky part. It counts the samples that pass. With [earlydepthstencil], Metal counts at the early depth test, before the shader runs, so it also counts samples that the bounds test discards. While a query is active, such a draw is therefore drawn twice. The first pass counts, using a generated shader that does only the bounds test and writes no colour. The second draws for real, with counting switched off.
The review found a second problem: multisampling combined with a pixel shader that writes to buffers. Metal runs such a shader once per sample, where D3D12 runs it once per pixel. Entry 041 added a gate (patch 0032) that lets only the first covered sample do the writes. The bounds test has to feed into that gate: a discarded pixel must not write, and "first covered" has to mean "first covered and in range".
While tracking down the last miscount, a small spike found something unrelated to depth bounds. Metal clips the usual full-screen triangle, which reaches past the screen edges, into pieces. A multisampled pixel on a seam between two pieces runs the shader twice, each run with half of the samples. The once-per-pixel gate then writes twice on those pixels. The test now draws a triangle that needs no clipping. The general case is open.
The gallery scene shows the test from the application's side. A terrain is drawn once, then three coloured bands are drawn as flat quads over the whole screen. The depth bounds cut each band down to a thin slice of the terrain's depth, which moves from near to far.
Numbers
- Full run: 796 pass, 26 fail, 92 skip of 914; 0 newly failing, 1 newly passing, the new api/depth_bounds (246 checks over 21 cases).
- Previous run: 795 pass, 26 fail, 92 skip of 913.
- In the spike, 9 of 40 pixels ran the "first sample" twice with the clipped triangle and none with the unclipped one.
- pass
- fail
- skip
suite · share of each row's total
- vkd3d-proton472 pass, 17 fail, 88 skip of 577
- vkd3d177 pass, 9 fail, 4 skip of 190
- apps77 pass, 0 fail, 0 skip of 77
- api (+1)66 pass, 0 fail, 0 skip of 66
- samples4 pass, 0 fail, 0 skip of 4