re(ui): the keyframe-time shift is favoured 26x by timing, rejected by a render
Follow-up on last iteration's unadopted candidate (+36 holds the NEXT pose's time, not its own). Two new results, pointing opposite ways, and both are reported. FOR, and calibration-free: the observed full-alpha hold : fade-out ratio on palogo_gamearts is 83 : 13 frames = 6.38. The shifted reading predicts 8.00. The current reading predicts 0.25 -- off by 26x. With the glow's 2 units/frame fixed and nothing else free, the current reading says the logo holds full alpha for 2.0 frames; the capture holds it for 83. This is no longer the shape argument the candidate rested on. Also for: rest()'s plain max-dwell fallback picks a=0 -- a transparent pose, for a publisher logo -- under the current reading, and the visible a=255 hold under the shift. Only the rest_plateau special case rescues the render today, and that is the case the port agent reported a bug in. AGAINST: rendering every build of six UI paks under both readings, 10 of 11 compared 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 verified against a live capture, is unchanged either way, so the single build the shift moves is the one with no capture to adjudicate it. The proxy goes against the shift: language twins are the same artwork, and build 7 reads 70.94 mean luminance as decoded against build 4's 71.41, but 76.32 shifted. Correlation does not separate them (0.6206 vs 0.6201). These constrain different things -- timing versus pose selection -- and rest() is a heuristic layered on the times, so moving the times moves its tie-breaks. Adopting the shift means revisiting that heuristic in the same change, with no build-7 capture to verify against. Default UNCHANGED. Experiment reachable via SYLPHEED_KF_TIME_SHIFT=1. Shifted-mode suite: 122 passed, 0 failed across 3 suites at commit time.
This commit is contained in:
@@ -450,6 +450,8 @@ fn mark_focused_states(elements: &mut [Element]) {
|
||||
/// Read the placement region that follows the declaration table, filling in each
|
||||
/// element's keyframe group.
|
||||
fn parse_placements(bundle: &[u8], elements: &mut [Element]) -> Vec<usize> {
|
||||
// Experiment gate, default off; see the `time` field below.
|
||||
let shift_times = std::env::var("SYLPHEED_KF_TIME_SHIFT").as_deref() == Ok("1");
|
||||
let count = elements.len();
|
||||
let mut order = Vec::with_capacity(count);
|
||||
let mut pos = DECL_TABLE_AT + count * DECL_ENTRY;
|
||||
@@ -485,7 +487,16 @@ fn parse_placements(bundle: &[u8], elements: &mut [Element]) -> Vec<usize> {
|
||||
x: be32(bundle, blk + 28) as i32,
|
||||
y: be32(bundle, blk + 32) as i32,
|
||||
// Only a block wholly inside the group carries a time.
|
||||
time: (blk + 40 <= group_end).then(|| be32(bundle, blk + 36)),
|
||||
//
|
||||
// ⚠️ Which block a time word BELONGS TO is under test — see
|
||||
// `docs/re/ui-keyframe-time-unit.md`. Set `SYLPHEED_KF_TIME_SHIFT=1`
|
||||
// to read `W[k-1]` as block `k`'s time ("the word is the time the
|
||||
// NEXT pose is reached") instead of `W[k]`. Default is unchanged.
|
||||
time: if shift_times {
|
||||
(k >= 1).then(|| be32(bundle, blk - KEYFRAME + 36))
|
||||
} else {
|
||||
(blk + 40 <= group_end).then(|| be32(bundle, blk + 36))
|
||||
},
|
||||
});
|
||||
}
|
||||
elements[idx].keyframes = group;
|
||||
|
||||
@@ -258,12 +258,26 @@ authored version can be deleted.
|
||||
drawn at all (and that is not culling — the same element is submitted down to
|
||||
`a=7` on the way out). Its declared fade-out also spends 12 of 16 units
|
||||
dropping only 23/255 of the alpha; the capture shows no such plateau.
|
||||
A candidate — that `+36` holds the *next* keyframe's time — fixes the shape
|
||||
(RMS 4.05 vs 12.13 on two elements) and removes the decoder's "last block's
|
||||
time is unreadable" special case, but does **not** explain the missing fade-in
|
||||
or the lateness, and the `_eff` elements cannot discriminate between the two
|
||||
readings. **Not adopted; the decoder is unchanged.** Take the interpolation law
|
||||
as settled and treat a multi-keyframe group's absolute timing as unverified.
|
||||
🔵 **Followed up, and the candidate is now strongly favoured — but not
|
||||
adopted.** That `+36` holds the *next* pose's time is supported by a
|
||||
calibration-free measurement: the observed full-alpha **hold : fade-out** ratio
|
||||
is 83 : 13 frames = **6.38**, the shifted reading predicts **8.00**, and the
|
||||
current reading predicts **0.25** — off by **26×**. With the glow's 2
|
||||
units/frame fixed and nothing else free, the current reading says the logo
|
||||
holds full alpha for **2.0 frames**; the game holds it for **83**. The shift
|
||||
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.
|
||||
**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
|
||||
2-frame hold where the game holds 83.
|
||||
✅ **The seconds conversion is settled: `1 unit = 1/60 s`, a 30 Hz title.** The
|
||||
re-test this page used to name has been run — 300 submitted frames timed on the
|
||||
**idle** title (nothing loading) came out at **28.8 and 28.3 fps**, the same
|
||||
|
||||
@@ -372,3 +372,11 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the
|
||||
fix against A's own next landmark, then apply it to element B in the same
|
||||
frames with **nothing left to tune**. That is what turned "the shapes look
|
||||
different" into "still at 255 nine frames after it should read 32".
|
||||
* **Two measurements can both be right and still disagree, when a heuristic sits
|
||||
between them.** A change to the keyframe time association is favoured 26× by an
|
||||
animation measurement and rejected by a static-render comparison. Both are
|
||||
sound: `rest()` picks a pose by *longest dwell*, which is a heuristic layered on
|
||||
top of the times, so moving the times moves its tie-breaks. The mistake would
|
||||
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.
|
||||
|
||||
@@ -385,3 +385,9 @@ neighbourhood, not just the line.
|
||||
after its declared `a=32`, and its declared 80-frame fade-in is never drawn.
|
||||
The `_eff` glows do reproduce, exactly — so this is about the group timeline,
|
||||
not about the interpolation law. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md)
|
||||
* "the `_eff` elements' agreement is the whole case for the keyframe-time shift,
|
||||
so it stays a shape argument" → superseded. The **hold duration** is
|
||||
calibration-free and decides it: observed 83 frames of full alpha against a
|
||||
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)
|
||||
|
||||
@@ -240,3 +240,96 @@ I first reported the `_eff` glows as holding "a constant α ≈ 33" and read tha
|
||||
contradicting the declared 255 plateau. They do no such thing — they ramp 34 →
|
||||
255 in exact steps of 34. I had printed only the series' **minimum** and read it
|
||||
as its range. Withdrawn; the trap is in [METHOD.md](METHOD.md).
|
||||
|
||||
### ✅ Settled the next iteration: the hold duration decides it
|
||||
|
||||
The section above left the candidate unadopted because its whole case rested on
|
||||
one fade-out's *shape*. There is a much blunter measurement in the same capture,
|
||||
and it needs **no calibration at all** — just the ratio of two observed spans:
|
||||
|
||||
| | full-alpha hold : fade-out | ratio |
|
||||
|---|---|---|
|
||||
| **observed** | 83 frames : 13 frames | **6.38** |
|
||||
| as decoded | 4 units : 16 units | 0.25 — **off by 26×** |
|
||||
| `+36` = the NEXT pose's time | 160 units : 20 units | **8.00** |
|
||||
|
||||
With the glow's 2 units/frame fixed and nothing else free, the current reading
|
||||
predicts `palogo_gamearts` holds full alpha for **2.0 frames**. The capture holds
|
||||
it for **83**. The shifted reading predicts **80.0**.
|
||||
|
||||
That is no longer a shape argument, and it is not a fit — it is a factor of 26.
|
||||
|
||||
### A fifth argument, from the corpus's own behaviour
|
||||
|
||||
`Element::rest()` tries `rest_plateau()` first and falls back to picking the
|
||||
keyframe with the longest **dwell**. Run that fallback on `palogo_gamearts`:
|
||||
|
||||
| reading | dwell winner |
|
||||
|---|---|
|
||||
| as decoded | **`a=0`** (dwell 160) — a fully transparent pose, for a logo |
|
||||
| shifted | **`a=255`** (dwell 160) — the visible hold |
|
||||
|
||||
Under the current reading the plain dwell rule picks an invisible pose for a
|
||||
publisher logo, and only the `rest_plateau` special case rescues the render. That
|
||||
special case has been repeatedly troublesome — it is the one the port agent
|
||||
reported a bug in on 2026-08-28. Under the shifted reading the dwell rule is
|
||||
simply correct on its own.
|
||||
|
||||
### What the shift is, exactly
|
||||
|
||||
Blocks `0…n−1`; `W[k]` is the word at block `k`'s `+36`. `W[n−1]` lies outside the
|
||||
group, because a group owns `frames·40 − 4` bytes.
|
||||
|
||||
* **as decoded:** `kf[k].time = W[k]`; the last keyframe has no time, as a special
|
||||
case for the missing word.
|
||||
* **shifted:** `kf[k].time = W[k−1]`; the **first** keyframe has no time, and
|
||||
`kf[n−1]` takes `W[n−2]` — which exists. Nothing is missing and nothing is
|
||||
special-cased: `W[k]` is simply *the time at which pose `k+1` is reached*.
|
||||
|
||||
Gated by `SYLPHEED_KF_TIME_SHIFT=1`, default unchanged. See below for what
|
||||
adopting it would cost.
|
||||
|
||||
### 🔴 …and what it costs — which is why the default is UNCHANGED
|
||||
|
||||
Rendering every build of six UI paks under both readings, and byte-comparing:
|
||||
|
||||
| | result |
|
||||
|---|---|
|
||||
| builds rendered identically | 10 of 11 compared |
|
||||
| builds that changed | **`GP_TITLE` build 7** — 13.1 % of pixels |
|
||||
|
||||
Build 7 is the **Japanese twin of build 4**, and build 4 — the one verified
|
||||
against a live capture — is byte-identical under both readings. So the one build
|
||||
the shift moves is the one with no capture to adjudicate it. The available proxy
|
||||
says the shift makes it *worse*:
|
||||
|
||||
| build 7 rendered | corr vs verified build 4 | mean luminance |
|
||||
|---|---|---|
|
||||
| as decoded | 0.6206 | **70.94** (build 4: 71.41) |
|
||||
| shifted | 0.6201 | 76.32 |
|
||||
|
||||
Two language twins are the same artwork with different text, so their brightness
|
||||
should match closely. As decoded it matches to **0.5**; shifted it is **4.9**
|
||||
brighter. Correlation does not separate them (0.6206 vs 0.6201) — the luminance
|
||||
does.
|
||||
|
||||
### The honest position
|
||||
|
||||
These two results are about **different things**, and both are real:
|
||||
|
||||
* For **animation timing** — what the port actually needs to play a screen — the
|
||||
shifted reading is favoured by a factor of 26 on a calibration-free
|
||||
measurement, and the current reading predicts a 2-frame hold where the game
|
||||
holds 83.
|
||||
* For **resting-pose selection** — what `screen render` does — the current
|
||||
reading matches the EN/JP twin brightness and the shifted one does not.
|
||||
|
||||
They can both be true: `rest()`'s dwell fallback is a heuristic layered *on top
|
||||
of* the times, and moving the times moves its tie-breaks. Adopting the shift
|
||||
would mean revisiting that heuristic in the same change, and there is no capture
|
||||
of build 7 to verify the result against.
|
||||
|
||||
**So the default stays as decoded.** 🟡 The port should treat a multi-keyframe
|
||||
group's *timing* as unverified — and specifically should not expect a
|
||||
2-frame hold where the game holds 83 — while `rest()` and `screen render` remain
|
||||
as they are.
|
||||
|
||||
Reference in New Issue
Block a user