Files
Sylpheed/docs/re/screen-transitions.md
sylph-decoder 4bcb35cef7 re: the transition is overlap, not ramp-then-hold -- and the fade-in was 5x wrong
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
2026-08-30 13:35:56 +00:00

266 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.