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:
2026-08-14 06:03:44 +00:00
parent 4f6fcf36dc
commit 6c9380ff13
7 changed files with 512 additions and 110 deletions

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Surgical rebuild of xenia_canary inside the sylph box.
#
# The image has no cmake/ninja/clang of its own and the build/ cache cannot be
# re-configured (see project_sylph_canary_patch_toolchain memory), so a full
# `ninja` is impossible: several TUs need dev headers this image lacks. What DOES
# work is compiling only the objects that changed, refreshing their archive, and
# re-running the link command lifted out of the generated ninja file.
#
# Usage: rebuild.sh <ninja object path> [more objects...]
# e.g. rebuild.sh src/xenia/kernel/CMakeFiles/xenia-kernel.dir/Release/xboxkrnl/xboxkrnl_video.cc.o
# Objects are paths relative to build/. The archive each belongs to is derived
# from the .dir name (xenia-kernel.dir -> obj/Linux/Release/libxenia-kernel.a).
set -eu
PROJ="${PROJECT_DIR:-/home/fabi/RE - Project Sylpheed}"
BUILD="$PROJ/xenia-canary-native/build"
PFX=/sylph-home/re/toolchain
export PATH="$PFX/usr/lib/llvm-18/bin:/sylph-home/.local/bin:$PATH"
export LD_LIBRARY_PATH="$PFX/usr/lib/x86_64-linux-gnu:$PFX/usr/lib/llvm-18/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
[ $# -ge 1 ] || { echo "usage: rebuild.sh <object.o> [...]" >&2; exit 2; }
cd "$BUILD"
echo "[rebuild] compiling $# object(s)"
ninja -f build-Release.ninja "$@"
declare -A LIBS=()
for o in "$@"; do
d="${o#*CMakeFiles/}"; d="${d%%.dir/*}"
lib="obj/Linux/Release/lib$d.a"
[ -f "$lib" ] || { echo "[rebuild] no archive $lib for $o" >&2; exit 3; }
ar r "$lib" "$o"
LIBS["$lib"]=1
done
for lib in "${!LIBS[@]}"; do ranlib "$lib"; echo "[rebuild] refreshed $lib"; done
# Lift the link command out of the generated ninja rather than hard-coding it, so
# it stays correct if the object or library list ever changes.
python3 - <<'PY' > /tmp/xenia_link.sh
import re, shlex
NJ = "CMakeFiles/impl-Release.ninja"
txt = open(NJ).read()
m = re.search(r"^build bin/Linux/Release/xenia_canary: (\S+) (.*?)(?=\n\S|\n\n)",
txt, re.M | re.S)
assert m, "link build statement not found"
head = m.group(2)
# $in = the explicit inputs, up to the first | (implicit deps)
ins = head.split("\n")[0].split(" | ")[0].split(" || ")[0].strip()
body = m.group(0)
var = dict(re.findall(r"^ (\w+) = (.*)$", body, re.M))
cmd = (f'{shlex.quote("/sylph-home/re/toolchain/usr/lib/llvm-18/bin/clang++")} '
f'{var["FLAGS"]} {var["LINK_FLAGS"]} {ins} -o {var["TARGET_FILE"]} '
f'{var.get("LINK_PATH","")} {var["LINK_LIBRARIES"]}')
print(cmd)
PY
echo "[rebuild] linking bin/Linux/Release/xenia_canary"
# The image ships runtime sonames only (libgtk-3.so.0, not libgtk-3.so) and no
# libstdc++fs.a; linkshim/ supplies both as symlinks + an empty archive.
LIBRARY_PATH="$PFX/linkshim${LIBRARY_PATH:+:$LIBRARY_PATH}" bash /tmp/xenia_link.sh
ls -la bin/Linux/Release/xenia_canary
echo "[rebuild] done"