Files
Sylpheed/docs/re/present-rate-instrument-failed.md
sylph-decoder 14fced07b6 re: I cannot measure this emulator's clock -- and that answers the port's 2%
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
2026-08-29 13:16:02 +00:00

116 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🔴 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)).