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}