Repository navigation
Render targets dead by overwrite are reclaimed (DM_NVN_RT_RECLAIM, off by default): Ocarina of Time's Kokiri load holds 1.1-1.2 GB of render targets instead of 3.5 GB - #1588
Conversation
|
Known issue, fix in progress: with |
|
Update on the page faults reported above: found and fixed in #1607, which this PR now depends on. The cause wasn't in the reclaim itself. In |
…carina of Time's Kokiri load no longer piles up 3.4 GB of render targets Symptom: at Ocarina of Time's Kokiri Forest load on a 16 GB M2 Pro, the residency set grew from 1.6 to 6.6 GB in about 30 s, 3.2 GB of it render targets (72 → 1,068 textures, 31 → 313 descriptors); swap followed wired (wired 3.0 → 9.8 GB while swap rose 1.1 → 3.2 GB and available memory fell to 15%), and the runs reached the +4 GB swap limit. Cause: the game keeps its render targets as texture objects over one 305 MB virtual pool, reused by overwrite: at f1,200, 781 live targets (2,788 MB of sizes) sat over 74 MB of storage, and 716 of the 828 live ones (2,697 MB) had their whole storage range written by other targets since their own last write. On the console those cost the pool's bytes; here each kept a private Metal texture, and the texture-ID registry (treg_tex) made one for every registered ID, dead or not. Fix (behind DM_NVN_RT_RECLAIM=1): a shared texture is dead once other textures' render passes have covered its whole storage range in its pool since its own last write. Coverage counts only passes whose draws and clears (viewport ∩ scissor) covered their whole attachment; a partial pass covers nothing. A dead texture leaves the sharing table with its serials' mappings; its Metal texture goes to a free list keyed on the full Metal descriptor, is handed to the next texture of that descriptor after the fence of its last use and zero-filled as a new one is, and is released after 15 idle frames. A reclaimed serial's texture ID reads as an empty slot until a render pass writes it again, which notes the serial so its IDs are taken again. Never reclaimed: IMAGE textures, copy / clear destinations, readback sources, anything a pool copy overlaps, sparse pools, textures filled from pool bytes, and textures read before their next write after more than 4 deaths (luminance and bloom chains: 429 textures, 324 MB in a run to f4,978). Checked: Ocarina of Time on an M2 Pro 16 GB, Kokiri Forest route to f7,500. Render targets in the set stayed at 1.1-1.2 GB instead of 3.5 GB through the load, peak wired 7.6-8.8 GB, and swap grew by 0.05-2.4 GB. A third of attachment passes were partial and cover nothing. Shots at f1,500 and f2,500 match a tree without it in scene and brightness (127.4 / 96.3 against 128.9 / 96.1). The free-list key and the aliasing rule rest on seedds#1583's heap_alias and rt_sizes probes: a texture placed over another's memory reads its bytes only with the same width, height, bytes per pixel and compression, and a single dynamic-resolution step scrambles it. Those game runs were on a tree that also carried PRs 1417 and 1484 and the bind-time pointers. Rebased on main, with the switch off by default: host build, host tests, t_alias_takeover (28), t_image_alias (329), test_docs. No unit test drives the reclaim itself yet, and regress.py check was not run (only Ocarina of Time is available here). Reference entry and game note line added; env_switches.tsv lists the switch.
… released texture on the pool tables' fast path (GPU page faults) (PR #1607) tex_by_id kept an ID's cached view when cur == v || v.parentTexture == cur. When the ID has no texture now, treg_tex returns nil, and a cached view that is the base texture has no parent either, so nil == nil kept it and stamped it valid; tex_fast_rid then gave the pool tables a released texture's resource ID. The view is kept only when cur is non-nil. Ocarina of Time with PR #1588's reclaim: the faults at f3,677 and f4,851 are gone. An A/B walk on main reached the path zero times, with the same red lines and pace. Co-authored-by: nafields <2613057+nafields@users.noreply.github.com>
…imed, DM_NVN_RT_RECLAIM=1, off by default: Ocarina's Kokiri load no longer piles up 3.4 GB of render targets) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013SzykpcKm2eZPvPjGPKx1C
|
Tested this PR on main. In my scene, turning the reclaim on corrupts the picture. Details below, in case they help. Setup
Results
The green shows in every reclaim-on run (two). There are no GPU errors in any run. On this base the set is only 136 MB smaller, and GPU allocations are 270 MB larger: main has already cut most of this pool's targets since you measured. What the reclaim's own log says. The "most read while dead" targets are the ones in the 305 MB virtual pool around the scene colour target:
That matches what I found on #1453 (#1453 (comment)):
Two small things
I can rerun with extra logging on this save if useful, for example the coverage decision for the target at 0x1aa1800. |
|
Same green area here with |
…nd a freed target's heap waits resident for the next target it fits: Ocarina of Time's dynamic resolution, free to move since 04419bc, made about 3 new Metal textures a frame in its 305 MB target pool; on an M6 (16 GB), alternating the switch every 300 frames while the resolution moved, the target site added 455-521 MB to the residency set per 300 frames off and 27-37 MB on, frame p90 33-50 ms against 17-18 ms (issue #1831) - nvn_mtl.m rh_texture: a COMPRESSIBLE texture in a CPU_NO_ACCESS pool (private storage; not display, video, linear or sparse; not a resolution family's heap) is made in a placement heap of its exact size. When the texture is freed (defer_drain, after its submissions completed) its heap goes to a free list, still resident; the next such target up to an eighth smaller than the heap is placed in it and filled as a new allocation is (tex_init_fill or zeros). No alias flush is added: the memory was the old texture's only up to its completed last use - rh_trim, when a submission begins: heaps that waited 3 s, and the oldest past 256 MB waiting, leave the residency set - The memory line counts the heaps made, reused, freed and waiting. DM_NVN_RT_RECYCLE=0 turns it off; ab alternates it with the DM_AB phases - Why not share the pool's memory as on the console: a heap mirroring the pool (PR #1453) and a reclaim of targets dead by overwrite (PR #1588) corrupt the picture, as the game keeps live targets in the part of the full-size scene target its current resolution leaves unused, which only NVN's layout allows. The live targets' steady total (about 2.1 GB in the village) stays open - t_rt_recycle (direct and deferred encoding, and with the switch off); t_image_alias's single-description check now asks that the texture is not in a family heap Measured with the shaders of chain f9f01e4f9994 (on disk), on a host pinned to it. Village screenshots clean, no GPU errors or red lines. Unit tests: t_rt_recycle, t_image_alias, t_alias_takeover, t_upgrade_alias, t_cpu_tex and the gfx run pass (t_present_crop once rebased onto its fix, 3b6b980). Regression: not run (the user's call). The reporter's 16 GB MacBook Air M4 and Hyrule Field are not tested.
|
Closing this one. Thank you, @nafields, and @PromethOps and @dpadr for testing it. #1607, which came out of chasing this PR's faults, is on main. The green area that two testers saw with the switch on (M5 Pro and M2 Air) means the rule "dead once other whole-attachment passes covered its range" marks some targets dead while the game still reads them. PromethOps' finding on #1453 fits that. The game keeps live targets in the part of the full-size scene target that its current resolution leaves unused, so a full-size pass over a range doesn't mean what was there is gone. Without a liveness rule that holds for that, a reclaim of live objects can't land. The churn part of the problem is handled on main in 7f50222 (#1831), without judging anything dead. Each render target in a GPU-only pool gets a placement heap of its own. When the host frees the texture (the sharing table's takeover, after its last use completed), the heap stays resident and the next target of about the same size goes into it. That uses your What this PR aimed at beyond that, the live total (about 2 GB in the village), stays open in the game note. A new PR is welcome if a sound rule for when a target is dead turns up. It would build on the recycled heaps rather than a free list of its own. |
… released texture on the pool tables' fast path (GPU page faults) (PR seedds#1607) tex_by_id kept an ID's cached view when cur == v || v.parentTexture == cur. When the ID has no texture now, treg_tex returns nil, and a cached view that is the base texture has no parent either, so nil == nil kept it and stamped it valid; tex_fast_rid then gave the pool tables a released texture's resource ID. The view is kept only when cur is non-nil. Ocarina of Time with PR seedds#1588's reclaim: the faults at f3,677 and f4,851 are gone. An A/B walk on main reached the path zero times, with the same red lines and pace. Co-authored-by: nafields <2613057+nafields@users.noreply.github.com>
…nd a freed target's heap waits resident for the next target it fits: Ocarina of Time's dynamic resolution, free to move since 141c8b4, made about 3 new Metal textures a frame in its 305 MB target pool; on an M6 (16 GB), alternating the switch every 300 frames while the resolution moved, the target site added 455-521 MB to the residency set per 300 frames off and 27-37 MB on, frame p90 33-50 ms against 17-18 ms (issue seedds#1831) - nvn_mtl.m rh_texture: a COMPRESSIBLE texture in a CPU_NO_ACCESS pool (private storage; not display, video, linear or sparse; not a resolution family's heap) is made in a placement heap of its exact size. When the texture is freed (defer_drain, after its submissions completed) its heap goes to a free list, still resident; the next such target up to an eighth smaller than the heap is placed in it and filled as a new allocation is (tex_init_fill or zeros). No alias flush is added: the memory was the old texture's only up to its completed last use - rh_trim, when a submission begins: heaps that waited 3 s, and the oldest past 256 MB waiting, leave the residency set - The memory line counts the heaps made, reused, freed and waiting. DM_NVN_RT_RECYCLE=0 turns it off; ab alternates it with the DM_AB phases - Why not share the pool's memory as on the console: a heap mirroring the pool (PR seedds#1453) and a reclaim of targets dead by overwrite (PR seedds#1588) corrupt the picture, as the game keeps live targets in the part of the full-size scene target its current resolution leaves unused, which only NVN's layout allows. The live targets' steady total (about 2.1 GB in the village) stays open - t_rt_recycle (direct and deferred encoding, and with the switch off); t_image_alias's single-description check now asks that the texture is not in a family heap Measured with the shaders of chain f9f01e4f9994 (on disk), on a host pinned to it. Village screenshots clean, no GPU errors or red lines. Unit tests: t_rt_recycle, t_image_alias, t_alias_takeover, t_upgrade_alias, t_cpu_tex and the gfx run pass (t_present_crop once rebased onto its fix, fd601de). Regression: not run (the user's call). The reporter's 16 GB MacBook Air M4 and Hyrule Field are not tested.
…ee switches that are not on main are no longer quoted as switches docs/env_switches.tsv: DM_NVN_SUB_DRAIN (A-2's switch to restore the old wait at each submission's end) gets its class and meaning. The docs quoted three names that no code reads. Each mention now drops its backticks and says the switch is not on main: - DM_NVN_VC_WAYS: another fork's switch, in memory.md. - DM_NVN_CUT_FIRST: a probe switch since removed, in Breath of the Wild's note. - DM_NVN_RT_RECLAIM: PR seedds#1588's switch, in rendering.md, and so in the symptom index too (docs_symptoms.py).
…ee switches that are not on main are no longer quoted as switches docs/env_switches.tsv: DM_NVN_SUB_DRAIN (A-2's switch to restore the old wait at each submission's end) gets its class and meaning. The docs quoted three names that no code reads. Each mention now drops its backticks and says the switch is not on main: - DM_NVN_VC_WAYS: another fork's switch, in memory.md. - DM_NVN_CUT_FIRST: a probe switch since removed, in Breath of the Wild's note. - DM_NVN_RT_RECLAIM: PR seedds#1588's switch, in rendering.md, and so in the symptom index too (docs_symptoms.py).
…ee switches that are not on main are no longer quoted as switches docs/env_switches.tsv: DM_NVN_SUB_DRAIN (A-2's switch to restore the old wait at each submission's end) gets its class and meaning. The docs quoted three names that no code reads. Each mention now drops its backticks and says the switch is not on main: - DM_NVN_VC_WAYS: another fork's switch, in memory.md. - DM_NVN_CUT_FIRST: a probe switch since removed, in Breath of the Wild's note. - DM_NVN_RT_RECLAIM: PR seedds#1588's switch, in rendering.md, and so in the symptom index too (docs_symptoms.py).
…ee switches that are not on main are no longer quoted as switches docs/env_switches.tsv: DM_NVN_SUB_DRAIN (A-2's switch to restore the old wait at each submission's end) gets its class and meaning. The docs quoted three names that no code reads. Each mention now drops its backticks and says the switch is not on main: - DM_NVN_VC_WAYS: another fork's switch, in memory.md. - DM_NVN_CUT_FIRST: a probe switch since removed, in Breath of the Wild's note. - DM_NVN_RT_RECLAIM: PR seedds#1588's switch, in rendering.md, and so in the symptom index too (docs_symptoms.py).
…ain, and DM_NVN_VCONV_KEEP's row says what =1 gives: env_switches.py lists DM_NVN_COPY_ABSORB, DM_NVN_CUT_FIRST, DM_NVN_RT_RECLAIM and DM_NVN_VC_WAYS as quoted in the docs but read nowhere, asking that each mention say so. COPY_ABSORB's (nvn-objects.md, gone with pkg_copy_absorb) and VC_WAYS's (memory.md, the fork's texture view cache, not on main) did; BotW's note named DM_NVN_CUT_FIRST=ab, a probe of its frame-step investigation, and rendering.md's texture-ID entry named DM_NVN_RT_RECLAIM=1 from PR #1588, neither saying the switch was never on main. They now do (the advisory line keeps listing the names: it reads no prose). vk_min reads DM_NVN_VCONV_KEEP=1 as the default 4096, which the row now says. The table's check failure at e2df31c (no DM_NVN_VCONV_KEEP row) was fixed by ccd834e. Symptom index regenerated. Checked, as the end of a review of the Python suite: every test_*.py in dm/tools/tests, dm/ios/tests and dm/gfx/tests in its own process with CI's settings (DM_GAME=010015100B514000, DMSH_TEST_XLATE the in-repo translator, the pinned venv): 187 files pass, 2,081 tests, 11 skipped in 6 files. The skipped ones run here: test_pso_archive, test_transform_feedback, test_dmsh_border and test_dmsh_geometry with their native fixtures (make in dm/gfx/tests) and test_dmsh_membar_fence with DMSH_TEST_METAL=1 pass; test_dmsh_meshstage with DMSH_TEST_GPU=1 fails in its GPU case (dmsh_meshstage has no attributes(), since ccd834e; CI does not set the flag), a separate fix. test_docs and test_env_switches after the edits; lint_python.py: 321 findings, all in the baseline. No game (docs only)
…ee switches that are not on main are no longer quoted as switches docs/env_switches.tsv: DM_NVN_SUB_DRAIN (A-2's switch to restore the old wait at each submission's end) gets its class and meaning. The docs quoted three names that no code reads. Each mention now drops its backticks and says the switch is not on main: - DM_NVN_VC_WAYS: another fork's switch, in memory.md. - DM_NVN_CUT_FIRST: a probe switch since removed, in Breath of the Wild's note. - DM_NVN_RT_RECLAIM: PR seedds#1588's switch, in rendering.md, and so in the symptom index too (docs_symptoms.py).
…ee switches that are not on main are no longer quoted as switches docs/env_switches.tsv: DM_NVN_SUB_DRAIN (A-2's switch to restore the old wait at each submission's end) gets its class and meaning. The docs quoted three names that no code reads. Each mention now drops its backticks and says the switch is not on main: - DM_NVN_VC_WAYS: another fork's switch, in memory.md. - DM_NVN_CUT_FIRST: a probe switch since removed, in Breath of the Wild's note. - DM_NVN_RT_RECLAIM: PR seedds#1588's switch, in rendering.md, and so in the symptom index too (docs_symptoms.py).
…ee switches that are not on main are no longer quoted as switches docs/env_switches.tsv: DM_NVN_SUB_DRAIN (A-2's switch to restore the old wait at each submission's end) gets its class and meaning. The docs quoted three names that no code reads. Each mention now drops its backticks and says the switch is not on main: - DM_NVN_VC_WAYS: another fork's switch, in memory.md. - DM_NVN_CUT_FIRST: a probe switch since removed, in Breath of the Wild's note. - DM_NVN_RT_RECLAIM: PR seedds#1588's switch, in rendering.md, and so in the symptom index too (docs_symptoms.py).
…ee switches that are not on main are no longer quoted as switches docs/env_switches.tsv: DM_NVN_SUB_DRAIN (A-2's switch to restore the old wait at each submission's end) gets its class and meaning. The docs quoted three names that no code reads. Each mention now drops its backticks and says the switch is not on main: - DM_NVN_VC_WAYS: another fork's switch, in memory.md. - DM_NVN_CUT_FIRST: a probe switch since removed, in Breath of the Wild's note. - DM_NVN_RT_RECLAIM: PR seedds#1588's switch, in rendering.md, and so in the symptom index too (docs_symptoms.py).
What and why
Refs #1587, which has the design and the data.
Symptom. Ocarina of Time's Kokiri Forest load adds 3.2 GB of render targets to the residency set in about 30 s, and on a 16 GB Mac swap follows wired memory.
Cause. The game keeps its render targets as texture objects over one 305 MB virtual pool and reuses the storage by overwrite: 781 live targets over 74 MB of storage. Each one keeps a private Metal texture, and
treg_texmakes one for every registered ID, dead or not.Change (
DM_NVN_RT_RECLAIM=1, off by default;nvn_mtl.m,rc_*):Reference entry: Render targets dead by overwrite. The game note has a line for it, and
env_switches.tsvlists the switch.What it builds on:
heap_aliasin Standalone Metal probes: heap aliasing, render-target sizes, mesh index ranges, mesh pipeline archive, mesh/task sweep, task-shader hang #1583: same width, height, bpp and compression, or the bytes are scrambled.rt_sizes(Standalone Metal probes: heap aliasing, render-target sizes, mesh index ranges, mesh pipeline archive, mesh/task sweep, task-shader hang #1583) is why a size-class key is left as the open follow-up.Checked
make -C dm/host GAME=0400486024DEC000;make -C dm/host/tests GAME=0400486024DEC000 -j run;check_t_alias_takeover(28, 0 failed) andcheck_t_image_alias(329, 0 failed);test_docs.py,env_switches.py --check,docs_symptoms.py.scripts/regress.py checkwasn't run: only Ocarina of Time is available here. The switch is off by default, so other games run as before.t_nvnandt_gfx_suspenddon't link on main b2d46b8 (_dm_vi_frame_queued), independent of this change.Docs
docs/reference/rendering.mdand the game note, andpython3 dm/tools/docs_symptoms.pywas run