Skip to content

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

Closed
PromethOps wants to merge 3 commits into
seedds:mainfrom
PromethOps:pool-heap-render-targets
Closed

PromethOps wants to merge 3 commits into
seedds:mainfrom
PromethOps:pool-heap-render-targets

Conversation

@PromethOps

@PromethOps PromethOps commented Nov 6, 2026 •

Copy link
Copy Markdown

What and why

Engines place their transient render targets over each other in a CPU_NO_ACCESS pool, 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:

render targets that move in the game's pool and change size with its dynamic resolution (1580 → 1579 → 1578 …) get a new Metal texture each time, about 2 GB added to the residency set per 300 frames.

pool_heap_texture (nvn_mtl.m) gives each CPU_NO_ACCESS pool, 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.

  • Ranges apart in the pool stay apart in the heap; ranges that overlap there may overlap here, as the memory is one on NVN. Memory then stays bounded by about 1.25 times the pool, however many sizes the targets take.
  • A texture that does not fit its scaled range keeps an allocation of its own.
  • Heap memory no texture has covered yet takes the texture's initial content (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.
  • An image placed there is never taken as a resolution family's heap by image_texture (pool_heap flag, jonpage0's fix from the review).
  • DM_NVN_POOL_HEAP=0 turns it off; restop prints the heaps.

Measured

Pool heap Before main's Ocarina changes On main 75c1a8c, fixed scene
CPU_NO_ACCESS render targets — 3,001 MB in 804 MB of pools 3 placed, in 3 heaps of 480 MB
residency set off 8–10 GB 5,201–5,247 MB
residency set on 6–7 GB (62 heaps, 671 MB) 5,158–5,187 MB

Main'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

Docs

  • The finding is in its docs/reference/rendering.md entry, and python3 dm/tools/docs_symptoms.py was run (the symptom index)
  • A chain tool changed: not the case
  • No game files, keys or data extracted from a game in the change or the attachments

@github-actions github-actions Bot added nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) docs Documentation labels Nov 6, 2026
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 6, 2026
… 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>
@PromethOps
PromethOps force-pushed the pool-heap-render-targets branch from 3d700bb to 91904d4 Compare November 6, 2026 06:31
@jonpage0

jonpage0 commented Nov 6, 2026

Copy link
Copy Markdown

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:

image_texture's same-storage lookup (g_alias entries with the same pool, offset and format, owner from g_image_heap_owner_key) takes any owner whose heap is big enough as the family heap. An NvPoolHeap is an NvImageHeap whose heap is the whole pool (up to 1 GB), so once a COMPRESSIBLE|IMAGE texture has been placed in the pool heap, a later description over the same storage is taken as a member of an offset-zero family and lands at heap offset 0, over whatever the pool placed there. The fix: a pool_heap flag on NvImageHeap, set when an NvPoolHeap is made, and image_texture returns to the pool-placement path when the lookup finds a pool-heap owner, so the later description keeps its scaled pool offset:

@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 t_pool_heap.m (a 512² COMPRESSIBLE|IMAGE at pool offset 24 MB, then a 256² description over the same storage): before the fix 23 checks, 2 failed ("late image descriptions retain their scaled pool offset", "late image writes do not reach separate pool storage"); after it 24 checks, 0 failed, and the PR's own checks are unchanged. With and without DM_NVN_POOL_HEAP the rest of the gfx tests pass here (t_image_alias 329, t_image_usage 44, t_cpu_tex 59, t_cpu_storage 38, t_pool_chunks 102, t_feedback 159, with and without DM_NVN_ENC_THREAD=1). Not yet run in the game; I will report the Kokiri walk when the rebuilt app is up. Diff available as a commit if you want it on the branch.

john62036203-ops added a commit to john62036203-ops/NXNativeWindows that referenced this pull request Nov 6, 2026
…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
john62036203-ops added a commit to john62036203-ops/NXNativeWindows that referenced this pull request Nov 6, 2026
… 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
john62036203-ops added a commit to john62036203-ops/NXNativeWindows that referenced this pull request Nov 6, 2026
…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
@PucaVaz

PucaVaz commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

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.

  • 3 GPU-only pool heaps, 480 MB in all (64, 288 and 32 MB pools), 3 render targets placed, 0 kept apart.
  • No GPU errors. The [RED] lines were the title movie's AV1 decoder, the partial library's notes and one texture type mismatch in the walk, which also appears with the heap off; none came from the pool heaps. Screenshots of the house, the door, Saria's greeting and the walk show nothing missing or garbled.
  • Same binary with DM_NVN_POOL_HEAP=0 DM_NVN_ALIAS_RETIRE=0, one run on the same route: house frame p50 21.9 ms (22.1 with the heap), walk p50 24.7 ms (23.9), footprint up to 11.0 GB (11.5). On this Mac and route the render targets in GPU-only pools are a small part of the scene's memory (the residency set held 4.9 GB in the house, 1.9 GB of it textures, and 5.4 GB right after leaving it), so I see no memory difference from it here. One run each, no repeats.
  • With jonpage0's pool_heap fix on top (t_pool_heap 19/19 in both encoding modes and 3/3 off, t_image_alias 329/329), a 9.5-minute run on the same save (60 s in the house, Saria's greeting, 3 minutes of walking): no GPU error, house 44.1 fps (frame p50 22.2 ms), walk 36.1 fps (p50 24.2 ms), footprint up to 11.6 GB.

gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 6, 2026
) replaces the pull request heads

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
…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>
@PromethOps

Copy link
Copy Markdown
Author

Thank you both.

@jonpage0: your pool_heap fix is in (85be790), with your case added to t_pool_heap.m: a 512x512 COMPRESSIBLE|IMAGE at 24 MB, then a 256x256 description over it, checked for its scaled offset and for writes that stay out of separate storage. 24/24 checks in both encoding modes, 3/3 with DM_NVN_POOL_HEAP=0, t_image_alias 329/329, and every gfx test builds.

@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 CPU_NO_ACCESS render targets in 804 MB of pools, residency 8–10 → 6–7 GB) were taken before main's Ocarina changes. On main the case left is the note's open item: render targets that move and resize with the dynamic resolution, each getting a new Metal texture. I will re-measure on main with the dynamic resolution moving before claiming more, and update the description with the result.

The failing checks (host unit tests, tool tests, sanitizers, clang-tidy) fail the same way on main 75c1a8c; the build check passes.

john62036203-ops pushed a commit to john62036203-ops/NXNativeWindows that referenced this pull request Nov 6, 2026
…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.
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 6, 2026
…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)
thomasruegsegger pushed a commit to thomasruegsegger/nxnative that referenced this pull request Nov 6, 2026
… 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>
thomasruegsegger pushed a commit to thomasruegsegger/nxnative that referenced this pull request Nov 6, 2026
…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>
thomasruegsegger pushed a commit to thomasruegsegger/nxnative that referenced this pull request Nov 7, 2026
…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)
thomasruegsegger pushed a commit to thomasruegsegger/nxnative that referenced this pull request Nov 7, 2026
…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)
thomasruegsegger pushed a commit to thomasruegsegger/nxnative that referenced this pull request Nov 8, 2026
…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)
thomasruegsegger pushed a commit to thomasruegsegger/nxnative that referenced this pull request Nov 8, 2026
…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)
thomasruegsegger pushed a commit to thomasruegsegger/nxnative that referenced this pull request Nov 8, 2026
…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)
seedds added a commit that referenced this pull request Nov 8, 2026
…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.
@seedds

seedds commented Nov 8, 2026

Copy link
Copy Markdown
Owner

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).

@seedds seedds closed this Nov 8, 2026
dsi-hub added a commit to dsi-hub/nxnative that referenced this pull request Nov 9, 2026
…ropped), the real-time policy re-check, the added mesh row

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dsi-hub added a commit to dsi-hub/nxnative that referenced this pull request Nov 9, 2026
…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>
Aeonitis pushed a commit to Aeonitis/nxnative that referenced this pull request Nov 9, 2026
…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)
stan69b pushed a commit to stan69b/nxnative that referenced this pull request Nov 9, 2026
…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.
Know-It-All-Brother pushed a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
… 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>
Know-It-All-Brother pushed a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
…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.
AmateurWizrd pushed a commit to AmateurWizrd/nxnative that referenced this pull request Nov 10, 2026
…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)
Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
…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
Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
…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
Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
…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
stan69b pushed a commit to stan69b/nxnative that referenced this pull request Nov 10, 2026
…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.
stan69b pushed a commit to stan69b/nxnative that referenced this pull request Nov 10, 2026
…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.
stan69b pushed a commit to stan69b/nxnative that referenced this pull request Nov 10, 2026
…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.
stan69b pushed a commit to stan69b/nxnative that referenced this pull request Nov 12, 2026
…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.
stan69b pushed a commit to stan69b/nxnative that referenced this pull request Nov 12, 2026
…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.
stan69b pushed a commit to stan69b/nxnative that referenced this pull request Nov 12, 2026
…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.
stan69b pushed a commit to stan69b/nxnative that referenced this pull request Nov 12, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants