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

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

+24 IS the kind tag — a three-way dispatch in the drain

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, the OTHER path
+24 written by path condition test what +28 is
0 built-in 19 0x8226D89Cbl 0x822737C8 sub_8226DAF8 a code offset (79/79 vs a 27.8 % control)
1 built-in 25 falls through at 0x8226D7CCno spawn sub_8226DC80 an index into [phase+244]

The non-spawn path is explicit about it:

8226D834  lwz  r10, 112(r31)     ; payload+28
8226D838  lwz  r11, 244(r29)     ; [phase+244] = SYMBOL TABLE 1
8226D848  lwzx r10, r10, r11     ; resolve it as a symtab-1 index

And built-in 25's +28 resolves to a ROUTE

Over all 25 call sites disc-wide, local[12] read through symbol table 1:

25 of 25 resolve, and every name is a route — Route_ADS101_p1F, Route_ADT107_p1F, Route_TCN004_p1S, Route_ADT202A_p2S, …

So the 112 values that looked like a puzzle are simply small symbol indices. This also confirms isl.py's SYM1_SLOTS, which lists slot 12 for built-in 25 and was derived from operand ranges alone — two independent routes, same conclusion.

The two built-ins register two kinds of trigger: built-in 19 a trigger whose payload is a handler routine, built-in 25 one whose payload is a route.

🟡 Not settled

  • 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 It does — see above.
  • The two condition testers are unread: sub_8226DAF8 (kind 0) and sub_8226DC80 (kind 1), each gating its path on a == 1 result.
  • What the kind-1 path DOES with the route past sub_8217FA08 / sub_8218CC88 / sub_8225FEA0 is unread.
  • The drain's second spawn site (0x8226DA10) is in a later block and was not traced.
  • 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.