Skip to content

nvn/barrier: image heaps take the alias flush once a submission and after memory writes (OoT −1.3 ms a frame on an M1) - #2525

Open
AdyaGMD wants to merge 2 commits into
seedds:mainfrom
AdyaGMD:heap-alias-per-frame
Open

AdyaGMD wants to merge 2 commits into
seedds:mainfrom
AdyaGMD:heap-alias-per-frame

Conversation

@AdyaGMD

@AdyaGMD AdyaGMD commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Update: on by default on M1 / M2 only

An M3 Max lost Ocarina of Time's Kokiri Forest foliage (bushes, ground ivy) in half to two thirds of frames with this rule (see the comments). The rule skips flushes Metal asks for: a draw pass's attachment stores are writes too, and the host places other texture objects over the same bytes.

Checked on an M1 (8-core GPU) at the same spot, one run, the three settings alternating every 300 frames:

Setting Frame p50 per window Foliage (frames 5 apart, pixels > 24 different)
This rule 33.36 / 33.33 / 33.47 ms steady (2.0–3.0 %)
This rule + the flush flagged after every draw pass 36.75 / 36.59 / 36.42 ms steady (2.6–2.9 %)
Flush on every barrier (before) 36.72 / 36.73 / 36.72 ms steady (3.0–4.5 %)

So the correct version gains nothing, here as on the M3 Max. The M1 draws the foliage without the skipped flushes; the M3 Max doesn't. The rule is now on by default only without MTLGPUFamilyApple9 (M1, M2). Everything else flushes on every barrier as before. DM_NVN_VIS_HEAPFRAME=1|0|ab still overrides.

Not checked: an M2.

What

resource_vis() put MTL4VisibilityOptionResourceAlias on every barrier while an image heap was alive, about 230 barriers a frame in Ocarina of Time. OoT keeps one from its first frame: its dynamic-resolution targets are a resolution family, one storage shared by several sizes.

A family changes the size it draws at between frames, not inside one. So the alias flush is needed at a submission's start, and after a write that another view of the memory might read. 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 goes on:

  • the first barrier of each submission (g_alias_heap_pend, set by mtl_sub_begin);
  • the first barrier after an encoder that may have written memory (g_alias_pend: any compute or copy encoder, or a draw pass with shader writes, g_pass_swrite).

Large chunked pools without an image heap keep the flush on every barrier, as before.

Why

A counter-free Metal System Trace of OoT's Kokiri Forest slow spot on an M1 showed the GPU's stages never overlapping, with the GPU idle a quarter of each frame. The relaxed draw-pass barriers allowed overlap; the alias flush on each of them took it away.

Before With this
Vertex work starting under a running fragment phase 0.9 % of kicks 32.8 %
GPU idle 25 % 21 %

Performance

Ocarina of Time (0400486024DEC000), Kokiri Forest, MacBook M1 (8-core GPU), fullscreen. Measured with the light-pass merge and mesh compute of #2441 / #2413 on, in a tree with other local changes. DM_NVN_VIS_HEAPFRAME=ab:1/3, 300-frame phases:

Frame p50 per phase Mean
With 36.61 / 36.25 / 37.20 / 37.27 ms 36.83 ms
Without 38.12 / 38.16 / 38.15 ms 38.14 ms

About −1.3 ms a frame, lower in every phase. GPU p50 (the sum of command-buffer durations) rises to 37.7–39.7 ms, because command buffers now overlap. Use frame time to judge it.

Dropping the flush altogether (a diagnostic, not this change) gave −1.5 to −1.8 ms; this keeps most of that. Screenshots look right, including the fog of #2487.

Switch (in docs/env_switches.tsv)

  • DM_NVN_VIS_HEAPFRAME=0|1|ab: on by default.

Not run

  • This branch's host in game (it was measured in another tree).
  • Any test suite.
  • iPhone.
  • M3 and later.
  • Other games.

🤖 Generated with Claude Code

…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>
@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
@UserDerezzed

Copy link
Copy Markdown

With this merged, Ocarina of Time's foliage in Kokiri Forest (the bushes and the ground ivy) is missing from most frames: whole draws are gone from about half to two thirds of frames, and they flicker in and out. Seen on an M3 Max (macOS 27.2) in a tree with other PRs merged; DM_NVN_VIS_HEAPFRAME=0 alone makes it steady again (#2526, in the same build, stays on).

What's missing

I made the rule switchable at run time (read once a submission, flipped through DM_CTL's env) and saved six frames per setting with shot, the camera still:

Setting Bushes drawn
As in this PR 1-3 of 6
Alias flush on every barrier (as before) 6 / 6
g_alias_pend set at the end of every draw pass 6 / 6
Every barrier flushes once a chunked pool exists (g_big_n) 6 / 6
The relaxed pass start's barrier always there, without the flush missing
Relaxed pass starts flushing without clearing the pending flags missing

So the missing flush follows a draw pass that writes no memory through shaders (g_pass_swrite is 0) but whose stores another host texture reads. A pass's attachments are a write too. I counted them over 100,000 passes: 23 % store into a texture in an image heap (g_image_heap_owner_key), none into a texture over a buffer, and nearly all the rest into textures placed in other heaps, which the host shares between several texture objects over the same bytes. Setting g_alias_pend only for image-heap attachments did not fix it, and neither did adding attachments over a buffer. Covering placed textures as well is close to "every pass", which is the 6 / 6 row above.

Two smaller gaps I found on the way, which didn't fix it either:

  • The upload batch's barriers (up_flush, the batch's first encoder, the clears on g_up_cb) and the present / snapshot command buffers call resource_vis() too, so they clear the main command buffer's pending flags. An upload batch is committed ahead of the main command buffer's open segment, so its flush can run on the GPU before the write it was taken for.
  • MetalFX's own encoders (up_mfx_encode, the DLSS path) write their output without setting g_alias_pend.

What it saves when correct

Same spot, 300-frame windows, the same run, switching at run time:

Setting Frame p50 GPU p50
As in this PR (foliage missing) 12.29 ms 12.02 ms
Flag after every draw pass 12.94 ms 12.23 ms
Flush on every barrier (before) 13.05 ms 12.35 ms

On this machine, once the flushes the foliage needs are back, about 0.1 ms is left. The first row has two clean windows, the others five each. An M1 may gain more, but most of the measured gain seems to come from skipping flushes that are needed.

I've taken it out of my build for now.

…(no MTLGPUFamilyApple9); an M3 Max lost Ocarina of Time's foliage with it

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

AdyaGMD commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for the careful bisection. You're right: the rule skips flushes Metal asks for, since a pass's attachment stores are writes and other texture objects sit over the same bytes.

I checked on an M1 (8-core GPU) at the same Kokiri Forest spot, one run, three settings alternating every 300 frames, six screenshots each:

Setting Frame p50 Foliage
This PR's rule 33.33–33.47 ms steady in all frames
+ flush flagged after every draw pass (your fix) 36.42–36.75 ms steady
Flush on every barrier 36.72–36.73 ms steady

So with the needed flushes back, nothing is left on the M1 either. The difference is that the M1 draws the foliage without them and the M3 Max doesn't.

I've pushed 7f9492a: the rule is now on by default only on Macs without MTLGPUFamilyApple9 (M1, M2), and every other Mac flushes on every barrier as before. DM_NVN_VIS_HEAPFRAME still overrides it. I haven't checked an M2.

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