screen-transitions.md carried a 14-unit "black hold" that the page itself flagged as arithmetic rather than measurement. Measured it against the running game; the guess was wrong, and finding the instrument to measure it turned up a second, larger error in the same page. 1. fade_quads.py was STALE. It read each pose's time from blk+36 -- the next record's time word -- the association the keyframe record-layout fix retired in the crate. sylpheed-cli was rebuilt at the time; the Python helper was never swept with it. Signature: it cannot time a group's last pose, so it printed a trailing `t=-`. Fixed, controlled against the rebuilt `screen info` ([0 12 70 80] for build 5's pteff00.prm). 2. Through it, the page labelled the quad's CLEAR-hold as its fade-in and published 0.87 s / 0.97 s / 4.08 s for a ramp that is 0.20 s / 0.20 s / 0.27 s. A port pacing its menu fade-in off that would run it 5x too slow. 3. The measurement. fade_decompose.sh boots to the main menu, arms the UI draw capture there, then presses (B), so one 260-frame window holds the whole screen change. The fade quad is identified rather than guessed: a .prm carries no tex[base=] and paints last, so it is the last full-screen untextured quad of a frame. Control first -- the quad's ramp is decoded at 10 units = 5 frames, and measures 4 submitted-frame steps with one unlogged frame in the span. Result: content elements begin fading at frame 34; the black quad first appears at 40 and is opaque by 43; the menu's last frame is 45; frame 46 has 6 draws against 12. So the ~14 extra units are the content's own fade-outs OVERLAPPING the quad's ramp, not a hold after it, and the inter-screen black is one frame. Refutation attempted: sylpheed-port's entries 13/14 twins. Re-derived off the disc -- 3.06 / 4.33 / 47.91, identical to two decimals. Recorded as confirming their addressing and arithmetic, NOT as independent support: same renderer, same disc, which is their own rule. Reach: one transition, one run; the frame axis has gaps (232 headers over frames 3..260), so every span is +-1 frame. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
266 lines
13 KiB
Markdown
266 lines
13 KiB
Markdown
# What happens between two screens — a fade through black, and where its timing lives
|
||
|
||
**Status:** ✅ `CONFIRMED`. The quad and its ramp are **decoded** (a keyframe
|
||
group on the disc, with a disc-wide check); the wall-clock timings are
|
||
**measured** off the running game at 30 fps. One piece is **undecodable from this
|
||
field** and is called out below.
|
||
|
||
Answers [MISSION Q7](../port/MISSION.md).
|
||
|
||
## The mechanism — decoded
|
||
|
||
Every title-side screen carries a full-screen untextured primitive that is
|
||
**black, and paints last**: `pteff00.prm` in `GP_TITLE`, `pfeff00.prm` in
|
||
`GP_SAVE_LOAD`. That it sorts last was already established
|
||
([`structures/ui-paint-order-key.md`](structures/ui-paint-order-key.md)); what is
|
||
new here is that its **keyframe group is the transition**.
|
||
|
||
The group is always four blocks, and always this shape:
|
||
|
||
| block | alpha | meaning |
|
||
|---|---|---|
|
||
| 1 | `0xff` at `t = T0` | the screen starts **black** |
|
||
| 2 | `0x00` at `t = T1` | ramp to fully clear — the screen **fades in** |
|
||
| 3 | `0x00` at `t = T2` | clear; this is the resting pose, the quad is invisible |
|
||
| 4 | `0xff`, **no time** | ramp back to black — the screen **fades out** on exit |
|
||
|
||
Read with the corpus's rule that a keyframe is the *start* of a ramp
|
||
([`structures/ui-resting-pose.md`](structures/ui-resting-pose.md)).
|
||
|
||
🔴 **The table this section used to print was taken with a STALE READER, and both
|
||
its numbers were wrong (corrected 2026-08-30).** `fade_quads.py` read each pose's
|
||
time from `blk+36` — the *next* record's time word — so every time was shifted one
|
||
slot and the last pose came out untimed (`t=—`). That is the same association the
|
||
[record-layout fix](ui-keyframe-record-layout.md) retired in the crate; the Python
|
||
helper was never swept with it. Fixed, and controlled against the rebuilt
|
||
`screen info`, which prints `[0 12 70 80]` for the same element:
|
||
|
||
```
|
||
$ tools/re-capture/fade_quads.py 4 5 6 # GP_TITLE
|
||
build 4 (title) pteff00.prm t= 0 α=255 t= 16 α=0 t=261 α=0 t=269 α=255
|
||
build 5 (main menu) pteff00.prm t= 0 α=255 t= 12 α=0 t= 70 α=0 t= 80 α=255
|
||
build 6 (EXTRAS) pteff00.prm t= 0 α=255 t= 12 α=0 t= 64 α=0 t= 74 α=255
|
||
```
|
||
|
||
**Every pose is timed. There is no untimed keyframe**, and the fade-out ramp is on
|
||
the disc after all: `70 → 80` = 10 units for the main menu, `64 → 74` = 10 for
|
||
`EXTRAS`, `261 → 269` = **8** for the title.
|
||
|
||
⚠️ **And the fade-IN was mislabelled by the same shift.** This page used to say the
|
||
screen "holds black for **0.20 s**, then fades in over **0.87 s** (`EXTRAS`),
|
||
**0.97 s** (main menu) or **4.08 s** (the title)". Those spans are `T2 − T1` under
|
||
the stale pairing — the stretch where the quad sits at **α = 0**, i.e. the screen
|
||
fully visible and *not* fading at all. Read correctly the screen starts black at
|
||
`t = 0` and fades in over **12 units (0.20 s)** on the menu and `EXTRAS`, **16
|
||
units (0.27 s)** on the title. A port pacing its menu fade-in off the old number
|
||
would have run it **5× too slow**.
|
||
|
||
### ❔ The fade-OUT duration is not in this field
|
||
|
||
The fourth block has **no time** — a group's last block stops 4 bytes short and
|
||
that word is already the next group's element index
|
||
([`ui_layout.rs`](../../crates/sylpheed-formats/src/ui_layout.rs) documents the
|
||
packing). So the disc gives the ramp's *target* (black) and not its length. That
|
||
duration is **measured** below, and the port is authoring it.
|
||
|
||
### The disc-wide check, and what it shows about overlays
|
||
|
||
Over every `GP_*.pak`, counting bundles that have a `.prm` element **and** ≥8
|
||
elements (screen-sized rather than a two-element fragment):
|
||
|
||
> **40 of 97** carry a 255 → 0 → 255 quad; 56 of the 60 found disc-wide have
|
||
> exactly 4 keyframes.
|
||
|
||
41 % sounds weak until it is read per pak, where it is nearly all-or-nothing:
|
||
|
||
| pak | with quad / screen-sized |
|
||
|---|---|
|
||
| `GP_MISSION_SELECT`, `GP_MOVIE_THEATER`, `GP_OPTIONS`, `GP_TUTORIAL`, `GP_SYSTEM` | 2/2 each |
|
||
| `GP_BUNK`, `GP_CHALLENGE` | 6/6 |
|
||
| `GP_STAGE_CLEAR` | 4/4 |
|
||
| `GP_SAVE_LOAD` | 10/12 |
|
||
| **`GP_TITLE`** | **6/12** |
|
||
| `GP_DIALOG` | 0/133 |
|
||
| `GP_DEBRIEFING_PILOTLOG` | 4/102 |
|
||
|
||
**`GP_TITLE`'s 6 of 12 is the interesting row, and it is not a gap.** The six
|
||
that carry the quad are exactly the six *screen* builds — title, main menu and
|
||
`EXTRAS`, English and Japanese. The six that do not are exactly the six
|
||
**overlays**: the `PRESS Ⓐ BUTTON` plate and the two `DELTASABER` plates
|
||
([`ui-title-build-map.md`](ui-title-build-map.md)). An overlay composited onto a
|
||
screen has no transition of its own, so it has no fade quad — which is
|
||
independent corroboration that those builds are overlays rather than screens.
|
||
`GP_DIALOG`'s 0/133 says the same thing about dialog boxes.
|
||
|
||
## The timing — measured
|
||
|
||
Recorded with `ffmpeg -f x11grab -framerate 30` over the game surface, mean
|
||
frame luminance per frame; raw data in
|
||
[`captures/transitions/transition-luminance.csv`](captures/transitions/transition-luminance.csv),
|
||
filmstrip in
|
||
[`transition-filmstrip.png`](captures/transitions/transition-filmstrip.png).
|
||
|
||
| | main menu → `EXTRAS` (Ⓐ) | `EXTRAS` → main menu (Ⓑ) |
|
||
|---|---|---|
|
||
| press → first visible change | 0.07 s | 0.37 s |
|
||
| **fade-out to black** | **0.367 s** | **0.400 s** |
|
||
| **pure black** (luminance 0.02) | 0.233 s | 0.167 s |
|
||
| luminance rise until settled | 0.567 s | 1.467 s |
|
||
|
||
The fade-out is the number the file cannot give, and it comes out the same both
|
||
ways: **~0.4 s**, i.e. ~24 units under Q1's rule.
|
||
|
||
The black hold is *consistent with* the file's 12 units (0.20 s) but does not
|
||
confirm it — the plateau spans the tail of the outgoing screen's fade-out and the
|
||
head of the incoming screen's black, and this measurement cannot separate them.
|
||
|
||
### ⚠️ The luminance rise is **not** the quad's ramp
|
||
|
||
The two columns differ by 2.6× where the quad's declared ramps differ by only
|
||
1.12× (58 units vs 52). The filmstrip says why: the background reappears *first*
|
||
and the labels arrive after it, so what the luminance curve is timing is the
|
||
incoming screen's **own element animations**, not the fade quad. Quoting 1.47 s
|
||
as "the main menu's fade" would be wrong. The quad's ramp is decoded; the
|
||
screen's build-in is a separate, longer thing.
|
||
|
||
## For the port
|
||
|
||
* a screen change is: **fade the outgoing screen to black over ~0.4 s**, hold
|
||
black briefly, then **fade the incoming screen in over its own declared ramp**
|
||
while its elements play their own keyframes;
|
||
* the fade-in ramp is **read from the file** (`T1 − T0` on the screen's fade
|
||
quad);
|
||
* the ~0.4 s fade-out and the black hold are **authored from this page** — the
|
||
disc does not carry them.
|
||
|
||
## ✅ Which quantity the ~0.4 s is — measured 2026-08-29
|
||
|
||
Asked by the port: is 0.4 s **(a)** the ramp from the hold to the exit pose, i.e.
|
||
exactly the missing duration of that untimed keyframe, **(b)** several keyframes'
|
||
worth, or **(c)** something the game does independently of the group?
|
||
|
||
**It is (a)** — and it is bigger than the fade quad. Two facts.
|
||
|
||
🔴 **1. "There is exactly one untimed keyframe, and every element has it" — REFUTED
|
||
2026-08-30. It was a STALE BINARY.**
|
||
|
||
That claim came from `screen info`, and the copy of `sylpheed-cli` in this container
|
||
was built **2026-08-29 12:38**, before the keyframe-record-layout fix landed. The old
|
||
parser shifted every time by one slot and could not time a group's final pose, so it
|
||
printed a trailing `-`. Rebuilt, the same element reads:
|
||
|
||
```
|
||
stale pteff00.prm 4 kf rest t=70 [12:0,0 70:0,0 80:0,0 -:0,0]
|
||
fresh pteff00.prm 4 kf rest t=12 [ 0:0,0 12:0,0 70:0,0 80:0,0]
|
||
```
|
||
|
||
**Four timed poses. There is no untimed keyframe and no unknown duration**, so the
|
||
question this section was answering — *"is 0.4 s the missing duration of that
|
||
untimed keyframe"* — no longer has its subject. The argument below (the ratio test)
|
||
is untouched and still shows the content fading rather than a quad arriving; what is
|
||
dead is the framing around it.
|
||
|
||
✅ **And the number is now decoded.** `pteff00.prm`'s final ramp is **70 → 80 = 10
|
||
units ≈ 0.167 s**, not the ~24 units this page authored. That confirms the port
|
||
agent's reading; I tried to refute it against the bytes and could not.
|
||
|
||
🔴 **So the measured ~0.4 s is NOT the ramp alone** — 0.4 s is ~24 units against a
|
||
decoded 10. Something else occupies the other ~14 units.
|
||
|
||
## ✅ What the other ~14 units are — MEASURED 2026-08-30, and it is not a hold
|
||
|
||
This page previously guessed: *"that the remainder is exactly the black hold is
|
||
arithmetic that fits (14 units = 0.233 s), **not a measurement**"*. It has now been
|
||
measured, and **the guess was wrong**. There is no black hold inside the fade-out.
|
||
|
||
**Instrument.** [`fade_decompose.sh`](../../tools/re-capture/fade_decompose.sh)
|
||
boots to the main menu, arms the UI draw capture there, then presses Ⓑ, so one
|
||
260-frame window contains the whole screen change.
|
||
[`fade_envelope.py`](../../tools/re-capture/fade_envelope.py) reads the fade quad's
|
||
alpha per submitted frame. The quad is **identified, not guessed at**: a `.prm`
|
||
primitive carries no `tex[base=…]`, and this element paints last
|
||
([paint-order key](structures/ui-paint-order-key.md)), so it is the last
|
||
full-screen *untextured* quad of a frame. Taking merely the last full-screen quad
|
||
picks up textured backdrops and gives a different answer.
|
||
|
||
**Control.** The quad's ramp is *decoded* (10 units), so the instrument can be
|
||
checked before it is believed. At Q1's 2 units per rendered frame, 10 units is 5
|
||
frames. Measured: the quad is absent at frame 39 and `α=255` at frame 43 — **4
|
||
submitted-frame steps**, with one unlogged frame inside the span. Agreement to
|
||
within that one frame. An instrument that could not reproduce the decoded ramp
|
||
could not be trusted on the undecoded remainder.
|
||
|
||
**The measurement** ([`data/fade-envelope-menu-to-title.txt`](data/fade-envelope-menu-to-title.txt)):
|
||
|
||
```
|
||
frame 34 content elements begin fading (a 255 quad appears and decays)
|
||
35..41 255 223 207 175 95 31 15 the content fade
|
||
frame 40 the BLACK QUAD first appears, α=102
|
||
41 α=127
|
||
43 α=255 fully black
|
||
45 last frame the menu draws
|
||
frame 46 6 draws (vs 12) — ONE frame of black
|
||
47+ the title's build starts
|
||
```
|
||
|
||
✅ **The ~14 extra units are the content elements' own fade-outs, which start six
|
||
frames BEFORE the quad's ramp and overlap it.** The blackout runs frame 34 → 43 =
|
||
**9 submitted frames ≈ 0.30 s at 30 Hz**, of which the quad's ramp is the last 4.
|
||
That is the same quantity the filmstrip measured as 0.367–0.400 s with coarser
|
||
timing, and it decomposes as **overlap, not sequence**.
|
||
|
||
✅ **The inter-screen black is ONE frame** (46), not ~14 units. The draw count
|
||
collapses from 12 to 6 for exactly one frame and the incoming build starts at 47.
|
||
|
||
📌 **For the port:** a transition is *not* "ramp the black quad for 10 units, then
|
||
hold black for 14". It is "start the content elements fading, and 6 frames later
|
||
ramp the black quad over its declared 10 units on top of them". Authoring it as a
|
||
hold puts a sixth of a second of dead black in the middle of every screen change
|
||
that the game does not have.
|
||
|
||
⚠️ **Reach.** One transition (main menu → title, via Ⓑ), one run. The frame axis
|
||
has gaps — 232 `--- frame` headers over frames 3…260, so ~10 % of submitted frames
|
||
carry no UI draw — which is ±1 frame on any span quoted here and is why the ramp is
|
||
given as 4 steps rather than a duration to three digits. Whether the six-frame lead
|
||
is constant across screens, or is a property of *these* elements' keyframes, is
|
||
**not** measured.
|
||
|
||
| elements | final untimed block | what it does |
|
||
|---|---|---|
|
||
| `pteff00.prm` (the fade quad) | `a = 255` | goes **black** |
|
||
| `pteff10`, `pteff12`, `ptbtn01…05`, `ptmsg` | `a = 0` | **fade out** |
|
||
| `ptframe1`, `ptframe2` | `a = 255` | hold, and get covered |
|
||
| `ptbase`, `pteff05`, `ptloop*`, `pteff02.prm` | single keyframe | hold |
|
||
|
||
**2. The capture shows the content fading, not just a black quad arriving.** This
|
||
has a null hypothesis that discriminates: under (c) — the game blackens the frame
|
||
independently — every region is scaled by the same `1 − α`, so the **ratio**
|
||
between a button region and a background region is *constant* through the
|
||
fade-out. Under (a) it must fall, because the buttons ramp to `a = 0` while the
|
||
background elements hold at 255 and are only dimmed.
|
||
|
||
Measured on [`transition-filmstrip.png`](captures/transitions/transition-filmstrip.png),
|
||
button column ÷ upper-right background art, frame by frame through the fade-out:
|
||
|
||
```
|
||
frame 0 1 2 3 4 5
|
||
ratio 6.495 5.574 3.105 2.125 1.935 (black)
|
||
```
|
||
|
||
**A 3.4× monotonic fall.** Constant is refuted. The buttons really are fading
|
||
independently of the overall dim, exactly as their declared final block says.
|
||
(The incoming screen runs it in reverse, 2.22 → 3.47 over frames 7–12.)
|
||
|
||
⚠️ **Reach.** The filmstrip is downsampled and the "button" region unavoidably
|
||
contains some background, so the ratio is a direction, not a clean alpha
|
||
measurement. It refutes the constant-ratio null decisively; it does not by itself
|
||
pin the 0.4 s to ±0.05 s. And it is measured on **one** transition pair.
|
||
|
||
### For the port
|
||
|
||
Write **one** authored constant — the duration of the final untimed keyframe,
|
||
~0.4 s / ~24 units — and **play the group to its end on every element**. Do not
|
||
model the exit as a black rectangle fading over a frozen screen: the buttons and
|
||
labels ramp to transparent at the same time, and that difference is visible.
|
||
|