The linear walk's 10% unknown was a floor imposed by the method: a block entered only
by a branch has a well-defined state, just not one a straight-line pass can see.
tools/re-capture/isl_cfg.py replaces it with a worklist fixpoint that joins each
block's state over its ACTUAL predecessors -- a value survives only if every
predecessor agrees.
Over all 28 stages:
instructions reached by the CFG 85.0%
condition sites, unknown LHS 756 (10.00%) -> 402 (5.32%)
of those, never reached at all 389
joined away (predecessors disagree) 13
both resolve but DISAGREE 161 <- linear walk was wrong here
Those 161 are on top of the 889 the previous jmp fix caught.
Two zero-results on the way, both my own bug, both caught because the number looked
wrong rather than because a test failed:
* The first CFG run reached only 36% of instructions and made things WORSE (35%
unknown). Cause: the phase bases reach almost nothing. Most routines are
COROUTINES the engine starts from its trigger queue, with no static predecessor,
so every start_coroutine target has to be seeded as an entry.
* That seeding then found ZERO entries in a file with 216 start_coroutine calls,
because the target is staged in TWO steps -- special[0] = imm, then
local[0] = special[0] -- and I matched only the direct-immediate form.
Reachability went 36% -> 64% -> 85% as each was fixed.
The 389 still unreached are an honest limit rather than a gap: nothing in the bytecode
starts them; they are entered from the trigger queue at phase+272, by data rather than
code, so no purely static analysis reaches them.
isl_report.py conditions now uses isl_cfg; calls and phase-ends regenerate
byte-identical. Stage 02 unknowns drop from 71 to 25.
Reading builtin80's body (0x82268460) to name it: it is NOT a predicate. It
allocates a 20-byte object, stamps vtable 0x820A8CB0, magic 0xAB0311BA and the
unit's live object into it, pushes it onto a queue via the same helper push.i uses,
and returns 1 -- or 0 when the unit is absent. A command.
That made the conditions listing impossible: it showed a six-way switch
`if builtin80(TCT206) == 0 … == 5` on a function returning 1 or 0. Disassembling the
site shows two unconditional `jmp`s between the call and the compare, so 0x1B6C0 is
reached ONLY by a branch and its special[0] has nothing to do with builtin80.
op12 is unconditional -- the next instruction is never reached by fall-through -- and
the tracker walked through it exactly as it had walked through end_coroutine. Last
iteration I fixed the instance and not the class, leaving 22x more bad sites in place
than the fix removed.
A/B over all 28 stages, 7563 sites, resetting at jmp as well:
sites whose operands change 889 (11.75%)
LHS unresolved, before -> after 34 (0.45%) -> 756 (10.00%)
So the previous commit's headline "0.0% unresolved" was a MISSING CHECK, not a strong
result: the linear walk always had some value to report, and reporting it was the bug.
10% is the honest figure and the other 90% is trustworthy for a reason.
Also corrected: isl-unit-args.md illustrated its diff with 0x1B6C0, which is one of
the bogus sites. The UNIT_ARG result itself stands -- it came from reading
implementations, not from this listing -- but the example was picked from bad output.
Not done, and said so: recovering the 756 needs a dataflow join over each block's
actual predecessors, a CFG fixpoint rather than a linear pass. The branch targets are
all known so the CFG is available; the analysis is not written.
calls and phase-ends regenerate byte-identical; conditions changes on 187 lines.
isl.py's UNIT_ARG decides whether a built-in's slot-4 operand prints as a unit name
or a raw number. It was inferred statistically from operand ranges and, by its own
comment, listed a slot "only when the ratio stayed below 1.0" -- conservative.
The vtable base makes it a lookup instead: every unit-taking built-in's implementation
opens with lwz 324(phase) / lwz 4(argbase) / rlwinm 2,0,29 / lwzx / lwz 4(rec). Read
directly for all 147:
implementation indexes [phase+324] by an argument 55
of the statistical set's 31, confirmed 31 (zero false positives)
UNIT_ARG claims a unit, implementation does not 0
implementation says unit, UNIT_ARG missed it 24
The 24 include builtin80, group_ratio_pct, is_engaged, set_unit_flags,
squadron_trace, wait_units_ready and deploy_and_wait. Hand-verified by reading
builtin7, 16, 80, 105, 117 and 136.
Recorded because it nearly passed: my FIRST control -- whether the additions' operands
resolve to a symbol-table-2 index -- is worthless. The additions score 100.0%, but so
do the 31 baseline (100.0%) AND the 92 built-ins in neither set (99.3%). Symtab 2 is
dense enough that almost any small integer lands in it. A control the negative class
also passes is not evidence.
The control that discriminates is the tag word: a symbol operand is a two-word pair
whose first word is the constant 1, so slot0 == 1 exactly when slot 4 is a unit --
100.0% (13677 calls) / 100.0% (140) / 2.5% (2903). A 40x separation.
Artefacts: isl-stage02.txt and -phase-ends.txt regenerate byte-identical; -conditions
changes on 28 sites, every diff line pairing, each a raw number becoming a unit name.
Left unnamed on purpose: all 24. builtin80 returns a small enum (tested 0..4 in a
switch) but its body past the liveness check is unread; builtin103 is a predicate over
[phase+10152]/[phase+10156]; builtin105 tests a unit record's +16 against 4.
The listing showed end_coroutine as the left-hand side of 34 comparisons disc-wide.
That is impossible -- it returns no value a script can test -- so it was the bug
reporting itself.
The recorded fix ("set special[0] only for built-ins that write [phase+164]") is
REFUTED. end_coroutine's handler 0x82272624 is `addi r11,r0,1 ; addi r3,r0,3 ;
stw r11,164(r31)` -- it DOES write [phase+164], so that filter would have kept it.
Reading the handler before writing the filter is what caught this.
The real cause: end_coroutine returns 3, which DESTROYS the thread. Execution does
not continue past it, so the instructions following it in the flat stream belong to
a different routine and every tracked value is stale. The linear walk that makes
the decode possible is exactly what walks across that boundary.
A/B over all 28 stages, 7563 sites, resetting the tracker at end_coroutine:
sites whose operands change 34 (0.45%)
LHS = end_coroutine, before -> after 34 -> 0
left as an explicit unknown 34 (0.45%)
The two counts being equal is the result: the leak was confined to exactly the sites
that displayed the impossible value, so the other 7529 conditions were never
affected. Those 34 now print "<unknown: reached after a coroutine boundary>".
Not done, and said so: their RHS is still exact and the LHS is recoverable by seeding
the tracker at coroutine entries, whose targets are staged slot 0 of start_coroutine.
data/isl-stage02-conditions.txt regenerated; calls and phase-ends both byte-identical.
The deque ops are an EXPRESSION STACK: push the left operand, evaluate the right
(a built-in call, whose result lands in special[0]), pop the comparand back into
special[1], compare. Tracking that through the linear decode is enough to recover
what each site tests.
Evidence the model is right, not just plausible:
push vs pop across all 28 stages 1877 vs 1877
files that underflow or end unbalanced 0 of 28
Stage 02 pop.i sites followed by cmp.i 319 / 319
ops immediately before a pop.i call x313, cmp.a x6
isl.conditions() recovers 7563 condition sites disc-wide with 0.0% left as an
unresolved special[N]; 83.2% have a built-in call as the LHS and 99.7% compare
against a plain number. Most-tested: hp_pct_test 1955, unit_state 1257,
unit_relation 796, dist_lt 450, unit_alive 413.
They read as conditions now:
if unit_alive(TCN105) != 1
if hp_pct_test(ADT308, 0) != 1
if dist_lt(ADT308, TCN000, 15000) != 1 (world unit = 1 m, so 15 km)
if unit_state(ADT308) == 1
data/isl-stage02-conditions.txt was a stale artefact with NO generator -- the thing
isl_report.py's docstring complained about. It has one now (isl_report.py
conditions). The calls and phase-ends artefacts both regenerate byte-identical, so
the change is additive.
Recorded rather than glossed: 15 of Stage 02's 965 sites (1.6%) attribute the LHS to
end_coroutine, which returns no value -- the tracker sets special[0] on EVERY call,
so those show a stale value and are wrong, not imprecise. The fix is to set it only
for built-ins that write [phase+164], which the vtable work makes checkable.
Answers what the previous commit left open: naming the branches did not give a
clear condition, because that needs the operand chain feeding each compare.
First, a correction to my own work. isl-bytecode.md -- which OWNS the opcode table
-- already named ops 21-24 push.i/push.f/pop.i/pop.f. isl-branches.md, which I
wrote last iteration, said op21 and op23 were unread. The stale file was mine.
Verified from the thunks rather than accepted: 21 pushes [phase+168] onto the deque
at phase+44, 22 pushes [phase+184] onto phase+64, and the 23/24 handlers touch only
r3+168 and r3+184. So pop.i lands in special[1].
New: the 147-entry built-in table is a thin DISPATCH LAYER, not implementations.
Each stub resolves the local[] argument base and tail-calls a fixed ScriptPhase
vtable slot. 112 of 147 dispatch that way; 17 write [phase+164] inline; 0 write
+184. Every named predicate is in the vtable group -- unit_state 184, unit_alive
188, hp_pct_test 64, dist_lt 56, is_engaged 252, timer_elapsed 372 -- which is the
control that the split separates engine queries from script bookkeeping.
The vtable is 0x820A84BC, derived from a known implementation rather than a stride:
MARK_LAST_PHASE is documented as [phase+300]=2; the function 0x8226B498 is exactly
that stub; it appears as a data word at exactly one address, 0x820A8570; built-in
39 uses slot 180. The check NOT used in the derivation: built-in 40 mark_not_last
uses slot 176, and slot 176 holds the [phase+300]=1 stub. Predicted and confirmed.
The db's own vptr_writes independently lists 0x820A84BC, written at 0x82261B80.
unit_state = slot 184 = 0x8226ADF0, which indexes [phase+324] by local[4] and writes
its answer to [phase+164] = special[0] at both exits. The phase-3 poll loop now
reads end to end: unit_state(ADT308) -> special[0]; pop.i -> special[1]; cmp.i; beq.
isl.py names ops 21-24; the calls artefact regenerates with NO diff.
Left open and said so: the other 111 vtable slots, which comparand each site pushes,
the 35 non-vtable built-ins, and the vtable's length.
Closes the backlog item that was the last thing between the flat decode and a
per-phase clear condition, and closes isl-builtins.md's standing "op10 + op13 look
like a switch -- NOT confirmed".
op10 resolves two operands, issues a SIGNED cmp, and writes three condition bits to
a bitset at phase+24: bit 0 = EQ, bit 1 = GT, bit 2 = LT. op11 is the same machine
for floats via fcmpu. op13-op18 branch on those bits to [phase+232] + word@+4 --
the same phase-relative target form as the unconditional op12:
13 bit0 set beq 16 bits 2 then 0 ble
14 bit0 clear bne 18 bits 1 then 0 bge
15 bit2 set blt 17 bit1 set bgt
13/14/15/17 are byte-identical apart from the bit index and the polarity. All six
relations are present and each appears exactly once; that completeness is the check
that the reading is right, rather than the usage pattern -- which the item
explicitly warned against.
Operand order recorded because it is easy to reverse: LHS = (kind byte[1], word@+4),
RHS = (kind byte[0], word@+8).
Method note in the doc: the jump table at 0x822635FC holds THUNKS, and the handler
is the bl target inside each. My first pass guessed handler addresses at a fixed
stride, landed mid-function, and produced a 20-line "difference" that was pure
misalignment.
isl.py names the ops; data/isl-stage02.txt is regenerated and every diff line pairs
exactly, only the op-name column changing (op10->cmp.i x5, op13->beq x4,
op14->bne x1). data/isl-stage02-phase-ends.txt now shows the phase-3 poll loop
reading as one: unit_state(ADT308) -> op23 -> cmp.i -> beq back to 0xFEB4.
Left unnamed on purpose: op23 (0x82271C30) and op21 (0x82175C20).
Two files (isl_report.py's docstring and structures/isl-builtins.md) recorded the
same blocker on a faithful per-phase condition listing: that it needs the coroutine
entry points from start_coroutine's operand. Measured against isl.call_sites(),
which enumerates by scanning the encoding rather than by decoding and so is an
independent denominator:
linear + jumps, stopping at ret (what the tool did) 133 / 2846 = 4.7%
linear + jumps, continuing past ret 2275 / 2846 = 79.9%
... + following start_coroutine (the recorded fix) 2355 / 2846 = 82.7%
plain linear decode, no control flow at all 2846 / 2846 = 100.0%
Following the coroutine entries buys 2.8 points. Disc-wide, a plain linear decode
from the first phase base reaches 25705/25705 call sites over all 28 stages, and
28/28 decode clean to code_end with no desync.
The real bug was isl.dis ending on `if op == 20: break`. Op 20 is `ret`, but this
is a coroutine VM -- the thread suspends and resumes at the FOLLOWING instruction,
so code continues past it. dis() now takes stop_at_ret (default True, preserving
the old output: data/isl-stage02.txt regenerates byte-identical) and
isl.linear_offsets() is the correct walk.
By-product, kept with its control: start_coroutine's target is staged slot 0 --
73/83 phase-1 sites land on a valid instruction, against a 38.7% chance rate for an
arbitrary 4-aligned offset.
New artefact data/isl-stage02-phase-ends.txt with a committed generator
(isl_report.py phase-ends). It shows END_PHASE's call site is the WRONG place to
read a clear condition: all 12 Stage-02 sites sit in one stereotyped outro. Not
settled, and stated as such: op10/op13/op14/op21/op23 are unread handlers, so the
condition in the poll loop upstream cannot be named yet.
Answers the backlog's open "first step: diff the two readers across the disc and
count disagreements", statically over every IDXD object.
Of 7750 objects and 738922 named fields whose true value is numeric, legacy
get_f32 is correct 39.42%, returns None (harmless) 43.04%, and returns a WRONG
NUMBER 17.54% (129612 fields).
The wrongness has an exact predicate: single-record objects 0 of 29822 wrong
(0.00%); multi-record objects 129612 of 709100 (18.28%). The mechanism is in
get_raw itself -- it flattens the pool to a token list, finds the FIRST occurrence
of the key, and returns the preceding token, with no notion of records. So every
record after the first inherits record 0's value: Weight truth=1.0 legacy=0.3,
Points truth=10000 legacy=4000.
Practical rule recorded: a get_f32 number from a single-record object is safe; from
a multi-record object only the first record is.
Withdrawn in the same document: my first sweep compared against "the string before
THIS field's own key" and reported 65.90% -- that is not what get_raw does, so the
figure is not the legacy reader's error rate.
Two updates to the mission-freeze entry, both measured.
DONE: the entry's "first step, revised" was "make pilot.py shoot, then re-run
ob_flag.py ... the actual obstacle is that nothing the pilot does moves the
counter". pilot.py now has SYLPH_WEAKEST=1 (target score scaled by remaining
hull) and the next run moved REMAINING OB 008 -> 007 concurrent with the live e010
floor dropping 16 -> 15. The counter is fully solved; the freeze work now needs
only a frozen sample for the v2 wait probe.
DEAD END, with numbers: --log_mask=0 does not surface kernel call traces. A full
Stage 02 run produced 199 MB at ~33 MB/min, and a 300k-line tail is 254127 A>
(Apu/XMA), 42444 d>, 2897 G>, 532 w>, with ZERO k> and only 14 K> lines per 58k of
boot. XamShowSigninUI / KeWaitForSingleObject / NtWaitForSingleObject each appear
exactly once in the whole log -- an export listing, not call traces. That
independently confirms the entry's own cost note: those calls are kHighFrequency
and silent without --log_high_frequency_kernel_calls=true.
The run also did not freeze (healthy TIME 00:24.28 -> 03:33.28), making it the
fourth consecutive non-freezing run.
Last iteration I ruled the speed route out because the emulator is not real-time
so the wall-clock denominator is unknown. Wrong: the game prints its own clock,
and flight-speed-law.md had already used it -- mission TIME across a wall interval
gives 1.26x, turning 443.6 units/wall-second into 352 per game-second against a
HUD 350. Withdrawn in place, with the reasoning, rather than deleted.
Closes the backlog item: one world unit is one metre, by two independent routes.
The HUD is reachable and the control is paired; what blocks the measurement is
narrower and now named -- two tool defects (entities2.py's VA window covering the
definitions rather than the instances, gworld.py's 0x600 instance window) plus the
missing piece itself, a locked target whose HUD range and position can be read in
the same second. Also records the speed shortcut as refuted so it is not retried.
The entry blamed a stale committed VA window for "0 unit definitions". Measured
2026-08-26: the scan works and returns 13 definitions + 42 named live instances
once the run reaches the mission via LOAD GAME -> slot 01 rather than via MISSION
SELECT. What still blocks the measurement itself is narrower and now stated: the
flight HUD was not up (green 0.03% vs 1.3-1.5%), so there was no distance readout
to compare positions against.
Every Xam UI dialog goes through xeXamDispatchDialog, and the calling guest
thread blocks on fence.Wait() until it is dismissed. With kernel logging on the
last call before the freeze is XamShowSigninUI(00000001, 00000001): the game
asks for a signed-in profile, Xenia opens a modal dialog, and nothing in a
scripted run ever dismisses an ImGui dialog.
That accounts for every symptom at once -- Main XThread futex-blocked at 0 ms CPU
rather than spinning, emulator alive, no guest progress, no faults, and
independence from both the GPU backend and the allocation outcome.
Fix: run-canary --logged_profile_slot_0_xuid=B13EBABEBABEBABE. Same route, one
variable changed:
no profile with profile
screen id 4, forever 4 -> 5 -> 6 -> 8 -> 9 -> 10
XamShowSigninUI called not called
alloc failures 1 0
guest throws 1 0
guest churn 0.000% 1.006%
XamShowDeviceSelectorUI is then requested but storage_selection_dialog defaults
to false, so it is not shown and does not block.
Why it took so long: --log_mask=13, used by every script in this corpus,
DISABLES kernel logging -- the one category that names the dialog. Without it
the freeze presents as a bare futex block, which is why it was attributed in
turn to a heap leak, rounding, MmQueryStatistics, a build regression, the route,
the savegame, shader compilation, an infinite guest loop and the software
rasterizer, each refuted in its own section.
Nearest miss worth recording: challenge-mission-gate.md reported a "Disc Read
Error" dialog on this failure. That is XamShowDirtyDiscErrorUI, which calls
exit(1) -- "This is death, and should never return" -- so it cannot be our
freeze, whose process stays alive. Checking that distinction is what led to the
dialog mechanism.
Open: no mission reached yet (DEF_VTABLE/INST_VTABLE still 0 at screen 10), so
the remaining screens are menus. Ordinary navigation, not a blocker.
Driving the null backend blind, using the memory screen-id as the guide, to the
SAME screen the rendered run froze on (screen 4):
guest churn 19 of 8,089,600 bytes in 4 s (0.000%)
Main XThread 0 ms CPU / 4 s, state S, wchan futex_do_wait
log AllocRange unable ... 134217728 bytes; guest throw
Frozen identically with no rasterizer at all.
The flaw is the comparison, not the data. The earlier "control" measured the
null-GPU run while it was still in the MENUS and the lavapipe run AT the freeze,
then reported the difference as a backend effect. The allocations I cited as
proof it had walked through the content load (114 -> 120) were menu-time
allocations. A control needs both arms verified to be in the same state -- and
the memory screen-id built this same session is exactly what makes that
checkable, which I failed to use on the arm that mattered.
Withdrawn: "the freeze requires the rasterizer", "the fault is in the host
rendering path", and the hardware-Vulkan blocker that followed from it. The
rendering question is no longer load-bearing.
Survives: the freeze happens on the first content load after the main menu, on
every route and BOTH GPU backends, with Main XThread futex-blocked at zero CPU
rather than spinning; and four runs froze with three different allocation
outcomes, so the 128 MB refusal is correlated but not necessary.
Also survives and is independently verified: the memory screen-id navigation,
now reproduced on a third run. It is what exposed this error.
Open again: what is Main XThread waiting on? Identify the futex and its holder,
carefully -- gdb perturbs this measurement.
The cleanest control available, and it settles which side the bug is on. Xenia
has a null graphics backend. Same ISO, same route, same presses:
lavapipe --gpu=null
Main XThread CPU 0 ms / 3 s 3860 ms / 4 s
state / wchan S / futex_do_wait R / not blocked
guest memory churn ~0 (18 B of 8.2 MB) 22.6% of 6 MB in 4 s
allocations stall 114 -> 120, no failures
process CPU 265% (all llvmpipe) 330% (mostly guest)
With no rasterizer the guest does not freeze: it runs at close to a full core and
walks through the content load -- the exact point that hangs every rendered run.
So the fault is in the host rendering path, which explains why every game-side
hypothesis on that page was refuted in turn.
Blocked, and recorded as such rather than improvised around: deciding whether
lavapipe is deadlocked or merely taking hours over one pathological draw needs
hardware Vulkan, which this container does not have. Six minutes of nine
saturated llvmpipe threads with no frame is suggestive, not conclusive.
Amber workaround: --gpu=null gives a live guest, and everything the backlog needs
-- entity positions, flight model, world unit -- is read from guest memory, not
pixels. The gap is navigation: menu-walking is screenshot-driven, and blind A
presses advanced allocations 114 -> 120 but never reached a mission (DEF_VTABLE
and INST_VTABLE scans both 0). Next step is to drive navigation from guest
memory instead of the screen, which is engineering rather than mystery.
challenge-mission-gate.md §5.6 attributes the 128 MB heap failure to a careless
cleared-stage mask poke, concludes that poking only real story ids does not blow
the heap, and ends by asking for the control: repeat without the poke.
Ran it. nav_to_flight.sh gains SYLPH_NO_POKE=1, which skips the write; only
Stage 1 is selectable without it, so the control changes stage too, which makes
the agreement stronger rather than weaker.
poked 0x0001FFFE Stage 02 frozen, 128 MB request refused
control untouched Stage 01 frozen, 128 MB request refused
Both logs carry not merely the same error but the same numbers:
requested 134217728 bytes, parent free 28969/131072 pages
28969 in both, across two stages and two boots. So the poke does not cause it
and neither does the stage; the guest reproducibly arrives at a 128 MB request
with ~113 MB free. An identical free-page count across independent runs also
says the allocation pattern is deterministic -- not a race, not host pressure.
The control was verified three ways, because the first attempt was confounded:
two emulators were alive at once (the previous one survived a pkill). The mask
was read back as 0x0 from the live mapping, the log was confirmed to be this
run's, and the liveness test was repeated after killing the stale process so
exactly one emulator was running -- three frames at rmse 0.00, and an 8 MB slab
of guest RAM with 0 bytes changed over 3 s.
Spent a session getting to flight. Each obstacle presents as "the emulator
died" and none of them is.
* --audio prevents boot. run-canary's header already says the flag is not a
cvar in this tree and that an unknown argument blocks in a message box
before logging starts. Measured anyway, because the corpus also holds runs
that passed it and booted: 3 trials each in BOTH orders, 67 565 bytes of log
without the flag and 209 with -- and 209 is run-canary's own banner, not one
line from xenia. Order was reversed on purpose; this corpus has a standing
lesson that an A/B from run order is noise. Eight scripts on branch
auto/idxd-unnamed-keys still pass it; main and this branch are clean, which
reconciles August's successes with today's failures.
* launch_mission.sh's skip_intro deadlocks. It calls the attract loop a
"movie" and refuses to tap, and waited out 600 s of unbroken movie verdicts
before timing out. nav_to_flight.sh, against the same running emulator,
reached the main menu in 12 s and flight in 2 min 20 s by tapping A at the
title. The "wait it out" premise is wrong: the loop does not end.
* "EMULATOR GONE at ~40 s" is this project's own Stop hook killing xenia when
a Claude turn ends. That is recorded further down this same file and I
rediscovered it over three boots because I did not look. Sequential tool
calls within one turn are fine; ending the turn is what kills it.
The world unit is still unmeasured. Flight was reached and the screen
classifier agrees, but entities2.py finds 0 unit definitions -- its committed VA
window does not match this run, the same run-dependent-address problem this file
documents for the OB counter. Next attempt must hunt the range.
An audit of BACKLOG.md turned up a class of error with a single root cause: the
README defines only the CONFIRMED/PROBABLE/HYPOTHESIS confidence scale, while
the pages actually use a second vocabulary -- and 🔴 appears 98 times without
ever being defined. It gets used for two different things, "refuted" and
"blocked", and three entries slid from one into the other.
README now defines ✅/🟡/❔/🔴/❌/🚧 and states the rule the corpus was missing:
🔴 never means "we have not run it yet". That is ❔ or 🚧. Its blocked sense is
only for a real limit of the box -- no push credentials, no hardware Vulkan, a
decision only the user can make -- and since the box can run the emulator,
script input, screenshot and read guest memory, "needs a run" is never blocked.
I made exactly this mistake on the world-unit item earlier today, which is what
prompted looking for others.
Fixed in BACKLOG.md:
* the elimination test, marked 🔴 UNRUN and in fact run and refuted nine
lines further down;
* the frozen capture, marked 🔴 STILL UNRUN and in fact taken eleven lines
down -- 🔴 wrong twice, since "the freeze did not happen this run" is a
scheduling outcome and not a refutation;
* a 🚧 STILL UNRUN item whose stated blocker (the boot-nav bug) is fixed;
* the objective-counter heading, which asserts 0xbdb59668 as the answer while
its own first body line refutes that address -- retitled to say what is
actually solved, the method;
* the paint-order "third measured permutation" question, answered inside its
own entry by a third, fourth and fifth screen;
* the UTF-16 endianness question -- resolved, and it is not a stale comment:
localization.rs both documents LE and decodes with u16::from_le_bytes, so
it is a code bug worth filing.
Also fixes the corpus's only dangling link (INDEX.md pointed at
structures/idxd-unnamed-keys.md, never written).
I wrote "blocked on the oracle" for the unit-to-metre conversion. That was a
mislabel: red is for what the container cannot do, and run-canary works here.
What the km-name sweep actually established is narrower -- no STATIC test can
settle it, because the disc has exactly one size-bearing asset name.
The run is well-supported by tooling that already exists: findplayer.py
recovers the player position triple from motion, the HUD prints the distance to
the selected target in the game own units, and the same separation read both
ways is the conversion. Recorded as amber with the experiment written out.
The census filtered pak entries whose own first four bytes are T8aD. A sprite
is usually a child of a RATC bundle, and a bundle entry's magic is RATC, so a
top-level magic filter cannot see one:
top-level T8aD entries (counted) 4 525 sprites, 45 keys
T8aD inside RATC bundles (missed) 16 659 sprites, 204 keys
both 21 184 sprites, 216 keys
171 of the 216 keys exist only inside bundles. The sharpest statement of the
error: that census never saw GP_TITLE.pak at all -- the pak holding both of the
screens this page's entire evidence comes from.
Retracted: "45 values", "the keys are pak-local", "each auxiliary pak occupies
its own narrow high-byte band". On the full population 68/216 keys (31%, not
9%) cross a pak family and the per-pak ranges overlap heavily -- GP_BUNK
0x8000-0xa110, GP_TITLE 0x8000-0xc150, GP_LEADERBOARD 0x8000-0xf100. The tidy
banding was an artifact of seeing one or two keys per pak. So the key looks
like a shared vocabulary, which is the opposite of what I published.
Survives, now on the full population: the field is a u16 at +0x0A (upper half
zero 21 184/21 184), and it is an enumeration (216 values for 21 184 sprites).
Three wrong numbers on this page now, all the same shape -- a statistic computed
over a population I had not checked was the population in question. Stated once
at the end of the section rather than three times: check the sampling frame
before the statistic.
The page rested on twelve values from two screens. This walks all 4525 sprites
on the disc.
* The field is a u16 at +0x0A. The upper half of the 32-bit word the page
reads is zero in 4525/4525. Nothing above changes -- 0x00008100 sorts the
same as 0x8100 -- but a future value with the high half set would mean
something had been misread rather than that the layer got deeper.
* It is an enumeration: 45 values for 4525 sprites, one of which (0x8100)
covers 1188 of them.
* The reading worth trying -- a global layer vocabulary shared across the UI
-- is refuted. Only 4 of 45 keys cross a pak family and 33 of 45 live only
in GP_MAIN_GAME_2D; every other pak owns a narrow high-byte band (0x90-0x94
for the in-game overlays, 0xa4 mission log, 0xb1-0xb2 save/load). A screen
that owns one or two keys is not ordering itself with them.
That supports "group id in the high bits, order in the low bits", which is what
the page already suspected, but it does NOT test it: paint order has been
measured on two screens and both are inside GP_MAIN_GAME_2D, so there is no
ground truth to check the split against. Left amber.
The first number I got was 37/45 shared, which would have supported precisely
the wrong conclusion. It came from counting paks instead of pak families: the
six GP_MAIN_GAME_*2D paks are the same screens in six languages and their key
sets are byte-for-byte identical. Recorded on the page, because the shape
recurs -- a corpus with near-duplicate members manufactures agreement.
Two follow-ups on yesterday's^Wthis morning's CollisionSet write-up.
1. The _cmesh <-> render-model link, which I recorded as UNTESTED because
matching stems against .xbg object names covered 4 of 158. The disc keeps
only one build manifest, so that corpus was never going to answer it. The
right corpus is the GameResourceID field of the DefTables / GP_MAIN_GAME
records -- 480 distinct values. Against those, with a control that shuffles
the characters of each stem:
ship/mob stems prefixed by a real resource id 108/112 = 96.4%
same stems, characters shuffled (control) 0/112 = 0.0%
asteroid stems prefixed (expected none) 0/46
So a CollisionSet entry is <GameResourceID>[_<part>]_cmesh. The 0/46 on
asteroids matters as much as the 108/112: a test that fired on everything
would be the bound-check hazard again.
2. The world unit. Sweeping every pak for a name carrying a kilometre figure
returns mapmesh_box_500km.col/.rgn and nothing else -- 162 references, all to
that one pair. The reading rests on a single filename with no corroborating
instance anywhere in the data, so no static test can settle it; marking it
blocked on the oracle rather than leaving it as an open static question.
My objection's premise did survive: rou_e010 is a real GameResourceID and
e010_ADAN_Attacker_S is in the stage tables, so the 133-unit mesh does belong
to a craft the game calls an attacker. Whether the trailing _S means "small"
is a further guess (there are _EX4 / _HF / _HF_Wayne variants), so it stays
suggestive rather than evidence.
All 18 blobs are byte-identical: the per-stage naming is nominal, and every
stage points at one shared 1675148-byte library stored eighteen times. That
identical size was the reason to open the item, and it turned out to be the
answer to it.
Record layout: {u32 size, u32 name_len, char name[name_len], u32 nv, u32 nt,
f32[3] x nv, u32[3] x nt}, next record at off + 8 + size. The indices are u32
here where MCOL uses u16 -- two different serialisers in one archive.
What makes this a decode rather than a plausible reading: the walk consumes the
file to the byte over 158 variable-length records, with the size word predicted
from the two counts 158/158. A wrong field would desynchronise within a few
records and could not land exactly on the end. All indices in range 158/158;
98.24% of edges shared by exactly two triangles; 147/158 fully manifold.
158 meshes, 90 836 triangles: per-part ship proxies (_bdy/_brg/_eng/_wep/_sld,
the XBG7 sub-part vocabulary) plus 46 stage asteroid meshes whose prefixes are
exactly the stages that have an _AsteroidVolume_wp MCOL.
Two things this file makes me walk back:
* The "1 unit = 1 metre" reading from mapmesh_box_500km is downgraded to
amber. The 500000 arithmetic stands, but it implies that a craft the game's
own tables call "small" is 133 m and that rob_f002 is 447 km -- 89% of the
arena width. The format check survives; the interpretation has no
independent support.
* The _cmesh <-> render-model name link is recorded as UNTESTED, not
confirmed: only one .xbg build manifest survives on the disc, so matching
stems against object names covers 4 of 158, which is no coverage at all.
The 0x50 header word, which the first section of this page had dismissed as "a
large value", is two u16 counts: vertices and triangles. They give the two
remaining blocks their stride, and every derived length is exact in 11/11 --
len(0x54) == align16(12*nv), len(0x58) == align16(6*nt), and nt equals the
bounding-sphere count decoded last iteration.
Checks that cannot pass by accident:
* sphere i is the TIGHT bounding sphere of triangle i, 4768/4768, with
max|v-c|/r median 0.99990 (a fixed 1.0001 epsilon), against a 1.32%
random-triangle control;
* the mesh is watertight -- every edge shared by exactly two triangles,
7152/7152, zero degenerate triangles, zero unreferenced vertices;
* the two smallest objects are 8 vertices and 12 triangles whose positions
are the eight +-250000 corners of the map bbox: a bare bounding cube.
The cell lists are a correct broad phase: with an exact triangle/box SAT test
only 3 overlapping triangles in 18 577 entries are absent, so a query walking
one cell's list cannot miss a hit. The 730 conservative extras bracket the
builder's own test between exact-SAT and AABB, which retires the 18 unexplained
"sphere misses" from the previous commit as that same margin.
mcol_probe.py gains `mesh` and `obj`; `verify` now runs all three checks and its
output is recorded in docs/re/data/mcol-verify.txt.
The unexplained ~0.75 ratio left at the end of the last iteration was my own
stride. I had read the block as 12-byte points because REGN's vertex section
is 12 bytes, and never checked it: len(0x5C) is not a multiple of 12 in 5 of
the 11 objects, so that stride was never arithmetically possible.
At stride 16 the relation is exact in 11/11 -- max u16 == len(0x5C)/16 - 1 --
and the record reads as {centre f32[3], radius f32}. Powered test, since a
u16 is reached through a specific grid cell: the sphere it names reaches that
cell in 18 559/18 577 = 99.90%, against a 12.02% random-sphere control. Both
fields carry signal (centre alone 26.75%, radius shuffled 70.19%).
The converse -- is the list *exactly* the intersecting set? -- is 0.38%, which
is the expected direction: a bounding sphere is conservative, so membership
implies overlap but not the reverse. The tighter geometry is in 0x54/0x58,
still undecoded. 18 entries (0.10%) go the wrong way and are recorded as open.
tools/re-capture/regn_decode.py is copied unchanged from auto/regn-reader so
the probe's POF0 reader is the known-good one rather than a second copy.
The original reason for investigating REGN was that a mission's enemy count
rises and falls, so a scheduler with parameters must exist somewhere, and a
per-map uniform grid is what such a thing would be indexed by.
Now that it is decoded that reasoning is answered: REGN is a tetrahedral
navigation mesh -- vertices, faces carrying plane equations and adjacency,
tetrahedra with portal costs between face pairs, and a grid indexing which tets
fall in each cell. Every section is accounted for by that structure, and there is
no time field, no unit reference and no trigger anywhere in it.
So the wave-scheduler search should treat REGN as excluded rather than unread.
The page's original hedge was right to keep the reading provisional, but the
reasoning it hedged was a guess from shape, and the shape belonged to
pathfinding -- which is what pointed the whole investigation here.
The arrival timetable in Route_S<NN>.tbl, keyframed per squadron per phase with
t in seconds, remains the only located part of the mechanism.
I recorded the static coupling search as exhausted and needing PE code. The PE
work was done on auto/regn-reader and it solved the whole thing: REGN is a
tetrahedral navigation mesh, reached via the POF0 fixup table -- the loader's own
list of which words are pointers, so nothing needed guessing. Six sections;
position -> cell -> 32-byte item -> tet refs -> tetrahedron, with section 2 a
face carrying a plane plus its 3 vertices and the two tets either side.
Controlled checks: face through 3 of 4 tet vertices 253722/253722 against a
0.07-2.2% random control; portal cost equals face-centroid distance
380460/380460.
Also records against myself that the base is chunk+0x10 and my offsets here were
16 bytes early, that the plane arithmetic survives only because those fields
landed on the same bytes, and that my points-in-bbox count was never evidence --
a shift inside a homogeneous f32 array yields other floats from the same array.
Leaving the wrong conclusion in the backlog would have told the next reader the
avenue was closed when it was the one that worked.
The previous test only tried section-1 targets. Closing that gap: the payload's
three index-shaped u32s, followed into the point list and the plane list and
checked for the target lying inside the referencing cell, all sit at the 0.203%
random control.
Two cells read 0.81%, 4x the baseline. I am not treating that as a lead: across
this and the previous iteration roughly twenty such tests have been run, and at
that count a single 4x enrichment on ~8000 trials is what noise looks like.
Calling it a signal would be the multiple-comparisons error a long hypothesis
sweep invites.
So REGN's header, grid, points, planes and cell index are decoded, section 1's
slot regions are censused, and the link between the grid and the geometry is not
reachable by any static test I can construct. The honest next step is the PE
code that reads a REGN object -- the same kind of work that cracked the .slb
packing phase -- rather than a twenty-first correlation.
The three sections recorded as undecoded are fixed-stride arrays and counts[0..2]
are their record counts: 12, 96 and 48 bytes. Section 1's remainder is exactly 0
in 11/11 objects and section 2's exactly 96 in 11/11, which is what makes these
strides rather than a coincidence of division.
Section 0 is a point list: 13467 of 13467 records lie inside their object's own
header bounding box.
Section 2 is a plane list, 12 f32: four zeros, a unit normal (|n|=1 in
133573/133573), a signed distance, a point inside the bbox (133573/133573), and
a trailing 1.0 (133573/133573). The decisive check is algebraic -- n.p + d must
vanish for a real plane, and over all 133573 records the relative residual has a
median of 2.29e-08 and a maximum of 2.15e-07. That is float round-off, not a fit.
So a REGN object carries a point list and a plane list beside its uniform grid,
which fits collision or region-boundary geometry and sits next to MCOL.
Still open: section 1 (96 B, 60631 records), what queries the planes, the zeros
at [0..3], and the constant 96-byte tail.
The u16 at +0x10 is 0 in all 2985 bundles; the content is a 16-bit flag word at
+0x12 with 83 distinct values. Reading it as a u32 inflates the field and hides
that the header is built from u16 pairs -- the same shape +0x0c turned out to
have. All 16 bits are used, from 1.4% to 91.5%.
Cross-tabulated every bit against four properties measurable from the bundle:
multi-element, animated, window-starts-at-zero, 30fps. No bit is close to a
clean predicate. The strongest is bit 10 against window-at-zero, 0.79 vs 0.21 --
a real association but not a rule, and exactly the kind of moderate split that
invites over-reading.
Bit meanings stay open, but four candidate readings are excluded rather than
untried and the field is correctly sized. Every property visible in the file has
now been tried, so assigning meanings likely needs the game observed with
individual bundles loaded.
Tested the alternative I recorded last iteration. Parsing every bundle's
keyframe times (2985/2985 parse), the derived-summary reading -- (high,low) ==
(min,max) keyframe time -- holds in 6 of 2985 (0.2%).
The apparent 34.2% match on 'high == min' is a coincidence of zeros: the minimum
keyframe time is 0 in 96% of bundles and high is 0 in 34.9%, so the 1022
'matches' are exactly the both-zero cases. Worth noting that last iteration I
declined to treat the high==0 share as support -- it turned out to be the
confound rather than the signal.
The interval is also narrow: (low-high)/(max-min) has a median of 0.019, about
2% of the keyframe span. It lies inside the keyframe range in 88.6%, entirely
after in 174 and entirely before in 68.
A short authored window is not the shape of a playback range or a whole-animation
loop region, so those readings weaken too. What it is stays open.
Read as a u32 it looks meaningless -- 179 distinct values up to 248581842. The
raw values give it away (0x0007000F, 0x000F001A, 0x003C0064): two big-endian
u16s. Over all 2985 bundles, high < low in 2985/2985 with no equal and no
inverted cases, and both are bounded by the animation length at +0x08. Span
runs 1-1200, clustering on 1/10/30/8/20; low equals the animation length in 4%.
A strict ordering holding 2985 times rules out flags or a packed count. Which
interval it is stays yellow -- playback range, loop region and active window all
fit equally.
Recorded the alternative I did NOT test: that (high, low) is simply the min and
max keyframe time, making it a derived summary rather than an authored range.
First step written down. The 34.9% of bundles with high == 0 leans against it
but is not evidence on its own.
The declared sizes are honest (seek magic at data_at + declared_size, 7620/7620)
and VOICE_TCAF_608 was stereo decoded as mono, not truncated. The four offsets
are a segment-packing phase, not a per-directory header size. Neither was closed
by finding something new; both were closed by correcting a mistake of mine.
Built-in 12 activate_unit returns 0 when the live object is NULL: it registers an
object that already exists and cannot create one. So a wave arrival is a craft
reaching a point on its route, not a new record, and a count of entity records
was never going to move.
Stage 02's UnitGroup totals Count = 116 across 111 squadrons, matching the live
probe's flat 116. The natural objection is that 116 might match by accident --
it does not: across all 28 stages the roster totals run 2..116, and 116 is the
MAXIMUM and unique to Stage 02, with nothing else within 5.
Three independent things agree: the handler cannot spawn, the roster totals 116,
and the live count sits at 116 while 22 of 113 routed squadrons have a route
starting after t=0.
Still n=1 for the per-member identity (only a Stage 02 save exists), and this
cannot settle timetable-vs-event -- that needs a position probe, not a count.
Route keyframes are (time, quat x4, pos x3), so each consecutive pair gives an
implied speed. The flight-speed law was measured live against a real-time clock
(cruise ~420, max ~1530 world units/s), which makes it an independent yardstick
in known units.
Across all 28 stage tables, 331 Route_* records and 1104 keyframe pairs: under
SECONDS not one pair exceeds the 1530 ceiling (median 147); under frames at 30
or 60 fps, 89% and 94% of the game's routes would demand impossible speeds.
Two corrections recorded. A first pass silently skipped non-numeric fields while
flattening records, shifting every later value -- it reported a median implied
speed of 65534, suspiciously 2^16, which is what exposed it. And the remaining
outliers were Frame_S<NN>_Asteroid records, not routes; the final measurement
filters by record kind rather than dropping whole stages.
The ties needed a different signal, not a longer scan. Banks carry one: a seek
chunk sitting on a packet boundary, so seek_pos % 2048 IS the data offset. On
the 6033 labelled banks with a seek before their first RIFF, 6031 agree
(99.97%) -- better than the packet scan and structural rather than statistical,
so scan_data_offset now tries it first.
On the scan's 28 ties it resolves 26 correctly and 0 wrongly (2 have no usable
seek). Combined rule scores 7354/7358 = 99.95%, up from 99.62%. 762 of the 1495
RIFF-less banks carry a seek, so the signal exists where it is needed.
Also ruled out, since a wrong offset was this page's whole subject: the header
is not audio being discarded. Adding 0 to the candidate set, it wins 6 of 7358.
7 disc tests pass.
16 of the 17 pages with an empty slot 4 belong to MSG_DEMO_600..604, whose
caption text matches, word for word, the five resupply lines documented from the
movie side as VOICE_D_450..454. Slot 4 is empty on purpose: that audio binds
through the movie path, not through a DEMO_nnn cue.
That also disposes of the '5 ids with more than one record' item -- each appears
once per stage that uses it, and the repeat counts equal the movie-slot counts
recorded independently for the matching banks: 3/3/4/4/2 both ways, 5 of 5. The
11 extra records over 138 ids are these repeats.
Worth more than closing two items: the cutscene message table and the movie
subtitle chain were decoded from different containers by different routes and
agree on the same five lines with the same multiplicities.
MSG_DEMO_500 remains: one page, KATANA, 2s, no text and no cue.
Of the 7586 banks with a RIFF and a data chunk after it, 5296 declare a data
size larger than the pak entry holds; 2290 declare less (the ordinary
multi-sub-wave case); NONE declare exactly what they hold. This contradicts the
decoder comment claiming the declared size 'is honest per sub-wave'. The code
clamps, so it is a documentation defect, not a crash.
It also closes the loose end from the offset work: eng\Voice\VOICE_TCAF_608,
the single bank where neither offset decoded, is 99% short -- there is nothing
there to decode.
Method note recorded: my first pass searched for 'data' from offset 0, which can
match by chance inside the leading audio region. Anchoring the search after the
first RIFF moved the count 5038 -> 5296.
Separately, the 55 'early RIFF' English banks are not an anomaly: all 55 sit at
exactly 1392 behind a zero-filled header -- a zero-length leading region, which
both the old and new code already handle correctly.
static.slb (8970240 bytes, the shared SE bank) and Pj_Silph.xgs (533 bytes, XACT
global settings) both hash into the TOC. Their names come from the BANK_SE and
SETTINGS records of the very IDXD object this page documents -- I had printed
them at the top of the write-up and then reported the entries as unidentified.
9519 of 9519 now: 5100 jpn + 4382 eng + 35 root + these 2.
My sound-cue write-up said the table 'does not explain' the VOICE_D_452
rejection and implied it was still open. It is not: voice-bank-leading-region.md
settled it earlier -- the bank holds one generic line reused across four stages,
so the binding is correct and the expectation was wrong. The narrower true
statement, which is what the cue table actually adds, is that the reuse is a
movie-slot -> bank relation and not a cue-level one.
to_xma_riffs now emits the leading headerless segment when it sits at a whole
number of XMA1 packets and carries a non-zero byte. VOICE_D_453 goes from a
0.14 s trailing fragment to a 45116-byte leading sub-wave that dominates it.
I withdrew this exact change earlier for two reasons. Both are now answered
rather than argued away:
* "It recovers no audio" -- it used the STEREO format. At two channels every
bank yields exactly 1792 bytes, one frame, whatever its size. Mono yields up
to 113x more.
* "It matches 1524 of 8021 RIFF-bearing entries" -- the byte-level reach is
still 1524, but the audible reach is not. Across the 84 movie-bound banks
the segment adds >1 s to exactly 7, the hokyu_*_H tankers on D_453/D_454 --
precisely the broken ones -- and <=0.25 s to 66 of the rest. The largest
non-resupply addition is S04A at +0.66 s on a 256 s movie.
The safety oracle is recorded with its limits: 8 of the 84 banks ALREADY
exceed their movie's duration before the change, by hundredths of a second,
so it cannot resolve differences at that scale. It establishes scoping, not
correctness. Callers clamp to the movie length regardless.
VOICE_D_451's all-zero leading region is skipped by the non-zero guard, so
the rule cannot prepend silence to a bank that does not need it. Pinned, as
is the packet arithmetic (n = 8, 1, 7, 22, 29) which has no tunable.
slb_disc, movie_subtitle_disc and movie_manifest_disc all still pass.
NOT verified by ear -- that needs a human.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
The last open question was whether the leading XMA1-mono region duplicates the
RIFF sub-wave, which would make the earlier totals double-count. It does not.
Decoding both parts of every bank to mono PCM and measuring energy:
bank leading secs / RMS riff secs / RMS
VOICE_D_450 0.49 / 158 2.82 / 9898
VOICE_D_451 0.01 / 0 1.58 / 9128
VOICE_D_452 0.31 / 301 2.18 / 9061
VOICE_D_453 2.12 / 9770 0.14 / 14462
VOICE_D_454 3.07 / 10428 0.43 / 11639
Two shapes, and no bank holds the same content twice. In 450/451/452 the
leading region is silence or near-silence (RMS 0-301 against ~9000 for
speech) and the RIFF holds the line. In 453/454 the leading region holds the
line and the RIFF is a short loud tail fragment. Sequential segments of one
clip, so the totals stand and with them the 48 kHz fit.
This also closes the mystery that started the whole thread. The corpus
recorded 450 = 2.8 s, 451 = 1.6 s, 452 = 2.2 s as plausible but 453 = 0.14 s
and 454 = 0.43 s as "far too short". The decoder skips everything before the
first RIFF: for the first three that discards only silence, so they looked
fine; for the last two it discards the line itself and leaves the trailing
fragment. One rule, two outcomes, depending on which segment holds the
speech.
The fix is now well-posed in a way the withdrawn attempt was not: emit the
leading region only when it carries signal. That also avoids the 1524-bank
blast radius that sank the earlier version.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
The load-bearing error of the whole voice-bank thread, and it is mine. It
stood for three iterations across two write-ups that each called the result
proven.
I treated a subtitle cue as a timestamp that must fall INSIDE the voice clip,
and concluded a 0.07 s clip could not host a cue at 4.70 s. A cue is when the
line STARTS. The voice plays from the cue, so the clip only has to fit the
window between the cue and the end of the movie. Under that reading every
bank fits at plain 48 kHz:
bank samples @48kHz cue window
D_450 158967 3.31 4.00 5.30
D_451 76084 1.59 3.70 5.60
D_452 119562 2.49 0.00 8.34
D_453 108608 2.26 4.70 4.60
D_454 167828 3.50 0.00 9.50
2-3.5 s is also the right length for the lines. Nothing is missing, and the
17091-20563 Hz window from the previous commit is void with it -- its lower
bound came from the same misreading.
What survives, because it was measured rather than interpreted: the leading
region is XMA1 mono, the decode runs to the final frame, and cue values are
seconds.
Separately settled, and it is what exposed the error: each shared bank holds
ONE generic line. The 3-5 movies bound to a bank have IDENTICAL subtitle
text, 5 banks out of 5 -- "Rhino 3 has landed. Commencing resupply.",
"Resupply complete. You are cleared for take-off!", and so on.
That also explains the historical in-game rejection of hokyu_DS_s13A ->
VOICE_D_452 that started this whole thread. The line is generic, identical
for s02A/s07A/s08A/s13A. Someone expecting a stage-13-specific line would
hear the generic one and call it wrong -- while the binding is exactly right.
The disc said so; the subtitle text now says so independently.
Still open: whether the leading mono region is additional audio or an
alternate take, since the totals above add it to the RIFF sub-waves.
Artifact: examples/shared_bank_takes.rs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE