tools: capture the title's blend states, and replicate the menu in a second session

Completes the reach of ui-blend-mode-measured.md, which was explicitly two
screens and one session and explicitly not the title.

Three things it does differently from menu_blend_capture.sh, each one paid for
yesterday: a 1200 s title deadline instead of 420 (the 420 fired while the game
was minutes from the settled title); the title arm is gated on the plate pulse
because build 2's PRESS (A) plate is part of what is being measured; and on
timeout it leaves the emulator up and says to attach rather than reboot.

Committed before it is run, per METHOD.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
This commit is contained in:
sylph-decoder
2026-08-31 06:11:50 +00:00
parent 4718e90c16
commit 01025ab494

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# Boot -> TITLE -> arm the UI draw/blend capture there -> (A) -> MAIN MENU -> arm again.
#
# Completes the blend table started in docs/re/structures/ui-blend-mode-measured.md,
# whose stated reach was "two screens, one session" and explicitly NOT the title.
# The second arm at the menu is deliberate: it replicates the main-menu result in a
# SECOND session, which is the part of that page's reach a rerun can actually widen.
#
# Differences from menu_blend_capture.sh, each one paid for:
#
# * DEADLINE defaults to 1200 s, not 420. On 2026-08-31 a 420 s wait reported
# "NEVER REACHED THE TITLE" and the emulator, left running, was at the settled
# title minutes later and took (A) first try. A timeout measures the timeout.
# * The title arm is gated on the PLATE PULSE (wait_plate_pulse.py), not on
# screen_id alone: build 2's `PRESS (A)` plate is part of what is being
# measured, and it pulses.
# * On timeout it LEAVES THE EMULATOR UP and says so, so the run can be rescued
# by attaching instead of rebooting.
# * No --log_ui_draws. Canary's own source records that arming is unconditional
# now and that launching with the flag correlates with the title refusing (A),
# 0 of 7 runs against 4 of 5 without.
#
# F10 opens Xenia's menu bar, and any Xenia UI makes IsUIActive() true, after
# which XamInputGetKeystrokeEx returns SUCCESS with a zeroed keystroke and the
# guest's unbounded pump queues one entry per poll -- the crash in
# docs/re/structures/title-a-press-fault.md. So the surface is clicked
# immediately after every F10, and stdout is size-checked afterwards.
#
# Usage: title_blend_capture.sh [out_dir]
set -u
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
SD="$(cd "$(dirname "$0")" && pwd)"
OUT="${1:-/sylph-home/re/titleblend}"
DEADLINE="${DEADLINE:-1200}"
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}'; }
shot(){ screenshot "$1" >/dev/null 2>&1; }
screen(){ shot /tmp/tbc.png; python3 "$SD/screen_id.py" /tmp/tbc.png | awk '{print $1}'; }
arm(){ # $1 = label
xdotool windowactivate --sync "$win"; sleep 1
xdotool key F10; sleep 2
xdotool mousemove 900 400 click 1; sleep 2
echo "ARMED ($1) at ${SECONDS}s -- stdout $(stat -c%s "$OUT/canary.stdout") bytes"
}
( cd "$OUT" && nohup run-canary --mem_watch=false \
--ui_draw_capture_frames="${FRAMES:-4}" --ui_draw_capture_max="${MAXDRAWS:-4000}" \
--logged_profile_slot_0_xuid=B13EBABEBABEBABE \
>"$OUT/canary.stdout" 2>"$OUT/canary.stderr" & )
sleep 8
until xdotool search --name "Xenia-canary" >/dev/null 2>&1; do
[ -n "$(alive)" ] || { echo "EMULATOR GONE"; exit 4; }; sleep 1
done
win="$(xdotool search --name "Xenia-canary" | tail -1)"
echo "WINDOW=$win"
s=""
until [ "$s" = "title" ]; do
[ $SECONDS -lt $DEADLINE ] || { echo "TIMEOUT at ${SECONDS}s, screen=$s -- EMULATOR LEFT RUNNING, attach rather than reboot"; exit 1; }
s="$(screen)"; echo "t=${SECONDS}s $s"
[ "$s" = "title" ] || sleep 4
done
echo "TITLE at ${SECONDS}s"
# The plate is part of the title. Gate on its pulse before arming.
timeout 180 python3 "$SD/wait_plate_pulse.py" || echo " (plate gate did not fire; arming anyway)"
shot "$OUT/title.png"
arm title
ls -l "$OUT"/xenia_re_ui_draws_*.log 2>/dev/null || echo "NO TITLE LOG -- F10 was lost"
# One tap. The title that ENDS the boot accepts a single (A); repeating is pointless.
python3 "$SD/pad.py" tap A 0.3
for _ in 1 2 3 4 5 6; do
sleep 4; s="$(screen)"; echo " after A: $s"
[ "$s" = "menu" ] && break
done
shot "$OUT/menu.png"
if [ "$s" = "menu" ]; then
arm menu
else
echo "NO MENU (screen=$s) -- title capture kept"
fi
ls -l "$OUT"/xenia_re_ui_draws_*.log 2>/dev/null
grep -i "UI-CAP" "$OUT/canary.stdout" | tail -6
echo "stdout $(stat -c%s "$OUT/canary.stdout") bytes (a guest fault dumps ~100 MB/30 s)"
pkill -x xenia_canary 2>/dev/null; sleep 2
echo "DONE at ${SECONDS}s"