The stalled loader thread is a lost wakeup in Xenia's POSIX threading, fixed on the canary branch as a60fe7d11 and written up here. A thread created suspended publishes state_ and suspend_count_ in two separate lock scopes, and Resume() waits only for state_ before testing suspend_count_ == 0 - so a resumer in the gap drops the resume and the thread waits forever. The Linux XThread::Resume discards that false, which is why the guest saw success. On the first clean boot after the fix the loader thread is the CALLER on 20 kernel-call lines and issues 4 ResolvePath reads. Every failed boot before it had exactly zero of both. Stated plainly as not shown: that boots now reach the menu RELIABLY. One post-fix boot, and it is confounded by the harness. Which is the second half. skip_intro.sh's title test has now been wrong twice in opposite directions: originally one absolute pixel (625,618) - a 1280x720 coordinate against the 1279x675 game surface, so it read the copyright line and timed out with the title on screen - and then my replacement, screen_id.py, which is too loose and called the SQUARE ENIX publisher logo "title" 151s into a boot, spending the script's single press there. is_title.py now counts the green (A) glyph over the whole frame: geometry-independent and specific, measured at 0 pixels on the logo and 1520 on a real title.
74 lines
4.1 KiB
Bash
Executable File
74 lines
4.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Drive the boot sequence to the main menu without a human in the loop.
|
|
# - movie playing (two grabs 0.6s apart differ a lot) -> tap A to skip
|
|
# - static screen -> if the green "PRESS (A) BUTTON" glyph is there, tap A and stop
|
|
# Static logo screens are left alone, so a stray tap can never land on NEW GAME.
|
|
#
|
|
# LIVENESS IS CHECKED EVERY ITERATION, and that is not a nicety. When the
|
|
# display died 3 minutes into a run, `screenshot` started failing silently and
|
|
# left /tmp/f1.png and /tmp/f2.png at their last contents — two *stale* files,
|
|
# which compare to a constant non-zero RMSE, which reads exactly like "the
|
|
# screen is changing a lot". So the loop reported "movie -> skip A" every 5 s
|
|
# for the full 600 s timeout with no emulator and no X server running. A dead
|
|
# display and a playing movie must never be able to look the same.
|
|
set -u
|
|
export HOME=/sylph-home/re
|
|
DISP="${DISPLAY:-:98}"
|
|
alive(){ ps -o pid=,stat= -C xenia_canary 2>/dev/null | awk '$2 !~ /^Z/ {print $1}'; }
|
|
# Press through pad.py, NOT `vgamepad`. That command no longer exists — the
|
|
# uinput pad was replaced by the `--hid=file` driver — and because this script
|
|
# runs without `set -e`, calling it failed silently: the title branch pressed
|
|
# nothing and still `exit 0`, so a caller was told "TITLE -> A" while the game
|
|
# sat on the title screen forever. Fail loudly instead.
|
|
SD_SI="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
tapA(){ python3 "$SD_SI/pad.py" tap A 0.25 || { echo "PAD PRESS FAILED"; exit 6; }; }
|
|
# The X root keeps the DEAD session's last frame, so a fresh launch would be
|
|
# detected as "already at the title". Blank it, and wait for the new window.
|
|
xsetroot -solid black 2>/dev/null || true
|
|
until xdotool search --name "Xenia-canary" >/dev/null 2>&1; do
|
|
xdpyinfo -display "$DISP" >/dev/null 2>&1 || { echo "DISPLAY LOST at ${SECONDS}s (before the window appeared)"; exit 3; }
|
|
[ -n "$(alive)" ] || { echo "EMULATOR GONE at ${SECONDS}s (before the window appeared)"; exit 4; }
|
|
sleep 1
|
|
done
|
|
deadline=$(( SECONDS + ${1:-900} ))
|
|
while [ $SECONDS -lt $deadline ]; do
|
|
rm -f /tmp/f1.png /tmp/f2.png
|
|
screenshot /tmp/f1.png >/dev/null 2>&1; sleep 0.6
|
|
screenshot /tmp/f2.png >/dev/null 2>&1
|
|
if [ ! -s /tmp/f1.png ] || [ ! -s /tmp/f2.png ]; then
|
|
xdpyinfo -display "$DISP" >/dev/null 2>&1 \
|
|
|| { echo "DISPLAY LOST at ${SECONDS}s"; exit 3; }
|
|
echo "SCREENSHOT FAILED at ${SECONDS}s with the display up"; exit 5
|
|
fi
|
|
[ -n "$(alive)" ] || { echo "EMULATOR GONE at ${SECONDS}s"; exit 4; }
|
|
d=$(compare -metric RMSE /tmp/f1.png /tmp/f2.png null: 2>&1 | sed 's/ .*//' | cut -d. -f1)
|
|
d=${d:-0}
|
|
# COUNT the green (A) glyph over the whole frame; do not probe one absolute
|
|
# pixel and do not trust screen_id.py here. The pixel probe used a 1280x720
|
|
# coordinate against the 1279x675 game surface and always read the copyright
|
|
# line (600s TIMEOUT with the title on screen). screen_id.py fixed that and
|
|
# then over-matched the other way: it called the SQUARE ENIX publisher logo
|
|
# "title" 151s into a boot, and this script spent its one press there.
|
|
# is_title.py counts glyph pixels, which only the interactive title has.
|
|
if python3 "$SD_SI/is_title.py" /tmp/f2.png >/dev/null 2>&1; then
|
|
echo "TITLE at ${SECONDS}s -> A"; tapA; exit 0
|
|
fi
|
|
# DO NOT tap through the movies. This branch used to, and for a long time it
|
|
# was harmless only because `vgamepad` did not exist and the call failed
|
|
# silently. Once the press became real, the boot reached the title in 90s
|
|
# instead of 434s — and that title accepts NOTHING. menu_draw_capture.sh
|
|
# records the same thing from the other direction: "a run that tapped (A)
|
|
# every 4s through the boot delivered 88 presses and ended on a black screen
|
|
# that never came back", and "the title the attract loop returns to accepts
|
|
# nothing at all, and 40 taps at 1/s do not change that".
|
|
#
|
|
# So: wait the intro out (~3.5 min) and spend the single press on the title
|
|
# that ends the boot sequence, which is the one that accepts it.
|
|
if [ "$d" -gt 1500 ]; then
|
|
echo "movie (rmse $d) at ${SECONDS}s -> waiting it out (tapping breaks the title)"
|
|
sleep 3
|
|
fi
|
|
sleep 1
|
|
done
|
|
echo "TIMEOUT"; exit 1
|