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:
@@ -268,12 +268,27 @@ authored version can be deleted.
|
|||||||
also makes `rest()`'s plain dwell rule pick the visible hold instead of a
|
also makes `rest()`'s plain dwell rule pick the visible hold instead of a
|
||||||
fully transparent pose, and removes the decoder's "last block's time is
|
fully transparent pose, and removes the decoder's "last block's time is
|
||||||
unreadable" special case.
|
unreadable" special case.
|
||||||
🔴 **What stops it:** rendering every build of six UI paks under both readings,
|
~~🔴 **What stops it:** … `GP_TITLE` build 7 … twins should match in
|
||||||
10 of 11 are byte-identical and one changes — `GP_TITLE` build 7, the Japanese
|
brightness …~~ **Withdrawn 2026-08-28.** That render difference is **not**
|
||||||
twin of build 4, by 13.1 % of pixels. Build 4 (the one with a live capture) is
|
evidence about the time association. It is one element — `ptlogo_eff3.t32`, a
|
||||||
unchanged, so the only build the shift moves is the one with no capture to
|
transient bloom that grows `0 % → 200 %` at full alpha while rotating, then
|
||||||
judge it, and the proxy goes against: twins should match in brightness, and
|
collapses — and it has **no resting pose at all**. `rest()` falls through to
|
||||||
build 7 is 70.94 as decoded against build 4's 71.41, but 76.32 shifted.
|
its dwell fallback and returns whichever end of that movement the indexing
|
||||||
|
lands on: the invisible frame as decoded, the 200 % peak shifted. An 896×389
|
||||||
|
sprite at 200 % is larger than the screen, which is the whole 13.1 % and the
|
||||||
|
whole luminance gap. I was comparing a heuristic, not a decode.
|
||||||
|
🔴 **And that is a real defect you should know about:** `rest()`'s dwell rule
|
||||||
|
is unsound *whenever it runs*. A dwell gap is time spent interpolating **from**
|
||||||
|
pose k **to** pose k+1; neither is held unless they are equal — which is a
|
||||||
|
plateau, and the plateau path has already returned by then. **So any element
|
||||||
|
with no two adjacent identical poses has a guessed rest pose**, in our renderer
|
||||||
|
and in anything built from it. You can test for that in one pass over the
|
||||||
|
keyframes; consider flagging it rather than inheriting our guess.
|
||||||
|
[`structures/ui-resting-pose.md`](../re/structures/ui-resting-pose.md)
|
||||||
|
🟡 **The shift is still not adopted**, now for a different reason: it flips
|
||||||
|
this element to the visibly wrong answer, so it and a decision about
|
||||||
|
plateau-less elements have to land together, and neither half has a capture to
|
||||||
|
verify against.
|
||||||
**Default unchanged**, experiment reachable via `SYLPHEED_KF_TIME_SHIFT=1`.
|
**Default unchanged**, experiment reachable via `SYLPHEED_KF_TIME_SHIFT=1`.
|
||||||
**What this means for you:** the interpolation *law* is settled (linear, 2
|
**What this means for you:** the interpolation *law* is settled (linear, 2
|
||||||
units/frame); a multi-keyframe group's *timing* is not — do not expect a
|
units/frame); a multi-keyframe group's *timing* is not — do not expect a
|
||||||
|
|||||||
@@ -380,3 +380,19 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the
|
|||||||
be to treat one as refuting the other and drop the loser. Name what each
|
be to treat one as refuting the other and drop the loser. Name what each
|
||||||
measurement actually constrains — here *timing* versus *pose selection* — before
|
measurement actually constrains — here *timing* versus *pose selection* — before
|
||||||
deciding they conflict.
|
deciding they conflict.
|
||||||
|
* **Before believing a render diff refutes a decode, find out which element moved.**
|
||||||
|
I reported a 13 % render change as evidence against a keyframe-time hypothesis,
|
||||||
|
on a brightness argument about language twins. One element accounted for all of
|
||||||
|
it, and that element is a transient flash with **no resting pose** — so the
|
||||||
|
difference was our own `rest()` heuristic guessing, and it would have guessed
|
||||||
|
under either reading. A whole-image statistic (pixels changed, mean luminance,
|
||||||
|
correlation) tells you *that* something moved, never *what*, and an argument
|
||||||
|
built on it can be entirely about your own code. Localise the diff to an
|
||||||
|
element and read its keyframes first.
|
||||||
|
* **A fallback that only runs when its precondition fails is not a fallback.**
|
||||||
|
`rest()` picks the longest-dwell keyframe when no plateau exists — but a dwell
|
||||||
|
gap is time spent *moving between* two poses, and the only case where an
|
||||||
|
endpoint is genuinely held is when the two poses are equal, which is exactly
|
||||||
|
the plateau the first path already caught. So the fallback is guaranteed to be
|
||||||
|
guessing every time it is reached. When a heuristic's justification is the same
|
||||||
|
condition that routes around it, it has no justification.
|
||||||
|
|||||||
@@ -391,3 +391,14 @@ neighbourhood, not just the line.
|
|||||||
predicted **2.0** as decoded and **80.0** shifted. The shift is nonetheless
|
predicted **2.0** as decoded and **80.0** shifted. The shift is nonetheless
|
||||||
**not adopted** — it moves `GP_TITLE` build 7 by 13 % of pixels, away from its
|
**not adopted** — it moves `GP_TITLE` build 7 by 13 % of pixels, away from its
|
||||||
verified English twin's brightness. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md)
|
verified English twin's brightness. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md)
|
||||||
|
* "the `GP_TITLE` build 7 render difference is evidence against the keyframe-time
|
||||||
|
shift" → **mine, and withdrawn.** It is one element, `ptlogo_eff3.t32`, a
|
||||||
|
transient bloom with no resting pose; `rest()`'s dwell fallback returns a
|
||||||
|
different endpoint of the same movement under each reading. The brightness
|
||||||
|
comparison measured our heuristic, not the decode.
|
||||||
|
[`structures/ui-resting-pose.md`](structures/ui-resting-pose.md)
|
||||||
|
* "`rest()`'s longest-dwell fallback picks the pose the element rests at" →
|
||||||
|
**refuted structurally.** A dwell gap is time spent interpolating *between*
|
||||||
|
poses; an endpoint is only held when the two poses are equal, which is a
|
||||||
|
plateau, which the earlier path already returned for. Every element that
|
||||||
|
reaches the fallback has a guessed rest pose.
|
||||||
|
|||||||
@@ -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
|
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
|
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?**
|
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