re: a keyframe time is worth half a rendered frame, and the ramp is linear
Q1 of the menu port, measured against the running game rather than reasoned
about. The developer-logo splash is the cheap target: it is the first thing the
guest draws and its bundle declares short, unambiguous ramps.
Two results, both frame-exact and both emulator-speed-independent (frame numbers
are VdSwap counts, the guest's own frames):
* the ramp is LINEAR. A declared 15-unit fade lands on round(255*k/15) for all
seven of its samples with zero error, k stepping 2,4,6,8,10,12,14. No ease
can reproduce a constant step of 34 at both ends.
* the animation clock advances 2.000 time units per submitted frame, over six
consecutive intervals with no residual, with 1 unit as the quantum
underneath (one frame in the fade-out advances by 1).
The conversion to seconds is one step further and is flagged as such: 300 frames
took 10.87 s = 27.6 present-frames/second, which reads as a 30 Hz title at 92 %
under the emulator and gives 1 unit = 1/60 s -- the title build 4.2 s, the main
menu build 1.1 s. That reading is not proven, because the rate was measured
while the guest was still streaming from the ISO; the page names the one test
that would settle it and says what changes if it goes the other way.
Committed beside it: the raw draw capture and the per-frame quad CSV, so the
numbers can be re-derived without a disc or an emulator.
This commit is contained in:
131
docs/re/ui-keyframe-time-unit.md
Normal file
131
docs/re/ui-keyframe-time-unit.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# 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:** 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. Until then the port should treat **2 units per rendered frame**
|
||||
as the measured fact and `1/60 s` as the working conversion.
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user