sylpheed-port's harness grabbed main_menu at t=9.00 in one session and t=8.00 in the next. One keyframe unit apart, mid-build-in, is 70 % of the picture, and it read as "the change broke two screens" -- a real measurement of the wrong thing. The instant was stable WITHIN a session and drifted BETWEEN them, so every cheap reproducibility check said deterministic. Their flags are their harness's, not sylpheed-cli's (checked: `screen` has only list/info/render), so the tool defect is not in my crate -- but the hazard generalises to every live capture here. It would void this iteration's ptlogo_eff3 adjudication if the JP capture had been taken at an arbitrary moment. It was not, and for two independent reasons recorded rather than assumed: the grab was gated on the plate pulse, the title's own settled signature, with the gate and a contrast control written beside the capture in jp-title-at-rest.txt; and the --at sweep shows the capture on a plateau flat to 1.2 RMSE across 105 units against edges at 78, where a capture caught mid-build would give a sharp minimum. The sweep was run for a noise scale and answers this too -- which is luck, so METHOD now names both defences. METHOD: pin a capture's instant explicitly, and do not infer stability from repeat runs inside one session. Gate the grab on a settled signal prospectively, and sweep --at retrospectively -- a broad flat minimum with sharp edges means at rest, a sharp minimum means the instant is load-bearing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
969 lines
46 KiB
Markdown
969 lines
46 KiB
Markdown
# 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`](../captures/title-builds/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 98–103, against 40–58 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`](../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`](../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.
|
||
|
||
⚠️ **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 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.
|
||
|
||
* [composited with the plateau rule](../captures/ui-layout/title-composited-plateau-rest.png)
|
||
* [composited with the old longest-dwell rule](../captures/ui-layout/title-composited-longest-dwell-rest.png)
|
||
|
||
## 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`](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](../captures/ui-layout/title-composited-fade-applied.png) 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`](../captures/ui-layout/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](../captures/ui-layout/pause-composited-fade-applied.png).
|
||
|
||
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`](../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`](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`](../captures/main-menu-oracle.png) | **0.9596 → 0.9748** |
|
||
| pixels changed, whole frame | 10 082, bounding box **x 440–839, y 108–577** — 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](../../agents/CONTAINER-NOTES.md) 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`](../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 construction** —
|
||
`rest_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`](../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`](../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.30–3.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`](../data/settle-midramp-census.txt)):
|
||
|
||
| settle window | elements | mid-ramp | share |
|
||
|---|---|---|---|
|
||
| < 10 | 3 571 | 1 460 | 40.9 % |
|
||
| **10–19** | 1 755 | 791 | **45.1 %** |
|
||
| 20–29 | 935 | 297 | 31.8 % |
|
||
| 30–59 | 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`](../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 **10–19** 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`](../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`](../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`](../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`](../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`](../../../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`](../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 byte** — `212` 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`](../captures/title-builds/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`](../../../tools/re-capture/rest_rule_blast_radius.py),
|
||
[data](../data/rest-rule-blast-radius.txt)):
|
||
|
||
| | 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](ui-rat-layout.md)) — 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](../ui-keyframe-time-unit.md)) 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 **94–115** and the logos on **116–211**. 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`](../../../tools/re-capture/plateauless_endstate.py),
|
||
[data](../data/plateauless-endstate.txt))
|
||
|
||
| 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 94–115,
|
||
with the logos on 116–211) 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 **94–115** and the logos on **116–211** — 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](../ui-keyframe-time-unit.md)). Use a
|
||
static composite for the title, main menu and `EXTRAS`, where the screen does
|
||
settle and `rest_plateau` is measurably right.
|