Files
Sylpheed/docs/re/structures/ui-resting-pose.md
sylph-decoder bfd3efe111 re: measure the capture-phase term -- 4.6 whole-frame, 0.32 in-box
sylpheed-port overturned their own phase-0 result using the identical-leaves fact
I gave them: the same leaf minimises at phase 240 against a title capture and 0
against a main_menu capture, so the best-matching phase is a property of when the
shutter fell rather than of the game's rest state. A continuously sweeping
element has no canonical rest phase. They warned that any whole-frame score
against a single capture carries a phase term of ~1.0 RMSE.

Measured on my own two JP sessions, which certainly differ in sweep phase
(44 025 px differ in the band the leaf crosses):

  whole frame                4.566
  sweep band x721..1241      4.088
  the adjudication box       0.3215

Their ~1.0 understates it for this screen: a whole-frame score against one
capture of the JP title carries ~4.6. Theirs is the leaf-phase component isolated
in a renderer; mine is everything that varies between sessions -- the plate pulse
alone contributes ~2.8, measured separately on the EN peak/trough pair -- and
includes theirs.

My margins are unaffected and now for a measured reason rather than an assumed
one. The era margin of 16.72 sits against an in-box term of 0.32, and
settle-vs-rest at 1.48 is 4.6x that term while remaining non-decisive against the
render-axis plateau of 1.2, exactly as stated.

Scoring the 388x423 box rather than the frame drops the between-session term from
4.566 to 0.3215, a factor of 14, because the sweep contributes at x 721..1241 and
the box is mostly clear of it. That was NOT why I cropped -- the crop was to stop
a local difference being diluted across 92 % of an identical frame -- so the
robustness is luck. The rule it earns: score inside a region that excludes the
free-running elements, and measure the residual term there rather than estimating
it.

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

56 KiB
Raw Blame History

A keyframe is the start of a ramp, not a pose that is held

2026-08-30 — the corrected association's rest pose is what the GAME shows

Status: measured against the oracle. Until now the keyframe record-layout fix rested on internal consistency — 0 of 1 042 multi-segment alpha ramps constant-rate under the old reading against 857 of 1 540 under the new. That is a strong argument and it is not a measurement of the game. It now has one, on the single screen in GP_TITLE where the two readings change pixels.

ptlogo_eff3.t32 on entry 7 (title_jp) is the only element whose rest pose moves between the two eras — (108,72) stale, (98,42) fixed — and it accounts for all 74 507 differing pixels. Scored against live-title-jp-at-rest.png over the 388×423 box where the two renders differ, so the result is not diluted by the ~92 % of the frame that is identical:

candidate RMSE vs the running game
stale era, rest (108,72) 58.412
fixed era, rest (98,42) 41.690
fixed era, --settle t=213 40.210

Three controls, run before believing any of it:

  • alignment found by sweep, not assumed — offset 0 → 87.29, 40 → 56.37, 45 → 32.41, 50 → 53.08, 60 → 72.84. A sharp minimum at the known game-surface offset;
  • the box discriminates — the same box against a different screen's capture (the EN title) scores 98103, against 4058 here;
  • --black changes nothing (58.412 / 41.690 either way) — every pixel in this box is covered by an element, so the canvas never shows through it. Recorded because the flag's own help says a framebuffer capture must be compared against a black canvas, and here it happens not to matter.

🟡 And the same run says settle-vs-rest is NOT decidable from this capture

Sweeping the screen's own timeline with --at gives the instrument's noise scale (data/ptlogo-eff3-rest-vs-oracle.txt):

t=  0  77.97   t= 90  58.50   t=150  40.59   t=210  40.23   t=270  78.41
t= 45  63.05   t=120  45.49   t=165  40.20   t=240  40.07   t=285  78.42

The capture sits on a broad plateau from t≈135 to t≈240, flat to 1.2 RMSE across 105 units, with sharp rises outside it. So:

  • the stale-vs-fixed margin of 16.7 is ~14× that flatness — decisive;
  • the settle-vs-rest margin of 1.5 is inside it — not decisive.

That is a better statement than "non-decisive": this capture separates the eras and cannot separate the policies, and the number that says so is the plateau's own width. The settle-instant proposal stays unadopted on the same evidence it had.

Why the capture's own instant is not a confound here

sylpheed-port found their harness capturing the same screen one keyframe unit apart in different sessions — 70 % of the picture, mid-build-in, and stable enough within a session that repeat runs said "deterministic". That hazard would void this adjudication if the JP capture had been grabbed at an arbitrary moment. It was not, and there are two independent reasons:

  • prospective — the grab was gated on the plate pulse, the title's own settled signature, and data/jp-title-at-rest.txt records the gate and a contrast control taken with it;
  • retrospective — the --at sweep above shows the capture on a plateau flat to 1.2 RMSE across 105 units with edges at 78. A capture caught mid-build would produce a sharp minimum instead. The sweep was run for a noise scale and answers this too.

The CAPTURE axis has a noise floor too, and it is 0 in the region measured

sylpheed-port found their main_menu row drifting 13.2513.30 across runs — a focus ring spinning on a free-running clock — and made the general point that a margin only means something against the noise it sits on. The --at plateau above measures the render axis. It says nothing about how much the score moves between two captures of the same screen, which is a second noise source and the one my single JP grab is exposed to.

Measured, from two independent captures of the settled EN title taken at different phases of its free-running plate pulse, scored against one render:

whole frame inside the measurement box
plate-pulse peak 31.302 21.230
plate-pulse trough 28.463 21.230
spread 2.839 0.000

⚠️ A 0.000 is the result this corpus distrusts most, so it carries its control: the two captures differ by 83 496 pixels whole-frame (max |d| 174) — they are genuinely different grabs — and by 0 inside the box. The screen's free-running element is the plate, which lies outside the logo region the adjudication uses.

And a second JP capture, from a fresh session, closes the transfer

The paragraph above transferred the EN title's capture noise to the JP box and flagged that as a limit. A second independent capture was taken — jp_title_session.sh, fresh boot, separate session, locale set and restored — live-title-jp-at-rest-run2.png.

Within-run stability reproduces (0 of 138 600 in the ROI, 47k73k px moving whole-frame as the contrast control). Between sessions, inside the box this adjudication uses: 645 of 164 124 px differ, RMSE 0.3215, against 116 492 px whole-frame — genuinely different sessions.

The verdict reproduces to three decimals:

vs session-1 capture vs session-2 capture
stale era rest (108,72) 58.412 58.413
fixed era rest (98,42) 41.690 41.692
margin 16.722 16.721

📌 Capture noise moves both candidates together, so it nearly cancels in a margin. The absolute scores moved 0.0010.002 between sessions while the margin moved 0.001, against an in-box capture noise of 0.32. A margin between two renders scored on one capture is far more robust than either score is — which is why the render axis, not the capture axis, is the binding constraint here.

The capture-phase term, measured — and why the box was the right choice

sylpheed-port overturned their own phase-0 result using the identical-leaves fact: sweeping the same leaf against a title capture minimises at phase 240 where main_menu minimises at 0, so the best-matching phase is a property of when the shutter fell, not of the game's rest state. A continuously sweeping element has no canonical rest phase. They then warned that any whole-frame score against a single capture carries an irreducible phase term of ~1.0 RMSE.

That term is measurable on my own two JP sessions, which certainly differed in sweep phase — 44 025 px differ in the band the leaf crosses:

region scored between-session RMSE
whole frame 4.566
the sweep band, x 721..1241 4.088
the adjudication box 0.3215

⚠️ Their ~1.0 understates it for this screen. A whole-frame score against one capture of the JP title carries ~4.6, not ~1.0 — theirs is the leaf-phase component isolated in a renderer, mine is everything that varies between sessions (the plate pulse alone contributes ~2.8, measured separately on the EN pair) and includes theirs. Anyone quoting a whole-frame number on this screen should use the larger figure.

📌 And it is why the box matters more than I realised when I chose it. Scoring the 388×423 region rather than the frame drops the between-session term from 4.566 to 0.3215 — a factor of 14 — because the sweep contributes at x 721..1241 and the box is mostly clear of it. That was not the reason I cropped (the crop was to avoid diluting a local difference across 92 % of an identical frame), so the robustness is luck. The general rule it earns: score inside a region that excludes the free-running elements, and measure the residual term there rather than estimating it.

So the margins re-stated against every noise floor now measured:

comparison margin render noise (1.2) in-box capture noise (0.32)
stale vs fixed era 16.72 14× — decisive 52× — decisive
settle vs rest 1.48 1.2× — NOT decisive 4.6×

🔴 Correction to a claim made earlier the same day. This page briefly said the settle-vs-rest negative was strengthened because 1.48 sits below the whole-frame capture spread of 2.8. That was the wrong comparison: the measurement lives in the box, and the in-box between-session noise is 0.32, so 1.48 is well above it. The negative rests on the render axis alone, exactly as first stated — the "strengthening" was reaching for a number that was to hand rather than the one that applies.

🔴 WITHDRAWN — "ptloop01/02 do not free-run on the settled title"

Retracted 2026-08-30, same day, by my own corpus. The claim below measured the parent's declared rect at (441,270) 200×90. That rect is a PIVOT ANCHOR, not the drawn extent. data/ptloop-leaf-sweep-positions.txt — written earlier in this same corpus — records ptloop01.rat's nested record at loop length 600, whose leaf pteff03.t32 sweeps a 400 px-wide quad with its centre running x≈921→1041 over t=340..370. That is nowhere inside x 441..640.

Checked against the two JP captures: the rect I measured differs by 0 px — but so does the entire region y 270..450 × x 480..960 around it, a dead zone — while the band the sweep actually occupies (x 721..1241) differs by 44 025 px. The zero was measured where nothing happens and proves nothing about the loops.

sylpheed-port's reading is right and is confirmed from the disc: the parent is static while the leaf record animates, and the two nested records cycle at different lengths, 600 and 720. My "single static keyframe" was a statement about the parent only.

Their full table also survives an independent check against the disc (data/ptloop-leaf-extent.txt), to the digit:

ptloop01 ptloop02
leaf pteff03 pteff03a
cycle span 600 720
x track (left edge) 639 … 1521 839 … 1721
scale (100 %, 600 %) (100 %, 800 %)

The quad is 400 px wide and not widened; scale_y makes it 1080 / 1440 px tall — a full-height strip taller than the screen, whose left edge travels right across the frame and off both sides.

📌 And a fact neither of us had: these leaves are IDENTICAL on entries 4, 5 and 7 — the title, the main menu, and the JP title. Same leaf names, spans, x tracks, scales and parent rest position. So the menu declares exactly the same sweep as the title; the open menu question is about the game's behaviour, not about a different declaration.

⚠️ My own "centre running x≈921→1041" was a 30-unit window of a 600-unit cycle whose centre spans 439…1721. A sub-range is not an extent — the same caution as a pivot not being a bounding box, one level up, and I made both errors in one day.

⚠️ The era adjudication is unaffected: its box is x 389..776, and the overlap with the sweep band (x 721..776) shows no between-session differences — the 645 in-box differing pixels all sit at y 99..128.

And ptloop01/02 do NOT free-run on the settled title (see above)

The transfer above was flagged as uncertain because build 7 carries ptloop01/02.rat, which might animate inside the adjudication box where the EN plate does not. sylpheed-port then found those same two leaves free-running in their renderer on the menu path, and was explicit that pinning a phase picks one pose rather than the game's — "a capture question, not a harness one". It is, and two captures from different sessions answer it:

ptloop01/02 rest at (441,270), 200x90 -- INSIDE the adjudication box
  differing px between session 1 and session 2:   0 of 18 000,  max |d| 0
  contrast, whole frame:                     116 492 px differ, max |d| 51

Byte-identical across sessions, while 12.6 % of the frame moves. So on the settled title the loops are static, and the in-box capture noise of 0.32 is not theirs — the 645 differing pixels all lie in a 30-row band at y 99..128, nowhere near the loop rect.

🟡 The menu is a different bundle and is NOT settled by this. GP_TITLE build 5 declares ptloop01/02 at the same rect with a single static keyframe, and that is the screen the port's row actually drifted on. A probe to capture five settled menu frames and diff that rect was written (menu_loop_rest.sh) and did not run to completion: the run reached a title at t=146 s and Ⓐ did not take across six attempts. That is the documented intermittency — the attract loop's title accepts nothing, unlike the boot title (canary-scripted-input-traps.md) — so the question is open, with the instrument written and one successful run away.

⚠️ Two committed main-menu captures exist but cannot answer it: they differ across 57 % of the surface (different geometries and capture paths), so the 88 % differing on the loop rect measures the mismatch, not the loops.

⚠️ Reach. One screen, one capture. The absolute residual is ~40 RMSE even at the best instant, because the JP title carries live animation a static render cannot reproduce; every comparison here is relative and none of it says our render is correct, only which of two candidates the game is closer to.

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.

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 while fade is unapplied.

🔴 rest_plateau is wrong for elements with no exit animation (2026-08-28)

Reported by the port agent with a capture that proves it, and it affects sylpheed-cli screen render too — this is not only a port concern.

rest_plateau drops a trailing run of identical keyframes because that run is normally the exit animation. On an element that has no exit, the trailing run is the hold, and dropping it puts the element back at its first keyframe — off-position and transparent.

The condition that identifies these exactly (no false positives across the port's whole export):

the final untimed keyframe has the same pose as the last timed one

Six elements on main_menu match it and rest() misses all six. The visible cost: on main_menu the timeline render and the rest render differ in exactly one region — 400 × 470 at (440,108), the bounding box of ptframe1 and ptframe2 and nothing else. That is the bright circuit bracket around the menu, plainly present in ../captures/main-menu-oracle.png and absent from the rest render. Cropping the same region from the capture and from both renders puts the ring and its elbow trace pixel-aligned with the game's in the timeline render.

This also explains a long-standing on ui-paint-order-key.md: "ptframe1/ptframe2 rest at 0x00ffffff (alpha 0) and are therefore not drawn, but the capture shows the menu frame plainly." Same two elements, same cause — now identified.

Fixed 2026-08-28 — but the condition is the ALPHA, not the pose

The report's proposed test — "the final untimed keyframe has the same pose as the last timed one"misfires, and on the exact case the exclusion was written for. pgptitle.rat's last two keyframes are also identical:

pgptitle.rat   kf4: fade=0x00ffffff pos=(220,69) t=30
               kf5: fade=0x00ffffff pos=(220,69) t=None     <- same pose

Adopting it as stated would erase the word PAUSE again. What separates the two is visibility:

trailing run alpha is it the hold?
ptframe1/ptframe2 (main menu) 3 × 0xffffffff at (440,108) 0xff yes — no exit animation
pgptitle (pause menu) 2 × 0x00ffffff 0x00 no — it is the fade-out

An exit fades the element out, so its last keyframe is transparent; an element with no exit ends on the pose you can see. So a trailing run is the hold exactly when it is visible, and that is what rest_plateau now tests.

Verified against a capture, not against another renderer

check result
ptframe1 rest (620,108) t=16(440,108) t=62
bracket region draws mean 61.74 → 62.79
oracle correlation over that region vs main-menu-oracle.png 0.9596 → 0.9748
pixels changed, whole frame 10 082, bounding box x 440839, y 108577 — exactly the 400 × 470 at (440,108) the report predicted
regression control: PAUSE wordmark, 3 pause builds unchanged (2833 / 2858 / 2833 bright px)

cargo test -p sylpheed-formats with SYLPHEED_DISC set: 131 passed, 0 failed across 6 binaries including the disc-gated ones. (One pre-existing ignored — the XBG7 shared-resource test — unrelated.)

The disc-wide check

Both rules reimplemented over the parsed keyframes of every RATC bundle on the disc — 2 859 bundles, 13 991 elements with ≥ 2 keyframes:

elements whose rest moves 30 (0.21 %)
invisible → visible 4
visible → invisible 0 — the safety property

The change is surgical and it never hides something that was being drawn. The remaining 26 move rest between two visible poses (position, not visibility).

The 4 revealed are ptframe1.t32 and ptframe2.t32 in GP_TITLE entry 5 and entry 8 — the same two elements, once per language build.

🟡 Reconciling with the report's "six elements"

On the English main menu exactly two elements satisfy the report's condition (last two keyframes identical), and both are ptframe1/ptframe2. So the six must span its whole 12-screen export, not that one screen — consistent with its own observation that the timeline and rest differ "in exactly one region … the bounding box of ptframe1/ptframe2 and nothing else".

Open, and it matters: if any of the other four have a transparent trailing run, this rule deliberately leaves them alone — that exclusion is what protects the word PAUSE. If a capture shows one of them drawn, the alpha rule is incomplete and needs a third discriminator. Which screens are they on?


🔴 The dwell fallback is unsound whenever it actually runs (2026-08-28)

rest() tries rest_plateau() first and, failing that, picks the keyframe with the longest dwell — the largest gap t[k+1] t[k]. That rule is not sound, and the reason is structural rather than a tuning problem.

A gap between t[k] and t[k+1] is time the element spends interpolating from pose k to pose k+1. Neither pose is held during it — unless the two poses are equal, which is exactly a plateau, and the plateau path has already handled that case and returned. So by the time the fallback runs, it is guaranteed that no pose is held, and the rule is choosing an endpoint of a movement.

The element that exposed it

🔴 This listing is the STALE PARSER's, and the example it supports is dead (2026-08-30). The times below are shifted by one with an untimed final pose — the pre-record-layout-fix reading. See CONTAINER-NOTES for the trap.

GP_TITLE build 7, ptlogo_eff3.t32 — a transient bloom, as it was printed:

 46: (98,42)  100%,100% a=0
 61: (108,72)   0%,0%   a=0
103: (108,72) 200%,200% a=255  r=80
  -: (108,72)   0%,0%   a=0    r=150      ← stale: shifted, final pose untimed

and as it actually reads:

  0: (98,42)  100%,100% a=0
 46: (108,72)   0%,0%   a=0
 61: (108,72) 200%,200% a=255  r=80
103: (108,72)   0%,0%   a=0    r=150
gaps longest its two ends
stale 15, 42 61→103 one is a=255 at 200 % — the screen-filling bloom
fresh 46, 15, 42 0→46 both a=0

So this element no longer discriminates. rest() returns (98,42) a=0 — invisible — and build 7 renders byte-identical under the corrected and legacy readings (0 pixels differ, max Δ 0). ⚠️ MISSION.md lists this element as the one case a Japanese-locale capture was needed to settle. It is not; that capture was still worth taking, for the port's title_jp question, but not for this.

No two adjacent poses are equal, so there is no plateau. The longest gap is 61 → 103 (42 units), during which the sprite grows from nothing to 200 % at full alpha while rotating 80°, then collapses again. The rule returns whichever end of that movement the indexing lands on:

returned "rest pose"
as decoded (108,72) 0%,0% a=0 — invisible
with SYLPHEED_KF_TIME_SHIFT=1 (108,72) 200%,200% a=255 — the peak

An 896×389 sprite at 200 % scale is 1792×778 — larger than the screen. Painting it permanently is what made build 7's render 13.1 % different and 4.9 luminance units brighter.

The element has no resting pose. It is a flash; after it plays there is nothing. Neither answer is derived — one of them is merely harmless.

🟡 A proposal — and my own control cannot validate it

Three iterations have measured how badly the fallback behaves without proposing anything. The proposal is pose every element at the screen's settle instant (UiBuild::settle_time(), the midpoint of the longest keyframe-free interval across the build) rather than asking each element for its own resting pose (../data/rest-vs-settle.txt).

Effect, on the 2 249 fallback elements in bundles that settle at all: the visible-pose rate falls 73.6 % → 34.7 % — consistent with transient peaks disappearing.

🔴 But the control fails, and then fails better, and still is not a pass:

control agreement
naive — every plateau element 46.6 %
fair — only those holding across the settle instant 78.1 %

The naive one was misspecified, caught by asking what 46.6 % means physically: rest() finds a held pose, and many elements hold one during the build-in then move on. Different questions; disagreement proves nothing.

⚠️ The fair control's 21.9 % residual looked ambiguous by constructionrest_plateau() picks one plateau, and an element with two whose settle instant falls in the other disagrees.

RESOLVED (2026-08-30, later): it is not ambiguous, and the residual is entirely the incumbent's (../data/plateau-choice.txt).

control — exactly one plateau, covering the settle instant 3 072 / 3 072 agree (100.0 %)
test — more than one plateau, at least one covering 1 622 elements, agree on 586 (36.1 %)
of the 1 036 disagreements, rest() landed on a run not covering the settle instant 1 036 — all of them

rest_plateau() selects the longest run (len >= any_len), which need not be the one the screen is actually sitting in. Both poses are genuinely held — these are plateau cases, not transients — so this is rest() returning a pose the screen has already left by the time it settles.

🔴 Comparing a candidate to the incumbent cannot adjudicate when the incumbent is the thing under suspicion. Too strong — corrected the same day. The bare comparison cannot. The comparison plus a structural property that independently says which side is wrong in each disagreement can, and "does the chosen run contain the settle instant" is such a property: it attributes 1 036 of 1 036. What was missing was not an oracle but a discriminator.

Closing the gap: settle_time() itself, against the game

The port ran the proposal against captures and favoured it 3/3 — but tested its own settled pose, not UiBuild::settle_time(), and said so. That gap is this crate's to close (../data/settle-vs-rest-against-captures.txt).

Geometry first, because the first attempt got it wrong. A 1280×720 render meets a 1279×675 capture by crop, not scale:

convention RMSE
crop rows 0…675 14.07
resize bilinear 68.89
crop rows 45…720 79.61

⚠️ The 45-row offset holds for a full display frame; these committed captures are already the game surface.

Gamma is fitted per pose, so each candidate gets its own best case and the comparison cannot be won by the fit:

screen pose γ RMSE % > 8
title settle 0.84 8.17 15.28
title rest 1.04 20.92 70.84
publisher settle / rest 0.30 — railed 36.38 / 36.69 2.36 / 2.74
developer settle / rest 0.30 — railed 33.07 / 33.42 6.16 / 6.54

🔴 The two splashes do not adjudicate and are not counted. Their gamma fit sits on the edge of the search range — widened to 0.303.00 and it still rails — so the photometric model is wrong for them, and with γ railed the margins collapse to 1.16× and 1.06×.

title does adjudicate, at an interior γ, and decisively: 15.28 % against 70.84 % differing (4.6×), RMSE 8.17 against 20.92 (2.6×). So the implementation and not merely the direction is supported.

⚠️ Absolute agreement is poor — the port's settled title row is 0.21 % where mine is 15.28 %. Its renderer draws things mine does not and a single global gamma is a crude model. Take the ordering from this table, not the values.

The stronger evidence remains the oracle, and it is the port agent's, not mine: its publisher splash against a committed capture, settle-instant pose RMSE 2.17 / 0.01 % differing against --pose=rest 9.05 / 0.75 % — 75× the differing area, against the game. That is the evidence for the proposal; the numbers above describe its effect and do not establish it.

🟡 The candidate's own failure mode — censused, and the obvious explanation is wrong

The port agent found ptmsg, the main menu's footer, at alpha 127.5 of 255 at that screen's settle instant. Verified: build 5's window is [44, 56] = 12 units, and screen render --settle already prints "narrow — this bundle may never settle".

Disc-wide, elements caught mid-ramp at their screen's settle instant (../data/settle-midramp-census.txt):

settle window elements mid-ramp share
< 10 3 571 1 460 40.9 %
1019 1 755 791 45.1 %
2029 935 297 31.8 %
3059 4 084 476 11.7 %
≥ 60 3 646 548 15.0 %
all 13 991 3 572 25.5 %

🔴 WITHDRAWN 2026-08-30 — the table below is wrong and so is the conclusion I drew from it. screen render --build N takes a build ordinal, not a pak entry: screen list says [10] entry 12, [11] entry 15. The splashes are entries 10 and 11 and are not screen builds at all, so my "--build 10/11" windows of 8 are the loading screens. From the file the splashes are 190 and 145 — the widest of the five, not the narrowest (../data/splash-settle-window-retraction.txt).

So width and mid-ramp are perfectly confounded across every screen either agent has measured, and the width hypothesis is NOT refuted. The port's predictor may still be the mechanism; this evidence does not establish it over width.

The withdrawn reading, kept because the mistake is instructive:

build screen window port's measurement
4 title 76 settle wins 9×
5 main menu 12 settle loses 1.2×
10 publisher 8 settle wins 75×
11 developer 8 settle wins 33×

The splashes are narrower than the menu and the settle pose wins by 75×. They are wider. The rows read 8 because they are the loading screens.

⚠️ One half of the filter criticism survives: dropping bundles with a window under 10 units admitted the 1019 bucket, the worst at 45.1 % mid-ramp. The other half — that it excluded the splashes — is withdrawn; at 190 and 145 they were never near the cutoff.

🔴 Losing the example did not close the question — it is larger than one element

Disc-wide (../data/rest-fallback-census.txt):

elements with ≥ 2 keyframes 13 991
have a plateau — the fallback never runs 11 686
have none — the fallback decides 2 305
of those, it returns a visible pose 1 697 (74 %)
of those, it returns the element's MAXIMUM alpha 1 457

⚠️ "1 697" is not a defect count and this page briefly implied it was. An element that genuinely ends visible should rest visible. The number that survives is 1 457: the fallback runs only when no two adjacent poses are equal — i.e. only when no pose is held — so every pose it can return is un-held by construction, and 1 457 times it hands back the brightest one.

🔴 A first attempt to correct this failed its own control, and is recorded because the failure is instructive (../data/rest-fallback-audit.txt). Splitting the 1 697 by whether the element's last keyframe is visible gave 347 / 1 350 — plausible, arithmetic sound. But 12 278 of 13 991 elements (87.8 %) end at alpha 0, because a screen's exit ramp drives everything to zero. The split carries almost no information. The port agent had been bitten by exactly this an hour earlier — its census called ptmsg, the main menu's permanent footer, "a 2-unit flash" — and I ran the control only because it said so.

GP_TITLE: 5 fires, 4 visible — and all four are on the splash screens, palogo_sqex_eff.t32 / palogo_anima_eff.t32 on entries 10/11/13/14. Each reads [0:a0 15:a255 30:a212 45:a0]: a flash peaking at t=15, dead by t=45, and the fallback returns t=30, a=212 — near the peak of a transient.

Independently converged on from the other side. The port agent, working from the Japanese title capture and knowing nothing of this census, found ptlogo_back2eff1's rest.t sitting at the peak of its own 4-unit sparkle, with six staggered across the logo — so --pose=rest fires every sparkle at once, a frame the game never shows.

⚠️ The consequence, and it is a rule about how a rest render may be used: a render posed at rest is a legitimate common reference for comparing two decoders, and is not a frame to score against a capture of the game.

Now an oracle number rather than an argument. The port measured its publisher splash against the committed capture in both poses: timeline RMSE 2.17 / 0.01 % differing, against --pose=rest RMSE 9.05 / 0.75 %75× the differing area, on a screen it ships. Nothing it ships is wrong; its settled pose evaluates pose_at(hold) and skips the flashes.

🔴 …and the rule is NOT a consequence of the fallback being unsound

The port also listed palogo_gamearts_eff and palogo_seta_eff among the four visible fallback fires. They are not — refutation attempt, and it succeeds (../data/palogo-eff-plateau-vs-fallback.txt):

element keyframes path rest
palogo_sqex_eff, palogo_anima_eff 0:a0 15:a255 30:a212 45:a0 dwell fallback (unsound) t=30, a=212
palogo_gamearts_eff, palogo_seta_eff 0:a0 15:a255 **30:a255** 45:a0 plateau (sound — the pose is held) t=15, a=255

The second pair holds a=255 at identical x, y and scale from t=15 to t=30. That is a plateau, rest_plateau() handles it, and t=15 is the correct answer. The census's four stand.

🔴 But that makes the port's point stronger, not weaker. Its rest pose for those two is the flash's peak, reached by the sound path. So "a rest render is not a frame to score against a capture" does not follow from the fallback being unsound — a plateau can itself be the held peak of a transient. The rule covers both paths, and the fallback census (2 305 / 1 697) understates the exposure rather than bounding it.

🔴 What this retracts

Last iteration I reported the build 7 render difference as evidence against the keyframe-time shift, on the reasoning that language twins should match in brightness. Withdrawn. The difference is not about the time association at all: it is the dwell fallback guessing, and it would guess on this element under any reading of the times. The brightness comparison was measuring a heuristic, not a decode.

What that leaves: the case for the shift (a factor of 26 on the hold:fade-out ratio, ui-keyframe-time-unit.md) is no longer opposed by render evidence — 10 of 11 builds are byte-identical and the 11th differs only through an unsound heuristic.

🟡 It is still not adopted, for a different reason than before: adopting it would flip this element to the visibly wrong answer, so the shift and a decision about what rest() should do for plateau-less elements have to land together, and the second half has no capture to verify against either.

⚠️ What the port should take from this

Any element whose keyframes contain no two adjacent identical poses has a rest() result that is guessed, not decoded — in our renderer and in anything built from it. That is a property a consumer can test for itself in one pass over the keyframes, and it is worth flagging in an export rather than silently inheriting our guess.

The size of the defect, and why it does not block the menu port

tools/re-capture/plateau_census.py walks every GP_*.pak placement region directly (the CLI route decodes every texture and is far too slow for a disc-wide pass). Its control reproduces GP_TITLE build 7's three fallback elements and names ptlogo_eff3.t32 among them before it counts anything. Output: data/plateau-census.txt.

elements with a keyframe group, disc-wide 15 493
no plateau 3 807 (24.57 %) — ⚠️ overstated, see the correction below
…of those, single-keyframe (trivially at rest, not a guess) 1 502
genuinely ambiguous (2+ keyframes, no plateau) 2 305 (14.88 %)
of those, current rule returns an invisible pose 1 711 (44.9 %)
of those, current rule returns a zero-scale pose 195 (5.1 %)
the two candidate rules agree 1 911 (50.2 %)

A returned pose with scale = 0 % is not a pose at all, and 195 elements get one. Disc-wide the choice of rule is not cosmetic: the candidates agree only half the time.

But on the five screens the port needs, the exposure is one element

screen plateau-less rules differ
main menu (entry 5) 5 / 16 0
EXTRAS (entry 6) 5 / 18 0
title (entry 4) 2 / 24 0
developer splash (entry 11) 2 / 7 1

Thirteen of the fourteen affected elements get the same answer either way. The one disagreement is palogo_anima_eff.t32.

🔴 And "rest = last keyframe" is refuted on it

That was the alternative I named last iteration. The developer splash carries three sibling glows, identical in structure and in every time:

palogo_gamearts_eff  15:a=0  30:a=255  45:a=255  -:a=0   → plateau → visible
palogo_seta_eff      15:a=0  30:a=255  45:a=255  -:a=0   → plateau → visible
palogo_anima_eff     15:a=0  30:a=255  45:a=212  -:a=0   → no plateau

They differ in one byte212 where the others have 255. Under "last keyframe", anima_eff alone goes invisible while its two siblings stay lit. A rule that makes one of three parallel elements behave differently because of a single alpha count is producing an artefact, not a decode.

The capture agrees weakly. Comparing box means in live-splash-developer.png against our render (the screenshot is 1279×675, top-aligned, so only ratios are comparable): gamearts 0.717, seta 0.723, anima 0.772. If our render were adding a glow the game does not draw, anima's ratio would sit below its siblings'. It sits above.

🟡 Where this leaves it

The defect is established and measured: rest() guesses for 14.88 % of elements disc-wide and returns a degenerate zero-scale pose for 195 of them. The fix is not decided — "last keyframe" is refuted, and the current rule survives on the only captured element that discriminates. ⚠️ For the menu port specifically this is not a blocker: one element on one screen, and our current answer for it is the defensible one.


🔴 The 24.57 % was overstated, and my proposed fix is refuted

2026-08-29. Two corrections, both from one experiment.

The prediction: draw nothing for an element with no held pose

A keyframe group is entry → hold → exit, and the exit ends invisible — on the five port screens the final keyframe is invisible for 21/24 (title), 8/16 (main menu), 12/18 (EXTRAS), 2/3 and 6/7 (splashes). So the screen "as seen" is the hold, which is why rest_plateau is the right primary rule and why "rest = the last keyframe" would empty every screen.

That suggested a fix: an element with no hold has no representative pose, so draw nothing rather than guess an endpoint. Tested via compose's visible mask (examples/plateauless_suppression.rs), correlated against the live captures:

screen as-is plateau-less suppressed Δ
title +0.9500 +0.6839 0.2661
main menu +0.9460 +0.9037 0.0423
EXTRAS +0.9440 +0.9094 0.0346

Refuted, decisively, on all three.

Why — and it invalidates the headline number

An element with a single keyframe has no adjacent pair, so the plateau test marks it plateau-less. But a single-keyframe element is not animated at all: its one pose is its rest, unambiguously. Suppressing those removes backgrounds and full-screen layers, which is where the title's 0.27 comes from.

The same flaw is in the census this page published:

no plateau (as published) 3 807 (24.57 %)
…of which single-keyframe 1 502 — trivially at rest
genuinely ambiguous 2 305 (14.88 %)

So the guessed-rest population is 2 305, not 3 807 — the published figure overstated it by 65 %. The defect is real and smaller than reported.


Where the fallback actually fires — and "last keyframe" wins there

2026-08-29. Two things fell out of scoring rules by rendering instead of arguing about them.

The port's exposure is TWO elements, not fourteen

The guessing fallback is reached only by an element that is plateau-less and has more than one keyframe — a single-keyframe element short-circuits at match len { 1 => first }. Per screen:

screen elements plateau-less reach the fallback
title (4) 24 2 0
main menu (5) 16 5 0
EXTRAS (6) 18 5 0
publisher splash (10) 3 2 1
developer splash (11) 7 2 1

On the three screens a port cares most about, rest() never guesses at all. That is why three different fallback rules render builds 4, 5 and 6 to identical correlations (+0.9500 / +0.9460 / +0.9440 for every rule) — the code is unreachable there.

On the two splashes, the last keyframe is markedly better

Rendering under each rule (SYLPHEED_REST_RULE=last|maxalpha, default off) and correlating against the live captures:

splash dwell (current) last maxalpha
publisher +0.9600 +0.9982 +0.9600
developer +0.9643 +0.9758 +0.9643

maxalpha is indistinguishable from the current rule; last gains +0.038 and +0.012, and 0.9982 is very close to exact.

🔴 This refutes my own refutation

I had killed "rest = the last keyframe" by argument: on the developer splash it makes palogo_anima_eff invisible while its two siblings stay lit, which looked like an artefact. The capture says otherwise — making it invisible is what improves the match. The sibling symmetry was my expectation, not evidence.

⚠️ The caveat that keeps this honest: both captures are single frames of a transient animation. They fix which pose matches these reference frames, not which pose is canonically "at rest" — a grab a second earlier would show the glows. What the measurement supports is: for reproducing the committed captures, use the last keyframe.

🟡 Default unchanged. The rule is better on both screens where it fires and provably identical on the other three, but it would alter the rest pose of 2 305 elements disc-wide and two of them have been measured. Reachable via SYLPHEED_REST_RULE=last for anyone who wants to take that further.

Unaffected by the earlier correction: all 195 zero-scale rest poses are inside the genuinely-ambiguous 2 305 — none is a single-keyframe element.

The disc-wide blast radius — measurable even though the rule is not scorable there

Only two ambiguous elements sit on a screen with a live capture, so the rule cannot be scored disc-wide. What can be quantified is what changing it would do (tools/re-capture/rest_rule_blast_radius.py, data):

dwell (current) last
the two rules differ 1 896 of 2 305 (82.3 %)
returns an invisible pose 1 711 (74.2 %) 1 618 (70.2 %)
returns a zero-scale pose 195 (8.5 %) 43 (1.9 %)

Two things worth reading off this.

It is not a marginal choice. The rules disagree on 82 % of the affected elements, so "either is fine" is not available.

The current rule produces 4.5× more degenerate poses. A zero-scale pose is collapsed to nothing (rat layout) — an element's pre-roll, before it has grown in. A pre-roll is definitionally not a rest, so 195 elements currently rest at a frame the element is only passing through, against 43 under last. That is an argument from the data's own structure rather than from the two captures, and it points the same way they do.

🟡 It remains indirect: fewer degenerate results is not the same as more correct results, and last still returns an invisible pose 70 % of the time — which for a transient element may well be right, and for a persistent one would be wrong. The default stays put.


Why rest_plateau is right, and why last is right only for a transient

2026-08-29. The shifted keyframe-time reading (time unit) appears to predict something simple: the final pose is reached at a definite time and nothing follows, so "rest" should just be the last keyframe, with no heuristic at all. Tested (SYLPHEED_REST_RULE=lastall, bypassing the plateau rule for every element):

screen current lastall Δ
title +0.9500 +0.6819 0.2681
main menu +0.9460 +0.6416 0.3044
EXTRAS +0.9440 +0.5745 0.3695
publisher splash +0.9600 blank
developer splash +0.9643 blank

The splashes render entirely empty — zero variance, so the correlation is not even defined. Refuted.

What the failure shows

A group's keyframes describe entry → hold → exit, and the exit is the screen's dismissal. While a screen is on display it has not reached its last keyframe; it is sitting at the hold, and the exit plays only when the screen leaves. That is why:

  • rest_plateau is the correct primary rule — the hold is where the screen lives, and it is what a capture of a displayed screen shows;
  • the last keyframe is the post-exit state, correct only at the moment the screen is already gone — which is why applying it everywhere blanks the splashes;
  • and it does not contradict the shifted time reading. That reading says when each pose is reached; it says nothing about the group being played to completion while the screen is still up.

And it explains why last wins for the plateau-less two

An element with no hold is a transient: it flashes and is over. At any settled moment it is gone, and "gone" is its last keyframe. That is not a general rule about rest — it is what a transient's settled state is.

The draw capture says exactly this independently: on the developer splash the _eff glows are drawn on frames 94115 and the logos on 116211. The glows are already finished when the logos are up, so a capture of the logo moment must show them invisible — which is what SYLPHEED_REST_RULE=last produces, and why it scored +0.9982 and +0.9758.

So the picture is coherent across three independent observables — animation timing, static composites, and the per-frame draw log — and the rule it supports is: plateau where there is one, last keyframe where there is not.

The structural case for last, disc-wide — 2 293 of 2 305

Only two ambiguous elements could be scored against a capture. But the entry → hold → exit model makes a prediction that can be checked on all 2 305: what does each element's final keyframe look like? (tools/re-capture/plateauless_endstate.py, data)

final keyframe count is last the right answer?
invisible (a = 0) 1 618 the element is a transient; at any settled moment it is gone model-consistent, and confirmed by the draw log for the two observed
visible, at maximum alpha 675 it faded in and stopped — the final pose is the settled one
visible, below maximum alpha 12 genuinely unclear

Of the 687 that end visible, 472 have monotonically non-decreasing alpha — a plain fade-in that stops, [0, 255] over two keyframes in the commonest case (pjex_eff.rat, pghud_speed_cut.t32) — and another 203 end at their maximum after dipping. Only 12 elements in 2 305 end visible at less than full alpha.

So last is structurally defensible for 2 293 of 2 305 (99.5 %), against a dwell rule that returns a mid-movement frame by construction. It is observed correct for 2, structural for 675, model-consistent for 1 618, and unclear for 12.

⚠️ The 1 618 carry the weight of an assumption, and it should be visible: that a plateau-less element's animation has finished by the time the screen is settled. The draw log establishes this for the two splash glows (drawn on frames 94115, with the logos on 116211) and nothing establishes it for the rest.


A static "rest" composite is only meaningful for a screen that SETTLES

2026-08-29. The model's sharpest prediction, tested with its control.

The draw log says that on the developer splash the _eff glows are drawn on frames 94115 and the logos on 116211 — so at the moment the reference capture was taken, every glow is already finished, including the two that have plateaus and which rest_plateau therefore renders visible. Suppressing them should improve the match. Suppressing them on a screen that genuinely settles should make it worse.

screen as-is eff elements suppressed Δ
publisher splash +0.9604 +0.9982 +0.0377
developer splash +0.9659 +0.9980 +0.0321
title (control) +0.9500 +0.9480 0.0020
main menu (control) +0.9460 +0.8544 0.0916
EXTRAS (control) +0.9440 +0.8370 0.1070

Both splashes jump to ≈0.998; all three persistent screens get worse. The control is what makes this a finding rather than a coincidence — the same edit helps on exactly the screens the model says it should and hurts on exactly the ones it says it should not.

What this settles

rest_plateau is not over-drawing in general — it over-draws on transient screens. A plateau in the middle of an animation is evidence the element is held at that point in the timeline, not that it is on screen when the screen has settled. On a screen that settles, the held pose is the settled pose and the rule is right (the controls). On a screen that plays through and leaves, there is no settled moment at all, and the reference capture is a picture of one arbitrary frame.

⚠️ So the honest answer to "what is the rest pose of a transient screen" is: there isn't one. The splashes are animations. A static composite of them can match a chosen frame — ≈0.998 for the frame these captures hold — but "the resting pose" is a question the format does not answer because the screen never rests.

For the port: play the timeline for the splashes, which the settled keyframe timing now supports (time unit). Use a static composite for the title, main menu and EXTRAS, where the screen does settle and rest_plateau is measurably right.