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:
sylph-decoder
2026-08-29 13:16:02 +00:00
parent f24304248c
commit 14fced07b6
7 changed files with 9078 additions and 0 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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)).

View 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)).