Files
Sylpheed/docs/re/guest-frame-rate-measured.md
sylph-decoder 9ea6ee7555 re: the blend bit was challenged on pteff10 and SURVIVES; finding 3 loses its second candidate
The port isolated main_menu's 10.88 -> 13.02 regression to pteff10 and
asked whether the per-draw log shows it alpha-over, which would be a
counter-example to ui-blend-mode-decoded.md.

It does not. blend-bit-vs-oracle.txt has pteff10 ADDITIVE on entry 5 (main
menu) AND entry 6 (extras), both labels read out of the guest command
stream, and HANDOFF states it from three independent menu sessions, every
frame.

The premise came from a stale coverage table of mine that the same HANDOFF
entry had already corrected in place. Recording that: a correction which
leaves the wrong table visible upstream of it has not landed.

And the regression was predicted on this exact element before adoption --
HANDOFF flagged pteff10 as the one row the port's renderer would not
corroborate, because for a dim wholly-semi-transparent glow (max alpha 130)
over a dark background additive and alpha-over nearly coincide. A renderer
metric getting worse while the render gets more correct is the standing
rule with a number on it.

Separately, the port answered my finding-3 candidate: a frozen sweep of the
plate region rises monotonically across the declared 214->236, so the port
fades rather than pops and its t=236 is a completion. That kills the onset
branch.

Two of the play-test's four named candidates for finding 3 are now dead --
units/s (measured at 60) and the ramp. Finding 3 has no surviving named
cause. The clock origin is the one to take next, because every measurement
so far is a DIFFERENCE between two events and a difference cannot detect a
common offset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-01 18:55:50 +00:00

185 lines
8.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.
# ✅ 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.
### 🔴 ANSWERED same day, and this branch is dead too
The port checked and reports a frozen sweep of the plate region across the
declared ramp — `210u → 0.1457`, `216u → 0.1573`, `222u → 0.1727`, `228u →
0.1900`, `236u → 0.2142`. **A clean monotone rise across `214 → 236`: the port
fades, it does not pop**, and its `t = 236` is its completion, not its onset.
So the onset branch is eliminated as well. Of the four candidates the play-test
named for finding 3 — the unit→seconds constant, the clock origin, `rest.t`, the
record layout — **two are now dead**: units/s is measured at 60 here, and the
plate's ramp is being drawn. **Finding 3 has no surviving named cause.**
The remaining two are the **clock origin** (does the port's shared clock start
when the game's does?) and **`rest.t`**, and the origin is the one worth taking
first: everything measured so far is a *difference* between two events, and a
difference is exactly what cannot detect a common offset. `REFUTED.md` already
records that believing `rest.t` put a port's plate **3.97 s late once**.
## 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.