Repository navigation
Conversation
…: large-pool chunks retire, pools from 128 MB are chunked, the pool walk checks the type first, and DM_NVN_TEX_MIP_SKIP drops the finest level of compressed CPU-written textures PRAGMATA (0400C76024710000) on an 8 GB M1 MacBook Air: its opening load took the residency set to 5.2 GB of the 5,461 MB working set. Command buffers failed with kIOGPUCommandBufferCallbackErrorOutOfMemory, the submit thread waited in the kernel, and WindowServer hit its watchdog. - big_lookup: a chunk that a longer one replaces is retired once the submissions that may use it complete, instead of staying resident until the pool goes. Every cache of pool addresses checks g_pool_gen, which big_new bumps. Before, a 48 MB and a 96 MB chunk at one base both counted. - Pools of 128 MB and more (was 512) are wrapped in chunks, so only what lookups reach is wired. BIG_MAX goes from 32 to 64. - pool_gpu skips IDs whose descriptor cannot be of the walked type (ttype_apart). It used to make a Metal texture for every registered ID before comparing types, so a cube-texture walk uploaded every texture. - DM_NVN_TEX_MIP_SKIP=<px> (1 = 512; off by default): CPU-written block-compressed 2D, array and cube textures with a mip chain are made without their finest level. Views, copies, readbacks and a sparse texture's level clamp take the guest's level less the skip (tex_mip_skipped, lv_host). - DM_NVN_GMEM_RES=0: pools reached only through the raw global memory table stay out of the residency set, and large pools stay in chunks. - DM_NVN_DESWZ_GPU=0: textures are deswizzled on the CPU. - Diagnostics: the out-of-memory report gives the distinct pages under the shared buffers and texture memory by pixel format. DM_NVN_RES_LIMIT_MB=<MB> stops the game gracefully before the set reaches the limit. Measured on that Mac, with seedds#1315's guest file: at the title the set fell from 3,985 MB to 3,241 MB. Through the opening load it peaked at 4,284 MB, with no GPU errors, and the game went on to its opening scene. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DeVTtdCXD7t64Ek6e1py6A
|
A data point from an 8 GiB iPhone, in case it helps: this PR on an iPhone Duo (iPhone19,4, A20 Pro, iOS 27.2, per-process limit 8,191 MB), with Mario Kart World 1.0.0 (base v0, handheld). Build: this PR's head (fc815c3), with #1315 (guest file) and #1306 (env.txt samplers) cherry-picked on top. Signed with Increased Memory Limit + Extended Virtual Addressing. Route: the same Grand Prix (4 races, ~19 min) as in the earlier runs, after a full app restart each time. Numbers come from the log's memory line,
The player reported races 1–3 at a steady 60 fps and only occasional spikes in race 4, the smoothest run so far. Before, the spikes started in race 2. I saw no visual problems, and there were no new My reading, not measured directly: on this phone the change mostly removes stalls, not footprint. The graphics ledger peaks where it did before, while GPU allocated and the stalls both drop, which fits pools being wired in chunks instead of whole. Caveats: this is one run per configuration. I did not separate the pool changes from |
… chunks, DM_NVN_GMEM_RES, DM_NVN_TEX_MIP_SKIP) # Conflicts: # dm/gfx/nvn/dmsh_chain.h # dm/gfx/nvn/nvn_mtl.m # dm/gfx/tools/dmsh_chain.lock # dm/gfx/tools/dmsh_xlate_bin/Ryujinx.Graphics.Shader.dll # dm/tools/tests/test_nsp_dump.py # docs/development.md # scripts/build_ipa_from_nsp.py
…: the GPU used the old pages seedds#1319's pool_reuse_dead lets a new pool over exactly the range of a freed one take back its no-copy buffer while the release waits. A no-copy buffer holds the physical pages it was made over, so when the host had given the range back in between (guest_release, from UnmapPhysicalMemory or a smaller heap) the GPU read and wrote the old pages while the CPU had the new ones. t_cpu_tex's finalized-texture readbacks read stale bytes and t_vbuf found the replacement pool at its old GPU address; seedds#1319 had not run the graphics tests. guest_release now calls mtl_guest_released, which drops the freed pools' buffers over that range from the reuse list; host tests without graphics link a weak empty definition. t_cpu_tex and t_vbuf call it where they unmap a pool's memory, as the host does. Reference entry in docs/reference/memory.md. Validation: make -C dm/gfx/tests GAME=0400486024DEC000 run (all pass; t_cpu_tex and t_vbuf failed before), make -C dm/host/tests GAME=0400C3F00006E000 run, test_docs; the Mac host builds and links nvn_mtl.m's definition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Tried this on Ocarina of Time ( Four runs, one per setting, all from the same Kokiri Forest save. After the load I stayed in Link's bedroom in every run: standing still, then turning the camera, then jumping around, and near the end looking out of the front door or the back window. No area change. A guard stopped the runs with this PR on Mac free memory. The run from before this PR went until it froze.
¹ From about 45 s after the load, with the Mac mostly at 6–9% free and Before this PR, the bedroom alone fills Metal's working set. 113 s after the load, with no area change, the residency set reached Metal's limit, and frames took 0.4–1.2 s until I closed it: That's the same 2,432 MB graphics pool, resident whole, as in nafields's report on #1265. This run also logged a
The footprint doesn't change with any of it. In every run it was about 3.8 GB on the title screen, about 10 GB right after the load, and 12–13 GB 1.5–2.5 minutes later. Footprint and GPU allocated rose by about the same 1.8–3.1 GB in every run while the pool count stayed flat, so the growth is host GPU memory. The log's shared-texture count also doubled (583 → 1,154 in the baseline). It rose through the whole run, not only once I looked outside. So on a 16 GB Mac, once the working set has headroom, system memory runs out at about the same point. This log doesn't say whether the new textures are host copies of the game's textures (#1281) or render targets, which @david-on-spring found most of the GPU memory to be at a scene change. Their 15 minutes at 10.4 GB on #1347 included their pool-table changes, which this build doesn't have; their ~40 ms a frame is about what both switches gave here. The pool table overflowed at the load in every run. Single runs with my own hands on the controller, so treat small differences as noise. The large ones, the allocator waits and the out-of-memory, are well outside it. If a by-format breakdown would help, I can rerun with |
…d of freezing the Mac A session of Ocarina of Time ended in a GPU out-of-memory and a stuck game, and seedds#1326 reports a Mac frozen until the power button: GPU memory is wired, IOGPUScheduler's freeToWireMemory fails at recommendedMaxWorkingSetSize, and the submit thread then waits in the kernel where the process cannot be killed. Nothing in the host looked at the working set. res_commit now reads the larger of the set's allocatedSize and the device's currentAllocatedSize before it commits; the set's size leaves out what was never committed (measured: a new 64 MB texture adds 0 to it until the commit), the device's counts it from its creation. From 80% of the working set the submit thread lets go of what can go: idle sharing entries nothing holds, finished deferred releases, spares and the upload slots' staging. From 95%, if that was not enough, the game stops with a [RED] report of what the set holds, by kind. A commit that adds to the set is not made at the working set itself. A 100 ms timer reads the sizes too, as a stuck submit thread never comes back to look. The stop reaches the launcher as a normal exit: the Mac app closes the graphics gate, leaves full screen, shows an alert and quits as Cmd-Q does, the path PucaVaz's PR seedds#1332 takes for a revoked GPU (front_stop_alert is written so front_gpu_lost can call it); without a window the host takes its graceful exit. DM_NVN_RES_LIMIT_MB and the report's distinct pages and texture formats come from JioChoi's PR seedds#1319, moved from after the commit to before it. iOS only evicts unless the switch sets a limit: no iPhone has tried the stop. t_res_guard covers the levels, the commit refused and removals passing, what stays (a live object, the parent of a live sRGB view, a recent lookup), the second look at the stop level, and the timer with the lock held.
Texture upload, view and copy remapping subset of fc815c3 (PR seedds#1319 by JioChoi). Kept as a separate prerequisite commit so the multi-level extension is independently reviewable. Pool, raw-residency and diagnostic changes are excluded.
…er, not the address A pool made over the range of one freed a moment before took back its buffer. After an unmap and a new map at that address the GPU read the old pages (found in review on seedds#1407; t_cpu_tex and t_vbuf failed with it there). The freed pool's buffer goes through deferred release again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DeVTtdCXD7t64Ek6e1py6A
|
Closing in favour of the draft PRs #1614 (large pools held once in the residency set, stacked on #1407) and #1615 (GPU memory guard measure, stacked on #1392). pool_reuse_dead was wrong, as #1407's review found: a no-copy buffer keeps the pages it was made over. DM_NVN_RES_LIMIT_MB moved into #1391's guard, and the mip skip continues in #1418. |
…d of freezing the Mac A session of Ocarina of Time ended in a GPU out-of-memory and a stuck game, and seedds#1326 reports a Mac frozen until the power button: GPU memory is wired, IOGPUScheduler's freeToWireMemory fails at recommendedMaxWorkingSetSize, and the submit thread then waits in the kernel where the process cannot be killed. Nothing in the host looked at the working set. res_commit now reads the larger of the set's allocatedSize and the device's currentAllocatedSize before it commits; the set's size leaves out what was never committed (measured: a new 64 MB texture adds 0 to it until the commit), the device's counts it from its creation. From 80% of the working set the submit thread lets go of what can go: idle sharing entries nothing holds, finished deferred releases, spares and the upload slots' staging. From 95%, if that was not enough, the game stops with a [RED] report of what the set holds, by kind. A commit that adds to the set is not made at the working set itself. A 100 ms timer reads the sizes too, as a stuck submit thread never comes back to look. The stop reaches the launcher as a normal exit: the Mac app closes the graphics gate, leaves full screen, shows an alert and quits as Cmd-Q does, the path PucaVaz's PR seedds#1332 takes for a revoked GPU (front_stop_alert is written so front_gpu_lost can call it); without a window the host takes its graceful exit. DM_NVN_RES_LIMIT_MB and the report's distinct pages and texture formats come from JioChoi's PR seedds#1319, moved from after the commit to before it. iOS only evicts unless the switch sets a limit: no iPhone has tried the stop. t_res_guard covers the levels, the commit refused and removals passing, what stays (a live object, the parent of a live sRGB view, a recent lookup), the second look at the stop level, and the timer with the lock held.
What and why
On an 8 GB M1 MacBook Air, PRAGMATA (0400C76024710000) runs out of GPU memory in its opening load, after New Game. The residency set reaches 5.2 GB of Metal's 5,461 MB working set. Command buffers fail with
kIOGPUCommandBufferCallbackErrorOutOfMemoryand the submit thread waits inIOGPUCommandQueueSubmitCommandBuffers. Twice, WindowServer hit its watchdog and the desktop session restarted.What filled the set at that point: 2.45 GB of textures and 2.37 GB of shared buffers.
The goal is the iPhone 15 Pro, which also has 8 GB. With #1315's guest file the footprint stays at 3.2–3.6 GB, so GPU memory is what is left to fit.
Changes, all in
nvn_mtl.m:big_lookup). A large-pool chunk that a longer one replaces is retired once the submissions that may use it complete, as a freed pool's buffer is. Before, it stayed resident until the pool went. Every cache of pool addresses checksg_pool_gen, whichbig_newbumps.BIG_MAX32 → 64). PRAGMATA's 256–300 MB pools were wired whole for any binding.ttype_apart). It used to make a Metal texture for every registered ID before it compared the type.DM_NVN_TEX_MIP_SKIP=<px>(1= 512; off by default). CPU-written block-compressed 2D, 2D array and cube textures with a mip chain, both sides at least that size, are made without their finest level: a quarter of the memory. Views, copies, CPU readbacks and a sparse texture's level clamp take the guest's level less the skip (tex_mip_skipped,lv_host). Render targets and shader-written textures are never in these formats. A shader that asks a texture's size gets the halved one.DM_NVN_GMEM_RES=0. Pools reached only through the raw global memory table stay out of the residency set until a binding addresses them, and large pools stay in chunks instead of one whole-pool entry.DM_NVN_DESWZ_GPU=0. Swizzled textures are deswizzled on the CPU.DM_NVN_RES_LIMIT_MB=<MB>stops the game gracefully, with that report, once the set passes that size. I used it to keep testing without taking WindowServer down.Checked
DM_GUEST_FILE=1 DM_GUEST_FILE_CHUNK=512M DM_NVN_GMEM_RES=0 DM_NVN_DESWZ_GPU=0 DM_NVN_TEX_MIP_SKIP=1.Insufficient Memoryat about 5.2 GB.test_docs.py,test_env_switches.py: pass.regress.py check --games(no regression harness or other game builds on this machine), and the host and gfx unit tests. Everything new except the pool changes is behind switches that are off by default. The pool changes (retiring a replaced chunk; chunks from 128 MB) apply to every game.Docs
docs/reference/memory.md: "GPU out of memory on an 8 GB Mac: the residency set passes Metal's working set". Symptom index regenerated (docs_symptoms.py).docs/env_switches.tsv:DM_NVN_TEX_MIP_SKIP,DM_NVN_RES_LIMIT_MB,DM_NVN_GMEM_RES,DM_NVN_DESWZ_GPU.🤖 Generated with Claude Code
https://claude.ai/code/session_01DeVTtdCXD7t64Ek6e1py6A