This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/structures/isl-schedule.md
Sylpheed RE agent 3cad5d063f 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.
2026-08-27 08:48:49 +00:00

160 lines
7.0 KiB
Markdown
Raw Permalink 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.
# ✅ 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 01170, 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.