re: the mission freeze is a failed 128 MB guest allocation, not an emulator hang
Reproduced on a Stage 02 run and root-caused. Two witnesses, both taken while
screen_id.py was calling the screen "flight": three screenshots over 8 s at rmse
0.00 with 0.00% of pixels changed, and a 4 MB slab of guest RAM with 0 bytes
changed over 2 s. The emulator is not deadlocked -- 399% CPU over 15 running
threads. It spins while the guest does nothing.
The log stops mid-stage-load on:
PhysicalHeap::AllocRange unable to alloc physical memory in parent heap
(requested 134217728 bytes, parent free 28969/131072 pages)
MmAllocatePhysicalMemoryEx: Allocation failed Size: 08000000
Guest attempted to throw a C++ exception!
128 MB requested against ~113 MB free. So it is not only fragmentation, which
is what "failed to find contiguous range" suggests on its own -- there was less
free memory in that heap than the request needed at all. Preceded by repeated
"BaseHeap::Release failed because address is not a region start", which
challenge-mission-gate.md already notes leaks the range; a leak that repeats
through a session supplies the mechanism the freeze's variable onset (27, 45,
83, 183 s) needs.
This refutes a standing claim. challenge-mission-gate.md §5.6 concludes that
poking only real story ids (0x0001FFFE) does not blow the heap. This run poked
exactly that and hit the same 128 MB failure. Bounded, though: there the
failure was on entering MISSION SELECT, here MISSION SELECT worked and the
failure came at the take-off load -- so the reading is that the poke value is
not what decides it. That page's own open question, repeat without the poke,
is now the load-bearing experiment and is still unrun.
Also recorded: a frozen game passes the screen classifier. A single-frame
statistic cannot distinguish flight from frozen-in-flight, which is why the
entity probes returned 0 definitions, 0 movers and 0 vtable hits with no sign
anything was wrong. A second frame costs nothing and is decisive.
This commit is contained in:
78
docs/re/mission-freeze-heap-exhaustion.md
Normal file
78
docs/re/mission-freeze-heap-exhaustion.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# The mission freeze is a failed 128 MB guest allocation
|
||||
|
||||
**✅ Root-caused 2026-08-26** on a Stage 02 run. The freeze this corpus lists as
|
||||
"THE blocker" is not a hang in the emulator: it is the *guest* asking for 128 MB
|
||||
of physical memory, being refused, throwing, and stopping.
|
||||
|
||||
## The freeze, measured rather than assumed
|
||||
|
||||
Two independent witnesses, both taken while `screen_id.py` was calling the
|
||||
screen `flight`:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| three screenshots over 8 s | **rmse 0.00**, `0.00 %` of pixels changed |
|
||||
| a 4 MB slab of guest RAM re-read after 2 s | **0 bytes changed** |
|
||||
|
||||
The emulator process is *not* deadlocked — 399 % CPU across 15 running threads.
|
||||
It is spinning while the guest does nothing.
|
||||
|
||||
## The cause is in the emulator log, and it is specific
|
||||
|
||||
The log stops dead mid-stage-load, last four lines:
|
||||
|
||||
BaseHeap::Alloc failed to find contiguous range
|
||||
PhysicalHeap::AllocRange unable to alloc physical memory in parent heap
|
||||
(requested 134217728 bytes, parent free 28969/131072 pages)
|
||||
MmAllocatePhysicalMemoryEx: Allocation failed: 00000000 Size: 08000000
|
||||
Guest attempted to throw a C++ exception!
|
||||
|
||||
`0x08000000` is **128 MB**. The parent heap holds 131 072 pages and had **28 969
|
||||
free — about 113 MB**. So this is not only fragmentation, as
|
||||
`failed to find contiguous range` suggests on its own: there was **less free
|
||||
memory in that heap than the request needed**, contiguous or not.
|
||||
|
||||
Immediately before it, repeatedly:
|
||||
|
||||
BaseHeap::Release failed because address is not a region start
|
||||
PhysicalHeap::Release failed due to parent heap failure
|
||||
|
||||
[`challenge-mission-gate.md`](challenge-mission-gate.md) already noted that this
|
||||
failed release "leaks the range". A leak that repeats through a session gives the
|
||||
mechanism the freeze's **variable onset** (27, 45, 83, 183 s across runs) needs:
|
||||
free pages drain until some 128 MB request loses, and *when* that happens depends
|
||||
on how much has leaked, not on the clock.
|
||||
|
||||
## 🔴 This refutes "poking only real story ids does not blow the heap"
|
||||
|
||||
`challenge-mission-gate.md` §5.6 records that poking `0xFFFFFFFF` into the
|
||||
cleared-stage mask blows the heap on a 128 MB request, and concludes:
|
||||
|
||||
> Poking only real story ids (`0x0001FFFE` = stages 1–16) does **not** blow the heap.
|
||||
|
||||
This run poked **`0x0001FFFE`** — the value called safe — and hit the **same 128 MB
|
||||
failure**, with `parent free 28969/131072` against that page's `30633/131072`.
|
||||
|
||||
🟡 The two are not identical and the refutation is bounded: there, the failure was
|
||||
on entering `MISSION SELECT`; here `MISSION SELECT` and the stage list worked, and
|
||||
the failure came later, during the **take-off load**. So the correct reading is
|
||||
not "the safe value is unsafe" but that **the poke value is not what decides it** —
|
||||
both values fail the same 128 MB request, at whichever load happens to arrive when
|
||||
free pages have drained below it. That page's own open question — *"repeat without
|
||||
the poke to see whether it fails the same way regardless"* — is now the load-bearing
|
||||
experiment, and it is still unrun.
|
||||
|
||||
## ⚠️ A frozen game passes the screen classifier
|
||||
|
||||
`AGENT.md` says to classify screens by whole-image statistics rather than named
|
||||
pixels, and that is right. But a *single-frame* statistic cannot tell **flight**
|
||||
from **frozen in flight** — `screen_id.py` called every frozen frame `flight`,
|
||||
which is exactly why the entity probes returned 0 definitions, 0 movers and 0
|
||||
vtable hits with no indication anything was wrong.
|
||||
|
||||
**A second frame costs nothing and is decisive.** Any probe that assumes the game
|
||||
is live should diff two frames a few seconds apart, or re-read a slab of guest
|
||||
RAM, before believing an empty result means "the thing is not there".
|
||||
|
||||
❔ Still open: why the guest needs a single 128 MB block, whether the heap can be
|
||||
given more, and whether the failure is reachable without the mask poke at all.
|
||||
Reference in New Issue
Block a user