sylpheed-port diagnosed their four instances of fixed-code-under-unfixed- description as a habit rather than inattention: corrections are ADDITIVE. They append a correction block and leave the original standing above it -- right for a record, wrong for a statement, because a reader takes the first assertion. Their fix is to keep the quote but demote it grammatically. Applied their diagnosis here and found a worse instance than theirs. screen-transitions.md carried the heading "### ❔ The fade-OUT duration is not in this field", with a section beneath it that is false in every sentence: "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. So the disc gives the ramp's target (black) and not its length. That duration is measured below, and the port is authoring it." All pre-fix. The record-layout fix times a group's final pose, so block 4 carries t=80 (menu), 74 (EXTRAS) and 269 (title), and the fade-out ramp is DECODED at 70->80 = 10 units, 64->74 = 10, 261->269 = 8. The section told the port to author a value that is decoded, and its correction sat 78 lines below. It also carried the dead rule's exact vocabulary -- "stops 4 bytes short" -- which is the grep I built for code last iteration and never ran against docs. Rewritten leading with the correction, the original quoted and demoted beneath it. METHOD: corrections are additive by default and that is wrong for a statement; the worst form is a HEADING, which asserts with maximum reach and minimum context, and a reader scanning headings never reaches the retraction. Audit headings first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
26 KiB
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.
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); 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).
🔴 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 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 in this field — 10 units on the menu, 8 on the title
What this section used to say, and it was wrong in every sentence: "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. So the disc gives the ramp's target (black) and not its length. That duration is measured below, and the port is authoring it."
🔴 All of that is the pre-fix reading. The
record-layout fix times a group's final pose,
so block 4 carries t = 80 (menu), 74 (EXTRAS) and 269 (title). The fade-out
ramp is therefore decoded: 70 → 80 = 10 units, 64 → 74 = 10, and
261 → 269 = 8. ⚠️ The port should NOT author it — this section told them
to, which is the most expensive kind of stale sentence: an instruction, stated
affirmatively, in a heading, a hundred lines above its own retraction.
⚠️ Kept as a record of what changed, but demoted below the correction so the false sentence cannot be read as the live one.
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). 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,
filmstrip in
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 − T0on 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
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 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), 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):
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. 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 overlap it" was WRONG, and is withdrawn (same day). sylpheed-port
read the lead off the disc independently — content fade-outs start at ptmsg 58
and pteff10/pteff12/ptbtn05 60, against the quad's ramp at 70 — which
reproduces the six-frame lead exactly (12 units = 6 frames), but has content
finishing at 68, two units before the quad starts. So I checked which draws I
had actually been watching, and they were right:
frame TEXTURED sprite alphas untextured
34 [144,148,169,191,254,255] [64, 255, 64]
36 [ 42,119,145,149,159,255] [64, 207, 64]
37 [ 71, 95,145,149,191,255] [64, 175, 64]
39 [146,150,255] <- fading ones GONE
40 [146,151,255] [64, 31, 64, 102] <- black quad appears
The content sprites are textured, they finish at frame 39, and the quad appears
at 40 — a one-frame gap, which is the port's two units. What overlaps the quad
is a different thing: a full-screen untextured quad decaying 255→…→15 across
frames 34–41, still at α=31 and α=15 while the quad ramps. ⚠️ That element is
unidentified: build 5 declares only two primitives, pteff00.prm and a
single-keyframe pteff02.prm, and neither is a 255→15 decay. It is not accounted
for by the outgoing screen's own primitive list, and I am not going to name it from
one capture.
So the correct shape is sequence, not overlap: content fades out, finishes, and one frame later the black quad ramps.
✅ 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.
✅ 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-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.
The tell is that TWO quads arrive together. A screen contributes both a
pteff00 (255 at t=0, decaying) and a pteff02 (64). On the settled menu the
untextured set is [64]; at frame 34 it becomes [64, 255, 64] — a 255 and a
second 64, which is exactly build 4's opening pair and cannot be explained by any
single element.
The discriminator, predicted in advance. A fit is not a measurement, and 8
frames matching build 4's declared 16 units is a fit. So: a transition whose
incoming screen declares something else. title → menu brings in build 5, whose
opening is 0→12 = 12 units = 6 frames against build 4's 8.
| transition | incoming build | declared open | measured decay |
|---|---|---|---|
| menu → title (Ⓑ) | 4 | 16 units = 8 frames | 8 (frames 34–41) |
| title → menu (Ⓐ) | 5 | 12 units = 6 frames | 5 (frames 73–77) |
Different incoming screen, different decay length, in the predicted direction. That kills "a fixed transition effect". ⚠️ The second came out 5 where 6 was predicted — one frame short, inside this capture's documented ±1 — so the direction is measured and the duration agrees to a frame, which is as far as one run reaches.
✅ And the fade-out ramp is exactly LINEAR — the alpha puzzle was a composite
The rise in the first capture (102, 127, 255) did not sit on a line, and this page flagged that as unexplained. In the second capture the outgoing title's quad ramps with no other untextured quad present:
frame 67 68 69 70
alpha 63 127 191 255 steps of exactly 64
Four frames, against build 4's declared fade-out 261→269 = 8 units = 4 frames.
Exact, and exactly linear. The first capture's curve was the composite of two
overlapping quads, not a non-linear ramp — which is what sylpheed-port proposed
when they saw the numbers, and it is right.
🔴 The two directions have different structures, and this is the part to author
title → menu(Ⓐ) is SEQUENTIAL. Outgoing content fades (58–67); the outgoing quad ramps to black (67–70); frames 70–72 draw almost nothing (6 draws, 2 textured); the incoming screen's elements appear at 73. There is a real black interval — fully black from frame 70 until the incoming quad first drops below 255 at frame 75, i.e. 5 frames ≈ 10 units.menu → title(Ⓑ) is a CROSS-FADE. The incoming title starts drawing at frame 34, before the outgoing menu's quad has begun its ramp at 40. Both screens draw together for ~6 frames. There is no black interval at all: the near-empty frame is a single one (46). ⚠️ This is the outlier of three — see the EXTRAS test below. It is not a property of Ⓑ.
🔴 A third transition REFUTES "Ⓑ has no black interval" — it was one screen pair
sylpheed-port asked for exactly this test and declined to act on the asymmetry
without it, on the grounds that one transition is not a rule. They were right not
to. EXTRAS → main menu, also via Ⓑ:
frame untextured full-screen draws tex
33 [64] 9 7 EXTRAS settled
34 [64, 51] 8 4 outgoing quad starts
38 [64, 255] 8 4 fully black
39 [] 3 0 <- EMPTY
40 [] 3 0 <- EMPTY
41 [64, 169] 7 3 incoming menu's quad, decaying
45 [64, 21] 7 3
46 [64] 7 3 clear
Two completely empty frames — 3 draws, zero textured. That is a harder black
than either earlier capture showed. So Ⓑ does not imply a cross-fade; menu → title is the outlier, and the thing I was one step from writing up as "Ⓑ has no
black" is false.
⚠️ The screen was verified, not assumed: screen_id.py cannot tell EXTRAS from
the main menu (both are dark blue GP_TITLE screens), so the armed frame was
checked with which_title_screen.py
— extras 18.58 against main_menu 29.85, margin 11.27, inside the 9.9–11.7 band
its control establishes on four known captures.
✅ What three transitions agree on
| transition | outgoing ramp | declared | black gap | incoming decay | declared |
|---|---|---|---|---|---|
| menu → title (Ⓑ) | frames 40–43 | 10 u = 5 f | none | 34–41 = 8 f | 16 u = 8 f |
| title → menu (Ⓐ) | 67–70 = 4 f | 8 u = 4 f | 3 f | 73–77 = 5 f | 12 u = 6 f |
| EXTRAS → menu (Ⓑ) | 34–38 = 5 f | 10 u = 5 f | 2 f | 41–45 = 5 f | 12 u = 6 f |
✅ The outgoing ramp is the declared final ramp — three for three, against three different declared values, and exactly linear where it is not composited (capture 2 steps of 64; capture 3 steps of ~51 = 255/5).
🟡 The incoming decay is exact for build 4 (8 = 8) and one frame short for build 5, twice (5 against 6, in two independent runs — so it is reproducible, not noise). Capture 3's rate settles what the count cannot: steps of −21, −42, −43, −42, i.e. 255/6 per frame after a half-step start, which is the declared 12 units exactly. So build 5's opening ramp is confirmed at 12 units and the frame count is a phase offset. ⚠️ Capture 2's decay (255, 127, 84, 63 → steps −64, −43, −21) does not 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.
✅ 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.
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_unitsat 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"
Last iteration I withdrew an overlap claim and wrote "the shape is sequence, not overlap". Both halves of that were partly wrong, in opposite directions. What is true:
- the outgoing screen's content finishes one frame before its own quad starts — the port's 2-unit gap, confirmed;
- the incoming screen genuinely overlaps all of it, on Ⓑ — so "cross-fade" was right about the screens and wrong about which elements;
- and on Ⓐ neither overlap happens.
The lesson is not about either reading: it is that one transition was being generalised to "a transition", and the two directions in this archive do not behave alike.
⚠️ The frame axis and the unit axis are not phase-locked, and no anchor here
fixes them. Aligning the capture's frames to the file's units two different ways
— content-start ↔ t=58, or ramp-start ↔ t=70 — differs by two frames, and
nothing in this run distinguishes them. The durations are robust (6-frame lead,
3–4-frame ramp, 1-frame gap, 1-frame inter-screen black); the absolute alignment is
not. Any total quoted as "N units" from this capture inherits that ±2 frames, so a
model total agreeing with a measured total to within one unit is agreement at one
alignment, not a confirmation. The quad's own alphas are a second reason for
caution: 102 at frame 40, 127 at 41, 255 at 43 do not sit on a linear 0→255 across
t=70→80, and that is unexplained.
⚠️ 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,
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.