#!/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)"