Files
Sylpheed/tools/re-capture/menu_blend_capture.sh
sylph-decoder 97b0362bbd tools: the EXTRAS walk pressed (A) on NEW GAME -- fix the stop test
'Press until the cursor stops moving' broke on its first comparison: one DOWN
was dropped, the row read the same twice, and it concluded the cursor had
stopped while sitting on the first item. It then pressed (A) on NEW GAME. No
harm this time -- neither press registered and the screen stayed put -- but that
is luck, not the design.

A stop test that cannot distinguish 'at the end' from 'the press was lost' is
the press-counting bug wearing a different hat. Now: a non-move ends the walk
only after at least one move has been observed, four consecutive dropped presses
abort, and (A) is pressed only if the cursor demonstrably moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 06:40:12 +00:00

125 lines
5.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Boot -> title -> MAIN MENU (or EXTRAS) -> arm the UI draw capture and read the
# BLEND STATE the guest set for each draw.
#
# Why this exists rather than `menu_draw_capture.sh`: that script launches with
# `--log_ui_draws=true`, and Canary's own source records why that is now a bad
# idea — `RequestUiDrawCapture()` in command_processor.cc says arming is
# unconditional since the flag "correlates, across 12 runs, with the title screen
# refusing (A) (0 of 7 with the flag, 4 of 5 without)". The flag is OBSOLETE and
# only kept so old command lines parse. So this one does not pass it.
#
# The measurement: `CaptureUiDrawForRE` now also logs RB_BLENDCONTROL0,
# RB_COLORCONTROL and RB_COLOR_MASK per draw, raw and decoded. That is what the
# GPU was actually told, which is the only thing that can settle whether the
# menu's `ptframe1`/`ptframe2` are composited with something other than
# alpha-over — nothing on the disc selects a per-element mode
# (docs/re/structures/t32-blend-mode-not-on-disc.md).
#
# Two traps this inherits from menu_draw_capture.sh, both measured:
# * (A) at the title is accepted only intermittently, but the title that ENDS
# the boot accepts a single tap; repeating is pointless.
# * F10 arms the capture AND opens the emulator's menu bar. Any Xenia UI makes
# IsUIActive() true, and then XamInputGetKeystrokeEx returns SUCCESS with a
# zeroed keystroke forever, which is the guest-side crash in
# docs/re/structures/title-a-press-fault.md. So click the game surface
# immediately after F10 to dismiss it.
#
# Usage: menu_blend_capture.sh [out_dir]
# SCREEN=extras walk one step right and (A) into EXTRAS before arming
set -u
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
SD="$(cd "$(dirname "$0")" && pwd)"
OUT="${1:-/sylph-home/re/blendcap}"
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/mbc.png; python3 "$SD/screen_id.py" /tmp/mbc.png | awk '{print $1}'; }
( 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"
# 1. WAIT for the title. Do not tap through the intro: it is ~3.5 minutes and it
# gets there on its own; a run that tapped every 4 s delivered 88 presses and
# ended on a black screen.
s=""
deadline=$(( SECONDS + ${DEADLINE:-1200} ))
while [ $SECONDS -lt $deadline ]; do
s="$(screen)"; echo "t=${SECONDS}s $s"
[ "$s" = "title" ] && break
sleep 4
done
[ "$s" = "title" ] || { echo "NEVER REACHED THE TITLE"; exit 1; }
# 2. ONE tap.
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"
[ "$s" = "menu" ] || { echo "NO MENU (screen=$s)"; exit 2; }
echo "MENU at ${SECONDS}s"
if [ "${SCREEN:-menu}" = "extras" ]; then
# EXTRAS is the LAST of the five items and the menu opens on NEW GAME. Do not
# count presses: on 2026-08-31 four DOWNs landed on OPTIONS because one was
# dropped. Press until the cursor STOPS MOVING instead, which needs no item
# count and no row calibration -- `ring_row.py`'s ROW0/SPACING are x11grab
# constants and are 45 px out on a `screenshot` grab (see METHOD.md), but the
# raw row it returns is still a monotone function of the item.
row(){ shot /tmp/mbc_row.png; python3 - <<'PY'
from PIL import Image
import sys; sys.path.insert(0,"/work/tools/re-capture")
import ring_row
try: print("%.1f" % ring_row.ring_row(Image.open("/tmp/mbc_row.png")))
except Exception: print("nan")
PY
}
# 🔴 The first version of this loop broke on its FIRST comparison -- one DOWN
# was dropped, the row read the same twice, it concluded "the cursor has
# stopped" and pressed (A) on NEW GAME. A stop test that cannot tell "at the
# end" from "the press was lost" is the counting bug wearing a different hat.
# So: a non-move only ends the walk AFTER at least one move has been seen, a
# run of dropped presses aborts instead of pressing (A), and (A) is pressed
# only if the cursor demonstrably moved.
prev="$(row)"; echo " cursor row $prev"
moved=0; stalls=0
for _ in 1 2 3 4 5 6 7 8 9 10; do
python3 "$SD/pad.py" tap DOWN 0.2; sleep 3
cur="$(row)"; echo " cursor row $cur"
if [ "$cur" = "$prev" ]; then
if [ "$moved" = 1 ]; then echo " cursor stopped -- at the last item"; break; fi
stalls=$((stalls+1))
[ "$stalls" -ge 4 ] && { echo "NO CURSOR MOVEMENT after 4 presses -- not pressing (A)"; exit 3; }
else
moved=1; stalls=0; prev="$cur"
fi
done
[ "$moved" = 1 ] || { echo "CURSOR NEVER MOVED -- not pressing (A)"; exit 3; }
python3 "$SD/pad.py" tap A 0.3; sleep 5
s="$(screen)"; echo " after EXTRAS attempt: $s"; shot "$OUT/extras.png"
# The blend map needs to know WHICH screen it is looking at, and screen_id
# calls both of these "menu". Ⓑ is advertised on EXTRAS and not on the main
# menu, so the footer is the discriminator a human would use; here the element
# count in the capture is checked instead, after the fact.
fi
# 3. arm, then dismiss the menu bar F10 opened
xdotool windowactivate --sync "$win"; sleep 1
xdotool key F10; sleep 3
xdotool mousemove 900 400 click 1; sleep 2
shot "$OUT/after-f10.png"
ls -l "$OUT"/xenia_re_ui_draws_*.log 2>/dev/null || echo "NO CAPTURE LOG"
grep -i "UI-CAP" "$OUT/canary.stdout" | tail -3
pkill -x xenia_canary 2>/dev/null; sleep 2
echo "DONE at ${SECONDS}s (emulator killed)"