Skip to content

Draw passes overlap again while image heaps are alive and after fragment-only game barriers - #1970

Closed
AdyaGMD wants to merge 3 commits into
seedds:mainfrom
AdyaGMD:pass-barrier-overlap
Closed

AdyaGMD wants to merge 3 commits into
seedds:mainfrom
AdyaGMD:pass-barrier-overlap

Conversation

@AdyaGMD

@AdyaGMD AdyaGMD commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

What and why

With DM_NVN_BARRIER=game, a draw pass that follows another draw pass can skip the full consumer barrier, so its vertex work overlaps the previous pass's fragments. Two conditions turned that off far more often than they needed to:

  • Image heaps. While any image heap (a resolution family, image_texture) was alive, every draw pass ended and began with the full barrier, in every game. Ocarina of Time (0400486024DEC000) keeps one from its first frame, and its log showed saved 0 for every window. Donkey Kong Bananza's latest run shows the same saved 0.
  • Game barriers. Any nvnCommandBufferBarrier since the last pass gave the next pass the full barrier. Ocarina of Time issues about 60 a frame between its passes, mostly bits 0x12: order fragments, invalidate texture.

A Metal System Trace in Ocarina of Time's slowest area on an M1 showed what this costs. In 103 frames, vertex work started while the previous pass's fragments still ran only 135 times out of about 10,000 pass changes. The seams between and inside passes held about 4.6 ms of idle GPU a frame.

What changes:

  • Image heaps: they no longer force the full barrier. A pass loads and stores its attachments in its fragment stage. The relaxed end barrier already orders the previous pass's vertex and fragment stages before the next pass's fragment, dispatch and copy stages, and it carries resource_vis(), so the alias flush still happens while a heap is alive. Encoders other than draw passes keep their full barriers.
  • Game barriers: the bits are now kept. NVN's ORDER_FRAGMENTS (and its tiled form) orders later fragment work only; later primitives may start at once, even on the Switch. INVALIDATE_TEXTURE / INVALIDATE_SHADER make writes visible to that ordered work. The relaxed end barrier covers all of that, so only ORDER_PRIMITIVES and ORDER_INDIRECT_DATA keep the next pass behind the full barrier.
  • Stats: a new line, draw-pass full barriers up to frame N, per frame, counts why passes still began behind a full barrier: after a non-draw encoder, after a game barrier, image heaps, or barrier mode full.
  • Switches: DM_NVN_BARRIER_HEAP=0 and DM_NVN_BARRIER_GAME_BITS=0 each restore the old behaviour.

Checked

  • Ocarina of Time 0400486024DEC000 v131072, Mac app, M1, macOS 26, 30 fps target, MetalFX upscaler on, gameplay through its slowest area. Picture checked by eye in both runs: no flicker, stale images or broken shadows.

    Before Heap change Heap + game-barrier bits
    Passes without the full barrier (per 300 frames) 0 of about 32,000 4,000–8,000 17,600–25,000
    Full barriers after a game barrier, per frame (not counted) about 60 about 5
    Slowest area, GPU p50 about 39.5 ms about 39.5 ms about 37.2 ms
    Slowest area, fps about 20 about 20 about 24
    • In the last run, the passes' own GPU times in a submission add up to more than its span, so they now overlap.
    • Dynamic resolution sat at a lower level during the last run's slowest stretch (1366x768 against 1440x810), so part of the gain may come from that.
  • No local tests run. This is encode-order logic in nvn_mtl.m with no unit test covering it.

  • Not checked: other games. Donkey Kong Bananza's logs show the same saved 0 pattern, so it should benefit too, but it was not run with this change. Games that read the previous pass's output in a vertex shader after only an ORDER_FRAGMENTS barrier would show it as stale or flickering geometry; NVN itself does not order that case.

Docs

  • No reference entry: the rules are described in the comments next to bar_heap_ok and bar_game_full in nvn_mtl.m
  • No game files, keys or data extracted from a game in the change or the attachments

🤖 Generated with Claude Code

…aps are alive and after game barriers that only order fragments: any live resolution family put every pass of every game behind a full barrier at both ends (Ocarina of Time keeps one from its first frame, and Donkey Kong Bananza's latest run saved none of about 85 a frame either), and every game barrier did the same for the next pass, although NVN's ORDER_FRAGMENTS lets later primitives start at once. The relaxed end barrier already orders the previous pass's vertex and fragment stages before the next one's fragment, dispatch and copy stages, with the alias flush while a heap is alive; only ORDER_PRIMITIVES and ORDER_INDIRECT_DATA keep the full one. A new stats line counts why passes still began behind a full barrier. On an M1 in Ocarina of Time's slowest area (a Metal System Trace showed vertex work under the previous pass's fragments 135 times in 10,000 pass changes): passes without the full barrier 0 → about 70%, GPU p50 about 39.5 → 37.2 ms, 20 → 24 fps, picture unchanged. DM_NVN_BARRIER_HEAP=0 and DM_NVN_BARRIER_GAME_BITS=0 restore each

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
@cover93-95

Copy link
Copy Markdown

iPhone test: it works, with one caveat on the end-barrier stages

Setup: iPhone Duo (iPhone19,4, iOS 27.2), Ocarina of Time v131072, docked, 30 fps target, the game's own upscaler (agl_ndsr_vr, no MetalFX), main pass pinned at 2048x1152, encode thread off. Shell built from a fork of main 37f076a (chain b70f92017083) with this PR picked. Same area both runs (Deku Tree), ~10 min each, unplugged.

Before With #1970
Draw-pass entry barriers, full / saved (whole session) 33,169 / 0 4,441 / 23,170
Full barriers left per frame – 11.5 after a non-draw encoder, 3.3 after a game barrier, 0 for image heaps
GPU span per frame (median) 29.7 ms 27.2 ms
CPU energy (mean) 0.73 W 0.59 W
Thermal state "serious" reached at 392 s 466 s
Attachment traffic per frame ~453 / 432 MB ~468 / 452 MB (unchanged, as expected)

One run each and the starting temperature wasn't controlled ("fair" came at 142 s vs 71 s), so take the thermal line as a trend, not a measurement.

Merge note: my base already had the end barrier with MTLStageObject | MTLStageMesh on both sides (the t_pass_end_stages fix: without them the next pass's task/mesh shaders start before this pass's fragments are done). This PR's enc_r_end_pass drops them. I kept the base's stage masks with your bar_heap_ok() / g_end_heap condition. You may want to rebase onto that.

Picture: fine overall. One possible artifact: a translucent, blurry, flickering shadow-like patch on the wall facing the door in the Deku Tree basement torch room. No baseline from that room, so it may be unrelated. I didn't A/B it with DM_NVN_BARRIER_HEAP=0 / DM_NVN_BARRIER_GAME_BITS=0.

AdyaGMD and others added 2 commits November 9, 2026 15:06
…ges on both sides: a mesh pipeline's pass ends after its task and mesh work, and the next pass's task and mesh shaders wait for this pass's fragments. Without them a pass of mesh-shader work could start before the fragments it reads were done; with more passes now taking the relaxed end, an iPhone run of Ocarina of Time (iOS 27.2, docked, the game's own upscaler) was tested with these masks

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

AdyaGMD commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the iPhone run, that's very useful: entry barriers 33,169 full / 0 saved → 4,441 / 23,170, and GPU span 29.7 → 27.2 ms.

End-barrier stages: you're right. I merged current main and added MTLStageObject | MTLStageMesh on both sides of the relaxed end barrier in enc_r_end_pass, the masks your base has, so the next pass's task and mesh shaders wait for this pass's fragments. Main didn't have them either. With this PR more passes take the relaxed end, so it matters more here.

The Deku Tree torch room patch: could you A/B it in the same room?

  • DM_NVN_BARRIER_HEAP=0 turns off the image-heap part.
  • DM_NVN_BARRIER_GAME_BITS=0 turns off the game-barrier part.

If the patch goes away with one of them, that tells me which rule it comes from. If it stays with both set to 0, it isn't this PR.

🤖 Generated with Claude Code

@dpadr

dpadr commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Data point from a MacBook Air M2 (10-core GPU, 16 GB), macOS 27.0, Ocarina of Time v131072.

Build: this PR as a host-only change on our OoT tree. The A side is the same tree without it: base 7f50222 plus #1833, #1902 and #1897, shaders unchanged. Handheld, 1440x810 main pass (dynamic resolution off), MetalFX, frame-rate hold off, target 60. The route walks Kokiri Forest.

It is active. Draw-pass entry barriers saved per 300 frames went from 0 to about 26k; about 10k stay full. Per frame, the ones that stay full are about 28 after a non-draw encoder (compute or copy) and about 5 after a game barrier.

But on the M2 it doesn't change the frame. Paired A/B, 5 runs a side plus a Metal System Trace of each, windows with the CPU clock below 2.9 GHz excluded:

without with #1970
frame p50 30.10 ms 30.20 ms
frame p95 33.36 ms 32.89 ms
GPU span p50 27.41 ms 27.71 ms
GPU busy (trace) 67.6 % 65.3 %

All of these are within our run-to-run noise. 0 hangs, and no new [RED] lines.

Why, from the traces: OoT has about 157 encoders a frame here (about 100 render passes, 53 compute), and about 4 ms a frame of encoder-to-encoder idle in gaps of about 17–18 µs each. The gap count, total and median are the same with and without the PR (4.04 vs 4.07 ms, 17.3 vs 18.1 µs). So on this GPU the seams look like a fixed cost per encoder boundary (start and end, load and store, tile setup), not the barrier. That matches an earlier M4 Pro test on this game where relaxing the draw-pass barriers left the GPU span unchanged. Fewer encoders or passes would be the lever here.

This doesn't argue against the PR: it's correct and active, and the M1 numbers in the description may hold on other GPUs. It just doesn't move OoT on an M2.

FeReuss pushed a commit to FeReuss/nxnative-oot that referenced this pull request Nov 10, 2026
…CT_DATA make wait, and a counter of why draw passes begin behind a full barrier

Both ideas are from PR seedds#1970 (AdyaGMD), ported onto our pass-overlap rules, each behind its own lever:

- DM_NVN_BARRIER_GAME_BITS (on by default; =0 off, =ab alternates with DM_AB): a game barrier makes the next draw pass wait for everything only when it carries ORDER_PRIMITIVES or ORDER_INDIRECT_DATA. The narrower rule it replaces (off) also made INVALIDATE_SHADER wait; the two differ on that one bit. The decision is one function, game_barrier_needs_full, instead of a mask in the barrier handler.
- DM_NVN_BARRIER_WHY (on by default; =0 off): the stats line 'draw-pass barriers up to frame N, per frame' says why passes still began behind a full barrier (after a non-draw encoder, after a game barrier that needs everything, image heaps alive, draw_barriers full) and how many began beside the ones before.

t_barrier_bits checks the truth table of both rules and the counter's hand-over, no GPU work, in the default and with each lever off (make check_t_barrier_bits). Not measured on a game: the effect on frame time is for an A/B run (DM_NVN_BARRIER_GAME_BITS=ab DM_AB=300).

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
…CT_DATA make wait, and a counter of why draw passes begin behind a full barrier

Both ideas are from PR seedds#1970 (AdyaGMD), ported onto our pass-overlap rules, each behind its own lever:

- DM_NVN_BARRIER_GAME_BITS (on by default; =0 off, =ab alternates with DM_AB): a game barrier makes the next draw pass wait for everything only when it carries ORDER_PRIMITIVES or ORDER_INDIRECT_DATA. The narrower rule it replaces (off) also made INVALIDATE_SHADER wait; the two differ on that one bit. The decision is one function, game_barrier_needs_full, instead of a mask in the barrier handler.
- DM_NVN_BARRIER_WHY (on by default; =0 off): the stats line 'draw-pass barriers up to frame N, per frame' says why passes still began behind a full barrier (after a non-draw encoder, after a game barrier that needs everything, image heaps alive, draw_barriers full) and how many began beside the ones before.

t_barrier_bits checks the truth table of both rules and the counter's hand-over, no GPU work, in the default and with each lever off (make check_t_barrier_bits). Not measured on a game: the effect on frame time is for an A/B run (DM_NVN_BARRIER_GAME_BITS=ab DM_AB=300).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
AmateurWizrd pushed a commit to AmateurWizrd/nxnative that referenced this pull request Nov 10, 2026
…M_NVN_BARRIER_HEAP=0 restores the full barriers)

Net change of PR seedds#1970 at 4b6ab0f (seedds/nxnative), merged by hand: the heap relaxation and the draw-pass full barriers line are taken; the game-barrier rule stays this tree's bar_frag_only (bar_game_full wraps it), so DM_NVN_BARRIER_GAME_BITS is not read.
@fheitzer

Copy link
Copy Markdown

Measured on an M1 Max (32-core GPU, applegpu_g13s, 32 GB, macOS 27.0.1, Xcode 27.2), Ocarina of Time v131072, main at f02d047 (chain cb2e91c138e3, seen-shader library), handheld (DM_OPMODE=0), 60 fps target, DM_FPS_HOLD=off, window on the built-in screen. scripts/bench.py, kokiri-deck route from a Kokiri Forest save, variants alternating in one invocation.

runs fps frame p50 / p95 GPU p50 (summed) frames > 25 ms
main 3 46.1 (45.9–46.3) 21.32 / 23.92 ms 19.41 ms 52
main + #1970 3 48.3 (48.0–48.3) 20.41 / 22.87 ms 18.15 ms 39

A second invocation (2 runs each) gave the same: 46.35 → 48.1 fps, GPU 19.31 → 18.20 ms. On the standing part of the route (identical view), frames held two vsyncs went from 28% to 22%. Screenshots match main. It cherry-picks cleanly onto f02d047.

@aCidcOde

Copy link
Copy Markdown

Data point from an M4 Pro (MacBook Pro, 24 GB, macOS 26.7.2), Ocarina of Time v131072, with this change at adb2c1b applied cleanly on our tree (an older main). The test was a Kokiri Forest route in a window, from the same save copy each run: two runs with this change and two with DM_NVN_BARRIER_HEAP=0 DM_NVN_BARRIER_GAME_BITS=0.

with this change both switches =0
draw-pass entry barriers per 300 frames full 8,190 / saved 21,135; full 8,161 / saved 22,715 full 29,535 / saved 0; full 29,853 / saved 0
full barriers per frame after a non-draw encoder ~20, after a game barrier 7.2–7.7, image heaps 0 after a non-draw encoder ~20, image heaps 78–80
GPU p50, mean of the run's windows 15.9, 15.3 ms 15.3, 15.1 ms
RED lines 0 0

The change does what it says: about 73% of the full barriers are gone. On this GPU, though, the GPU p50 stayed the same within the route's noise, so the seams cost little here. The M1 numbers in the description may be the case where it pays.

These numbers are for adb2c1b, before the merge with main and the end-barrier stage change (4b6ab0f).

@seedds

seedds commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Superseded by e775054 on main (from #2334), with you as co-author for the diagnosis. Both rules are there: image heaps no longer turn off the relaxed barriers, and fragment-only game barriers keep the overlap. The difference is where the alias flush goes. #2334 puts it on the next pass's start barrier, before its fragments, instead of on the end barrier, which costs about 100 KB of command allocator per encoder (see 'The alias flush on every encoder's end barrier' in frame-rate.md). The DM_NVN_BARRIER_HEAP / _GAME_BITS switches weren't taken, since DM_NVN_BARRIER=full gives the A/B. On the stages: MTLStageObject is now on the end barrier's 'after' side. It isn't on the 'before' side: NVN doesn't order later task or mesh work after ORDER_FRAGMENTS, just as it doesn't order vertex work. The device numbers from this thread (M1 Max, iPhone, M2 Air, M4 Pro) are in the reference entry. @cover93-95: the Deku Tree torch-room patch is listed as open. An A/B with DM_NVN_BARRIER=full in that room would settle it.

@seedds seedds closed this Nov 12, 2026
Kayyluhh added a commit to Kayyluhh/nxnative that referenced this pull request Nov 14, 2026
…he object and mesh stages on both sides: a mesh pipeline's pass ends after its task and mesh work, and the next pass's task and mesh shaders wait for thi

# Conflicts:
#	dm/gfx/nvn/nvn_mtl.m
Kayyluhh added a commit to Kayyluhh/nxnative that referenced this pull request Nov 14, 2026
…BARRIER_HEAP (seedds#1970), DM_NVN_IND_SPLIT (seedds#2226) and DM_NVN_VCONV_KEEP (main), from the code that reads them
Kayyluhh added a commit to Kayyluhh/nxnative that referenced this pull request Nov 14, 2026
…closed, superseded by seedds#2334, on main as e775054; its two env_switches rows (39aa802) go with it
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.

6 participants