This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/structures/isl-trigger-node.md
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

6.1 KiB
Raw Blame History

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 112 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_8226A0D8built-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 112, and SYM1_SLOTS lists 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 +24 selects 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.