Skip to content

Ocarina of Time: diagnostics, off by default: TEXDUMP over a range of frames with depth targets, DM_NVN_A5288_C3 for the task hang - #1843

Open
ycarogabriel-lgtm wants to merge 7 commits into
seedds:mainfrom
ycarogabriel-lgtm:codex/pr-diag-switches
Open

ycarogabriel-lgtm wants to merge 7 commits into
seedds:mainfrom
ycarogabriel-lgtm:codex/pr-diag-switches

Conversation

@ycarogabriel-lgtm

@ycarogabriel-lgtm ycarogabriel-lgtm commented Nov 8, 2026 •

Copy link
Copy Markdown

What and why

Three switches we used, or will use, while chasing Ocarina of Time (0400486024DEC000) problems on an M1, 16 GB. All are off by default and cost one getenv when off.

  1. TEXDUMP over a range, with depth targets.

    • DM_NVN_TEXDUMP=<first>-<last>:<path> dumps every frame of the range. Each file name gets _F<frame> before its extension, because a serial that lives across frames would otherwise be overwritten.
    • DM_NVN_TEXDUMP_DEPTH=1 (or =WxH) also saves the depth attachments of the dumped frames: the raw depth with a text header, and an 8-bit PGM stretched over the depths below 1.
    • Why: OoT's per-actor shadow maps are 76x76 depth-only passes whose textures are made anew each frame. DM_NVN_TEXRAW, which takes a known serial, cannot reach them, and TEXDUMP saved only one frame's colour targets.
    • The single-frame, size= and layers= forms behave as before.
    • DM_CTL paths that start with ~/ are under $HOME (an iOS app's data container), and DM_NVN_TEXDUMP_MINW keeps a texdump to the wide targets #1483 adds DM_NVN_TEXDUMP_MINW. The two do not overlap in code, but they touch the same function.
  2. DM_NVN_A5288_C3=1.

  3. DM_NVN_MESH_GRID_LOG was dropped from this PR after merging main: 09dcb62 launches a mesh shader with no task in blocks of 1024, so a log of draws past the 1,024 mesh grid no longer applies.

Evidence

  • TEXDUMP range and depth were used on the M1 to follow OoT's shadow chain over 9 consecutive frames while the camera turned (our test log EXP-meshM1-71). Every input of the shadow projection came from the same frame, so we found no stale data in that chain; the visual glitch we still see is under investigation elsewhere.
  • DM_NVN_A5288_C3 builds and is documented, but has not been run in a game yet. The first run is planned with DM_NVN_ISOLATE_TASK=0 against =1 on the M1. No result is claimed here.

Risks and caveats

  • Off by default.
  • The range dump synchronizes with the GPU every frame, like the single-frame dump. It changes timing and hides timing-dependent problems.
  • DM_NVN_A5288_C3 reads guest memory on the completion handler thread, for a5288 draws only.

Checked

  • Host builds; no new compiler warnings.
  • With MTL_DEBUG_LAYER=1: t_pso_archive 135 checks, 0 failed; t_clear_tex 51 checks, 0 failed.
  • test_env_switches: the new rows are accepted. The test still fails on DM_NVN_BC_ALIAS and DM_YIELD_CLOCK, which also fail on main 7f50222.

🤖 Generated with Claude Code

ycarogabriel-lgtm and others added 2 commits November 8, 2026 16:04
…Time's shadow lag

Ocarina of Time's actor shadows trail the camera when it turns. Each actor's shadow is a 76x76
depth-only pass, followed by a pass that projects it into a screen-space mask, and the game makes
these textures anew every frame. So DM_NVN_TEXRAW (which takes a serial known beforehand) cannot
reach them, and DM_NVN_TEXDUMP saved only colour targets of a single frame. Deciding whether the
mask projects a shadow map from the same frame needs the maps of several consecutive frames while
the camera turns.

DM_NVN_TEXDUMP=first-last:path now dumps every frame of the range, with _F<frame> in each file
name. DM_NVN_TEXDUMP_DEPTH=1 (or =WxH, to keep only that size) also saves the depth attachments of
those frames: the raw depth and an 8-bit PGM stretched over the depths below 1. Both are off unless
set; the single-frame, size= and layers= forms behave as before.

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

On Apple silicon (M1, 16 GB here; seedds#1326, seedds#1605) Ocarina of Time's GPU hangs fall in passes where its
Hi-Z task a5288d0a6c4971fd launches mesh 3439aaa071d868ae. Reading the hung pass offline (seedds#1605)
found no raw global loads in its task, mesh or vertex shaders. The task's only loop scans a Hi-Z
rectangle per object, clamped to c3[193].xy - 1: the occlusion tile grid's width and height, ints at
byte 3088 of Maxwell constant buffer 3 (NVN uniform binding 0), with the tiles as floats of the same
block from byte 3120 (seedds#1326). Garbage there (float bits read as ints) would make the scan run for
seconds, or read past the block. Telling the suspects apart needs those values per draw: the game
rewriting the block while the GPU reads it, a stale binding, or a write the host loses.

DM_NVN_A5288_C3=1 keeps, for every a5288 draw, c3[193].xy, the block's address and size, whether an
inline update of the block is deferred in the pass, and the fence of its command buffer. It logs a
change of value, address or size (and once every 900 frames), and logs in red a W x H whose tiles
do not fit the block, with the values read as floats too. At each submission's completion it reads
the values again and counts the draws where they changed since encode, that is, a block rewritten
while the GPU could read it. On kIOGPUCommandBufferCallbackErrorHang it lists the hung command
buffer's draws against the values of the 64 command buffers before it.

DM_NVN_MESH_GRID_LOG=1 logs direct task / mesh draws of more than 1,024 workgroups. seedds#1708 confirms
1,024 as the mesh grid limit on M1 / M2. Indirect counts are written by the GPU and are not seen here.

Both are off by default and cost one getenv when off. docs/env_switches.tsv lists them, with
DM_NVN_TEXDUMP_DEPTH from the previous commit.

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 8, 2026
@ycarogabriel-lgtm

Copy link
Copy Markdown
Author

A note on the red clang-tidy (ratchet) check: it is not from this diff. The same job on main 7f50222 (run 42513044169) fails with the same counts in dm/gfx/nvn/nvn_mtl.m: clang-analyzer-core.BitwiseShift 1 (baseline 0), CallAndMessage 1 (baseline 0), NullDereference 10 (baseline 9). The findings the report lists for this PR are at the same places as in main's report (e.g. lines 5902, 9229 and the di/src/dst dereferences), none in the code this PR adds. So the baseline probably needs a refresh on main; I did not add any NOLINT here so as not to hide it.

…d has not been run

The three switches were used or are meant for this game's open problems
(shadow lag, the Hi-Z task hang of seedds#1326), and a reader of the game's note had
no way to find them. The note says which was used (the TEXDUMP range and depth
dump, on an M1 over 9 frames) and which has only been built (DM_NVN_A5288_C3,
DM_NVN_MESH_GRID_LOG), so that no result is implied for the second kind, and
that the dump changes timing. It also keeps the profile directory touched, from
which the pr-areas workflow takes the "game: 0400486024DEC000" label, as on the
game's other pull requests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@ycarogabriel-lgtm ycarogabriel-lgtm changed the title Diagnostics, off by default: TEXDUMP over a range of frames with depth targets, DM_NVN_A5288_C3 for Ocarina of Time's task hang, DM_NVN_MESH_GRID_LOG Ocarina of Time: diagnostics, off by default: TEXDUMP over a range of frames with depth targets, DM_NVN_A5288_C3 for the task hang, DM_NVN_MESH_GRID_LOG Nov 9, 2026
main moved (270b6fd, 09dcb62 and others) and mtk_encode conflicted: 09dcb62
makes a mesh shader with no task launch through an object function in blocks of
1024, and an indirect or direct draw now passes its {count, first} record to
the shader (ind_rec, DMSH_MESH_DRAW_SLOT).

Resolution: mtk_encode is main's, unchanged. DM_NVN_MESH_GRID_LOG is removed
from this PR (code, docs/env_switches.tsv row and the game note). It logged
direct task / mesh draws of more than 1,024 workgroups to find the mesh grid
limit of seedds#1708; main now launches such a draw in blocks of 1024 (the
workgroups are no longer the mesh grid), so the log would only report draws
that no longer exceed the limit, and its text for a mesh with no task
("a mesh grid past 1,024") is no longer true. The other diagnostics of the PR
(TEXDUMP range and depth, DM_NVN_A5288_C3) do not touch this function and stay,
off by default.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@ycarogabriel-lgtm ycarogabriel-lgtm changed the title Ocarina of Time: diagnostics, off by default: TEXDUMP over a range of frames with depth targets, DM_NVN_A5288_C3 for the task hang, DM_NVN_MESH_GRID_LOG Ocarina of Time: diagnostics, off by default: TEXDUMP over a range of frames with depth targets, DM_NVN_A5288_C3 for the task hang Nov 9, 2026
… made

The clang-tidy ratchet failed on this PR with one more
clang-analyzer-nullability.NullablePassedToNonnull in nvn_mtl.m than the
baseline (16 against 15): td_dump_depth, added here, passed the result of
newBufferWithLength: (nullable) as the toBuffer: of a blit copy and then read
buf.contents. The finding was ours, not main's (main's lint-c is green since
its baseline refresh).

A failed allocation now logs a [texdump] line and skips that texture, so the
diagnostic cannot blit into or read from a nil buffer. Behaviour with the
switch off, or with a buffer made, is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@ycarogabriel-lgtm

Copy link
Copy Markdown
Author

Update on the red clang-tidy (ratchet) mentioned above. The failure I described then (counts in nvn_mtl.m that main 7f50222 also had) no longer applies: main's baseline has been refreshed and its lint-c run at 09dcb62 passes.

The failure on head bf62ae0 is a different one and it is from this PR: dm/gfx/nvn/nvn_mtl.m has 16 clang-analyzer-nullability.NullablePassedToNonnull findings against 15 in the baseline (run 42547297527; the job reports 224 findings, baseline 223, one file/check pair above it). The extra one is in td_dump_depth, added here: it passed the result of newBufferWithLength: (nullable) as the toBuffer: of a blit copy.

Pushed 0ae9505: a failed allocation now logs a [texdump] line and skips that texture. Host build, test_docs, test_lint_c and test_env_switches pass locally; I cannot run clang-tidy 23.1.4 here, so whether the count is back to 15 is for the job to show once it re-runs.

@ycarogabriel-lgtm

Copy link
Copy Markdown
Author

The Sanitizers job failed on head 0ae9505 (run 42558669890) in the step ThreadSanitizer, the host tests that start threads, not in ASan/UBSan. The log ends with Assertion failed: (signaled(device_stopped, 3)), function blocked_start, file t_audio_device.c, line 191 (the audio device test waiting on a semaphore, built with -fsanitize=thread).

This PR changes only dm/gfx/nvn/nvn_mtl.m, docs/env_switches.tsv and the Ocarina of Time note, and does not touch the audio code or that test. The same job passed on the previous head bf62ae0 of this branch, and tsan-threads passed locally on that head. That points to a timing-dependent test on a loaded runner, not to this diff, but I have not reproduced the failure or shown it is flaky. I cannot re-run it (the API asks for admin rights); a re-run of the failed job would settle it.

@ycarogabriel-lgtm

Copy link
Copy Markdown
Author

The Unsigned iOS app job failed on head 0ae9505 (run 42558669890) at the link step of the iOS project build: ld: framework 'IOBluetooth' not found.

It is not from this diff (nvn_mtl.m, docs/env_switches.tsv, the Ocarina of Time note; none mentions IOBluetooth or a framework). IOBluetooth is imported by dm/host/pad_switch2_apple.m, added on main by a342d64 (Switch 2 controllers over Bluetooth LE on the Mac), which this branch does not contain; the job builds the pull request merged onto current main. The same error, framework 'IOBluetooth' not found, fails the same job on #1928 (run 42578128891, head c890f78a). The iOS job passed on this PR's earlier heads, before that commit was on main. I have not tried to fix it here, since it is main's.

main 9983d65 (seedds#1906) fixed the iOS link that main's Bluetooth commit a342d64
had broken ("framework 'IOBluetooth' not found"), which was failing this PR's
"Unsigned iOS app" job although the diff does not touch it. Merging it gives
the job a tree that links, and re-runs every job of the PR on the current main,
the Sanitizers one among them (its failure was an audio test under TSan, which
this diff does not touch, and I cannot re-run a single job without admin rights).
No conflicts, no change to this PR's own files.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@ycarogabriel-lgtm

Copy link
Copy Markdown
Author

The clang-tidy (ratchet) job failed on head dfed038 (run 42632137357), and this time it is not from this PR: dm/gfx/tests/t_pso_archive.m has 3 clang-analyzer-nullability.NullablePassedToNonnull findings against 2 in the baseline (224 findings in total, baseline 223, one file/check pair above it). The 0ae9505 fix for nvn_mtl.m held: that file is not in the list any more.

This PR does not touch t_pso_archive.m. The file changed on main in 70b9a68 (the pipeline archive index), and the baseline on main still lists 2 for it. Main's own lint-c run 42634192860 (head 1d37720, which contains 70b9a68) fails with the same line: the same file, 3 against 2, 224 findings against 223. So the baseline needs a refresh on main; I have not added a NOLINT here.

@ycarogabriel-lgtm

Copy link
Copy Markdown
Author

The Tool tests failure on head dfed038 (test_no_new_abi_literals: dm/gfx/nvn/nvn_mtl.m '?' went from 2 to 3) is not from this PR. I ran python3 -m unittest dm.tools.tests.test_dmsh_abi on main 04811a6 alone, on this branch's head dfed038, and it fails the same way on both: {'dm/gfx/nvn/nvn_mtl.m': {"'?'": '2 -> 3'}}. The diff of this PR against main adds no '?' literal to nvn_mtl.m. #1981 (open) changes that ratchet so it no longer counts "?"; I have not changed anything here for it.

main moved (0d84a3f) and nvn_mtl.m conflicted where 26062f9 made the
uniform-buffer slot of a binding come from cbuf_slot(): the zero-bank fallback,
the deferred-update shadow and the bound-size padding moved out of
encode_args_'s binding loop.

Resolution: the loop is main's. The one line this PR keeps in it, the
DM_NVN_A5288_C3 note of binding 0 of the task stage (a5c_note), now runs after
cbuf_slot, only when the diagnostic is on, the binding is bound and mapped
(mtl_gpu_addr), and with the same arguments as before (shader key, address,
bound size, whether an inline update is deferred over the block). Task draws
still always take the full encode (mtk_encode sets g_args_dirty[3]), so the
reuse and patch paths added by adaff73 do not skip it. Nothing else in this
PR changes; it stays off by default.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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