Skip to content

Game-mode draw passes hold the next pass's task and mesh shaders until their fragments are done - #1707

Closed
cocoman93 wants to merge 1 commit into
seedds:mainfrom
cocoman93:pr/mesh-stage-barrier
Closed

cocoman93 wants to merge 1 commit into
seedds:mainfrom
cocoman93:pr/mesh-stage-barrier

Conversation

@cocoman93

@cocoman93 cocoman93 commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

What and why

With draw_barriers game (or DM_NVN_BARRIER=game) a draw pass ends with a barrier that lets the next pass start its vertex work while this pass's fragments still run (enc_r_end_pass in nvn_mtl.m). Its later stages (beforeQueueStages) were fragment, dispatch and blit. Metal tracks the object (task) and mesh stages apart from the vertex stage, so the next pass's task and mesh shaders, including the mesh pipelines that replay some geometry-shader draws, did not wait either and could read what the previous pass had not written yet. Its earlier stages (barrierAfterStages) did not list object.

The end barrier adds MTLStageObject to its earlier and MTLStageObject | MTLStageMesh to its later stages. Vertex work still overlaps, as the mode intends. New reference entry "Mesh shaders that start before the previous draw pass is done" in docs/reference/rendering.md; the game note gets a line under "Found on this game", and the barrier mode's rows in docs/agents/04-graphics.md, docs/env_switches.tsv and dm/games/README.md say which stages wait.

Checked

  • New test check_t_pass_end_stages: two production passes in game mode; each of the 4,096 fragments of a slow pass adds to a counter, and the next pass reads it from a vertex shader (the control: it reads early in nearly every run, so the window exists), a mesh shader and a task shader, which must see every add. 32 runs, with and without the encode thread, under the Metal debug layer. Without the change 18 to 29 of 32 mesh and task reads came early; with it none.
  • Tests: make -C dm/gfx/tests GAME=0400486024DEC000 run on the Mac, every check passed; python3 dm/tools/tests/test_docs.py and python3 dm/tools/docs_symptoms.py --check pass. python3 dm/tools/lint_c.py tidy on the two changed C files: no new finding (nvn_mtl.m's two checks above the baseline are the same on main).
  • Games run, with title ID and version: The Legend of Zelda: Ocarina of Time 0400486024DEC000, v131072 (1.1.1), the one profile that sets draw_barriers game, Mac apps on the same route from the same save, 58 s recordings of every frame, a flash counted as a one-frame drop of at least 3.5 in average brightness after recording frame 500:
    • On a build of main 7fe9fc4d with two local shader changes: the whole picture turned darker for one frame at a fixed place in Kokiri Forest in 4 of 9 walks. It did not show with full barriers (0 of 4 walks), an All → All end barrier (0 of 4) or object and mesh added to the later stages (0 of 6); with only the earlier stages widened it did (2 of 8).

      The flash on that build, slowed down (the fog over the path drops out for one frame), and the average brightness of every frame around it: a one-frame drop, then a smooth recovery; the rest of the curve is the camera moving.

      flash flash-graph
    • On main 24fe1e5a, one build, with and without this change (the host swapped, nothing else different): no flash in 15 walks either way, while the build above still showed it in 2 of 5 walks the same day (both looked at by eye: the haze over the whole picture weaker for one frame). What hides it on main is not separated, so on main the change rests on the test above.

    • GPU time per frame, median of 300-frame windows over two walks each: 14.88 ms without, 14.82 ms with this change, 14.98 ms with DM_NVN_BARRIER=full; the scene passes stayed at the game's full 1536x864 in every profiled frame of all three. Full barriers cost nothing measurable here either, so the game mode's gain recorded in frame-rate.md cannot be compared on this Mac.

    • The game runs used main 24fe1e5a; the branch is on bd979ced, where the tests above ran.

  • Regression: regress.py impact maps the change to every NVN game through nvn_mtl.m, but the changed barrier only runs under draw_barriers game or DM_NVN_BARRIER=game / ab, and only Ocarina of Time's profile sets it; Ocarina of Time has no smoke route, so its walks above stand in for regress.py check.
  • Devices and OS: M4 Max, macOS 27.2.
  • Not checked: iOS; other games run with DM_NVN_BARRIER=game.

Docs

  • The finding is in its game note or its docs/reference/<subsystem>.md entry, and python3 dm/tools/docs_symptoms.py was run (the symptom index)
  • No game files, keys or data extracted from a game in the change or the attachments

🤖 Generated with Claude Code

…l their fragments are done, as they already did fragments, compute and copies

With draw_barriers game a draw pass ends with a barrier that lets the next pass start its vertex work while this pass's fragments still run. Its later stages (beforeQueueStages) were fragment, dispatch and blit. Metal tracks the object (task) and mesh stages apart from the vertex stage, so the next pass's task and mesh shaders, including the mesh pipelines that replay some geometry-shader draws, did not wait either and could read what the previous pass had not written yet. Its earlier stages (barrierAfterStages) did not list object. The end barrier now adds object to its earlier and object and mesh to its later stages; vertex work still overlaps, as the mode intends.

t_pass_end_stages drives two production passes in game mode: each fragment of a slow pass adds to a counter, and the next pass reads it from a vertex shader (the control: it reads early in nearly every run), a mesh shader and a task shader, which must see every fragment's add. Without the change 18 to 29 of 32 mesh and task reads came early, in both encoder modes; with it none, on an M4 Max.

In Ocarina of Time, with this profile's draw_barriers game, the whole picture turned darker for one frame at a fixed place in Kokiri Forest in 4 of 9 walks on a build of main 7fe9fc4 with two local shader changes; it did not show there with full barriers (0 of 4 walks), an All -> All end barrier (0 of 4) or object and mesh added to the later stages (0 of 6). On main 24fe1e5 it did not show in 15 walks with or without the change, while the 7fe9fc4 build showed it in 2 of 5 the same day. GPU time unchanged (median 14.88 ms without, 14.82 ms with, 14.98 ms with full barriers; the scene passes stayed at the game's full 1536x864 throughout).

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

Copy link
Copy Markdown

A run of this PR against the Ocarina of Time hang (#1326, #1605), on an M1 Pro (16 GB, macOS 26.6.1): main e940e1f plus this PR (the profile's draw_barriers game), #1497, #1331, #1453, #1377 and #1379.

The hang still came: two sessions, both ended in GPU access revoked and exit 70 at about 7 min, while leaving Hyrule Castle. The culling tasks 47f9b38490dcf9a2 and 99280b7b8aae78df were in every failed command buffer, and macOS's gpuEvent reports show MMU interrupt with fw_ta_state busy, as before. Details are on #1605.

That agrees with @ralph-ferza's run with DM_NVN_BARRIER=full on #1326. It says nothing against this change for the race it describes, only that it isn't that hang.

@seedds

seedds commented Nov 8, 2026

Copy link
Copy Markdown
Owner

Thanks for the careful runs and the test. We're holding this for now. Adding object to the barrier's earlier side is correct, but making the next pass's task and mesh stages wait while vertex doesn't goes further than NVN, where all of them overlap the previous pass's fragments without a barrier. The flash also doesn't reproduce on main, and which shader reads early is still unknown.

What would move it forward, on a build where the flash still reproduces:

  • the task or mesh program that reads early (the passdump Y bind_program lines);
  • whether the game calls nvnCommandBufferBarrier between the pass that writes and the pass that reads (the I <pass> barrier bits lines).

If the game issues a barrier that the host loses, that's a host fix for every game. If it doesn't, the fix belongs in game mode's rules for all the early stages, not in the mesh and task bits alone.

@cocoman93

Copy link
Copy Markdown
Contributor Author

Thanks, that was the right question. Here is what a build where the flash still reproduces shows.

Build: main 7fe9fc4d with earlier versions of the two shader changes in #1742 and #1824 (chain 97f47bd82b33). It flashed in 4 of 9 walks on 2026-11-07 and in about 1 of 15 today; main 24fe1e5a showed 0 of 15.

Barriers: the game's are not lost. Game mode gives the next draw pass the full consumer barrier whenever the game called nvnCommandBufferBarrier since the last draw pass, or anything other than a draw pass ran in between (enc_r_begin_pass). In a pass dump of frame 1897 (110 passes), 11 passes run task or mesh shaders. 9 of them start after a game barrier (I ... barrier bits 0x12, 0x11 or 0x28 in the pass before) or after a compute pass, so with the full barrier. Two start without one:

  • pass 35121, the mist: mesh 066f5daf12e43c71, which reads constant buffers only;
  • pass 35126: task 47f9b38490dcf9a2, mesh eed1add7c89f5d34, right after draw pass 35125, with no I line between them:
    [pass] F1897 Y bind_program cmd=0x7c1b32c210 program=0x7e2106e7c0 stages=0xdf vs=- fs=bd09ab5fc4fd7680_b gs=-
    [pass] F1897 K 35125 st3 key=a5288d0a6c4971fd_b nb=4
    [pass] F1897 K 35125 st4 key=3439aaa071d868ae_b nb=8
    [pass] F1897 Y bind_program cmd=0x7c1d32e8f8 program=0x7e22f2df28 stages=0xdf vs=- fs=bc214ce5c4f88db8_b gs=-
    [pass] F1897 K 35126 st3 key=47f9b38490dcf9a2_b nb=6
    [pass] F1897 K 35126 st4 key=eed1add7c89f5d34_b nb=8
    

The reader: not found. The dump shows no dependency between 35125 and 35126: 35125 writes only its attachments, 35126's task and mesh bind no textures, and no binding in the frame writes the storage buffers they read. Their raw global reads have no addresses in the dump. I measured instead, with a diagnostic host that is not part of this PR: a queue barrier from earlier work's vertex, object, mesh and fragment stages before the matching task / mesh draws, 58 s walks, the same flash rule:

  • before task 47f9b38490dcf9a2 draws only: 4 flashes in 23 walks, against 1 in 23 without, so it is not that task (4 against 1 is no sign that it makes things worse either);
  • before every task and mesh draw: 0 in 4; with this PR's end barrier it was 0 in 6 earlier, and with the vertex stage added instead of object | mesh, 0 in 5.

I could not narrow it further: at about 1 in 15 walks a split needs far more walks than is reasonable, and a GPU load beside the game, which I tried to raise the rate, produced a different one-frame effect that stays even with every task and mesh draw waiting.

So by your rule: the game issues no barrier between the overlapping passes, so this is not a lost barrier, and a fix would belong in game mode's rules for all the early stages. On this M4 Max even full barriers cost nothing measurable in this walk (GPU p50 14.88 ms base, 14.98 ms full), but that walk does not exercise what game mode was made for.

I can rework this into one rule for all the early stages and measure its cost on the games I have that draw with vertex and mesh shaders (Donkey Kong Bananza, Mario Kart World, Kirby Air Riders), or close it if you would rather keep game mode as it is until a game shows the race on main. Your call.

@cocoman93

Copy link
Copy Markdown
Contributor Author

Found the cause, and it is not a barrier problem. The flash is most likely the cross-queue wait race from #1703 and #1245, which main fixed in ea0c98d, whose message says these fixes "address Ocarina's scene flashes". All the mains I measured on before (7fe9fc4d, 24fe1e5a, bd979ced) predate it. So the race was still there on the ones that showed no flash, and only showed rarely.

Test: main c78d7375 (which contains ea0c98d) with the shader changes of #1742 and #1824, plus a diagnostic switch, not part of any PR, that makes the queue parser skip OP_WAIT_EVENT, so event waits are ignored as before ea0c98d. Same walk, 58 s recordings, 12 busy loops beside the game (the host stall one #1245 tester names), the two arms alternating, the flash rule from my last comment (a one-frame drop of the picture's mean grey level, 0-255, of 3.5 or more):

What this does and does not show: it proves the race on current main once the waits are dropped. That the rare flash on the older builds was the same race rests on the identical look and on ea0c98d's own description. The same load on the old 7fe9fc4d build gave 0 flashes in 6 walks, so dropping the waits on current main is not exactly the old host; other changes since move the timing. The earlier barrier runs without a flash most likely only shifted timing too, and at 1 in 8 to 1 in 15 walks they were not significant anyway.

Either way, on main the waits ea0c98d honours remove the flash and this PR is not needed, so I'm closing it. If you want object added to the barrier's earlier side on its own, as you said is correct, tell me and I'll open it separately.

Side finding on Kokiri's shaded path: the flat near-black leaves that lay on the stones in builds before ea0c98d also appeared in three of the seven flash frames checked by eye, and only in the flash frame. So ea0c98d most likely fixed those as well.

AmateurWizrd added a commit to AmateurWizrd/nxnative that referenced this pull request Nov 9, 2026
# Conflicts:
#	dm/games/0400486024DEC000/README.md
#	docs/env_switches.tsv
Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
…d mesh shaders until their fragments are done

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#1707 by cocoman93 (closed): Game-mode draw passes hold the next pass's task and mesh shaders until their fragments are done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0400486024DEC000 A game 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.

3 participants