Files
Sylpheed/docs/re/clock-rate-follows-the-vblank.md
sylph-decoder 33a6b4ea6f re: the three routes to 60 are one witness in three coats -- audit, and the capture sized
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
2026-09-02 15:08:55 +00:00

7.6 KiB
Raw Blame History

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 5255 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.