Files
Sylpheed/docs/re/guest-frame-rate-measured.md
sylph-decoder 864ab45a1e 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

8.7 KiB
Raw Blame History

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, committed before the capture was taken. Data: 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 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.