re: a menu/EXTRAS discriminator that failed its control, and the fix

sylpheed-port's BLOCKED.md ask #1 -- does (B) from EXTRAS also show no black
interval, or is "(B) has no black" one screen pair -- needs the harness to know
it is on EXTRAS. screen_id.py cannot tell: both are dark blue GP_TITLE screens
and it reports `menu` for either.

which_title_screen.py correlates a grab against our build 5 / build 6 renders.
First version FAILED its control: it called live-main-menu.png "extras" and
live-extras.png "main_menu", both backwards, margins under 1.1 on RMSE ~33.
Cause: it applied the y=45 game-surface offset unconditionally, but only a full
1280x720 display frame has the menu bar -- a 1279x675 grab IS the surface, and
two of the three reference captures are surface-sized. Offset made conditional;
the control now passes 4/4 with margins 9.9-11.7 against ~18 within-class.

The tool is a navigation aid for driving the emulator and says so: it identifies
a screen by agreeing with our own renders, so nothing measured may rest on it.

Also records the METHOD entry sylpheed-port offered from their own wedged check
script: an absence of output is not a status. They reported "still running, two
lines, both ok" for three iterations while the first attempt had died silently
under its own timeout with block-buffered output -- zero information, reported
as patience. An orphan from an older form of the same script was found running
after 9.5 hours. Line-buffer and bound long jobs, and check the artifact rather
than the exit code: the artifact reached its correct duration while the process
never returned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
This commit is contained in:
sylph-decoder
2026-08-30 13:59:47 +00:00
parent aa4ffafbc5
commit 2a1ecc1c63
3 changed files with 85 additions and 0 deletions

View File

@@ -203,6 +203,19 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the
or `-` is that bug's signature. Grep the corpus for readers of a structure
before calling its fix done.
* **An absence of output is not a status.** `sylpheed-port` reported "still
running, two lines, both ok" for three consecutive iterations of a check script.
The first attempt had already died silently under its own timeout with
block-buffered output — so there had been *zero* information from it, and "still
two lines" was being read as patience rather than as the alarm it was. The
underlying process was wedged on an ffmpeg that finishes its work and never
exits (an orphan from an older form of the same script was found still running
after **9.5 hours**). Two rules fall out, and this corpus has now paid for both:
a long-running job needs **line-buffered output and a bound**, and *"no new
output"* must be treated as **no information**, never as progress. Check the
artifact, not the exit code — the artifact reached its correct 8.0 s duration
while the process it came from never returned.
## Runtime / emulator
* **Look at the PNG** — and check its dimensions.

View File

@@ -23,6 +23,9 @@
#
# WHERE=menu (default) arms on the main menu and presses (B) -> menu -> title.
# WHERE=title arms on the boot title and presses (A) -> title -> menu.
# WHERE=extras navigates to EXTRAS, arms there, presses (B) -> menu.
# Tests whether "(B) has no black interval" is a rule or
# one screen pair -- sylpheed-port's BLOCKED.md ask #1.
# The two transitions have DIFFERENT declared fade-ins for the incoming screen --
# build 4 is 0->16 (16 units, 8 frames), build 5 is 0->12 (12 units, 6 frames) --
# which is what makes the pair a discriminator rather than a fit.
@@ -68,6 +71,31 @@ if [ "$WHERE" = "menu" ]; then
done
[ "$s" = "menu" ] || { echo "NO MENU (screen=$s)"; exit 2; }
BTN=B
elif [ "$WHERE" = "extras" ]; then
python3 "$SD/pad.py" tap A 0.5
for _ in 1 2 3 4 5 6; do
sleep 4; s="$(screen)"; echo " after A: $s"
[ "$s" = "menu" ] && break
done
[ "$s" = "menu" ] || { echo "NO MENU (screen=$s)"; exit 2; }
# EXTRAS is the only main-menu button that stays inside GP_TITLE, and initial
# focus varies boot to boot, so search: press (A), ask which screen we got, and
# if it is not EXTRAS come back with (B) and step the cursor. screen_id.py
# cannot tell EXTRAS from the main menu -- both are dark blue GP_TITLE screens
# -- so the check is which_title_screen.py, whose control separates them by
# ~11 RMSE against ~18 within-class.
found=""
for try in 1 2 3 4 5 6; do
python3 "$SD/pad.py" tap A 0.5; sleep 5
shot "$OUT/try$try.png"
w="$(python3 "$SD/which_title_screen.py" "$OUT/try$try.png")"
echo " try $try: $w"
case "$w" in extras*) found=1; break;; esac
python3 "$SD/pad.py" tap B 0.5; sleep 5
python3 "$SD/pad.py" dpad UP 0.3; sleep 2
done
[ -n "$found" ] || { echo "NEVER REACHED EXTRAS"; exit 3; }
BTN=B
else
# arm on the title itself and press (A): the incoming screen is then build 5,
# whose declared fade-in is 12 units where build 4's is 16.

View File

@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Is this screenshot the MAIN MENU or EXTRAS? screen_id.py cannot tell them apart.
Both are dark-blue `GP_TITLE` screens, so `screen_id.py` reports `menu` for
either. This correlates the grab against our own build 5 / build 6 renders and
reports which is closer, plus the margin -- a small margin means "cannot tell",
not "the closer one".
⚠️ These are OUR renders, so this identifies a screen by agreeing with our
decoding. It is a navigation aid for driving the emulator, NOT evidence about the
game. Nothing measured is allowed to rest on it.
which_title_screen.py <grab.png> [ref_dir]
"""
import sys
import numpy as np
from PIL import Image
def load(p, shape=None):
im = Image.open(p).convert("RGB")
a = np.asarray(im, dtype=float)
return a
def main():
grab = load(sys.argv[1])
d = sys.argv[2] if len(sys.argv) > 2 else "/sylph-home/re/ref"
# The game surface sits at y=45 ONLY in a full 1280x720 display frame; a
# 1279x675 grab is the surface already. Hard-coding the 45 made this tool fail
# its own control -- it called the main menu "extras" and extras "main_menu",
# because two of the three reference captures are surface-sized.
off = 45 if grab.shape[0] >= 716 else 0
best = []
for b, name in ((5, "main_menu"), (6, "extras")):
r = load(f"{d}/build{b}.png")
h = min(grab.shape[0] - off, r.shape[0]); w = min(grab.shape[1], r.shape[1])
g = grab[off:off + h, :w]; rr = r[:h, :w]
best.append((float(np.sqrt(((g - rr) ** 2).mean())), name))
best.sort()
margin = best[1][0] - best[0][0]
print(f"{best[0][1]} rmse={best[0][0]:.2f} (other {best[1][1]} {best[1][0]:.2f}, margin {margin:.2f})")
return 0
if __name__ == "__main__":
raise SystemExit(main())