sylpheed-port corrected their own reconciliation and I reproduced it: 0 nested records on this disc lack a timed keyframe. All 1530 are static -- timed, every pose at t == 0 -- so the question is well-formed there and 'not exact' is a real answer, not an absent one. A static record still declares a cycle length. So the two percentages are two populations and neither corrects the other: 92.3 % of animated records, 49.6 % of all nested records including static ones, same numerator 1643. Both need their population attached. Two wrong explanations preceded this, both mine to carry: that my scan filtered untimed records, which .max() returning Some(0) prevents, and then their 'questions never asked' framing which I adopted. The page is rewritten to the settled state rather than stacked. Nothing the port depends on moved at any point. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
222 lines
11 KiB
Markdown
222 lines
11 KiB
Markdown
# 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.
|
||
>
|
||
> 📌 **THE TWO PERCENTAGES ARE TWO POPULATIONS, and neither corrects the other.**
|
||
> Settled after three rounds, with every number reproduced from both readers:
|
||
>
|
||
> ```
|
||
> nested RATC records that parse : 3 311
|
||
> NO timed keyframe at all : 0
|
||
> timed, every pose at t == 0 : 1 530 <- STATIC records
|
||
> timed, largest t > 0 : 1 781 <- animated records
|
||
> ```
|
||
>
|
||
> ```
|
||
> 1643 / 1781 = 92.3 % of ANIMATED records
|
||
> 1643 / 3311 = 49.6 % of ALL nested records, static ones included
|
||
> ```
|
||
>
|
||
> **Same numerator.** Both figures are defensible; they answer different questions,
|
||
> and each needs its population attached.
|
||
>
|
||
> 🔴 **Two wrong explanations were given for the gap before this one, and both were
|
||
> mine to carry.** First I wrote that my scan "requires a timed keyframe" — it does
|
||
> not, because `.max()` returns `Some(0)` rather than `None`. Then, adopting
|
||
> `sylpheed-port`'s reconciliation, I wrote that the 1 530 are *"questions never
|
||
> asked"* with *"no content"* — **also wrong, and they corrected it themselves**:
|
||
> **zero records on this disc lack a timed keyframe.** The 1 530 are **static
|
||
> records**, timed with every pose at 0. A static record still declares a cycle
|
||
> length, so a nonzero `+0x08` against a largest time of 0 is a **real
|
||
> disagreement, not an absent one**.
|
||
>
|
||
> ⚠️ **92.3 % needs the qualifier it has never carried here**: it is *of animated
|
||
> records*, not *of nested records*.
|
||
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.
|
||
|
||
## ✅ 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`](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.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.
|
||
|
||
## ❔ 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 138–1220) | 1008 | −3.02 … −0.33 | **1** (frame 1161) |
|
||
| title dwell 2 (frames 5963–7025) | 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 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
|
||
```
|