Ran the "who writes this" move on the two 3-double vectors the trigger condition pulls out of the unit record. It did not settle them, and it is worth recording why. 99 sites across 40 functions write +32/+40/+48; 16 sites across 13 write +64/+72/+80; six functions write both. The most inviting is sub_821AD218, which is a float-to-double widening copy of exactly 16 floats -- lfs from r4+0..60, stfd to r3+0..120. Sixteen floats is a 4x4 matrix, under which +32/+40/+48 is row 1 and +64/+72/+80 is row 2, making A and B basis vectors rather than positions. That is a tidy story and it is wrong for this record. The disproof was already in hand: sub_8226DAF8 reads `lwz r10, 100(r11)`, a WORD at +100, which falls inside where a 16-double matrix's row 3 (+96..+120) would sit. A second writer, sub_8226A348 -- same built-in shape, [phase+324] indexed by local[4] with the rec+4 liveness check -- confirms a mixed layout: words at +0/+4/+8/+12/+16 and at +80, doubles only at +32/+40/+48. So these offsets recur at the same positions in several unrelated object layouts, and nothing found here ties a specific writer to the object the trigger actually reads. A and B stay unnamed. That is the second tidy reading rejected for them, after previous/current-position. All artefacts regenerate byte-identical; documentation only.
322 lines
14 KiB
Markdown
322 lines
14 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.
|
||
|
||
## ✅ `+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*.
|
||
|
||
## ✅ 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.
|
||
|
||
## ✅ What the kind-0 condition TESTS — a point-to-segment proximity check
|
||
|
||
`sub_8226DAF8` past the route-name lookup:
|
||
|
||
```
|
||
8226DB84 bl 0x823012D8 ; look the route name up -> a route object; bail if absent
|
||
8226DBB8 lwz r11, 324(r30) ; the unit array
|
||
8226DBBC rlwinm r10, r26, 2,0,29; r26 = payload+0, the unit index
|
||
8226DBC8 lwz r10, 100(r11) ; require rec+100 != 0, else return 0
|
||
8226DBD4 lfd f0, 32(r11) … ; THREE doubles: rec+32, +40, +48 -> out vector B
|
||
8226DBF8 lfd f0, 64(r11) … ; THREE doubles: rec+64, +72, +80 -> out vector A
|
||
8226DBFC fmr f1, f31 ; the node's double (payload+16)
|
||
8226DC1C lwz r11, 0(r30) ; the ScriptPhase vptr
|
||
8226DC28 lwz r11, 60(r11) ; VTABLE SLOT 60
|
||
8226DC30 bcctrl ; test(phase, A, B, P=the route, f1)
|
||
```
|
||
|
||
**Slot 60 → `sub_82268068`**, which no built-in uses — it is engine-internal — and
|
||
its arithmetic is unambiguous:
|
||
|
||
```
|
||
f29 = |B - A| ; fsub per axis, fmul + 2x fmadd, fsqrt
|
||
if |B - A| < 0.1 -> 0 ; degenerate segment (the constant is 0.1)
|
||
f28 = |P - A| ; if > f1 -> 0
|
||
|P - B| ; if > f1 -> 0
|
||
t = (P-A)·(B-A) / |B-A|² ; the projection parameter
|
||
if t < 0.0 -> 0
|
||
```
|
||
|
||
Per-axis `fsub`, a sum of three squares, `fsqrt`, and a dot product divided by
|
||
the squared length: this is a **point-to-segment distance test**, and the node's
|
||
double is therefore a **RADIUS**.
|
||
|
||
So a kind-0 trigger fires when a route point lies within `local[24]` of the
|
||
segment between two vectors taken from the unit's record.
|
||
|
||
🟡 **What A and B are is not established.** `rec+64/72/80` and `rec+32/40/48` are
|
||
two positions; a previous/current pair would make this the standard
|
||
frame-rate-robust waypoint test, and that is exactly the kind of tidy reading
|
||
this corpus makes itself prove. Not proven here.
|
||
|
||
## ✅ The kind-1 condition is the SAME test with a cheap pre-filter
|
||
|
||
`sub_8226DC80` turns out to be `sub_8226DAF8`'s twin. Diffing them instruction
|
||
by instruction: of the first 86, 33 differ — and every difference before index 65
|
||
is a register rename or a branch target. Both do exactly the same thing up to
|
||
that point:
|
||
|
||
* resolve the route name (`sub_8217FA08` → `sub_823012D8`), leaving the route
|
||
point in `[r31+112/120/128]`;
|
||
* index `[phase+324]` by `payload+0`, require `rec+100 != 0`;
|
||
* copy the two 3-double vectors out of the unit record.
|
||
|
||
Then they part:
|
||
|
||
| | kind 0 — `sub_8226DAF8` | kind 1 — `sub_8226DC80` |
|
||
|---|---|---|
|
||
| next | calls vtable slot 60 straight away | **inline distance first** |
|
||
| | | `fsub` ×3 against the route point, `fmul` + 2 `fmadd`, `fsqrt` |
|
||
| | | `fcmpu` vs `f31`; **if > the radius, return 0** |
|
||
| then | — | **calls the same vtable slot 60** |
|
||
|
||
```
|
||
8226DDC4 fsqrt f0, f0
|
||
8226DDC8 fcmpu cr6, f0, f31 ; f31 = the node's double, the radius
|
||
8226DDCC bc 4, gt, return-0 ; too far -> reject without the segment test
|
||
8226DDD0 lwz r11, 0(r30)
|
||
8226DDE8 lwz r11, 60(r11) ; the SAME slot 60
|
||
8226DDF0 bcctrl
|
||
```
|
||
|
||
**So both trigger kinds run the identical point-to-segment test.** Kind 1 adds a
|
||
point-to-point early-out against the same radius and, per the drain, does not
|
||
spawn. The trigger mechanism is now read end to end.
|
||
|
||
## 🟡 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 kind-0 condition tests~~ ✅ Read. ~~`sub_8226DC80` (kind 1)~~ ✅ Read —
|
||
the same test with an inline distance pre-filter.
|
||
* **Which unit-record fields A and B are** — `rec+64/72/80` vs `rec+32/40/48`.
|
||
🔎 **Searched, not settled — and one tidy reading positively excluded.** The
|
||
"who writes this" move found **99 sites in 40 functions** writing `+32/+40/+48`
|
||
and 16 sites in 13 writing `+64/+72/+80`, with six functions writing both. The
|
||
most inviting of them, `sub_821AD218`, is a **float→double widening copy of
|
||
exactly 16 floats** (`lfs r4+0…60` → `stfd r3+0…120`) — a **4×4 matrix**, in
|
||
which `+32/+40/+48` would be row 1 and `+64/+72/+80` row 2, i.e. basis vectors
|
||
rather than positions.
|
||
🔴 **That reading is wrong for the trigger's record**, and the disproof was
|
||
already in hand: `sub_8226DAF8` reads **`lwz r10, 100(r11)` — a WORD at +100**,
|
||
which falls inside where a 16-double matrix's row 3 (+96…+120) would sit. A
|
||
second writer, `sub_8226A348` (same built-in shape: `[phase+324]` by `local[4]`,
|
||
`rec+4` liveness), confirms a mixed layout — words at `+0/+4/+8/+12/+16` and
|
||
**`+80`**, doubles only at `+32/+40/+48`.
|
||
So `+32/+40/+48` appears at the same offsets in **several unrelated object
|
||
layouts**, and nothing ties a specific writer to the object the trigger reads.
|
||
A and B stay unnamed.
|
||
* **The rest of slot 60** past the `t < 0` rejection (a second constant at
|
||
`0x820B0000+25192` is loaded next).
|
||
* **`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.
|