# 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 | **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) | | 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); SE audio is **undecodable from the disc** β€” no XACT container exists anywhere β€” [`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 β‰ˆ 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. ⚠️ **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 `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. 🟑 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. * **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.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(…, "", 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 `.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` 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) | 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 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.