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.
This commit is contained in:
@@ -83,7 +83,7 @@ two tables already parsed in [mission-script-ssb](mission-script-ssb.md).
|
||||
| **33 / 34** | `global_counter0/1` | two global counters read straight into `[+164]` |
|
||||
| **132–134** | player gauges | speed/boost ratios and a player byte |
|
||||
| **73, 123–127** | timer family | start / resume / stop / reset / read elapsed / read limit |
|
||||
| **8 / 9 / 93** | `set_flag` / `read_freg` / `clear_flag` | latch a result into the 32-entry files |
|
||||
| **8 / 9 / 93** | `set_flag` / `read_freg` / `clear_flag` | start / read / stop one of 32 **stopwatches** — [isl-timers](isl-timers.md); the names predate the reading |
|
||||
| **100 / 115** | `reset_phase_threads` / `named_event` | ❌ 100 is **not** `push_trigger` — see below |
|
||||
|
||||
**The state machine is therefore:** a trigger fires a coroutine → the coroutine
|
||||
@@ -690,6 +690,33 @@ That closes the middle of the `set_flag → … → END_PHASE` chain: a conditio
|
||||
coroutine latches a flag, and another coroutine reads it back with `read_freg`
|
||||
and branches on it.
|
||||
|
||||
### 🔴 CORRECTION (2026-08-27) — it is not a latch, it is a STOPWATCH
|
||||
|
||||
Two things above are wrong, and the second follows from the first.
|
||||
Full reading in **[isl-timers](isl-timers.md)**.
|
||||
|
||||
* **`set_flag(i)` writes `0.0`, not `1.0`.** The constant it loads
|
||||
(`addis 0x820A ; addi -724 ; lfs f0,-4(r10)` → `0x8209FD28`) is literally
|
||||
`0.0` — the same constant the phase initialiser uses to clear the array.
|
||||
* **`clear_flag(i)` does not zero entry `i`.** It writes `[phase+120][i] = 0`
|
||||
only; the value in `[phase+88][i]` is left alone. All 133 call sites disc-wide
|
||||
pass **−1**, which clears all 32 flags.
|
||||
|
||||
The array is not a flag file. `sub_822710D0`, called every frame from
|
||||
`ScriptPhase::Update`, does `[phase+104][i] = [phase+88][i]` and then
|
||||
`[phase+88][i] += dt` **for every i whose `[phase+120][i] == 1`**. So:
|
||||
|
||||
```
|
||||
set_flag(i) -> START/RESTART timer i (value 0.0, running 1)
|
||||
read_freg(i) -> SECONDS since that start, as a double in [phase+176]
|
||||
clear_flag(i) -> STOP timer i (or all 32 for i = -1); value retained
|
||||
```
|
||||
|
||||
`dt` is seconds — `frames * (1/60) * 10000` ticks, clamped at 3200, scaled back
|
||||
by `1e-4`. The `set_flag → … → END_PHASE` chain is still real, but it is a
|
||||
*timeout*, not a latch: the condition coroutine starts a clock and a later one
|
||||
branches on how long it has run.
|
||||
|
||||
## 🟡 `op10` + `op13` look like a switch
|
||||
|
||||
Seen repeatedly, e.g. at `0x5774`:
|
||||
|
||||
@@ -143,8 +143,17 @@ inference from the layout; it is now a read.
|
||||
## 🟡 Not settled
|
||||
* **This is not what starts the unreachable code.** All 675 targets are already
|
||||
reachable — **0 are unreached run-starts** — so the ~15 % gap stands.
|
||||
* **What the six clocks are.** `kind` indexes arrays at `[phase+88]` and
|
||||
`[phase+104]`; only indices 0 and 5 are ever used by the data, and neither
|
||||
array's contents were traced to a source.
|
||||
* ~~**What the six clocks are.**~~ ✅ **SETTLED — [isl-timers](isl-timers.md).**
|
||||
The two arrays are a bank of **32 stopwatches**: `[phase+88]` is the current
|
||||
value, `[phase+104]` last frame's, `[phase+120]` a *running* flag, and
|
||||
`sub_822710D0` — called from `ScriptPhase::Update` — does
|
||||
`prev[i] = cur[i]; if running[i] then cur[i] += dt` every frame. Built-in 8
|
||||
starts a timer, 9 reads it, 93 stops it. So the walker's `prev <= t < cur`
|
||||
test is "did timer `kind` cross `t` this frame", and **`t` is in SECONDS** —
|
||||
`dt` is `frames * (1/60) * 10000` ticks scaled back by `1e-4`, clamped at
|
||||
3200 ticks = 0.32 s. That the numbers in the table *looked* like mission times
|
||||
is no longer the evidence. **675 / 675 timeline entries name a timer their own
|
||||
phase starts** (control 11.2 %), which is why `kind` is only ever 0 or 5:
|
||||
every phase opens `set_flag(5); set_flag(0)`.
|
||||
* **`sub_822737C8`**, which actually starts the routine, is not read — it is
|
||||
presumably the same spawner `start_coroutine` uses, but that is unchecked.
|
||||
|
||||
122
docs/re/structures/isl-timers.md
Normal file
122
docs/re/structures/isl-timers.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# ✅ 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;
|
||||
`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.
|
||||
Reference in New Issue
Block a user