The port observed that the justification for 60 has changed three times while the number never moved, and that three routes sharing an upstream assumption are weaker than they look. Checked. They do share one. Route B needs "the guest presents 60x/s", which comes from the vblank histogram UNDER XENIA'S 60 HZ LIMITER. Route C needs "the vblank is 60 Hz", which is that limiter's cvar directly. Route D is a wall-clock duration that lands on 60 only because the vblank is 60 Hz and Canary roughly keeps up. All three reduce to one fact: the display refreshes 60 times per second on this emulator. I presented them as corroboration and that was wrong. What is actually established, by manipulation rather than agreement: units/second EQUALS the display refresh rate. Forcing 30 Hz gave 30.2 units/s; at 60 Hz it is 59.8/61.3. One factor changed, the output tracked it. It becomes "60 units/s" only via an external fact this corpus has not measured -- that an Xbox 360 outputs 60 Hz. That is a hardware specification and it is solid, but it belongs outside the measurement rather than laundered into a third agreeing route. The conditional form is more useful anyway: it says what to do on hardware that is not 60 Hz, which is exactly why the port's time-based clock at a fixed 60 units/s is the right construction rather than a coincidence. And it keeps the port's `authored` classification correct -- nothing here promotes it. Also sizes the clock-origin capture properly. FRAMES=9000 ran to completion still in the movie at 8999, so the cap really fired this time. Content hashes show 3967 distinct decoded frames against ADV.wmv's declared 4131 -- 96% complete -- at 1.79 presents per decoded frame, not the 2.00 I assumed, which is why the estimate was short. Needs ~294 more presents to clear the movie plus >=236 for the plate, so FRAMES=11000. Free on the way past: 3967 decoded frames over 7091 presents at ~52 presents/host-s is 29.1 movie frames/s against the disc's declared 30.000, a 3% match that ties the present rate to a disc fact rather than a clock. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
7.6 KiB
The clock rate follows the vblank rate — and that reconciles three pages that disagreed
Status: ✅ measured for the rate; 🟡 for the mechanism, with the discriminating test named and my own sample declared underpowered. 2026-09-01. Instrument: ⟨capture⟩ ×3, one at a forced frame rate.
The disagreement this resolves
Three pages held incompatible positions on how the UI clock advances:
| page | says |
|---|---|
h3-units-per-frame-measured.md |
2 units per guest frame (later self-weakened to 🟡) |
units-per-second-measured.md |
time-integrated at 56.8 units/s — "how many per frame is whatever that frame took" |
clock-is-frame-based-one-unit-per-present.md (mine) |
1 unit per present, from a forced-frame-rate test |
Each rests on real data. All three are explained by one mechanism.
The reconciliation
The clock advances one unit per VBLANK. Presents can be dropped; the clock does not care.
| observation | page | explained by |
|---|---|---|
steps +17, +51, +34, +34, +17, +17 — always multiples of 17 |
units-per-second |
1, 3, 2, 2, 1, 1 vblanks between two logged presents |
| the same animation spans 21 labels in one capture and 33 in another | units-per-second |
different drop rates. A strict per-present clock cannot do this; a per-vblank clock must |
| three consecutive plate steps of exactly 23 (2 units, T=22) | h3 |
a run dropping every other present |
| 14 consecutive steps of exactly 17 | mine | a run dropping almost none |
splash dwell 4.26 s at a 60 Hz vblank, 8.45 s at --framerate_limit=30 |
mine | the vblank rate sets the unit rate: 59.9 and 30.2 units/s |
📌 My "1 unit per present" was nearly right and named the wrong clock. At
--framerate_limit=30 Xenia vblanks at 30 Hz and the guest presents ~30 times a
second, so presents and vblanks coincide and the test could not tell them apart.
It correctly refuted time-integration; it could not locate the tick.
What is solid, and what is not
✅ Solid — the rate follows the vblank rate. Two forced conditions:
| vblank | splash dwell, 255 declared units | ⇒ units/s |
|---|---|---|
| 60 Hz (default) | 4.263 / 4.162 s | 59.8 / 61.3 |
30 Hz (--framerate_limit=30) |
8.450 s | 30.2 |
A time-integrated clock predicts 4.25 s in both. It doubled. So the rate is set by the vblank, and on a console — which vblanks at 60 Hz — that is 60 units/s. The port's value stands, and this is the third independent route to it.
🟡 Not solid — per-vblank vs per-present, because my own step samples cannot separate them:
hashcap 52.2 presents/s +17 x13 (100%)
boot2 55.1 presents/s +17 x9 (82%) +34 x2 (18%)
fr30 28.7 presents/s +17 x11 (92%) +34 x1 ( 8%)
Twelve-odd steps per run is far too few. At 52–55 presents/s under a 60 Hz vblank a per-vblank clock predicts ~10 % of intervals spanning two vblanks; boot2's 18 % and hashcap's 0 % straddle that, and both are two events either way. I am not claiming the distribution supports the model — it is consistent with it and powerless against the alternative.
⚠️ The per-vblank model is favoured by the corpus's data, not mine. The
21-vs-33 label observation is the load-bearing one, and it is
units-per-second-measured.md's, not this page's. A strict per-present clock
makes that impossible.
The discriminating experiment, not run
Log the vblank counter beside each present in the draw logger — Xenia
increments D1MODE_V_COUNTER in MarkVblank() — and read the step against
vblanks elapsed rather than presents elapsed. If every step is exactly
17 × (vblanks since last present), the mechanism is decoded rather than inferred.
That is a small addition to tools/canary-patches/, and it would settle a
question three pages have now circled.
What this changes for the port
Nothing. 60 units/s stands, now reached three ways. What changes is the reason: not "2 units per frame", not "time-integrated at 56.8", but "one unit per 60 Hz vblank, and a console vblanks at 60".
Reach
⟨capture⟩ ×3, one machine, English locale, at two vblank rates. The rate result is a manipulation; the mechanism is a synthesis of other pages' observations with mine, and is 🟡 until the vblank counter is logged.
🔴 Dependency audit — the port is right that three routes are not three witnesses
The port observed that the justification for 60 has changed three times while the
number never moved, and that "three routes to the same number is weaker evidence
than it looks if they share an upstream assumption." Checked, and they do share
one.
| route | needs | where that comes from |
|---|---|---|
| A — 2 units/frame × 30 fps | retired | — |
| B — 1 unit/present × 60 presents/s | "the guest presents 60×/s" | the vblank histogram's mass at one vblank, under Xenia's 60 Hz limiter |
| C — 1 unit/vblank × 60 Hz vblank | "the vblank is 60 Hz" | Xenia's limiter cvar, directly |
| D — 255 declared units / 4.26 s | a wall-clock duration | a duration off Canary, which only lands on 60 because its vblank is 60 Hz and it roughly keeps up |
B, C and D all reduce to the same upstream fact: the display refreshes 60 times per second on this emulator. They are one witness wearing three coats, and I presented them as corroboration. That was wrong and the port caught it.
What is actually established, stated so it does not overclaim
units/second= the display refresh rate. Measured by manipulation, not by agreement: forcing the refresh to 30 Hz gave 30.2 units/s; at 60 Hz it is 59.8 / 61.3. Two conditions, one factor changed, the output tracked it.
That is the finding, and it is conditional rather than absolute. It becomes "60 units/s" only via an external fact this corpus has not measured: an Xbox 360 outputs 60 Hz. That fact is solid — it is a hardware specification, not an inference — but it belongs outside the measurement and should be cited as such, not laundered into a third agreeing route.
📌 The conditional form is the more useful one anyway. It says what to do on hardware that is not 60 Hz, which the absolute form cannot, and it is exactly why the port's time-based clock at a fixed 60 units/s is the right construction rather than a coincidence.
⚠️ And it keeps the port's authored classification correct. Nothing here
promotes it. A value whose reason has changed three times, resting on one upstream
fact plus a specification, is not measured in this corpus's sense.
The clock-origin capture, sized properly at last
FRAMES=9000 ran to completion and was still in the movie at frame 8999 — this
time the frame cap really did fire. The diagnosis, from the content hashes:
movie luma draws 7091 (frames 426..8999)
DISTINCT content frames decoded 3967
ADV.wmv declares 4131 ⟨disc⟩
-> the movie was 96 % complete
presents per decoded frame 1.79 (not 2.00 -- drops)
My 1.79, not 2.0, is why the estimate was short. Remaining: ~164 decoded frames
× 1.79 ≈ 294 presents to clear the movie, then ≥236 more for the plate to
reach t = 236. So FRAMES = 11000 with the same 1200 s timeout, which has
margin over the ~9 600 minimum.
📌 Free corroboration on the way past: 3967 decoded frames over 7091 presents at ~52 presents/host-s is 29.1 movie frames/s against the disc's declared 30.000. A 3 % match, and it ties the present rate to a disc fact rather than to a clock.