Skip to content

Honor NVN event dependencies across guest queues - #1245

Open
sn0wst0rm wants to merge 1 commit into
seedds:mainfrom
sn0wst0rm:pr/mkw-event-queues
Open

sn0wst0rm wants to merge 1 commit into
seedds:mainfrom
sn0wst0rm:pr/mkw-event-queues

Conversation

@sn0wst0rm

Copy link
Copy Markdown
Contributor

Event and sync waits were ignored, allowing consumers to read stale input when another guest queue had not yet submitted its producer. This implements ready-prefix scheduling: a blocked queue yields so its producer can be submitted later, including by the same CPU thread. CPU-visible event values are published after GPU completion.

Preserve render targets, conditional rendering, visibility queries and submission-mode timestamps across event boundaries. Reuse command allocators across chunks without resetting GPU-owned data, while keeping the allocator pool bounded. Wait-only submissions retain the producer's completion fence.

Validation covers forward and bidirectional dependencies, CPU signals, queue ordering, resource retirement, allocator pressure, both timestamp modes and capture compatibility. Scheduler tests also pass with AddressSanitizer and UndefinedBehaviorSanitizer; GPU tests pass with Metal validation enabled.

Front-ends: break: rebuild host and replay tools together for the updated capture layout. Older captures are rejected; raw replay of event/sync waits remains unsupported.

Event and sync waits were ignored, allowing consumers to read stale input when another guest queue had not yet submitted its producer. This implements ready-prefix scheduling: a blocked queue yields so its producer can be submitted later, including by the same CPU thread. CPU-visible event values are published after GPU completion.

Preserve render targets, conditional rendering, visibility queries and submission-mode timestamps across event boundaries. Reuse command allocators across chunks without resetting GPU-owned data, while keeping the allocator pool bounded. Wait-only submissions retain the producer's completion fence.

Validation covers forward and bidirectional dependencies, CPU signals, queue ordering, resource retirement, allocator pressure, both timestamp modes and capture compatibility. Scheduler tests also pass with AddressSanitizer and UndefinedBehaviorSanitizer; GPU tests pass with Metal validation enabled.

Front-ends: break: rebuild host and replay tools together for the updated capture layout. Older captures are rejected; raw replay of event/sync waits remains unsupported.
@b1tdr3amer24

Copy link
Copy Markdown
Contributor

Tested this on The Legend of Zelda: Ocarina of Time (Switch 2, 0400486024DEC000, v131072) on a Mac (M4 Pro, macOS 27.2). It fixes a long-standing flicker there that nothing else had touched.

Symptom: while turning the camera, big chunks of the shadowing (sunlit steps and grass in Kokiri Forest) switched between lit and shadowed from one frame to the next. It went away whenever the game ran slowly, and no setting or host switch changed it.

Cause: OoT uses several NVN queues. Every frame, queue 1 signals timeline events at points spread through its submission. Queue 5's command buffers wait on those one by one and signal back, and queues 6 and 8 wait on queue 1 too. Some of the waits fall in the middle of a command buffer. With waits treated as no-ops, a logging probe found more than half of them not yet satisfied when the host replayed them, so their work read the previous frame's data. How often that happened depended on thread timing. Holding back whole submissions doesn't fix it, because the queues depend on each other within one submission, so splitting at each wait the way this PR does is what OoT needs.

Result: I applied this PR onto a local OoT branch (four conflicts in nvn_mtl.m, all resolved). The shadow flicker is completely gone. The frame rate holds at 60 (frame time p50 about 16.7 ms), and the log shows no new errors. The PR's own tests and the NVN suite pass on that branch.

Thanks for this!

@sleepyeyedjoe

sleepyeyedjoe commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Tested on Ocarina of Time (0400486024DEC000 v131072) on an M4 Pro, macOS 27. #1245 fixes every one-frame shadow glitch we had chased in #1703, and with it the game no longer needs its resolution pinned.

Setup. I merged #1245 onto main a260844 (chain 3ee493b2c3a4; no shader rebuild needed). There were five conflicts:

  • Four in nvn_mtl.m. Both sides now copy the bound target descriptions into queue-owned storage. I kept a260844's rt_preserve_bound and made mtl_queue_seal call it.
  • One in nvn_capture.c. I took the PR's layout plus COPY_STRIDES_LAYOUT.

Deterministic replays use the same key recordings and saves, with DM_FRAME_DELTA=0. Counts are one-frame outliers in the scene image:

Route Without #1245 With #1245
Mossy passages, 700 frames, resolution pinned (DM_NVN_GPU_TS=submission) 18, including the dark blobs at 1667, 1785, 1868 and 2258 none. The one outlier left is camera motion
Rock and bridge, 380 frames, pinned shadows drop at 1651 and 1765 none
Mossy passages, dynamic resolution 11, including the blob at 1889 none. The outliers are a wall passing the camera and Navi close up

In play. The player played without the resolution pin, at the rock, the bridge, the mossy passages and around Kokiri. They saw:

  • no shadow glitches;
  • no pale halo around Link while he moves;
  • no grey band along the screen edges, which they had seen on the old host.

Thanks @sn0wst0rm, and @padsiwono for pointing us here.

@lunks

lunks commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Data point for the queue-order fix, from a port of this PR onto our stack (535f2e3b): Ocarina of Time v131072 on an M5 Pro, fixed 720p internal resolution, MetalFX with jitter on. The character lighting flips we saw before are gone over about 18 minutes of play. The session ended in an unrelated guest crash: nvnBufferInitialize with no memory pool, the #1445 class.

@aksmith-git

Copy link
Copy Markdown
Contributor

Measured on an iPad Pro (M5) with Ocarina of Time (0400486024DEC000 v131072), Docked 1536x864, this PR merged onto our stack (main of 2026-11-08 plus our changes; the nvn_mtl.m conflicts resolved as sleepyeyedjoe described, rt_preserve_bound called from mtl_queue_seal).

Picture: it fixes two things. The Kokiri ground-shadow drop (#1703), and the game's whole-screen one-frame fog flicker: the frame-start fog compute binds the sun cascade right after a signal_event, and with the waits ignored it ran against the wrong cascade whenever the host stalled. In-process A/B on an M4 Pro under CPU load (the stall that reproduces it), 150-frame phases alternating the waits honoured / dropped in one run: 0 whole-screen events in 600 frames honoured against 7 dropped.

Cost on the iPad: the chunk granularity. qe_submit cuts at every signal, wait and recorded fence whether or not anything waits on it, and each chunk is a host submission (command buffer, allocator lease, visibility buffer, completion record, commit, and with the encode thread a rec_drain()):

same scene, iPad Pro (M5) waits ignored this PR
host submissions per frame 6 37
GPU idle inside the frame, p50 0.00 ms 2.5 ms
frame, p50 20.4 ms 22.9 ms
64-draw segment commits per frame 6.4-6.9 1-3

GPU time per frame is the same. The frame listing shows 35 chunks for 6 guest submissions, alternating queue 5 (compute) and queue 1 (graphics) about 12 times, cut by 19 signals and 26 waits all satisfied by the scheduler before the next chunk; the GPU profile shows 1.6 ms of idle before the 760-draw colour pass (the segment counter restarts per chunk, so the pass is committed whole where it used to be fed in 64-draw segments) and about 60 smaller gaps at the other boundaries. It is not a CPU wait on the previous chunk's fence (qe_read only picks the projected value, sync waits are satisfied by nvn_qs_scheduled).

Follow-up offered as #1749, on this PR's head: DM_NVN_CHUNK_MERGE=1 keeps the chunk's command buffer open across the steps one pump offers and encodes the next chunks into it (mtl_chunk_end / mtl_chunk_begin do the chunk's landing and per-chunk state without a commit); the buffer commits when the pump has no ready step left, before a present, and in submission timestamp mode at a logical-submission change (one clock per buffer). The open buffer's fence is g_fence + 1 for the chunk and the signal's projection, and every CPU wait on a fence runs its pump first under the submission lock, so nothing waits on an uncommitted fence. Ordering is unchanged. Off by default; the PR's tests pass both ways. If you would rather have it inside this PR, the one commit cherry-picks onto 5269ecc5; otherwise it waits for this one to land. The iPad numbers of the merged form follow when the device run is done.

@lunks

lunks commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Queue scheduler (ea0c98d): CPU cost on Ocarina of Time, M5 Pro

ea0c98d copies, snapshots and replays every submission on a worker thread. On Ocarina of Time (0400486024DEC000 v131072) headless on an M5 Pro, same saves, against our port of #1245 (the scheduler only when waits are recorded, one allocator lease per logical submission), steady 300-frame windows:

#1245 port ea0c98d
Field: frame p50 16.60 ms 17.19 ms
Field: submit+present p50 / translation CPU / instructions per frame 6.01 ms / 5.79 ms / 67 M 7.70 ms / 6.90 ms / 78 M
Market: frame p50 16.66 ms 20.1-20.5 ms
Market: submit+present p50 / translation CPU / instructions 6.0-6.4 ms / 5.7-6.4 ms / 71 M 8.3-8.6 ms / 7.6-7.8 ms / 83 M
command allocator waits 0-1 in a whole run 0.86 per frame (pool of 8)

GPU time is the same (Field 13.8 ms, Market 16-16.5 ms); instructions per frame are +16-17 % at about 3,100 draws a frame.

Reading the code (no profile): nq_flatten copies each record into a node whose buffer grows one 4 KB page at a time, cap_rec_fields decodes every record, and each distinct program and texture of a submission is copied and pinned (mtl_tex_pin takes the texture-state lock the replay also takes). Each node is a full mtl_sub_begin / mtl_sub_commit taking its own command allocator, so the pool of 8 runs out and mtl_sub_begin waits on the GPU with g_sub_mu held, which the next nvn_queue_submit also needs.

Possible fixes, not tried: nodes of one submission share their first node's allocator; replay a submission's leading nodes with no wait inline from the guest's bytes; double the buffer in nq_bytes.

Same sentence as the commit message: broad NVN scheduling, regression reach uncertain. This is one game and one machine; Three Houses, Octopath and Okami were not run here.

@sleepyeyedjoe

Copy link
Copy Markdown
Contributor

A controlled A/B of this PR against main's ea0c98d, on an M4 Pro (Mac16,8, macOS 27.0.1) with Ocarina of Time v131072.

Setup. Both hosts share the same shader chain (058136f61e9e) and the same tree: main 1794a35 + #1707 + #1742 + #1752. Host B reverts ea0c98d and merges this PR; the conflicts were resolved as in my earlier comment. Replay of a fixed Kokiri route with DM_FRAME_DELTA=0, fullscreen, present_snap 1, the same warm pipeline cache, prewarm list off. Each run is 200 s; the figures are the mean of the 300-frame windows after frame 1,500.

Host frame p95 frames > 25 ms GPU p50 submit+present p50
main (ea0c98d) 16.84 ms 3 9.55 ms 5.08 ms
this PR 17.93 ms 8 10.48 ms 0.34 ms

On that light route ea0c98d paces better and has less GPU idle.

Real play (Kokiri village, handheld, present_snap 1, the same app) went the other way:

  • ea0c98d: submit+present was 6.7–9.0 ms a frame and the frame step reported "CPU 98–99 % on performance cores". There were 12 hold changes, one whole 300-frame window at 33.3 ms, and several with p95 at 33 ms. The player saw the picture turn blocky after loading.
  • This PR, in the same place: submit+present 0.4–0.6 ms, no 30 fps windows, frame p50 16.4–17.0 ms.

That matches lunks's M5 Pro numbers above.

Not measured: 04419bc, which landed after this build. It fixes the stale frame-start timestamp and the WRITE-signal waits, but I don't see it changing the per-submission CPU cost.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0400C3F00006E000 A game profile 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.

5 participants