Repository navigation
Raw global memory through a pointer read when the draw is bound: the host fills each fixed-offset pointer's pool range before the draw (chain 1ffcb6b77740) - #1585
Conversation
…host fills each fixed-offset pointer's pool range before the draw, and the shader searches the pool table only outside it (chain 1ffcb6b77740)
Symptom: Ocarina of Time's culling task shaders (f75708d176bb6591, 47f9b38490dcf9a2, 99280b7b8aae78df and five more of Model.Product.110) hang the GPU when the pool their range pointer names is not in the raw pool table, and any raw load into such a pool reads 0. The table lists only pools the game was handed a GPU address for (PR 1394's filter, in main), which leaves out 172 of Ocarina's pools (3.7 GB).
Cause: these tasks read both ends of a range through a pointer loaded from a storage buffer and loop (min(end - start, 256) - 1) >> 5 times; a miss reads both ends as 0, so 134,217,727 iterations. 99.5% of the raw-table accesses of Ocarina's 2,247 raw shaders take their pointer from a fixed byte offset of a bound storage buffer (every model shader: five pointers at 0x0-0x20 of one slot), so the CPU can read it before the draw.
Fix: GlobalToStorage.FindPointerSite numbers such loads per shader (at most DMSH_GPTR_MAX = 64, noted "gptr site k: c<slot>[<word>] +<byte>"), and the raw load helper tests the site's entry {start, end, delta} (DMSH_KIND_GPTR, sentinel slot offset 2) before main's last-entry check, hint and search. dmsh_build.py gptr_sites gives the host [NVN storage slot, byte]; encode_args (NVN_RB_GPTR) reads each pointer word and fills the entry from mtl_gpu_addr, which also makes the pool resident. The shader checks the address against the entry, so a stale or empty entry costs only the search it would have done. Stores and atomics are unchanged (the write log is per table entry). PR 1444's loop guard ends the same loop but is a workaround and is not taken.
Checked: translation of the 4,680 raw-table jobs gives sites to 2,173 shaders. The generated helper of 0031f9442b11b682, run on the GPU against an 8,191-entry table: 11 of 11 cases (site path, table path, empty and no site, range ends, no pool, the last-entry cache). The eight task shaders run on their own on an M3: pool in the table completes; left out with no site hangs every time (about 22 ms of GPU time); left out with the site filled completes. In the game on an M2 Pro 16 GB (Kokiri Forest route): 1,091,044 sites by frame 2,700, all naming a pool, 16,250 of 258,329 raw-load bindings without a site. test_dmsh_reflect, t_refl (52 checks), test_nsp_chain, test_xlate_anycpu, test_docs, the host build and host tests pass. Only Ocarina of Time was run with this chain; regress.py check not run.
…ad when the draw is bound: Ocarina's culling task shaders no longer loop 134 million times and hang the GPU when their pool is not in the table) spirv-cross stays at the Windows fingerprint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013SzykpcKm2eZPvPjGPKx1C
|
Tested this branch in game on an M2 Pro. The pointer sites work as described, but the GPU hangs still come: 5 of 6 runs hung. Setup: MacBook Pro M2 Pro, 32 GB, macOS 27.2. Ocarina of Time (0400486024DEC000, v131072, from the NUP), Mac app. The branch is cherry-picked last onto main 863bc6b, together with #1331, #1368, #1176 and a few host changes of ours. The only conflict was The sites: each of the three culling task shaders gets one site, The hangs: six headless runs from the Link's-house autosave, down the ladder towards the cave pond by Mido's gate. The forest load happens at about frame 3000. Runs 4 to 6 used a host without one of our pool-table changes, to rule it out; it made no difference.
No page fault in any run. Four of the five hanging runs failed first within about 700 frames of the load. Run 1's route drifted, and Link spent the time elsewhere. What the failed buffers had in common:
The two effect pixel shaders have the light loop described in #1326 ( So either a case of the range loop is still left uncovered, or the remaining hang is another loop. I did not run without this branch: that needs another chain and a full rebuild. The six logs are available as a |
…a, gmem commit repoint on the test stack seedds#1589 (carrying seedds#1391 + seedds#1482) the GPU memory guard reads the residency set's size and stops before macOS runs out of RAM; hand-merged with seedds#1590's ghost-active alias tables. seedds#1599 soft textures (gated, default off). seedds#1662's tip commit: the ocarina plays into the Mac's microphone (DM_MIC=1). seedds#1427 raw pool table words follow the pools at commit, as a candidate for seedds#1326's page faults (its window is skipped, not faulted: kept for the record). Full-map shader library (42,848), clearing the no-stamp and c1_full red lines. In-game: the GPU page fault at scene changes (seedds#1326) remains on this stack - one session lost the picture at frame 55,940 with the device revoked; the fault predates seedds#1585, which was excluded after the M2 Pro report but is not its cause. Front-ends: host change only, chain 2027aa29b1b7.
|
A data point for the remaining task/mesh hangs, on a MacBook Pro 14" M2 Pro (32 GB), macOS 27.2. Build: main
You wrote that the remaining task/mesh hangs on main were cured by an encoder boundary after the task/mesh run. Is that change available somewhere? |
|
@reefbluesky The sentence you quote is in this PR's description, written by its author (@nafields). It is not ours, so I can't point you to that change. I found no merged or open PR with an encoder boundary after the task/mesh run; the description calls it "a separate change, still being narrowed". The nearest open change I know of is #1707: game-mode draw passes hold the next pass's task and mesh shaders until their fragments are done. It is a different change, and I have not tested it against a hang like yours. |
|
A data point from an M1 Pro (16 GB, macOS 26.6.1): with this PR the hang came much earlier. Build: main bd979ce plus this PR, rebased onto it (one conflict in the Result: three sessions, all ended by a GPU hang within the first minute, then For comparison, the same Mac on main 24fe1e5 with the same PRs but without this one: five sessions. Three ended in a fatal hang, 16 to 44 min in, and two recovered from every hang. One session also had a hang at 36 s that the game recovered from. So the base moved too (24fe1e5 → bd979ce), and this is not a clean A/B. What the logs show
I'm going back to main without this PR, with #1707, and can test a newer revision of this PR on this Mac if that helps. |
|
@Amost- Thanks for the six runs! That's more than I had on this branch. The sites are doing what they should: They all named a pool. The bindings with an empty site or none fallback to the table. They should hang only if their pool is missing from it. Im fairly confident that a table miss looks different from what you saw: the standalone test (task_hang.m in #1583) hangs on every run of the shader, ~22ms of GPU time. AND it hangs at the first frame that runs it. It shouldn't hang a 700 one time, and 3,200 the next. The failed buffers also hold the three culling tasks every time but a different "most specific" pair in every run, which is what a buffer-level hang looks like, not a shader-level one. So I read your runs as the other hang, the one the description calls separate, and I should have been explicit there. What I know about that, from my own runs on the same route (M2 Pro 16 GB), so far: With a command-buffer cut around that run (DM_NVN_ISOLATE_TASK, a scratch switch in my tree, not in this PR), 4 of 4 warmed runs went past f6,600 with no hang; without it 6 of 6 hung, two or three times each. A cut-only variant, which split the run from the work before it in only about 30 % of cases, also had no hang in 2 runs. Yes to the bundle, please; attach it here. Two things I will look for in it: whether any failed buffer's log line names a raw load whose address fell outside its site entry (a word the GPU rewrote after bind would do that; the culling tasks' site is c0[412] +0 and the comment in nvn_mtl.m names FoliageShader as the case), and which shaders make the 17,408 short bindings. On the chain: 3f146103c47b against 1ffcb6b77740 is expected once the tool files differ after a cherry-pick; dmsh_chain.py --header gives the value for the tree it runs in. @reefbluesky No, and the sentence in the description promised more than exists. There is no PR with an encoder boundary after the task/mesh run. What I have is the isolation switch described above, which forces a command-buffer cut around one task shader's draws; it prevents the hang in every run so far, but it is a diagnostic that says where the hang lives, not a fix, because the cause is not yet named. I will reword the description, and I will put the switch up as a diagnostic in a PR of its own so the hang can be reproduced and bisected on other machines if people will find that helpful. Your two runs match mine and Amost-'s in every respect that matters (no page fault, the three culling tasks in every failed buffer, the forest). The frame-time drop with DM_NVN_SKIP_MESH=1 (25.5 → 19.9 ms p50) is a useful number: it bounds what the task/mesh path costs here. One note on reading them against this PR: your build list (main bd979ce, #1368, two local cuts) does not include #1585, so those hangs are main's, and they say nothing about the change here either way; they do say the hang is not something this branch introduced. @lunks Thank you for the pointer to #1707! It is a different mechanism (holding the next pass's task and mesh shaders until the fragments are done), but it acts at the same place. It is worth a run against this hang. |
|
A correction to my run above. I wrote that the The hang on that build still came at about 7 min, with |
|
@nafields here are the six runs, redacted the way
They are the host's own logs of headless runs, so there is no report form with them. Runs 4–6 are the host without one of our pool-table changes. Since then the same tree, plus two movie-timing PRs, hung in 21 of 21 runs on that route, with full barriers, a lighter GPU load or a fresh save as well (#1326), so I can run your isolation switch as soon as it is up. |
|
Independent base M1 result for the isolation nafields describes (MacBook Pro M1, 16 GB, macOS 26.6.2; OoT v131072; local branch of open PRs on an older main, not this PR). Measured: a host switch that ends the command buffer before every draw of |
On Apple silicon (M1, 16 GB) the draws of OoT's Hi-Z task a5288d0a6c4971fd, with its mesh 3439aaa071d868ae (cliffs and giant trunks), hang the GPU now and then after an area change (fw_ta_state 1: the tiler), and after 2 to 5 such hangs macOS revokes the process's GPU access. The mesh's outputs, reads, compiler options, residency, barriers, pool table and pool chunking were each ruled out; with the body running and no primitive emitted it no longer hangs, and degenerating its odd triangles does not help. Putting each a5288 draw in a command buffer of its own (the pass ends and the segment is committed right before and right after it) removed the hang: 4 of 4 walks of 420 s without one, against about 15 of 16 with one (registro-testes EXP-meshM1-51..54), for +0 to 2 ms of GPU a frame before the area change and no measurable change after it. A third party saw the same on an M2 Pro (seedds#1585, an unpublished DM_NVN_ISOLATE_TASK switch). The cause inside the tiler is still unknown, so the hang guard (13dead4d) stays as the net behind it. On by default for the task key list "a5288d0a6c4971fd" (a key only OoT has); DM_NVN_ISOLATE_TASK=0 turns it off, =<keys> names other tasks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Apple silicon (M1, 16 GB) the draws of OoT's Hi-Z task a5288d0a6c4971fd, with its mesh 3439aaa071d868ae (cliffs and giant trunks), hang the GPU now and then after an area change (fw_ta_state 1: the tiler), and after 2 to 5 such hangs macOS revokes the process's GPU access. The mesh's outputs, reads, compiler options, residency, barriers, pool table and pool chunking were each ruled out; with the body running and no primitive emitted it no longer hangs, and degenerating its odd triangles does not help. Putting each a5288 draw in a command buffer of its own (the pass ends and the segment is committed right before and right after it) removed the hang: 4 of 4 walks of 420 s without one, against about 15 of 16 with one (registro-testes EXP-meshM1-51..54), for +0 to 2 ms of GPU a frame before the area change and no measurable change after it. A third party saw the same on an M2 Pro (seedds#1585, an unpublished DM_NVN_ISOLATE_TASK switch). The cause inside the tiler is still unknown, so the hang guard (13dead4d) stays as the net behind it. On by default for the task key list "a5288d0a6c4971fd" (a key only OoT has); DM_NVN_ISOLATE_TASK=0 turns it off, =<keys> names other tasks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…draw is bound: the host fills each fixed-offset pointer's pool range before the draw (chain 1ffcb6b77740) 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#1585 by nafields (open): Raw global memory through a pointer read when the draw is bound: the host fills each fixed-offset pointer's pool range before the draw (chain 1ffcb6b77740)
…e pointer resolve their pool once; seedds#1585's site path joins EmitAccess) 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#1585 by nafields (open): Raw global memory through a pointer read when the draw is bound: the host fills each fixed-offset pointer's pool range before the draw (chain 1ffcb6b77740) - seedds#1751 by deanpektas27 (open): DM_NVN_GPUPROF_GEOM: split each render pass's GPU time into geometry and pixels - seedds#1752 by deanpektas27 (open): Raw global memory loads through one pointer resolve its pool once, and the pool table search is a loop: Ocarina of Time's colour-pass mesh shaders stop looking up the pool table per word (chain 723dc3a3e17d)
…s), the resolved helper takes (lo, hi, site, entry, delta): without this no run was resolved; the two raw tests take the site 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#1585 by nafields (open): Raw global memory through a pointer read when the draw is bound: the host fills each fixed-offset pointer's pool range before the draw (chain 1ffcb6b77740) - seedds#1752 by deanpektas27 (open): Raw global memory loads through one pointer resolve its pool once, and the pool table search is a loop: Ocarina of Time's colour-pass mesh shaders stop looking up the pool table per word (chain 723dc3a3e17d)
…), on top of the seedds#1585 site fix 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#1585 by nafields (open): Raw global memory through a pointer read when the draw is bound: the host fills each fixed-offset pointer's pool range before the draw (chain 1ffcb6b77740) - seedds#1752 by deanpektas27 (open): Raw global memory loads through one pointer resolve its pool once, and the pool table search is a loop: Ocarina of Time's colour-pass mesh shaders stop looking up the pool table per word (chain 723dc3a3e17d)
…t raw loads as one vector load, occlusion conditions read once landed, dynamic-resolution stub; hand-merged with the seedds#1585 sites and the seedds#1752 resolved runs) 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#1585 by nafields (open): Raw global memory through a pointer read when the draw is bound: the host fills each fixed-offset pointer's pool range before the draw (chain 1ffcb6b77740) - seedds#1752 by deanpektas27 (open): Raw global memory loads through one pointer resolve its pool once, and the pool table search is a loop: Ocarina of Time's colour-pass mesh shaders stop looking up the pool table per word (chain 723dc3a3e17d)
…he vector helper (-1: no site) 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#1585 by nafields (open): Raw global memory through a pointer read when the draw is bound: the host fills each fixed-offset pointer's pool range before the draw (chain 1ffcb6b77740)
…ion by size tier and archive indexes, seedds#1844 merged, 3D texture mirrors, native mesh pipelines in the archive, follower-scan groups) Hand-resolved: main's compaction and mesh-pipeline archive replace the tail-compaction marker and the mesh-parts draw we carried; the copy mirrors keep our `mir` flag for pkg_copy_absorb; patch_nso.py keeps fixed_resolution beside main's fs_address; the translator keeps the seedds#1585 site argument. Translator rebuilt: chain 1268cc9dae8a. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7 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#1585 by nafields (open): Raw global memory through a pointer read when the draw is bound: the host fills each fixed-offset pointer's pool range before the draw (chain 1ffcb6b77740) - seedds#1844 by nawangwe (closed): Shader libraries compile with -fwrapv: Ocarina of Time's mesh-shader models no longer take their ambient light from the wrong probe texels - seedds#1879 by bettterlabs (closed): Timer flushes wait during any prewarm, so the start after a cold start does not compact (Ocarina of Time 762 s → 3.3 s) - seedds#1880 by bettterlabs (closed): Pipeline archive compaction by size tier, so the start after play recaptures what play added, not the whole archive (Ocarina of Time 713 s → 24 s)
What and why
Symptom. Ocarina of Time's culling task shaders (f75708d176bb6591, 47f9b38490dcf9a2, 99280b7b8aae78df and five more) hang the GPU when the pool their range pointer names is missing from the raw pool table. Any raw load into such a pool reads 0. The table lists only pools the game was handed a GPU address for (the #1394 filter, in main), and that leaves out 172 of Ocarina's pools (3.7 GB).
Cause. These tasks read both ends of a range through a pointer loaded from a storage buffer, then loop
(min(end - start, 256) - 1) >> 5times. A miss reads both ends as 0, which makes 134,217,727 iterations. 99.5% of the raw-table accesses of Ocarina's 2,247 raw shaders take their pointer from a fixed byte offset of a bound storage buffer. Every model shader, for example, has five pointers at 0x0–0x20 of one slot. So the CPU can read the pointer before the draw.Change.
GlobalToStorage.FindPointerSitenumbers such loads per shader: at mostDMSH_GPTR_MAX= 64, notedgptr site k: c<slot>[<word>] +<byte>.{start, end, delta}(DMSH_KIND_GPTR) before main's last-entry check, hint and search.dmsh_build.pywritesgptr_sitesas[NVN storage slot, byte].encode_args(NVN_RB_GPTR) reads each pointer word and fills the entry frommtl_gpu_addr, which also makes the pool resident.Chain 1ffcb6b77740. Reference entry: Raw global memory through a pointer read when the draw is bound. The game note has a line for it.
What this builds on, and what it doesn't take:
GlobalToStorage, which this extends, and Lichii's indirect offset).What it doesn't claim. In the game, the remaining task/mesh hangs on main were cured by an encoder boundary after the task/mesh run, not by this. That's a separate change, still being narrowed. This PR removes the table-miss path, which the standalone test shows hangs every time.
Checked
dm/gfx/tests/probes/raw_sites.min Standalone Metal probes: heap aliasing, render-target sizes, mesh index ranges, mesh pipeline archive, mesh/task sweep, task-shader hang #1583).task_hang.min Standalone Metal probes: heap aliasing, render-target sizes, mesh index ranges, mesh pipeline archive, mesh/task sweep, task-shader hang #1583):test_dmsh_reflect,t_refl(52 checks),test_nsp_chain,test_xlate_anycpu,test_docs,make -C dm/host GAME=0400486024DEC000,make -C dm/host/tests GAME=0400486024DEC000 -j run.make -C dm/gfx/tests GAME=0400486024DEC000 run: t_nvn and t_gfx_suspend don't link on main b2d46b8 either (_dm_vi_frame_queuedundefined: their link lines leave outt_host_stubs.c). The other 42 checks were run one by one: all pass except t_fuse_check (2 of 10, "the later pass's logical target is kept afterwards"), which fails the same way on main b2d46b8.scripts/regress.py checkwasn't run: only Ocarina of Time is available here.regress.py impactputs the reach at every game with raw global memory (the NVN entry points throughencode_args). A shader with no fixed-offset pointer gets no site and translates as before.Docs
docs/reference/shaders.mdand the game note, andpython3 dm/tools/docs_symptoms.pywas rundmsh_chain.py --headerwas run anddm/gfx/nvn/dmsh_chain.his part of the change