Skip to main content

rustyn64_core/
scheduler.rs

1//! The canonical master-clock scheduler — the heart of the emulator.
2//!
3//! # Timebase model (ADR 0006)
4//!
5//! The tick unit is **187.5 MHz** ([`MASTER_HZ`]) — the LCM of the VR4300's
6//! 93.75 MHz `PClock` and the RCP's 62.5 MHz `MClock`. That makes **every** emulated
7//! clock domain an integer divisor of one counter:
8//!
9//! | Component | Rate | Divider |
10//! |-----------|------------|---------|
11//! | VR4300 `PClock` | 93.75 MHz | 2 |
12//! | RCP (`MClock` / `SClock`) | 62.5 MHz | 3 |
13//! | COP0 `Count` (half `PClock`) | 46.875 MHz | 4 |
14//! | Serial Interface | 15.625 MHz | 12 |
15//! | Cartridge / PIF | 1.953125 MHz | 96 |
16//!
17//! No accumulator, no remainder, no drift — drift is *unrepresentable* rather
18//! than merely avoided. Note the hardware derives the CPU **from** `MClock` (a 3/2
19//! PLL at `DivMode = 0b01`), not the reverse; ADR 0001 had the CPU as master,
20//! which inverted that and is why the RCP needed a fractional remainder.
21//!
22//! Only the VI (VCLK, ~48.68 MHz, off a *different* crystal) and the AI genuinely
23//! are not rational multiples of this tick; those keep a fractional accumulator.
24//!
25//! # The one rule
26//!
27//! **`master_ticks` is the only counter in the core that is ever incremented.**
28//! Every other cycle position is *derived* — see [`System::cpu_cycles`] and
29//! [`System::rcp_cycles`], which are accessors rather than fields. A second
30//! incremented counter agrees with the first only because every call site
31//! remembers to step it: an invariant held by construction rather than by
32//! derivation, correct until one path forgets. The `residue_invariants_never_move`
33//! test exists to catch exactly that.
34//!
35//! A *retired-work* tally is a different thing and is legitimate
36//! (`Cpu::retired`); it counts work done and nothing schedules against it.
37//!
38//! # Lockstep, edge to edge
39//!
40//! This is **LOCKSTEP**, not catch-up: an RCP event (a DP-done IRQ, an SP halt) is
41//! visible to the very next CPU step. But nothing iterates 187.5M times a second —
42//! the CPU lands on every 2nd tick and the RCP on every 3rd, so the pattern repeats
43//! every 6 ticks and [`System::step_to_next_edge`] jumps straight to the next tick
44//! where something is due. The unit is a time base, not a loop counter.
45//!
46//! There are **never** OS threads in the core; one timeline is the whole reason the
47//! determinism contract holds (ADR 0004). Rollback / run-ahead live in the frontend.
48//!
49//! See `docs/scheduler.md` and `docs/adr/0006-one-canonical-master-clock.md`.
50
51// The PRNG step and the skeleton `reset` are flagged const-able, but they will
52// gain non-const bodies (reset warms the Bus subsystems); accept at module level.
53#![allow(clippy::missing_const_for_fn)]
54
55use crate::bus::Bus;
56use rustyn64_cpu::Cpu;
57use serde::{Deserialize, Serialize};
58
59/// The canonical master tick rate: **187.5 MHz**, the LCM of the CPU and RCP clocks.
60///
61/// Beware the name: the *hardware* documentation uses `MasterClock` for the
62/// 62.5 MHz [`RCP_HZ`], and superseded ADR 0001 used it for [`CPU_HZ`]. Always
63/// state the rate — see `docs/glossary.md`.
64pub const MASTER_HZ: u64 = 187_500_000;
65
66/// The VR4300 pipeline clock (`PClock`), 93.75 MHz. `MASTER_HZ / CPU_DIVIDER`.
67pub const CPU_HZ: u64 = 93_750_000;
68
69/// The RCP clock (`MClock`, and the CPU's `SClock` bus rate), 62.5 MHz.
70pub const RCP_HZ: u64 = 62_500_000;
71
72/// Master ticks per VR4300 `PClock`.
73pub const CPU_DIVIDER: u64 = 2;
74
75/// Master ticks per RCP (`MClock`) cycle.
76pub const RCP_DIVIDER: u64 = 3;
77
78/// Ticks after which the whole edge schedule repeats: `lcm(CPU_DIVIDER,
79/// RCP_DIVIDER)`.
80///
81/// A domain steps when `(tick + phase) % divider == 0`, and the phases are
82/// power-on constants (ADR 0006), so *which* domains are due at a tick is a
83/// function of `tick mod` this period and nothing else. The `fast-scheduler`
84/// block is exactly one period: its shape is computed once and replayed, which is
85/// only sound because this really is the repeat length.
86///
87/// Written as an explicit LCM rather than the literal 6 so that changing a divider
88/// cannot leave a stale period behind — a period that is too short would replay a
89/// pattern that no longer aligns, silently stepping the wrong domains.
90pub const EDGE_PERIOD: u64 = {
91    // `lcm(a, b) = a / gcd(a, b) * b`, const-evaluated.
92    const fn gcd(a: u64, b: u64) -> u64 {
93        if b == 0 { a } else { gcd(b, a % b) }
94    }
95    CPU_DIVIDER / gcd(CPU_DIVIDER, RCP_DIVIDER) * RCP_DIVIDER
96};
97
98const _: () = {
99    assert!(
100        EDGE_PERIOD.is_multiple_of(CPU_DIVIDER) && EDGE_PERIOD.is_multiple_of(RCP_DIVIDER),
101        "EDGE_PERIOD must be a common multiple of both dividers, or the \
102         fast-scheduler's replayed pattern drifts out of alignment"
103    );
104};
105
106/// [`EDGE_PERIOD`] as an array length.
107///
108/// The cast cannot truncate — it is the LCM of two single-digit divider constants,
109/// so it is 6 on every target — but clippy sees only `u64 as usize` and cannot know
110/// the value. The assertion below checks exactly that: **that the value survives
111/// the narrowing**, which is the property at issue. (It is not a general round-trip
112/// proof, and an earlier version of this comment overstated it as one.) That is the
113/// difference between a justified `allow` and a silenced lint.
114#[allow(
115    clippy::cast_possible_truncation,
116    reason = "value-checked by the const assertion immediately below"
117)]
118const EDGE_PERIOD_LEN: usize = EDGE_PERIOD as usize;
119
120const _: () = {
121    assert!(
122        EDGE_PERIOD_LEN as u64 == EDGE_PERIOD,
123        "EDGE_PERIOD does not survive narrowing to usize on this target"
124    );
125};
126
127/// Master ticks per COP0 `Count` increment — `Count` runs at **half** `PClock`
128/// (46.875 MHz), per VR4300 User's Manual §6.3.3 Figure 6-3. Forgetting the
129/// halving is a documented source of 2x timing bugs.
130pub const COUNT_DIVIDER: u64 = 4;
131
132/// Master ticks per Serial Interface cycle (15.625 MHz).
133pub const SI_DIVIDER: u64 = 12;
134
135/// Master ticks per cartridge / PIF cycle (1.953125 MHz).
136pub const PIF_DIVIDER: u64 = 96;
137
138/// The CPU:RCP interleaving period: `lcm(CPU_DIVIDER, RCP_DIVIDER)` master ticks.
139///
140/// The CPU lands on ticks 0, 2, 4 and the RCP on 0, 3, so they coincide only
141/// every 6. Seeded power-on phases are offsets within this period.
142pub const PHASE_PERIOD: u64 = 6;
143
144/// A tiny deterministic `SplitMix64` PRNG.
145///
146/// Used ONLY for the seeded power-on phase alignment — the determinism contract
147/// forbids the OS RNG (and system time / thread scheduling) anywhere in the core.
148#[derive(Debug, Clone)]
149struct SplitMix64 {
150    state: u64,
151}
152
153impl SplitMix64 {
154    const fn new(seed: u64) -> Self {
155        Self { state: seed }
156    }
157
158    fn next_u64(&mut self) -> u64 {
159        self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
160        let mut z = self.state;
161        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
162        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
163        z ^ (z >> 31)
164    }
165}
166
167/// The seeded power-on phase offsets, one per clock domain.
168///
169/// These are **constants, not counters** — nothing increments them, so the
170/// one-incremented-counter rule is intact. They must be per-domain: if every
171/// domain keyed off the same absolute tick, the CPU/RCP interleaving would be
172/// byte-identical for every seed from tick 6 onward, and the seeded phase ADR 0004
173/// requires would be decorative — `reset_preserves_phase` would still pass while
174/// testing nothing.
175///
176/// The hardware basis is UM Table 11-1's "**1 to 2** `PCycles`: synchronize with
177/// `SClock`" line, an indeterminacy the vendor documents at exactly this boundary.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
179struct Phases {
180    cpu: u64,
181    rcp: u64,
182}
183
184impl Phases {
185    /// Derive both offsets from one seed. Split out so the modulo reduction is
186    /// provably in range and never appears in the hot path.
187    fn from_seed(seed: u64) -> Self {
188        let mut rng = SplitMix64::new(seed);
189        Self {
190            cpu: rng.next_u64() % CPU_DIVIDER,
191            rcp: rng.next_u64() % RCP_DIVIDER,
192        }
193    }
194}
195
196/// Owns the run loop and ties the CPU to the Bus on one timeline.
197///
198/// Determinism contract: same seed + ROM + input ⇒ bit-identical A/V (ADR 0004).
199#[derive(Debug, Serialize, Deserialize)]
200pub struct System {
201    /// The CPU.
202    pub cpu: Cpu,
203    /// The Bus — owns everything else mutable (RDRAM / RSP / RDP / AI / cart /
204    /// controllers / RCP registers).
205    pub bus: Bus,
206    /// **The** counter. Nothing else in the core is incremented.
207    master_ticks: u64,
208    /// Seeded per-domain power-on phase offsets (constants, not counters).
209    phases: Phases,
210    /// The determinism seed, retained so `reset` re-derives the same phases.
211    seed: u64,
212}
213
214impl System {
215    /// Power on with a determinism seed (drives the per-domain phase alignment).
216    #[must_use]
217    pub fn new(seed: u64) -> Self {
218        Self {
219            cpu: Cpu::new(),
220            bus: Bus::default(),
221            master_ticks: 0,
222            phases: Phases::from_seed(seed),
223            seed,
224        }
225    }
226
227    /// Reset (warm). Re-derives the SAME phase alignment from the retained seed
228    /// so a reset mid-run stays deterministic.
229    pub fn reset(&mut self) {
230        self.phases = Phases::from_seed(self.seed);
231        self.master_ticks = 0;
232        self.cpu = Cpu::new();
233        // The VI scan timeline is keyed off `master_ticks`, so it must rebase to
234        // the new zero — otherwise its delta baseline stays in the old timeline
235        // and the VI interrupt is suppressed until the run catches up.
236        self.bus.vi.reset_scan();
237        // Unlock the PIF ROM and clear the boot NMI freeze so a warm reset can
238        // re-run IPL1→IPL2 on the real-PIF path (the PIF unlocks its ROM on the
239        // reset NMI, `PIF-NUS.md` §Console Reset). No-op under HLE. `Cpu::new`
240        // above already put the PC back at the reset vector `0xBFC0_0000`.
241        self.bus.reset_boot_latches();
242        // TODO(T-CORE-03): warm-reset the remaining Bus subsystems (RSP halt,
243        // clear DMA) without zeroing RDRAM — see `docs/scheduler.md`.
244    }
245
246    /// Master ticks elapsed since power-on. The canonical time position.
247    #[must_use]
248    pub const fn master_ticks(&self) -> u64 {
249        self.master_ticks
250    }
251
252    /// The CPU's position on the timeline, in `PClock`s. **Derived, not stored.**
253    ///
254    /// This is a *position*, not a count of work done — with a nonzero seeded
255    /// phase it is nonzero before the CPU has stepped, which is correct and is
256    /// what the residue invariant pins. For work retired, use [`Cpu::retired`].
257    #[must_use]
258    pub const fn cpu_cycles(&self) -> u64 {
259        (self.master_ticks + self.phases.cpu) / CPU_DIVIDER
260    }
261
262    /// The RCP's position on the timeline, in `MClock`s. **Derived, not stored.**
263    #[must_use]
264    pub const fn rcp_cycles(&self) -> u64 {
265        (self.master_ticks + self.phases.rcp) / RCP_DIVIDER
266    }
267
268    /// The COP0 `Count` *offset* since power-on, at half `PClock`.
269    ///
270    /// Not the architectural register: `Count` is guest-writable via `MTC0`, so
271    /// the real value is affine — `epoch_value + (master_ticks - epoch_tick) /
272    /// COUNT_DIVIDER`, re-based on every write. The affine half lives in the CPU
273    /// (`rustyn64_cpu::cop0`, T-12-003) and is fed from here via `tick_at`; this
274    /// is the timeline half.
275    #[must_use]
276    pub const fn count_ticks(&self) -> u64 {
277        (self.master_ticks + self.phases.cpu) / COUNT_DIVIDER
278    }
279
280    /// Is `tick` an edge for a domain with this divider and phase?
281    const fn is_edge(tick: u64, phase: u64, divider: u64) -> bool {
282        (tick + phase).is_multiple_of(divider)
283    }
284
285    /// The next tick strictly after `tick` at which this domain steps.
286    const fn next_edge_after(tick: u64, phase: u64, divider: u64) -> u64 {
287        let next = tick + 1;
288        let rem = (next + phase) % divider;
289        if rem == 0 {
290            next
291        } else {
292            next + (divider - rem)
293        }
294    }
295
296    /// The next tick strictly after the current one at which any domain is due.
297    const fn next_edge(&self) -> u64 {
298        let next_cpu = Self::next_edge_after(self.master_ticks, self.phases.cpu, CPU_DIVIDER);
299        let next_rcp = Self::next_edge_after(self.master_ticks, self.phases.rcp, RCP_DIVIDER);
300        if next_cpu < next_rcp {
301            next_cpu
302        } else {
303            next_rcp
304        }
305    }
306
307    /// Step every domain due at the *current* tick.
308    ///
309    /// CPU first, then the RCP, so an RCP event lands where the next CPU step
310    /// sees it. Reversing this changes which engine observes whose write first
311    /// and is a determinism-visible change.
312    fn step_due_here(&mut self) {
313        let cpu = Self::is_edge(self.master_ticks, self.phases.cpu, CPU_DIVIDER);
314        let rcp = Self::is_edge(self.master_ticks, self.phases.rcp, RCP_DIVIDER);
315        self.step_domains(cpu, rcp);
316    }
317
318    /// Step the domains the caller has *already determined* are due here.
319    ///
320    /// Split out of [`System::step_due_here`] so the fast path can drive it from a
321    /// precomputed schedule instead of re-deriving the same two predicates on every
322    /// edge. The ordering rule above lives here, in the one place both callers
323    /// reach, so it cannot come apart between them.
324    fn step_domains(&mut self, cpu: bool, rcp: bool) {
325        // A failed real-PIF boot checksum freezes the CPU via NMI until power-off
326        // (`PIF-NUS.md`). Stop stepping it; the RCP keeps running, as on hardware.
327        if cpu && !self.bus.boot_nmi_halt() {
328            // `count_ticks` is derived from `master_ticks`, never incremented,
329            // and the CPU turns it into the guest-writable `Count` (ADR 0006).
330            let count_now = self.count_ticks();
331            self.cpu.tick_at(&mut self.bus, count_now);
332        }
333        if rcp {
334            self.step_rcp();
335        }
336    }
337
338    /// Advance to the next tick at which **any** domain is due, and step every
339    /// domain due at that tick. Returns the tick landed on.
340    ///
341    /// Edge-to-edge: the master tick is never iterated. Hot path — allocation-free.
342    ///
343    /// Note the tick the machine currently sits on is never re-stepped, so tick 0
344    /// is a position rather than an executed edge. Only the *intervals* between
345    /// ticks carry work, which is what keeps the residue invariant constant.
346    pub fn step_to_next_edge(&mut self) -> u64 {
347        self.master_ticks = self.next_edge();
348        self.step_due_here();
349        self.master_ticks
350    }
351
352    /// Run until `master_ticks() == target`, stepping every domain on every edge
353    /// in `(now, target]` — and **not** one past it.
354    ///
355    /// Overshooting would make "how many CPU steps in N ticks" depend on where
356    /// the edges happened to fall, which is exactly the kind of off-by-a-phase
357    /// error the residue invariant is meant to make impossible.
358    pub fn run_until(&mut self, target: u64) {
359        while self.next_edge() <= target {
360            self.master_ticks = self.next_edge();
361            self.step_due_here();
362        }
363        if self.master_ticks < target {
364            self.master_ticks = target;
365        }
366    }
367
368    /// The **fast-path** counterpart to [`System::run_until`] (ADR 0011 / ADR 0012),
369    /// behind the default-off `fast-scheduler` feature.
370    ///
371    /// **The block is one period of the edge schedule**, replayed from a shape
372    /// computed once instead of re-derived per edge. The tail — a partial period,
373    /// plus the `master_ticks = target` landing — still goes through the accurate
374    /// path, which is the ADR 0011 §6 fallback and is where the "bailout" in that
375    /// section's language now lives.
376    ///
377    /// It is a **separate entry point**, not a branch inside `run_until`, which is
378    /// what keeps ADR 0011 §1 exactly true: with the feature off this function does
379    /// not exist and the accurate path has no added test. It also stores no mode on
380    /// [`System`], so the save-state layout is untouched and ADR 0011 §4's header
381    /// marker is not yet owed — that falls due when the fast path acquires state of
382    /// its own.
383    ///
384    /// **The bailout invariant** (ADR 0011 §6) — the accurate scheduler must resume
385    /// with exactly the state it would have held had it run that stretch itself,
386    /// the same `master_ticks` above all, since right state at the wrong tick is
387    /// *correct-but-late* and no AV comparison can see it — is satisfied here **by
388    /// construction rather than by argument**: this enumerates the same edges in the
389    /// same order at the same ticks, so there is no divergence to resynchronize.
390    /// A later slice that skips or reorders work will have to earn the invariant
391    /// instead of inheriting it, and the differential gate is what will say whether
392    /// it did.
393    ///
394    /// # The return value is ADR 0012's completion witness
395    ///
396    /// [`FastRunReport`](crate::fastpath::FastRunReport) says how many whole blocks
397    /// ran and which
398    /// [`BailOut`](crate::fastpath::BailOut) reasons handed a stretch back. ADR
399    /// 0012 §2 makes both suite-wide gate failures — *"the fast path never
400    /// engaged"* and *"no bail-out boundary was reached"* — and neither is
401    /// observable from the outside otherwise: a fast path that quietly deferred
402    /// everything would agree with the accurate one perfectly, which is exactly
403    /// what #224 shipped on purpose while the gate was being written.
404    ///
405    /// It is a **return value, not a field and not a hook**. A field would make
406    /// ADR 0011 §4's save-state mode marker fall due; a hook compiled into a
407    /// release build for its tests is what ADR 0011 §6 forbids. A return value
408    /// adds neither state nor a branch — the function already knows both numbers.
409    #[cfg(feature = "fast-scheduler")]
410    pub fn run_until_fast(&mut self, target: u64) -> crate::fastpath::FastRunReport {
411        use crate::fastpath::{BailOut, BailOutSet, FastRunReport};
412        // The edge schedule is PERIODIC. A domain steps when
413        // `(tick + phase) % divider == 0`, so the pattern of which domains are due
414        // repeats every `lcm(CPU_DIVIDER, RCP_DIVIDER)` ticks — and the phases are
415        // constants fixed at power-on (ADR 0006), so it never changes mid-run
416        // either. The accurate loop re-derives it on every single edge anyway:
417        // `next_edge` for the position, then two `is_edge` tests to attribute it.
418        //
419        // So the block here is one period. Its shape is computed once and then
420        // replayed, which removes the per-edge arithmetic without changing which
421        // domains step, in what order, or at which `master_ticks` — the fast path
422        // is a different *enumeration* of the same edges, not a different schedule.
423        // That is why the bailout invariant (ADR 0011 §6) is satisfied by
424        // construction rather than by argument: there is nothing to resynchronize,
425        // because nothing ever diverged.
426        // Nothing to do, and saying so up front keeps every addition below inside a
427        // range the loop condition has already bounded.
428        //
429        // This is NOT a bail-out: nothing is handed to the accurate scheduler,
430        // because there is nothing to run on either path. Recording it as a reason
431        // would put a variant in the coverage witness that says only "someone
432        // called this twice", and ADR 0012 §2's enumeration is about exits to the
433        // accurate scheduler, not about every early return.
434        if target <= self.master_ticks {
435            return FastRunReport::default();
436        }
437
438        // Only the two predicates are stored: the offset is always `index + 1`, so
439        // keeping it in the array would be a third of the footprint carrying no
440        // information.
441        //
442        // Offsets are 1..=EDGE_PERIOD because `run_until` advances to the next edge
443        // *strictly after* the current tick; offset 0 is the position the machine
444        // already sits on and has already been stepped.
445        let mut pattern = [(false, false); EDGE_PERIOD_LEN];
446        for (i, slot) in pattern.iter_mut().enumerate() {
447            // `saturating_add`: `master_ticks` is at most `u64::MAX`, and a machine
448            // that close to the end of a 3,000-year timeline has no edges left to
449            // find. Saturating keeps the probe from wrapping to an early tick and
450            // reporting an edge that is not there; the loop below then does not run,
451            // because no `base + EDGE_PERIOD` can be `<= target`.
452            let t = self.master_ticks.saturating_add(i as u64 + 1);
453            *slot = (
454                Self::is_edge(t, self.phases.cpu, CPU_DIVIDER),
455                Self::is_edge(t, self.phases.rcp, RCP_DIVIDER),
456            );
457        }
458
459        // Whole periods only. `base` tracks the period boundary while
460        // `master_ticks` lands on each edge inside it, which is what keeps the
461        // pattern's alignment valid: `base` advances by exactly one period, so
462        // `(base + off + phase) % divider` is invariant across iterations.
463        //
464        // `checked_add` on the bound rather than `base + EDGE_PERIOD`: the sum is
465        // the loop's own guard, so it is the one addition here that is not already
466        // bounded by something else. Overflow ends the loop, which is correct —
467        // there is no whole period left below `target`.
468        let mut base = self.master_ticks;
469        let mut blocks = 0u64;
470        while base
471            .checked_add(EDGE_PERIOD)
472            .is_some_and(|end| end <= target)
473        {
474            for (i, &(cpu, rcp)) in pattern.iter().enumerate() {
475                if cpu || rcp {
476                    // In range: `base + EDGE_PERIOD <= target <= u64::MAX`.
477                    self.master_ticks = base + i as u64 + 1;
478                    self.step_domains(cpu, rcp);
479                }
480            }
481            base += EDGE_PERIOD;
482            blocks += 1;
483        }
484
485        // The tail — a partial period, plus the `master_ticks = target` landing —
486        // goes through the accurate path. It is at most `EDGE_PERIOD` ticks, so the
487        // fallback costs nothing measurable, and reusing it means the two paths
488        // cannot disagree about the end of a run.
489        //
490        // The reason is recorded on the tail being non-empty, NOT on this call
491        // happening: `run_until(target)` runs unconditionally, and when `target`
492        // falls exactly on a period boundary all it does is the landing assignment.
493        // Reporting `PartialPeriodTail` there would make the witness true of every
494        // call, which is the same as it being true of none.
495        let mut bailed = BailOutSet::new();
496        if target > base {
497            bailed.insert(BailOut::PartialPeriodTail);
498        }
499        self.run_until(target);
500
501        FastRunReport {
502            work_units: blocks,
503            bailed,
504        }
505    }
506
507    /// The **instruction-granular** counterpart to [`System::run_until`]
508    /// (ADR 0013), behind the default-off `fast-exec` feature.
509    ///
510    /// # How time advances here
511    ///
512    /// The accurate loop walks edge to edge and steps whichever domains are due.
513    /// This one lets the **CPU set the pace**: it executes one instruction, is told
514    /// what that cost in `PCycles`, advances `master_ticks` by that many CPU
515    /// periods, and runs the RCP over every one of its edges in the span that just
516    /// elapsed. The CPU therefore no longer lands on a derived edge at all — which
517    /// is precisely the relaxation ADR 0013 §2 authorizes and ADR 0011 §5 excludes.
518    ///
519    /// **ADR 0006 still holds.** `master_ticks` remains the only counter that is
520    /// ever incremented, and every other position — the RCP's edges, COP0 `Count` —
521    /// is still derived from it. What changed is *how far* it moves per step, not
522    /// who owns it.
523    ///
524    /// # It may land PAST `target`, and that is the divergence, not a defect
525    ///
526    /// A cost is only known after the instruction has run, so the last instruction
527    /// of a call can carry `master_ticks` beyond `target` — by at most one
528    /// instruction's cost. Nothing drifts: the next call's `target` is absolute, so
529    /// an overshoot simply means less work next time. This is the timing divergence
530    /// ADR 0013 §4 requires to be *measured and bounded* rather than eliminated,
531    /// and it is the reason this returns a report rather than nothing.
532    ///
533    /// # A halted CPU advances on RCP edges
534    ///
535    /// A failed real-PIF boot checksum freezes the CPU via NMI while the RCP keeps
536    /// running (`PIF-NUS.md`). With no instruction to time the advance with, this
537    /// steps to the next RCP edge instead. It is deliberately **not** a bail-out to
538    /// the accurate scheduler: forcing an ADR 0012 bail-out reason here would need
539    /// a fixture that can reach a checksum failure, and ADR 0011 §6 only sanctions
540    /// a test-only seam where a boundary *genuinely* cannot be reached — which is
541    /// not the case when the fast path can simply handle it.
542    ///
543    /// `fast-exec` consequently adds **no new [`BailOut`](crate::fastpath::BailOut)
544    /// variant**. Saying so plainly is better than inventing an exit to justify the
545    /// machinery; the enumeration exists so that a *real* one cannot be added
546    /// silently.
547    #[cfg(feature = "fast-exec")]
548    pub fn run_until_exec(&mut self, target: u64) -> crate::fastpath::FastRunReport {
549        use crate::fastpath::FastRunReport;
550
551        let mut report = FastRunReport::default();
552        while self.master_ticks < target {
553            if self.bus.boot_nmi_halt() {
554                let next = Self::next_edge_after(self.master_ticks, self.phases.rcp, RCP_DIVIDER);
555                if next > target {
556                    break;
557                }
558                self.master_ticks = next;
559                self.step_rcp();
560                continue;
561            }
562
563            // `count_ticks` is derived from `master_ticks` and sampled BEFORE the
564            // instruction runs, which is the same instant the accurate path samples
565            // it at the CPU edge that would issue this instruction.
566            let count_now = self.count_ticks();
567            let cost = self.cpu.step_instruction_at(&mut self.bus, count_now);
568            report.work_units = report.work_units.saturating_add(1);
569
570            // One `PCycle` is `CPU_DIVIDER` master ticks (ADR 0006).
571            //
572            // **`checked`, not `saturating`.** Saturating was the first version and
573            // it is a real hazard here rather than a theoretical one: it can put
574            // `end` at `u64::MAX` *without the machine ever having run there*, and
575            // `next_edge_after` then evaluates `tick + 1` at `u64::MAX` — a panic in
576            // debug, and in release a wrap to a low tick that keeps `rcp <= end`
577            // true forever. That is the difference between this and the accurate
578            // loop, whose top-of-range is unreachable because reaching it means
579            // emulating three thousand years (the note in the fast-scheduler gate).
580            // Raised in review.
581            //
582            // Overflowing means the timeline is exhausted, so the run ends; the
583            // landing below carries `master_ticks` to `target` if it is not already
584            // past it.
585            // **This loop's termination depends on `cost > 0`, and nothing here
586            // enforced it.** `Pipeline::step_instruction` charges one `PCycle` to
587            // issue before adding anything, so zero is not reachable today — but
588            // that is an invariant held in another crate, and a reader of *this*
589            // loop cannot see it. A zero would leave `end == master_ticks`, step no
590            // RCP edges, and spin forever. Raised in review as blocking, correctly:
591            // the hazard is that the guarantee is remote, not that it is absent.
592            //
593            // The `debug_assert` is the real check; the `max(1)` is a floor so a
594            // release build makes progress rather than hanging. It is deliberately
595            // NOT a fix — a zero cost would be a defect either way — but running
596            // one `PCycle` fast is a far better way to report one than a hang.
597            debug_assert!(
598                cost > 0,
599                "an instruction cost 0 PCycles, which cannot happen"
600            );
601            let Some(end) = u64::from(cost.max(1))
602                .checked_mul(CPU_DIVIDER)
603                .and_then(|span| self.master_ticks.checked_add(span))
604            else {
605                break;
606            };
607
608            // The RCP catches up over the span the instruction consumed, on its own
609            // edges. CPU-before-RCP still holds: the instruction has already run.
610            let mut rcp = Self::next_edge_after(self.master_ticks, self.phases.rcp, RCP_DIVIDER);
611            while rcp <= end {
612                self.master_ticks = rcp;
613                self.step_rcp();
614                // `end` is a real tick because it came from `checked_add` above, so
615                // this cannot be asked for an edge past `u64::MAX`: the loop
616                // condition fails at or before it.
617                rcp = Self::next_edge_after(rcp, self.phases.rcp, RCP_DIVIDER);
618            }
619            self.master_ticks = end;
620        }
621        // Only reachable through the halted branch's `break`; an executing CPU has
622        // already carried `master_ticks` to or past `target`.
623        if self.master_ticks < target {
624            self.master_ticks = target;
625        }
626        report
627    }
628
629    /// One RCP step: the RSP microcode unit, then the RDP rasterizer, then the
630    /// AI/interface DMA progress — all on the SAME `&mut self.bus`.
631    fn step_rcp(&mut self) {
632        // The chips each see only their narrow trait of `self.bus`.
633        // The LLE RSP runs the microcode scalar+vector stream (Phase 2).
634        self.bus.rsp_tick();
635        // The RDP consumes the DPC command stream and rasterizes the implemented
636        // commands (FILL, triangles, texture rects, sync); the live path is still
637        // incomplete — remaining opcodes are recognized-not-dispatched (T-31-004)
638        // and per-command timing is deferred. See ledger R-18 for the end-to-end
639        // commercial-video gap.
640        self.bus.rdp_tick();
641        // AI / interface sub-clock advance — derives sample emission off the
642        // canonical `master_ticks` (ADR 0006), like the VI scan below.
643        self.bus.audio_tick(self.master_ticks);
644        // The PI's asynchronous direct-I/O write finalizes on this clock.
645        self.bus.pi_tick();
646        // The VI scan position advances off `master_ticks` (the one fractional
647        // domain, ADR 0006 / `docs/scheduler.md`); a `VI_V_INTR` crossing raises
648        // the VI line into the MI.
649        if self.bus.vi.tick(self.master_ticks) {
650            self.bus.rcp.mi_intr.vi = true;
651        }
652    }
653}
654
655#[cfg(test)]
656mod tests {
657    use super::*;
658    use alloc::vec::Vec;
659
660    /// `rustyn64-audio` duplicates `MASTER_HZ` (the chip-crate graph forbids it
661    /// depending on `-core`); this pins the two copies together so the AI's DAC
662    /// period cannot silently drift from the canonical clock.
663    #[test]
664    fn audio_crate_master_hz_matches() {
665        assert_eq!(rustyn64_audio::MASTER_HZ, MASTER_HZ);
666    }
667
668    /// The divisors must be exact. A wrong one is silent and poisons everything.
669    #[test]
670    fn every_divider_is_exact() {
671        assert_eq!(MASTER_HZ / CPU_DIVIDER, CPU_HZ);
672        assert_eq!(MASTER_HZ % CPU_DIVIDER, 0);
673        assert_eq!(MASTER_HZ / RCP_DIVIDER, RCP_HZ);
674        assert_eq!(MASTER_HZ % RCP_DIVIDER, 0);
675        // COP0 Count is half `PClock` (UM §6.3.3).
676        assert_eq!(MASTER_HZ / COUNT_DIVIDER, CPU_HZ / 2);
677        assert_eq!(MASTER_HZ / SI_DIVIDER, 15_625_000);
678        assert_eq!(MASTER_HZ / PIF_DIVIDER, 1_953_125);
679        // 187.5 MHz really is the LCM of the two core clocks.
680        assert_eq!(CPU_HZ * CPU_DIVIDER, MASTER_HZ);
681        assert_eq!(RCP_HZ * RCP_DIVIDER, MASTER_HZ);
682    }
683
684    #[test]
685    fn three_cpu_and_two_rcp_steps_per_six_ticks() {
686        for seed in [0, 1, 0xDEAD_BEEF, u64::MAX] {
687            let mut sys = System::new(seed);
688            let rcp_before = sys.bus.rcp_steps_for_test();
689            // Count CPU *steps* from the derived position, not from `Cpu::retired`
690            // -- since ADR 0007 the CPU is a 5-stage pipeline, so retirement lags
691            // stepping by the pipeline depth and the two are not interchangeable.
692            let cpu_before = sys.cpu_cycles();
693            sys.run_until(PHASE_PERIOD);
694            assert_eq!(
695                sys.cpu_cycles() - cpu_before,
696                3,
697                "3 CPU steps per 6 master ticks (seed {seed})"
698            );
699            assert_eq!(
700                sys.bus.rcp_steps_for_test() - rcp_before,
701                2,
702                "2 RCP steps per 6 master ticks (seed {seed})"
703            );
704        }
705    }
706
707    #[test]
708    fn reset_preserves_phase() {
709        let mut sys = System::new(0xDEAD_BEEF);
710        let phases = sys.phases;
711        sys.step_to_next_edge();
712        sys.reset();
713        assert_eq!(sys.phases, phases);
714        assert_eq!(sys.master_ticks(), 0);
715    }
716
717    /// Different seeds must produce genuinely different CPU↔RCP interleavings,
718    /// not merely a different starting point in an identical pattern.
719    ///
720    /// This is the test that fails if the per-domain phase offsets are ever
721    /// collapsed into one offset on `master_ticks` — the defect the ADR review
722    /// caught in the design before it was written.
723    #[test]
724    fn seeds_produce_distinct_interleavings() {
725        let fingerprint = |seed: u64| -> Vec<(bool, bool)> {
726            let sys = System::new(seed);
727            (0..PHASE_PERIOD)
728                .map(|t| {
729                    (
730                        System::is_edge(t, sys.phases.cpu, CPU_DIVIDER),
731                        System::is_edge(t, sys.phases.rcp, RCP_DIVIDER),
732                    )
733                })
734                .collect()
735        };
736        let mut seen: Vec<Vec<(bool, bool)>> = Vec::new();
737        for seed in 0..64u64 {
738            let f = fingerprint(seed);
739            if !seen.contains(&f) {
740                seen.push(f);
741            }
742        }
743        assert!(
744            seen.len() > 1,
745            "all seeds produced one interleaving -- the per-domain phase offsets \
746             have collapsed and the seeded power-on phase is decorative"
747        );
748    }
749
750    /// **The residue invariant.** Every derived position must stay in a fixed
751    /// affine relationship with `master_ticks`. A position that has become
752    /// independently incremented drifts out of it on the first path that forgets
753    /// to step it — the failure mode ADR 0006 exists to prevent.
754    #[test]
755    fn residue_invariants_never_move() {
756        fn sample(s: &System) -> (i64, i64, i64) {
757            let master = i64::try_from(s.master_ticks()).unwrap();
758            let cpu_pos = i64::try_from(s.cpu_cycles()).unwrap();
759            let rcp_pos = i64::try_from(s.rcp_cycles()).unwrap();
760            (
761                master - i64::try_from(CPU_DIVIDER).unwrap() * cpu_pos,
762                master - i64::try_from(RCP_DIVIDER).unwrap() * rcp_pos,
763                // the two domains against each other -- catches inter-domain
764                // drift even if each stayed affine to master individually.
765                // NOT `cpu.retired`: since ADR 0007 the CPU is a 5-stage
766                // pipeline, so retirement lags stepping and is a CPU property
767                // rather than a clock one.
768                i64::try_from(CPU_DIVIDER).unwrap() * cpu_pos
769                    - i64::try_from(RCP_DIVIDER).unwrap() * rcp_pos,
770            )
771        }
772
773        let mut sys = System::new(0x1234_5678_9ABC_DEF0);
774        // Sample at period boundaries so the comparison point is consistent; the
775        // residues must then be constant forever.
776        sys.run_until(PHASE_PERIOD);
777        let first = sample(&sys);
778        for period in 1..64u64 {
779            sys.run_until(PHASE_PERIOD * (period + 1));
780            assert_eq!(
781                sample(&sys),
782                first,
783                "residues moved at period {period} -- a cycle position is being \
784                 incremented independently instead of derived from master_ticks"
785            );
786        }
787    }
788
789    /// Same seed ⇒ identical timeline. The determinism contract's floor.
790    #[test]
791    fn same_seed_same_timeline() {
792        let mut a = System::new(42);
793        let mut b = System::new(42);
794        for _ in 0..256 {
795            assert_eq!(a.step_to_next_edge(), b.step_to_next_edge());
796            assert_eq!(a.cpu_cycles(), b.cpu_cycles());
797            assert_eq!(a.rcp_cycles(), b.rcp_cycles());
798        }
799    }
800
801    #[test]
802    fn edges_are_never_skipped() {
803        let mut sys = System::new(7);
804        let mut prev = sys.master_ticks();
805        for _ in 0..512 {
806            let now = sys.step_to_next_edge();
807            assert!(now > prev, "the scheduler must advance");
808            // The gap can never exceed the coarsest divider we step on.
809            assert!(now - prev <= RCP_DIVIDER, "an edge was skipped");
810            prev = now;
811        }
812    }
813
814    /// **A running system raises the VI interrupt as the scan crosses
815    /// `VI_V_INTR`.** With a standard NTSC field (525 half-lines) and the VI on,
816    /// stepping past `per-half-line × V_INTR` master ticks drives `MI_INTR.vi`
817    /// through the scheduler's per-step `Vi::tick` call.
818    #[test]
819    fn a_running_system_raises_the_vi_interrupt_at_v_intr() {
820        use crate::vi::{VI_CTRL, VI_V_INTR, VI_V_TOTAL};
821        let mut sys = System::new(1);
822        sys.bus.vi.regs[VI_V_TOTAL as usize] = 524; // 525 half-lines
823        sys.bus.vi.regs[VI_V_INTR as usize] = 2;
824        sys.bus.vi.regs[VI_CTRL as usize] = 2; // VI on
825        assert!(!sys.bus.rcp.mi_intr.vi, "clear before running");
826        // per-half-line ≈ 5952 ticks; 15_000 is well past half-line 2.
827        while sys.master_ticks() < 15_000 {
828            sys.step_to_next_edge();
829        }
830        assert!(
831            sys.bus.rcp.mi_intr.vi,
832            "the VI interrupt fired during the run"
833        );
834    }
835
836    /// **The VI keeps firing after a reset.** A reset zeroes `master_ticks`, so
837    /// the VI scan timeline must rebase (`Vi::reset_scan`) — otherwise its delta
838    /// baseline stays in the old timeline and the interrupt is suppressed until
839    /// the new run catches up. Acknowledge → reset → the next field fires again.
840    #[test]
841    fn a_reset_rebases_the_vi_scan_so_it_fires_again() {
842        use crate::vi::{VI_CTRL, VI_V_INTR, VI_V_TOTAL};
843        let mut sys = System::new(2);
844        sys.bus.vi.regs[VI_V_TOTAL as usize] = 524;
845        sys.bus.vi.regs[VI_V_INTR as usize] = 2;
846        sys.bus.vi.regs[VI_CTRL as usize] = 2;
847        while sys.master_ticks() < 15_000 {
848            sys.step_to_next_edge();
849        }
850        assert!(sys.bus.rcp.mi_intr.vi, "fires before reset");
851        sys.bus.rcp.mi_intr.vi = false; // the CPU would ack via VI_V_CURRENT
852        sys.reset();
853        assert!(!sys.bus.rcp.mi_intr.vi, "clear immediately after reset");
854        while sys.master_ticks() < 15_000 {
855            sys.step_to_next_edge();
856        }
857        assert!(
858            sys.bus.rcp.mi_intr.vi,
859            "fires again after the reset rebases"
860        );
861    }
862}