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
124 lines
5.8 KiB
Markdown
124 lines
5.8 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.
|
||
|
||
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
|
||
|
||
```bash
|
||
cargo run -p sylpheed-formats --example record_loop_length
|
||
SYLPHEED_DISC=/disc cargo test -p sylpheed-formats --test ui_record_loop_length_disc
|
||
```
|