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-builtin-dispatch.md
Sylpheed RE agent 044b98dcc4 re: the ScriptPhase vtable is 113 slots, and there is a SilphScriptPhaseDemo
RTTI at vtable[-1] names the class .?AVSilphScriptPhase@silph@@ and the
next class's COL at 0x820A8680 terminates the table at 113 entries.
The earlier ">=200 slots" reading used a bad terminator -- scanning for
a non-code word runs straight into the next vtable.  Cross-check: the
147 built-in stubs use 109 distinct slots, min 0 max 110.

The next vtable is .?AVSilphScriptPhaseDemo@silph@@, also 113 slots,
overriding 109 of them with mostly one shared stub -- a cut-down phase.
That explains why sub_822710D0 has two callers: sub_82263408 and
sub_82275800 are the two classes' Update, both slot 111.

Both "unread spawner callers" are placed: sub_82264058 is slot 0 of
SilphScriptPhase and sub_82273910 is slot 0 of the Demo class -- the two
destructors.

Also records a negative: the Stage%02d construction site I proposed last
iteration does not exist (no stage format string among the 43 short %d
strings; no precomputed name hash anywhere), so the stage-index lead
cannot be settled that way.

Docs only; artefacts byte-identical.
2026-08-27 10:05:01 +00:00

147 lines
6.8 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.
# ✅ How a built-in's result reaches a condition — the operand chain
The previous iteration named the branches and stated plainly that this was still
missing: *"naming the branch does not by itself give the clear condition — that
needs the operand chain feeding each compare."* It is read now, and it closes.
## ⚠️ First: the corpus already had half of it, and I had written the stale file
[`isl-bytecode.md`](isl-bytecode.md) — which **owns** the opcode table — already
named ops 2124 (`push.i` / `push.f` / `pop.i` / `pop.f`) and ops 1318 as the
six branches. My own [`isl-branches.md`](isl-branches.md), written one iteration
earlier, said `op23` and `op21` were unread. **The stale file was mine.**
Verified rather than assumed, from the thunks and handlers:
| op | moves | verified by |
|---|---|---|
| 21 `push.i` | `[phase+168]` → deque at `phase+44` | thunk: `addi r4,r30,168` / `addi r3,r30,44` |
| 22 `push.f` | `[phase+184]` → deque at `phase+64` | thunk: `addi r4,r30,184` / `addi r3,r30,64` |
| 23 `pop.i` | → `[phase+168]` | handler `0x82271C30`, only r3-offset touched is **168** |
| 24 `pop.f` | → `[phase+184]` | handler `0x82271CB8`, only r3-offset touched is **184** |
With `isl-bytecode.md`'s operand-kind table (`special[0] = [phase+164]`,
`special[1] = [phase+168]`), `pop.i` lands in **`special[1]`**.
## ✅ The built-in table is a thin dispatch layer over a vtable
Each of the 147 entries is a **stub**, not an implementation. The stub resolves
the `local[]` argument base and tail-calls a fixed slot of the `ScriptPhase`
vtable at `[phase+0]`:
```
82272DFC addi r3, r31, 20 ; local[] base
82272E00 bl 0x82454A40 ; resolve
82272E04 lwz r11, 0(r31) ; the vptr
82272E10 lwz r11, 184(r11) ; <- fixed slot, one per built-in
82272E14 b 0x822724F0 ; mtspr CTR / bcctrl, then return 0
```
Measured over all 147:
| | count |
|---|---|
| dispatched through a `ScriptPhase` vtable slot | **112** |
| write `[phase+164]` (`special[0]`) inline in the stub | 17 |
| write `[phase+184]` inline | 0 |
**Every named predicate is in the vtable group**`unit_state` 184,
`unit_alive` 188, `hp_pct_test` 64, `dist_lt` 56, `unit_hp_pct` 256,
`is_engaged` 252, `group_ratio_pct` 196, `timer_elapsed` 372,
`player_gauge0/1_test` 396/400 — which is the control: the split is not
arbitrary, it separates engine queries from script-local bookkeeping.
## ✅ The vtable is at `0x820A84BC`, derived self-checkingly
Not guessed from a stride — the trap this corpus already paid for. Derived from
a **known implementation**:
1. `isl-bytecode.md` documents built-in **39** `MARK_LAST_PHASE` as `[phase+300] = 2`.
2. The function `0x8226B498` is exactly `addi r11,r0,2 ; stw r11,300(r3) ; blr`.
3. It appears as a data word at exactly **one** address: `0x820A8570`.
4. Built-in 39's stub uses slot **180** → base = `0x820A8570 180` = **`0x820A84BC`**.
**The check, which was not used in the derivation:** built-in **40**
`mark_not_last` (`[phase+300] = 1`) uses slot **176**, so the base predicts
`0x820A84A8`… and slot 176 holds `0x8226B4A8`, which is
`addi r11,r0,1 ; stw r11,300(r3) ; blr` — the `= 1` stub sitting immediately
after the `= 2` one. Predicted and confirmed.
**Third, independent:** the database's own `vptr_writes` lists
`0x820A84BC` as a vtable, written at `0x82261B80`.
## ✅ And the result lands in `special[0]`
`unit_state` is slot 184 → **`0x8226ADF0`**. It indexes `[phase+324]` — the unit
array `isl-builtins.md` already documents — by `local[4]`, reads the record, and
writes its answer to **`[phase+164]` = `special[0]`** at both its normal exit
(`0x8226AEC4`) and its early exit (`0x8226AF48`).
That completes the chain, and the phase-3 poll loop now reads end to end:
```
call unit_state(ADT308) ; result -> special[0]
pop.i ; special[1] <- the pushed comparand
cmp.i special[0], special[1]
beq -> 0xFEB4 ; loop back while they are equal
```
**A built-in's return value is `special[0]`; the comparand is popped into
`special[1]`; the compare and branch do the rest.** That is the shape of every
clear condition in the corpus.
## 🟡 Not settled
* **The other 111 vtable slots are not read.** The base is now known, so each is
a lookup rather than a search — but knowing where `hp_pct_test` lives is not
the same as having read it.
* **Which comparand each site pushes.** The loop above compares against whatever
`push.i` put on the deque; recovering that per site needs the push tracked
through the decode, which `isl.py` does not do.
* **The 35 non-vtable built-ins**, and `op21`/`op22`'s generic deque helpers
(`0x82175C20`, `0x82274BA0`), were not read — only their arguments.
* ~~**The vtable's length.**~~ ✅ **SETTLED (2026-08-27): 113 slots (0…112).**
## ✅ (2026-08-27) The class is `silph::SilphScriptPhase`, the vtable is 113 slots, and there is a SECOND one
The MSVC RTTI locator sits at `vtable[-1]`. At **`0x820A84B8`** it is `0x8210DDC0`,
whose type descriptor `0x8289CD18` spells **`.?AVSilphScriptPhase@silph@@`** — so
the class is named, not inferred.
The vtable **ends at `0x820A867C`, 113 entries**. The terminator is not "a
non-code word" — a scan on that rule runs straight past the end, which is why an
earlier pass reported "≥200 slots, no non-code word in the first 200". The real
terminator is **the next class's COL**: `0x820A8680` holds `0x8210DD74`, and a
second vtable begins at `0x820A8684`.
**Cross-check, independent of the RTTI:** the 147 built-in stubs between them
reference **109 distinct slots, minimum 0, maximum 110** — every one inside
0…112. A wrong length would have stubs pointing past the end.
### The second class: `silph::SilphScriptPhaseDemo`
`0x8210DD74` → type descriptor `0x8289CCC0` = **`.?AVSilphScriptPhaseDemo@silph@@`**,
vtable at `0x820A8684`, **also 113 slots**. It **overrides 109 of the 113**, and
the overwhelming majority point at a single shared stub, `0x8226C160`. So the Demo
phase implements almost none of the script surface — it keeps a handful of real
methods (slot 15 → `sub_82391BA8`, slot 111 `Update``sub_82275800`,
slot 112 → `sub_8237EF08`) and stubs the rest.
🔑 That explains a loose end from [isl-timers](isl-timers.md): the stopwatch
advance `sub_822710D0` has **two** callers, `sub_82263408` and `sub_82275800`.
They are the two classes' `Update` methods — same slot 111, base and derived.
### And both "unread spawner callers" are placed
`isl-coroutine-spawner.md` lists `sub_82273910` and `sub_82264058` as unread
callers of the spawner. They are a **matched pair**:
| | slot | class |
|---|---|---|
| `sub_82264058` | **0** | `SilphScriptPhase` |
| `sub_82273910` | **0** | `SilphScriptPhaseDemo` |
Slot 0 in this ABI is the scalar deleting destructor, so these are the two
classes' teardown paths — not two separate mysteries. 🟡 *Why* a destructor
reaches the spawner is not read here.