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-timers.md
Sylpheed RE agent b86c8c82ba isl: rename built-ins 8/9/93 to stopwatch_start/_elapsed/_stop
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.
2026-08-27 09:21:04 +00:00

6.3 KiB
Raw Blame History

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 (0x82270EC00x82270FAC), eight words per iteration, base 12, 44, 76, 10832 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,f1fmr 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] = 1start / 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 since set_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.py now emits stopwatch_start (8) / stopwatch_elapsed (9) / stopwatch_stop (93); built-ins 123127 keep timer_* 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 a set_flag(15) in that phase. Cross-phase carry-over or a start in the unreached code — not chased.
  • s24 / s40 / s44 are never written after construction in any site read here; a game-speed or slow-motion setter was not searched for.
  • The 31 read_freg sites whose index is computed rather than immediate.