Files
Sylpheed/docs/re/isl-trigger-queue.md
Sylpheed RE agent da6eb236ef re: the trigger-queue appender is built-in 25 -- the script appends, not the engine
isl-builtins.md carried the trigger queue at phase+272 a long way -- layout, live
count at +272+20, the generic appender sub_8226EAB8 -- and stopped at "what actually
appends a node is still unidentified", proposing a gdb watchpoint on a running
emulator.  It is a static question after all.

sub_8226EAB8 is a shared helper with 16 callers, so it cannot be identified by name.
But the corpus records that the trigger container EMBEDS its inner list at +12, so the
appender must hand it container+12.  Only two of the sixteen callers do, and one sets
its base 34 instructions earlier:

  8226A22C  addi r29, r24, 272     ; r24 = the ScriptPhase
  8226A2B4  addi r3,  r29, 12      ; -> phase + 272 + 12
            bl   0x8226EAB8

phase+272+12 is exactly the inner list whose count the corpus watched at +272+20.

sub_8226A0D8 is ScriptPhase vtable SLOT 28, which is what BUILT-IN 25's stub
tail-calls, and there are ZERO real `bl 0x8226A0D8` instructions in the image -- it is
reached only through the vtable.  So the queue is populated by the SCRIPT, retiring
the residual "the engine moves records into the phase's queue each frame" reading that
isl-builtins.md had already flagged as unsupported by its own call site.  Built-in 25's
arguments agree with two independently measured tables: UNIT_ARG (a unit at local[4])
and SYM1_SLOTS[12] (a symtab-1 name at local[12]).

REFUTED in the same pass, with a control: built-in 25 does NOT carry the coroutine
entry that a trigger fires, which would have seeded the 389 routines reachable from no
static entry.  Taking each staged operand as a code offset from the phase base, 0 of 8
land on an instruction boundary, against 38.7% for an arbitrary 4-aligned offset --
below chance, not above it.  And the scale is wrong regardless: built-in 25 has only
2 call sites in Stage 02.

So what starts the unreachable routines is still open, and it is not this.  A trigger
node's own layout and its consumer are unread, and built-in 25 stays unnamed.
2026-08-27 06:42:47 +00:00

3.3 KiB

What appends to the trigger queue — built-in 25, not the engine

structures/isl-builtins.md took the trigger queue at phase+272 a long way — its layout, its live count at +272+20, and the generic appender sub_8226EAB8 — and stopped at: "What actually appends a node is still unidentified." Its suggested next step was a gdb watchpoint on a running emulator.

It is a static question after all.

The search that works

sub_8226EAB8 is a shared list helper with 16 callers, so it cannot be identified by name. But the corpus records that the trigger container embeds its inner list at +12, so the caller must hand it container + 12. Two of the 16 do:

0x8226E208   addi r3, r30, 12
0x8226A2B4   addi r3, r29, 12

and in the second, r29 is set 34 instructions earlier:

8226A22C  addi r29, r24, 272     ; r24 = the ScriptPhase
8226A2B4  addi r3,  r29, 12      ; -> phase + 272 + 12
          bl   0x8226EAB8        ; the generic append

phase + 272 + 12 is exactly the inner list whose count the corpus watched at phase + 272 + 20. sub_8226A0D8 is the appender.

🔑 And it is a BUILT-IN, so the script appends its own triggers

sub_8226A0D8 is ScriptPhase vtable slot 28, and slot 28 is what built-in 25's stub tail-calls. Its entry is the standard built-in shape — r3 = the phase, r4 = the local[] base, [phase+324] indexed by local[4].

There are ZERO real bl 0x8226A0D8 instructions in the image. It is reached only through the vtable, i.e. only as a built-in. So the queue is populated by the script, not by the engine — which retires the residual "the engine moves records into the phase's queue each frame" reading that isl-builtins.md already flagged as unsupported by its call site.

Built-in 25's argument shape agrees with the corpus's independently measured tables: it is in UNIT_ARG (a unit at local[4]) and in SYM1_SLOTS[12] (a symbol-table-1 name — a route, message or objective — at local[12]).

🔴 REFUTED: this does not explain the unreachable routines

The attractive next step was that built-in 25 carries the coroutine entry a trigger fires, which would have seeded the 389 routines reachable from no static entry. It does not.

At Stage 02's built-in 25 sites, taking each staged operand as a code offset from the phase base:

staged slot values landing on an instruction boundary
0 2 0 — 0 %
4 2 0 — 0 %
8 2 0 — 0 %
12 2 0 — 0 %
control: an arbitrary 4-aligned offset 38.7 %

Nothing scores even at chance. And the scale is wrong anyway: built-in 25 has only 2 call sites in Stage 02 — far too few to account for ~15 % of the code being unreachable.

So the trigger queue is now identified, but what starts the unreachable routines remains open, and it is not this.

🟡 Not settled

  • What a trigger node contains, and what consumes it. The append is located; the node's own layout is not read.
  • The other +12 caller, 0x8226E208 in sub_8226E160, appends to some other embedded list — not shown to be the trigger queue.
  • Built-in 25 is still unnamed. "Appends to the trigger container" is what it does mechanically; what the trigger means needs the consumer.