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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user