From 3cad5d063f4c63c2b93ae00605a8af65a3c33d9c Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Thu, 27 Aug 2026 08:48:49 +0000 Subject: [PATCH] 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. --- docs/re/BACKLOG.md | 44 +++++++++++ docs/re/INDEX.md | 1 + docs/re/data/isl-timers.txt | 61 +++++++++++++++ docs/re/structures/isl-builtins.md | 29 ++++++- docs/re/structures/isl-schedule.md | 15 +++- docs/re/structures/isl-timers.md | 122 +++++++++++++++++++++++++++++ 6 files changed, 268 insertions(+), 4 deletions(-) create mode 100644 docs/re/data/isl-timers.txt create mode 100644 docs/re/structures/isl-timers.md diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 288ba83f..1daeb901 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -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 diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index b9c6a1aa..47f9a4e8 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -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 | β€” | diff --git a/docs/re/data/isl-timers.txt b/docs/re/data/isl-timers.txt new file mode 100644 index 00000000..9b61c60c --- /dev/null +++ b/docs/re/data/isl-timers.txt @@ -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} diff --git a/docs/re/structures/isl-builtins.md b/docs/re/structures/isl-builtins.md index 973716d2..94f34cef 100644 --- a/docs/re/structures/isl-builtins.md +++ b/docs/re/structures/isl-builtins.md @@ -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`: diff --git a/docs/re/structures/isl-schedule.md b/docs/re/structures/isl-schedule.md index 9b2a0807..3838de79 100644 --- a/docs/re/structures/isl-schedule.md +++ b/docs/re/structures/isl-schedule.md @@ -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. diff --git a/docs/re/structures/isl-timers.md b/docs/re/structures/isl-timers.md new file mode 100644 index 00000000..b3fe1ec4 --- /dev/null +++ b/docs/re/structures/isl-timers.md @@ -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.