Files
Sylpheed/docs/re/structures/ui-record-loop-length.md
sylph-decoder e2b7139273 re: settle the 49.6/92.3 split -- both are real, and the second explanation was wrong too
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
2026-08-31 04:01:01 +00:00

222 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.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`](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
```bash
cargo run -p sylpheed-formats --example record_loop_length
SYLPHEED_DISC=/disc cargo test -p sylpheed-formats --test ui_record_loop_length_disc
```