Skipping what a repeated state block already decided
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.
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.
- old
- new
measurement · % of old build
- bench-draws submit100 old, 77.7 new
- drawstress frame100 old, 84.8 new
- drawstress unix CPU100 old, 76.8 new
- pass
- fail
- skip
suite · share of each row's total
- vkd3d-proton490 pass, 15 fail, 72 skip of 577
- vkd3d177 pass, 9 fail, 4 skip of 190
- apps77 pass, 0 fail, 0 skip of 77
- api75 pass, 0 fail, 0 skip of 75
- wine-dxgi16 pass, 4 fail, 0 skip of 20
- samples4 pass, 0 fail, 0 skip of 4