MOOR security

Dieser Eintrag ist auf Englisch verfasst.

Skipping what a repeated state block already decided

Entry 059839 of 943 tests pass

When a draw uses the same block of pipeline state as the draw before it, mtl12 now skips recomputing and re-setting that state, which cut the CPU time of a draw-heavy scene by about a quarter.

What changed

Since entry 057, a recorded draw points at a shared, immutable block of state: pipeline, render targets, viewports and so on. In a draw-heavy scene almost every draw points at the same block as the one before it. The Unix side (the part that turns recorded D3D12 commands into Metal calls) still re-derived everything from that block on every draw. It computed the render area, viewports and scissors, compared the render targets with the open render pass (Metal's unit of drawing into one set of targets), and compared each piece of encoder state before setting it.

Blocks can't change while a submission replays, so the block's address is a valid cache key. The encoder now remembers which block produced its viewports, its open pass and its state, and a draw of the same block skips all three. The state skip still yields to every event that already reset the old cache: any non-draw command, a new encoder, or draws that take special paths such as indirect or tessellation draws.

What a draw of a repeated block skips, and what resets it.draw record points at state block B → B == viewports_of?; B == viewports_of? → render area, viewports, scissors computed and kept (no); B == viewports_of? → pass open and B == pass_of? (yes); render area, viewports, scissors computed and kept → pass open and B == pass_of?; pass open and B == pass_of? → compare attachments (memcmp of 8 targets + depth) (no); pass open and B == pass_of? → state_valid and B == state_of? (yes); compare attachments (memcmp of 8 targets + depth) → state_valid and B == state_of?; state_valid and B == state_of? → draw_state: compare and set pipeline, depth-stencil, cull, bias, viewports... (no); state_valid and B == state_of? → bindings, root arguments, draw call (yes); draw_state: compare and set pipeline, depth-stencil, cull, bias, viewports... → bindings, root arguments, draw call; clear, barrier, query, new encoder → state_valid and B == state_of? (resets state_valid / pass_of)draw recordpoints at state block BB == viewports_of?render area, viewports, scissorscomputed and keptpass open and B == pass_of?compare attachments(memcmp of 8 targets + depth)state_valid and B == state_of?draw_state: compare and set pipeline,depth-stencil, cull, bias, viewports...bindings, root arguments, draw callclear, barrier, query, new encodernoyesnoyesnoyesresets state_valid / pass_of
What a draw of a repeated block skips, and what resets it. (Mermaid source)

The interesting part

A profile with Xcode's xctrace put about 8% of the replay thread's samples in compares of a block against itself. I predicted 1.5 to 2.5 ms of drawstress's 21.8 ms of CPU and wrote that down before measuring. The measured gain was 5.3 ms, more than twice that. My first explanation, that the skip also saved Metal setter calls, was wrong: the old code already compared each value with the last one and called Metal only for changes, so a repeated block cost no Metal call before either. What a repeated draw now skips is plain CPU work: the viewport and scissor math, two compares of the render targets, and building and comparing the state struct field by field. Which of these costs most I haven't measured. The profile undercounted them, which fits the way samples get misattributed under Rosetta (entry 058).

Before trusting it, I broke it on purpose. Dropping the "state still valid" condition passed both draw-state tests. In those tests, every draw after a clear or a barrier also came after a state change, so it had a new block anyway. The test now repeats one block around a clear and a barrier, and runs that list twice in one submission. The broken build fails 40 of its checks. A later review found no stale path. It did point out that the new debug check compares the encoder's software mirror, not Metal's state, and the docs now say so.

Numbers

Three alternating rounds against the previous commit:

  • bgfx drawstress, Unix CPU per frame: 21.68 to 22.28 ms before, 16.42 to 16.87 ms after; frame 33.55 to 34.14 ms before, 28.34 to 28.67 ms after.
  • bench-draws (10,000 draws a frame), submit: 3.44 to 3.48 ms before, 2.67 to 2.69 ms after.
  • A single run of the perf history agrees: drawstress Unix CPU -24%, bench-draws submit -22%.
  • Tests: 839 of 943 pass, as before; no test changed status.
Draws that repeat the last draw's state block skip its work, % of the old build (medians of 3 rounds) (data as CSV)
  • old
  • new

measurement · % of old build

  1. bench-draws submit100 old, 77.7 new
  2. drawstress frame100 old, 84.8 new
  3. drawstress unix CPU100 old, 76.8 new
Tests by suite at the end of this entry: 839 of 943 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. apps77 pass, 0 fail, 0 skip of 77
  4. api75 pass, 0 fail, 0 skip of 75
  5. wine-dxgi16 pass, 4 fail, 0 skip of 20
  6. samples4 pass, 0 fail, 0 skip of 4