Files
Sylpheed/docs/re/structures/isl-timers.md
Sylpheed RE agent dde62c9afe re: read_freg counts SECONDS -- the 32 "flags" are a stopwatch bank
The open question was the unit, not the array.  Following the writers of
[phase+88] settles it:

  * sub_822710D0(phase, dt), called from ScriptPhase::Update, does
    prev[i] = cur[i] then, only while [phase+120][i] == 1, cur[i] += dt --
    so +88/+104/+120 are current / previous / running, 32 entries each.
  * dt is seconds by a non-circular round trip: frames * (1/60) * 10000
    -> clamp 3200 -> * 1e-4, in the timing singleton at [0x828F35B4].
    The clamp is 0.32 s, a frame ceiling.
  * 675/675 timeline kinds are indices their own phase starts (control
    11.2 %), which is why kind is only ever 0 or 5.

Corrects isl-builtins.md twice: set_flag writes 0.0 not 1.0, and
clear_flag clears the running flag rather than the value.  Confirms its
grouping of 8/9/93 as one family.  Docs only -- all seven ISL artefacts
regenerate byte-identical.
2026-08-27 08:48:49 +00:00

6.0 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 (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 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. set_flag / read_freg / clear_flag describe none of this; timer_start / timer_elapsed / timer_stop would. They are not renamed here because built-ins 127/123/124 already hold timer_set / timer_resume / timer_stop in the corpus for a different timer, and untangling the two is its own change. Every artefact regenerates byte-identical as a result.
  • 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.