Skip to content

An indirect dispatch no longer joins a copy encoder that holds work: Ocarina of Time's flower bushes no longer show holes in Hyrule Castle's courtyard - #2226

Open
humanperzeus wants to merge 1 commit into
seedds:mainfrom
humanperzeus:q194-indsplit-pr
Open

humanperzeus wants to merge 1 commit into
seedds:mainfrom
humanperzeus:q194-indsplit-pr

Conversation

@humanperzeus

Copy link
Copy Markdown
Contributor

What and why

Ocarina of Time (0400486024DEC000, v131072) on an M1 Max: the flower bushes in Hyrule Castle's courtyard showed their dark inner body through holes in about 40 % of frames, standing still, at random.

The foliage is GPU-driven: a chain of compute dispatches culls the clusters (83df46a0b1b379b4, 60dd0e6eac91f712, cee08c634ef1f343, 58f47abfa0a84773, e855420a39e60874) and builds the list the leaf and flower mesh shaders draw (d98076e4148db34f, eebdb9347d09b344). A later dispatch of that chain is indirect, its group counts written by an earlier one. Dispatches share the copy encoder, each after a barrier inside it (dispatch_enc), and that barrier did not order the indirect dispatch's counts after the dispatch that wrote them: in the frames that went wrong the cull kept 260, 268 or 250 of its 276 clusters, and the leaf list held 1,584 / 1,680 / 1,488 leaves instead of 1,749.

  • dispatch (ind_split): an indirect dispatch does not join a copy encoder that already holds work. The encoder ends, and the dispatch goes into the next one, which begins with a queue barrier (blit_open) that waits for everything before it. Direct dispatches keep sharing the encoder. DM_NVN_IND_SPLIT=0 brings the old order back, to measure.
  • Which of Metal's reads misses the barrier inside the encoder, the indirect-argument fetch or the copy of the counts into the driver segment, is not known.

How it was found, in short: the leaf list's count read once each submission completed gave one number a frame; it dropped in 39 % of frames, the same share as the holes in screenshots, and each frame independently of the one before. The cull's own list dropped with it. Every uniform and small storage input of the five cull shaders, dumped each frame for 662 frames, followed none of the drops (|r| ≤ 0.12), so the inputs were the same and the result was not: an ordering problem, not the shaders. Lab switches flipped live in one session (not part of this change) then gave: an encoder of its own for every dispatch 0 %, for the indirect ones only 0 %, for the direct ones only 41 %.

Checked

  • Games run: The Legend of Zelda: Ocarina of Time 0400486024DEC000 v131072, standing still in Hyrule Castle's courtyard, on our build (base 641636e2 plus our changes, the same dispatch code as main), M1 Max, macOS 27.2:
    • the leaf list's count, 240-300 frames a setting, one session: old order 39 %, 41 %, 47 % of frames short; this change 0 of 600 (two runs of 300);
    • 24 shots each: 0 with holes, 9 with DM_NVN_IND_SPLIT=0.
  • Cost: GPU time per frame in the courtyard (DM_NVN_GPUPROF=every:30, frame-rate target 60, M1 Max): 21.08 ms mean / 22.36 ms median over 43 frames with this change, 20.50 / 21.95 ms over 44 frames with DM_NVN_IND_SPLIT=0; 141 against 131 encoders a frame. One run each, so about +0.4 to +0.6 ms (2 to 3 %); how much of that is run-to-run spread is not measured.
  • Tests: make -C dm/gfx/tests GAME=0400486024DEC000 run on this branch: exit 0, 75,079 checks in 158 result lines, 0 failed, the Python groups OK. Left out: t_gl_imports, which does not link on main itself (f3ec8fc8: _g_key18, referenced from hle.c, defined in loader.c, which the test does not link); built on a clean checkout of main it fails the same way. No test reproduces the race: a synthetic pair (a dispatch writes the counts, an indirect one reads them, in a small pool and through two chunk buffers of a large one) passed with the old order too (0 of 100 runs each), so it is not part of this PR; what in the game's chain lets the old order fail is not pinned down.
  • python3 dm/tools/tests/test_docs.py: OK. Host builds (make -C dm/host GAME=0400486024DEC000 dmhost).
  • scripts/regress.py: impact reaches every NVN game (with manifests: Breath of the Wild, Pokémon Scarlet, Tears of the Kingdom; Hades not affected). check --games was not run: only Ocarina of Time is set up here, and its library is of an older chain than main's.
  • Not checked: a game other than Ocarina of Time; the iPhone; a full Ocarina build on main's chain (the change is host-only; the run above is on our build with the same dispatch code).

Docs

  • docs/reference/rendering.md: "An indirect dispatch read its group counts before the dispatch that wrote them" (symptom, cause, fix, check, with the measurements and what is not known).
  • The game note's "Found on this game"; docs/agents/symptoms.md regenerated.

🤖 Generated with Claude Code

…Ocarina of Time's flower bushes no longer show holes in Hyrule Castle's courtyard

The foliage cull is a chain of compute dispatches in which a later one is indirect, its
group counts written by an earlier one. Dispatches share the copy encoder, each after a
barrier inside it, and that barrier did not order the indirect dispatch's counts after
the earlier dispatch: in about 40 % of frames (M1 Max) the cull left 16 of its 276 leaf
clusters out, and the leaf and flower meshes drew a short list, the bush's dark inner
body showing through.

- dispatch (ind_split): an indirect dispatch does not join a copy encoder that already
  holds work; the encoder ends, the next begins with a queue barrier. Direct dispatches
  keep sharing it. DM_NVN_IND_SPLIT=0 brings the old order back (to measure).
- docs/reference/rendering.md: the entry, with the measurements and what is not known;
  the game note; the symptom index regenerated.

Measured on Ocarina of Time standing still in the courtyard, the leaf list's count read
when each submission completed, 240-300 frames a setting, flipped live in one session
by lab switches not in this change: old order 39 %, 41 %, 47 % of frames short; this
change 0 of 600; an encoder for every dispatch 0; for the direct ones only 41 %. Every
uniform and small storage input of the five cull shaders, dumped for 662 frames,
followed none of the drops. 24 shots: 0 with holes, 9 with DM_NVN_IND_SPLIT=0. Which
read misses the encoder barrier (the indirect-argument fetch or the copy of the counts
into the driver segment) is not known; a synthetic writer / indirect reader pair did not
fail with the old order (0 of 100, also through two chunk buffers of a large pool).

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 game: 0400486024DEC000 A game labels Nov 10, 2026
Fasen24-AI pushed a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
…Ocarina of Time's flower bushes no longer show holes in Hyrule Castle's court (PR seedds#2226)

The code of pull request seedds#2226 by humanperzeus, placed by hand: its notes (the game's note, the symptom index, the rendering reference) conflict with this branch's and are left for
the next update on main. From the pull request: the game's foliage is GPU-driven, a chain of compute dispatches culls the clusters and builds the list the leaf and flower mesh shaders
draw, and a later dispatch of the chain is indirect, its group counts written by an earlier one. Inside one Metal 4 compute encoder the barrier between two dispatches did not order
the indirect dispatch's counts after the dispatch that wrote them: on an M1 Max the cull came out short in 39 % of frames, 16 of its 276 clusters missing at random; with the encoder
ended before an indirect dispatch, 0 of 600 frames. DM_NVN_IND_SPLIT=0 is the old behaviour.

In this branch: a row for the switch in docs/env_switches.tsv. On our M5 the holes were not looked for before; the walk through Castle Town with this commit is in the notes of the
commit that follows it, if one does.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7
Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
…t is measured and what is not

Five changes since the last update of the page: three fixes of pictures (PR seedds#2219's change for Switch 2 uniform reads, PR seedds#2226's indirect dispatches, main's 6d9edb0 read mark),
the hold over its bound that a long session found, and the Memory setting with its review. The first three ran in a short walk; the memory changes build and have not run in the
game. The page says so, and gives the switch that avoids the long-session storm on the commit before this push.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7
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.

1 participant