Files
Sylpheed/docs/re/screen-transitions.md
sylph-decoder d9accf9403 re: (B) from EXTRAS DOES go black -- "(B) has no black interval" refuted
sylpheed-port's BLOCKED.md ask #1. They declined to suppress their uniform
black_hold on the cancel path because (B) menu->title was one transition. The
test says they were right.

EXTRAS -> main menu, also via (B): the outgoing quad ramps frames 34-38 (5
frames, exactly build 6's declared 10 units), then frames 39 AND 40 are
completely empty -- 3 draws, zero textured, a harder black than either earlier
capture -- then the incoming menu's quad decays 41-45. So (B) does not imply a
cross-fade; menu->title is the outlier of three, and the generalisation I was one
step from publishing is false.

The screen was verified, not assumed. screen_id.py cannot separate EXTRAS from
the main menu, so which_title_screen.py checked the armed frame: extras 18.58 vs
main_menu 29.85, margin 11.27, inside the 9.9-11.7 band its control sets on four
known captures.

Three transitions now agree on one thing and disagree on another:

  outgoing ramp = the declared final ramp, THREE FOR THREE, against three
  different declared values (10u/5f, 8u/4f, 10u/5f), and exactly linear where
  nothing overlaps it. Authorable from the file.

  black gap = none / 3 frames / 2 frames. Not a per-button property, not a
  per-direction property, not a constant. black_hold_units should not be
  authored as one.

Build 5's incoming ramp is confirmed at 12 units by its RATE rather than its
count: the count came out 5 against a predicted 6 in both runs -- reproducible,
so not noise -- but capture 3's steps are -21, -42, -43, -42, i.e. 255/6 per
frame after a half-step start. Capture 2's decay does not fit that and is
unexplained.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 14:15:58 +00:00

22 KiB
Raw Blame History

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 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 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). 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 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 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.3670.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 3441, 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-three-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 3441)
title → menu (Ⓐ) 5 12 units = 6 frames 5 (frames 7377)

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 (5867); the outgoing quad ramps to black (6770); frames 7072 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.pyextras 18.58 against main_menu 29.85, margin 11.27, inside the 9.911.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 4043 10 u = 5 f none 3441 = 8 f 16 u = 8 f
title → menu (Ⓐ) 6770 = 4 f 8 u = 4 f 3 f 7377 = 5 f 12 u = 6 f
EXTRAS → menu (Ⓑ) 3438 = 5 f 10 u = 5 f 2 f 4145 = 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.

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

⚠️ 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, 34-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 712.)

⚠️ 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.