The port put two of my pages against each other: settle->plate 2.135 s and one
focus-ring revolution 2.177 s, both a declared 120 units during a static hold,
2% apart against a 6 ms run-to-run agreement. Fair challenge.
The resolution is that the question assumes a stable wall clock. Same interval,
same container, same day: 2.138, 2.132, and 2.549 s -- a 19% swing, caused by
adding --log_ui_draws=true. The 2% is a fifth of that. The two pages were never
in conflict about the game; they are three readings of one declared quantity
through a clock that moves. What settles the quantity is the disc.
Wall clock cannot separate the hypotheses, so I tried to measure frames instead.
Both instruments are recorded as failures rather than published as numbers:
* Canary's own [UI-CAP] counter -- the one that produced the corpus's 28.5 fps
-- costs a third of the frame rate. 300 frames in 16.567 s = 18.11 fps on a
screen that gives ~28 without it. That reclassifies 28.5 as a load-dependent
lower bound; it does not overturn it.
* A distinct-frame counter over the spinning ring FAILED its decisive control:
15.88 fps against the game's own 17.59 in the same window, 10% low, so the
ring does not change on every presented frame. Its static control also read
2.63 instead of ~0. Dead, not tuneable, per METHOD.md.
The rule that follows, and it applies to everything I hand the port: a measured
interval landing near a round number of declared units almost certainly IS that
number of units. Ship the units.
Also recovered here, because the same question needed it: the static PPC route.
Four tools open /work/xenia-rs/sylpheed.db and nothing in this repository builds
it -- no disassembler, no PPC decoder, and default.xex is encrypted (zero
plaintext "GamePart"). Xenia decompresses the image at load, so dump_image.py
reads it out of guest memory and validates it against the corpus's own landmarks:
the 29-entry GamePart id table at 0x820A1630 and the Xbox 360 D3D runtime
strings. String search and table dumps work again; instruction-level work does
not, and the present interval I wanted is an immediate, not a string.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014voBspJ6kFncNErZJuZcLw
116 lines
5.7 KiB
Markdown
116 lines
5.7 KiB
Markdown
# 🔴 I cannot measure this emulator's presentation rate — and the 2 % between two of my pages is not a disagreement about the game
|
||
|
||
**Status:** one 🔴 **instrument failure** (recorded, not published as a number),
|
||
and one ✅ **resolution of a challenge** that follows from it. 2026-08-29.
|
||
|
||
## The challenge
|
||
|
||
The port put two of my pages against each other. Both measure the same declared
|
||
quantity — **120 keyframe units of wall clock during a static hold, in Canary** —
|
||
and they differ by 2 %:
|
||
|
||
| page | measured | implied presentation |
|
||
|---|---|---|
|
||
| [`title-plate-delay-measured.md`](title-plate-delay-measured.md), settle→plate, 2 runs | 2.138, 2.132 → **2.135 s** | 28.10 fps |
|
||
| [`focus-ring-spin-measured.md`](focus-ring-spin-measured.md), 7 spacings | 2.16 … 2.20 → **2.177 s** | 27.56 fps |
|
||
|
||
0.042 s apart — seven times the 6 ms run-to-run agreement the plate page rests
|
||
on. Its own corroboration argument (*"the build-in is where frames are dropped;
|
||
the static hold is not"*) is aimed at exactly this, and these are two static
|
||
holds. Fair challenge.
|
||
|
||
## ✅ The resolution: 2 % is far inside this emulator's own variation
|
||
|
||
The question assumes the wall clock is stable enough for 2 % to mean something.
|
||
It is not, and the counterexample is the same interval, in the same container, on
|
||
the same day:
|
||
|
||
| run | conditions | settle → plate |
|
||
|---|---|---|
|
||
| 1 | 8 fps grab | **2.138 s** |
|
||
| 2 | 8 fps grab | **2.132 s** |
|
||
| **3** | 8 fps grab **+ `--log_ui_draws=true`** | **2.549 s** |
|
||
|
||
**A 19 % swing on the declared interval, from a logging flag.** The 2 % the two
|
||
pages differ by is a fifth of that. They were taken in different sessions under
|
||
different load, and nothing in either can separate "the game timed it
|
||
differently" from "the emulator ran slower" — because both are wall clock.
|
||
|
||
**So the two pages were never in conflict about the game.** They are three
|
||
readings of one declared quantity through a clock that moves. What settles the
|
||
quantity is the disc: `t=118 → t=238` is 120 units, and the port's own structural
|
||
rule for the ring (two keyframes differing only by a 360° rotation, first timed
|
||
and second untimed — 16/212 elements matched, all of them focus rings) gives the
|
||
ring's period the same way.
|
||
|
||
⚠️ **This removes the evidence that the ring is not 120 units. It does not prove
|
||
that it is.** The disc-side rule does that, and it is the port's, not mine.
|
||
|
||
## 🔴 The instrument I built to answer it properly, and why it is dead
|
||
|
||
Wall clock cannot separate the two hypotheses; **frames** can. So I tried to
|
||
measure the presentation rate.
|
||
|
||
### Canary's own frame counter perturbs by a third
|
||
|
||
`--log_ui_draws=true --ui_draw_capture_frames=N` logs `[UI-CAP] capture armed`
|
||
and `[UI-CAP] done: D draws over F frames`. This is the instrument that produced
|
||
the corpus's **28.5 fps** ([`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md)).
|
||
|
||
Measured here, armed on the title with a concurrent 8 fps grab:
|
||
|
||
> **300 frames in 16.567 s = 18.11 fps**, against ~28 for the same screen without
|
||
> it.
|
||
|
||
⚠️ **A frame counter that costs a third of the frame rate cannot measure the
|
||
frame rate.** This does not overturn the 28.5 fps — that run had no concurrent
|
||
grab — but it does mean the figure is a **lower bound taken under its own
|
||
instrument's load**, and it should not be treated as *the* rate.
|
||
|
||
### And the unperturbing replacement FAILED its own decisive control
|
||
|
||
The alternative: count **distinct frames** in an oversampled crop of something
|
||
that moves every frame (the spinning focus ring). At 60 fps against a source
|
||
presenting at R, the fraction of consecutive samples that differ is R/60.
|
||
|
||
Three controls were written before the run. It failed the one that matters:
|
||
|
||
| control | result |
|
||
|---|---|
|
||
| a static crop must read ≈ 0 | **2.63 fps of "change"** — not clean |
|
||
| two sampling rates must agree | 45 fps → **12.73**, 60 fps → **12.08** ✅ |
|
||
| **must agree with the game's own counter while both run** | counter **15.88** vs `[UI-CAP]` **17.59** — **10 % low** 🔴 |
|
||
|
||
The third is decisive and it is a failure: **the ring does not change on every
|
||
presented frame**, so the counter measures the ring's animation rate, not the
|
||
presentation rate. It also drifted 12.1 → 15.9 → 18.1 across one session, which
|
||
a real rate estimator on a settled screen should not do.
|
||
|
||
**Dead, not tuneable** — per [`METHOD.md`](METHOD.md). No rate is published from
|
||
it. Controls preserved:
|
||
[`data/present-rate-controls-2026-08-29.json`](data/present-rate-controls-2026-08-29.json).
|
||
|
||
## What this means for the port, and for everything I hand over
|
||
|
||
**Do not take a wall-clock duration off this container as a game constant.**
|
||
Demonstrated range for one declared interval: 2.13 – 2.55 s, and the emulator's
|
||
own rate read anywhere from 12 to 28 fps depending on what was watching it.
|
||
|
||
The rule that follows: **a measured interval landing near a round number of
|
||
declared units almost certainly IS that number of units**, and the units are what
|
||
to ship. Wall clock is for ordering and for sanity, not for constants.
|
||
|
||
## Reach
|
||
|
||
* One container, one day, one machine. It says nothing about how a different host
|
||
runs Canary, and nothing about hardware.
|
||
* It does **not** refute the 28.5 fps in `ui-keyframe-time-unit.md`; it reclassifies
|
||
it as a load-dependent lower bound.
|
||
* ❔ **The game's true update rate is still not grounded in the disc.** "2 units
|
||
per submitted frame" *is* grounded — it was read off a frame-indexed draw
|
||
capture, so it is independent of how fast the emulator runs. What rests on the
|
||
emulator is only the step from *a submitted frame* to *1/30 s*, i.e. the
|
||
present interval. That is a constant in the executable, and reading it is
|
||
blocked on the missing disassembly route
|
||
([`static-route-recovered.md`](static-route-recovered.md)).
|