Skip to content

Compute moves ahead of scene passes past occlusion queries and conditional rendering; clears wait across dispatches - #1952

Open
AdyaGMD wants to merge 4 commits into
seedds:mainfrom
AdyaGMD:oot-scene-pass-hoist
Open

AdyaGMD wants to merge 4 commits into
seedds:mainfrom
AdyaGMD:oot-scene-pass-hoist

Conversation

@AdyaGMD

@AdyaGMD AdyaGMD commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What and why

In Ocarina of Time (0400486024DEC000) the heavy frames on an M1 run about 150 encoders and 110 passes, and the GPU spends about 19% of its time in gaps between them. Compute work splits the scene passes, and cs_hoist_try, which moves a dispatch ahead of the pass it would split, never moved one. Every scan stopped at one of these:

  • Occlusion queries. The game resets and reports a samples-passed query around nearly every scene draw (58 in one pass).
  • Conditional rendering. Each object is drawn only if its query passed, so render-enable switches sit between the draws.
  • Read-only storage buffers. Any storage buffer bound to a draw stopped the scan.
  • Pending clears. A pass with clears waiting to land was not scanned.
  • Uniform-buffer updates. The game writes about 80 a frame inside its scene passes.
  • Game barriers. About 70 a frame between the scene draws.

Each case, and what this change does:

  • Queries: a dispatch draws no samples, so it moves past samples-passed resets and reports as long as it reads and writes none of the reports' memory (16 bytes each).

  • Conditional rendering: it skips draws and clears, never a dispatch, so a dispatch moves past it. If a dispatch writes the two counters a condition compares, the scan stops.

  • Storage buffers: a draw's read-only storage buffer counts as a read range instead of stopping the scan.

  • Clears:

    • A pending clear no longer stops the scan.
    • While a dispatch moves, the pass's pending clears are set aside, so it neither lands nor queues them.
    • A dispatch that touches none of a waiting clear's targets no longer forces the clear into a clear-only pass. The clear waits for the next pass to take as its load action.
  • Uniform updates: a dispatch moves past an update whose bytes it neither reads nor writes. The updated range joins the ranges the scan checks.

  • Barriers: a barrier orders nothing the moved dispatch depends on, for three reasons:

    • The draws before it in the pass write no memory and no attachment it samples.
    • Moved dispatches keep a barrier between each other, because each dispatch is recorded as writing everything.
    • The pass that opens after them starts behind a full barrier.

    Replay still runs the barrier at its original place, for the draws after it. DM_NVN_CSH_BARRIER=0 restores the old stop.

  • Stats: a new log line names the ops that stopped the most scans. In Ocarina of Time, render-target changes are now almost all of them, and those really do end the pass.

Checked

  • Ocarina of Time 0400486024DEC000 v131072, Mac app, M1, macOS 26, handheld, 30 fps target, opening and gameplay. The pass-dump and gpuprof data come from the same game.
    • Before: compute dispatches moved ahead of their pass 0.
    • After the query, conditional-rendering, storage-buffer and clear changes: 300 in 300 frames (about one a frame).
    • After the uniform-update and barrier changes: 760–900 in 300 frames.
    • waiting clears taken by a later pass: about 300 in 300 frames. The clears that still land on their own are cleared right before a dispatch that uses them.
    • The picture was right.
    • Gameplay went from 24–25 fps to 27–30 fps, measured together with the MetalFX upscaler PR.
    • In the heavy area at 1366x768, the uniform-update and barrier changes took play from 18–22 to 27–30 fps, and the median frame from about 44 ms to 33–36 ms. The heavy submission's GPU time went from 30–35 to 25–27 ms, and its attachment loads from 238–260 MB to about 200 MB. The two runs may not have been in exactly the same spot.
    • No new RED lines.
  • Tests: dm/gfx/tests/t_perf_order.m was updated for csh_cs's new arguments. The GPU tests were not run locally (Apple-silicon Metal tests; CI has no Metal 4).
  • Not checked: other games. This is host code every game runs, and no regress.py report was made. The new paths only add moves past ops the scan used to stop at. Each is guarded by the memory ranges it reads or writes, except barriers, which rely on the ordering described above.

Docs

  • No reference entry: this is a scan rule in nvn_mtl.m, described in its comments
  • No game files, keys or data extracted from a game in the change or the attachments

🤖 Generated with Claude Code

AdyaGMD and others added 2 commits November 9, 2026 09:35
…dispatch moves ahead of the pass past occlusion-query resets and reports (samples passed) whose memory it neither reads nor writes, and past draws whose storage buffers are read-only, where every scan used to stop at the first query; and clears with no draw wait across a dispatch that touches none of their targets, for the next pass to take as its load action, instead of a clear-only pass that stores both targets for the next to load again

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dering too: Ocarina of Time draws each object only if its occlusion query passed, and every scan stopped at the first render-enable switch, so no dispatch moved. Conditional rendering skips draws and clears, never a dispatch; a dispatch that writes the two counters a condition compares stops the scan. On an M1 one dispatch a frame now moves ahead of the pass it used to split (300 in 300 frames, 0 before)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) label Nov 9, 2026
…updates and game barriers too: once conditional rendering passed, Ocarina of Time's scans stopped at its uniform updates (about 80 a frame in the scene passes) and then at its barriers (about 70). A dispatch moves past an update whose bytes it neither reads nor writes. A barrier orders nothing the moved dispatch depends on: the draws before it write no memory and no attachment it samples, moved dispatches keep a barrier between each other, and the pass that opens after them starts behind a full barrier; replay still runs the barrier in place (DM_NVN_CSH_BARRIER=0 stops the scan there). The stats now log which ops stop the scans. On an M1 at 1366x768: 760-900 dispatches moved per 300 frames (300 before), the heavy submission 30-35 → 25-27 ms of GPU, heavy-area play 18-22 → 27-30 fps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cover93-95

Copy link
Copy Markdown

iPhone test: no dispatch moved

Setup: same as my #1970 report. iPhone Duo (iOS 27.2), Ocarina v131072, docked, 30 fps, the game's own NDSR upscaler (not MetalFX), main pass 2048x1152, Deku Tree, ~8 min. This PR was picked together with #1970 onto main 37f076a + a few fixes.

  • compute dispatches moved ahead of their pass: 0 for the whole session.
  • compute hoist … scans stopped by op: 8=25978 38=432 5=300, i.e. almost always OP_SET_RT, then 38 (OP_CLEAR_TEX if I count the enum right) and OP_REPORT_COUNTER.

So here the game's dispatches seem to sit right after a render-target change, at the start of a pass, and the scan never gets past it. Your run was handheld with MetalFX. Could docked plus the game's own upscaler change where the dispatches land? Happy to run a build with more logging if that helps. No picture issue seen (nothing moved, as expected).

…h to the targets the pass already has: replay continues the pass across such a switch (same-target continuation), but the hoist's scans stopped there. In Ocarina of Time almost every scan stopped at OP_SET_RT (about 27,000 per 300 frames on an M1; 25,978 in a docked iPhone session, where no dispatch moved at all), and the passes on its 1440x810 scene targets split around the compute after each switch. A switch to other targets, with the continuation off, or at a deferred pass's start still stops the scan. The hoist's stats line adds the same-target switches the scans went past. Not yet run

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AdyaGMD

AdyaGMD commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, this matches what I'm seeing on the Mac. There, about 27,000 scans per 300 frames stopped at op 8 (OP_SET_RT), and the moves came only from the few that got past it.

The game sets its scene targets again before each group of compute between its draws. Replay already continues the pass across a switch to the same targets (same-target continuation), but the hoist's scan stopped at every OP_SET_RT. So docked or not, with or without MetalFX, a dispatch right after such a switch never moved. That fits your 0.

I've added a commit: the scan goes past a render-target switch when replay would continue the pass across it, meaning the same targets, the continuation on, and not a deferred pass's start. Any other switch still stops it. The stats line now also prints same-target switches passed N, which tells us whether the switches are really to the same targets. It hasn't been run yet. If you can run it again in the Deku Tree, the numbers to look at are moved ahead of their pass, the new count, and the scans stopped by op list.

38 is OP_CLEAR_TEX, yes.

🤖 Generated with Claude Code

thomasruegsegger pushed a commit to thomasruegsegger/nxnative that referenced this pull request Nov 9, 2026
… ahead of Ocarina of Time's scene passes past queries, conditional rendering, uniform updates, game barriers and same-target switches (M1: heavy-area play 18-22 -> 27-30 fps)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aax1HnchwCweUDRNWjhHx9

# Conflicts:
#	dm/gfx/nvn/nvn_mtl.m
thomasruegsegger pushed a commit to thomasruegsegger/nxnative that referenced this pull request Nov 9, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aax1HnchwCweUDRNWjhHx9
FeReuss pushed a commit to FeReuss/nxnative-oot that referenced this pull request Nov 10, 2026
…e passes past occlusion queries and conditional rendering; clears wait across dispatches

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
FeReuss pushed a commit to FeReuss/nxnative-oot that referenced this pull request Nov 10, 2026
… and left out of the table

test_env_switches failed on the integrated tree with it missing; the meaning is from the code comment at cs_hoist_try.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
FeReuss pushed a commit to FeReuss/nxnative-oot that referenced this pull request Nov 10, 2026
…f by default

PR seedds#1952's commit 6a512fa lets a dispatch move ahead of the pass past an explicit game barrier. t_perf_order's 'explicit game barrier prevents hoist' check fails with it on (the integrated tree failed make check_t_perf_order; the PR never ran that GPU test and left it as it was). Off, a barrier stops the scan as on main and the check passes. The rest of seedds#1952 stays on.

The PR measured the barrier part at 760-900 dispatches moved per 300 frames, 30-35 -> 25-27 ms for the heavy submission and 18-22 -> 27-30 fps in the heavy area (M1, 1366x768, Ocarina of Time), so it is worth an A/B run: DM_NVN_CSH_BARRIER=1.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
seedds#1901 seedds#1459 seedds#1928 seedds#1942 seedds#1940 seedds#1952 seedds#1611 seedds#2065 seedds#2083 seedds#2008 seedds#2079 seedds#2022 seedds#2075 seedds#2077 seedds#2020 seedds#2021 onto main 04811a6

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7

Upstream pull requests applied in this commit (their authors' work, brought onto this tree with a three-way merge; the description is in each PR):
- seedds#1459 by PromethOps (open): DM_UPSCALER=metalfx takes agl's agl_ndsr_vr.pck networks too, and a variant with no curve and no power is drawn as one colour space: Ocarina of Time's upscaler is MetalFX's
- seedds#1611 by Esteban-V (open): An argument block whose only change is a uniform buffer is patched instead of encoded again
- seedds#1774 by uncraftablexd (closed): Compute threads that exit before a barrier keep arriving at it, and every shader loop has a cap (GPU hang defences), chain change
- seedds#1824 by cocoman93 (open): Clamp texel fetches past a sampled 2D texture's edge: no hard edge in Ocarina of Time's mist
- seedds#1901 by FifteenPen (open): The game's compute dispatches with no game barrier between them run side by side (profile key dispatch_overlap, on for Ocarina of Time): Kokiri Forest GPU p50 15.63 → 14.76 ms
- seedds#1928 by FeReuss (open): Ocarina of Time: enc_thread on, the replay encodes on a thread of its own
- seedds#1929 by FeReuss (open): Unused border corrections are compiled out of pipelines that bind no sampler with a twin (function constant DMSH_BORDER_FC)
- seedds#1930 by FeReuss (open): A float add to storage memory with no result read is one native atomic add instead of a compare-and-swap loop
- seedds#1940 by uncraftablexd (open): A prewarm list's lines after its #@after_first_frame line are built after the first frame, in the background, instead of in the prepare step (issue seedds#1747)
- seedds#1942 by uncraftablexd (open): Frame step: late frames with the GPU idle do not step up, and a longer hold the GPU does not explain is probed
- seedds#1952 by AdyaGMD (open): Compute moves ahead of scene passes past occlusion queries and conditional rendering; clears wait across dispatches
- seedds#2008 by bquilty25 (open): nvn: reactive memory watchdog, 16-way view cache, and texture upload batching
- seedds#2020 by padsiwono (open): Ocarina of Time v131072: Output Native draws the full output size instead of the game's 80 % frame (`DM_OUTPUT`)
- seedds#2021 by padsiwono (open): Ocarina of Time v131072 renders at the launcher's Resolution (`DM_RENDER_SIZE`), docked
- seedds#2022 by sleepyeyedjoe (closed): Ocarina of Time: large models no longer go black for one frame twice, 4–5 frames apart, when the game streams new textures into them (a texture ID a shader has read keeps its read mark when it gets another texture)
- seedds#2030 by humanperzeus (open): A NaN colour at alpha 0 is returned as 0, as NVIDIA's blender takes a zero factor: Ocarina of Time's white clouds over the ferns are gone
- seedds#2035 by Know-It-All-Brother (open): The shader build runs as worker processes, and no shader waits for a batch (Ocarina of Time's cold Mac map: 3,002 s to 1,298 s)
- seedds#2065 by Raymon82777 (closed): A vertex library with layer entry points of its own gets bases for its main0 pairs: Ocarina of Time's 230 ms camera-turn hitch in Hyrule Field goes to 39 ms
- seedds#2075 by sleepyeyedjoe (closed): Ocarina of Time: a patch of scenery no longer draws dark for one frame when the game gives it a sharper texture (a pool texture the walk does not make holds a stand-in of its own colours until its first read)
- seedds#2077 by roandegraaf (closed): Ocarina of Time: fix the "plaid" terrain / missing texture tiles (GPU-drawn textures now reach the storage the game copies)
- seedds#2079 by uncraftablexd (open): Render pipelines that share a vertex function are taken from the archive or compiled one at a time: the AGX driver crashes in createVertexProgramVariant's completion block
- seedds#2083 by FifteenPen (closed): Ocarina of Time's prewarm list gets 41 more task / mesh pipelines from later play
thomasruegsegger pushed a commit to thomasruegsegger/nxnative that referenced this pull request Nov 10, 2026
…s, and t_perf_order follows seedds#1952

pool_heap_texture (DM_NVN_POOL_HEAP) ran before rh_texture (DM_NVN_RT_RECYCLE, 7f50222) in tex_mtl_ and took almost every render target it covers, so the recycling that keeps Ocarina of Time's dynamic resolution from adding hundreds of MB per 300 frames never ran; upstream also found that placing by pool offset corrupts the picture while that resolution moves. DM_NVN_POOL_HEAP=1 turns it back on. On the Mac, t_rt_recycle failed 8 of 18, t_image_alias 1 of 336 and t_pool_heap off 1 of 3 with the heap first; t_pool_heap now asks for the heap, and its off mode turns rh_texture off too.

t_perf_order: since seedds#1952 a game barrier after draws that write no memory no longer stops the compute hoist (DM_NVN_CSH_BARRIER=0 restores the stop), so the hazard case expects the move unless that switch is off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aax1HnchwCweUDRNWjhHx9
@aCidcOde

Copy link
Copy Markdown

Data point from an M4 Pro (MacBook Pro, 24 GB, macOS 26.7.2), Ocarina of Time v131072. This change, at 6a512fa, was ported onto our tree (an older main): 7 of 8 hunks applied as is, and the lc_land line was placed by hand because our condition has (g_ret_n || g_nrest).

Tests: check_t_perf_order fails 1 of 36: explicit game barrier prevents hoist. It passes 36/36 with DM_NVN_CSH_BARRIER=0.

  • In that case the dispatch writes only the storage buffer, and the draw before the barrier writes only the colour target.
  • The new barrier rule therefore allows the move, and the check's expectation looks out of date.
  • compute writes expected shared memory still passes.
  • The check is unchanged on this branch (t_perf_order.m:153), so it probably fails there too. The description says the GPU tests were not run.

Game: Kokiri Forest route in a window, from the same save copy each time, comparing this change with DM_NVN_CS_HOIST=0. Without this change, our build moved 0 dispatches, as in the description.

with this change DM_NVN_CS_HOIST=0
dispatches moved per 300 frames about 900 0
GPU p50, first 4 windows (ms) 15.5, 16.8, 15.8, 12.2 15.4, 16.9, 15.9, 12.6
frame p50, same windows (ms) 17.4, 18.6, 17.6, 16.6 17.3, 18.6, 17.6, 16.6
RED lines / picture 0 / right 0 / right

About 3 dispatches a frame moved, which matches uncraftablexd's M4 Pro count above. On this GPU there was no measurable gain, so for now our build for the player leaves the change out.

These numbers are for 6a512fa. The render-target switch commit that came after it (7d26a8d) was not tested.

@dpadr

dpadr commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

At head 7d26a8d, dm/gfx/tests/t_perf_order.m fails its own barrier check: "explicit game barrier prevents hoist".

This PR lets the hoist pass a game barrier when the draws before it write no memory (csh_bar_pass(), on by default). The test still expects no hoist whenever a barrier is present. It gets one hoist, so the check fails.

A one-line fix that keeps both behaviours covered: expect a hoist exactly when csh_bar_pass() allows one.

-        CHECK(hazard?"explicit game barrier prevents hoist":"independent dispatch hoisted once",g_csh_moved-before==(cshoist_on() && !hazard));
+        CHECK(hazard?"explicit game barrier: hoist only when barriers can be passed":"independent dispatch hoisted once",g_csh_moved-before==(cshoist_on() && (!hazard || csh_bar_pass())));

With that change the test passes here. I also ran the PR on Ocarina of Time (0400486024DEC000 v131072) on a MacBook Air M2 (10-core GPU, 16 GB), macOS 27.0: handheld at 1440x810, Kokiri walk, in-run A/B, 8 pairs at ~3.0 GHz. GPU p50 went down 0.05 ms [−0.09, −0.035] and the frame went down ~0.035 ms. That's small but consistent. On this M2 the scans mostly stop at render-target switches, not at the cases this PR handles.

🤖 Generated with Claude Code

@XIIYAZ

XIIYAZ commented Nov 13, 2026

Copy link
Copy Markdown

This PR conflicts with main in nvn_mtl.m: since it was opened, main added NVN_RB_SBUFQ to the cases that stop the scan. How I merged it so both changes hold:

  • csh_draw_reads: keep this PR's read-only NVN_RB_SBUFP case, and put main's NVN_RB_SBUFQ into the refusing line: case NVN_RB_SBUFU: case NVN_RB_SBUFQ: case NVN_RB_IMG: case NVN_RB_TBUF: case NVN_RB_GMEM: return 0;
  • csh_cs: keep this PR's CBUF / UBOP case that also records the read, with the same NVN_RB_SBUFQ added to the refusing line after it.

I've played Ocarina of Time on that merge for several days on an M4 Max, with no stutter and no picture problem.

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

Labels

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