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}