Files
Sylpheed/docs/re/structures/ui-record-loop-length.md
sylph-decoder 1adb4e2ee7 re: the +0x08 falsifier does not identify +0x08 -- the value is right, my argument was wrong
sylpheed-port aimed my own boundary finding at my loop-length control. Reproduced
from my reader over every pak: +0x04 has 0 violations too, so the falsifier rejects
+0x0c and accepts +0x04 and never discriminated. What identifies +0x08 is the
exactness statistic the page presents as secondary -- exact match in half the
records against 0 % for +0x04.

Population differs from theirs, 3311 records against 1781, because this scan takes
every pak and requires a timed keyframe, so the percentage moves but the
discrimination does not.

Second time this week with the weight on the wrong leg: a count taking credit for an
exclusion argument, now a falsifier taking credit for an exactness statistic, and
both times the real discriminator sat beside it described as a formality.

Their general form is sharper than my boundary rule: an interior consistency check
is satisfied by any internally consistent reading, and that is what a wrong offset
into a regular structure usually is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 03:52:09 +00:00

9.6 KiB
Raw Blame History

A nested record's +0x08 is its loop length — and the plate holds dark for 15 units

Classification: decoded. The field is read from the disc and checked disc-wide (1 781 records, 0 violations).

🔴 CORRECTED 2026-08-31 — the value is right and this argument for it was wrong. "0 violations" was published as the load-bearing evidence: an animation cannot restart before its own last pose, so a wrong reading should produce violations. sylpheed-port re-ran it at the neighbouring offsets and it does not discriminate. Reproduced from my own reader over every pak:

offset violations (word < max t) exact (word == max t)
+0x04 0 — passes the falsifier 0.0 %
+0x08 0 49.6 %
+0x0c 1 287 (38.9 %) 11.8 %

A wrong reading one word left produces no violations either. The falsifier rejects +0x0c and accepts +0x04, so it never identified +0x08.

What does identify it is the exactness row below, which this page presents as a secondary statistic: +0x08 equals the largest keyframe time exactly in half the records and +0x04 in none. No unrelated word reproduces that.

⚠️ My population differs from theirs — 3 311 records against 1 781, because this scan takes every pak and requires a timed keyframe — so the exact percentage moves (49.6 % vs 92.3 %). The discrimination does not: 0 % for +0x04 under both counts. The consequence for the PRESS Ⓐ plate is then tested against the corpus's existing measurements of the running game, which it passes and the previous reading fails.

The question

The port asked it directly: the plate's pulse group runs t=0 → t=105 with alpha 0 at both ends. Does it loop from its start, or hold at alpha 0 between cycles?

It matters because a 105-unit period is 1.750 s, and the corpus timed the real pulse four times at 2.12 / 2.19 / 2.34 / 2.31 s — about 17 % longer. The port shipped 105 anyway, saying the disc's number was wrong, because the alternative (129) was built on an exit_ramp_units constant that a decode had just deleted.

The answer: it holds, and the period is 120

A nested record is itself a RATC bundle, with its own header, and that header's +0x08 is a frame count — the same field ui-header-time-disc already tests as an animation length at the top level. Read it on the record and the answer is immediate:

record +0x08 largest keyframe slack
ptbtn00f.rat — the plate glow 120 105 15
ptbtn01fptbtn05f — main menu focus 120 120 0
ptloop01.rat 600 600 0
ptloop02.rat 720 720 0

The glow ramps 0 → 80 → 0 over 105 units inside a 120-unit cycle, so it rests dark for 15 units between pulses. The five main-menu focus records fill their cycle exactly, which is what shows the slack is a property of this record rather than of the format.

Disc-wide

record-loop-length-census.txt — every nested record on the disc that has a timed keyframe:

count share
nested records with timed keyframes 1 781
+0x08 == largest keyframe time 1 643 92.3 %
+0x08 > largest keyframe time (a hold) 138 7.7 %
+0x08 < largest keyframe time 0 0.00 %

The last row is the falsifier and it never fires: no record declares a cycle that would restart before its own last pose. The 7.7 % is what keeps the reading from being an unfalsifiable relabelling of the keyframes — if every record declared exactly its own last keyframe time, the field would carry nothing.

The falsification test against the running game

Both hypotheses are periods in declared units, and both have to be converted by the same emulator pacing factor. That factor is measured independently, on the main menu's focus ring: declared 120 units, measured 2.177 s (focus-ring-spin-measured.md), so the factor is 1.0885 against a nominal 60 units/s.

plate period nominal factor it would need to reach 2.122.34 s verdict
105 units 1.750 s 1.211 … 1.337 🔴 excludes the ring's 1.0885
120 units 2.000 s 1.060 … 1.170 contains the ring's 1.0885

At 120 units and the ring's own factor the plate should pulse every 2.177 s, against a measured 2.122.34 s. 105 cannot reach the measured range under any pacing factor that the ring also satisfies.

This is a genuine test rather than a fit: the ring and the plate are different elements in different bundles, measured in separate runs, and the only thing tying them together is that both declare a 120-unit cycle.

Measured in the running game

The glow's quad carries a per-vertex colour whose alpha is the element's fade alpha, so the ramp can be read straight out of the guest's draw stream (ui-title-buildin-measured.md):

  • observed alpha range 0 … 80, against a decoded peak of 80 — exact, unfitted;
  • period 51.158 presented frames over 20 consecutive cycle starts;
  • the draw is omitted entirely while the glow is dark, which is the 11↔10 draw alternation visible in the settled title;
  • fitting the decoded ramp gives RMS 13.16 alpha levels against 38.18 for the same ramp reversed — the asymmetry is real and in the decoded direction.

And a calibration-free test that refutes 105 outright

The measurements above still need a units-per-frame conversion. This one does not.

The glow's draw is omitted entirely when its alpha reaches zero, and the smallest alpha actually submitted in 807 drawn frames is 1 — so the renderer's culling threshold is 1, read off the data rather than assumed. The fraction of settled frames with no glow draw is then a pure ratio within one measurement:

dark-frame fraction
measured (173 of 980 settled frames) 17.7 %
predicted by a 120-unit cycle (15-unit dark hold) at threshold 1 14.4 %
predicted by a 105-unit cycle (no dark hold) at threshold 1 2.2 %

🔴 105 is out by a factor of eight. For a 105-unit cycle to produce 17.7 % dark, the culling threshold would have to be alpha 11 out of a peak of 80 — and the capture contains submitted draws at alpha 1, 2, 3, 4, 5, 6, 7, 8, 9, 11 and 12, which refutes any such threshold directly.

No frame rate, no pacing factor, no wall clock: the declared 15-unit dark hold is visible in the draw stream as the frames where the game submits no draw at all.

What this replaces

  • 🔴 105 is wrong and the port should stop shipping it. The number is 120, and it comes from the disc — not from exit_ramp_units, the deleted constant whose 129 happened to fit.
  • The port's ambiguity is genuinely resolved, just not the way it read: the old 123-vs-129 pair straddled the right answer without containing it.
  • ⚠️ The 2.24 s mean is still ~3 % above the 2.177 s prediction. That is inside the spread of the four measurements (2.122.34) and is not evidence of a further hold; it is what a four-sample wall-clock measurement of a ~2 s period in this emulator looks like.

What this field does NOT settle: do the ptloop sweeps loop?

Asked by the port, hoping +0x08 would decide it. It does not.

ptloop01.rat declares +0x08 = 600 with keyframes reaching exactly t=600; ptloop02.rat declares 720, keyframes to t=720. Slack zero — and a zero-slack record is precisely the case this field cannot discriminate: "loops at 600" and "runs once for 600 and stops" produce the identical header. 92.3 % of records on the disc are in that state.

The oracle answers it for the title, and the answer is that they keep moving. Tracking the sweep quad across two title dwells in a draw capture:

sweep draws x range (NDC) backward jumps to the start
title dwell 1 (frames 1381220) 1008 3.02 … 0.33 1 (frame 1161)
title dwell 2 (frames 59637025) 908 3.02 … +0.42 2 (frames 6370, 6822)

The x position oscillates across the whole range for the entire dwell and resets hard to the same start value (2.38). A run-once-and-park would show one traverse and then a constant x. It does not park.

⚠️ This is the TITLE, and the port asked about the MAIN MENU. Both declare ptloop01/ptloop02 with the same 600/720, but I have not captured the menu, and the port's own evidence — an idle menu capture matching best with the sweeps off-screen — points the other way. Either the menu's sweeps behave differently, or "best match" is doing badly at detecting an absence, which the port said itself. Unresolved for the menu; measured for the title.

Reach

⚠️ This says where a cycle ends, not that every record cycles. 92.3 % of records declare no slack at all, and a record whose element is not in a repeating state (a button's base record, a one-shot build-in) has a length that is simply its own duration. Nothing here establishes which records the game restarts — only that when one does, +0x08 is where.

The top-level +0x08 is not the same thing. Every GP_TITLE entry declares 300 there while its elements end at 244269, and no screen visibly repeats every 5 s. Whether the top-level field is a loop length, a budget or something else is untouched by this.

Reproducing

cargo run -p sylpheed-formats --example record_loop_length
SYLPHEED_DISC=/disc cargo test -p sylpheed-formats --test ui_record_loop_length_disc