From 2a1ecc1c635cf9f0f319ef50e05613607f36cddc Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Sun, 30 Aug 2026 13:59:47 +0000 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v --- docs/re/METHOD.md | 13 ++++++++ tools/re-capture/fade_decompose.sh | 28 ++++++++++++++++ tools/re-capture/which_title_screen.py | 44 ++++++++++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100755 tools/re-capture/which_title_screen.py diff --git a/docs/re/METHOD.md b/docs/re/METHOD.md index 3d3a59bf..0547c32c 100644 --- a/docs/re/METHOD.md +++ b/docs/re/METHOD.md @@ -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. diff --git a/tools/re-capture/fade_decompose.sh b/tools/re-capture/fade_decompose.sh index 72478009..05654921 100755 --- a/tools/re-capture/fade_decompose.sh +++ b/tools/re-capture/fade_decompose.sh @@ -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. diff --git a/tools/re-capture/which_title_screen.py b/tools/re-capture/which_title_screen.py new file mode 100755 index 00000000..4a045469 --- /dev/null +++ b/tools/re-capture/which_title_screen.py @@ -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 [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())