re: the main menu has no idle self-return -- and four of my own durations were a backlog

Two results and one retraction, all from the same session.

REFUTED: 'an ~8-10 s idle returns to the title' does not apply to the main menu.
Held untouched it stayed put for >= 60 s, correlation never leaving
0.9245-0.9249. That timer is real but belongs to the TITLE. It was the only
reason 'B leaves the main menu' was classed as authored, so Q5's B rule is
upgraded to measured-ordering: B is delivered (canary logs vk=5801) and is the
only input in >= 100 s before the return.

The PRESS (A) plate: the boot title presents build 4 WITHOUT the plate first --
green-glyph 154, against 159 on the committed no-plate capture and 753/977/1493
on plate titles -- and the plate arrives after. That is the port's third option.

RETRACTED: four durations taken the same day. classify_array costs 1503 ms per
frame; running it per frame against an 8 fps x11grab drained the pipe at
0.64 fps, so every classified frame was stale and increasingly so. It
manufactured 'plate 24.66 s after the title art', 'B->title 15.58 s', 'B->title
25.60 s' and 'A->menu 20.26 s'. The tell: a transition, a press and a fade do
not share a duration, and the two B figures GREW across a longer run.

A backlog preserves ordering and destroys durations, which is why the sequence
results above stand and every timing does not. The ring's period is unaffected
and that was checked, not assumed -- ring_period ran at 15.03 fps against a
requested 15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNR5Y79D1T4bBr6gJQaWFP
This commit is contained in:
sylph-decoder
2026-08-29 11:50:46 +00:00
parent 4fa3099249
commit 9a10258738
7 changed files with 427 additions and 8 deletions

View File

@@ -32,7 +32,7 @@ Confidence: ✅ seen in a capture · 🟡 inferred · ❔ unknown.
| 1 | **SQUARE ENIX** in white on black, the two dots in red, `™` after it ✅ | nothing | fades on to the next logo |
| 2 | **GAME ARTS**, **SETA** and **studio anima** stacked on black ✅ | nothing | fades on into the cinematic |
| 3 | The opening cinematic | **Ⓐ** skips it ✅ | ends into the title |
| 4 | **Title screen** — the wordmark, then `PRESS Ⓐ BUTTON` below it and the 2006/2007 Square Enix copyright line at the bottom ✅ | press **Ⓐ** | goes to the main menu ✅ |
| 4 | **Title screen** — the wordmark appears **first, with no prompt**; `PRESS Ⓐ BUTTON` fades in a beat later, above the 2006/2007 Square Enix copyright line ✅ | press **Ⓐ** | goes to the main menu ✅ |
| 5 | **Main menu** | — | see §2 |
Both logo screens are **still pictures the game draws**, not video — neither is a
@@ -62,6 +62,14 @@ predicts which ✅ — budget retries.
Five options in a vertical stack, roughly centred, with a highlighted state on
the focused one.
> ✅ **The focused option carries a small ring to the left of its label, and the
> ring turns — continuously, about once every 2.2 s.** It has a bright head, so
> you can see it go round. It is the **only** thing moving on this screen once it
> has settled: the labels, the bracket and the footer are all completely still
> (temporal std exactly 0.000 over 20 s). Ⓑ
> [five frames, 4 s apart](../re/captures/focus-ring/ring-single-frames-4s-apart.png) ·
> [the measurement](../re/focus-ring-spin-measured.md)
| position | label | what it opens |
|---|---|---|
| 1 | **NEW GAME** | a **DIFFICULTY** prompt, then **SELECT DATA** ✅ |
@@ -97,11 +105,14 @@ assume the top item, and do not assume the middle one either.
> ⚠️ **The main menu is the one screen whose footer does not offer Ⓑ.** It reads
> `⊙ : Select Ⓐ : OK` — every submenu adds `Ⓑ : Back`. Measured: **zero**
> red-Ⓑ glyph pixels anywhere in the frame, on two captures, with the same
> detector finding the glyph on `EXTRAS` and `DIFFICULTY` ✅. Ⓑ *was* observed
> returning to the title, but only once, without a capture, and the title also
> returns on its own after ~810 s idle — so an observer could not have told the
> two apart. Treat "Ⓑ leaves the main menu" as likely but unproven
> ([the measurement](../re/menu-navigation-semantics.md#-refutation-attempt-2026-08-29--the-main-menus-own-footer-does-not-advertise-ⓑ)).
> detector finding the glyph on `EXTRAS` and `DIFFICULTY` ✅.
> ✅ **But Ⓑ does leave it, and the objection that stood here is refuted
> (2026-08-29).** This page used to say the title "returns on its own after
> ~810 s idle", so an observer could not tell Ⓑ from the timer. That timer
> belongs to the **title**, not to this screen: the main menu was held untouched
> for **≥ 60 s** and never moved. Ⓑ is delivered and is the only input in ≥ 100 s
> before the return, so the ordering is measured — the *latency* is not
> ([the measurement](../re/menu-idle-and-b-2026-08-29.md)).
*Internals: `GP_TITLE.pak` build 5; buttons `ptbtn01``ptbtn05` top to bottom.*

View File

@@ -49,6 +49,54 @@ a bind mount on another device, and which is what the withdrawn banner ran.
`sylpheed-cli` against a pak, run the disc-gated tests and read the executable
again. New measurements are available; ask for them.
## ✅ 2026-08-29 — three answers from one oracle session (the port's asks 1, 2 and 3)
* **1 — the focus ring SPINS CONTINUOUSLY. Period 2.18 s wall-clock; author it
as 120 units = 60 frames = 2.00 s at 30 Hz.** It does not ramp once and stop.
Measured with no angle estimated anywhere — the angular estimator written for
this **failed its own control** (a synthetic 30° came back as 0°) and was not
used. What settles it instead: total annulus brightness is conserved to
**0.4 %** while individual angular bins swing by **24** — brightness moving
*around* the ring, which excludes a pulse — and the profile's autocorrelation
has **eight evenly spaced peaks, mean 2.177 s**, over nine revolutions.
⚠️ Do not read the committed 20 s mean image as a frame: the spin averages to
a uniform circle, which is why it looks headless. Five single frames 4 s apart
show the head at five different angles.
[`focus-ring-spin-measured.md`](../re/focus-ring-spin-measured.md) ·
[frames](../re/captures/focus-ring/ring-single-frames-4s-apart.png)
***And the ring is the ONLY thing that moves on the settled main menu.**
Temporal std over 20 s untouched is **exactly 0.000** on every unfocused
button, on the labels and on the `ptmsg` footer. Static menu + spinning ring
draws everything that moves.
* **3 — the idle timer that made your Ⓑ rule unprovable is REFUTED on the main
menu.** Held untouched, the menu stayed put for **≥ 60 s** (49 samples, menu
correlation never leaving 0.92450.9249), against the "~810 s idle returns to
the title" this page carried. ✅ That timer is real but belongs to the
**title**, not the menu — the corpus had it attached to the wrong screen.
🟡 Ⓑ itself: delivered (Canary logs `vk=5801`), and in both runs the only input
in ≥ 100 s, followed by the title. **Ordering measured, timing not** — keep
Ⓑ→title, now better supported than authored.
[`menu-idle-and-b-2026-08-29.md`](../re/menu-idle-and-b-2026-08-29.md)
* **Your new #1 — the boot title shows build 4 FIRST, and the plate arrives
after.** It is your third option, not the first two. Green-Ⓐ glyph count on the
boot title went **154 → 781**, and 154 is the same reading the committed
`live-title-build4-no-plate.png` gives (159) while plate titles give
753/977/1493. 🔴 **The delay is NOT measured** — see the next bullet.
* 🔴 **Four durations I took the same day are WITHDRAWN, including the plate
delay.** `screen_match.classify_array` costs **1503 ms/frame**; a probe running
it per frame drained an 8 fps stream at **0.64 fps**, so its frames were stale
and increasingly so. It manufactured "plate 24.66 s after the title art",
"Ⓑ→title in 15.58 s", "Ⓑ→title in 25.60 s" and "Ⓐ→menu in 20.26 s". The tell:
a transition, a press and a fade do not share a duration — a backlog does.
A backlog **preserves ordering and destroys durations**, which is exactly why
the sequence results above stand and the timings do not. Fixed (`fast=True`,
3875 ms, re-controlled 8/8 on both paths, agreeing to ±0.005); the ring's
numbers are unaffected and that was checked, not assumed.
## Status
| | Question | State | Answer / link |
@@ -926,7 +974,7 @@ here until 2026-08-28 and is now settled.)
| 🟡 | **the paint-order tie-break** (Q3) | eight candidates refuted; costs one element's blend on one screen |
| 🟡 | **GamePart ids behind the buttons** (Q4) | the *screens* are measured; the ids are a name match onto the executable's class names |
| 🟡 | **the boot transitions in code** (Q6) | both levels decoded — phase at `this+132` (`entry→2`, `2→0`, `2→3`, `3→4`, `4→2`) and state at `this+136` inside phase 4. Phase 0 = splash (`LOGO`), phase 2 = title + `PRESS Ⓐ`, phase 4 = menu. Unknown: what the event *numbers* mean |
| 🟡 | **Ⓑ leaving the main menu** (Q5) | **downgraded 2026-08-29.** The behaviour was seen once, with no capture, and the main menu is the only screen whose footer does not advertise Ⓑ — measured, **0** red-Ⓑ glyph pixels in the whole frame on two captures, against 514/518 on `EXTRAS`/`DIFFICULTY` with the same detector. An ~810 s idle also returns to the title, and nobody has separated the two. Implement it (it is the only exit) but treat it as **authored** — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md#-refutation-attempt-2026-08-29--the-main-menus-own-footer-does-not-advertise-ⓑ) |
| 🟡 | **Ⓑ leaving the main menu** (Q5) | **upgraded 2026-08-29 (later).** The idle half of this objection is **refuted**: the main menu does not self-return for **≥ 60 s** untouched, and the ~810 s idle belongs to the **title**. Ⓑ is delivered (Canary logs `vk=5801`) and is the only input in ≥ 100 s before the return, so the **ordering is measured**; the latency is not (a backlogged probe void). The footer point stands — the main menu is still the only screen not advertising Ⓑ — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md#-refutation-attempt-2026-08-29--the-main-menus-own-footer-does-not-advertise-ⓑ) |
| ❔ | **builds 0/1 and 10/11**, the `DELTASABER` plates (Q2) | never seen anywhere in the boot path, the title-side screens or the attract loop. A mission load is the remaining candidate and this container kills runs before one completes |
(An earlier version of this table called the audio items blocked on "an emulator

View File

@@ -147,7 +147,7 @@ files, which is how the same ground got covered twice.
| [`structures/unit-struct-runtime.md`](structures/unit-struct-runtime.md) | Runtime `Unit` struct (craft / vessel definitions) — read from live guest memory | — |
| [`structures/weapon-struct-runtime.md`](structures/weapon-struct-runtime.md) | Runtime `Weapon` / `Shell` structs — read from live guest memory | — |
| [`structures/xbg7-mesh.md`](structures/xbg7-mesh.md) | XBG7 — mesh geometry (inside XPR2 model containers) | — |
| [`capture-harness-status.md`](capture-harness-status.md) | Why the harness stops reaching the title — and, since 2026-08-29, why there is no oracle at all | 🔴 **the decoder container has NO disc**: no ISO, no XEX, no pak anywhere on the filesystem. Root-caused to `06676d3` moving `/work` to a named volume without replacing the disc mount; `sylph-port` mounts it, `sylph-decoder` does not. Earlier sections: `screenshot` costs 10.8 s under xenia (92×), and `trace_gpu_stream` is a no-op in the Release build |
| [`capture-harness-status.md`](capture-harness-status.md) | Why the harness stops reaching the title — and the two instruments that could not see the disc | **the disc is BACK** (2026-08-29, container replaced at 11:07:38): `/disc` is a real 6.2 GB read-only mount and `screen list` returns 12 builds. The "no disc" section is withdrawn — and its two instruments were blind either way: `find / -xdev` cannot cross into a bind mount on another device, and `sylph-doctor` only ever looks under `/work`. Earlier sections: `screenshot` costs 10.8 s under xenia (92×), and `trace_gpu_stream` is a no-op in the Release build |
| [`title-crash-stl-tree.md`](title-crash-stl-tree.md) | The title-screen crash is an STL `map`/`set` erase on a bad iterator | ✅ CONFIRMED — the guest throws std::out_of_range from an STL |
| [`ui-paint-order-third-permutation.md`](ui-paint-order-third-permutation.md) | A third measured paint order — tool built and validated, screen not reached | ✅ the reader works and is CONFIRMED against both previously |
| [`ui-quad-class-foothold.md`](ui-quad-class-foothold.md) | The guest's UI quad class — a foothold found from the capture's vertex layout | 🟡 PROBABLE for the identification below (it is a static read, but |
@@ -162,3 +162,5 @@ files, which is how the same ground got covered twice.
| [`upstream-baseline.md`](upstream-baseline.md) | A stock-upstream baseline runs Stage 02 crash-free | ✅ CONFIRMED — upstream canary_experimental + only the pad |
| [`weapon-datasheet-runtime.md`](weapon-datasheet-runtime.md) | Weapon DATA SHEET — runtime capture (Route B) | 🟡 first dynamic capture, 2026-07-28. The Arsenal's Gallery Mode panel is a |
| [`xpr2-colour-check.md`](xpr2-colour-check.md) | XPR2 colours: channel order ✅ confirmed against the running game | — |
| [`focus-ring-spin-measured.md`](focus-ring-spin-measured.md) | The main menu's focus ring spins continuously — and how fast | ✅ **measured**: period **2.177 s** over 9 revolutions (8 evenly spaced autocorrelation peaks) = 120 units = 60 frames = 2.00 s at 30 Hz. A pulse is excluded — annulus total conserved to 0.4 % while per-bin brightness swings by 24. ✅ the ring is the **only** moving thing on the settled main menu (std exactly 0.000 elsewhere). 🔴 no angle is quoted: the angular estimator FAILED its own control (30° → 0°) |
| [`menu-idle-and-b-2026-08-29.md`](menu-idle-and-b-2026-08-29.md) | The main menu does not idle back to the title — and four durations that were a pipeline | ✅ **refuted**: no self-return in **≥ 60 s** untouched; the ~810 s idle belongs to the **title**. 🟡 Ⓑ→title ordering measured, latency not. 🔴 `classify_array` at **1503 ms/frame** drained an 8 fps stream at 0.64 fps and manufactured four latencies (24.66 s / 15.58 s / 25.60 s / 20.26 s) — all withdrawn; a backlog preserves ordering and destroys durations |

View File

@@ -617,3 +617,35 @@ neighbourhood, not just the line.
`10 144 of 10 148 references resolve`) → **withdrawn; it is on the disc.** It is
the `GP_STAGE_CLEAR` child the same scan named `8AX`. With the name decoded the
count is **10 148 of 10 148**. [`ratc-child-names.md`](structures/ratc-child-names.md)
## The oracle harness and the container (2026-08-29)
* "the decoder container has no disc" → **refuted the same day.** The container
was replaced and `/disc` is a real 6.2 GB read-only mount. Worse, both
instruments behind the claim were blind to the answer either way:
`find / -xdev` **cannot cross** into a bind mount on another device, and
`sylph-doctor` only checks `/work` and never `$SYLPHEED_DISC`. "sylph-doctor
agrees" was two instruments sharing one blind spot.
→ To test for the disc, ask the variable that names it:
`sylpheed-cli screen list "$SYLPHEED_DISC/dat/GP_TITLE.pak"`.
* "the main menu returns to the title on its own after ~810 s idle" → **refuted.**
The menu sat untouched for **≥ 60 s** without moving (correlation never leaving
0.92450.9249). The ~810 s idle is real but belongs to the **title**. This was
the only reason "Ⓑ leaves the main menu" was classed as authored.
* "whole-image statistics (green / white / mean) can tell the title from the
attract movie" → **refuted.** A frame of `ADV.wmv` with a bright green laser
reads green 0.0018 / white 0.086 / mean (53,67,76) — the title's numbers. A
probe built on it tapped Ⓐ into the movie and waited 120 s for a menu that was
never coming. → Correlate against a committed capture instead, and keep movie
frames as the negative controls.
* "a 360-bin angular cross-correlation can measure the focus ring's rotation
angle" → **refuted by its own control**: a synthetic **30°** rotation of a live
frame came back as **0°** (peak 0.596), while 90/180/270° came back exactly
(peak 1.000) — it only resolves exact pixel permutations. No angle was quoted;
the spin was established from brightness conservation instead.
* "a latency read off a classified `x11grab` stream is a duration" → **refuted.**
At 1503 ms per classification against an 8 fps stream the consumer ran at
0.64 fps, so frames were stale and increasingly so. Four "durations" died with
it. The tell was that a screen transition, a button press and a plate fade all
came out at ~2025 s. → A backlog **preserves ordering and destroys
durations**; check consumed-fps against requested-fps before quoting a time.

View File

@@ -0,0 +1,120 @@
# 🔴 The main menu does NOT self-return to the title — and three "latencies" were my own pipeline
**Status:** one ✅ **measured** negative, one 🟡 ordering-only result, and one
🔴 **instrument defect that voids three numbers I took the same day.** Taken
2026-08-29.
## ✅ Refuted: "an ~810 s idle returns to the title" does not apply to the main menu
HANDOFF's residue table downgraded *Ⓑ leaves the main menu* to **authored**, on
the grounds that "an ~810 s idle also returns to the title, and nobody has
separated the two". That reason is now gone.
**Measured:** the main menu was held with **no input at all** and classified every
~1.2 s by [`screen_match.py`](../../tools/re-capture/screen_match.py):
| phase | duration untouched | screens seen |
|---|---|---|
| period capture | 24 s | menu only |
| idle probe | **60 s** (49 samples) | **menu only** |
Correlation against the committed main-menu capture never moved outside
**0.9245 0.9249** across the whole idle window — not a drift, not a fade, a
screen sitting still. Conservatively that is **≥ 60 s of continuous idle with no
self-return**, against a claim of 810 s.
**And the 810 s idle is real — it belongs to the TITLE.** Immediately after
this run, a probe that expected to find the title still on screen found it had
left on its own into the attract movie. So the corpus's idle timer is a property
of the **title screen** (title → `ADV.wmv` → title), and the residue table
attached it to the wrong screen.
**What this gives the port:** the idle alternative that made Ⓑ unprovable is
refuted *on the screen in question*. Ⓑ is no longer competing with a timer there.
## 🟡 Ⓑ on the main menu: the ordering survives, the timing does not
Ⓑ was **delivered** — Canary's own log records `[file-pad] keystroke vk=5801
down` / `up` and `XamInputGetKeystrokeEx -> user=0 vk=5801`, so this is not a
dropped press. In both runs the menu was followed by the title, and in both runs
**Ⓑ was the only input** in a window of ≥ 100 s either side.
🟡 **But it is still two observations with a confound I cannot yet exclude**, and
the reason is the next section: the "latency" I measured is worthless, so I
cannot say the return followed Ⓑ *promptly*. What stands is ordering plus the
absence of any other cause:
* no input for ≥ 100 s before → no transition;
* Ⓑ delivered → transition to the title.
**Classification: measured ordering, unmeasured timing.** The port should keep
Ⓑ→title, and it is now better supported than "authored" — but it is not yet a
timed measurement.
## 🔴 The defect: an oracle that cost 1503 ms per frame produced three fake latencies
`screen_match.classify_array` does a ±8 px ZNCC search over a 675×1279 surface
against two references. **Measured: 1503 ms per frame.** A probe calling it on
every frame of an 8 fps `x11grab` stream therefore drained the pipe at
**0.64 frames/s** — verified from the probe's own trace, 107 samples over 166 s.
The pipe backed up at ~7.4 fps, so every frame classified was **stale, and
increasingly so**. That is not a subtle bias; it manufactured three numbers:
| reported | actually |
|---|---|
| plate appears 24.66 s after the title art | unknown |
| Ⓑ → title in 15.58 s (run 1) | unknown |
| Ⓑ → title in 25.60 s (run 2) | unknown |
| Ⓐ → menu in 20.26 s | unknown |
🔴 **All four are withdrawn.** The tell was that they are all ~2025 s: a screen
transition, a button press and a plate fade do not share a duration, but a
backlog does. The two Ⓑ figures *growing* 15.6 → 25.6 s across a longer run is
the backlog accumulating, and it is the signature to remember.
⚠️ **What a backlog does and does not destroy.** It delays every frame by the
same growing amount, so it **preserves ordering** and destroys **durations**.
That is why the ordering results above survive and every duration here does not.
**Fixed and re-controlled.** `screen_match` now has a `fast=True` path
(4× decimation, ±2 decimated px) at **3875 ms**, a 2060× reduction, and the
control was re-run on **both** paths: 8/8 each, with the fast path agreeing with
the exact path to **±0.005** on every score.
**The ring measurements are NOT affected**, and this was checked rather than
assumed: `ring_period.py` does a greyscale conversion and a crop per frame, and
achieved **15.03 fps against a requested 15** — it kept up exactly, so its
timestamps carry no backlog. Its period also has an internal check a drifting
clock cannot pass: eight *evenly spaced* autocorrelation peaks
([`focus-ring-spin-measured.md`](focus-ring-spin-measured.md)).
## 🟡 The `PRESS Ⓐ` plate: sequence answered, duration not
The port asked whether the boot title is build 4 alone, build 4 with the plate
composited from the start, or build 4 **then** the plate after a delay.
**It is the third.** On the boot title the green-Ⓐ glyph count went
**154 → 781** with the title art already matching at 0.946. The 154 is the
decisive number: the committed no-plate capture
`live-title-build4-no-plate.png` reads **159** with the same counter, and plate
titles read 753 / 977 / 1493. So the title genuinely presents **without** the
plate first, and the plate arrives afterwards.
🔴 **How long afterwards is NOT measured** — that figure came from the backlogged
probe and is withdrawn with the rest. The port needs one more run with the fast
path to get it.
## Instrument controls, now committed
The negative controls for `screen_match` are **movie frames**, because that is
the class the oracle exists to reject — a statistics-based oracle
(green/white/mean) called a frame of `ADV.wmv` containing a bright green laser
`title`, and a probe built on it tapped Ⓐ into the movie and then waited 120 s
for a menu that was never coming.
An earlier version of the control list pointed at two **scratch** grabs, and a
later run of the same probe overwrote one of them — turning a negative control
into a title frame and failing the control for the wrong reason. They are now
committed fixtures under
[`captures/instrument-controls/`](captures/instrument-controls/).

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Does (B) leave the main menu -- and does the menu self-return to the title?
HANDOFF downgraded "(B) on the main menu returns to the title" to authored,
because the corpus also carries "an ~8-10 s idle returns to the title" and one
unrecorded observation cannot separate the two causes. This separates them by
ordering: hold the menu UNTOUCHED for an idle window several times longer than
the claimed 8-10 s and timestamp what happens, THEN press (B) and timestamp
again. If the idle window passes with the menu still up, the idle cause is
gone and the (B) observation is unambiguous.
Screen identity comes from screen_match.py, whose control includes the movie
frames that broke the statistics-based oracle.
Usage: menu_b_probe.py IDLE_SECONDS AFTER_SECONDS
"""
import os, subprocess, sys, time
import numpy as np
from PIL import Image
SD = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SD)
from screen_match import classify_array
W, H = 1280, 720
IDLE = float(sys.argv[1]) if len(sys.argv) > 1 else 60.0
AFTER = float(sys.argv[2]) if len(sys.argv) > 2 else 30.0
OUT = "/sylph-home/re/ringcap"
def stream():
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 main():
p = stream(); n = W * H * 3
t0 = time.time(); seg = t0; last = None; prev = None
phase = "IDLE"; pressed_at = None
log = []
while True:
el = time.time() - t0
if phase == "IDLE" and el >= IDLE:
subprocess.run(["python3", f"{SD}/pad.py", "tap", "B", "0.3"], check=False)
pressed_at = time.time() - t0
print(f"t={pressed_at:6.2f}s >>> (B) PRESSED", flush=True)
phase = "AFTER"
if phase == "AFTER" and el >= IDLE + AFTER:
break
if time.time() - seg > 25:
p.kill(); p = stream(); seg = time.time()
b = p.stdout.read(n)
if len(b) < n:
p.kill(); p = stream(); seg = time.time(); continue
a = np.frombuffer(b, np.uint8).reshape(H, W, 3)
last = a
c, sc = classify_array(a)
log.append((el, c, sc["title"], sc["menu"]))
if c != prev:
print(f"t={el:6.2f}s screen={c:<6} title={sc['title']:+.3f} "
f"menu={sc['menu']:+.3f}", flush=True)
Image.fromarray(a).save(f"{OUT}/b-{el:06.2f}-{c}.png")
prev = c
p.kill()
with open(f"{OUT}/menu-b-trace.tsv", "w") as f:
f.write("t_s\tscreen\tcorr_title\tcorr_menu\n")
for r in log:
f.write(f"{r[0]:.3f}\t{r[1]}\t{r[2]:.4f}\t{r[3]:.4f}\n")
idle = [r for r in log if r[0] < IDLE]
aft = [r for r in log if pressed_at and r[0] > pressed_at + 1.0]
print(f"\nIDLE phase : {len(idle)} samples over {IDLE:.0f}s, "
f"screens seen = {sorted(set(r[1] for r in idle))}")
print(f"AFTER (B) : {len(aft)} samples, "
f"screens seen = {sorted(set(r[1] for r in aft))}")
first_title = next((r[0] for r in aft if r[1] == "title"), None)
if first_title:
print(f" first 'title' at t={first_title:.2f}s = "
f"{first_title - pressed_at:.2f}s after the (B) press")
print(f"trace: {OUT}/menu-b-trace.tsv")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""One boot, two answers: when the PRESS (A) plate appears, and what (B) does.
Q(plate): the port's boot ends on GP_TITLE build 4, which carries no plate, and
(A) is the only way off it -- so it ships a screen that needs a press and does
not say so. Builds 2/3 are the plate. The open half is the SEQUENCE: build 4
alone, build 4 with the plate composited from the start, or build 4 and THEN
the plate after a delay. This logs the title-art correlation and the green-(A)
glyph count on EVERY frame from before the title appears, so the two crossings
are read off one trace rather than inferred.
Q(B): whether (B) leaves the main menu, timed against the idle alternative.
Controls, both pre-run on committed captures:
* screen identity -- screen_match.py, 8/8 including the movie frames that
broke the statistics oracle;
* the plate -- green-(A) glyph count: 753/977/1493 px on plate titles, 159 on
`live-title-build4-no-plate.png`, 327 on the main menu. Threshold 400.
Usage: title_plate_and_b_probe.py OUTDIR
"""
import os, subprocess, sys, time
import numpy as np
from PIL import Image
SD = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SD)
from screen_match import classify_array
W, H = 1280, 720
PLATE = 400
OUT = sys.argv[1] if len(sys.argv) > 1 else "/sylph-home/re/platecap"
os.makedirs(OUT, exist_ok=True)
def stream():
return subprocess.Popen(
["ffmpeg", "-loglevel", "error", "-f", "x11grab", "-draw_mouse", "0",
"-video_size", f"{W}x{H}", "-i", ":98", "-r", "8",
"-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
stdout=subprocess.PIPE, bufsize=W * H * 3 * 2)
def glyph(a):
r, g, b = a[:, :, 0].astype(int), a[:, :, 1].astype(int), a[:, :, 2].astype(int)
return int(((g > 130) & (g - r > 45) & (g - b > 45)).sum())
def main():
p = stream(); n = W * H * 3
t0 = time.time(); seg = t0; prev = None
skipped = False; stage = 0; marks = {}
rows = []
while time.time() - t0 < 420:
if time.time() - seg > 25:
p.kill(); p = stream(); seg = time.time()
b = p.stdout.read(n)
if len(b) < n:
p.kill(); p = stream(); seg = time.time(); continue
a = np.frombuffer(b, np.uint8).reshape(H, W, 3)
el = time.time() - t0
c, sc = classify_array(a)
gl = glyph(a)
rows.append((el, c, sc["title"], sc["menu"], gl))
if c != prev:
print(f"t={el:7.2f}s screen={c:<6} title={sc['title']:+.3f} "
f"menu={sc['menu']:+.3f} glyph={gl}", flush=True)
prev = c
if not skipped and el > 45:
subprocess.run(["python3", f"{SD}/pad.py", "tap", "A", "0.3"], check=False)
skipped = True
print(f"t={el:7.2f}s one (A) to skip the intro movie", flush=True)
elif stage == 0 and c == "title":
marks["title_art"] = el; stage = 1
Image.fromarray(a).save(f"{OUT}/title-first-{el:07.2f}.png")
print(f"t={el:7.2f}s TITLE ART (glyph={gl}) — watching for the plate",
flush=True)
elif stage == 1 and gl >= PLATE:
marks["plate"] = el; stage = 2
Image.fromarray(a).save(f"{OUT}/title-plate-{el:07.2f}.png")
print(f"t={el:7.2f}s PLATE (glyph={gl}) — "
f"{el-marks['title_art']:.2f}s after the title art", flush=True)
elif stage == 2 and el > marks["plate"] + 6:
subprocess.run(["python3", f"{SD}/pad.py", "tap", "A", "0.3"], check=False)
marks["A"] = el; stage = 3
print(f"t={el:7.2f}s >>> (A) on the title", flush=True)
elif stage == 3 and c == "menu":
marks["menu"] = el; stage = 4
print(f"t={el:7.2f}s MENU — idling 25 s before (B)", flush=True)
elif stage == 4 and el > marks["menu"] + 25:
subprocess.run(["python3", f"{SD}/pad.py", "tap", "B", "0.3"], check=False)
marks["B"] = el; stage = 5
print(f"t={el:7.2f}s >>> (B) PRESSED", flush=True)
elif stage == 5 and c != "menu":
marks["left_menu"] = el
print(f"t={el:7.2f}s LEFT THE MENU -> {c}, "
f"{el-marks['B']:.2f}s after (B)", flush=True)
stage = 6
elif stage == 6 and el > marks["left_menu"] + 12:
break
p.kill()
with open(f"{OUT}/trace.tsv", "w") as f:
f.write("t_s\tscreen\tcorr_title\tcorr_menu\tglyph\n")
for r in rows:
f.write(f"{r[0]:.3f}\t{r[1]}\t{r[2]:.4f}\t{r[3]:.4f}\t{r[4]}\n")
print("\nmarks:", {k: round(v, 2) for k, v in marks.items()})
if "title_art" in marks and "plate" in marks:
print(f"PLATE DELAY: {marks['plate']-marks['title_art']:.2f}s "
f"after the title art first matched")
if "B" in marks and "left_menu" in marks:
print(f"(B) -> left the menu in {marks['left_menu']-marks['B']:.2f}s")
print(f"trace: {OUT}/trace.tsv")
return 0
if __name__ == "__main__":
sys.exit(main())