diff --git a/docs/re/mission-freeze-heap-exhaustion.md b/docs/re/mission-freeze-heap-exhaustion.md index 87240dbc..a7a6282c 100644 --- a/docs/re/mission-freeze-heap-exhaustion.md +++ b/docs/re/mission-freeze-heap-exhaustion.md @@ -122,6 +122,77 @@ So `parent heap` is **the emulated console's physical memory**, not anything of the host's. The game runs a real 512 MB console down to 113 MB free and then asks for 128 MB. Nothing about the container is involved. +## ✅ The mechanism, confirmed in Xenia's source — the failed release leaks, provably + +The adjacency argument below is no longer the evidence. `src/xenia/memory.cc` +closes the loop by itself. + +**1. The number in the error is `unreserved_page_count_`.** The message is +emitted at `memory.cc:1807`: + +```cpp +XELOGE("PhysicalHeap::Alloc unable to alloc physical memory in parent heap " + "(requested {} bytes, parent free {}/{} pages)", + size, parent_heap_->unreserved_page_count(), + parent_heap_->total_page_count()); +``` + +So the `28969/131072` in the log *is* that counter, on the **parent** heap. + +**2. That counter is incremented in exactly one place** — `memory.cc:1445`, +inside `BaseHeap::Release`'s page-table loop: + +```cpp +for (uint32_t page_number = base_page_number; page_number <= end_page_number; + ++page_number) { + auto& page_entry = page_table_[page_number]; + page_entry.qword = 0; + unreserved_page_count_++; // <- the only increment +} +InsertFreeBlock(base_page_number, base_page_entry.region_page_count); +``` + +**3. The failing path returns before reaching it** — `memory.cc:1399`, the very +top of the same function: + +```cpp +if (base_page_entry.base_address != base_page_number) { + XELOGE("BaseHeap::Release failed because address is not a region start"); + return false; // <- page table untouched, no free block +} +``` + +**4. And it is the same heap.** `PhysicalHeap::Release` delegates to +`parent_heap_->Release(...)`, so the release that fails and the allocation that +later comes up short are against the same 512 MB parent heap. + +Therefore every `address is not a region start` is a release that returns +**zero** pages to the counter the allocator later consults. The pages stay +reserved for the life of the process. This is not an inference from adjacency — +it is the control flow. + +## 🟡 What is still not measured: the magnitude + +Mechanism confirmed, quantity not. 512 − 113 = **399 MB** is unaccounted for, and +there are only **22–23** failed releases per run, which would need an implausible +~18 MB average each to explain the whole deficit alone. So leaked releases are +**a** contributor and possibly not the dominant one; the game may simply hold a +lot of live memory at stage load. + +The experiment that would settle it is an allocation ledger — log every +`MmAllocatePhysicalMemoryEx` / `MmFreePhysicalMemory` with its size and balance +the books — rather than more freeze reproductions. + +## 🔴 There is no memory-size knob to work around it + +Worth stating so nobody looks: Xenia has **no cvar for guest memory size**. The +only memory cvars in `memory.cc` are `protect_zero`, `protect_on_release`, +`scribble_heap` and the MMIO ones, and +`xboxkrnl_memory.cc` says outright *"We don't support separate devkit memory, so +just ignore this flag"* when a game asks for the devkit's extra RAM. The 512 MB +is hardcoded to the retail console. So the freeze cannot be dodged by giving the +emulator more — the fix has to be the release path, in a canary build. + ## 🟡 The likely mechanism: a release path that leaks Immediately before the failed allocation, in **both** runs: