They are start / read / stop of one of 32 per-phase stopwatches, not flag operations. 123-127 keep timer_* -- that is the mission timer, five scalars at [phase+304..320], a different clock. Artefact check: 84 lines changed across 5 files and all 84 pair exactly with their old-name partners once column padding is normalised (0 removed lines without an old name, 0 added lines without a new one). data/isl-timers.txt reproduces the same 675/675, 11.2 % control, 82/1 and identical histograms, which is what shows the rename is cosmetic. Also withdraws a label from the previous commit: sub_8230C398 is NOT the message pump. It runs every frame but drains nothing -- a state machine on [0x828E1F8C] that only allocates, builds strings, looks up and PUSHES. And bus+8216 is weak evidence: sub_82254A08 is a generic map find with ~120 sites, and the key looked up is a pointer, not a tag. The open handle is now the ring buffer at bus+4, not bus+8216.
6.3 KiB
✅ A ScriptPhase owns 32 STOPWATCHES, and they count SECONDS
read_freg's unit was the open question: the corpus knew the built-in returns a
float out of [phase+88], and that seconds was the unit had only ever been
inferred from the values a mission compares it against (210, 300, 600, 1200).
It is now read, end to end, from the code that advances it.
The three parallel arrays
The phase initialiser sub_82270DF8 clears three arrays in one unrolled loop
(0x82270EC0–0x82270FAC), eight words per iteration, base 12, 44, 76, 108
— 32 entries each, offsets 0…127:
| field | type | init | meaning |
|---|---|---|---|
[phase+88][i] |
f32 | 0.0 | the timer's current value |
[phase+104][i] |
f32 | 0.0 | its value last frame |
[phase+120][i] |
int | 0 | 1 = running |
sub_822700C0 clears the same three the same way (the phase reset).
What advances them — sub_822710D0(phase, dt)
Called from ScriptPhase::Update (sub_82263408) at 0x82263480, with
Update's own float argument passed straight through (fmr f30,f1 … fmr f1,f30).
The body is the same eight-way unrolled shape, and per entry it is exactly:
82271100 stfsx f0, r11, r9 ; prev[i] = cur[i] ([+104] <- [+88])
82271104 lwz r10, 120(r31)
8227110C cmpi cr6, 0, r10, 1
82271110 bc 4, eq, ... ; skip unless running[i] == 1
8227111C fadds f0, f31, f0 ; cur[i] += dt
82271120 stfsx f0, r11, r10
The three built-ins are one family — start / read / stop
isl-builtins.md groups 8, 9 and 93 together; that grouping is confirmed, but
the names are wrong. All three are inline in the dispatch switch sub_82272220,
and all three bounds-check 0 <= local[0] < 32 — the array length above.
| built-in | corpus name | what it actually does |
|---|---|---|
| 8 | set_flag |
cur[i] = 0.0 (the constant at 0x8209FD28 is literally 0.0) and running[i] = 1 — start / restart timer i; returns 1 |
| 9 | read_freg |
returns cur[i] — as a double into [phase+176], not [phase+164] |
| 93 | clear_flag |
running[i] = 0; i == -1 clears all 32 (0x82273078, loop to 128 step 4) — stop, without resetting the value |
clear_flag is called 133 times disc-wide and every one passes −1, always in
the sequence reset_phase_threads ; timer_stop ; clear_flag(-1) — a phase
teardown. So [phase+120] is not "a different 32-entry array" belonging to some
other family, as the previous iteration's note allowed for: it is the running
column of this one.
✅ The unit: SECONDS, by four constants
dt comes from a 48-byte timing singleton at [0x828F35B4] (constructed by
sub_8231A830), field +8. Its value is built in the frame loop
sub_821AA1B0 and in sub_821A49A8:
821AA2F8 lfs f0, -24328(r11) ; 0x8289A0F8 = 0.016666668 == 1/60
821AA300 fmuls f13, f31, f0 ; f31 = frames elapsed -> SECONDS
821AA308 lfs f0, 5772(r25) ; 0x820A13B4 = 10000.0
821AA30C fmuls f0, f13, f0
821AA310 fctiwz f0, f0 ; ticks = round(seconds * 10000) [100 us]
821AA32C cmpi cr6, 0, r29, 3200 ; clamp -> 0.32 s max frame
821AA390 lfs f0, -24324(r11) ; 0x8289A0FC = 1e-4
821AA398 fmuls f0, f13, f0
821AA39C stfs f0, 12(r11) ; [obj+12] = ticks * 1e-4 -> SECONDS again
and sub_821A49A8 writes the two scaled copies the game actually reads —
[obj+16] = ticks·s24·1e-4·s44 and [obj+8] = ticks·s40·1e-4, where
s24 = s40 = s44 = 1.0 at construction (0x8208583C). [obj+16] is the field
the flight/physics code reads (≈40 sites in 0x8238…–0x823B…); [obj+8] is
the one the script VM gets.
The round-trip is what makes this non-circular: the value is produced as
seconds × 10000 and consumed as × 1e-4. It is seconds on both ends, and the
clamp is 3200 ticks = 0.32 s — a frame-time ceiling, which is only a
sensible number in seconds.
⇒
read_freg(i)returns the number of SECONDS sinceset_flag(i).
✅ The refutation test — and it passes 675 / 675
If these really are stopwatches that only run once started, then every
timeline entry (isl-schedule) must name a timer its own
phase starts — otherwise cur and prev both stay 0.0 and the walker's fire
test prev <= t < cur can never be true, and the entry would be dead.
timeline entries whose kind is started by set_flag in the same phase |
675 / 675 = 100 % |
| entry-weighted control (a random index 0…31) | 11.2 % |
read_freg indices started in the same phase |
82 / 83 |
Every phase opens with set_flag(5) and set_flag(0) — which is why kind only
ever takes the values 0 and 5. Indices used disc-wide run 0…21, inside the
bounds check.
⚠️ The first run of this test scored 433/675 and was wrong. isl.dis
defaults to stop_at_ret=True, so a set_flag sitting at the start of a
coroutine lost its operand staging to the preceding ret and came back
unresolved. With stop_at_ret=False all 172 set_flag arguments resolve. The
failure looked exactly like a real refutation (the misses were all kind = 0,
in a coherent block of stages) — checked before believing.
🟡 What this leaves
The naming.✅ RENAMED (2026-08-27).isl.pynow emitsstopwatch_start(8) /stopwatch_elapsed(9) /stopwatch_stop(93); built-ins 123–127 keeptimer_*for the other clock (isl-mission-timer), which acts on five scalar fields rather than a 32-entry bank. The rename touched 84 lines across 5 artefacts and every one pairs exactly with its old-name partner once column padding is normalised — nothing but the three names changed, and this file's own artefact reproduces the same 675/675, 11.2 %, 82/1 numbers under the new labels. ⚠️ Older prose in the corpus still uses the old names where it is quoting what was believed at the time; that is deliberate.- The one exception:
read_freg(15)in Stage 06 phase 1 is not started by aset_flag(15)in that phase. Cross-phase carry-over or a start in the unreached code — not chased. s24/s40/s44are never written after construction in any site read here; a game-speed or slow-motion setter was not searched for.- The 31
read_fregsites whose index is computed rather than immediate.