The settle-time decode was confirmed only against a SETTLED frame, which shows the end state is right and says nothing about whether the five flashes ever happen. This runs the oracle: a draw capture armed before the title exists, so the window contains the frames in which the screen is built. The flashes fire in a six-frame window and are absent from all 155 other sampled frames. `ptlogo_back2eff1` is drawn in exactly two frames at t = 54.0 against a decoded peak of t54-56; `ptlogo1` first appears at t = 42.2 against a decoded t42. Units-per-frame was taken from the GLOW's period alone, a different element, so the timings are not circular. The two holders are continuous from frame 134. The plate glow's quad carries a per-vertex colour whose alpha IS the element's fade alpha, so the ramp is read straight out of the guest: observed range 0..80 against a decoded peak of 80, exact and unfitted; period 51.158 presented frames over 20 cycle starts. Fitting the decoded ramp gives RMS 13.16 alpha levels against 38.18 for the same ramp REVERSED -- if the shape carried no information those would be equal, so the asymmetry is real and correctly directed. Further controls: symmetric triangle 15.73, flat 31.13. `ptlogo_back2eff3` was never drawn, and that is expected rather than a miss: a 2-unit flash peak is 0.85 of a presented frame, so catching one is a matter of phase. A port drawing all five every time shows more sweep than the console. METHOD.md gains the trap this cost: a 2D draw's identity is its vertex geometry, not its bound texture. These sprites sample shared pages, and matching texture dimensions produced a false negative (no flash is ever drawn) and a false positive (the intro movie's 640x360 YUV planes read as `ptbase2`) in the same pass. Also records the top-level restriction on the settle window, which the port raised and which is verified here: top-level [160,236] width 76, including the `ptloop` leaves [269,540] width 271 -- an instant past the end of every top-level element's timeline. Evidence committed as a derived per-frame series, not the 7 MB raw log. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
5.8 KiB
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). 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 |
ptbtn01f … ptbtn05f — 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.12–2.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.12–2.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.
This does not re-measure the period in seconds (the run's frame rate was not recorded), so the falsification below stands on the corpus's wall-clock timings rather than on this run.
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.12–2.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.
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 244–269, 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