The port asked for two wall-clock timestamps across the boot splashes. Measured, and the measurement's own result is that timestamps are not the invariant. The dwells are the bundles' own declared timelines: publisher t=0..255 = 4.250 s at 60 units/s, developer t=0..210 = 3.500 s. The corpus's independent screenshot timing over three cold boots gives 4.30/4.60/4.37 and 3.51/3.50/3.37 -- the developer agreeing to 1.1%, two of its three runs to 0.3%. A fresh no-input boot with a frame->wall-clock map puts the same two dwells at 5.10-5.61 s and 3.83-4.30 s, 15-20% longer than both the declared values and the corpus's runs, on the same disc and the same declared timeline. So the wall-clock dwell is an emulator-pacing artefact that varies run to run, and a port authoring seconds is authoring one run's pacing. Boundaries from the draw stream, read per quad: publisher glow frame 1, wordmark 6-119, three frames with NO sprite drawn, developer glows 123, wordmarks 140-209, intro video 216. The 3-frame gap replicates the earlier 4-frame measurement within the +-1 both are quantised to. New tool frame_clock.sh, and its limitation found by its own control: it resolves to one BUFFER FLUSH, not one frame. The capture writes through a C++ ofstream, so tail sees the log in bursts -- 69 of 125 samples showed no advance and the rest jumped 7-15 frames. Naive interpolation inside a burst made the apparent rate swing between 0.0164 and 0.0316 s/frame, which is the flush and not the guest. Frames 119 and 123 fall in one burst, so the inter-splash gap is not separable by this clock at all. Everything is quoted as brackets and the point estimates were withdrawn before being reported. palogo_anima never appears in the log and is NOT reported as undrawn: the developer bundle batches 7 elements into one draw and only the first two quads are logged. That is the trap that produced the eff3 false negative, so it is named rather than claimed. Also records the port's correction: ptcopyright has 105 instants with alpha >= 1 (t=139..243) against 105.89 units of span; I had quoted the rounded span. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2389 lines
155 KiB
Markdown
2389 lines
155 KiB
Markdown
# Handoff — what the menu port needs, and where it stands
|
||
|
||
The single page the **port agent** reads. Everything here is produced by the
|
||
container agent's reverse engineering; nothing here is a design decision about
|
||
the port itself.
|
||
|
||
Keep it current. It is a summary with links into `docs/re/`, not a second copy of
|
||
the findings — but an answer that is not reachable from this page has not been
|
||
delivered.
|
||
|
||
## How to read an answer
|
||
|
||
Every row below is one of exactly three things, and the distinction is the point:
|
||
|
||
| | meaning | what the port should do |
|
||
|---|---|---|
|
||
| **decoded** | a field on the disc, with a disc-wide check | read it from the data |
|
||
| **measured** | not on the disc in any form we found, but the running game does *this* | hardcode it, and cite this page |
|
||
| **undecodable** | we looked in these places, it is not there, here is the reach of the negative | author it by hand, knowingly |
|
||
|
||
There is no fourth kind. If a row says *measured* or *undecodable*, the port is
|
||
**authoring** that value, not transcribing it — and it should be kept somewhere a
|
||
human can see it is a human decision, so that when it is later decoded the
|
||
authored version can be deleted.
|
||
|
||
## 🔴 2026-08-29 — A KEYFRAME'S TIME COMES BEFORE ITS POSE. Change `pose_at`.
|
||
|
||
**This is the one you said had a wide blast radius, and it is bigger than a
|
||
shift.** Read
|
||
[`ui-keyframe-record-layout.md`](../re/ui-keyframe-record-layout.md) before
|
||
touching `screen_view.gd`.
|
||
|
||
A placement group is an 8-byte header `{u32 element_index, u32 frame_count}`
|
||
followed by `frame_count` records of **40 bytes**, each `{u32 time; 36-byte
|
||
pose}`. The time word **precedes** the pose it belongs to. Our parser's window
|
||
opened at the pose — four bytes into the record — and then read the word at its
|
||
`+36` as that pose's time, which is the *next* pose's.
|
||
|
||
So neither of the two readings the corpus was arguing between was right:
|
||
|
||
* the old default (`+36` is this pose's time) is off by one;
|
||
* `SYLPHEED_KF_TIME_SHIFT=1` had the association right but left **pose 0
|
||
untimed**, because it never asked what the group's "lead-in word" was. It is
|
||
pose 0's time.
|
||
|
||
`SYLPHEED_KF_TIME_SHIFT` is gone. `SYLPHEED_KF_TIME_LEGACY=1` restores the old
|
||
reading if you want to A/B.
|
||
|
||
**Disc-wide, 33 archives, 13 991 groups, each test with a control:**
|
||
|
||
| | corrected | control / old |
|
||
|---|---|---|
|
||
| lead-in prepended to the shifted times is non-decreasing | **13 991 / 13 991** | — |
|
||
| a non-zero lead-in is strictly below the next time (5 058 of them) | **5 058 / 5 058** | another group's lead-in: 70.9 % |
|
||
| multi-segment alpha ramp runs at a constant `dα/dt` | **857 / 1 540** | old reading: **0 / 1 042** |
|
||
|
||
The last row is the one that cannot be argued with. Interpolation between
|
||
keyframes is linear; under the old reading **not one** multi-keyframe ramp on the
|
||
whole disc comes out at a constant rate.
|
||
|
||
### What it changes for you, by the list you sent me
|
||
|
||
* **`pose_at` (line ~184).** The comment *"a keyframe is the start of a ramp
|
||
toward the next"* is still true as a statement about ramps. What changes is
|
||
**which time each pose is at**: pose `k`'s time is the word before it. Every
|
||
screen's build-in timing moves.
|
||
* **The final pose now has a time.** Anything you authored to cover "the last
|
||
keyframe carries no time" — an exit ramp with no end, `exit_ramp_units = 24` —
|
||
can come out and be read instead. `exit_ramp_units` is now a decodable number,
|
||
not an authored one.
|
||
* **`settle_units` / `settle_time`.** `rest.t` still is not when a screen settles
|
||
(`5b0a6e6` stands), but its *value* moves. Re-derive it.
|
||
* **`spin_period_units`.** The focus ring's first keyframe's declared `t=120` is
|
||
now the time that pose is **reached**, not left. Check which end of the ring's
|
||
group you were reading.
|
||
* **The `PRESS Ⓐ` plate.** `a=255` is at **`t=238`** — the corrected reading
|
||
agrees with the old default here, not with the old shift's 236. Your `5b0a6e6`
|
||
note is unaffected.
|
||
* **`ramp: "linear"` in `authored/timing.json` — KEEP IT.** Interpolation between
|
||
two keyframes is linear, and this work reinforces that rather than touching it.
|
||
⚠️ But 44 % of multi-segment ramps still are not constant-rate under the
|
||
corrected reading, and that is not a defect: authors shape a curve by placing
|
||
extra keyframes unevenly. Your `_lerp_pose` is right; do not add an easing
|
||
function.
|
||
|
||
### And it costs nothing on the static composites
|
||
|
||
This is the change the corpus previously declined to make. `SYLPHEED_KF_TIME_SHIFT=1`
|
||
moved `GP_TITLE` build 7 by 13.1 % of its pixels; with pose 0's time restored:
|
||
|
||
* all **12** `GP_TITLE` builds render **byte-identical** PNGs under both readings;
|
||
* over **217** builds in six archives, exactly **two** elements pick a different
|
||
`rest()` pose — and both times the two candidates are equally invisible (α = 0),
|
||
so no render changes.
|
||
|
||
So `screen render` is still the reference you diff against, unchanged. **If your
|
||
static screens move, that is your bug, not this change.**
|
||
|
||
## ✅ 2026-08-29 — `GP_TITLE` **entries 0, 1, 12, 15** are the LOADING SCREEN (your ask #2)
|
||
|
||
🔴 **Address these by PAK ENTRY, not by `is_build` ordinal.** The port raised
|
||
this and it is a real foot-gun: over the twelve bundles `is_build` accepts
|
||
(entries 0,1,2,3,4,5,6,7,8,9,12,15) the *ordinals* 10 and 11 are **entries 12 and
|
||
15**, while **entries** 10 and 11 are `palogo_sqex` and
|
||
`palogo_gamearts`/`seta`/`anima` — the two splashes. A key written `"10"`/`"11"`
|
||
from an ordinal therefore names the publisher wordmark and the developer logos as
|
||
loading screens, and everything still validates. The loading bundles are
|
||
**entries 0, 1, 12, 15** (= build ordinals 0, 1, 10, 11).
|
||
|
||
**Decoded** — the authors' own element names, straight out of the declaration
|
||
table. Every element of all four bundles is prefixed `pgloading_`: the 7-element
|
||
pair (entries 0/1) is the plain plate, the 10-element pair (entries 12/15) adds
|
||
`pgloading_eff00.prm`, `pgloading_loop5.rat` and `pgloading_baseeff.t32` over a
|
||
circuit-line background. `DELTASABER / SYLPHEED A.I.` is the caption art on
|
||
`pgloading_str.t32`, not the screen's identity.
|
||
|
||
⚠️ **Do not name them `LOADING` / `LOADING2` in an asset path.** The executable
|
||
does name five title-side screens — `TITLE_SCREEN`, `BUTTON`, `TITLE_MENU`,
|
||
`LOADING`, `LOADING2`, verified in the image at `sub_821C4EB0` — so there really
|
||
are two, but nothing observed says which bundle is which. 🟡 undecided.
|
||
|
||
🟡 **Which member of each pair is English: the first half of the data segment.**
|
||
All eight pairs put exactly one member in each half of `GP_TITLE.p00`, and all
|
||
three pairs whose language is visible (entries 4/7, 5/8, 6/9) put English in the
|
||
first. So **entries 0, 2, 4, 5, 6, 10, 11, 12 are English**; entries 1, 3, 7, 8,
|
||
9, 13, 14, 15 Japanese. 🟡 not
|
||
✅ — the three pairs this is *used* for are exactly the three no capture can
|
||
check, and nothing in the bundle bytes differs between those twins at all.
|
||
[`ui-title-build-map.md`](../re/ui-title-build-map.md)
|
||
|
||
## ✅ 2026-08-29 — the disc is back in the decoder container; the red banner that stood here is withdrawn
|
||
|
||
**This supersedes the "the decoder container has no disc" banner** written at
|
||
commit `b9aca6a` (10:42 UTC). That diagnosis was true for the container it was
|
||
written in, and a human has since fixed it: this container's PID 1 started at
|
||
**11:07:38 UTC**, 25 minutes later, and it has the disc mounted.
|
||
|
||
Verified, not assumed:
|
||
|
||
| | |
|
||
|---|---|
|
||
| `/disc` | a real read-only bind mount on device 2050 (`/` is device 92), **6.2 GB**, 74 entries under `dat/`, `default.xex` present |
|
||
| `/iso/game.iso` | present, 7 835 492 352 B |
|
||
| end-to-end | `sylpheed-cli screen list $SYLPHEED_DISC/dat/GP_TITLE.pak` returns **12 screen builds**, matching [`ui-title-build-map.md`](../re/ui-title-build-map.md) |
|
||
|
||
⚠️ **`sylph-doctor` still reports "no ISO" and "no extracted disc", and it is
|
||
wrong.** It looks only under `/work` (`find /work -maxdepth 2 -iname '*.iso'`
|
||
and `-d /work/sylph_extract/dat`); it never consults `$SYLPHEED_DISC`. Do not
|
||
take its two ✖/! lines as evidence about the disc — the disc is at `/disc`
|
||
and works. Same trap for `find / -xdev`, which by definition cannot cross into
|
||
a bind mount on another device, and which is what the withdrawn banner ran.
|
||
|
||
**What that means for you:** the decoder can boot the oracle, run
|
||
`sylpheed-cli` against a pak, run the disc-gated tests and read the executable
|
||
again. New measurements are available; ask for them.
|
||
|
||
## ✅ 2026-08-29 — the "third sub-wave" on a music bank was OUR reader, and it is fixed
|
||
|
||
**You were right to refuse to choose which one to drop.** `BGM_103.slb` really
|
||
does return three from `sound_bank_riffs` — and the third is the **bank header**,
|
||
not a stem. Our own `to_xma_riffs` was emitting it.
|
||
|
||
The cause is arithmetic, not a judgement call: the hybrid branch derives a
|
||
leading packet stream's start as `first_riff % 2048`, which is correct only when
|
||
the bank header is smaller than one XMA1 packet. A music bank's header is exactly
|
||
**five** packets (10 240 B), so the modulus returned 0 and the whole header came
|
||
back as sub-wave 0. Voice banks are unaffected — their headers really are shorter
|
||
than a packet, which is why the branch looked right for two months.
|
||
|
||
Checked before believing it, three ways:
|
||
|
||
* **disc-wide** — of `sound.pak`'s 9 519 entries, **28** carry a header at offset
|
||
0 (ids 1001–1023, 1101–1105 — every music bank), and on **28/28** the header's
|
||
own declared length ends *exactly* at the first `RIFF`. **Zero** have a gap, so
|
||
a header and a leading packet stream never coexist on this disc, and **zero**
|
||
false positives among the other 9 491;
|
||
* **decode control, same chain, same bank** — the emitted region gives **0.009 s**
|
||
of PCM; the same bank's real wave 0 gives **87.744 s** against a declared
|
||
87.75. It is also 99.1 % zero bytes;
|
||
* **the oracle already said two** — the XMA probe at the main menu saw exactly
|
||
two streams, of 3 876 864 and 3 930 112 B, which are `BGM_103`'s two declared
|
||
wave sizes.
|
||
|
||
**What you should do:** bump your `sylpheed-formats` pin to the tag below and
|
||
delete the manifest warning's special case — `sound_bank_riffs` now returns
|
||
**2** for every music bank, and your "count != 2" warning becomes a real
|
||
invariant rather than a symptom. ⚠️ Do **not** apply a "drop the smallest
|
||
sub-wave" rule; on a voice bank the leading region is genuine audio and dropping
|
||
it is the `VOICE_D_453` bug all over again.
|
||
|
||
[`structures/slb-bank-header-not-a-wave.md`](../re/structures/slb-bank-header-not-a-wave.md)
|
||
|
||
## ✅ 2026-08-29 — the interactive title is reachable again, and the "emulator-blocked" banner in MISSION is withdrawn
|
||
|
||
Two consecutive boots reached the interactive title **with no pad input at all**,
|
||
passed through the attract loop in ~3.5 minutes, took Ⓐ to the main menu and Ⓑ
|
||
back. The standing negative ("three runs, two locales, two launch paths, ~35
|
||
minutes of emulator time, no interactive title") does not hold in this container.
|
||
|
||
❔ **Why it changed is not established.** The container came up with **no Xenia
|
||
storage root at all** — no profile, no `xconfig.settings`, no shader cache — so
|
||
run 1 created one with canary's `--create_profile_if_none`. That is a correlation
|
||
across two runs, not a cause, and it is written down so the next session can test
|
||
it rather than re-derive the reachability.
|
||
[`capture-harness-status.md`](../re/capture-harness-status.md)
|
||
|
||
**What it means for you:** the oracle is live. Anything you need timed or
|
||
observed on the five screens can now be asked for and taken, including the two
|
||
items MISSION parks as emulator-blocked.
|
||
|
||
## ✅ 2026-08-29 — three answers from one oracle session (the port's asks 1, 2 and 3)
|
||
|
||
* **1 — the focus ring SPINS CONTINUOUSLY. Period 2.18 s wall-clock; author it
|
||
as 120 units = 60 frames = 2.00 s at 30 Hz.** It does not ramp once and stop.
|
||
Measured with no angle estimated anywhere — the angular estimator written for
|
||
this **failed its own control** (a synthetic 30° came back as 0°) and was not
|
||
used. What settles it instead: total annulus brightness is conserved to
|
||
**0.4 %** while individual angular bins swing by **24** — brightness moving
|
||
*around* the ring, which excludes a pulse — and the profile's autocorrelation
|
||
has **eight evenly spaced peaks, mean 2.177 s**, over nine revolutions.
|
||
⚠️ Do not read the committed 20 s mean image as a frame: the spin averages to
|
||
a uniform circle, which is why it looks headless. Five single frames 4 s apart
|
||
show the head at five different angles.
|
||
[`focus-ring-spin-measured.md`](../re/focus-ring-spin-measured.md) ·
|
||
[frames](../re/captures/focus-ring/ring-single-frames-4s-apart.png)
|
||
|
||
* ✅ **And the ring is the ONLY thing that moves on the settled main menu.**
|
||
Temporal std over 20 s untouched is **exactly 0.000** on every unfocused
|
||
button, on the labels and on the `ptmsg` footer. Static menu + spinning ring
|
||
draws everything that moves.
|
||
|
||
* **3 — the idle timer that made your Ⓑ rule unprovable is REFUTED on the main
|
||
menu.** Held untouched, the menu stayed put for **≥ 60 s** (49 samples, menu
|
||
correlation never leaving 0.9245–0.9249), against the "~8–10 s idle returns to
|
||
the title" this page carried. ✅ That timer is real but belongs to the
|
||
**title**, not the menu — the corpus had it attached to the wrong screen.
|
||
🟡 Ⓑ itself: delivered (Canary logs `vk=5801`), and in both runs the only input
|
||
in ≥ 100 s, followed by the title. **Ordering measured, timing not** — keep
|
||
Ⓑ→title, now better supported than authored.
|
||
[`menu-idle-and-b-2026-08-29.md`](../re/menu-idle-and-b-2026-08-29.md)
|
||
|
||
* **Your new #1 — the boot title shows build 4 FIRST, and the plate arrives
|
||
after.** It is your third option, not the first two. Green-Ⓐ glyph count on the
|
||
boot title went **154 → 781**, and 154 is the same reading the committed
|
||
`live-title-build4-no-plate.png` gives (159) while plate titles give
|
||
753/977/1493.
|
||
✅ **So `ScreenView` does have to draw two builds at once, and your `--boot`
|
||
end state is NOT plate-free** — that is the structural answer you said this
|
||
question decides, and it is unchanged.
|
||
🔴 **But my INSTRUCTION was wrong and you refuted it — do not author a delay at
|
||
all.** I said "when build 4 has settled, wait 2.13 s, composite build 2". Build
|
||
2 has a group of its own, and starting that group at settle puts the plate at
|
||
settle + 2.13 + 3.97 s. **Correct instruction: run build 4 and build 2 on ONE
|
||
clock, started together, and play both groups from their own keyframes.** The
|
||
plate then arrives at its declared `t=238` with nothing authored.
|
||
🔴 **The premise that broke it is yours and it will bite again: `rest.t` is NOT
|
||
when a screen settles.** It is the last *hold* keyframe before the exit.
|
||
`ptlogo1` has `rest.t = 251` and stops moving at **`t=42`**. The title's visible
|
||
build-in is over at **`t≈118`**, where `pteff01`, `pteff02.prm` and
|
||
`ptlogoall_eff` all end their ramps together — and `238 − 118 = 120 units =
|
||
**2.000 s**`, which is the 2.13 s I measured. The number was on the disc.
|
||
⚠️ **If you author a gap anyway, author 120 units, not my 2.13 s.** 120 units in
|
||
2.135 s is the game presenting at **28.06 / 28.14 fps** against a nominal 30 —
|
||
and the corpus had already measured the idle title at **28.5 fps**,
|
||
independently and before these runs. My wall-clock was this emulator's frame
|
||
rate baked into a game constant; a port at a true 30 Hz would be visibly late.
|
||
✅ That it is presentation rate and not the game is checkable in the same two
|
||
runs: *first pixels → settle* is 1.643 s and 2.131 s (a 30 % spread) while
|
||
*settle → plate* is 2.138 s and 2.132 s. Frames are dropped during the
|
||
build-in, not during the hold.
|
||
Pulse the plate at ≈ **2.24 s** (four intervals: 2.12 / 2.19 / 2.34 / 2.31),
|
||
which replicates the corpus's ≈2.3 s rather than replacing it.
|
||
[`title-plate-delay-measured.md`](../re/title-plate-delay-measured.md) ·
|
||
[figure](../re/captures/ui-timing/plate-onset-two-runs.png) ·
|
||
[run 1](../re/data/plate-timing-run1.tsv) · [run 2](../re/data/plate-timing-run2.tsv)
|
||
|
||
* 🟡 **Your black hold survives a real clock — keep 0.17–0.23 s.** Measured on
|
||
the Ⓐ path in both runs, the frame is pure black (surface mean 0.070) for
|
||
**0.14–0.30 s** and **0.14–0.27 s**. At an 0.125 s sample interval that is as
|
||
tight as this instrument goes, and it brackets both your authored value and
|
||
the file's declared 12 units (0.20 s). It is the one authored constant you
|
||
ship that a measurement now agrees with.
|
||
|
||
* 🔴 **The Ⓐ→menu latency is STILL not a number you may have, and now I know
|
||
why.** Both runs contain a **frozen frame** on the Ⓐ path — 14 frames (1.53 s)
|
||
and 12 frames (1.39 s) held at surface mean **26.626**, agreeing between two
|
||
independent runs to six decimals. Run 2 had stream restarts disabled for the
|
||
whole window, so it is **not** the capture path: the guest starts the fade,
|
||
re-presents one frame for ~1.4 s, then shows the full title again and fades
|
||
properly. That is a **load stall**, and the Ⓑ path — nothing to load — has none.
|
||
So any Ⓐ→menu figure from this harness is an emulator load time. Your
|
||
zero-dwell sequencer is the right call; do not add one.
|
||
|
||
* 🔴 **Four durations I took the same day are WITHDRAWN, including the plate
|
||
delay.** `screen_match.classify_array` costs **1503 ms/frame**; a probe running
|
||
it per frame drained an 8 fps stream at **0.64 fps**, so its frames were stale
|
||
and increasingly so. It manufactured "plate 24.66 s after the title art",
|
||
"Ⓑ→title in 15.58 s", "Ⓑ→title in 25.60 s" and "Ⓐ→menu in 20.26 s". The tell:
|
||
a transition, a press and a fade do not share a duration — a backlog does.
|
||
A backlog **preserves ordering and destroys durations**, which is exactly why
|
||
the sequence results above stand and the timings do not. Fixed (`fast=True`,
|
||
38–75 ms, re-controlled 8/8 on both paths, agreeing to ±0.005); the ring's
|
||
numbers are unaffected and that was checked, not assumed.
|
||
✅ **One of the four is now re-taken properly** — the plate delay, above. The
|
||
probe that took it costs **8.7 ms/frame** (173× cheaper) and both runs sampled
|
||
at **7.97 / 7.98 fps against a requested 8**, so there was no backlog to
|
||
destroy them.
|
||
|
||
## ✅ 2026-08-29 — the paint-order tie-break costs your screens ZERO pixels (Q3 closed for you)
|
||
|
||
You challenged the advertised blast radius of the unknown tie-break and were
|
||
right; the census that came back said **24 overlapping tied pairs** and admitted
|
||
nobody had measured how many of them *change a pixel*. Measured now.
|
||
|
||
**Overlap was an upper bound and it was loose.** Each bundle was rendered twice,
|
||
once in the derived order and once with one tied pair swapped, and diffed —
|
||
elements sharing a layer key are contiguous in the derived order, so a swap
|
||
paints nothing else in between. Every entry ran a **control** first: a swap of an
|
||
overlapping pair with *different* keys, which must move pixels (it moved 36 305
|
||
to 771 479 px, max Δ 254). Where no control was available the page says so.
|
||
|
||
* ✅ **On `EXTRAS` (entries 6/9) and the main menu (5/8): 0 px.** The tied
|
||
`ptframe` pairs each ink ~3 600 px and **share 0**. The bounding-box overlap
|
||
was an artefact of approximating an element's rect as pivot × 2. Layers that
|
||
never touch the same pixel cannot be mis-ordered under *any* blend, so this
|
||
does not depend on our compositor being right.
|
||
* ✅ **The splashes have no ties; the title and developer splash use a measured
|
||
order.** So across all five screens the tie-break's cost is **zero**, not "one
|
||
drawable pair, consistent with a capture".
|
||
* 🔴 **A claim of ours is withdrawn.** This page's source said a wrong tie-break
|
||
"can be wrong by a whole layer", from the geometry that `ptlogo_back2eff5`
|
||
*fully contains* two other glows. Rendered, that swap moves 6 390 px by a
|
||
maximum of **Δ2 out of 255**. The glows are near-transparent; containment is
|
||
not occlusion. Nobody had rendered it before asserting it.
|
||
* 🟡 **Outside your set**, the worst tie-break cost anywhere in `GP_TITLE` is
|
||
**Δ3/255** (the Japanese title, entry 7). ⚠️ Those Δ figures *do* assume our
|
||
alpha blend — what they rule out is a structural error, a layer appearing or
|
||
vanishing, not a shading one.
|
||
|
||
[`ui-paint-order-derived-check.md`](../re/structures/ui-paint-order-derived-check.md#-what-the-tie-break-actually-costs-in-pixels-2026-08-29) ·
|
||
[data](../re/data/paint-order-tie-pixel-cost.txt) ·
|
||
tool `cargo run -p sylpheed-formats --example tie_break_pixel_cost -- dat/GP_TITLE.pak`
|
||
|
||
## 🟡 2026-08-29 — your voice-region third chunk is NOT the BGM bank-header case
|
||
|
||
You asked me to look rather than take your word, so I did, disc-wide rather than
|
||
on the asset that raised it. **The two 2+1 signatures are different structures**,
|
||
and the discriminator is mechanical — [`voice-region-leading-chunk.md`](../re/structures/voice-region-leading-chunk.md),
|
||
census at [`data/voice-region-chunk-census.txt`](../re/data/voice-region-chunk-census.txt).
|
||
|
||
Over all **95** English movie-voice regions the manifest binds:
|
||
|
||
* **78** open with a **bank header** — `bank_header_len` fires, 10 240 B = 5
|
||
packets exactly, every time. That is the BGM case and it is already consumed.
|
||
* **17** open with a **leading headerless stream** — `bank_header_len` is `None`,
|
||
and **all 17** have a length ≡ **1392 (mod 2048)**, the disc's own derived data
|
||
offset. That is a whole number of XMA1 packets after a 1392-byte preamble.
|
||
* **0** begin at a `RIFF`.
|
||
|
||
⚠️ **Counting chunks cannot tell you which case you are in.** Eight bank-header
|
||
regions *also* yield three chunks. Test `bank_header_len`, not `riffs.len()`.
|
||
|
||
🔴 **And "it is the previous cue's audio" fails a test.** Take each leading span
|
||
and ask whether any other resolved region covers it: **0 of 17**, 0.0 % on every
|
||
one. The test can find overlaps — the regions themselves have 16 overlapping
|
||
pairs and 60 exactly-adjacent boundaries, and 73 of 78 bank-header regions start
|
||
exactly where another region ends — it just finds none here.
|
||
|
||
✅ **RESOLVED, same day, by your own suggestion — and my leading hypothesis was
|
||
wrong.** You said: widen the enumeration past the 95 manifest-bound movies and
|
||
the byte-span test settles it with nobody listening. It does. Scanning the stream
|
||
for **every** trailer descriptor (287 found in a 116.2 MB window; all 287 carry
|
||
an id the 4 280-name registry names) gives the complete cue partition, mission
|
||
lines included.
|
||
|
||
🔴 **The leading chunk is the MOVIE'S OWN dialogue — 17 of 17.** Every leading
|
||
span is bracketed by `desc(N-1) .. desc(N)` where `desc(N)` is that movie's own
|
||
cue id (`ADV` → 1600 `VOICE_ADV`, `S00A` → 1501 `VOICE_S00A`, …). **Zero** are
|
||
in-mission `VOICE_D_*` lines. So "drop it, it is somebody else's audio" is dead.
|
||
|
||
✅ **And the mechanism is a guard in our own resolver.**
|
||
`resolve_movie_voice_region` takes the predecessor trailer as the region start,
|
||
guards it with `end - start < 1_500_000`, and falls back to the `.slb` anchor
|
||
when that fails. Cues with a true span ≥ 1.5 MB: **17, of which 17 are
|
||
stream-opening.** Cues under it: **78, of which 0 are.** Perfect discrimination
|
||
both ways. A long cue's region starts mid-cue, at the anchor, and the bytes from
|
||
there to the next `RIFF` become the leading chunk.
|
||
|
||
⚠️ **But do NOT turn that into "the export truncates N seconds".** Your own
|
||
decode has `ADV`'s region at 359 s against a 137 s movie — it over-covers, so the
|
||
byte↔time mapping is not linear and I will not convert 504 464 B into missing
|
||
dialogue. I have no XMA1 decoder here to check.
|
||
|
||
✅ **CLOSED, later the same day — and here is the confirmation you asked for
|
||
before acting.**
|
||
|
||
**Yes: drop the leading chunk. It is a DUPLICATE, not a truncation.** Your
|
||
exporter's current behaviour is right, and now for a stated reason rather than a
|
||
hedge — make the manifest note cite
|
||
[`voice-region-leading-chunk.md`](../re/structures/voice-region-leading-chunk.md).
|
||
|
||
Your correlation result and a structural check of mine agree by independent
|
||
routes. Mine used byte rates and no decoder: the full leading stream for `ADV` is
|
||
504 464 + 808 304 = **1 312 768 B**, and at chunk 0's byte rate (9 559.7 B/s) that
|
||
is **137.323 s** against chunk 1's measured **137.324 s** — 1 ms over 137 s.
|
||
|
||
✅ **And the byte structure settles the shape disc-wide.** Counting stream starts
|
||
inside every cue's true span: **258 spans hold 1 stream, 28 hold 3, and nothing
|
||
holds 2 or any other number.** All 20 spans ≥ 1.5 MB are 3-stream. So:
|
||
|
||
| your 95 regions | cue shape | chunks you get |
|
||
|---|---|---|
|
||
| 70 | 1-stream | 1 |
|
||
| 8 | 3-stream, short | 3 (bank header) |
|
||
| 17 | 3-stream, long — our guard clips it | 3 (headerless leading) |
|
||
|
||
`359 s = 84.55 + 137.32 + 137.32`. **The 2.6× is three presentations of one
|
||
take**, one clipped by our own guard. That was the last open one and it is shut.
|
||
|
||
🔴 **One change you have NOT made yet and should: stop summing chunk 1 and chunk
|
||
2.** They are the same take, not two stems — `ADV` chunk 2 is 0.60 × chunk 1
|
||
(residual 26.8 dB down), `S00A` chunk 2 is digital silence. Summing a take with a
|
||
scaled copy of itself adds ~4 dB and colours it. **Take one stream.**
|
||
|
||
🔴 **Which stream — my instruction was self-contradictory and you were right to
|
||
flag it.** I wrote "the highest-rate, highest-gain one (chunk 1)". Those two
|
||
criteria select *different* streams: on `ADV` chunk 1 is 0.0 dBFS at 1 118 268 B
|
||
and chunk 2 is −8.3 dBFS at 1 171 516 B. You implemented "highest rate"
|
||
faithfully and got the quieter one — the opposite of what the parenthetical
|
||
meant. Withdrawn; keep it as an authored 🟡 exactly as you have it.
|
||
|
||
✅ **What I can decode is the rate, and only the rate.** The `fmt ` chunk is a
|
||
32-byte `XMAWAVEFORMAT`; `+0x20` is a declared `PsuedoBytesPerSec` — 8 142 and
|
||
8 530 for `ADV`'s two, matching the computed rates to 0.02 %, and 48 000 Hz at
|
||
`+0x24`. ❔ **But `wEncodeOptions` (`0x10d6`), channel count and channel mask are
|
||
byte-identical across the presentations.** Nothing in the header ranks them. Your
|
||
"more bytes is consistent with a better encode and also with the opposite" is
|
||
exactly right and the file will not adjudicate it.
|
||
|
||
🔴 **Your dual-mono explanation does NOT generalise — but your decision survives
|
||
it.** Checked disc-wide over all 28 three-stream cues
|
||
([`data/voice-three-stream-sizes.txt`](../re/data/voice-three-stream-sizes.txt)):
|
||
if stream 3 were systematically the same take with its channel duplicated, its
|
||
size ratio to stream 2 would be tight. It runs **min 0.0778, median 1.2565, max
|
||
2.9163, sd 0.5057**, with only **12 of 28** within 15 % of 1.0 — a 37× spread.
|
||
Declared rates scatter with them (`S06A` 5 661 vs 16 513 B/s). ✅ Your `ADV`
|
||
channel measurement stands and **your choice of "loudest" is untouched**, because
|
||
it is a per-asset content measurement rather than a structural rule. What must
|
||
not harden is the *explanation*: "more bytes means a duplicated channel" is true
|
||
of `ADV` and is not a fact about the format.
|
||
⚠️ Two curiosities: `S12B`'s three streams are **byte-size identical** (14 396
|
||
each), and `BIRD_224` is 3-stream while being a **non-movie** cue.
|
||
|
||
⚠️ Also: **do not trust `sylpheed-cli audio info` on these.** Its "16 channels /
|
||
4310 Hz / 2-bit" is `wBitsPerSample`, `wEncodeOptions` and the channel fields
|
||
read at wrong offsets. Its reader is misaligned for XMA1.
|
||
|
||
❔ *Why* the disc stores three presentations is not answered.
|
||
|
||
⚠️ On your offer to convert the 504 464 B constant: **don't spend the decode.** It
|
||
is structural, not proportional — identical on all 17 despite differing
|
||
durations. `ADV`'s proportional prediction lands within 8 bytes of it, which is a
|
||
coincidence (`S00A`'s is 4 305 B out) and I nearly built on it.
|
||
|
||
🔴 **And the reason I gave for not concatenating was WRONG — withdrawn the same
|
||
day.** I wrote that chunks 1 and 2 are "the two-stem pattern, not consecutive
|
||
segments". That claim was yours, I adopted it on equal duration alone, and you
|
||
then refuted it by decoding: `S00A` chunk 2 is **digital silence** (peak −∞) and
|
||
`ADV` chunk 2 is **0.60 × chunk 1**, residual 26.8 dB down. Equal duration was a
|
||
shape match, and Q10's *music* census should not have been carried across to
|
||
voice on it. ✅ Do not concatenate — that part survives, measured (359 s against a
|
||
137 s movie) — but not for the stated reason, and ❔ what `ADV` chunk 2 actually
|
||
is stays open and is mine.
|
||
|
||
⚠️ Worth naming the failure mode: this was asserted in one place, adopted in a
|
||
second, and the second citing the first would have made it look corroborated by
|
||
two documents. It was caught only because you measured your own claim.
|
||
|
||
## ✅ 2026-08-29 — `settle_time()` MEASURED. Stop pacing off `rest.t`.
|
||
|
||
Your top ask, from one cold boot — container had no Xenia storage root at all, so
|
||
this is a fresh profile with **no shader cache**, the slowest case.
|
||
[`boot-settle-times-measured.md`](../re/boot-settle-times-measured.md) ·
|
||
[frame log](../re/data/boot-settle-run1.tsv) · `tools/re-capture/settle_analyse.py`
|
||
|
||
**Classification: measured.** None of it is on the disc as a settle time; you are
|
||
authoring these from this page.
|
||
|
||
| | measured | |
|
||
|---|---|---|
|
||
| title build-in (first ink → art fully drawn) | **0.23 s** | 1.63 s if you start from where the crossfade begins |
|
||
| **title settled → `PRESS Ⓐ` plate on** | **2.247 s** | matches the disc's declared **120 units** |
|
||
| plate pulse period | **≈2.37 s** | |
|
||
| **main menu build-in** | **0.531 s** | |
|
||
| **Ⓑ → title** | **0.482 s** | |
|
||
| Ⓐ → menu | 3.763 s | 🔴 **do not author** — contains a 1.53 s emulator load stall |
|
||
|
||
🔴 **`rest.t` is confirmed to be the wrong landmark, with the number you need.**
|
||
The title's `rest.t` is 251 units = **4.183 s**; its art is finished at ~2 s and
|
||
the plate is on at **2.247 s**. Your sequencer holds the title about twice as
|
||
long as the game does. That is the defect you described, measured.
|
||
|
||
**Instrument controlled first**: 9/9 on the content classifier including the
|
||
movie-frame and difficulty-screen negatives, 4/4 on the plate detector; the run
|
||
sampled 7.99 fps against a requested 8 with an independent grab cross-checking
|
||
every 20 s, so this is not the backlog mode that voided four durations before.
|
||
|
||
🟢 **A refutation attempt of mine that FAILED, and you should know it failed.**
|
||
The probe's own marks gave a plate delay of **3.203 s** against the corpus's
|
||
2.13 s — a 50 % disagreement I expected to be a real cold-cache effect. It was my
|
||
instrument. The plate **pulse period** is an internal clock for presentation
|
||
rate, and it measures 2.369 s here against the corpus's ≈2.3 s, so the run is not
|
||
slowed; re-measured from content, the delay is **2.247 s**. ⚠️ The cause is worth
|
||
your knowing: the probe's `title_static` mark fires during the **crossfade out of
|
||
the attract movie, before the wordmark has drawn** — glyph was still 0 when it
|
||
fired. Do not use `title_static` for a duration.
|
||
|
||
🟢 **And your load stall reproduces a third time, on a cold cache** — 13 frames,
|
||
1.53 s, surface mean 26.631, against the earlier 14/1.53 s and 12/1.39 s at
|
||
26.626. So it is not a warm-cache artefact, and "do not author an Ⓐ→menu dwell"
|
||
stands. ⚠️ Honest qualification: the earlier pair agreed to six decimals; mine
|
||
agrees to three.
|
||
|
||
⚠️ **Reach: one run — and 🔴 one of my two arguments for trusting it is
|
||
withdrawn.** I said the plate *pulse period* proved the run was not slowed. That
|
||
estimate rests on **one interval** at a 125 ms sample interval (±6.7 %), and
|
||
re-running the trough-picking gives 2.628 s rather than the 2.369 I quoted —
|
||
**+17.3 % against the corpus's 2.24 s**, not agreement. It cannot resolve a
|
||
real-time factor below ~7 % at all and should not have carried the argument.
|
||
✅ The conclusion survives on the other leg: the content-measured **2.247 s**
|
||
agrees with three independent prior readings (2.13 / 2.132 / 2.138), and both its
|
||
landmarks are sharp content transitions. A 17 % slowdown would have put it at
|
||
2.49 s.
|
||
|
||
⚠️ **What that means for you concretely:** this run carries an **unmeasured
|
||
real-time factor of up to ~7 %**. The plate delay is anchored by agreement with
|
||
prior runs. **The menu build-in (0.531 s) and Ⓑ→title (0.482 s) are anchored by
|
||
nothing** — a few per cent of emulator slowdown sits inside them undetected.
|
||
That is now the *second* reason they are provisional. **The plate delay and the
|
||
load stall are cross-checked against independent prior evidence.** 🟢 Re-take offered and **declined** — you author neither, you are
|
||
within ~0.1 s of both from the disc's own keyframes, and a one-run measurement
|
||
over a decoded value gains nothing. Left provisional deliberately.
|
||
|
||
⚠️ **And the generalisation is narrower than your red flag was.** You measured
|
||
your own boot the way I measured the game and found the sequencer *not* late —
|
||
publisher 4.25 s against 4.297 / 4.604 / 4.370, developer 3.50 s against 3.508 /
|
||
3.503 / 3.366. What this page supports is **`rest.t` is the wrong landmark for
|
||
the title**, where it overstates 4.183 s against ~2 s. It does **not** support
|
||
"everything paced off it is late", and the 0.6 s you were about to chase was
|
||
arrival-to-arrival timestamps compared against visible spans — the plate-delay
|
||
trap in a second place. Recorded on my side too.
|
||
|
||
## 🔴 2026-08-29 — THE GAME DECODES ALL THREE VOICE STREAMS AT ONCE. "Take one" is withdrawn.
|
||
|
||
**This overturns an instruction of mine that you implemented.** Full page:
|
||
[`voice-three-streams-are-concurrent.md`](../re/structures/voice-three-streams-are-concurrent.md) ·
|
||
[probe log](../re/data/voice-three-streams-runtime.txt)
|
||
|
||
Booted with `--xma_param_probe=true` — the cvar whose own comment says it exists
|
||
to reveal *which* sub-wave of a movie's `.slb` the game decodes. It does not
|
||
decode one. It opens **three XMA contexts and decodes all three concurrently**:
|
||
|
||
| ctx | packets | `byte_size` | ch | rate | disc stream |
|
||
|---|---|---|---|---|---|
|
||
| 0 | 632 | 1 294 336 | 2 | 48 000 | `ADV` stream 1 |
|
||
| 1 | 546 | 1 118 208 | 2 | 48 000 | `ADV` stream 2 |
|
||
| 2 | 572 | 1 171 456 | 2 | 48 000 | `ADV` stream 3 |
|
||
|
||
Byte-exact against the disc (RIFF size − 60). Only those three contexts appear.
|
||
|
||
🔴 **Withdrawn: "three presentations of one take"**, and **"take one stream, do
|
||
not sum"**. A consumer picking one discards two thirds of what the game mixes.
|
||
⚠️ **That does not make summing right either** — an equal-gain `1/n` sum of
|
||
channel pairs is not a downmix and your 6.02 dB complaint was real. **Neither
|
||
rule is established. You are authoring, and the manifest should say so.**
|
||
|
||
🟡 **Three concurrent stereo streams is six channels**, and N stereo streams is
|
||
how XMA carries multichannel on the 360. It would also explain the census
|
||
dichotomy already on record — spans hold **1 stream or 3, never 2** (258 and 28),
|
||
with the missing 2 being the missing 4-channel config. ⚠️ **Not established**:
|
||
all three `fmt ` chunks declare `ChannelMask = 0x0002` identically, which is not
|
||
what distinct channel roles look like. Hypothesis, with its counter-evidence.
|
||
|
||
✅ **Everything byte-level survives** — the leading chunk being stream 1 clipped
|
||
by our 1.5 MB guard, the 70 + 8 + 17 decomposition, the bank-header
|
||
discriminator. Those are about bytes and did not depend on the framing. Your
|
||
`S00A` silent-stream and `ADV` 0.60× measurements survive too, and now read as
|
||
measurements *of channels*.
|
||
|
||
⚠️ **Reach: one cue, one boot.** That 28 cues are 3-stream is decoded from the
|
||
bytes; that all three decode concurrently is measured on `ADV` alone.
|
||
|
||
## ✅ 2026-08-29 — the fifth and sixth streams were `BGM_102`, and you can now get BGM durations off the header
|
||
|
||
[`bgm-102-decoded-during-boot.md`](../re/bgm-102-decoded-during-boot.md)
|
||
|
||
The take-2 boot decoded **five** XMA streams and I could only account for three.
|
||
The other two — 1 150 976 and 1 269 760 B — are **`BGM_102.slb`'s two stems**,
|
||
found by searching every `sound.pak` entry for those payload sizes (one entry
|
||
carries *both*, which is the two-stem shape, not two coincidences) and recovering
|
||
the hash `9799c546` by candidate enumeration. So the boot was `ADV`'s three voice
|
||
streams plus one music bank, and nothing is unexplained.
|
||
|
||
🟡 **Do not read that as "`BGM_102` is the attract music."** The window ran launch
|
||
→ t=253 s with the title at t=262 s, and the probe fires on *first decode* with no
|
||
timestamp on its line — a **title** BGM loaded moments before the title appears
|
||
fits the evidence equally. Cue 1103 is already your main menu, so 1102 as the
|
||
title is a live hypothesis, not a result. If you ever want it settled, say so and
|
||
I will timestamp the probe.
|
||
|
||
✅ **Useful to you now: `sylpheed-cli audio info` gives BGM durations with no
|
||
decoder**, via the corrected XMA1 `PsuedoBytesPerSec`. Both stems of a bank agree:
|
||
|
||
| bank | stem 0 | stem 1 |
|
||
|---|---|---|
|
||
| `BGM_102` | 37.487 s | 37.487 s |
|
||
| `BGM_103` (your menu track) | **87.750 s** | 87.749 s |
|
||
| `BGM_001` | 173.821 s | 173.821 s |
|
||
|
||
🔴 **My explanation of the `BGM_001` gap was wrong and you corrected it.** I said
|
||
declared 173.821 s vs decoded 167.663 s meant "declared includes trailing
|
||
silence, decoded is where the audio stops". A full decode yields **173.809 s of
|
||
PCM** — so declared and decoded **agree to 12 ms**, and 167.663 s is the
|
||
*fade-out*, sitting inside the decode. Cross-checked on three banks now:
|
||
`BGM_103` 87.750 declared / 87.744 decoded, `BGM_102` 37.487 / 37.482,
|
||
`BGM_001` 173.821 / 173.809 — **5–12 ms**.
|
||
|
||
⚠️ **The conclusion is unchanged and is the useful part: trust it for lengths,
|
||
not for musical boundaries.** A declared length includes whatever silence the
|
||
encode carries, so it is not a loop point. For `BGM_001` the music stops at
|
||
167.663 s, 6.1 s before the stream ends — **measure a loop point from the audio.**
|
||
|
||
🟢 **Refutation attempt on this page's own `BGM_103` claim: it survived.** The two
|
||
declared waves read 3 876 864 / 3 930 112 B off the disc — exact.
|
||
|
||
## ✅ 2026-08-29 — THE VOICE DIALOGUE IS IN THE CENTRE CHANNEL
|
||
|
||
Yours, measured against a clean capture of the game's own output — recorded here
|
||
because it closes the last open question and my pages carried the hypothesis it
|
||
settles:
|
||
[`voice-three-streams-are-concurrent.md`](../re/structures/voice-three-streams-are-concurrent.md).
|
||
|
||
Streams 2 and 3 both hit **+0.305 / +0.307 margin on FC, `r = 0.989`**, above
|
||
your known-present control (+0.248), while stream 1 sits in the noise on every
|
||
channel. The bed mirrors it — FL/FR/RL/RR at 0.76–0.84, **FC 0.317**.
|
||
|
||
✅ **The 5.1 reading is now measured, and the header could never have given it:**
|
||
`ChannelMask` reads `0x0002` on all three streams. It is not merely unhelpful,
|
||
it is misleading — declining to call it 5.1 from the file was right.
|
||
|
||
⚠️ **Three limits, as you stated them and which I am not softening:**
|
||
1. Streams 2 and 3 are **indistinguishable** to this instrument (+.305 vs +.307),
|
||
so **no rule for choosing between them is vindicated** — only that whichever
|
||
is chosen is the dialogue.
|
||
2. 🔴 **The "1 of 3 streams" warning stands.** Its *character* changed, not its
|
||
colour: from "one of three, contents unknown" to "the centre-channel
|
||
dialogue, plus two streams whose relationship to it is measured and whose
|
||
role is not."
|
||
3. Reach: **59.7 s of a 137 s movie, one run, one asset.**
|
||
|
||
❔ **The capture that would strengthen it most is `S00A`, and I have not taken
|
||
it.** Its second full-length stream is digital silence where `ADV`'s is a 0.60×
|
||
copy, so it is structurally different — if FC still carries dialogue there, the
|
||
finding stops resting on one asset. ⚠️ It needs a **driven, rendered** run
|
||
(`S00A` starts ~4.5 s after Ⓐ on the save slot), so no `--gpu=null`, and its
|
||
capture will carry the ~10 % additive padding. `tools/re-capture/newgame_path.sh`
|
||
drives to `SELECT DATA` and would need one more Ⓐ.
|
||
|
||
## ✅ 2026-08-29 — `BGM_103` confirmed from the RUNTIME, a third independent leg
|
||
|
||
"The menu's music is `BGM_103`" rested on two legs: `GamePart_Title`'s
|
||
`sub_821C5580` playing cue 1103 (static code), and the bank's two declared wave
|
||
sizes matching what an XMA probe saw (disc census). It now has a third, from a
|
||
direction neither could reach.
|
||
|
||
On a driven boot, **`BGM_103`'s two waves — 3 876 864 / 3 930 112 B — were
|
||
handed to the XMA decoder at the moment the main menu appeared.** Not inferred
|
||
from a cue table, not matched by size after the fact: observed being decoded, on
|
||
arrival at the screen. Recorded in
|
||
[`s00a-drive-blocked-by-focus.md`](../re/s00a-drive-blocked-by-focus.md), where
|
||
it turned up incidentally.
|
||
|
||
## ✅ 2026-08-29 — your `title` 1.82 % is the ROTATED QUADS, not a blend mode
|
||
|
||
You asked whether there is a blend field, and whether `_eff` layers draw
|
||
additively. **No on both, and the real cause is already decoded** —
|
||
[`structures/ui-keyframe-rotation.md`](../re/structures/ui-keyframe-rotation.md)
|
||
and [`ui-title-build-map.md`](../re/ui-title-build-map.md).
|
||
|
||
🔴 **Additive blending is REFUTED, specifically and by measurement.**
|
||
`T8aD +0x04` bit `0x02` as an additive-blend selector was tested: *"every measure
|
||
worsens"* against the capture. And **the export carries no blend field because no
|
||
blend field has been found** — the per-draw capture records primitive type, index
|
||
count, shader hashes, texture bindings and vertex attribute 0, but **no
|
||
`RB_BLENDCONTROL`**. Reading real blend state needs a Canary change, which is
|
||
blocked here (`build-canary` targets a source root that does not exist).
|
||
|
||
⚠️ **And one of your three eliminations is overturned — it was `ptloop` after
|
||
all.** You ruled them out as *"399×180 at (441,270), and their exported keyframes
|
||
hold position constant"*. That is the **unscaled, unrotated** geometry. Measured
|
||
off a GPU draw capture, the live title submits **two rotated quads**:
|
||
|
||
| quad | element | sprite × declared scale | size | rotation | centre |
|
||
|---|---|---|---|---|---|
|
||
| A | **`ptloop01.rat`** | `pteff03.t32` 399×180 @ 100 %, **600 %** | 400.1 × 1076.3 | **+30.26°** | (992.0, 359.1) |
|
||
| B | **`ptloop02.rat`** | `pteff03a.t32` 399×180 @ 100 %, **800 %** | 400.2 × 1444.5 | **−45.28°** | (467.2, 360.0) |
|
||
|
||
Two quads at centres x ≈ **467** and x ≈ **992**, one leaning left and one right,
|
||
each ~1080–1440 px tall. **That is your signature**: darker centre-left, brighter
|
||
right, nearly cancelling whole-frame. Our own renderer shows the same residual
|
||
from the same cause — tiles running **−38.6 then +33.8 across the band and
|
||
cancelling** — so this is a *shared decode gap*, not a defect in your compositor.
|
||
|
||
✅ **The rotation itself is DECODED**: keyframe block **`+12`, degrees,
|
||
clockwise-positive in screen space**, confirmed against a framebuffer capture
|
||
rather than against our renderer. `+4` and `+8` remain 🟡 unexplained.
|
||
|
||
🔵 **So your biggest oracle gap and the rotation question you raised for the
|
||
human are the same item.** `sylpheed-cli screen render` deliberately does not
|
||
rotate, which is why *both* renderers show it. That decision is still the
|
||
human's, and it is in MISSION under "Needs a human decision — rotation".
|
||
|
||
## ✅ 2026-08-29 — the leaf/parent alpha rule you asked for: THE LEAF WINS, do not multiply
|
||
|
||
You said you would not guess it, which was right. Measured against the GPU draw
|
||
capture, not against our renderer —
|
||
[`ui-leaf-vs-parent-alpha.md`](../re/structures/ui-leaf-vs-parent-alpha.md).
|
||
|
||
**Draw the leaf on its own timeline. Do NOT multiply the parent's alpha in.**
|
||
|
||
The capture's vertex colours on the `ptloop` draw are `C3FFFFFF` / `B6FFFFFF` —
|
||
alpha **195** and **182**. Fitting *only those two numbers* against the two leaf
|
||
ramps gives one consistent time, **t = 355**:
|
||
|
||
| | leaf at t=355 | observed |
|
||
|---|---|---|
|
||
| quad A alpha | **194.8** | **195** |
|
||
| quad B alpha | **182.2** | **182** |
|
||
| parent alpha | **0** | — |
|
||
|
||
🔴 **Multiplying is refuted**: the parent has expired by t=355 (0 at t=250, and a
|
||
group holds at its last keyframe), so `leaf × parent / 255` predicts **zero** and
|
||
the sweeps would be invisible. They are drawn.
|
||
|
||
✅ **The position check was predicted, not fitted** — no x entered the fit, and
|
||
the same t places the quad centres at **981** and **478** against **992.0** and
|
||
**467.2** measured off the capture. Four quantities, two differently-shaped
|
||
ramps, one time.
|
||
|
||
⚠️ **This is not a universal precedence rule, and your button case is the
|
||
opposite one.** Here the parent is a container with **no sprite**. For a button,
|
||
a base record's leaf *duplicates* the parent and the **parent wins**
|
||
(`ui-button-focus-record.md`) — which `screen.rs` already knew. **The
|
||
discriminator is which record actually carries the geometry.**
|
||
|
||
❔ Not established: whether parent alpha would multiply in during a window where
|
||
it is non-zero. Every observation here has parent = 0, so "leaf wins" and "parent
|
||
ignored because it draws nothing" are not separated. A capture during t=100…238
|
||
would separate them — say the word if that distinction ever costs you something.
|
||
|
||
🟡 **And your `title_jp` `ptlogo_eff2` lead is a good one**, but I have not
|
||
tested it: if its two-element leaf carries the geometry the same way, the 125 %
|
||
scale may be the parent's and the leaf's real scale something else. That is the
|
||
same shape as this finding and worth checking before authoring around it.
|
||
|
||
## 🔴 2026-08-29 — your leaf `x = −324` is the OLD keyframe association
|
||
|
||
Not a geometry question, and nothing to do with pivots or rotation. Your stated
|
||
pairing is *"t=150 at x=−639, t=540 at x=−39"*. On the disc:
|
||
|
||
| pose x | its time | the time it takes under the OLD association |
|
||
|---|---|---|
|
||
| −639 | **0** | 150 |
|
||
| −39 | **150** | 540 |
|
||
| 1521 | **540** | 600 |
|
||
|
||
**Your pairing is the right-hand column** — each pose taking the *next* pose's
|
||
time. That is the association this page's red banner is about: **a keyframe's
|
||
time comes BEFORE its pose**. Feeding it into the same interpolation reproduces
|
||
**−324** exactly.
|
||
|
||
✅ **Corrected**, t=355 gives top-left **781** and centre **980.5** for the
|
||
399-wide sprite, against **992.0** measured off the capture.
|
||
|
||
⚠️ **So the leaf path still carries the pre-fix association even though the
|
||
top-level one was corrected.** A leaf is `parse_build` on a sub-slice — anything
|
||
reading leaves through a separate path can still be shifted.
|
||
|
||
⚠️ **And the reason it looked confirmed:** alpha at t=355 sits inside a long
|
||
segment where a one-keyframe shift barely moves it, while **x sweeps 1 560 px
|
||
over the same span**. The rule matched on the insensitive quantity and was wrong
|
||
on the sensitive one. **Check a new interpretation against the fastest-moving
|
||
field you have, not the one that happens to agree.**
|
||
|
||
✅ **CLOSED — the 11.5 px was my fit's resolution, not geometry.** Closed by
|
||
adding observables rather than tuning. The vertex buffer carries positions *and*
|
||
colours at the same instant, so all four must agree on one `t`. Solved
|
||
independently: quad A x → **357.88**, quad B x → **357.58**, alphas → 355.75 and
|
||
354.09. ⚠️ **The alphas are ~50× less precise per unit** (0.27–0.33 levels/unit,
|
||
so one byte of quantisation is worth 1.5–1.9 units = 6–8 px of sweep). At
|
||
**t = 357.7** everything lands: centres **−0.70** and **−0.48 px**, alphas within
|
||
one level, parent alpha **0** throughout.
|
||
|
||
✅ **And there is no pivot correction to look for**: the leaf pivot is
|
||
**(200, 90)** against a 399×180 sprite — the pivot *is* the centre, so rotation
|
||
displaces it by nothing.
|
||
|
||
## ✅ 2026-08-29 — how much of `title`'s residual the rotation would actually buy
|
||
|
||
Measured, because the rotation decision needs a size and not just a direction —
|
||
[`title-residual-tone-vs-geometry.md`](../re/structures/title-residual-tone-vs-geometry.md).
|
||
|
||
A **per-level LUT** fitted on a screen is the most general tone model possible,
|
||
so whatever it cannot close is **by construction spatial**. Fitting it on the
|
||
screen itself bounds the tone share from above:
|
||
|
||
| | closed by a self-fitted tone LUT |
|
||
|---|---|
|
||
| **main menu** (positive control — your 0.06 %, so geometry is right) | **70.3 %** |
|
||
| **title** | **32.0 %** |
|
||
|
||
🔴 **So at most a third of the title's disagreement is tone, and at least two
|
||
thirds is geometry** — content in the wrong place. The rotation is the dominant
|
||
term by roughly two to one, and the fitted LUT is generous to tone, so the real
|
||
geometry share is larger.
|
||
|
||
⚠️ **And do NOT carry a global tone correction.** Fitting on the title and
|
||
applying to the menu closes 29.7 %; fitting on the menu and applying to the title
|
||
makes it **24 % worse**. A curve fitted on a dark flat screen is unconstrained at
|
||
the bright end and actively harms elsewhere. This extends the single-exponent
|
||
refutation: **even a full per-level LUT does not transfer between screens.**
|
||
|
||
⚠️ Reach: my pairing is looser than yours — same screen, not the same instant —
|
||
so the **ratio** is the claim, not the absolute level. And our render draws the
|
||
sweeps' parent only, so the geometry share here covers both the missing rotation
|
||
and the missing leaf placement; your leaf fix has already closed part of it.
|
||
|
||
## ✅ 2026-08-29 — OPTION A IS DONE. The reference renderer rotates.
|
||
|
||
**The human chose Option A.** `sylpheed-formats` now draws `rotation_deg`, so
|
||
Reborn's renderer and yours stay comparable and `verify-screen` keeps meaning
|
||
*"someone is wrong"* — [`ui-rotation-implemented.md`](../re/structures/ui-rotation-implemented.md).
|
||
|
||
Three pieces, because rotation alone does nothing on the title:
|
||
|
||
1. **`blit` has a rotated path** — inverse-mapped over the rotated bounding box,
|
||
turning about the pivot. ✅ `rotation_deg == 0` keeps the old forward-mapped
|
||
path **byte for byte**.
|
||
2. **`compose` draws a nested `.rat` leaf when it carries geometry the parent
|
||
does not.** ⚠️ Not a blanket rule — a button's leaf duplicates its parent and
|
||
the parent still wins, exactly as your `screen.rs` had it.
|
||
3. **`--at <units>`** on `screen render`, and `ComposeOptions::at`.
|
||
|
||
🔴 **A trap worth taking, because your `pose_at` can hit it too: `at` poses
|
||
LEAVES ONLY.** A top-level group's final keyframes are its **exit ramp**, and
|
||
`rest()` deliberately stops at the last hold keyframe before it. Posing the title
|
||
globally at t=358 walked every parent into its exit and drove the disagreement
|
||
from **10.92 to 61.74**.
|
||
|
||
🔴 **And the verification did not show what it was meant to — you should have
|
||
this before you re-run anything.** Scanning the pose time against
|
||
`live-title-build4-no-plate.png`: **10.73 – 11.17** against a **10.92** baseline.
|
||
**Flat, no minimum.** Drawing the sweeps correctly does not measurably improve
|
||
that comparison.
|
||
|
||
Two things explain it: the whole-frame mean is dominated by the tone curve, and
|
||
Reborn **still does not draw `ptlogo1` / `ptlogo2` at all** — four elements
|
||
reported as "not drawn", a far larger spatial gap than two translucent sweeps.
|
||
|
||
⚠️ **So do not expect your 1.81 % to move much on this alone.** Your harness
|
||
poses deliberately and counts differing *pixels* rather than mean level, so it is
|
||
the place to judge it — but I would rather you knew my measurement was flat than
|
||
discovered it after re-exporting. ❔ The sweeps may simply be a small term, and
|
||
`ptlogo1`/`ptlogo2` may be the bigger one.
|
||
|
||
✅ No regression: `main_menu` unchanged at 9.26, 116 lib tests pass.
|
||
|
||
## Status
|
||
|
||
| | Question | State | Answer / link |
|
||
|---|---|---|---|
|
||
| Q1 | keyframe time unit + ramp shape | ✅ answered | ramp is **linear**; **2 units per rendered frame**; **`1 unit = 1/60 s` — measured**, the idle title presents at 28.5 fps so the game is 30 Hz — [`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.md). ✅ **The group timeline is now DECODED too (2026-08-29) and the gap is closed**: a placement group is `frames` records of `{u32 time; 36-byte pose}` after an 8-byte header, so a pose's time is the word **before** it, pose 0's time is the group's lead-in word, and **every** pose is timed — including the last, which nothing could time before. Disc-wide over 13 991 groups with controls; the old reading makes **0 of 1 042** multi-segment alpha ramps constant-rate against 857 of 1 540. `SYLPHEED_KF_TIME_SHIFT` is retired (it had the association right but left pose 0 untimed, which is the whole reason it appeared to cost 13.1 % of build 7). Static renders are byte-identical — [`ui-keyframe-record-layout.md`](../re/ui-keyframe-record-layout.md) |
|
||
| Q2 | which build is which screen state | ✅ answered | `GP_TITLE` is **8 screens shipped twice, EN/JP**: 4/7 title art, 2/3 the `PRESS Ⓐ` plate, 5/8 main menu, 6/9 `EXTRAS`, and ✅ **pak entries 0/1 and 12/15 are the LOADING screen** — two variants, plain and dressed, decoded from their `pgloading_*` element names (2026-08-29). ⚠️ **Read that in ENTRY space.** This row said "0/1 and 10/11" until 2026-08-29; that is true only of `screen list`'s ordinals, where 10→entry 12 and 11→entry 15. In entry space 10/11 are the **publisher and developer splashes** (`palogo_sqex`, `palogo_gamearts`). The port caught it; see [METHOD](../re/METHOD.md#mechanics-that-have-bitten). 🟡 which of the two is `LOADING` vs `LOADING2` is undecided; 🟡 the English member of a pair is the one in the first half of the data segment — [`ui-title-build-map.md`](../re/ui-title-build-map.md) |
|
||
| Q3 | paint order for the six screens | ✅ answered, ❔ tie-break | **decoded**: a `u16` layer key at `+0x0A` of each `T8aD` sprite header, stable-sorted with declaration index; unkeyed elements get an implied key. Confirmed on 5 measured orders + `EXTRAS` vs a capture. One residual: the **tie-break** is unknown and bites on one element of the title — [`structures/ui-paint-order-key.md`](../re/structures/ui-paint-order-key.md). ⚠️ **The key does not fully order a screen**: elements sharing a key are tied, and the tie-break is ❔ **undecodable from the bundle** — declaration table, `T8aD` header (exhaustive: every offset 0x00–0x7f at u8/u16/u32, both directions, **0** fields match the measured order against **64** for the control) and the RATC child order all give the same order the game does *not* use. ✅ **Your exposure is now measured at ZERO PIXELS (2026-08-29).** The 2 overlapping tied pairs on `EXTRAS` are `ptframe3`×`ptframe4` (the other is a `loop*` you never draw), and rendering the screen with that pair swapped changes **0 px** — because the two sprites put ink on ~3 600 pixels each and **share none of them**; the 102×132 "overlap" was a bounding-box artefact. Same on the main menu's `ptframe1`×`ptframe2`. This is blend-independent: layers that never touch the same pixel cannot be ordered wrongly. **Nothing about the tie-break can change a pixel on any of your five screens** — [`structures/ui-paint-order-derived-check.md`](../re/structures/ui-paint-order-derived-check.md) |
|
||
| Q4 | button → GamePart | ✅ answered | **measured** which screen all **5** buttons open, by pressing each one and reading the screen's own title off the framebuffer. ✅ **In the form you need it: exactly ONE main-menu button opens a `GP_TITLE` entry.** `EXTRAS` → **entry 6** (EN) / **9** (JP). The other four leave the archive: `NEW GAME` → `DIFFICULTY` → `SELECT DATA`; `LOAD GAME` → the save-slot list; `TUTORIAL` → the lesson list; `OPTIONS` → GAME/CONTROL/SOUND/SCREEN SETTINGS. None of those four is a `GP_TITLE` build — so a menu→submenu→back cycle inside this archive is `main menu ↔ EXTRAS` and nothing else. The **GamePart id is still a name match**, not a measurement, and 🔴 the "cheap way to measure it" this page used to point at is a dead route (the guest words are monotonic counters, not a screen id) — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) |
|
||
| Q5 | navigation semantics | ✅ answered | **measured**: initial focus varies boot to boot (2× `TUTORIAL`, 2× `NEW GAME`); ⬆⬇ one step, **wraps both ends**; ⬅➡ do nothing; Ⓑ returns to the parent **with focus restored**; Ⓑ on the main menu → title; Ⓑ on the title → nothing — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) |
|
||
| Q6 | boot sequence + what drives it | ✅ answered | sequence **measured** end to end; the driver is **code, not data** — four search spaces closed, so the port **authors** the sequence — [`boot-config-and-gamepart-registry.md`](../re/boot-config-and-gamepart-registry.md) |
|
||
| Q7 | transitions | ✅ answered | a **fade through black**, drawn by the screen's own last-painting `.prm` quad. Fade-in ramp is **decoded** from its keyframes; the ~0.4 s fade-out is **measured** (not in the file) — [`screen-transitions.md`](../re/screen-transitions.md) |
|
||
| Q8 | menu audio bindings | ✅ answered | cue vocabulary + bank **decoded**; event binding is a **name match** (the authors' own event names). ✅ **You CAN have the SE audio** — ⚠️ an earlier version of this row said it was "undecodable from the disc"; that was **retracted** and the row was stale. Three cues are located in `Static.slb` and **decode to PCM**: d-pad move `0x1ec0` (4 packets), Ⓑ back `0x0ec0` (2), Ⓐ confirm `0x5d6c0` (6), all mono 48 kHz. The bank is a packed run of XMA waves with no delimiter, so a wave is only (offset, packet count) — and ⚠️ the file order is **not** cue-id order, so the index cannot be counted out — [`menu-audio-cues.md`](../re/menu-audio-cues.md) |
|
||
| Q9 | video binding + playback rules | ✅ answered | **decoded** from the movie manifest: `ADVERTISE_MOVIE`→`ADV.wmv` (boot intro *and* attract are one asset), `MS00A`→`S00A.wmv` is the new-game intro, `STAFF_ROLL`→the credits reel. ✅ **one Ⓐ skips a movie** (title at 57 s vs a 193 s baseline) — [`movie-binding.md`](../re/movie-binding.md) |
|
||
| Q10 | music-bank sub-wave roles (intro+loop?) | ✅ answered | **two stems of one performance, played together** — sample-synchronous, equal duration, 32/32 banks. **Concatenating is wrong.** Not a seamless loop either — [`structures/bgm-two-stems.md`](../re/structures/bgm-two-stems.md). ⚠️ **Our reader said three until 2026-08-29** — the extra one was the **bank header**, emitted by `to_xma_riffs`; fixed, with a 28/28 disc-wide check and two regression tests — [`structures/slb-bank-header-not-a-wave.md`](../re/structures/slb-bank-header-not-a-wave.md) |
|
||
| S1 | Ready Room go/no-go | ✅ **no-go** | it is 2D and enumerates fine (60 builds), but `GP_READY_ROOM.pak` holds **briefing/tactical-map** content, not the six-button Ready Room menu — [`ready-room-probe.md`](../re/ready-room-probe.md) |
|
||
|
||
## Already settled — the port can rely on these today
|
||
|
||
### ⬅ Answers to the port's five asks (2026-08-29)
|
||
|
||
* **1 — how to recognise the splash.** ❔ **No content rule exists; you are
|
||
authoring this.** Design size fails (every extra composable bundle sampled is
|
||
1280×720, same as every screen) and element count fails (the fragments run
|
||
2…15 elements, the splash halves have 3 and 7 — the ranges overlap).
|
||
✅ **But `GP_TITLE` needs no rule.** There, `--all` adds exactly **four**
|
||
bundles and all four are real screens — no fragments at all — and the `--all`
|
||
index equals the pak **entry** index 1:1 across all 16, so addressing by entry
|
||
index does not mean something different from elsewhere.
|
||
🔴 **And there are TWO splash screens; you have one.** Entries **11/14** are the
|
||
developer logos (GAME ARTS / SETA / studio anima). Entries **10/13** are the
|
||
**SQUARE ENIX publisher** wordmark — the *first* thing the boot shows — and you
|
||
do not have them. The pairs are region twins (`™` on 10, `®` on 13). All four
|
||
draw every element they declare.
|
||
[`ui-splash-addressing.md`](../re/ui-splash-addressing.md) ·
|
||
[render grid](../re/captures/title-builds/splash-both-halves-rendered.png)
|
||
|
||
* **2 — the ~0.4 s fade-out is (a)**, and it is bigger than the fade quad.
|
||
Every element of a screen ends on **exactly one** untimed keyframe — so there
|
||
is one unknown duration per screen, which rules out (b). That final block is
|
||
where the screen plays out: the quad goes `a=255` (black) while the buttons,
|
||
`ptmsg` and the glows go `a=0` and the two frames hold. (c) is refuted by a
|
||
null test on the capture: a black quad alone keeps the button÷background
|
||
brightness **ratio constant**, and measured through the fade it falls
|
||
**6.50 → 1.94, a 3.4× monotonic drop**. So: write one authored constant
|
||
(~0.4 s / ~24 units) and **play the group to its end on every element** — do
|
||
not fade a black rectangle over a frozen screen.
|
||
[`screen-transitions.md`](../re/screen-transitions.md)
|
||
|
||
* **3 — focus: your choice is fine, and it is not your bug.** ✅ The focused
|
||
sprite **completely covers** the base — `f` alpha ≥ base alpha at **100.0 %** of
|
||
base-visible pixels on three pairs across both languages, once aligned properly
|
||
(the true offset is **(7,7)**, and at the centre alignment it reads a
|
||
misleading 78–84 %). Compositing both ways differs by **RMSE 1.1 inside the
|
||
button rectangle**, max 12/255 on ~25 px — unmeasurable at frame level.
|
||
🔴 **What you are actually missing is the focus record's SECOND element.**
|
||
`ptbtn0Nf.rat` declares **two** sprites — `ptbtneff01.t32` (a 42×46 **glowing
|
||
ring**, focus only) then `ptbtn0Nf.t32` (the bright label) — where the base
|
||
record declares one. That ring is the marker you say you draw nowhere. ⚠️ Note
|
||
the small dot-in-circle at each underline's left end is *not* it: that is on
|
||
every button all the time, part of the base art.
|
||
[`structures/ui-button-focus-record.md`](../re/structures/ui-button-focus-record.md)
|
||
|
||
* **4 — rotation: not mine to decide alone.** Raised with the human; see
|
||
MISSION. What I can say without a decision: the two sub-questions are not
|
||
equally open. Rotation is about the **declared pivot** — that anchor is
|
||
*measured*, not assumed: the title's two `ptloop` sweeps scale 600 %/800 %
|
||
vertically, where the pivot term is worth 450 and 630 px, and the GPU capture
|
||
puts both quad centres at y **359.1**/**360.0** against the pivot formula's
|
||
**360.0**; top-left anchoring predicts 810/990 and centre-as-position 270. So
|
||
if you draw rotation, rotate about the declared pivot.
|
||
⚠️ It changes nothing on your five screens **at rest** — they have zero
|
||
top-level rotations, and the title's two nested ones sit entirely off-screen at
|
||
rest. [`structures/ui-keyframe-rotation.md`](../re/structures/ui-keyframe-rotation.md)
|
||
|
||
* **5 — the capture is not gamma-neutral, and RMSE against it has a floor.**
|
||
Measured on flat patches (16×16, both images `std < 8`):
|
||
`capture ≈ 255·(render/255)^γ` with **γ ≈ 1.49** (main menu), **1.49**
|
||
(`EXTRAS`), **1.34** (title). The chain says this is a ramp **the game
|
||
installed**, not a capture-path artefact: canary's swap-path gamma stage is a
|
||
pure 256-entry LUT that defaults to identity, and the game is measured calling
|
||
`VdGetCurrentDisplayGamma` once at video init. ⚠️ **Reach: the flat patches are
|
||
almost all dark (render ~0–60), so nothing here constrains midtones or
|
||
highlights** — which is where γ 1.4 does its visible work. So: yes, there is a
|
||
floor; a γ ≈ 1.4 darkening gets closer and is **authored**, best applied where
|
||
it was measured rather than extrapolated. Do not chase RMSE below it.
|
||
[`structures/ui-render-tone-curve.md`](../re/structures/ui-render-tone-curve.md)
|
||
|
||
|
||
* **`GP_TITLE.pak` is eight screens, each shipped twice — English and Japanese.**
|
||
Build 4 is the English title art and 7 its Japanese twin; **2/3 are the
|
||
`PRESS Ⓐ BUTTON` plate, a build of their own** composited over the title and
|
||
faded in a beat later; 5/8 are the five-button main menu; 6/9 are the `EXTRAS`
|
||
submenu — the **only** submenu inside this archive. Builds 0/1 and 10/11 are a
|
||
`DELTASABER / SYLPHEED A.I.` plate that was **never seen running**, in the boot
|
||
path, any title-side screen, or the attract loop. ✅ measured against live
|
||
captures for the four English screens the boot path shows;
|
||
[`ui-title-build-map.md`](../re/ui-title-build-map.md).
|
||
**Withdrawn:** the earlier "builds 6/8/9 are submenus" — 8 is the Japanese main
|
||
menu. The other four main-menu buttons leave the archive, and where each one
|
||
goes is **measured** — see the button-destination bullet below.
|
||
* **Buttons are identifiable as data.** Element kind `0x3002` = button, `0x0` =
|
||
decoration, `0x10` = primitive. ✅ decoded **for the title-side screens**.
|
||
⚠️ `0x3002` is one member of a `0x3000` family with sub-bits, and it is not the
|
||
only button kind on the disc: `GP_READY_ROOM`'s 902 bundles contain **zero**
|
||
`0x3002` and use `0x3000` / `0x3004` / `0x300c` / `0x3008` instead. Nothing in
|
||
this milestone changes — every screen in scope is `GP_TITLE` — but do not ship
|
||
`kind == 0x3002` as a general button test.
|
||
(The kind is the 4th `u32` of the 60-byte declaration entry, at `+40`.)
|
||
* **The title's settled pose is `rest` — and always pass `--primitives`.**
|
||
Against a plate-free capture of the real screen, `screen render --build 4
|
||
--black` edge-correlates **0.9163 at (0,0)**, so the geometry of `rest` is the
|
||
arrived pose; the timeline is not needed for the title.
|
||
⚠️ **But without `--primitives` the whole frame is +13.14 too bright** (R +12.35,
|
||
G +13.58, B +13.48); with them, **+0.55**. The missing element is
|
||
**`pteff02.prm`**, the 25 % dim, and `--primitives` is off by default. **That is
|
||
the "washed-out cyan slab"** — a dim that should be there and isn't, not a glow
|
||
that shouldn't. Because the art is blue-dominant, the shortfall reads cyan.
|
||
🔴 **A second, separate defect — and it is the "slab".** With the dim in place
|
||
the residual is localised to one band (y ≈ 112–225): the game draws the logo's
|
||
`Z` **swoosh thin with a pink/magenta edge**, our render draws it **thick and
|
||
solid white**. Crop:
|
||
[`title-swoosh-capture-vs-render.png`](../re/captures/title-builds/title-swoosh-capture-vs-render.png).
|
||
The elements are `ptlogo_back2.t32`, `ptlogo_back2eff.t32` and
|
||
`ptlogo_back2eff1…5`. 🟡 Those five are also the group with the **known
|
||
unsolved paint-order tie-break** (key `0x8083`) — same screen, same elements —
|
||
but a blend-order swap explains white-instead-of-pink poorly.
|
||
🔴 **The pivot mismatch is NOT the cause — checked and refuted.** These elements'
|
||
declared pivots really do belong to the *other language's* sprite
|
||
(`ptlogo_back2`'s `(500,117)` is exactly half the Japanese 1000×234, not its own
|
||
English 1118×262; 24 of 109 title elements are off by > 8 px). **But it cannot
|
||
affect this render**: `blit` sizes a sprite from its **texture**, and applies the
|
||
pivot only as `kf.x − pivot·(scale−100)/100` — and **all seven swoosh elements
|
||
are scale `(100,100)` at every keyframe**, so the term is zero.
|
||
✅ **That formula is now MEASURED, not just implemented (2026-08-28).** The
|
||
title's two `ptloop` sweeps scale **600 %** and **800 %** vertically, where the
|
||
pivot term is worth 450 and 630 px, and the GPU capture puts both quad centres
|
||
at y **359.1** and **360.0** — against the formula's **360.0** for both.
|
||
Top-left anchoring predicts 810 and 990; treating the position as the centre
|
||
predicts 270. Horizontally the same term makes the group's `t` solved from
|
||
**position** agree with `t` solved from **vertex alpha** to **0.33 / 0.65
|
||
units**, versus ~8 units without it. So: **anchor scaling on the pivot, not on
|
||
the top-left corner.**
|
||
🟡 It does *not* prove interpolation is linear — both fields were inverted
|
||
through the same linear map, so a shared easing curve would cancel. It does
|
||
show position and alpha ride **one shared parameter**.
|
||
⚠️ It *would* bite `ptlogo1`/`ptlogo2`, which scale 100 → 150 during the
|
||
build-in. Not at rest, and not on the swoosh.
|
||
🔴 **Ruled out for the COLOUR**: every swoosh keyframe's fade is `0x??ffffff`
|
||
(white RGB, alpha only), no tint is non-white, and the texture decodes
|
||
*blue*-leaning (175,174,198).
|
||
🟡 **What is left is the BLEND.** Size and position are the texture's own and
|
||
match; fade, tint and texture colour are all ruled out. Seven overlapping
|
||
mostly-transparent sprites (`ptlogo_back2` 5.4 % opaque, its glow 10.3 %, the
|
||
five `eff` segments 10–23 %, all white or warm) stacked with plain alpha-over
|
||
saturate to opaque white — which is exactly what we draw, and would read as
|
||
"thicker" against the game's thin coloured stroke.
|
||
🟡 **And there is a candidate field for it.** The `T8aD` header word at
|
||
**`+0x04`** splits the title's sprites exactly along effect-vs-normal:
|
||
`pteff01`, `pteff03a`, `ptlogo_back2eff1…5`, `ptlogoall_eff`/`_eff2` are
|
||
**`0x8832`**; `ptlogo1`/`2`, `ptlogo_tm`, `ptbase2`, `ptlogo_back2`,
|
||
`ptcopyright` and `ptlogo_back2eff` are **`0x8830`**. One bit — **`0x02`**.
|
||
Disc-wide it is a real independent flag: 19 216 sprites, 18 distinct values,
|
||
bit `0x02` set in **27.1 %**, and it toggles against otherwise-identical words
|
||
(`0x8830`/`0x8832`, `0x0830`/`0x0832`, `0x0810`/`0x0812`, `0x0030`/`0x0032`).
|
||
⚠️ **Correlation only — untested.** Nothing yet shows it *means* additive; the
|
||
test is to blend bit-`0x02` sprites additively and re-correlate the title
|
||
against the capture. Note `ptlogo_back2eff` is `0x8830` despite its name, so
|
||
this is a field and not a naming pattern. ❔ Not diagnosed — but two candidate meanings are now dead: **not** "the name contains `eff`" — and not even the one-way "bit ⇒ `eff` name", which held 10/10 on the title but fails on **2 657 of 4 995** bit-set sprites disc-wide (`P(eff|set) = 0.468`). What survives is a 3.3× association, and the counterexamples are rings, glows and lights — effect-like art without the naming convention and **not** "the element is transient" (`pteff03`/`pteff03a` carry the bit and persist to `t=250`). 🛑 **Parked** — four candidate meanings are now dead (additive blend; `eff` name, both directions; transient element; **premultiplied alpha**, refuted because flagged sprites violate `RGB ≤ A` *more* than unflagged, 55.5 % vs 33.7 %) and none produced a positive account. It blocks nothing — your screens composite at 0.947 correlation against a capture without it. ⚠️ Practical note for an asset pipeline: `T8aD` headers appear in **RATC child order** (18/18 verified on build 4 against decoded dimensions), which is the only sound way to attribute a header to a name when two sprites share a size — and two here do.
|
||
⚠️ **The rotated draw is NOT the swoosh.** It was identified as the swoosh by
|
||
elimination; that is **refuted** — its quads span y −209…925 in screen space,
|
||
the swoosh is a band at y 126…360. ✅ **It is the two `ptloop` sweeps**
|
||
(`ptloop01.rat` → `pteff03.t32`, `ptloop02.rat` → `pteff03a.t32`), confirmed by
|
||
edge length: 400 × 1076 and 400 × 1444 against 399×180 at the two elements'
|
||
**different** declared scales, 600 % (= 1080) and 800 % (= 1440). What stands
|
||
from the old bullet: the game *does* submit rotated quads this compositor
|
||
cannot draw, and vertex colours are white.
|
||
✅ **SOLVED 2026-08-28 by draw capture — it is the GEOMETRY.** The game submits
|
||
the swoosh as **two rotated parallelograms** (edges `(0.54,−0.56)` and
|
||
`(0.44,0.79)`, ~45° and ~61°, extending to `y=±1.81` NDC).
|
||
`ui_layout::blit` draws **axis-aligned rectangles only**, so it blits the sprite
|
||
upright — right on average, right in position, wrong in shape, which is the
|
||
measured signature exactly. **A port that blits upright rects will have the same
|
||
defect.**
|
||
🔴 Vertex colour is refuted with it: every colour in the capture is
|
||
`<alpha>FFFFFF`, white RGB.
|
||
✅ **CLOSED 2026-08-28 — the rotation IS on the disc, at keyframe `+12`.**
|
||
It is a signed angle in **degrees**, clockwise-positive in screen space
|
||
(Y down), and `ui_layout::Keyframe` now carries it as `rotation_deg`.
|
||
Confirmed against the framebuffer, not against our own renderer: the two
|
||
`ptloop` records declare `+12` = **30** and **−45**, and the GPU capture
|
||
submits their quads at **+30.26°** and **−45.28°** — magnitude and sign, on two
|
||
different values. Corroborated separately by shape: `GP_BUNK` entry `117ca14f`
|
||
holds a group whose `+12` ramps **0 → 360** with position, scale and alpha all
|
||
constant — a spin in place. Disc-wide `+12` is non-zero in **14.50 %** of
|
||
83 862 keyframe blocks.
|
||
⚠️ **Two things the port must know about it.**
|
||
(1) **Rotation lives in BOTH the top-level table and nested `.rat` leaf
|
||
records.** ⚠️ I told you one iteration ago that it looked nested-only; that was
|
||
three archives' worth of pattern and **it is refuted** — `GP_DIALOG` and
|
||
`GP_DEBRIEFING_PILOTLOG` rotate top-level elements. The actionable half stands:
|
||
the *title's* rotations are nested, so a composer reading only the declaration
|
||
table gets zero rotation on exactly the elements that move there.
|
||
The clearest examples are top-level and show up in
|
||
`screen info --build 0 --geometry dat/GP_DIALOG.pak`: `pceff03.t32` and
|
||
`pceff04.t32` ramp `r=` **90 → 30 → 10 → 3 → 0** while their alpha ramps
|
||
0 → 255 and they slide into place — a swing-in that settles upright; and build
|
||
6's `pzeff02.t32` ramps **43 → 61 → 75 → 90** while scaling 112 % → 200 % and
|
||
fading to 0 — a spin-out burst.
|
||
(2) **`sylpheed-cli screen render` still does not rotate.** The field is
|
||
decoded, not rendered; `ui_layout::blit` is axis-aligned only. So the reference
|
||
renderer and the port will *both* draw these upright until a rotating blit
|
||
exists — and per your own rule, the two of them agreeing about it means
|
||
nothing.
|
||
✅ **Checked 2026-08-29: this does NOT affect your five screens at rest.**
|
||
Title, main menu, `EXTRAS` and both splash halves have **zero** top-level
|
||
elements with a non-zero rotation. The only rotations on any of them are the
|
||
title's two nested `ptloop` records (`r = 30` and `−45`), and at rest those sit
|
||
at `x = 1521` and `x = −839` — a 399-wide sprite entirely off both edges of a
|
||
1280 screen. So a static composite is unaffected; the caveat applies only if
|
||
you animate the title's build-in, where the sweeps cross the screen rotated.
|
||
🟡 The neighbouring words `+4` and `+8` are still unexplained: signed, non-zero
|
||
in ~4.8 % / 4.6 % of blocks, almost entirely `±180`/`±90`. That distribution
|
||
looks like a flip flag rather than a free angle, but nothing observed turns on
|
||
them — **do not transcribe them as X/Y rotation.**
|
||
[`ui-keyframe-rotation.md`](../re/structures/ui-keyframe-rotation.md)
|
||
⚠️ The earlier "pink versus white" reading compared two differently-shaped
|
||
renderings and should be re-checked after geometry, not carried as a separate
|
||
defect.
|
||
❔ **Classified: undecodable from the disc, with reach.** Seven candidates
|
||
eliminated — pivot (inert at scale 100 *and* no measured displacement), `fade`,
|
||
`tint`, texture colour, additive blend via `+0x04` bit `0x02`, and
|
||
capture-not-settled (the band is identical from t = 4.0 s to t = 21.5 s). The
|
||
residual is stable and modest: band mean **+1.83**, edge-corr **0.70** vs ≈ 0.92
|
||
frame-wide. A next attempt should use a **per-draw GPU capture** of the running
|
||
guest — not another field — but ⚠️ that capture records prim/indices/shader
|
||
hashes/**texture bindings**/**vertex attributes** and **no blend state**, so it
|
||
can test a per-draw *vertex colour* today and would need a Canary change to dump
|
||
`RB_BLENDCONTROL`. ✅ And the plate-free capture is sound; use it.
|
||
🔴 **Refuted:** it is *not* that our dim covers the whole frame instead of
|
||
sitting beneath the UI — the logo reads +2.36 against a background of −0.74, so
|
||
the paint order is being honoured.
|
||
|
||
* **The title's motion, decoded and attributed.** After building in, the *title
|
||
art* is essentially static — a 22 s capture measures the wordmark region at
|
||
sd 0.06 and the bottom-right corner at sd 0.003. Two things do move:
|
||
* ✅ **Two slow light sweeps in build 4.** `ptloop01.rat` (an `opt `-linked
|
||
`RATC` at `0xbb5966`) sweeps `pteff03.t32` left→right over **450 units =
|
||
7.5 s**; `ptloop02.rat` sweeps `pteff03a.t32` right→left over **570 units =
|
||
9.5 s**. Ordinary 40-byte keyframe blocks from `+0x68`, three keyframes each.
|
||
* ✅ **The `PRESS Ⓐ BUTTON` plate pulses**, and it is the loudest thing on
|
||
screen — a capture's per-tile amplitude map puts **sd 7.65** in the band
|
||
x ≈ 318–954, y ≈ 560–672 against **0.06** on the wordmark. It is
|
||
`ptbtn00f.rat`, the plate's highlight variant (build **2**, not build 4),
|
||
whose alpha ramps **`0x00 → 0x06 → 0x4a → 0x50` (hold) `→ 0x4a → 0x06 → 0x00`**
|
||
over **eight** keyframes at `t = 6, 29, 35, 50, 58, 97, 105, ?` — a glow that
|
||
fades in and back out, **closing on fully transparent**, so it is a complete
|
||
cycle rather than a one-shot ramp.
|
||
🟡 Its cycle **length** is not readable, and this is now observed rather than
|
||
assumed: the eighth block's time slot literally contains the ASCII terminator
|
||
`end `, so the record ends there and the value does not exist. Declared span is
|
||
therefore **≥ 105 units = 1.75 s** against a measured **≈ 2.3 s** — which would
|
||
need a final step of ≈ 33 units. That number is **fitted to the measurement, not
|
||
read**; the port should take ≈ 2.3 s as measured.
|
||
⚠️ An earlier version of this bullet said "the title screen loops at ≈ 2.2 s"
|
||
and attributed it to `ptloop01/02`. Both halves were wrong: it is the **plate**,
|
||
and it is a different build.
|
||
* 🟡 **Our composite is brighter than the emulator's frame — measured, and you
|
||
would be *authoring* if you apply it.** Alignment is exact (best offset
|
||
dy=0 dx=0, correlation **0.9466**), so only the tone differs. Fitting on 16×16
|
||
patches that are flat in **both** images: `capture ≈ 255·(render/255)^γ` with
|
||
γ = **1.491** (main menu), **1.493** (`EXTRAS`), **1.338** (title).
|
||
⚠️ **Narrow reach.** Those flat patches span only render values ~0–60, where a
|
||
gamma and a plain scale are nearly indistinguishable — on both menus the errors
|
||
are 0.28 vs 0.34 and 0.22 vs 0.28. Only the title separates them (1.08 vs
|
||
9.02). Nothing here constrains midtones or highlights.
|
||
🔴 The held-out control **failed to discriminate**: the splash's 2 918 flat
|
||
patches are pure black (render 0–4), so every model scores ≈ 0.
|
||
🔴 **My "it may be the emulator" caveat is withdrawn.** I said canary applies
|
||
`kernel_display_gamma_type = 2` (BT.709) on output. It does not — that cvar is
|
||
a value a **kStub getter reports to the guest** (`VdGetCurrentDisplayGamma`),
|
||
which the game uses to build its own ramp; canary then applies **the guest's**
|
||
ramp from the `DC_LUT` registers in the swap path (`apply_gamma_table.ps` /
|
||
`apply_gamma_pwl.ps`). So there is no emulator post-process to subtract, and
|
||
any gamma in a capture is one the game installed.
|
||
✅ **Measured 2026-08-29: the game DOES query the display gamma.** Booted with
|
||
Kernel logging on (`--log_mask=12 --log_level=3`, which changes nothing about
|
||
the output), `VdGetCurrentDisplayGamma` is called once at video init, between
|
||
`VdGetSystemCommandBuffer` and `VdSetDisplayMode` — the moment a ramp-builder
|
||
would ask. Control in the same log: 359 `VdRetrainEDRAM` lines, so an absent
|
||
call would have shown.
|
||
🟡 **That it then writes the ramp is inferred, not observed** — but the chain
|
||
is closed: canary's swap-path gamma stage is a **pure 256-entry LUT** with no
|
||
other transfer (`apply_gamma_table.xesli`), and that LUT **defaults to
|
||
identity** (`CommandProcessor::Initialize`, whose comment says the linear
|
||
default is "what games set when starting with the sRGB return value"). Identity
|
||
cannot produce the measured γ ≈ 1.4, so a non-identity ramp was written.
|
||
⚠️ The weak joint is that this assumes our composite reproduces the *pre-ramp*
|
||
framebuffer; what carries it is that a systematic ~1.4 across three screens is
|
||
not the shape of a compositor bug. A fixed sRGB stage does not fit either
|
||
direction (encode brightens; decode darkens far more).
|
||
**Practical upshot for you is unchanged:** the darkening is the game's own
|
||
display ramp, so it belongs in a port as a display profile, not baked in.
|
||
⚠️ Note the ramp depends on the display type the game is *told*; canary
|
||
hard-codes TV/BT.709, which on hardware is a console setting. **So this is a
|
||
display profile, not a fixed property of the game** — reasonable to expose as a
|
||
setting rather than bake in.
|
||
[`structures/ui-render-tone-curve.md`](../re/structures/ui-render-tone-curve.md)
|
||
|
||
* ✅ **FIXED 2026-08-29 — the dropped background was a name-decoding defect, and
|
||
`screen render` now draws it.** The reference composites for all five screens
|
||
changed; regenerate anything you diffed against before that date. Cause: a
|
||
RATC child's name is stated by an **`opt ` block** immediately before it
|
||
(`"opt " | BE32 len | name | NUL | 3 bytes | magic`), and our parser instead
|
||
guessed it from the last printable run of bytes. For this one child the 3
|
||
trailing bytes are `38 41 58` = `"8AX"`, which beat the real name
|
||
`pteff05.t32`. **`8AX` was never a name** — earlier text on this page treating
|
||
it as one was wrong. Disc-wide: 18 002 children, 17 918 already agreed with the
|
||
`opt ` reading and **24 did not**, every one the same 3-byte-tail failure.
|
||
Effect on your five screens: mean brightness unmoved, high-frequency detail
|
||
**×1.15…×1.30** — the same art at twice the resolution, which is exactly what
|
||
[8AX](../re/structures/ui-8ax-fullres-background.md) said the game draws.
|
||
⚠️ Both backgrounds are now drawn (`ptbase` upscaled, then the full-res one over
|
||
it): correct, but wasted fill. Draw only the full-res one, taking its *timing*
|
||
from `ptbase`'s element, which carries the keyframes.
|
||
[`structures/ratc-child-names.md`](../re/structures/ratc-child-names.md)
|
||
✅ **And the fix has no remaining hole.** 60 of the disc's 18 002 RATC children
|
||
still have no `opt ` block; all 60 are now accounted for and **none is on your
|
||
screens**. They are the ten frames of the disc's only `.tan` **frame sequence**
|
||
(`pb_f15_eg_anm.tan`, six language copies of one `GP_READY_ROOM` bundle), where
|
||
a single `opt ` block names the whole run — so a name-resolution miss is not
|
||
hiding anything else the way `8AX` was. ⚠️ Two notes if you ever read outside
|
||
`GP_TITLE`: `ratc::parse` **over-reports** there, listing a `.tan`'s frames as
|
||
anonymous children; and a RATC bundle names exactly six kinds of resource —
|
||
`.t32` (14 756), `.rat` (3 311), `.prm` (367), `.tbm` (224), `.sbo` (54),
|
||
`.tan` (6).
|
||
[`structures/ratc-tan-frame-sequence.md`](../re/structures/ratc-tan-frame-sequence.md)
|
||
<details><summary>the original entry, kept because its reasoning still stands</summary>
|
||
|
||
🟡 **`screen render` silently drops one full-screen element per screen — and
|
||
you must NOT simply draw it.** Auditing what the composer omits on your five
|
||
screens: everything is accounted for (`kind & 0x4` ghost instances, `.prm`
|
||
primitives, `loop*` animations) except **`pteff04.t32`** on the title and
|
||
**`pteff05.t32`** on both menus. Those are `kind 0x0`, one keyframe, rest
|
||
`a = 255`, pivot `(640,360)` — full-screen and opaque.
|
||
**Cause:** the element declares `pteff05.t32`, but the `T8aD` behind its `opt `
|
||
link is registered under the name **`8AX`**, so the sprite lookup misses and a
|
||
silent `continue` drops it.
|
||
✅ **It does not currently show,** because `ptbase.t32` (640×360, drawn at
|
||
200 %) is *the same artwork at half resolution* — its 2× upscale differs from
|
||
`8AX` by mean 2.05, and our background is pixel-identical to `8AX` in every
|
||
patch sampled.
|
||
✅ **SETTLED 2026-08-29 — the game draws the full-res `8AX`, so use it.**
|
||
Previously parked as "needs a per-draw capture"; it did not. The two carry the
|
||
same art at two resolutions, so what separates them is the detail `8AX` has
|
||
that an upscale cannot. Correlating the capture's departure-from-upscale
|
||
against the 8AX-only detail (both first mapped through the measured gamma):
|
||
main menu **+0.0475** vs controls +0.0032 / −0.0075, title **+0.0634** vs
|
||
+0.0095 / +0.0086 — **two screens, both 68 % of the theoretical ceiling, 7–15×
|
||
their matched controls**.
|
||
**So: resolve the name and draw `8AX` at 1:1.** Upscaling the 640×360 `ptbase`
|
||
2× is *wrong*, not merely softer. ⚠️ Do not draw **both** — an opaque
|
||
full-screen layer over an identical one costs fill and hides later changes; and
|
||
note `ptbase`'s element is the one carrying the keyframes, so you need its
|
||
timing with `8AX`'s pixels.
|
||
⚠️ It does not show whether `ptbase` is *also* drawn underneath — the full-res
|
||
background is
|
||
~86 % opaque and would hide it either way.
|
||
[`structures/ui-8ax-fullres-background.md`](../re/structures/ui-8ax-fullres-background.md)
|
||
</details>
|
||
|
||
* ✅ **Paint order: your exposure is two element pairs, on one screen.** We use
|
||
an order *measured from the running game* where one exists and a derived order
|
||
(a sort on each sprite's layer key) elsewhere. Checked, rather than assumed:
|
||
the derived order reproduces the measured one **exactly** on the main menu
|
||
(0 inverted pairs) and the developer splash (0). On the **title** it differs by
|
||
8 pairs — **all same-layer-key ties** — and two of those are total occlusions
|
||
(`back2eff5` is 1133×280 and *fully contains* `back2eff3` and `back2eff4`;
|
||
derived puts it on top, the game puts it underneath). The title is unaffected
|
||
in practice because it has a measured order.
|
||
Per screen: title **measured**, main menu **measured**, developer splash
|
||
**measured**, publisher splash derived but with **0 ties** (fully determined),
|
||
and **`EXTRAS` derived with 15 tied pairs of which only 2 overlap**.
|
||
✅ **That narrows again to ONE, and the capture is consistent with it.** Of the
|
||
two overlapping pairs, `ptloop01`×`ptloop02` are `loop*` animations that
|
||
`compose` skips by default, so their tie is unreachable. The remaining pair is
|
||
`ptframe3`×`ptframe4`, overlapping 102×132 px — and against `live-extras.png`
|
||
that contested region correlates **+0.9622**, *better* than the whole frame
|
||
(+0.9440) and inside the range of regions where order cannot matter (+0.8502 /
|
||
+0.9903). 🟡 Consistent with, not proof — correlation cannot see a swap between
|
||
locally similar art. **15 → 2 → 1 → consistent** is the whole paint-order risk
|
||
on your five screens.
|
||
[`structures/ui-paint-order-derived-check.md`](../re/structures/ui-paint-order-derived-check.md)
|
||
|
||
* ✅ **How good are the five screens, actually?** One page with the numbers:
|
||
[`five-screens-acceptance.md`](../re/five-screens-acceptance.md). Rendered and
|
||
correlated against the live captures — title **0.9500**, main menu **0.9460**,
|
||
`EXTRAS` **0.9440**, publisher splash **0.9600**, developer splash **0.9643**,
|
||
and every one aligns at **exactly dy=0 dx=0** over a ±2 px search, so placement
|
||
and scale are right and the residual is tone and detail rather than geometry.
|
||
Every undrawn element is accounted for (ghost instances, `.prm` primitives,
|
||
`loop*` animations, and the `8AX` name mismatch whose art is on screen anyway) —
|
||
the counts add up exactly, with nothing unexplained. The residual, ranked: tone
|
||
(γ≈1.4, the game's own ramp), `8AX` resolution, one `EXTRAS` paint-order tie,
|
||
and rotation-not-rendered which does not affect these five at rest.
|
||
⚠️ Reach: static composites at rest against single frames — this says nothing
|
||
about animation.
|
||
|
||
* ✅ **A static composite is only meaningful for a screen that SETTLES — the two
|
||
splashes are animations.** Measured with its control: suppressing the `_eff`
|
||
glows takes the **publisher splash 0.9604 → 0.9982** and the **developer splash
|
||
0.9659 → 0.9980**, while the same edit makes the title −0.002, the main menu
|
||
**−0.092** and `EXTRAS` **−0.107** worse. The draw log says why — on the
|
||
developer splash the glows draw on frames 94–115 and the logos on 116–211, so
|
||
at the captured moment every glow is already finished, including the two with
|
||
plateaus that `rest_plateau` renders visible.
|
||
So `rest_plateau` is right where a screen settles and over-draws where it does
|
||
not. **There is no "resting pose" for the splashes** — they play through and
|
||
leave, and a static composite of them is a picture of one arbitrary frame
|
||
(≈0.998 for the frame these captures hold). **Play the timeline for the two
|
||
splashes; composite statically for title / main menu / `EXTRAS`.**
|
||
[`structures/ui-resting-pose.md`](../re/structures/ui-resting-pose.md)
|
||
|
||
* ❔ **A group's DURATION is in the data; its START TIME is not.** Testing whether
|
||
the splash timeline *played* reproduces the capture: each element is on screen
|
||
for its declared span to within 2 % (glows 44 units observed vs 45 declared;
|
||
logos 192 vs 195). But every glow declares the same times `15,30,45` and every
|
||
logo the same `15,30,190,194,206,210`, so on one clock they would overlap almost
|
||
entirely — and they **do not overlap at all**. The glows run frames 94–115, the
|
||
logos 116–211, strictly sequential.
|
||
🔴 The obvious candidate is dead: each group header carries an undecoded
|
||
**lead-in word**, and it is `0x00000000` for all seven elements. Not the
|
||
keyframe times, not that word, not declaration order, not the RATC child order.
|
||
✅ **Here is the sequence to author, measured** (1 frame = 1/30 s):
|
||
publisher `palogo_sqex` **3.00 s+** (a floor — it is already at full alpha on
|
||
the capture's first frame), a **0.10 s** gap with nothing drawn, then the
|
||
developer **glows for 0.73 s**, then the developer **logos for 3.20 s**, the
|
||
glow→logo switch being a single frame boundary with no overlap. Each phase is
|
||
within 2 % of its element's declared span, so the *durations* come from the
|
||
bundle and only the *ordering* is authored.
|
||
⚠️ One capture, one run; and `palogo_anima`/`_eff` get 0 draws in all 214
|
||
frames, so a third pair's phase is not in this measurement.
|
||
[`data/splash-phase-timeline.txt`](../re/data/splash-phase-timeline.txt)
|
||
⚠️ **So you must author the sequencing.** The observed order on the developer
|
||
splash — both glows, then both logos — is *measured for one screen*, not a
|
||
decoded rule.
|
||
✅ **Verified against its own refutation:** across all 235 captured frames,
|
||
**zero** contain both a glow and a logo; the switch is one clean boundary with
|
||
two sprites either side.
|
||
🔴 **And it goes further than start times — a bundle's declared elements are
|
||
not what gets drawn.** ✅ **And the mechanism is now established, by
|
||
elimination.** The competing story — "two compositions shown in sequence" —
|
||
needs a bundle declaring the glows *without* the logos, and no such bundle
|
||
exists: only four entries in `GP_TITLE` carry `palogo` elements, and each
|
||
developer entry (11, 14) declares **all six** logos and glows. So whichever
|
||
bundle was active, only a subset of its elements was drawn at a time.
|
||
(An earlier texture-base test could not separate the two — it failed its own
|
||
control, since the publisher splash is a different bundle and shares the base
|
||
`0x11C30000`, a reused upload slot rather than an identity.) Entry 11 declares *three* logo/glow pairs; only two are ever drawn.
|
||
`palogo_anima` gets **0 frames** while `palogo_gamearts` gets **95**, from
|
||
byte-identical keyframe times. (Reach: the capture covers frames 1–214, so
|
||
"never in the window".) **Compositing every element of a bundle does not
|
||
reproduce what the game shows over time** — it is right for a static screen
|
||
that settles, and it is not a timeline.
|
||
[`structures/ui-group-start-time.md`](../re/structures/ui-group-start-time.md)
|
||
|
||
* ✅ **The splash's backdrop is an opaque black `.prm` from the bundle, painted
|
||
first.** Measured: every frame opens with the clear and then an **untextured
|
||
1280×720 quad, vertex colour `FF000000` in all 212 frames** — matching
|
||
`palogo_eff0.prm`'s declaration (`kind 0x10`, pivot 640×360, one keyframe,
|
||
`a=255`). That is why a splash render needs `--black` rather than the default
|
||
backdrop. ⚠️ Not a general rule: the main menu's measured order puts one `.prm`
|
||
at position 4 and the other **last** (the transition fade).
|
||
✅ **The splash's recorded paint order `[0,2,4,6,1,3,5]` is a real depth
|
||
order** — read off the runtime child array, and independently backed by the
|
||
static layer key (glows `0xa100` sort before logos `0xa110`;
|
||
`paint_order_audit` reports derived == measured, 0 ties). A same-day claim on
|
||
this page that it was "a temporal sequence, not depth" was **withdrawn**; see
|
||
the ❌ section in the linked page for why, if you read it before the fix.
|
||
⚠️ Separately and still true — and it is an **activation** fact, not a
|
||
paint-order one: the splash's glows and logos are never on screen together
|
||
(glows f94–115, logos f116–211,
|
||
[timeline](../re/data/splash-phase-timeline.txt)). Compositing all seven at
|
||
once reproduces no frame of the real screen. Depth says *what covers what*;
|
||
it does not say *what is up*.
|
||
[`structures/ui-prm-primitives.md`](../re/structures/ui-prm-primitives.md)
|
||
|
||
* **Menu order is geometric.** Buttons sorted top-to-bottom by resting Y. This is
|
||
✅ correct for a vertical menu and is **not** a decoded neighbour graph — the
|
||
disc's real navigation structure is unknown, and `opt ` is *not* a focus link
|
||
(measured and refuted, see
|
||
[`ui-focus-and-effect-elements.md`](../re/structures/ui-focus-and-effect-elements.md)).
|
||
* **Highlighted states pair by name** — `ptbtn01.rat` ↔ `ptbtn01f.rat`. 🟡 a
|
||
naming convention that holds for all 54 real pairs, not a decoded field.
|
||
* **`rest()` was wrong for elements with no exit animation — fixed 2026-08-28.**
|
||
A trailing run of identical keyframes was always treated as the exit and
|
||
excluded; on an element that has no exit it *is* the hold, and `rest()` fell
|
||
back to the element's **first** keyframe — off-position and transparent. Six
|
||
elements on the main menu were affected, including `ptframe1`/`ptframe2`, the
|
||
bright circuit bracket around the menu, which both the port's composite **and**
|
||
`sylpheed-cli screen render` were dropping.
|
||
The rule now: **a trailing run is the hold exactly when it is visible** (alpha
|
||
≠ 0). ⚠️ Not the pose-equality test the report proposed — `pgptitle.rat`'s
|
||
trailing run also matches its last timed keyframe, and adopting that would erase
|
||
the word PAUSE. Oracle correlation over the bracket region improved
|
||
**0.9596 → 0.9748**; the PAUSE control is unchanged.
|
||
✅ **Disc-wide**: over 2 859 bundles / 13 991 elements, `rest` moves for **30**
|
||
(0.21 %) — **4 invisible → visible, 0 visible → invisible**. Tests green
|
||
(131 passed).
|
||
❔ **Back to the port agent:** on the English main menu exactly **two** elements
|
||
satisfy your pose-equality condition (`ptframe1`/`ptframe2`), so your six span
|
||
the whole export. If any of the other four have a **transparent** trailing run,
|
||
this rule leaves them alone on purpose. **Which screens are they on, and does a
|
||
capture show any of them drawn?** If so the alpha rule is incomplete.
|
||
This also closes the old ❔ on `ptframe1`/`ptframe2` "resting at alpha 0 but the
|
||
capture shows the frame plainly".
|
||
* **The resting pose is the hold**, not the first, last or longest-dwell keyframe;
|
||
a keyframe is the **start of a ramp**.
|
||
[`ui-resting-pose.md`](../re/structures/ui-resting-pose.md). ✅
|
||
* **That ramp is linear, and it runs at 2 keyframe time units per rendered
|
||
frame.** Measured frame-by-frame off the running game's own draw stream: a
|
||
declared 15-unit fade lands on `round(255·k/15)` for all seven of its samples,
|
||
with `k` stepping 2, 4, 6, 8, 10, 12, 14 on seven consecutive submitted frames.
|
||
**measured**, not decoded — the disc says `t=30`, it does not say what a `t` is.
|
||
🟡 **But a multi-keyframe element's TIMELINE does not reproduce (2026-08-28).**
|
||
The linear law and the 2-units-per-frame rate rest on the splash's `_eff`
|
||
glows, and those are exact. Applying *their* calibration — with no free
|
||
parameter left — to `palogo_gamearts` in the same bundle and the same frames:
|
||
the logo is still at `a=255` nine frames after its declared `a=32`, its
|
||
fade-out runs ~17 frames late, and its declared 80-frame fade-in is never
|
||
drawn at all (and that is not culling — the same element is submitted down to
|
||
`a=7` on the way out). Its declared fade-out also spends 12 of 16 units
|
||
dropping only 23/255 of the alpha; the capture shows no such plateau.
|
||
🔵 **Followed up, and the candidate is now strongly favoured — but not
|
||
adopted.** That `+36` holds the *next* pose's time is supported by a
|
||
calibration-free measurement: the observed full-alpha **hold : fade-out** ratio
|
||
is 83 : 13 frames = **6.38**, the shifted reading predicts **8.00**, and the
|
||
current reading predicts **0.25** — off by **26×**. With the glow's 2
|
||
units/frame fixed and nothing else free, the current reading says the logo
|
||
holds full alpha for **2.0 frames**; the game holds it for **83**. The shift
|
||
also makes `rest()`'s plain dwell rule pick the visible hold instead of a
|
||
fully transparent pose, and removes the decoder's "last block's time is
|
||
unreadable" special case.
|
||
~~🔴 **What stops it:** … `GP_TITLE` build 7 … twins should match in
|
||
brightness …~~ **Withdrawn 2026-08-28.** That render difference is **not**
|
||
evidence about the time association. It is one element — `ptlogo_eff3.t32`, a
|
||
transient bloom that grows `0 % → 200 %` at full alpha while rotating, then
|
||
collapses — and it has **no resting pose at all**. `rest()` falls through to
|
||
its dwell fallback and returns whichever end of that movement the indexing
|
||
lands on: the invisible frame as decoded, the 200 % peak shifted. An 896×389
|
||
sprite at 200 % is larger than the screen, which is the whole 13.1 % and the
|
||
whole luminance gap. I was comparing a heuristic, not a decode.
|
||
🔴 **And that is a real defect you should know about:** `rest()`'s dwell rule
|
||
is unsound *whenever it runs*. A dwell gap is time spent interpolating **from**
|
||
pose k **to** pose k+1; neither is held unless they are equal — which is a
|
||
plateau, and the plateau path has already returned by then. **So any element
|
||
with no two adjacent identical poses has a guessed rest pose**, in our renderer
|
||
and in anything built from it. Measured disc-wide: **2 305 of 15 493 elements
|
||
(14.88 %)** — ⚠️ **corrected 2026-08-29 down from a published 3 807 (24.57 %)**,
|
||
which counted 1 502 *single-keyframe* elements as guesses; those have one pose
|
||
and it is unambiguously their rest. Of the real 2 305, **195 get a degenerate
|
||
`scale = 0 %` pose**, and the two
|
||
candidate rules agree only **50.2 %** of the time.
|
||
✅ **This does not block you — your exposure is TWO elements, both on the
|
||
splashes.** The fallback is reached only by an element that is plateau-less
|
||
*and* multi-keyframe; a single-keyframe element short-circuits. Title, main
|
||
menu and `EXTRAS` reach it **zero** times, which is why three different
|
||
fallback rules render them to identical correlations. The publisher and
|
||
developer splashes reach it once each — and there, `SYLPHEED_REST_RULE=last`
|
||
(the final keyframe) scores **+0.9982** and **+0.9758** against the current
|
||
rule's +0.9600 and +0.9643. ⚠️ Measured against single frames of a transient
|
||
animation, so it fixes which pose matches *those* captures, not which is
|
||
canonically at rest. Default unchanged — it is better on both screens where it
|
||
fires and identical on the other three, but it would move 2 305 elements
|
||
disc-wide on two measurements.
|
||
✅ **And the picture is now coherent.** Applying the last keyframe to *every*
|
||
element (not just the plateau-less ones) collapses all five screens — title
|
||
0.9500→0.6819, main menu 0.9460→0.6416, `EXTRAS` 0.9440→0.5745, and both
|
||
splashes render **blank**. The reason is the model: a group is **entry → hold →
|
||
exit**, and the exit is the screen's *dismissal*. A displayed screen is sitting
|
||
at the **hold**, not at its final pose — so `rest_plateau` is right, and the
|
||
last keyframe is the *post-exit* state. It is right for a **transient** element
|
||
precisely because a transient's settled state is "gone". The draw capture agrees
|
||
independently: on the developer splash the `_eff` glows draw on frames 94–115
|
||
and the logos on 116–211, so the glows are already over when the logos are up.
|
||
**Three independent observables — animation timing, static composites and the
|
||
per-frame draw log — all support: plateau where there is one, last keyframe
|
||
where there is not.**
|
||
📊 **Structurally, `last` is defensible for 2 293 of the 2 305 (99.5 %)**: 1 618
|
||
end invisible (a transient, gone at rest), 675 end at maximum alpha (faded in
|
||
and stopped — the final pose *is* settled), and only **12** end visible below
|
||
full alpha, which are genuinely unclear. The dwell rule returns a mid-movement
|
||
frame by construction. ⚠️ The 1 618 rest on an assumption worth seeing: that
|
||
such an element's animation has finished by the time the screen settles — shown
|
||
by the draw log for the two splash glows, unshown for the rest.
|
||
📊 The disc-wide blast radius, for whoever decides: the rules **differ on 82.3 %**
|
||
of those 2 305, so "either is fine" is not available — and the current rule
|
||
returns a **zero-scale** (collapsed, pre-roll) pose for **195** of them against
|
||
**43** under `last`, a 4.5× reduction in provably-degenerate results. That
|
||
argues the same way as the captures, from the data's own structure. The single disagreement
|
||
is `palogo_anima_eff.t32` on the developer splash, and the current answer is
|
||
the defensible one there — see below. Still worth flagging plateau-less
|
||
elements in an export rather than silently inheriting our guess; it is one pass
|
||
over the keyframes.
|
||
✅ **One concrete part of it is fixed (2026-08-28): `scale = 0` no longer
|
||
renders at full size.** `blit`/`fill_quad` coerced `scale == 0` to 100 %, so an
|
||
element collapsed to nothing was drawn full-size. The disc settles the reading:
|
||
2 166 elements have a zero-scale keyframe, **not one is zero throughout**, and
|
||
1 762 grow back out of zero — so 0 means collapsed, not "unset". **Renders are
|
||
24/24 byte-identical** on `GP_TITLE` (all 16 builds), `GP_PAUSE_MENU` and
|
||
`GP_OPTIONS`, so nothing you rely on moves; the 126 elements the old
|
||
code actually painted are **all in `GP_READY_ROOM.pak`**, already a no-go.
|
||
[`structures/ui-rat-layout.md`](../re/structures/ui-rat-layout.md)
|
||
🔴 **"rest = last keyframe" is refuted** as the fix. The splash has three
|
||
sibling glows with identical structure and times, differing in one byte
|
||
(`a=212` vs `a=255` at `t=45`); that rule would make `anima_eff` alone
|
||
invisible while its two siblings stay lit. The capture agrees weakly — box-mean
|
||
ratios capture/render are gamearts 0.717, seta 0.723, **anima 0.772**, and a
|
||
glow we drew that the game does not would put anima *below* its siblings.
|
||
[`structures/ui-resting-pose.md`](../re/structures/ui-resting-pose.md)
|
||
🟡 **The shift is still not adopted**, now for a different reason: it flips
|
||
this element to the visibly wrong answer, so it and a decision about
|
||
plateau-less elements have to land together, and neither half has a capture to
|
||
verify against.
|
||
**Default unchanged**, experiment reachable via `SYLPHEED_KF_TIME_SHIFT=1`.
|
||
**What this means for you:** the interpolation *law* is settled (linear, 2
|
||
units/frame); a multi-keyframe group's *timing* is not — do not expect a
|
||
2-frame hold where the game holds 83.
|
||
✅ **The seconds conversion is settled: `1 unit = 1/60 s`, a 30 Hz title.** The
|
||
re-test this page used to name has been run — 300 submitted frames timed on the
|
||
**idle** title (nothing loading) came out at **28.8 and 28.3 fps**, the same
|
||
rate as the 27.6 fps measured during the loading splash. The competing 60 Hz
|
||
reading is excluded: it needs the emulator at 47 % of real time while idling on
|
||
a screen that costs ~5 draws per frame.
|
||
A second, independent line agrees — the transition quad is declared black for
|
||
12 units (0.20 s under this conversion) and a capture measured the pure-black
|
||
plateau at 0.17–0.23 s
|
||
([`screen-transitions.md`](../re/screen-transitions.md)).
|
||
⚠️ Still **measured, not decoded**: no field on the disc says "sixtieths of a
|
||
second".
|
||
* **The paint order is derivable from the file.** Each `T8aD` sprite header
|
||
carries a **`u16` layer key at `+0x0A`** (the upper half of the 32-bit word at
|
||
`+0x08` is zero in all 21 184 sprites on the disc). Paint order is that key,
|
||
**stable-sorted** so equal keys keep declaration order; elements with no sprite
|
||
(`.prm` primitives, `.tbm`) have no key and take an implied one — the backdrop
|
||
and dim quads sort early, the screen-transition fade (`pteff00.prm`,
|
||
`pfeff00.prm`) sorts **last**. ✅ decoded. Checked against five paint orders read
|
||
off the running game, one of them (`GP_SAVE_LOAD`'s slot-list header, 6
|
||
instances) **exact and independent of the screens the rule was fitted to**, and
|
||
against a fresh `EXTRAS` capture. It reorders 341 of the disc's 965 builds, so
|
||
it is not a no-op dressed as a rule.
|
||
🟡 **The one residual: ties.** Where two elements share a key the game
|
||
sometimes paints them in an order nothing predicts — eight candidates refuted,
|
||
including declaration order, RATC child order, keyframe times, resting X/Y and
|
||
`kind`. Measured cost: on the three screens with ground truth it changes the
|
||
blend of **one element on one screen** (a title glow). Take the stable sort and
|
||
accept that.
|
||
|
||
* **Ⓑ returns to the title, and that title still works.** Ⓐ on the Ⓑ-returned
|
||
title opens the main menu — measured, with Ⓐ on the boot title as the control in
|
||
the same run. (Only the *attract*-returned title is inert, which is an emulator-
|
||
harness curiosity, not a port concern.)
|
||
* **Menu movement, measured off the running game.** ⬆⬇ move one item per press
|
||
and **wrap at both ends** (5-item main menu and 3-item `EXTRAS` both). ⬅➡ do
|
||
nothing. Ⓑ goes up one level **and restores focus to the item you came from**;
|
||
Ⓑ on the main menu returns to the title; Ⓑ on the title does nothing. **Initial
|
||
focus is not stable**: four boots of the same script gave `TUTORIAL`,
|
||
`TUTORIAL`, `NEW GAME`, `NEW GAME`. Do not hardcode it; pick one and say you
|
||
picked it. All **measured**, none of it on the disc.
|
||
* **Each button's destination is measured; its GamePart id is not.**
|
||
**`NEW GAME` → `DIFFICULTY`** (`EASY`/`NORMAL`/`HARD`/`BACK`, opening on
|
||
`NORMAL`) **→ `SELECT DATA`** — it does *not* hang; the run then hits the
|
||
already-documented `sub_823070B0` cache crash, which is not a menu problem.
|
||
`LOAD GAME` → the save-slot list, `TUTORIAL` → the lesson list, `OPTIONS` →
|
||
the settings menu, `EXTRAS` → `GP_TITLE` build 6, `EXTRAS ▸ MISSION SELECT` →
|
||
the stage list. The
|
||
GamePart ids (`3`, `25`, `8`, `5`, `7`) are the entries of the decoded id table
|
||
whose **names match the screens seen**; that binding is authored, not measured.
|
||
|
||
* **A screen change is a fade through black.** Each screen carries a full-screen
|
||
black `.prm` quad that paints last (`pteff00.prm` / `pfeff00.prm`) whose
|
||
keyframe group *is* the transition: black at `T0`, clear by `T1`, clear until
|
||
`T2`, then back to black on exit. ✅ decoded, with a disc-wide check — and in
|
||
`GP_TITLE` exactly the six **screen** builds carry it while the six overlays do
|
||
not. The fade-in length is `T1 − T0` and is read from the file (0.87 s for
|
||
`EXTRAS`, 0.97 s main menu, 4.08 s title). ❔ **The fade-OUT length is not on
|
||
the disc** — the last keyframe has no time slot; **measured ~0.4 s**, twice.
|
||
The black hold measures 0.17–0.23 s. ⚠️ Do not time the fade-in off a capture's
|
||
brightness: the incoming screen's own element animations dominate it and run
|
||
much longer than the quad.
|
||
|
||
* **The movie manifest names every boot-side video by role.** `dat/tables.pak`
|
||
entry `0x5b983a08`: `LOGO1`–`LOGO4` → `logo1.wmv`–`logo4.wmv` (**not on the
|
||
disc** — this is why the splash is a screen), **`ADVERTISE_MOVIE` → `ADV.wmv`**,
|
||
`STAFF_ROLL` → `SYLPH_HD720p_8M-CBR_2ch.wmv`, **`MS00A` → `S00A.wmv`** (the
|
||
new-game intro, 93.9 s, with subtitle + `VOICE_S00A` + a text overlay),
|
||
`MS01A` → `S01A.wmv`. ✅ decoded — and **`S00A.wmv` is now also measured**: matched off the running game at 0.96–1.000 with a strictly monotone playhead over 25 consecutive 0.5 s samples. It starts ~4.5 s after Ⓐ on the save slot.
|
||
**The boot intro and the attract movie are the SAME asset** — there is no
|
||
separate boot slot, and 15 of 19 captured attract frames match `ADV.wmv` with a
|
||
monotonically advancing playhead ending at its full 137 s. One video, not two.
|
||
✅ The attract movie **plays to its end**; nothing cuts it short.
|
||
✅ **A movie is skippable with a single Ⓐ.** Measured: one tap ~45 s into the
|
||
boot brought the title at ~57 s against a ~193 s no-input baseline over three
|
||
boots, with Canary's own keystroke counter proving exactly one press was
|
||
delivered — and the skipped-to title is **fully functional** (`PRESS Ⓐ` plate
|
||
present, Ⓐ opens the main menu). What breaks the boot is *hammering*: 88
|
||
presses left a permanent black screen. One press is fine.
|
||
|
||
* **The menu's sound events are named on the disc.** `tables.pak`'s `SOUNDS`
|
||
record carries 322 `SE_*` cues, and the low block is the UI vocabulary — named
|
||
after the *event*: `SE_UI_CURSOR` (2), `SE_UI_DECIDE` (3), `SE_UI_CANSEL` (4),
|
||
`SE_UI_IMPOSI` (5, the error), `SE_UI_SUB_WIN_OPN`/`_CLS` (8/9),
|
||
`SE_UI_SPLASH_IN`/`_OUT` (12/13). ✅ decoded, full list in
|
||
[`data/se-ui-cues.txt`](../re/data/se-ui-cues.txt). They all live in **one**
|
||
bank — `BANK_SE` is a single field naming `Static.slb`, and 0 of the 322 has
|
||
its own `FILES` entry.
|
||
🟡 **Which event fires which cue is a name match**, not a measurement — strong,
|
||
because these are the authors' own event names, but nobody has watched the game
|
||
emit cue 2 on a d-pad press. The port is authoring it.
|
||
✅ **The SE audio IS extractable — by playing it.** (This corrects an earlier
|
||
"cannot be extracted" on this page.) The disc carries no index: `Static.slb` has
|
||
**0 `RIFF`/`seek`/`WAVE`** and there is **no XACT container anywhere** (0 ×
|
||
`XGSF`/`SDBK`/`WBND` in 1.08 GB of `sound.pak`; no `XACT` string in the
|
||
executable — those extensions are the authoring tool's). But Canary's
|
||
`--xma_param_probe=true` logs every stream's head bytes, and searching them in
|
||
`Static.slb` locates the wave exactly:
|
||
**d-pad move → `0x1ec0` (4 pkts / 8 192 B, 0.533 s); Ⓐ confirm → `0x5d6c0`
|
||
(6 pkts / 12 288 B, 1.016 s); Ⓑ back → `0x0ec0` (2 pkts / 4 096 B, 0.344 s)** —
|
||
every cue the five screens need. Move and back reproduce with identical head
|
||
bytes across **two independent boots**. Each matched at one offset only, and
|
||
the first two are contiguous
|
||
(`0x0ec0 + 4096 = 0x1ec0`) — the bank is a packed run of whole 2 048-byte
|
||
packets with no delimiters, which is why nothing could be scanned for.
|
||
❔ **⬅ and ➡ play nothing distinct** — no new stream on either, so leave them
|
||
silent (the probe dedups, so this excludes a *distinct* invalid cue, not a
|
||
quiet replay of an already-heard one).
|
||
🟡 The Ⓐ press both confirms and opens a screen, so whether its wave is the cue
|
||
named `SE_UI_DECIDE` or `SE_UI_SUB_WIN_OPN` is not separated.
|
||
⚠️ The order is **not** cue-id order, so the index must be observed per cue, not
|
||
counted. ✅ **The slices decode**: 0.533 s, 0.344 s and 1.016 s of mono 48 kHz
|
||
audio with the attack-and-decay shape of UI blips, via
|
||
`tools/re-capture/slb_extract_wave.py` — whose wrapper reproduces a known-good
|
||
`BGM_001` decode to the same 173.808875 s, so it is verified, not assumed.
|
||
|
||
* **The boot sequence is not data-driven — the port authors it.** ❔ Four places
|
||
were checked and the order is in none: `config.ini`'s `[SYSTEM]` is empty, the
|
||
movie manifest carries assets not transitions, the requested GamePart id lives
|
||
**only as a stack argument in flight** (no persistent field, no literal store),
|
||
and the string `GP_ADVERTISE_DEMO` has **zero xrefs**. A transition is a call
|
||
with an id argument, chosen by code.
|
||
🟡 **But the states have names, and the transition uses them.** `sub_821C6458`,
|
||
the title part's state function, calls **`sub_821CC860(…, "<NAME>", 0)`** with
|
||
`TITLE_SCREEN`, `TITLE_MENU` and `LOADING` — the three states measured off the
|
||
game, in the game's own words — and installs the result. So a transition is a
|
||
call with a **name** argument, which is also why `GP_ADVERTISE_DEMO` has no
|
||
xrefs: at this level the screen graph is name-keyed, not id-keyed.
|
||
✅ The argument is now **decoded** at 46 of that function's 48 call sites (28
|
||
distinct names). ⚠️ It is a **generic name-keyed lookup**, not a screen factory —
|
||
its arguments include `BG`, `BLACK`, `FADE`, `FILE`, `KEY`, `PAD`, `SOUND`,
|
||
`GAMMA_RGB`. An earlier version of this page claimed `DIFFICULTY` and
|
||
`EXTRA_MENU` as corroborated screen names; **neither is ever the argument**, and
|
||
only `TUTORIAL_MENU` survives.
|
||
✅ **And the state machine itself is decoded**: `state = this+136`, ten states
|
||
dispatched through a jump table at `0x821C6498`, with **18 transitions** each a
|
||
literal `li`/`stw`. States **0, 2, 8** install `TITLE_SCREEN`, `TITLE_MENU`,
|
||
`LOADING`. `4 → 0` is the only edge back to the title, reached from `2 → 4` —
|
||
which matches Ⓑ-returns-to-title as measured. Full graph in
|
||
[`data/title-state-machine.txt`](../re/data/title-state-machine.txt).
|
||
⚠️ **All of that is ONE PHASE.** `GamePart_Title` dispatches on an outer phase
|
||
field at `this+132` (five values) before reaching any of it: phase 0 is the
|
||
**developer splash** (`sub_821C5690`, the same function the corpus fingered
|
||
independently), phase 4 is the title/menu machine. The ten states and
|
||
eighteen edges above live inside phase 4 alone.
|
||
✅ **State 4's edge conditions are an event code** — `sub_821C6458`'s third
|
||
argument. State 4 is the input-waiting state (reached straight after the menu
|
||
is installed) and handles 6 of 26 events: **`0` → title**, **`3`, `5`, `8`,
|
||
`25` → `LOADING`**, `10` → state 5.
|
||
🟡 What the event *numbers* mean is not decoded — button id, menu row, or
|
||
message id — so the port still takes the button→destination map from
|
||
measurement. The one-to-title / four-to-loading shape matches the five-item
|
||
menu with Ⓑ, but that is a **count-match, not a mapping**. The sequence itself is fully measured:
|
||
splash → `ADV.wmv` → title + `PRESS Ⓐ` → (idle ~8–10 s → `ADV.wmv` in full →
|
||
title) → Ⓐ → main menu, with Ⓑ from the main menu returning to the title.
|
||
|
||
* **`config.ini` picks the language, and that is what picks the EN/JP build.**
|
||
The disc's **only** config file (400 bytes, at the root; one `find` over the
|
||
whole extract). Its `[LANGUAGE]` section maps the console's `XC_LANGUAGE_*`
|
||
value to `eng`/`jpn`/`deu`/`fra`/`esp`/`ita`, defaulting to `eng` — that code
|
||
selects `GP_TITLE`'s English or Japanese build and the `<lang>.pak` families.
|
||
✅ decoded.
|
||
❔ Its `[SYSTEM]` section — which the file's own comment says holds what the
|
||
game and every game part share — is **empty**, so the boot *order* is not in
|
||
disc-side configuration at all.
|
||
* **Five GameParts are named but never registered.** Pulling every
|
||
`RegisterToFactory<N, class silph::GamePart_X>` diagnostic string binds **24 of
|
||
the 29 ids to a C++ class**
|
||
([`data/gamepart-class-ids.txt`](../re/data/gamepart-class-ids.txt)). Ids `1`,
|
||
`2`, `16`, `18`, `28` have no registration site — including
|
||
**`GP_ADVERTISE_DEMO` (1)**, which agrees with the measurement that the attract
|
||
loop is the *title* replaying `ADV.wmv` rather than a separate part. 🟡 an
|
||
argument from a diagnostic string, not from the code.
|
||
Also: **`3` and `4` are both `GamePart_SaveLoad`** — one part, two ids.
|
||
|
||
* **The GamePart id table** — 29 entries at `.rdata 0x820A1630`, confirmed by the
|
||
executable's own registration strings. ✅ This is the screen vocabulary; which
|
||
button reaches which entry is Q4 and is *not* part of it.
|
||
* **The logo splash is a screen, not a video, and it renders.** `logo1`–`logo4`
|
||
are manifest-bound with no `.wmv` on the disc. ✅ The screen is **two** bundles
|
||
in `GP_TITLE.pak`, each shipped twice: entries **10/13** the white
|
||
`SQUARE ENIX` publisher logo, entries **11/14** `GAME ARTS`/`SETA`/`studio
|
||
anima`. ⚠️ They are **invisible to the default `screen list`/`render`** —
|
||
`is_build` rejects them for having no `.rat` child. Pass **`--all`**, which
|
||
renumbers `--build`. So the first of the five screens does have a reference
|
||
composite. ✅ **And a capture**: both halves edge-correlate to their renders at
|
||
**0.91** and **0.98** at zero shift, each rejected by the other (0.03, −0.03).
|
||
✅ **Its timing is DECODED, not just measured.** The splash **fades both ways**
|
||
and the bundles carry the keyframes: `SQUARE ENIX` `[15 30 235 239 251 255]`,
|
||
developer logos `[15 30 190 194 206 210]`, each with an `_eff` glow child on
|
||
`[15 30 45]`. Under `1 unit = 1/60 s` that is a 0.25 s ramp in, a **3.42 s**
|
||
/ **2.67 s** hold, and a **0.33 s** fade out — and a 10 fps capture measures
|
||
≈ 3.5 s / ≈ 2.4 s holds and **≈ 0.3 s** fade-outs. The visible brightness
|
||
overshoot on the way in is the `_eff` glow ramping after the logo, not a
|
||
rendering artifact. Wall-clock: logos ≈ 0.4–4.7 s and ≈ 4.9–8.4 s, then
|
||
`ADV.wmv` from ≈ 8.9 s. ⚠️ The cyan `SQUARE ENIX` at ≈ 9.5 s is the
|
||
**movie's** opening, not a third splash screen.
|
||
* **Sprites carry their own labels.** No font rendering or localisation is needed
|
||
for this milestone. ✅ — and the localisation is *already baked in*: the
|
||
Japanese screens are separate builds in the same pak, not a text swap.
|
||
|
||
## Facts the port will trip over
|
||
|
||
* **`ADV.wmv` is WMV3 video + WMA Pro audio**, 1280×720 at 30 fps, 137 s. Godot 4
|
||
plays only Ogg Theora natively. How to handle that is the port's decision, not
|
||
ours — but it is not optional.
|
||
* **The disc holds 3.3 GB of video, and exactly two files are in scope:**
|
||
`dat/movie/ADV.wmv` (the boot intro *and* the attract loop — one asset) and
|
||
`dat/movie/S00A.wmv` (the new-game intro, 93.9 s). Named from the movie
|
||
manifest, ✅ decoded.
|
||
* **`Static.slb` over-declares its size** by 616 768 bytes — it is the
|
||
highest-offset entry in `sound.pak` and its size field is an allocation size. A
|
||
reader must allow a short read there and only there.
|
||
* **Voice downmixes to mono, music does not.** The left-channel downmix is correct
|
||
for spoken lines and discards half a music mix.
|
||
* **A music bank is TWO STEMS THAT PLAY TOGETHER — do not concatenate.**
|
||
✅ The "10 KB + 4.47 MB + 4.67 MB" reading was wrong: the 10 KB is the bank
|
||
header, and a bank is exactly **two waves of identical duration** (32/32 banks
|
||
on the disc). `BGM_001`'s two are **sample-synchronous** — transient
|
||
correlation peaks at lag 0.00 s over ±5 s, and both stop at the same
|
||
millisecond, 167.663 s. Wave 1 is quieter, far more L/R-decorrelated and has
|
||
almost no bass, so it reads as a surround-rear pair or a second intensity
|
||
layer — 🟡 which of those is unsettled, and `ChannelMask` is `0x0002` on both,
|
||
so the file will not say. Today's 347 s concatenation plays the piece twice,
|
||
the second time as a bass-less stem.
|
||
❔ **And it is not a seamless loop**: `BGM_001` fades out at 167.663 s and is
|
||
followed by 6.15 s of silence, with no loop-point field identified. A menu loop
|
||
is authored.
|
||
✅ **The menu's music is `BGM_103`.** The cue *table* cannot say — its BGM
|
||
entries are numeric — but `GamePart_Title`'s `sub_821C5580` plays **cue 1103**,
|
||
and `BGM_103.slb`'s two declared waves (3 876 864 / 3 930 112 B) are
|
||
byte-for-byte the two streams the XMA probe saw decoding at the main menu.
|
||
Static code, disc census and runtime all agree. The port does **not** have to
|
||
choose a track.
|
||
* **`JNGL_001.slb` does not decode.** One bank in 9 519; its payload is not a whole
|
||
number of XMA1 packets from any known data offset.
|
||
|
||
## What is still open
|
||
|
||
Every question above is answered, so this is the honest residue rather than a
|
||
work queue. **None of it blocks the five screens.** (Q1's seconds conversion was
|
||
here until 2026-08-28 and is now settled.)
|
||
|
||
| | what | why it is stuck |
|
||
|---|---|---|
|
||
| 🟡 | **cue NAME → event binding** (Q8) | event→**wave** is measured for move/confirm/back; that the cursor's wave is the cue *named* `SE_UI_CURSOR` is still read off the authors' identifiers |
|
||
| ❔ | **the other ~319 SE cues** (Q8) | located one at a time by triggering them; only the three the menu needs have been done |
|
||
| ✅ | **the paint-order tie-break** (Q3) | ✅ **CLOSED 2026-08-29 — the cost is measured and it is one pixel.** At the instant the player sees, the tie-break changes **at most 1 px at Δ1**, on the **Japanese title only**; **exactly 0 px on all five screens you ship**. The previous 24-pair bound was a `rest()` count and survives as such (entry 7's 16 reproduces exactly), but 10 of the title's 11 tied pairs are between `ptlogo_back2eff1`…`eff5` — five transient flashes that are **transparent** on the settled screen. Not a knife-edge: the live-pair count is flat across the whole settle window, and the loading bundles' tie is live only during the build-in (t17–t33). Controls live on every entry reporting zero (a different-key swap moves 25 310 / 268 698 / ~765 000 px); ⚠️ except entries 0/1/12/15, whose zeros rest on keyframe data rather than a render. ❔ *Why* the game orders ties as it does is still unknown — and now costs one pixel — [`ui-tie-break-cost-at-settle.md`](../re/structures/ui-tie-break-cost-at-settle.md) · [cost run](../re/data/tie-break-pixel-cost-gp_title.txt) · [time sweep](../re/data/tie-break-live-over-time-gp_title.txt) |
|
||
| 🟡 | **GamePart ids behind the buttons** (Q4) | the *screens* are measured; the ids are a name match onto the executable's class names |
|
||
| 🟡 | **the boot transitions in code** (Q6) | both levels decoded — phase at `this+132` (`entry→2`, `2→0`, `2→3`, `3→4`, `4→2`) and state at `this+136` inside phase 4. Phase 0 = splash (`LOGO`), phase 2 = title + `PRESS Ⓐ`, phase 4 = menu. Unknown: what the event *numbers* mean |
|
||
| 🟡 | **Ⓑ leaving the main menu** (Q5) | **upgraded 2026-08-29 (later).** The idle half of this objection is **refuted**: the main menu does not self-return for **≥ 60 s** untouched, and the ~8–10 s idle belongs to the **title**. Ⓑ is delivered (Canary logs `vk=5801`) and is the only input in ≥ 100 s before the return, so the **ordering is measured**; the latency is not (a backlogged probe void). The footer point stands — the main menu is still the only screen not advertising Ⓑ — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md#-refutation-attempt-2026-08-29--the-main-menus-own-footer-does-not-advertise-ⓑ) |
|
||
| 🟡 | **which loading bundle is `LOADING` and which `LOADING2`** (Q2) | ✅ the pair is identified — they are the loading screen, decoded from `pgloading_*` element names, and the executable names exactly two. What is open is only the assignment, and nothing observed maps a name to a bundle. ⚠️ The old row here said the pair was *unidentified `DELTASABER` plates never seen running*; that is withdrawn — a loading screen is not supposed to appear on the title path |
|
||
|
||
(An earlier version of this table called the audio items blocked on "an emulator
|
||
whose audio path can be observed". That was wrong — this build already has
|
||
`--xma_param_probe`, and using it settled both.)
|
||
|
||
⚠️ **A container caveat that bounds all of the above:** the emulator has twice
|
||
been killed mid-run with no crash line in its own log (at ~50 s and ~145 s), on a
|
||
box sitting at ~1 GB free with swap exhausted. Dynamic experiments here have to
|
||
fit in roughly two minutes of guest time, which is why several of these residuals
|
||
are unfinished rather than unattempted.
|
||
|
||
## The player's-eye map of the menus
|
||
|
||
[`docs/game/navigation.md`](../game/navigation.md) is the screen-by-screen walk
|
||
through the game from the chair — every label, what the cursor does, what each
|
||
footer offers. It was filled in on 2026-08-29 from the committed oracle frames,
|
||
and it is the page to read if you want to know what a screen *looks like* rather
|
||
than how its bundle is laid out. Every ✅ there is a capture, and what is still ❔
|
||
is what no capture answers.
|
||
|
||
## Reference data
|
||
|
||
Committed alongside the findings, so the port can be built without a disc in the
|
||
loop during development:
|
||
|
||
* `sylpheed-cli screen info --build <n> GP_TITLE.pak` — the element table, per
|
||
build, with pivots, kinds, focus links, keyframes and resting poses.
|
||
* `sylpheed-cli screen render` — the reference composite. When the port draws a
|
||
screen, this is what it should be diffed against; where they disagree, one of
|
||
them is wrong and the disagreement is worth reporting back.
|
||
* `docs/re/captures/` — framebuffer captures of the real screens, for anything
|
||
that has to be checked against the game rather than against our renderer.
|
||
|
||
## 2026-08-29 — the settled screen is one instant (`settle_units` is decodable)
|
||
|
||
✅ **decoded.** You author `settle_units` per screen. The disc gives it:
|
||
`UiBuild::settle_time()` returns the midpoint of the **longest keyframe-free
|
||
interval** in a build, and `settle_window()` returns the interval so you can judge
|
||
it. Computed from the keyframe table alone — no capture involved.
|
||
|
||
**Why it matters beyond saving an authored constant.** Our renderer posed each
|
||
element at `Element::rest()`, its last *hold* keyframe, chosen independently of
|
||
every other element. For a transient that is exactly wrong: `ptlogo_back2eff1` is
|
||
a two-frame flash (`a=0` until t52, `255` at t54–56, `0` by t58), so its last hold
|
||
*is* the peak and it burned forever. `GP_TITLE` build 4 has **five** such flashes
|
||
— one light sweep drawn as five staggered frames, all out by t110 — and drawing
|
||
them at once saturated the light arc behind the logo.
|
||
|
||
Against `live-title-build4-no-plate.png`, posing at the predicted t=198:
|
||
|
||
| | mean abs diff | arc band | pixels at clipping |
|
||
|---|---|---|---|
|
||
| console capture | — | — | **1 459** |
|
||
| `rest()` | 14.07 | 33.22 | 8 581 |
|
||
| **`--at 198`** | **12.06** | **11.79** | **1 452** |
|
||
|
||
The clipped count is unfitted and lands within 0.5 % of the console's.
|
||
|
||
⚠️ **Scope it.** Of the 1 758 composable bundles with ≥ 2 keyframe times, only
|
||
**30 %** have a window ≥ 30 units; **42 %** have one under 10 units. The latter are
|
||
mostly `loop*` fragments — they are *meant* to be in motion and have no settled
|
||
pose. Check `settle_window()`'s width before taking the midpoint.
|
||
|
||
🔴 **A retraction you should act on.** I previously told you Reborn "does not draw
|
||
`ptlogo1` / `ptlogo2` at all", and that our two renderers were therefore not
|
||
comparable on the title. **Both are wrong.** Build 4 declares *six* ptlogo
|
||
elements: indices 0 and 1 are kind `0x0`, alpha 255, and are drawn; indices 2–5
|
||
are kind `0x4` ghost instances at (−116,−7) and (437,508), alpha 0, skipped
|
||
deliberately. Hiding element 0 makes the error *worse* by +5.20 whole-frame and
|
||
+7.61 in the band. The renderers are comparable, and the ptlogos were never the
|
||
residual.
|
||
|
||
🔴 **And `ComposeOptions::at` posed leaves only** — my own fix for a bug I
|
||
mis-diagnosed. That is why the earlier rotation pose scan was flat over t = 0…600:
|
||
it moved the sweeps and never touched the top-level flashes. `at` now poses
|
||
everything; `at = None` is byte-identical (verified with `cmp`) and the
|
||
pre-rotation tag renders identically at rest, so nothing regressed.
|
||
|
||
❔ **Not settled:** the remaining 12.06, which is broad and level-like rather than
|
||
spatial — consistent with the tone term. And the **10.92** baseline quoted in
|
||
`ui-rotation-implemented.md` is **not reproducible**: the same command gives 14.07
|
||
at that document's own pre-change tag and 14.07 today. Treat conclusions resting
|
||
on it as unverified.
|
||
|
||
Detail, controls and census: [`docs/re/structures/ui-settle-time.md`](../re/structures/ui-settle-time.md).
|
||
|
||
## 2026-08-29 (later) — Q3's last open half is closed: the tie-break costs one pixel
|
||
|
||
✅ **You can stop worrying about the paint-order tie-break.** Its cost is now
|
||
measured rather than bounded, and on the five screens you ship it is **zero
|
||
pixels**. The single non-zero anywhere in `GP_TITLE` is **1 pixel at Δ1** on the
|
||
**Japanese** title, where `ptlogo2` and `ptlogo_tm` share 5 pixels of ink.
|
||
|
||
**Why the earlier 24-pair figure looked alarming.** It was counted at `rest()`,
|
||
and 10 of the title's 11 overlapping tied pairs are between
|
||
`ptlogo_back2eff1`…`eff5` — the five transient flashes from the settle-time
|
||
finding, which are **transparent on the settled screen**. A tie between two
|
||
invisible elements cannot cost a pixel. The 24 itself is not wrong; it is a
|
||
rest-pose upper bound, and I reproduced its entry-7 component (16) exactly.
|
||
|
||
**It does not hinge on picking one instant.** Sweeping every keyframe time and
|
||
every midpoint, the number of live tied pairs is **flat across the entire settle
|
||
window** — 1 on the EN title, 2 on the JP title, 0 on all four loading bundles,
|
||
whose tie is live only at t17–t33 during the build-in.
|
||
|
||
⚠️ **One honest gap:** entries 0, 1, 12 and 15 report zero with **no live
|
||
control** — no overlapping different-key pair is drawn there, so nothing
|
||
demonstrates the renderer would notice a swap on those bundles. Their zeros come
|
||
from the keyframe data (no tied pair has both elements opaque at any instant in
|
||
the window), which is why I state them, but they are a weaker kind of zero than
|
||
the other six.
|
||
|
||
❔ **Still unknown:** *why* the game orders ties as it does. Eight candidate rules
|
||
remain refuted. This finding does not answer it — it makes it cheap to get wrong.
|
||
|
||
Detail, controls and reach: [`docs/re/structures/ui-tie-break-cost-at-settle.md`](../re/structures/ui-tie-break-cost-at-settle.md).
|
||
|
||
## 2026-08-29 (later still) — the plate's pulse period is 120, not 105
|
||
|
||
✅ **decoded, and it answers the question you asked.** The `ptbtn00f` group holds
|
||
at alpha 0 between cycles. It does not loop from t=105.
|
||
|
||
**A nested record is itself a RATC bundle with its own header, and that header's
|
||
`+0x08` is the loop length.** The keyframes do not have to fill it; the slack is a
|
||
hold at the final pose:
|
||
|
||
| record | `+0x08` | largest keyframe | slack |
|
||
|---|---|---|---|
|
||
| `ptbtn00f.rat` — the plate glow | **120** | 105 | **15** |
|
||
| `ptbtn01f` … `ptbtn05f` — your main-menu focus records | 120 | 120 | 0 |
|
||
| `ptloop01` / `ptloop02` | 600 / 720 | 600 / 720 | 0 |
|
||
|
||
So the glow ramps 0→80→0 over 105 units inside a **120-unit** cycle and rests dark
|
||
for 15. Your five menu focus records fill their cycle exactly, which is what shows
|
||
the slack belongs to this record rather than to the format.
|
||
|
||
**Disc-wide, 1 781 timed nested records:** 92.3 % declare exactly their last
|
||
keyframe time, **7.7 % declare more**, and **0 declare less** — a cycle never
|
||
restarts before its own last pose. That last row is the falsifier and it never
|
||
fires; the 7.7 % is what stops the reading being a relabelling of the keyframes.
|
||
|
||
**And it survives the test your objection implies.** Both candidate periods have to
|
||
be converted by the same emulator pacing factor, and that factor is measured
|
||
*independently* on your focus ring — declared 120 units, measured 2.177 s, so
|
||
**1.0885**:
|
||
|
||
| plate period | nominal | factor needed to reach the measured 2.12–2.34 s | |
|
||
|---|---|---|---|
|
||
| 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 it** |
|
||
|
||
At 120 units the predicted period is **2.177 s** against a measured 2.12–2.34 s.
|
||
**105 cannot reach that range under any pacing factor the ring also satisfies.**
|
||
The ring and the plate are different elements in different bundles measured in
|
||
separate runs; the only thing tying them together is that both declare 120.
|
||
|
||
🔴 **So stop shipping 105.** The number is 120 and it is on the disc — not from
|
||
`exit_ramp_units`, the constant you correctly deleted, whose 129 merely happened to
|
||
fit. Your 123-vs-129 ambiguity straddled the right answer without containing it.
|
||
|
||
⚠️ The 2.24 s mean is still ~3 % above the 2.177 s prediction. That sits inside the
|
||
spread of four wall-clock samples of a ~2 s period and is not evidence of a further
|
||
hold — I looked for one and the disc does not declare it.
|
||
|
||
⚠️ **Scope:** this says where a cycle *ends*, not which records cycle. 92.3 % of
|
||
records declare no slack, and a one-shot build-in's length is simply its duration.
|
||
❔ The **top-level** `+0x08` is a different field and is untouched: every `GP_TITLE`
|
||
entry declares 300 while its elements end at 244–269, and no screen visibly repeats
|
||
every 5 s.
|
||
|
||
Detail, census and the falsification test:
|
||
[`docs/re/structures/ui-record-loop-length.md`](../re/structures/ui-record-loop-length.md).
|
||
|
||
## 2026-08-29 (later still) — the settle-time mechanism confirmed in the running game
|
||
|
||
✅ **measured.** The settle-time finding was previously confirmed only against a
|
||
*settled* frame, which shows the end state is right and says nothing about whether
|
||
the five flashes ever happen. They do. From a draw capture armed before the title
|
||
exists:
|
||
|
||
| 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_back2eff4` | 133–135 | 61.0 – 65.7 | flash, peak t~64 |
|
||
| `ptlogo_back2eff` / `ptlogo_back2` | 134–260 | 63.3 – … | **hold** |
|
||
| `ptlogo1` | **125** | **42.2** | stops moving at **t42** |
|
||
|
||
The flashes occupy a six-frame window and are absent from all 155 other sampled
|
||
frames. Units-per-frame came from the **glow's period alone** — a different
|
||
element — so the timings are not circular.
|
||
|
||
🔴 ~~**Do not draw all five flashes every time.**~~ **WITHDRAWN the same day — see
|
||
the correction at the end of this file. Your sequential drawing is correct; ignore
|
||
this.**
|
||
|
||
✅ **And your 120 is confirmed from the guest's own vertex data.** The glow quad's
|
||
per-vertex colour alpha *is* the element's fade alpha: observed range **0…80**
|
||
against a decoded peak of **80**, exact and unfitted; period **51.158 presented
|
||
frames** over 20 cycle starts; the draw is omitted entirely while dark. Fitting the
|
||
decoded ramp gives RMS 13.16 alpha levels against **38.18 for the same ramp
|
||
reversed**, so the asymmetry is real and pointing the right way.
|
||
|
||
✅ **Your top-level restriction is right and is now in the page**, verified rather
|
||
than taken: top-level gives `[160, 236]` (width 76), including the `ptloop` leaves
|
||
gives `[269, 540]` (width 271) — a "settled instant" *after every top-level
|
||
element has exited*. Thank you for catching that the description permitted the
|
||
wrong reading.
|
||
|
||
🔴 **A trap worth having before you write any draw-stream tooling:** a 2D draw's
|
||
identity here is its **vertex geometry, not its bound texture**. These sprites
|
||
sample large shared pages. Matching texture dimensions told me first that no flash
|
||
is ever drawn, and second that `ptbase2` and `pteff04` are drawn in frames 75–105 —
|
||
those frames are the **intro movie**, whose YUV planes are 640×360 and whose target
|
||
is 1280×720. Both wrong, neither loud.
|
||
|
||
Detail, controls and reach:
|
||
[`docs/re/structures/ui-title-buildin-measured.md`](../re/structures/ui-title-buildin-measured.md).
|
||
|
||
## 2026-08-29 — retraction: the flash advice was wrong
|
||
|
||
🔴 **I told you not to draw all five title flashes. That was wrong and you should
|
||
ignore it.** You checked it against your renderer instead of reasoning about it,
|
||
and you were right to.
|
||
|
||
Two claims of mine, both withdrawn:
|
||
|
||
* **"`eff3` was never drawn because a 2-unit peak is sub-frame."** `eff3` is
|
||
non-zero for t ∈ (58, 64), and the capture's frames 133 and 134 sit at t = 60.0
|
||
and 62.2 — squarely inside that window, with `eff2` and `eff4` both drawn in the
|
||
same frames. It should have been submitted and was not. **The absence is real
|
||
and unexplained**; it is not sampling phase, and it is not evidence that the
|
||
element is inert. Your sweep drawing it at t=60–62 is what the disc says.
|
||
* **"A port drawing all five shows more sweep than the console."** No evidence.
|
||
The pile-up worth warning about was the `rest()` bug, which is fixed.
|
||
|
||
⚠️ **What does hold is your own point, and it is now on my page.** The game's
|
||
timeline is 60 units/s against a 30 Hz present — 2 units per submitted frame — and
|
||
this capture ran at **2.231 units per presented frame**. So a frame-by-frame
|
||
comparison of the build-in against this capture **will** disagree about which
|
||
flash lands in which frame, and neither side is wrong. The settled comparison is
|
||
unaffected: at t=198 none of the five is drawn.
|
||
|
||
✅ **And a better confirmation of 120 than the one I sent, needing no calibration
|
||
at all.** The glow's draw is omitted when its alpha hits zero, and the smallest
|
||
alpha actually submitted across 807 drawn frames is **1** — so the culling
|
||
threshold is 1, read off the data. Then:
|
||
|
||
| | dark-frame fraction |
|
||
|---|---|
|
||
| **measured** (173 of 980 settled frames) | **17.7 %** |
|
||
| a **120**-unit cycle (15-unit dark hold) predicts | **14.4 %** |
|
||
| a **105**-unit cycle (no dark hold) predicts | **2.2 %** |
|
||
|
||
🔴 105 is out by a factor of eight, and would need a culling threshold of alpha 11
|
||
out of a peak of 80 — while the capture contains submitted draws at alpha 1, 2, 3,
|
||
4, 5, 6, 7, 8, 9, 11 and 12. **The declared 15-unit dark hold is directly visible
|
||
as the frames where the game submits no draw.** No frame rate, no pacing factor,
|
||
no wall clock.
|
||
|
||
⚠️ **One honest gap, since I am correcting myself anyway.** With units/frame from
|
||
a regression over five build-in events (residuals ≤ 0.9 frames, and it recovers
|
||
t=0 at frame 106.1 against a composite spike at 107 that was not in the fit), the
|
||
glow's 51.158-frame period implies a **114**-unit cycle, not 120. The
|
||
dark-fraction test settles 120 against 105; the 5 % gap in the period does not
|
||
have an explanation yet.
|
||
|
||
⚠️ And a limit on the vertex-alpha trick: it holds for the **glow** and does not
|
||
generalise. Read the same way, `eff4` gives 255 / 127 / 254 on frames 133 / 134 /
|
||
135 — non-monotonic. The glow's exact agreement is evidence about the glow, not a
|
||
decoded rule about vertex colour.
|
||
|
||
## 2026-08-29 — `ptlogo_back2eff3` is declared on the disc and never drawn by the game
|
||
|
||
🔴 ~~**measured**, and this one is actionable: **you draw `eff3` at t=60–62 and the
|
||
console does not.**~~ **RETRACTED — see the correction at the end of this file.
|
||
The console DOES draw it. You were right to keep drawing all five.**
|
||
|
||
Last time I called `eff3`'s absence unexplained and withdrew a bad explanation for
|
||
it. Re-examined against the **second** title build-in in the same capture — the
|
||
attract loop returns, so one run contains two — it is absent there too, and three
|
||
alternative explanations now fail:
|
||
|
||
* **Sampling phase.** `eff3` is non-zero for t ∈ (58, 64) — **six units** — against
|
||
a step of **2.23 units per presented frame**. A window wider than the step cannot
|
||
be missed. Frames 133 (t = 60.1) and 134 (t = 62.3) sit inside it and draw `eff2`
|
||
and `eff4`, not `eff3`.
|
||
* **A draw the log cannot see.** Exactly **2** draws per frame carry no geometry,
|
||
on all 932 settled title frames, always the same full-screen-triangle shader, and
|
||
present on frames where no wipe element is active. `eff3` is not among them.
|
||
* **A bad position guess on my side.** Dropping position entirely: across both
|
||
build-in windows, **zero** quads anywhere on screen have a width within ±30 of
|
||
408. The width spectrum jumps straight from 262 to 748.
|
||
|
||
Draw counts across both entries — `eff1` **4**, `eff2` **3**, **`eff3` 0**, `eff4`
|
||
**6**, against ~5 expected each.
|
||
|
||
The four are a right-aligned wipe (`eff2` 938+258, `eff3` 788+408, `eff4` 447+749,
|
||
`eff5` 64+1133, all ending at x≈1196) — a left-growing reveal in four widths, of
|
||
which the game draws three.
|
||
|
||
❔ **Why is not established.** Nothing in `eff3`'s element record distinguishes it
|
||
from its neighbours: same kind `0x0`, same keyframe shape, same `u4`/`u8`, same
|
||
scale. So this is *measured*, not decoded — if you drop `eff3` you are authoring a
|
||
behaviour I cannot derive from the file, and you should know that.
|
||
|
||
⚠️ **Two corrections to what I sent you before**, both mine and both found by
|
||
following up my own claims:
|
||
|
||
* **"Frame 107 is the title composited once" was an over-read.** It is a 27-draw
|
||
spike between the movie's last frame and the title's first, it binds **no
|
||
texture**, and only 4 of its 27 draws log geometry. I do not know what it is. The
|
||
second title entry has no such frame at all.
|
||
* **The two build-ins are NOT frame-identical.** I had that impression from a
|
||
coincidentally aligned pair of rows. Aligned properly, only **4 of 46** frames
|
||
match. They are the same animation sampled at different phases — which is the
|
||
reason the `eff3` result is robust rather than a coincidence.
|
||
|
||
Detail and the ruled-out explanations:
|
||
[`docs/re/structures/ui-title-buildin-measured.md`](../re/structures/ui-title-buildin-measured.md).
|
||
|
||
## 2026-08-29 — the boot splash black gap is ~9 units, not 12; and I was wrong to dismiss it
|
||
|
||
🔴 **First, my part in the miss.** You put the `publisher_logo` residual at 0.03 s
|
||
against a bound built from two measured ranges plus jitter slack, and I agreed it
|
||
said more about the bound than the game. It did not. You filmed it and the gap was
|
||
0.2 s. **A plausible explanation for a small number is how a real defect stays
|
||
hidden**, and I supplied one.
|
||
|
||
✅ **Now measured properly, in the draw stream rather than luminance** — which
|
||
matters, because luminance cannot separate the outgoing screen's fade tail from
|
||
true black, and the draw stream can:
|
||
|
||
| frames | submitted |
|
||
|---|---|
|
||
| 21 – 125 | `palogo_sqex`, fading to alpha **7** |
|
||
| **126 – 129** | 🔴 **no sprite quad at all** |
|
||
| 130 – 153 | the developer splash, fading in from alpha **34** |
|
||
|
||
**4 presented frames**, the only such run in the whole sequence.
|
||
|
||
Converted with the **disc as its own clock** rather than a frame rate (this run
|
||
presented at 13.1 fps, against 28 elsewhere — not usable): `palogo_sqex` declares
|
||
alpha ≥ 1 for **239.8 units** and is drawn in **105** frames → **2.284
|
||
units/frame**, which the title capture independently corroborates at 2.231.
|
||
|
||
| | units | seconds |
|
||
|---|---|---|
|
||
| **measured, 4 frames** | **9.1** | **0.152** |
|
||
| ±1 frame | 6.9 – 11.4 | 0.114 – 0.190 |
|
||
| *your authored 12* | *12* | *0.200* |
|
||
|
||
**Author ~9 units, not 12.** ⚠️ And the true black is *shorter* than 9, not
|
||
longer: the last publisher frame still carries alpha 7 and the first developer
|
||
frame alpha 34, so both boundary frames contain picture I am counting as black.
|
||
|
||
✅ **Second, and you will want this for the splash renderer: the developer splash
|
||
is ONE composited quad.** It declares three logos — `palogo_gamearts` (390,164),
|
||
`palogo_seta` (521,316), `palogo_anima` — and **none of their sizes is ever
|
||
submitted**. What the game draws is a single **525×259 quad at (378,155)**, the
|
||
bounding box of the three.
|
||
|
||
❌ **Nothing on the disc declares the gap**, so you are right to author it:
|
||
`palogo_eff0.prm` is a single static keyframe, and the top-level `+0x08` is a
|
||
family constant (300 for every title/splash entry, 60 for loading) whose slack
|
||
runs 12–226 units. ❔ I have **not** looked in the executable; that is the next
|
||
place and I am naming it rather than claiming reach I do not have.
|
||
|
||
### Your sweep question: `+0x08` does not settle it, but the oracle does — for the title
|
||
|
||
`ptloop01.rat` declares 600 with keyframes to exactly t=600; `ptloop02.rat` 720 to
|
||
720. **Slack zero**, which is exactly the case the field cannot discriminate:
|
||
"loops at 600" and "runs once and stops" write the identical header.
|
||
|
||
The draw stream is unambiguous for the **title**: across two dwells the sweep quad
|
||
oscillates over its whole x range and resets hard to the same start value —
|
||
**1 reset inside dwell 1, 2 inside dwell 2**. It does not park.
|
||
|
||
⚠️ **But you asked about the main menu, and that is not what I measured.** Both
|
||
screens declare the same 600/720. Either the menu behaves differently, or "best
|
||
match" is weak at detecting an absence — your own caveat. **Unresolved for the
|
||
menu.**
|
||
|
||
📌 And your bounding-box refutation is taken: I have had those box figures from you
|
||
and did not question them. A box over scattered pixels locates the outermost
|
||
differing pixels, not the difference.
|
||
|
||
Detail: [`docs/re/structures/boot-splash-gap-measured.md`](../re/structures/boot-splash-gap-measured.md).
|
||
|
||
## 2026-08-29 — RETRACTION: the console draws all five flashes, including `eff3`
|
||
|
||
🔴🔴 **My "the game never draws `ptlogo_back2eff3`" was wrong.** You declined to act
|
||
on it — *"I will not stop drawing an element on a claim whose own identification
|
||
excludes that element from its bounding box"* — and that judgement was correct
|
||
twice over: the identification was broken, and so was the finding built on the
|
||
same instrument.
|
||
|
||
Parsed properly, **all five flashes fire in both title entries in the declared
|
||
stagger**:
|
||
|
||
| element | entry 1 | entry 2 | declared |
|
||
|---|---|---|---|
|
||
| `eff1` | 130–131 | 5953–5955 | flash t54–58 |
|
||
| `eff2` | 133 | 5955–5957 | flash t58–62 |
|
||
| **`eff3`** | **133–134** | **5957–5958** | **flash t60–64** |
|
||
| `eff4` | 133–135 | 5957–5959 | flash t62–66 |
|
||
| `eff`/`eff5` | 134 → | 5958 → | holds |
|
||
| `ptlogo_back2` | 136 → | 5962 → | holds |
|
||
|
||
Frames 133/134 are t = 60.1 and 62.3 — inside `eff3`'s declared window. **The disc
|
||
was right about every element; my reading of the oracle was wrong.**
|
||
|
||
### The mechanism, because it invalidated your developer-splash correction's twin
|
||
|
||
A draw **batches several quads** — `indices=4` is one, `indices=8` two,
|
||
`indices=24` six — and the log dumps only the first **8 vertices**. Min/max over a
|
||
line's vertex list therefore *merges* quads.
|
||
|
||
`eff3` is batched with `eff4`, and because the wipe family is right-aligned, `eff3`
|
||
(788…1196) lies **entirely inside** `eff4` (447…1196). The union is **exactly
|
||
`eff4`'s extent** — the merged box matched `eff4` to 1 px and `eff3` simply
|
||
disappeared, with nothing anomalous to notice.
|
||
|
||
🔴 **And your developer-splash refutation was the same bug**, which you found by
|
||
arithmetic before I found it by measurement. `525×259` was `gamearts_eff` merged
|
||
with `seta_eff`. The splash draws three logos and three glows as separate quads.
|
||
⚠️ The **9-unit black hold is unaffected** — those glows are the developer
|
||
splash's first draw, so frame 130 is still its first drawn frame.
|
||
|
||
### The part worth keeping
|
||
|
||
I reported three alternative explanations "ruled out". All three were aimed at the
|
||
wrong failure — in particular, my "a draw the log cannot see" check counted draws
|
||
with **no** geometry line, when the hiding place was draws with **partial**
|
||
geometry. **Refuting three wrong hypotheses is not evidence for a fourth**, and a
|
||
list of failure modes written by whoever built the instrument is the least likely
|
||
to contain that instrument's blind spot.
|
||
|
||
`tools/re-capture/quads_per_frame.py` now parses vertices in groups of four and
|
||
warns whenever the logged quad count falls short of `indices / 4`.
|
||
|
||
✅ Your independent derivation of **239.816** from the exported keyframes, against
|
||
my 239.8 from the draw stream, is the check that conversion needed — it is the
|
||
denominator of the 9-unit hold you now ship.
|
||
|
||
❔ **Still not settled: the main-menu sweeps.** Two capture attempts failed — one
|
||
crashed the guest (a double Ⓐ tap, now guarded), one drifted to a flight screen.
|
||
The title answer stands; the menu is unmeasured.
|
||
|
||
## 2026-08-29 — a keyless primitive's position, where the file forces it
|
||
|
||
✅ **decoded — and it answers your `build_12`/`build_15` contradiction. Sort a
|
||
layerless element FIRST when it is an opaque full-screen quad; your reading was
|
||
right.**
|
||
|
||
The rule, and it is a constraint rather than a preference:
|
||
|
||
> An element that covers the screen and is **fully opaque** at some instant cannot
|
||
> paint above anything visible at that instant. Where the elements visible during
|
||
> its opaque span are **all** of them, its position is forced to first.
|
||
|
||
`pgloading_eff00.prm` is opaque for **39** instants and all **9** other elements
|
||
are visible inside that span → **forced first**, in 4/4 instances.
|
||
|
||
**Two controls, both measured orders from the running game, and the first is the
|
||
one that matters:**
|
||
|
||
| primitive | measured | opaque instants | forced below | rule |
|
||
|---|---|---|---|---|
|
||
| `palogo_eff0.prm` | **FIRST** | 211 | **6 of 6** | ✅ forced first |
|
||
| `pteff00.prm` | **LAST** | 2 | 3 of 23 | ✅ permitted on top |
|
||
|
||
🔴 `palogo_eff0.prm` is *named like an overlay*. **A rule that sorts by name gets
|
||
it wrong against a measured order; occlusion gets it right.** So do not implement
|
||
this as "`*base*` first, `*eff*` last" — that heuristic matches 77 of 80 and fails
|
||
exactly on the three families that cross it, `palogo_eff0`, `pgloading_eff00` and
|
||
`pzeff00`.
|
||
|
||
⚠️ **`pteff00.prm` must stay on top.** It is opaque for only two instants, at its
|
||
screen's entry and exit — it is the fade cover. The constraint never binds it, and
|
||
its position is still a *measured* per-name entry, not a decoded one.
|
||
|
||
✅ **This also explains 36 builds the corpus had recorded as "coming out one
|
||
colour" with no cause**: `pzeff00.prm` is forced first in 32 of 32 instances, so
|
||
they were wiped by our own sort rather than by the game.
|
||
|
||
🔴 **One limit, found when the rule's own disc-wide test failed.** Applied to
|
||
`.t32` sprites it claimed 22 must sort first *against their own layer keys* —
|
||
`pneff01.t32` (key `0xd850`, #8 of 13), `pbfriendly.t32` (`0x9230`, #17 of 49). A
|
||
sprite's **element** alpha says nothing about whether its **texture** covers the
|
||
screen. It is now restricted to untextured primitives. If you implement this,
|
||
apply the same restriction.
|
||
|
||
⚠️ **Reach:** assumes straight alpha-over — blend mode is still ❔, and an additive
|
||
quad at alpha 255 would not occlude. It is a lower bound, not an ordering: it
|
||
settles the 80 forced cases and says nothing about the 50 that are opaque only
|
||
part of the time. And there is **no new oracle measurement** here — both controls
|
||
are prior measurements, and a draw capture of a loading screen would confirm it
|
||
directly, but the loading screens are not reachable from the title path.
|
||
|
||
Detail: [`docs/re/structures/ui-forced-backdrop.md`](../re/structures/ui-forced-backdrop.md).
|
||
|
||
## 2026-08-29 — the opaque span: your 256 and my 211 are the same definition
|
||
|
||
✅ **No disagreement.** `palogo_eff0.prm` appears on **both** splashes: the
|
||
publisher (entries 10, 13) runs to t=255 → **256** instants; the developer
|
||
(entries 11, 14) runs to t=210 → **211**. You computed the publisher, my page
|
||
quoted the developer. Both right, same rule. The page now names the entries.
|
||
|
||
**The definition, to answer your question directly:**
|
||
|
||
* the span is `0 ..= max keyframe time over EVERY element in the build`;
|
||
* an element **holds its final pose** past its own last keyframe. **Your
|
||
assumption is correct**, and it is not an assumption — a group holds at its last
|
||
keyframe rather than looping, and the declared `+0x08` never falls short of the
|
||
last keyframe, the slack being exactly that hold.
|
||
|
||
**You were right that it is doing real work.** Over the 130 keyless full-screen
|
||
primitives with an opaque interval:
|
||
|
||
| alternative convention | verdicts changed |
|
||
|---|---|
|
||
| span = the header's declared `+0x08` | **0** |
|
||
| span = the primitive's own last keyframe | 72 |
|
||
| elements **gone** after their last keyframe | **72** |
|
||
|
||
🔴 **The hold decides 55 % of verdicts, and dropping it is refuted by a measured
|
||
order.** `palogo_eff0.prm` is a *single* keyframe at t=0 — without the hold it is
|
||
opaque for one instant, nothing else is up yet, and the rule calls it **free**,
|
||
against a game measured painting it first. That is now a test.
|
||
|
||
✅ **Your verdicts are safe regardless.** `pgloading_eff00.prm` comes out **first**
|
||
under all four conventions and `pteff00.prm` **free** under all four. Only
|
||
`palogo_eff0.prm` moves, and only under the one its own measured order rules out.
|
||
|
||
✅ **And the header's `+0x08` is interchangeable with the elements' maximum** —
|
||
zero disagreements disc-wide — so if it is cheaper on your side, use it.
|
||
|
||
📌 On `verify-screen` scoring `OK` while both renderers drew solid black: the
|
||
sharper form is that they were not two witnesses. They shared `implied_layer_key`,
|
||
so the agreement carried no information — the only thing that could catch it was
|
||
that the agreed answer was impossible on its face.
|
||
|
||
## 2026-08-29 — the 114-vs-120 gap was mine, and it is closed
|
||
|
||
✅ **The declared 120 stands. Nothing you ship changes.** The gap I flagged as
|
||
unexplained was a category error in my own arithmetic.
|
||
|
||
**What I found in the draw stream:** `GP_TITLE` build 4 declares `t = 0…269` —
|
||
about 120 presented frames at this run's pacing — and the title dwell lasted
|
||
**~1 100**. `ptcopyright` declares alpha ≥ 1 for **106 units** and is drawn for
|
||
**1 050 frames**; `ptlogo1` declares an exit at t=264 and is drawn for 1 095. Both
|
||
disappear within three frames of the dwell ending.
|
||
|
||
> **The top-level clock advances through the build-in, stops inside the settle
|
||
> window `[160, 236]`, and holds. The exit ramp is not on a timer — it plays when
|
||
> something makes the screen leave.**
|
||
|
||
That is the settle-time decode observed from the other side, in the game rather
|
||
than in the file — and it is worth having explicitly if you drive transitions:
|
||
**do not schedule a screen's exit off its own timeline.**
|
||
|
||
🔴 **And it explains the 114.** My 2.231 units/frame was regressed over *build-in*
|
||
events — the only stretch where the top-level clock advances — and I applied it to
|
||
the glow's period, measured over the settled dwell where that clock is frozen and
|
||
only the record's own clock runs. Two different clocks. The 120 was never in doubt
|
||
from the dark-fraction test, which needs no conversion at all.
|
||
|
||
✅ The 51.158-frame period is now confirmed by a **second independent estimator**
|
||
(autocorrelation: lag 51, harmonics at 102 and 154).
|
||
|
||
❔ **The sweeps' period is still unmeasured**, and I would rather say so than give
|
||
you a number: the same estimator disagrees between two dwells of the same screen
|
||
(515 vs 452 frames). Combined with the zero-slack `+0x08`, neither the file nor
|
||
this capture settles whether they loop. **On the title they demonstrably do not
|
||
park**; the menu remains open.
|
||
|
||
🔴 **Blocker you should know about, because it bounds what I can answer:** a single
|
||
Ⓐ press on the title **faults the guest** in this container. Three menu-capture
|
||
attempts, two ending in register dumps of 223 MB and 519 MB, against three runs in
|
||
the same session that pressed nothing and all completed. It is the crash the
|
||
capture script's own header records from 2026-08-18. Menu-side dynamic RE is
|
||
blocked here until that is understood; the corpus's existing menu measurements
|
||
predate it.
|
||
|
||
Detail: [`docs/re/structures/ui-clock-freezes-at-settle.md`](../re/structures/ui-clock-freezes-at-settle.md).
|
||
|
||
## 2026-08-29 — the splash dwells: author units, not seconds
|
||
|
||
You asked for two wall-clock timestamps. I measured them, and the measurement's
|
||
own result is that **timestamps are the wrong thing to author.**
|
||
|
||
✅ **The dwells are declared on the disc:**
|
||
|
||
| splash | declared | at 60 units/s | corpus wall clock, 3 cold boots |
|
||
|---|---|---|---|
|
||
| publisher (entries 10, 13) | t = 0…**255** | **4.250 s** | 4.30 / 4.60 / 4.37 |
|
||
| developer (entries 11, 14) | t = 0…**210** | **3.500 s** | **3.51 / 3.50** / 3.37 |
|
||
|
||
The developer splash agrees to **1.1 %**, two of its three runs to 0.3 %.
|
||
|
||
🔴 **And my fresh boot is the argument against seconds.** With a frame→wall-clock
|
||
map it puts the same two dwells at **5.10–5.61 s** and 3.83–4.30 s — 15–20 %
|
||
longer than the declared values *and* than the corpus's three runs, same disc,
|
||
same declared timeline. Three independent measurements of this container's rate
|
||
(13.1 fps, ~28 fps, this one) say the same thing. **A seconds figure is one run's
|
||
emulator pacing.** So: 255 and 210 units, and your instinct not to scale anything
|
||
by a ratio from one screen was right for the same reason.
|
||
|
||
**Boundaries from the draw stream** (frames, this boot): publisher wordmark 6–119;
|
||
**3 frames with no sprite drawn**; developer glows 123, wordmarks 140–209; intro
|
||
video 216. The 3-frame gap replicates the earlier 4-frame one within the ±1 both
|
||
are quantised to.
|
||
|
||
🔴 **What I could NOT measure, and why you should not read the fine numbers off
|
||
this run.** `frame_clock.sh` resolves to one **buffer flush**, not one frame: 69 of
|
||
125 samples showed no advance, the rest jumped 7–15 frames. Interpolating inside a
|
||
burst made the apparent rate swing 0.0164–0.0316 s/frame — the flush, not the
|
||
guest. **Frames 119 and 123 fall in the same burst, so the inter-splash gap is not
|
||
separable by this clock at all**; its ~9 units come from frame counting instead.
|
||
Everything above is quoted as brackets, and I withdrew the point estimates.
|
||
|
||
❔ Still open: the publisher's 4.1 % error against its declared 4.250 s, where the
|
||
developer's is 1.1 %. And the developer→intro gap is only bounded (5.70–6.21 s end
|
||
to end) because the movie loads inside a flush burst.
|
||
|
||
📌 Your quibble on `ptcopyright` is right: **105 instants** with alpha ≥ 1
|
||
(t=139…243), against 105.89 units of span. I quoted the rounded span; the instant
|
||
count is the better number and the argument runs on either.
|
||
|
||
Detail: [`docs/re/structures/boot-splash-dwells-are-declared.md`](../re/structures/boot-splash-dwells-are-declared.md).
|
||
|