From b947fa8e8e7213db0539feb858a78a769efd39f2 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Fri, 28 Aug 2026 23:10:41 +0000 Subject: [PATCH] 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). --- docs/port/HANDOFF.md | 27 ++++++++--- docs/re/METHOD.md | 16 +++++++ docs/re/REFUTED.md | 11 +++++ docs/re/structures/ui-resting-pose.md | 69 +++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 6 deletions(-) diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 974761d..0f8a35e 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -268,12 +268,27 @@ authored version can be deleted. 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 unreadable" special case. - 🔴 **What stops it:** rendering every build of six UI paks under both readings, - 10 of 11 are byte-identical and one changes — `GP_TITLE` build 7, the Japanese - twin of build 4, by 13.1 % of pixels. Build 4 (the one with a live capture) is - unchanged, so the only build the shift moves is the one with no capture to - judge it, and the proxy goes against: twins should match in brightness, and - build 7 is 70.94 as decoded against build 4's 71.41, but 76.32 shifted. + ~~🔴 **What stops it:** … `GP_TITLE` build 7 … twins should match in + brightness …~~ **Withdrawn 2026-08-28.** That render difference is **not** + evidence about the time association. It is one element — `ptlogo_eff3.t32`, a + transient bloom that grows `0 % → 200 %` at full alpha while rotating, then + collapses — and it has **no resting pose at all**. `rest()` falls through to + 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`. **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 diff --git a/docs/re/METHOD.md b/docs/re/METHOD.md index c784afc..3224ee9 100644 --- a/docs/re/METHOD.md +++ b/docs/re/METHOD.md @@ -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 measurement actually constrains — here *timing* versus *pose selection* — before 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. diff --git a/docs/re/REFUTED.md b/docs/re/REFUTED.md index 621eac4..07b979a 100644 --- a/docs/re/REFUTED.md +++ b/docs/re/REFUTED.md @@ -391,3 +391,14 @@ neighbourhood, not just the line. 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 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. diff --git a/docs/re/structures/ui-resting-pose.md b/docs/re/structures/ui-resting-pose.md index 2334f24..b119aab 100644 --- a/docs/re/structures/ui-resting-pose.md +++ b/docs/re/structures/ui-resting-pose.md @@ -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.