challenge-mission-gate.md §5.6 attributes the 128 MB heap failure to a careless
cleared-stage mask poke, concludes that poking only real story ids does not blow
the heap, and ends by asking for the control: repeat without the poke.
Ran it. nav_to_flight.sh gains SYLPH_NO_POKE=1, which skips the write; only
Stage 1 is selectable without it, so the control changes stage too, which makes
the agreement stronger rather than weaker.
poked 0x0001FFFE Stage 02 frozen, 128 MB request refused
control untouched Stage 01 frozen, 128 MB request refused
Both logs carry not merely the same error but the same numbers:
requested 134217728 bytes, parent free 28969/131072 pages
28969 in both, across two stages and two boots. So the poke does not cause it
and neither does the stage; the guest reproducibly arrives at a 128 MB request
with ~113 MB free. An identical free-page count across independent runs also
says the allocation pattern is deterministic -- not a race, not host pressure.
The control was verified three ways, because the first attempt was confounded:
two emulators were alive at once (the previous one survived a pkill). The mask
was read back as 0x0 from the live mapping, the log was confirmed to be this
run's, and the liveness test was repeated after killing the stale process so
exactly one emulator was running -- three frames at rmse 0.00, and an 8 MB slab
of guest RAM with 0 bytes changed over 3 s.
142 lines
6.3 KiB
Bash
Executable File
142 lines
6.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Drive an ALREADY-RUNNING Canary from the title/attract movie to flight on any
|
|
# story stage. Split out of fly_stage.sh so a live emulator can be re-used: a
|
|
# boot to the title costs minutes under lavapipe, and a run that only failed to
|
|
# navigate should not have to pay for it twice.
|
|
#
|
|
# Poking ONE word marks every story stage cleared, so MISSION SELECT will launch
|
|
# any of them — 0x828F40C0 = 0x0001FFFE (cleared-stage mask, bits 1..16); see
|
|
# docs/re/challenge-mission-gate.md. Nothing is written to disc.
|
|
#
|
|
# Usage: nav_to_flight.sh <stage 1..16> [menu_timeout_s] [--snapshot]
|
|
set -u
|
|
STAGE="${1:?usage: nav_to_flight.sh <stage 1..16>}"
|
|
MENU_TIMEOUT="${2:-400}"
|
|
SNAPSHOT="${3:-}"
|
|
# DISPLAY is overridable: the container entrypoint's own Xvfb owns :99's lock as
|
|
# root, and once that server dies an unprivileged relaunch cannot clear
|
|
# /tmp/.X99-lock — so a session may have to bring its display up elsewhere.
|
|
export HOME=/sylph-home/re DISPLAY="${DISPLAY:-:99}" SDL_AUDIODRIVER=dummy
|
|
export XENIA_PAD_FILE=/tmp/xenia_pad.txt
|
|
|
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
# bin/screenshot crops the emulator's menu bar away so a saved shot looks like
|
|
# the bare game image; the oracles below no longer depend on that, but the shots
|
|
# kept for evidence are easier to compare against older ones.
|
|
export PATH="$HERE/bin:$PATH"
|
|
pad() { python3 "$HERE/pad.py" "$@"; }
|
|
poke() { python3 "$HERE/gpoke.py" "$@"; }
|
|
SHOTS="$HOME/shots"; mkdir -p "$SHOTS"
|
|
say() { echo "[$(date +%H:%M:%S)] $*"; }
|
|
shot() { screenshot "$SHOTS/fly$STAGE-$1.png" >/dev/null 2>&1; }
|
|
screen() { python3 "$HERE/screen_id.py" /tmp/nav-probe.png --json 2>/dev/null; }
|
|
|
|
# Screens are identified by WHOLE-IMAGE statistics (screen_id.py), never by named
|
|
# pixels. The pixel oracles this replaces were measured against a bare 1280x720
|
|
# game image, and xenia's window puts a menu bar above it on some displays -- so
|
|
# every constant read ~25 px too high, silently. That cost one run 300 s staring
|
|
# at a visible MAIN MENU and another ten minutes of attract movie.
|
|
# One grab per poll, classified once: a screenshot costs seconds while lavapipe
|
|
# has every core, so asking three separate questions per second does not work.
|
|
which_screen() {
|
|
screenshot /tmp/nav-probe.png >/dev/null 2>&1 || { echo none; return; }
|
|
screen | python3 -c 'import json,sys; print(json.load(sys.stdin)["screen"])' 2>/dev/null || echo none
|
|
}
|
|
|
|
pgrep -x xenia_canary >/dev/null || { say "no xenia_canary running"; exit 1; }
|
|
|
|
say "waiting for the main menu"
|
|
MENU=0
|
|
LAST=
|
|
for i in $(seq 1 "$MENU_TIMEOUT"); do
|
|
NOW=$(which_screen)
|
|
[ "$NOW" != "$LAST" ] && { say " screen: $NOW"; LAST=$NOW; }
|
|
# Two consecutive reads, because a movie frame can momentarily look like a menu.
|
|
if [ "$NOW" = menu ] && sleep 1 && [ "$(which_screen)" = menu ]; then
|
|
say "MAIN MENU after ${i}s"; MENU=1; break
|
|
fi
|
|
[ "$NOW" = title ] && { say " title — tapping A"; pad tap A 0.25; sleep 2; }
|
|
sleep 1
|
|
done
|
|
[ "$MENU" = 1 ] || { say "TIMEOUT: no main menu"; shot 00-timeout; exit 1; }
|
|
|
|
# SYLPH_NO_POKE=1 skips the mask poke, so a run can test whether the 128 MB
|
|
# heap failure in mission-freeze-heap-exhaustion.md depends on it. Only Stage 1
|
|
# is selectable without it.
|
|
if [ "${SYLPH_NO_POKE:-0}" = "1" ]; then
|
|
say "SYLPH_NO_POKE=1 — NOT poking the cleared-stage mask (Stage 1 only)"
|
|
else
|
|
say "poking the cleared-stage mask so every story stage is selectable"
|
|
poke w32 0x828F40C0 0x0001FFFE
|
|
fi
|
|
|
|
# main menu -> EXTRAS -> MISSION SELECT
|
|
for _ in 1 2 3 4; do pad dpad down 0.06; sleep 0.35; done
|
|
sleep 0.5; pad tap A 0.15; sleep 3; shot 01-extras
|
|
pad tap A 0.15; sleep 4; shot 02-missionselect
|
|
|
|
# the list starts on Stage01
|
|
say "stepping to Stage$STAGE"
|
|
for _ in $(seq 2 "$STAGE"); do pad dpad down 0.06; sleep 0.30; done
|
|
sleep 1; shot 03-selected
|
|
pad tap A 0.20; sleep 3; shot 04-after-A
|
|
pad tap A 0.20; sleep 3; shot 05-after-A2
|
|
|
|
# Wait for a screen instead of sleeping a guess. The fixed sleeps below used to be
|
|
# enough and then were not: one run's stage load ran long, the script pressed START
|
|
# into a black loading screen, and every later step went to nothing while the shots
|
|
# recorded a plausible-looking sequence. Same lesson as the menu oracles.
|
|
wait_not_black() { # $1 = timeout s
|
|
for _ in $(seq 1 "$1"); do
|
|
if screenshot /tmp/nav-probe.png >/dev/null 2>&1; then
|
|
lit=$(screen | python3 -c 'import json,sys; d=json.load(sys.stdin); print(1 if d.get("r",0)+d.get("g",0)+d.get("b",0) > 20 else 0)' 2>/dev/null)
|
|
[ "$lit" = 1 ] && return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Selecting a stage lands on the mission BRIEFING (A: Continue), which leads to the
|
|
# READY ROOM (TAKE OFF / BRIEFINGS / HANGAR / ...) with BRIEFINGS pre-highlighted.
|
|
# A snapshot taken at the briefing yields ZERO unit-definition objects — they are
|
|
# instantiated at stage load proper — so the run has to reach flight.
|
|
# START (Skip) clears the briefing. `A` does NOT: it pages through the brief and
|
|
# ten taps still left the run sitting on a briefing screen, twice. One START press
|
|
# lands on the READY ROOM.
|
|
say "waiting for the stage to finish loading"
|
|
if ! wait_not_black 180; then
|
|
say "STILL BLACK after 180s — the load hung (this happens; check the log for"
|
|
say " 'PhysicalHeap::Release failed'), aborting instead of pressing into nothing"
|
|
shot 06-stuck-black
|
|
exit 2
|
|
fi
|
|
sleep 3
|
|
say "skipping the briefing (START, not A)"
|
|
pad tap START 0.30; sleep 6
|
|
shot 06-readyroom
|
|
say "READY ROOM -> TAKE OFF (one up from the pre-highlighted BRIEFINGS)"
|
|
pad dpad up 0.06; sleep 1; shot 07-takeoff-hl
|
|
pad tap A 0.20; sleep 8
|
|
# TAKE OFF starts its own load, and it hangs black just as the stage load can --
|
|
# guarding only the first transition left a run pressing A into a black screen and
|
|
# then reporting "player entity not found" from a game that never reached flight.
|
|
say "waiting for the take-off load"
|
|
if ! wait_not_black 180; then
|
|
say "STILL BLACK after take-off — load hung, aborting"
|
|
shot 08-stuck-black
|
|
exit 2
|
|
fi
|
|
for _ in $(seq 1 6); do pad tap A 0.20; sleep 4; done
|
|
say "waiting for flight to settle"
|
|
sleep 25
|
|
shot 08-flight
|
|
if [ "$SNAPSHOT" = "--snapshot" ]; then
|
|
SHM=$(ls /dev/shm/xenia_memory_* 2>/dev/null | head -1)
|
|
if [ -n "$SHM" ]; then
|
|
OUT="$HOME/snap-stage$STAGE.bin"
|
|
cp --sparse=always "$SHM" "$OUT" && say "snapshot -> $OUT ($(du -h "$OUT" | cut -f1) actual)"
|
|
fi
|
|
fi
|
|
say "shots: $SHOTS/fly$STAGE-*.png"
|