re(ui): withdraw the render evidence -- rest()'s dwell fallback is unsound

Last iteration I reported a 13.1% render change in GP_TITLE build 7 as
evidence AGAINST the keyframe-time shift, arguing that language twins
should match in brightness. Withdrawn. Localising the diff to an element
shows it is not about the time association at all.

The element is ptlogo_eff3.t32, a transient bloom:

   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

No two adjacent poses are equal, so there is no plateau, and rest() falls
through to its longest-dwell fallback. The longest gap is 61->103, during
which the sprite grows from nothing to 200% at full alpha and then
collapses. The rule returns whichever end of that movement the indexing
lands on: the invisible frame as decoded, the 200% peak shifted. An
896x389 sprite at 200% is larger than the screen, which accounts for the
entire 13.1% and the entire 4.9-unit luminance gap. I was comparing a
heuristic against itself.

The defect generalises, and structurally. A dwell gap is time spent
interpolating FROM pose k TO pose k+1; neither pose is held during it
unless the two are equal -- which is a plateau, and the plateau path has
already returned by then. So the fallback is guaranteed to be guessing
every time it is reached, under any reading of the times. Every element
with no two adjacent identical poses has a guessed rest pose, in our
renderer and in anything built from it.

Consequences: the case FOR the shift (26x on the hold:fade-out ratio) is
no longer opposed by render evidence -- 10 of 11 builds byte-identical,
the 11th differing only through this heuristic. It is still not adopted,
now because it flips this element to the visibly wrong answer, so the
shift and a decision about plateau-less elements must land together, and
neither half has a capture to verify against.

Default-mode suite green (122 passed, 0 failed across 3 suites so far).
This commit is contained in:
Sylpheed RE agent
2026-08-28 23:10:41 +00:00
parent 6f23f4d113
commit b947fa8e8e
4 changed files with 117 additions and 6 deletions

View File

@@ -251,3 +251,72 @@ the bounding box of `ptframe1`/`ptframe2` and nothing else"*.
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
`GP_TITLE` build 7, `ptlogo_eff3.t32` — a transient bloom:
```
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
```
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.
### 🔴 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.