1/60 s, and the 60 Hz reading is excluded ui-keyframe-time-unit.md named its own re-test and nobody had run it: time 300 submitted frames on the IDLE title, where nothing is streaming from the ISO. Reaching the title is cheap now that one A skips the intro, so this cost one boot. Two clean trials: 300 frames in 10.40 s and 10.60 s, i.e. 28.8 and 28.3 fps. That is the same rate as the 27.6 fps measured during the loading splash, which is exactly the page's own criterion for "the game is 30 Hz and the unit is 1/60 s". The 60 Hz alternative is now excluded rather than disfavoured: it needs the emulator at 47% of real time while sitting idle on a screen the capture says costs 1526 draws over 300 frames, about five per frame. Nothing there halves an emulator. So the durations stand as written -- title 4.2 s, main menu 1.1 s, EXTRAS 0.87 s -- and Q1 leaves the handoff's open-residue table. It stays MEASURED, not decoded: no field on the disc says sixtieths of a second. One trap, and it nearly cost the conclusion. My first attempt reported 39.5 fps, which sits between the two hypotheses and would have left the question open. It was a harness bug: I polled the log for a "done" line rather than for the COUNT of them increasing, so it matched a capture that had already completed and timed 0.1 s of nothing. METHOD gets the general form -- a log-polling probe must compare a count, not test for presence.
164 lines
8.3 KiB
Markdown
164 lines
8.3 KiB
Markdown
# What a keyframe time is worth, and what shape the ramp has
|
||
|
||
**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:
|
||
|
||
| capture quad | declared element (build 11) | declared placement |
|
||
|---|---|---|
|
||
| `666x65 @ (307,331)` | `palogo_sqex.t32` 666×68 | (309,330) |
|
||
| `525x90 @ (378,155)` | `palogo_gamearts_eff.t32` 521×91 | (379,154) |
|
||
| `262x108 @ (512,306)` | `palogo_seta_eff.t32` 261×110 | (511,305) |
|
||
| `499x72 @ (390,162)` | `palogo_gamearts.t32` 500×71 | (390,164) |
|
||
| `243x86 @ (518,317)` | `palogo_seta.t32` 240×89 | (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.
|