MOOR security

Dieser Eintrag ist auf Englisch verfasst.

Variable-rate shading on an M1: four threads share the work of a coarse pixel

Entry 047804 of 916 tests pass

D3D12 variable-rate shading, where one pixel-shader run colours a block of up to 4x4 pixels, is now emulated on an M1 Mac by having each 2x2 group of shader threads shade coarse pixels and share the results.

What changed

Variable-rate shading (VRS) lets a game shade parts of the screen more coarsely. One run of the pixel shader (the program that computes a pixel's colour) then colours a block of 1x2 up to 4x4 pixels. The rate can be set per draw, per triangle from the vertex shader (SV_ShadingRate), or per 16x16 screen tile from a small image. Metal on this M1 Max has no shading rate. Its "rasterization rate maps" shrink parts of the render target instead, which is a different thing. The layer reported tier 0 and ignored the calls. It now reports tier 2, the highest.

The layer emulates coarse pixels inside the pixel shader, with a patch to SPIRV-Cross (the tool that turns the shaders into Metal's language). GPUs run pixel shaders in quads, groups of 2x2 threads that can read each other's values. With the patch, each quad does this:

  1. It works out the rate from the draw, the triangle and the image tile.
  2. It picks a 2x2 block of coarse pixels. Each thread shades one of them at its centre, reading its inputs there with interpolate_at_offset. So derivatives (the differences between neighbouring threads, used for texture filtering) are coarse-pixel sized, as D3D12 requires.
  3. Every pixel copies its colour, and whether it was discarded, from the thread that shaded its coarse pixel (quad_shuffle). The other threads are marked as helpers, so a store to memory happens once per coarse pixel.
What one quad does: the rate, the block choice, shading at coarse centres, and copying outputs from the owner.Quad of 4 pixel threads (Metal runs every pixel) → Rate: base, SV_ShadingRate, 16x16 image tile, combiners and clamps; Rate: base, SV_ShadingRate, 16x16 image tile, combiners and clamps → Rate 1x1?; Rate 1x1? → Shade normally (yes); Rate 1x1? → Pick a 2x2 block of coarse pixels whose owners are covered (no); Pick a 2x2 block of coarse pixels whose owners are covered → Block found?; Block found? → Shade normally (no); Block found? → Each thread shades one coarse pixel at its centre (interpolate_at_offset) (yes); Each thread shades one coarse pixel at its centre (interpolate_at_offset) → Coarse-sized ddx / ddy; Coarse-sized ddx / ddy → quad_shuffle outputs and discard from the owner; quad_shuffle outputs and discard from the owner → Non-owners become helpers: no UAV storesQuad of 4 pixel threads(Metal runs every pixel)Rate: base, SV_ShadingRate,16x16 image tile,combiners and clampsRate1x1?Shade normallyPick a 2x2 blockof coarse pixels whoseowners are coveredBlockfound?Each thread shades onecoarse pixel at its centre(interpolate_at_offset)Coarse-sizedddx / ddyquad_shuffle outputsand discard from the ownerNon-owners becomehelpers: no UAV storesyesnonoyes
What one quad does: the rate, the block choice, shading at coarse centres, and copying outputs from the owner. (Mermaid source)
An enlarged crop of curved stripes rendered at several shading rates: a purple region in coarse square blocks, a teal region in mid-size blocks and a white region at full detail, with a small yellow ring marking the sampled pixel.
A 4x enlargement of part of the still. The spiral is smooth at 1x1 and in 2x2 and 4x4 blocks further out.
Gallery scene vrs: a fine spiral drawn with a shading-rate image (one rate per 16x16 tile), like foveated rendering: 1x1 near the yellow focus ring (grey), 2x2 around it (teal) and 4x4 elsewhere (purple), where the spiral turns into 4-pixel blocks; the tint comes from the shader's SV_ShadingRate; animated, the focus circles the centre once. All scenes in the gallery

The interesting part

The first version had a fixed thread own each coarse pixel. The colours were right, but a new test case with a triangle edge crossing coarse pixels lost 72 memory writes. On an edge, the owner's own pixel is often outside the triangle, and Metal then runs it as a helper thread. Metal silently drops a helper's stores and atomics, while quad_shuffle still reads its values. Now each quad chooses among up to four block positions one where every covered pixel's owner is itself covered. If none fits, it falls back to 1x1.

A spike first had to show that interpolate_at_offset works 6 pixels away, well beyond the half pixel it is documented for. It does: exact without perspective, within 3.8e-6 with it.

This emulation is correct but saves no work. Every thread still runs the shader.

Numbers

  • Full run: 804 pass, 26 fail, 86 skip of 916; 0 newly failing, 7 newly passing.
  • Previous entry's run: 797 pass, 26 fail, 92 skip of 915.
  • The new api/vrs test: 90,113 checks over 34 cases, all passing.
  • The six vkd3d-proton VRS tests, which skipped before, pass: 1,656 checks, up from 63.
  • The edge bug: 72 failed checks before the block choice, 0 after (api/vrs).
Tests by suite at the end of this entry: 804 of 916 pass (data as CSV)
  • pass
  • fail
  • skip

suite · share of each row's total

  1. vkd3d-proton (+6)478 pass, 17 fail, 82 skip of 577
  2. vkd3d177 pass, 9 fail, 4 skip of 190
  3. apps77 pass, 0 fail, 0 skip of 77
  4. api (+1)68 pass, 0 fail, 0 skip of 68
  5. samples4 pass, 0 fail, 0 skip of 4
Checks run by vkd3d-proton's VRS tests, before (skipped) and after (data as CSV)
  • before
  • after

vkd3d-proton test (test_vrs…) · checks

  1. (plain)11 before, 90 after
  2. dxil12 before, 190 after
  3. image11 before, 205 after
  4. clip dist.9 before, 39 after
  5. depth dxbc10 before, 566 after
  6. depth dxil10 before, 566 after