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
This commit is contained in:
4192
docs/re/data/boot-timeline-2026-08-29.tsv
Normal file
4192
docs/re/data/boot-timeline-2026-08-29.tsv
Normal file
File diff suppressed because it is too large
Load Diff
4171
docs/re/data/present-rate-controls-2026-08-29.json
Normal file
4171
docs/re/data/present-rate-controls-2026-08-29.json
Normal file
File diff suppressed because it is too large
Load Diff
115
docs/re/present-rate-instrument-failed.md
Normal file
115
docs/re/present-rate-instrument-failed.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# 🔴 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)).
|
||||
104
docs/re/static-route-recovered.md
Normal file
104
docs/re/static-route-recovered.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# 🟡 The static PPC route was lost with the container migration — the image is back, the disassembly is not
|
||||
|
||||
**Status:** ✅ the **image** is recovered and validated; 🔴 the **disassembly
|
||||
database** it is analysed with does not exist in this repository at all, and
|
||||
never did. 2026-08-29.
|
||||
|
||||
## What broke
|
||||
|
||||
Four tools in `tools/re-capture/` open a DuckDB database at
|
||||
`/work/xenia-rs/sylpheed.db` — `name_block_bases.py`, `archive_naming.py`,
|
||||
`isl_cmdtab.py` and the census work that produced most of the `sub_82xxxxxx`
|
||||
findings in [`INDEX.md`](INDEX.md). In this container:
|
||||
|
||||
```
|
||||
$ ls /work/xenia-rs
|
||||
ls: cannot access '/work/xenia-rs': No such file or directory
|
||||
```
|
||||
|
||||
🔴 **And nothing in the repository builds it.** A grep for `duckdb` finds only
|
||||
*consumers*; there is no disassembler, no PPC decoder vendored, and `capstone` is
|
||||
not installed (and `pip install` is refused here by PEP 668). So the static route
|
||||
is four read-only clients of a producer that is not in the tree.
|
||||
|
||||
⚠️ **This is not a small gap.** Every finding that cites a function address —
|
||||
the GamePart registry, the challenge gate, the ISL command table, `PlayerParams`,
|
||||
the boot sequencer — is currently **unre-checkable in this container**. They are
|
||||
not wrong; they are unverifiable, which is a different and quieter problem.
|
||||
|
||||
## `default.xex` is not a substitute
|
||||
|
||||
The disc's executable is encrypted and LZX-compressed:
|
||||
|
||||
```
|
||||
$ xxd -l 16 /disc/default.xex
|
||||
00000000: 5845 5832 0000 0001 0000 3000 ... XEX2......0.
|
||||
$ strings -a /disc/default.xex | grep -c GamePart
|
||||
0
|
||||
```
|
||||
|
||||
The header is intact — `XEX2`, media id `535107D4`, original PE name
|
||||
`default.pe` — and everything after it is noise. No decrypt/decompress exists in
|
||||
the tree either.
|
||||
|
||||
## ✅ What is recovered, and how it is validated
|
||||
|
||||
Xenia decrypts, decompresses and relocates the image at load, so a **running
|
||||
guest holds the flat VA image** the corpus calls the `.pe`
|
||||
(`VA = 0x82000000 + offset`). `tools/re-capture/dump_image.py` reads it straight
|
||||
out of `/dev/shm/xenia_memory_*` — no debugger, no emulator patch, no pause.
|
||||
|
||||
```
|
||||
$ tools/re-capture/dump_image.py /sylph-home/re/sylpheed-image.pe
|
||||
wrote ... 4194304 bytes VA 0x82000000..0x82400000
|
||||
validated: GamePart id table + D3D runtime strings; 1013/1024 non-empty 4K pages
|
||||
```
|
||||
|
||||
**The validation is the corpus's own landmarks, not the tool's.** A mis-based or
|
||||
partial dump fails both:
|
||||
|
||||
* `0x820A1630` holds the **GamePart id table** — 29 `.rdata` pointers resolving
|
||||
to `GP_TITLE` (0) … `GP_TEST` (28), with `GP_CHALLENGE` at **26**, exactly as
|
||||
[`challenge-mission-gate.md`](challenge-mission-gate.md) records;
|
||||
* the image carries the **Xbox 360 D3D runtime's own error strings**
|
||||
(`ERR[D3D]: Unanticipated CPU_INTERRUPT`, `D3D9D.LIB`), which only the real
|
||||
loaded executable has.
|
||||
|
||||
So string search, table dumps and pointer chasing work again today. **What does
|
||||
not** is anything needing decoded instructions: no `mnemonic`/`operands`, so no
|
||||
xref search, no base solving, no call graphs.
|
||||
|
||||
⚠️ It also depends on a **booted emulator**, which is a bad dependency for the
|
||||
foundation of the static corpus. Dump once and keep the file.
|
||||
|
||||
## 🔵 For the human — what is actually needed
|
||||
|
||||
Not the image; that is solved. What is missing is the **producer of the
|
||||
database**, and its schema, which the four consumers pin exactly:
|
||||
|
||||
| table | columns used |
|
||||
|---|---|
|
||||
| `functions` | `address`, `end_address`, `name` |
|
||||
| `instructions` | `address`, `mnemonic`, `operands` |
|
||||
| `strings` | `address`, `content` |
|
||||
|
||||
An XEX unpacker would also be worth having on its own, so the static route stops
|
||||
needing a running emulator to bootstrap.
|
||||
|
||||
## What it blocks right now
|
||||
|
||||
The one open question this iteration wanted it for: **the game's present
|
||||
interval**, i.e. the step from *one submitted frame* to *1/30 s of real time*.
|
||||
|
||||
"2 units per submitted frame" is already grounded and emulator-independent — it
|
||||
was read off a frame-indexed draw capture
|
||||
([`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md)). The remaining link is
|
||||
`D3DPRESENT_PARAMETERS.PresentationInterval` at device creation: `_ONE` means the
|
||||
game runs at the console's 60 Hz vblank, `_TWO` at 30. That is a constant loaded
|
||||
into a register, so **finding it needs disassembly, not string search** — the
|
||||
image contains the D3D runtime but the interval is an immediate, not a string.
|
||||
|
||||
Settling it would move Q1's seconds-per-unit from **measured** to **decoded**,
|
||||
and it is the only part of the UI clock still resting on a wall clock this
|
||||
container has now been shown to move by 19 %
|
||||
([`present-rate-instrument-failed.md`](present-rate-instrument-failed.md)).
|
||||
Reference in New Issue
Block a user