re: a nested record's +0x08 is its loop length -- the plate's period is 120, not 105

Answers the question the port agent asked: does the `PRESS A` plate's pulse
group loop from its start, or hold at alpha 0 between cycles? It holds.

A nested record is itself a RATC bundle with its own header, and that header's
`+0x08` is the loop length -- the same field ui_header_time_disc already tests
as an animation length at the top level. Its keyframes need not fill it, and
the slack is a hold at the final pose. `ptbtn00f` is 105 units of ramp inside
a 120-unit cycle, so the glow rests dark for 15 units between pulses. The five
main-menu focus records fill their 120 exactly, which is what shows the slack
belongs to this record rather than to the format.

Disc-wide over 1 781 timed nested records: 92.3% declare exactly their last
keyframe time, 7.7% declare more, and 0 declare less. That last row is the
falsifier -- a cycle cannot restart before its own last pose -- and it never
fires; the 7.7% is what keeps the reading from being an unfalsifiable
relabelling of the keyframes.

Falsification against the running game, using a pacing factor measured
INDEPENDENTLY on the main menu's focus ring (declared 120 units, measured
2.177 s, factor 1.0885): to reach the corpus's four measurements of the plate
pulse (2.12/2.19/2.34/2.31 s), a 105-unit period needs a factor of 1.211-1.337,
which EXCLUDES the ring's; a 120-unit period needs 1.060-1.170, which CONTAINS
it. Predicted 2.177 s against a measured 2.12-2.34. The two elements are in
different bundles and were measured in separate runs; the only thing tying
them together is that both declare 120.

So the port should stop shipping 105. Its 123-vs-129 ambiguity straddled the
right answer without containing it, and 129 only fitted because it was
105 + the exit_ramp_units constant it has since correctly deleted.

Reach is stated: this says where a cycle ends, not which records cycle, and
the TOP-level +0x08 is a different field left untouched -- every GP_TITLE
entry declares 300 while its elements end at 244-269.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
This commit is contained in:
sylph-decoder
2026-08-29 19:40:23 +00:00
parent 3e731c68ce
commit 07e93ce041
8 changed files with 439 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
# 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`](../../crates/sylpheed-formats/tests/ui_header_time_disc.rs)
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`](../data/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`](../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.
## 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
```bash
cargo run -p sylpheed-formats --example record_loop_length
SYLPHEED_DISC=/disc cargo test -p sylpheed-formats --test ui_record_loop_length_disc
```