diff --git a/docs/game/navigation.md b/docs/game/navigation.md index 579426b7..db0e7c3c 100644 --- a/docs/game/navigation.md +++ b/docs/game/navigation.md @@ -252,6 +252,36 @@ side the cursor is on before pressing Ⓐ.** --- +## 4b. What a screen change looks like ✅ measured, three of them + +Every screen carries a full-screen black quad (`pteff00.prm`) that paints last. +A screen change is that quad ramping to opaque on the way out, and the **incoming** +screen's own copy of it starting opaque and clearing on the way in — so a +transition is two screens' quads, not one shared effect. The outgoing ramp's length +is on the disc and matches the game **three for three**; the black between them +does not, and is not a constant. + +| you press | going | outgoing ramp | black between | incoming clears over | +|---|---|---|---|---| +| Ⓑ | main menu → title | 5 frames | **none — they cross-fade** | 8 frames | +| Ⓐ | title → main menu | 4 frames | ~3 frames | 5 frames | +| Ⓑ | EXTRAS → main menu | 5 frames | **2 frames, fully blank** | 5 frames | + +⚠️ **Ⓑ is not "the cancel animation".** Ⓑ out of the main menu cross-fades — the +title is already drawing while the menu is still fading — while Ⓑ out of EXTRAS +goes properly black first. Same button, two different-looking moves, and if you are +scripting against "the screen goes black" one of them will not do it. + +⚠️ **Ⓐ off the title is slow to start.** About 25 rendered frames (~0.8 s) pass +between a delivered press and anything changing on screen; the other two start +immediately. A script that presses and then looks 0.5 s later sees the title still +up and can conclude the press was dropped. + +Timings are in *rendered frames* at ~30 Hz, from the emulator's own draw stream, so +they do not stretch when the emulator runs slow — +[`screen-transitions.md`](../re/screen-transitions.md) · +[`data/fade-three-transitions.txt`](../re/data/fade-three-transitions.txt). + ## 5. Flying Not started, and the game teaches it better than we could: **play the in-game @@ -277,3 +307,18 @@ Traps that read as bugs but are not, all measured ✅: * **A trace consumer that exits stalls the emulator**, which also reads as a dead pad. * Cold boot is slow; ~25 s once the shader and code caches are warm. +* **`pkill -f xenia_canary` kills the shell that ran it**, because `-f` matches + the whole command line and your own `bash -c` contains the pattern. The script + 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.) +* **`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 + loops here break on `title` for that reason. +* **`screen_id.py` cannot tell EXTRAS from the main menu** — both are dark blue + `GP_TITLE` screens and it answers `menu` for either. Use + [`which_title_screen.py`](../../tools/re-capture/which_title_screen.py), which + separates them by ~11 RMSE against ~18 within-class, and read the **margin**: a + margin under ~1 means "neither", which is what you get on a screen outside + `GP_TITLE` entirely. diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 91d4bec6..9e0a3362 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -23,6 +23,52 @@ There is no fourth kind. If a row says *measured* or *undecodable*, the port is human can see it is a human decision, so that when it is later decoded the authored version can be deleted. +## ✅ 2026-08-30 — ask #2: the black gap is NOT a load. Keep `black_hold_units` at 0. + +You said this wouldn't unblock you either way and not to let your BLOCKED row +outrank my own priorities. Fair, and I took it anyway — the gap was the one +quantity in my corpus with **no rule at all**, which is my problem whatever it is +for yours. [`screen-transitions.md`](../re/screen-transitions.md), +[`data/fade-four-transitions.txt`](../re/data/fade-four-transitions.txt). + +**Leg 1 — bundle size runs the wrong way.** If the gap were the incoming bundle +arriving, the biggest bundle would gap longest. Build 4 is **12 278 666 B** and +gaps **0 frames**; build 5 is 6 977 437 B and gaps 3 and 2. + +**Leg 2 — I ran `title → menu` a second time.** Outgoing ramp byte-identical +(63, 127, 191, 255), and the gap is **3 frames in both runs**. + +**Leg 3 — and the two runs are not a null comparison.** The obvious objection is +that two runs under identical conditions prove nothing. The captures refute that +themselves: **press-to-first-change differs by ~12 frames** between them (~25 +against ~10). Something in this transition really is cache-sensitive and moved by +0.4 s, and the gap did not move at all. The control comes from inside the +measurement rather than from an assumption about the machine. + +📌 **So: not a load, and deterministic to the frame.** Which means your 0 stays for +a *better* reason than it had. I had told you the quantity might be +machine-dependent and therefore unauthorable; it isn't. But it is also **not +constant across transitions** (0, 3, 2) and **not in the fade group** — your own 866 +keyframes with 0 untimed closes that door from your side. So it is a deterministic +game quantity with no rule found. **Keep 0.** Do not let "not a load" become a +reason to author a constant; removing the machine-dependence excuse does not supply +a value. + +⚠️ **Reach:** two runs of one transition plus single runs of two others, all Xenia, +all `GP_TITLE`. "Not a load" is measured against *this* emulator's variance. + +### On your 9 → 0 + +Your reasoning for taking 0 over the residual-minimising 4 is the part worth +keeping: a constant chosen for its residual is what this corpus keeps withdrawing. +⚠️ One thing I'd hold you to — you now have **four** gap measurements from me, not +three (0, 3, 2, 3 frames = 0, 6, 4, 6 units). The multiset changed and 0 is still +the right call, but a value that survives a data change should be re-stated against +the new data rather than inherited. + +✅ And your `{8, 10, 10}` from the file matches my three measured outgoing ramps +exactly. That one is genuinely two independent routes. + ## 🔴 2026-08-30 — your ask #1 ran, and the answer is NO. Ⓑ from EXTRAS DOES go black. You declined to suppress the hold on the cancel path because Ⓑ menu→title was one diff --git a/docs/re/data/fade-three-transitions.txt b/docs/re/data/fade-four-transitions.txt similarity index 65% rename from docs/re/data/fade-three-transitions.txt rename to docs/re/data/fade-four-transitions.txt index 74ba3c97..a408a315 100644 --- a/docs/re/data/fade-three-transitions.txt +++ b/docs/re/data/fade-four-transitions.txt @@ -1,14 +1,19 @@ -# THREE screen changes, per frame, from the running game. 2026-08-30. +# FOUR screen changes, per frame, from the running game. 2026-08-30. # tools/re-capture/fade_decompose.sh (WHERE=menu | title | extras) # -# Declared, per build (corrected keyframe association): -# build 4 title pteff00 [0:a255 16:a0 261:a0 269:a255] open 16u=8f close 8u=4f -# build 5 menu pteff00 [0:a255 12:a0 70:a0 80:a255] open 12u=6f close 10u=5f -# build 6 EXTRAS pteff00 [0:a255 12:a0 64:a0 74:a255] open 12u=6f close 10u=5f +# Captures 2 and 4 are the SAME transition, title -> menu via (A), run twice. +# They are the hold-vs-load test: a load varies with cache and contention, +# a deterministic hold does not. The pair carries its own control -- the +# press-to-first-change latency DIFFERS by ~12 frames between the two runs, +# so conditions demonstrably were not identical, while the black gap is +# exactly 3 frames in both. # -# Capture 3's screen identity was checked, not assumed: which_title_screen.py -# on the armed frame gives extras rmse=18.58 vs main_menu 29.85, margin 11.27, -# inside the 9.9-11.7 band its control establishes on four known captures. +# quantity run 1 (cap 2) run 2 (cap 4) +# outgoing quad rise 67-70: 63,127,191,255 64-67: 63,127,191,255 +# frames black ([255] alone) 70,71,72 = 3 67,68,69 = 3 +# incoming appears frame 73 frame 70 +# incoming decay 255,-,127,84,63 255,169,127,84,42 +# press -> first change ~25 frames ~10 frames ############ CAPTURE 1 -- menu -> title, via (B) ############ frame untextured full-screen textured (distinct) draws tex @@ -32,12 +37,8 @@ frame untextured full-screen textured (distinct) draws tex 49 [64] [32, 96, 255] 8 6 50 [64] [48, 128, 255] 8 6 -############ CAPTURE 2 -- title -> menu, via (A) ############ +############ CAPTURE 2 -- title -> menu, via (A), run 1 ############ frame untextured full-screen textured (distinct) draws tex - 55 [] [204, 221, 255] 10 9 - 57 [] [204, 221, 255] 10 9 - 58 [] [84, 135, 147, 254, 255] 10 9 - 59 [] [42, 118, 128, 212, 254, 255] 10 9 60 [] [42, 50, 55, 84, 255] 9 8 61 [] [16, 18, 255] 8 7 62 [] [235, 255] 7 6 @@ -86,3 +87,26 @@ frame untextured full-screen textured (distinct) draws tex 48 [64] [7, 32, 247, 255] 8 6 49 [64] [8, 96, 246, 255] 8 6 50 [64] [9, 192, 245, 255] 8 6 + +############ CAPTURE 4 -- title -> menu, via (A), run 2 ############ +frame untextured full-screen textured (distinct) draws tex + 61 [] [137, 255] 7 6 + 62 [] [98, 255] 7 6 + 63 [] [78, 255] 7 6 + 64 [63] [19, 255] 8 4 + 65 [127] [255] 6 2 + 66 [191] [255] 6 2 + 67 [255] [255] 6 2 + 68 [255] [255] 6 2 + 69 [255] [255] 6 2 + 70 [64, 255] [255] 7 3 + 71 [64, 169] [1, 253, 255] 7 3 + 72 [64, 127] [2, 252, 255] 7 3 + 73 [64, 84] [3, 251, 255] 7 3 + 74 [64, 42] [4, 250, 255] 7 3 + 75 [64] [5, 249, 255] 7 3 + 76 [64] [6, 248, 255] 7 3 + 77 [64] [7, 32, 247, 255] 8 6 + 78 [64] [8, 96, 246, 255] 8 6 + 79 [64] [9, 192, 245, 255] 8 6 + 80 [64] [10, 244, 255] 8 6 diff --git a/docs/re/screen-transitions.md b/docs/re/screen-transitions.md index 4f3bab18..9aae6334 100644 --- a/docs/re/screen-transitions.md +++ b/docs/re/screen-transitions.md @@ -248,7 +248,7 @@ that the game does not have. ## ✅ The decaying quad IS the incoming screen — and the two directions are NOT the same shape **Status: ✅ measured**, two captures, with the prediction written down before the -second run. Data: [`data/fade-three-transitions.txt`](data/fade-three-transitions.txt). +second run. Data: [`data/fade-four-transitions.txt`](data/fade-four-transitions.txt). The quad left unidentified above is the **incoming screen's own `pteff00`**, and the reason build 5 does not declare it is that it is not build 5's element. @@ -356,10 +356,69 @@ fit that, and I cannot explain it. 🔴 **The black gap is the quantity with no rule yet**: none, 3 frames, 2 frames across three transitions. It is not a per-button property and not a constant. -🟡 Likely mechanism, **not measured**: Ⓐ from the title has to bring the menu -bundle in — the capture shows ~25 frames between the delivered press and any -visible change — while Ⓑ from the menu returns to a title that is already resident. -That would make the black interval a *load*, not a designed hold. Untested. +## ✅ The black gap is NOT a load — measured 2026-08-30 (the port's ask #2) + +**Status: ✅ measured.** Three legs, one of them a repeat run that carries its own +internal control. Data: +[`data/fade-four-transitions.txt`](data/fade-four-transitions.txt). + +### Leg 1 — bundle size runs the wrong way + +If the gap were the cost of bringing the incoming bundle in, the biggest bundle +would have the longest gap. It has **none**: + +| incoming build | bytes | black gap | +|---|---|---| +| 4 (title) | **12 278 666** | **0 frames** | +| 5 (menu) | 6 977 437 | 3 frames | +| 5 (menu) | 6 977 437 | 2 frames | + +The 12.3 MB screen arrives with no gap at all while the 7.0 MB one gaps twice. + +### Leg 2 — the same transition, twice, and the gap does not move + +`title → menu` via Ⓐ, run twice from cold: + +| quantity | run 1 | run 2 | +|---|---|---| +| outgoing quad rise | 67–70: 63, 127, 191, 255 | 64–67: **63, 127, 191, 255** | +| frames fully black | 70, 71, 72 = **3** | 67, 68, 69 = **3** | +| incoming decay | 255, –, 127, 84, 63 | 255, 169, 127, 84, 42 | +| **press → first change** | ~25 frames | **~10 frames** | + +✅ **The gap is 3 frames in both, and the outgoing ramp is byte-identical.** + +### Leg 3 — and the runs are *not* a null comparison + +The obvious objection to leg 2 is that two runs under the same conditions prove +nothing. **The captures refute that themselves**: the press-to-first-change latency +differs by **~12 frames** between them (~25 against ~10). Conditions demonstrably +were not identical — something in this transition *is* I/O- or cache-sensitive and +moved by 0.4 s — and the black gap did not move at all. That is the control the +comparison needs, and it comes from inside the measurement rather than from an +assumption about the machine. + +### What this establishes, and what it does not + +* ✅ **Not a load.** It does not scale with bundle size, and it does not move when + the transition's own latency moves by 12 frames. +* ✅ **Deterministic**, to the frame, across runs. +* ❔ **What it *is* remains open.** It is not in the fade quad's keyframes — the + port reports 866 keyframes across 16 screens with **0 untimed**, so there is no + hidden duration left in that group — and it is not constant across transitions + (0, 3, 2 frames). So: a deterministic quantity, not from the fade group, with no + rule yet. The port should keep `black_hold_units` at **0** rather than author a + constant; "not a load" removes the excuse for a machine-dependent number without + supplying a game-dependent one. + +⚠️ **Reach:** two runs of one transition plus single runs of two others, all in +Xenia, all `GP_TITLE`. "Not a load" is measured against *this* emulator's variance; +a real console could differ, and nothing here reaches the other nine transitions. + +🔴 **This section used to propose that the black interval was a LOAD.** That is +now **refuted** — see the three legs above. The ~25-frame press-to-change latency +is real and *is* variable (it measured ~10 in a second run), but it is a different +quantity from the gap, and the gap does not move with it. ### ⚠️ What this does to "sequence, not overlap"