Measured in the title draw capture, re-read with the per-quad parser. GP_TITLE build 4 declares t = 0..269, about 120 presented frames at this run's pacing. The title dwell lasted ~1100. ptcopyright declares alpha >= 1 for 106 units (t=138..244) and is drawn for 1050 frames; ptlogo1 declares an exit at t=264 and is drawn for 1095. Both vanish within three frames of the dwell ending. So the top-level clock advances through the build-in, stops inside the settle window [160,236], and holds. The exit ramp is not played on a timer -- it plays when something makes the screen leave. That is ui-settle-time.md's decode seen from the other side and observed in the running game rather than inferred from the file. A nested record keeps looping on its own clock throughout. This closes the 114-vs-120 gap, and it was my arithmetic rather than a discrepancy in the decode. The 2.231 units/frame was regressed over BUILD-IN events -- the only stretch in which the top-level clock advances -- and applied to a period measured over the settled dwell, where that clock is frozen and only the plate's own record is running. Two different clocks. The declared 120 was never in doubt from the calibration-free dark-fraction test. The 51.158-frame period is now confirmed by a second independent estimator: autocorrelation returns lag 51 with clean harmonics at 102 and 154. Its FIRST version failed its control, returning 48 for a period known to be 51.158, because it indexed by sample position where the log's frame numbers have gaps. Recorded, because the failure is the reason the second version can be trusted. Not settled: the sweeps' period. The same validated estimator disagrees between two dwells of one screen -- 515 vs 452 frames for the same family -- and a 14% disagreement within one screen is not a period. The +0x08 field cannot settle it either, since ptloop01/ptloop02 have zero slack. Blocker recorded in CONTAINER-NOTES: a single A press on the title faults the guest. Three menu-capture attempts, two ending in register dumps of 223 MB and 519 MB, against three no-input runs in the same session that all completed. And a guest fault writes an UNBOUNDED register dump to stdout on a filesystem at 91%, so any scripted button press needs a size guard. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
232 lines
12 KiB
Markdown
232 lines
12 KiB
Markdown
# The title's build-in, measured in the guest's own draw stream
|
||
|
||
**Classification: measured.** Xenia Canary, `ui_draw_capture.sh ARM=early
|
||
FRAMES=9000`, 2026-08-29. Evidence:
|
||
[`title-glow-alpha-per-frame.csv`](../data/title-glow-alpha-per-frame.csv) — the
|
||
per-frame series the numbers below come from. (The 7 MB raw draw log is a scratch
|
||
capture and is not committed.)
|
||
|
||
## What this tests
|
||
|
||
[`ui-settle-time.md`](ui-settle-time.md) decodes the title's light arc as **five
|
||
staggered two-frame flashes** (`ptlogo_back2eff1`…`eff5`) that fire around t54–66
|
||
and are gone by t110, with `ptlogo_back2eff` and `ptlogo_back2` holding for the
|
||
rest of the screen. That decode was confirmed only against a **settled** frame:
|
||
posing at the settle time matched the console, which shows the *end state* is
|
||
right and says nothing about whether the flashes ever happen.
|
||
|
||
[`ui-record-loop-length.md`](ui-record-loop-length.md) decodes the `PRESS Ⓐ`
|
||
glow as a **120-unit cycle** containing a 105-unit ramp that peaks at **alpha 80**.
|
||
|
||
Both are predictions about the running game. This is the run.
|
||
|
||
## 🔴 First, the trap that nearly produced a false negative
|
||
|
||
The obvious instrument — match a bound texture's dimensions to a decoded sprite's
|
||
— **does not work, and fails silently in both directions.**
|
||
|
||
A first pass reported that none of the five flashes are ever drawn. It also
|
||
reported `ptbase2` (640×360) and `pteff04` (1280×720) drawn during frames 75–105.
|
||
Both were wrong. Those frames are the **intro movie**: four full-screen quads per
|
||
frame sampling a pair of 640×360 planes and a 1280×720 target — a video decode
|
||
whose plane sizes happen to collide with two sprite sizes. And the flashes were
|
||
missing because the title's sprites **sample large shared texture pages**, so the
|
||
bound texture identifies a page, not an element.
|
||
|
||
Canary's own capture code says so in a comment, and the corpus had already
|
||
recorded that the settled title's textures are all 1280×768. **The identity of a
|
||
2D draw here is its vertex geometry, not its texture.** Re-run against the quad
|
||
rects, everything appears.
|
||
|
||
## The structure of the title in the draw stream
|
||
|
||
| frames | draws/frame | what |
|
||
|---|---|---|
|
||
| 2–105 | 3–5 | splashes, then the intro movie |
|
||
| **107** | **27** | a transition frame — see the caveat below |
|
||
| 109–167 | 6 → 14 | the build-in |
|
||
| 168–1217 | 10–11 | the settled title |
|
||
| 1218– | 4–5 | back to the attract loop |
|
||
|
||
The settled screen is 10–11 draws that never name a sprite. **A capture armed at
|
||
the title therefore sees nothing** — which is why `ARM=early` exists.
|
||
|
||
⚠️ **What frame 107 is, is NOT established.** An earlier version of this page called
|
||
it "the title composited, once". It is a 27-draw spike sitting between the movie's
|
||
last frame (105) and the title's first (109), it **binds no texture at all**, and
|
||
only 4 of its 27 draws log any geometry. It marks the transition; calling it the
|
||
composite was an over-read. (The regression below independently puts t=0 at frame
|
||
106.1, which is consistent with the title's clock starting here — but that is a fit
|
||
landing nearby, not evidence about what the 27 draws do.)
|
||
|
||
## The flashes are real, and they are transient
|
||
|
||
Matching quads by design-space rect over frames 100–260, and converting frames to
|
||
keyframe units with **2.346 units/frame — derived from the glow's period alone,
|
||
a different element**, taking the composite spike (frame 107) as t=0:
|
||
|
||
| element | drawn in frames | → t units | decoded |
|
||
|---|---|---|---|
|
||
| `ptlogo_back2eff1` | **130–131** | **54.0 – 56.3** | flash, peak **t54–56** ✅ |
|
||
| `ptlogo_back2eff2` | **133** | **61.0** | flash, peak t58–60 |
|
||
| `ptlogo_back2eff3` | **never** | — | flash, peak t62–64 ⚠️ |
|
||
| `ptlogo_back2eff4` | **133–135** | 61.0 – 65.7 | flash, peak **t~64** ✅ |
|
||
| `ptlogo_back2eff` / `eff5` | 134–260 | 63.3 – … | 255 at t64–66, **holds** ✅ |
|
||
| `ptlogo_back2` | 134–260 | 63.3 – … | 255 at t80, **holds** ✅ |
|
||
| `ptlogo1` | **125**–148 | **42.2** | stops moving at **t42** ✅ |
|
||
|
||
**The flashes fire inside a six-frame window and are absent from every one of the
|
||
other 155 frames sampled.** The two holders are present continuously from frame
|
||
134 onward. That is the decoded mechanism, observed.
|
||
|
||
## 🔴🔴 RETRACTED: "the game does not draw `ptlogo_back2eff3`"
|
||
|
||
**It draws it. All five flashes fire, in both title entries, in exactly the
|
||
declared stagger.** The claim was an instrument artefact, and the instrument was
|
||
mine.
|
||
|
||
| element | entry 1 | entry 2 | declared |
|
||
|---|---|---|---|
|
||
| `ptlogo_back2eff1` | 130–131 | 5953–5955 | flash t54–58 |
|
||
| `ptlogo_back2eff2` | 133 | 5955–5957 | flash t58–62 |
|
||
| **`ptlogo_back2eff3`** | **133–134** | **5957–5958** | **flash t60–64** |
|
||
| `ptlogo_back2eff4` | 133–135 | 5957–5959 | flash t62–66 |
|
||
| `ptlogo_back2eff` / `eff5` | 134 → | 5958 → | holds |
|
||
| `ptlogo_back2` | 136 → | 5962 → | holds |
|
||
|
||
Frames 133 and 134 sit at t = 60.1 and 62.3 — inside `eff3`'s declared t ∈ (58, 64).
|
||
The disc was right about every one of them.
|
||
|
||
### Why it looked absent, and why the checks that "ruled that out" did not
|
||
|
||
**A draw can batch several quads.** `indices=4` is one quad, `indices=8` is two,
|
||
`indices=24` is six — and the log dumps **only the first 8 vertices**. Taking
|
||
min/max over a line's whole vertex list therefore *merges* quads into a single
|
||
box.
|
||
|
||
`eff3` is batched with `eff4` in an `indices=8` draw. And `eff3` (788…1196) sits
|
||
**entirely inside** `eff4`'s x-range (447…1196), because the wipe family is
|
||
right-aligned — so the union of the two is **exactly `eff4`'s own extent**. The
|
||
merged box matched `eff4` to 1 px, `eff3` disappeared, and nothing looked wrong.
|
||
|
||
Every check I ran was aimed at the wrong failure:
|
||
|
||
* "sampling phase" — correctly refuted, and irrelevant;
|
||
* "a draw the log cannot see" — I counted draws with **no** geometry line. The
|
||
hiding place was draws with **partial** geometry, which I never looked for;
|
||
* "a bad position guess" — I searched for a 408-wide box. The box did not exist
|
||
because it had been merged, not because the quad had not been drawn.
|
||
|
||
**Three refutations of the wrong hypothesis do not add up to one confirmation.**
|
||
|
||
`tools/re-capture/quads_per_frame.py` now parses vertices in groups of four, one
|
||
per quad, and warns when a batch exceeds the 8-vertex cap.
|
||
|
||
### What this also explains
|
||
|
||
`eff4`'s alpha read 255 / 127 / 254 on consecutive frames — non-monotonic, which I
|
||
flagged as "the vertex-alpha identity does not generalise". It was not the
|
||
identity failing; those were **merged boxes carrying the first quad's colour**.
|
||
|
||
🔴 **A second claim, also withdrawn: that a port drawing all five flashes shows
|
||
"more sweep than the console".** The opposite is true — the console draws all
|
||
five. Drawing them sequentially at their declared times is exactly right.
|
||
|
||
⚠️ **What a frame-by-frame comparison of the build-in WILL show, and it is not a
|
||
defect on either side.** The game's timeline is 60 units/s against a 30 Hz
|
||
present, so 2 units per submitted frame; this capture ran at **2.231 units per
|
||
presented frame**. A 2-unit flash peak therefore gets about one frame here and
|
||
would get more on a faster present. **A build-in compared frame-by-frame against
|
||
this capture will disagree about which flashes appear in which frame**, and
|
||
neither side is wrong. The settled comparison is unaffected — at t=198 none of the
|
||
five is drawn.
|
||
|
||
## The glow's ramp, read out of the guest
|
||
|
||
The plate glow's quad carries a **per-vertex colour whose alpha is the element's
|
||
fade alpha**, so the ramp can be read directly rather than inferred from pixels.
|
||
|
||
* **Observed alpha range: 0 … 80. Decoded peak: 80.** Exact, and not fitted.
|
||
* ⚠️ **This identity holds for the glow and does NOT generalise.** Read the same
|
||
way, `eff4` gives 255 at frame 133, 127 at 134 and 254 at 135 — non-monotonic,
|
||
so a per-vertex alpha is not simply the element's fade alpha for every element.
|
||
The glow's agreement (peak exactly 80) is evidence for the glow, not a decoded
|
||
rule about vertex colour.
|
||
* **Period: 51.158 presented frames**, from the first to the last of **20
|
||
consecutive cycle starts** (individual periods 49–53).
|
||
* The draw is **omitted entirely** while the glow is dark — which is what the
|
||
11↔10 draw alternation in the settled title is.
|
||
|
||
Fitting the decoded 8-keyframe ramp to the 972 measured frames, with **one
|
||
disclosed free parameter** (a constant phase offset):
|
||
|
||
| curve | RMS residual, alpha levels |
|
||
|---|---|
|
||
| **the decoded ramp**, phase +2.25 units (= **+0.96 frames**, sub-frame) | **13.16** |
|
||
| the decoded ramp, no phase fit | 13.68 |
|
||
| a symmetric triangle of the same period and peak | 15.73 |
|
||
| flat at the mean | 31.13 |
|
||
| 🔴 **the decoded ramp REVERSED** | **38.18** |
|
||
|
||
The reversal control is the one that matters: if the shape carried no
|
||
information, forwards and backwards would fit equally. They differ by **2.9×**, so
|
||
the measured ramp has the decoded ramp's asymmetry — fast rise, slow fall — in the
|
||
decoded direction.
|
||
|
||
⚠️ The +0.96-frame phase is the expected bias, not a correction: a cycle start is
|
||
detected at the first frame with a non-zero alpha, and the decoded curve leaves
|
||
zero part-way through a frame.
|
||
|
||
## Two entries, different phases
|
||
|
||
The attract loop returns to the title, so one capture contains **two** build-ins
|
||
(frames 119–1220 and 5942–7025). They are the same animation and are **not**
|
||
frame-aligned: aligning them on `eff1`'s first frame, only **4 of 46** frames have
|
||
an identical quad list, and the draw-count sequences drift by a frame partway
|
||
through.
|
||
|
||
That is what makes the `eff3` result robust rather than weaker. Two independent
|
||
samplings of the same animation, at different phases, both miss it.
|
||
|
||
⚠️ Only the **first** entry has the 27-draw frame; the second goes straight from
|
||
3–6 draws into the build-in. Whatever frame 107 is, the re-entry does not repeat it.
|
||
|
||
## Reach and what is not settled
|
||
|
||
⚠️ **One run, one machine.** ~2.23 units per presented frame is *this run's*
|
||
pacing, not a property of the game — the game's own quantum is **2 units per
|
||
submitted frame** ([`ui-keyframe-time-unit.md`](../ui-keyframe-time-unit.md)). The
|
||
internal ratios are what transfer.
|
||
|
||
✅ **A validation worth stating: the fit recovers t=0 on its own.** Regressing the
|
||
observed frame of five events against their declared keyframe times (residuals
|
||
≤ 0.9 frames over t = 42…138) gives a slope of 2.231 units/frame and an intercept
|
||
at **frame 106.1** — and the composite spike, which was not part of the fit, is
|
||
frame **107**.
|
||
|
||
✅ **The 114-unit gap is CLOSED, and it was my arithmetic.** The 2.231 units/frame
|
||
is regressed over *build-in* events — the only stretch in which the top-level clock
|
||
advances — while the 51.158-frame period is measured over the settled dwell, where
|
||
that clock is **frozen** and only the plate's own record is running. Two different
|
||
clocks; there was never a reason they should agree. The declared 120 was never in
|
||
doubt from the calibration-free dark-fraction test. See
|
||
[`ui-clock-freezes-at-settle.md`](ui-clock-freezes-at-settle.md).
|
||
|
||
❔ **The absolute frame rate of this run was not measured** — Canary logged no fps
|
||
and the log has no timestamps — so nothing here is stated in seconds. It did not
|
||
need to be: every comparison above is a ratio of measured quantities.
|
||
|
||
❔ **`ptlogo_tm` was never matched.** At 37×17 it is below the rect tolerance used;
|
||
absence here is not evidence.
|
||
|
||
⚠️ **`eff5` and `ptlogo_back2eff` are the same rect at the same position and were
|
||
not separated.** Distinguishing them needs the per-vertex alpha, which the decode
|
||
says differs — not attempted.
|
||
|
||
## Reproducing
|
||
|
||
```bash
|
||
FRAMES=9000 MAXDRAWS=5000000 ARM=early tools/re-capture/ui_draw_capture.sh 900 /tmp/uicap
|
||
python3 tools/re-capture/buildin_timeline.py /tmp/uicap/xenia_re_ui_draws_01.log
|
||
```
|