re: the black gap between screens is NOT a load -- measured, three legs

sylpheed-port's BLOCKED ask #2. The gap was the one quantity in the transition
with no rule: measured at 0, 3 and 2 frames across three transitions, and I had
proposed it might be a load, which would make it emulator- and storage-dependent
and unauthorable.

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
ZERO frames; build 5 is 6 977 437 B and gaps 3 and 2.

Leg 2, ran title->menu a second time from cold. The outgoing ramp is
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 -- which is the objection leg 2
invites. The captures refute it 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, while the gap did not move at all. The
control comes from inside the measurement rather than from an assumption that
conditions differed.

So the gap is deterministic to the frame and not a load. It is also not constant
across transitions (0, 3, 2, 3) and not in the fade group -- the port reports 866
keyframes across 16 screens with 0 untimed. A deterministic game quantity with no
rule found; black_hold_units stays 0, and "not a load" must not become a reason
to author a constant.

The load proposal in screen-transitions.md is marked refuted rather than deleted.

Also fills in docs/game/navigation.md, which the standing brief asks me to keep
and which I had not touched while measuring four transitions: a player-side
section on what a screen change looks like, and three scripting traps -- that
`pkill -f xenia_canary` kills the shell that ran it (cost a launch today, and the
same trap is already in METHOD for pgrep), that screen_id.py reports `menu`
during the attract loop, and that it cannot tell EXTRAS from the main menu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
This commit is contained in:
sylph-decoder
2026-08-30 14:28:00 +00:00
parent 5a5cfef745
commit c177eb47c6
4 changed files with 192 additions and 18 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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 | 6770: 63, 127, 191, 255 | 6467: **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"