Files
Sylpheed/docs/re/structures/isl-trigger-node.md
Sylpheed RE agent bee8d96394 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

4.0 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.

🟡 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. [stack+112] is filled from payload+28, yet the spawner takes a code offset — so some step in that chain is not what I read. Left open rather than guessed.
  • sub_8226E458's effect on the container.
  • What starts the ~15 % of unreached code — still open, and this was not it.