re(flight): a per-frame sampler, and nav oracles that a menu bar cannot break
WIP toward the residual flight-speed-law question (does a 1 s burst reach the
steady angular rate, or is there a per-axis multiplier?). The write-up already
concluded that host-side polling cannot answer it and named a Canary-side hook
as the tool required; that hook now exists (--frame_probe_log, committed as
auto/re-frame-probe in xenia-canary-native) and this is the harness for it.
- `rebuild_canary.sh` -- the surgical rebuild the box can actually do, kept in
the repo this time instead of in /tmp: compile only the changed objects, `ar`
them into their archive, and re-run the link command lifted out of the
generated ninja. A full `ninja` is impossible here (several TUs need dev
headers the image lacks) and the build cache cannot be re-configured. 31 s.
- `frame_burst.py` -- points the probe at the player craft's transform block
(pos-112, the three 16-byte-strided rows plus the position) and drives full
stick holds, recording each hold's start and end in the same clock the probe
stamps its lines with.
- `frame_session.sh` -- the whole run as ONE blocking foreground call, per the
session-lifetime rule; REUSE=1 drives a Canary that is already up.
- `nav_to_flight.sh` -- fly_stage.sh's navigation, split out 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 pay for it twice.
The navigation change is the one worth reading. Every screen oracle here tested
named pixels ("648,221 is white"), which is only valid while the game image sits
at a known place on the root window -- and it does not: xenia's GTK window has a
menu bar, so on this display the image is ~25 px lower and every constant reads
the wrong row. Nothing errors. One run sat 300 s in front of a plainly visible
MAIN MENU reporting "no main menu"; the next missed the title screen entirely
and let the attract movie loop for ten minutes.
So `screen_id.py` identifies screens by WHOLE-IMAGE statistics instead -- the
fraction of green UI-text pixels, the fraction of near-white pixels, and the
per-channel means -- which no vertical shift, scale or letterbox can move. It is
calibrated against known-good captures and classifies all of them correctly:
title, three different menu screens, in-flight, and four movie frames as
"other". `bin/screenshot` additionally crops the menu bar off saved evidence
shots, deriving the offset from the window's own height rather than a constant.
Not yet a finding: the run has not reached flight, so no rate has been measured.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
134
tools/re-capture/nav_to_flight.sh
Executable file
134
tools/re-capture/nav_to_flight.sh
Executable file
@@ -0,0 +1,134 @@
|
||||
#!/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; }
|
||||
|
||||
say "poking the cleared-stage mask so every story stage is selectable"
|
||||
poke w32 0x828F40C0 0x0001FFFE
|
||||
|
||||
# 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"
|
||||
Reference in New Issue
Block a user