Commit Graph

209 Commits

Author SHA1 Message Date
Sylpheed RE agent
6c9e2ca44d re: the trigger-queue appender is built-in 25 -- the script appends, not the engine
isl-builtins.md carried the trigger queue at phase+272 a long way -- layout, live
count at +272+20, the generic appender sub_8226EAB8 -- and stopped at "what actually
appends a node is still unidentified", proposing a gdb watchpoint on a running
emulator.  It is a static question after all.

sub_8226EAB8 is a shared helper with 16 callers, so it cannot be identified by name.
But the corpus records that the trigger container EMBEDS its inner list at +12, so the
appender must hand it container+12.  Only two of the sixteen callers do, and one sets
its base 34 instructions earlier:

  8226A22C  addi r29, r24, 272     ; r24 = the ScriptPhase
  8226A2B4  addi r3,  r29, 12      ; -> phase + 272 + 12
            bl   0x8226EAB8

phase+272+12 is exactly the inner list whose count the corpus watched at +272+20.

sub_8226A0D8 is ScriptPhase vtable SLOT 28, which is what BUILT-IN 25's stub
tail-calls, and there are ZERO real `bl 0x8226A0D8` instructions in the image -- it is
reached only through the vtable.  So the queue is populated by the SCRIPT, retiring
the residual "the engine moves records into the phase's queue each frame" reading that
isl-builtins.md had already flagged as unsupported by its own call site.  Built-in 25's
arguments agree with two independently measured tables: UNIT_ARG (a unit at local[4])
and SYM1_SLOTS[12] (a symtab-1 name at local[12]).

REFUTED in the same pass, with a control: built-in 25 does NOT carry the coroutine
entry that a trigger fires, which would have seeded the 389 routines reachable from no
static entry.  Taking each staged operand as a code offset from the phase base, 0 of 8
land on an instruction boundary, against 38.7% for an arbitrary 4-aligned offset --
below chance, not above it.  And the scale is wrong regardless: built-in 25 has only
2 call sites in Stage 02.

So what starts the unreachable routines is still open, and it is not this.  A trigger
node's own layout and its consumer are unread, and built-in 25 stays unnamed.
2026-08-27 06:42:47 +00:00
Sylpheed RE agent
6cbfe0524f re: the sufficient side -- each phase exit now names the condition that FIRES it
Dominance said a phase cannot end unless X.  A port also needs "once X holds, it
must end", and that is a must-reach set: nodes from which END_PHASE is unavoidable,
as a least fixpoint where n qualifies when it has successors and ALL of them qualify.

The conservatism is deliberate and is the honest answer: a loop never enters the set,
because a poll loop reaches its exit only if the polled predicate eventually becomes
true, which is a liveness property rather than a graph one.

A dominating condition is a TRIGGER when the successor it takes on being satisfied
lies in that set.  Over all 28 stages: 732 dominating conditions, 234 triggers
(31.97%).  isl_report.py phase-guards now tags every line precond / TRIGGER.

The split lands where it should.  Stage 02's phase-1 objective exit is six
preconditions -- player alive, TCN004 destroyed, t <= 210, ADT102/ADT107/ADT113
destroyed -- and exactly ONE trigger: hp_pct_test(ADN101, 0) != 1.  Destroying ADN101
is what fires the phase.  That is a sentence a port can implement.

Per-exit distribution over 172 reachable exits: 89 have exactly one trigger, 42 have
none, 41 have several.  The 42 with none are not a failure -- they are the exits no
branch fires; Stage 02's 0x006260 ends on read_freg(0) < 1200, a timeout, and time
passing is not a property of the graph, so declining to call it a trigger is correct.

Recorded as a heuristic rather than a rule: "the first trigger is the point of no
return" holds for 33 of the 41 multi-trigger exits, with 8 counterexamples where a
precondition appears after a trigger.  The likely cause is that the listing is
ordered by file offset, which is not execution order -- coroutines and jumps let a
lower offset run later.  Not asserted.

calls, phase-ends and conditions all regenerate byte-identical; the two phase-guards
artefacts change only by gaining the tags.
2026-08-27 06:34:39 +00:00
Sylpheed RE agent
6c892b5e25 re: read the three built-ins that appear inside clear conditions -- none named
isl-phase-guards produced the per-phase clear conditions, but three of the built-ins
in them were unread, leaving the conditions half-readable.  All three are read now
via the vtable at 0x820A84BC (control: 69 unit_state -> slot 184 -> 0x8226ADF0).
NONE is named: the corpus has withdrawn two names taken from usage shape, and what I
read does not support naming these.

builtin104 (slot 288, 0x8226BFE0) is a THREE-INSTRUCTION PURE GETTER:
    lwz r11, 10160(r3) ; stw r11, 164(r3) ; blr
So all six tutorial stages S18-S23 end on the value of a single engine-written word,
which is why their exits each have exactly one dominating condition and why
isl-builtins.md only ever saw 104 inside a poll loop.  That word has EXACTLY ONE
writer in the image -- sub_821AA1B0 at 0x821AAD9C, gated on a kind field being in
(16, 32] -- and r29 there is the return of a preceding call, so the meaning of the
value and of the gate are both unestablished.

builtin7 (slot 40) indexes [phase+324] by local[4], bails when rec+16 (the documented
unit state) is 1, 3 or 4, and resolves local[12] through [phase+244] -- symbol table 1.
isl.py's SYM1_SLOTS already listed slot 12 for built-in 7, derived from operand ranges
alone; reading the implementation shows the mechanism behind that.  Two independent
methods agreeing.  What it asks ABOUT the route is still unread.

builtin141 (slot 428) has the same unit-array entry and returns 0 when the unit is
gone; everything past 0x8226CA0C is unread.  Stage 16 calls it twice with arguments
differing in one position (0 vs -4000), which looks like a coordinate -- and looking
like one is exactly the evidence this corpus does not accept.

No artefact changes: the listings already printed builtin104/7/141 and still do.
All four Stage 02 artefacts regenerate byte-identical.
2026-08-27 06:27:00 +00:00
Sylpheed RE agent
2876449965 re: phase-guards for all 28 stages, and a 6/6 cross-check from an unrelated method
isl_report.py now accepts a directory, so the dominance analysis runs over the whole
disc: data/isl-phase-guards-all.txt, 177 phase exits, of which only 5 (2.8%) are
reachable from no static entry.  CFG reach ranges 69.5% (S26) to 95.8% (S25), median
about 4 dominating conditions per exit.

The lopsided number in the per-stage table was the six TUTORIAL stages, S18-S23, each
with exactly ONE exit and exactly ONE dominating condition.  That could have been a
degenerate result, so I looked: it is the same condition in all six,

    END_PHASE  <-  builtin104() != 1

and isl-builtins.md reached built-in 104 from call-site USAGE alone -- "S18-S23 only,
followed by wait_s 39/39, preceded by end_coroutine 37/39, a textbook poll loop".
Usage said 104 is the tutorial's polled test; dominance says it is the tutorial's
clear condition.  Two unrelated methods, six for six.

Stage 16 -- the corpus outlier whose script may be compiled C++ -- resolves as well:
read_freg(0) < 600, player_gauge0_test, player_gauge1_test, and two builtin141 calls
differing in a single argument (0 vs -4000), which is the shape of a position or zone
test.  builtin141 is unread, so it is not named.

Stage 02's separate artefact regenerates byte-identical.

Also added: an RLIMIT_AS cap in isl_report's entry point.  The dominator pass
OOM-killed a run earlier on this 15 GB box; a bad input should now fail the process
rather than the machine.

Still not settled and stated in the doc: dominance gives necessary, not sufficient,
conditions; the 5 unreachable exits need the trigger queue at phase+272; builtin104,
builtin141 and builtin7 all appear in clear conditions and are unread.
2026-08-27 06:19:44 +00:00
Sylpheed RE agent
983c5d112c re: the per-phase clear conditions, by dominance over the ISL CFG
Closes the backlog's "which condition guards each END_PHASE".  With the CFG from the
previous commit this is a graph query, not new machinery.

The obvious query is WRONG for this language, and I implemented it first: "one
successor reaches END_PHASE and the other does not" finds 1/62/1 guards across Stage
02's three phases, and the 1s are both the same read_freg(0) < 1200 timeout -- every
objective test missed.  The cause is the dominant idiom: a POLL LOOP's loop-back
branch also reaches the exit, one iteration later, so neither successor discriminates.
The asymmetric 1/62/1 is what exposed it; a uniform number would have read as
plausible.

Dominance has no such blind spot: a condition dominates an exit when every path from
an entry passes through it, so it is NECESSARY for the phase to end that way, and a
poll loop's test dominates its own exit by construction.  Iterative dominators
converge in 3 passes over 15670/18739 instructions (83.6%).

Result for Stage 02 -- every exit in all three phases is dominated by
unit_hp_pct(TCN001, Character_Player_Test) != 0, the player's ship being alive, which
falls out rather than being assumed.  Beyond that, phase 1's objective exit requires
hp_pct_test on ADT102, ADT107 and ADT113; phase 3's requires ADT301 and ADT302;
read_freg(0) gates at 210 / 300 and times out at 1200; random(3) and random(5)
dominate only the exits that pick one of several closing lines.

Two of the 15 exits are reachable from NO static entry, both FORCE_END_PHASE.  That
agrees with the independently measured 389 unreachable routines: they are started from
the trigger queue at phase+272, by data rather than code.

Practical note recorded: the first dominator run was OOM-killed -- 6743 nodes each
holding a Python set of up to 6743 elements.  Integer bitmasks run in seconds.

Not settled, and said so: dominance gives necessary, not sufficient, conditions; only
Stage 02's artefact is committed; one listed condition is still an unresolved
<unknown>; read_freg's units are inferred from the gate values, not read.

calls, phase-ends and conditions all regenerate byte-identical.
2026-08-27 06:12:41 +00:00
Sylpheed RE agent
29eda81ede re: recover ISL conditions by CFG dataflow instead of a linear walk
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.
2026-08-27 06:02:37 +00:00
Sylpheed RE agent
b42cd7183c re: builtin80 is a command -- and finding that exposed an 11.75% bug in my tracker
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.
2026-08-27 05:53:20 +00:00
Sylpheed RE agent
8af97adbc5 re: derive UNIT_ARG from the implementations -- 31 built-ins take a unit, not 31 of 55
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.
2026-08-27 05:45:10 +00:00
Sylpheed RE agent
4f37a4eb4c re: fix the wrong ISL conditions -- the cause was a coroutine boundary, not the filter
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.
2026-08-27 05:36:02 +00:00
Sylpheed RE agent
f32def9a05 re: resolve every ISL condition's comparand -- the clear conditions are readable
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.
2026-08-27 05:28:50 +00:00
Sylpheed RE agent
0287a78633 re: close the ISL operand chain -- built-in result is special[0], via a vtable
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.
2026-08-27 05:21:10 +00:00
Sylpheed RE agent
504a042698 re: read the ISL branch handlers -- it is a condition-code machine
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).
2026-08-27 05:10:50 +00:00
Sylpheed RE agent
228680edf0 re: the ISL stream is flat -- refute the "needs coroutine entry points" blocker
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.
2026-08-27 05:00:49 +00:00
Sylpheed RE agent
3e4dd3ca72 re: diff the legacy IDXD reader against the field table -- 17.54% wrong, with an exact predicate
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.
2026-08-27 04:52:05 +00:00
Sylpheed RE agent
157c4558af backlog: mark the freeze entry's "first step" done, and rule out --log_mask=0
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.
2026-08-27 04:43:18 +00:00
Sylpheed RE agent
a1e36df498 re: withdraw "the speed cannot be used", and close the world-unit item
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.
2026-08-26 21:19:22 +00:00
Sylpheed RE agent
ed2836777e backlog: record what the world-unit measurement now actually needs
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.
2026-08-26 21:10:26 +00:00
Sylpheed RE agent
4608d7c416 backlog: the world-unit blocker was the ROUTE, not entities2.py's VA window
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.
2026-08-26 20:47:43 +00:00
Sylpheed RE agent
93bc7bf061 re: SOLVED -- the mission freeze is a modal sign-in dialog
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.
2026-08-26 18:41:09 +00:00
Sylpheed RE agent
78696338b6 re: WITHDRAW "the freeze requires the rasterizer" -- the control was not controlled
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.
2026-08-26 18:28:47 +00:00
Sylpheed RE agent
da17e05c68 re: the freeze requires the rasterizer -- --gpu=null does not freeze
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.
2026-08-26 18:12:35 +00:00
Sylpheed RE agent
480b699cd5 re: the freeze is independent of the mask poke -- control run settles it
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.
2026-08-26 11:26:44 +00:00
Sylpheed RE agent
b641034be7 re: three boot-path failures that look like the emulator and are not
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.
2026-08-26 11:02:15 +00:00
Sylpheed RE agent
de1ff6ed76 docs: define the status markers, and fix six mislabelled or superseded entries
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).
2026-08-26 10:29:15 +00:00
Sylpheed RE agent
ce0720a3e9 re: correct a mislabelled blocker -- the world unit needs a run, not a waiver
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.
2026-08-26 10:07:01 +00:00
Sylpheed RE agent
35f67a2a37 re: retract the paint-key census and redo it over all 21 184 sprites
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.
2026-08-26 10:06:23 +00:00
Sylpheed RE agent
e0394647b2 re: census the T8aD paint-order key -- it is pak-local, not a global vocabulary
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.
2026-08-26 09:50:29 +00:00
Sylpheed RE agent
83d019515f re: confirm the cmesh<->model link via GameResourceID; world unit is oracle-blocked
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.
2026-08-26 09:41:46 +00:00
Sylpheed RE agent
cc1f9e6734 re: decode CollisionSet_*.bin -- the per-object collision-mesh library
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.
2026-08-26 09:31:22 +00:00
Sylpheed RE agent
e30881189b re: record the MiscBin name resolution in the backlog 2026-08-26 09:21:15 +00:00
Sylpheed RE agent
e1db4d84ff re: MCOL solved -- a closed triangle collision mesh in a uniform grid
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.
2026-08-26 09:12:36 +00:00
Sylpheed RE agent
cda5a31601 re: MCOL's 0x5C block is bounding spheres at stride 16 -- the 0.75 was 12/16
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.
2026-08-26 09:04:30 +00:00
Sylpheed RE agent
da499901b5 re: REGN is excluded as the wave scheduler -- it is a navmesh
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.
2026-08-26 08:11:36 +00:00
Sylpheed RE agent
c54924d08f re: backlog said REGN's coupling was unreachable statically -- it has been reached
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.
2026-08-26 08:05:31 +00:00
Sylpheed RE agent
7c1507a466 re: REGN cell payload indexes neither points nor planes -- static search exhausted
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.
2026-08-26 07:32:08 +00:00
Sylpheed RE agent
eb278c7941 re: REGN sections 0/1/2 have strides, and section 2 is a plane list
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.
2026-08-26 07:07:10 +00:00
Sylpheed RE agent
fd71f37d8f re: RATC +0x10 is a u16 pair too, and no bit is a clean predicate
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.
2026-08-26 06:59:08 +00:00
Sylpheed RE agent
a191569167 re: the RATC +0x0c interval is authored, not a keyframe extent
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.
2026-08-26 06:52:44 +00:00
Sylpheed RE agent
1fc68e391d re: RATC +0x0c is two u16s forming an ordered interval, not one number
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.
2026-08-26 06:46:50 +00:00
Sylpheed RE agent
e676210e9b re: close both remaining .slb questions in the backlog -- both were my errors
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.
2026-08-26 05:30:57 +00:00
Sylpheed RE agent
c2e7f8f8e4 re: the flat entity count is explained -- arrivals are not spawns
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.
2026-08-26 04:59:32 +00:00
Sylpheed RE agent
ea155ea035 re: the wave timetable's t is SECONDS, measured against the live speed law
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.
2026-08-26 04:47:10 +00:00
Sylpheed RE agent
220e250453 slb: the seek chunk gives the data offset structurally, and breaks the 28 ties
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.
2026-08-26 04:32:23 +00:00
Sylpheed RE agent
ce726eaa9c re: the cue-less cutscene pages ARE the resupply lines, and they cross-check the movie work
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.
2026-08-26 04:23:52 +00:00
Sylpheed RE agent
0abe908edd re: 69.8% of banks declare more data than they store -- and that explains TCAF_608
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.
2026-08-26 04:11:45 +00:00
Sylpheed RE agent
16476e94c3 re: sound.pak is fully accounted for -- the last 2 entries were named in the table
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.
2026-08-26 04:09:33 +00:00
Sylpheed RE agent
e4ca3ff36b re: withdraw the 'Japanese banks are a separate format' item, one iteration later 2026-08-26 03:57:08 +00:00
Sylpheed RE agent
165dbb4d81 re: index + a new open backlog item for the undecoded Japanese banks 2026-08-26 03:34:43 +00:00
Sylpheed RE agent
cb1967795a re: withdraw an overstatement -- the wrong-recording case was already settled
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.
2026-08-26 03:29:41 +00:00
Sylpheed RE agent
953c757560 re: index + backlog rows for the cue table and the cutscene message table 2026-08-26 03:24:30 +00:00