MOOR security

Dieser Eintrag ist auf Englisch verfasst.

Triangle fans from ExecuteIndirect, where only the GPU knows how many there are

Entry 077873 of 967 tests pass

mtl12 now draws triangle fans and strips with adjacency that a D3D12 program issues with ExecuteIndirect, by sizing and converting their indices on the GPU.

What changed

Metal has no triangle fans (triangles that all share the first vertex), and no strips with adjacency (strips whose every other vertex is extra data for a geometry shader). mtl12 already draws both for ordinary draw calls. A compute pre-pass rewrites the draw's indices into a plain triangle list, and the CPU works out how long that list will be.

ExecuteIndirect is D3D12's draw call whose arguments live in a GPU buffer: the vertex count, the start and, optionally, which index buffer to use. Often another shader wrote them, so the CPU never sees them. mtl12 used to drop such draws whenever they used a fan or adjacency.

A new test, api/execute_indirect_fan, issues fans and a strip with adjacency through ExecuteIndirect:

  • non-indexed fans, with a count buffer that hides the last command;
  • 16-bit fans cut by the "strip cut" value 0xFFFF (an index that ends one fan and starts the next);
  • 32-bit fans from per-command index buffers, cut at 0xFFFFFFFF;
  • a fan of 300 triangles.

Before the change, 800 of its 867 checks failed.

An ExecuteIndirect fan's path through the pre-passes into the indirect command buffer.ExecuteIndirect fan or adjacency → expand_indirect one record per command (args, IBV, VBVs); expand_indirect one record per command (args, IBV, VBVs) → predication zeroes args; predication zeroes args → assembly_plan 1 thread: mode, count, offset per command; assembly_plan 1 thread: mode, count, offset per command → assemble_indices 1 threadgroup per command; assemble_indices 1 threadgroup per command → 16 MiB scratch 32-bit list indices; 16 MiB scratch 32-bit list indices → encode_draws indexed list draw per command; encode_draws indexed list draw per command → executeCommandsInBufferExecuteIndirectfan or adjacencyexpand_indirectone record per command(args, IBV, VBVs)predicationzeroes argsassembly_plan1 thread: mode, count,offset per commandassemble_indices1 threadgroupper command16 MiB scratch32-bit list indicesencode_drawsindexed list drawper commandexecuteCommandsInBuffer
An ExecuteIndirect fan's path through the pre-passes into the indirect command buffer. (Mermaid source)
How the plan chooses each command's conversion, and the capacity check.command record → indexed?; indexed? → mode 6: indices 0..n-1 assembled as a list (no); indexed? → pipeline's strip cut value (yes); pipeline's strip cut value → mode 4: cut at 0xFFFF (16- or 32-bit IBV) (0xFFFF); pipeline's strip cut value → this command's IBV format (0xFFFFFFFF); this command's IBV format → mode 5: cut at 0xFFFFFFFF (R32_UINT); this command's IBV format → mode 3: no cut (R16_UINT); pipeline's strip cut value → mode 3: no cut (disabled); mode 4: cut at 0xFFFF (16- or 32-bit IBV) → fits in 2^22 indices?; mode 5: cut at 0xFFFFFFFF → fits in 2^22 indices?; mode 3: no cut → fits in 2^22 indices?; mode 6: indices 0..n-1 assembled as a list → fits in 2^22 indices?; fits in 2^22 indices? → count 0: draws nothing (no)command recordindexed?mode 6: indices 0..n-1assembled as a listpipeline'sstrip cut valuemode 4: cut at 0xFFFF(16- or 32-bit IBV)this command'sIBV formatmode 5: cut at 0xFFFFFFFFmode 3: no cutfits in 2^22indices?count 0: draws nothingnoyes0xFFFF0xFFFFFFFFR32_UINTR16_UINTdisabledno
How the plan chooses each command's conversion, and the capacity check. (Mermaid source)

The interesting part

The pre-pass needs each draw's size before it runs, so for ExecuteIndirect the sizing moved to the GPU too. One extra compute thread walks the commands. For each it chooses the conversion, computes how many list indices it will produce, and gives it a slice of a 16 MiB scratch buffer. A second dispatch then converts each command in its own threadgroup, reusing the direct path's helpers. The indirect command buffer (Metal's GPU-encoded draw list) then draws each command as a 32-bit triangle list from its slice.

The conversion has to be chosen per command, not per call. In D3D12 the pipeline fixes the cut value at 0xFFFF or 0xFFFFFFFF, but each command may bring its own index buffer, in either format. A 0xFFFFFFFF cut can't occur in 16-bit indices, so those commands convert without cuts. I checked that this matters by swapping the per-command choice for a plain conversion: 206 checks failed, in all four regions with cuts or a start index.

A command that doesn't fit in the 16 MiB (2^22 indices, about 1.4 million triangles per call) draws nothing. That limit is not tested yet.

Numbers

  • api/execute_indirect_fan: 800 of 867 checks failed before, 0 after. The plain-conversion mutation fails 206.
  • Full run: 873 of 967 tests pass, 26 fail, 68 skip; 871 of 965 at the end of entry 076. Newly failing: none; newly passing: the two new api tests.
Tests by suite at the end of this entry: 873 of 967 pass (data as CSV)
  • pass
  • fail
  • skip

suite · share of each row's total

  1. vkd3d-proton499 pass, 13 fail, 65 skip of 577
  2. vkd3d178 pass, 9 fail, 3 skip of 190
  3. api (+2)96 pass, 0 fail, 0 skip of 96
  4. apps80 pass, 0 fail, 0 skip of 80
  5. wine-dxgi16 pass, 4 fail, 0 skip of 20
  6. samples4 pass, 0 fail, 0 skip of 4
api/execute_indirect_fan: failed checks of 867 (data as CSV)

build · failed checks

  1. draws dropped (before)800
  2. plain assembly (cut mode removed)206
  3. per-command cut mode0