#!/usr/bin/env bash # ONE blocking session: boot -> title screen -> arm the UI draw-order capture. # # Unlike skip_intro.sh this deliberately does NOT tap A on the title: the title # screen IS the subject, and a stray A there sends the guest into the save-data # probe (which crashed the guest on run 1 of 2026-08-18). It taps A only while a # movie is playing, and the instant the title is classified it presses F10, # which arms `log_ui_draws`' 3-frame submission-order dump. # # The log lands in the emulator's CWD, so the emulator is launched from $OUT. # # A grab that is not full-width is treated as a HARD ERROR rather than # classified. Run 1 spent 13 minutes acting on a 10-pixel sliver: the screenshot # wrapper had picked a 10x10 helper window that shares the class "xenia_canary" # with the real one. Nothing failed loudly; the classifier just returned noise. # # Usage: ui_draw_capture.sh [timeout_s] [out_dir] set -u export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98 SD="$(cd "$(dirname "$0")" && pwd)" TIMEOUT="${1:-900}" OUT="${2:-/sylph-home/re/uicap}" mkdir -p "$OUT" rm -f "$OUT"/xenia_re_ui_draws_*.log alive(){ ps -o pid=,stat= -C xenia_canary 2>/dev/null | awk '$2 !~ /^Z/ {print $1}'; } wide(){ [ "$(identify -format '%w' "$1" 2>/dev/null || echo 0)" -gt 1000 ]; } xsetroot -solid black 2>/dev/null || true # ATTACH=1 watches an emulator that is already up instead of launching one. if [ "${ATTACH:-0}" != "1" ]; then ( cd "$OUT" && nohup run-canary --log_ui_draws=true --ui_draw_capture_frames="${FRAMES:-3}" --ui_draw_capture_max="${MAXDRAWS:-20000}" ${EXTRA_FLAGS:-} >"$OUT/canary.stdout" 2>"$OUT/canary.stderr" & ) # Grace period before the liveness check: run-canary is a shell that execs the # binary, and polling `ps -C xenia_canary` in the first moments reports GONE # for a process that is merely not exec'd yet. sleep 8 fi until xdotool search --name "Xenia-canary" >/dev/null 2>&1; do [ -n "$(alive)" ] || { echo "EMULATOR GONE before the window appeared"; exit 4; } sleep 1 done win="$(xdotool search --name "Xenia-canary" | tail -1)" echo "WINDOW=$win" xdotool windowactivate "$win" 2>/dev/null; xdotool windowfocus "$win" 2>/dev/null # ARM=early presses F10 before the title exists, so a long window contains the # frame in which the screen is BUILT. Armed at the title instead, a capture only # ever sees the steady state — and on this title screen the steady state is 11 # draws a frame that never mention a sprite. if [ "${ARM:-title}" = "early" ]; then xdotool windowactivate "$win" 2>/dev/null xdotool key --window "$win" F10 xdotool key F10 echo "ARMED EARLY at ${SECONDS}s" fi deadline=$(( SECONDS + TIMEOUT )) while [ $SECONDS -lt $deadline ]; do rm -f /tmp/u1.png /tmp/u2.png screenshot /tmp/u1.png >/dev/null 2>&1; sleep 0.6 screenshot /tmp/u2.png >/dev/null 2>&1 [ -s /tmp/u1.png ] && [ -s /tmp/u2.png ] || { echo "SCREENSHOT FAILED at ${SECONDS}s"; exit 5; } wide /tmp/u2.png || { echo "GRAB IS NOT THE GAME SURFACE ($(identify -format '%wx%h' /tmp/u2.png)) at ${SECONDS}s"; exit 6; } [ -n "$(alive)" ] || { echo "EMULATOR GONE at ${SECONDS}s"; exit 4; } d=$(compare -metric RMSE /tmp/u1.png /tmp/u2.png null: 2>&1 | sed 's/ .*//' | cut -d. -f1); d=${d:-0} s=$(python3 "$SD/screen_id.py" /tmp/u2.png | awk '{print $1}') echo "t=${SECONDS}s screen=$s rmse=$d" # TARGET=menu walks one step further: tap A ONCE on the title, then arm on the # main menu. One tap, and only from a full-width grab — the run that tapped # repeatedly (off a 10-pixel sliver it had classified) crashed the guest in the # save-data probe behind that button. if [ "${TARGET:-title}" = "menu" ]; then if [ "$s" = "title" ] && [ "${tapped:-0}" = "0" ]; then echo "TITLE at ${SECONDS}s -> one A"; python3 "$SD/pad.py" tap A 0.25; tapped=1 elif [ "$s" = "menu" ]; then cp /tmp/u2.png "$OUT/menu-before-f10.png" xdotool windowactivate "$win" 2>/dev/null xdotool key --window "$win" F10 xdotool key F10 sleep 3 screenshot "$OUT/menu-after-f10.png" >/dev/null 2>&1 if ls "$OUT"/xenia_re_ui_draws_*.log >/dev/null 2>&1; then echo "MENU CAPTURED at ${SECONDS}s"; break fi echo "F10 pressed on the menu; no log yet" elif [ "$d" -gt 1500 ]; then # Still in the attract loop. Without this the menu target waits forever: # the attract movie returns to the title and away again, and a run that # only ever watches never gets there. echo "movie (rmse $d) -> skip A"; python3 "$SD/pad.py" tap A 0.25; sleep 3 fi sleep 1 continue fi if [ "$s" = "title" ] && [ "${ARM:-title}" = "early" ]; then cp /tmp/u2.png "$OUT/title-reached.png" echo "TITLE REACHED at ${SECONDS}s (capture was armed early)" break elif [ "$s" = "title" ]; then cp /tmp/u2.png "$OUT/title-before-f10.png" xdotool windowactivate "$win" 2>/dev/null xdotool key --window "$win" F10 xdotool key F10 sleep 2 screenshot "$OUT/title-after-f10.png" >/dev/null 2>&1 sleep 2 if ls "$OUT"/xenia_re_ui_draws_*.log >/dev/null 2>&1; then echo "ARMED and writing at ${SECONDS}s" # A long window is deliberately NOT waited out here: the caller polls the # log, because the interesting frames may be many seconds away. [ "${WAIT_DONE:-0}" = "1" ] || break fi echo "F10 pressed; window still open" elif [ "$d" -gt 1500 ]; then echo "movie (rmse $d) -> skip A"; python3 "$SD/pad.py" tap A 0.25; sleep 3 fi sleep 1 done grep -i "UI-CAP" "$OUT/canary.stdout" 2>/dev/null | tail -5 ls -l "$OUT"/xenia_re_ui_draws_*.log 2>/dev/null || echo "NO UI DRAW LOG" echo "UI DRAW CAPTURE SESSION DONE (emulator left running on purpose)"