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

229 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ✅ 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 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_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 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 ROUTE**`payload+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.