Skip to content

Deferred pass merge without TBDR fusion (DM_NVN_PASS_DEFER): OoT's Kokiri Forest on an M1, GPU 38.5 -> 35.6 ms a frame - #2441

Open
AdyaGMD wants to merge 2 commits into
seedds:mainfrom
AdyaGMD:pass-defer-m1
Open

AdyaGMD wants to merge 2 commits into
seedds:mainfrom
AdyaGMD:pass-defer-m1

Conversation

@AdyaGMD

@AdyaGMD AdyaGMD commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

What

The deferred pass merge (defer_try) has only run with TBDR fusion (tbdr_on()). With DM_NVN_PASS_DEFER it also runs without it: on by default on a Mac whose GPU lacks MTLGPUFamilyApple9 (the M1 and M2, where it was measured), =0 turns it off, off elsewhere.

Ocarina of Time draws its light volumes one per render pass into one RGBA8 buffer, each right after the shadow depth pass of that light. The merge moves all eleven light draws into one pass after the shadow passes: 126 → 115 passes a frame.

Extra rules when TBDR is off:

  • The deferred draw must not read bindless resources (DM_NVN_PASS_DEFER_BINDLESS=1 allows it) or write shader memory.
  • No draw in between may write shader memory.
  • The look-ahead stops after 6000 ops.
  • The pass that replays deferred draws does not hoist dispatches (cs_hoist_try).

The existing rules still apply: a dispatch, uniform update or copy ends the deferral, and nothing in between may touch what the deferred draw reads or writes.

Every 300 frames a log line counts the accepted groups and the rejections by reason.

Performance

Ocarina of Time (0400486024DEC000), Kokiri Forest looking down at the map, MacBook with an M1 (8-core GPU), fullscreen. Mesh compute from #2413 was on throughout. DM_NVN_PASS_DEFER=ab with DM_AB=600, one run, phases 1–4:

GPU p50 Frame p50 Passes a frame Attachment loads a frame
Pass defer on (2 phases) 36.08 / 35.57 ms 38.60 / 38.02 ms 115 ~200 MB
Off (2 phases) 38.61 / 38.54 ms 41.10 / 41.05 ms 126 ~300 MB

About −2.9 ms a frame. 11 groups were accepted every frame. The lighting looked right.

Switches (in docs/env_switches.tsv)

  • DM_NVN_PASS_DEFER=0|1|ab: the feature (default on for M1 / M2 Macs).
  • DM_NVN_PASS_DEFER_BINDLESS=1: allow bindless reads in the deferred draw (diagnostic).

Not run

  • Any test suite.
  • Without mesh compute.
  • iPhone.
  • M3 and later.
  • Other games.

🤖 Generated with Claude Code

…Ocarina of Time's slow spot on an M1, GPU 38.5 -> 35.6 ms a frame

The deferred pass merge (defer_try), so far only with tbdr_on(), now also runs with DM_NVN_PASS_DEFER=1|ab (off by default). OoT draws its light volumes one per pass into
one RGBA8 buffer, each after the shadow depth pass of that light; the merge moves all eleven into one pass after the shadow passes (126 -> 115 passes a frame). Extra rules
when TBDR is off: the deferred draw must not read bindless resources (DM_NVN_PASS_DEFER_BINDLESS=1 allows it) or write shader memory, no draw between may write shader
memory, the scan stops after 6000 ops, and the pass that replays deferred draws does not hoist dispatches (cs_hoist_try). A summary every 300 frames counts the accepted
groups and the rejections by reason.

Measured (0400486024DEC000, Kokiri Forest looking down at the map, M1 8-core GPU, fullscreen, with seedds#2413's mesh compute on throughout): DM_NVN_PASS_DEFER=ab,
DM_AB=600, one run, phases 1-4: on GPU p50 36.08 / 35.57 ms, frame p50 38.60 / 38.02; off 38.61 / 38.54, frame 41.10 / 41.05. Attachment loads 300 -> 200 MB a frame.
11 groups a frame accepted (3300 per 300 frames). The lighting looked right.

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
@AdyaGMD AdyaGMD changed the title Deferred pass merge without TBDR fusion (DM_NVN_PASS_DEFER): OoT's slow spot on an M1, GPU 38.5 -> 35.6 ms a frame Deferred pass merge without TBDR fusion (DM_NVN_PASS_DEFER): OoT's Kokiri Foest spot on an M1, GPU 38.5 -> 35.6 ms a frame Nov 12, 2026
@AdyaGMD AdyaGMD changed the title Deferred pass merge without TBDR fusion (DM_NVN_PASS_DEFER): OoT's Kokiri Foest spot on an M1, GPU 38.5 -> 35.6 ms a frame Deferred pass merge without TBDR fusion (DM_NVN_PASS_DEFER): OoT's Kokiri Forest on an M1, GPU 38.5 -> 35.6 ms a frame Nov 12, 2026
…e (M1, M2)

Where it was measured: the GPU lacks MTLGPUFamilyApple9 on macOS (the M1 and M2), -2.9 ms a frame at Ocarina of Time's slow spot. =0 turns it off; off elsewhere (the
iPhone and the M3 on are not measured).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UserDerezzed pushed a commit to UserDerezzed/nxnative that referenced this pull request Nov 13, 2026
…fter memory writes, not on every barrier: Ocarina of Time -1.3 ms a frame on an M1

resource_vis() put MTL4VisibilityOptionResourceAlias on every barrier while an image heap was alive, about 230 barriers a frame. Ocarina of Time keeps one from its first frame
(its dynamic-resolution targets are a resolution family, one storage shared by several sizes), and a counter-free Metal System Trace of its Kokiri Forest slow spot on an M1
showed the GPU's stages never overlapping: vertex work started under a running fragment phase in 0.9 % of kicks, compute under fragment in 0 %, and the GPU was idle a quarter
of each frame. The relaxed draw-pass barriers allowed the overlap; the alias flush on each of them took it away. A family changes the size it draws at between frames, not
inside one, so the flush is needed at a submission's start and after a write that another view of the memory might read. Now, with DM_NVN_VIS_HEAPFRAME (on by default; =0
flushes on every barrier again, ab for the DM_AB phases), an image heap's alias flush is on the first barrier of each submission (g_alias_heap_pend, set by mtl_sub_begin) and
on the first barrier after an encoder that may have written memory (g_alias_pend: any compute or copy encoder, a draw pass with shader writes, g_pass_swrite). Large chunked
pools without an image heap keep the flush on every barrier, as before.

Measured (0400486024DEC000, Kokiri Forest, MacBook M1 8-core GPU, fullscreen, with the light-pass merge and mesh compute of seedds#2441 / seedds#2413 on, in a tree with
other local changes): DM_NVN_VIS_HEAPFRAME=ab:1/3 in 300-frame phases, frame p50 36.61 / 36.25 / 37.20 / 37.27 ms with it (mean 36.83) against 38.12 / 38.16 / 38.15 without
(38.14): -1.3 ms, lower in every phase. Its GPU p50 (the sum of command-buffer durations) rises to 37.7-39.7 ms, as command buffers now overlap. With it a Metal System
Trace shows vertex under fragment in 32.8 % of kicks and the GPU idle 21 % of the time. Screenshots look right (the fog of seedds#2487 included). Dropping the flush altogether
(a diagnostic) gave -1.5 to -1.8 ms; this keeps most of that.

Not run: this branch's host in game, any test suite, the iPhone, M3 and later, other games.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant