Add Monolith archive and shader extraction, a build-gated compact memory layout, vertex storage writes under rasterizer discard, primitive ordering and interleaved geometry feedback. Correct native decoder/audio wait accounting and the oe calls used after movies. Validated opening combat and movement on Mac with real audio, the full Mac/iOS shader build, GPU regressions, and the unsigned IPA archive. iOS runtime, later areas and DLC gameplay remain untested.
316 lines
62 KiB
Markdown
316 lines
62 KiB
Markdown
# Memory and address space
|
||
|
||
The guest address space, heaps, pools, virtual address memory and the allocators games bring. Each entry: symptom, cause, fix, the games it was found on. The [symptom index](../agents/symptoms.md) lists every symptom.
|
||
|
||
## Guest layout
|
||
|
||
### Partitioning the guest block between alias region, stacks and heap
|
||
|
||
- **Symptom:** large resources silently skipped, later a null deref where the resource is used; also visible as `SetHeapSize` asking for much less than the pool.
|
||
- **Cause:** the game reserves "virtual address memory" (Mario: 1.75 GB in `HeapDef.byml`) and maps it into the alias region with `MapPhysicalMemory` on demand; the alias region left after the node area ([The SDK's address-region node area](#the-sdks-address-region-node-area)) was too small.
|
||
- **Fix:** repartition the guest block (`hle_svc.c` `mem_init`): Mario uses alias 2.875 GB, stacks 256 MB, heap 1.625 GB (it asks for 1.47 GB), 5 GB in total.
|
||
- **Found on:** [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md).
|
||
- **Check:** measure before sizing (`DM_SVC=1`: the `SetHeapSize` request and the highest `MapPhysicalMemory` address).
|
||
- **Note:** on iOS without the extended-virtual-addressing entitlement a single 6 GB reservation succeeds and 7 GB fails, but Metal and the rest of the process need address space too; 5 GB is the total proven on a phone (D2R).
|
||
|
||
## Heap and application pool
|
||
|
||
### Provisional Unreal layout leaves no heap
|
||
|
||
- **Symptom:** `SetHeapSize OOM: req 0x600000 > cap 0`, followed by an SDK abort in `nninitStartup` on the first provisional boot.
|
||
- **Cause:** the provisional Unreal profile reserved `0x1028000000` bytes but assigned `0x1008000000` to the alias region. The 256 MB module region and 256 MB stacks consumed the remainder, leaving no heap.
|
||
- **Fix:** `profile_guess.py` gives the alias region exactly 64 GB (`0x1000000000`), leaving 128 MB for the initial heap before the heap check measures the game. `test_profile_guess.py` checks the partition through the real profile generator.
|
||
- **Found on:** [SHIN MEGAMI TENSEI V: Vengeance](../../dm/games/010069C01AB82000/README.md) 1.0.3, whose C library asks for 6 MB.
|
||
|
||
### Heap sized from the application pool less used memory
|
||
|
||
- **Symptom:** `nnMain` returns right after `nv::InitializeGraphics`, with no error (Hyrule Warriors: Age of Calamity); `[mem] SetHeapSize … + modules … > app pool` (Monster Hunter Rise), `[mem] SetHeapSize … + modules 0x5c00000 > app pool` (Catherine: Full Body); `SetHeapSize` asks for almost the whole application pool and fails at boot (Metroid Dread: 0xc9400000, larger than the heap region); the memory pool for the game's 96 MB GPU heap has no storage (`MemoryPoolInitialize` red line), then a null read at `main+0x2eb954` (Catherine: Full Body); heap allocations fail late in a busy scene.
|
||
- **Cause:** the SDK sizes the heap as total − used memory (`QueryMemoryInfo`), so the reported application pool and used memory decide the game's heap size. Age of Calamity's `nninitStartup` does so, calls `SetMemoryHeapSize` and hands the block to its own malloc. The host reported 64 MB used beside the heap ("used" = heap + 64 MB), but `SetHeapSize` checks a request against the real size of the loaded modules (66.6 MB in Age of Calamity), so `SetHeapSize(pool − 64 MB)` failed with ResultLimitReached, `AllocateMemoryBlock` returned 0x1003, malloc was never set up, the 866 MB graphics heap allocation came back null and `nnMain` returned. Monster Hunter Rise's modules take 320 MB (`main` alone 314 MB), so its `SetHeapSize(pool − 64 MB − bss)` failed the same way. Catherine: Full Body's rewritten modules take 92 MB, so `SetHeapSize(pool − 64 MB)` failed and the game ran on a 32 MB heap. The 64 MB covers ordinary modules (Mario's two are about 74 MB, and its heap request matches hardware), but Metroid Dread's `main` carries a 1.8 GB bss that the Switch counts as used.
|
||
- **Fix:** report Switch-accurate numbers. The application pool was a host constant (`APP_POOL`, 3285 MB); it is now the profile key `app_pool` (`hle_svc.c`: `#define APP_POOL DM_GAME_APP_POOL`, Mario 0xCD500000, the same 3285 MB). The profile key `mem_used` sets the used-memory figure: 72 MB for Age of Calamity (the image plus stacks); 328 MB for Monster Hunter Rise, which gives a 2.88 GB heap; 96 MB for Catherine: Full Body, the NSP script's guess after the rewrite. Used memory now also adds the bss of modules in the front-patch layout (`big_bss_bytes` in `hle_svc.c`; the layout is in [Patch area before the module base for a large bss](rewriter.md#patch-area-before-the-module-base-for-a-large-bss)); Dread then asks for 1.38 GB.
|
||
- **Found on:** [Hyrule Warriors: Age of Calamity](../../dm/games/01002B00111A2000/README.md); [Monster Hunter Rise](../../dm/games/0100B04011742000/README.md); [Metroid Dread](../../dm/games/010093801237C000/README.md); [Catherine: Full Body](../../dm/games/0100BF00112C0000/README.md) (SDK 8.2.1; one boot). The source names no game for the late failures in a busy scene.
|
||
- **Check:** probe `DM_SVC=1`'s `svc 0x1` request and the `AllocateMemoryBlock` result.
|
||
|
||
### A heap request beyond the heap region returns ResultOutOfMemory
|
||
|
||
- **Symptom:** `SetHeapSize OOM: req 0x120000000 > cap 0xd0000000`, then `Break#1 reason=0` in `nninitStartup`, with a backtrace through `nn::os::SetMemoryHeapSize` (`sdk+0x967a4`).
|
||
- **Cause:** a game may look for the largest heap it can have by asking for more than there is. Donkey Kong Country: Tropical Freeze's `nninitStartup` (`main+0x1e84c`) binary-searches `SetMemoryHeapSize` between 3 and 6 GB (6 GB is the heap region of a 39-bit address space) and narrows on every failure. SDK 4's `SetMemoryHeapSize` (`sdk+0x9672c`) turns the kernel's ResultOutOfMemory (description 104), 103 and ResultLimitReached (132) into `os::ResultOutOfMemory` (0x1003) and aborts on any other result. For a size larger than the heap region the host returned 0xD401 (InvalidCurrentMemory, 106), so the SDK aborted. Horizon's `KPageTable::SetHeapSize` returns ResultOutOfMemory there.
|
||
- **Fix:** a request larger than the heap region returns 0xD001 (`hle_svc.c`, svc 0x1). The search then settles on 3.09 GB, the pool less the rewritten modules (the same LimitReached check as before).
|
||
- **Found on:** [Donkey Kong Country: Tropical Freeze](../../dm/games/0100C1F0051B6000/README.md) (SDK 4.4.0).
|
||
|
||
### `nninitStartup`'s heap only without virtual address memory
|
||
|
||
- Not needed in the end for [Ace Attorney Investigations Collection](../../dm/games/010005501E68C000/README.md) (1.0.0.2, SDK 17.5.3): the process heap of pull request #16. `nninitStartup` calls `SetMemoryHeapSize` / `AllocateMemoryBlock` only without virtual address memory, and nothing else in `main` calls them ([Virtual address memory follows the NPDM's system resource](#virtual-address-memory-follows-the-npdms-system-resource)).
|
||
|
||
### Sizing the pool and alias region for virtual address memory
|
||
|
||
- A title that uses virtual address memory may never call `SetHeapSize`: everything comes from `MapPhysicalMemory` in the alias region. The `[mem]` log lines show what it maps ([Measuring address regions and commits](#measuring-address-regions-and-commits)).
|
||
- What fills the alias region first is address space, not memory. StandardAllocator reserves the whole free pool (pool − 64 MB, 1 GB aligned) but Engage commits only 0.58 GB of it, and Unity's own regions commit 0.97 GB. So Engage's profile reports a 2.5 GB pool (`app_pool`) and uses a 4.375 GB alias region in a 5 GB block (reserved address space peaks at 3.47 GB of 4.31 GB).
|
||
- iOS without the extended-virtual-addressing entitlement allows a single 6 GB reservation, but Metal needs address space too; 5 GB is the total proven on a phone ([Partitioning the guest block between alias region, stacks and heap](#partitioning-the-guest-block-between-alias-region-stacks-and-heap)).
|
||
- The pool a game is told decides how much StandardAllocator reserves (`Initialize(nullptr, pool − used)`), and the host does not enforce the pool (only `nninitStartup` reads it), so a pool a little above what the game maps leaves the most address space:
|
||
- Ace Attorney Investigations Collection: with Engage's 2.5 GB pool the game ran, but StandardAllocator's 2.44 GB reservation beside Unity's pools (1.06 GB live, from `main+0x40f1d4`) ended 4.37 GB into the 4.375 GB alias region after 140 s, while the `MapPhysicalMemory` log showed StandardAllocator committing 0.19 GB. The profile reports a 1 GB pool, a little less than the 1.02 GB the game maps at its peak; ten minutes then peak at +2.85 GB of address space, against +3.91 GB with a 1.5 GB pool.
|
||
- Brain Memory Training Academy: with Ace Attorney's 1 GB pool and 5 GB block the game boots to its title in 5 s with no red line; StandardAllocator ends at +1.0 GB, and five minutes of level loads take the alias high-water mark to +2.25 GB with at most 0.5 GB mapped.
|
||
- Patrick's Parabox: with Ace Attorney's 1 GB pool and 5 GB block (in place of the provisional profile's hardware pool) it boots to its title with no red line, and the alias region is mapped up to +1.5 GB with 0.27 GB in use.
|
||
- Advance Wars 1+2: Re-Boot Camp: the hardware pool used the alias region up to +4.25 GB; the heap check's 1 GB pool starved its malloc ([Unity's GPU pool block allocation spins at size 0](#unitys-gpu-pool-block-allocation-spins-at-size-0)); with a 2 GB pool the game maps up to 2.26 GB of the alias region through its menus and first battle.
|
||
- **Found on:** [Fire Emblem Engage](../../dm/games/0100A6301214E000/README.md); [Ace Attorney Investigations Collection](../../dm/games/010005501E68C000/README.md) (Unity 2022.3, SDK 17.5.3); [Brain Memory Training Academy](../../dm/games/0100F5D01DA3A000/README.md); [Patrick's Parabox](../../dm/games/0100E44016C22000/README.md); [Advance Wars 1+2: Re-Boot Camp](../../dm/games/0100300012F2A000/README.md).
|
||
- **Note:** to measure regions and commits, see [Measuring address regions and commits](#measuring-address-regions-and-commits).
|
||
|
||
## Virtual address memory and the alias region
|
||
|
||
### Virtual address memory follows the NPDM's system resource
|
||
|
||
- **Symptom:** `StandardAllocator::Initialize` aborts in `nninitStartup` (Fire Emblem Engage); `nn::diag` abort in `StandardAllocator::Initialize(void *, size_t, bool)` from `nninitStartup` (`sdk+0x2f98e4` ← `main+0x5b69e0`, Big Brain Academy: Brain vs. Brain).
|
||
- **Cause:** the SDK's `IsVirtualAddressMemoryEnabled` is `GetInfo(TotalSystemResourceSize) != 0` (`VammManager::IsVirtualAddressMemoryEnabled`, `sdk+0x34e074`), the system resource the NPDM declares (offset 0x14 of `META`). With it the game's allocator takes its memory as address regions in the alias region. Engage's NPDM declares one, so `IsVirtualAddressMemoryEnabled` is true and the allocator reserves all free memory (about 3.2 GB for Engage) as one region in the alias region, after the ~1 GB node area ([The SDK's address-region node area](#the-sdks-address-region-node-area)). Big Brain Academy's `nninitStartup` (`main+0x5b6984`) asks `os::IsVirtualAddressMemoryEnabled()`: when true it gives StandardAllocator a 60 GB address region (`0xf00000000`), when false it sets a heap of pool − used (2 MB aligned) and hands that over. The host reported a 32 MB system resource to every game, while this game's NPDM declares none.
|
||
- **Fix:** the profile key `sys_resource` reports the NPDM's size (0 for Big Brain Academy, so the heap path, as on hardware; `hle_svc.c`); the survey writes it for every new game, and profiles without it keep the 32 MB answer. A game with virtual address memory needs a profile `mem_alias` / `mem_reserve` that holds its regions: Engage first used 5.5 / 8 GB on the Mac; its profile now has `mem_reserve 0x140000000` / `mem_alias 0x118000000` (5 GB / 4.375 GB) with the node area cut and a 2.5 GB `app_pool` ([Sizing the pool and alias region for virtual address memory](#sizing-the-pool-and-alias-region-for-virtual-address-memory)).
|
||
- **Found on:** [Fire Emblem Engage](../../dm/games/0100A6301214E000/README.md); [Big Brain Academy: Brain vs. Brain](../../dm/games/0100620012D6E000/README.md) (SDK 11.4.3, NintendoWare Bezel Engine).
|
||
|
||
### The SDK's address-region node area
|
||
|
||
- **Symptom:** the first `AllocateAddressRegion` fails with 0x1803; `StandardAllocator::Initialize` aborts in `nninitStartup` (an `nn::diag` abort; the NSP script's heap check reports "the host stopped before nnMain"); the alias region fills in minutes; regions a game reserves later fail ([Size-aligned regions exhaust the alias address space](#size-aligned-regions-exhaust-the-alias-address-space), [The movie player's own StandardAllocator](#the-movie-players-own-standardallocator)).
|
||
- **Cause:** SDK 16's AddressRegionManager takes 0x3fe00000 at the start of the alias region as a node area, so the alias region must be larger than 1 GB; SDK 8 has the same 1 GB node area as the newer SDKs, and SDK 7 / 8 builds load it in another form (below). On hardware the alias region is 64 GB. On Ace Attorney Investigations Collection's SDK 17 build, `CentralHeap::Init(nullptr, pool − used)` got no 1 GB-aligned region for 3.14 GB after the 1 GB node area.
|
||
- **Fix:** shrink the node area with a rewriter cut for that SDK build (`dm/tools/sdk_patches.py`): the two `mov w8/w9, #0x3fe00000` in the AddressRegionManager constructor → 62 MB.
|
||
- **Finding the constructor on a new `sdk` build:** search its text for the two `mov w8 / w9, #0x3fe00000` words (`08fca752`, `09fca752`); `sdis.py --fn` names the function `AddressRegionManagerC2Emm` from its dynamic symbols. SDK 7 / 8 builds load the node area with `orr w8 / w9, wzr, #0x3fe00000` (`e8230b32`, `e9230b32`), so the `mov` search finds nothing there.
|
||
- **Found on** (the builds that take the cut):
|
||
- [Xenoblade Chronicles 3](../../dm/games/010074F013262000/README.md) (SDK 12.3.9): constructor `sdk+0x346420`, cuts `+0x346430` / `+0x3464cc`, the same offsets as Engage's SDK 12.3.7, beside the large-region alignment cut. The game's own allocator needs a separate compact layout.
|
||
- [Pokémon Brilliant Diamond](../../dm/games/0100000011D90000/README.md) (Unity 2019.4.27f1, SDK 11.4.3): constructor `sdk+0x34e0b8`, node-area limit `+0x34e0c8` and occupied length `+0x34e164`. The original node area prevents StandardAllocator's pool-minus-used reservation from fitting at startup.
|
||
- [Fire Emblem Engage](../../dm/games/0100A6301214E000/README.md): the two `mov w8/w9, #0x3fe00000` in the constructor → 62 MB; without it the alias region ran out of address space minutes into play ([Size-aligned regions exhaust the alias address space](#size-aligned-regions-exhaust-the-alias-address-space)).
|
||
- [Ace Attorney Investigations Collection](../../dm/games/010005501E68C000/README.md) (Unity 2022.3, SDK 17.5.3): `StandardAllocator::Initialize` aborts in `nninitStartup` on SDK 17, then the alias region fills in minutes; the same node area as Engage's SDK; the constructor is at `sdk+0x139458` on that build and takes the same cut.
|
||
- [Brain Memory Training Academy](../../dm/games/0100F5D01DA3A000/README.md) (Unity 2022.3.5f1, SDK 16.2.4): `nn::diag` abort at `sdk+0x126490`, `StandardAllocator::Initialize`, called from `nninitStartup` at `main+0x73adcc`. Its `nninitStartup` is Ace Attorney's: `Initialize(nullptr, pool − used)` with virtual address memory and no `SetMemoryHeapSize`. The `sdk` build is Unicorn Overlord's, whose NPDM declares no system resource, so it never needed the cut; the constructor (`AddressRegionManagerC2`, `sdk+0x134768`) takes it at `+0x134778` / `+0x13480c`.
|
||
- [Sea of Stars](../../dm/games/01008C0016544000/README.md) (Unity 2021.3.25f1, SDK 16.2.0, the `sdk` build of Super Mario Bros. Wonder 1.0.0): `nn::diag` abort at `sdk+0x126440`, `StandardAllocator::Initialize`, called from `nninitStartup` at `main+0x8b6710`; the heap check reports "the host stopped before nnMain". Brain Memory Training Academy's constructor at `sdk+0x134718` takes the cut at `+0x134728` / `+0x1347bc`, beside the [region alignment cut](#size-class-alignment-doubles-unitys-regions).
|
||
- [Patrick's Parabox](../../dm/games/0100E44016C22000/README.md) (Unity 2020.3.30f1, SDK 13.3.7): `StandardAllocator::Initialize` at `sdk+0x324dec`, called from `nninitStartup` at `main+0x1cd080`; the same `nninitStartup` and the same cut, on this build's constructor (`sdk+0x37c3a0`: `+0x37c3b0` / `+0x37c44c`).
|
||
- [Advance Wars 1+2: Re-Boot Camp](../../dm/games/0100300012F2A000/README.md) (Unity 2020.2.1f3, SDK 10.4.1): `StandardAllocator::Initialize` at `sdk+0x2edf90`, called from `nninitStartup` at `main+0x365310`; the heap check reports "the host stopped before nnMain"; the same `nninitStartup`. The `sdk` build is The Great Ace Attorney Chronicles', which has no virtual address memory; its constructor (`sdk+0x340e90`) has the `orr` form (`e8230b32` at `+0x340ea0`, `e9230b32` at `+0x340f3c`).
|
||
- [Pikmin 4](../../dm/games/0100B7C00933A000/README.md) (SDK 10.4.8): the build's AddressRegionManager is the 10.4.1 build's at the same offsets and takes the same three cuts (node area and alignment).
|
||
- [Super Smash Bros. Ultimate](../../dm/games/01006A800016E000/README.md) (SDK 8.2.1): constructor `sdk+0x336b40`, `orr w9, wzr, #0x3fe00000` at `+0x336b50` / `+0x336bf0` ([A 62 GB page allocator behind `nu::VirtualAllocHook`](#a-62-gb-page-allocator-behind-nuvirtualallochook)).
|
||
- [DRAGON QUEST XI S](../../dm/games/01006C300E9F0000/README.md) (SDK 7.3.2): the node area cut on that SDK build (`sdk_patches.py`).
|
||
- [Paper Mario: The Thousand-Year Door](../../dm/games/0100ECD018EBE000/README.md) (SDK 15.3.2, Celeste's `sdk` build, which has no virtual address memory): constructor `sdk+0x12e0e0`: `+0x12e0f0` / `+0x12e184`.
|
||
- [Super Mario Galaxy 2](../../dm/games/0100FD8022DAA000/README.md) (SDK 18.3.5): `sdk+0x13be4c` / `+0x13bee0` ([The movie player's own StandardAllocator](#the-movie-players-own-standardallocator)).
|
||
- [Absolum](../../dm/games/0100A83022A22000/README.md) (SDK 18.3.2): the node-area cut beside the alignment cut.
|
||
- [Octopath Traveler 0](../../dm/games/01005270232F2000/README.md) (SDK 19): SDK 19's AddressRegionManager node area is cut to 62 MB as Engage's (`sdk+0x1423fc` / `+0x142468`).
|
||
- [Pokémon Legends: Z-A](../../dm/games/0100F43008C44000/README.md) (SDK 19.3.11): the same constructor offsets, `sdk+0x1423fc` and `+0x142468`, beside a cut on `main` ([StandardAllocators over 12 to 62 GB of address space](#standardallocators-over-12-to-62-gb-of-address-space)).
|
||
- [FINAL FANTASY RESONANCE](../../dm/games/0100606026F12000/README.md) (SDK 20.5.17): the `sdk` is [STEINS;GATE RE:BOOT](../../dm/games/01008BC024FDE000/README.md)'s build; its node area takes the same cut (`sdk+0x145cbc` / `+0x145d28`).
|
||
- [Hollow Knight: Silksong](../../dm/games/010013C00E930000/README.md) (SDK 20.5.8): `sdk+0x145bfc` / `+0x145c68` ([Unity 6 takes its memory with no StandardAllocator and no heap](#unity-6-takes-its-memory-with-no-standardallocator-and-no-heap)).
|
||
- [The Legend of Zelda: Tears of the Kingdom](../../dm/games/0100F2C0115B6000/README.md) (SDK 20.5.22, the same `sdk` build as [Breath of the Wild](../../dm/games/01007EF00011E000/README.md) 1.9.0): constructor `sdk+0x145cfc`, `+0x145d0c` / `+0x145d78`; no alignment cut (`VammManager::AllocateAddressRegion`, `sdk+0x145df0`, aligns regions to 64 KB or 2 MB only). Its resource heaps live in address regions after the node area: at the first descent from the Great Sky Island the alias region's high-water mark was +2.25 GB of its 2.375 GB with 1.00 GB mapped, and the game took a memory shortage (its frame check at `main+0x1fd32ec` logs `MSF` / `R` / `Rf` and plays the memory-shortage Blood Moon, which puts Link back at the start; GitHub issue #70, 7 of 8 Mac runs from the issue's save). With an 8 GB alias region the mark went on to +2.50 GB; with the cut it stays at +1.75 GB, and neither showed the shortage.
|
||
- [BALL x PIT](../../dm/games/010086A022444000/README.md) (SDK 20.5.19): `sdk+0x145d0c` / `+0x145d78`, the SDK 20.5.22 build's offsets.
|
||
|
||
### Size-aligned regions exhaust the alias address space
|
||
|
||
- **Symptom:** a null-based write minutes into play, at the pool size minus 0x20 (Engage: a write to `NULL + 0x1fffe0`, `addr=0x1fffe0` at `main+0x9a4664`); NULL index pointers written on several job threads at once (`main+0xdb0ad4`, `0xdaea58`, `0xdadb5c`), with 32-bit mesh indices turning up inside texture objects.
|
||
- **Cause:** the alias region ran out of *address space*, not memory (the SDK's node area took about 1 GB of it), so new address regions came back empty. Unity's TLSF heap allocator (`main+0x9a45d0`) added a pool (a new 2 MB pool), and its low-level virtual allocator (vtable at `main+0x61ffeb0`; reserve `main+0x9aa0f4` → `nn::os::AllocateAddressRegion`) got no address region and returned 0. Buffers mapped fresh past 2 MB got no mapping: Unity's particle geometry scheduler (`main+0xd91e0c`) takes its vertex and index chunks from a fallback that creates and maps a fresh buffer each time (`main+0x7b9c64`; Unity's Switch renderer always takes it, as on hardware). The AddressRegionManager aligns each region by its size (64 KB under 2 MB, 2 MB under 4 MB, 4 MB under 32 MB, 32 MB under 1 GB, else 1 GB) and Unity asks for its block plus alignment slack, so a 2.25 MB block takes a 4 MB slot and a 16.25 MB block a 20 MB one. At the crash Engage held 4.17 of the 4.375 GB left after the node area (StandardAllocator 3.14 GB, Unity about 1.05 GB, the rest fragments). On hardware the alias region is 64 GB.
|
||
- **Fix:** shrink the node area with a rewriter cut for the game's SDK build (`dm/tools/sdk_patches.py`, Engage: the two `mov w8/w9, #0x3fe00000` in the constructor → 62 MB; [The SDK's address-region node area](#the-sdks-address-region-node-area)).
|
||
- **Found on:** [Fire Emblem Engage](../../dm/games/0100A6301214E000/README.md): the prologue battle crash (scripted run, about 357 s; a 12-minute scripted run now plays through the battle) and the idle attract demo crash after about 220 s (from about 180 s 32-bit mesh indices also turned up inside Unity's texture objects). With the cut an 850 s idle run is clean, with no corrupted texture objects; with the old SDK the same run dies at 159 s in Unity's heap allocator.
|
||
- **Check:** probe the SDK's `VammManager::AllocateAddressRegion` at its success path and `FreeAddressRegion` after its prologue, and sum the live regions by caller ([Measuring address regions and commits](#measuring-address-regions-and-commits)).
|
||
- **Note:** to reproduce the old state: `boot.sh <log> 245` with no input on an SDK rewritten without the cut.
|
||
|
||
### 1 GB alignment of regions of 1 GB and more
|
||
|
||
- **Symptom:** a cut engine region still does not fit; two large regions lose up to 1 GB each to alignment, depending on where `mmap` put the guest block (Absolum); the region comes 0.69 GB past the 1 GB node area, at the next 1 GB boundary (Paper Mario: The Thousand-Year Door).
|
||
- **Cause:** `AllocateAddressRegion` aligns a region of 1 GB or more to 1 GB in absolute address (SDK 18: `sdk+0x13c03c`; 32 MB from 32 MB up).
|
||
- **Fix:** a cut on that `sdk` build aligns them to 32 MB as well, beside the node-area cut (`sdk_patches.py`).
|
||
- **Found on:**
|
||
- [Absolum](../../dm/games/0100A83022A22000/README.md) (SDK 18.3.2: `sdk+0x13c03c`): the two large regions, with a 4 KB one between them, lost up to 1 GB each.
|
||
- [DRAGON QUEST XI S](../../dm/games/01006C300E9F0000/README.md) (SDK 7.3.2): 32 MB alignment for large regions on that SDK build.
|
||
- [Advance Wars 1+2: Re-Boot Camp](../../dm/games/0100300012F2A000/README.md) (SDK 10.4.1): `AllocateAddressRegion` (`sdk+0x341030`) aligns a region of 1 GB or more to 1 GB (`+0x34109c`), as DRAGON QUEST XI S's SDK 7.3.2 build, and takes the same 32 MB cut.
|
||
- [Super Smash Bros. Ultimate](../../dm/games/01006A800016E000/README.md) (SDK 8.2.1): alignment at `sdk+0x336d5c`, cut as DRAGON QUEST XI S's SDK 7.3.2 build.
|
||
- [Paper Mario: The Thousand-Year Door](../../dm/games/0100ECD018EBE000/README.md) (SDK 15.3.2, Celeste's `sdk` build): `AllocateAddressRegion`'s alignment at `sdk+0x12e2e0`, as Absolum's did.
|
||
- [Pikmin 4](../../dm/games/0100B7C00933A000/README.md) (SDK 10.4.8): the 10.4.1 build's offsets.
|
||
- [Xenoblade Chronicles 3](../../dm/games/010074F013262000/README.md) (SDK 12.3.9): large-region alignment at `sdk+0x34662c`, 1 GB to 32 MB; its compact 4 GB allocator reservation then fits after the 62 MB node area.
|
||
- **Note:** FINAL FANTASY RESONANCE's SDK 20.5.17 build aligns regions of 2 MB and more to 2 MB only (`VammManager::AllocateAddressRegion`, `sdk+0x145e00`), so it needs no alignment cut.
|
||
|
||
### Size-class alignment doubles Unity's regions
|
||
|
||
- **Symptom:** a write at NULL + 0x3fffe0 (a 4 MB pool − 0x20) at `main+0x48b770`, about six minutes into the prologue, with Unity's `ALLOC_CACHEOBJECTS_THREAD` allocator in x19; the `[mem]` high-water mark at +4.26 GB of the 4.375 GB alias region while 1.26 GB is mapped (Sea of Stars).
|
||
- **Cause:** before SDK 20, `VammManager::AllocateAddressRegion` aligns a region by its size (64 KB under 2 MB, 2 MB under 4 MB, 4 MB under 32 MB, 32 MB under 1 GB, else 1 GB) and starts the next one at that alignment past a 4 KB guard. Unity 2021.3's virtual allocator (`main+0x48deb0` → `main+0x12e12c0`) asks for N MB + 252 KB and aligns its blocks to 256 KB itself, so every region spills just past a size class: a 16.25 MB region takes a 20 MB slot, a 4.25 MB one an 8 MB slot, a 32.5 MB one 64 MB. Measured with probes on the success path (`sdk+0x134868`: x21 size, x22 address) and on `FreeAddressRegion` (`sdk+0x1350cc`: x1 address): 94 regions of 16.25 MB, 35 of 4.25 MB and 15 of 2.25 MB among 167 live regions holding 2.59 GB, with 0.86 GB of gaps between them, ending at +4.22 GB. On a Switch the alias region is 64 GB.
|
||
- **Fix:** a cut on that `sdk` build gives the 4 MB, 32 MB and 1 GB classes SDK 20's 2 MB (`sdk_patches.py`). The same route then ends its live regions (2.59 GB) at +3.24 GB with 0.58 GB of gaps, and the high-water mark stays at +3.0 GB over 20 minutes.
|
||
- **Found on:** [Sea of Stars](../../dm/games/01008C0016544000/README.md) (SDK 16.2.0: `sdk+0x134914`, `+0x13491c`, `+0x134920`), beside the node-area cut and a 1.5 GB pool.
|
||
|
||
### Unity 6 takes its memory with no StandardAllocator and no heap
|
||
|
||
- **Symptom:** nothing aborts at startup as with Unity 2022.3, but the provisional profile gave a 1 GB block and a 256 MB alias region while Unity maps 1.5 GB in the first room.
|
||
- **Cause:** `nninitStartup` (`main+0x118cb70`) only calls `QueryMemoryInfo` and `SetMemoryAllocatorForThreadLocal`; Unity's own allocator reserves address regions with `nn::os::AllocateAddressRegion` and commits pages with `AllocateMemoryPages`. `main` imports no `SetMemoryHeapSize`, and the provisional profile took that for a game living in its bss.
|
||
- **Fix:** `profile_guess.py` now gives every game with an NPDM system resource the 5 GB block (Absolum and Pokémon Legends: Z-A, heapless too, came out right with it). The node area still sits at the start of the alias region: without the cut the high-water mark was +1.00 GB at `nninitStartup` and +3.00 GB in the first room, with it (`sdk+0x145bfc` / `+0x145c68`) +2.26 GB.
|
||
- **Found on:** [Hollow Knight: Silksong](../../dm/games/010013C00E930000/README.md) (Unity 6000.0.50f1, SDK 20.5.8); [BALL x PIT](../../dm/games/010086A022444000/README.md) (Unity 6000.0.62f1, SDK 20.5.19) is the same: the provisional profile was right as generated, and its `sdk` build only needed the same cut (`sdk+0x145d0c` / `+0x145d78`, the SDK 20.5.22 build's offsets): +2.00 GB in the base. Also [Absolum](../../dm/games/0100A83022A22000/README.md) and [Pokémon Legends: Z-A](../../dm/games/0100F43008C44000/README.md) (the 5 GB rule).
|
||
|
||
### The movie player's own StandardAllocator
|
||
|
||
- **Symptom:** `nn::diag` abort from `StandardAllocator::Initialize` long after `nninitStartup`, right after the first Vulkan query pool (`sdk+0x12dad0` ← `main+0x62c52c`).
|
||
- **Cause:** the movie player initializes its own allocator with virtual address memory, `Initialize(nullptr, 384 MB)` (`main+0x62c280`); Super Mario Galaxy, on the same SDK, allocates that block from its main allocator first. The guessed 256 MB alias region holds no region after the 1 GB node area.
|
||
- **Fix:** the same node area cut for that `sdk` build (`sdk+0x13be4c` / `+0x13bee0`) and a 512 MB alias region: the region is 32 MB aligned, so it starts 64 MB in.
|
||
- **Found on:** [Super Mario Galaxy 2](../../dm/games/0100FD8022DAA000/README.md) (SDK 18.3.5); compare [Super Mario Galaxy](../../dm/games/010099C022B96000/README.md).
|
||
|
||
### Measuring address regions and commits
|
||
|
||
- **What a game really needs:** boot it once on the Mac with a 64 GB alias region (`mem_reserve 0x1030000000`, `mem_alias 0x1008000000`) with `DM_SVC=1`'s map log (Absolum, Super Smash Bros. Ultimate, Paper Mario: The Thousand-Year Door were sized this way).
|
||
- **Regions:** probe the SDK's `VammManager::AllocateAddressRegion` at its success path (`ReleaseWriteLock` return: x21 = size, x22 = address) and `FreeAddressRegion` after its prologue (x1 = address), and sum the live regions by caller. Offsets seen: Ace Attorney Investigations Collection's SDK 17.5.3 build `sdk+0x1395a8` (success path) and `sdk+0x139df0` (`FreeAddressRegion`); Absolum's SDK 18.3.2 build `sdk+0x13bf8c` (success path).
|
||
- **Commits:** probe `VammManager::AllocateMemoryPages` / `FreeMemoryPages` (x1 address, x2 size) and split them by region. On DRAGON QUEST VII Reimagined, `nn::os::AllocateMemoryPages` was probed past its `adrp` (`sdk+0x140f84`).
|
||
- **Windows:** `DM_ALIAS_WINDOWS=<window MB>` (for example `DM_ALIAS_WINDOWS=512`) logs, every 30 s, the mapped memory of each window of that size in the alias region (`hle_svc.c`); it measured Octopath Traveler 0's allocator pools and Super Smash Bros. Ultimate's battle commits.
|
||
- **Log:** the `[mem]` lines report the alias high-water mark (one line per 256 MB), the total actually mapped (page bitmap) and every heap growth.
|
||
|
||
## Engine allocators
|
||
|
||
### StandardAllocators over 12 to 62 GB of address space
|
||
|
||
- **Symptom:** `StandardAllocator::Initialize` aborts in `nninitStartup` (Pokémon Legends: Z-A: `main+0x1b2e690`); `nn::diag` abort at `sdk+0x11ff34` (`StandardAllocator::Initialize`) in `nninitStartup`, SDK 15.3.2, called from `main+0x31370c` (Paper Mario: The Thousand-Year Door).
|
||
- **Cause:** with virtual address memory the game initializes a StandardAllocator over more address space than a phone gives a process, as the Switch's 64 GB alias region allows.
|
||
- Pokémon Legends: Z-A (a 16 MB system resource) initializes its allocator over 12 GB, `StandardAllocator::Initialize(nullptr, 0x300000000)` (`main+0x1b3e3ec`). The alias region must hold the SDK's 1 GB node area and that region, 13 GB.
|
||
- Paper Mario: The Thousand-Year Door's malloc is a StandardAllocator over 62 GB of address space (`Initialize(nullptr, 0xf80000000, false)`, `main+0x3136e8`), out of which it cuts 256 MB, 16 MB and 1 MB allocators. Booted once with a 64 GB alias region, the region came 0.69 GB past the 1 GB node area, at the next 1 GB boundary: this `sdk` build (Celeste's, which has no virtual address memory) aligns regions of 1 GB and more to 1 GB, as Absolum's did ([1 GB alignment of regions of 1 GB and more](#1-gb-alignment-of-regions-of-1-gb-and-more)).
|
||
- **Fix:** a cut on `main` to 4 GB of address space, with the node-area cut on the `sdk` build ([The SDK's address-region node area](#the-sdks-address-region-node-area)) and, where that build aligns large regions to 1 GB, the alignment cut:
|
||
- Pokémon Legends: Z-A: two cuts for 2.0.2 (its `patches.py` and `sdk_patches.py`) make it fit the 5 GB block Tears of the Kingdom runs in: the allocator's address space 12 GB → 4 GB (`main+0x1b3e3e4`) and the node area 1 GB → 62 MB (the AddressRegionManager constructor, `sdk+0x1423fc` and `+0x142468`, Engage's cut). SDK 19's AddressRegionManager puts the region right after the node area (+0x3fe2c000 before the cut), not 1 GB aligned as Engage's SDK did, so a 4.25 GB alias region holds both (`mem_alias 0x110000000`).
|
||
- Paper Mario: The Thousand-Year Door: Absolum's three cuts on the `sdk` build (the AddressRegionManager constructor `sdk+0x12e0e0`: `+0x12e0f0` / `+0x12e184`; `AllocateAddressRegion`'s alignment at `+0x12e2e0`) and a cut on `main` to 4 GB of address space, as Pokémon Legends: Z-A's, put the region at alias +62 MB to +4.06 GB in Engage's 5 GB block.
|
||
- **Found on:** [Pokémon Legends: Z-A](../../dm/games/0100F43008C44000/README.md) (2.0.2, SDK 19.3.11; one boot); [Paper Mario: The Thousand-Year Door](../../dm/games/0100ECD018EBE000/README.md): the allocator maps 1.5 GB at boot and 2.0 GB by the arrival in Rogueport. The `sdk` build is [Celeste](../../dm/games/01002B30028F6000/README.md)'s. Absolum's malloc StandardAllocator (40 GB, cut to 2.5 GB) is in [.NET NativeAOT malloc and the GC region range](#net-nativeaot-malloc-and-the-gc-region-range).
|
||
- **Note:** on Pokémon Legends: Z-A, `QueryMemoryInfo` reporting more than 4 GB would take the game's big-memory path (heaps of allocatable − 4.27 GB instead of − 2.76 GB).
|
||
|
||
### Separate virtual ranges in Snowdrop's page allocator
|
||
|
||
- **Symptom:** a write to `0x20000ffff` in static initialization (`main+0x164990`), after `AllocateAddressRegion` returns `0x1803`; reducing the request to 3 GB still returns `0x1803` with the SDK's original large-region alignment. With only the outer ranges reduced, a later free crashes at `main+0x1668dc` after reading compressed file data as a slab-owner pointer.
|
||
- **Cause:** the engine reserves three adjacent virtual ranges before committing pages in them. Its enclosing reservation exceeds the iPhone's available guest address space, and the engine does not check the failed reserve before deriving the page addresses. A smaller reservation also needs the SDK's node area and large-region alignment adapted: a separate metadata reservation comes first, and the 1 GB alignment wastes the remaining contiguous space. The large-allocation child has an independent maximum page count; reducing only the outer range lets it allocate across the boundary used by `free` to distinguish allocation classes.
|
||
- **Fix:** the build-gated game cuts reduce the range sizes, their combined reservation and the large allocator's maximum page count together. The fixed metadata reserve remains large enough, and page allocation, commits and frees retain their behavior. The SDK build gets the existing [node-area cut](#the-sdks-address-region-node-area) and [32 MB alignment](#1-gb-alignment-of-regions-of-1-gb-and-more). Per-range occupancy and the original growth fallbacks remain **open** until sustained gameplay validates the smaller ranges; see the game note for the layout, cut sites and current validation.
|
||
- **Found on:** [Mario + Rabbids Sparks of Hope](../../dm/games/0100317013770000/README.md) (1.6.2225577, SDK 13.3.5).
|
||
|
||
### .NET NativeAOT malloc and the GC region range
|
||
|
||
- **Symptom:** `nn::diag` abort at `sdk+0x12dad0` in `nninitStartup`, called from `main+0x756310`.
|
||
- **Cause:** malloc's `StandardAllocator::Initialize(nullptr, 0xa00000000)`: 40 GB of address space (`main+0x28bff4`). Absolum's regions (measured on a 64 GB alias region): malloc's (2.19 GB used into the first level, 1.75 GB of it FNA3D's GPU heap, allocated whole at `FNA3D_CreateDevice`), the .NET GC's region range (3.75 GB) and a few small ones. The GC sizes its range as 5 × its hard limit (768 MB = 75% of the 1 GB its `GetPhysicalMemoryLimit`, `main+0x28bffc`, reports), and it reads `DOTNET_GCRegionRange` through `getenv` first.
|
||
- **Fix:** the host now answers `getenv` from the profile key `game_env` (`hle_os.c`; every other name goes to the SDK's own `getenv`, an empty environment), which sets 1.5 GB. The GC then uses its range from both ends, 756 MB from the start and 748 MB from the end, where it used only the first 1.14 GB of 3.75. A cut on `main` gives malloc 2.5 GB. One more thing kept it from fitting, the 1 GB alignment of large regions ([1 GB alignment of regions of 1 GB and more](#1-gb-alignment-of-regions-of-1-gb-and-more)), cut beside the node-area cut. The regions then end 4.13 GB into the 4.375 GB alias region of Engage's 5 GB block.
|
||
- **Found on:** [Absolum](../../dm/games/0100A83022A22000/README.md) (.NET NativeAOT on SDK 18.3.2).
|
||
- **Check:** boot once on the Mac with a 64 GB alias region (`mem_reserve 0x1030000000`, `mem_alias 0x1008000000`) and probe `VammManager::AllocateAddressRegion`'s success path (`sdk+0x13bf8c`: x21 size, x22 address) with `DM_SVC=1`'s map log.
|
||
|
||
### A 62 GB page allocator behind `nu::VirtualAllocHook`
|
||
|
||
- **Symptom:** the game aborts in its static initialisers, before `nnMain`, when its own 62 GB address region fails (`main+0x37c9bd0`, `AllocateAddressRegion(&p, 0xf80000000)` at `main+0x37c9a5c`).
|
||
- **Cause:** the allocator behind `nu::VirtualAllocHook` is a 64 KB page allocator whose own tables fill the region's last 14 MB, so the size, the header offset, the page count and three region-end bounds all follow from 62 GB.
|
||
- **Fix:** the profile's `patches.py` cuts all 11 instructions to a 4 GB region. The cut region still did not fit: SDK 8 has the same 1 GB node area and 1 GB alignment of regions of 1 GB and more as the newer SDKs (constructor `sdk+0x336b40`, `orr w9, wzr, #0x3fe00000` at `+0x336b50` / `+0x336bf0`; alignment at `+0x336d5c`), cut in `sdk_patches.py` as DRAGON QUEST XI S's SDK 7.3.2 build; the region then starts 64 MB into the 4.375 GB alias region of the 5 GB block.
|
||
- **Found on:** [Super Smash Bros. Ultimate](../../dm/games/01006A800016E000/README.md) (13.0.5, SDK 8.2.1). Booted once with a 64 GB alias region (`mem_reserve 0x1030000000`, `mem_alias 0x1008000000`) and `DM_SVC=1`'s map log, the game commits from the region's start upward (1.73 GB contiguous at the title) and its page tables from the top; a battle commits up to 2.5 GB (`DM_ALIAS_WINDOWS`).
|
||
|
||
### Unreal's engine address region and its options
|
||
|
||
- **Symptom:** the game calls `exit(1)` in `nninitStartup` (Octopath Traveler 0: `main+0x12e74`); `nn::diag::Abort` from `main+0x352fbec` (Unreal's fatal error) after the first commit, `AllocateMemoryPages(0, 2 MB)` while the RHI starts (`main+0x353ff8c`, result 0xe03) (Pikmin 4); a crash in `NumberLineAllocator::Compaction` at boot (DRAGON QUEST XI S, region option 4224).
|
||
- **Cause:** Unreal's `nninitStartup` needs virtual address memory and reserves one address region for the engine with `AllocateAddressRegion`, 62 GB by default (`-SwitchVirtualAddressSpaceSize=` in MB, or in bytes above 4095). The other options it reads are `-SwitchMaxMemorySize=`, `-NVNCommandBufferGranularity=`, `-NVNControlBufferSize=`, `-SwitchMainThreadStackSize=`, `-NVNTempMemoryPageSize=`, `-ResX=` / `-ResY=` and `-SwitchDisableVirtualAllocator`, which also exits. It reads them from the game arguments, which the host used to set only after `nninitStartup`.
|
||
- **Fix:** the game arguments are set before `nninitStartup` now, as on hardware, and the profile key `game_args` supplies them on iOS.
|
||
- **Found on:**
|
||
- [Octopath Traveler 0](../../dm/games/01005270232F2000/README.md) (Unreal Engine 5): with the host allocator ([Unreal's FMallocBinned3 replaced by the host allocator](#unreals-fmallocbinned3-replaced-by-the-host-allocator)) the region is capped at 2 GB; with SDK 19's node area cut the guest block is 3.75 GB.
|
||
- [DRAGON QUEST VII Reimagined](../../dm/games/0100A9D01C446000/README.md) (Unreal Engine 4.27, SDK 19.3.1): the same 62 GB region from `nninitStartup` (`main+0xe8a2c0`). With `-SwitchVirtualAddressSpaceSize=2048` the region is mapped up to +0.75 GB through the opening and the new-game menus.
|
||
- [FINAL FANTASY RESONANCE](../../dm/games/0100606026F12000/README.md) (Unreal Engine 5.6.1, SDK 20.5.17): `nninitStartup` (`main+0x13370`) reserves the same 62 GB region; the profile gives it 4064 MB in Engage's 5 GB block, as DRAGON QUEST XI S's ([Unreal 5.6's page allocator never gives address space back](#unreal-56s-page-allocator-never-gives-address-space-back)).
|
||
- [Pikmin 4](../../dm/games/0100B7C00933A000/README.md) (Unreal Engine 4.26.2, 1.1.1, SDK 10.4.8): the region option is the trap. This `nninitStartup` reserves the region only when `-SwitchVirtualAddressSpaceSize=` is absent (`ldr x8,[x22,#0x130]; cbnz x8` past the default 62 GB and its `AllocateAddressRegion`, `main+0x353f564`), so with the argument the base stayed 0 and the first commit went into Unreal's fatal error. The default is cut to 4064 MB instead (two `orr #0xf80000000` → `mov #0xfe000000`, its `patches.py`) and the profile has no `game_args`. With Engage's 5 GB block, from the boot into the Olimar prologue: up to +1.5 GB of the alias region with 1.0 GB mapped, an Unreal heap of 1 GB.
|
||
- [DRAGON QUEST XI S](../../dm/games/01006C300E9F0000/README.md) (Unreal Engine 4.18, SDK 7.3.2) reads the same option and the others from `argv[1]` on (`main+0x177b5a4`; `main+0x177c440` parses each one: `atoi`, MB up to 4095, bytes above). Its region is 16 GB by default. A value of 4224 was taken as bytes: a 4 KB region, and the first 2 MB request crashed in `NumberLineAllocator::Compaction` at boot. With the node area cut and 32 MB alignment for large regions on that SDK build (`sdk_patches.py`), a 4064 MB region starts 64 MB into Engage's 4.375 GB alias region. On the Mac the mapped total stays at 1.5 GB from the title to the first field (Cobblestone Tor), used up to +1.75 GB of the alias region; the process holds about 2.1 GB, 1.1 GB of it Metal allocations.
|
||
|
||
- **Note:** without virtual address memory there is no engine region: [OCTOPATH TRAVELER](../../dm/games/010057D006492000/README.md) (Unreal Engine 4.18, SDK 3.5.1, no system resource in its NPDM) has `nninitStartup` set a heap of pool − used (3.08 GB) and the engine runs on it, so it takes the 4 GB heap layout. The provisional profile used to give every Unreal game the 64 GB alias layout, which left this one no heap region (`SetHeapSize OOM: req 0xc5400000 > cap 0` and a `Break` in `nninitStartup`); `profile_guess.py` now gives it only with virtual address memory.
|
||
|
||
### Unreal's FMallocBinned3 replaced by the host allocator
|
||
|
||
- **Symptom:** a null read in the out-of-memory report at `main+0x1005824` when the engine region is smaller than the allocator's pools (Octopath Traveler 0).
|
||
- **Cause:** Unreal's allocator, an FMallocBinned3, reserves 37 GB of the engine region (74 pools of 512 MB, pool index = offset >> 29). The pools are uneven (at character creation the largest holds 68 MB, the 74 together 732 MB; measured with `DM_ALIAS_WINDOWS=512`), so no smaller window fits a phone's ~5 GB.
|
||
- **Fix:** the constructor call is cut (the profile's `patches.py`) and the host puts its own allocator in the object (`hle_uemalloc.c`: the FMalloc vtable on a host malloc zone, the trivial slots kept). With it the region holds only what the engine maps itself (709 MB at Octopath Traveler 0's character creation).
|
||
- **Found on:**
|
||
- [Octopath Traveler 0](../../dm/games/01005270232F2000/README.md) (Unreal Engine 5): the allocator is constructed in place in the bss.
|
||
- [DRAGON QUEST VII Reimagined](../../dm/games/0100A9D01C446000/README.md) (Unreal Engine 4.27, SDK 19.3.1): an FMallocBinned3 whose 37 GB of pools are cut out of the 62 GB region (the mapped high-water mark reached +38 GB of a 64 GB alias region at boot), so a capped region would starve it. Here the allocator is not in the bss: `GCreateMalloc` (`main+0x32758b0`) mallocs 0x1c38 bytes on the first allocation, during the static constructors, and constructs it there. Found from the commit calls (probe `nn::os::AllocateMemoryPages` past its `adrp`, `sdk+0x140f84`: every backtrace runs through the allocator's methods around `main+0x3270000`), the relative relocations that point at them (the vtable `main+0x5e4c0e8`; this build has no RTTI) and the one store of that vtable outside the destructors. The constructor call is cut and an observation gate right after it (`OBS_UE_MALLOC`, x19 = the new object) puts the host allocator's vtable in (`hle_uemalloc.c`); no code outside the allocator reads its pool base or calls its methods directly. UE 4.27's FMalloc has one FExec slot, not four, so the slot numbers differ from UE5's (Malloc 3 … GetAllocationSize 9; `Free` is `main+0x10`, right after the 16-byte module header).
|
||
- [FINAL FANTASY RESONANCE](../../dm/games/0100606026F12000/README.md) (Unreal Engine 5.6.1, SDK 20.5.17): `nninitStartup` constructs its FMallocBinned3 in place in the bss, as Octopath Traveler 0's (`main+0x77c6210`; the call at `main+0x137f0` is cut, and its constructor would reserve 38 GB). UE 5.5 and later add `MallocZeroed` / `TryMallocZeroed` after `Free`, two TLS cache calls (`MarkTLSCachesAsUsed` / `Unused`) and two cached-memory queries, so the FMalloc vtable has 32 slots (layout `UE5_6` in `hle_uemalloc.c`: Malloc 6 … Free 10, QuantizeSize 13, GetAllocationSize 14). The base vtable shows it: its three pure slots are 0, and its slots 11 and 12 are one function that calls slot 6, then `memset`.
|
||
- [Pikmin 4](../../dm/games/0100B7C00933A000/README.md) (Unreal Engine 4.26.2, 1.1.1, SDK 10.4.8): the allocator is made as in 4.27, with the same 21-slot FMalloc vtable (`GCreateMalloc` `main+0x353f770` mallocs 0x1c38 bytes and calls the constructor `main+0x35ba610`, the one store of the vtable `main+0x8da4758` outside its two destructors; found from the FMallocBinned3 error strings through the relocated slots). The cut and the gate are as DRAGON QUEST VII Reimagined's, the gate on the `ldp x29, x30` after the cut call (the `ret` case of [Observation gates where x9–x17 are dead](rewriter.md#observation-gates-where-x9x17-are-dead)).
|
||
- [OCTOPATH TRAVELER II](../../dm/games/0100A3501946E000/README.md) (Unreal Engine 4.27, 1.1.1, SDK 12.3.2): as DRAGON QUEST VII Reimagined's (`GCreateMalloc` `main+0xbecbf0`, the constructor `main+0xc4d778`, the vtable `main+0x64710b0`), with 20 slots: no `OnMallocInitialized`, so the 21st word the host copies is the next vtable's offset to top, which nothing calls.
|
||
- [SHIN MEGAMI TENSEI V: Vengeance](../../dm/games/010069C01AB82000/README.md) (1.0.3, SDK 15.3.2): the same UE4 layout and constructor replacement; its note records the measured call sites and vtable.
|
||
- **Note:** Unreal Engine 4.18 (DRAGON QUEST XI S) needs no allocator cut: the SDK's `NumberLineAllocator` hands its region out in 64 KB units (`main+0x177c4d0`), and the engine maps only what it uses.
|
||
|
||
### Thread stacks from the host's Unreal heap
|
||
|
||
- **Symptom:** an abort right at `nnMain`, under `nn::os::CreateThread` → `ThreadManagerImplByHorizon::MapAliasStack` → `StackGuardManager::MapAtRandomAddress`; with the log line, `[RED] MapMemory refused (0xd401): <stack region> <- <address outside the guest block>`.
|
||
- **Cause:** Unreal allocates the stacks of the threads it creates with FMemory (OCTOPATH TRAVELER II's game thread: `nnMain` mallocs it with 4 KB alignment at `main+0x544`; its FRunnableThreads take theirs from inside larger allocations), and `CreateThread` moves such a stack into the stack region with `svcMapMemory`. With [the host allocator](#unreals-fmallocbinned3-replaced-by-the-host-allocator) that memory is a host malloc zone outside the guest block, and `MapMemory` takes only a backed source: the heap, mapped alias pages or a module's writable data (`map_source_backed`, `hle_svc.c`).
|
||
- **Fix:** with the host allocator installed, a source outside the guest block that the host has mapped readable and writable is taken too (`dm_ue_heap_owns`, `hle_uemalloc.c`; `vm_region_64`, since malloc has no cheap lookup from an interior pointer). A refused map now logs its arguments.
|
||
- **Found on:** [OCTOPATH TRAVELER II](../../dm/games/0100A3501946E000/README.md), after the stricter source check of `MapMemory`; [Pikmin 4](../../dm/games/0100B7C00933A000/README.md) boots with the same host.
|
||
|
||
### Unreal 5.6's page allocator never gives address space back
|
||
|
||
- **Symptom:** `nn::diag` abort from `main+0x11bc`, no message, in the first battle, with about 1 GB mapped (FINAL FANTASY RESONANCE with DRAGON QUEST VII Reimagined's 2 GB region).
|
||
- **Cause:** the engine hands its region out with a page allocator of its own (`main+0x13ef0`), a bump pointer plus free lists per size class, so the address space it has used only grows, and past the region's end it calls `OnOutOfMemory` (`main+0x13c74`).
|
||
- **Fix:** the profile gives the region 4064 MB in Engage's 5 GB block, as DRAGON QUEST XI S's (profile key `game_args`). This SDK build aligns regions of 2 MB and more to 2 MB only (`VammManager::AllocateAddressRegion`, `sdk+0x145e00`), so it needs no alignment cut.
|
||
- **Found on:** [FINAL FANTASY RESONANCE](../../dm/games/0100606026F12000/README.md) (Unreal Engine 5.6.1, SDK 20.5.17). Probed where each new block comes back (`DM_PROBE=main:13fcc`: x0 the block, x21 its size, x8 the region's end): a 1 GB block at boot from a static constructor, +1.5 GB 15 s in, +2.0 GB in the battle, level at +2.03 GB over seven minutes.
|
||
|
||
### Ring heaps cut from a pool at 64 KB alignment
|
||
|
||
- **Symptom:** write to `0x268` right after the title screen (`main+0x5d6708`, a constructor whose `this` is `0x200`).
|
||
- **Cause:** `operator new` (`main+0x5a55d4`) takes the thread's ring heap, a bump allocator whose cursor starts at its base. One ring heap kept base 0, as in 13 Sentinels: the engine cuts ring heaps out of a memory pool at 64 KB GPU alignment, and the host's pool storage from malloc is only 4 KB aligned. The first allocation returned 0, which `operator new`'s null check skipped, and the next returned `0x200`.
|
||
- **Fix:** the profile key `alloc_big_align` aligns allocations of 64 KB or more to 64 KB ([dm/games/README.md](../../dm/games/README.md)); the game then plays from the title screen into its opening story scene.
|
||
- **Found on:** [Unicorn Overlord](../../dm/games/010069401ADB8000/README.md) (SDK 16.2.4, Vanillaware's engine as in [13 Sentinels: Aegis Rim](../../dm/games/01003FC01670C000/README.md); one boot).
|
||
|
||
## Mapping SVCs
|
||
|
||
### `svcQueryMemory` must report each module's real layout
|
||
|
||
- **Symptom:** C++ exception aborts in `nn::ro::detail::GetExceptionInfo`.
|
||
- **Cause:** the SDK's unwinder finds a module from `svcQueryMemory`: it walks back over Code+RX regions to the module start, forward over Code then CodeData, and reads `MOD0` for `.eh_frame_hdr`.
|
||
- **Fix:** the host reports each loaded module's real layout and reports other host memory as free (`hle_svc.c`).
|
||
- **Found on:** [Fire Emblem Engage](../../dm/games/0100A6301214E000/README.md) (its note lists it among the host changes first needed there). IL2CPP throws a C++ exception for every managed exception, so Unity games need this; Mario ([Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md)) never threw.
|
||
|
||
### `MapPhysicalMemory` in 16 KB host pages
|
||
|
||
- **Symptom:** `MapPhysicalMemory` faults on part of a page.
|
||
- **Fix:** round to 16 KB host pages.
|
||
- **Found on:** the source names no game.
|
||
|
||
### Maps past the application pool fail as on Horizon
|
||
|
||
- **Symptom:** the alias region's mapped total runs past the application pool in the first minute.
|
||
- **Cause:** `nninitStartup` allocates and frees everything allocatable (pool − used, 3.15 GB, mapped at the top of the region) and the SDK keeps the freed pages mapped; later allocations map new pages at the bottom until a map fails, and only then does it unmap the cached block and retry. The host never refused a map, so the block stayed mapped beside the 0.4 GB the game used.
|
||
- **Fix:** profile key `mem_map_limit` (`hle_svc.c`): a map past the pool fails with ResultLimitReached, and the used memory `GetInfo` reports counts the mapped pages, as on Horizon (without that, the startup probe itself, sized as total − used, was refused).
|
||
- **Found on:** [Pokémon Legends: Z-A](../../dm/games/0100F43008C44000/README.md) (2.0.2, SDK 19.3.11; one boot). Measured: one refused 4 MB map, the 3.15 GB unmap, the same map again.
|
||
|
||
### `GetInfo(UserExceptionContextAddress)` returns a real buffer
|
||
|
||
- **Symptom:** a write through a null pointer at +0x1c0 during startup.
|
||
- **Fix:** `GetInfo(UserExceptionContextAddress)` must return a real buffer.
|
||
- **Found on:** the source names no game.
|
||
|
||
## Running out of memory
|
||
|
||
### A null-pool buffer from a game heap that ran out
|
||
|
||
- **Symptom:** a null-pool buffer even with virtual pools working ([Virtual memory pools mapped onto one physical pool](nvn-objects.md#virtual-memory-pools-mapped-onto-one-physical-pool)).
|
||
- **Cause:** the game's own heap ran out.
|
||
- **Tracing it with probes** ([05](../agents/05-debugging.md#probes-without-a-debugger-dm_probe)): the gsys GPU allocator at `main+0x4290c0` and the heap's `tryAlloc` (vtable slot +0x30) show which heap, its size and every allocation into it; the `!xN` heap walk shows the free and used blocks at the moment of failure.
|
||
- **Note:** read the probe log carefully: `tryAlloc` probes show *requests*, not successes, so a running total that already exceeds the heap means an earlier request failed.
|
||
- **Found on:** [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md), [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md) (the two entries below).
|
||
|
||
### Model heaps sized by the host's command record sizes
|
||
|
||
- **Symptom:** a model load exhausts its resource heap: a null `operator new` at the world load, or `BufferInitialize: pool has no storage` followed by a null-pool crash.
|
||
- **Cause:** games size model allocations by measuring scratch command recordings; oversized host records can exceed the budgets in their resource-size tables. In Breath of the Wild, each model's heap is its table entry + 0x1008, the file goes in 4 KB aligned, and the setup (`main+0x155a2ac`) takes the rest as a frame heap (vtable `main+0x1d4f950`, head `+0xe0`, tail `+0xe8`). Its use is the same in every run; only the file's alignment pad varies (0 to 0xff8), and the entries leave room for any pad, about 0xd00 to spare on most models. Three models were a 4 KB page short, so the loads with a large pad failed. The game sizes the model's prerecorded commands by recording each mesh's draw and each shape's per-pass state into a scratch command buffer and summing `GetCommandMemoryUsed` (`main+0xf9aeb4`, `main+0xf9bb54`), and our records were larger than the driver's (draws carried the indirect and multi-draw fields, every recording began with a BEGIN record).
|
||
- **Fix:** plain draws leave unused indirect fields out and recordings start with their first command. Uniform and single vertex-buffer bindings also use compact 24-byte records instead of 32, preserving full GPU addresses and retaining full-width fallback records for ranges above `UINT32_MAX`; multi-vertex ranges retain their array representation. Replay expands the compact forms into the same canonical binding state. The native inline uniform command takes 20 bytes and the native vertex-buffer command 24, so these small differences can accumulate into an extra 4 KB GPU-pool chunk. Report the bytes actually recorded; do not underreport usage or add a game-specific heap allowance. See [What `GetCommandMemoryUsed` counts](nvn-objects.md#what-getcommandmemoryused-counts).
|
||
- **Check:** for Breath of the Wild's whole scene, probe where the setup heap is cut (`main+0x155a520` with `!x21`, the resource heap) and the end of the setup (`main+0x155a5f4` with `!x2`, the frame heap), with `DM_PROBE_HEAP=28,38,40,e0,f8 DM_PROBE_HEAPFRAME=1d4f950 DM_PROBE_HEAPWORDS=1` ([05](../agents/05-debugging.md#probes-without-a-debugger-dm_probe)). A model fits any placement when entry + 0x1008 covers the resource heap's header and object (0x310), the file, the frame heap's block header and its peak use, plus 0xff8 of pad.
|
||
- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md), [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md#memory) (buffer bindings; the game note records the failed allocation, measured sizes and validation).
|
||
|
||
### Unity's GPU pool block allocation spins at size 0
|
||
|
||
- **Symptom:** the frames stop at the first loading screen, a graphics worker at 100% CPU (17 s in, `UnityGfxDeviceWorker` running, the main thread waiting on a condition variable, no red line).
|
||
- **Cause:** the heap check's 1 GB pool (the hardware pool had used the alias region up to +4.25 GB) left the game's malloc too little: Unity's GPU memory pool block allocation (`main+0x96f430`) calls `aligned_alloc` through `main+0x866810`, halves the request after each failure, and once the request rounds to 0 it calls on for ever with a size of 0.
|
||
- **Fix:** a 2 GB pool: the game then maps up to 2.26 GB of the alias region through its menus and first battle.
|
||
- **Found on:** [Advance Wars 1+2: Re-Boot Camp](../../dm/games/0100300012F2A000/README.md) (Unity 2020.2.1f3, SDK 10.4.1), after the node-area and alignment cuts.
|
||
- **Check:** `sample <pid>` on the Mac host shows such a spin (the guest frames are `???` in the host's image list; subtract the module base from the log's `base` lines); it worked here where lldb does not.
|
||
|
||
### Host calls on small guest stacks
|
||
|
||
- **Symptom:** SIGBUS inside a host hook (`snprintf` under `mount_ex` / `h_fs_MountSave`), then the process hangs (Super Mario 3D World + Bowser's Fury); SIGBUS inside Metal (`AGCDeserializedReply`, an AGX compiler block) at an address in the guest stack region (Breath of the Wild).
|
||
- **Cause:** host hooks run on the calling guest thread's stack. Super Mario 3D World's save thread has an 8 KB stack (CreateThread stack top `0x…9fe0`, fault `0x21a0` below it), and the file system hooks size their path buffers by PATH_MAX: one file open takes 6–7 KB (`-fstack-usage`: `OpenFile` 1.3 KB → `host_path_ex` 1.9 KB → `path_component` 3.2 KB); on hardware the same call is one IPC. In Breath of the Wild a host NVN call did Metal work on a game thread with a small stack: `nvnQueueSubmitCommands` replays the frame, and ending a render pass made the driver compile its fast-clear blit program on the calling thread.
|
||
- **Fix:** every hook in `hle_fs.c` now registers through `HLE_FORCE_BIG` (`hle.h`): a compile-time stub loads the implementation into x16 and jumps to `dm_bigstack_enter` (`hle_bigstack.c`), which moves the call to a per-thread 512 KB host stack (with a guard page) when sp lies in the guest address block, passing x0–x8 and d0–d7 through. Only for functions with register arguments only (no variadic or stack arguments). iOS forbids generating code at run time, so the stubs are static. NVN functions are not registered through `HLE_FORCE`, so they got their own big-stack entry: every implemented NVN function except the `nvnCommandBuffer*` recording calls goes through `nvn_big_entry` (`nvn_entry.S`), which finds the implementation by slot and hands it to `dm_bigstack_enter` ([NVN entry points on the big host stack](nvn-objects.md#nvn-entry-points-on-the-big-host-stack)).
|
||
- **Found on:** [Super Mario 3D World + Bowser's Fury](../../dm/games/010028600EBDA000/README.md) (file system hooks); [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md) (NVN entry points).
|
||
|
||
### Deferred-release drain without an autorelease pool
|
||
|
||
- **Symptom:** the iPhone app is killed for memory; resident memory grows steadily (about 0.2 MB a frame) while the residency set and the texture counts stay flat.
|
||
- **Cause:** the deferred-release drain ran on the guest submit thread with no autorelease pool, so autoreleased Metal objects were never freed (the Metroid Dread leak).
|
||
- **Fix:** `defer_drain` has its own `@autoreleasepool`.
|
||
- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md): GitHub issue #9 (iPhone 16 Pro Max, handheld), killed in the first shrine. An 8 GB iPhone gives this app about 3.4 GB (resident plus `headroom to the system memory limit` stayed at 3,375 MB); resident memory grew from 2.6 GB at the shrine load until the kill. On the Mac a host without that pool grew 1 MB a frame in the shrine (GPU 3.0 → 4.4 GB in 2,400 frames); with it the shrine stays at 2.7–2.9 GB resident and 1.5–1.7 GB GPU, idle or walking (vmmap at 2.8 GB: about 1.6 GB guest memory, 0.75 GB private textures, 0.17 GB IOAccelerator).
|
||
- **Note:** found on the way: dead texture serials and the fixed-size pool tracking table ([Dead texture serials kept in a ghost table](nvn-objects.md#dead-texture-serials-kept-in-a-ghost-table), [Memory-pool tracking table grows in blocks](nvn-objects.md#memory-pool-tracking-table-grows-in-blocks)).
|
||
|
||
### Upload staging grows within one rendering submission
|
||
|
||
- **Symptom:** travelling produces a brief multi-GB footprint spike, then memory returns to its earlier level; periodic memory lines can miss the peak. Upload diagnostics show many swapped-out blocks still alive in one batch.
|
||
- **Cause:** `up_alloc` could append unlimited staging blocks before `up_flush`. Overflow staging then retired on the main rendering fence, so splitting uploads alone still retained completed staging while that submission was being built. Weak-table insertion and enumeration in the staging diagnostics also left temporary strong references in the submission's autorelease pool.
|
||
- **Fix:** split GPU deswizzle batches before another block would exceed 128 MiB. The three upload allocator slots provide backpressure, publishing deferred commits before waiting. Retire overflow and CPU-upload staging from the upload completion handler, remove residency and clear its retained array before freeing the slot; keep diagnostic weak-table operations in their own autorelease pools. Rendering destinations keep their existing lifetime. A single texture larger than the batch budget still gets its own block; this is not a cap on all renderer memory.
|
||
- **Check:** `make -C dm/gfx/tests GAME=<id> check_t_upload_budget` uses actual Metal uploads during an open render pass, with direct and deferred encoding and Metal validation. A 2 GiB burst retained 2,048 MiB before the fix; afterwards it peaks at 384 MiB and retains only the three reusable blocks (96 MiB) after upload completion, before the rendering fence. It also checks every uploaded pixel and the render pass's clear. For a game, capture both frequent `phys_footprint` samples and the kernel's peak ledger; the existing `resident` log label denotes footprint, and staging blocks `created in total` is cumulative, not live memory.
|
||
- **Found on:** [The Legend of Zelda: Tears of the Kingdom](../../dm/games/0100F2C0115B6000/README.md#memory), investigating issue #147. Mac reproduction and device validation are tracked in the game note.
|
||
|
||
### GPU readback without an autorelease pool
|
||
|
||
- **Symptom:** GPU memory grows with repeated `nvnTextureReadTexels` calls on a long-lived game thread, even after each readback completes.
|
||
- **Cause:** the GPU path in `mtl_read_texels` runs outside a submission's autorelease pool. Metal's temporary command buffer and blit encoder keep the readback buffer alive in the calling thread's undrained pool.
|
||
- **Fix:** `mtl_read_texels` encloses texture resolution, readback encoding, the GPU wait and the CPU copy in its own `@autoreleasepool`. The guest-memory path needs no Metal objects.
|
||
- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md), measured upstream in MacNativeGames commit `635fb31` and ported in [PR #195](https://github.com/seedds/nxnative/pull/195): about 130 readbacks a second retained 3.7 GB of GPU memory after 30 minutes. Those game measurements have not been repeated here.
|
||
- **Check:** `make -C dm/gfx/tests GAME=010015100B514000 check_t_readback` checks literal GPU and CPU readback pixels and observes the real readback buffer through a weak reference while the caller's outer pool remains open. Removing only the local pool leaves that buffer alive and fails the lifetime check.
|