This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/port/HANDOFF.md
Sylpheed RE agent 489ea12759 re(ui): the eff-name implication for bit 0x02 is refuted disc-wide
Last iteration I killed the biconditional and reported that the one-way
reading survived: all 10 bit-set sprites on GP_TITLE build 4 are eff
names, so "bit set => eff name". Checked over the disc, that is false.

  sprites with a resolvable preceding name   14 709
    bit SET   & name has 'eff'                2 338
    bit SET   & name lacks 'eff'              2 657   <-- counterexamples
    bit clear & name has 'eff'                1 399
    bit clear & name lacks 'eff'              8 315

  P(eff | set)   = 0.468
  P(eff | clear) = 0.144

The implication fails more often than it holds. What survives is an
association -- 3.3x enrichment -- and build 4's 10/10 was a local naming
habit in an 18-element bundle, not a format rule.

The counterexamples are the useful part: pv_loading_ring0,
pv_loading_light0-3, pv_loading_line, px_bunk_line, px_top_extra. Rings,
glows, lights, thin lines -- effect-like artwork that does not carry the
eff naming convention. Consistent with the bit marking effect sprites by
authoring intent rather than by name, which is a description and not a
decode, and is labelled as such.

Names here come from the string immediately preceding each T8aD,
validated 17/18 on build 4 against the RATC child order; the single
mismatch is the known pteff04.t32 -> registered as 8AX case, so this is
the element (opt) name rather than the sprite's registered name. That
mismatch is itself an independent confirmation of the 8AX finding,
reached from the opposite direction.

METHOD: a pattern perfect on one screen can be near-chance on the disc;
and when an association survives a refuted implication, the
counterexamples are the finding.
2026-08-29 03:20:24 +00:00

694 lines
52 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
## Status
| | Question | State | Answer / link |
|---|---|---|---|
| Q1 | keyframe time unit + ramp shape | ✅ answered, 🟡 one gap | ramp is **linear**; **2 units per rendered frame**; **`1 unit = 1/60 s` — settled**, the idle title presents at 28.5 fps so the game is 30 Hz. 🟡 **The interpolation law is settled; the group TIMELINE for multi-keyframe elements is not**`palogo_gamearts` is still at full alpha 9 frames after its declared `a=32`, and its declared 80-frame fade-in never draws — [`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.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`, 0/1 and 10/11 two unidentified `DELTASABER` plates — [`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 0x000x7f 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 **2 overlapping tied pairs on `EXTRAS`** — [`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 — `NEW GAME``DIFFICULTY``SELECT DATA`, not a hang. The **GamePart id is still a name match**, not a measurement — [`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) |
| 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
* **`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 ≈ 112225): 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·(scale100)/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 1023 %, 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`). ⚠️ 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 ≈ 318954, y ≈ 560672 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 ~060, 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 04), 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)
* 🟡 **`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, 715×
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 — `8AX` is
~86 % opaque and would hide it either way.
[`structures/ui-8ax-fullres-background.md`](../re/structures/ui-8ax-fullres-background.md)
***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)
* **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: **3 807 of 15 493 elements
(24.57 %)**, of which **195 get a degenerate `scale = 0 %` pose**, and the two
candidate rules agree only **50.2 %** of the time.
**This does not block you.** On the five screens the port needs, 14 elements
are affected and the candidate rules **agree on 13**. 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.170.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.170.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.961.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 ~810 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.44.7 s and ≈ 4.98.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) | eight candidates refuted; costs one element's blend on one screen |
| 🟡 | **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 |
| ❔ | **builds 0/1 and 10/11**, the `DELTASABER` plates (Q2) | never seen anywhere in the boot path, the title-side screens or the attract loop. A mission load is the remaining candidate and this container kills runs before one completes |
(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.
## 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.