# ✅ The trigger node's layout — from a matched writer/reader pair [isl-trigger-queue](../isl-trigger-queue.md) 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](../isl-coroutine-spawner.md) 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. ## ✅ `+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** | `0x8226D89C` → `bl 0x822737C8` | `sub_8226DAF8` | a **code offset** (79/79 vs a 27.8 % control) | | **1** | **built-in 25** | falls through at `0x8226D7CC` — **no 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 1–12 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.