Skip to content

A GPU hang or page fault names its command buffer: frame, segment, passes and the shader keys it ran - #1382

Closed
PucaVaz wants to merge 1 commit into
seedds:mainfrom
PucaVaz:gpu-hang-names
Closed

PucaVaz wants to merge 1 commit into
seedds:mainfrom
PucaVaz:gpu-hang-names

Conversation

@PucaVaz

@PucaVaz PucaVaz commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

What and why

Refs #1326. This is the second piece I described there: when the GPU hangs or faults, the log now names the work that failed.

Symptom. dacONJA's Ocarina of Time run on #1326 hung the GPU about 7 minutes in. Like every GPU error so far, the log had only [RED] segment command buffer execution error: Error Domain=MTL4CommandQueueErrorDomain Code=1 "(null)" with kIOGPUCommandBufferCallbackErrorHang nested inside. It gave no frame, segment, pass or shader. Naming the shader took a rerun with lunks's DM_NVN_HANGLOG=1 DM_NVN_CUT=1 on his branch.

Cause. Metal 4 reports a GPU error only as the commit feedback's error; it has nothing like Metal 3's encoder execution status. The feedback handlers of the main command buffers kept nothing about what their command buffer held.

Change (nvn_mtl.m, always on):

  • Each main command buffer is labelled f<frame> s<segment> p<first pass>-<last pass>, for Xcode GPU captures and Instruments. That covers sub_cut's segments, mtl_sub_commit's last segment and mdi_wait's count wait; the count wait now has a feedback handler for this. With DM_NVN_ENC_THREAD the label goes through the encode thread's record (rec_cb_block).
  • Each command buffer's entry keeps the same fields plus its fence, its draw and dispatch counts, and the keys of the game shaders its draws and dispatches ran. Each key is stored once, through a stamp on NvShader, and only the first 32 are kept; the rest are counted. Keys are noted in draw_ (the program, plus task/mesh and geometry-mesh stages), in dispatch and in geo_gs_one.
  • The feedback handler keeps its command buffer's entry by value, so a hang reported seconds later, after hundreds more segments, still names it. A ring keeps the last 256 for the ones before it.
  • A command buffer whose error is its own prints a header with the error's kind, then its entry and the three before it, oldest first. Innocent victims print nothing (the same InnocentVictim test mtl_sub_commit already uses). The kinds are: a hang, a page fault, out of memory, or ignored once macOS revoked the GPU access, in which case the ones before it are the last the GPU ran.
  • An entry prints once while the ring holds it. An ignored command buffer prints only the first time, and there are at most 8 headers per run. From the test:
[RED] failed GPU work: main command buffer #2 f7 s1 p2-2 caused a GPU hang (imitated by DM_TEST_GPU_HANG); the game's shaders it and the command buffers before it ran, oldest first (v vertex, f fragment, g geometry, h hull, d domain, t task, m mesh, c compute):
[RED]   #1 f7 s0 p1-1 (segment of fence 1): draws 2, dispatches 0; v:a0a0a0a0a0a0a0a1 f:a0a0a0a0a0a0a0a2
[RED]   #2 f7 s1 p2-2 (segment of fence 1): draws 1, dispatches 1; c:c0c0c0c0c0c0c0c3 v:b0b0b0b0b0b0b0b1 f:b0b0b0b0b0b0b0b2  <- failed

DM_TEST_GPU_HANG=<n> makes the nth main command buffer report a hang in the shape of #1326's log while its work still runs. That number is the #n in the lines.

Credit. I took these from lunks's DM_NVN_HANGLOG (on fix/gmem-pool-table): the segment tagging, reading the error's kind from its description, and noting shaders at the draw and the dispatch. Printing the keys of the failed work and of the work before it, each key once, follows wk_dump in #1275 (uncraftablexd). Here it is kept per command buffer, not per submission. #1275's watchdog is not part of this PR.

Interplay with #1332. This PR is independent of #1332, and either can land first:

Reference entry: Failed GPU work named by its command buffer. The symptom index is regenerated.

Checked

  • Tests:

    • make -C dm/gfx/tests GAME=010015100B514000 check_t_gpu_hang_names: the new test, run with and without DM_NVN_ENC_THREAD, 12 checks each, all pass. It runs one submission of three segments with known shaders, and the second one imitates a hang. The checks cover:

      • the dump names f7 s1 p2-2 with its compute and draw keys, lists the segment before it, and leaves out the one after it;
      • no error and an innocent victim print nothing;
      • a page fault prints only what was not printed yet;
      • an ignored command buffer prints once;
      • a hang reported after the ring has gone round still names its command buffer;
      • the label is on the command buffer.

      6 of the 12 checks fail with the segment's cbn_failed call taken out.

    • make -j8 -C dm/gfx/tests GAME=010015100B514000 run: passes.

    • python3 -m unittest dm/tools/tests/test_docs.py dm/tools/tests/test_env_switches.py dm/tools/tests/test_gfx_tests_makefile.py: OK.

    • clang-tidy (dm/tools/lint_c.py tidy on t_gpu_hang_names.m and nvn_mtl.m) finds nothing above the baseline. I ran LLVM 22.1.1 locally; CI uses 23.1.4.

    • nvn_mtl.m compiles with the host's flags with the same 56 warnings as main.

  • Cost when nothing fails, timed by the test on an M4 without the Metal debug layer, with or without the encode thread:

    • about 1.9 ns per draw whose shaders are already noted;
    • 9.5 ns for a program's first draw in a command buffer;
    • 175–190 ns per command buffer for its label and the ring copy.

    Over a 17,000-frame session, Ocarina of Time averaged 531 draws and 84 passes per frame with 4.7 ms of submit time per frame. That puts this under 10 µs per frame, so it stays on with no switch.

  • Games run: The Legend of Zelda: Ocarina of Time (0400486024DEC000, v131072), windowed, on lunks's 574a38f with this commit cherry-picked ([WIP] The Legend of Zelda: Ocarina of Time (Switch 2, 0400486024DEC000, v0, SDK 22.2.8) runs on the Mac through the title movie into Kokiri Forest #1299 has the profile), with DM_TEST_GPU_HANG=8000. At frame 1155, 41 s in, during the title, the log printed the lines below, using the game's own shader keys. The game had run 1,164 frames with no other new red line, and the run harness stopped it on the line. I did not play further.

    [RED] failed GPU work: main command buffer #8000 f1155 s0 no pass caused a GPU hang (imitated by DM_TEST_GPU_HANG); …
    [RED]   #7997 f1154 s0 p3375-3376 (segment of fence 6931): draws 1, dispatches 14; c:49dff010b9832251_b c:ba079f68ae288dba_b c:a8b4fb48fee0ccf8_b c:e2eebd7ba18fe35f_b c:e6be82d835fbe4d3_b c:9442573ee4ace011_b c:0a56002cdc7e1fce_b c:d15370a382630d4e_b c:7f7ff0f36b54d90f_b c:5a7d69b067a7f011_b c:4f41ccf24f163e0d_b v:8f9728d9b2b88600_b f:29e3c9dd92b8a571_b
    [RED]   #7998 f1154 s1 p3377-3377 (last segment of fence 6931): draws 1, dispatches 0; v:a5b1bd74e47458d7_b f:29e3c9dd92b8a571_b
    [RED]   #7999 f1155 s0 no pass (last segment of fence 6932): draws 0, dispatches 0; no game shaders
    [RED]   #8000 f1155 s0 no pass (last segment of fence 6933): draws 0, dispatches 0; no game shaders  <- failed
    
  • Devices and OS: M4 (24 GB), macOS 27.2, Xcode 27.2. Not checked on iOS.

  • regress.py was not run. The change adds bookkeeping (a stamp compare per shader of a draw, a label and two entry copies per command buffer) and log lines that print only on a GPU error, and Ocarina of Time's profile is not on main yet ([WIP] The Legend of Zelda: Ocarina of Time (Switch 2, 0400486024DEC000, v0, SDK 22.2.8) runs on the Mac through the title movie into Kokiri Forest #1299). No real hang or page fault was reproduced; only the imitation was.

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)
  • A chain tool changed: dmsh_chain.py --header was run and dm/gfx/nvn/dmsh_chain.h is part of the change
  • No game files, keys or data extracted from a game in the change or the attachments

Drafted with Claude; reviewed by me.

🤖 Generated with Claude Code

@github-actions github-actions Bot added nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) docs Documentation labels Nov 5, 2026
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 5, 2026
…mand buffer: frame, segment, passes and the shader keys it ran

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g

# Conflicts:
#	dm/gfx/nvn/nvn_mtl.m
#	docs/env_switches.tsv
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 5, 2026
…dds#1382)

seedds#1382's cbn_key names a shader with no key "?" in its failed GPU work lines, a
log placeholder like the two the ratchet already allows, not an ABI value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
@PucaVaz

PucaVaz commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

clang-tidy: the one finding above the baseline is in dm/gfx/tests/t_int_float_view.m from main's f083873 (NullablePassedToNonnull), which this PR doesn't touch; #1338 fixes it. This PR's files have nothing above the baseline.

Drafted with Claude; reviewed by me.

…sses and the shader keys it ran

In issue seedds#1326, Ocarina of Time hung the GPU about 7 minutes into a session, and the log had only "[RED] segment command buffer execution error" with kIOGPUCommandBufferCallbackErrorHang nested in the error: no frame, segment, pass or shader. Naming the shader took a rerun with lunks's DM_NVN_HANGLOG and DM_NVN_CUT=1 on his branch.

Metal 4 reports a GPU error only as the error of the commit's feedback; it has no counterpart of Metal 3's encoder execution status. The feedback handlers of the main command buffers kept nothing of what their command buffer held.

Each main command buffer (sub_cut's segments, mtl_sub_commit's last segment, and mdi_wait's count wait, which gets a feedback handler for it) is labelled f<frame> s<segment> p<first pass>-<last pass>, and its entry keeps the same with its fence, its draw and dispatch counts and the keys of the game's shaders its draws and dispatches ran: each once, through a stamp on the shader, and the first 32. The feedback handler keeps the entry by value, so a hang reported seconds later, after hundreds more segments, still names its command buffer; a ring of the last 256 keeps the ones before it. A command buffer whose error is its own (not an InnocentVictim) prints a [RED] header with the error's kind (a hang, a page fault, out of memory, or ignored once macOS revoked the GPU access) and then its entry and the three before it, oldest first. An entry prints once while the ring holds it, an ignored command buffer only the first time, and at most 8 headers a run. DM_TEST_GPU_HANG=<n> makes the nth main command buffer report a hang while its work runs.

The segment tagging, the error kinds read from the error's description and the notes at draw and dispatch follow lunks's DM_NVN_HANGLOG; printing the shader keys of the failed work and of the work before it, each key once, follows wk_dump in seedds#1275, kept per command buffer here rather than per submission.

Checked on an M4 (24 GB), macOS 27.2, Xcode 27.2:
- t_gpu_hang_names, a new test, with and without DM_NVN_ENC_THREAD: one submission of three segments with known shaders, the second imitating a hang. The dump names it f7 s1 p2-2 with its compute and draw shader keys and the segment before it, not the one after; no error and an innocent victim print nothing; a page fault prints only what was not printed; an ignored command buffer prints once; a hang reported after the ring has gone round still names its command buffer; the label is on the command buffer. 6 of its 12 checks fail with the segment's handler call taken out.
- Cost when nothing fails, timed by the test: about 1.9 ns a draw whose shaders are already noted, 9.5 ns a program's first draw in a command buffer, and 175-190 ns a command buffer closed (its label and the ring copy), with or without the encode thread. Ocarina of Time averaged 531 draws and 84 passes a frame over a 17,000-frame session, with 4.7 ms of submit time a frame, so this comes to under 10 µs a frame there; it stays on, with no switch.
- make -C dm/gfx/tests GAME=010015100B514000 run.
- The docs, switch-table and test-Makefile tests; clang-tidy (LLVM 22.1.1; CI has 23.1.4) finds nothing above the baseline; nvn_mtl.m compiles with the host's flags with the same 56 warnings as main.
- Ocarina of Time (lunks's 574a38f with this change cherry-picked; its profile is in seedds#1299), windowed, DM_TEST_GPU_HANG=8000: at frame 1155, 41 s in, the log named main command buffer #8000 and listed the three before it with the game's shader keys, one of them a segment with 14 dispatches of 11 compute shaders and a draw. The game had run 1,164 frames with no other new red line. No real hang or page fault was reproduced.

_Drafted with Claude; reviewed by me._
@PucaVaz

PucaVaz commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

The Tool tests failure was mine: test_dmsh_abi's ratchet counted the "?" placeholder for a shader without a key as a third ABI literal in nvn_mtl.m ("?" is also an ABI kind name). It now reads "unkeyed" (c87cc8c). test_dmsh_abi.py and the whole tool-test loop pass locally, and check_t_gpu_hang_names still gives 12 checks, 0 failed. The clang-tidy finding left is main's t_int_float_view.m, as noted above.

Drafted with Claude; reviewed by me.

gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 6, 2026
… layers, seedds#1345 and seedds#1373 landed)

Upstream's c83c48f makes tex_fit's views from layer counts, which covers the cube read as a 2D array, so NXForge's own version of that (f17b3e7) is reverted before this merge. hle_audren.c and its notes take upstream's landed seedds#1345. The failed command buffer report keeps both seedds#1382's segment names (cbn_close) and upstream's shader keys (cbs_feedback), and seedds#1332's gpu_lost_check. The present keeps NXForge's sidebar inset on upstream's present_fit. The translator binaries, lock and chain header are upstream's until the translator is rebuilt on the Mac.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 6, 2026
) replaces the pull request heads

Upstream landed Ocarina of Time in main (chain adec7d81d970: "holds 60 fps in
Kokiri Forest, 4 GB less") and closed the pull requests it folded in, with
reworded and partly different code. Mixing a pull request head with its landed
version hunk by hunk silently broke things (two GlobalToStorage tables, a
duplicate AF2ip, two mtk_pso definitions), so the rule here is one version per
feature:

- Pull requests upstream closed (seedds#1299, seedds#1300, seedds#1312, seedds#1343, seedds#1345, seedds#1353,
  seedds#1354, seedds#1359, seedds#1360, seedds#1373, seedds#1428, seedds#1430, seedds#1434): their hunks are taken out
  of our side before the three-way merge, so upstream's landed version stands
  alone. Gone with them: bindless pool table patching (reg_bump), the ghost
  table's per-submission hold, the growing raw pool table and its write-log
  sizing, seedds#1428's pool_buf spares (upstream's pool_table_take is the same
  idea), the ixc-based indirect mesh draw (upstream uses mdx), and their tests
  (t_pool_table, t_mesh_indirect, t_gmem_raw, finalized_bindless_pool,
  test_bnsh_nvn2, test_ndve).
- Open pull requests that upstream's landing supersedes: seedds#1394 (upstream's
  mtl_pool_addressed) and seedds#1393 pool_skip_retired (upstream's budgeted pool
  walk; the profile does not use it) are dropped with their tests and docs.
  seedds#1368's mesh pipeline prewarm is dropped too: upstream kept the background
  build in nvn_geometry.inc, and the prewarm's mtk_build / mtk_claim /
  g_warm_mps went with it.
- Kept, as upstream does not have them: NXForge's own features (GPU watchdog,
  present filters and anti-aliasing, aniso floor, quick settings, pipeline
  archive compaction, upload breakdown, memory report lines) and the still
  open pull requests seedds#1304, seedds#1305, seedds#1308, seedds#1320, seedds#1331, seedds#1332, seedds#1355, seedds#1356,
  seedds#1369, seedds#1376, seedds#1377, seedds#1379, seedds#1382, seedds#1390, seedds#1391, seedds#1392, seedds#1414, seedds#1420,
  seedds#1436, seedds#1439, seedds#1446, seedds#1453.
- Combined where both sides changed the same lines: GW_N 1 << 16 with seedds#1320's
  wide-write intervals (GW_PAGES = SBW_WPAGES, 128 slots), the CPU upload log
  with upstream's linear upload route, arch_open_end before the compaction
  limit, cbn_prog in geo_gs_one (seedds#1382).
- Translator: upstream's sources and binaries, plus seedds#1414's
  InstEmitAmpereMem diff and the LDSM SubgroupBarrier in
  InstEmitAmpereTensor.cs. The binaries and dmsh_chain.lock are upstream's
  until the Mac rebuilds them (dotnet publish, tool_lock --update-lock,
  dmsh_chain --header); dmsh_chain.h is regenerated for now.
- RED_ONCE keys: upstream took 153 and 154, so the present anti-aliasing lines
  move to 156 and 157 and seedds#1420's packaged-entry line to 158 (array 159).
- Host: hle_net.c and t_net_eventfd.c are upstream's (its eventfd poll waits
  out the whole timeout, which our 5d7920c fixed another way).
- Kept ours: dm/tools/romfs_image.py and its test, so the 26 GB RomFS image is
  not packed again.

dm/tools tests on Linux: same failures as on 875449e (Linux-only quirks),
plus test_nlg4 and test_dmsh_pool_arrays, which fail on upstream main too.
Not yet built on the Mac.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
@PucaVaz

PucaVaz commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

Closing: main's f956ffb now names a failed command buffer's game shaders (cbs_feedback), which covers what this PR did.

@PucaVaz PucaVaz closed this Nov 6, 2026
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