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 d79f59d24e re: timer_set's second argument is a SECOND countdown, not a warning threshold
mission-phase-timers.md left 180 open as "a limit and a warning
threshold is the obvious reading, but not established".  Reading
sub_822639B8 -- ScriptPhase::Update 0x82263528, same dt as the
stopwatch bank -- settles it the other way:

    if running: [+304] += dt
    if armed:   [+308] -= dt   while [+308] > 0
                else [+312] -= dt, clamped at 0

[+312] is never compared with [+308]; it is decremented, and only in
the A<=0 arm.  Two sequential countdowns.  Disc-wide the second
argument is 180 in all 29 timer_set sites while the first varies
(600 x19, 1200 x8, 900, 1800).

Built-ins 123-127 are vtable slots 90-94 on five scalars at
[phase+304..320].  125 and 126 have ZERO call sites in all 28 scripts:
the script arms, starts and stops this clock but never reads it.

Corrects mission-phase-timers.md, which merged this clock with
stopwatch 0 -- timer_resume starts [+304], set_flag(0) one instruction
later starts the stopwatch the timeline's kind=0 reads.

Docs only; all seven ISL artefacts regenerate byte-identical.
2026-08-27 09:03:18 +00:00

125 lines
6.1 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.
# ✅ 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](isl-schedule.md)) 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.
✅ The other timer is now read — [isl-mission-timer](isl-mission-timer.md) —
so the collision is understood: 123/124/125/126/127 act on five *scalar*
fields (`[phase+304…320]`), this family on a 32-entry *bank*. `stopwatch_start`
/ `stopwatch_elapsed` / `stopwatch_stop` would separate them. Still not renamed
here, so every artefact regenerates byte-identical; the rename is its own
change and its own artefact diff.
* 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.