Resolves the puzzle the previous commit left open: payload+28 is a 1-12 index, yet
the drain hands it to a spawner that wants a code offset. The cause is that 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: sub_8226A0D8 (built-in 25, vtable slot 28) which fills the
payload inline, and sub_8226E160, a free-list recycling variant whose sole caller
sub_82269ED0 is vtable slot 68 = BUILT-IN 19.
Built-in 19 passes local[4] -> node+0 (the unit), local[12] -> +4, local[24] -> +16 as
a double, and local[32] -> +28. That last one is the field the drain spawns from, and
it is a genuine 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%
values 12164 .. 111080, 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 that handler. The two builders simply
put different things in the same slot.
Observed alongside: built-in 25 writes +24 = 1 and built-in 19 writes +24 = 0, which
is kind-tag shaped -- but the drain was not shown branching on it, so that is recorded
as an observation and not a reading.
All artefacts regenerate byte-identical; documentation only.
Still open: whether +24 selects between the node kinds, and this STILL does not
explain the unreached code -- 0 of the 79 handler offsets are unreached run-starts.
144 lines
6.1 KiB
Markdown
144 lines
6.1 KiB
Markdown
# ✅ 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.
|
||
|
||
## 🟡 Not settled
|
||
|
||
* **What `local[12]` indexes.** Values 1–12, 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.~~ ✅ Resolved above: built-in 19's nodes carry
|
||
a real routine offset at `+28`.
|
||
* **Whether `+24` selects between the two node kinds** — 1 from built-in 25, 0
|
||
from built-in 19 — is unshown; the drain reads it into a stack slot that the
|
||
spawn path does not visibly use.
|
||
* **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.
|