re: WITHDRAW "ptloop01/02 do not free-run" -- I measured a pivot, not an extent

sylpheed-port noted that build 5's ptloop parent can be static while the leaf
record animates, and asked me to check it against my table. My own corpus
refutes my claim outright.

ptloop-leaf-sweep-positions.txt -- written earlier in this same corpus -- records
ptloop01.rat's nested record at loop length 600, whose leaf pteff03.t32 sweeps a
400 px-wide quad with its centre running x~921->1041 over t=340..370. The
parent's declared rect is (441,270) 200x90. The leaf draws 300 px outside it: the
parent rect is a PIVOT ANCHOR, not the drawn extent.

Checked against the two JP captures: my measured rect differs by 0 px -- and so
does the whole dead region y 270..450 x 480..960 around it -- while the band the
sweep actually occupies (x 721..1241) differs by 44 025 px. The zero was measured
where nothing happens.

So the port's reading is right and now confirmed from the disc: parent static,
leaf animates, and the two nested records cycle at DIFFERENT lengths, 600 and
720. My "single static keyframe" described the parent only. The era adjudication
is unaffected -- its box overlaps the sweep band only at x 721..776, which shows
no between-session differences.

The menu-loop question is still unsettled after a second attempt, and the second
attempt's failure REFUTES my diagnosis of the first. menu_loop_probe.py gated on
the plate pulse (glyph in [500,2500] held 12 samples), fired at t=484.5 s with
glyph 1723 -- a verified settled BOOT title, not the attract one -- pressed A,
and the press was delivered ([file-pad] keystroke vk=5800 down/up, 8 RE-INPUT
lines). Twenty seconds later all five frames still classified as the title
(rmse ~67-70, margins 0.06-0.16, the "neither" signature; screen_id says title).
So "the attract title accepts nothing" does not explain attempt 1, and the
corpus's "the boot title accepts a single A, 2 of 2 runs" is no longer 2 of 2.

METHOD: a declared rect can be an anchor, not an extent -- confirm an element
draws in a region before diffing that region to ask whether it moves.
navigation.md: confirm the screen changed, do not infer it from a delivered
press.

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 15:55:31 +00:00
parent 36c00f1a5c
commit b6d4242068
5 changed files with 176 additions and 1 deletions

View File

@@ -321,6 +321,16 @@ Traps that read as bugs but are not, all measured ✅:
if you must use `-9`, `rm -f /tmp/xenia-canary.lock` after. **And assert the
emulator is alive before entering any wait loop** — `ps -C xenia_canary` — so
the loop cannot spend its whole deadline on nothing.
* 🔴 **Ⓐ on a settled boot title does NOT reliably reach the menu.** This page's
§1 and `canary-scripted-input-traps.md` record "the boot title accepts a single
Ⓐ (2 of 2 runs)". A run on 2026-08-30 gated on the **plate pulse** (glyph in
[500, 2500] held 12 consecutive samples), fired at t=484.5 s with glyph 1723 —
a verified settled boot title, not the attract one — pressed Ⓐ, and **the press
was delivered** (`[file-pad] keystroke vk=5800 down` / `up`, 8 `[RE-INPUT]`
lines). Twenty seconds later every captured frame still classified as the title.
So the sample is no longer 2 of 2, and a script that presses once and proceeds
can be left on the title with nothing in its log to say so. **Confirm the screen
changed; do not infer it from a delivered press.**
* **`screen_id.py` reports `menu` during the attract loop.** Two boots on
2026-08-30 logged `menu` at t=106 s and t=418 s while the game was still in the
intro movie. A wait-loop that breaks on `menu` will act on the wrong screen; the

View File

@@ -345,6 +345,16 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the
precision against the quantity before quoting the number, and prefer comparing
*frames* to comparing a statistic about them.
* **A declared rect can be an ANCHOR, not an extent.** A UI element's placement
`(x, y)` plus its `w×h` reads like the box it draws in. For a `RATC` parent with
a nested record it is not: `ptloop01.rat` declares `(441,270)` 200×90 while its
**leaf** sweeps a 400 px-wide quad whose centre runs x≈921→1041 — 300 px outside
the parent's box. Measuring "does this element move" over the parent's rect
returned **0 differing pixels** across two sessions, in a region where *nothing*
moves, and that zero was published. ⚠️ Before diffing a region to ask whether an
element animates, confirm the element **draws there** — from a leaf sweep, a draw
capture, or the rendered quad's own coordinates. A pivot is not a bounding box.
## Runtime / emulator
* **Look at the PNG** — and check its dimensions.

View File

@@ -135,7 +135,30 @@ measurement lives in the box, and the in-box between-session noise is **0.32**,
first stated — the "strengthening" was reaching for a number that was to hand
rather than the one that applies.
### ✅ And `ptloop01/02` do NOT free-run on the settled title
### 🔴 WITHDRAWN — "`ptloop01/02` do not free-run on the settled title"
**Retracted 2026-08-30, same day, by my own corpus.** The claim below measured the
parent's declared rect at `(441,270)` 200×90. **That rect is a PIVOT ANCHOR, not
the drawn extent.** [`data/ptloop-leaf-sweep-positions.txt`](../data/ptloop-leaf-sweep-positions.txt)
— written earlier in this same corpus — records `ptloop01.rat`'s nested record at
loop length **600**, whose leaf `pteff03.t32` sweeps a **400 px-wide** quad with
its centre running x≈921→1041 over t=340..370. That is nowhere inside x 441..640.
Checked against the two JP captures: the rect I measured differs by **0 px** — but
so does the entire region y 270..450 × x 480..960 around it, a dead zone — while
the band the sweep actually occupies (x 721..1241) differs by **44 025 px**. The
zero was measured where nothing happens and proves nothing about the loops.
**`sylpheed-port`'s reading is right and is confirmed from the disc**: the
parent is static while the **leaf record animates**, and the two nested records
cycle at *different* lengths, **600 and 720**. My "single static keyframe" was a
statement about the parent only.
⚠️ The era adjudication is unaffected: its box is x 389..776, and the overlap with
the sweep band (x 721..776) shows no between-session differences — the 645 in-box
differing pixels all sit at y 99..128.
### ~~✅ And `ptloop01/02` do NOT free-run on the settled title~~ (see above)
The transfer above was flagged as uncertain because build 7 carries
`ptloop01/02.rat`, which might animate inside the adjudication box where the EN

View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""Do `ptloop01/02` animate on the SETTLED main menu?
`sylpheed-port` found those two leaves free-running in their renderer on the menu
path and pinned them, noting that pinning picks one pose rather than the game's --
"a capture question, not a harness one". On the title it is answered: two captures
from different sessions are byte-identical over the loop rect (0 of 18 000). The
MENU is a different bundle and is where their row actually drifted.
⚠️ A first attempt used `screen_id.py`'s single `title` classification as the cue
to press Ⓐ, hit a title at t=146 s, and Ⓐ never took across six tries -- that was
the ATTRACT loop's title, which accepts nothing, and the classifier cannot tell it
from the boot title. This gates on the same signal `jp_title_capture.py` uses: the
green Ⓐ-plate glyph count inside a band, HELD for 12 consecutive samples (~3 s).
menu_loop_probe.py OUTDIR [wait_s]
"""
import subprocess
import sys
import time
import numpy as np
from PIL import Image
OUT = sys.argv[1]
WAIT = float(sys.argv[2]) if len(sys.argv) > 2 else 700
W, H = 1280, 720
SURF_Y = 45
NEED, CEIL, HOLD = 500, 2500, 12
LOOP = (441, 270, 200, 90) # ptloop01/02 rest rect, design space
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 glyph(a):
r, g, b = a[:, :, 0], a[:, :, 1], a[:, :, 2]
return int(((g > 130) & (g - r > 45) & (g - b > 45)).sum())
def tap(btn, secs=0.5):
import os
for state in (f"press={btn}", ""):
with open("/tmp/xenia_pad.txt.tmp", "w") as f:
f.write(state)
os.replace("/tmp/xenia_pad.txt.tmp", "/tmp/xenia_pad.txt")
if state:
time.sleep(secs)
T0 = time.time()
p, n, seg, streak = _open(), W * H * 3, time.time(), 0
state, pressed_at, frames = "wait_title", None, []
while True:
el = time.time() - T0
if el > WAIT:
print("TIMEOUT in state " + state, 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(np.uint8)
c = glyph(a.astype(int))
if state == "wait_title":
streak = streak + 1 if NEED <= c <= CEIL else 0
if streak >= HOLD:
print(f"[{el:7.1f}s] BOOT TITLE SETTLED (plate pulse, glyph {c}) — pressing A",
flush=True)
tap("A", 0.5)
pressed_at, state, streak = time.time(), "wait_menu", 0
elif state == "wait_menu":
# the menu has no green plate; wait for the glyph count to fall and stay
# down, which is the plate leaving, then let the build-in finish.
streak = streak + 1 if c < NEED else 0
if streak >= HOLD and time.time() - pressed_at > 8:
print(f"[{el:7.1f}s] MENU (glyph {c}) — taking 5 frames 2 s apart", flush=True)
for k in range(5):
t0 = time.time()
while time.time() - t0 < 2.0:
b2 = p.stdout.read(n)
if len(b2) < n:
p.kill(); p = _open(); break
a = np.frombuffer(b2, np.uint8).reshape(H, W, 3).astype(np.uint8)
Image.fromarray(a).save(f"{OUT}/menu-{k}.png")
frames.append(a.astype(int))
print(f" frame {k}: glyph {glyph(a.astype(int))}", flush=True)
break
p.kill()
if len(frames) == 5:
x, y, w, h = LOOP
rois = [f[SURF_Y + y:SURF_Y + y + h, x:x + w] for f in frames]
print("\nptloop01/02 rect, 200x90 at design (441,270) — do they move?")
for i in range(1, 5):
d = np.abs(rois[i] - rois[0])
print(f" frame {i} vs 0: max |d| {d.max():3d}, px differing >8: "
f"{int((d.max(axis=2) > 8).sum()):5d} / {d.shape[0]*d.shape[1]}", flush=True)
print(" whole frame, as the CONTRAST CONTROL (if this is 0 too, the")
print(" instrument is blind and the rect's 0 means nothing):")
for i in range(1, 5):
d = np.abs(frames[i] - frames[0]).max(axis=2)
print(f" frame {i} vs 0: px differing >8: {int((d > 8).sum()):7d}", flush=True)
else:
print("NO FRAMES — nothing measured", flush=True)

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Boot, then run menu_loop_probe.py. Asserts the emulator is ALIVE before the
# probe enters its wait loop -- a previous run polled a dead display for 484 s
# because run-canary had refused on a lockfile orphaned by `kill -9`, and
# "no emulator" is indistinguishable from "not the title yet" to a classifier.
set -u
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
SD="$(cd "$(dirname "$0")" && pwd)"
OUT="${1:-/sylph-home/re/menuloop3}"; mkdir -p "$OUT"
ps -o pid= -C xenia_canary | xargs -r kill # plain kill: it clears its own lock
rm -f /tmp/xenia-canary.lock
( cd "$OUT" && nohup run-canary --mem_watch=false \
--logged_profile_slot_0_xuid=B13EBABEBABEBABE \
>"$OUT/canary.stdout" 2>"$OUT/canary.stderr" & )
sleep 10
if ! ps -C xenia_canary >/dev/null 2>&1; then
echo "EMULATOR DID NOT START -- stderr says:"; tail -3 "$OUT/canary.stderr"; exit 4
fi
echo "emulator alive, starting probe"
timeout 900 python3 "$SD/menu_loop_probe.py" "$OUT" 780
echo "MENU LOOP PROBE DONE"