From 7a4a74f8d7324c31f0baa7fe2cf9c49aeecd2fab Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Sat, 29 Aug 2026 11:50:46 +0000 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01KNR5Y79D1T4bBr6gJQaWFP --- docs/game/navigation.md | 23 +++- docs/port/HANDOFF.md | 50 +++++++- docs/re/INDEX.md | 4 +- docs/re/REFUTED.md | 32 ++++++ docs/re/menu-idle-and-b-2026-08-29.md | 120 ++++++++++++++++++++ tools/re-capture/menu_b_probe.py | 87 ++++++++++++++ tools/re-capture/title_plate_and_b_probe.py | 119 +++++++++++++++++++ 7 files changed, 427 insertions(+), 8 deletions(-) create mode 100644 docs/re/menu-idle-and-b-2026-08-29.md create mode 100644 tools/re-capture/menu_b_probe.py create mode 100644 tools/re-capture/title_plate_and_b_probe.py diff --git a/docs/game/navigation.md b/docs/game/navigation.md index e45c0be3..605abcc8 100644 --- a/docs/game/navigation.md +++ b/docs/game/navigation.md @@ -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 ~8–10 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 +> ~8–10 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.* diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 2d8911cf..4b1f957f 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -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.9245–0.9249), against the "~8–10 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`, + 38–75 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 ~8–10 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 ~8–10 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 diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index 2f00d97d..98ac1d0f 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -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 ~8–10 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 | diff --git a/docs/re/REFUTED.md b/docs/re/REFUTED.md index 82337139..9aa46663 100644 --- a/docs/re/REFUTED.md +++ b/docs/re/REFUTED.md @@ -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 ~8–10 s idle" β†’ **refuted.** + The menu sat untouched for **β‰₯ 60 s** without moving (correlation never leaving + 0.9245–0.9249). The ~8–10 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 ~20–25 s. β†’ A backlog **preserves ordering and destroys + durations**; check consumed-fps against requested-fps before quoting a time. diff --git a/docs/re/menu-idle-and-b-2026-08-29.md b/docs/re/menu-idle-and-b-2026-08-29.md new file mode 100644 index 00000000..b6424b87 --- /dev/null +++ b/docs/re/menu-idle-and-b-2026-08-29.md @@ -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 ~8–10 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 ~8–10 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 8–10 s. + +βœ… **And the 8–10 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 ~20–25 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 **38–75 ms**, a 20–60Γ— 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/). diff --git a/tools/re-capture/menu_b_probe.py b/tools/re-capture/menu_b_probe.py new file mode 100644 index 00000000..0eb25854 --- /dev/null +++ b/tools/re-capture/menu_b_probe.py @@ -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()) diff --git a/tools/re-capture/title_plate_and_b_probe.py b/tools/re-capture/title_plate_and_b_probe.py new file mode 100644 index 00000000..0254df46 --- /dev/null +++ b/tools/re-capture/title_plate_and_b_probe.py @@ -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())