Skip to main content

rustyn64_cpu/
lib.rs

1//! `rustyn64-cpu` — NEC VR4300 (MIPS R4300i) main CPU.
2//!
3//! 64-bit MIPS III core: 32 general-purpose registers, HI/LO, the CP0
4//! system-control coprocessor (TLB + exceptions), the CP1 FPU, and the `SysAD`
5//! bus interface. This is a **skeleton** — the real interpreter/JIT, the TLB,
6//! and the FPU are major roadmap phases. Behavior is pinned against
7//! `n64-systemtest` FIRST (test-ROM-is-spec), then implemented until it passes.
8//!
9//! Part of the one-directional chip-crate graph (see `docs/architecture.md`):
10//! this crate does NOT depend on any other chip crate. It talks to the rest of
11//! the machine through the [`Bus`] trait, which `rustyn64-core` implements.
12//! `#![no_std]` + `alloc` so it cross-compiles to a bare-metal target; only the
13//! frontend carries `std` + `unsafe`.
14
15#![no_std]
16#![forbid(unsafe_code)]
17#![warn(missing_docs)]
18// Truncating / sign casts are the canonical encoding for MIPS register
19// arithmetic; we annotate once at module level rather than per line.
20#![allow(
21    clippy::cast_possible_truncation,
22    clippy::cast_lossless,
23    clippy::cast_possible_wrap,
24    clippy::cast_sign_loss
25)]
26// Skeleton `tick`/step methods are deliberately non-`const`: they will gain
27// real (non-const) bus-driven bodies as the chip is implemented. Accept the
28// pedantic const-fn suggestion at module level rather than salt every stub.
29#![allow(clippy::missing_const_for_fn)]
30
31extern crate alloc;
32
33use serde::{Deserialize, Serialize};
34
35pub mod addr;
36pub mod alu;
37pub mod cache;
38pub mod cop0;
39pub mod cop1;
40pub mod decode;
41pub mod exception;
42pub mod exec;
43pub mod fpr;
44pub mod fpu;
45pub mod mem;
46pub mod pipeline;
47pub mod regs;
48pub mod softfloat;
49pub mod sysad;
50pub mod tlb;
51
52pub use addr::{Cached, Physical, Segment, segment, translate_via};
53pub use alu::{HiLo, MulDiv};
54pub use decode::{Decoded, Op, decode};
55pub use exec::{Executed, WriteBack, execute};
56pub use mem::{LoadKind, StoreKind};
57pub use pipeline::{Exception, Interlock, Latch, Pipeline, Stage};
58pub use regs::Regs;
59pub use sysad::{BlockOrder, Phase, Transaction, Width, block_order};
60
61/// Which half of a `SysAD` bus transaction is on the wire.
62///
63/// The VR4300 talks to the RCP over the `SysAD` bus, which multiplexes the command
64/// and the data onto the same lines; `SYSCMD` bit 4 is documented as "Command or
65/// Data" and this enum mirrors that split.
66///
67/// **This describes the bus protocol and carries no interrupt semantics.** An
68/// earlier revision paired it with a `poll_irq_at_phase(BusPhase)` hook, on the
69/// assumption that interrupts are sampled at a particular half of a transaction.
70/// No such coupling is documented anywhere — not in the User's Manual, not on the
71/// wiki. The documented rule (UM §4.7.1) is per-`PCycle` and gated on stall state:
72/// *"NMI and interrupt exception requests are accepted only if the previous
73/// `PCycle` was a run cycle."* That hook was shaped on the wrong axis and was
74/// removed rather than completed (ADR 0007).
75///
76/// `SysAD` runs at `SClock` = `MClock` = 62.5 MHz, so one bus cycle is 1.5 `PCycles` — 3
77/// master ticks against the CPU's 2 (ADR 0006). This is *not* the deferred ADR
78/// 0005 refactor, which concerns resolution finer than one `PClock`.
79///
80/// TODO(T-11-008): the transaction model that actually uses this.
81#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
82pub enum BusPhase {
83    /// The command half — the CPU drives `SYSCMD` and the address.
84    Command,
85    /// The data half — the transfer itself, and where the result commits.
86    Data,
87}
88
89/// The system-memory bus the VR4300 borrows during [`Cpu::tick`].
90///
91/// Implemented by `rustyn64-core::Bus`. Kept as a trait so the CPU can be
92/// fuzzed and benchmarked against a tiny in-crate bus without pulling in the
93/// rest of the machine.
94pub trait Bus {
95    /// Read a byte at a 32-bit physical address (post-TLB).
96    fn read_u8(&mut self, addr: u32) -> u8;
97    /// Write a byte at a 32-bit physical address (post-TLB).
98    fn write_u8(&mut self, addr: u32, val: u8);
99
100    /// Read an aligned big-endian 32-bit word. Default composes four byte
101    /// reads; `rustyn64-core` overrides with a fast RDRAM path.
102    fn read_u32(&mut self, addr: u32) -> u32 {
103        let b = [
104            self.read_u8(addr),
105            self.read_u8(addr.wrapping_add(1)),
106            self.read_u8(addr.wrapping_add(2)),
107            self.read_u8(addr.wrapping_add(3)),
108        ];
109        u32::from_be_bytes(b)
110    }
111
112    /// Write an aligned big-endian 32-bit word.
113    fn write_u32(&mut self, addr: u32, val: u32) {
114        let b = val.to_be_bytes();
115        self.write_u8(addr, b[0]);
116        self.write_u8(addr.wrapping_add(1), b[1]);
117        self.write_u8(addr.wrapping_add(2), b[2]);
118        self.write_u8(addr.wrapping_add(3), b[3]);
119    }
120
121    /// Write `value` as a `width`-byte store, **carrying the access width and
122    /// the untruncated source register to the bus**.
123    ///
124    /// The CPU cannot narrow the value itself, because whether narrowing is
125    /// correct is a property of the *target*, not of the instruction. RDRAM
126    /// honors the byte enables and stores exactly `width` bytes; every device
127    /// on the RCP's internal bus ignores the access size entirely and latches
128    /// the whole 32-bit word the VR4300 placed on `SysAD` — including the bits
129    /// of the source register that a narrow store was never meant to send
130    /// (N64brew *Memory map* §Physical Memory Map accesses).
131    ///
132    /// So the width and the full register both have to survive the call, and
133    /// the implementor decides. The default here is the RDRAM-style narrowing,
134    /// which is right for a plain memory and for the test buses; `rustyn64-core`
135    /// overrides it to model the RCP's size-blindness.
136    fn write_sized(&mut self, addr: u32, width: u64, value: u64) {
137        match width {
138            1 => self.write_u8(addr, value as u8),
139            2 => {
140                self.write_u8(addr, (value >> 8) as u8);
141                self.write_u8(addr.wrapping_add(1), value as u8);
142            }
143            4 => self.write_u32(addr, value as u32),
144            8 => {
145                self.write_u32(addr, (value >> 32) as u32);
146                self.write_u32(addr.wrapping_add(4), value as u32);
147            }
148            _ => {}
149        }
150    }
151
152    /// Does this host offer the **EMUX** emulator extensions?
153    ///
154    /// Default: **no**, and that default is the load-bearing part. Real hardware
155    /// has no EMUX — COP0 CO `funct` 0x20-0x3F retires inertly (ledger C-8), so
156    /// `xdetect` leaves its destination register untouched and the guest
157    /// concludes no extensions exist. Advertising them changes which console
158    /// path n64-systemtest takes -- it changes the instruction stream.
159    ///
160    /// ares takes exactly this position: every EMUX handler in its `emux.cpp`
161    /// opens with `if(!system.homebrewMode) return;`, off by default. A default
162    /// build of `RustyN64` must behave like hardware; a harness that wants the
163    /// faster console opts in deliberately.
164    fn emux_enabled(&self) -> bool {
165        false
166    }
167
168    /// `EMUX xlog`: the guest has asked the *emulator* to print `bytes`.
169    ///
170    /// Default: discard. `rustyn64-core` collects it. This is n64-systemtest's
171    /// out-of-band console channel — it reaches the host without any PI, SI or
172    /// `ISViewer` emulation, which is why it is worth implementing well before the
173    /// cartridge subsystem exists.
174    fn emux_log(&mut self, bytes: &[u8]) {
175        let _ = bytes;
176    }
177
178    /// `EMUX xioctl(EXIT)`: the guest has asked the emulator to terminate.
179    ///
180    /// Default: ignore. A harness that honors it gets a definite end-of-run
181    /// signal instead of a tick budget, which is the difference between "the
182    /// suite finished" and "we stopped watching".
183    fn emux_exit(&mut self) {}
184
185    /// Sample the pending-interrupt level: the MI lines masked by `MI_MASK`.
186    ///
187    /// Default: no interrupt pending. `rustyn64-core` overrides it.
188    ///
189    /// Sampling happens **once per `PClock` in the DC stage** (UM Figure 4-12 and
190    /// §4.7.6, which lists the interrupt exception among the DC-stage priorities)
191    /// and is accepted only if the previous `PCycle` was a run cycle (§4.7.1). The
192    /// run-cycle gate lives in the pipeline, not here — this hook only reports the
193    /// level. Exactly one recognition predicate exists in the tree.
194    fn poll_irq(&mut self) -> bool {
195        false
196    }
197}
198
199/// NEC VR4300 architectural state.
200///
201/// This is the **skeleton** register file only; the decode/execute pipeline,
202/// the CP0 TLB, and the CP1 FPU are roadmap phases left as marked TODOs.
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct Cpu {
205    /// The register file: 32 GPRs plus `HI`/`LO`. `$zero`'s hardwiring lives in
206    /// [`Regs::read`]/[`Regs::write`] so no call site can forget it.
207    pub regs: Regs,
208    /// Program counter (virtual address).
209    pub pc: u64,
210    /// The five-stage pipeline: four inter-stage latches plus control state
211    /// (ADR 0007). Advanced one `PClock` per [`Cpu::tick`].
212    pub pipeline: Pipeline,
213    /// Retired-work tally: instructions retired since power-on, for the
214    /// golden-log differ.
215    ///
216    /// This is **not** a time position and nothing schedules against it — the
217    /// scheduler derives every cycle position from its one `master_ticks` counter
218    /// (ADR 0006). A work tally is the one kind of counter that is still allowed
219    /// to be incremented; see `System::cpu_cycles()` for the CPU's *position*.
220    pub retired: u64,
221    // This struct deliberately holds no CP0/TLB/CP1/LL state. All of it exists and
222    // lives one level down, in [`Pipeline`], because it is pipelined state rather
223    // than architectural state the fetch loop owns (ADR 0007): `pipeline.cop0`,
224    // `pipeline.tlb`, `pipeline.fpr`, `pipeline.ll_bit`, and the branch-delay-slot
225    // flag as `in_delay_slot` riding in the inter-stage latch rather than a global
226    // CPU flag.
227    //
228    // Stated positively because this previously read as a `TODO` listing all five
229    // as unimplemented, long after they shipped.
230}
231
232impl Default for Cpu {
233    fn default() -> Self {
234        Self::new()
235    }
236}
237
238impl Cpu {
239    /// Construct at power-on / cold reset.
240    ///
241    /// `gpr[0]` is the architectural zero register. Any phase alignment, where
242    /// applicable, comes from the *seeded* scheduler PRNG (the determinism
243    /// contract — see `docs/adr/0004`), never the OS RNG.
244    #[must_use]
245    pub const fn new() -> Self {
246        Self {
247            regs: Regs::new(),
248            // The reset vector, as a SIGN-EXTENDED 64-bit address. In 32-bit
249            // addressing mode every valid address is one, and `0x0000_0000_BFC0_0000`
250            // is not the same address -- it is an address error. n64-systemtest
251            // asserts exactly that distinction ("LW with address not sign
252            // extended"), so the truncated form is not a harmless shorthand.
253            pc: 0xFFFF_FFFF_BFC0_0000,
254            pipeline: Pipeline::new(),
255            retired: 0,
256        }
257    }
258
259    /// Set the program counter (used by golden-log automation harnesses).
260    pub const fn set_pc(&mut self, pc: u64) {
261        self.pc = pc;
262    }
263
264    /// Advance the CPU by **one `PClock`** — not one instruction.
265    ///
266    /// The scheduler calls this on every CPU edge (every 2nd master tick, ADR
267    /// 0006). At least 5 `PCycles` are required to execute an instruction (UM
268    /// §4.1), and `DDIV` stalls the whole pipeline for 69 (UM Table 3-12), so a
269    /// tick is emphatically not an instruction.
270    ///
271    /// Hot path: keep allocation-free (no `Vec`/`Box` in `tick`). The `bus`
272    /// argument is the `&mut Bus` the scheduler hands down each step.
273    /// **Prefer [`Cpu::tick_at`] when a scheduler is present.** This path holds
274    /// the COP0 `Count` timeline still, because `Count` is derived from the
275    /// master clock (ADR 0006) and this function has no access to it — so
276    /// `Count`/`Compare` and the timer interrupt do not advance. That is
277    /// deliberate: guessing a rate here would be wrong, and `Count` runs at half
278    /// `PClock`, not one step per call.
279    pub fn tick<B: Bus>(&mut self, bus: &mut B) {
280        self.pipeline.advance(bus, &mut self.regs, &mut self.pc);
281        self.retired = self.pipeline.retired;
282    }
283
284    /// Step one `PCycle` with the scheduler's `Count` timeline supplied.
285    ///
286    /// The scheduler owns `master_ticks` and derives `count_ticks` from it
287    /// (ADR 0006); the CPU turns that into the architectural, guest-writable
288    /// `Count`. Passing it in rather than incrementing locally is what keeps
289    /// `master_ticks` the only incremented counter in the core.
290    pub fn tick_at<B: Bus>(&mut self, bus: &mut B, count_now: u64) {
291        self.pipeline
292            .advance_at(bus, &mut self.regs, &mut self.pc, count_now);
293        self.retired = self.pipeline.retired;
294    }
295
296    /// Execute one instruction (plus its delay slot, if it branches) and return
297    /// the `PCycle`s it cost — the **instruction-granular** path (ADR 0013),
298    /// behind the default-off `fast-exec` feature.
299    ///
300    /// A **separate entry point**, not a branch inside [`Cpu::tick_at`], for the
301    /// same reason `System::run_until_fast` is one: with the feature off this
302    /// function does not exist, so ADR 0011 §1's "default builds are unchanged" is
303    /// true by construction rather than by inspection, and the accurate path
304    /// carries no branch that exists only for its sibling.
305    ///
306    /// The unit differs from every other step function in this crate, and the
307    /// signature says so: [`Cpu::tick`] and [`Cpu::tick_at`] advance **one
308    /// `PClock`** and return nothing; this one advances **one instruction** and
309    /// returns what that cost. A caller that treats the return value as a tick
310    /// count will run the machine roughly five times too fast.
311    ///
312    /// `count_now` is the scheduler's derived COP0 `Count` position, passed in for
313    /// the same reason [`Cpu::tick_at`] takes it (ADR 0006).
314    #[cfg(feature = "fast-exec")]
315    #[must_use = "the return value is the instruction's cost in PCycles; \
316                  discarding it makes the machine run at one PCycle per instruction"]
317    pub fn step_instruction_at<B: Bus>(&mut self, bus: &mut B, count_now: u64) -> u32 {
318        let cost = self
319            .pipeline
320            .step_instruction(bus, &mut self.regs, &mut self.pc, count_now);
321        self.retired = self.pipeline.retired;
322        cost
323    }
324}
325
326/// Returns the crate version string.
327#[must_use]
328pub const fn version() -> &'static str {
329    env!("CARGO_PKG_VERSION")
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    struct NullBus;
337    impl Bus for NullBus {
338        fn read_u8(&mut self, _addr: u32) -> u8 {
339            0
340        }
341        fn write_u8(&mut self, _addr: u32, _val: u8) {}
342    }
343
344    #[test]
345    fn constructs_with_zero_register() {
346        let cpu = Cpu::new();
347        assert_eq!(cpu.regs.read(0), 0);
348        assert_eq!(cpu.pc, 0xFFFF_FFFF_BFC0_0000);
349    }
350
351    /// A tick is one `PClock`, not one instruction. The pipeline is 5 stages
352    /// deep, so nothing retires until it has filled (UM §4.1: "at least 5
353    /// `PCycle`s are required to execute an instruction").
354    #[test]
355    fn a_tick_is_a_pclock_not_an_instruction() {
356        let mut cpu = Cpu::new();
357        let mut bus = NullBus;
358        for cycle in 1..=4 {
359            cpu.tick(&mut bus);
360            assert_eq!(cpu.retired, 0, "retired on cycle {cycle}, before WB ran");
361        }
362        cpu.tick(&mut bus);
363        assert_eq!(cpu.retired, 1, "the first instruction retires on cycle 5");
364    }
365
366    #[test]
367    fn version_is_non_empty() {
368        assert!(!version().is_empty());
369    }
370}