Commit Graph

3 Commits

Author SHA1 Message Date
Sylpheed RE agent
3a56cc6998 re: +24 is the trigger kind tag -- built-in 19 registers a routine, built-in 25 a route
Read straight off the drain, immediately after the pop:

    8226D7AC  lwz   r11, 92(r31)        ; payload+24
    8226D7B8  cmpli cr6, 0, r11, 0x1
    8226D7C4  bc    12, lt, 0x8226D89C  ; +24 == 0 -> the SPAWN path
    8226D7C8  bc     4, eq, 0x8226DA44  ; +24  > 1 -> bail
                                        ; +24 == 1 -> fall through, a NON-spawn path

So the field I recorded last iteration as "kind-tag shaped, but the drain was not
shown branching on it" does branch on it, three ways.

  +24 = 0, written by built-in 19: condition sub_8226DAF8, then bl 0x822737C8 --
      +28 is a code offset, matching the 79/79-vs-27.8%-control measurement.
  +24 = 1, written by built-in 25: condition sub_8226DC80, no spawn, and the path
      resolves +28 through [phase+244] -- symbol table 1:
          8226D834  lwz  r10, 112(r31)     ; payload+28
          8226D838  lwz  r11, 244(r29)     ; symtab 1
          8226D848  lwzx r10, r10, r11

And that resolution checks out: over all 25 call sites disc-wide, built-in 25's
local[12] resolves to a symbol-table-1 entry 25 times out of 25, and every name is a
route -- Route_ADS101_p1F, Route_ADT107_p1F, Route_TCN004_p1S, Route_ADT202A_p2S.
The 1-12 values that looked like a puzzle two iterations ago are simply small symbol
indices.  This independently confirms isl.py's SYM1_SLOTS, which lists slot 12 for
built-in 25 and was derived from operand ranges alone.

So there are two kinds of trigger: built-in 19 registers one whose payload is a
HANDLER ROUTINE, built-in 25 one whose payload is a ROUTE.

All artefacts regenerate byte-identical; documentation only.

Unread: both condition testers (sub_8226DAF8, sub_8226DC80), what the kind-1 path
does with the route past sub_8217FA08 / sub_8218CC88 / sub_8225FEA0, and the drain's
second spawn site at 0x8226DA10.
2026-08-27 07:53:26 +00:00
Sylpheed RE agent
6402a34d9b re: two trigger appenders -- built-in 19's node carries the handler routine
Resolves the puzzle the previous commit left open: payload+28 is a 1-12 index, yet
the drain hands it to a spawner that wants a code offset.  The cause is that built-in
25 is not the only thing that appends a trigger node.

Searching for functions that write a double at +16 AND a word at +28 of the same
register finds exactly two: sub_8226A0D8 (built-in 25, vtable slot 28) which fills the
payload inline, and sub_8226E160, a free-list recycling variant whose sole caller
sub_82269ED0 is vtable slot 68 = BUILT-IN 19.

Built-in 19 passes local[4] -> node+0 (the unit), local[12] -> +4, local[24] -> +16 as
a double, and local[32] -> +28.  That last one is the field the drain spawns from, and
it is a genuine code offset:

  over all 79 call sites disc-wide
    land on the instruction stream    79 / 79 = 100.0%
    control, random 4-aligned offsets           27.8%
    values 12164 .. 111080, 73 distinct

Large, distinct, and every one resolves against a control that resolves barely a
quarter of the time.  So built-in 19 registers a trigger whose handler is a routine,
and the drain's `base + payload+28` spawn is that handler.  The two builders simply
put different things in the same slot.

Observed alongside: built-in 25 writes +24 = 1 and built-in 19 writes +24 = 0, which
is kind-tag shaped -- but the drain was not shown branching on it, so that is recorded
as an observation and not a reading.

All artefacts regenerate byte-identical; documentation only.

Still open: whether +24 selects between the node kinds, and this STILL does not
explain the unreached code -- 0 of the 79 handler offsets are unreached run-starts.
2026-08-27 07:38:24 +00:00
Sylpheed RE agent
9ee32f7940 re: decode the trigger node -- and withdraw two of my own claims about it
The reader is sub_8226E220, called by the per-frame drain sub_8226D740 as
(phase+272, six out-params).  It pops one node and copies seven payload fields out.
Built-in 25, the appender, writes exactly those seven offsets:

    +0   local[4]  -- the unit          +24  the constant 1
    +4   local[12]                      +28  local[12] again
    +8   computed                       +32  --
    +16  a DOUBLE from local[16]

Neither side was derived from the other, so the agreement is the check.  Container:
+16 head, +20 pending count -- which matches the count isl-builtins.md watched live at
phase+272+20 from a completely different direction -- and +24 a cursor.

WITHDRAWN, from the previous commit: "the drain spawns from [node+112]".  r31 = r1 -
256, the stack frame, so +112 is an output slot and not a node field.

REFUTED, the follow-up hypothesis that the trigger carries the routine offset and so
names the code nothing else starts: payload+28 is local[12], and across all 25 call
sites disc-wide those values are the small integers 1 through 12 -- 1 of 25 (4.0%)
land on the instruction stream against a 16.0% control, and none are unreached
run-starts.  Below chance.  My first version of that test used n=2, Stage 02 only; it
happened to agree, but two samples could not have supported it either way.

One correction in the other direction, to the corpus: isl-builtins.md withdrew
sub_8226E458's link to the trigger queue on the grounds that "the argument is
lwz r4, 324(r29), the unit array, not the trigger container".  That is the SECOND
argument; the first is r26 = phase + 272, set twelve instructions earlier.  The drain
does operate on the container.  What sub_8226E458 does to it remains unread, so only
the argument is corrected, not the conclusion.

All artefacts regenerate byte-identical; documentation only.

Still open: what local[12] indexes; what the drain actually spawns, since [stack+112]
is filled from payload+28 yet the spawner wants a code offset, so some step in that
chain is not what I read; and what starts the ~15% of unreached code.
2026-08-27 07:30:41 +00:00