Element::rest() picked the keyframe with the largest gap to the next keyframe's
time. That reads a keyframe as a value held until the next one; it is the start
of a ramp toward it. A long gap after keyframe k means the screen spends that
time arriving at k+1, so the settled pose is at the far end of the gap.
The title wordmark zooms in over five frames and holds at (184,193) at 100% from
t=251 to t=264. The old rule picked the frame before the long gap: (179,186) at
101%, still mid-zoom.
Measured against the framebuffer capture of the running title screen, which is a
1:1 crop so frame coordinates map directly (confirmed: the copyright line lands
on row 669 in the capture and in both composites). Edge-correlated over the
wordmark box:
plateau (landed) best 0.4597 at shift (0,0)
longest dwell (old) best 0.1511 at shift (+3,+8), 0.1268 at (0,0)
The old composite scores 3x lower and only peaks after being moved, by about the
(-5,-7) that picking kf4 instead of kf5 predicts.
It also fixes six title elements the old rule rested at alpha 0x00 where the
capture plainly shows them, and pteff00.prm - the full-screen fade quad painted
last - which rested at opaque black. That was the blocker on .prm compositing.
Adds tools/re-capture/align_to_capture.py, which is how this was scored, and
turns the .prm test that deliberately asserted the old defect into a guard on
the fix.
Not settled and now the next item: compose ignores the keyframe fade alpha
entirely (blit modulates by tint only), which is why choosing the wrong keyframe
was invisible until now.
4.9 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.
What is not settled
- ❔
composeignores the keyframefadealpha entirely.blitusestint. Applyingfadeis the obvious next step and is what makes the resting pose visible rather than academic — but it changes every screen and needs its own A/B against the capture. It is also the prerequisite for drawing.prmquads, whose entire content is that alpha. - 🟡 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.