Files
Sylpheed/docs/re/INDEX.md
sylph-decoder 5b04661248 re: the guest presents at 30 fps -- 60 units/s, and the port was right
Answered against the pre-registration committed before the capture.

Units per second was the last open number on the PRESS (A) plate, and both
prior measurements of it were wall-clock readings off an emulator that runs
the guest slow by an unknown factor. They disagreed by 2.9x, because a 30 Hz
guest at full speed and a 60 Hz guest at half speed look identical on a wall
clock.

The ruler here is not a clock. ADV.wmv declares 30.0000 fps in its own ASF
header, so a decoded movie frame is a tick the emulator's speed cannot
stretch. Presented frames per decoded movie frame is guest_fps/30 with no
wall clock in the chain.

  predicted  H_A 30 fps ->  60 units/s -> 1.0
             H_B 60 fps -> 120 units/s -> 2.0
  measured                                1.0000

Both pre-registered guards pass. Guard 2: a perfect repeating 3-buffer
cycle, 52 uses each (exactly 156/3), 2 chroma planes per luma on 156 of 156.
Guard 1: run lengths are 156 runs ALL of length 1 -- no smear, so the
dropped-movie-frame bias that would have pushed the answer toward 120 is
measurably absent rather than argued away.

So H_A. The port keeps its 60 and changes nothing.

REFUTES the live H3 hypothesis that 120 units/s explains the play-test's
late plate. That hypothesis was well-formed and attractive precisely because
it would have explained the complaint, which is why it needed a ruler that
is not a clock.

Which means finding 3 still has no cause. The strongest remaining candidate
is decoded rather than speculative: the plate's declared onset is t=214, not
t=236 -- a 22-unit fade, matching the T=22 the oracle confirmed by measuring
+23 alpha per presented frame on that element.

The pre-registered control could NOT be run: this logger build emits vb=
addresses, not vertex contents, so there was no alpha to check +34 against.
A weaker control is substituted and labelled -- the splash shader/blend
census, which validates the log's structure (what this measurement uses) and
not alpha extraction (which it does not).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-01 18:49:40 +00:00

108 KiB
Raw Blame History

RE knowledge index

Confidence: CONFIRMED · 🟡 PROBABLE · HYPOTHESIS. See README.

Also durable, and worth reading before proposing anything: REFUTED.md — what has already been tested and died · METHOD.md — the traps this corpus has already paid for · BACKLOG.md — what is still open.

Formats we've already reversed are, for now, documented by their parser + disc round-trip tests (the executable spec) rather than a prose file — the "Spec" column points there. Promote to a prose structures/…md file when a format needs behavioural notes beyond layout.

Data structures / formats

Format Conf. Spec (parser + tests) Notes
IPFB .pak archive sylpheed-formats/src/pak.rs + tests/pak_idxd_disc.rs header + 12-byte TOC, Z1/zlib payloads
name-hash (TOC keys) sylpheed-formats/src/hash.rs Barrett-reduction hash; recovers original paths
IDXD object/table sylpheed-formats/src/idxd.rs + tests/idxd_records_disc.rs (container) The binary record/index region in front of the string pool is DECODED (2026-08-25), closing the parser's long-standing "not yet decoded" note. Uniform 16-byte records {name_hash, name_off, field_begin, field_end} sorted by hash and binary-searched, then a field count, 12-byte fields {key, name_off, value_off} sorted by key, a pool size, and the string pool; the trailing pool_size == file_len - pool_base identity makes the layout self-checking. Verified over the whole disc with zero failures: 7 750/7 750 objects, 190 782/190 782 records reproducing their stored tag_hash, 1 271 462/1 271 462 named fields reproducing their key — and IXUD is the same container with ixud_hash, UTF-16BE and all offsets in chars (1 104/1 104 objects, 628 165/628 165 fields). Field names are stored on disc — a field's middle word points at its own name — so nothing needs preimage search except the 504 field entries disc-wide that are hash-keyed with no name — which are only 42 distinct keys, each in 12 places (the page for these was never written — the finding is in this row), and are provably unrecoverable from the hash alone; the other 1 485 073 nameless fields are positional, keyed by a literal integer (line slots, movie ids). ⚠️ Two long-held beliefs WITHDRAWN: the word at 0x08 is not a schema hash, it is record 0's name_hash (7 750/7 750) — the header has no type field at all, so an object's kind is known only from the caller that loads it; and the field's middle word is not an always-0xFFFFFFFF flags word. The first was caught by a test asserting that every movie id names a real record: 1005 -> STAGE10_PHASE01 failed because tag_hash("STAGE10_PHASE01") is 0x067025B9, that table's supposed schema id. 🟡 the legacy value-before-key string-pool reader is now known to be an approximation of the real table, and every number derived from it is re-checkable but not yet re-checked
IDXD nameless field keys / idxd-unnamed-keys + tools/re-capture/idxd_unnamed_keys.py Census of every field entry whose name_off is 0xFFFFFFFF, disc-wide: 7 750 objects, 2 757 039 field entries, 0 parse failures, tag_hash reproducing 1 271 462/1 271 462 named keys. 7 094 distinct keys are never named — and 7 052 of them are not hashes at all, but author-assigned element ids (equal to the field's own index in 1 404 924 of 1 485 577 cases; tag_hash("BGM_001") is 0xC662435B while the key valued BGM_001.slb is 0x000003E9). ⚠️ The "504 hash-keyed nameless fields" figure is 504 ENTRIES, not 504 names — 42 distinct keys × 6 language copies × 2 records. All 42 are ISL script-symbol hashes in <lang>\script\ID.tbl (GP_READY_ROOM.pak), the link map built by PrepareScript's "isl script prescanning"; 41 of 42 appear as little-endian call targets inside the .isb bytecode, forming a coherent launcher/helper call graph. The hash's own algebra pins the trailing digits of 30 of the 42 names (deltas of exactly +0x01000001 across stage01..09, stage10..16, challenge01..06; +0x01010000 across tutorial0101..0601). No name was cracked, and the negative is quantified: seven attacks up to a 3.5×10⁸ composition space found nothing above the noise floor; exhaustive preimage search recovers "Stage01" from its own hash but returns nothing for the real targets at ≤6 characters, and at 7 characters one target already has 1 176 preimages — a 24-bit modulus cannot name an 8+ character identifier uniquely
XPR2 texture + cubemap 🟡/ sylpheed-formats/src/texture.rs + colour check de-tile + A8R8G8B8 and DXT1. Channel order confirmed against the running game: the Delta Saber's decoded atlas is orange-dominant (median saturated hue 23.3°, zero cool pixels) and the game renders the same hull at 9.3° — a red↔blue swap would sit at ≈200°. Exact fidelity (gamma/sRGB curve, premultiplied alpha, per-channel scale) is 🟡 untested, since a hue comparison cannot see it; cubemap face ordering . 2026-08-29, for the UI path only: our composite is brighter than the emulator's frame by a gamma of ≈1.341.49 across three screens (tone curve) — 🟡 measured, not decoded, constrained only over render values ~060, and possibly canary's own kernel_display_gamma_type = 2 (BT.709) output stage rather than the game's
T8aD 2D texture sylpheed-formats/src/t8ad.rs 100 % of the disc decodes (19 216/19 216, measured). The "~15 % deferred variants" were a wrong model, not a variant: a surface is a list of arbitrary sub-rectangles, each with a 16-byte header of dst X, dst Y, width, height, not a 256×256 grid — 0x1c is the rectangle count. Uncovered area stays transparent. Colours CONFIRMED (k8888)
RATC bundle sylpheed-formats/src/ratc.rs 2026-08-29: a child's NAME is stated by an opt block (`"opt "
LSTA sprite list sylpheed-formats/src/lsta.rs A display list of inline elements: T8aD sprites and PRMD primitives. The count at 0x04 is exact and counts bothcount == T8aD + PRMD for 64/64 lists on the disc, which retires the old "a few entries disagree" note (it compared sprites against a total including primitives). All 1 281 sprite frames decode after the T8aD rectangle-list fix
IXUD subtitle + caption text sylpheed-formats/src/ixud.rs + movie_subtitle.rs (container · movie link) The IXUD record/field table is decoded and wired in (2026-08-26)IxudObject mirrors IdxdObject; uniform 16-byte records, 12-byte fields, every offset in chars, and the word at 0x08 is record 0's hash, not a schema id. Verified disc-wide: 1104/1104 objects, 1476/1476 records, 628 165/628 165 named fields reproducing their ixud_hash (tests/ixud_records_disc.rs). Caption text: 537 → 8800 lines, which is 8800 of 8800 distinct keys. Two steps — generalising the key parser from MSG_DEMO_* to all eight families (ACRO ADAN ADPL BIRD DEMO RHIN TCAF use MSG_<FAM>_<id>_<page>_<line>, VOICE alone inserts a family letter) took 537 → 8074; switching from token adjacency to record fields took it to 8800. ⚠️ An earlier "1.3 % of the game's text" figure of mine counted occurrences across blocks — the honest denominator is 8800 distinct keys, so the real starting point was 6.1 %. The DEMO control shows why the field route matters: token adjacency finds 537 lines there, fields find 541 — it was dropping lines in the one family it was written for.
Fonts (ttf/otf/ttc) sylpheed-formats/src/font.rs standard OpenType, parsed via ttf-parser
XBG7 mesh /🟡 sylpheed-formats/src/mesh.rs + tests/mesh_disc.rs (xbg7) 6 294 resources, 6 209 decode (98.7 %), 82 searched-and-missed (2026-08-12, up from 5 480 / 87.1 %). Five evidence-driven fixes got there: distinct anchor assignment (no two resources may claim one buffer — proved by a capture showing the container holds both mirrored e106 hull halves), the connectivity cap replaced by a winding-consistency gate at 0.70, structural requirements on pre-pivot sub-meshes (index range, then exact pool coverage), and filtering after the assignment so a subset query cannot differ from the full decode. Validated against a runtime capture that names the file offset of every buffer the engine drew: 46/46 drawn buffers claimed, 45 anchored exactly. No real mesh now decodes differently in different containers — all 89 remaining cross-container disagreements are interchangeable 24-vertex bounding boxes, which no anchoring rule can pin (monotone order re-tested and refuted). Remaining misses attribute to the degeneracy/extent gate (42), winding (31) and coverage (9); the first was probed and its "obvious" fix refuted. Every decoded sub-mesh covers its own vertex pool. The [index buffer][vertex buffer] layout is now runtime-verified (2026-08-13): with the F10 capture extended to log each draw's index buffer, all 42 drawn Stage_S02 buffers match our decoded index count exactly, all 42 have their index union cover the pool exactly, and the 30 single-block cases all sit at pad ≤ 3 — so e106_eng_02_l's old rejection was the connectivity gate, not a misplaced index buffer. The indices= mystery was the capture keeping only the first of several index batches per buffer. And comparing index VALUES found the biggest silent defect yet: the anchor took the first pad that validated, so a block whose index data sits at pad 2 was read one element late — 76/93 captured runs matched, all 17 differences a one-element shift. Scoring pads by degenerate triangles + winding fixes it: 93/93 captured runs now match byte for byte, disc-wide degenerate runs 582 → 1 (the grouped path had the same bug; and two resources were anchored on a degenerate lookalike earlier in file order), 590 of 8 850 sub-meshes re-wired with 10 vertex anchors moved, resources decoded unchanged at 6 209. Cross-container minority decodes 89 → 96 — because the decoder improved: _rou_f402_dead now has a majority (32×25×8) so its seven wrong copies are named instead of hidden. One dirty run remains, blocked by distinct assignment on a 24-vertex box. Then the descriptor gave up its last structural secret: it declares a vertex layout per sub-mesh (n201_01 → strides 24/24/24/28, capture-confirmed), and grouped selection must prefer the candidate explaining the whole pool rather than the first whose pivot validates — together they take never-decoding resources 85 → 47 (6 247 / 6 294 = 99.25 % decode), put n201_01 on all four capture-proven offsets and raise the stage-05 capture oracle to 128/128. The residual 47 is 30 pose/proxy composites (0.010-unit marker boxes), 6 .DAT particle composites, 8 damage/LOD variants and 3 props — not a threshold away
Capital-ship part placement sylpheed-formats/src/ship.rs (static) + runtime capture Placement is sound (hull static-exact against the e106 capture; cross-id mounting genuinely narrow, 2 pairs across 335 ships). The XBG7 mis-decode this row used to blame for "ships assemble wrong" — a shared turret ~100× too large in some containers — is fixed (2026-08-12, the exact-coverage requirement): e303_wep_01 now decodes 49×23×42 everywhere and places at ±179 on the e106 hull, and no real mesh disagrees across containers. A composite-node audit confirmed the assembler itself never applied a bad scale (all nodes scale 1.0, orthonormal). Still open: static_assembly_matches_runtime_capture walks capture parts only, so extra static placements cannot fail it
Weapon fields defaulted on disc ⚠️ / runtime struct · DATA SHEET route Solved. Canary maps guest RAM into /dev/shm, so the parsed Weapon/Shell objects are readable live; their layout is solved against disc ground truth (zero contradictions over 100+ records). All 126 weapons, exact numbers, no story progress needed — 4 393 values. ⚠️ CORRECTED 2026-08-26: the disc DOES carry a third of them. Re-read through the record table, 1 514 of the 4 393 rows labelled defaulted-on-disc have a value on disc (2 879 genuinely absent) — measured here as an upper bound, since a field is counted when it appears in any record of the object; a per-record count gives ~1 448. The old reader could not name a record, so per-record fields read as absent. Spot-checked exactly: wep_05/wep_60 TriggerShotCount = 4, wep_02 Power = 100.0, wep_60 Power = 1000.0 (refuting the recorded "C band ≈150…500" bracket), wep_25 MaximumRange = 4000.0, wep_11/28/36/70 LoadingCount = 6/5/5/0. The runtime capture's numbers all match the disc — what is withdrawn is the premise that it was reaching values the disc lacks. See weapon-datasheet-runtime Supersedes the letter-bucket limit of the DATA SHEET route, which now serves as the independent cross-check
Unit (craft/vessel) fields defaulted on disc ⚠️ / runtime struct The parsed unit\UN_*.tbl definition object, vtable 0x820af844, ≥0x380 bytes, one per unit — discovered, not assumed (unit_discover.py), and distinguished from the spawned-entity class 0x820af030 by being one-per-ID and byte-constant within a run. Across runs only pointer words move — --crosscheck proves no reported field offset is run-dependent (two words, +0x2c8/+0x2d0, are stage-dependent and remain unidentified). 27 fields (21 units, 7 runs); the Maneuver block is schema declaration order, 4 bytes/field, base 0x9c with a two-slot gap after AA_Roll_Min (29 anchors, 0 conflicts), which also pins 5 fields no disc record ever values. Angles are radians at runtime, degrees on disc. Re-derived independently 2026-08-13 from the loader's own key strings (sub_82341A20; the field name for each store is a string in the image): 159 fields, agreeing with this solver on 25 of 25 shared offsets, verified at 406 values matching the disc and 0 disagreeing over 11 live objects spanning UNIT and VESSEL — landed as data/unit_definition_layout.txt + sylpheed_formats::unit_layout + a no-emulator test, with 121 defaulted fields read out (live-unit-definitions). ⚠️ CORRECTED 2026-08-26: those fields are on the disc. The ~30-field player-craft table is on disc at exactly the values the runtime "recovered", spread across the Generic / Shield / Mass / SE records — which is why a reader that could not name a record saw them as absent. And "18 of 23 vessel records are missing at least one of Size_X/Y/Z/HP" is false: 0 of 114 objects with a Generic.Type (43 Craft + 71 Vessel) miss any of the four. See unit-struct-runtime Unlike weapons, unit definitions are instantiated per stage, so coverage (21/110) grows by visiting missions — the sibling-default rules are WITHDRAWN (2026-08-25)Size_Y is on disc for 114/114 unit tables and differs from Size_X in 90; the old reader missed it exactly when the two were equal (a string-pool dedup artefact, cross-tab seen+equal = 0 for all three pairs), so the rule was re-deriving the condition that hid the field. It also predicts wrong twice — UN_e104_ADAN_Carrier.DefencePoint and UN_e011_ADAN_Attacker_B_HF_Wayne.FCSRange. Read the record table instead — values
Arsenal develop economy / arsenal-develop-economy + conditions The Arsenal reads weapon.tbl (item ids, in the 8-category display order) and strings.tbl (names, descriptions, and a "Conditions to obtain" block per item) out of GP_HANGAR_ARSENAL.pak. All 60 conditions are extracted: gates are stage completion, a predecessor item, or an ace kill; costs run 3 000350 000 P and 20 items are free once gated. weapon.tbl's first record reproduces the in-game DATA SHEET exactly (Range D / Power E / Speed / Weight 0.3 = Light / 4000 P) — later records are unreadable from the string pool alone because IDXD dedupes repeated values. Used to identify the save blob's index space, now solved: the blob follows strings.tbl's order — the display order plus the cut items only the localisation file lists (Adhesive Mine B2A, Ballista GSH, …) — pinned by four hand-written probe saves (9 Stiletto, 21 Falcon, 39 Tomahawk, 48 Jamming System) and closing exactly at index 53. weapon.tbl's id list is not the index space; that it is also 54 long is a coincidence, and the two agree only to index 32. The retail save's five unexplained owned entries are the cut items, shipped owned and never rendered
UI screen layout (.rat) /🟡 ui-rat-layout One pak per UI screen; each RATC = one (context × language) build; every <name>.t32 sprite has a <name>.rat layout record (BE u32; 1280×720 design space; scale/tint/X/Y, keyframes for animated elements, opt link to the focused state). The tutorial PAUSE menu and the title main menu both rebuild pixel-accurately from the disc. loop1.rat is decoded — it is a looping sprite animation, not a composition. ⚠️ DEMOTED 2026-08-18 — the declaration table is not the paint order: a per-draw capture of the running title screen (ui-title-paint-order-capture) paints element 13 first and elements 0/1 late, and the visible screen composites two bundles. The rest of the table's reading stands. Previously claimed: the screen's draw list is the RATC bundle's own declaration table (elements in back-to-front order, including the eff*/deli*/msg sprites that have no .rat, and excluding focused button variants reached via opt ); its entry also carries a parent element index at +32. A screen is fully reconstructible from its bundle: the placement region right after the declaration table gives every element a keyframe group (header = element index + keyframe count, then 40-byte blocks of scale/tint/X/Y), including the .rat-less sprites — verified 11/11 on the tutorial pause bundle, with pgp_ttrl_btn10's inline (546,288) matching its own record exactly. 2026-08-28: the keyframe block's +12 is a screen-plane ROTATION in degrees, clockwise-positive — confirmed against a GPU capture (declared 30 / 45 vs measured +30.26° / 45.28°) and non-zero in 14.50 % of 83 862 blocks disc-wide (ui-keyframe-rotation). ⚠️ Every rotation on the disc is inside a nested .rat leaf record, and ui_layout::blit is axis-aligned, so the field is decoded but not rendered. 🟡 +4/+8 remain unexplained
UI screen paint order runtime screen object + title capture SOLVED: the game's screen object keeps a second, reordered list of its elements — the child array at +0x30 — and that is the paint order, not the declaration table. Read live from guest memory (found by the item vtable 0x820b30b4) and checked against the draw capture: the seven nameable elements sit at child slots 0, 6, 7, 13, 16, 17, 22, strictly ascending, exactly as captured; it also settles the one pair no static field could order. 🟡 deriving that order from the bundle — what the port needs — is still open
Scripted input / profile traps /🟡 canary-scripted-input-traps Why a scripted run appears unable to press Ⓐ: F10 opens the emulator menu bar, and any Xenia UI makes XamInputGetKeystrokeEx return SUCCESS with an empty keystroke before any driver is asked (Canary now logs [RE-INPUT] … swallowed by IsUIActive); the title needs a signed-in profile (hence --create_profile_if_none); and a FIFO trace consumer that exits stalls the emulator, which reads exactly like a dead pad. 🟡 The main menu HAS been reached — Ⓐ works, but only intermittently (1 in ~4), which is the open question
Title-screen guest crash title-crash-stl-tree The guest throws std::out_of_range from its cache-manager flush (sub_823070B0, an STL map/set erase that builds 'invalid map/set<T> iterator'); the access violation after it is only the throw returning, because this build does not unwind guest EH. Trigger found and controlled: an incomplete on-disc cache (~/.local/share/Xenia/cache/aab216c3) throws ~100 s into a boot, a complete one never does — 2 runs each way. mem_watch, the handoff's suspect #1, is eliminated: cold cache + --mem_watch=false throws anyway
Save file (savedata) / savegame-format + tools/re-capture/savegame.py GDHA container, zlib payload, chunk stream (GDAA / phase name / GHAD 122 B progress block / 16×20 B slot table / trailer). Container and layout read off the title's own serializer 0x822C00E8 and verified by a byte-identical round-trip; the whole save is 545 B. Payload offsets are also the live save object's offsets (save+8 GHAD, save+136 slots). A second save made in-game names Points (+24), flight time in ms (+4) and clear ratio % (+8) off the game's own Details panel; the payload is a pure function of game state (same state saved twice = byte-identical, only the header FILETIME and its uninitialised pointer padding move), and the 16 SHAB records are not the UI's 20 save slots. Difficulty vs stage is undecided — three fields hold 2. A third save, taken after developing exactly one Arsenal weapon (Light Machine Gun MG I, 4000 P), moves exactly three things: +24 Points 4101→101 (which separates it from +28, that did not move), +8 clear ratio 5→6 (so the ratio counts collection, not only stages), and two entries of the 54-byte blob — 2→4 for the item bought and 0→2 for the successor the game announced as newly developable, giving the blob its alphabet 0 locked / 2 developable / 4 developed (only the 4s are stored — 2 is re-derived at load). Saves can also be written back: three derived header fields (length at +0x30, payload length at +0x8c, adler32 at +0x8e) are all that stand between a parse and a hand-written save that the title loads, and savegame_edit.py re-wraps a real save byte-identically. That turned the blob's index space from blocked-on-story-progress into four probe saves — see the economy note
--build N addressing (ordinal vs pak entry) build-ordinal-vs-entry + data/ordinal-entry-map.txt screen --build N indexes a predicate-filtered list, not the pak. Disc-wide: 21 of 24 build-bearing archives diverge, 18 at ordinal 0--build 0 is entry 108 in each GP_MAIN_GAME_*2D, entry 24/26 in GP_HANGAR_ARSENAL/GP_READY_ROOM. GP_TITLE is the only archive whose ordinals 09 are the identity, which is the sole reason 207 of the corpus's 226 build citations are safe. ⚠️ --all swaps the predicate and renumbers 18 archives, so --build N and --build N --all differ. Instrument controlled against the CLI's own screen list on GP_TITLE (12 builds, [10]→12, [11]→15) — a first version using parse_build as the predicate failed that control, reporting ordinal==entry throughout. Audit of all 226 citations: 1 defect found and fixed (a five-row table in ui-keyframe-time-unit.md labelled "build 11" spanned builds 10 and 11 — placements all correct, only the label wrong); GP_DIALOG --build 0 and GP_DEBRIEFING_PILOTLOG --build 10 re-run and reproduce

Runtime / dynamic-capture technique

Technique Conf. Spec Notes
Scripted input without a virtual controller tools/re-capture/pad.py + Canary --hid=file The old vgamepad path created its device through /dev/uinput, which is not namespaced — a pad made inside the container registers with the HOST's input stack, so every scripted press leaked to the desktop. Canary now carries a header-only driver that reads pad state from a text file (--hid=file --pad_file=…): no kernel device, nothing leaves the container, and analogue values are exact. ⚠️ The trap: 360 menus poll XamInputGetKeystrokeEx, not GetState — with GetKeystroke stubbed the pad looks completely dead on a title screen while its own log shows the press arriving. Implemented edge-triggered, no auto-repeat (scripted input wants one event per press). Proven end to end: boot → title → main menu → EXTRAS from the file alone
Live guest-memory write tools/re-capture/gpoke.py Write companion to gmem.py, same guest-VA → /dev/shm map; prints before/after per word. Used to confirm the challenge gate's cleared-stage mask on the running game
Live guest-memory read tools/re-capture/gmem.py Canary backs the guest address space with /dev/shm/xenia_memory_*; guest VAs map in through Xenia's fixed table. Full-RAM search ~0.2 s (sparse, SEEK_DATA). No debugger, no emulator patch, game keeps running
IDXD object layout solver tools/re-capture/weapon_runtime.py Scan RAM for a class's vtable → enumerate its objects → brute-force (field, offset, encoding) against the disc records. Accepts a binding only on zero contradictions. Generalizes to any IDXD-backed definition
Live entity state, anchored on the definition tools/re-capture/own_state.py · autopilot An undamaged craft holds its definition's own numbers, so a solved definition field locates the matching live field without a value scan: definition HP (1500) → hull at position+0x154, confirmed by a trace across a death (30/60/90 per hit, negative at 0). Reusable for any live counter whose maximum the definition carries
Mission / escort state, every entity's hull tools/re-capture/mission_state.py · escort state hull = position + 0x154 is a property of the entity class, not of the player object: at t=0 it equals each entity's own definition HP across 7 classes and 5 distinct HP values (turret 100, fighter 500, destroyer 10000, cruiser 30000, ACROPOLIS 25000), falls under fire (780 damage events in 240 s), goes negative at death, and the object then leaves the heap. So an escort objective is scoreable live — UN_f101_TCAF_Acropolis measured at 25000 → 23038 over 240 s, attack starting only at t≈170 s. REMAINING OB counts objectives, not hostiles (012 on the HUD vs 118 live ADAN); its address is found — see mission-objective-counter — but it is run-dependent and must be re-derived per run (refuted as a constant 2026-08-26)
In-flight control mapping /🟡 tools/re-capture/fire_probe.sh · controls Measured by holding each pad input and photographing the HUD ammo counters: RB = nose gun (6000→5956 in 4 s, ~11 rounds/s, HEAT rises), Y = main mount (missiles, 300→299), d-pad = tactical map overlay, nothing else moves a counter. No target-cycle input exists — the TARGET marker is present with nothing pressed, so targeting is automatic and a missile lock is time-on-target. That, not target choice or ballistics, is what caps lethality at 2 kills per 98 missiles
Throttle → speed law /🟡 flight-speed-law The throttle selects a TARGET SPEED, it does not add thrust: no input settles at ~420 (CruisingVelocity 350), RT at ~1 530 (MaximumVelocity 1200), LT at ~125 (MinimumVelocity 100), and releasing either returns to cruise. Acceleration/Deceleration govern the convergence rate (measured ~440560 units/s² against 500/600). 🟡 measured world speeds run ≈1.21.3× the definition numbers while the HUD shows the definition value exactly, so world units are a constant (~1.25) multiple of the definition's velocity unit
Input → dynamics calibration tools/re-capture/ctrl_probe.py · binq.py Hold each pad input in turn and measure the craft's speed as displacement/s of its own position triple — no speed field needed first. Settled the throttle: RT accelerates, LT brakes, and the setting persists (488 → 1510 → 174 units/s), overturning an earlier field-scan conclusion

Functions / code paths

Function Conf. Reimpl. Summary
Achievement table + XAM read path /🟡 achievements The XEX's XACH resource (.pe 0x8FBCBC, 36-byte records) defines 24 achievements summing to 1000G — the retail total, which self-checks the stride and field offsets. GamePart_Debriefing (0x8218CF380x82191B18) does two things: it walks the on-disc ACHIEVEMENTS_REQUIREMENTS list (tables.pak #16, entries ACHIEVEMENT01…24, in achievement-id order — its ShootDownAircrafts/ShootDownShips/ShootDownWeight/GetAllWeapons/GetAllAchievements types line up with ids 1924 exactly as XACH names them), evaluating each and setting a bit; and it enumerates XACHIEVEMENT_DETAILS from XAM — 36-byte records confirmed by the 0x38E38E39+srawi 3 divide-by-36, dwId at +0, dwFlags & 0x00020000 (…_ACHIEVED) at +32, behind a waited-then-closed async handle. So earned state comes from the console profile, not the 545-byte save — and the masks it builds are 1 << dwId, i.e. bit = the 1-based id. The challenge missions do NOT gate on this — an earlier claim here, refuted by finding +80's sole writer: it is GamePart_StageClear setting 1 << stage, so that word is a cleared-stage mask and the shared "24" was a coincidence. GetAllAchievements/GetAllWeapons are requirement types, not debug cheats
Challenge-mission gate + stage-config switch challenge-mission-gate GamePart_ChallengeMission gates each of the six challenge missions on a CLEARED-STAGE bit: REQUIREMENT absent or "Always" → available, else n = atoi(v) and it tests bit n of singleton +80 (n < 24) or bit n-24 of +1956 (n ≥ 24) — which is the disc's own stage numbering (story 116 and tutorial 1823 below 24, challenge 2429 above). Word A's sole writer is 0x821C1820 in GamePart_StageClear, doing 1 << (this+84) where this+84 is the stage number (it also indexes a 20-byte per-stage record array and the debriefing STAGE sprite list). So TimeAttack needs stage 16 — the last story mission — and the rest chain off challenge stages 2529. The six-mission table is on disc in tables.pak (schema 54a10697). Separately, the stage loader picks its config section from a mission-kind field at object+144: 3EXTRA, 5/6CHALLENGE, anything else → FILE; two further sites treat {3,5,6} as one class. Constructed as 0 (sub_821783D8) and only ever cleared inside the class, so the kind comes from the launching GamePart, not from the stage number — which is a mechanism (🟡, unproven) for why patching the save's stage field to a challenge stage kills the load. Same note carries the GamePart id table (0x820A1630, 29 ids, GP_CHALLENGE = 26, cross-checked against the image's own RegisterToFactory<26, …> text) and the disc's three stage familiesS01S16 story, S18S23 tutorial, S24S29 challenge, plus Test, matching weapon.tbl's 16 + 6 + 6 key set exactly. GP_CHALLENGE.pak holds 0 IDXD objects — it is the menu screen; challenge missions reuse GP_MAIN_GAME_E.pak's stage records

All RE notes (generated)

Every file under docs/re/. Search this table before starting a new investigation — the index was previously hand-maintained and listed 20 of 43 files, which is how the same ground got covered twice.

Note Title Status
SESSION-2026-08-11.md Session log — 2026-08-11 (autonomous run)
arsenal-develop-economy.md The Arsenal develop economy, and what the save's blob indexes
autopilot-knowledge-sources.md Where the autopilot's missing knowledge lives on the disc 🟡 this is a map, not the knowledge itself — it says which files
autopilot-memory-driven.md Memory-driven autopilot — build log and current state 🟢 IT FLIES, KILLS AND SURVIVES — but it loses the mission anyway.
canary-scripted-input-traps.md Getting past the title screen in the container — three traps and one blocker CONFIRMED for the three traps (each reproduced, and two of them
challenge-mission-gate.md Challenge / EX missions — the stage set, the GamePart graph, and the kind field for the static structure (stage set, GamePart ids, the config-section
dynamic-re-state-restore.md The container's dynamic-RE state is not durable — how to rebuild it CONFIRMED by rebuilding it (2026-08-23). Everything the dynamic
flight-controls-runtime.md In-flight control mapping — measured, not assumed for the weapon bindings (ammo counters move), 🟡 for the rest (HUD
flight-speed-law.md The throttle is a TARGET-SPEED selector — measured against the definition (2026-08-13) for the shape of the law, 🟡 for the unit scale.
guest-stalls.md The guest stalls, often — and the probe now detects it the stall witness works and is validated against independent
live-unit-definitions.md Live unit definitions from a running Stage 02 (2026-08-12)
mission-arrival-watch.md Six runs, no arrival — and an accidental control the deployment structure reproduces exactly; losses require the
mission-clock-advances.md The mission clock is running — the runs were too short in game time 🔴 "the phase clock is stopped" is refuted; something advances
mission-escort-state.md Escort / mission state from guest RAM — every entity's hull CONFIRMED (2026-07-30). Capture: tools/re-capture/mission_state.py,
mission-freeze-and-ob-flag.md An in-mission freeze, and the first cut at what REMAINING OB counts
mission-freeze-resume-spin.md The in-mission freeze — the resume-spin lead is DEAD, and the freeze is a guest-side spin 🔴 the reading this file was named for is REFUTED (2026-08-23, later
mission-liveness-probe.md A motion-independent live roster — and what n probably is the enumeration method; the hunting pilot gets kills; 🔴 "no
mission-objectives-text.md The mission script in plain English — and why phase 1 never completed the localised string tables are decoded and readable; the phase
mission-outcome-stage02.md Why Stage 02 is never won — the escort sinks at ~11 minutes (2026-08-10) measured, one 500 s run. The standing open item since 2026-07-29 was
mission-per-record-strength.md Per-record craft strength — the measurement works, the run does not reproduce the per-record measurement is internally consistent; 🔴 it does not
mission-phase-deployment.md Enemies come into play per PHASE, deployed at phase start 🟡 strongly supported and internally consistent, one unexplained
mission-phase-objectives.md What each phase asks for — from the game's own objective text pinned by a disc test (crates/sylpheed-formats/tests/phase_objectives_disc.rs).
mission-phase-runtime.md Where the runtime phase state is not the stage tables are resident in guest RAM, so the static decode
mission-wave-arrivals.md The arrival timetable, and what the entity table is really counting the static arrival timetable is in Route_S.tbl; 🟡 the runtime
movie-subtitle-link.md The movie ↔ subtitle ↔ voice link (static)
pilot-never-fires.md pilot.py never pulls the trigger — the proximal cause, measured ROOT CAUSE FOUND AND FIXED (2026-08-23, later the same day) — the
probe-harness.md A shared probe harness — so the same lessons stop being re-learned built and verified on a live run.
remaining-ob-hunt.md Hunting REMAINING OB by correlation — method works, run did not finish the correlation method is sound and demonstrated; 🔴 the hunt is
roster-to-craft-link.md The 116 roster records and the ~300 live craft are not directly linked 🔴 a direct pointer link is refuted in both directions; the two
ship-placement-capture-generalisation.md Capital-ship placement — does the e106 result generalise? (WIP, 2026-07-31) 🚧 WIP, time-boxed session. Two results so far: a static audit across all
ship-placement-runtime-capture.md Capital-ship part placement — runtime capture (ground truth) STATIC ASSEMBLY IS EXACT — no captures needed anymore
structures/achievements.md Achievements — the 24-entry table, and where the earned state comes from
idxd-legacy-reader-audit.md The legacy IDXD string-pool reader vs the real field table — what the old numbers got wrong 🟡 shape CONFIRMED by hand (FCSRange, ShieldRatio, hangar Model); disc-wide rates are single-source
structures/idxd-container.md The IDXD/IXUD container — record/field table, and the two beliefs it withdraws CONFIRMED disc-wide, 7 750/7 750 objects and 1 271 462/1 271 462 named fields, zero failures
structures/hud-glyph-quad.md The HUD's glyph quad — vtable 0x820B2A64 CONFIRMED for the object layout and the atlas size, read live off
structures/slb-data-offset.md .slb data offset = (cumulative start of the .pNN segment) mod 2048; the leading bytes are the previous bank's audio, not a header CONFIRMED — exact for 8 783/8 783 banks, 0 mismatches; the four values (1392/1468/1600/1728) are the running sums of the five segment sizes mod 2048, which supersedes the earlier first_riff % 2048 scan heuristic. Decoding at the right offset yields more audio in 85 of 140 sampled banks (median 70×), 54 identical controls. Wave boundaries are exact — seek magic at data_at + declared_size, 7 620/7 620 — and Channels must be read from RIFF+49 (2.12 % are stereo)
structures/sound-pak-contents.md Census of sound.pak, and the limit of the leading-region rule CONFIRMED, 5 135/5 135 names hash into the TOC, 9 519/9 519 entries accounted for, and a full 4 114-bank manifest (408.3 min of audio) computed from PsuedoBytesPerSec without decoding; ⚠️ leading-region rule holds for 1 571/4 382 eng and 0/5 100 jpn
structures/sound-cue-table.md The cue index in tables.pak — message id -> cue -> sound id -> .slb bank CONFIRMED, 1 326/1 338 script message ids bind to a bank; SOUNDS and FILES agree on the same 12 absentees, 0 orphan files
structures/bgm-two-stems.md A music bank is two stems of one performance, played together decoded structure, 32/32 banks two waves of equal duration (4 explained by the leading-region straddle); stems measured sample-synchronous; 🟡 rear-pair vs intensity-layer unsettled; no bank names a screen
structures/cutscene-message-table.md Cutscene dialogue — speaker, portrait, on-screen seconds, audio cue per page CONFIRMED, field count = 9·PageCount+2 for all 7 PageCounts, 1 252/1 252 caption keys match, 138 ids close both ways
structures/preset-message-rules.md The reactive-chatter rule table — 64 events, the (message, Yes/No) pair payload, the Sperkers roster CONFIRMED, the seven fields read off sub_82213980, phase tables merge non-additively (224 variants discarded); 8 firing sites share one entry, Probability is a per-cent roll against rand100.0; Yes = one-shot line; queue is Priority-ordered and capped at 128; the cooldown is Interval + Fluctuationrand held in the lingering queue node; the 14 player-facing events are voiced by the player set alone (13/15 Katana tables, 0/129 others) (three are seconds ×60, the Yes/No element is a u32 mask, hence the 32 clamp); MessageCount·2 == positional count in 9 216/9 216; five naming routes union to 144/144 (six names predicted from the speaker roster before hashing, control 0/4); 2 388/2 405 message ids bind to a cue
structures/mission-scoring.md Mission scoring and the S/A/B/C/D rank thresholds — 24 tables x 3 difficulties CONFIRMED, one 22-field schema over 72 records; difficulty moves 10 of 22 fields and never the RankScore thresholds; all 24 named stage\StageParameter_S.tbl (+ _Tutorial shared by six, + _Test)
structures/stage-settings-table.md The per-stage settings object — Camera rigs, Player limits, Difficulty_* damage record CONFIRMED, 24 settings objects vs 29 resource objects both using Phase_1/2/3; Camera is 13/14 fields constant; GravityFactor non-zero in 4 stages; IsBoss16Enable identifies one object; the Phase block is 94 fields of which 68 never vary between phases; all 24 objects named StageParameter_S
structures/isl-mission-timer.md The mission timer — two sequential countdowns, not a limit and a warning CONFIRMED — read from sub_822639B8; the threshold reading is
structures/archive-naming.md Which archives the disc can name — 100% for menu paks, 0.0% for the six 2D paks and GP_READY_ROOM CONFIRMED — 6027 names × 16 prefixes, bimodal; unnamed entries split by content — three UI paks have ZERO unnamed IDXD (artwork only), DefTables is the only real data gap (1295 LOD/motion tables); route 2 = the Enumerate declaration tables closes DefTables to 1413/1425 IDXD (99.2 %), 12 left; XPR2 manifests refuted as a naming source
structures/stage16-boss.md The Stage 16 boss end to end — object, loader sub_82315AE8, collision CONFIRMED — 19/19 of the loader's names are Guardian/Core fields; 5 disc fields have no string in the image (two same-length controls do)
structures/leaderboard-screen.md The leaderboard screen — sub_8219E560, 12 panel elements, 4 board scopes, 6 modes CONFIRMED — 18 names = 13 records + 5 fields; all 12 Detail_* records named, zero residual; ⚠️ 12 Detail* records and 12 Detail* fields are DISJOINT
structures/result-screens.md The debriefing and pilot-record screens — STAGE_RESULT 21 fields, OVERVIEW 9, EX_OVERVIEW 7 CONFIRMED — the compiled key lists sub_822814D8 (24/24) and sub_8227A3A0 (21/21) match the tables.pak records exactly; 2+21+1=24, 5+7+9=21
structures/player-tuning-tables.md PlayerParams — the player parameter table: Booster flight model, special attacks, 8 analog curves, the S16 boss sub_822F9498 is PlayerParams's loader, not the unit loader; the analog block's reader SOLVED — sub_821A6CF0, base 0x820A1630, via the new base-solver (261 name-block loaders image-wide after the r0 correction, control 217/226 on sub_82341A20); 🟡 nothing selects Booster
structures/hud-config.md The in-game HUD configuration — 16 records, 419 asset paths, and a per-stage ResourceTable CONFIRMED — 0 of 419 config paths resolve as pak entries
structures/mission-script-manifest.md Stage\script.tbl's 11 non-MISSION fields, and the mission dialogue table they name CONFIRMED — 1 of 7 values resolves (message\MissionDialogMessage.tbl, 200 records), controls in the same sweep
structures/stage-numbering-and-player-craft.md The disc's stage numbering (S01S16, S18S29) and the player's craft per stage CONFIRMED — two independent files close on 6 + 15 + 1 + 6 = 28
structures/hangar-loadout-system.md The Hangar loadout system — loadout → per-slot allow-list → arsenal item CONFIRMED — 60/60 slot refs, 70/88 item refs (18 = the No_Equipment sentinel)
structures/arsenal-item-weapon-chain.md The 59-of-131 arsenal question — an item names a hardpoint slot, not a weapon CONFIRMED — 0/59 PlayerWeapon are a Weapon.ID, 59/59 are a Turret_NNN slot
structures/unit-substructure-records.md The destructible-subsystem model — a unit's Turret/Bridge/Hatch/ShieldGenerator/Thruster sub-records CONFIRMED — 835 turrets, 835/835 WeaponID resolve, StructureCount control 667/684
structures/unit-datasheet-static.md The static unit datasheet and AI flight model — Generic, Maneuver, Effect CONFIRMED — 394 Generic + 114 Maneuver + 114 Effect records read; AA_/AV_ are one interleaved block (AV at X, AA at X+8), selection 🔴 BLOCKED for static RE (4 routes, all controlled)
structures/weapon-datasheet-static.md The static weapon datasheet — Weapon, Shell, AssortMissileParam CONFIRMED — 131 Weapon + 131 Shell + 9 AssortMissileParam records read
structures/isl-timers.md A ScriptPhase owns 32 stopwatches, and they count seconds CONFIRMED — the advance is read from sub_822710D0, the unit from
structures/isl-builtins.md The 147 ISL built-ins — dispatch table, calling convention, and which one does what for the table/ABI and ~135 handlers; 26 set_unit_hp_pct, 29 set_unit_damage_taken_pct, 101 all_units_invulnerable; 🟡 28
structures/isl-message-dialogue-link.md Mission scripts as dialogue — built-in 64 -> message id -> caption text CONFIRMED total, 2 683/2 683 call sites across all 28 stages resolve, no residue
structures/mission-objective-counter.md REMAINING OB — the mission's own objective counter, in RAM CONFIRMED for one Stage 02 run: a big-endian u32 whose value
structures/movie-subtitles.md Movie subtitles & the movie ↔ mission ↔ text chain
structures/asteroid-fields.md AsteroidGroup_00N — the asteroid-field tables a Phase_N.AsteroidDefinition names 10 objects/pack, 384 group records, 3 fields each (AsteroidModelName, AsteroidFrameName, EnumAsteroid = the frame record's FrameCount, 57/57, and the frame holds index, quaternion(x,y,z,w), position(x,y,z) per asteroid — 37 518 items, unit norm 37 518/37 518); Enum<Thing> counts while Enumerate<Thing> names a table (54/60); the join resolves as name_hash("stage\" + name) 9/9; 🔑 a tenth Frame_Alpha_S01_* object nothing references — it miscounts AND its 7 frames have no placement records; 🔑 S28.Phase_1 borrows S14_p2_asteroid.tbl — the per-phase join shows 9 tables ↔ 9 volumes with one table shared
structures/mcol-collision.md MCOL — same container and map parameters as REGN 🟡 OPENED — POF0 at data_size+16 11/11, bbox pad words 11/11, extent == maxmin 11/11, and bbox + cell-size distributions identical to REGN (2/6/3 and 2/9); everything past 0x40
structures/regn-map-grid.md REGN — a stage's tetrahedral navigation mesh (and MCOL beside it) CONFIRMED — tet mesh + face adjacency + portal costs + uniform grid; decoded from the file's own POF0 fixup table
structures/savegame-format.md Save file (savedata) — container exact, 3 fields named , rest (2026-08-11) CONFIRMED for the container and the chunk layout — parsed off the
structures/sound-slb.md Sound bank audio — sound.pak / .slb / XMA1
structures/stage-definition-table.md Stage definition table and the squadron (UnitGroup) roster for the record vocabulary and the stage→table wiring;
structures/stage-mission-tables.md The stage table set — phases, routes, sub-objectives and AI parameters the table set and how the stage record reaches it, validated across; AIParams disc-wide: 23 objects, one shared 34-profile roster (782 records), loader sub_8233C368; Type→field-count holds except the two _Test templates
structures/texture-color-k8888.md Texture colour interpretation — k_8_8_8_8 (32bpp UI/HUD textures)
ui-keyframe-time-unit.md What a keyframe time is worth, and what shape the ramp has CONFIRMED from the running game's own draw stream — the ramp is linear (a declared 15-unit fade lands on round(255·k/15) for all seven samples) and the animation clock advances 2 time units per submitted frame. 🟡 the seconds conversion (1 unit = 1/60 s) rests on a measured 27.6 present-frames/second
guest-frame-rate-measured.md Does the guest present at 30 or 60 fps — i.e. is the UI clock 60 or 120 units/s MEASURED — 30 fps, so 60 units/s. Answered against a pre-registration committed before the capture. The ruler is ⟨disc⟩, not a clock: ADV.wmv declares 30.0000 fps in its ASF header, so a decoded movie frame is a tick Canary's speed cannot stretch; presented frames per decoded movie frame = guest_fps/30 with no wall clock in the chain. Predicted 1.0 (H_A) vs 2.0 (H_B); measured 1.0000. Both guards pass: a perfect repeating 3-buffer cycle (52/52/52), 2 chroma per luma on 156/156, and 156 runs all of length 1 — the dropped-frame bias that would have favoured 120 is measurably absent. 🔴 Refutes the live H3 hypothesis that 120 units/s explains the play-test's late plate. 🔴 So finding 3 has no cause yet; the strongest remaining candidate is decoded — the plate's onset is t=214 (a 22-unit fade to t=236), not t=236. ⚠️ The pre-registered +34/frame control could NOT be run (this logger emits vb= addresses, not vertex contents); a weaker shader/blend census was substituted and labelled. ⚠️ Reach: one boot
splash-declared-vs-captured.md Does the declared keyframe timeline reproduce the captured splash, and is the unit rate global? YES, to ONE alpha level, 2026-09-01 — settles the R1-re-opened 🟡 ⟨our-reader⟩ entry with a disc-table-vs-vertex-stream comparison, no renderer in the chain. 50 captured alphas: 39 exact under truncation (30 under rounding), max error 1 level in 255 (0.39 %), and all 11 non-exact are low by exactly 1 on falling segments. The old rested on an a=32 that the fixed record layout puts at t=206, four units from the end. The per-GamePart RATE claim on this page is WITHDRAWN — see splash-rate-withdrawn.md. The three rates came from three regions of one run at 3.39 / 15.33 / 23.20 labels per guest second and order the same way; it is emulator pacing, already documented in boot-splash-dwells-are-declared.md. 60 units/s stands for every screen. §1 above is untouched — it compares a disc table against vertex alphas at integer t and never divides by a duration.
splash-rate-withdrawn.md Why my per-GamePart rate was the emulator's frame rate my claim, refuted within the hour — by the Port's arithmetic (a 160-unit sub-interval cannot outlast the 210-unit group containing it) and then by my own capture: the three regions ran at 3.39 / 15.33 / 23.20 labels per guest second and produced 35 / 40 / 57 units/s, monotonically. 🔴 Instrument lesson: the guest timebase does NOT remove the pacing artefact — the game's animation clock is frame-coupled, so a slow run advances less animation per guest second. My control verified that the timebase tracks real time, which is capability, not the configuration that mattered. No rate measured on this emulator is the console's; all are biased low. The best estimate stays the declared timeline against the fastest runs: 60 units/s.
units-per-second-measured.md + pre-reg Is the animation clock frame-counted or time-integrated, and at what rate 🟡 half-answered, 2026-09-01, one prediction HELD and one FAILED. The clock is NOT frame-counted: the same animation takes 21 labels in one capture and 33 in another, and splash A's logo steps +136,+34 in one and +17,+51,+34,+34,+17,+17 in the other. Steps are always integer multiples of 17 (255/15 = one unit), so the clock advances in whole units at a rate set by frame duration. 📌 This retires "2 units per submitted frame" as a MECHANISM — 2 was that run's frame pacing, so units = 2 × frames computes an emulator artefact. RESOLVED later the same iteration: 56.8 units per guest second, on ptbtn00's ramp with the clamped final step excluded, T=22 attested by two readers with no clock in the chain — inside the pre-registered 5565 band; 30 and 120 both excluded. Control passed at 1.15 %: ptcopyright gives 650.4 α/s against the plate's 657.9, implying its own T=22.25. The earlier 29.9 was a borrowed T=15 that does not apply to those elements (implied T there is 2334). ⚠️ 60 is not refuted — 5.6 % away against ~5 % quantisation — so the port keeps it; but the unit constant is eliminated as a cause of a late plate (t=236 → 4.15 s vs the port's 3.93 s, i.e. fractionally early). Instrument: guest timebase (50 MHz, scalar 1.0), control passed — 123.24 guest s over ~118 wall s.
h3-units-per-frame-measured.md How many keyframe units elapse per guest frame, and which declared time the title's settle anchor is measured against a pre-registration, 2026-09-01. 2 units per guest frame, on ptbtn00's own declared ramp (t=214→236, T=22): three consecutive gap-free steps of exactly 23 = 255×2/22, ramp span 10 labels against a predicted 11 (±1). The Port's inferred 5 is excluded by >2×. 🔴 Why 5 appeared: an alpha step is not a clock rateΔα/frame = 255·(units/frame)/T, so splash B's 34-with-T=15 and the plate's 23-with-T=22 are ONE clock. Also: the title settle anchor is t≈160, not t=118 — it is ptcopyright, the last build-in element and the only glyph one, full at label 5350 → t≈168176. 🔴 units/SECOND is NOT settled and two of my own captures disagree ~2.9× on it; the gap is in frames→seconds, not units→frames.
h3-units-per-frame-preregistration.md The prediction, committed before the capture was read kept as the control on the row above
input-pad-read-path.md What the game asks the console for, on the pad decoded from the image, 2026-09-01. sub_82457038 is the only function that reads controller data, and it compares every field of XINPUT_GAMEPADwButtons (full 16 bits), both triggers, all four stick axes — against the previous state. 14/14 loads verified byte-for-byte against /image/sylpheed.pe, database used as an index only. Two input paths: the polled state and an 8-byte-record XamInputGetKeystrokeEx ring. ⚠️ This is the superset the game can see, not the per-screen set — reading a field is not acting on it. Which bits each screen tests is open; footholds are the image's own C_PAD_DECODER / C_PAD_RINGBUF trace strings (sub_8220B610 / sub_821A6470).
input-button-numbering-is-remapped.md The bit numbering the game's pad decoder actually uses DECODED ⟨image⟩, and 🔴 refutes input-pad-read-path.md's central claim that there is "no shift and no remap — the bit positions are XINPUT's own". sub_8220D500 rebuilds the word out of XINPUT_GAMEPAD into the game's numbering: bits 03 A B X Y, 47 left stick (±20000), 811 right stick, 1215 D-pad, 1617 START/BACK, 1819 LB/RB, 2021 LT/RT (digital >220), 2223 L3/R3. Control: the 24 assignments land on bits 0…23, each used exactly once — a misdecode does not yield a clean bijection. 🔴 The old page's negative "LB and RB are not menu inputs" is FALSE — bound at config fields +0x70/+0x84; it was searched at XINPUT's bit positions in a word that does not use them. Also decodes the ring record: +12 HELD, +16 PRESSED, +20 RELEASED, +28/+32 raw trigger bytes — so edge and level are one struct and the keystroke queue is not needed to tell a press from a hold. 📌 The left stick is digitised to 4 direction bits at 61 % deflection, which is why the game cannot move a cursor at a speed (play-test finding 2). ⚠️ Not decoded: which output bit means which ACTION, and per-screen sets; 5 of 18 output sites unresolved, so the map is a lower bound
ui-splash-draw-pass.md The splash draw pass — is there a post-process, and where does the fade come from decoded from GPU state, 2026-09-01. No post-process pass exists: over all 1 048 draws of both splashes, one render target (rt0=[tile=0…] 1 048/1 048), one pitch, no MSAA, only kColorDepth/kCopy modes, resolve destinations only the two front buffers, and no texture base anywhere equals a resolve destination. The only texture bound is the sprite page. Three trivial pixel shaders; the sprite shader premultiplies, so ONE/ONE_MINUS_SRC_ALPHA is algebraically source-over. Parameters come from per-vertex k_8_8_8_8 colour in a per-frame vertex buffer — the shaders read zero float constants (ps_c[n=0] 1 048/1 048). Ramp reproduces the 34/frame law. ⚠️ Censusing the whole 600-frame log instead finds the attract movie's 640×360 chroma planes and reads as a half-res blur chain — the frame window is what avoids that.
splash-glow-is-a-baked-texture.md What the splash "blur" actually is, and which quad is which sprite DECODED (disc), confirmed against the oracle 8/8. Answers play-test finding 4 as a mechanism: there is no blur pass and no filter — each logo ships a SECOND texture that IS the blur. palogo_<x>_eff.t32 is the same artwork outset by exactly 10 px on every side, concentric to ≤ 1.5 px, drawn as its own alpha-over quad. The capture's Q0…Q7 are now NAMED: predicting each NDC rect from the declared position + decoded sprite size matches all eight bijectively, every match ≤ 0.0061 with every runner-up ≥ 0.0272 (4.58.9× margin — the control). 🔴 Refutes splash-quad-timeline.txt's "the same rects scaled slightly larger": x/y scale factors differ by up to 0.28, so a port must load the _eff texture, not transform the logo. Blend bit tested OUT of sample on entries 10/11 (never in its 35-row fit): pre-registered additive = false for all eight against 0 additive draws in 1 048 — held 8/8, control still reports 9 additive on entry 6. 📌 The ⟨our-reader⟩ 🟡 on splash geometry resolves in the reader's favour — the prediction is ours, the target is the oracle, so agreement is evidence about the reader
ui-keyframe-record-layout.md A keyframe's time word comes before its pose — the placement record, decoded CONFIRMED, decoded. A group is an 8-byte header then frames records of {u32 time; 36-byte pose}, so the time precedes the pose; the group's lead-in word at header+8 is pose 0's time and every pose is timed. Disc-wide over 13 991 groups in 33 archives, each test with a control: lead-in prepended is non-decreasing 13 991/13 991; a non-zero lead-in is strictly below the next time 5 058/5 058 (control 70.9 %); a multi-segment alpha ramp runs at a constant dα/dt 857/1 540 against 0/1 042 under the old reading. 🔴 Retires two long-standing corpus claims — "a group's data stops 4 bytes short of its final block's time slot" and "the last keyframe carries no time" — both of which were this off-by-one. Adoption is free: all 12 GP_TITLE builds render byte-identically, and over 217 builds only two elements pick a different rest() pose, both between equally invisible ones. the executable's own parser was not found (the 40/60 stride query is weak, not negative)
structures/ui-composable-bundles.md A screen build is not the only thing compose can draw CONFIRMED by measurement over the disc, with the artifact to
structures/ui-focus-and-effect-elements.md _eff glow layers are not focused-state records CONFIRMED by measurement over all 965 screen builds on the disc,
structures/ui-paint-order-key.md The paint order comes from a layer key in the T8aD sprite header CONFIRMED on both screens whose paint order has been measured —
structures/ui-prm-primitives.md .prm elements are untextured full-screen quads, and kind bit 0x10 says so CONFIRMED statically across all 965 screen builds on the disc.
structures/ui-rat-layout.md .rat — the UI element layout / animation record CONFIRMED for placement (2026-07-28). The retail UI can be
structures/ui-resting-pose.md A keyframe is the start of a ramp, not a pose that is held CONFIRMED against the framebuffer capture of the running title
structures/ui-screen-runtime.md The UI screen object at runtime — found in live guest memory CONFIRMED for the object's identity and its element array (five
structures/unit-group-table.md stage\UnitGroup_S<NN>.tbl — the per-stage squadron roster container format and field semantics, validated across all 28 stage
structures/unit-struct-runtime.md Runtime Unit struct (craft / vessel definitions) — read from live guest memory
structures/weapon-struct-runtime.md Runtime Weapon / Shell structs — read from live guest memory
structures/xbg7-mesh.md XBG7 — mesh geometry (inside XPR2 model containers)
capture-harness-status.md Why the harness stops reaching the title — and the two instruments that could not see the disc the disc is BACK (2026-08-29, container replaced at 11:07:38): /disc is a real 6.2 GB read-only mount and screen list returns 12 builds. The "no disc" section is withdrawn — and its two instruments were blind either way: find / -xdev cannot cross into a bind mount on another device, and sylph-doctor only ever looks under /work. Earlier sections: screenshot costs 10.8 s under xenia (92×), and trace_gpu_stream is a no-op in the Release build
title-crash-stl-tree.md The title-screen crash is an STL map/set erase on a bad iterator CONFIRMED — the guest throws std::out_of_range from an STL
ui-paint-order-third-permutation.md A third measured paint order — tool built and validated, screen not reached the reader works and is CONFIRMED against both previously
ui-quad-class-foothold.md The guest's UI quad class — a foothold found from the capture's vertex layout 🟡 PROBABLE for the identification below (it is a static read, but
menu-navigation-semantics.md The title menu — how it moves, and where each button goes measured: wraps both ends, Ⓑ restores focus, ⬅➡ inert; all 5 destinations driven. 🟡 GamePart id is a name match. Ⓑ leaving the MAIN menu re-measured 2026-08-30 — goes to the title, delivery-confirmed, ≤ 0.4 s, no loading screen, and the plate is re-drawn ~7 s later; downgraded 2026-08-29 as uncited, and the main menu is the only screen whose footer omits Ⓑ (0 glyph px in frame vs 514/518 elsewhere). MISSION SELECT's stuck cursor was a LOCKED stage list — labels have three brightnesses, locked 104 / unfocused 183 / focused 254
screen-transitions.md Between two screens — a fade through black, and where its timing lives the fade quad's keyframe group is decoded (disc-wide: per-pak all-or-nothing; GP_TITLE = the 6 screens, not the 6 overlays); the ~0.4 s fade-OUT is measured, not on the disc
menu-audio-cues.md Menu audio — the event vocabulary is on the disc, the binding is not SE_UI_* cue names/ids decoded and BANK_SEStatic.slb (0/322 in FILES); 🟡 event binding is a name match; SE audio IS extractable — the waves are located in Static.slb by playing them (move 0x1ec0, confirm 0x5d6c0, back 0x0ec0); Static.slb has no wave boundaries, so SE audio is not extractable is refuted and was still asserted here
boot-config-and-gamepart-registry.md What the game reads at boot — config.ini, and which GameParts exist config.ini selects the language (the disc's only config); its [SYSTEM] is empty so the boot order is not in config; 🟡 24/29 ids bind to a class, GP_ADVERTISE_DEMO is never registered
movie-binding.md Which movie plays where — boot intro, attract loop, new-game intro decoded from the movie manifest (ADVERTISE_MOVIEADV.wmv, MS00AS00A.wmv); attract identity confirmed independently by frame matching; skippability settled 2026-08-28: one Ⓐ skips a movie — title at 57 s against a 193/196/193 s three-boot baseline, the press proved singular by Canary's own delivery counter (3→4), and the skipped-to title fully functional. 🟡 skippability unsettled was stale here while the page and HANDOFF.md both had it answered
ready-room-probe.md S1 — the Ready Room probe: no-go, and not for the reason expected it is 2D and enumerates (60 builds), but the pak is briefing/tactical-map content; and kind == 0x3002 finds 0 buttons there
ui-title-build-map.md Which GP_TITLE build is which screen state CONFIRMED for title / PRESS Ⓐ / main menu / EXTRAS against live captures; the archive is 8 screens × EN/JP, and "6/8/9 are submenus" is withdrawn 2026-08-29: the two "unidentified DELTASABER plates" are the LOADING screen — builds 0/1 the plain variant, 10/11 the dressed one, decoded from their pgloading_* element names, and the executable (sub_821C4EB0, bytes checked in the image) names exactly five title-side screens: TITLE_SCREEN, BUTTON, TITLE_MENU, LOADING, LOADING2. 🟡 which loading bundle takes which of the two names is undecided. 🟡 the English member of a pair is the one in the first half of GP_TITLE.p00 — 8/8 structurally, 3/3 where a capture can check it.
ui-title-paint-order-capture.md The title screen's paint order, measured from the guest's draw submissions CONFIRMED — the order in which the running game paints the title
upstream-baseline.md A stock-upstream baseline runs Stage 02 crash-free CONFIRMED — upstream canary_experimental + only the pad
weapon-datasheet-runtime.md Weapon DATA SHEET — runtime capture (Route B) 🟡 first dynamic capture, 2026-07-28. The Arsenal's Gallery Mode panel is a
xpr2-colour-check.md XPR2 colours: channel order confirmed against the running game
focus-ring-spin-measured.md The main menu's focus ring spins continuously — and how fast measured: period 2.177 s over 9 revolutions (8 evenly spaced autocorrelation peaks) = 120 units = 60 frames = 2.00 s at 30 Hz. A pulse is excluded — annulus total conserved to 0.4 % while per-bin brightness swings by 24. the ring is the only moving thing on the settled main menu (std exactly 0.000 elsewhere). 🔴 no angle is quoted: the angular estimator FAILED its own control (30° → 0°)
structures/slb-bank-header-not-a-wave.md Why a music bank read as THREE sub-waves when the census says two decoded: the third is the bank header, emitted by our own reader. to_xma_riffs's hybrid branch derives a leading packet stream's start as first_riff % 2048, which is right only for a header shorter than one packet; a music bank's header is exactly 5 packets (10 240 B), so the modulus gave 0 and the whole header came back as sub-wave 0. The header states its own length at +0x24 in blocks. Disc-wide over 9 519 sound.pak entries: 28 match the header signature at offset 0 (ids 10011023, 11011105), 28/28 end exactly at the first RIFF, 0 have a gap, 0 false positives — so a header at offset 0 and a leading packet stream never coexist. Decode control, same chain, same bank: the emitted region gives 0.009 s against 87.744 s for the real wave 0. Corroborated by the runtime XMA probe, which saw exactly two streams at the main menu. Fixed + 2 regression tests; the VOICE_D_453 recovery is untouched (10/10 green)
title-plate-delay-measured.md How long the boot title shows build 4 before the PRESS Ⓐ plate decoded after a refutation: build 2 and build 4 run on one clock started together, and the plate's own ptbtn00 reaches a=255 at t=238; the last build-in ramp ends at t=118, so the interval is a declared 120 units = 2.000 s. 🔴 The instruction that shipped first — "wait 2.13 s after build 4 settles" — was refuted by the port with disc arithmetic and is corrected in place; 🔴 rest.t is not when a screen settles (it is the last hold keyframe before the exit: ptlogo1 rests at t=251 and stops moving at t=42). ⚠️ The wall-clock 2.13 s is 6.7 % long because Canary presents at 28.06 / 28.14 fps against a nominal 30, matching the corpus's independent 28.5 fps; author the 120 units. measured, two independent boots: 2.138 s and 2.132 s from the frame build 4 settles (glyph = its no-plate 154, motion → 0). Agreeing to 6 ms. So the boot title's end state is not plate-free and a compositor must draw two builds at once. ⚠️ Measure from settled, not from first pixels — "first drawn → plate" is 3.78 s vs 4.26 s across the same two runs, because the build-in animation's own duration varies with emulator frame pacing. Plate pulse re-measured at 2.12/2.19/2.34/2.31 s (mean 2.24), replicating the corpus's ≈2.3 s. black hold between screens bracketed at 0.140.30 s, consistent with the declared 12 units. 🔴 the Ⓐ→menu latency is still not available: both runs freeze one frame for ~1.4 s at surface mean 26.626 — agreeing between runs to 1e-6, and reproduced with stream restarts disabled — which is a guest load stall, not the capture path. Probe: 8.7 ms/frame, 7.97/7.98 fps against a requested 8, controls 9/9 + 4/4
menu-idle-and-b-2026-08-29.md The main menu does not idle back to the title — and four durations that were a pipeline refuted: no self-return in ≥ 60 s untouched; the ~810 s idle belongs to the title. 🟡 Ⓑ→title ordering measured, latency not. 🔴 classify_array at 1503 ms/frame drained an 8 fps stream at 0.64 fps and manufactured four latencies (24.66 s / 15.58 s / 25.60 s / 20.26 s) — all withdrawn; a backlog preserves ordering and destroys durations
structures/ui-settle-time.md Which instant a "settled screen" composite depicts decoded: a settled screen is one instant every element is posed at, and the disc names it — the midpoint of the longest keyframe-free interval in the build (UiBuild::settle_time / settle_window). 🔴 rest() is not that: it picks each element's last hold independently, so a two-frame flash holds at its peak and burns forever. GP_TITLE build 4 has five staggered flashes (ptlogo_back2eff1eff5, all extinguished by t110) that rest() draws simultaneously and permanently, saturating the light arc. Predicted t=198 from [160,236] before scoring: arc band 33.22 → 11.79, clipped pixels 8 581 → 1 452 against the console's 1 459 (an unfitted statistic), whole frame 14.07 → 12.06; controls at t=100 and t=358 are far worse, and a hand-picked visibility list reaches the identical 12.06/11.79/1 452. Controls: at=None byte-identical (cmp), pre- and post-rotation tags both 14.07, 13 paint-order tests green. ⚠️ Reach: of 1 758 bundles with ≥2 keyframe times only 30 % have a window ≥ 30 units and 42 % under 10 — mostly loop* fragments that never settle; check the width. 🔴 Withdraws two claims in ui-rotation-implemented.md — its "Flat. No minimum." (at posed leaves only) and its "Reborn does not draw ptlogo1/ptlogo2" (both are drawn; only kind-0x4 ghosts are skipped, and hiding the real ones makes the error worse by +5.20/+7.47). its 10.92 baseline is unreproducible — 14.07 at both tags
structures/ui-tie-break-cost-at-settle.md What the unknown paint-order tie-break costs, in pixels decoded, closing the open half of Q3: at the settled instant the tie-break costs at most 1 px at Δ1, on the Japanese title only (ptlogo2×ptlogo_tm, 5 px shared ink); exactly 0 px on all five port screens. The earlier 24-pair bound was a rest() count — and 10 of the title's 11 tied pairs are between ptlogo_back2eff1eff5, five transient flashes that are transparent on the settled screen (ui-settle-time.md). Live pairs at settle: entry 4 → 1, entry 7 → 2, the four loading bundles → 0. Not a knife-edge — sweeping every keyframe time and midpoint, the count is flat across the whole settle window, and the loading bundles' tie is live only at t17t33. Controls: an overlapping different-key swap moves 25 310 / 268 698 / ~765 000 px on the entries reporting zero; zeros are explained by shared-ink counts (the ptframe pairs share 0 px of ink). ⚠️ Entries 0/1/12/15 have no live control — their zeros rest on keyframe data, not a render. 🟡 Refutation attempt on the corpus's "24 pairs": survives as a rest-pose bound, 16/16 on entry 7. Why ties order as they do is still unknown — and now worth one pixel
structures/ui-record-loop-length.md Where a looping record's cycle restarts — and the PRESS Ⓐ plate's real period decoded: a nested record is itself a RATC bundle and its header +0x08 is the loop length; its keyframes need not fill it, and the slack is a hold at the final pose. Disc-wide over 1 781 timed nested records: 92.3 % declare exactly their last keyframe time, 7.7 % declare more, and 0 declare less — the falsifier (a cycle cannot restart before its own last pose) never fires. 🔴 The plate's ptbtn00f is 105 units of ramp inside a 120-unit cycle, so it holds dark for 15 units — the port was shipping 105, and the answer is 120. Falsification test against the running game, using a pacing factor measured independently on the focus ring (declared 120 → 2.177 s, factor 1.0885): to reach the corpus's measured 2.122.34 s, 105 units needs a factor of 1.2111.337 (🔴 excludes the ring's) while 120 needs 1.0601.170 ( contains it). Different elements, different bundles, separate runs — tied only by both declaring 120. ⚠️ Says where a cycle ends, not which records cycle. the top-level +0x08 (300 on every GP_TITLE entry, elements ending at 244269) is a different question, untouched
structures/ui-focus-record-pulse-census.md Every focus record whose glow pulses, and where rest() puts it decoded, disc-wide: 1 130 focus records, 2 664 timed elements, 210 with a varying alpha — of which 202 have rest() == the peak (burns bright forever) and 8 land mid-ramp. By pak: PILOTLOG 116, MOVIE_THEATER 54, HANGAR_ARSENAL 30, LEADERBOARD 8, GP_TITLE 2. 🟡 Bounds rather than refutes the port's "34 in the export, 2 varying, nothing to fix" — correct, and correct because GP_TITLE has 2; the pathology sits in the screens a wider port needs next. 🔴 The 8 mid-ramp ones are the worse mode: py_ranking_btn01f swings 255→127→255 and rest() returns 244, neither extreme, which looks entirely plausible and nothing reports it. Control: ptbtn01f is genuinely constant (255 throughout) and is not flagged; two hits verified keyframe by keyframe. ⚠️ A pulsing element has no resting pose — the question is malformed, not mis-answered; pose_at(t) inside the record's declared cycle (ui-record-loop-length.md) is the only well-formed query. ⚠️ 210 is a floor: focus records are matched by the Xf.rat name rule, and varying scale/rotation/position is not counted
structures/ui-title-buildin-measured.md The title's build-in and the plate glow, read out of the guest's own draw stream measured (Canary, ARM=early draw capture): the decoded mechanism is observed, not just its end state. The five flashes fire in a six-frame window and are absent from all 155 other sampled frames; ptlogo_back2eff1 is drawn in exactly 2 frames at t = 54.0 against a decoded peak of t5456, and ptlogo1 first appears at t = 42.2 against a decoded t42 — with units/frame taken from the glow's period alone, a different element. The two holders (ptlogo_back2eff, ptlogo_back2) are continuous from frame 134. The glow's per-vertex colour alpha IS its fade alpha: observed range 0…80 against a decoded peak of 80, exact and unfitted; period 51.158 presented frames over 20 cycle starts; fitting the decoded ramp gives RMS 13.16 against 38.18 reversed (2.9×), so the asymmetry is real and correctly directed. Structure: the settled title is 1011 draws naming no sprite — which is why arming at the title sees nothing. ⚠️ Frame 107 is a 27-draw spike between the movie's last frame and the title's first; calling it "the composite" was an over-read — it binds no texture and only 4 of its 27 draws log geometry. The second title entry has no such frame. ⚠️ The two entries are the same animation at different sampling phases (only 4 of 46 aligned frames match), which is what makes the eff3 result robust. 🔴🔴 RETRACTED — the game DOES draw ptlogo_back2eff3, and all five flashes fire in both entries in the declared stagger (eff3 at frames 133134 / 59575958, i.e. t=60.1 and 62.3, inside its declared t∈(58,64)). The absence was an instrument artefact: a draw batches several quads (indices=8 is two) and the log dumps only the first 8 vertices, so min/max over a line merges them — and because the wipe is right-aligned, eff3 (788…1196) lies entirely inside eff4 (447…1196), making the union exactly eff4's extent. The merged box matched eff4 to 1 px. 🔴 Three explanations had been "ruled out" and all three were aimed at the wrong failure — notably the invisible-draw check counted draws with no geometry, where the hiding place was partial geometry. Superseded text follows: three alternative explanations tested and failed: phase (its window is 6 units against a 2.23-unit step, so it cannot be missed — frames 133/134 sit at t=60.1/62.3 inside it and draw eff2 and eff4 instead), an unlogged draw (exactly 2 blind draws/frame, always the same full-screen-triangle shader, present when no wipe is active), and a bad position guess (dropping position entirely, zero quads anywhere have a width within ±30 of 408; the spectrum jumps 262 → 748). Draw counts across both entries: eff1 4, eff2 3, eff3 0, eff4 6. (all from the merged-box parse, and wrong) 🔴 The port draws eff3 at t=6062 and the console does not. Why is not established — nothing in its element record differs from its neighbours. ⚠️ An earlier "sub-frame phase" explanation and the advice that drawing all five "shows more sweep than the console" are both withdrawn. ⚠️ What a frame-by-frame build-in comparison will show is disagreement about which flash lands in which frame — 2 units/submitted frame against this run's 2.231 units/presented frame — and neither side is wrong. 🔴 Trap: matching a bound texture's dimensions to a sprite fails both ways — it missed every flash and read the intro movie's 640×360 YUV planes as ptbase2. A regression of five events' observed frames against their declared times (residuals ≤0.9 frames) recovers the intercept at frame 106.1 when the composite spike, not in the fit, is frame 107. ⚠️ Per-vertex alpha = fade alpha holds for the glow and does not generalise — eff4 reads 255/127/254 on consecutive frames. Frame rate not recorded, so nothing is in seconds; the glow's period implies a 114-unit cycle against a declared 120, unexplained; eff5 vs ptlogo_back2eff not separated
structures/boot-splash-gap-measured.md The black gap between the two boot splashes measured in the guest's draw stream, which separates true black from a fade tail where luminance cannot: the publisher's last sprite is frame 125 (alpha 7), then frames 126129 submit NO sprite quad at all, then the developer fades in at alpha 34. The gap is 4 presented frames. Converted with the disc as its own clock — palogo_sqex declares alpha≥1 for 239.8 units and is drawn in 105 frames → 2.284 units/frame (the title capture independently gave 2.231) — that is ~9.1 units ≈ 0.152 s, against the 12 the port authored; ⚠️ and the true black is shorter, since both boundary frames still carry picture. 🔴 RETRACTED: "the developer splash is ONE composited 525×259 quad" — the same batching artefact. It draws three logos and three glows as separate quads in one indices=24 call; the 525×259 was gamearts_eff merged with seta_eff. The port refuted it with arithmetic (a 259-tall box cannot hold logos spanning y 164…585) before I checked. ⚠️ The gap measurement is unaffected — those glows are the developer splash's first draw. Not declared on the disc: palogo_eff0.prm is a single static keyframe, and the top-level +0x08 is a family constant (300 / 60) whose slack ranges 12226 units. The executable is not looked at — named, not claimed. 🔴 The instrument was perturbing the measurement: the capture script taps Ⓐ on "screen changed a lot", which is also true of a fading splash — it tapped through the publisher and the developer never appeared. GRACE=1 and NOTAP=1 knobs added
structures/ui-forced-backdrop.md Where a keyless primitive paints, when the file forces it decoded, partly closing ui-prm-primitives.md's standing blocker: an element covering the screen and fully opaque at some instant cannot paint above anything visible then, and where that set is every other element its position is forced first. Disc-wide 80 instances forced, 50 constrained but not forced, 0 unconstrained. Two controls, both measured orders from the running game: it reproduces palogo_eff0.prm = FIRST (opaque 211 instants, below 6/6) — which a name-based rule gets wrong, since it is named like an overlay — and permits pteff00.prm on top (opaque 2 instants, below 3/23), which is where it is measured. Answers the port's build_12/build_15 blank-screen contradiction: pgloading_eff00.prm is forced first, 4/4. Explains 36 builds the corpus recorded as "one colour" with no cause — pzeff00.prm forced first 32/32, so our own sort wiped them. 🔴 The rule's limit was found by its own test failing: applied to .t32 sprites it claimed 22 must sort first against their own keys (pneff01 0xd850 at #8/13, pbfriendly 0x9230 at #17/49) — a sprite's element alpha says nothing about its texture's coverage, so it is now restricted to untextured primitives. ⚠️ Assumes straight alpha-over; blend mode is still . ⚠️ A lower bound, not an ordering. ⚠️ No new oracle run — the controls are prior measurements
structures/tbm-submenu-not-reached.md Does a .tbm draw pixels in the running game? YES — measured 2026-08-30, and it closes the surviving "inert" reading in ui-forced-backdrop. The TUTORIAL screen was reached and captured: a full-screen blue circuit/hex background, where GP_TUTORIAL build 0's element 0 is pubase.tbm, pivot (640,360) = 1280×720, the only full-screen textured element in the bundle (the other, pueff00.prm, is an untextured primitive the colour census puts at pure black). Our render of the same build is the identical layout on pure black — 6.06.4 % inked against the game's 99.7 %. So the rule's 24 .tbm deciders are correct, not harmless, and 🔴 screen render silently omits the background of every screen carrying a .tbm. ⚠️ One .tbm observed; the class question is settled, the ten other families are not. 🔴 Getting there took three runs and cost two instrument failures worth reading: a 0.12 s Ⓐ during a screen load is never delivered ([RE-INPUT] is the only witness), and correlation cannot identify a screen when the candidate renders are near-blank — masked correlation failed its control (picked EXTRAS over the known menu by 0.004), a high-passed variant passed by only 1.28×, and reading the title off the screen settled it in one look
structures/ui-forced-backdrop.md (span sensitivity) How much of the forced-backdrop rule rests on the timeline convention decoded: the span is 0..=max keyframe time over every element, and an element holds its final pose — decoded, not assumed (ui-keyframe-time-unit.md, ui-record-loop-length.md). Sensitivity over the 130 keyless full-screen primitives with an opaque interval: using the header's declared +0x08 instead changes 0 verdicts (interchangeable); using the primitive's own last keyframe changes 72; counting elements gone after their last keyframe changes 72. 🔴 So the hold decides 55 % of verdicts — and dropping it is refuted by a measured order: palogo_eff0.prm is a single keyframe at t=0, so without the hold it is opaque for one instant, nothing else is up, and the rule calls it free against a game measured painting it first. The verdicts that matter are convention-independent — pgloading_eff00.prm is FIRST under all four, pteff00.prm FREE under all four. ⚠️ The port's 256 vs 211 was a bundle mismatch, not a definitional one: palogo_eff0.prm runs to t=255 on the publisher splash (entries 10/13) and t=210 on the developer (11/14)
structures/ui-clock-freezes-at-settle.md The top-level clock stops at the settle point — observed in the running game measured: GP_TITLE build 4 declares t = 0…269, about 120 presented frames at this run's pacing, and the dwell lasted ~1 100. ptcopyright declares alpha≥1 for 106 units (t=138…244) and is drawn for 1 050 frames; ptlogo1 declares an exit at t=264 and is drawn for 1 095. Both vanish within three frames of the dwell ending. The clock advances through the build-in, stops inside the settle window [160,236], and holds; the exit ramp plays when the screen leaves, not on a timerui-settle-time.md's decode observed from the other side. A nested record keeps looping on its own clock throughout. 🔴 This closes the 114-vs-120 gap, and it was my arithmetic: 2.231 units/frame was regressed over build-in events (the only stretch the top-level clock advances) and applied to a period measured during the freeze — two different clocks. The declared 120 was never in doubt from the calibration-free dark-fraction test. The 51.158-frame period is now confirmed by a second independent estimator (autocorrelation, lag 51 with harmonics at 102/154) — ⚠️ whose first version failed its control, returning 48, because it indexed by sample position where the log's frame numbers have gaps. The sweeps' period stays unmeasured: the same validated estimator disagrees between two dwells of one screen (515 vs 452 frames). 🔴 Blocker: a single Ⓐ on the title faults the guest — 3 attempts, 2 register dumps of 223 MB and 519 MB, against 3 no-input runs that all completed; bounds menu-side dynamic RE here, and any scripted button press needs a canary.stdout size guard
structures/boot-splash-dwells-are-declared.md How long each boot splash is shown decoded: the dwells are the bundles' own declared timelines — publisher t=0…255 = 4.250 s, developer t=0…210 = 3.500 s at 60 units/s. The corpus's independent screenshot timing over 3 cold boots gives 4.30/4.60/4.37 and 3.51/3.50/3.37 — the developer agreeing to 1.1 %, two of its three runs to 0.3 %. 🔴 Wall clock is the wrong unit to author: a fresh no-input boot measured the same two dwells at 5.105.61 s and 3.834.30 s, 1520 % longer than both the declared values and the corpus's runs, on the same disc — so a seconds figure is one run's emulator pacing. Boundaries from the draw stream: publisher wordmark frames 6119, 3 frames with no sprite drawn, developer glows 123, wordmarks 140209, intro video 216. 🔴 The frame→wall-clock instrument resolves to one BUFFER FLUSH, not one frame — 69 of 125 samples showed no advance and the rest jumped 715 frames, making the apparent rate swing 0.01640.0316 s/frame; frames 119 and 123 fall in one burst, so the inter-splash gap is not separable by it. Quoted as brackets; sub-flush estimates withdrawn before reporting. ⚠️ palogo_anima never appears — almost certainly the 8-vertex cap (7 elements batched, 2 logged), the same trap as the eff3 false negative, so it is named not reported. the publisher's 4.1 % error vs the developer's 1.1 % is unexplained
structures/ui-forced-backdrop.md (colour census + self-refutation) What colour a keyless element is, and which forced verdicts the argument actually supports decoded, disc-wide: every full-screen *eff00* primitive is pure black at its various alphas (ff000000, 7f000000, 40000000, b2000000, cc000000, d4000000, 00000000) — exactly an alpha-over dim or fade, and an additive black quad would be a no-op nobody would author. The only non-black primitive on the disc is pbafc.prm, RGB 00e8e0 cyan at alphas to ff, and it is 844×600, not full-screen, so outside the backdrop rule's geometry guard — it is now the sole additive candidate. 🔴 Self-refutation: of the 80 forced-first instances only 42 are .prm; 38 are .tbm carrying fade ffffffff. A solid white quad painted first would make the screen white and no screen is white, so a .tbm is a white modulation on a texture — and element alpha does not establish its coverage. That is the .t32 error one extension further out: I had fixed the symptom (el.sprite.is_some()) not the cause, an element's alpha is not its texture's opacity, and only an untextured primitive makes the two the same fact. So 42 verdicts stay decoded, 38 drop to 🟡 (still almost certainly right — all named *base*, full-screen, and pfbase.tbm's first position is measured — but on a name-and-role argument this page elsewhere calls the weaker kind). ⚠️ Code deliberately unchanged: restricting to .prm would send eleven screens' backgrounds back to last, the blank-screen bug the rule fixed. Split pinned by a test
structures/ui-blend-mode-decoded.md Which blend the game draws a sprite with DECODEDT8aD +0x04 bit 0x02 set ⇒ ADDITIVE (RB_BLENDCONTROL0 = 0x01010101), clear ⇒ premultiplied alpha-over (0x07010701). 🔴 Reverses two of my own pages: t32-blend-mode-not-on-disc.md ("not on the disc") and ui-blend-mode-measured.md (classified measured, "which field selects the mode is still unknown"). Fit: 35 elements, 3 screens, 16 set/additive + 19 clear/alpha-over, 0 errors, every label read out of the guest command stream. Control: of every bit of the first 12 header words, exactly one separates those 35 without error — no tie, which is what +0x08 = 0x8050 failed. Within-pair: ptbtn00 0x0110 alpha-over vs ptbtn00f 0x0112 additive — same screen, same bundle, adjacent draws, one bit apart; and other f variants (ptbtn01f, ptbtn11f) are bit-clear and alpha-over, killing "focused variants are additive". Out-of-sample prediction, committed before its capture (bbd85e9): GP_OPTIONS entry 19 predicted 3 additive of 16 — the game drew exactly po_menu_eff01/02/03 additive and nothing else. 🔴 Revives a REFUTED claim, whose refutation was a claim about our renderer while that renderer had a stale keyframe association, no leaf geometry and no rotation. ⚠️ Two other readings of the same bit stay refuted (eff in the name; premultiplied storage) — those were its meaning, this is its effect. ⚠️ .prm primitives have no T8aD header, so the bit cannot speak for them
structures/ui-blend-mode-measured.md Which blend the UI draws each element with measured off the GPU, per draw, on the main menu and EXTRAS. Two states and one pixel shader: RB_BLENDCONTROL0 = 0x07010701 (src ONE, dst 1SRC_ALPHA, alpha-over) for ptbase, pteff05, the fade quad, ptmsg, ptmsg2, pttitle and every button; 0x01010101 (src ONE, dst ONE, ADDITIVE) for ptframe1, ptframe2, ptframe3, pteff20 and both rotated sweep strips. Control 1 — the NDC→pixel conversion that names a draw by its quad size reproduces 1134 and 1303 px for the two sweep strips, measured by a different tool in a different session, on both screens; the tool prints PASS/FAIL and disclaims itself on FAIL. Control 2 — pixel shader 0xE59B2B3DA4AA9008 is used with both states (12 additive, 18 alpha-over), so ptframe1 and ptbase run the same shader and this is a blend result, not a shader result. Independently corroborated by the port, which solved the composite per pixel from two backgrounds and ranked additive 34.3/28.9 against alpha-over's 65.0/71.3. 🔴 Supersedes the conclusion of t32-blend-mode-not-on-disc.md — its negative and reach stand, its instruction "any blend you choose is authored" does not. Which field selects the mode is still unknown; 🔴 and a first phrasing of this page — "elements sharing a mode are batched into one draw call" — is refuted by its own log: menu draws 5, 6 and 7 are three separate additive draws. Only the one-way implication holds, elements inside one draw share a state. ⚠️ Two additive menu draws unidentified; the title screen was not captured
structures/ui-prm-blend-mode.md Whether a primitive blends additively or alpha-over undecodable, with reach — but the consequence is closed. Looked in the bundle (no field: the declaration words are constant and a primitive has no RATC child at all), the colour census (every full-screen *eff00* primitive is pure black; the only non-black primitive on the disc is pbafc.prm, cyan 00e8e0), the occlusion constraint (inapplicable — pbafc.prm strobes 255/124 every 2 units, travels, and is scaled 2 %×3 %, so it draws ~17×18 px, not its declared 844×600), and the oracle (GP_READY_ROOM is a recorded no-go and gameplay needs the Ⓐ that faults the guest). Why it stopped mattering: for a black quad the hypotheses differ only in whether it hides what is beneath — drawn first it is correct under both, drawn last only under additive. So forced_backdrop's verdict is robust to the open question, and the port's original "layerless sorts last" was wrong under alpha-over and merely pointless under additive. ⚠️ This is not evidence for alpha-over. 🔴 The investigation found forced_backdrop judged coverage from the pivot alone, ignoring scale; checked first, all 80 forced instances are at 100 %, so no verdict moved and the added guard is defensive
structures/title-a-press-fault.md Why a single Ⓐ on the title faults the guest — the blocker on all menu-side dynamic RE SOLVED 2026-08-30, and it is the emulator, not the game. Xenia returns X_ERROR_SUCCESS with a zeroed keystroke on every XamInputGetKeystrokeEx while a XAM dialog is up (xam_input.cc:197, upstream); the game's pump is an unbounded while (GetKeystrokeEx()==SUCCESS) queue.push_back(), so it queued 8 388 608 empty keystrokes, grew its vector to 64 MB, asked for 128 MB, got a failed allocation back unchecked and copied off the top of the guest thread stack. The number is the argument: the Canary counter reports 8 388 601 swallowed calls at the last report before the crash, the dump's r29 says the vector held 8 388 608 — two independent instruments, 7 apart, inside the 600-call reporting granularity. No new boot: the failing run's 326 MB log was still on disk. 🔴 RETRACTED — "r9 is a wild pointer above 4 GB". Xenia prints si_addr, a host address, and the guest is mapped at 0x100000000: 0x1701D0000 0x100000000 = 0x701D0000, which is r9 in the dump — an ordinary guest heap address on an uncommitted page. Subtract 0x100000000 from every Access Violation … at 0x1________ before reading it. Decoded code path, image-checked with 0 mismatches over 586 instructions: sub_824574C0 the input-manager singleton at 0x828F3888, sub_82457038 the keystroke pump, sub_82457780 its vector<X_INPUT_KEYSTROKE> insert-with-grow. It explains the earlier successes: whether a XAM dialog is up is emulator state, so "reproduced 4/4" and "Q4/Q5 pressed Ⓐ fine" were both always true. 🟡 Which dialog is still open — XamShowDeviceSelectorUI is ruled out (storage_selection_dialog = false takes the headless path), XamShowSigninUI / XamShowMessageBoxUIEx are not; the settling experiment is one log line per is_xam_dialog_present_.store(true) site, not another blind boot. 🟡 Three untried routes out: dismiss the dialog, --headless, or return X_ERROR_EMPTY from the swallow. frame_clock.sh's 300 MB guard killed the run as designed — keep it
structures/plate-pulse-phase-lock.md Does gating on the plate pulse bias what a title capture can show? measured — it PHASE-LOCKS the shutter. The plate's pulse is part of the animation, so wait_plate_pulse.py does not only wait for settling, it synchronises the shutter to the animation's phase: at the shutter instant the sweep strips sit 2526 px apart across two runs in different locales and different sessions1.6 % of a ~1600 px traverse. 🔴 Consequence: the RMSE 0.32 recorded as "between-session capture noise" is a lower bound produced by the instrument, not a property of the game; the honest figure at an arbitrary phase is 11.9, a factor of 37, and I had read 0.32 as evidence the JP title is still when it is evidence the gate works. The era adjudication survives — margin 16.72 clears even 11.9 — and survives for the reason its own file gave: correlated noise moves both candidates together and cancels in a margin, so prefer margins to absolute scores under a gated shutter. The within-run at-rest result also survives (five frames ~1.5 s apart are not gated individually). ⚠️ Reach: this shows the lock, not its mechanism — both runs boot the same ISO from the same state, so a deterministic boot could produce it without the gate; two runs deliberately shuttered at gate + k frames would separate those and were not run
structures/plate-pulse-measured.md Does the PRESS Ⓐ plate stay up, pulse, or blink once? measured — it PULSES, continuously and without decay, on a title held with no input: two windows in one boot, 58 s and 57 s, ~23 cycles each, periods 2.530 / 2.540 s agreeing to 0.4 %. ⚠️ It never goes off — the plate-absent floor is 159 green pixels (the title art's own, from live-title-build4-no-plate.png) and the pulse bottoms at 714, 4.5× that. So the port's "flash and nothing after", reasoned from ptbtn00 expiring at t=244, is wrong; ptbtn00f's 120-unit cycle is what runs. 🔴 Two estimators, one misspecified: mid-crossings replicate to 0.4 %, a single-sinusoid fit does not (2.553 vs 2.413) because the waveform is fast-rise/slow-decay — and its own r² of 0.468/0.228 is the tell. Both were controlled on synthetics at 2.24/2.55/3.10 s laid on the real timestamps and recovered all three exactly. 🟡 wall-clock is 13 % longer than the corpus's earlier 2.24 s mean — same declared 120 units, different pacing (×1.27 vs ×1.12), so author the units. ⚠️ Reach: one boot; does not distinguish the boot title from an attract-loop title; the glyph count is a thresholded pixel count and not an alpha, so no duty cycle can be read off it