blit modulated by tint only, which is 0xffffffff on essentially every keyframe, so the fade word was decoded, stored and then discarded. Applying it as an ARGB modulate takes the title composite's edge correlation against the framebuffer capture of the running game from 0.4597 to 0.9538, both at zero shift. The white wordmark with its blue outline, the trademark, the copyright and the orange exploding planet all appear -- the planet because a full-screen blue effect that rests at alpha 0 had been painting over it at full opacity. ARGB is measured: across a fade-in the high byte walks 0x00 -> 0x80 -> 0xc0 -> 0xe0 -> 0xff while the low three stay ffffff, and the low 24 bits are 0xffffff on 5276 of the disc's 5453 resting keyframes. A modulate can only remove pixels, so the risk is a blank screen. Measured: no-op on 4060 of 5200 sprite elements, partial on 453, hides 687 -- transient HUD indicators that should not be lit at rest -- and NO build is left with nothing visible. A disc test asserts that, and that the no-op share stays high, which also guards the resting rule against drifting onto ramp frames. Both changes are in one commit because the second is invisible without the first. Applying fade erased the word PAUSE, which the running-game capture plainly shows: pgptitle.rat has three runs of two identical keyframes -- pre-roll, hold, exit -- and the 'later run wins' tie-break grabbed the exit. A group carries the screen's entry animation AND its exit, so a run ending on the last keyframe is now excluded unless it is the only one. The title correlation is unchanged.
7.3 KiB
A keyframe is the start of a ramp, not a pose that is held
Status: ✅ CONFIRMED against the framebuffer capture of the running title
screen — the new rule aligns at zero shift, the old one had to be moved.
🟡 the fallback for groups that never hold is unverified. ❔ interpolation
between keyframes is still not implemented, only the resting pose.
The rule that was wrong
Element::rest() answers "where is this element when the screen is just sitting
there", and every composite the port draws depends on it. It used to pick the
keyframe with the largest gap to the next keyframe's time — the frame that
"dwells longest".
That reads a keyframe as a value held until the next one. It is not: a keyframe
is the start of a ramp toward the next one. So a long gap after keyframe k
means the screen spends that whole time arriving at k+1 — the settled pose is
at the far end of the gap, not the near one.
The title wordmark makes it concrete. ptlogo1.t32 zooms in from off-screen:
kf0 150% (-116, -7) a=0x00 t=26 off-screen, invisible
kf1 150% (-116, -7) a=0x00 t=34
kf2 112% ( 109, 123) a=0x80 t=38
kf3 103% ( 165, 171) a=0xc0 t=40
kf4 101% ( 179, 186) a=0xe0 t=42 ← old rule picked this
kf5 100% ( 184, 193) a=0xff t=251 ← the settled pose
kf6 100% ( 184, 193) a=0xff t=264 ← held here
kf7 100% ( 184, 193) a=0x00 t=None fades out
The gap 42 → 251 is by far the largest, so the old rule picked kf4 — 1 % too large and 5 px up-left, a frame from mid-zoom. The pose the screen actually holds is kf5–kf6.
The rule that is right
The resting pose is the hold: the longest run of consecutive keyframes with an identical pose. Ties go to the later run, matching the in → hold → out shape. Groups that ramp through every frame and never hold fall back to longest-dwell.
The measurement
docs/re/captures/title-screen-oracle.png is a framebuffer capture of the
running title screen. It is a 1:1 crop of the 1280×720 frame (1279×675) —
verified by the copyright line landing on row 669 in the capture and in both
composites — so frame coordinates map directly and a shift is meaningful.
Edge-correlated over the wordmark box (x 150–1150, y 200–400) with
tools/re-capture/align_to_capture.py. Gradient magnitude, not colour: the
capture's planet is mid-explosion and orange while ours is blue, and the wordmark
materials are undecoded, so a pixel diff would measure everything except the
question being asked.
| resting rule | best correlation | at shift | at (0,0) |
|---|---|---|---|
| plateau (landed) | 0.4597 | (0, 0) | 0.4597 |
| longest dwell (old) | 0.1511 | (+3, +8) | 0.1268 |
The old composite peaks 3× lower and only after being moved — displaced by about the (−5,−7) that kf4-instead-of-kf5 predicts. The new one is already where the game puts it.
What else it fixed, on the same screen
The old rule systematically picked the invisible end of a fade-in. On the
title screen it rested these at alpha 0x00, where the capture plainly shows
them:
ptlogo_tm (the ™), ptcopyright, ptlogo_back2, ptlogo_back2eff,
ptloop01/ptloop02.
And pteff00.prm — the full-screen fade quad that paints last — rested at
opaque black. That is the whole screen, and it is why .prm compositing was
blocked (ui-prm-primitives.md).
None of that was visible before, because compose never applied the fade
alpha at all: blit modulates by tint only, and tint is 0xffffffff on
essentially every keyframe. The wrong keyframes were being chosen and then their
one distinguishing field was ignored. That is worth stating as its own finding —
see below.
The fade alpha, applied (same day)
blit modulated by tint only — 0xffffffff on essentially every keyframe — so
the fade word was decoded, stored, and then thrown away. Applying it as an
ARGB modulate on top of tint is what turns the resting pose from an
academic result into a picture:
| composite | best correlation vs the capture | at shift |
|---|---|---|
plateau rest, fade applied |
0.9538 | (0, 0) |
plateau rest, fade ignored |
0.4597 | (0, 0) |
| longest-dwell rest | 0.1511 | (+3, +8) |
0.95 against a framebuffer capture of the running game. The composite now has the white wordmark with its blue outline, the ™, the copyright, and the orange exploding planet — the last of which appeared because a full-screen blue effect that rests at alpha 0 had been painting over it at full opacity.
ARGB is measured, not assumed. Across a fade-in the high byte walks
0x00 → 0x80 → 0xc0 → 0xe0 → 0xff while the low three stay ffffff, and the low
24 bits are 0xffffff on 5 276 of the disc's 5 453 resting keyframes (with
0x000000 on 165 — the .prm blacks — and 0x5dc9ff on 12).
Risk checked, because a modulate can only ever remove pixels: it is a no-op on
4 060 of 5 200 sprite elements, partial on 453, and hides 687 — which are
transient HUD indicators (pb_emergency, pb_refilling, pbcm1_arrow1) that
should not be lit on a resting screen. No build is left with nothing visible,
and a disc test asserts it.
And it caught a bug in the resting rule
With fade applied, the pause menu lost the word PAUSE, which the capture of
the running game plainly shows
(pause-tutorial-real-vs-rebuilt.png).
pgptitle.rat has three runs of two identical keyframes — invisible,
visible, invisible:
kf0 a=0x00 t=5 kf1 a=0x00 t=13 pre-roll
kf2 a=0xff t=23 kf3 a=0xff t=28 the hold
kf4 a=0x00 t=30 kf5 a=0x00 t=None the exit
A group carries the screen's entry animation and its exit. The tie-break "later run wins" grabbed the exit. Fixed: a run ending on the last keyframe is excluded unless it is the only one. The title's correlation is unchanged at 0.9538, and PAUSE is back.
This is why the two changes landed together: the trailing-run defect is
invisible — in the literal sense — until fade is applied.
What is not settled
- ❔ Blend mode. Everything above is straight alpha-over. The near-white
flash quads and the coloured ones may well be additive, and nothing has been
measured; the title capture cannot separate the two because its resting
elements are all
0xffffff. - 🟡 The fallback. Groups with no two adjacent keyframes alike still use longest-dwell. How many there are, and whether the correct answer for them is the last keyframe instead, is unmeasured.
- ❔ Interpolation. Only the resting pose is decoded; nothing tweens. A viewer that animates these screens needs the ramp, and whether it is linear is unknown.
- 🟡 One-shot flashes (
ptlogoall_eff,ptlogoall_eff2) ramp 0 → 0x80 → 0x4b → 0 and never hold at a visible value, so the plateau rule rests them at alpha 0 — invisible. That is probably right for a settled screen, but the capture cannot confirm it whilefadeis unapplied.