Files
Sylpheed/docs/re/structures/isl-trigger-node.md
Sylpheed RE agent d27bf54998 re: both trigger kinds watch a ROUTE -- the unit-reaches-route mechanism
The kind-0 condition tester sub_8226DAF8 resolves a symbol as well, but from
payload+4 rather than +28:

    8226DB14  rlwinm r10, r5, 2, 0, 29   ; r5 = payload+4, x4
    8226DB24  lwz    r11, 244(r30)       ; [phase+244] = symbol table 1
    8226DB3C  lwzx   r10, r10, r11
    8226DB50  addi   r4, r11, 4          ; -> the name string

Both appenders write local[12] into whichever slot their own path reads -- built-in 19
into +4, built-in 25 into +4 and +28 -- so the same operand is the symbol either way,
and it resolves cleanly:

    built-in 19   local[12] -> symtab 1   79 / 79   all symbol type 1
    built-in 25   local[12] -> symtab 1   25 / 25   all symbol type 1

isl-builtins.md records symtab-1 type 1 as the Route_* names, and the samples agree:
Route_ADN106_p1F, Route_ADN1xxe1_p1F, Route_ADS101_p1F, Route_TCN004_p1S.

So both built-ins register a trigger on a UNIT reaching a ROUTE -- payload+0 is the
unit, local[12] the route.  They differ only in the payoff: built-in 19 additionally
carries a handler routine (local[32] -> payload+28, kind 0 -> spawn) while built-in 25
carries none (kind 1 -> no spawn).  That is a third independent agreement with
isl.py's SYM1_SLOTS, which lists slot 12 for both and was derived from operand ranges
alone.

It also joins up with the closed REMAINING OB work, where the counter was measured to
rise at a squadron's route arrival time -- route triggers are the mechanism that would
do that.  Recorded as a connection, not a demonstration: nothing here traces a trigger
to that counter.

All artefacts regenerate byte-identical; documentation only.

Still unread: what the conditions actually COMPARE -- only sub_8226DAF8's first ~34
instructions are read, up to the name lookup, and sub_8226DC80 not at all -- and
payload+8, a computed value passed to both testers.  A waypoint index would fit it,
which is exactly why it is not being called one.
2026-08-27 08:00:22 +00:00

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

BOTH trigger kinds watch a ROUTE

The kind-0 condition tester sub_8226DAF8 resolves a symbol too — from payload+4, not +28:

8226DB14  rlwinm r10, r5, 2, 0, 29   ; r5 = payload+4, x4
8226DB24  lwz    r11, 244(r30)       ; [phase+244] = SYMBOL TABLE 1
8226DB3C  lwzx   r10, r10, r11       ; resolve it
8226DB50  addi   r4, r11, 4          ; -> the name string
8226DB54  bl     0x8217FA08

Both appenders write local[12] into the slot their path reads — built-in 19 into +4, built-in 25 into +4 and +28. So the same operand is the symbol in both cases, and it resolves:

local[12] through symbol table 1 symbol type
built-in 19 79 / 79 all type 1
built-in 25 25 / 25 all type 1

isl-builtins.md records symtab-1 type 1 as the Route_* names (1362 entries), and the samples bear it out: Route_ADN106_p1F, Route_ADN1xxe1_p1F, Route_ADS101_p1F, Route_TCN004_p1S.

So both built-ins register a trigger on a UNIT reaching a ROUTEpayload+0 is the unit, local[12] the route. They differ only in what happens when it fires: built-in 19 additionally carries a handler routine (local[32]payload+28, kind 0 → spawn); built-in 25 carries none (kind 1 → no spawn).

This also confirms isl.py's SYM1_SLOTS, which lists slot 12 for both 19 and 25 and was derived from operand ranges alone — a third independent agreement with that table.

🔑 It joins up with the closed REMAINING OB result, where the counter was measured to rise at a squadron's route arrival time. Route triggers are the mechanism that would do that. 🟡 Stated as a connection, not a demonstration — nothing here traces a trigger to that counter.

🟡 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.
  • What the conditions actually TEST. Only sub_8226DAF8's first ~34 instructions are read — up to the route-name lookup. What it compares the route against, and the whole of sub_8226DC80 (kind 1), are unread.
  • payload+8 is a computed value in both appenders (r29-1 in built-in 19) and is passed to both testers; a waypoint index would fit, which is exactly why it is not being called one.
  • 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.