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.
6.1 KiB
✅ The trigger node's layout — from a matched writer/reader pair
isl-trigger-queue located the appender (built-in 25). The node's contents are now read, because the reader was found too and the two agree field for field.
The reader: sub_8226E220
sub_8226D740 — the per-frame engine→script drain — calls it as
8226D758 addi r26, r29, 272 ; r29 = the phase, so r26 = the TRIGGER CONTAINER
8226D79C or r3, r26, r26
8226D7A0 bl 0x8226E220 ; (container, &out88, &out84, &out80,
; &out120, &out92, &out112)
and it pops one node:
8226E228 lwz r10, 20(r11) ; the PENDING COUNT -- return 0 if empty
8226E244 lwz r10, 16(r11) ; the list head
8226E250 stw r10, 24(r11) ; a cursor
8226E254 lwz r11, 8(r10) ; -> the node PAYLOAD
✅ The layout, confirmed from both ends
| payload | written by built-in 25 | read by sub_8226E220 |
source |
|---|---|---|---|
| +0 | stw r26, 0(r30) |
→ out88 | local[4] — the unit |
| +4 | stw r28, 4(r30) |
→ out84 | local[12] |
| +8 | stw r27, 8(r30) |
→ out80 | computed |
| +16 | stfd f31, 16(r30) |
→ out120 (a double) | local[16] |
| +24 | stw r25, 24(r30) |
→ out92 | the constant 1 |
| +28 | stw r28, 28(r30) |
→ out112 | local[12] again |
| +32 | stw r11, 32(r30) |
→ out116 | — |
| +36 | stw r23, 36(r30) |
not read here | — |
Container: +16 head, +20 pending count, +24 cursor. The +20 matches
the count isl-builtins.md watched live at phase+272+20, from a completely
different direction.
That the writer and the reader touch the same seven offsets, in the same order, is the check — neither was derived from the other.
payload+0 being local[4] also agrees with UNIT_ARG, which lists built-in 25
and was derived from operand ranges.
🔴 Two corrections, both mine
[node+112] does not exist
isl-coroutine-spawner recorded the drain's spawns
as taking their offset from [node+112]. r31 = r1 - 256 — it is the
stack frame, so +112 is an output slot, not a node field. Withdrawn.
And the trigger does NOT carry a routine offset
The follow-up hypothesis — that payload+28 reaches the spawner and so names the
routines nothing else starts — does not survive. payload+28 is local[12], and
across all 25 call sites disc-wide those values are the small integers
1 … 12:
| land on the instruction stream | 1 / 25 = 4.0 % |
| control, random 4-aligned offsets | 16.0 % |
| are unreached run-starts | 0 |
Below chance. They are indices of some kind, not code offsets.
⚠️ My first version of this test used n = 2 — Stage 02 only. It happened to give the same answer, but two samples could not have supported it either way.
✅ One correction to the corpus, in the other direction
isl-builtins.md withdrew the idea that sub_8226E458 touches the trigger
queue, reasoning: "At 0x8226D780 the argument is lwz r4, 324(r29) —
[ScriptPhase+324], the unit array, not the trigger container."
The call is sub_8226E458(r3 = r26, r4 = [phase+324]), and r26 was set to
phase + 272 twelve instructions earlier. The withdrawal read the second
argument. sub_8226D740 does operate on the trigger container; what
sub_8226E458 does to it is still unread, so only the argument is corrected
here, not the conclusion.
✅ RESOLVED: there are TWO appenders, and only one carries a routine
The puzzle above — payload+28 holding a 1–12 index while the drain hands it to
a spawner that wants a code offset — had a simple cause: 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:
| builder | how |
|---|---|
sub_8226A0D8 — built-in 25, vtable slot 28 |
fills the payload inline |
sub_8226E160 |
a free-list recycling variant, called from sub_82269ED0 |
sub_82269ED0 is vtable slot 68 = built-in 19, and it calls the builder as
8226A034 addi r3, r28, 272 ; the trigger container
8226A040 lwz r4, 4(r30) ; local[4] -> node+0 (the unit)
8226A03C lwz r5, 12(r30) ; local[12] -> node+4
8226A038 lfd f1, 24(r30) ; local[24] -> node+16 (double)
8226A030 lwz r8, 32(r30) ; local[32] -> node+28
✅ Built-in 19's local[32] IS a 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 % |
| value range | 12 164 … 111 080, 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 exactly that handler.
Built-in 25's payload+28 really is a small index; the two builders simply put
different things in the same slot. Observed alongside it: built-in 25 writes
+24 = 1 and built-in 19 writes +24 = 0, which is the shape of a kind
tag — 🟡 but the drain was not shown branching on it, so that is an observation,
not a reading.
🟡 Not settled
- What
local[12]indexes. Values 1–12, andSYM1_SLOTSlists slot 12 for built-in 25 (symbol table 1 — routes, messages, objectives), which is consistent with a small index but was not verified here. What the drain actually spawns.✅ Resolved above: built-in 19's nodes carry a real routine offset at+28.- Whether
+24selects between the two node kinds — 1 from built-in 25, 0 from built-in 19 — is unshown; the drain reads it into a stack slot that the spawn path does not visibly use. - This still does not explain the unreached code. All 79 of built-in 19's handler offsets are already reachable — 0 are unreached run-starts.
sub_8226E458's effect on the container.- What starts the ~15 % of unreached code — still open, and this was not it.