Files
Sylpheed/docs/re/structures/ui-resting-pose.md
Sylpheed RE agent b5c44b7c2a formats: the resting pose is the hold, not the longest dwell
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.
2026-08-19 06:41:24 +00:00

4.9 KiB
Raw Blame History

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

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 1501150, y 200400) 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

  • compose ignores the keyframe fade alpha entirely. blit uses tint. Applying fade is 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 .prm quads, 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 while fade is unapplied.