From 4f7630b7500df5237aa4ee94b372beb2fa8da830 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Wed, 19 Aug 2026 07:48:16 +0000 Subject: [PATCH] tools: two dead flags that made every emulator boot script fail silently Trying to read a third paint order off the running game turned up two bugs in the capture harness, both of which fail in ways that look like the game misbehaving rather than the script being wrong. 1. `--audio` is not a cvar in this tree, and eight boot scripts passed it. Xenia calls ShowSimpleMessageBox from ParseLaunchArguments, BEFORE logging is initialised, so the symptom is a 10x10 window, no log, no guest memory and a dialog that blocks on XIfEvent forever - i.e. a hang deep in the emulator. run-canary`s own header documents this exact trap; the scripts predate it. Removed from all eight. 2. `vgamepad` no longer exists - the uinput pad was replaced by the --hid=file driver and pad.py - but skip_intro.sh still called it. The script runs without `set -e`, so the call failed silently and the title branch pressed nothing while still exiting 0. A caller was told "TITLE -> A" with the game sitting on the title screen. It now presses through pad.py and exits 6 if that fails. The first bug is fixed and verified: the boot now reaches the title screen with PRESS (A) BUTTON. The second is fixed but does NOT unblock the title - see the next commit. --- tools/re-capture/boot_menu.sh | 2 +- tools/re-capture/challenge_probe.sh | 2 +- tools/re-capture/find_partslot.sh | 2 +- tools/re-capture/fly_stage.sh | 2 +- tools/re-capture/grab_tutorial.sh | 2 +- tools/re-capture/launch_mission.sh | 2 +- tools/re-capture/nav_probe.sh | 2 +- tools/re-capture/screen_children.py | 105 +++++++++++++++++++++++++++ tools/re-capture/skip_intro.sh | 11 ++- tools/re-capture/tutorial_capture.sh | 2 +- 10 files changed, 122 insertions(+), 10 deletions(-) create mode 100755 tools/re-capture/screen_children.py diff --git a/tools/re-capture/boot_menu.sh b/tools/re-capture/boot_menu.sh index 940177a1..d4e6fba9 100755 --- a/tools/re-capture/boot_menu.sh +++ b/tools/re-capture/boot_menu.sh @@ -37,7 +37,7 @@ ensure_display(){ rm -f /dev/shm/xenia_memory_* /dev/shm/xenia_code_cache_* 2>/dev/null ensure_display cd /sylph-home/re -nohup run-canary --audio --apu=sdl --log_mask=13 \ +nohup run-canary --apu=sdl --log_mask=13 \ --logged_profile_slot_0_xuid=E0300000EFBEA3D4 /dev/null 2>&1 & sleep 5 "$SD/skip_intro.sh" 600 || { echo "BOOT FAILED (skip_intro exit $?)"; exit 1; } diff --git a/tools/re-capture/challenge_probe.sh b/tools/re-capture/challenge_probe.sh index 8648ac27..1b496b91 100755 --- a/tools/re-capture/challenge_probe.sh +++ b/tools/re-capture/challenge_probe.sh @@ -46,7 +46,7 @@ LOG="$HOME/canary.stdout" # --- launch ------------------------------------------------------------------ say "launching canary (lavapipe, file pad) — POKE=$POKE tag=$TAG" -run-canary --audio --apu=sdl --log_mask=13 \ +run-canary --apu=sdl --log_mask=13 \ --logged_profile_slot_0_xuid=E0300000EFBEA3D4 \ --hid=file --pad_file="$XENIA_PAD_FILE" & diff --git a/tools/re-capture/find_partslot.sh b/tools/re-capture/find_partslot.sh index e662d3ed..34cbfaec 100755 --- a/tools/re-capture/find_partslot.sh +++ b/tools/re-capture/find_partslot.sh @@ -39,7 +39,7 @@ snap() { SHM=$(ls /dev/shm/xenia_memory_* 2>/dev/null | head -1); cp --sparse=al pkill -9 -x xenia_canary 2>/dev/null; sleep 1 rm -f /dev/shm/xenia_* 2>/dev/null; : > "$XENIA_PAD_FILE" say "launching" -run-canary --audio --apu=sdl --log_mask=13 \ +run-canary --apu=sdl --log_mask=13 \ --logged_profile_slot_0_xuid=E0300000EFBEA3D4 \ --hid=file --pad_file="$XENIA_PAD_FILE" & xsetroot -solid black 2>/dev/null || true diff --git a/tools/re-capture/fly_stage.sh b/tools/re-capture/fly_stage.sh index 73ca7de4..72a35d15 100755 --- a/tools/re-capture/fly_stage.sh +++ b/tools/re-capture/fly_stage.sh @@ -26,7 +26,7 @@ say "launching canary for stage $STAGE" # CANARY_EXTRA lets a caller add cvars (e.g. --frame_probe_log=...) without # forking this script; unquoted on purpose so it can carry several. # shellcheck disable=SC2086 -run-canary --audio --apu=sdl --log_mask=13 \ +run-canary --apu=sdl --log_mask=13 \ --logged_profile_slot_0_xuid=E0300000EFBEA3D4 \ --hid=file --pad_file="$XENIA_PAD_FILE" ${CANARY_EXTRA:-} & # Xvfb keeps the previous instance's framebuffer until the new one draws. diff --git a/tools/re-capture/grab_tutorial.sh b/tools/re-capture/grab_tutorial.sh index 6574c975..d13798d4 100755 --- a/tools/re-capture/grab_tutorial.sh +++ b/tools/re-capture/grab_tutorial.sh @@ -42,7 +42,7 @@ rm -f /dev/shm/xenia_memory_* /dev/shm/xenia_code_cache_* 2>/dev/null ensure_display cd /sylph-home/re -setsid nohup run-canary --audio --apu=sdl --log_mask=13 \ +setsid nohup run-canary --apu=sdl --log_mask=13 \ --logged_profile_slot_0_xuid=E0300000EFBEA3D4 /dev/null 2>&1 & sleep 5 "$RC/skip_intro.sh" 600 || { echo "BOOT FAILED"; exit 1; } diff --git a/tools/re-capture/launch_mission.sh b/tools/re-capture/launch_mission.sh index 5cf8449f..4e2739cd 100755 --- a/tools/re-capture/launch_mission.sh +++ b/tools/re-capture/launch_mission.sh @@ -47,7 +47,7 @@ rm -f /dev/shm/xenia_memory_* /dev/shm/xenia_code_cache_* 2>/dev/null ensure_display cd /sylph-home/re -nohup run-canary --audio --apu=sdl --log_mask=13 \ +nohup run-canary --apu=sdl --log_mask=13 \ --logged_profile_slot_0_xuid=E0300000EFBEA3D4 /dev/null 2>&1 & sleep 5 "$SD/skip_intro.sh" 600 || { echo "BOOT FAILED (skip_intro exit $?)"; exit 1; } diff --git a/tools/re-capture/nav_probe.sh b/tools/re-capture/nav_probe.sh index febb0f8b..ae6ec1c9 100755 --- a/tools/re-capture/nav_probe.sh +++ b/tools/re-capture/nav_probe.sh @@ -39,7 +39,7 @@ pkill -x xenia_canary 2>/dev/null; sleep 2 rm -f /dev/shm/xenia_memory_* /dev/shm/xenia_code_cache_* 2>/dev/null ensure_display cd /sylph-home/re -nohup run-canary --audio --apu=sdl --log_mask=13 \ +nohup run-canary --apu=sdl --log_mask=13 \ --logged_profile_slot_0_xuid=E0300000EFBEA3D4 /dev/null 2>&1 & sleep 5 "$SD/skip_intro.sh" 600 || { echo "BOOT FAILED (skip_intro exit $?)"; exit 1; } diff --git a/tools/re-capture/screen_children.py b/tools/re-capture/screen_children.py new file mode 100755 index 00000000..d60d4939 --- /dev/null +++ b/tools/re-capture/screen_children.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Dump the PAINT ORDER of every live UI screen object, off a running Canary. + +A screen object (vtable `0x820b30b4`) holds its elements at `+0x08` as +`{ptr, count, capacity}` over 48-byte records in **declaration** order, and a +second list at `+0x30` — pointers to the same records, in the order the screen +is **painted**. `docs/re/structures/ui-screen-runtime.md` establishes both. + +This walks every resident screen object and prints, per object, the permutation +and each element's pivot, which is what identifies the build in the file (a +declaration entry's pivot is exactly half the decoded sprite). + + screen_children.py [/dev/shm/xenia_memory_XXX] + +Point `$GMEM_FILE` at a snapshot to work offline: + cp --sparse=always /dev/shm/xenia_memory_* /tmp/snap.bin +""" +import os +import struct +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import gmem + +VTABLE = 0x820B30B4 +ELEMENTS_AT = 0x08 # {ptr, count, capacity} -> 48-byte element records +CHILDREN_AT = 0x30 # {ptr, count, capacity} -> pointers into those records +RECORD = 48 + + +def main(): + path = gmem.mem_path() + size = os.path.getsize(path) + with open(path, "rb") as f: + def rd(va, n): + f.seek(gmem.va_to_off(va)) + return f.read(n) + + def u32(va): + return struct.unpack(">I", rd(va, 4))[0] + + def f32(va): + return struct.unpack(">f", rd(va, 4))[0] + + # every object whose word 0 is the screen vtable + objs = [] + pat = struct.pack(">I", VTABLE) + CHUNK = 1 << 24 + for start, end in gmem.extents(f.fileno(), size): + pos = start + while pos < end: + f.seek(pos) + buf = f.read(min(CHUNK, end - pos)) + if not buf: + break + i = buf.find(pat) + while i != -1: + va = gmem.primary_va(pos + i) + if va is not None: + objs.append(va) + i = buf.find(pat, i + 1) + pos += len(buf) + + print(f"{len(objs)} object(s) with vtable {VTABLE:#010x}") + for obj in objs: + try: + eptr, ecount = u32(obj + ELEMENTS_AT), u32(obj + ELEMENTS_AT + 4) + cptr, ccount = u32(obj + CHILDREN_AT), u32(obj + CHILDREN_AT + 4) + except ValueError: + continue + if not (0 < ecount <= 4096) or eptr == 0: + continue + # element record pointer -> declaration index + index_of, pivots = {}, [] + ok = True + for i in range(ecount): + rec = eptr + i * RECORD + try: + index_of[u32(rec)] = i + pivots.append((f32(rec + 0x10), f32(rec + 0x14))) + except ValueError: + ok = False + break + if not ok: + continue + print(f"\n== object {obj:#010x}: {ecount} elements, {ccount} children") + print(" pivots: " + " ".join(f"{i}:({x:g},{y:g})" for i, (x, y) in enumerate(pivots))) + if not (0 < ccount <= 4096) or cptr == 0: + print(" (no child array)") + continue + order, unknown = [], 0 + for i in range(ccount): + p = u32(cptr + i * 4) + if p in index_of: + order.append(index_of[p]) + else: + order.append(None) + unknown += 1 + print(f" paint order: {order}") + if unknown: + print(f" ({unknown} child pointer(s) did not match an element record)") + + +if __name__ == "__main__": + main() diff --git a/tools/re-capture/skip_intro.sh b/tools/re-capture/skip_intro.sh index c4548ce5..9959467f 100755 --- a/tools/re-capture/skip_intro.sh +++ b/tools/re-capture/skip_intro.sh @@ -15,6 +15,13 @@ set -u export HOME=/sylph-home/re DISP="${DISPLAY:-:98}" alive(){ ps -o pid=,stat= -C xenia_canary 2>/dev/null | awk '$2 !~ /^Z/ {print $1}'; } +# Press through pad.py, NOT `vgamepad`. That command no longer exists — the +# uinput pad was replaced by the `--hid=file` driver — and because this script +# runs without `set -e`, calling it failed silently: the title branch pressed +# nothing and still `exit 0`, so a caller was told "TITLE -> A" while the game +# sat on the title screen forever. Fail loudly instead. +SD_SI="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +tapA(){ python3 "$SD_SI/pad.py" tap A 0.25 || { echo "PAD PRESS FAILED"; exit 6; }; } # The X root keeps the DEAD session's last frame, so a fresh launch would be # detected as "already at the title". Blank it, and wait for the new window. xsetroot -solid black 2>/dev/null || true @@ -38,10 +45,10 @@ while [ $SECONDS -lt $deadline ]; do d=${d:-0} read -r r g b < <(convert /tmp/f2.png -format "%[fx:int(255*p{625,618}.r)] %[fx:int(255*p{625,618}.g)] %[fx:int(255*p{625,618}.b)]" info:) if [ "$g" -gt 130 ] && [ $((g - r)) -gt 45 ] && [ $((g - b)) -gt 45 ]; then - echo "TITLE at ${SECONDS}s -> A"; vgamepad tap A 250; exit 0 + echo "TITLE at ${SECONDS}s -> A"; tapA; exit 0 fi if [ "$d" -gt 1500 ]; then - echo "movie (rmse $d) at ${SECONDS}s -> skip A"; vgamepad tap A 250; sleep 3 + echo "movie (rmse $d) at ${SECONDS}s -> skip A"; tapA; sleep 3 fi sleep 1 done diff --git a/tools/re-capture/tutorial_capture.sh b/tools/re-capture/tutorial_capture.sh index 6ea5638b..9aeb4bc3 100755 --- a/tools/re-capture/tutorial_capture.sh +++ b/tools/re-capture/tutorial_capture.sh @@ -36,7 +36,7 @@ if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then fi cd /sylph-home/re -nohup run-canary --audio --apu=sdl --log_mask=13 \ +nohup run-canary --apu=sdl --log_mask=13 \ --logged_profile_slot_0_xuid=E0300000EFBEA3D4 /dev/null 2>&1 & sleep 5 "$SD/skip_intro.sh" 600 || { echo "BOOT FAILED (skip_intro exit $?)"; exit 1; }