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 f11fde51e5 re(ui): rotation is not nested-only, and the pivot-anchored scale is measured
Two corrections and one new confirmation, all from finishing the sweep I
left running last iteration.

Refuted, by my own sweep, within the hour: "rotation appears to live only
in nested .rat leaf records". That held for GP_TITLE, GP_BUNK and
GP_CHALLENGE -- the three archives the sweep had reached when I wrote it
-- and fails on GP_DIALOG and GP_DEBRIEFING_PILOTLOG, which rotate
top-level elements. Scoping the claim made it cheap to withdraw, but the
sentence should have waited for the sweep.

Those top-level cases are the best evidence on the disc, and they show up
in `screen info --geometry` without a hex dump. GP_DIALOG build 0:
pceff03/pceff04 ramp r = 90 -> 30 -> 10 -> 3 -> 0 while alpha ramps
0 -> 255 and they slide into place -- a swing-in settling upright. Build
6: pzeff02 ramps 43 -> 61 -> 75 -> 90 while scaling 112% -> 200% and
fading to 0 -- a spin-out. Rows committed as reference data.

New: the pivot-anchored scale term in blit, kf.x - pivot*(scale-100)/100,
was implemented and reasoned about but never measured, because every
element previously examined sits at 100% scale where the term is exactly
zero. The ptloop pair scale 600% and 800%, where it is worth 450 and 630
px. Formula predicts centre y = 360.0 for both; capture measures 359.1
and 360.0. Top-left anchoring predicts 810 and 990. Horizontally it makes
t-from-position agree with t-from-alpha to 0.33 / 0.65 units against ~8
without it.

That agreement does NOT prove linear interpolation -- both fields were
inverted through the same linear map, so a shared easing curve cancels.
It shows position and alpha ride one shared parameter. Recorded as such.

The disc-wide sweep is still running; the count is incomplete, the
existence is settled.
2026-08-28 22:46:44 +00:00

38 KiB
Raw Blame History

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 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 — 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
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
Q4 button → GamePart answered measured which screen all 5 buttons open — NEW GAMEDIFFICULTYSELECT DATA, not a hang. The GamePart id is still a name match, not a measurement — 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
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
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
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
Q9 video binding + playback rules answered decoded from the movie manifest: ADVERTISE_MOVIEADV.wmv (boot intro and attract are one asset), MS00AS00A.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
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
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

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. 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. 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. ⚠️ 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.ratpteff03.t32, ptloop02.ratpteff03a.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 refutedGP_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 ⚠️ 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.
  • 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).

  • Highlighted states pair by nameptbtn01.ratptbtn01f.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.

  • 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. 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). ⚠️ 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 GAMEDIFFICULTY (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, EXTRASGP_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: LOGO1LOGO4logo1.wmvlogo4.wmv (not on the disc — this is why the splash is a screen), ADVERTISE_MOVIEADV.wmv, STAFF_ROLLSYLPH_HD720p_8M-CBR_2ch.wmv, MS00AS00A.wmv (the new-game intro, 93.9 s, with subtitle + VOICE_S00A + a text overlay), MS01AS01A.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. 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. ⚠️ 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 codesub_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, 25LOADING, 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). 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. logo1logo4 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/renderis_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.