Repository navigation
Profile key pool_skip_retired: pool tables give a finalized texture's ID the dummy instead of making its texture again (Ocarina of Time out of GPU memory at a scene change) - #1393
Conversation
… ID the dummy instead of making its texture again, which ran Ocarina of Time out of GPU memory at a scene change A pool table for handles traced to no slot (pool_gpu) resolves every registered texture ID. At a scene change Ocarina of Time (0400486024DEC000, profile in seedds#1299) finalizes the old scene's textures and leaves their IDs registered: mtl_tex_release dropped their views, and the next table rebuild made a Metal texture for each of them again from its storage. At the house-to-forest change that was about 1,500 textures and 2.5 GB held only through retired IDs, as much again in deferred release, 16 GB of GPU memory allocated against Metal's 12.1 GB working set on a 16 GB Mac, and command buffers failed with kIOGPUCommandBufferCallbackErrorOutOfMemory. With pool_skip_retired on (DM_NVN_POOL_SKIP_RETIRED=1 / 0 overrides it), a retired ID that holds no view gets the dummy texture in the table; the table log counts them. It is a key, off by default: on the console a handle to a finalized texture still reads its storage while that is there. With it, Ocarina of Time played 25 minutes in Kokiri Forest with no out-of-memory, GPU memory peaking at about 12 GB and frames from 44 to 31 ms. t_pool_retired checks a live and a retired ID in one table, with the key and without; it fails without the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Testing this now on a second machine: Apple M4, 24 GB, macOS 27.2, Ocarina of Time v131072, docked. It's on lunks's Drafted with Claude; reviewed by me. |
|
Second-machine confirmation: Apple M4, 24 GB, macOS 27.2. Method: lunks's Results:
Caveats:
Drafted with Claude; reviewed by me. |
…bles give a finalized texture's ID the dummy instead of making its texture again (Ocarina of Time out of GPU memory at a scene change) Its retired-ID check goes into pool_entry, which the table patch and the full build share here (the patch passes no counter); Ocarina of Time's profile turns the key on, as upstream f3ac5ea does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
|
Tested on a real device, and it fixes the stutter on entering Kokiri Village / Forest for me. Setup: MacBook Pro, M4 Pro (14-core CPU / 20-core GPU), 48 GB, macOS 26.7.2, update-only NUP v131072, Mac app. Tree: stan69b's Without it, entering Kokiri Village gave 3–5 s at roughly 10–17 fps. In that 300-frame window:
A second, smaller burst came on the way into the forest (135,795 additions, 2,224 ms of commits). stan69b's B-6 fix was already in, so these were CPU-written (packaged) textures being made again on every walk, not new private ones. With Drafted with Claude; reviewed by me. |
…ss over IDs of another type (Ocarina of Time's residency commits 12 times fewer) Resolved against NXForge's pool table (seedds#1360's patching, seedds#1393's pool_skip_retired): - pool_entry passes over an ID whose registration resolved to another texture type (treg_t.tt) after the gone check, so both the full walk and the patch path skip it - the full build and the patch path take their buffers from the spares (pool_buf) and retire the replaced one (pool_buf_retire); pool_snap keeps its sub field - the pool table log line keeps NXForge's why/patched/kept counts and adds made/reused/passed over - t_int_float_view.m keeps main's checks; the Makefile has t_gmem_raw and t_pool_table - t_pool_table marks the 2D table out of date before its second rebuild, since a patch on registrations alone would not walk the 3D ID Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
) 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
…ures (seedds#1404)" and "Retire obsolete NVN chunks after whole-pool mapping (seedds#1417)" They tank Ocarina of Time's submit thread: on the same route and save, the host with them ran 12.4 fps with the submit thread at 56-78 ms CPU (73-78 ms wall), and without them 58.6-59.1 fps with the GPU at 12 ms. seedds#1393's pool_skip_retired stays (it is not implicated, and it fixes the scene-change OOM); seedds#1394's bitmap stays reverted in favour of upstream's address table.
… ID the dummy Net change of PR seedds#1393 at 3a534ed (seedds/nxnative), ported into pool_entry (this tree's walk); the log line keeps this tree's form. Off unless the key or DM_NVN_POOL_SKIP_RETIRED=1 is set.
What and why
Ocarina of Time (Switch 2,
0400486024DEC000, v131072; profile in #1299) runs out of GPU memory when Link leaves his house for Kokiri Forest. Command buffers fail withkIOGPUCommandBufferCallbackErrorOutOfMemory, with 16 GB of GPU memory allocated against Metal's recommended working set of 12.1 GB (16 GB Mac).Cause: a pool table for handles traced to no slot (
pool_gpu) resolves every registered texture ID, as a shader reading textures through handles may reach any of them. Ocarina of Time's texture pool has about 33,000 IDs. At a scene change the game finalizes the old scene's textures and leaves their IDs registered.mtl_tex_releasedrops their views, but the next table rebuild made a Metal texture for each of them again from its storage, and dropped it again at the end of the submission. At the house-to-forest change that was about 1,500 textures and 2.5 GB held only through retired IDs, plus as much again waiting in deferred release.Fix: a new profile key,
pool_skip_retired, off by default (DM_NVN_POOL_SKIP_RETIRED=1/0overrides it). With the key on, a retired ID that holds no view gets the dummy texture in a pool table. Not affected:The table log line now counts the IDs it skipped.
It's a key rather than the default because on the console, a handle to a finalized texture still reads the texture's storage while that storage exists. A game that keeps sampling through such handles would read the dummy. If you'd rather make it the default, or drop retired IDs from the tables some other way, I'm happy to change it.
The profile line (
pool_skip_retired on) can only go in #1299'sgame.tsv. I've put it in a comment there rather than stacking this PR on #1299. #1299 also splits this loop out intopool_entry, so the check moves there; the comment has those lines.Reference entry: Finalized textures made again by pool tables. The symptom index is regenerated.
Checked
make -C dm/gfx/tests GAME=0400380020056000 check_t_pool_retired(new, 11 checks): one table with a live ID and a retired ID, with the key and without. With the skip line disabled, the "retired ID gets the dummy" check fails.check_t_cpu_tex(56 checks, retired descriptors) andcheck_t_pool_chunks(75) pass.test_game_profile.py,test_env_switches.pyandtest_docs.pypass.lint_c.py tidyonnvn_mtl.mand the new test: no finding above the baseline. My clang-tidy is LLVM 21, and the baseline was made with 23.make -C dm/host GAME=0400380020056000 dmhostbuilds.make -C dm/gfx/tests run) was not run.mainhas no profile for the game.mainchanges behavior.Docs
docs/reference/nvn-objects.mdentry, andpython3 dm/tools/docs_symptoms.pywas run (the symptom index)dm/games/README.md), and the switch is indocs/env_switches.tsvDrafted with Claude; reviewed by me.
🤖 Generated with Claude Code