sylpheed-port warned that wall clock on their box carries a +6.7 % decode deficit and that cross-agent timing comparisons must go through media length. It reaches this page's headline: a guest at ~92 % of real time and a game presenting at 27.6 fps produce identical measurements, and the three trials share the container, so they are three samples of one confound rather than three confirmations. Same shape as the plate-pulse phase lock. The 8.5 % splash-dwell excess is the same number from the other side. The port is not exposed -- it authors declared units and reads seconds from nothing here. And the BGM_103 loop figures cannot audit it: that page states both derivations are wall-clock, converting no bits to seconds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
469 lines
24 KiB
Markdown
469 lines
24 KiB
Markdown
# What a keyframe time is worth, and what shape the ramp has
|
||
|
||
> ## ✅ 2026-08-29 — the argument on this page about WHICH BLOCK OWNS A TIME is over
|
||
>
|
||
> It was never a choice between two readings. A placement group is
|
||
> `frames` records of `{u32 time; 36-byte pose}` after an 8-byte header, so the
|
||
> time word **precedes** its pose; the group's "lead-in word" is pose 0's time,
|
||
> and **no** time is missing. `SYLPHEED_KF_TIME_SHIFT` had the association right
|
||
> and pose 0 untimed, which is the only reason it looked like it cost build 7
|
||
> 13.1 % of its pixels. Decoded disc-wide, with controls, in
|
||
> [`ui-keyframe-record-layout.md`](ui-keyframe-record-layout.md); the gate is now
|
||
> `SYLPHEED_KF_TIME_LEGACY=1`.
|
||
>
|
||
> **Everything else on this page stands** — the ramp is linear, the clock advances
|
||
> 2 units per submitted frame, and `1 unit = 1/60 s` is measured. Read the
|
||
> sections below with that correction applied: where a table pairs a time with a
|
||
> pose, the pairing is the corrected one.
|
||
|
||
**Status:** ✅ `CONFIRMED` for the two things the port is blocked on — the ramp is
|
||
**linear**, and the animation clock advances **2 keyframe time units per frame the
|
||
game submits**. 🟡 the conversion to *seconds* rests on one further step: the game
|
||
was measured presenting **27.6 frames/second**, which reads as a 30 Hz title
|
||
running at ~92 % under the emulator, giving **1 unit = 1/60 s**. That last step is
|
||
reasoning over a measurement, not a measurement — see *the reach of the negative*
|
||
below.
|
||
|
||
This answers `docs/port/MISSION.md` **Q1**. Everything animated downstream — the
|
||
title wordmark zoom, the main menu's staggered buttons, every fade — is scaled by
|
||
this number.
|
||
|
||
## The measurement
|
||
|
||
The question cannot be asked of a screen that is sitting still, and
|
||
`log_ui_draws` armed *at* a screen only ever sees the steady state. So arm it
|
||
repeatedly through the boot and keep every log: each F10 opens a new numbered
|
||
file and **closes** the previous one, which stays on disk complete. Re-arming
|
||
every 3 s tiles the whole approach to a screen, and whichever file straddles the
|
||
build contains it. `tools/re-capture/screen_build_capture.sh` does this; it sends
|
||
no pad input at all, because Ⓐ during the boot has ended a run on a permanent
|
||
black screen (`canary-scripted-input-traps.md`).
|
||
|
||
The developer-logo splash is the cheap target: it is the first thing the guest
|
||
draws, roughly ten seconds in, and its bundle (`GP_TITLE.pak`, `--all --build 11`
|
||
/ `14` — the `palogo` group) declares short, unambiguous ramps.
|
||
|
||
The capture is committed: [`captures/ui-timing/splash-build-draws.log`](captures/ui-timing/splash-build-draws.log)
|
||
(the raw draw stream) and [`captures/ui-timing/splash-build-quads.csv`](captures/ui-timing/splash-build-quads.csv)
|
||
(`tools/re-capture/kf_time_probe.py`, one row per quad per frame: pixel rect and
|
||
the per-vertex colour whose high byte is the element's fade).
|
||
|
||
Frame numbers are the emulator's **VdSwap count** — frames the guest submitted —
|
||
so an emulator running at 80 % of real time does not move them. That is the whole
|
||
reason to measure in this unit rather than with a stopwatch.
|
||
|
||
### The quads are the splash, by position and size
|
||
|
||
Every sprite in the capture lands on its declared placement:
|
||
|
||
⚠️ The rows span **two** bundles, not one: `palogo_sqex` is in `--all --build 10`
|
||
and the `gamearts`/`seta` group is in `--all --build 11`. This table said
|
||
"build 11" over all five until a corpus-wide index audit
|
||
([`structures/build-ordinal-vs-entry.md`](structures/build-ordinal-vs-entry.md))
|
||
checked the header against the bundle. Every placement below re-verified and
|
||
correct; only the label was wrong.
|
||
|
||
| capture quad | declared element | in bundle | declared placement |
|
||
|---|---|---|---|
|
||
| `666x65 @ (307,331)` | `palogo_sqex.t32` 666×68 | `--all --build 10` | (309,330) |
|
||
| `525x90 @ (378,155)` | `palogo_gamearts_eff.t32` 521×91 | `--all --build 11` | (379,154) |
|
||
| `262x108 @ (512,306)` | `palogo_seta_eff.t32` 261×110 | `--all --build 11` | (511,305) |
|
||
| `499x72 @ (390,162)` | `palogo_gamearts.t32` 500×71 | `--all --build 11` | (390,164) |
|
||
| `243x86 @ (518,317)` | `palogo_seta.t32` 240×89 | `--all --build 11` | (521,316) |
|
||
|
||
(A quad runs a few pixels under its sprite; that offset is already recorded in
|
||
`ui-title-paint-order-capture.md` and is not what is being measured here.)
|
||
|
||
## Result 1 — the ramp is linear, exactly
|
||
|
||
`palogo_gamearts_eff` and `palogo_seta_eff` declare a fade-in of
|
||
`t=15 a=0 → t=30 a=255`: a **15-unit** ramp. Their fade alpha, frame by frame,
|
||
straight out of the capture:
|
||
|
||
| frame | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 |
|
||
|---|---|---|---|---|---|---|---|---|
|
||
| alpha | `0x22` 34 | `0x44` 68 | `0x66` 102 | `0x88` 136 | `0xAA` 170 | `0xCC` 204 | `0xEE` 238 | `0xFF` 255 |
|
||
| ⇒ units into the ramp *k* | 2 | 4 | 6 | 8 | 10 | 12 | 14 | ≥15 |
|
||
|
||
`round(255·k/15) = 17k` reproduces **all seven samples with zero error**. An eased
|
||
ramp cannot do that: any ease-in/ease-out would bend the first and last steps, and
|
||
these are a constant 34 throughout. **Linear interpolation, refuted-nothing-left.**
|
||
|
||
The same element's fade-*out* gives the same law with a one-count offset —
|
||
`255·(1−k/15) − 1` = 254, 220, 186, 152, 118, 84, 67, 33 for
|
||
*k* = 0, 2, 4, 6, 8, 10, 11, 13, which is exactly what the capture holds on frames
|
||
108–115.
|
||
|
||
## Result 2 — 2 time units per submitted frame
|
||
|
||
Read *k* off the row above: 2, 4, 6, 8, 10, 12, 14 on seven consecutive submitted
|
||
frames. **The clock advances 2.000 units per frame, over six consecutive
|
||
intervals, with no residual.** The fade-out shows the same 2-per-frame step with
|
||
one single-unit frame (*k* goes 10 → 11 → 13), so the quantum underneath is 1 unit
|
||
and the normal step is two of them.
|
||
|
||
Cross-checks in the same capture, all consistent:
|
||
|
||
* the declared hold `t=30 → t=45` (15 units) is held for frames 101–107, **7
|
||
frames** ≈ 14 units;
|
||
* `palogo_sqex`'s fade-out steps in multiples of 17 per frame (−34 with the odd
|
||
−17), the same 255/15 quantum;
|
||
* the `gamearts`/`seta` pair holds full alpha for frames 116–198 and fades out
|
||
over frames 199–211.
|
||
|
||
## Result 3 — the conversion to seconds, and how far it reaches
|
||
|
||
300 submitted frames took **10.87 s** wall-clock, measured by arming one bounded
|
||
capture over the splash and timing it to its own `[UI-CAP] done` line:
|
||
**27.6 frames/second**.
|
||
|
||
With 2 units per frame that is **55 units/second measured**. The two readings that
|
||
fit are:
|
||
|
||
* **30 Hz present, 60-unit-per-second timeline** — the emulator running at 92 % of
|
||
real time. A 30 Hz renderer stepping a 60 Hz timeline is exactly the 2-units-
|
||
per-frame quantum that was measured, and 92 % is an ordinary number for this
|
||
container.
|
||
* 60 Hz present at 46 % of real time, giving a 120-unit-per-second timeline. This
|
||
requires the emulator to be running at *less than half speed* while drawing four
|
||
quads over a black screen, and it requires the game's timeline to tick at 120 Hz.
|
||
|
||
**Taking the first**: `1 unit = 1/60 s`. The title build (`GP_TITLE.pak` build 4,
|
||
t = 16 … 269) is then **4.2 s**, and the main menu build (build 5, t = 12 … 80)
|
||
**1.1 s**.
|
||
|
||
**The reach of this negative** *(as written before the test below — kept for the
|
||
reasoning)*: the present rate was measured *during the boot splash*, where the
|
||
guest is also streaming from the ISO, so it is a lower bound on the emulator's
|
||
speed and cannot by itself exclude the 60 Hz reading. What would settle it is the
|
||
same 300-frame timing taken on the **idle title screen**, where nothing is
|
||
loading — if that also comes out near 28 fps the game is 30 Hz and the unit is
|
||
1/60 s; if it doubles to ~55 fps the game is 60 Hz and every duration on this page
|
||
halves.
|
||
|
||
## ✅ That test was run — 2026-08-28. The game is 30 Hz; `1 unit = 1/60 s`
|
||
|
||
Reached the title with a single Ⓐ to skip the intro
|
||
([`movie-binding.md`](movie-binding.md)), let it settle 12 s so nothing was
|
||
loading, then armed the bounded 300-frame capture and timed it to its own
|
||
`[UI-CAP] done` line:
|
||
|
||
| | 300 frames in | rate |
|
||
|---|---|---|
|
||
| idle title, trial 1 | 10.40 s | **28.8 fps** |
|
||
| idle title, trial 2 | *arm produced no capture* | — |
|
||
| idle title, trial 3 | 10.60 s | **28.3 fps** |
|
||
| *(prior)* boot splash | 10.87 s | 27.6 fps |
|
||
|
||
**~28.5 fps on an idle title — the same rate as the loading splash.** By this
|
||
page's own criterion that settles it: the title presents at **30 Hz**, the
|
||
timeline ticks at **60 units/second**, and **`1 unit = 1/60 s`**.
|
||
|
||
**The 60 Hz reading is now excluded, not merely disfavoured.** It would require
|
||
the emulator to be running at 47 % of real time *while sitting idle on a static
|
||
title* — and the capture says that screen costs **1 526 draws over 300 frames,
|
||
about 5 draws per frame**. Nothing there is expensive enough to halve the
|
||
emulator's speed, and the splash and the idle title returning the same rate is
|
||
exactly what a constant ~95 %-of-real-time emulator looks like.
|
||
|
||
So the durations on this page stand as written: title build 4 ≈ **4.2 s**, main
|
||
menu build 5 ≈ **1.1 s**, and `EXTRAS` build 6's declared fade-in ≈ **0.87 s**.
|
||
|
||
⚠️ Still **measured, not decoded** — no field on the disc says "sixtieths of a
|
||
second". What changed is that the measurement now has an idle-state control and
|
||
the competing reading is ruled out.
|
||
|
||
## What this does not say
|
||
|
||
* Nothing here is a *decode*: no field on the disc says "sixtieths of a second".
|
||
The number is measured from the running game, and the port is transcribing a
|
||
measurement, not a disc value.
|
||
* The splash drew `palogo_sqex`, then `palogo_gamearts` + `palogo_seta` with their
|
||
`_eff` sprites — but **not** `palogo_anima` (declared at 388×136 @ (446,449)) and
|
||
not `palogo_sqex_eff`, in the ~7 s of capture after the pair faded out. The
|
||
`_sqex_eff` absence is explainable (the capture joined the SQUARE ENIX logo
|
||
mid-hold); the missing studio-anima logo is not, and is left as an open
|
||
observation for Q2 rather than a claim.
|
||
|
||
---
|
||
|
||
## 🟡 Open: the `palogo_*` LOGO elements do not play their declared timeline
|
||
|
||
**Added 2026-08-28.** Everything above stands — it rests on the `_eff` glows, and
|
||
they reproduce exactly. What follows is a **different element in the same bundle
|
||
and the same capture**, and it does not.
|
||
|
||
Reproduce with
|
||
[`tools/re-capture/splash_ramp_check.py`](../../tools/re-capture/splash_ramp_check.py);
|
||
output committed at [`data/splash-ramp-check.txt`](data/splash-ramp-check.txt).
|
||
|
||
### Why this is a test and not a fit
|
||
|
||
The clock is calibrated on `palogo_gamearts_eff` — its declared 15-unit fade-in
|
||
`0@15 → 255@30` against its captured alphas 34, 68, 102, 136, 170, 204, 238, a
|
||
constant step of exactly 34. That gives `t = 2f − 171`, and the calibration
|
||
checks itself: the glow's declared hold ends at `t=45`, predicted **frame 108.0**,
|
||
and the observed last full-alpha frame is **107**.
|
||
|
||
That calibration is then applied to `palogo_gamearts` — same bundle, same frames,
|
||
**no free parameter left**:
|
||
|
||
| declared | predicted frame | observed alpha |
|
||
|---|---|---|
|
||
| `a=0` at `t=15` | 93.0 | not drawn |
|
||
| `a=0` at `t=30` | 100.5 | not drawn |
|
||
| `a=255` at `t=190` | 180.5 | 255 |
|
||
| `a=232` at `t=206` | 188.5 | **255** |
|
||
| `a=32` at `t=210` | 190.5 | **255** |
|
||
|
||
The logo is still at full alpha nine frames after it should have been at `a=32`.
|
||
Its fade-out actually runs frames **199–211**, some 17 frames late, and its
|
||
declared 80-frame fade-in (`t=30→190`) is **not drawn at all** — the element's
|
||
first appearance, frame 116, is already at 255.
|
||
|
||
⚠️ "Not drawn" is not a culling artefact: the same element is submitted all the
|
||
way down to `a=7` on the way out, so low-alpha quads plainly do reach the GPU.
|
||
|
||
### The shape mismatch, which needs no calibration at all
|
||
|
||
The declared fade-out spends **12 of its 16 units** dropping only **23/255** of
|
||
the alpha — a near-flat leg — then **200/255** in the remaining 4. The captured
|
||
per-frame drops are `1, 11, 6, 22, 34, 33, 17, 33, 33, 17, 25, 8, 8`. There is no
|
||
near-flat leg.
|
||
|
||
### 🟡 A candidate, offered as one and NOT adopted
|
||
|
||
If the word at `+36` were the time of the **next** keyframe rather than of its own
|
||
block, the logo's fade-out would read `255@190 → 232@194 → 32@206 → 0@210`:
|
||
slow, fast, slow — which is the captured shape. Fitting both readings to the
|
||
captured fade-out gives RMS alpha error **4.05** shifted against **12.13** as
|
||
decoded, on two elements independently (`gamearts` and `sqex`).
|
||
|
||
It also removes a special case. The decoder currently notes that a group's data
|
||
"stops 4 bytes short of its final block's time slot", so the last block's time is
|
||
unreadable. Under the shifted reading the last block simply *has no successor*,
|
||
so it has no time word — the same bytes, no special case.
|
||
|
||
**Not adopted, for three reasons:**
|
||
|
||
1. It does not explain the missing fade-in, which is the larger anomaly.
|
||
2. It does not fix the 17-frame lateness of the fade-out.
|
||
3. The `_eff` elements **do not discriminate** between the two readings — I
|
||
checked: with four blocks the shift merely relabels which phase is which, and
|
||
both reproduce the observed fade-in / hold / fade-out. So the entire case for
|
||
the shift rests on one element's fade-out shape.
|
||
|
||
**The decoder is unchanged.** 🟡 What a port should take from this: the
|
||
interpolation *law* is settled (linear, 2 units per frame), and the *group
|
||
timeline* for multi-keyframe elements is not.
|
||
|
||
### 🔴 One thing I got wrong in the course of this
|
||
|
||
I first reported the `_eff` glows as holding "a constant α ≈ 33" and read that as
|
||
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*.
|
||
|
||
🔴 **This sentence used to read "Gated by `SYLPHEED_KF_TIME_SHIFT=1`, default
|
||
unchanged."** That gate **no longer exists** — it was removed with the
|
||
[record-layout fix](ui-keyframe-record-layout.md) and appears **nowhere in
|
||
`crates/`**. ⚠️ A reader following the old instruction sets an environment
|
||
variable that does nothing, gets default behaviour, and concludes the two readings
|
||
agree: **a stale instruction that no-ops manufactures a false confirmation**,
|
||
which is worse than a stale description. 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.
|
||
|
||
---
|
||
|
||
## ✅ Replicated: three elements, two screens — evidence that SUPPORTED the shifted reading
|
||
|
||
⚠️ **This heading used to end "and the shifted reading wins every time".** Demoted
|
||
because the shifted reading was itself superseded: the
|
||
[record-layout fix](ui-keyframe-record-layout.md) established the same association
|
||
by a better route and timed pose 0 as well, which the shifted reading never did.
|
||
The evidence below stands; it is now evidence for the *corrected* reading, and the
|
||
gate it was collected behind no longer exists.
|
||
|
||
**2026-08-29.** The case for reading `+36` as *"the time the NEXT pose is
|
||
reached"* rested on one element's fade-out shape, then on one element's hold
|
||
duration. Both splash halves supply more, and they all say the same thing.
|
||
|
||
Phase durations in frames (2 units/frame, from the `_eff` glow ramp):
|
||
|
||
| element | screen | observed hold at `a=255` | as decoded | shifted |
|
||
|---|---|---|---|---|
|
||
| `palogo_gamearts` | developer splash | **83 f** | 8 f | **80 f** |
|
||
| `palogo_seta` | developer splash | **83 f** | 6 f | **80 f** |
|
||
| `palogo_sqex` | publisher splash | **≥ 77 f** * | 6 f | **102 f** |
|
||
|
||
\* the capture opens mid-hold at frame 1, so 77 is a floor, not the length.
|
||
|
||
The two readings predict **opposite structures** for these elements. On
|
||
`palogo_gamearts`, as decoded: `hold 8f, in 80f, hold 2f, out 6f, out 2f` —
|
||
an eighty-frame *fade-in* and a two-frame hold. Shifted: `in 8f, hold 80f,
|
||
out 2f, out 6f, out 2f` — an eight-frame fade-in and an eighty-frame *hold*.
|
||
The capture shows a **83-frame hold** and no fade-in at all.
|
||
|
||
### And the elements that cannot discriminate are not contradicted
|
||
|
||
`palogo_gamearts_eff` observed `in 7f, hold 7f, out 8f`. As decoded it reads
|
||
`in 8f, hold 8f`; shifted it reads `hold 8f, out 8f`. Both fit — with only four
|
||
blocks the shift relabels which phase is which without changing any duration. So
|
||
the glows, which are what Q1's linear law was measured on, **do not argue against
|
||
the shift**; they simply say nothing about it.
|
||
|
||
### 🟡 Why the decoder's default is still unchanged
|
||
|
||
The one thing that opposes the shift is `Element::rest()` on `ptlogo_eff3.t32`,
|
||
where the shifted reading makes the longest-dwell fallback return the bloom's
|
||
200 % peak instead of an invisible frame. But that fallback is **unsound whenever
|
||
it runs** ([resting pose](structures/ui-resting-pose.md)) — it returns an
|
||
endpoint of a movement, and neither endpoint is held. Checked: the shift does not
|
||
fix it either, so this is not a case of two readings disagreeing about the times.
|
||
It is a heuristic guessing, in both.
|
||
|
||
**So the two questions are separable, and only one of them has evidence.** The
|
||
times govern animation; `rest()` picks a static pose through a rule that consults
|
||
them only after its own precondition has failed.
|
||
|
||
### ⚠️ What a port should do
|
||
|
||
For **animation timing**, read `+36` as the *next* pose's time: three elements
|
||
across two screens, each off by an order of magnitude under the other reading.
|
||
For **static composites**, nothing changes — `screen render` is unaffected, and
|
||
the five screens' correlations stand ([acceptance](five-screens-acceptance.md)).
|
||
|
||
🟡 Classified **measured, not decoded**: this is three elements in one game
|
||
screen family, not a disc-wide field check, and our own decoder still defaults to
|
||
the other reading behind `SYLPHEED_KF_TIME_SHIFT=1` — ⚠️ **a gate since removed;
|
||
there is no way to select the old reading today, and nothing to set.**
|
||
|
||
## ⚠️ 2026-08-30 — "the game presents at 27.6 fps" is not separated from "my container runs the guest slow"
|
||
|
||
`sylpheed-port` warned that their boot timings carry a host deficit — 146.6 s of
|
||
wall clock for 137.44 s of media, **+6.7 %**, because 720p Theora decodes below
|
||
real time on a box with no GPU — and that any comparison between their seconds and
|
||
mine must go through **media length, not wall clock**. Checking whether that
|
||
reaches this page: **it does, and to the page's own headline.**
|
||
|
||
Every rate here is a frame count over a wall-clock window on one container:
|
||
|
||
| | window | rate |
|
||
|---|---|---|
|
||
| idle title, trials 1–3 | 10.40 / — / 10.60 s | 28.8 / 28.3 fps |
|
||
| boot splash | 10.87 s | 27.6 fps |
|
||
|
||
🔴 **A guest running at ~92 % of real time produces exactly these numbers**, and so
|
||
does a game genuinely presenting at 27.6 fps. The two hypotheses are
|
||
**indistinguishable by any measurement on this page**, and the three trials do not
|
||
separate them — they share the container, so they are three samples of one
|
||
confound, not three independent confirmations. This is the same shape as the
|
||
plate-pulse phase lock: agreement across runs that measures the instrument.
|
||
|
||
The same applies to the **8.5 %** splash-dwell excess in
|
||
[`boot-order-and-splash-dwell.md`](boot-order-and-splash-dwell.md) — `1.085` and
|
||
`1/0.92` are the same number arriving from the two readings.
|
||
|
||
✅ **The port is not exposed.** It authors **declared units** — 240 and 195 — and
|
||
reads seconds from nothing here. That row is still `decoded`, and its evidence is
|
||
the disc, not the clock.
|
||
|
||
❌ **What will not settle it, and I checked:** the `BGM_103` loop figures. Both the
|
||
`9.44 s` start and the `61.87 s` cycle are wall-clock derived — that page states it
|
||
outright, *"two derivations, neither converting bits to seconds"* — so they carry
|
||
the same container pacing and cannot audit it.
|
||
|
||
**What would settle it** is a media-length reference: play an asset whose duration
|
||
is fixed by its own data — a movie, or a wave with a declared byte rate — and
|
||
compare its wall clock against its media length in *this* container. Not run.
|