From 8bcb1d19760175e78e691021a45db86aef78b9e7 Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Sun, 30 Aug 2026 21:29:01 +0000 Subject: [PATCH] re: the main menu remembers its cursor across menu -> title -> menu F1 TUTORIAL, two delivery-confirmed DOWNs to EXTRAS, B to the title, A back: F3 is EXTRAS. Re-entry restores the item you left. Reframes the initial-focus disagreement rather than settling it: if focus persists, any 'initial focus' reading not taken on a fresh boot's first menu entry measures history. It still says nothing about what the menu opens on -- this run's F1 was itself carried over from a prior probe's press. Reached on the plate-pulse gate, not boot_menu.sh, whose stillness test cannot fire on this title -- TITLE at 422.7 s on a boot skip_intro could not gate at all. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v --- docs/port/HANDOFF.md | 26 +++ docs/re/data/focus-persists-across-title.txt | 62 ++++++ docs/re/menu-navigation-semantics.md | 24 +++ tools/re-capture/focus_persistence.py | 196 ++++++++++++++++++ tools/re-capture/focus_persistence.sh | 58 ------ tools/re-capture/focus_persistence_session.sh | 36 ++++ 6 files changed, 344 insertions(+), 58 deletions(-) create mode 100644 docs/re/data/focus-persists-across-title.txt create mode 100755 tools/re-capture/focus_persistence.py delete mode 100755 tools/re-capture/focus_persistence.sh create mode 100755 tools/re-capture/focus_persistence_session.sh diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index d13cf5ba..084bf188 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -10,6 +10,32 @@ delivered. + +## ✅ 2026-08-30 — the main menu remembers its cursor; re-entry is not a reset + +**measured, and it changes what `on_cancel` should do.** Ⓑ from the menu to the +title and Ⓐ back returns you to **the item you left**, not to a default. + +| step | focus | +|---|---| +| on the menu | `TUTORIAL` | +| after 2× DOWN (both delivery-confirmed) | `EXTRAS` | +| after Ⓑ → title → Ⓐ → menu | **`EXTRAS`** | + +Control passed: two DOWN presses moved the cursor exactly two items, and the run +is discarded if they do not. +[`data/focus-persists-across-title.txt`](../re/data/focus-persists-across-title.txt) + +📌 **This also reframes the initial-focus warning I sent earlier today.** If focus +persists, an "initial focus" reading not taken on a fresh boot's *first* menu entry +is measuring history — so the records that disagree need not disagree about the +game. **It still does not say what the menu opens on.** Your authored `NEW GAME` +stands on its own reasoning; nothing here confirms or refutes it. + +⚠️ Reach: one boot, one round trip, one direction. Persistence across a **reboot** +is untested and is the reading that would matter for authoring a default. + + ## 🔴 2026-08-30 — do not hardcode the menu's initial focus; the sources disagree **New, and it is a warning rather than an answer.** `menu-navigation-semantics.md` diff --git a/docs/re/data/focus-persists-across-title.txt b/docs/re/data/focus-persists-across-title.txt new file mode 100644 index 00000000..21e04bd4 --- /dev/null +++ b/docs/re/data/focus-persists-across-title.txt @@ -0,0 +1,62 @@ +# Does the main menu REMEMBER its cursor across menu -> title -> menu? +# MEASURED 2026-08-30. Answer: YES. +# +# WHY IT MATTERS: menu-navigation-semantics.md records initial focus as TUTORIAL +# (2/2 boots) while boot_menu.sh's closing line says NEW GAME and +# menu-state-in-memory.md reaches EXTRAS in four downs, which only counts from +# NEW GAME. If focus PERSISTS, then any "initial focus" reading not taken on a +# fresh boot's FIRST menu entry is measuring history, not initial focus -- which +# would explain the disagreement without any of the records being wrong. +# +# HARNESS: tools/re-capture/focus_persistence.py, on the path b_from_menu.py +# validated -- plate-pulse title detector, glyph-327 menu detector, and every +# press confirmed from the guest's own [RE-INPUT] log rather than from the pad. +# NOT boot_menu.sh: its title gate tests for stillness and this title never +# stills (harness-title-gate-assumes-a-static-title.md). The plate-pulse gate +# reached TITLE at 422.7 s on the boot skip_intro could not gate at all. +# +# ✅ CONTROL, and it is the reason run 1 is discarded rather than reported: +# two DOWN presses must move the cursor exactly two items. If they do not, +# the reader is not tracking and F3 may not be read. +# +# [ 1.0s] MENU (glyph 327) F1 = TUTORIAL ring 130 76 254 69 64 +# [ 7.3s] DOWN delivered (attempt 1) +# [ 8.2s] DOWN delivered (attempt 1) +# [ 1.0s] after 2x DOWN F2 = EXTRAS ring 130 76 66 69 254 +# ✅ CONTROL PASSED: 2x DOWN moved TUTORIAL -> EXTRAS +# [ 13.3s] B delivered (attempt 1) +# [ 24.0s] BACK AT TITLE (glyph 1403) +# [ 25.8s] A delivered (attempt 1) +# [ 33.6s] MENU AGAIN F3 = EXTRAS ring 130 76 66 69 254 +# +# F1=TUTORIAL F2=EXTRAS F3=EXTRAS => FOCUS PERSISTS +# +# The ring vector is menu_focus.py's geometry byte for byte: the peak moves from +# index 2 to index 4 and STAYS at 4 across the round trip. +# +################################################################################ +# 🔴 RUN 1 IS DISCARDED, AND ITS FAILURE IS THE USEFUL PART. +# +# The first attempt read F1 = LOAD GAME and F2 = LOAD GAME -- the cursor had not +# moved -- and the control refused to report F3. The cause was NOT the reader: +# the two frames genuinely differ by 911 px, so they are different frames. It is +# that run 1 sent the DOWN presses through pad.py with NO delivery confirmation, +# while A and B were confirmed. The guest's own log settles it: +# +# vk=5811 (dpad down) flags=0001 ... delivered ONCE for TWO presses +# vk=5800 (A) flags=0001 ... delivered once +# +# So a press that leaves the harness is not a press the guest received, and the +# corpus already knew that for A and B and had not applied it to the d-pad. +# Fixed: every press in this probe is now confirmed the same way, and the run +# above shows 2/2 DOWN delivered. +# +# ⚠️ REACH. One boot, one round trip, one direction. NOT tested: +# * persistence across a full REBOOT -- this is within one boot; +# * whether F1 = TUTORIAL here is an initial focus. IT IS NOT: run 1 had +# already moved the cursor LOAD GAME -> TUTORIAL with its one delivered +# press, and run 2 found it still on TUTORIAL. That is a second, incidental +# confirmation of persistence -- across two separate probe processes -- but +# it means this run says NOTHING about what the menu opens on. +# * whether a longer absence, or a submenu round trip, behaves the same. +# (Ⓑ from a submenu restoring the entered-from item is already 4/4.) diff --git a/docs/re/menu-navigation-semantics.md b/docs/re/menu-navigation-semantics.md index 9b1f214c..251c578d 100644 --- a/docs/re/menu-navigation-semantics.md +++ b/docs/re/menu-navigation-semantics.md @@ -30,6 +30,30 @@ with [`tools/re-capture/menu_focus.py`](../../tools/re-capture/menu_focus.py). Wrap holds on both screens tested — the 5-item main menu and the 3-item `EXTRAS` submenu — so it is a menu rule, not a per-screen table. +## ✅ 2026-08-30 — the menu REMEMBERS its cursor across menu → title → menu + +**measured** — [`data/focus-persists-across-title.txt`](data/focus-persists-across-title.txt), +`tools/re-capture/focus_persistence.py`. One run, control passed: + +| step | focus | ring vector | +|---|---|---| +| F1, on the menu | `TUTORIAL` | 130 76 **254** 69 64 | +| F2, after 2× DOWN (**both delivery-confirmed**) | `EXTRAS` | 130 76 66 69 **254** | +| F3, after Ⓑ → title → Ⓐ → menu | **`EXTRAS`** | 130 76 66 69 **254** | + +**F3 == F2, so focus persists** — the menu is re-entered on the item you left, +not on a fixed one. + +📌 **And this reframes the disagreement below rather than settling it.** If focus +persists, then *any* "initial focus" reading not taken on a fresh boot's **first** +menu entry is measuring history. The records need not disagree about the game at +all — they may differ in what the cursor had already been moved to. **Nothing here +says what the menu opens on**; this run's F1 was itself carried over from a prior +probe's press. + +⚠️ Reach: one boot, one round trip, one direction. Persistence across a **reboot** +is untested, and is the reading that would matter for authoring a default. + ⚠️ **2026-08-30 — and the sources DISAGREE, which nothing here had noticed.** `boot_menu.sh`'s own closing line says *"AT MAIN MENU (cursor on NEW GAME)"*, and [`menu-state-in-memory.md`](menu-state-in-memory.md)'s run reaches `EXTRAS` in diff --git a/tools/re-capture/focus_persistence.py b/tools/re-capture/focus_persistence.py new file mode 100755 index 00000000..a1349c9b --- /dev/null +++ b/tools/re-capture/focus_persistence.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""Does the main menu REMEMBER its cursor across menu -> title -> menu? + +menu-navigation-semantics.md carries this 🟡: "Initial focus is reproducible but +not established as invariant. Both of my boots opened on TUTORIAL, and both used +boot_menu.sh." Two runs through one harness are not two samples. And the sources +DISAGREE: boot_menu.sh's own closing line says NEW GAME, and +menu-state-in-memory.md reaches EXTRAS in four downs, which only counts from NEW +GAME. Two say NEW GAME, one says TUTORIAL. + +⚠️ THIS DELIBERATELY DOES NOT USE boot_menu.sh. Its title gate admits a "static" +screen at d <= 1500 between grabs 0.6 s apart, and the title never stills -- the +sweep leaves free-run. Minimum observed 1551 over 72 samples, 0 able to pass. +See harness-title-gate-assumes-a-static-title.md. Everything here is instead the +harness b_from_menu.py validated: plate-pulse title detector, glyph-327 menu +detector, delivery confirmed from [RE-INPUT] rather than from the pad. + +SEQUENCE + F1 focus when the menu first appears <- re-measures initial focus + F2 focus after 2x DOWN <- CONTROL for the focus reader + F3 focus after B (to title) then A (back) <- persist or reset? + +F3 == F2 => the menu restores where you were. F3 == F1 => it resets. + +🔴 CONTROL GATE: if F2 is not exactly two items below F1 (with wrap), the reader +is not tracking the cursor and NOTHING after it may be read. The run says so and +stops rather than reporting a number it cannot justify. + + focus_persistence.py LOG OUTDIR [wait_s] +""" +import os, re, subprocess, sys, time +import numpy as np +from PIL import Image + +LOG, OUT = sys.argv[1], sys.argv[2] +WAIT = float(sys.argv[3]) if len(sys.argv) > 3 else 900 +W, H = 1280, 720 +NEED, CEIL, HOLD = 500, 2500, 12 # title plate pulse band +MENU_LO, MENU_HI, MENU_HOLD = 250, 420, 6 # glyph-327 menu detector +PAD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pad.py") + +# menu_focus.py's geometry, byte for byte: the focus RING sits in the gutter +# left of the label and nothing else is bright there. +NAMES = ["NEW GAME", "LOAD GAME", "TUTORIAL", "OPTIONS", "EXTRAS"] +YS = [166, 241, 315, 390, 465] + + +def focus(a): + g = np.asarray(Image.fromarray(a.astype(np.uint8)).convert("L"), dtype=float) + v = [g[y - 20:y + 20, 500:542].max() for y in YS] + return int(np.argmax(v)), v + + +def deliveries(vk): + pat = re.compile((r"RE-INPUT\] XamInputGetKeystrokeEx -> user=\d+ vk=%s flags=0001" % vk).encode()) + try: + return len(pat.findall(open(LOG, "rb").read())) + except FileNotFoundError: + return 0 + + +def press(btn, vk, tries=5): + for k in range(tries): + before = deliveries(vk) + subprocess.run([sys.executable, PAD, "tap", btn, "0.5"], check=False) + for _ in range(20): + time.sleep(0.25) + if deliveries(vk) > before: + print(f"[{time.time()-T0:7.1f}s] {btn} delivered (attempt {k+1})", flush=True) + return True + print(f"[{time.time()-T0:7.1f}s] {btn} NOT delivered (attempt {k+1})", flush=True) + return False + + +def _open(): + return subprocess.Popen( + ["ffmpeg", "-loglevel", "error", "-f", "x11grab", "-draw_mouse", "0", + "-video_size", f"{W}x{H}", "-i", ":98", "-r", "4", + "-f", "rawvideo", "-pix_fmt", "rgb24", "-"], + stdout=subprocess.PIPE, bufsize=W * H * 3 * 2) + + +def fresh(proc): + """A CURRENT frame, not a buffered one. + + ⚠️ Reading one frame from the pipe after a sleep returns whatever ffmpeg + buffered while we were not reading. Reopening the stream is the only cheap + way to be sure the frame is now. + """ + proc.kill() + q = _open() + a = None + for _ in range(3): + buf = q.stdout.read(W * H * 3) + if len(buf) == W * H * 3: + a = np.frombuffer(buf, np.uint8).reshape(H, W, 3).astype(int) + return q, a + + +def glyph(a): + r, g, b = a[:, :, 0], a[:, :, 1], a[:, :, 2] + return int(((g > 130) & (g - r > 45) & (g - b > 45)).sum()) + + +T0 = time.time() +p, n, seg = _open(), W * H * 3, time.time() +os.makedirs(OUT, exist_ok=True) +log = open(f"{OUT}/series.tsv", "w"); log.write("# t_s\tglyph\tphase\n") +phase, streak, mark = ("frommenu" if "--from-menu" in sys.argv else "wait"), 0, None +F1 = F2 = F3 = None + +while True: + el = time.time() - T0 + if el > WAIT: + print(f"TIMEOUT in phase {phase}", flush=True); break + if time.time() - seg > 30: + p.kill(); p = _open(); seg = time.time() + buf = p.stdout.read(n) + if len(buf) < n: + p.kill(); p = _open(); seg = time.time(); continue + a = np.frombuffer(buf, np.uint8).reshape(H, W, 3).astype(int) + c = glyph(a) + log.write(f"{el:.3f}\t{c}\t{phase}\n"); log.flush() + + if phase == "frommenu": + # already sitting on the menu: fall straight into the menu handler + streak = MENU_HOLD; phase = "tomenu" + + elif phase == "wait": + streak = streak + 1 if NEED <= c <= CEIL else 0 + if streak >= HOLD: + print(f"[{el:7.1f}s] TITLE (glyph {c})", flush=True) + press("A", "5800"); phase, streak = "tomenu", 0 + + elif phase == "tomenu": + streak = streak + 1 if MENU_LO <= c <= MENU_HI else 0 + if streak >= MENU_HOLD: + time.sleep(2.0) # let the menu settle + p, a = fresh(p) + Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/1-F1.png") + F1, v = focus(a) + print(f"[{el:7.1f}s] MENU (glyph {c}) F1 = {NAMES[F1]} ring " + + " ".join(f"{x:5.0f}" for x in v), flush=True) + # 🔴 Run 1 pressed DOWN twice through pad.py with NO delivery + # confirmation and the guest logged vk=5811 exactly ONCE. A and B + # were confirmed; the d-pad was not, so the run was unreadable. + # Confirm every press the same way. + ok = all(press("DOWN", "5811") for _ in range(2)) + if not ok: + print("🔴 a DOWN was never delivered — refusing to read F2", flush=True) + break + time.sleep(1.5) + p, a = fresh(p) + Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/2-F2.png") + F2, v = focus(a) + print(f"[{el:7.1f}s] after 2x DOWN F2 = {NAMES[F2]} ring " + + " ".join(f"{x:5.0f}" for x in v), flush=True) + want = (F1 + 2) % len(NAMES) + if F2 != want: + print(f"🔴 CONTROL FAILED: 2x DOWN from {NAMES[F1]} should give " + f"{NAMES[want]}, read {NAMES[F2]}. The reader is not tracking " + f"the cursor; refusing to report F3.", flush=True) + break + print(f"✅ CONTROL PASSED: 2x DOWN moved {NAMES[F1]} -> {NAMES[F2]}", flush=True) + press("B", "5801"); mark = time.time(); phase, streak = "backtitle", 0 + + elif phase == "backtitle": + streak = streak + 1 if NEED <= c <= CEIL else 0 + if streak >= HOLD: + Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/3-title.png") + print(f"[{el:7.1f}s] BACK AT TITLE (glyph {c})", flush=True) + press("A", "5800"); phase, streak = "remenu", 0 + elif time.time() - mark > 60: + print(f"[{el:7.1f}s] B did not reach the title in 60 s (glyph {c})", flush=True) + break + + elif phase == "remenu": + streak = streak + 1 if MENU_LO <= c <= MENU_HI else 0 + if streak >= MENU_HOLD: + time.sleep(2.0) + p, a = fresh(p) + Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/4-F3.png") + F3, v = focus(a) + print(f"[{el:7.1f}s] MENU AGAIN F3 = {NAMES[F3]} ring " + + " ".join(f"{x:5.0f}" for x in v), flush=True) + print(f"\nF1={NAMES[F1]} F2={NAMES[F2]} F3={NAMES[F3]}") + if F3 == F2: + print("=> FOCUS PERSISTS across menu -> title -> menu") + elif F3 == F1: + print("=> FOCUS RESETS to its initial item") + else: + print("=> NEITHER — F3 matches neither F1 nor F2; unexplained") + break + +p.kill() +print("FOCUS PERSISTENCE RUN DONE", flush=True) diff --git a/tools/re-capture/focus_persistence.sh b/tools/re-capture/focus_persistence.sh deleted file mode 100755 index ff75eb02..00000000 --- a/tools/re-capture/focus_persistence.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash -# Does the main menu REMEMBER its cursor across menu -> title -> menu? -# -# menu-navigation-semantics.md carries this 🟡: "Initial focus is reproducible -# but not established as invariant. Both of my boots opened on TUTORIAL, and -# both used boot_menu.sh." Two runs through one harness are not two samples -- -# the same shape as two captures through one gate (plate-pulse-phase-lock.md). -# -# And the sources DISAGREE. boot_menu.sh's own final line says "cursor on NEW -# GAME"; the page says TUTORIAL 2/2; menu-state-in-memory.md reaches EXTRAS in -# four downs, which only counts from NEW GAME. Two say NEW GAME, one says -# TUTORIAL. -# -# ONE BOOT settles both, and the round trip is the part no run has done: -# F1 focus when the menu first appears <- re-measures initial focus -# F2 focus after 2x DOWN <- CONTROL for the reader -# F3 focus after B (to title) then A (back) <- persist or reset? -# -# F3 == F2 => the menu restores where you were; F3 == F1 => it resets. -# If F2 is not two items below F1 the reader is not tracking the cursor and -# NOTHING below it may be read. Run as ONE BLOCKING FOREGROUND call. -set -u -export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98 -SD="$(cd "$(dirname "$0")" && pwd)" -OUT="${OUT:-/sylph-home/re/focuspersist}"; mkdir -p "$OUT" -NAMES=(NEW_GAME LOAD_GAME TUTORIAL OPTIONS EXTRAS) - -. "$SD/ensure_single_emulator.sh" - -echo "── EFFECTIVE CONFIG ──────────────────────────────" -echo " out dir = $OUT" -echo " harness = boot_menu.sh (the one under suspicion)" -echo " sequence = F1 -> 2x DOWN -> F2 -> B -> plate pulse -> A -> F3" - -BOOT_MENU_LOG="$OUT/boot.stdout" "$SD/boot_menu.sh" fp >"$OUT/boot.log" 2>&1 \ - || { echo "BOOT FAILED"; tail -5 "$OUT/boot.log"; exit 1; } -tail -2 "$OUT/boot.log" - -shot(){ screenshot "$OUT/$1.png" >/dev/null 2>&1; } -focus(){ python3 "$SD/menu_focus.py" "$OUT/$1.png" | tail -1; } - -shot f1; echo "F1 $(focus f1)" - -python3 "$SD/pad.py" dpad down 0.06; sleep 1 -python3 "$SD/pad.py" dpad down 0.06; sleep 2 -shot f2; echo "F2 $(focus f2)" - -echo "-- B to the title --" -python3 "$SD/pad.py" tap B 0.25 -python3 "$SD/wait_plate_pulse.py" 120 >"$OUT/pulse.log" 2>&1 \ - && echo " title settled (plate pulse)" || echo " ⚠️ NO PLATE PULSE in 120 s" -shot title - -echo "-- A back to the menu --" -python3 "$SD/pad.py" tap A 0.25; sleep 12 -shot f3; echo "F3 $(focus f3)" - -echo "FOCUS PERSISTENCE RUN DONE" diff --git a/tools/re-capture/focus_persistence_session.sh b/tools/re-capture/focus_persistence_session.sh new file mode 100755 index 00000000..b808ce07 --- /dev/null +++ b/tools/re-capture/focus_persistence_session.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Boot, then run focus_persistence.py. NO skip_intro -- the probe finds the title +# itself by the plate pulse, which is the gate that works on a screen whose sweep +# leaves never stop (harness-title-gate-assumes-a-static-title.md). +set -u +export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98 +SD="$(cd "$(dirname "$0")" && pwd)" +OUT="${OUT:-/sylph-home/re/focuspersist}"; mkdir -p "$OUT" +LOG="$OUT/canary.stdout" + +bash "$SD/ensure_single_emulator.sh" + +if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then + rm -f "/tmp/.X${DISPLAY#:}-lock" 2>/dev/null || true + nohup bash -c 'Xvfb "$0" -screen 0 1280x720x24 -ac -nolisten tcp \ + +extension GLX +extension RANDR >/tmp/xvfb98.log 2>&1' "$DISPLAY" /dev/null 2>&1 & + for _ in $(seq 1 50); do xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 && break; done + nohup env DISPLAY="$DISPLAY" HOME=/sylph-home openbox /tmp/openbox98.log 2>&1 & +fi +xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 || { echo "DISPLAY UNAVAILABLE"; exit 1; } + +# The profile is NOT optional: naming a XUID with no profile opens a sign-in +# dialog, and IsUIActive() then swallows every keystroke for the rest of the run. +XUID="${SYLPH_XUID:-$(ls "${XENIA_CONTENT:-$HOME/.local/share/Xenia/content}" 2>/dev/null | head -1)}" +[ -n "$XUID" ] || { echo "NO PROFILE"; exit 2; } + +echo "── EFFECTIVE CONFIG ──────────────────────────────" +echo " out dir = $OUT" +echo " profile = $XUID" +echo " title gate= plate pulse (NOT skip_intro's stillness test)" +echo " log = $LOG [RE-INPUT] delivery confirmation reads this" + +cd /sylph-home/re +nohup run-canary --apu=sdl --log_mask=13 --log_level=2 \ + --logged_profile_slot_0_xuid="$XUID" "$LOG" 2>&1 & +python3 "$SD/focus_persistence.py" "$LOG" "$OUT" "${WAIT:-900}"