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:
Sylpheed RE agent
2026-08-26 11:14:24 +00:00
parent c5b67b17f6
commit 78f097ef1f

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