Last iteration I wrote that a bundle is a palette whose elements are
selectively activated. The disjoint glow/logo phases have two
explanations and I asserted one:
1. one bundle, some elements run then others;
2. two bundle-loads shown in sequence (entries 11 and 14 are twins
declaring identical sprites).
The draw log's tex[base=...] looked like it would separate them. It does
not, and the control is in the same table:
publisher splash f1-90 0x11C30000, 0x10000000
glows f94-115 0x11C30000, 0x10000000
logos f116-211 0x11C30000, 0x10000000
The publisher splash is certainly a DIFFERENT bundle from the developer
splash, and it uses the same base. So 0x11C30000 is a reused upload slot,
not a bundle identity, and the test cannot choose between the two
hypotheses.
Survives: a bundle's declared elements are not what gets drawn.
palogo_anima and palogo_gamearts carry byte-identical keyframe times and
in the same run one is drawn 95 frames and the other none -- and
whichever twin was active declares both. The phases are strictly disjoint
(0 overlapping frames in 235).
Withdrawn: the mechanism. The practical consequence is unchanged --
compositing every element of a bundle does not reproduce what the game
shows over time -- but the why is not established and I stated it as
though it were.
What would separate them: a per-draw capture recording the bundle each
draw came from, or a file-IO log showing whether a second RATC entry is
read between frames 115 and 116.
METHOD: a shared resource address does not identify the resource's owner;
and state the mechanism as a separate claim from the observation, or the
weaker one inherits the stronger one's evidence.
61 KiB
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. ✅ REPLICATED 2026-08-29 — for ANIMATION, read +36 as the time the NEXT pose is reached. Three elements across two screens: palogo_gamearts and palogo_seta hold full alpha for 83 frames and palogo_sqex for ≥77, where the current reading predicts 6–8 and the shifted one 80–102. The elements that cannot discriminate (the _eff glows, on which the linear law was measured) fit both. ⚠️ Our decoder still defaults to the other reading (SYLPHEED_KF_TIME_SHIFT=1 to flip) because it changes rest() on one element — but that is an unsound fallback guessing either way, so static rendering is unaffected and animation timing should use the shift |
| 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, ❔ 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. ⚠️ The key does not fully order a screen: elements sharing a key are tied, and the tie-break is ❔ undecodable from the bundle — declaration table, T8aD header (exhaustive: every offset 0x00–0x7f at u8/u16/u32, both directions, 0 fields match the measured order against 64 for the control) and the RATC child order all give the same order the game does not use. Your exposure is 2 overlapping tied pairs on EXTRAS — 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 |
| 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). ✅ 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 |
| 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 |
| 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.pakis eight screens, each shipped twice — English and Japanese. Build 4 is the English title art and 7 its Japanese twin; 2/3 are thePRESS Ⓐ BUTTONplate, 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 theEXTRASsubmenu — the only submenu inside this archive. Builds 0/1 and 10/11 are aDELTASABER / 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. ⚠️0x3002is one member of a0x3000family with sub-bits, and it is not the only button kind on the disc:GP_READY_ROOM's 902 bundles contain zero0x3002and use0x3000/0x3004/0x300c/0x3008instead. Nothing in this milestone changes — every screen in scope isGP_TITLE— but do not shipkind == 0x3002as a general button test. (The kind is the 4thu32of 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 --blackedge-correlates 0.9163 at (0,0), so the geometry ofrestis the arrived pose; the timeline is not needed for the title. ⚠️ But without--primitivesthe whole frame is +13.14 too bright (R +12.35, G +13.58, B +13.48); with them, +0.55. The missing element ispteff02.prm, the 25 % dim, and--primitivesis 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'sZswoosh thin with a pink/magenta edge, our render draws it thick and solid white. Crop:title-swoosh-capture-vs-render.png. The elements areptlogo_back2.t32,ptlogo_back2eff.t32andptlogo_back2eff1…5. 🟡 Those five are also the group with the known unsolved paint-order tie-break (key0x8083) — 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:blitsizes a sprite from its texture, and applies the pivot only askf.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 twoptloopsweeps 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'stsolved from position agree withtsolved 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 biteptlogo1/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 is0x??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_back25.4 % opaque, its glow 10.3 %, the fiveeffsegments 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. TheT8aDheader word at+0x04splits the title's sprites exactly along effect-vs-normal:pteff01,pteff03a,ptlogo_back2eff1…5,ptlogoall_eff/_eff2are0x8832;ptlogo1/2,ptlogo_tm,ptbase2,ptlogo_back2,ptcopyrightandptlogo_back2effare0x8830. One bit —0x02. Disc-wide it is a real independent flag: 19 216 sprites, 18 distinct values, bit0x02set 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-0x02sprites additively and re-correlate the title against the capture. Noteptlogo_back2effis0x8830despite its name, so this is a field and not a naming pattern. ❔ Not diagnosed — but two candidate meanings are now dead: not "the name containseff" — and not even the one-way "bit ⇒effname", 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/pteff03acarry the bit and persist tot=250). 🛑 Parked — four candidate meanings are now dead (additive blend;effname, both directions; transient element; premultiplied alpha, refuted because flagged sprites violateRGB ≤ Amore than unflagged, 55.5 % vs 33.7 %) and none produced a positive account. It blocks nothing — your screens composite at 0.947 correlation against a capture without it. ⚠️ Practical note for an asset pipeline:T8aDheaders 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 twoptloopsweeps (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 toy=±1.81NDC).ui_layout::blitdraws 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), andui_layout::Keyframenow carries it asrotation_deg. Confirmed against the framebuffer, not against our own renderer: the twoptlooprecords 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_BUNKentry117ca14fholds a group whose+12ramps 0 → 360 with position, scale and alpha all constant — a spin in place. Disc-wide+12is 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.ratleaf records. ⚠️ I told you one iteration ago that it looked nested-only; that was three archives' worth of pattern and it is refuted —GP_DIALOGandGP_DEBRIEFING_PILOTLOGrotate 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 inscreen info --build 0 --geometry dat/GP_DIALOG.pak:pceff03.t32andpceff04.t32rampr=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'spzeff02.t32ramps 43 → 61 → 75 → 90 while scaling 112 % → 200 % and fading to 0 — a spin-out burst. (2)sylpheed-cli screen renderstill does not rotate. The field is decoded, not rendered;ui_layout::blitis 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,EXTRASand 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 nestedptlooprecords (r = 30and−45), and at rest those sit atx = 1521andx = −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+4and+8are 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+0x04bit0x02, 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 dumpRB_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(anopt-linkedRATCat0xbb5966) sweepspteff03.t32left→right over 450 units = 7.5 s;ptloop02.ratsweepspteff03a.t32right→left over 570 units = 9.5 s. Ordinary 40-byte keyframe blocks from+0x68, three keyframes each. - ✅ The
PRESS Ⓐ BUTTONplate 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 isptbtn00f.rat, the plate's highlight variant (build 2, not build 4), whose alpha ramps0x00 → 0x06 → 0x4a → 0x50(hold)→ 0x4a → 0x06 → 0x00over eight keyframes att = 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 terminatorend, 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 toptloop01/02. Both halves were wrong: it is the plate, and it is a different build.
- ✅ Two slow light sweeps in build 4.
-
🟡 Our composite is brighter than the emulator's frame — measured, and you would be authoring if you apply it. Alignment is exact (best offset dy=0 dx=0, correlation 0.9466), so only the tone differs. Fitting on 16×16 patches that are flat in both images:
capture ≈ 255·(render/255)^γwith γ = 1.491 (main menu), 1.493 (EXTRAS), 1.338 (title). ⚠️ Narrow reach. Those flat patches span only render values ~0–60, where a gamma and a plain scale are nearly indistinguishable — on both menus the errors are 0.28 vs 0.34 and 0.22 vs 0.28. Only the title separates them (1.08 vs 9.02). Nothing here constrains midtones or highlights. 🔴 The held-out control failed to discriminate: the splash's 2 918 flat patches are pure black (render 0–4), so every model scores ≈ 0. 🔴 My "it may be the emulator" caveat is withdrawn. I said canary applieskernel_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 theDC_LUTregisters 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),VdGetCurrentDisplayGammais called once at video init, betweenVdGetSystemCommandBufferandVdSetDisplayMode— the moment a ramp-builder would ask. Control in the same log: 359VdRetrainEDRAMlines, 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 -
🟡
screen rendersilently 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 & 0x4ghost instances,.prmprimitives,loop*animations) exceptpteff04.t32on the title andpteff05.t32on both menus. Those arekind 0x0, one keyframe, resta = 255, pivot(640,360)— full-screen and opaque. Cause: the element declarespteff05.t32, but theT8aDbehind itsoptlink is registered under the name8AX, so the sprite lookup misses and a silentcontinuedrops it. ✅ It does not currently show, becauseptbase.t32(640×360, drawn at 200 %) is the same artwork at half resolution — its 2× upscale differs from8AXby mean 2.05, and our background is pixel-identical to8AXin every patch sampled. ✅ SETTLED 2026-08-29 — the game draws the full-res8AX, 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 detail8AXhas that an upscale cannot. Correlating the capture's departure-from-upscale against the 8AX-only detail (both first mapped through the measured gamma): main menu +0.0475 vs controls +0.0032 / −0.0075, title +0.0634 vs +0.0095 / +0.0086 — two screens, both 68 % of the theoretical ceiling, 7–15× their matched controls. So: resolve the name and draw8AXat 1:1. Upscaling the 640×360ptbase2× 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 noteptbase's element is the one carrying the keyframes, so you need its timing with8AX's pixels. ⚠️ It does not show whetherptbaseis also drawn underneath —8AXis ~86 % opaque and would hide it either way.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 (
back2eff5is 1133×280 and fully containsback2eff3andback2eff4; 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), andEXTRASderived 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×ptloop02areloop*animations thatcomposeskips by default, so their tie is unreachable. The remaining pair isptframe3×ptframe4, overlapping 102×132 px — and againstlive-extras.pngthat 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 -
✅ How good are the five screens, actually? One page with the numbers:
five-screens-acceptance.md. Rendered and correlated against the live captures — title 0.9500, main menu 0.9460,EXTRAS0.9440, publisher splash 0.9600, developer splash 0.9643, and every one aligns at exactly dy=0 dx=0 over a ±2 px search, so placement and scale are right and the residual is tone and detail rather than geometry. Every undrawn element is accounted for (ghost instances,.prmprimitives,loop*animations, and the8AXname mismatch whose art is on screen anyway) — the counts add up exactly, with nothing unexplained. The residual, ranked: tone (γ≈1.4, the game's own ramp),8AXresolution, oneEXTRASpaint-order tie, and rotation-not-rendered which does not affect these five at rest. ⚠️ Reach: static composites at rest against single frames — this says nothing about animation. -
✅ A static composite is only meaningful for a screen that SETTLES — the two splashes are animations. Measured with its control: suppressing the
_effglows takes the publisher splash 0.9604 → 0.9982 and the developer splash 0.9659 → 0.9980, while the same edit makes the title −0.002, the main menu −0.092 andEXTRAS−0.107 worse. The draw log says why — on the developer splash the glows draw on frames 94–115 and the logos on 116–211, so at the captured moment every glow is already finished, including the two with plateaus thatrest_plateaurenders visible. Sorest_plateauis right where a screen settles and over-draws where it does not. There is no "resting pose" for the splashes — they play through and leave, and a static composite of them is a picture of one arbitrary frame (≈0.998 for the frame these captures hold). Play the timeline for the two splashes; composite statically for title / main menu /EXTRAS.structures/ui-resting-pose.md -
❔ A group's DURATION is in the data; its START TIME is not. Testing whether the splash timeline played reproduces the capture: each element is on screen for its declared span to within 2 % (glows 44 units observed vs 45 declared; logos 192 vs 195). But every glow declares the same times
15,30,45and every logo the same15,30,190,194,206,210, so on one clock they would overlap almost entirely — and they do not overlap at all. The glows run frames 94–115, the logos 116–211, strictly sequential. 🔴 The obvious candidate is dead: each group header carries an undecoded lead-in word, and it is0x00000000for all seven elements. Not the keyframe times, not that word, not declaration order, not the RATC child order. ⚠️ So you must author the sequencing. The observed order on the developer splash — both glows, then both logos — is measured for one screen, not a decoded rule. ✅ Verified against its own refutation: across all 235 captured frames, zero contain both a glow and a logo; the switch is one clean boundary with two sprites either side. 🔴 And it goes further than start times — a bundle's declared elements are not what gets drawn. ⚠️ I previously called this "a bundle is a palette, selectively activated"; the mechanism is withdrawn — the evidence does not choose between "one bundle, selective activation" and "two compositions shown in sequence". The texture-base test that looked like it would separate them fails its own control: the publisher splash is a different bundle and uses the same base0x11C30000, so that address is a reused upload slot, not an identity. The consequence for you is unchanged; the reason is not established. Entry 11 declares three logo/glow pairs; only two are ever drawn.palogo_animagets 0 frames whilepalogo_gameartsgets 95, from byte-identical keyframe times. (Reach: the capture covers frames 1–214, so "never in the window".) Compositing every element of a bundle does not reproduce what the game shows over time — it is right for a static screen that settles, and it is not a timeline.structures/ui-group-start-time.md -
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
optis not a focus link (measured and refuted, seeui-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, andrest()fell back to the element's first keyframe — off-position and transparent. Six elements on the main menu were affected, includingptframe1/ptframe2, the bright circuit bracket around the menu, which both the port's composite andsylpheed-cli screen renderwere 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,restmoves 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 ❔ onptframe1/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, withkstepping 2, 4, 6, 8, 10, 12, 14 on seven consecutive submitted frames. measured, not decoded — the disc sayst=30, it does not say what atis. 🟡 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_effglows, and those are exact. Applying their calibration — with no free parameter left — topalogo_gameartsin the same bundle and the same frames: the logo is still ata=255nine frames after its declareda=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 toa=7on 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+36holds 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 makesrest()'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: …Withdrawn 2026-08-28. That render difference is not evidence about the time association. It is one element —GP_TITLEbuild 7 … twins should match in brightness …ptlogo_eff3.t32, a transient bloom that grows0 % → 200 %at full alpha while rotating, then collapses — and it has no resting pose at all.rest()falls through to its dwell fallback and returns whichever end of that movement the indexing lands on: the invisible frame as decoded, the 200 % peak shifted. An 896×389 sprite at 200 % is larger than the screen, which is the whole 13.1 % and the whole luminance gap. I was comparing a heuristic, not a decode. 🔴 And that is a real defect you should know about:rest()'s dwell rule is unsound whenever it runs. A dwell gap is time spent interpolating from pose k to pose k+1; neither is held unless they are equal — which is a plateau, and the plateau path has already returned by then. So any element with no two adjacent identical poses has a guessed rest pose, in our renderer and in anything built from it. Measured disc-wide: 2 305 of 15 493 elements (14.88 %) — ⚠️ corrected 2026-08-29 down from a published 3 807 (24.57 %), which counted 1 502 single-keyframe elements as guesses; those have one pose and it is unambiguously their rest. Of the real 2 305, 195 get a degeneratescale = 0 %pose, and the two candidate rules agree only 50.2 % of the time. ✅ This does not block you — your exposure is TWO elements, both on the splashes. The fallback is reached only by an element that is plateau-less and multi-keyframe; a single-keyframe element short-circuits. Title, main menu andEXTRASreach it zero times, which is why three different fallback rules render them to identical correlations. The publisher and developer splashes reach it once each — and there,SYLPHEED_REST_RULE=last(the final keyframe) scores +0.9982 and +0.9758 against the current rule's +0.9600 and +0.9643. ⚠️ Measured against single frames of a transient animation, so it fixes which pose matches those captures, not which is canonically at rest. Default unchanged — it is better on both screens where it fires and identical on the other three, but it would move 2 305 elements disc-wide on two measurements. ✅ And the picture is now coherent. Applying the last keyframe to every element (not just the plateau-less ones) collapses all five screens — title 0.9500→0.6819, main menu 0.9460→0.6416,EXTRAS0.9440→0.5745, and both splashes render blank. The reason is the model: a group is entry → hold → exit, and the exit is the screen's dismissal. A displayed screen is sitting at the hold, not at its final pose — sorest_plateauis right, and the last keyframe is the post-exit state. It is right for a transient element precisely because a transient's settled state is "gone". The draw capture agrees independently: on the developer splash the_effglows draw on frames 94–115 and the logos on 116–211, so the glows are already over when the logos are up. Three independent observables — animation timing, static composites and the per-frame draw log — all support: plateau where there is one, last keyframe where there is not. 📊 Structurally,lastis defensible for 2 293 of the 2 305 (99.5 %): 1 618 end invisible (a transient, gone at rest), 675 end at maximum alpha (faded in and stopped — the final pose is settled), and only 12 end visible below full alpha, which are genuinely unclear. The dwell rule returns a mid-movement frame by construction. ⚠️ The 1 618 rest on an assumption worth seeing: that such an element's animation has finished by the time the screen settles — shown by the draw log for the two splash glows, unshown for the rest. 📊 The disc-wide blast radius, for whoever decides: the rules differ on 82.3 % of those 2 305, so "either is fine" is not available — and the current rule returns a zero-scale (collapsed, pre-roll) pose for 195 of them against 43 underlast, a 4.5× reduction in provably-degenerate results. That argues the same way as the captures, from the data's own structure. The single disagreement ispalogo_anima_eff.t32on 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 = 0no longer renders at full size.blit/fill_quadcoercedscale == 0to 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 onGP_TITLE(all 16 builds),GP_PAUSE_MENUandGP_OPTIONS, so nothing you rely on moves; the 126 elements the old code actually painted are all inGP_READY_ROOM.pak, already a no-go.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=212vsa=255att=45); that rule would makeanima_effalone 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🟡 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 viaSYLPHEED_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). ⚠️ Still measured, not decoded: no field on the disc says "sixtieths of a second". -
The paint order is derivable from the file. Each
T8aDsprite header carries au16layer key at+0x0A(the upper half of the 32-bit word at+0x08is 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 (.prmprimitives,.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 freshEXTRAScapture. 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 andkind. 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
EXTRASboth). ⬅➡ 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 gaveTUTORIAL,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 onNORMAL) →SELECT DATA— it does not hang; the run then hits the already-documentedsub_823070B0cache crash, which is not a menu problem.LOAD GAME→ the save-slot list,TUTORIAL→ the lesson list,OPTIONS→ the settings menu,EXTRAS→GP_TITLEbuild 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
.prmquad that paints last (pteff00.prm/pfeff00.prm) whose keyframe group is the transition: black atT0, clear byT1, clear untilT2, then back to black on exit. ✅ decoded, with a disc-wide check — and inGP_TITLEexactly the six screen builds carry it while the six overlays do not. The fade-in length isT1 − T0and is read from the file (0.87 s forEXTRAS, 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.pakentry0x5b983a08: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 — andS00A.wmvis 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 matchADV.wmvwith 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'sSOUNDSrecord carries 322SE_*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 indata/se-ui-cues.txt. They all live in one bank —BANK_SEis a single field namingStatic.slb, and 0 of the 322 has its ownFILESentry. 🟡 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.slbhas 0RIFF/seek/WAVEand there is no XACT container anywhere (0 ×XGSF/SDBK/WBNDin 1.08 GB ofsound.pak; noXACTstring in the executable — those extensions are the authoring tool's). But Canary's--xma_param_probe=truelogs every stream's head bytes, and searching them inStatic.slblocates 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 namedSE_UI_DECIDEorSE_UI_SUB_WIN_OPNis 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, viatools/re-capture/slb_extract_wave.py— whose wrapper reproduces a known-goodBGM_001decode 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 stringGP_ADVERTISE_DEMOhas 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, callssub_821CC860(…, "<NAME>", 0)withTITLE_SCREEN,TITLE_MENUandLOADING— 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 whyGP_ADVERTISE_DEMOhas 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 includeBG,BLACK,FADE,FILE,KEY,PAD,SOUND,GAMMA_RGB. An earlier version of this page claimedDIFFICULTYandEXTRA_MENUas corroborated screen names; neither is ever the argument, and onlyTUTORIAL_MENUsurvives. ✅ And the state machine itself is decoded:state = this+136, ten states dispatched through a jump table at0x821C6498, with 18 transitions each a literalli/stw. States 0, 2, 8 installTITLE_SCREEN,TITLE_MENU,LOADING.4 → 0is the only edge back to the title, reached from2 → 4— which matches Ⓑ-returns-to-title as measured. Full graph indata/title-state-machine.txt. ⚠️ All of that is ONE PHASE.GamePart_Titledispatches on an outer phase field atthis+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.wmvin full → title) → Ⓐ → main menu, with Ⓑ from the main menu returning to the title. -
config.inipicks the language, and that is what picks the EN/JP build. The disc's only config file (400 bytes, at the root; onefindover the whole extract). Its[LANGUAGE]section maps the console'sXC_LANGUAGE_*value toeng/jpn/deu/fra/esp/ita, defaulting toeng— that code selectsGP_TITLE's English or Japanese build and the<lang>.pakfamilies. ✅ 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). Ids1,2,16,18,28have no registration site — includingGP_ADVERTISE_DEMO(1), which agrees with the measurement that the attract loop is the title replayingADV.wmvrather than a separate part. 🟡 an argument from a diagnostic string, not from the code. Also:3and4are bothGamePart_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–logo4are manifest-bound with no.wmvon the disc. ✅ The screen is two bundles inGP_TITLE.pak, each shipped twice: entries 10/13 the whiteSQUARE ENIXpublisher logo, entries 11/14GAME ARTS/SETA/studio anima. ⚠️ They are invisible to the defaultscreen list/render—is_buildrejects them for having no.ratchild. 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_effglow child on[15 30 45]. Under1 unit = 1/60 sthat 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_effglow ramping after the logo, not a rendering artifact. Wall-clock: logos ≈ 0.4–4.7 s and ≈ 4.9–8.4 s, thenADV.wmvfrom ≈ 8.9 s. ⚠️ The cyanSQUARE ENIXat ≈ 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.wmvis 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) anddat/movie/S00A.wmv(the new-game intro, 93.9 s). Named from the movie manifest, ✅ decoded. Static.slbover-declares its size by 616 768 bytes — it is the highest-offset entry insound.pakand 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, andChannelMaskis0x0002on 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_001fades 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 isBGM_103. The cue table cannot say — its BGM entries are numeric — butGamePart_Title'ssub_821C5580plays cue 1103, andBGM_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.slbdoes 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.