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:
Sylpheed RE agent
2026-08-27 08:48:49 +00:00
parent 0033047768
commit dde62c9afe
6 changed files with 268 additions and 4 deletions

View File

@@ -249,6 +249,50 @@ unknown, what evidence exists, and what the first step would be. Move an item in
INFERRED from the 210/300/600/1200 gate values, exactly as before. 🟡 Also open:
what writes `[phase+88]` — 73 writers in the ISL region alone.
* ✅🔴 **(2026-08-27) THE UNIT IS SETTLED — `read_freg` RETURNS SECONDS, and the
three "flag" built-ins are a STOPWATCH BANK. [structures/isl-timers](structures/isl-timers.md).**
Last iteration found what `read_freg` reads and said plainly it had *not*
answered the item, which was the unit. Following the writers does.
**`[phase+88]`, `[phase+104]`, `[phase+120]` are three parallel 32-entry
arrays**, cleared together by the phase initialiser in one unrolled loop
(bases 12/44/76/108, offsets 0…127). **`sub_822710D0(phase, dt)` — called from
`ScriptPhase::Update` at `0x82263480` with Update's own float passed straight
through — does `prev[i] = cur[i]` then `if running[i] == 1: cur[i] += dt`.**
So `[phase+104]` is last frame's value and `[phase+120]` the running flag, and
the walker's `prev <= t < cur` is "did timer `kind` cross `t` this frame".
🔑 **`dt` is SECONDS, by a round trip that is not circular**: the frame loop
`sub_821AA1B0` computes `frames × (1/60)` (`0x8289A0F8` = 0.016666668), turns
it into integer ticks with `× 10000.0` (`0x820A13B4`), **clamps at 3200**, and
`sub_821A49A8` scales back by `1e-4` (`0x8289A0FC`) into the timing singleton
at `[0x828F35B4]``+8` for the script VM, `+16` for the flight code. Produced
as seconds × 10⁴, consumed as × 10⁻⁴; the clamp is **0.32 s**, a frame ceiling.
**Refutation test, 675 / 675 vs an 11.2 % control**: if a timer only runs
once started, every timeline `kind` must be an index its own phase starts —
and every one is. That is also why `kind` is only ever 0 or 5: **every phase
opens `set_flag(5); set_flag(0)`.** Artefact `data/isl-timers.txt`; all seven
existing artefacts regenerate **byte-identical**.
🔴 **Two corrections to `isl-builtins.md`.** `set_flag(i)` writes **`0.0`**,
not `1.0` — the constant at `0x8209FD28` is literally zero, the same one the
initialiser clears with; and `clear_flag(i)` does **not** zero the value, it
clears `[phase+120][i]` only (all 133 sites disc-wide pass **1** = all 32, in
the teardown `reset_phase_threads ; timer_stop ; clear_flag(-1)`). So the
family is start / read / stop, and the earlier "latch" reading was a timeout
all along. ✅ The corpus's grouping of 8/9/93 is **confirmed**`[phase+120]`
is this family's *running* column, not a different array as last iteration's
note allowed.
⚠️ **The first run of the test scored 433/675 and looked like a real
refutation** (misses all `kind = 0`, in a coherent block of stages). Cause:
`isl.dis` defaults to `stop_at_ret=True`, so a `set_flag` at the *start* of a
coroutine lost its operand staging to the preceding `ret`. Checked before
believing — the corpus's own trap, paid again.
🟡 Not settled: the **names** (`set_flag`/`read_freg`/`clear_flag` describe
none of this, but 127/123/124 already hold `timer_set`/`timer_resume`/
`timer_stop` for a different timer, so renaming is its own change); one
exception, `read_freg(15)` in Stage 06 phase 1 has no `set_flag(15)` in that
phase; the three scale fields `s24`/`s40`/`s44` (all 1.0 at construction) have
no writer searched for — a game-speed setter would be one; and 31 `read_freg`
sites index by a computed value rather than an immediate.
## ✅✅ SOLVED — the mission freeze was a modal sign-in dialog (2026-08-26)
`XamShowSigninUI` opens a modal dialog and `xeXamDispatchDialog` blocks the

View File

@@ -98,6 +98,7 @@ files, which is how the same ground got covered twice.
| [`structures/sound-pak-contents.md`](structures/sound-pak-contents.md) | Census of `sound.pak`, and the limit of the leading-region rule | ✅ CONFIRMED, 5 135/5 135 names hash into the TOC, **9 519/9 519** entries accounted for, and a full 4 114-bank manifest (408.3 min of audio) computed from PsuedoBytesPerSec without decoding; ⚠️ leading-region rule holds for 1 571/4 382 eng and 0/5 100 jpn |
| [`structures/sound-cue-table.md`](structures/sound-cue-table.md) | The cue index in `tables.pak` — message id -> cue -> sound id -> `.slb` bank | ✅ CONFIRMED, 1 326/1 338 script message ids bind to a bank; SOUNDS and FILES agree on the same 12 absentees, 0 orphan files |
| [`structures/cutscene-message-table.md`](structures/cutscene-message-table.md) | Cutscene dialogue — speaker, portrait, on-screen seconds, audio cue per page | ✅ CONFIRMED, field count = 9·PageCount+2 for all 7 PageCounts, 1 252/1 252 caption keys match, 138 ids close both ways |
| [`structures/isl-timers.md`](structures/isl-timers.md) | A ScriptPhase owns 32 stopwatches, and they count seconds | ✅ CONFIRMED — the advance is read from `sub_822710D0`, the unit from |
| [`structures/isl-message-dialogue-link.md`](structures/isl-message-dialogue-link.md) | Mission scripts as dialogue — built-in 64 -> message id -> caption text | ✅ CONFIRMED total, 2 683/2 683 call sites across all 28 stages resolve, no residue |
| [`structures/mission-objective-counter.md`](structures/mission-objective-counter.md) | `REMAINING OB` — the mission's own objective counter, in RAM | ✅ CONFIRMED for one Stage 02 run: a big-endian u32 whose value |
| [`structures/movie-subtitles.md`](structures/movie-subtitles.md) | Movie subtitles & the movie ↔ mission ↔ text chain | — |

View File

@@ -0,0 +1,61 @@
# The 32 per-phase stopwatches -- set_flag / read_freg / clear_flag vs the timeline
Written by hand from tools/re-capture/isl.py; see docs/re/structures/isl-timers.md.
Claim under test: `[phase+88][i]` only advances while `[phase+120][i] == 1`,
and built-in 8 is the only thing that sets that flag. So every timeline
entry's `kind` must be an index its own phase starts, or the entry is dead.
file phase set_flag(i) started read_freg(i) timeline kinds
Stage01.ssb 1 [0, 5, 14] [0] {0: 27, 5: 3}
Stage01.ssb 2 [0, 5, 14] [0] {0: 24, 5: 3}
Stage02.ssb 1 [0, 5, 8] [0, 8] {0: 21, 5: 4}
Stage02.ssb 2 [0, 5, 8, 14, 15] [0, 8] {0: 10, 5: 3}
Stage02.ssb 3 [0, 5, 14, 15] [0] {0: 14, 5: 4}
Stage03.ssb 1 [0, 5, 14] [0] {0: 16, 5: 4}
Stage03.ssb 2 [0, 1, 5, 14] [0, 1, 14] {0: 29, 5: 3}
Stage03.ssb 3 [0, 1, 5, 14, 15] [0, 1] {0: 14, 5: 3}
Stage04.ssb 1 [0, 5, 14] [0, 14] {0: 8, 5: 3}
Stage04.ssb 2 [0, 1, 5, 14] [0, 1] {0: 13, 5: 3}
Stage05.ssb 1 [0, 1, 5] [0, 1] {0: 16, 5: 3}
Stage05.ssb 2 [0, 5, 8] [0, 8] {0: 6, 5: 3}
Stage06.ssb 1 [0, 5] [0, 15] {0: 18, 5: 3}
Stage06.ssb 2 [0, 5, 14, 15] [0] {0: 6, 5: 3}
Stage06.ssb 3 [0, 1, 5, 14, 15] [0, 1, 15] {0: 7, 5: 3}
Stage07.ssb 1 [0, 5, 15] [0, 15] {0: 10, 5: 3}
Stage07.ssb 2 [0, 5, 15] [0, 15] {0: 3, 5: 3}
Stage08.ssb 1 [0, 1, 5, 15] [0, 1, 15] {0: 17, 5: 5}
Stage08.ssb 2 [0, 5, 15] [0, 15] {0: 13, 5: 3}
Stage09.ssb 1 [0, 5, 14, 15] [0] {0: 10, 5: 3}
Stage09.ssb 2 [0, 5, 14] [0] {0: 7, 5: 3}
Stage09.ssb 3 [0, 5, 14, 15] [0] {0: 11, 5: 3}
Stage10.ssb 1 [0, 5, 14, 15] [0] {0: 8, 5: 3}
Stage11.ssb 1 [0, 5] [0] {0: 14, 5: 3}
Stage11.ssb 2 [0, 5, 15] [0, 15] {0: 5, 5: 3}
Stage12.ssb 1 [0, 1, 5, 15] [0, 1, 15] {0: 17, 5: 3}
Stage13.ssb 1 [0, 2, 5, 15] [0, 2, 15] {0: 25, 5: 3}
Stage14.ssb 1 [0, 5, 15] [0, 15] {0: 8, 5: 3}
Stage14.ssb 2 [0, 5, 15] [0, 15] {0: 11, 5: 3}
Stage15.ssb 1 [0, 5, 15] [0, 15] {0: 20, 5: 3}
Stage15.ssb 2 [0, 5, 10, 15] [0, 10, 15] {0: 21, 5: 3}
Stage16.ssb 1 [0, 5] [0] {0: 17, 5: 3}
Stage18.ssb 1 [0] [] {0: 3}
Stage19.ssb 1 [0] [] {0: 3}
Stage20.ssb 1 [0] [] {0: 3}
Stage21.ssb 1 [0] [] {0: 3}
Stage22.ssb 1 [0] [] {0: 3}
Stage23.ssb 1 [0] [] {0: 3}
Stage24.ssb 1 [0, 1, 5] [0, 1] {0: 11, 5: 3}
Stage25.ssb 1 [0, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] [0, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] {0: 14, 5: 3}
Stage26.ssb 1 [0, 5] [0] {0: 22, 5: 3}
Stage27.ssb 1 [0, 5] [0] {0: 16, 5: 3}
Stage28.ssb 1 [0, 5] [0] {0: 5, 5: 3}
Stage29.ssb 1 [0, 5] [0] {0: 24, 5: 3}
timeline entries whose kind is started in the same phase : 675 / 675 = 100.0%
entry-weighted control (a random index in 0..31) : 11.2%
read_freg indices started in the same phase : 82 yes / 1 no ['Stage06.ssb ph1 read_freg(15)']
set_flag argument histogram: {0: 44, 1: 10, 2: 1, 4: 2, 5: 38, 6: 2, 7: 2, 8: 5, 9: 2, 10: 4, 11: 2, 12: 1, 13: 1, 14: 21, 15: 24, 16: 1, 17: 3, 18: 3, 19: 2, 20: 2, 21: 2}
read_freg argument histogram: {0: 105, 1: 13, 2: 2, 4: 1, 6: 2, 7: 2, 8: 5, 9: 2, 10: 3, 11: 2, 12: 3, 13: 1, 14: 5, 15: 28, 16: 1, 17: 3, 18: 3, 19: 2, 20: 2, 21: 2, None: 31}
clear_flag argument histogram: {-1: 133}

View File

@@ -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]` |
| **132134** | player gauges | speed/boost ratios and a player byte |
| **73, 123127** | 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`:

View File

@@ -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.

View 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.