MOOR security

Dieser Eintrag ist auf Englisch verfasst.

Godot's effects scene on our D3D12, after one missing 4-bit texture format

Entry 062846 of 951 tests pass

Godot 4.7's SDFGI, volumetric fog, screen-space reflections and GPU particles now render through mtl12. The only thing missing was a 16-bit texture format whose bits Metal stores in a different order.

What changed

The Godot editor and a second, heavier Godot scene are now part of the test suite.

The editor test needed no fix in mtl12. In the last session the editor stopped presenting after 10 to 19 frames. That was Godot's low-processor mode, which presents only when something on screen changes. The test now runs the editor with continuous updates turned on and checks frame 120.

The effects scene turns on most of Godot's Forward+ renderer at once:

  • screen-space reflections (SSR) on a mirror floor;
  • ambient occlusion (SSAO) and screen-space indirect light (SSIL);
  • SDFGI, Godot's global illumination from signed distance fields;
  • glow, volumetric fog, three shadowed lights and 200 GPU particles.

At first Godot crashed, because CreateResource failed for one texture format, B4G4R4A4_UNORM. SDFGI keeps a 256x128x512 3D texture in it. After that one format, the scene rendered.

The effects scene, a three-second clip: GPU particles rise in front of the box, and the fog and SDFGI brighten the background over the first frames, so the loop has a seam.
How a B4G4R4A4 texel is read through Metal's ABGR4Unorm and the view swizzle, and what is refused.D3D12 B4G4R4A4_UNORM, one 16-bit texel bits 0-3 B, 4-7 G, 8-11 R, 12-15 A → Metal ABGR4Unorm reads the same bits as A from 0-3, B from 4-7, G from 8-11, R from 12-15; Metal ABGR4Unorm reads the same bits as A from 0-3, B from 4-7, G from 8-11, R from 12-15 → view swizzle (G, B, A, R): red from Metal's G, green from B, blue from A, alpha from R; view swizzle (G, B, A, R): red from Metal's G, green from B, blue from A, alpha from R → the shader sees the D3D12 channels (composed over the SRV's own mapping); Metal ABGR4Unorm reads the same bits as A from 0-3, B from 4-7, G from 8-11, R from 12-15 → render targets, UAVs, typed buffers, MSAA: refused, as Metal can't swizzle writesD3D12 B4G4R4A4_UNORM, one 16-bit texelbits 0-3 B, 4-7 G, 8-11 R, 12-15 AMetal ABGR4Unorm reads the same bits asA from 0-3, B from 4-7, G from 8-11, R from 12-15view swizzle (G, B, A, R):red from Metal's G, green from B,blue from A, alpha from Rthe shader sees the D3D12 channels(composed over the SRV's own mapping)render targets, UAVs, typed buffers, MSAA:refused, as Metal can't swizzle writes
How a B4G4R4A4 texel is read through Metal's ABGR4Unorm and the view swizzle, and what is refused. (Mermaid source)

The interesting part

B4G4R4A4 packs four 4-bit channels into 16 bits. D3D12 puts blue in the lowest four bits, then green, red and alpha. Metal's closest format, ABGR4Unorm, holds the same bits but reads them the other way round: alpha from the lowest four, then blue, green and red.

mtl12 keeps the bytes as they are and gives every shader view a swizzle, a per-view remap of channels: red comes from Metal's green, green from blue, blue from alpha, and alpha from red. The swizzle is applied on top of whatever channel mapping the application asked for.

Metal can swizzle reads but not writes. So the format is offered for sampling only, and render targets and unordered-access views of it are refused. That is enough for SDFGI, which creates its texture without render-target or unordered-access flags.

The scene's checks were chosen by turning each effect off in turn and seeing which pixels move. With SSR off, the probe on the ball's reflection reads (61,54,65) instead of (233,146,81). With SDFGI off, every probe changes. SSAO moved no pixels in this scene; on a plain floor without SDFGI it changes 755, so it does run.

I also measured what a draw costs Metal 4 on the CPU, for the performance work. Under Rosetta, where Wine's Unix side runs, an indexed draw costs about 88 ns when the argument table (Metal 4's list of buffer addresses for a draw) is unchanged. It costs about 191-197 ns when one address in it changes first. mtl12 changes the root-argument address before every draw. The size of the table makes no difference, and indirect command buffers are no cheaper under Rosetta. No code changed from this yet.

Numbers

  • Full run: 846 pass, 29 fail, 76 skip of 951; entry 061 ended at 844 of 948.
  • Three tests are new and passing: api/bgra4_texture (194 checks), apps/godot-editor and apps/godot-effects.
  • One test is newly failing, apps/Tutorial08_Tessellation: 270 pixels over tolerance against 266 allowed. The same frame appeared once before, in an earlier full run. In four solo reruns it passed with 228, so it is intermittent; the cause is unknown.
  • Draw cost under Rosetta, in ns per draw: 88.5 with the table unchanged, 197.0 with one setAddress before each draw, 287.1 with three. Natively: 35.0 and 73.8.
Cost of one Metal 4 indexed draw on the encoding thread (ns per draw, median of 5 runs of 50,000 draws) (data as CSV)

variant · ns per draw

  1. native: table unchanged35
  2. native: 1 setAddress per draw73.8
  3. native: ICB draw only50.3
  4. Rosetta: table unchanged88.5
  5. Rosetta: 1 setAddress per draw197
  6. Rosetta: 3 setAddress per draw287.1
  7. Rosetta: setAddress + setArgumentTable215.3
Tests by suite at the end of this entry: 846 of 951 pass (data as CSV)
  • pass
  • fail
  • skip

suite · share of each row's total

  1. vkd3d-proton490 pass, 15 fail, 72 skip of 577
  2. vkd3d177 pass, 9 fail, 4 skip of 190
  3. api (+1)80 pass, 0 fail, 0 skip of 80
  4. apps (+1)79 pass, 1 fail, 0 skip of 80
  5. wine-dxgi16 pass, 4 fail, 0 skip of 20
  6. samples4 pass, 0 fail, 0 skip of 4