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; }