Repository navigation
A GPU-only pool's render targets share one placement heap: Ocarina of Time's 3 GB of render targets fit in 0.7 GB - #1453
PromethOps wants to merge 3 commits into
Conversation
… one placement heap (Ocarina of Time's 3 GB of render targets fit in 0.7 GB) Resolved: the residency report keeps NXForge's distinct-pages and by-format lines and adds seedds#1453's pool heap line; docs/env_switches.tsv has DM_NVN_POOL_HEAP beside DM_NVN_POOL_SKIP_RETIRED. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
… Time's 3 GB of render targets fit in 0.7 GB, as in the game's pools Engines place their transient render targets over each other in a CPU_NO_ACCESS pool, as a frame graph does; the host gave each (pool, offset, description) a private Metal texture. pool_heap_texture gives each such pool (up to 1 GB) one placement heap and places each COMPRESSIBLE texture at its pool offset scaled by 1.25: ranges apart in the pool stay apart, ranges that overlap may overlap, as the memory is one on NVN. Memory no texture has covered takes the texture's initial content; over covered memory a new texture inherits what is there and nothing is written. DM_NVN_POOL_HEAP=0 turns it off. restop prints the heaps. Ocarina of Time (Switch 2, 24 GB M5 Pro): 3,001 MB of render targets in 804 MB of CPU_NO_ACCESS pools become 62 heaps of 671 MB; residency set 8-10 GB -> 6-7 GB, inside a 16 GB Mac's working set. Tests: t_pool_heap (new: 19 checks, both encoding modes, and the switch off), t_image_alias (318). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3d700bb to
91904d4
Compare
|
Applied this on today's main (671c68e) for an Ocarina of Time rebase; the seven files apply as they are. One interaction with main's image-family heaps showed up in a unit test, so passing it on with the fix I used:
@interface NvImageHeap : NSObject { @public id<MTLHeap> heap; int pool_heap; } @end
…
NvImageHeap *o = objc_getAssociatedObject(other, &g_image_heap_owner_key);
// Once this storage is placed in the pool heap, later descriptions must keep its scaled offset.
if (o && o->pool_heap) { pthread_mutex_unlock(&g_smu); return nil; }
…
if (heap) { h = [NvPoolHeap new]; h->heap = heap; h->pool_heap = 1; …The case in |
…ort) over seedds#1453 (GPU-only pool render targets in one placement heap): both kept Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013SzykpcKm2eZPvPjGPKx1C
… of uploaded textures) over seedds#1453 and seedds#1455 pool_heap_texture's fill takes seedds#1455's again argument (0: the placement is new). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013SzykpcKm2eZPvPjGPKx1C
…red pool buffers no longer pile up) over seedds#1447, seedds#1453 and seedds#1455 alias_let_go also clears seedds#1455's lookup stamps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013SzykpcKm2eZPvPjGPKx1C
|
Tested on main 75c1a8c, with #1455, #1332 and #1446 also applied, on an M4 MacBook with 24 GB, from a Kokiri Forest save at Link's house (handheld, upscale 2.0), in two scripted runs: 10 minutes in the house; and from a fresh load, out of the door, Saria's greeting and 10 minutes of walking around the house.
|
) 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
…ily's heap: a later description over its storage keeps its scaled offset image_texture took any owner whose heap was big enough as the family heap; a pool heap (NvPoolHeap) is the whole pool, so a later COMPRESSIBLE|IMAGE description over storage placed there landed at heap offset 0, over what the pool placed there. NvImageHeap's pool_heap flag, set on a pool heap, makes image_texture leave that storage to pool_heap_texture. Found and fixed by jonpage0 in seedds#1453's review; t_pool_heap gets their case (a 512x512 image at 24 MB, then a 256x256 description over it): 24 checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thank you both. @jonpage0: your @PucaVaz: thanks for the runs. On today's main your residency set stays around 5 GB, and only 3 render targets were placed in 480 MB of pool heaps, so the gain I measured is not there in your scene. My numbers (3,001 MB of The failing checks (host unit tests, tool tests, sanitizers, clang-tidy) fail the same way on main 75c1a8c; the build check passes. |
…atedSize currentAllocatedSize counts each no-copy buffer in full however many cover the same pages: 50 over one 64 MB region add 3,200 MB. In Ocarina of Time's Kokiri Forest on a 24 GB Mac it read 17.3 GB within a minute or a few, with 6.5 GB in the residency set and the driver's In use system memory near 7.7 GB, so the guard stopped the game each time. With its stop raised past Metal's working set, the refused commit was followed by InnocentVictim command buffer errors. The guard now reads the set's own size, so an addition counts from its commit. A large burst is seen one commit later than before, and the timer sees the size at the last commit. Checked: t_res_guard, both runs. Ocarina of Time v131072 on an M6 Mac with 24 GB, on main with seedds#1357, seedds#1390, seedds#1391, seedds#1453 and seedds#1455: about 49 minutes and 170,400 frames through Kokiri Forest and the Great Deku Tree, currentAllocatedSize at about 20 GB, driver in use at most 9.5 GB, wired about 10 GB, no GPU errors. Not checked: other games, a smaller Mac.
…ily's heap: a later description over its storage keeps its scaled offset image_texture took any owner whose heap was big enough as the family heap; a pool heap (NvPoolHeap) is the whole pool, so a later COMPRESSIBLE|IMAGE description over storage placed there landed at heap offset 0, over what the pool placed there. NvImageHeap's pool_heap flag, set on a pool heap, makes image_texture leave that storage to pool_heap_texture. Found and fixed by jonpage0 in seedds#1453's review; t_pool_heap gets their case (a 512x512 image at 24 MB, then a 256x256 description over it): 24 checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 85be790)
… only COMPRESSIBLE|IMAGE: Ocarina of Time's dynamic-resolution depth buffer (COMPRESSIBLE alone, in a virtual pool) kept 15-16 live sizes at one offset as separate 14.5 MB Metal textures - Symptom: in Kokiri Forest on an M3 Pro (18 GB), 15-16 Depth32Float textures of 1,882-1,920 pixels described one pool offset, each with an allocation of its own. - Cause: image_shape required the flags to be exactly COMPRESSIBLE|IMAGE. A depth target carries COMPRESSIBLE alone, so its resolution family never formed. - Fix: image_shape takes any COMPRESSIBLE image without DISPLAY, VIDEO_DECODE, LINEAR, SPARSE or LINEAR_RENDER_TARGET; the other conditions (GPU-only mapped storage, 2D, one level, one sample, same pool, offset and format) are unchanged. - DM_CTL restop also prints a memory census (mem_census): textures by Metal format; which ones live descriptors, only retired ones, only the deferred-release queue or nothing names; storage addresses held as several Metal textures, with their texture and pool flags; memory pools by whether texture uploads or other uses looked them up. It is what found these textures. - Measured, before main's Ocarina fixes: Depth32Float 95 textures (458 MB) to 60 (191 MB) plus 21 family heaps (82 MB); GPU allocation 7,710 to 7,520 MB. On main at 2eb7182 the 17 sizes still form and share one heap, all Depth32Float textures 225 MB. - PR seedds#1453's per-pool placement heaps leave virtual pools out, so they do not take this buffer. - Checked: check_t_image_alias (332 checks, direct and deferred encoding) with a new depth family case (Depth32F, COMPRESSIBLE alone) and a linear-exclusion case; the rest of make -C dm/gfx/tests run as on main; Ocarina of Time played to Kokiri Forest with a correct picture, with this and the end-barrier change together on main at 2eb7182 (not rerun on this base, b2d46b8). rendering.md entry and the game's note updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ily's heap: a later description over its storage keeps its scaled offset image_texture took any owner whose heap was big enough as the family heap; a pool heap (NvPoolHeap) is the whole pool, so a later COMPRESSIBLE|IMAGE description over storage placed there landed at heap offset 0, over what the pool placed there. NvImageHeap's pool_heap flag, set on a pool heap, makes image_texture leave that storage to pool_heap_texture. Found and fixed by jonpage0 in seedds#1453's review; t_pool_heap gets their case (a 512x512 image at 24 MB, then a 256x256 description over it): 24 checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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. On this branch: a render target placed in its GPU-only pool's heap (pool_heap_texture, PR seedds#1453) is excluded from the reclaim, since the free list would hand a texture placed at one pool offset to a texture at another. (cherry picked from commit 4b231ac) (cherry picked from commit 3cde45e)
…rina of Time's depth family heap had turned draw_barriers game off for the whole run (saved 0) Symptom: Ocarina of Time on an M1 Pro (16 GB), Kokiri Forest, GPU-bound at about 39 ms a frame. Every residency-set line of the run read "draw-pass entry barriers full 30,271-33,563 / saved 0": about 200 full barriers a frame, although the profile sets draw_barriers game (the game's note: GPU 25 -> 18 ms with it). Cause: enc_r_begin_pass and enc_r_end_pass skipped the mode whenever g_image_heap_n was non-zero. Since main's resolution families take every GPU-written compressible 2D image (863bc6b), the game's dynamic-resolution depth buffer gets a family heap at load, and the GPU-only pool heaps (PR seedds#1453) add three more: the mode was off from then on. Fix: the relaxed end barrier already carries resource_vis(), the resource-alias flush when heaps exist, so a pass's writes are flushed for aliases before the next pass's fragment, compute and copy work, as the full start barrier flushed them. The mode now stays on with heaps; a game barrier other than fragment ordering still gives the full barrier. DM_NVN_BARRIER_HEAP=0 restores the old gate. Not run in play yet; the host was not compiled here (Linux). env_switches row, frame-rate.md entry and the game's note updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Aax1HnchwCweUDRNWjhHx9 (cherry picked from commit 43e0a46)
…ily's heap: a later description over its storage keeps its scaled offset image_texture took any owner whose heap was big enough as the family heap; a pool heap (NvPoolHeap) is the whole pool, so a later COMPRESSIBLE|IMAGE description over storage placed there landed at heap offset 0, over what the pool placed there. NvImageHeap's pool_heap flag, set on a pool heap, makes image_texture leave that storage to pool_heap_texture. Found and fixed by jonpage0 in seedds#1453's review; t_pool_heap gets their case (a 512x512 image at 24 MB, then a 256x256 description over it): 24 checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 0848ba4) (cherry picked from commit 2c3d874)
…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. On this branch: a render target placed in its GPU-only pool's heap (pool_heap_texture, PR seedds#1453) is excluded from the reclaim, since the free list would hand a texture placed at one pool offset to a texture at another. (cherry picked from commit 4b231ac) (cherry picked from commit 3cde45e) (cherry picked from commit 9a04e57)
…rina of Time's depth family heap had turned draw_barriers game off for the whole run (saved 0) Symptom: Ocarina of Time on an M1 Pro (16 GB), Kokiri Forest, GPU-bound at about 39 ms a frame. Every residency-set line of the run read "draw-pass entry barriers full 30,271-33,563 / saved 0": about 200 full barriers a frame, although the profile sets draw_barriers game (the game's note: GPU 25 -> 18 ms with it). Cause: enc_r_begin_pass and enc_r_end_pass skipped the mode whenever g_image_heap_n was non-zero. Since main's resolution families take every GPU-written compressible 2D image (863bc6b), the game's dynamic-resolution depth buffer gets a family heap at load, and the GPU-only pool heaps (PR seedds#1453) add three more: the mode was off from then on. Fix: the relaxed end barrier already carries resource_vis(), the resource-alias flush when heaps exist, so a pass's writes are flushed for aliases before the next pass's fragment, compute and copy work, as the full start barrier flushed them. The mode now stays on with heaps; a game barrier other than fragment ordering still gives the full barrier. DM_NVN_BARRIER_HEAP=0 restores the old gate. Not run in play yet; the host was not compiled here (Linux). env_switches row, frame-rate.md entry and the game's note updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Aax1HnchwCweUDRNWjhHx9 (cherry picked from commit 43e0a46) (cherry picked from commit 0a8f9e2)
…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, @PromethOps, and everyone who measured it. The analysis here is what settled the design that landed for #1831. Your finding explains why mirroring the pool can't work for Ocarina's 305 MB pool: the game keeps live targets in the part of the full-size scene target that its current resolution leaves unused, which only works with NVN's own memory layout. The virtual pool is exactly where the churn is: since 04419bc the resolution moves, and on an M6 that pool made 3,468 new textures in about 1,000 village frames. Without that pool, this PR's gain on main was about 1% (yours, PucaVaz's and enjoyerofslop's runs). What landed instead, in 7f50222, keeps each texture's own layout and recycles only memory the host has already let go. Each render target in a GPU-only pool gets a placement heap of its own. When the texture is freed, the heap stays resident for the next target of about the same size. With the switch alternating every 300 frames on an M6, the target site added 455-521 MB to the residency set per 300 frames off and 27-37 MB on. That also covers the non-virtual GPU-only pools this PR placed, so the two would compete for the same textures. The live targets' steady total (about 2 GB in the village) stays open in the game note. Your numbers are linked from the reference entry ("Render targets that move with the dynamic resolution get new Metal memory each time" in docs/reference/rendering.md). |
…ropped), the real-time policy re-check, the added mesh row Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…armed and archived, render-target recycling in place of PR seedds#1453), and the frame thread runs user-interactive; this merge keeps the 1.3 engine's history Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e one placement heap (Ocarina of Time's 3 GB of render targets fit in 0.7 GB; DM_NVN_POOL_HEAP=0 turns it off)
…share bytes in a placement heap per 32 MB span, as on NVN: Ocarina of Time no longer runs out of GPU memory going outdoors on an M1 iPad With the render targets' recycled heaps (rh_texture, 7f50222) every live description in a GPU-only pool is a Metal texture of its own. On the M1 iPad (8 GB, recommended working set 5,461 MB) that reached 5,184 MB resident at the outdoor save load (544 heaps of 1,384 MB over 1,455 MB of textures), and every command buffer then failed with kIOGPUCommandBufferCallbackErrorOutOfMemory. window_texture (nvn_mtl.m): a texture whose storage lies in a CPU_NO_ACCESS pool (not sparse, not linear, one sample) is placed in a heap standing for a 32 MB span of guest addresses, at its NVN offset scaled by 9/8 and rounded to Metal's alignment, if its Metal size ends within its scaled NVN range, so textures overlap here where they overlap on NVN. A span's heap is made only once it saves memory; a texture across a span's end tries the half-offset spans; covered bytes are inherited, new ones zero-filled. Placed textures have no GPU-optimized contents: with lossless compression, another format's writes over the same bytes hung the iPad's GPU (kIOGPUCommandBufferCallbackErrorHang by Link's house). Anything that does not fit takes the usual path (image_texture, then rh_texture). On only where recommendedMaxWorkingSetSize is under 8 GB (window_wants): sharing a target pool's bytes corrupted the picture on the Mac in PR seedds#1453 (the game keeps live targets in the part of the full-size scene target its current resolution leaves unused, which only NVN's layout makes safe), so larger devices keep each target's own layout. 8 GB devices have 5.3-5.5 GB of working set, a 16 GB M4 Mac 12,124 MB; the separate heaps needed 5.2 GB at the outdoor save load, sharing 4.3 GB. DM_NVN_WINDOW_ALIAS=0/1 forces it, and a log line says which way it went. The gfx test Makefile runs every test with it off unless the test turns it on, so an 8 GB Mac runs them as a 16 GB one does. Also for reading the set (res_top: the first GPU out-of-memory error, DM_CTL restop, and once when the set passes three quarters of the working set): the set by the source line that added each object, the resident pools by raw table / binding, by who gave an address in them (hle_nvn.c passes the kind: mtl_pool_addressed_by) and by pool flags (mtl_pool_tag), the GPU-made textures by pool, and the spans' counts. Tests: t_window_alias (new; 25 checks with the spans on, direct and deferred encoding, Metal validation; 8 fail with them off, the separate allocations), including the 8 GB rule. t_image_alias, t_rt_recycle, t_alias_takeover, t_upgrade_alias, t_copy_alias, t_rt_lifetime, t_pool_chunks, t_addr_known, t_file_wire pass. memory.md "Textures in GPU-only memory took an allocation per description"; rendering.md's recycled-heap entry links it; env_switches.tsv.
… only COMPRESSIBLE|IMAGE: Ocarina of Time's dynamic-resolution depth buffer (COMPRESSIBLE alone, in a virtual pool) kept 15-16 live sizes at one offset as separate 14.5 MB Metal textures (PR seedds#1557) - Symptom: in Kokiri Forest on an M3 Pro (18 GB), 15-16 Depth32Float textures of 1,882-1,920 pixels described one pool offset, each with an allocation of its own. - Cause: image_shape required the flags to be exactly COMPRESSIBLE|IMAGE. A depth target carries COMPRESSIBLE alone, so its resolution family never formed. - Fix: image_shape takes any COMPRESSIBLE image without DISPLAY, VIDEO_DECODE, LINEAR, SPARSE or LINEAR_RENDER_TARGET; the other conditions (GPU-only mapped storage, 2D, one level, one sample, same pool, offset and format) are unchanged. - DM_CTL restop also prints a memory census (mem_census): textures by Metal format; which ones live descriptors, only retired ones, only the deferred-release queue or nothing names; storage addresses held as several Metal textures, with their texture and pool flags; memory pools by whether texture uploads or other uses looked them up. It is what found these textures. - Measured, before main's Ocarina fixes: Depth32Float 95 textures (458 MB) to 60 (191 MB) plus 21 family heaps (82 MB); GPU allocation 7,710 to 7,520 MB. On main at 077fb5c the 17 sizes still form and share one heap, all Depth32Float textures 225 MB. - PR seedds#1453's per-pool placement heaps leave virtual pools out, so they do not take this buffer. - Checked: check_t_image_alias (332 checks, direct and deferred encoding) with a new depth family case (Depth32F, COMPRESSIBLE alone) and a linear-exclusion case; the rest of make -C dm/gfx/tests run as on main; Ocarina of Time played to Kokiri Forest with a correct picture, with this and the end-barrier change together on main at 077fb5c (not rerun on this base, 99edc18). rendering.md entry and the game's note updated. - Landed on 622391d with seedds#1556 and seedds#1557 together: the host builds; t_copy_alias, t_image_alias, t_pool_chunks, t_alias_takeover and t_rt_range pass. No game was run and scripts/regress.py was skipped. Co-authored-by: James <12531452+capibawa@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.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.
…share bytes in a placement heap per 32 MB span, as on NVN: Ocarina of Time no longer runs out of GPU memory going outdoors on an M1 iPad With the render targets' recycled heaps (rh_texture, 7f50222) every live description in a GPU-only pool is a Metal texture of its own. On the M1 iPad (8 GB, recommended working set 5,461 MB) that reached 5,184 MB resident at the outdoor save load (544 heaps of 1,384 MB over 1,455 MB of textures), and every command buffer then failed with kIOGPUCommandBufferCallbackErrorOutOfMemory. window_texture (nvn_mtl.m): a texture whose storage lies in a CPU_NO_ACCESS pool (not sparse, not linear, one sample) is placed in a heap standing for a 32 MB span of guest addresses, at its NVN offset scaled by 9/8 and rounded to Metal's alignment, if its Metal size ends within its scaled NVN range, so textures overlap here where they overlap on NVN. A span's heap is made only once it saves memory; a texture across a span's end tries the half-offset spans; covered bytes are inherited, new ones zero-filled. Placed textures have no GPU-optimized contents: with lossless compression, another format's writes over the same bytes hung the iPad's GPU (kIOGPUCommandBufferCallbackErrorHang by Link's house). Anything that does not fit takes the usual path (image_texture, then rh_texture). On only where recommendedMaxWorkingSetSize is under 8 GB (window_wants): sharing a target pool's bytes corrupted the picture on the Mac in PR seedds#1453 (the game keeps live targets in the part of the full-size scene target its current resolution leaves unused, which only NVN's layout makes safe), so larger devices keep each target's own layout. 8 GB devices have 5.3-5.5 GB of working set, a 16 GB M4 Mac 12,124 MB; the separate heaps needed 5.2 GB at the outdoor save load, sharing 4.3 GB. DM_NVN_WINDOW_ALIAS=0/1 forces it, and a log line says which way it went. The gfx test Makefile runs every test with it off unless the test turns it on, so an 8 GB Mac runs them as a 16 GB one does. Also for reading the set (res_top: the first GPU out-of-memory error, DM_CTL restop, and once when the set passes three quarters of the working set): the set by the source line that added each object, the resident pools by raw table / binding, by who gave an address in them (hle_nvn.c passes the kind: mtl_pool_addressed_by) and by pool flags (mtl_pool_tag), the GPU-made textures by pool, and the spans' counts. Tests: t_window_alias (new; 25 checks with the spans on, direct and deferred encoding, Metal validation; 8 fail with them off, the separate allocations), including the 8 GB rule. t_image_alias, t_rt_recycle, t_alias_takeover, t_upgrade_alias, t_copy_alias, t_rt_lifetime, t_pool_chunks, t_addr_known, t_file_wire pass. memory.md "Textures in GPU-only memory took an allocation per description"; rendering.md's recycled-heap entry links it; env_switches.tsv. (cherry picked from commit b914817)
…nt heap: Ocarina of Time's 3 GB of render targets fit in 0.7 GB 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#1453 by PromethOps (closed): A GPU-only pool's render targets share one placement heap: Ocarina of Time's 3 GB of render targets fit in 0.7 GB
…he dynamic-resolution climb, frag_z_snap, mul_extended; main's rh_texture and frag_z_snap replace seedds#1453's pool heaps and seedds#1742's frag_z; our seedds#1731 hints, compaction, cond_conservative and fixed resolution kept) 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#1453 by PromethOps (closed): A GPU-only pool's render targets share one placement heap: Ocarina of Time's 3 GB of render targets fit in 0.7 GB - seedds#1731 by ksolenk38ze (open): nvn: preserve texture upgrades, BC render targets, and retired descriptor bounds - seedds#1742 by cocoman93 (open): position.z one ULP down against a surface's own stored depth: fixes Ocarina of Time mist specks
…heaps: prova 19 without them held 15.4 GB resident on a 24 GB Mac (prova 18: 10.9 GB), shared textures 4,400 against 1,150, and stuttered from frame 4000 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#1453 by PromethOps (closed): A GPU-only pool's render targets share one placement heap: Ocarina of Time's 3 GB of render targets fit in 0.7 GB
…share bytes in a placement heap per 32 MB span, as on NVN: Ocarina of Time no longer runs out of GPU memory going outdoors on an M1 iPad With the render targets' recycled heaps (rh_texture, 7f50222) every live description in a GPU-only pool is a Metal texture of its own. On the M1 iPad (8 GB, recommended working set 5,461 MB) that reached 5,184 MB resident at the outdoor save load (544 heaps of 1,384 MB over 1,455 MB of textures), and every command buffer then failed with kIOGPUCommandBufferCallbackErrorOutOfMemory. window_texture (nvn_mtl.m): a texture whose storage lies in a CPU_NO_ACCESS pool (not sparse, not linear, one sample) is placed in a heap standing for a 32 MB span of guest addresses, at its NVN offset scaled by 9/8 and rounded to Metal's alignment, if its Metal size ends within its scaled NVN range, so textures overlap here where they overlap on NVN. A span's heap is made only once it saves memory; a texture across a span's end tries the half-offset spans; covered bytes are inherited, new ones zero-filled. Placed textures have no GPU-optimized contents: with lossless compression, another format's writes over the same bytes hung the iPad's GPU (kIOGPUCommandBufferCallbackErrorHang by Link's house). Anything that does not fit takes the usual path (image_texture, then rh_texture). On only where recommendedMaxWorkingSetSize is under 8 GB (window_wants): sharing a target pool's bytes corrupted the picture on the Mac in PR seedds#1453 (the game keeps live targets in the part of the full-size scene target its current resolution leaves unused, which only NVN's layout makes safe), so larger devices keep each target's own layout. 8 GB devices have 5.3-5.5 GB of working set, a 16 GB M4 Mac 12,124 MB; the separate heaps needed 5.2 GB at the outdoor save load, sharing 4.3 GB. DM_NVN_WINDOW_ALIAS=0/1 forces it, and a log line says which way it went. The gfx test Makefile runs every test with it off unless the test turns it on, so an 8 GB Mac runs them as a 16 GB one does. Also for reading the set (res_top: the first GPU out-of-memory error, DM_CTL restop, and once when the set passes three quarters of the working set): the set by the source line that added each object, the resident pools by raw table / binding, by who gave an address in them (hle_nvn.c passes the kind: mtl_pool_addressed_by) and by pool flags (mtl_pool_tag), the GPU-made textures by pool, and the spans' counts. Tests: t_window_alias (new; 25 checks with the spans on, direct and deferred encoding, Metal validation; 8 fail with them off, the separate allocations), including the 8 GB rule. t_image_alias, t_rt_recycle, t_alias_takeover, t_upgrade_alias, t_copy_alias, t_rt_lifetime, t_pool_chunks, t_addr_known, t_file_wire pass. memory.md "Textures in GPU-only memory took an allocation per description"; rendering.md's recycled-heap entry links it; env_switches.tsv.
…share bytes in a placement heap per 32 MB span, as on NVN: Ocarina of Time no longer runs out of GPU memory going outdoors on an M1 iPad With the render targets' recycled heaps (rh_texture, 7f50222) every live description in a GPU-only pool is a Metal texture of its own. On the M1 iPad (8 GB, recommended working set 5,461 MB) that reached 5,184 MB resident at the outdoor save load (544 heaps of 1,384 MB over 1,455 MB of textures), and every command buffer then failed with kIOGPUCommandBufferCallbackErrorOutOfMemory. window_texture (nvn_mtl.m): a texture whose storage lies in a CPU_NO_ACCESS pool (not sparse, not linear, one sample) is placed in a heap standing for a 32 MB span of guest addresses, at its NVN offset scaled by 9/8 and rounded to Metal's alignment, if its Metal size ends within its scaled NVN range, so textures overlap here where they overlap on NVN. A span's heap is made only once it saves memory; a texture across a span's end tries the half-offset spans; covered bytes are inherited, new ones zero-filled. Placed textures have no GPU-optimized contents: with lossless compression, another format's writes over the same bytes hung the iPad's GPU (kIOGPUCommandBufferCallbackErrorHang by Link's house). Anything that does not fit takes the usual path (image_texture, then rh_texture). On only where recommendedMaxWorkingSetSize is under 8 GB (window_wants): sharing a target pool's bytes corrupted the picture on the Mac in PR seedds#1453 (the game keeps live targets in the part of the full-size scene target its current resolution leaves unused, which only NVN's layout makes safe), so larger devices keep each target's own layout. 8 GB devices have 5.3-5.5 GB of working set, a 16 GB M4 Mac 12,124 MB; the separate heaps needed 5.2 GB at the outdoor save load, sharing 4.3 GB. DM_NVN_WINDOW_ALIAS=0/1 forces it, and a log line says which way it went. The gfx test Makefile runs every test with it off unless the test turns it on, so an 8 GB Mac runs them as a 16 GB one does. Also for reading the set (res_top: the first GPU out-of-memory error, DM_CTL restop, and once when the set passes three quarters of the working set): the set by the source line that added each object, the resident pools by raw table / binding, by who gave an address in them (hle_nvn.c passes the kind: mtl_pool_addressed_by) and by pool flags (mtl_pool_tag), the GPU-made textures by pool, and the spans' counts. Tests: t_window_alias (new; 25 checks with the spans on, direct and deferred encoding, Metal validation; 8 fail with them off, the separate allocations), including the 8 GB rule. t_image_alias, t_rt_recycle, t_alias_takeover, t_upgrade_alias, t_copy_alias, t_rt_lifetime, t_pool_chunks, t_addr_known, t_file_wire pass. memory.md "Textures in GPU-only memory took an allocation per description"; rendering.md's recycled-heap entry links it; env_switches.tsv.
…share bytes in a placement heap per 32 MB span, as on NVN: Ocarina of Time no longer runs out of GPU memory going outdoors on an M1 iPad With the render targets' recycled heaps (rh_texture, 7f50222) every live description in a GPU-only pool is a Metal texture of its own. On the M1 iPad (8 GB, recommended working set 5,461 MB) that reached 5,184 MB resident at the outdoor save load (544 heaps of 1,384 MB over 1,455 MB of textures), and every command buffer then failed with kIOGPUCommandBufferCallbackErrorOutOfMemory. window_texture (nvn_mtl.m): a texture whose storage lies in a CPU_NO_ACCESS pool (not sparse, not linear, one sample) is placed in a heap standing for a 32 MB span of guest addresses, at its NVN offset scaled by 9/8 and rounded to Metal's alignment, if its Metal size ends within its scaled NVN range, so textures overlap here where they overlap on NVN. A span's heap is made only once it saves memory; a texture across a span's end tries the half-offset spans; covered bytes are inherited, new ones zero-filled. Placed textures have no GPU-optimized contents: with lossless compression, another format's writes over the same bytes hung the iPad's GPU (kIOGPUCommandBufferCallbackErrorHang by Link's house). Anything that does not fit takes the usual path (image_texture, then rh_texture). On only where recommendedMaxWorkingSetSize is under 8 GB (window_wants): sharing a target pool's bytes corrupted the picture on the Mac in PR seedds#1453 (the game keeps live targets in the part of the full-size scene target its current resolution leaves unused, which only NVN's layout makes safe), so larger devices keep each target's own layout. 8 GB devices have 5.3-5.5 GB of working set, a 16 GB M4 Mac 12,124 MB; the separate heaps needed 5.2 GB at the outdoor save load, sharing 4.3 GB. DM_NVN_WINDOW_ALIAS=0/1 forces it, and a log line says which way it went. The gfx test Makefile runs every test with it off unless the test turns it on, so an 8 GB Mac runs them as a 16 GB one does. Also for reading the set (res_top: the first GPU out-of-memory error, DM_CTL restop, and once when the set passes three quarters of the working set): the set by the source line that added each object, the resident pools by raw table / binding, by who gave an address in them (hle_nvn.c passes the kind: mtl_pool_addressed_by) and by pool flags (mtl_pool_tag), the GPU-made textures by pool, and the spans' counts. Tests: t_window_alias (new; 25 checks with the spans on, direct and deferred encoding, Metal validation; 8 fail with them off, the separate allocations), including the 8 GB rule. t_image_alias, t_rt_recycle, t_alias_takeover, t_upgrade_alias, t_copy_alias, t_rt_lifetime, t_pool_chunks, t_addr_known, t_file_wire pass. memory.md "Textures in GPU-only memory took an allocation per description"; rendering.md's recycled-heap entry links it; env_switches.tsv.
…share bytes in a placement heap per 32 MB span, as on NVN: Ocarina of Time no longer runs out of GPU memory going outdoors on an M1 iPad With the render targets' recycled heaps (rh_texture, 7f50222) every live description in a GPU-only pool is a Metal texture of its own. On the M1 iPad (8 GB, recommended working set 5,461 MB) that reached 5,184 MB resident at the outdoor save load (544 heaps of 1,384 MB over 1,455 MB of textures), and every command buffer then failed with kIOGPUCommandBufferCallbackErrorOutOfMemory. window_texture (nvn_mtl.m): a texture whose storage lies in a CPU_NO_ACCESS pool (not sparse, not linear, one sample) is placed in a heap standing for a 32 MB span of guest addresses, at its NVN offset scaled by 9/8 and rounded to Metal's alignment, if its Metal size ends within its scaled NVN range, so textures overlap here where they overlap on NVN. A span's heap is made only once it saves memory; a texture across a span's end tries the half-offset spans; covered bytes are inherited, new ones zero-filled. Placed textures have no GPU-optimized contents: with lossless compression, another format's writes over the same bytes hung the iPad's GPU (kIOGPUCommandBufferCallbackErrorHang by Link's house). Anything that does not fit takes the usual path (image_texture, then rh_texture). On only where recommendedMaxWorkingSetSize is under 8 GB (window_wants): sharing a target pool's bytes corrupted the picture on the Mac in PR seedds#1453 (the game keeps live targets in the part of the full-size scene target its current resolution leaves unused, which only NVN's layout makes safe), so larger devices keep each target's own layout. 8 GB devices have 5.3-5.5 GB of working set, a 16 GB M4 Mac 12,124 MB; the separate heaps needed 5.2 GB at the outdoor save load, sharing 4.3 GB. DM_NVN_WINDOW_ALIAS=0/1 forces it, and a log line says which way it went. The gfx test Makefile runs every test with it off unless the test turns it on, so an 8 GB Mac runs them as a 16 GB one does. Also for reading the set (res_top: the first GPU out-of-memory error, DM_CTL restop, and once when the set passes three quarters of the working set): the set by the source line that added each object, the resident pools by raw table / binding, by who gave an address in them (hle_nvn.c passes the kind: mtl_pool_addressed_by) and by pool flags (mtl_pool_tag), the GPU-made textures by pool, and the spans' counts. Tests: t_window_alias (new; 25 checks with the spans on, direct and deferred encoding, Metal validation; 8 fail with them off, the separate allocations), including the 8 GB rule. t_image_alias, t_rt_recycle, t_alias_takeover, t_upgrade_alias, t_copy_alias, t_rt_lifetime, t_pool_chunks, t_addr_known, t_file_wire pass. memory.md "Textures in GPU-only memory took an allocation per description"; rendering.md's recycled-heap entry links it; env_switches.tsv. Rebased onto main's pool and render-target structure: the virtual pool's runs are tagged after main's one-step mtl_pool_replace (vrun_set); the new pools' tag is set after pool_new_put; the storage range for res_top is kept on the texture before own_alias_publish (nvn_owner.m); a span's heap is an NvImageHeap with window set, which main's family-heap search (own_alias_at_locked) skips and which is freed with its last texture rather than waiting in rh_wait's free list. rendering.md keeps main's Fix and Check wording and adds the window heap.
…share bytes in a placement heap per 32 MB span, as on NVN: Ocarina of Time no longer runs out of GPU memory going outdoors on an M1 iPad With the render targets' recycled heaps (rh_texture, 7f50222) every live description in a GPU-only pool is a Metal texture of its own. On the M1 iPad (8 GB, recommended working set 5,461 MB) that reached 5,184 MB resident at the outdoor save load (544 heaps of 1,384 MB over 1,455 MB of textures), and every command buffer then failed with kIOGPUCommandBufferCallbackErrorOutOfMemory. window_texture (nvn_mtl.m): a texture whose storage lies in a CPU_NO_ACCESS pool (not sparse, not linear, one sample) is placed in a heap standing for a 32 MB span of guest addresses, at its NVN offset scaled by 9/8 and rounded to Metal's alignment, if its Metal size ends within its scaled NVN range, so textures overlap here where they overlap on NVN. A span's heap is made only once it saves memory; a texture across a span's end tries the half-offset spans; covered bytes are inherited, new ones zero-filled. Placed textures have no GPU-optimized contents: with lossless compression, another format's writes over the same bytes hung the iPad's GPU (kIOGPUCommandBufferCallbackErrorHang by Link's house). Anything that does not fit takes the usual path (image_texture, then rh_texture). On only where recommendedMaxWorkingSetSize is under 8 GB (window_wants): sharing a target pool's bytes corrupted the picture on the Mac in PR seedds#1453 (the game keeps live targets in the part of the full-size scene target its current resolution leaves unused, which only NVN's layout makes safe), so larger devices keep each target's own layout. 8 GB devices have 5.3-5.5 GB of working set, a 16 GB M4 Mac 12,124 MB; the separate heaps needed 5.2 GB at the outdoor save load, sharing 4.3 GB. DM_NVN_WINDOW_ALIAS=0/1 forces it, and a log line says which way it went. The gfx test Makefile runs every test with it off unless the test turns it on, so an 8 GB Mac runs them as a 16 GB one does. Also for reading the set (res_top: the first GPU out-of-memory error, DM_CTL restop, and once when the set passes three quarters of the working set): the set by the source line that added each object, the resident pools by raw table / binding, by who gave an address in them (hle_nvn.c passes the kind: mtl_pool_addressed_by) and by pool flags (mtl_pool_tag), the GPU-made textures by pool, and the spans' counts. Tests: t_window_alias (new; 25 checks with the spans on, direct and deferred encoding, Metal validation; 8 fail with them off, the separate allocations), including the 8 GB rule. t_image_alias, t_rt_recycle, t_alias_takeover, t_upgrade_alias, t_copy_alias, t_rt_lifetime, t_pool_chunks, t_addr_known, t_file_wire pass. memory.md "Textures in GPU-only memory took an allocation per description"; rendering.md's recycled-heap entry links it; env_switches.tsv. Rebased onto main's pool and render-target structure: the virtual pool's runs are tagged after main's one-step mtl_pool_replace (vrun_set); the new pools' tag is set after pool_new_put; the storage range for res_top is kept on the texture before own_alias_publish (nvn_owner.m); a span's heap is an NvImageHeap with window set, which main's family-heap search (own_alias_at_locked) skips and which is freed with its last texture rather than waiting in rh_wait's free list. rendering.md keeps main's Fix and Check wording and adds the window heap.
…share bytes in a placement heap per 32 MB span, as on NVN: Ocarina of Time no longer runs out of GPU memory going outdoors on an M1 iPad With the render targets' recycled heaps (rh_texture, 7f50222) every live description in a GPU-only pool is a Metal texture of its own. On the M1 iPad (8 GB, recommended working set 5,461 MB) that reached 5,184 MB resident at the outdoor save load (544 heaps of 1,384 MB over 1,455 MB of textures), and every command buffer then failed with kIOGPUCommandBufferCallbackErrorOutOfMemory. window_texture (nvn_mtl.m): a texture whose storage lies in a CPU_NO_ACCESS pool (not sparse, not linear, one sample) is placed in a heap standing for a 32 MB span of guest addresses, at its NVN offset scaled by 9/8 and rounded to Metal's alignment, if its Metal size ends within its scaled NVN range, so textures overlap here where they overlap on NVN. A span's heap is made only once it saves memory; a texture across a span's end tries the half-offset spans; covered bytes are inherited, new ones zero-filled. Placed textures have no GPU-optimized contents: with lossless compression, another format's writes over the same bytes hung the iPad's GPU (kIOGPUCommandBufferCallbackErrorHang by Link's house). Anything that does not fit takes the usual path (image_texture, then rh_texture). On only where recommendedMaxWorkingSetSize is under 8 GB (window_wants): sharing a target pool's bytes corrupted the picture on the Mac in PR seedds#1453 (the game keeps live targets in the part of the full-size scene target its current resolution leaves unused, which only NVN's layout makes safe), so larger devices keep each target's own layout. 8 GB devices have 5.3-5.5 GB of working set, a 16 GB M4 Mac 12,124 MB; the separate heaps needed 5.2 GB at the outdoor save load, sharing 4.3 GB. DM_NVN_WINDOW_ALIAS=0/1 forces it, and a log line says which way it went. The gfx test Makefile runs every test with it off unless the test turns it on, so an 8 GB Mac runs them as a 16 GB one does. Also for reading the set (res_top: the first GPU out-of-memory error, DM_CTL restop, and once when the set passes three quarters of the working set): the set by the source line that added each object, the resident pools by raw table / binding, by who gave an address in them (hle_nvn.c passes the kind: mtl_pool_addressed_by) and by pool flags (mtl_pool_tag), the GPU-made textures by pool, and the spans' counts. Tests: t_window_alias (new; 25 checks with the spans on, direct and deferred encoding, Metal validation; 8 fail with them off, the separate allocations), including the 8 GB rule. t_image_alias, t_rt_recycle, t_alias_takeover, t_upgrade_alias, t_copy_alias, t_rt_lifetime, t_pool_chunks, t_addr_known, t_file_wire pass. memory.md "Textures in GPU-only memory took an allocation per description"; rendering.md's recycled-heap entry links it; env_switches.tsv. Rebased onto main's pool and render-target structure: the virtual pool's runs are tagged after main's one-step mtl_pool_replace (vrun_set); the new pools' tag is set after pool_new_put; the storage range for res_top is kept on the texture before own_alias_publish (nvn_owner.m); a span's heap is an NvImageHeap with window set, which main's family-heap search (own_alias_at_locked) skips and which is freed with its last texture rather than waiting in rh_wait's free list. rendering.md keeps main's Fix and Check wording and adds the window heap.
…share bytes in a placement heap per 32 MB span, as on NVN: Ocarina of Time no longer runs out of GPU memory going outdoors on an M1 iPad With the render targets' recycled heaps (rh_texture, 7f50222) every live description in a GPU-only pool is a Metal texture of its own. On the M1 iPad (8 GB, recommended working set 5,461 MB) that reached 5,184 MB resident at the outdoor save load (544 heaps of 1,384 MB over 1,455 MB of textures), and every command buffer then failed with kIOGPUCommandBufferCallbackErrorOutOfMemory. window_texture (nvn_mtl.m): a texture whose storage lies in a CPU_NO_ACCESS pool (not sparse, not linear, one sample) is placed in a heap standing for a 32 MB span of guest addresses, at its NVN offset scaled by 9/8 and rounded to Metal's alignment, if its Metal size ends within its scaled NVN range, so textures overlap here where they overlap on NVN. A span's heap is made only once it saves memory; a texture across a span's end tries the half-offset spans; covered bytes are inherited, new ones zero-filled. Placed textures have no GPU-optimized contents: with lossless compression, another format's writes over the same bytes hung the iPad's GPU (kIOGPUCommandBufferCallbackErrorHang by Link's house). Anything that does not fit takes the usual path (image_texture, then rh_texture). On only where recommendedMaxWorkingSetSize is under 8 GB (window_wants): sharing a target pool's bytes corrupted the picture on the Mac in PR seedds#1453 (the game keeps live targets in the part of the full-size scene target its current resolution leaves unused, which only NVN's layout makes safe), so larger devices keep each target's own layout. 8 GB devices have 5.3-5.5 GB of working set, a 16 GB M4 Mac 12,124 MB; the separate heaps needed 5.2 GB at the outdoor save load, sharing 4.3 GB. DM_NVN_WINDOW_ALIAS=0/1 forces it, and a log line says which way it went. The gfx test Makefile runs every test with it off unless the test turns it on, so an 8 GB Mac runs them as a 16 GB one does. Also for reading the set (res_top: the first GPU out-of-memory error, DM_CTL restop, and once when the set passes three quarters of the working set): the set by the source line that added each object, the resident pools by raw table / binding, by who gave an address in them (hle_nvn.c passes the kind: mtl_pool_addressed_by) and by pool flags (mtl_pool_tag), the GPU-made textures by pool, and the spans' counts. Tests: t_window_alias (new; 25 checks with the spans on, direct and deferred encoding, Metal validation; 8 fail with them off, the separate allocations), including the 8 GB rule. t_image_alias, t_rt_recycle, t_alias_takeover, t_upgrade_alias, t_copy_alias, t_rt_lifetime, t_pool_chunks, t_addr_known, t_file_wire pass. memory.md "Textures in GPU-only memory took an allocation per description"; rendering.md's recycled-heap entry links it; env_switches.tsv. Rebased onto main's pool and render-target structure: the virtual pool's runs are tagged after main's one-step mtl_pool_replace (vrun_set); the new pools' tag is set after pool_new_put; the storage range for res_top is kept on the texture before own_alias_publish (nvn_owner.m); a span's heap is an NvImageHeap with window set, which main's family-heap search (own_alias_at_locked) skips and which is freed with its last texture rather than waiting in rh_wait's free list. rendering.md keeps main's Fix and Check wording and adds the window heap. Rebased onto main 0c07813: res_add_at records the adding line in g_res_line beside main's queued change (res_q_put), and sets no g_res_dirty, which the queue replaced.
What and why
Engines place their transient render targets over each other in a
CPU_NO_ACCESSpool, as a frame graph does: NVN gives them that pool's memory, whatever their number. The host makes a private Metal texture for each(pool, offset, description), so every description takes memory of its own. Ocarina of Time's note on main lists the case that remains as open:pool_heap_texture(nvn_mtl.m) gives eachCPU_NO_ACCESSpool, up to 1 GB, one placement heap. Each of the pool's render targets (COMPRESSIBLE, not sparse, linear or display) is placed at its pool offset scaled by 1.25, since a Metal texture can be a little larger than NVN's layout of it.tex_init_fill, or zeros). Over memory another texture has covered, a new texture inherits what is there and nothing is written, as on NVN.image_texture(pool_heapflag, jonpage0's fix from the review).DM_NVN_POOL_HEAP=0turns it off;restopprints the heaps.Measured
CPU_NO_ACCESSrender targetsMain's own Ocarina changes have since removed most of what this saved: on main, in a fixed Kokiri Forest scene, the gain is about 1 %. PucaVaz's runs on main (M4, 24 GB) found the same, with no errors. The case it is still meant for, the targets that resize with the dynamic resolution, was not exercised by these scenes (the resolution stayed put), so it is not demonstrated here. I am leaving it to the maintainer whether the general change is worth carrying for that case and for other engines' frame graphs.
Checked
0400486024DEC000, update v131072, SDK 22.2.8), on main 75c1a8c with this PR, Shared textures no live object resolves to are retired when GPU memory runs short: Ocarina of Time no longer freezes after the talk with Saria #1455 and DM_UPSCALER=metalfx takes agl's agl_ndsr_vr.pck networks too, and a variant with no curve and no power is drawn as one colour space: Ocarina of Time's upscaler is MetalFX's #1459: Kokiri Forest fixed scene, heap on and off,restopat frames 1500 and 2400; earlier sessions of 5 to 15 minutes (Saria, the Great Deku Tree in and out) on the pre-main branch. No GPU errors, no visual difference on screenshots.make -C dm/gfx/tests GAME=<id> check_t_pool_heap: 24 checks with direct and deferred encoding, 3 withDM_NVN_POOL_HEAP=0. It covers:COMPRESSIBLE, sparse textures and CPU-visible pools stay apart;t_image_alias(329). Every gfx test builds. The failing checks (host unit tests, tool tests, sanitizers, clang-tidy) fail the same way on main 75c1a8c.scripts/regress.py check --gameson other games; only Ocarina of Time was available here.Docs
docs/reference/rendering.mdentry, andpython3 dm/tools/docs_symptoms.pywas run (the symptom index)