The port found its boot had no black frame between the publisher and developer splashes and authored 12 units by analogy with the menus' transition quad. On the boot path that analogy has nothing behind it -- palogo_eff0.prm is a single static keyframe, so the splash bundles declare no fade quad. I had agreed with the dismissal that hid the defect: told the residual was 0.03 s against a bound built from two measured ranges plus jitter slack, I said it said more about the bound than the game. The real gap was 0.2 s. Measured in the draw stream, which separates true black from a fade tail where luminance cannot: palogo_sqex is drawn to frame 125 at alpha 7, frames 126-129 submit NO sprite quad at all, and the developer fades in at frame 130 from alpha 34. Four presented frames, the only such run in the sequence. Converted with the disc as its own clock rather than a frame rate -- this run presented at 13.1 fps against 28 elsewhere -- palogo_sqex declares alpha >= 1 for 239.8 units and is drawn in 105 frames, giving 2.284 units per presented frame, which the title capture independently corroborates at 2.231. So the gap is ~9.1 units (0.152 s), against the 12 authored; +-1 frame is 6.9-11.4. And the true black is SHORTER, since both boundary frames still carry picture. Second finding: the developer splash is ONE composited 525x259 quad at the bounding box of its three declared logos, none of whose individual sizes is ever submitted. That is why an earlier pass reported "developer splash: 0 frames". Declaration sites ruled out: the splash bundles (no fade quad) and the top-level +0x08 (a family constant, 300/60, slack 12-226 units). The executable is NOT looked at and is named as the next place rather than claimed. Also answers the port's sweep question: +0x08 canNOT settle it, because ptloop01/ptloop02 have zero slack and a zero-slack record cannot distinguish "loops" from "runs once and stops". The oracle settles it for the TITLE -- the sweep oscillates over its whole range and resets hard to the same start, once in dwell 1 and twice in dwell 2, so it does not park. The MENU is unmeasured and stays open. Tooling: GRACE and NOTAP knobs for ui_draw_capture.sh. The script taps A on "the screen changed a lot", which is also true of a fading splash -- a first run tapped through the publisher and the developer never appeared. The instrument was perturbing what it measured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
172 lines
8.4 KiB
Markdown
172 lines
8.4 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). 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.
|
||
|
||
## ✅ 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
|
||
```
|