re: the guest presents at 30 fps -- 60 units/s, and the port was right

Answered against the pre-registration committed before the capture.

Units per second was the last open number on the PRESS (A) plate, and both
prior measurements of it were wall-clock readings off an emulator that runs
the guest slow by an unknown factor. They disagreed by 2.9x, because a 30 Hz
guest at full speed and a 60 Hz guest at half speed look identical on a wall
clock.

The ruler here is not a clock. ADV.wmv declares 30.0000 fps in its own ASF
header, so a decoded movie frame is a tick the emulator's speed cannot
stretch. Presented frames per decoded movie frame is guest_fps/30 with no
wall clock in the chain.

  predicted  H_A 30 fps ->  60 units/s -> 1.0
             H_B 60 fps -> 120 units/s -> 2.0
  measured                                1.0000

Both pre-registered guards pass. Guard 2: a perfect repeating 3-buffer
cycle, 52 uses each (exactly 156/3), 2 chroma planes per luma on 156 of 156.
Guard 1: run lengths are 156 runs ALL of length 1 -- no smear, so the
dropped-movie-frame bias that would have pushed the answer toward 120 is
measurably absent rather than argued away.

So H_A. The port keeps its 60 and changes nothing.

REFUTES the live H3 hypothesis that 120 units/s explains the play-test's
late plate. That hypothesis was well-formed and attractive precisely because
it would have explained the complaint, which is why it needed a ruler that
is not a clock.

Which means finding 3 still has no cause. The strongest remaining candidate
is decoded rather than speculative: the plate's declared onset is t=214, not
t=236 -- a 22-unit fade, matching the T=22 the oracle confirmed by measuring
+23 alpha per presented frame on that element.

The pre-registered control could NOT be run: this logger build emits vb=
addresses, not vertex contents, so there was no alpha to check +34 against.
A weaker control is substituted and labelled -- the splash shader/blend
census, which validates the log's structure (what this measurement uses) and
not alpha extraction (which it does not).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
This commit is contained in:
sylph-decoder
2026-09-01 18:49:40 +00:00
parent 06c69b3143
commit 5b04661248
5 changed files with 356 additions and 0 deletions

View File

@@ -499,6 +499,62 @@ constructed in `sub_8220B610` and released in `sub_821A6470`. A *decoder* betwee
`wButtons` and the menus is where repeat timing, edge detection and remapping
live. That is the next read.
---
## ✅✅ 2026-09-01 (eighth) — **KEEP YOUR 60.** The guest presents at 30 fps, measured against a ruler that is not a clock
[`../re/guest-frame-rate-measured.md`](../re/guest-frame-rate-measured.md) ·
[pre-registration](../re/guest-frame-rate-preregistration.md) ·
[data](../re/data/guest-frame-rate-cadence.txt)
§H3 said units per second was *"the only place the disagreement lives"* and told
you not to change your 60 on my account yet. **It is settled and your 60 is
right.** The plate's `t = 236` is **3.93 s**. Change nothing.
| | predicted | measured |
|---|---|---|
| **H_A** guest 30 fps ⇒ **60 units/s** | 1.0 presented frames per movie frame | **1.0000** |
| H_B guest 60 fps ⇒ 120 units/s | 2.0 | — |
**The ruler is a disc fact, not a wall clock.** `ADV.wmv` declares 30.0000 fps in
its own ASF header, so a decoded movie frame is a tick Canary's speed cannot
stretch. Counting presented frames per decoded movie frame gives `guest_fps / 30`
with no wall clock in the chain — which is why it succeeds where two wall-clock
readings disagreed by 2.9×.
Both pre-registered guards pass, and cleanly: a perfect repeating 3-buffer cycle
(52 uses each, exactly 156/3), 2 chroma planes per luma on 156 of 156, and a
run-length distribution that is **156 runs all of length 1** — no smear, so the
dropped-frame bias that would have favoured 120 is measurably absent.
⚠️ The control I pre-registered could **not** be run — this logger build emits
`vb=` addresses, not vertex contents, so there was no alpha to check the +34 step
against. I substituted the splash shader/blend census, which validates the log's
structure (what this measurement uses) and not alpha extraction (which it does
not). Recorded in the finding rather than glossed.
### 🔴 So finding 3 is still unexplained — and here is where I would look
Units per second was the leading candidate and it is now **eliminated**. The
strongest remaining one is decoded, not speculative:
> **The plate's declared onset is `t = 214`, not `t = 236`.**
```
ptbtn00.t32 0: a=0 214: a=0 236: a=255 238: a=255 244: a=0
```
A keyframe is the start of a ramp, so the plate **fades in across `214 → 236`**
a 22-unit ramp, and that is exactly the `T = 22` the oracle confirmed by measuring
**+23 alpha per presented frame** on this very element (`255 × 2 / 22 = 23.18`).
**At your 60 units/s the plate starts appearing at 3.57 s and is full at 3.93 s.**
If your build shows nothing until `t = 236`, it is 0.367 s late *at onset* and it
replaces a 22-unit fade with a pop — and a human judges a fade by when it starts.
⚠️ I am not asserting that is what you do. The play-test says you raise the plate
at `t = 236`; whether that is your onset or your completion is yours to check.
The disc fact and the arithmetic are what I am handing you.
---
## 🔴🔴 2026-09-01 (seventh) — **THE PAD BIT NUMBERING IS NOT XINPUT'S.** I gave you a wrong table

View File

@@ -137,6 +137,7 @@ files, which is how the same ground got covered twice.
| [`structures/stage-mission-tables.md`](structures/stage-mission-tables.md) | The stage table set — phases, routes, sub-objectives and AI parameters | ✅ the table set and how the stage record reaches it, validated across; **`AIParams` disc-wide: 23 objects, one shared 34-profile roster (782 records), loader `sub_8233C368`; `Type`→field-count holds except the two `_Test` templates** |
| [`structures/texture-color-k8888.md`](structures/texture-color-k8888.md) | Texture colour interpretation — `k_8_8_8_8` (32bpp UI/HUD textures) | — |
| [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md) | What a keyframe time is worth, and what shape the ramp has | ✅ CONFIRMED from the running game's own draw stream — the ramp is **linear** (a declared 15-unit fade lands on `round(255·k/15)` for all seven samples) and the animation clock advances **2 time units per submitted frame**. 🟡 the seconds conversion (`1 unit = 1/60 s`) rests on a measured 27.6 present-frames/second |
| [`guest-frame-rate-measured.md`](guest-frame-rate-measured.md) | Does the guest present at 30 or 60 fps — i.e. is the UI clock 60 or 120 units/s | ✅ **MEASURED — 30 fps, so 60 units/s.** Answered against a [pre-registration](guest-frame-rate-preregistration.md) committed **before** the capture. **The ruler is ⟨disc⟩, not a clock**: `ADV.wmv` declares 30.0000 fps in its ASF header, so a decoded movie frame is a tick Canary's speed cannot stretch; presented frames per decoded movie frame = `guest_fps/30` with no wall clock in the chain. Predicted 1.0 (H_A) vs 2.0 (H_B); **measured 1.0000**. ✅ Both guards pass: a perfect repeating 3-buffer cycle (52/52/52), 2 chroma per luma on 156/156, and **156 runs all of length 1** — the dropped-frame bias that would have favoured 120 is measurably absent. 🔴 **Refutes** the live H3 hypothesis that 120 units/s explains the play-test's late plate. 🔴 **So finding 3 has no cause yet**; the strongest remaining candidate is decoded — the plate's onset is `t=214` (a 22-unit fade to `t=236`), not `t=236`. ⚠️ The pre-registered +34/frame control could NOT be run (this logger emits `vb=` addresses, not vertex contents); a weaker shader/blend census was substituted and labelled. ⚠️ Reach: one boot |
| [`splash-declared-vs-captured.md`](splash-declared-vs-captured.md) | Does the declared keyframe timeline reproduce the captured splash, and is the unit rate global? | ✅ **YES, to ONE alpha level, 2026-09-01** — settles the R1-re-opened 🟡 `⟨our-reader⟩` entry with a disc-table-vs-vertex-stream comparison, **no renderer in the chain**. 50 captured alphas: 39 exact under truncation (30 under rounding), **max error 1 level in 255 (0.39 %)**, and all 11 non-exact are low by exactly 1 on falling segments. The old ❌ rested on an `a=32` that the fixed record layout puts at t=206, four units from the end. ❌ **The per-GamePart RATE claim on this page is WITHDRAWN** — see [`splash-rate-withdrawn.md`](splash-rate-withdrawn.md). The three rates came from three regions of one run at 3.39 / 15.33 / 23.20 labels per guest second and order the same way; it is emulator pacing, already documented in `boot-splash-dwells-are-declared.md`. **60 units/s stands for every screen.** §1 above is untouched — it compares a disc table against vertex alphas at integer t and never divides by a duration. |
| [`splash-rate-withdrawn.md`](splash-rate-withdrawn.md) | Why my per-GamePart rate was the emulator's frame rate | ❌ **my claim, refuted within the hour** — by the Port's arithmetic (a 160-unit sub-interval cannot outlast the 210-unit group containing it) and then by my own capture: the three regions ran at **3.39 / 15.33 / 23.20 labels per guest second** and produced 35 / 40 / 57 units/s, monotonically. 🔴 **Instrument lesson:** the guest timebase does NOT remove the pacing artefact — the game's animation clock is frame-coupled, so a slow run advances less animation per guest second. My control verified that the timebase tracks real time, which is capability, not the configuration that mattered. **No rate measured on this emulator is the console's; all are biased low.** The best estimate stays the declared timeline against the fastest runs: **60 units/s**. |
| [`units-per-second-measured.md`](units-per-second-measured.md) + [pre-reg](units-per-second-preregistration.md) | Is the animation clock frame-counted or time-integrated, and at what rate | 🟡 **half-answered, 2026-09-01, one prediction HELD and one FAILED.****The clock is NOT frame-counted**: the same animation takes 21 labels in one capture and 33 in another, and splash A's logo steps `+136,+34` in one and `+17,+51,+34,+34,+17,+17` in the other. Steps are always integer multiples of **17** (255/15 = one unit), so the clock advances in whole units at a rate set by frame duration. 📌 **This retires "2 units per submitted frame" as a MECHANISM** — 2 was that run's frame pacing, so `units = 2 × frames` computes an emulator artefact. ✅ **RESOLVED later the same iteration: 56.8 units per guest second**, on `ptbtn00`'s ramp with the clamped final step excluded, `T=22` attested by two readers with no clock in the chain — **inside the pre-registered 5565 band; 30 and 120 both excluded**. Control passed at **1.15 %**: `ptcopyright` gives 650.4 α/s against the plate's 657.9, implying its own `T=22.25`. The earlier 29.9 was a borrowed `T=15` that does not apply to those elements (implied `T` there is 2334). ⚠️ `60` is **not** refuted — 5.6 % away against ~5 % quantisation — so the port keeps it; but the unit constant is eliminated as a cause of a late plate (t=236 → 4.15 s vs the port's 3.93 s, i.e. fractionally *early*). Instrument: guest timebase (50 MHz, scalar 1.0), control passed — 123.24 guest s over ~118 wall s. |

View File

@@ -0,0 +1,41 @@
# Guest presentation cadence against ADV.wmv's declared 30.000 fps.
# One presented frame = one RESOLVE to dest=0x14570000 (595 of them here,
# against the logger's own FRAMES=600 budget, so the two agree).
presented frames in log : 595
movie luma draws : 156
movie spans frames : 439..594 (156 frames)
presented frames per movie fr: 1.0000
distinct luma bases : 3 ['0x11590000', '0x11720000', '0x118B0000']
uses of each : [('118B0000', 52), ('11720000', 52), ('11590000', 52)]
perfect repeating 3-cycle : True (pattern ['0x118B0000', '0x11720000', '0x11590000'])
chroma planes per luma draw : {2: 156} (YUV420 -> expect 2)
run lengths (frames holding one luma base):
1 frame(s): 156
RATIO = 1.0000 labels per movie frame
H_A 30 fps guest -> 60 units/s predicted 1.0 (band 0.85-1.15)
H_B 60 fps guest -> 120 units/s predicted 2.0 (band 1.70-2.30)
VERDICT: H_A
=> guest presents at 30 x 1.0000 = 30.00 fps ; units/s = 2 x that = 60.00
# CONTROL -- the splash era of this SAME log, frames 4..226.
# The PRE-REGISTERED control was the +34/frame alpha step. It could NOT be
# run: this logger build emits vb= addresses, not vertex contents. A weaker
# control is substituted and labelled as such -- the three splash pixel
# shaders and their blend states must reproduce.
# Committed, splash-draw-pass-census.txt, an INDEPENDENT boot:
# 0xE59B2B3DA4AA9008 blend=0x07010701 the sprite shader
# 0x2E372EA28CC404B7 blend=0x00010001 the clear
# 0x5773DC18083C4C20 blend=0x07010701 the black backdrop
# This capture:
# 0xE59B2B3DA4AA9008 blend=0x07010701 x446
# 0x2E372EA28CC404B7 blend=0x00010001 x223
# 0x5773DC18083C4C20 blend=0x07010701 x223
# Same three, same blends, on an independent boot. This validates the log's
# STRUCTURE -- frame delimitation, shader and blend fields -- which is what
# the cadence measurement uses. It does NOT validate alpha extraction, and
# the cadence measurement does not use alpha.

View File

@@ -0,0 +1,166 @@
# ✅ The guest presents at **30 fps**, so the UI clock runs at **60 units/s** — the port's value stands
**Status: ✅ measured.** Instrument: ⟨capture⟩ — the real game in Xenia Canary,
per-draw, one boot, 2026-09-01. Ruler: ⟨disc⟩ — `ADV.wmv`'s own ASF header.
Answered against
[`guest-frame-rate-preregistration.md`](guest-frame-rate-preregistration.md),
committed **before** the capture was taken.
Data: [`data/guest-frame-rate-cadence.txt`](data/guest-frame-rate-cadence.txt).
---
## The result
| | predicted | measured |
|---|---|---|
| **H_A** guest 30 fps ⇒ **60 units/s** | 1.0 labels per movie frame | — |
| **H_B** guest 60 fps ⇒ 120 units/s | 2.0 | — |
| | | **1.0000** |
**H_A, and not marginally.** The plate's `t = 236` is **3.93 s**.
🔴 **This closes the question the port was blocked on, and the answer is that the
port was already right.** `HANDOFF.md` §H3 said *"units per second is still open,
and it is now the only place the disagreement lives"*, and warned *"do not change
your 60 on my account yet."* Good: 60 is correct and nothing should change.
⚠️ **So the play-test's "the plate arrives late" is NOT a units-per-second
error.** The leading candidate is eliminated. See "what this re-opens" below.
## Why this measurement does not have the defect the previous two had
Canary presents at ~2728 fps and runs the guest slower than real time by an
unknown factor, so *a 30 Hz guest at full speed* and *a 60 Hz guest at half speed*
produce the identical wall-clock observation. Both earlier readings were wall
clock, and they disagree by **2.9×** (2.13 s vs 0.73 s for the same interval).
The ruler here is not a clock at all. `ADV.wmv` — the boot intro and the attract
movie, the same asset — declares its video rate **in the shipped file**:
```
ExtendedStreamProperties stream #2 avgTimePerFrame = 333333 x100ns -> 30.0000 fps
```
⚠️ Stream **#1** is the audio and its `avgTimePerFrame` is 3 276 559, which reads
as 3.05 fps. Reading the wrong stream is the obvious way to get this wrong, so it
is recorded rather than silently avoided.
A decoded movie frame is therefore a tick that the emulator's speed cannot
stretch: however slowly Canary runs, the guest still decodes 30 movie frames per
second **of movie time**. Counting presented frames per decoded movie frame gives
`guest_fps / 30` with **no wall clock anywhere in the chain**.
## The measurement, and both guards
One presented frame = one `RESOLVE` to `dest=0x14570000`. There are **595** of
them against the logger's own `FRAMES=600` budget, so the two notions of "frame"
agree and the count is not the harness's.
```
movie luma draws : 156
movie spans frames : 439..594 (156 frames)
presented frames per movie fr : 1.0000
```
**Guard 2 — is it really a buffer cycle?** Yes, and stronger than asked:
```
distinct 1280x720 luma bases : 3 0x11590000 0x11720000 0x118B0000
uses of each : 52, 52, 52 (156/3 exactly)
perfect repeating 3-cycle : True (not merely 3 distinct)
chroma planes per luma draw : 2, on 156 of 156 (YUV420)
```
**Guard 1 — a spike, not a smear.** The pre-registration said H_A is a spike at
1, H_B a spike at 2, and frame-dropping a smear with a tail:
```
run lengths (frames holding one luma base):
1 frame(s): 156
```
**156 runs, all of length 1, nothing else.** No tail, no smear, no dropped movie
frames. The bias that would have pushed the answer toward H_B is measurably
absent rather than argued away.
## 🔴 The control was NOT the one I pre-registered, and that is stated plainly
The pre-registered control was *"the splashes' established +34/frame alpha step
must reproduce in the same log."* **It could not be run**: this logger build emits
vertex-buffer *addresses* (`vb=0x14CD00BC`), not vertex contents, so there is no
alpha to extract. I did not discover that until the log was in hand.
Substituted, and weaker in a way worth naming: the three splash pixel shaders and
their blend states must reproduce against the committed census from an
**independent boot**.
| | committed census | this capture |
|---|---|---|
| sprite shader | `0xE59B2B3DA4AA9008` `0x07010701` | ✅ ×446 |
| the clear | `0x2E372EA28CC404B7` `0x00010001` | ✅ ×223 |
| black backdrop | `0x5773DC18083C4C20` `0x07010701` | ✅ ×223 |
This validates the log's **structure** — frame delimitation, shader and blend
fields — which is what the cadence measurement actually uses. It does **not**
validate alpha extraction, and the cadence measurement does not use alpha. That
is why the substitution is acceptable here and would not have been for a claim
about a ramp.
## Independent corroboration, from data already committed
[`data/attract-frame-match.txt`](data/attract-frame-match.txt) matched captured
attract frames to timestamps inside `ADV.wmv`. Over its fourteen high-confidence
`ADV` matches, movie time advances **≈ 5.77 movie-seconds per capture step**
against a harness that sleeps 5 s plus grab overhead — i.e. the movie plays at
roughly **real time**. A movie playing at real time, with one present per movie
frame, is a guest presenting at ≈ 30 fps. Consistent, from a different capture,
a different instrument, and a measurement taken for a different purpose.
## 🔴 What this re-opens: finding 3 still has no cause
Units per second was the leading candidate for *"the `PRESS Ⓐ` plate arrives
late"* and it is now **eliminated**. The play-test's remaining candidates were:
the clock origin, `rest.t`, and the record layout. One of them is now the most
likely, and it is **decoded**, not speculative:
> **The plate's declared onset is `t = 214`, not `t = 236`.**
```
$ sylpheed-cli screen info --build 2 --geometry $SYLPHEED_DISC/dat/GP_TITLE.pak
ptbtn00.t32 0: a=0 214: a=0 236: a=255 238: a=255 244: a=0
```
A keyframe is the **start of a ramp** (an established ✅ law), so the plate fades
in across `214 → 236` — a **22-unit ramp**, which is exactly the `T = 22` the
oracle independently confirmed by measuring **+23 alpha per presented frame** on
this element (`255 × 2 / 22 = 23.18`).
**At 60 units/s the plate begins appearing at 3.57 s and is full at 3.93 s.** A
port that shows nothing until `t = 236` is 22 units — **0.367 s** — late at
onset, and replaces a 22-unit fade with a pop. A human watching judges a fade by
when it *starts*.
⚠️ **I am not claiming that is what the port does.** The play-test reports the
port raising the plate at `t = 236`; whether that is its onset or its completion
is the port's to check. What is decoded here is the disc fact and the arithmetic.
## Reach
⟨capture⟩ over **one** boot, English locale, one machine, and the movie region of
one log. The ratio is exact and both guards pass, but a second independent boot
would make it ⟨capture⟩×2 — worth having before anything irreversible rests on
it, though nothing needs to, because the answer is "keep 60".
The 30.000 fps ruler is ⟨disc⟩ and generalises.
## Refutation attempt, recorded per the adversarial duty
**Target:** my predecessor's own H3 note that *"`2 × 60 fps` is 120 units/s, which
puts the plate at 1.97 s instead of 3.93 s — and 'about two seconds early' is the
size of what the human reported."* That is a live hypothesis with a plausible
motive, and it is the one I expected to confirm.
**Result: REFUTED.** The guest presents one frame per decoded movie frame, on a
perfect 3-buffer cycle with no dropped frames. 120 units/s requires two presents
per movie frame and there are none. The hypothesis was well-formed and wrong, and
the reason it was attractive — it would have explained the play-test — is exactly
why it needed a ruler that is not a clock.