Skip to content

nvn/queue: keep the replay of several NVN queues in the class of work in progress (DM_NVN_QDEFER, off by default) - #2523

Open
dpadr wants to merge 2 commits into
seedds:mainfrom
dpadr:nvn-qdefer
Open

dpadr wants to merge 2 commits into
seedds:mainfrom
dpadr:nvn-qdefer

Conversation

@dpadr

@dpadr dpadr commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • The queue scheduler replays each piece of an NVN queue as its own Metal command buffer, and alternates ready pieces of different queues round robin.
  • A game with an async compute queue (Ocarina of Time's queue 5 holds all of its frame's compute; queue 1 holds every render pass) is replayed render, compute, render, compute. That makes 22 compute↔render switches a frame, at 35–46 µs median each on an M2, with a long compute → render tail.
  • With DM_NVN_QDEFER=1, a ready piece of the other class is held while another queue offers a ready wait or a piece of the class in progress. Offline, one OoT frame's queue order replayed with its waits goes from 22 switches to 5, the fewest those waits allow.
  • The held piece runs at once:
    • for a CPU fence wait (nvn_sync_wait);
    • before a present or another frame's work;
    • when nothing else qualifies;
    • after DM_NVN_QDEFER_US (default 3000).
  • Ordering across queues stays the game's event and sync waits, and one queue's order never changes, so NVN's guarantees hold.
  • Log: a [nvn] queue defer line gives the holds a frame and why held pieces ran.

With #2250 and e775054 (in this branch's base; not in the measured build)

Measurements

Apple M2 MacBook Air (16 GB), macOS 27.0. Ocarina of Time 0400486024DEC000 v131072 on our cut7 build (a406a81f: an older main plus OoT-specific host changes, shader chain bf6035002b98), handheld 1440x810, 60 fps cap. Route4 to frame 12000, in-run A/B DM_NVN_QDEFER=ab with DM_AB=240: 21 phases on and 21 off, each on phase against the mean of its two off neighbours, bootstrap 95 %.

Measure Change with QDEFER on 95 % range
GPU idle within the frame −0.72 ms [−4.28, −0.49]
GPU span (first to last work) −2.2 ms [−4.28, −0.03]
Frame p90 −3.3 ms [−10.2, −0.12]
Frame p50 −1.77 ms [−4.16, +0.28], the interval crosses 0
GPU busy +0.15 ms noise

Tests

  • t_wait_event: a new queue-defer case (each policy reason, the cap's edge, op classes, a recorded copy-only piece classed compute).
  • check_t_wait_event and check_t_queue_lifetime: also run with DM_NVN_QDEFER=1.
  • test_env_switches: OK.
  • t_queue_origin: the queue-switch barrier case, also run with DM_NVN_BARRIER=game (sensitivity).
  • Quick-lane run on this branch (qdmtests2, 61967a5): all 0 failed.
    • t_wait_event: 116 checks in each of its 3 runs (ENC_THREAD 0, 1, and 1 with QDEFER=1).
    • t_queue_lifetime: 38 checks in each of its 3 runs.
    • t_queue_origin: 399, 399, and 400 with DM_NVN_BARRIER=game, where the sensitivity check runs.
    • test_env_switches: OK.

Regression (rule: scripts/regress.py check --games)

🤖 Generated with Claude Code

seedds and others added 2 commits November 12, 2026 08:52
…l NVN queues in the class of work it is in

Each piece the queue scheduler replays is its own Metal command buffer. Between pieces of different NVN queues, the order is the host's choice wherever no event or sync wait orders them, and round robin alternates them. Ocarina of Time's queue 5 holds all of its frame's compute (139 dispatches in 11 pieces, no render pass) and queue 1 every render pass. Round robin replays them render, compute, render, compute: 22 switches between compute and render a frame from the pieces alone, at 35-46 us median each on an M2, with a long compute -> render tail.

With the switch on, a ready command piece whose first class (render or compute) differs from the class the last replayed piece ended in is held. Meanwhile, another queue's ready wait (bookkeeping) or ready piece of the class in progress goes first. The held piece runs at once when:
- a CPU thread waits on a fence (nvn_sync_wait);
- it has been held DM_NVN_QDEFER_US (default 3000);
- the alternative is a present or another frame's work;
- nothing else qualifies.

NVN orders two queues' work only by event and sync waits. The scan only takes pieces whose waits are met, and one queue's order never changes. Pieces are classed at record time by their ops: draws and texture clears are render; dispatches, copies and buffer clears are compute. A "[nvn] queue defer" line reports holds and why held pieces ran. nvn_mtl.m exports its ab switch helpers (mtl_sw_mode / mtl_sw_on) for it.

Offline, one OoT frame's queue order replayed with its waits goes from 22 switches to 5, the fewest those waits allow.

Measured on an M2 Air: Ocarina of Time at 1440x810, route4 to frame 12000, DM_NVN_QDEFER=ab with DM_AB=240 (21 + 21 phases, bootstrap 95 %, on a cut7 build):
- GPU idle within the frame: -0.72 ms [-4.28, -0.49]
- GPU span: -2.19 ms [-4.28, -0.03]
- frame p90: -3.31 ms [-10.2, -0.12]
- frame p50: -1.77 ms [-4.16, +0.28] (noise)
- GPU busy: +0.15 (noise)

Tests: a t_wait_event queue-defer case (every policy reason, the cap's edge, op classes, a recorded copy-only piece classed compute). check_t_wait_event and check_t_queue_lifetime also run with DM_NVN_QDEFER=1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_NVN_BARRIER=game)

With draw_barriers game, a draw pass after a draw pass that ended with the relaxed end barrier skips the full entry barrier unless the game's nvnCommandBufferBarrier bits since then ask for it (e775054). The relaxed end barrier orders later fragment, dispatch and copy stages only, not vertex or indirect fetch. That is enough within one queue, where the game's own barrier bits say what it needs.

g_last_end_relaxed is global, though, and survived a switch to another NVN queue. That queue's work after an event or sync wait must see all of the first queue's writes, vertex and indirect fetch included, and it issues no barrier for them. mtl_queue_activate now clears the flag when another queue's work begins, so that queue's first pass takes the full barrier.

Round robin already hands render work from one queue to another. DM_NVN_QDEFER makes it more common: it holds a compute piece so that two queues' render pieces run back to back. No cost to Ocarina of Time, the one profile with draw_barriers game, whose second queue is compute only.

Test: t_queue_origin draws on queue 0, then queue 1, then queue 1 again. The second pass takes the full barrier. With DM_NVN_BARRIER=game (a new check_t_queue_origin line), the third takes the relaxed one, which shows the check is sensitive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) docs Documentation labels Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants