diff --git a/docs/game/navigation.md b/docs/game/navigation.md index db0e7c3c..185250f8 100644 --- a/docs/game/navigation.md +++ b/docs/game/navigation.md @@ -312,6 +312,15 @@ Traps that read as bugs but are not, all measured ✅: dies before the emulator does, silently, with no output at all. Kill by process **name**: `ps -o pid= -C xenia_canary | xargs -r kill -9`. (The same trap is in `METHOD.md` for `pgrep` wait-loops; it cost another launch on 2026-08-30.) +* **`kill -9` on xenia ORPHANS `/tmp/xenia-canary.lock`**, and the next + `run-canary` refuses with *"an emulator is already running"* — to **stderr**, + where a polling script never looks. A probe of mine then sampled a dead display + for **484 s**, reporting `other` every 4 s, because `screen_id.py` on an empty + screen returns `other` and *"not the title yet"* is indistinguishable from + *"there is no emulator"*. Kill with a plain `kill` so it can clear its own lock; + 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. * **`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 diff --git a/docs/re/structures/ui-resting-pose.md b/docs/re/structures/ui-resting-pose.md index 36f209fa..039d6480 100644 --- a/docs/re/structures/ui-resting-pose.md +++ b/docs/re/structures/ui-resting-pose.md @@ -135,11 +135,40 @@ 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. -⚠️ **Reach of the transfer.** This is the *EN* title's capture noise applied to the -*JP* title's box. Build 7 carries `ptloop01/02.rat`, which may animate inside that -region where the EN plate does not — so the in-box 0.000 is a measurement on a -neighbouring screen, not on the one adjudicated. A second JP capture would settle -it and has not been taken. +### ✅ And `ptloop01/02` do NOT free-run on the settled title + +The transfer above was flagged as uncertain because build 7 carries +`ptloop01/02.rat`, which might animate inside the adjudication box where the EN +plate does not. `sylpheed-port` then found those same two leaves free-running in +*their renderer* on the menu path, and was explicit that pinning a phase picks one +pose rather than the game's — *"a capture question, not a harness one"*. It is, and +two captures from different sessions answer it: + +``` +ptloop01/02 rest at (441,270), 200x90 -- INSIDE the adjudication box + differing px between session 1 and session 2: 0 of 18 000, max |d| 0 + contrast, whole frame: 116 492 px differ, max |d| 51 +``` + +**Byte-identical across sessions**, while 12.6 % of the frame moves. So on the +settled title the loops are static, and the in-box capture noise of 0.32 is not +theirs — the 645 differing pixels all lie in a 30-row band at `y 99..128`, nowhere +near the loop rect. + +🟡 **The menu is a different bundle and is NOT settled by this.** `GP_TITLE` build +5 declares `ptloop01/02` at the same rect with a single static keyframe, and that +is the screen the port's row actually drifted on. A probe to capture five settled +menu frames and diff that rect was written +([`menu_loop_rest.sh`](../../tools/re-capture/menu_loop_rest.sh)) and **did not +run to completion**: the run reached a title at t=146 s and Ⓐ did not take across +six attempts. That is the documented intermittency — the *attract loop's* title +accepts nothing, unlike the boot title +([`canary-scripted-input-traps.md`](../canary-scripted-input-traps.md)) — so the +question is open, with the instrument written and one successful run away. + +⚠️ Two committed main-menu captures exist but **cannot** answer it: they differ +across **57 %** of the surface (different geometries and capture paths), so the +88 % differing on the loop rect measures the mismatch, not the loops. ⚠️ **Reach.** One screen, one capture. The absolute residual is ~40 RMSE even at the best instant, because the JP title carries live animation a static render diff --git a/tools/re-capture/menu_loop_rest.sh b/tools/re-capture/menu_loop_rest.sh new file mode 100755 index 00000000..8e5b14c0 --- /dev/null +++ b/tools/re-capture/menu_loop_rest.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Do ptloop01/02 ANIMATE on the settled main menu? +# +# sylpheed-port pinned a leaf phase for these and was explicit that doing so +# picks ONE pose rather than the one the game shows -- "a capture question, not a +# harness one". On the JP title, two captures from different sessions are +# byte-identical over the ptloop rect (0 of 18 000), so they do not free-run +# there. The menu is a different bundle and is where their row actually drifted. +# +# If the loops animate at all, consecutive frames of a SETTLED menu differ over +# their rect. A whole-frame count runs alongside as the contrast control: if the +# frame is entirely static the instrument cannot see motion and the zero is void. +# +# Usage: menu_loop_rest.sh OUTDIR +set -u +export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98 +SD="$(cd "$(dirname "$0")" && pwd)" +OUT="${1:-/sylph-home/re/menuloop}"; mkdir -p "$OUT" +alive(){ ps -o pid=,stat= -C xenia_canary 2>/dev/null | awk '$2 !~ /^Z/ {print $1}'; } +shot(){ screenshot "$1" >/dev/null 2>&1; } +screen(){ shot /tmp/mlr.png; python3 "$SD/screen_id.py" /tmp/mlr.png | awk '{print $1}'; } + +ps -o pid= -C xenia_canary | xargs -r kill -9 +( cd "$OUT" && nohup run-canary --mem_watch=false \ + --logged_profile_slot_0_xuid=B13EBABEBABEBABE \ + >"$OUT/canary.stdout" 2>"$OUT/canary.stderr" & ) +sleep 8 +deadline=$(( SECONDS + 900 )); s="" +while [ $SECONDS -lt $deadline ]; do + s="$(screen)"; echo "t=${SECONDS}s $s"; [ "$s" = "title" ] && break; sleep 4 +done +[ "$s" = "title" ] || { echo "NEVER REACHED THE TITLE"; exit 1; } +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; } +sleep 6 # let the menu settle past its build-in +for i in 0 1 2 3 4; do shot "$OUT/menu-$i.png"; sleep 2; done +python3 - "$OUT" <<'PY' +import sys, numpy as np +from PIL import Image +out=sys.argv[1] +def load(i): + a=np.asarray(Image.open(f"{out}/menu-{i}.png").convert("RGB"),dtype=int) + return a, (45 if a.shape[0]>=716 else 0) +a0,o0=load(0) +print("\nptloop01/02 rect, 200x90 at design (441,270) -- do they move?") +for i in (1,2,3,4): + ai,oi=load(i) + la=a0[o0+270:o0+360, 441:641]; lb=ai[oi+270:oi+360, 441:641] + d=np.abs(la-lb) + h=min(a0.shape[0]-o0, ai.shape[0]-oi); w=min(a0.shape[1], ai.shape[1]) + dw=np.abs(a0[o0:o0+h,:w]-ai[oi:oi+h,:w]).sum(axis=2) + print(f" frame {i} vs 0: loop rect {int((d.sum(axis=2)>0).sum()):6d}/18000 px max |d| {int(d.max()):3d}" + f" | CONTRAST whole frame {int((dw>0).sum()):7d} px") +PY +echo "MENU LOOP PROBE DONE"