From 608a0cc7103ceca526eb235fe0f98501a683b411 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 25 Aug 2026 22:42:33 +0000 Subject: [PATCH] =?UTF-8?q?re:=20measure=20the=20boot-nav=20title=20gate?= =?UTF-8?q?=20=E2=80=94=20and=20withdraw=20my=20own=20diagnosis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last iteration I said launch_mission died because skip_intro only tests for the title on a static frame, gated at rmse <= 1500, and that run logged 1503 and 1549 just above the cut. I also said the fix was NOT to nudge 1500 but to measure both signals through a boot first. Measured, and the diagnosis does not survive. boot_trace.sh logs the two signals skip_intro decides on -- frame-to-frame RMSE and the is_title.py green-glyph count -- through a clean boot with no presses at all. One run, 29 samples over 484 s: 8 samples had rmse <= 1500, so the gate OPENED eight times 0 samples had glyph > 0, so the title was never seen At t=145 s the RMSE was 1205, comfortably inside the threshold, and the glyph test was called and answered zero. A frame can be perfectly static without being the title -- the intro movie has long quiet stretches, three reading RMSE exactly 0. So 1503/1549 were almost certainly movie frames too, and raising the constant would have admitted two more of them. What is left is narrower and honest: the interactive title never appeared, rather than appearing and being missed by a threshold. The limitation is recorded rather than buried: the tracer intended 1 s sampling and achieved 16.9 s, because each iteration forks two screenshots, ImageMagick compare and a fresh Python. So this does NOT prove the title never appeared -- only that it was absent from 29 samples. A window shorter than ~17 s falls between them. The recorded next step is to make the tracer sample at the rate it claims before concluding anything stronger. Artifact: docs/re/captures/boot-signal-trace.tsv. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE --- docs/re/BACKLOG.md | 44 +++++++------ docs/re/boot-nav-title-gate.md | 85 ++++++++++++++++++++++++++ docs/re/captures/boot-signal-trace.tsv | 30 +++++++++ tools/re-capture/boot_trace.sh | 41 +++++++++++++ 4 files changed, 181 insertions(+), 19 deletions(-) create mode 100644 docs/re/boot-nav-title-gate.md create mode 100644 docs/re/captures/boot-signal-trace.tsv create mode 100755 tools/re-capture/boot_trace.sh diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 454ddec0..e54a6f39 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -1033,25 +1033,31 @@ premise was wrong.** Candidates: the **7 `.embsec_` sections** (VAs 0x84D0000–0x86AC000, ~129 KB total, executable) or a hashed record in `hidden/MiscBin.pak`. **Finding it gives the actual per-phase clear condition for every stage.** -* 🚧 **(2026-08-25) BLOCKED — the live test of the `reset_phase_threads` rename - never got to run, because boot-nav cannot reach the title.** The experiment is - ready and worth doing: built-in 100 clears the trigger container and frees - every thread but the caller, so at a phase terminator **both** - `[phase+272+20]` (triggers queued) and `[phase+216+8]` (coroutines alive) - should collapse; if they climb straight through a phase boundary instead, the - reading is wrong. `tools/re-capture/phase_watch.py` now prints both counters — - **written this session but never yet exercised against a live guest.** - **The blocker, measured:** `skip_intro.sh` only tests for the title when the - frame is static, gated at `rmse <= 1500`. A 381 s run produced 18 "waiting it - out" samples, and two of them — **1503 at 104 s and 1549 at 139 s** — sat just - above the cut, so `is_title.py` was never even called and the single allowed - press was never spent. The attract loop then restarts and the wait never ends. - ▶️ **First step, and it is NOT "raise the constant":** log `rmse` *and* the - `is_title.py` glyph count every second through a boot, so the true-title and - attract-demo distributions can be seen; only then decide the gate. Tuning 1500 - to make one run pass would be fitting to a single sample. Note the surrounding - comments are emphatic that tapping through the movies is what breaks the - title, so the press budget really is one. +* ❌ **(2026-08-25) My own boot-nav diagnosis, MEASURED AND WITHDRAWN.** I said + the run died because `skip_intro.sh` gates the title test at `rmse <= 1500` + and the run logged 1503/1549, just above the cut. Measured over a clean + no-press boot ([`boot-nav-title-gate.md`](boot-nav-title-gate.md), + `captures/boot-signal-trace.tsv`): the gate **opened eight times** in 29 + samples, and at `t=145 s` the RMSE was **1205** — inside the threshold — with + `is_title.py` answering **0 glyph pixels**. The glyph count was 0 in *all* 29 + samples over 484 s. A static frame is not the title; the intro movie has long + quiet stretches, three of them reading RMSE exactly 0. So the gate is not what + stopped it, and raising the constant would have admitted two more movie frames. + 🟡 Narrowed: **the interactive title never appeared**, rather than appearing and + being missed. ❔ **But the run does not prove that** — the tracer intended 1 s + sampling and achieved **16.9 s** (two screenshots + ImageMagick `compare` + a + fresh Python per iteration), so a title window under ~17 s falls between + samples. ▶️ First step: make the tracer sample at the rate it claims — one + long-lived Python process computing both signals — then re-run. Second: check + whether the fast 2026-08-25 run had a warm shader cache that this one, started + after `rm -f /dev/shm/xenia_*`, did not. +* 🚧 **(2026-08-25) STILL UNRUN — the live test of the `reset_phase_threads` + rename.** Built-in 100 clears the trigger container and frees every thread but + the caller, so at a phase terminator **both** `[phase+272+20]` (triggers + queued) and `[phase+216+8]` (coroutines alive) should collapse; if they climb + straight through a phase boundary instead, the reading is wrong. + `tools/re-capture/phase_watch.py` prints both — **written, still never + exercised against a live guest.** Blocked behind the boot-nav item above. * 🟡 **(2026-08-25) The legacy IDXD string-pool reader is wrong far more often than assumed** — now measurable for the first time, since the record table gives a ground truth. Verified by hand: `FCSRange` (the module doc's own canonical diff --git a/docs/re/boot-nav-title-gate.md b/docs/re/boot-nav-title-gate.md new file mode 100644 index 00000000..74158008 --- /dev/null +++ b/docs/re/boot-nav-title-gate.md @@ -0,0 +1,85 @@ +# The boot-nav title gate — my own diagnosis, measured and withdrawn + +Status: ❌ **the previous diagnosis is WITHDRAWN.** 🟡 the real failure is +narrowed but not settled. ❔ the measurement has a limitation that matters, and +it is stated below rather than buried. + +## What I claimed last iteration + +`launch_mission.sh` failed to reach the mission: 381 s of `skip_intro.sh` +printing "movie … waiting it out", 18 times, and the ready room never arrived. +`skip_intro.sh` only tests for the interactive title when the frame is **static**, +gated at `rmse <= 1500`, and that run logged **1503 at 104 s** and **1549 at +139 s** — just above the cut. I wrote that the gate had therefore blocked the +title test and eaten the run. + +I also wrote, correctly, that the fix was *not* to nudge 1500, and that the first +step was to measure both signals through a whole boot. That is what this is. + +## The measurement + +`tools/re-capture/boot_trace.sh` logs, without pressing anything, the two signals +`skip_intro.sh` decides on: the frame-to-frame RMSE (two grabs 0.6 s apart) and +the green-glyph pixel count from `is_title.py` — independent tests for "is this +static" and "is this the interactive title". + +One clean boot, no input at any point: +[`captures/boot-signal-trace.tsv`](captures/boot-signal-trace.tsv). + +``` +29 samples spanning 484 s + 8 samples with rmse <= 1500 (the gate OPENED eight times) + 0 samples with glyph > 0 (the title was never seen) +``` + +The eight low-RMSE samples: `t=12, 29, 62` (rmse 0), `t=99` (1486), `t=115` +(1350), `t=145` (**1205**), `t=316` (0), `t=484` (0). + +## ❌ Why the old diagnosis is wrong + +The gate is not what stopped it. It **opened eight times**, and at `t=145` the +RMSE was **1205** — comfortably inside the threshold — so `is_title.py` was +called and answered **0 glyph pixels**. A frame can be perfectly static and still +not be the title; the intro movie has long quiet stretches, and three samples +read an RMSE of exactly 0. + +So `1503`/`1549` in the failing run were almost certainly *also* not the title, +and raising the threshold to admit them would have admitted two more movie +frames, not the title. The instinct to leave the constant alone was right; the +reason I gave for it was wrong. + +## 🟡 What the failure actually is + +Narrowed, not settled: **the interactive title never appeared at all** in 484 s +of a no-press boot — not "appeared and was missed by the gate". The glyph count +was 0 in all 29 samples, including the last one at 484 s where the frame was +completely static (rmse 0). + +A static frame with no glyph at 484 s is consistent with the attract loop having +returned to a title that, per `menu_draw_capture.sh`, "accepts nothing at all" — +but this run never pressed anything, so nothing can have broken it. That points +at the boot itself, not at the navigation. + +## ❔ The limitation, stated plainly + +**The intended sampling rate was 1 s; the achieved rate was 16.9 s.** Each +iteration takes two screenshots, runs ImageMagick `compare` and starts a Python +interpreter, and that costs ~15 s per loop in this container. So 29 samples cover +484 s with big gaps between them. + +That means this run does **not** prove the title never appeared — only that it +was absent from every one of 29 samples. A title window shorter than ~17 s could +fall between two samples. Before concluding anything stronger, the tracer needs +to get cheap enough to sample at the rate it claims: keep one Python process +alive and compute the RMSE and the glyph count in it, instead of forking +`compare` and a fresh interpreter every second. + +## What this does not settle + +* Whether the title ever appears on this boot path at all. +* Whether the earlier "readyroom at 9 s, IN FLIGHT at 37 s" run (recorded + 2026-08-25 in `BACKLOG.md`) used a warm shader cache that this run lacked — + this one was started after `rm -f /dev/shm/xenia_*`, which the fast run may not + have been. +* The live test this was all for — whether the trigger container and the + coroutine list both collapse at a phase terminator — remains unrun. diff --git a/docs/re/captures/boot-signal-trace.tsv b/docs/re/captures/boot-signal-trace.tsv new file mode 100644 index 00000000..1893e5a8 --- /dev/null +++ b/docs/re/captures/boot-signal-trace.tsv @@ -0,0 +1,30 @@ +secs rmse glyph +12 0 0 +29 0 0 +47 11264 0 +62 0 0 +80 3250 0 +99 1486 0 +115 1350 0 +130 1512 0 +145 1205 0 +159 2714 0 +171 2712 0 +189 34429 0 +201 30721 0 +218 10379 0 +234 12487 0 +252 14940 0 +273 23650 0 +293 12200 0 +308 35782 0 +316 0 0 +332 8713 0 +351 21680 0 +369 7567 0 +386 4642 0 +412 9647 0 +436 6524 0 +458 18867 0 +476 11594 0 +484 0 0 diff --git a/tools/re-capture/boot_trace.sh b/tools/re-capture/boot_trace.sh new file mode 100755 index 00000000..627358d0 --- /dev/null +++ b/tools/re-capture/boot_trace.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Log the two signals `skip_intro.sh` decides on, every second, through a whole +# boot -- WITHOUT pressing anything. +# +# Why: skip_intro only runs the title test when the frame is static, gated at +# `rmse <= 1500`. A 381 s run measured 1503 and 1549 -- just above the cut -- so +# the title test was never called and the one allowed press was never spent. +# The fix is NOT to nudge 1500; a threshold moved to make one run pass is fitted +# to a single sample. This produces the distribution the gate should be chosen +# from: for every second, the frame-to-frame RMSE and the green-glyph count, +# which are independent measurements of "is this a static frame" and "is this +# the interactive title". +# +# Output is TSV on stdout: seconds, rmse, glyph_px. +set -u +export HOME=/sylph-home/re +DISP="${DISPLAY:-:98}" +SD="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +alive(){ ps -o pid=,stat= -C xenia_canary 2>/dev/null | awk '$2 !~ /^Z/ {print $1}'; } + +until xdotool search --name "Xenia-canary" >/dev/null 2>&1; do + [ -n "$(alive)" ] || { echo "# EMULATOR GONE before the window appeared" >&2; exit 4; } + sleep 1 +done +printf 'secs\trmse\tglyph\n' +deadline=$(( SECONDS + ${1:-600} )) +while [ $SECONDS -lt $deadline ]; do + rm -f /tmp/bt1.png /tmp/bt2.png + screenshot /tmp/bt1.png >/dev/null 2>&1; sleep 0.6 + screenshot /tmp/bt2.png >/dev/null 2>&1 + # Same liveness guard as skip_intro: two STALE files compare to a constant + # non-zero RMSE, which reads exactly like a playing movie. + if [ ! -s /tmp/bt1.png ] || [ ! -s /tmp/bt2.png ]; then + echo "# SCREENSHOT FAILED at ${SECONDS}s" >&2; exit 5 + fi + [ -n "$(alive)" ] || { echo "# EMULATOR GONE at ${SECONDS}s" >&2; exit 4; } + d=$(compare -metric RMSE /tmp/bt1.png /tmp/bt2.png null: 2>&1 | sed 's/ .*//' | cut -d. -f1) + g=$(python3 "$SD/is_title.py" /tmp/bt2.png 400 2>/dev/null | sed 's/[^0-9]*\([0-9]*\).*/\1/') + printf '%s\t%s\t%s\n' "$SECONDS" "${d:-0}" "${g:-0}" + sleep 1 +done