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.
160 lines
7.0 KiB
Markdown
160 lines
7.0 KiB
Markdown
# ✅ Each phase carries a TIMELINE — 675 scheduled routines across the disc
|
||
|
||
The trailing data table found at the end of every phase region
|
||
([isl-stream-entry-points](../isl-stream-entry-points.md)) is decoded. It is the
|
||
mission's **scripted event schedule**.
|
||
|
||
## Layout
|
||
|
||
After the phase's code ends — at the first value of that phase's mission-level
|
||
`0x1883` record — comes a run of 8-byte typed records, tag `0x19` = int,
|
||
`0x1A` = IEEE float:
|
||
|
||
```
|
||
int N -- entry count
|
||
N x [ int offset ; float t ; int kind ]
|
||
```
|
||
|
||
`1 + 3N` matches the record count in **every** phase measured: Stage 02's three
|
||
phases hold 76, 40 and 55 records for N = 25, 13 and 18.
|
||
|
||
## ✅ The checks
|
||
|
||
| | |
|
||
|---|---|
|
||
| schedule entries disc-wide | **675** |
|
||
| `0x1A` float records disc-wide | **675** — the same number, independently counted |
|
||
| offsets landing on the instruction stream | **675 / 675 = 100.0 %** |
|
||
| control: random 4-aligned offsets | 33.3 % |
|
||
|
||
The float count and the entry count are derived by different routes and agree
|
||
exactly, and every single offset resolves. `kind` is **0** (556) or **5** (119).
|
||
|
||
## ✅ The floats are seconds
|
||
|
||
The distribution is unmistakable: 0, 0.5, 1, 4, 5, 30, 50, 60, 90, 120, 150,
|
||
170, 180, 210, 240, 270, 300, 330, 360, 420, 570, 1020, 1080, 1140, 1170 …
|
||
mission times, not fractions.
|
||
|
||
And the targets are what a schedule would point at — small one-shot coroutines:
|
||
|
||
```
|
||
002C04: set.i local[0] = 1
|
||
002C1C: set.i local[4] = 1
|
||
002C34: set.f local[8] = 0.4
|
||
002C50: call builtin106(0x1, 0x1, 0.4)
|
||
002C5C: call end_coroutine
|
||
```
|
||
|
||
## 🟡 A runtime cross-check — consistent, and not proof
|
||
|
||
The closed `REMAINING OB` work measured, on the emulator over n=5 runs, that
|
||
Stage 02's squadrons arrive at **t = 0, 120 and 210 s**. All three times are in
|
||
phase 1's static schedule, and **120 and 210 each appear twice**, which is what
|
||
two squadrons arriving together would look like.
|
||
|
||
⚠️ Stated as consistency rather than confirmation: these are round numbers, and
|
||
phase 1 has ~22 distinct times spread over 0–1170, so three specified round
|
||
values all being present is not by itself unlikely. The **doubling** is the
|
||
sharper detail, and it was not predicted in advance.
|
||
|
||
## Artefacts
|
||
|
||
`data/isl-stage02-schedule.txt` and `data/isl-schedule-all.txt` (all 28 stages,
|
||
675 entries), generator `isl_report.py schedule`. The four earlier artefacts
|
||
regenerate byte-identical.
|
||
|
||
## ✅ The CONSUMER — `sub_822748D0`, called from `ScriptPhase::Update`
|
||
|
||
Found statically. The phase initialiser `sub_82270DF8` takes the `0x1883`
|
||
record's six words as arguments and stores
|
||
|
||
```
|
||
82270FC8 stw r26, 232(r30) ; [phase+232] = code base (already documented)
|
||
82270FDC add r10, r26, r21 ; base + entry_a
|
||
82270FEC stw r10, 240(r30) ; [phase+240] = THE TABLE POINTER
|
||
```
|
||
|
||
`[phase+240]` has exactly **two** readers in the ISL region, and one of them,
|
||
`sub_822748D0`, is called from **`sub_82263408` = `ScriptPhase::Update`**. It is
|
||
the timeline walker, and it confirms every field of the decode independently:
|
||
|
||
```
|
||
822748E0 lwz r11, 240(r30) ; the table
|
||
822748E4 lwz r10, 4(r11) ; N -- the count, at table+4 ✓
|
||
822748E8 addi r11, r11, 8 ; skip the count record ✓
|
||
822748F4 addi r31, r11, 20 ; -> group+20
|
||
822748FC lwz r11, 0(r31) ; kind at group+20 ✓
|
||
82274900 lfs f0, -8(r31) ; t at group+12, as a FLOAT ✓
|
||
82274904 lwz r10, 104(r30) ; clock array A
|
||
82274908 rlwinm r11, r11, 2,0,29 ; kind * 4
|
||
8227490C lfsx f13, r10, r11 ; A[kind]
|
||
82274910 fcmpu cr6, f13, f0
|
||
82274914 bc ... skip
|
||
82274918 lwz r10, 88(r30) ; clock array B
|
||
8227491C lfsx f13, r10, r11 ; B[kind]
|
||
82274920 fcmpu cr6, f0, f13
|
||
82274924 bc ... skip
|
||
8227492C lwz r5, -16(r31) ; offset at group+4 ✓
|
||
82274930 lwz r4, 232(r30) ; the code base
|
||
82274934 bl 0x822737C8 ; start the routine
|
||
8227493C addi r31, r31, 24 ; STRIDE 24 = three 8-byte records ✓
|
||
```
|
||
|
||
Count position, float position, offset position and the 24-byte stride are all
|
||
confirmed by the engine's own reader rather than by my structural inference.
|
||
|
||
### ✅ `kind` is a CLOCK INDEX, not a flag
|
||
|
||
🔑 **(2026-08-27) And the array it indexes is the one `read_freg` exposes.**
|
||
Built-in 9's stub bounds-checks `0 <= local[0] < 32` and then
|
||
|
||
```
|
||
822725E8 lwz r10, 88(r31) ; [phase+88]
|
||
822725EC rlwinm r11, r11, 2, 0, 29 ; i * 4
|
||
822725F0 lfsx f0, r11, r10 ; a FLOAT
|
||
```
|
||
|
||
— exactly the array and the indexing the timeline walker uses. So `kind` is not a
|
||
selector over some small private set of clocks: **it is an index into the same
|
||
32-entry float register file the script reads with `read_freg(i)`.** A clear
|
||
condition like `read_freg(0) < 1200` and a timeline entry with `kind = 0` read
|
||
the same register.
|
||
|
||
🔴 The "six clocks" framing below was mine and is too narrow — the file has **32**
|
||
slots; the timeline data merely never uses more than indices 0 and 5.
|
||
|
||
`rlwinm r11, r11, 2, 0, 29` then `lfsx` — `kind` is multiplied by 4 and used to
|
||
index **float arrays at `[phase+104]` and `[phase+88]`**. So `kind` selects
|
||
*which timer* the entry is scheduled against, and 0 / 5 are two of at least six.
|
||
|
||
The entry fires only when its `t` lies between the two clock readings — the
|
||
classic "which events did this frame cross" test, with one array holding the
|
||
previous value and the other the current. 🟡 Which array is which follows from
|
||
the comparison directions (`A[kind] > t` skips; `t >= B[kind]` skips) and is
|
||
stated as a reading of those two branches, not measured at runtime.
|
||
|
||
### ✅ And the clock is PER PHASE
|
||
|
||
Both arrays are fields of the phase object, so each phase has its own timers —
|
||
which is why every phase's table restarts at t = 0. That was previously an
|
||
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.**~~ ✅ **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.
|