The console draws all five title flashes. My claim that ptlogo_back2eff3 is never drawn was an instrument artefact, and I had reported it to the port with three alternative explanations "ruled out". A GPU draw can batch several quads -- indices=4 is one, indices=8 two, indices=24 six -- and the UI draw log dumps only the first 8 vertices. Taking min/max over a line's whole vertex list merges quads into one box. eff3 is batched with eff4, and because the wipe family is right-aligned, eff3 (788..1196) lies ENTIRELY INSIDE eff4 (447..1196). The union is exactly eff4's own extent, so the merged box matched eff4 to 1 px, eff3 vanished, and nothing looked wrong. Parsed per quad, all five fire in both title entries in the declared stagger: eff1 130-131, eff2 133, eff3 133-134, eff4 133-135, eff/eff5 134+, back2 136+; and 5953-5955 / 5955-5957 / 5957-5958 / 5957-5959 / 5958+ / 5962+ in entry 2. Frames 133 and 134 are t=60.1 and 62.3, inside eff3's declared t in (58,64). Also retracts "the developer splash is one composited quad" -- the same bug, which the port refuted by arithmetic first (a 259-tall box cannot contain three logos spanning y 164..585). It draws three logos and three glows as separate quads in one indices=24 call; the 525x259 was gamearts_eff merged with seta_eff. The 9-unit black hold is unaffected: those glows are the developer splash's first draw. The three "ruled out" explanations were all aimed at the wrong failure. In particular the invisible-draw check counted draws with NO geometry line, when the hiding place was draws with PARTIAL geometry. Refuting three wrong hypotheses is not evidence for a fourth, and a list of failure modes written by whoever built the instrument is the least likely to contain its blind spot. Recorded in METHOD.md, along with the tell that was present and explained away: a merged box carries the first quad's colour, which made one element's alpha read 255/127/254 on consecutive frames. New tool: tools/re-capture/quads_per_frame.py parses vertices in groups of four and warns when the logged quad count falls short of indices/4. Also guards a double-A-tap in ui_draw_capture.sh: the movie branch ignored that TARGET=menu had already tapped, so a run tapped A on the title at t=23s and again at t=27s on the transition; the guest faulted and Xenia dumped registers to stdout until the file reached 519 MB. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
145 lines
7.0 KiB
Bash
Executable File
145 lines
7.0 KiB
Bash
Executable File
#!/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.
|
|
#
|
|
# ⚠️ GRACE is why an ARM=early capture still MISSES BOTH BOOT SPLASHES. They
|
|
# run at roughly 1.2-9.5 s of guest time (`boot-order-and-splash-dwell.md`),
|
|
# and a fixed 8 s wait plus the window poll arms after the publisher has been
|
|
# and gone. Set GRACE=1 to catch them; the liveness check below tolerates it
|
|
# because it polls rather than sampling once.
|
|
sleep "${GRACE:-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
|
|
|
|
# 🔴 NOTAP=1 stops this script tapping A at all. The tap exists to skip the
|
|
# attract movie, but its trigger is "the screen changed a lot", which is ALSO
|
|
# true while a boot splash is fading. A run measuring the BOOT sequence must set
|
|
# it: with the tap on, a 2026-08-29 run classified the publisher splash as a
|
|
# movie at t=3 s, tapped through it, and the developer splash never appeared at
|
|
# all. The instrument was perturbing the thing it was measuring.
|
|
#
|
|
# 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 ] && [ "${tapped:-0}" = "0" ]; 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.
|
|
#
|
|
# 🔴 `tapped` guard added 2026-08-29, and it is not cosmetic. The trigger
|
|
# here is "the screen changed a lot", which is ALSO true of the
|
|
# title→menu transition this target has just started. A run tapped A on
|
|
# the title at t=23 s and again at t=27 s on the transition; the guest
|
|
# faulted, and Xenia dumped registers to stdout until the file reached
|
|
# **519 MB**. That is the double-tap into the save-data probe this
|
|
# script's own header warns about, arriving through the movie branch.
|
|
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 ] && [ "${NOTAP:-0}" != "1" ]; 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)"
|