Files
Sylpheed/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

128 lines
6.3 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**.~~ ✅ **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](isl-mission-timer.md)), 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.