re: the focus reader was two items out -- initial focus is NEW GAME, and my labels were wrong

menu_focus.py's row centres are design-space rows from screenshot output; my
probes fed it whole-display x11grab frames carrying Xenia's chrome and a surface
scaled 1.060. Caught by ground truth, not by a control: the probe announced 'on
EXTRAS', pressed A, and opened OPTIONS.

Measured directly with ring_row.py: initial focus on a fresh boot is NEW GAME,
2/2 fresh boots, both the first menu entry. That agrees with boot_menu.sh's own
line and menu-state-in-memory.md's four-downs, and withdraws this page's
'TUTORIAL 2/2' as the outlier.

Persistence stands and is now geometry-free -- 384.0 vs 385.5, 1.5 px apart. An
equality test is immune to a constant offset, which is why the conclusion survived
a broken reader when the published item names did not.

The control was structurally blind: 'two DOWNs move two items' tests relative
motion, and a constant offset preserves it exactly.

EXTRAS remains unmeasured; that run navigated to OPTIONS.

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 21:54:32 +00:00
parent addf753c44
commit 8ebbfc70c1
6 changed files with 412 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""Does the EXTRAS submenu remember its cursor across leave -> re-enter?
sylpheed-port's contract-check asserted that EXTRAS does NOT persist. Nothing
measured that. The corpus has EXTRAS' initial focus as MISSION SELECT from a
SINGLE entry, and Ⓑ-from-a-submenu restoring the PARENT's focus 4/4 -- neither
says what a submenu's own cursor does on re-entry. Their label
`initial_focus: ptbtn11, kind: "measured"` is exposed to the same history problem
raised for the main menu: taken on one entry, it may be reading history.
The main menu was measured to PERSIST (focus-persists-across-title.txt), so the
question is live in both directions.
⚠️ NO HAND-GUESSED GEOMETRY. menu_focus.py's ys are the five-item main menu and
do not transfer to a three-item submenu, and the only EXTRAS capture in the
corpus is a 562x182 crop. So the ring is located by DIFFERENCE instead: the
pixels that change when the cursor moves ARE the ring's footprint. Persistence is
then decided inside that mask, which needs no coordinates at all.
🔴 CONTROL, and the run stops on it: the E1->E2 difference must be ONE COMPACT
CLUSTER. If moving the cursor changes the whole frame, the mask is not the ring
and nothing after it may be read. (sylpheed-port measured one 46x44 cluster,
0.155 % of frame, across a full ring cycle on their renderer -- an independent
expectation for what this should look like.)
extras_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 600
W, H = 1280, 720
MENU_LO, MENU_HI, MENU_HOLD = 250, 420, 6
PAD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pad.py")
NAMES = ["NEW GAME", "LOAD GAME", "TUTORIAL", "OPTIONS", "EXTRAS"]
YS = [166, 241, 315, 390, 465]
T0 = time.time()
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", 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():
"""A CURRENT frame. Reading the pipe after a sleep returns a buffered one."""
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)
q.kill()
return a
def glyph(a):
r, g, b = a[:, :, 0], a[:, :, 1], a[:, :, 2]
return int(((g > 130) & (g - r > 45) & (g - b > 45)).sum())
def mainfocus(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 wait_menu(limit=90):
t = time.time(); streak = 0
while time.time() - t < limit:
a = fresh()
if a is None:
continue
if MENU_LO <= glyph(a) <= MENU_HI:
streak += 1
if streak >= 2:
return a
else:
streak = 0
return None
os.makedirs(OUT, exist_ok=True)
# ---- 1. on the main menu, walk to EXTRAS ------------------------------------
a = wait_menu()
if a is None:
sys.exit("never saw the main menu")
f, v = mainfocus(a)
print(f"[{time.time()-T0:7.1f}s] MAIN MENU focus = {NAMES[f]} ring "
+ " ".join(f"{x:5.0f}" for x in v), flush=True)
need = (4 - f) % 5
print(f" {need} DOWN to reach EXTRAS", flush=True)
for _ in range(need):
if not press("DOWN", "5811"):
sys.exit("a DOWN was never delivered")
time.sleep(0.8)
time.sleep(1.5)
a = fresh(); f2, v2 = mainfocus(a)
print(f" now on {NAMES[f2]} ring " + " ".join(f"{x:5.0f}" for x in v2), flush=True)
if f2 != 4:
sys.exit(f"CONTROL FAILED: wanted EXTRAS, on {NAMES[f2]}")
print("✅ on EXTRAS", flush=True)
# ---- 2. enter EXTRAS, capture E1, move, capture E2 --------------------------
if not press("A", "5800"):
sys.exit("A never delivered")
time.sleep(6.0)
E1 = fresh(); Image.fromarray(E1.astype(np.uint8)).save(f"{OUT}/E1.png")
print(f"[{time.time()-T0:7.1f}s] IN EXTRAS (glyph {glyph(E1)}) — E1 captured", flush=True)
if not press("DOWN", "5811"):
sys.exit("a DOWN was never delivered inside EXTRAS")
time.sleep(2.5)
E2 = fresh(); Image.fromarray(E2.astype(np.uint8)).save(f"{OUT}/E2.png")
print(f"[{time.time()-T0:7.1f}s] E2 captured after 1 DOWN", flush=True)
# ---- 3. CONTROL: the difference must be one compact cluster -----------------
d = (np.abs(E1 - E2).max(axis=2) > 24)
frac = d.mean()
ys, xs = np.nonzero(d)
if len(ys) == 0:
sys.exit("🔴 CONTROL FAILED: the cursor did not move at all (E1 == E2)")
bbox = (int(xs.min()), int(ys.min()), int(xs.max()), int(ys.max()))
bw, bh = bbox[2] - bbox[0] + 1, bbox[3] - bbox[1] + 1
dens = len(ys) / (bw * bh)
print(f" diff: {100*frac:.3f}% of frame, bbox {bw}x{bh} at ({bbox[0]},{bbox[1]}), "
f"fill {100*dens:.0f}%", flush=True)
if frac > 0.05:
sys.exit(f"🔴 CONTROL FAILED: {100*frac:.1f}% of the frame changed — that is not a ring")
print("✅ CONTROL PASSED: the move changed one localised region", flush=True)
# ---- 4. leave, re-enter, capture E3 ----------------------------------------
if not press("B", "5801"):
sys.exit("B never delivered")
a = wait_menu()
if a is None:
sys.exit("B did not return to the main menu")
f3, _ = mainfocus(a)
print(f"[{time.time()-T0:7.1f}s] back on the MAIN MENU, focus = {NAMES[f3]}", flush=True)
if not press("A", "5800"):
sys.exit("A never delivered on re-entry")
time.sleep(6.0)
E3 = fresh(); Image.fromarray(E3.astype(np.uint8)).save(f"{OUT}/E3.png")
print(f"[{time.time()-T0:7.1f}s] RE-ENTERED EXTRAS — E3 captured", flush=True)
# ---- 5. decide, inside the ring mask only ----------------------------------
m = d
d1 = float(np.abs(E3 - E1).max(axis=2)[m].mean())
d2 = float(np.abs(E3 - E2).max(axis=2)[m].mean())
print(f"\n inside the ring mask ({int(m.sum())} px):")
print(f" mean |E3 - E1| = {d1:6.2f} (E1 = where EXTRAS opened)")
print(f" mean |E3 - E2| = {d2:6.2f} (E2 = where I left it)")
if d2 < d1 * 0.5:
print("\n=> EXTRAS PERSISTS: re-entry matches where I left the cursor")
elif d1 < d2 * 0.5:
print("\n=> EXTRAS RESETS: re-entry matches where it first opened")
else:
print(f"\n=> UNDECIDED: {d1:.2f} vs {d2:.2f} are too close to separate")
print("EXTRAS FOCUS RUN DONE", flush=True)

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Boot and run extras_focus_persistence.py. Plate-pulse path, not skip_intro.
set -u
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
SD="$(cd "$(dirname "$0")" && pwd)"
OUT="${OUT:-/sylph-home/re/extrasfocus}"; 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 >/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 </dev/null >/tmp/openbox98.log 2>&1 &
fi
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=$OUT profile=$XUID gate=plate pulse"
cd /sylph-home/re
nohup run-canary --apu=sdl --log_mask=13 --log_level=2 \
--logged_profile_slot_0_xuid="$XUID" </dev/null >"$LOG" 2>&1 &
# reach the title and the menu with the probe that already does it
python3 "$SD/focus_persistence.py" "$LOG" "$OUT/reach" 900 >"$OUT/reach.log" 2>&1
echo "-- reached the menu; now the EXTRAS question --"
python3 "$SD/extras_focus_persistence.py" "$LOG" "$OUT" 600

78
tools/re-capture/ring_row.py Executable file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Locate the menu focus ring by MEASUREMENT, in whatever frame you have.
🔴 Why this exists. `menu_focus.py`'s row centres [166,241,315,390,465] are
DESIGN-SPACE rows read off `screenshot` output. Feeding it an ffmpeg x11grab
frame of the whole X display silently reads the wrong rows: the frame carries
Xenia's title bar and menu bar, and the game surface is scaled. On 2026-08-30 a
probe announced "on EXTRAS", pressed Ⓐ, and opened OPTIONS -- two items out.
⚠️ AND THE CONTROL COULD NOT CATCH IT. "Two DOWNs must move the cursor two
items" tests RELATIVE motion, which a constant offset preserves exactly. It
passed on a reader that was two items wrong. So this returns the ring's measured
ROW, and callers compare rows; naming an item needs a calibration, below.
Measured on the x11grab frames of 2026-08-30:
spacing 79.25 px per item (design spacing 74.75 -> surface scaled 1.060)
capture_y = 49.5 + 1.060 * design_y
Checks: design 241 -> 305 predicted vs 304.75 measured; 315 -> 383.4 vs 384.0.
ring_row.py FRAME.png [FRAME.png ...]
"""
import sys
import numpy as np
from PIL import Image
GUTTER = (500, 542)
DECOR_ROWS = 50 # window title bar + menu bar live above this
FOOTER_Y = 620 # the button-legend strip is bright in the gutter too
NAMES = ["NEW GAME", "LOAD GAME", "TUTORIAL", "OPTIONS", "EXTRAS"]
ROW0, SPACING = 225.5, 79.25 # measured, main menu, x11grab
def ring_row(img):
"""The ring's y centre in THIS frame's own pixels, or None."""
g = np.asarray(img.convert("L"), dtype=float)
col = g[:, GUTTER[0]:GUTTER[1]].max(axis=1)
col[:DECOR_ROWS] = 0
ys = np.nonzero(col > 150)[0]
if len(ys) == 0:
return None
groups, cur = [], [int(ys[0])]
for y in ys[1:]:
if y - cur[-1] <= 4:
cur.append(int(y))
else:
groups.append(cur)
cur = [int(y)]
groups.append(cur)
groups = [g_ for g_ in groups if len(g_) >= 5 and g_[0] < FOOTER_Y]
if not groups:
return None
b = max(groups, key=len)
return (b[0] + b[-1]) / 2
def main_menu_item(y):
"""Item index on the MAIN MENU, from the measured calibration.
Only valid for the five-item main menu in an x11grab frame. Returns None if
the row is not within half a step of a row centre -- refusing is the point,
since a wrong name is what this file exists to prevent.
"""
if y is None:
return None
i = round((y - ROW0) / SPACING)
if not (0 <= i < len(NAMES)):
return None
if abs(y - (ROW0 + i * SPACING)) > SPACING * 0.4:
return None
return i
if __name__ == "__main__":
for p in sys.argv[1:]:
y = ring_row(Image.open(p))
i = main_menu_item(y)
nm = NAMES[i] if i is not None else "<not a main-menu row>"
print(f"{p.split('/')[-1]:22} ring y = {y} -> {nm}")