Files
Sylpheed/tools/re-capture/menu_blend_capture.sh
sylph-decoder 698409fdfe tools: walk to EXTRAS by cursor movement, and size-match against several builds
menu_blend_capture.sh counted two DOWNs to reach EXTRAS, which is wrong twice
over -- EXTRAS is the fifth item, and on 2026-08-31 four DOWNs landed on OPTIONS
because one press was dropped. It now presses until the cursor stops moving,
which needs no item count and no row calibration. Its title deadline follows the
same change as title_blend_capture.sh, 1200 s not 420.

ui_blend_map.py takes a comma-separated build list, because the live title is
TWO builds composited -- 4 draws the art, 2 draws the PRESS (A) plate -- and a
one-build size table cannot name the elements of a title capture.
frame_alpha_census takes its builds from argv for the same reason.

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

107 lines
4.6 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
}
prev="$(row)"; echo " cursor row $prev"
for _ in 1 2 3 4 5 6 7 8; do
python3 "$SD/pad.py" tap DOWN 0.2; sleep 2
cur="$(row)"; echo " cursor row $cur"
[ "$cur" = "$prev" ] && break
prev="$cur"
done
python3 "$SD/pad.py" tap A 0.3; sleep 5
s="$(screen)"; echo " after EXTRAS attempt: $s"; shot "$OUT/extras.png"
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)"