rustyn64_cpu/pipeline.rs
1//! The VR4300's five-stage pipeline (ADR 0007).
2//!
3//! `IC` → `RF` → `EX` → `DC` → `WB` (VR4300 User's Manual §4.1, Figure 4-1):
4//! Instruction Cache fetch, Register Fetch, Execution, Data Cache fetch, Write
5//! Back. In-order, single-issue, with one architectural delay slot. At least 5
6//! `PCycle`s are required to execute an instruction, and up to five are in flight
7//! at once when the pipe flows.
8//!
9//! # Latches, not stages
10//!
11//! Five stages have four boundaries, and the state lives on the **boundaries**.
12//! [`Latch`] is what travels with an instruction as it advances.
13//!
14//! `in_delay_slot` riding in the latch rather than in a global CPU flag is
15//! load-bearing: a multi-cycle stall between a branch and its delay slot
16//! desynchronizes a global flag, and that is the classic bug in this area. With
17//! the flag attached to the instruction, `Cause.BD` and `EPC` come out right for
18//! free. `delay_slot_flag_survives_a_multi_cycle_stall` pins it.
19//!
20//! # Reverse step order is the latching
21//!
22//! [`Pipeline::advance`] runs **WB → DC → EX → RF → IC**. Each stage reads its
23//! input latch and writes its output latch, so running downstream-first means a
24//! stage's input still holds the *previous* cycle's value when it is read. No
25//! value can therefore propagate two stages in one cycle, and **no double
26//! buffering is needed** — the reverse order *is* the latching.
27//!
28//! This is a load-bearing invariant, not a style choice. Reversing it silently
29//! makes the pipeline one-cycle-too-fast; `a_value_advances_exactly_one_stage_per_cycle`
30//! is the guard.
31//!
32//! # Status
33//!
34//! **Structure only.** The stages move latches and account for time; they do not
35//! decode or execute yet (T-11-002 onward). What is real here is the shape, the
36//! stall/interlock mechanism, the delay-slot carriage, and the interrupt gate —
37//! the parts that cannot be retrofitted later without rewriting every consumer.
38
39use crate::Bus;
40use crate::alu::HiLo;
41use crate::cop0::Cop0;
42use crate::cop1::Cop1Control;
43use crate::decode::{Decoded, decode};
44use crate::exception;
45use crate::exec::{Cop0Access, Cop1Access, MemOp, Redirect, TlbOp, WriteBack, execute};
46use crate::fpr::Fpr;
47use crate::mem;
48use crate::regs::Regs;
49use crate::softfloat;
50use crate::tlb::Tlb;
51use serde::{Deserialize, Serialize};
52
53/// The five pipeline stages, in hardware order (UM §4.1, Figure 4-1).
54///
55/// Note the names: **IC** and **DC**, not `IF`/`DF`. The manual's whole interlock
56/// and exception taxonomy is stated stage-relative, so these spellings are what
57/// make a citation resolvable.
58#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
59pub enum Stage {
60 /// Instruction Cache fetch.
61 Ic,
62 /// Register Fetch.
63 Rf,
64 /// Execution.
65 Ex,
66 /// Data Cache fetch — the bus access, and where interrupts are sampled.
67 Dc,
68 /// Write Back.
69 Wb,
70}
71
72/// An aborting condition travelling down the pipe.
73///
74/// Deliberately **not** called `Fault`. UM §4.5 defines a *fault* as the union of
75/// interlocks and exceptions (Figure 4-11: Faults = Interlocks ∪ Exceptions,
76/// split into Stalls vs Abort), and CEN64 follows that wider usage. What rides in
77/// a latch here is only the aborting subset, so it carries the narrower name.
78#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
79pub enum Exception {
80 /// An interrupt was accepted (`Cause.IP` unmasked, `IE` set, `EXL`/`ERL` clear).
81 Interrupt,
82 /// Address error on an instruction fetch or data access.
83 ///
84 /// Carries the direction because the architecture does: `AdEL` (4) and
85 /// `AdES` (5) are **different** `ExcCode` values (UM Table 6-2, p. 172), and
86 /// a handler distinguishes them. An instruction fetch is a load.
87 AddressError {
88 /// The faulting access was a store.
89 store: bool,
90 },
91 /// Integer overflow (`ADD`, `ADDI`, `SUB`, `DADD`, …).
92 Overflow,
93 /// `SYSCALL`.
94 Syscall,
95 /// `BREAK`.
96 Breakpoint,
97 /// A conditional trap (`TGE`, `TEQ`, `TNEI`, …) whose condition held.
98 Trap,
99 /// A reserved / unimplemented opcode.
100 ReservedInstruction,
101 /// A reserved encoding **within a coprocessor's** instruction space.
102 ///
103 /// Reports the same `ExcCode` as [`Exception::ReservedInstruction`] but
104 /// also sets `Cause.CE` to the coprocessor number. A plain
105 /// `ReservedInstruction` leaves `CE` at zero, so the two are not
106 /// interchangeable — n64-systemtest checks the whole `Cause` register, and
107 /// `DCFC2` with `CU2` set expects `CE = 2`.
108 CoprocessorReserved {
109 /// Which coprocessor, for `Cause.CE`.
110 unit: u8,
111 },
112 /// A coprocessor instruction with that unit disabled in `Status.CU`.
113 CoprocessorUnusable {
114 /// Which coprocessor, for `Cause.CE`.
115 unit: u8,
116 },
117 /// **Non-maskable interrupt** (UM §6.4.6).
118 ///
119 /// Not one of the `Cause.ExcCode` exceptions: it writes no `Cause`, saves to
120 /// `ErrorEPC` rather than `EPC`, sets `ERL` rather than `EXL`, and is taken
121 /// *"regardless of the settings of the EXL, ERL, and the IE bits"*. It rides
122 /// in this enum anyway so it can reuse the pipeline-flush machinery every
123 /// other exception uses; [`crate::exception::dispatch`] intercepts it before
124 /// any of the general path runs.
125 Nmi,
126 /// A TLB refill — no entry matched. Takes the **refill** vector.
127 TlbRefill {
128 /// The faulting access was a store (`TLBS` rather than `TLBL`).
129 store: bool,
130 /// The access was made with **64-bit addressing** enabled for its mode
131 /// (`Status.KX`/`SX`/`UX`), which selects the **XTLB** refill vector at
132 /// offset `0x080` instead of the 32-bit one at `0x000`.
133 ///
134 /// Carried on the exception rather than re-derived at dispatch because
135 /// by then `EXL` is set, which forces `Pipeline::access_mode` to
136 /// report Kernel and would answer for the handler's mode instead of the
137 /// faulting access's.
138 wide: bool,
139 },
140 /// A TLB entry matched but was invalid. Takes the **general** vector, with
141 /// the same `ExcCode` as a refill — the vector is the only difference, which
142 /// is why they are separate variants rather than one with a flag nobody
143 /// reads.
144 TlbInvalid {
145 /// The faulting access was a store.
146 store: bool,
147 },
148 /// A store to a valid but non-writable page.
149 TlbModified,
150 /// A floating-point operation raised a condition whose `FCSR.Enable` bit is
151 /// set.
152 ///
153 /// Carries nothing: which condition fired is reported in `FCSR.Cause`, not
154 /// in COP0 `Cause`, and the handler reads it from there. Adding a field
155 /// here would duplicate — and could contradict — the architectural record.
156 FloatingPoint,
157}
158
159/// What a COP1 operation writes when it does not trap.
160///
161/// Named rather than a `(u64, bool)` pair because the destinations are of
162/// genuinely different kinds — two FPR widths and a single `FCSR` bit — and a
163/// flag pair makes "write the condition to `fd`" representable.
164#[derive(Clone, Copy, Debug, Eq, PartialEq)]
165enum FpCommit {
166 /// A 32-bit result into `fd`'s low half: `.S` values and `.W` integers.
167 Single(u32),
168 /// A 64-bit result into `fd` through the `FR` view: `.D` values and `.L`
169 /// integers.
170 Double(u64),
171 /// `FCSR.C`. Only `C.cond.fmt` produces this, and it writes no FPR at all.
172 Condition(bool),
173}
174
175/// The documented interlocks (UM Table 4-3).
176///
177/// Held as a named enum rather than a bare cycle count so a stall is always
178/// attributable — "why did this stall" is answerable from a trace.
179#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
180pub enum Interlock {
181 /// Load interlock — 1 cycle (UM §4.6.5).
182 ///
183 /// Deliberately **imprecise**, matching hardware: it fires when the next
184 /// instruction's `rs` *or* `rt` field equals the load's `rt`, whether or not
185 /// that field is actually used as a source. See [`load_interlocks`].
186 Ldi,
187 /// Data cache busy — a cached store keeps the cache busy for its `DC` *and*
188 /// `WB` stages, so a following cache access stalls 1 cycle (UM §4.6.7).
189 Dcb,
190 /// Data cache miss — the fill cost is `8..=9 + M` `PCycle`s (UM Table 11-1).
191 Dcm,
192 /// Instruction cache busy (UM §4.6.3).
193 Icb,
194 /// Instruction micro-TLB miss — 3 `PCycle`s (UM §4.6.2).
195 Itm,
196 /// Multi-cycle interlock: `MULT`/`DIV`/FPU stall the whole pipeline for the
197 /// documented count (UM Tables 3-12, 7-14).
198 Mci,
199 /// Cache operation (UM Table 4-3).
200 Cop,
201 /// CP0 bypass interlock — **1 `PCycle`** (UM §4.6.9, p. 113).
202 ///
203 /// **Named but never raised.** On hardware it fires when an instruction that
204 /// caused an exception reaches `WB` while the next instruction in `DC` reads
205 /// any CP0 register. Here, taking an exception already flushes the pipeline
206 /// and stalls for the 2-`PCycle` epilogue, so the two triggers overlap and
207 /// no case distinguishes them — charging this on top would invent a cycle
208 /// the hardware may not spend. Separating them needs the timing set
209 /// n64-systemtest ships default-off, the same instrument ledger C-1 waits on.
210 ///
211 /// The variant stays because [`Interlock`] enumerates the documented
212 /// taxonomy, and a gap in a taxonomy is worth more than a silently missing
213 /// row. This doc previously said it *fires*, in the present tense, which is
214 /// the comment-is-not-an-implementation hazard `docs/engineering-lessons.md`
215 /// §3.3c describes -- the cost was itself recorded as undocumented in three
216 /// files while sitting in the very paragraph they cited (§3.3b).
217 Cp0i,
218 /// Taking an exception — **2 `PCycle`s** (UM §4.7, p. 114).
219 ///
220 /// Not strictly one of Table 4-3's eight interlocks: the pipeline stalls
221 /// while the epilogue runs and the aborted instructions drain. Named here so
222 /// a trace can attribute the cycles rather than showing an unexplained gap.
223 Exception,
224}
225
226/// A stall request: how long, and what caused it.
227///
228/// ADR 0007 describes an interlock as `(cycles, resume_stage)`. The `resume`
229/// half is **deliberately absent** until it can be load-bearing. Today
230/// [`Pipeline::advance`] always runs the full cascade when not stalled, so a
231/// stored `resume` would be read by nothing — and a field that looks like it
232/// carries information while carrying none is the exact hazard
233/// `docs/engineering-lessons.md` §3.2 is about. `Bus::poll_irq_at_phase` was
234/// removed for the same reason rather than left in place looking wired.
235///
236/// It lands with T-11-002, when stages can stall independently and a partial
237/// resume becomes meaningful.
238#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
239pub struct Stall {
240 /// `PCycle`s remaining.
241 pub cycles: u32,
242 /// Which documented interlock caused it.
243 pub cause: Interlock,
244}
245
246/// State carried across one inter-stage boundary — what travels *with* an
247/// instruction as it advances.
248#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
249pub struct Latch {
250 /// Is an instruction present at this boundary? A bubble is `false`.
251 pub occupied: bool,
252 /// The PC of the instruction in flight.
253 pub pc: u64,
254 /// The raw instruction word (decode is T-11-002).
255 pub word: u32,
256 /// Is this instruction in a branch delay slot?
257 ///
258 /// Travels with the instruction, never as global CPU state — that is the
259 /// whole point (see the module docs).
260 pub in_delay_slot: bool,
261 /// An aborting condition stamped into this latch and every latch upstream.
262 pub abort: Option<Exception>,
263 /// The decoded instruction, filled at `IC`.
264 pub decoded: Decoded,
265 /// `rs` value, read at `RF`.
266 pub rs_val: u64,
267 /// `rt` value, read at `RF`.
268 pub rt_val: u64,
269 /// What `EX` computed, committed at `WB`.
270 pub write_back: WriteBack,
271 /// A memory access `EX` resolved for `DC` to perform.
272 pub mem: Option<MemOp>,
273 /// A COP0 access `EX` resolved: read performed in `DC`, write in `WB`.
274 ///
275 /// Split across those two stages because UM §4.6.9 defines the CP0 bypass
276 /// interlock in terms of a write reaching `WB` while the next instruction
277 /// reads in `DC` — a rule that cannot be expressed if both happen in `EX`.
278 pub cop0: Option<Cop0Access>,
279}
280
281// The inter-stage latch is **copied four times per emulated CPU cycle**, so its size is
282// a performance fact and not merely a layout detail: at ~1.56 M steps a frame those
283// copies are ~15% of the frame (`docs/performance.md` §"The latch copies, anatomized").
284//
285// This pins it. `repr(Rust)` layout is not stable across compiler versions, and the
286// measured breakdown in that document — 120 bytes, with the six scalars occupying
287// exactly their naive sum, so no padding is wasted — would otherwise decay silently
288// into a claim about a toolchain nobody is using any more.
289//
290// **If this fires, do not just change the number.** Either a field was added, in which
291// case re-measure and update the breakdown, or the layout algorithm moved, in which
292// case the "no padding is wasted" conclusion needs re-deriving before it is re-quoted.
293//
294// **120 is not a universal fact**, and the distinction was learned in review. Every
295// field is fixed-width — no `usize`, no references, no pointers — but that does *not*
296// make the layout width-independent, because it is `u64` **alignment** that varies, not
297// pointer size:
298//
299// | target | `size_of::<Latch>()` |
300// | --- | --- |
301// | `x86_64`, `thumbv7em-none-eabihf`, `wasm32-unknown-unknown`, `armv7-*` | 120 |
302// | `i686-*` (32-bit x86, `u64` aligns to 4) | 108 |
303//
304// `armv7` is in that first row by measurement, not by assumption: it is 32-bit with an
305// 8-byte-aligned `u64`, which is the combination the implication below is keyed on, so
306// it is the case that would fail first if the keying were wrong.
307//
308// So there are two assertions rather than one, and neither breaks a cross-compile:
309//
310// 1. **No padding**, universally. This is the property the breakdown actually rests on
311// — that no field ordering would make the struct smaller — and it holds on every
312// target above, `i686` included.
313// 2. **120 where a `u64` aligns to 8**, written as an implication rather than a
314// `#[cfg]`. It pins the documented figure on every ABI the figure describes, and
315// makes no claim on the ones it does not.
316//
317// `#[repr(C)]` is not the alternative either, and this was measured rather than
318// reasoned: `repr(C)` lays fields out in declaration order, which costs **128 bytes**
319// instead of 120 because the two `bool`s can no longer sit in alignment gaps. On a
320// struct copied four times per emulated cycle that adds ~1.2 ms a frame, to exactly
321// the copies `docs/performance.md` is trying to shrink.
322const _: () = {
323 // The scalars: `occupied` + `pc` + `word` + `in_delay_slot` + `rs_val` + `rt_val`.
324 // Written as `size_of` of each field's type rather than as literals, so a field
325 // whose type changes updates this term instead of silently desynchronizing it —
326 // which would mask exactly the padding this is meant to detect.
327 let scalars = core::mem::size_of::<bool>()
328 + core::mem::size_of::<u64>()
329 + core::mem::size_of::<u32>()
330 + core::mem::size_of::<bool>()
331 + core::mem::size_of::<u64>()
332 + core::mem::size_of::<u64>();
333 let parts = core::mem::size_of::<Decoded>()
334 + core::mem::size_of::<Option<Exception>>()
335 + core::mem::size_of::<WriteBack>()
336 + core::mem::size_of::<Option<MemOp>>()
337 + core::mem::size_of::<Option<Cop0Access>>()
338 + scalars;
339 assert!(
340 core::mem::size_of::<Latch>() == parts,
341 "Latch has acquired padding; docs/performance.md's copy-cost breakdown assumes none"
342 );
343 // And the documented figure itself, conditioned on the property that actually
344 // determines it. Writing it as an implication rather than a `#[cfg]` keeps it
345 // compilable everywhere while still pinning 120 on every ABI the number describes:
346 // on 32-bit x86 a `u64` aligns to 4 and `Latch` is 108, which is not a defect and
347 // not something the document claims.
348 assert!(
349 core::mem::align_of::<u64>() != 8 || core::mem::size_of::<Latch>() == 120,
350 "Latch is no longer 120 bytes where a u64 aligns to 8; re-measure docs/performance.md"
351 );
352};
353
354/// Does a load into `load_rt` interlock with the following instruction?
355///
356/// `rs` / `rt` are the *raw encoded fields* of the next instruction,
357/// deliberately named for the encoding rather than for operands — the hardware
358/// checks the fields whether or not they are used as sources, and naming them
359/// `next_rs`/`next_operand` would imply a semantics the check does not have.
360///
361/// Reproduces the hardware's **imprecision**, which is the specification here —
362/// emulating precise behavior is the bug. From
363/// `n64brew_wiki/markdown/VR4300.md` § Microarchitecture → Load Delay Interlock:
364///
365/// - Matches the load's `rt` against the next instruction's `rs` **or** `rt`
366/// field, "whether or not they are actually used as a source". So a load
367/// followed by `LUI` into the same register stalls, and two consecutive loads
368/// into the same register stall.
369/// - A load into `$zero` never interlocks.
370/// - GPR loads interlock only with non-float instructions, and FPR loads only
371/// with float instructions.
372#[must_use]
373pub const fn load_interlocks(load_rt: u8, rs: u8, rt: u8, same_reg_file: bool) -> bool {
374 // The zero register is exempt: a load into $zero is discarded, so nothing
375 // downstream can depend on it.
376 if load_rt == 0 {
377 return false;
378 }
379 if !same_reg_file {
380 return false;
381 }
382 load_rt == rs || load_rt == rt
383}
384
385/// `Status.FR` — whether the FP register file presents 32 independent 64-bit
386/// registers (set) or 16 built from FGR pairs (clear).
387fn fr_of(cop0: &Cop0) -> bool {
388 cop0.read(crate::cop0::reg::STATUS) & (1 << 26) != 0
389}
390
391/// An exception captured at its raising site, with the context the epilogue
392/// needs.
393#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
394struct Pending {
395 /// What happened.
396 exc: Exception,
397 /// The faulting instruction's address.
398 pc: u64,
399 /// Was the faulting instruction in a branch delay slot?
400 in_delay_slot: bool,
401 /// The offending address, for the exceptions that write `BadVAddr`.
402 bad_vaddr: u64,
403}
404
405/// The instruction-granular execution path (ADR 0013), behind `fast-exec`.
406///
407/// A child module so it can reach `Pipeline`'s private fields — privacy extends to
408/// descendants — without widening the surface for anyone else. Absent entirely
409/// with the feature off, so the default build gains nothing (ADR 0011 §1).
410#[cfg(feature = "fast-exec")]
411mod fastexec;
412
413/// The four inter-stage latches plus the pipeline control state.
414#[derive(Clone, Debug, Default, Serialize, Deserialize)]
415// The bools are independent hardware lines and latches -- `prev_was_run`,
416// `flush_pending`, `nmi_pending`, `ll_bit`. Clippy suggests folding them into a
417// state machine; they do not form one, because any combination is reachable and
418// each is set and cleared by a different part of the machine. Naming them is
419// what makes the exception and interrupt rules readable.
420#[allow(clippy::struct_excessive_bools)]
421pub struct Pipeline {
422 /// The **whole** of COP2: one 64-bit latch.
423 ///
424 /// COP2 is not populated on the VR4300, and what remains is a single
425 /// value that every `MTC2`/`DMTC2` writes and every `MFC2`/`DMFC2` reads,
426 /// with the register index ignored. See the `EX` stage and ledger C-20.
427 pub cop2_latch: u64,
428 /// `IC` → `RF`.
429 pub ic_rf: Latch,
430 /// `RF` → `EX`.
431 pub rf_ex: Latch,
432 /// `EX` → `DC`.
433 pub ex_dc: Latch,
434 /// `DC` → `WB`.
435 pub dc_wb: Latch,
436 /// Remaining stall cycles; while non-zero the pipeline does not advance.
437 stall: Option<Stall>,
438 /// Was the *previous* `PCycle` a run cycle (not a stall)?
439 ///
440 /// UM §4.7.1: *"NMI and interrupt exception requests are accepted only if the
441 /// previous `PCycle` was a run cycle."* This is the gate, and it is the
442 /// reason the flag exists at all.
443 prev_was_run: bool,
444 /// The NMI line, latched until an instruction boundary lets it be taken.
445 ///
446 /// A level rather than an edge here: the hardware signal is the falling edge
447 /// of the NMI pin (or bit 6 of the internal interrupt register written over
448 /// `SysAD`), and the edge is what `signal_nmi` represents. Holding it means
449 /// an NMI raised during a stall is taken when the stall drains rather than
450 /// being dropped -- the same reason the timer latches `IP7`.
451 nmi_pending: bool,
452 /// An abort was raised this cycle, so `IC` must fetch a bubble rather than a
453 /// live instruction — the wrong-path fetch would otherwise escape the flush.
454 ///
455 /// Cleared at the end of each [`Pipeline::advance`].
456 flush_pending: bool,
457 /// Instructions retired at `WB` — a work tally, not a time position.
458 pub retired: u64,
459 /// An exception raised this cycle, awaiting dispatch at the end of it.
460 ///
461 /// Captured where it is *raised* rather than reconstructed afterwards,
462 /// because the epilogue needs the faulting instruction's PC and delay-slot
463 /// flag — and by the end of the cycle the reverse cascade has moved every
464 /// latch, so the faulting instruction is no longer where it was.
465 pending: Option<Pending>,
466 /// The virtual address of the access that faulted this cycle.
467 ///
468 /// Recorded where the fault is *detected*, because `BadVAddr` needs the
469 /// address and `Self::access` reports only which exception. Reconstructing
470 /// it at dispatch time is impossible — the `MemOp` has been consumed.
471 fault_vaddr: u64,
472 /// COP1 **control** registers (T-12-006).
473 pub cop1: Cop1Control,
474 /// The floating-point register file (T-13-001).
475 pub fpr: Fpr,
476 /// The joint TLB and its instruction micro-TLB (T-12-004).
477 pub tlb: Tlb,
478 /// The 16 KiB primary instruction cache (T-11-003).
479 pub icache: crate::cache::Icache,
480 /// The 8 KiB primary write-back data cache (T-11-003).
481 pub dcache: crate::cache::Dcache,
482 /// The COP0 register file (T-12-001).
483 ///
484 /// Public because exception dispatch, the TLB and the interrupt path all
485 /// read it, and they land in separate tickets.
486 pub cop0: Cop0,
487 /// The link bit, `LLbit`.
488 ///
489 /// *"set by the LL instruction, cleared by an ERET, and tested by the SC
490 /// instruction"* (UM §3.1). Note what is **absent** from that list: `SC`
491 /// itself does not clear it, and neither does an intervening load or store.
492 /// Clearing it in `SC` is the natural-looking mistake, and it makes a
493 /// retried `LL`/`SC` loop fail forever on the second iteration.
494 ///
495 /// `ERET` is the other half and lands with the exception model (Sprint 2);
496 /// until then nothing clears this, which is correct-so-far rather than
497 /// finished — recorded as such in `docs/cpu.md`.
498 ll_bit: bool,
499}
500
501/// `FCSR.C`, the compare condition — bit 23, above the `Cause` field.
502///
503/// Written only by `C.cond.fmt` and read only by the `BC1` family, so it is
504/// deliberately outside the `Cause`/`Flags` bookkeeping that every other FP
505/// operation touches.
506const FCSR_C: u32 = 1 << 23;
507
508impl Pipeline {
509 /// A fresh, empty pipeline.
510 #[must_use]
511 pub const fn new() -> Self {
512 const EMPTY: Latch = Latch {
513 occupied: false,
514 pc: 0,
515 word: 0,
516 in_delay_slot: false,
517 abort: None,
518 cop0: None,
519 decoded: Decoded {
520 op: crate::decode::Op::Reserved,
521 rs: 0,
522 rt: 0,
523 rd: 0,
524 sa: 0,
525 imm: 0,
526 dest: 0,
527 target: 0,
528 },
529 rs_val: 0,
530 rt_val: 0,
531 write_back: WriteBack::None,
532 mem: None,
533 };
534 Self {
535 // Power-on value: a documented zero (ADR 0004).
536 cop2_latch: 0,
537 ic_rf: EMPTY,
538 rf_ex: EMPTY,
539 ex_dc: EMPTY,
540 dc_wb: EMPTY,
541 stall: None,
542 prev_was_run: false,
543 nmi_pending: false,
544 flush_pending: false,
545 retired: 0,
546 pending: None,
547 fault_vaddr: 0,
548 cop1: Cop1Control::new(),
549 fpr: Fpr::new(),
550 tlb: Tlb::new(),
551 icache: crate::cache::Icache::new(),
552 dcache: crate::cache::Dcache::new(),
553 cop0: Cop0::new(),
554 ll_bit: false,
555 }
556 }
557
558 /// The interlock currently stalling the pipeline, if any.
559 #[must_use]
560 pub const fn stalled_by(&self) -> Option<Interlock> {
561 match self.stall {
562 Some(s) => Some(s.cause),
563 None => None,
564 }
565 }
566
567 /// Was the previous `PCycle` a run cycle? Gates interrupt acceptance.
568 #[must_use]
569 pub const fn prev_cycle_was_run(&self) -> bool {
570 self.prev_was_run
571 }
572
573 /// The link bit, as `SC` would test it.
574 ///
575 /// Exposed for the COP0 / `ERET` work in Sprint 2, which must clear it.
576 #[must_use]
577 pub const fn ll_bit(&self) -> bool {
578 self.ll_bit
579 }
580
581 /// `LLAddr` (COP0 register 17): `PA(31:4)` of the most recent `LL`.
582 ///
583 /// Reads straight out of the COP0 file. `LL` writes it there and nowhere
584 /// else — there is deliberately no second copy, because two stores of one
585 /// architectural value drift, and `MFC0 $rt, $17` would then disagree with
586 /// the CPU's own idea of the link address.
587 #[must_use]
588 pub const fn ll_addr(&self) -> u64 {
589 self.cop0.read(crate::cop0::reg::LL_ADDR)
590 }
591
592 /// Request a stall of `cycles` `PCycle`s.
593 ///
594 /// A zero-cycle request is **not** a stall and is ignored. Recording it would
595 /// still consume a cycle in [`Pipeline::advance`] and mark it as not-a-run
596 /// cycle, which silently inserts a bubble *and* suppresses interrupt
597 /// acceptance on the following cycle (UM §4.7.1) — a one-cycle timing error
598 /// with no visible cause.
599 pub const fn stall_for(&mut self, cycles: u32, cause: Interlock) {
600 if cycles == 0 {
601 return;
602 }
603 self.stall = Some(Stall { cycles, cause });
604 }
605
606 /// Assert the **NMI** line.
607 ///
608 /// Latched, then taken at the next instruction boundary. Nothing in the core
609 /// calls this yet: on hardware the source is the console's reset button,
610 /// which reaches the CPU as PRENMI followed by NMI, and the frontend owns
611 /// that button (Phase 6). It exists now because the exception itself is
612 /// Phase 1 work and belongs with the rest of the exception model — the wire
613 /// from the button is a separate, later concern.
614 pub const fn signal_nmi(&mut self) {
615 self.nmi_pending = true;
616 }
617
618 /// Stamp an abort into `at` **and every latch upstream of it** — the
619 /// kill-younger-instructions step. Instructions older than `at` have already
620 /// passed and are unaffected.
621 ///
622 /// # Ordering contract
623 ///
624 /// **A stage must call this BEFORE it moves its latch.** The instruction
625 /// executing in stage S this cycle sits in S's *input* latch until the move,
626 /// so stamping first is what makes the abort travel with the instruction that
627 /// caused it. Calling it after the move stamps the abort onto the *younger*
628 /// instruction instead, and the causing one escapes — a misalignment that no
629 /// single-cycle assertion catches. `an_abort_survives_the_cascade` advances
630 /// the pipeline to verify it, rather than checking latch state in place.
631 ///
632 /// The abort also raises an internal pending-flush flag, so the instruction
633 /// fetched later in the same cycle is a bubble rather than a live
634 /// wrong-path fetch.
635 pub const fn abort_from(&mut self, at: Stage, exc: Exception) {
636 self.abort_with(at, exc, 0);
637 }
638
639 /// [`Pipeline::abort_from`], additionally recording the offending address
640 /// for the exceptions that write `BadVAddr`.
641 pub const fn abort_with(&mut self, at: Stage, exc: Exception, bad_vaddr: u64) {
642 // Capture the faulting instruction's context NOW. The latch holding it
643 // is the one this stage is reading -- `ex_dc` for DC, `rf_ex` for EX --
644 // and by the end of the cycle the cascade will have moved it on.
645 //
646 // Priority: an exception already pending this cycle came from a LATER
647 // stage (the cascade runs WB first), and UM §4.7.2 gives a later stage
648 // precedence over an earlier one. So the first capture wins.
649 if self.pending.is_none() {
650 let src = match at {
651 Stage::Wb => &self.dc_wb,
652 Stage::Dc => &self.ex_dc,
653 Stage::Ex => &self.rf_ex,
654 Stage::Rf | Stage::Ic => &self.ic_rf,
655 };
656 self.pending = Some(Pending {
657 exc,
658 pc: src.pc,
659 in_delay_slot: src.in_delay_slot,
660 bad_vaddr,
661 });
662 }
663 self.flush_pending = true;
664 match at {
665 Stage::Wb => {
666 self.dc_wb.abort = Some(exc);
667 self.ex_dc.abort = Some(exc);
668 self.rf_ex.abort = Some(exc);
669 self.ic_rf.abort = Some(exc);
670 }
671 Stage::Dc => {
672 self.ex_dc.abort = Some(exc);
673 self.rf_ex.abort = Some(exc);
674 self.ic_rf.abort = Some(exc);
675 }
676 Stage::Ex => {
677 self.rf_ex.abort = Some(exc);
678 self.ic_rf.abort = Some(exc);
679 }
680 Stage::Rf | Stage::Ic => self.ic_rf.abort = Some(exc),
681 }
682 }
683
684 /// Advance the pipeline by exactly one `PCycle`.
685 ///
686 /// Stages run **WB → DC → EX → RF → IC**. Because each stage reads its input
687 /// latch before any upstream stage writes it, no value moves two stages in
688 /// one cycle and no double buffering is required. Do not reorder this.
689 ///
690 /// Hot path: allocation-free.
691 pub fn advance<B: Bus>(&mut self, bus: &mut B, regs: &mut Regs, next_pc: &mut u64) {
692 // The timeline is HELD, not advanced. `Count` runs at half PClock, so
693 // bumping it once per PClock here would run the timer at double rate --
694 // and inventing a parity bit to halve it would be a second incremented
695 // counter, which is what ADR 0006 exists to forbid.
696 //
697 // Holding is the honest option: with no scheduler attached there is no
698 // timeline, so `Count` does not move. Anything exercising `Count` or
699 // `Compare` must call `advance_at` and supply the position.
700 self.advance_at(bus, regs, next_pc, self.cop0.count_now());
701 }
702
703 /// [`Pipeline::advance`], with the scheduler's `Count` timeline supplied.
704 ///
705 /// `Count` is **derived** from the master clock (ADR 0006), so the position
706 /// is passed in rather than incremented here. This is the path the scheduler
707 /// uses ([`crate::Cpu::tick_at`]); [`Pipeline::advance`] is a convenience
708 /// for callers with no scheduler, and **holds** the timeline rather than
709 /// guessing at it.
710 pub fn advance_at<B: Bus>(
711 &mut self,
712 bus: &mut B,
713 regs: &mut Regs,
714 next_pc: &mut u64,
715 count_now: u64,
716 ) {
717 self.cop0.set_now(count_now);
718 // A stall consumes the cycle. The pipeline holds its state, and the cycle
719 // is recorded as NOT a run cycle so an interrupt cannot be accepted on the
720 // cycle following it (UM §4.7.1).
721 if let Some(mut s) = self.stall {
722 s.cycles = s.cycles.saturating_sub(1);
723 self.stall = if s.cycles == 0 { None } else { Some(s) };
724 self.prev_was_run = false;
725 // The timer belongs to the clock, not to the pipeline. `Count` keeps
726 // advancing through the stall, so `Compare` can be reached -- and
727 // passed -- entirely inside one. Latching `IP7` here does not accept
728 // the interrupt (that still needs a run cycle, guarded by
729 // `prev_was_run` below); it only records in `Cause` what the hardware
730 // records, so the handler can run once the pipeline resumes.
731 return;
732 }
733
734 self.wb_stage(regs);
735 self.dc_stage(bus);
736 self.ex_stage(regs, next_pc);
737 self.rf_stage(regs);
738 self.ic_stage(bus, next_pc);
739
740 self.prev_was_run = true;
741 self.flush_pending = false;
742
743 // Dispatch AFTER the cascade, so every stage has seen the abort and the
744 // pipeline is drained, and exactly once per cycle regardless of how many
745 // stages raised.
746 if let Some(p) = self.pending.take() {
747 let d = exception::dispatch(&mut self.cop0, p.exc, p.pc, p.in_delay_slot, p.bad_vaddr);
748 *next_pc = d.vector;
749 self.stall_for(d.stall_cycles, Interlock::Exception);
750 }
751 }
752
753 /// `WB` — commit the result and retire the instruction.
754 fn wb_stage(&mut self, regs: &mut Regs) {
755 if self.dc_wb.occupied && self.dc_wb.abort.is_none() {
756 // The COP0 WRITE lands here (UM §4.6.9). A `Read` in this latch was
757 // already performed in DC and left its value in `write_back`.
758 // The TLB instructions land in WB, with the COP0 write: they are
759 // CP0 operations and `TLBR`/`TLBP` write COP0 registers, so doing
760 // them earlier would let a following `MFC0` read the result a cycle
761 // before hardware produces it.
762 if let Some(Cop0Access::Cop1(Cop1Access::WriteFpr { dest, value, wide })) =
763 self.dc_wb.cop0
764 {
765 // DMTC1 mirrors DMFC1: the FR view, not the physical register.
766 if wide {
767 let fr = fr_of(&self.cop0);
768 self.fpr.write_d(dest, fr, value);
769 } else {
770 self.fpr.write_s(dest, fr_of(&self.cop0), value as u32);
771 }
772 }
773 if let Some(Cop0Access::Cop1(Cop1Access::WriteControl { dest, value })) =
774 self.dc_wb.cop0
775 {
776 self.cop1.ctc1(dest, value);
777 // **`CTC1` can raise on its own.** Writing `FCSR` with a Cause
778 // bit whose Enable is also set meets the trap condition
779 // immediately -- no arithmetic required. n64-systemtest writes
780 // `enable_overflow | cause_overflow` in one go and expects the
781 // FP exception to report the `CTC1` itself as `ExceptPC`.
782 //
783 // Bit 17 (Unimplemented) is unmaskable and traps regardless,
784 // which is why it is tested outside the enable comparison.
785 if self.fcsr_traps_now() {
786 self.abort_from(Stage::Wb, Exception::FloatingPoint);
787 self.dc_wb.occupied = false;
788 return;
789 }
790 }
791 if let Some(Cop0Access::Cop1(Cop1Access::Arith {
792 fmt,
793 funct,
794 ft,
795 fs,
796 fd,
797 })) = self.dc_wb.cop0
798 {
799 if self.fp_arith(fmt, funct, ft, fs, fd) {
800 // Trapped. The instruction does **not** complete, so it must
801 // not reach the retirement tail below: `Random` "decrements
802 // as each instruction executes" (UM §5.4.2), and one that
803 // took an exception did not execute.
804 //
805 // No delay is charged either. A trapped operation abandons
806 // its result, and the exception's own 2-PCycle epilogue is
807 // the cost that applies.
808 self.dc_wb.occupied = false;
809 return;
810 }
811 // The multi-cycle FPU operations stall the pipeline for their
812 // documented rate (UM Table 7-14). The manual's "latency = rate
813 // + 1 for a dependent consumer" falls out of this rather than
814 // being added: the stall holds every stage, so a consumer spends
815 // its own cycle once the stall drains.
816 self.stall_for(crate::fpu::stall_cycles(funct, fmt), Interlock::Mci);
817 }
818 if let Some(Cop0Access::Tlb(op)) = self.dc_wb.cop0 {
819 match op {
820 TlbOp::Read => {
821 let i = (self.cop0.read(crate::cop0::reg::INDEX) & 0x3F) as usize;
822 self.tlb.read_entry(i, &mut self.cop0);
823 }
824 TlbOp::WriteIndexed => {
825 let i = (self.cop0.read(crate::cop0::reg::INDEX) & 0x3F) as usize;
826 // TLBWI CAN overwrite a wired entry; only TLBWR cannot
827 // (UM §5.4.4, p. 150). Guarding both is a natural-looking
828 // mistake that makes wired entries unwritable at all.
829 self.tlb.write_entry(i, &self.cop0);
830 }
831 TlbOp::WriteRandom => {
832 // `Random` never goes below `Wired`, so the wired entries
833 // are protected by the counter's range rather than by a
834 // check here -- which is how hardware does it.
835 let i = (self.cop0.read(crate::cop0::reg::RANDOM) & 0x3F) as usize;
836 self.tlb.write_entry(i, &self.cop0);
837 }
838 TlbOp::Probe => self.tlb.probe(&mut self.cop0),
839 }
840 }
841 if let Some(Cop0Access::Write { dest, value, wide }) = self.dc_wb.cop0 {
842 if wide {
843 self.cop0.dmtc0(dest, value);
844 } else {
845 self.cop0.mtc0(dest, value);
846 }
847 }
848 match self.dc_wb.write_back {
849 WriteBack::None => {}
850 // `Regs::write` discards `$zero`, so no guard is needed here --
851 // and must not be added, or the rule lives in two places.
852 WriteBack::Gpr { dest, value } => regs.write(dest, value),
853 WriteBack::HiLo(hl) => {
854 regs.hi = hl.hi;
855 regs.lo = hl.lo;
856 }
857 WriteBack::Hi(v) => regs.hi = v,
858 WriteBack::Lo(v) => regs.lo = v,
859 }
860 self.retired = self.retired.wrapping_add(1);
861 // "Random decrements as each instruction executes" (UM §5.4.2,
862 // p. 147) -- advanced HERE, at retirement, so it counts executed
863 // instructions rather than cycles.
864 //
865 // This was implemented and then never called from the pipeline, so
866 // `Random` sat at 31 forever and **every `TLBWR` overwrote the same
867 // entry**. A refill handler that needs more than one mapping live at
868 // once therefore destroys its previous entry on each miss and faults
869 // again immediately -- an infinite refill loop, which is exactly what
870 // n64-systemtest hit. A stuck counter is invisible to any test that
871 // calls `tick_random` itself.
872 self.cop0.tick_random();
873 }
874 self.dc_wb.occupied = false;
875 }
876
877 /// Perform the COP0/COP1 **reads** an instruction resolved, leaving the value
878 /// in the latch's `write_back` for the commit to store.
879 ///
880 /// The fourth latch-independent primitive (`docs/cpu.md`), and the one PR #231
881 /// deliberately left alone: at that point it had a single caller, and the shape
882 /// of a seam guessed without a second caller is a guess. `fast-exec`'s
883 /// sequential path is that caller, so the extraction is now driven rather than
884 /// speculative.
885 ///
886 /// It takes the latch **by reference** rather than the fields by value because
887 /// the three reads each replace `write_back` wholesale, and threading four
888 /// values in and one out would be a worse signature than the one the accurate
889 /// path already has.
890 ///
891 /// A **read** happens here, in `DC`; a **write** happens at `WB`. They are
892 /// split across the two stages because UM §4.6.9 defines the CP0 bypass
893 /// interlock in terms of a write reaching `WB` while the next instruction reads
894 /// in `DC` — a rule that cannot be expressed if both happen in one stage.
895 fn apply_cop0_read(&self, out: &mut Latch) {
896 if out.occupied
897 && out.abort.is_none()
898 && let Some(Cop0Access::Cop1(Cop1Access::ReadFpr { src, dest, wide })) = out.cop0
899 {
900 out.write_back = WriteBack::Gpr {
901 dest,
902 // DMFC1 applies the FR view -- it does NOT move the physical
903 // register. UM Ch. 17's pseudocode is explicit:
904 //
905 // if FR = 1 then data <- FGR[fs]
906 // else if fs0 = 0 then data <- FGR[fs+1] || FGR[fs]
907 // else data <- undefined
908 //
909 // So with FR = 0 and an even `fs` it reads the PAIR, exactly like
910 // LDC1. Only an odd `fs` with FR = 0 is undefined -- and it is
911 // *undefined*, not a Reserved Instruction exception.
912 //
913 // MFC1 moves the low word, sign-extended, in both modes.
914 value: if wide {
915 self.fpr.read_d(src, fr_of(&self.cop0))
916 } else {
917 crate::alu::sext32(self.fpr.read_s(src, fr_of(&self.cop0)))
918 },
919 };
920 }
921 if out.occupied
922 && out.abort.is_none()
923 && let Some(Cop0Access::Cop1(Cop1Access::ReadControl { src, dest })) = out.cop0
924 {
925 out.write_back = WriteBack::Gpr {
926 dest,
927 // CFC1 is a 32-bit move, so the result is sign-extended into the
928 // 64-bit GPR exactly as MFC0's is.
929 value: crate::alu::sext32(self.cop1.cfc1(src)),
930 };
931 }
932 if out.occupied
933 && out.abort.is_none()
934 && let Some(Cop0Access::Read { src, dest, wide }) = out.cop0
935 {
936 let value = if wide {
937 self.cop0.dmfc0(src)
938 } else {
939 self.cop0.mfc0(src)
940 };
941 out.write_back = WriteBack::Gpr { dest, value };
942 }
943 }
944
945 /// Update the `Cause.IP` lines from the hardware: the RCP's aggregate
946 /// interrupt and the `Count == Compare` timer edge.
947 ///
948 /// **Asserting a line is not recognizing an interrupt**, and separating the
949 /// two is the point. The `IP` bits track what the hardware is *asserting*
950 /// regardless of masks; recognition then applies `IE`/`EXL`/`ERL`/`IM` and is
951 /// the caller's business. Folding them together would make a masked interrupt
952 /// invisible to `MFC0 Cause`, which software polls.
953 ///
954 /// Latch-independent, like [`Self::fetch_word`] and [`Self::ex_gate`]: it
955 /// stamps nothing and names no [`Stage`]. Asserting a line happens on
956 /// hardware whether or not the CPU is in a position to take the interrupt, so
957 /// the instruction-granular path (ADR 0013) needs exactly this and nothing
958 /// more.
959 ///
960 /// `IP2` is the RCP's aggregate line from the MI (libdragon `cop0.h`:
961 /// `C0_INTERRUPT_RCP = C0_INTERRUPT_2`; ledger U-4). `IP3` is CART, `IP4`
962 /// PRENMI, `IP7` the timer; the rest are unused on this board.
963 fn sample_interrupt_lines<B: Bus>(&mut self, bus: &mut B) {
964 self.cop0.set_ip(2, bus.poll_irq());
965 // IP7 is LATCHED on the match and stays set until `Compare` is written
966 // (UM §6.4.18, p. 200) -- note the one-way `if`, with no `else` clearing
967 // it. Modeling it as a level tied to `Count == Compare` looks tidier
968 // and silently DROPS any timer interrupt that fires while `EXL` is set,
969 // because the equality holds for one tick and the handler never sees it.
970 //
971 // The trigger is the rising EDGE of the match, not the standing
972 // equality: both `Count` and `Compare` reset to zero, so an equality
973 // test latches `IP7` before a single instruction retires. See
974 // `Cop0::timer_edge`.
975 if self.cop0.timer_edge() {
976 self.cop0.set_ip(7, true);
977 }
978 }
979
980 /// `DC` — the data-cache access, and the interrupt sampling point.
981 ///
982 /// The stage placement is documented, not inherited from a reference
983 /// implementation: UM Figure 4-12 puts `INTR` in the `DC` column and §4.7.6
984 /// "DC-Stage Interlock and Exception Priorities" lists the interrupt
985 /// exception among them.
986 fn dc_stage<B: Bus>(&mut self, bus: &mut B) {
987 // Sample the lines once per PCycle here. `sample_interrupt_lines` carries
988 // why asserting and recognizing are two different steps; what follows is
989 // the recognition half.
990 self.sample_interrupt_lines(bus);
991
992 // Accepted only if the previous PCycle was a run cycle (UM §4.7.1). This
993 // is the ONLY interrupt recognition predicate in the tree -- carrying two
994 // subtly different ones is a known source of one-cycle discrepancies in
995 // other emulators.
996
997 // NMI is checked FIRST and without consulting `interrupt_pending`.
998 // *"Unlike all other interrupts, this interrupt is not maskable; it
999 // occurs regardless of the settings of the EXL, ERL, and the IE bits"*
1000 // (UM SS6.4.6). Only the run-cycle gate applies, because NMI is still
1001 // *"taken only at instruction boundaries"* -- which is exactly what
1002 // `prev_was_run` expresses (UM SS4.7.1).
1003 if self.prev_was_run && self.nmi_pending {
1004 self.nmi_pending = false;
1005 self.abort_from(Stage::Dc, Exception::Nmi);
1006 } else if self.prev_was_run
1007 && self.cop0.interrupt_pending()
1008 && self.interrupt_has_an_instruction_to_charge()
1009 {
1010 self.abort_from(Stage::Dc, Exception::Interrupt);
1011 }
1012 // The memory access. This is the point the scheduler interleaves the RCP
1013 // around -- the whole reason the pipeline is modeled at all (ADR 0007).
1014 let mut out = self.ex_dc;
1015 if out.occupied
1016 && out.abort.is_none()
1017 && let Some(op) = out.mem
1018 {
1019 match self.access(bus, op) {
1020 Ok(wb) => out.write_back = wb,
1021 // Stamp before the latch move so the abort travels with the
1022 // instruction that caused it -- see `abort_from`.
1023 Err(exc) => {
1024 self.abort_with(Stage::Dc, exc, self.fault_vaddr);
1025 out = self.ex_dc;
1026 }
1027 }
1028 }
1029 // The COP0 READ happens here, in DC (UM §4.6.9). The write does not --
1030 // it happens in WB, and keeping them in different stages is what makes
1031 // the CP0 bypass interlock expressible at all.
1032 self.apply_cop0_read(&mut out);
1033 self.dc_wb = out;
1034 self.ex_dc.occupied = false;
1035 }
1036
1037 /// The operand bypass network (UM §4.6).
1038 ///
1039 /// *"Bypassing ... allows data and conditions produced in the `EX`, `DC` and
1040 /// `WB` stages to be made available to the `EX` stage of the next cycle."*
1041 ///
1042 /// Without this, back-to-back dependent instructions read stale registers and
1043 /// essentially every real program computes wrong values — `LUI`+`ORI`, the
1044 /// standard way to build a 32-bit constant, breaks immediately. Its absence
1045 /// was invisible to every unit test in this crate and was caught only by
1046 /// `a_program_executes_through_the_whole_pipeline`.
1047 ///
1048 /// By the time `EX` runs, the reverse cascade has already committed one
1049 /// instruction (`WB` ran first, so the register file is current) and moved the
1050 /// next into `dc_wb`. Exactly **one** producer can therefore still be
1051 /// uncommitted, and `dc_wb` is it.
1052 ///
1053 /// Loads are the case this does *not* cover — a load's value is not ready in
1054 /// time, which is precisely why the hardware has a load-delay interlock. That
1055 /// lands with T-11-003 alongside the loads themselves.
1056 fn bypass(&self, reg: u8, regs: &Regs) -> u64 {
1057 if reg != 0
1058 && self.dc_wb.occupied
1059 && self.dc_wb.abort.is_none()
1060 && let WriteBack::Gpr { dest, value } = self.dc_wb.write_back
1061 && dest == reg
1062 {
1063 return value;
1064 }
1065 regs.read(reg)
1066 }
1067
1068 /// `HI`/`LO` as `EX` should see them, bypassing an uncommitted producer.
1069 fn bypass_hi_lo(&self, regs: &Regs) -> HiLo {
1070 if self.dc_wb.occupied && self.dc_wb.abort.is_none() {
1071 match self.dc_wb.write_back {
1072 WriteBack::HiLo(hl) => return hl,
1073 WriteBack::Hi(v) => return HiLo { hi: v, lo: regs.lo },
1074 WriteBack::Lo(v) => return HiLo { hi: regs.hi, lo: v },
1075 _ => {}
1076 }
1077 }
1078 HiLo {
1079 hi: regs.hi,
1080 lo: regs.lo,
1081 }
1082 }
1083
1084 /// Map a TLB fault to the exception it raises.
1085 ///
1086 /// The `store` flag selects `TLBL` vs `TLBS`; the *variant* selects the
1087 /// vector. Both matter and they are independent. `wide` is the addressing
1088 /// width of the faulting access, which picks the XTLB refill vector over the
1089 /// 32-bit one -- it is only meaningful for a refill, since every other TLB
1090 /// fault takes the general vector regardless.
1091 const fn tlb_exception(f: crate::tlb::TlbFault, store: bool, wide: bool) -> Exception {
1092 match f {
1093 crate::tlb::TlbFault::Refill => Exception::TlbRefill { store, wide },
1094 crate::tlb::TlbFault::Invalid => Exception::TlbInvalid { store },
1095 // Modified only ever arises on a store, so it carries no flag.
1096 crate::tlb::TlbFault::Modified => Exception::TlbModified,
1097 }
1098 }
1099
1100 /// Translate a data address through the TLB.
1101 ///
1102 /// Returns the whole [`crate::addr::Physical`], **cacheability included**.
1103 /// It used to return the address alone, which is exactly the information a
1104 /// D-cache needs and cannot recover: once KSEG0 and KSEG1 have both become
1105 /// the same physical number, nothing downstream can tell them apart.
1106 fn translate_data(
1107 &mut self,
1108 vaddr: u64,
1109 store: bool,
1110 ) -> Result<crate::addr::Physical, Exception> {
1111 let asid = (self.cop0.read(crate::cop0::reg::ENTRY_HI) & 0xFF) as u8;
1112 let access = self.access_mode();
1113 match crate::addr::translate_via(&mut self.tlb, vaddr, asid, store, access) {
1114 Ok(p) => Ok(p),
1115 Err(e) => {
1116 self.fault_vaddr = vaddr;
1117 self.note_shutdown();
1118 Err(match e {
1119 crate::addr::TranslateError::Address => Exception::AddressError { store },
1120 crate::addr::TranslateError::Tlb(f) => {
1121 Self::tlb_exception(f, store, access.wide)
1122 }
1123 })
1124 }
1125 }
1126 }
1127
1128 /// The unaligned `LWL`/`LWR`/`LDL`/`LDR`/`SWL`/`SWR`/`SDL`/`SDR` family.
1129 ///
1130 /// Split out of [`Pipeline::access`] purely for size; the merging rules live
1131 /// in [`crate::mem`] and the alignment exemption is by construction — being
1132 /// usable at any byte offset is the entire reason these instructions exist.
1133 ///
1134 /// # Errors
1135 ///
1136 /// A TLB fault on the container address.
1137 fn access_unaligned<B: Bus>(
1138 &mut self,
1139 bus: &mut B,
1140 op: crate::decode::Op,
1141 addr: u64,
1142 rt: u64,
1143 dest: u8,
1144 ) -> Result<WriteBack, Exception> {
1145 use crate::decode::Op;
1146 // The unaligned family splits into loads and stores, and the
1147 // TLB check differs: a store must find the `D` bit set.
1148 let is_store = matches!(op, Op::Swl | Op::Swr | Op::Sdl | Op::Sdr);
1149 // On a fault, `BadVAddr` reports the address the INSTRUCTION named, not
1150 // the container address translated on its behalf. `translate_data`
1151 // records whatever it was handed, so the aligned-down value has to be
1152 // corrected back -- otherwise a fault on `SWL 0x12345001` reports
1153 // `0x12345000`, and n64-systemtest checks that exact case.
1154 let translate = |p: &mut Self, a: u64| match p.translate_data(a, is_store) {
1155 Ok(v) => Ok(v),
1156 Err(e) => {
1157 p.fault_vaddr = addr;
1158 Err(e)
1159 }
1160 };
1161 // `Status.RE` applies the **byte** swap here, `addr ^ 7`, not the swap for
1162 // the container's width. These instructions address individual bytes, so
1163 // the byte lane is what moves — and one XOR relocates the container and
1164 // complements the byte index together, which is why the two do not need
1165 // separate treatment:
1166 //
1167 // `LWL 0` becomes container 4 with byte index 3, because
1168 // `0 ^ 7 == 7`, `7 & !3 == 4` and `7 & 3 == 3`.
1169 //
1170 // Derived from n64-systemtest's own expected tables rather than guessed:
1171 // `SWL` at offset 0 writes a single byte, `rt`'s most significant, into
1172 // the doubleword's LAST byte, which no width-based swap produces.
1173 let eaddr = if self.reverse_endian() {
1174 addr ^ 7
1175 } else {
1176 addr
1177 };
1178 let word_addr = translate(self, eaddr & !3)?;
1179 let dword_addr = translate(self, eaddr & !7)?;
1180 let byte4 = eaddr & 3;
1181 let byte8 = eaddr & 7;
1182 Ok(match op {
1183 Op::Lwl | Op::Lwr => {
1184 let w = self.read_width(bus, word_addr, 4) as u32;
1185 let v = if matches!(op, Op::Lwl) {
1186 mem::lwl(rt, w, byte4)
1187 } else {
1188 mem::lwr(rt, w, byte4)
1189 };
1190 WriteBack::Gpr { dest, value: v }
1191 }
1192 Op::Ldl | Op::Ldr => {
1193 let d = self.read_width(bus, dword_addr, 8);
1194 let v = if matches!(op, Op::Ldl) {
1195 mem::ldl(rt, d, byte8)
1196 } else {
1197 mem::ldr(rt, d, byte8)
1198 };
1199 WriteBack::Gpr { dest, value: v }
1200 }
1201 Op::Swl | Op::Swr => {
1202 let w = self.read_width(bus, word_addr, 4) as u32;
1203 let merged = if matches!(op, Op::Swl) {
1204 mem::swl(rt, w, byte4)
1205 } else {
1206 mem::swr(rt, w, byte4)
1207 };
1208 self.write_width(bus, word_addr, 4, u64::from(merged));
1209 WriteBack::None
1210 }
1211 Op::Sdl | Op::Sdr => {
1212 let d = self.read_width(bus, dword_addr, 8);
1213 let merged = if matches!(op, Op::Sdl) {
1214 mem::sdl(rt, d, byte8)
1215 } else {
1216 mem::sdr(rt, d, byte8)
1217 };
1218 self.write_width(bus, dword_addr, 8, merged);
1219 WriteBack::None
1220 }
1221 // `MemOp::Unaligned` is only ever constructed for the eight
1222 // forms above.
1223 _ => WriteBack::None,
1224 })
1225 }
1226
1227 /// Which coprocessor an instruction needs, if that unit is disabled.
1228 ///
1229 /// `Status.CU` (31:28) is one bit per unit. Two rules that are easy to miss:
1230 ///
1231 /// - **COP0 is usable from kernel mode regardless of `CU0`** — otherwise the
1232 /// CPU could not run an exception handler before `Status` had been set up,
1233 /// which is a chicken-and-egg the hardware does not have. Kernel mode is
1234 /// `KSU == 0`, or `EXL`/`ERL` set.
1235 /// - A **valid but unimplemented** COP1 encoding still checks `CU1`. With
1236 /// `CU1` set it must *not* raise here, so that Sprint 3's arithmetic is an
1237 /// addition rather than a behavior change.
1238 fn unusable_coprocessor(&self, d: Decoded) -> Option<u8> {
1239 use crate::decode::Op;
1240 let unit = match d.op {
1241 Op::Mfc0
1242 | Op::Dmfc0
1243 | Op::Mtc0
1244 | Op::Dmtc0
1245 | Op::Tlbr
1246 | Op::Tlbwi
1247 | Op::Tlbwr
1248 | Op::Tlbp
1249 | Op::Eret => 0,
1250 Op::Cfc1
1251 | Op::Ctc1
1252 | Op::Mfc1
1253 | Op::Dmfc1
1254 | Op::Mtc1
1255 | Op::Dmtc1
1256 | Op::Lwc1
1257 | Op::Ldc1
1258 | Op::Swc1
1259 | Op::Sdc1
1260 | Op::Cop1Unimplemented
1261 | Op::Cop1ReservedControl
1262 // FP arithmetic is a COP1 instruction like any other and must raise
1263 // Coprocessor Unusable with `CU1` clear. It was omitted when
1264 // `FpArith` was introduced, which left the arithmetic executing
1265 // unconditionally -- a program that had not enabled COP1 would get
1266 // results instead of an exception.
1267 | Op::FpArith => 1,
1268 Op::Cop2 | Op::Cop2ReservedControl | Op::Mfc2 | Op::Dmfc2 | Op::Mtc2 => 2,
1269 _ => return None,
1270 };
1271 let status = self.cop0.read(crate::cop0::reg::STATUS);
1272 if unit == 0 {
1273 /// `Status.KSU` (4:3) — 0 is kernel, 1 supervisor, 2 user.
1274 const KSU: u64 = 0b11 << 3;
1275 /// `Status.EXL` (1) or `Status.ERL` (2): either forces kernel mode
1276 /// regardless of `KSU`, which is what makes an exception handler's
1277 /// first instructions safe.
1278 const EXL_OR_ERL: u64 = 0b110;
1279 let kernel = status & KSU == 0 || status & EXL_OR_ERL != 0;
1280 if kernel {
1281 return None;
1282 }
1283 }
1284 if status & (1 << (28 + u64::from(unit))) == 0 {
1285 return Some(unit);
1286 }
1287 None
1288 }
1289
1290 /// `Status.RE` — reverse endian, and **only in User mode** (UM §5.2).
1291 ///
1292 /// `EXL`/`ERL` force kernel mode regardless of `KSU`, so an exception
1293 /// handler reads memory the way it wrote it even with `RE` set. That is the
1294 /// same rule the coprocessor-usability check applies, for the same reason.
1295 fn reverse_endian(&self) -> bool {
1296 /// `Status.RE`, bit 25.
1297 const RE: u64 = 1 << 25;
1298 /// `Status.KSU` (4:3) — 2 is user.
1299 const KSU_USER: u64 = 0b10 << 3;
1300 /// `Status.KSU` (4:3).
1301 const KSU: u64 = 0b11 << 3;
1302 /// `Status.EXL` (1) or `Status.ERL` (2).
1303 const EXL_OR_ERL: u64 = 0b110;
1304 let status = self.cop0.read(crate::cop0::reg::STATUS);
1305 status & RE != 0 && status & KSU == KSU_USER && status & EXL_OR_ERL == 0
1306 }
1307
1308 /// The reverse-endian byte-lane swap for an access of `width` bytes.
1309 ///
1310 /// Reversing endianness on a 64-bit datapath is a permutation of byte lanes
1311 /// within the doubleword, expressed as an XOR of the low address bits: a
1312 /// doubleword access covers the whole lane set and does not move, a word
1313 /// moves by 4, a halfword by 6 and a byte by 7.
1314 const fn re_swap(width: u64) -> u32 {
1315 match width {
1316 1 => 7,
1317 2 => 6,
1318 4 => 4,
1319 _ => 0,
1320 }
1321 }
1322
1323 /// Translate a data address and apply the reverse-endian swap, if any.
1324 ///
1325 /// The swap is applied to the **physical** address, after translation. It
1326 /// touches only bits 2:0, which every translation maps identically, so this
1327 /// is exactly equivalent to swapping the virtual address first — and it
1328 /// keeps `BadVAddr` raw on a fault, which n64-systemtest asserts directly
1329 /// ("RE unmapped access keeps raw `BadVAddr`").
1330 ///
1331 /// # Errors
1332 ///
1333 /// Whatever [`Pipeline::translate_data`] raises on the untransformed
1334 /// address: a TLB fault, **or** [`Exception::AddressError`] when the address
1335 /// is not valid in the current privilege mode.
1336 fn translate_re(
1337 &mut self,
1338 vaddr: u64,
1339 store: bool,
1340 width: u64,
1341 ) -> Result<crate::addr::Physical, Exception> {
1342 let mut p = self.translate_data(vaddr, store)?;
1343 if self.reverse_endian() {
1344 p.addr ^= Self::re_swap(width);
1345 }
1346 Ok(p)
1347 }
1348
1349 /// The privilege mode and addressing width the next access runs under.
1350 ///
1351 /// `EXL`/`ERL` force Kernel regardless of `KSU` — the same rule the
1352 /// coprocessor-usability check applies, and what makes an exception
1353 /// handler's first instructions safe.
1354 ///
1355 /// The addressing-width bit is picked **for the resolved mode**, not for
1356 /// `KSU`: a User-mode program that takes an exception runs the handler in
1357 /// Kernel mode, and the handler's addresses are governed by `KX`.
1358 fn access_mode(&self) -> crate::addr::Access {
1359 /// `Status.KSU`, bits 4:3.
1360 const KSU: u64 = 0b11 << 3;
1361 /// `Status.EXL` (1) or `Status.ERL` (2).
1362 const EXL_OR_ERL: u64 = 0b110;
1363 let status = self.cop0.read(crate::cop0::reg::STATUS);
1364 let mode = if status & EXL_OR_ERL != 0 {
1365 crate::addr::Mode::Kernel
1366 } else {
1367 match (status & KSU) >> 3 {
1368 1 => crate::addr::Mode::Supervisor,
1369 2 => crate::addr::Mode::User,
1370 // 0 is Kernel. 3 is not a defined encoding and the manual
1371 // gives it no behavior; it falls in with Kernel because that is
1372 // what `KSU == 0` already does and it keeps the match total
1373 // without inventing a fourth mode. Note this is the most
1374 // PERMISSIVE choice, not the safest one -- if a test ever pins
1375 // `KSU == 3`, it belongs in the accuracy ledger, not here.
1376 _ => crate::addr::Mode::Kernel,
1377 }
1378 };
1379 // `Status.UX` (5), `SX` (6), `KX` (7).
1380 let wide = match mode {
1381 crate::addr::Mode::User => status & (1 << 5) != 0,
1382 crate::addr::Mode::Supervisor => status & (1 << 6) != 0,
1383 crate::addr::Mode::Kernel => status & (1 << 7) != 0,
1384 };
1385 crate::addr::Access {
1386 mode,
1387 wide,
1388 erl: self.erl(),
1389 }
1390 }
1391
1392 /// Is there an instruction in `DC` that an interrupt can legitimately be
1393 /// charged to (ledger **R-18**)?
1394 ///
1395 /// An interrupt is taken *"at instruction boundaries"* (UM §4.7.1), and
1396 /// [`Pipeline::abort_from`] records `EPC` from whatever sits in `ex_dc`. Two
1397 /// things can sit there that are **not** an instruction boundary, and
1398 /// charging either one corrupts the return address:
1399 ///
1400 /// - **A bubble.** A default [`Latch`] has `pc == 0`, so the interrupt is
1401 /// charged to address 0 and the handler returns there.
1402 /// - **An `ERET` that has already redirected.** `ERET` resolves in `EX`
1403 /// (see [`Pipeline::ex_stage`]), clearing `Status.EXL` and pointing
1404 /// `next_pc` at `EPC`. One cycle later the `DC` check sees `EXL == 0` and
1405 /// fires — onto the `ERET` itself, overwriting the very `EPC` it was about
1406 /// to consume. The handler then returns to the `ERET`, which resumes at its
1407 /// own address: an architectural **livelock**. This is what stopped
1408 /// Banjo-Tooie dead, measured at 390,625 `ERET` retirements per frame with
1409 /// no other instruction ever reaching `WB`.
1410 ///
1411 /// Both must be excluded together. Excluding only the `ERET` moves the
1412 /// corruption to the refill bubble that follows its redirect (`EPC == 0`);
1413 /// excluding only bubbles leaves the `ERET` livelock intact.
1414 ///
1415 /// **Deferring loses nothing, by two different mechanisms** — and the
1416 /// distinction matters, because `Cause.IP` is *not* uniformly a bus level:
1417 ///
1418 /// - **`IP2`** (the RCP line) is re-sampled from `bus.poll_irq()` every
1419 /// cycle, so a still-asserted line is simply seen again next cycle.
1420 /// - **`IP7`** (the timer) is **latched** on the `Count`/`Compare` edge and
1421 /// stays set until `Compare` is written (UM §6.4.18) — see the one-way
1422 /// `if` in [`Pipeline::dc_stage`]. It survives deferral because it is
1423 /// sticky, not because it is re-sampled.
1424 ///
1425 /// Either way the interrupt is taken on the next cycle that presents a real
1426 /// instruction — after an `ERET`, the instruction it returned to. It is
1427 /// re-attributed, never dropped.
1428 fn interrupt_has_an_instruction_to_charge(&self) -> bool {
1429 self.ex_dc.occupied && !matches!(self.ex_dc.cop0, Some(Cop0Access::Eret))
1430 }
1431
1432 /// Are the MIPS III 64-bit operations reserved right now?
1433 ///
1434 /// Yes in User or Supervisor mode with that mode's `UX`/`SX` bit clear; never
1435 /// in Kernel mode, whatever `KX` says. Both halves are load-bearing: gating
1436 /// on the width bit alone would reserve them for a 32-bit kernel, and gating
1437 /// on the mode alone would reserve them for a 64-bit user program.
1438 fn sixty_four_bit_is_reserved(&self) -> bool {
1439 let a = self.access_mode();
1440 !matches!(a.mode, crate::addr::Mode::Kernel) && !a.wide
1441 }
1442
1443 /// `Status.ERL` — the error level, which makes KUSEG unmapped (UM §5.2.2).
1444 fn erl(&self) -> bool {
1445 self.cop0.read(crate::cop0::reg::STATUS) & (1 << 2) != 0
1446 }
1447
1448 /// Mirror a TLB shutdown into `Status.TS`.
1449 ///
1450 /// `TS` is read-only to software (UM Fig. 6-6, p. 167), so it goes through
1451 /// `set_hardware`. Without this the shutdown flag would be recorded inside
1452 /// the TLB and never observed, which is worse than not tracking it: software
1453 /// polls `Status.TS` precisely to discover that the TLB has died.
1454 fn note_shutdown(&mut self) {
1455 if self.tlb.is_shutdown() {
1456 let status = self.cop0.read(crate::cop0::reg::STATUS);
1457 self.cop0
1458 .set_hardware(crate::cop0::reg::STATUS, status | (1 << 21));
1459 }
1460 }
1461
1462 /// Perform a memory access.
1463 ///
1464 /// # Errors
1465 ///
1466 /// [`Exception::AddressError`] when an *aligned* access is misaligned. The
1467 /// `LWL`/`LWR` family is exempt by construction — being usable at any byte
1468 /// offset is the entire reason it exists.
1469 fn access<B: Bus>(&mut self, bus: &mut B, op: MemOp) -> Result<WriteBack, Exception> {
1470 // TODO(T-11-003): charge the cache-miss cost (8..=9 + M PCycles for a
1471 // D-cache fill, UM Table 11-1) once `M` is measured -- accuracy-ledger C-1.
1472 match op {
1473 MemOp::Load { kind, addr, dest } => {
1474 if !kind.is_aligned(addr) {
1475 self.fault_vaddr = addr;
1476 return Err(Exception::AddressError { store: false });
1477 }
1478 let phys = self.translate_re(addr, false, kind.width())?;
1479 let raw = self.read_width(bus, phys, kind.width());
1480 Ok(WriteBack::Gpr {
1481 dest,
1482 value: kind.shape(raw),
1483 })
1484 }
1485 MemOp::Store { kind, addr, value } => {
1486 if !kind.is_aligned(addr) {
1487 self.fault_vaddr = addr;
1488 return Err(Exception::AddressError { store: true });
1489 }
1490 let phys = self.translate_re(addr, true, kind.width())?;
1491 self.write_width(bus, phys, kind.width(), value);
1492 Ok(WriteBack::None)
1493 }
1494 // Load linked: an ordinary aligned load that also arms the link.
1495 MemOp::LinkedLoad { kind, addr, dest } => {
1496 if !kind.is_aligned(addr) {
1497 // "If either of the low-order two bits of the address are
1498 // not zero, an address error exception takes place" (UM §16
1499 // p. 453) -- and the link is NOT armed, because the
1500 // instruction did not complete.
1501 self.fault_vaddr = addr;
1502 return Err(Exception::AddressError { store: false });
1503 }
1504 let phys = self.translate_re(addr, false, kind.width())?;
1505 let raw = self.read_width(bus, phys, kind.width());
1506 self.ll_bit = true;
1507 // "the value with the high-order four bits of the physical
1508 // address PA(31:4) ... zero-extended" (UM Figure 5-17). Written
1509 // via `set_hardware` because LLAddr is software-writable too:
1510 // this is the hardware side effect, not an MTC0.
1511 self.cop0
1512 .set_hardware(crate::cop0::reg::LL_ADDR, u64::from(phys.addr >> 4));
1513 Ok(WriteBack::Gpr {
1514 dest,
1515 value: kind.shape(raw),
1516 })
1517 }
1518 // Store conditional: the store is conditional, the flag write is not.
1519 MemOp::ConditionalStore {
1520 kind,
1521 addr,
1522 value,
1523 dest,
1524 } => {
1525 if !kind.is_aligned(addr) {
1526 // "If this instruction both fails and causes an exception,
1527 // the exception takes precedence" (UM §16 p. 487) -- so the
1528 // address check runs before the link bit is even consulted,
1529 // and `dest` is left alone.
1530 self.fault_vaddr = addr;
1531 return Err(Exception::AddressError { store: true });
1532 }
1533 let stored = self.ll_bit;
1534 if stored {
1535 let phys = self.translate_re(addr, true, kind.width())?;
1536 self.write_width(bus, phys, kind.width(), value);
1537 }
1538 // Written whether or not the store happened. Note the link bit
1539 // is deliberately NOT cleared here -- see `Pipeline::ll_bit`.
1540 Ok(WriteBack::Gpr {
1541 dest,
1542 value: u64::from(stored),
1543 })
1544 }
1545 // FP loads and stores. Same alignment and translation rules as the
1546 // integer forms -- only the destination register file differs.
1547 MemOp::Fp { op, addr, ft } => {
1548 use crate::decode::Op;
1549 let double = matches!(op, Op::Ldc1 | Op::Sdc1);
1550 let store = matches!(op, Op::Swc1 | Op::Sdc1);
1551 let align = if double { 7 } else { 3 };
1552 if addr & align != 0 {
1553 self.fault_vaddr = addr;
1554 return Err(Exception::AddressError { store });
1555 }
1556 let phys = self.translate_re(addr, store, if double { 8 } else { 4 })?;
1557 // `Status.FR` selects the register-file view; a double under
1558 // FR = 0 occupies an FGR pair.
1559 let fr = fr_of(&self.cop0);
1560 match op {
1561 Op::Lwc1 => {
1562 let v = self.read_width(bus, phys, 4) as u32;
1563 self.fpr.write_s(ft, fr_of(&self.cop0), v);
1564 }
1565 Op::Ldc1 => {
1566 let v = self.read_width(bus, phys, 8);
1567 self.fpr.write_d(ft, fr, v);
1568 }
1569 Op::Swc1 => {
1570 let v = self.fpr.read_s(ft, fr_of(&self.cop0));
1571 self.write_width(bus, phys, 4, u64::from(v));
1572 }
1573 _ => {
1574 let v = self.fpr.read_d(ft, fr);
1575 self.write_width(bus, phys, 8, v);
1576 }
1577 }
1578 Ok(WriteBack::None)
1579 }
1580 // EMUX — n64-systemtest's emulator-extension protocol, executed
1581 // here because it needs the bus. See `docs/cpu.md` and ledger C-8:
1582 // hardware leaves COP0 CO `funct` 0x20-0x3F inert, which is exactly
1583 // why emux claimed the range.
1584 MemOp::Emux {
1585 funct,
1586 code,
1587 ptr,
1588 len,
1589 dest,
1590 } => self.emux(bus, funct, code, ptr, len, dest),
1591 MemOp::Cache { addr, op } => self.cache_op(bus, addr, op),
1592 // The unaligned family accesses the ALIGNED container holding `addr`
1593 // and merges, so it can never raise an address error -- but it CAN
1594 // still raise a TLB fault, which is why it is fallible.
1595 MemOp::Unaligned { op, addr, rt, dest } => {
1596 self.access_unaligned(bus, op, addr, rt, dest)
1597 }
1598 }
1599 }
1600
1601 /// **EMUX** — the emulator-extension protocol n64-systemtest uses to talk to
1602 /// its host (`ref-proj/n64-systemtest/src/emux.rs`).
1603 ///
1604 /// Three operations, all COP0 CO encodings in the `funct` 0x20-0x3F range
1605 /// that hardware retires inertly (ledger **C-8**):
1606 ///
1607 /// | `funct` | name | effect |
1608 /// |---|---|---|
1609 /// | `0x20` | `xdetect` | return a capability bitmask in `rd` |
1610 /// | `0x25` | `xlog` | print `GPR[rt]` bytes from `GPR[rd]` |
1611 /// | `0x2C` | `xioctl` | `code 1` exit, `code 2` "fast mode" |
1612 ///
1613 /// # Why implement it rather than keep the no-op
1614 ///
1615 /// `xlog` is a console that needs **no PI, SI or `ISViewer` emulation** — the
1616 /// suite's output reaches the host directly. `xioctl(EXIT)` turns "the run
1617 /// finished" from a tick-budget guess into a definite signal. Both are worth
1618 /// having long before the cartridge subsystem exists.
1619 ///
1620 /// Advertising a capability obliges us to implement it: the bitmask below
1621 /// claims exactly `xlog` and `xioctl`, so the suite routes text to us only
1622 /// because we can actually take it.
1623 fn emux<B: Bus>(
1624 &mut self,
1625 bus: &mut B,
1626 funct: u8,
1627 code: u16,
1628 ptr: u64,
1629 len: u64,
1630 dest: u8,
1631 ) -> Result<WriteBack, Exception> {
1632 /// `XDETECT` — capability probe.
1633 const XDETECT: u8 = 0o40;
1634 /// `XLOG` — write a string to the host.
1635 const XLOG: u8 = 0o45;
1636 /// `XIOCTL` — control operations.
1637 const XIOCTL: u8 = 0o54;
1638 /// `xdetect` code 1: "which of funct 0x20-0x3F do you support?"
1639 const CODE_EXTENSIONS: u16 = 1;
1640 /// `xioctl` code 1: terminate.
1641 const XIOCTL_EXIT: u16 = 1;
1642
1643 // Hardware has no EMUX. Unless the host opts in, every encoding in the
1644 // range stays inert -- which is both ledger C-8 and what a default ares
1645 // build does (`if(!system.homebrewMode) return;`). Getting this wrong is
1646 // observable: advertising capabilities makes n64-systemtest switch
1647 // console backends, which changes the retired-instruction stream and
1648 // diverges from a hardware-accurate reference trace.
1649 if !bus.emux_enabled() {
1650 return Ok(WriteBack::None);
1651 }
1652 match funct {
1653 XDETECT if code == CODE_EXTENSIONS => {
1654 // Bit N answers for `funct` 0x20 + N. We claim `xlog` and
1655 // `xioctl` and nothing else, because those are what is below.
1656 let mask = (1u64 << (XLOG - XDETECT)) | (1u64 << (XIOCTL - XDETECT));
1657 Ok(WriteBack::Gpr { dest, value: mask })
1658 }
1659 XLOG => {
1660 // Bounded deliberately: `len` is guest-controlled, and a corrupt
1661 // or hostile value must not turn a log call into an unbounded
1662 // read. 4 KiB is far above any line the suite emits.
1663 const MAX: u64 = 4096;
1664 let n = len.min(MAX);
1665 let mut buf = alloc::vec::Vec::with_capacity(n as usize);
1666 for i in 0..n {
1667 // Translated per byte: the string may straddle a page, and a
1668 // fault here is a real fault the guest must see.
1669 let p = self.translate_data(ptr.wrapping_add(i), false)?;
1670 // Read THROUGH the D-cache, exactly as a guest `LB` would.
1671 // The string was just formatted by cached stores, so it is
1672 // still sitting in dirty cache lines -- going straight to
1673 // the bus reads stale RDRAM and prints blanks where the
1674 // digits should be. That is precisely the staleness the
1675 // cache model exists to reproduce, so the log channel has to
1676 // respect it too.
1677 buf.push(self.read_width(bus, p, 1) as u8);
1678 }
1679 bus.emux_log(&buf);
1680 Ok(WriteBack::None)
1681 }
1682 XIOCTL if code == XIOCTL_EXIT => {
1683 bus.emux_exit();
1684 Ok(WriteBack::None)
1685 }
1686 // Every other encoding in the range stays inert, which is the
1687 // hardware behavior C-8 records and the reason the range is usable
1688 // as extension space at all.
1689 _ => Ok(WriteBack::None),
1690 }
1691 }
1692
1693 /// `CACHE`: dispatch to the index- or address-addressed half.
1694 ///
1695 /// Only the ADDRESS-addressed operations translate. `op4..2`
1696 /// (UM Ch. 16, p. 404):
1697 ///
1698 /// - `0..=2` `Index_Invalidate` / `Index_Load_Tag` / `Index_Store_Tag` —
1699 /// address the cache "at the index specified", so they never consult the
1700 /// TLB and cannot fault.
1701 /// - `3` `Create_Dirty_Exclusive` — "set the cache block tag to the
1702 /// specified physical address", so it does.
1703 /// - `4..=6` `Hit_*` — "if the cache block contains the specified address",
1704 /// so they do.
1705 ///
1706 /// Translating unconditionally raises spurious TLB refills on `Index_*` ops
1707 /// against unmapped addresses, which is exactly what cache-init code does at
1708 /// boot: walk every index with an arbitrary base. An earlier revision of
1709 /// this comment described the distinction while the code ignored it.
1710 ///
1711 /// # Errors
1712 ///
1713 /// A TLB fault, on the address-addressed operations only.
1714 fn cache_op<B: Bus>(&mut self, bus: &mut B, addr: u64, op: u8) -> Result<WriteBack, Exception> {
1715 if (op >> 2) >= 3 {
1716 let p = self.translate_data(addr, false)?;
1717 self.cache_hit_op(bus, op, p.addr);
1718 } else {
1719 // An `Index_*` op indexes on the raw VIRTUAL address, which is what
1720 // the hardware does: both primary caches are VIRTUALLY indexed.
1721 //
1722 // This model indexes by physical address (ledger D-6), so on a
1723 // TLB-mapped page the two can select different lines -- translation
1724 // preserves only the low 12 bits, while the D-cache index reaches
1725 // bit 12 and the I-cache bit 13. That divergence is exactly what D-6
1726 // records. It is NOT the claim an earlier revision of this comment
1727 // made -- that translation leaves the index bits alone -- which is
1728 // false for every mapped segment.
1729 self.cache_index_op(bus, op, addr as u32);
1730 }
1731 Ok(WriteBack::None)
1732 }
1733
1734 /// The index-addressed half of `CACHE` (`op4..2` in `0..=2`).
1735 ///
1736 /// `addr` is the *virtual* address: these operations select a line by index
1737 /// and never translate. Bit 0 of `op` picks the cache.
1738 fn cache_index_op<B: Bus>(&mut self, bus: &mut B, op: u8, addr: u32) {
1739 let dcache = op & 1 == 1;
1740 match (op >> 2, dcache) {
1741 // Index_Invalidate (I) / Index_WriteBack_Invalidate (D).
1742 //
1743 // Both clear the valid bit and LEAVE THE TAG in place, which
1744 // `Index_Load_Tag` then reports. Clearing the tag as well would look
1745 // tidier and would be wrong: n64-systemtest asserts the PFN is
1746 // unchanged across an invalidate.
1747 (0, false) => self.icache.invalidate_index(addr),
1748 (0, true) => {
1749 if let Some(w) = self.dcache.flush_index(addr, true, false) {
1750 Self::push_line(bus, w.addr, &w.data);
1751 }
1752 }
1753 // Index_Load_Tag -> COP0 TagLo. Written with `set_hardware` because
1754 // this is the hardware side effect of CACHE, not an MTC0, and the
1755 // register's software write mask must not apply to it.
1756 (1, false) => {
1757 let t = self.icache.load_tag(addr);
1758 self.cop0
1759 .set_hardware(crate::cop0::reg::TAG_LO, u64::from(t));
1760 }
1761 (1, true) => {
1762 let t = self.dcache.load_tag(addr);
1763 self.cop0
1764 .set_hardware(crate::cop0::reg::TAG_LO, u64::from(t));
1765 }
1766 // Index_Store_Tag <- COP0 TagLo.
1767 (2, false) => {
1768 let t = self.cop0.read(crate::cop0::reg::TAG_LO) as u32;
1769 self.icache.store_tag(addr, t);
1770 }
1771 (2, true) => {
1772 let t = self.cop0.read(crate::cop0::reg::TAG_LO) as u32;
1773 self.dcache.store_tag(addr, t);
1774 }
1775 _ => {}
1776 }
1777 }
1778
1779 /// The address-addressed half of `CACHE` (`op4..2` in `3..=6`).
1780 ///
1781 /// `addr` is PHYSICAL — the caller has already translated it, which is what
1782 /// makes a TLB fault on these operations possible.
1783 fn cache_hit_op<B: Bus>(&mut self, bus: &mut B, op: u8, addr: u32) {
1784 let dcache = op & 1 == 1;
1785 match (op >> 2, dcache) {
1786 // Create_Dirty_Exclusive (D only): claim the line without a fill.
1787 (3, true) => {
1788 if let Some(w) = self.dcache.create_dirty_exclusive(addr) {
1789 Self::push_line(bus, w.addr, &w.data);
1790 }
1791 }
1792 // Hit_Invalidate: only if the line is actually resident.
1793 (4, false) => self.icache.hit_invalidate(addr),
1794 (4, true) => {
1795 if self.dcache.hits(addr) {
1796 self.dcache.flush_index(addr, true, false);
1797 }
1798 }
1799 // Fill (I) — an unconditional line fill, no hit test.
1800 (5, false) => self.icache_fill(bus, addr),
1801 // Hit_WriteBack_Invalidate (D).
1802 (5, true) => {
1803 if self.dcache.hits(addr)
1804 && let Some(w) = self.dcache.flush_index(addr, true, false)
1805 {
1806 Self::push_line(bus, w.addr, &w.data);
1807 }
1808 }
1809 // Hit_WriteBack: the line stays resident, and stops being dirty.
1810 (6, false) => {
1811 if let Some(w) = self.icache.flush_hit(addr) {
1812 Self::push_line(bus, w.addr, &w.data);
1813 }
1814 }
1815 (6, true) => {
1816 if self.dcache.hits(addr)
1817 && let Some(w) = self.dcache.flush_index(addr, false, true)
1818 {
1819 Self::push_line(bus, w.addr, &w.data);
1820 }
1821 }
1822 _ => {}
1823 }
1824 }
1825
1826 /// D-cache line fill cost in PClocks (`8..=9 + M(RDRAM)`, UM Table 11-1).
1827 ///
1828 /// `M(RDRAM)` is **not a scalar** (ledger C-1) — it is bank-state dependent
1829 /// (C-4): a 2 KiB RDRAM row spans 128 D-cache lines, so sequential access hits
1830 /// the open row (**fast**) and random access misses it (**slow**). This code
1831 /// charges this one value for **every** miss — there is **no row-state
1832 /// dispatch yet** — so 40 is a **provisional row-hit-typical estimate**, not a
1833 /// measured number: the UM gives the fill *formula* but not this warm value,
1834 /// and ares (40) / cen64 (44) are corroboration only. The documented
1835 /// **cold/row-miss** fill is ~60 PClocks (copetti's ~640 ns external estimate
1836 /// x 93.75 MHz PClock, `M ~= 52`); charging it, and the dirty-writeback case,
1837 /// needs the undocumented `RasInterval` cycles and is deferred to C-4.
1838 #[allow(clippy::doc_markdown)]
1839 const M_DCACHE_FILL: u32 = 40;
1840 /// I-cache line fill cost in PClocks (`14..=15 + M(RDRAM)`, UM Table 11-2).
1841 ///
1842 /// Defined as [`Self::M_DCACHE_FILL`] plus 6, not a bare literal, so the two
1843 /// stay in lockstep by construction: the fills share the same row-hit
1844 /// `M(RDRAM)` and differ only by the UM's line-transfer size — the I-line
1845 /// moves 8 words vs the D-line's critical doubleword of 2, and `14 - 8 = 6`
1846 /// (Table 11-2 minus 11-1). Currently `40 + 6 = 46` (cen64 uses 48); a future
1847 /// measured `M(RDRAM)` (ledger C-1/C-4) changes both together.
1848 #[cfg_attr(test, allow(dead_code))]
1849 #[allow(clippy::doc_markdown)]
1850 const M_ICACHE_FILL: u32 = Self::M_DCACHE_FILL + 6;
1851
1852 /// Make the I-cache line covering `addr` resident.
1853 fn icache_fill<B: Bus>(&mut self, bus: &mut B, addr: u32) {
1854 let base = addr & !(crate::cache::ICACHE_LINE - 1);
1855 let mut data = [0u8; 32];
1856 Self::pull_line(bus, base, &mut data);
1857 self.icache.install(base, data);
1858 // Called only on an I-cache miss (`ic_stage`), so the fill cost applies
1859 // here (fitted, ledger C-1). **Deliberate test seam:** an I-cache miss
1860 // fires on *every* cold fetch, and this crate's fine-grained pipeline unit
1861 // tests step fixed cycle counts for a free-fetch model AND assert on the
1862 // interlock/FPU stalls the fill would confound -- so the stall is charged
1863 // in real execution and in every INTEGRATION test (the i-cache microbench,
1864 // the systemtest, golden-log, residue -- where the pipeline runs as a
1865 // dependency with `cfg(test)` false), but not in this crate's own units.
1866 // The D-cache fill, by contrast, fires only on a rare cached load, so it
1867 // is charged unconditionally (two units absorbed it directly).
1868 #[cfg(not(test))]
1869 self.stall_for(Self::M_ICACHE_FILL, Interlock::Icb);
1870 }
1871
1872 /// Make the D-cache line covering `addr` resident, writing back whatever it
1873 /// evicts.
1874 ///
1875 /// A dirty-line writeback (the `push_line` below) is charged the **same**
1876 /// fixed `M_DCACHE_FILL` as a clean miss -- the fitted 40 does not separate
1877 /// clean- from dirty-eviction cost (a real hardware measurement would; the
1878 /// writeback is an extra RDRAM transaction). Honest given the value is a
1879 /// fitted anchor, not a measurement (ledger C-1).
1880 fn dcache_fill<B: Bus>(&mut self, bus: &mut B, addr: u32) {
1881 let Some(evicted) = self.dcache.miss_plan(addr) else {
1882 return; // hit -- no fill, no cost
1883 };
1884 if let Some(w) = evicted {
1885 Self::push_line(bus, w.addr, &w.data);
1886 }
1887 let base = addr & !(crate::cache::DCACHE_LINE - 1);
1888 let mut data = [0u8; 16];
1889 Self::pull_line(bus, base, &mut data);
1890 self.dcache.install(base, data);
1891 self.stall_for(Self::M_DCACHE_FILL, Interlock::Dcm);
1892 }
1893
1894 /// Write a whole cache line out to the bus, a word at a time.
1895 ///
1896 /// Word-wide rather than byte-wide because [`Bus::read_u32`]/[`Bus::write_u32`]
1897 /// are the calls `rustyn64-core` gives a fast RDRAM path; a byte loop would
1898 /// issue four times the bus traffic on every fill and eviction, and a line
1899 /// is always naturally aligned so the split is exact.
1900 fn push_line<B: Bus>(bus: &mut B, addr: u32, data: &[u8]) {
1901 for (k, w) in data.chunks_exact(4).enumerate() {
1902 let word = u32::from_be_bytes([w[0], w[1], w[2], w[3]]);
1903 bus.write_u32(addr.wrapping_add(k as u32 * 4), word);
1904 }
1905 }
1906
1907 /// Read a whole cache line from the bus into `data`.
1908 fn pull_line<B: Bus>(bus: &mut B, addr: u32, data: &mut [u8]) {
1909 for (k, w) in data.chunks_exact_mut(4).enumerate() {
1910 let word = bus.read_u32(addr.wrapping_add(k as u32 * 4));
1911 w.copy_from_slice(&word.to_be_bytes());
1912 }
1913 }
1914
1915 /// Uncached RCP-register access latency `M`, in PClocks — **measured, not
1916 /// tuned** (ledger C-1, T-11-003).
1917 ///
1918 /// Derived from the PeterLemon `CPUTIMINGNTSC` mult/div differential: the
1919 /// model `expected_i = W / (B + c_i)`, where `c_i` is each timed
1920 /// instruction's documented stall (UM Table 3-12: `mult` 5, `dmult` 8,
1921 /// `div` 37, `ddiv` 69), regresses to a window `W ~= 1.52e6` PClocks and a
1922 /// hardware base-loop `B ~= 25.5` PClocks (fit < 1.5% on the high-leverage
1923 /// mult/div points). Our base loop measured exactly 4.00 PClocks (four
1924 /// one-cycle instructions, no memory latency), so the missing `B - 4 ~= 21.5`
1925 /// PClocks is the uncached `lw VI_V_CURRENT` latency. Charging 22 drives the
1926 /// ROM's absolute iteration count from 304_180 to 56_330 vs the
1927 /// hardware-baked 56_092 (0.4%) -- the *independent* confirmation, since 22
1928 /// came from the differential, not from fitting the absolute count. Measured
1929 /// on VI; the sibling RCP registers share the bus and are charged the same
1930 /// pending their own vectors. RDRAM / cache-fill `M` stay unmeasured (0).
1931 #[allow(clippy::doc_markdown)]
1932 const M_RCP_REGISTER: u32 = 22;
1933 /// Low bound of the RCP MMIO block (SP..SI) charged [`Self::M_RCP_REGISTER`].
1934 const M_RCP_REGISTER_LO: u32 = 0x0400_0000;
1935 /// High bound of the RCP MMIO block.
1936 const M_RCP_REGISTER_HI: u32 = 0x04FF_FFFF;
1937
1938 /// Read `width` big-endian bytes, right-justified, through the D-cache when
1939 /// the access is cached.
1940 ///
1941 /// Dispatches on width so 4- and 8-byte *uncached* accesses go through
1942 /// [`Bus::read_u32`], which `rustyn64-core` overrides with a fast RDRAM path.
1943 /// A byte loop would issue 4-8x more bus calls on the *most common*
1944 /// operations, and memory access is the hot path for a core targeting full
1945 /// speed (`docs/performance.md`).
1946 ///
1947 /// Alignment is **not** rechecked here. `access` has already validated it
1948 /// against the specific [`crate::mem::LoadKind`]/[`crate::mem::StoreKind`],
1949 /// and the unaligned family passes an address it has aligned down itself.
1950 /// Duplicating the check would put the rule in two places, where it can drift.
1951 fn read_width<B: Bus>(&mut self, bus: &mut B, p: crate::addr::Physical, width: u64) -> u64 {
1952 let addr = p.addr;
1953 if p.cached == crate::addr::Cached::Yes {
1954 self.dcache_fill(bus, addr);
1955 return self.dcache.read(addr, width as usize);
1956 }
1957 let v = match width {
1958 1 => u64::from(bus.read_u8(addr)),
1959 2 => (u64::from(bus.read_u8(addr)) << 8) | u64::from(bus.read_u8(addr.wrapping_add(1))),
1960 4 => u64::from(bus.read_u32(addr)),
1961 // Big-endian: the high word is at the lower address.
1962 8 => {
1963 (u64::from(bus.read_u32(addr)) << 32)
1964 | u64::from(bus.read_u32(addr.wrapping_add(4)))
1965 }
1966 _ => 0,
1967 };
1968 // Charge the uncached RCP-register access latency `M` (T-11-003, ledger
1969 // C-1). `addr` is physical (`p.addr`), so the KSEG1 `0xA4xx_xxxx` a game
1970 // uses arrives here as `0x04xx_xxxx`. Reuses the `Dcm` interlock (whose
1971 // doc already carries the `+ M` term) rather than adding a serialized enum
1972 // variant that would perturb the save-state format (ADR 0005). Only this
1973 // region is *measured*; other uncached reads (RDRAM) stay at 0.
1974 //
1975 // Charged once per access. `M` was measured for a 32-bit `lw`; the width
1976 // == 8 case issues two bus words, but a 64-bit uncached read of an RCP
1977 // register (an `ld` spanning two 32-bit registers) is not something the
1978 // oracle covers, so its two-transaction cost is left unmeasured rather
1979 // than assumed to be `2 * M`.
1980 if Self::is_rcp_mmio(addr) {
1981 self.stall_for(Self::M_RCP_REGISTER, Interlock::Dcm);
1982 }
1983 v
1984 }
1985
1986 /// Is the physical address `addr` in the RCP MMIO block charged
1987 /// [`Self::M_RCP_REGISTER`]?
1988 const fn is_rcp_mmio(addr: u32) -> bool {
1989 Self::M_RCP_REGISTER_LO <= addr && addr <= Self::M_RCP_REGISTER_HI
1990 }
1991
1992 /// Write the low `width` big-endian bytes of `value`, through the D-cache
1993 /// when the access is cached.
1994 ///
1995 /// A cached store is a **write-allocate**: the line is filled first, then
1996 /// modified and left dirty. The VR4300's D-cache has no write-around path,
1997 /// so a partial store to an absent line must read the rest of it from memory
1998 /// or the eventual write-back would push out fifteen bytes of nothing.
1999 ///
2000 /// Width-dispatched for the same reason as [`Pipeline::read_width`].
2001 fn write_width<B: Bus>(
2002 &mut self,
2003 bus: &mut B,
2004 p: crate::addr::Physical,
2005 width: u64,
2006 value: u64,
2007 ) {
2008 let addr = p.addr;
2009 if p.cached == crate::addr::Cached::Yes {
2010 self.dcache_fill(bus, addr);
2011 self.dcache.write(addr, width as usize, value);
2012 return;
2013 }
2014 // Hand the bus the width and the *untruncated* register. Narrowing here
2015 // would discard the upper bits before the target can decide whether it
2016 // wants them -- and every device on the RCP's internal bus does (see
2017 // `Bus::write_sized`).
2018 bus.write_sized(addr, width, value);
2019 }
2020
2021 /// The four checks an instruction must pass **before** it executes.
2022 ///
2023 /// Latch-independent, for the reason [`Self::fetch_word`] is: the caller
2024 /// decides what a refusal means. `ex_stage` turns an `Err` into an `Ex`-stage
2025 /// abort; the instruction-granular path (ADR 0013) needs the same four
2026 /// decisions with different consequences. Keeping one copy is what stops the
2027 /// two paths disagreeing about *which* exception an encoding raises — a
2028 /// disagreement n64-systemtest would report as a wrong cause code rather than
2029 /// as a missing check, which is much harder to read backwards.
2030 ///
2031 /// **The order is preserved exactly, and the first two refusals turn out to
2032 /// be disjoint.** The accurate path's comment says an unusable coprocessor is
2033 /// reported as such *"even when the encoding is also a 64-bit one"* — but
2034 /// mutation-checking that while extracting this changed nothing
2035 /// (n64-systemtest stayed at 0 failing in the Phase 1 categories, 90
2036 /// suite-wide), and the reason is stronger than a gap in the suite:
2037 /// [`Op::is_64_bit`](crate::decode::Op::is_64_bit) covers only CPU integer
2038 /// and load/store operations, while [`Self::unusable_coprocessor`] answers
2039 /// only for COP0/COP1/COP2 encodings. **No input is in both sets**, so no
2040 /// input can reach the second check by way of the first.
2041 ///
2042 /// So the ordering between those two is *unobservable*, not merely untested.
2043 /// It is kept because it is what the accurate path has always done, and
2044 /// `ex_gates_first_two_refusals_cannot_both_apply` sweeps the encoding space
2045 /// to keep the disjointness true — if a 64-bit coprocessor move is ever
2046 /// classified as 64-bit, that test fails and whoever changed it has to
2047 /// justify the precedence against the manual instead of inheriting it.
2048 ///
2049 /// Written out because "the order matters" is exactly the shape of claim this
2050 /// project keeps finding stale: nothing fails when it is wrong. A first draft
2051 /// of this comment asserted the order was load-bearing.
2052 ///
2053 /// # Errors
2054 ///
2055 /// One of [`Exception::CoprocessorUnusable`], [`Exception::ReservedInstruction`],
2056 /// [`Exception::CoprocessorReserved`], or [`Exception::FloatingPoint`].
2057 fn ex_gate(&mut self, decoded: Decoded) -> Result<(), Exception> {
2058 // Coprocessor usability is checked BEFORE execution, in EX (UM §4.7.5
2059 // lists CPU among the EX-stage exceptions). COP0 is exempt in kernel mode
2060 // regardless of `CU0`, which is why the CPU can run exception handlers
2061 // before any `Status` setup has happened.
2062 if let Some(unit) = self.unusable_coprocessor(decoded) {
2063 return Err(Exception::CoprocessorUnusable { unit });
2064 }
2065 // A 64-bit operation is RESERVED in 32-bit User or Supervisor mode.
2066 // Kernel may use them at any width, which is why this cannot be a
2067 // property of `Status.KX` alone.
2068 //
2069 // Checked after coprocessor usability, not before: an unusable
2070 // coprocessor is reported as such even when the encoding is also a
2071 // 64-bit one, and the suite distinguishes the two causes.
2072 if decoded.op.is_64_bit() && self.sixty_four_bit_is_reserved() {
2073 return Err(Exception::ReservedInstruction);
2074 }
2075 // `DCFC1`/`DCTC1` are usable-but-unimplemented: the `CU1` check above has
2076 // already passed, so this is the *other* outcome. The whole `Cause` field
2077 // is replaced, leaving only bit 17 -- the suite pre-loads unrelated cause
2078 // bits specifically to check they clear. COP2's equivalent declines
2079 // differently: Reserved Instruction, and `FCSR` is not involved at all.
2080 if decoded.op == crate::decode::Op::Cop2ReservedControl {
2081 return Err(Exception::CoprocessorReserved { unit: 2 });
2082 }
2083 if decoded.op == crate::decode::Op::Cop1ReservedControl {
2084 let fcsr = self.cop1.fcsr();
2085 self.cop1.ctc1(
2086 31,
2087 (fcsr & !crate::fpu::CAUSE_MASK) | crate::fpu::CAUSE_UNIMPLEMENTED,
2088 );
2089 return Err(Exception::FloatingPoint);
2090 }
2091 Ok(())
2092 }
2093
2094 /// `EX` — execute.
2095 fn ex_stage(&mut self, regs: &Regs, next_pc: &mut u64) {
2096 let mut out = self.rf_ex;
2097 if out.occupied && out.abort.is_none() {
2098 // Resolve operands through the bypass network rather than trusting
2099 // the values latched at RF, which may be one cycle stale.
2100 out.rs_val = self.bypass(out.decoded.rs, regs);
2101 out.rt_val = self.bypass(out.decoded.rt, regs);
2102 let hilo = self.bypass_hi_lo(regs);
2103 // The pre-execution refusals, all four of which are EX-stage
2104 // exceptions (UM §4.7.5). `ex_gate` holds the checks and the order
2105 // they must run in; here is only what an `Ex`-stage refusal *does*.
2106 if let Err(exc) = self.ex_gate(out.decoded) {
2107 self.abort_from(Stage::Ex, exc);
2108 out = self.rf_ex;
2109 self.rf_ex.occupied = false;
2110 self.ex_dc = out;
2111 return;
2112 }
2113 // `FCSR.C` — read here, in EX, where the branch resolves, THROUGH A
2114 // BYPASS. See `pending_fp_condition`.
2115 let fp_condition = self.pending_fp_condition().unwrap_or_else(|| {
2116 let p: &Self = self;
2117 p.cop1.fcsr() & FCSR_C != 0
2118 });
2119 match execute(
2120 out.decoded,
2121 out.rs_val,
2122 out.rt_val,
2123 hilo,
2124 out.pc,
2125 fp_condition,
2126 ) {
2127 Ok(e) => {
2128 out.write_back = e.write_back;
2129 out.mem = e.mem;
2130 out.cop0 = e.cop0;
2131 // Control flow. The delay slot has ALREADY been fetched -- it
2132 // is in `ic_rf` right now, because IC ran a cycle ahead. That
2133 // is the architectural delay slot, not a modeling artifact.
2134 //
2135 // Because the cascade runs backwards, `ic_stage` executes
2136 // AFTER this in the same cycle, so writing `next_pc` here
2137 // makes the very next fetch land on the target with exactly
2138 // one delay slot in between. No wrong-path fetch needs
2139 // squashing -- that falls out of the reverse order rather
2140 // than being arranged.
2141 // **The link is `next_pc`, read BEFORE this instruction's
2142 // own redirect is applied.**
2143 //
2144 // At `EX` time `next_pc` already holds the address of the
2145 // instruction that will run after this one's delay slot:
2146 // `pc + 8` for an ordinary jump, and the OUTER target `+ 4`
2147 // when this jump is itself in a delay slot, because the
2148 // outer jump redirected a cycle earlier and `IC` has since
2149 // advanced past it.
2150 //
2151 // Computing `pc + 8` in `execute` was right for the
2152 // ordinary case and silently wrong for the nested one; the
2153 // live `next_pc` is that address by construction rather
2154 // than by a second formula that can disagree.
2155 // **COP2 is one 64-bit latch, not a register file.** The
2156 // register index is ignored entirely: n64-systemtest writes with
2157 // one index and reads back with several others, including 30 and
2158 // 31, and gets the same value every time. `MTC2` writes all 64
2159 // bits despite being nominally a 32-bit move; `MFC2` returns the
2160 // low half sign-extended and `DMFC2` the whole thing.
2161 //
2162 // The same shape as the reserved COP0 registers (ledger C-15) --
2163 // this processor's answer to "a coprocessor that is not really
2164 // there" is a single latch, twice over.
2165 match out.decoded.op {
2166 crate::decode::Op::Mtc2 => self.cop2_latch = out.rt_val,
2167 crate::decode::Op::Mfc2 => {
2168 out.write_back = WriteBack::Gpr {
2169 dest: out.decoded.dest,
2170 value: crate::alu::sext32(self.cop2_latch as u32),
2171 };
2172 }
2173 crate::decode::Op::Dmfc2 => {
2174 out.write_back = WriteBack::Gpr {
2175 dest: out.decoded.dest,
2176 value: self.cop2_latch,
2177 };
2178 }
2179 _ => {}
2180 }
2181 self.resolve_branch_control(&mut out, e.link, e.redirect, next_pc);
2182 // Multiply and divide stall the ENTIRE pipeline for the
2183 // documented count (UM Table 3-12), so the request is raised
2184 // here and honored from the next cycle onward.
2185 if e.stall_cycles > 0 {
2186 self.stall_for(e.stall_cycles, Interlock::Mci);
2187 }
2188 // ERET. Resolved here rather than in `execute` because its
2189 // target comes out of COP0, not out of the instruction.
2190 //
2191 // It has NO delay slot (UM Ch. 16, p. 434) -- alone among
2192 // the control transfers -- so the instruction IC already
2193 // fetched must be squashed. Every branch reaches this point
2194 // with its delay slot legitimately in flight, which is why
2195 // the squash is spelled out here instead of falling out of
2196 // the reverse cascade as a branch's does.
2197 if matches!(e.cop0, Some(Cop0Access::Eret)) {
2198 *next_pc = exception::eret(&mut self.cop0);
2199 // "cleared by an ERET" (UM §3.1) -- the other half of
2200 // the LL/SC contract, which had nothing clearing it
2201 // until now.
2202 self.ll_bit = false;
2203 self.ic_rf = Latch::default();
2204 }
2205 }
2206 // Stamp BEFORE the latch move, so the abort travels with the
2207 // instruction that caused it -- see `abort_from`.
2208 Err(exc) => {
2209 self.abort_from(Stage::Ex, exc);
2210 out = self.rf_ex;
2211 }
2212 }
2213 }
2214 self.ex_dc = out;
2215 self.rf_ex.occupied = false;
2216 }
2217
2218 /// Resolve a branch's link write and its taken-redirect, honoring the
2219 /// delay-slot-fault rule (ledger R-19).
2220 ///
2221 /// The delay slot IC fetched last cycle is in `ic_rf` right now; if it
2222 /// aborted, the exception has already pointed `next_pc` at the vector by the
2223 /// time this branch reaches EX. The branch must NOT take -- the exception PC
2224 /// wins -- yet it STILL retires and writes its link
2225 /// (`ExecuteTLBMappedMissInDelay` asserts `RA == fault_address + 4`). Letting
2226 /// the redirect fire would clobber the vector every cycle, so the handler
2227 /// never runs: a two-state hang between the faulting delay slot and the
2228 /// exception vector.
2229 fn resolve_branch_control(
2230 &mut self,
2231 out: &mut Latch,
2232 link: Option<u8>,
2233 redirect: Option<Redirect>,
2234 next_pc: &mut u64,
2235 ) {
2236 let delay_slot_faulted = out.decoded.op.has_delay_slot()
2237 && self.ic_rf.occupied
2238 && self.ic_rf.in_delay_slot
2239 && self.ic_rf.abort.is_some();
2240 if let Some(dest) = link {
2241 // The link is the address after the delay slot: normally the live
2242 // `next_pc`, but the architectural `pc + 8` when a fault has already
2243 // overwritten `next_pc` with the exception vector.
2244 let value = if delay_slot_faulted {
2245 out.pc.wrapping_add(8)
2246 } else {
2247 *next_pc
2248 };
2249 out.write_back = WriteBack::Gpr { dest, value };
2250 }
2251 if let Some(r) = redirect
2252 && !delay_slot_faulted
2253 {
2254 *next_pc = r.target;
2255 // A branch-LIKELY that was not taken squashes its already-fetched
2256 // delay slot. An ordinary branch never does.
2257 if r.nullify_delay_slot {
2258 self.ic_rf = Latch::default();
2259 }
2260 }
2261 }
2262
2263 /// `RF` — register fetch, and where the load interlock is detected.
2264 fn rf_stage(&mut self, regs: &Regs) {
2265 let mut out = self.ic_rf;
2266 if out.occupied {
2267 out.rs_val = regs.read(out.decoded.rs);
2268 out.rt_val = regs.read(out.decoded.rt);
2269 }
2270 // These reads are a first approximation: EX re-resolves them through the
2271 // bypass network, since a producer one instruction ahead has not
2272 // committed yet. RF still performs the read because that is where the
2273 // load interlock is detected (T-11-003).
2274 //
2275 // TODO(T-11-002): the MFHI/MFLO hazard window -- a MFHI followed within
2276 // two instructions by a HI write reads hardware's WRONG value, and that
2277 // is non-interlocked (alu::MFHI_MFLO_HAZARD_INSTRUCTIONS).
2278 // The load-delay interlock (UM §4.6.5). A load's result is not ready in
2279 // time to bypass, so if the NEXT instruction names the loaded register
2280 // the pipeline stalls one cycle. The detection is deliberately imprecise,
2281 // matching hardware -- see `load_interlocks`.
2282 //
2283 // Compare against `ex_dc`, not `rf_ex`. In the reverse cascade `EX` runs
2284 // before `RF`, so by now the instruction that was in `EX` this cycle has
2285 // already moved into `ex_dc` and `rf_ex` has been vacated. Checking
2286 // `rf_ex` here silently never fires -- which is exactly what it did
2287 // before `a_load_followed_by_its_use_interlocks...` caught it.
2288 if out.occupied
2289 && self.ex_dc.occupied
2290 && self.ex_dc.decoded.is_load()
2291 && load_interlocks(
2292 self.ex_dc.decoded.dest,
2293 out.decoded.rs,
2294 out.decoded.rt,
2295 self.ex_dc.decoded.targets_fpr() == out.decoded.targets_fpr(),
2296 )
2297 {
2298 self.stall_for(1, Interlock::Ldi);
2299 }
2300 self.rf_ex = out;
2301 self.ic_rf.occupied = false;
2302 }
2303
2304 /// The FP condition an in-flight `C.cond.fmt` is about to commit, if any.
2305 ///
2306 /// `BC1` resolves in `EX`; `C.cond.fmt` computes *and* commits `FCSR.C` in
2307 /// `WB` (ADR 0007's single commit-or-trap point). An adjacent pair therefore
2308 /// has the branch sampling the previous condition, and the ROM emits exactly
2309 /// that pair with no separating instruction.
2310 ///
2311 /// # Why this is a bypass and not a stall
2312 ///
2313 /// Stalling cannot work here. `stall_for` freezes the whole pipeline, so
2314 /// holding the branch delays the compare's `WB` by the same amount and the
2315 /// branch never catches up — an interlock on `ex_dc`/`dc_wb` was written,
2316 /// fired once, and changed nothing. This is the same shape as the **load**
2317 /// interlock, which works only because its consumer reads through the
2318 /// bypass network; the one-cycle stall buys `DC` time, and forwarding does
2319 /// the rest. The FP condition had no such path, so this is it.
2320 ///
2321 /// # Why recomputing is sound
2322 ///
2323 /// A compare reads two FP registers and writes only `FCSR.C`. Nothing
2324 /// between it and the branch can change those registers — the branch has no
2325 /// destination — so evaluating it early yields the value `WB` will commit.
2326 /// The flags are deliberately discarded: this is a forwarding path, and
2327 /// raising an exception from it would make the branch report the *compare's*
2328 /// trap.
2329 ///
2330 /// `ex_dc` is checked before `dc_wb` because it holds the **younger**
2331 /// instruction, and the most recent compare is the one whose value stands.
2332 fn pending_fp_condition(&self) -> Option<bool> {
2333 let fr = fr_of(&self.cop0);
2334 for latch in [&self.ex_dc, &self.dc_wb] {
2335 if !latch.occupied {
2336 continue;
2337 }
2338 if let Some(Cop0Access::Cop1(Cop1Access::Arith {
2339 fmt, funct, ft, fs, ..
2340 })) = latch.cop0
2341 && funct >= 0o60
2342 {
2343 if let (FpCommit::Condition(c), _, false) =
2344 self.fp_compare(fmt, funct & 0xF, ft, fs, fr)
2345 {
2346 return Some(c);
2347 }
2348 return None;
2349 }
2350 }
2351 None
2352 }
2353
2354 /// Fetch the instruction word at `pc`: alignment, the segment map, the
2355 /// micro-ITLB and JTLB, `Status.RE`, and the I-cache.
2356 ///
2357 /// **Latch-independent on purpose.** Everything here is a function of `pc`
2358 /// and the CPU's own state — it stamps no latch, raises no abort, and names
2359 /// no [`Stage`] — so the caller decides what a failure means. `ic_stage`
2360 /// turns an `Err` into an `Ic`-stage abort; the instruction-granular path
2361 /// (ADR 0013) needs the same fetch with different consequences, and
2362 /// duplicating 100 lines of segment/TLB/cache logic to get it is how the two
2363 /// paths would drift apart in exactly the place a test would not notice.
2364 ///
2365 /// The side effects it *does* keep are the ones that belong to fetching
2366 /// rather than to sequencing: an ITLB reload and its 3-`PCycle` stall, and an
2367 /// I-cache fill. Those happen on hardware whatever the caller does next.
2368 ///
2369 /// # Errors
2370 ///
2371 /// - [`Exception::AddressError`] — `pc` is not word-aligned, or the segment
2372 /// is not valid in the current mode. The bus is **not** touched in either
2373 /// case: the access itself is what is invalid.
2374 /// - A TLB exception from [`Self::tlb_exception`] on a JTLB miss or an
2375 /// invalid entry. An instruction fetch is a load, so `TLBL`, never `TLBS`.
2376 fn fetch_word<B: Bus>(&mut self, bus: &mut B, pc: u64) -> Result<u32, Exception> {
2377 // An instruction fetch must be word-aligned. An unaligned PC raises an
2378 // address error (AdEL) rather than fetching.
2379 //
2380 // Not reachable from straight-line execution, which advances by 4 from an
2381 // aligned reset vector. It becomes reachable with the jump and branch
2382 // family (T-11-004), where a computed target can be unaligned, and it is
2383 // already reachable through the public `Cpu::set_pc` the golden-log
2384 // harness uses.
2385 if !pc.is_multiple_of(4) {
2386 return Err(Exception::AddressError { store: false });
2387 }
2388
2389 // The fetch itself goes through the I-cache (see below); what is still
2390 // outstanding is the COST.
2391 // TODO(T-11-003): charge the I-cache miss cost (14..=15 + M PCycles, UM
2392 // Table 11-2) once `M` is measured -- accuracy-ledger C-1.
2393 // Every address handed to the Bus is PHYSICAL (`docs/cpu.md`); the
2394 // segment map is applied here, in the CPU, not by the Bus.
2395 // Instruction fetch goes through the micro-ITLB in front of the JTLB
2396 // (UM §1.5.1). A micro-TLB miss is a STALL of 3 PCycles (UM §4.6.2); a
2397 // JTLB miss is an exception. Only the mapped segments involve either --
2398 // KSEG0/KSEG1 fetches, which is all of early boot, bypass both.
2399 let asid = (self.cop0.read(crate::cop0::reg::ENTRY_HI) & 0xFF) as u8;
2400 let fetch_access = self.access_mode();
2401 let (phys, cached) = match crate::addr::segment(pc, fetch_access) {
2402 crate::addr::Segment::Direct { addr, cached } => (addr, cached),
2403 // Not a valid address in this mode: an address error, raised without
2404 // consulting the TLB at all.
2405 crate::addr::Segment::Invalid => {
2406 return Err(Exception::AddressError { store: false });
2407 }
2408 crate::addr::Segment::Mapped => {
2409 // The 3-PCycle penalty is "incurred when the micro-TLB is
2410 // updated from the JTLB" (UM §4.6.2) -- so it is charged only
2411 // when a reload can actually happen. A fetch that misses BOTH
2412 // levels goes straight to its exception without paying for a
2413 // reload that never occurred.
2414 if !self.tlb.itlb_probe(pc, asid) && self.tlb.jtlb_has_match(pc, asid) {
2415 self.tlb.itlb_fill(pc, asid);
2416 self.stall_for(crate::tlb::ITLB_MISS_PCYCLES, Interlock::Itm);
2417 }
2418 match self.tlb.lookup(pc, asid, false) {
2419 Ok(t) => (
2420 t.addr,
2421 if t.uncached {
2422 crate::addr::Cached::No
2423 } else {
2424 crate::addr::Cached::Yes
2425 },
2426 ),
2427 // An instruction fetch is a load, so TLBL never TLBS.
2428 Err(f) => return Err(Self::tlb_exception(f, false, fetch_access.wide)),
2429 }
2430 }
2431 };
2432 // Instruction fetch is a 4-byte access, so `Status.RE` swaps it within
2433 // its doubleword exactly as it swaps a `LW` -- which is why the test ROM
2434 // has to emit its reverse-endian programs with each instruction PAIR
2435 // exchanged for them to execute in order.
2436 let phys = if self.reverse_endian() {
2437 phys ^ Self::re_swap(4)
2438 } else {
2439 phys
2440 };
2441 // Fetch through the I-cache when the segment is cached. This is what
2442 // makes an uncached patch to already-fetched code invisible until a
2443 // CACHE invalidate -- the behavior n64-systemtest's ICACHE group
2444 // asserts, and the reason the cache is modeled at all.
2445 Ok(if cached == crate::addr::Cached::Yes {
2446 if !self.icache.hits(phys) {
2447 self.icache_fill(bus, phys);
2448 }
2449 self.icache.read_word(phys)
2450 } else {
2451 bus.read_u32(phys)
2452 })
2453 }
2454
2455 /// `IC` — instruction-cache fetch, and where the delay-slot flag is set.
2456 fn ic_stage<B: Bus>(&mut self, bus: &mut B, next_pc: &mut u64) {
2457 // An abort raised earlier this cycle flushes younger instructions. The
2458 // fetch happening now is younger than all of them, so it must not become
2459 // a live instruction -- otherwise it escapes the flush entirely and
2460 // executes down the wrong path.
2461 //
2462 // TODO(T-11-002): redirect `next_pc` to the exception vector instead of
2463 // bubbling. Until the vector exists, a bubble is the honest behavior:
2464 // it declines to execute rather than executing the wrong thing.
2465 if self.flush_pending {
2466 self.ic_rf = Latch::default();
2467 return;
2468 }
2469 let pc = *next_pc;
2470
2471 // Computed BEFORE the alignment check, so a faulting fetch carries the
2472 // right delay-slot flag into its latch and therefore into `Cause.BD`.
2473 // Depends only on `rf_ex`, never on the fetch, so hoisting it is safe.
2474 //
2475 // Check `rf_ex`, not `ic_rf`: `rf_stage` runs immediately before this in
2476 // the reverse cascade and has already moved the previous instruction out
2477 // of `ic_rf`, so a branch fetched last cycle is in `rf_ex` by now.
2478 // Reading `ic_rf` here makes the flag silently always false.
2479 let in_delay_slot = self.rf_ex.occupied && self.rf_ex.decoded.op.has_delay_slot();
2480
2481 let word = match self.fetch_word(bus, pc) {
2482 Ok(word) => word,
2483 Err(exc) => {
2484 // Populate the latch BEFORE raising. `abort_with` captures the
2485 // faulting instruction's context out of the latch its stage reads,
2486 // which for `Stage::Ic` is `ic_rf` -- so raising first would capture
2487 // the PREVIOUS fetch's `pc` and delay-slot flag and write a wrong
2488 // `EPC`. Stamp before you move, and populate before you stamp.
2489 self.ic_rf = Latch {
2490 cop0: None,
2491 occupied: true,
2492 pc,
2493 in_delay_slot,
2494 abort: Some(exc),
2495 ..Latch::default()
2496 };
2497 self.abort_with(Stage::Ic, exc, pc);
2498 // `next_pc` is deliberately NOT realigned on an alignment fault.
2499 // Rounding it down would silently "fix" the faulting address and
2500 // let execution continue on a path hardware never takes -- turning
2501 // a raised exception into a wrong answer. The redirect to the
2502 // exception vector happens in `advance`, after the cascade
2503 // (T-12-002).
2504 return;
2505 }
2506 };
2507 // Decode here rather than at RF: a branch must be decoded before the
2508 // NEXT fetch, so that fetch can be marked as its delay slot.
2509 //
2510 // A branch decoded last cycle is in `rf_ex` by now, so the instruction
2511 // being fetched here is its delay slot -- see `in_delay_slot` above,
2512 // computed once at the top of the stage and used by both paths.
2513 self.ic_rf = Latch {
2514 occupied: true,
2515 pc,
2516 word,
2517 in_delay_slot,
2518 abort: None,
2519 decoded: decode(word),
2520 rs_val: 0,
2521 rt_val: 0,
2522 write_back: WriteBack::None,
2523 mem: None,
2524 cop0: None,
2525 };
2526 *next_pc = pc.wrapping_add(4);
2527 }
2528
2529 /// Perform a COP1 arithmetic operation against the FPR file.
2530 ///
2531 /// Lives here rather than in `exec::execute` because it reads two FPRs and
2532 /// writes a third, and `execute` has no access to the register file — the
2533 /// same reason the COP1 moves are split this way.
2534 ///
2535 /// # Register access goes through the `FR` view
2536 ///
2537 /// Operands are read with [`Fpr::read_s`]/[`Fpr::read_d`], **not**
2538 /// `read_raw`. Using the raw register was ledger U-7's bug: with `FR = 0` a
2539 /// double lives across an FGR *pair*, and a raw read returns half of it.
2540 ///
2541 /// # `FCSR`
2542 ///
2543 /// `Cause` is bits **17:12** and reports what *this* operation raised; it
2544 /// is replaced wholesale each time. `Flags` (6:2) is the sticky
2545 /// accumulation and is OR-ed in. `Flags::to_fcsr_bits` produces both, so
2546 /// clearing only `Cause` before OR-ing preserves the sticky half.
2547 ///
2548 /// **The field is 17:12, not 16:12.** Bit 17 is `Cause.E`, Unimplemented
2549 /// Operation — part of `Cause` despite having no `Enable` bit and no sticky
2550 /// `Flags` twin, which means the mask is the *only* thing that ever clears
2551 /// it. This comment said 16:12 while the mask covered 16:12 too, and the
2552 /// result was a bit that could never be cleared once raised. Only the five
2553 /// *maskable* conditions live in 16:12; that narrower range is what the
2554 /// enable comparison below uses, and it is a different statement.
2555 ///
2556 /// The mask is now [`fpu::CAUSE_MASK`](crate::fpu::CAUSE_MASK), defined once.
2557 /// It had reached **four** local copies of the same literal — in a file where
2558 /// getting this exact constant wrong has already shipped a bug.
2559 ///
2560 /// # Enabled traps
2561 ///
2562 /// A condition whose `FCSR.Enable` bit is set raises
2563 /// [`Exception::FloatingPoint`] instead of completing, and **three** things
2564 /// then differ from the untrapped path. All three are architectural, and
2565 /// each is separately observable by n64-systemtest:
2566 ///
2567 /// 1. **`fd` is not written.** The trap is precise, so the destination keeps
2568 /// its old value — which is what the suite checks with its
2569 /// `Result after operation (with exception)` assertion.
2570 /// 2. **The sticky `Flags` field is not updated.** Only `Cause` is. This is
2571 /// easy to get wrong because the untrapped path sets both from the same
2572 /// helper, and a trapped operation that also OR-ed into `Flags` looks
2573 /// right in every test that does not read `FCSR` back.
2574 /// 3. **The instruction does not retire**, so it must not tick `Random`.
2575 ///
2576 /// Returns `true` when it trapped, so `wb_stage` can skip its retirement
2577 /// tail.
2578 ///
2579 /// # Still not handled
2580 ///
2581 /// The **unimplemented-operation** cause (bit 17) is unmaskable and is not
2582 /// produced by the arithmetic here — the VR4300 raises it for subnormal
2583 /// operands and results, which this FPU computes normally instead. That is
2584 /// a separate body of work from the maskable enables, and the suite's
2585 /// `expected_unimplemented` cases still fail.
2586 fn fp_arith(&mut self, fmt: u8, funct: u8, ft: u8, fs: u8, fd: u8) -> bool {
2587 use crate::fpu;
2588 let fr = fr_of(&self.cop0);
2589 // `fmt` is 16 (single) or 17 (double) -- decode admits no other value
2590 // into `FpArith`, so this is a two-way split, not a table.
2591 // `funct` 5/6/7 read only `fs`, so they are handled ahead of the
2592 // arithmetic split and `ft` is never read for an instruction whose
2593 // `ft` field is architecturally zero.
2594 //
2595 // **`MOV` (funct 6) alone is the pure bit move.** `ABS` (5) and `NEG`
2596 // (7) look like sign flips and are not: they classify their operand,
2597 // raising Invalid on a signaling NaN and unimplemented-operation on a
2598 // subnormal or an MSB-clear NaN, and they REPLACE the `Cause` field.
2599 // An earlier version of this comment described all three as raising
2600 // nothing, which was true of `MOV` and never of its neighbors.
2601 //
2602 // n64-systemtest settles which is which by construction rather than by
2603 // description: `MOV.S` is driven through
2604 // `test_floating_point_f32_which_preserves_cause_bits`, while `ABS.S`
2605 // and `NEG.S` go through the ordinary `test_floating_point_f32`, which
2606 // asserts `Cause` was cleared. Treating all three alike was worth 52
2607 // assertions.
2608 if matches!(funct, 5 | 7) {
2609 return self.fp_sign_op(fmt, funct, fs, fd, fr);
2610 }
2611 if funct == 6 {
2612 // **`MOV.S` moves all 64 bits, not just the formatted half.**
2613 // n64-systemtest's "Upper bits of 32 bit operation" reads the
2614 // destination back with `DMFC1` after a `MOV.S` and expects the
2615 // SOURCE's upper half there, not the destination's previous
2616 // contents -- so this is a whole-register transfer that happens to
2617 // be spelled `.S`.
2618 let v = self.fpr.read_d_fs(fs, fr);
2619 self.fpr.write_d_arith(fd, fr, v);
2620 // **`FCSR` is left completely alone**, `Cause` included.
2621 //
2622 // Clearing `Cause` here was written first, on no evidence, and was
2623 // measurably wrong: the compiler emits `MOV.fmt` to move an FP
2624 // return value, so a `MOV` sitting between an arithmetic operation
2625 // and the `CFC1` that reads its result wiped the very `Cause` bits
2626 // the program was about to inspect. n64-systemtest saw
2627 // `flags: inexact` with `causes: ""` — the sticky half surviving
2628 // and the per-operation half erased — which is the signature of a
2629 // later instruction overwriting it, not of a flag never set.
2630 //
2631 // The architectural rule is that `Cause` is written by operations
2632 // that *can* raise. These cannot, so they write nothing.
2633 return false;
2634 }
2635
2636 // `FCSR.RM` is read **here**, per operation, rather than being captured
2637 // anywhere earlier: software changes it between instructions, and
2638 // n64-systemtest sweeps all four modes over the same operand pair.
2639 let mode = fpu::Rounding::from_rm(self.cop1.rounding_mode());
2640
2641 // Computed but **not committed**. Whether the write happens depends on
2642 // the enables, and they cannot be consulted until the flags are known.
2643 // Writing inside a branch and undoing it afterwards would be wrong
2644 // under `FR = 0`, where a `.S` write can disturb a neighboring
2645 // register's half.
2646 let (commit, flags, unimplemented) = match funct {
2647 0o00..=0o03 => self.fp_binary(fmt, funct, ft, fs, fr, mode),
2648 0o04 => self.fp_sqrt(fmt, fs, fr, mode),
2649 0o10..=0o17 => self.fp_to_integer(fmt, funct, fs, fr),
2650 0o40 | 0o41 | 0o44 | 0o45 => self.fp_convert(fmt, funct, fs, fr, mode),
2651 // 0o60..=0o77 -- `C.cond.fmt`. The low four bits ARE the condition
2652 // (UM Table 7-11), so the sixteen mnemonics need no table.
2653 _ => self.fp_compare(fmt, funct & 0xF, ft, fs, fr),
2654 };
2655
2656 let raised = flags.to_fcsr_bits()
2657 | if unimplemented {
2658 fpu::CAUSE_UNIMPLEMENTED
2659 } else {
2660 0
2661 };
2662 let fcsr = self.cop1.fcsr();
2663
2664 // `Cause` bits 16:12 and the `Enable` field bits 11:7 hold the five
2665 // conditions in the SAME order, so shifting `Cause` down by 12 lines it
2666 // up with what `Cop1Control::enables` returns. Comparing them in
2667 // different orders is a silent mis-map that only shows up on whichever
2668 // condition happens to be tested first.
2669 //
2670 // **Unimplemented Operation (bit 17) is unmaskable** and sits above
2671 // that field, so it is checked separately rather than being folded into
2672 // the enable comparison — where it would have been silently ignored,
2673 // since no enable bit corresponds to it.
2674 if unimplemented || (raised >> 12) & self.cop1.enables() != 0 {
2675 // Cause only. The sticky `Flags` field is deliberately left
2676 // untouched — see the doc comment.
2677 self.cop1
2678 .ctc1(31, (fcsr & !fpu::CAUSE_MASK) | (raised & fpu::CAUSE_MASK));
2679 self.abort_from(Stage::Wb, Exception::FloatingPoint);
2680 return true;
2681 }
2682
2683 match commit {
2684 // CLEARS the upper half -- `write_s_arith`, not `write_s`. An
2685 // arithmetic `.S` result is not an `MTC1`: the suite reads the
2686 // destination back with `DMFC1` and expects zero above (C-10).
2687 // Going through the `Fpr` accessor rather than `write_raw` is what
2688 // keeps the `FR` view applied, which is the part ledger U-7 records.
2689 FpCommit::Single(v) => self.fpr.write_s_arith(fd, fr, v),
2690 FpCommit::Double(v) => self.fpr.write_d_arith(fd, fr, v),
2691 // `FCSR.C` is bit 23, and it is NOT part of the `Cause`/`Flags`
2692 // bookkeeping — a compare writes it and no other operation touches
2693 // it. Confirmed against n64-systemtest's own `FCSR` bitfield rather
2694 // than inferred.
2695 FpCommit::Condition(c) => {
2696 let base = (fcsr & !fpu::CAUSE_MASK & !FCSR_C) | raised;
2697 self.cop1.ctc1(31, base | if c { FCSR_C } else { 0 });
2698 return false;
2699 }
2700 }
2701 self.cop1.ctc1(31, (fcsr & !fpu::CAUSE_MASK) | raised);
2702 false
2703 }
2704
2705 /// `ABS` and `NEG` — sign manipulation, but **not** a pure bit flip.
2706 ///
2707 /// The VR4300 classifies the operand first: a subnormal or an MSB-clear NaN
2708 /// raises unimplemented-operation, and an MSB-set (signaling, ledger C-12)
2709 /// NaN raises Invalid and yields the default NaN rather than the operand
2710 /// with its sign changed. Only when the operand is ordinary does the sign
2711 /// bit move.
2712 ///
2713 /// Unlike `MOV`, these REPLACE the `Cause` field — clearing it on success.
2714 fn fp_sign_op(&mut self, fmt: u8, funct: u8, fs: u8, fd: u8, fr: bool) -> bool {
2715 use crate::fpu;
2716 let fcsr = self.cop1.fcsr();
2717 let (commit, flags, unimplemented) = if fmt == 0o20 {
2718 let a = f32::from_bits(self.fpr.read_s_fs(fs, fr));
2719 if fpu::is_subnormal_f32(a) || fpu::is_unimplemented_nan_f32(a) {
2720 (0u64, fpu::Flags::NONE, true)
2721 } else if fpu::is_snan_f32(a) {
2722 (
2723 u64::from(crate::softfloat::F32.default_nan() as u32),
2724 fpu::Flags::INVALID,
2725 false,
2726 )
2727 } else {
2728 let v = if funct == 5 {
2729 fpu::abs_s(a)
2730 } else {
2731 fpu::neg_s(a)
2732 };
2733 (u64::from(v.to_bits()), fpu::Flags::NONE, false)
2734 }
2735 } else {
2736 let a = f64::from_bits(self.fpr.read_d_fs(fs, fr));
2737 if fpu::is_subnormal_f64(a) || fpu::is_unimplemented_nan_f64(a) {
2738 (0u64, fpu::Flags::NONE, true)
2739 } else if fpu::is_snan_f64(a) {
2740 (
2741 crate::softfloat::F64.default_nan(),
2742 fpu::Flags::INVALID,
2743 false,
2744 )
2745 } else {
2746 let v = if funct == 5 {
2747 fpu::abs_d(a)
2748 } else {
2749 fpu::neg_d(a)
2750 };
2751 (v.to_bits(), fpu::Flags::NONE, false)
2752 }
2753 };
2754
2755 let raised = flags.to_fcsr_bits()
2756 | if unimplemented {
2757 fpu::CAUSE_UNIMPLEMENTED
2758 } else {
2759 0
2760 };
2761 if unimplemented {
2762 self.cop1
2763 .ctc1(31, (fcsr & !fpu::CAUSE_MASK) | (raised & fpu::CAUSE_MASK));
2764 self.abort_from(Stage::Wb, Exception::FloatingPoint);
2765 return true;
2766 }
2767 if flags.invalid && self.cop1.enables() & (1 << 4) != 0 {
2768 self.cop1
2769 .ctc1(31, (fcsr & !fpu::CAUSE_MASK) | (raised & fpu::CAUSE_MASK));
2770 self.abort_from(Stage::Wb, Exception::FloatingPoint);
2771 return true;
2772 }
2773 if fmt == 0o20 {
2774 self.fpr.write_s_arith(fd, fr, commit as u32);
2775 } else {
2776 self.fpr.write_d_arith(fd, fr, commit);
2777 }
2778 self.cop1.ctc1(31, (fcsr & !fpu::CAUSE_MASK) | raised);
2779 false
2780 }
2781
2782 /// `SQRT.fmt`.
2783 ///
2784 /// Unary, so it takes the operand policy but not the *result* policy: the
2785 /// square root of the smallest normal is about `1e-19`, nowhere near the
2786 /// subnormal range, so a normal operand cannot produce a subnormal result.
2787 ///
2788 /// A negative operand is Invalid rather than unimplemented — except `-0`,
2789 /// whose root is `-0` and raises nothing. [`softfloat::sqrt`] draws that
2790 /// distinction.
2791 fn fp_sqrt(
2792 &self,
2793 fmt: u8,
2794 fs: u8,
2795 fr: bool,
2796 mode: crate::fpu::Rounding,
2797 ) -> (FpCommit, crate::fpu::Flags, bool) {
2798 use crate::fpu;
2799 if fmt == 0o20 {
2800 let bits = u64::from(self.fpr.read_s_fs(fs, fr));
2801 let a = f32::from_bits(bits as u32);
2802 if fpu::is_subnormal_f32(a) || fpu::is_unimplemented_nan_f32(a) {
2803 return (FpCommit::Single(0), fpu::Flags::NONE, true);
2804 }
2805 let r = softfloat::sqrt(bits, softfloat::F32, mode);
2806 (FpCommit::Single(r.bits as u32), r.flags, false)
2807 } else {
2808 let bits = self.fpr.read_d_fs(fs, fr);
2809 let a = f64::from_bits(bits);
2810 if fpu::is_subnormal_f64(a) || fpu::is_unimplemented_nan_f64(a) {
2811 return (FpCommit::Double(0), fpu::Flags::NONE, true);
2812 }
2813 let r = softfloat::sqrt(bits, softfloat::F64, mode);
2814 (FpCommit::Double(r.bits), r.flags, false)
2815 }
2816 }
2817
2818 /// The VR4300's policy for a **subnormal result**, applied wherever one can
2819 /// be produced — arithmetic and the narrowing `CVT.S.D`.
2820 ///
2821 /// Three outcomes, in order:
2822 ///
2823 /// 1. `FCSR.FS` clear — the processor cannot represent the result at all,
2824 /// so *unimplemented operation*.
2825 /// 2. `FS` set but underflow or inexact **enabled** — it cannot deliver a
2826 /// trapped underflow's defined result either, so unimplemented again.
2827 /// n64-systemtest's own comment on this case reads "(wow)".
2828 /// 3. `FS` set and both disabled — flush per
2829 /// [`fpu::flush_subnormal_f32`], reporting underflow and inexact.
2830 fn subnormal_policy_s(
2831 &self,
2832 out: crate::fpu::Outcome<f32>,
2833 mode: crate::fpu::Rounding,
2834 ) -> (FpCommit, crate::fpu::Flags, bool) {
2835 use crate::fpu;
2836 // **Either** condition triggers, and both are needed.
2837 //
2838 // `is_subnormal` alone misses a result that underflows past the
2839 // subnormal grid all the way to zero (`f64::MIN_POSITIVE` narrowed to
2840 // `f32`; `MIN_POSITIVE` squared) — the VR4300 refuses those exactly as
2841 // it refuses a subnormal.
2842 //
2843 // `flags.underflow` alone misses an *exact* subnormal, because IEEE
2844 // signals underflow only when tiny **and inexact**. Replacing the
2845 // first test with the second was tried and regressed the oracle from
2846 // 89 to 131.
2847 if !fpu::is_subnormal_f32(out.value) && !out.flags.underflow {
2848 return (FpCommit::Single(out.value.to_bits()), out.flags, false);
2849 }
2850 if !self.cop1.flush_denorm_to_zero() || self.underflow_traps() {
2851 return (FpCommit::Single(0), fpu::Flags::NONE, true);
2852 }
2853 let mut flags = out.flags;
2854 flags.underflow = true;
2855 flags.inexact = true;
2856 let v = fpu::flush_subnormal_f32(out.value, mode);
2857 (FpCommit::Single(v.to_bits()), flags, false)
2858 }
2859
2860 /// See [`Pipeline::subnormal_policy_s`].
2861 fn subnormal_policy_d(
2862 &self,
2863 out: crate::fpu::Outcome<f64>,
2864 mode: crate::fpu::Rounding,
2865 ) -> (FpCommit, crate::fpu::Flags, bool) {
2866 use crate::fpu;
2867 // **Either** condition triggers, and both are needed.
2868 //
2869 // `is_subnormal` alone misses a result that underflows past the
2870 // subnormal grid all the way to zero (`f64::MIN_POSITIVE` narrowed to
2871 // `f32`; `MIN_POSITIVE` squared) — the VR4300 refuses those exactly as
2872 // it refuses a subnormal.
2873 //
2874 // `flags.underflow` alone misses an *exact* subnormal, because IEEE
2875 // signals underflow only when tiny **and inexact**. Replacing the
2876 // first test with the second was tried and regressed the oracle from
2877 // 89 to 131.
2878 if !fpu::is_subnormal_f64(out.value) && !out.flags.underflow {
2879 return (FpCommit::Double(out.value.to_bits()), out.flags, false);
2880 }
2881 if !self.cop1.flush_denorm_to_zero() || self.underflow_traps() {
2882 return (FpCommit::Double(0), fpu::Flags::NONE, true);
2883 }
2884 let mut flags = out.flags;
2885 flags.underflow = true;
2886 flags.inexact = true;
2887 let v = fpu::flush_subnormal_f64(out.value, mode);
2888 (FpCommit::Double(v.to_bits()), flags, false)
2889 }
2890
2891 /// Does the CURRENT `FCSR` already meet a trap condition?
2892 ///
2893 /// Used after a `CTC1` writes it: the register can be put into a trapping
2894 /// state directly, without any arithmetic having run.
2895 fn fcsr_traps_now(&self) -> bool {
2896 /// `FCSR.Cause` maskable bits 16:12, shifted to line up with `Enable`.
2897 const CAUSE_SHIFT: u32 = 12;
2898 let fcsr = self.cop1.fcsr();
2899 if fcsr & crate::fpu::CAUSE_UNIMPLEMENTED != 0 {
2900 return true;
2901 }
2902 (fcsr >> CAUSE_SHIFT) & self.cop1.enables() != 0
2903 }
2904
2905 /// Is underflow or inexact enabled? Either turns a flushed subnormal into
2906 /// an unimplemented operation.
2907 fn underflow_traps(&self) -> bool {
2908 /// `FCSR.Enable` underflow (bit 8) and inexact (bit 7), as
2909 /// `Cop1Control::enables` returns them — shifted down by 7.
2910 const ENABLE_UNDERFLOW_OR_INEXACT: u32 = 0b11;
2911 self.cop1.enables() & ENABLE_UNDERFLOW_OR_INEXACT != 0
2912 }
2913
2914 /// `ADD`/`SUB`/`MUL`/`DIV` in either format.
2915 ///
2916 /// # The VR4300 cannot compute with subnormals
2917 ///
2918 /// It raises the unmaskable *unimplemented operation* cause instead, and
2919 /// there are three distinct occasions (UM §7.5; pinned by n64-systemtest):
2920 ///
2921 /// 1. **A subnormal operand** — checked before the operation is attempted,
2922 /// and it outranks everything, including a NaN that would otherwise
2923 /// raise Invalid.
2924 /// 2. **A subnormal result with `FCSR.FS` clear.**
2925 /// 3. **A subnormal result with `FS` set but underflow or inexact
2926 /// *enabled*.** The processor cannot deliver a trapped underflow's
2927 /// defined result, so it declines instead — the suite's own comment on
2928 /// this case reads "(wow)".
2929 ///
2930 /// With `FS` set and those enables clear it flushes, per
2931 /// [`fpu::flush_subnormal_f32`], and reports underflow + inexact.
2932 ///
2933 /// The returned flags are deliberately [`fpu::Flags::NONE`] on every
2934 /// unimplemented path: `FCSR` must end up with bit 17 and *nothing else*,
2935 /// which is what the suite asserts.
2936 fn fp_binary(
2937 &self,
2938 fmt: u8,
2939 funct: u8,
2940 ft: u8,
2941 fs: u8,
2942 fr: bool,
2943 mode: crate::fpu::Rounding,
2944 ) -> (FpCommit, crate::fpu::Flags, bool) {
2945 use crate::fpu;
2946 if fmt == 0o20 {
2947 let a = f32::from_bits(self.fpr.read_s_fs(fs, fr));
2948 let b = f32::from_bits(self.fpr.read_s_ft(ft, fr));
2949 if fpu::arith_unimplemented_s(a, b) {
2950 return (FpCommit::Single(0), fpu::Flags::NONE, true);
2951 }
2952 let out = match funct {
2953 0 => fpu::add_s(a, b, mode),
2954 1 => fpu::sub_s(a, b, mode),
2955 2 => fpu::mul_s(a, b, mode),
2956 _ => fpu::div_s(a, b, mode),
2957 };
2958 self.subnormal_policy_s(out, mode)
2959 } else {
2960 let a = f64::from_bits(self.fpr.read_d_fs(fs, fr));
2961 let b = f64::from_bits(self.fpr.read_d_ft(ft, fr));
2962 if fpu::arith_unimplemented_d(a, b) {
2963 return (FpCommit::Double(0), fpu::Flags::NONE, true);
2964 }
2965 let out = match funct {
2966 0 => fpu::add_d(a, b, mode),
2967 1 => fpu::sub_d(a, b, mode),
2968 2 => fpu::mul_d(a, b, mode),
2969 _ => fpu::div_d(a, b, mode),
2970 };
2971 self.subnormal_policy_d(out, mode)
2972 }
2973 }
2974
2975 /// `ROUND`/`TRUNC`/`CEIL`/`FLOOR` to `.W` or `.L` (funct 8..=15).
2976 ///
2977 /// These carry their rounding mode **in the opcode** and ignore `FCSR.RM`
2978 /// entirely — that is the whole reason they exist alongside `CVT.W`/`CVT.L`,
2979 /// which do consult it. Passing the live `RM` here would make all four
2980 /// behave identically whenever `RM` happened to match, and the difference
2981 /// would only show up under a non-default mode.
2982 fn fp_to_integer(
2983 &self,
2984 fmt: u8,
2985 funct: u8,
2986 fs: u8,
2987 fr: bool,
2988 ) -> (FpCommit, crate::fpu::Flags, bool) {
2989 use crate::fpu::{self, Rounding};
2990 let mode = match funct & 0o3 {
2991 0 => Rounding::Nearest,
2992 1 => Rounding::TowardZero,
2993 2 => Rounding::TowardPlusInf,
2994 _ => Rounding::TowardMinusInf,
2995 };
2996 // Refusal is decided BEFORE the source is read as a float: an integer
2997 // source format has no float to widen, and reading one anyway produces a
2998 // plausible number for an instruction that does not exist.
2999 if self.integer_conversion_unimplemented(fmt, fs, fr) {
3000 return (FpCommit::Single(0), fpu::Flags::NONE, true);
3001 }
3002 // The source is widened to `f64` first, which is EXACT for an `f32`, so
3003 // no rounding happens before the one the instruction asks for.
3004 let v = self.fp_source_as_f64(fmt, fs, fr);
3005 // funct 8..=11 target `.L`, 12..=15 target `.W`.
3006 let wide = funct < 0o14;
3007 if wide {
3008 let out = fpu::to_i64(v, mode);
3009 // `to_i64` reports NaN and out-of-range as Invalid, which is the
3010 // IEEE answer and NOT this processor's: the VR4300 declines with
3011 // *unimplemented operation* instead. n64-systemtest expects `Err`
3012 // for infinities, NaNs and anything past the target's range.
3013 if out.flags.invalid {
3014 return (FpCommit::Double(0), fpu::Flags::NONE, true);
3015 }
3016 #[allow(clippy::cast_sign_loss)] // a bit pattern, not a magnitude
3017 (FpCommit::Double(out.value as u64), out.flags, false)
3018 } else {
3019 let out = fpu::to_i32(v, mode);
3020 if out.flags.invalid {
3021 return (FpCommit::Single(0), fpu::Flags::NONE, true);
3022 }
3023 #[allow(clippy::cast_sign_loss)] // a bit pattern, not a magnitude
3024 (FpCommit::Single(out.value as u32), out.flags, false)
3025 }
3026 }
3027
3028 /// Is the source of a float-to-integer conversion one the VR4300 refuses?
3029 ///
3030 /// Subnormality, plus a magnitude of `2^53` or more. NaN and infinity are
3031 /// detected from the conversion's own result, because "out of range"
3032 /// depends on the target width.
3033 ///
3034 /// The limit applies to `.W` targets as well, where it is **unobservable**:
3035 /// `2^53` is far outside `i32`, so such a value is refused either way. It
3036 /// was originally guarded on the target width, and the guard was removed
3037 /// because no test could distinguish the two — an undistinguishable branch
3038 /// is one that rots.
3039 ///
3040 /// # The `2^53` limit is narrower than `i64`
3041 ///
3042 /// `9007198717870080` converts; `9007199254740992` (`2^53`) raises
3043 /// unimplemented — both far inside `i64`'s range. n64-systemtest brackets
3044 /// the threshold with adjacent values on either side, which is what
3045 /// identifies it as `2^53` rather than some larger bound. `2^53` is the
3046 /// last integer a `double` represents exactly, so the natural reading is
3047 /// that the conversion runs through double precision internally and
3048 /// declines whatever it cannot hold exactly.
3049 fn integer_conversion_unimplemented(&self, fmt: u8, fs: u8, fr: bool) -> bool {
3050 use crate::fpu;
3051 /// The last integer a `double` represents exactly.
3052 const TWO_POW_53: f64 = 9_007_199_254_740_992.0;
3053
3054 // An INTEGER source format is not a conversion this processor has:
3055 // `CVT.W.W`, `CVT.W.L`, `CVT.L.W`, `CVT.L.L` and the whole
3056 // `ROUND`/`TRUNC`/`CEIL`/`FLOOR` family from `.W`/`.L` do not exist, and
3057 // the VR4300 declines them with Unimplemented Operation rather than
3058 // reinterpreting the source. Checked first, because every branch below
3059 // would happily read the register as a float and return a number.
3060 if fmt != 0o20 && fmt != 0o21 {
3061 return true;
3062 }
3063 let v = if fmt == 0o20 {
3064 let a = f32::from_bits(self.fpr.read_s_fs(fs, fr));
3065 if fpu::is_subnormal_f32(a) {
3066 return true;
3067 }
3068 f64::from(a)
3069 } else {
3070 let a = f64::from_bits(self.fpr.read_d_fs(fs, fr));
3071 if fpu::is_subnormal_f64(a) {
3072 return true;
3073 }
3074 a
3075 };
3076 v.abs() >= TWO_POW_53
3077 }
3078
3079 /// `CVT.S`/`CVT.D`/`CVT.W`/`CVT.L`, from any source format.
3080 fn fp_convert(
3081 &self,
3082 fmt: u8,
3083 funct: u8,
3084 fs: u8,
3085 fr: bool,
3086 mode: crate::fpu::Rounding,
3087 ) -> (FpCommit, crate::fpu::Flags, bool) {
3088 use crate::fpu;
3089 match funct {
3090 // To single.
3091 0o40 => match fmt {
3092 0o21 => {
3093 let bits = self.fpr.read_d_fs(fs, fr);
3094 let a = f64::from_bits(bits);
3095 if fpu::is_subnormal_f64(a) || fpu::is_unimplemented_nan_f64(a) {
3096 return (FpCommit::Single(0), fpu::Flags::NONE, true);
3097 }
3098 // Narrowing is ARITHMETIC: it rounds 53 significand bits
3099 // into 24, so it can be inexact, can overflow, and can land
3100 // in the subnormal range -- each depending on `FCSR.RM`.
3101 // `v as f32` reports none of that and rounds to nearest
3102 // only, which is where ledger C-11 found this operation
3103 // after the arithmetic had already been fixed.
3104 let r = softfloat::convert(bits, softfloat::F64, softfloat::F32, mode);
3105 let out = fpu::Outcome {
3106 value: f32::from_bits(r.bits as u32),
3107 flags: r.flags,
3108 };
3109 // Narrowing can produce a subnormal from a perfectly normal
3110 // double, so the result policy applies exactly as it does
3111 // to the arithmetic.
3112 self.subnormal_policy_s(out, mode)
3113 }
3114 #[allow(clippy::cast_possible_wrap)] // reinterpreting a word as signed
3115 0o24 => {
3116 // Through `softfloat`, not a Rust `as` cast: an i32 can need
3117 // more than 24 significand bits, so this rounds -- and must
3118 // round the way `FCSR.RM` says.
3119 let v = i64::from(self.fpr.read_s_fs(fs, fr) as i32);
3120 let r = softfloat::from_int(v, softfloat::F32, mode);
3121 (FpCommit::Single(r.bits as u32), r.flags, false)
3122 }
3123 // From `.L`, which the VR4300 restricts: bits 63:55 must be all
3124 // zeroes or all ones (UM §7.5.2). Outside that it raises
3125 // Unimplemented rather than converting, and there is no defined
3126 // result -- so the commit value is a placeholder the trap path
3127 // discards.
3128 #[allow(clippy::cast_possible_wrap)]
3129 _ => {
3130 let v = self.fpr.read_d_fs(fs, fr) as i64;
3131 if fpu::long_convertible(v) {
3132 let r = softfloat::from_int(v, softfloat::F32, mode);
3133 (FpCommit::Single(r.bits as u32), r.flags, false)
3134 } else {
3135 // No defined result when the restriction is violated, so
3136 // the value is a placeholder the trap path discards.
3137 (FpCommit::Single(0), fpu::Flags::NONE, true)
3138 }
3139 }
3140 },
3141 // To double.
3142 0o41 => match fmt {
3143 0o20 => {
3144 let bits = u64::from(self.fpr.read_s_fs(fs, fr));
3145 let a = f32::from_bits(bits as u32);
3146 if fpu::is_subnormal_f32(a) || fpu::is_unimplemented_nan_f32(a) {
3147 return (FpCommit::Double(0), fpu::Flags::NONE, true);
3148 }
3149 // Widening cannot lose anything, so no result policy is
3150 // needed — but it goes through the same path so there is
3151 // one conversion rather than two that can disagree.
3152 let r = softfloat::convert(bits, softfloat::F32, softfloat::F64, mode);
3153 (FpCommit::Double(r.bits), r.flags, false)
3154 }
3155 #[allow(clippy::cast_possible_wrap)]
3156 0o24 => {
3157 // Exact for every i32 -- 53 bits hold 32 -- but routed the
3158 // same way as the others so there is one conversion rather
3159 // than two that can disagree.
3160 let v = i64::from(self.fpr.read_s_fs(fs, fr) as i32);
3161 let r = softfloat::from_int(v, softfloat::F64, mode);
3162 (FpCommit::Double(r.bits), r.flags, false)
3163 }
3164 #[allow(clippy::cast_possible_wrap)]
3165 _ => {
3166 let v = self.fpr.read_d_fs(fs, fr) as i64;
3167 if fpu::long_convertible(v) {
3168 let r = softfloat::from_int(v, softfloat::F64, mode);
3169 (FpCommit::Double(r.bits), r.flags, false)
3170 } else {
3171 // No defined result when the restriction is violated, so
3172 // the value is a placeholder the trap path discards.
3173 (FpCommit::Double(0), fpu::Flags::NONE, true)
3174 }
3175 }
3176 },
3177 // To word / to long, both honoring `FCSR.RM` -- which is what
3178 // separates them from the fixed-mode family above.
3179 0o44 => {
3180 if self.integer_conversion_unimplemented(fmt, fs, fr) {
3181 return (FpCommit::Single(0), fpu::Flags::NONE, true);
3182 }
3183 let out = fpu::to_i32(self.fp_source_as_f64(fmt, fs, fr), mode);
3184 if out.flags.invalid {
3185 return (FpCommit::Single(0), fpu::Flags::NONE, true);
3186 }
3187 #[allow(clippy::cast_sign_loss)]
3188 (FpCommit::Single(out.value as u32), out.flags, false)
3189 }
3190 _ => {
3191 if self.integer_conversion_unimplemented(fmt, fs, fr) {
3192 return (FpCommit::Double(0), fpu::Flags::NONE, true);
3193 }
3194 let out = fpu::to_i64(self.fp_source_as_f64(fmt, fs, fr), mode);
3195 if out.flags.invalid {
3196 return (FpCommit::Double(0), fpu::Flags::NONE, true);
3197 }
3198 #[allow(clippy::cast_sign_loss)]
3199 (FpCommit::Double(out.value as u64), out.flags, false)
3200 }
3201 }
3202 }
3203
3204 /// `C.cond.fmt` — writes `FCSR.C`, never an FPR.
3205 fn fp_compare(
3206 &self,
3207 fmt: u8,
3208 cond: u8,
3209 ft: u8,
3210 fs: u8,
3211 fr: bool,
3212 ) -> (FpCommit, crate::fpu::Flags, bool) {
3213 use crate::fpu;
3214 let out = if fmt == 0o20 {
3215 fpu::compare_s(
3216 f32::from_bits(self.fpr.read_s_fs(fs, fr)),
3217 f32::from_bits(self.fpr.read_s_ft(ft, fr)),
3218 cond,
3219 )
3220 } else {
3221 fpu::compare_d(
3222 f64::from_bits(self.fpr.read_d_fs(fs, fr)),
3223 f64::from_bits(self.fpr.read_d_ft(ft, fr)),
3224 cond,
3225 )
3226 };
3227 (FpCommit::Condition(out.value), out.flags, false)
3228 }
3229
3230 /// Read `fs` in `fmt` and widen to `f64`.
3231 ///
3232 /// `f32` to `f64` is exact, so a `.S` source loses nothing on the way in and
3233 /// the only rounding is the one the instruction performs.
3234 fn fp_source_as_f64(&self, fmt: u8, fs: u8, fr: bool) -> f64 {
3235 if fmt == 0o20 {
3236 f64::from(f32::from_bits(self.fpr.read_s_fs(fs, fr)))
3237 } else {
3238 f64::from_bits(self.fpr.read_d_fs(fs, fr))
3239 }
3240 }
3241}
3242
3243#[cfg(test)]
3244mod tests {
3245 use super::*;
3246
3247 struct NullBus {
3248 irq: bool,
3249 }
3250 impl Bus for NullBus {
3251 fn read_u8(&mut self, _addr: u32) -> u8 {
3252 0
3253 }
3254 fn write_u8(&mut self, _addr: u32, _val: u8) {}
3255 fn poll_irq(&mut self) -> bool {
3256 self.irq
3257 }
3258 }
3259 fn quiet() -> NullBus {
3260 NullBus { irq: false }
3261 }
3262
3263 /// **The reverse-order invariant.** A value must advance exactly one stage
3264 /// per cycle. If the cascade is ever reordered to run forwards, a value falls
3265 /// through several stages in one cycle and the pipeline is silently too fast.
3266 #[test]
3267 fn a_value_advances_exactly_one_stage_per_cycle() {
3268 let mut p = Pipeline::new();
3269 let mut pc = 0xFFFF_FFFF_8000_0000;
3270 let mut regs = Regs::new();
3271 let mut bus = quiet();
3272
3273 // Cycle 1 fetches a sentinel into ic_rf.
3274 p.advance(&mut bus, &mut regs, &mut pc);
3275 let sentinel = p.ic_rf.pc;
3276 assert!(p.ic_rf.occupied);
3277 assert_eq!(sentinel, 0xFFFF_FFFF_8000_0000);
3278
3279 // Each subsequent cycle moves it exactly one boundary along.
3280 p.advance(&mut bus, &mut regs, &mut pc);
3281 assert_eq!(p.rf_ex.pc, sentinel, "after 1 cycle it should be at RF->EX");
3282 assert_ne!(p.ic_rf.pc, sentinel, "and no longer at IC->RF");
3283
3284 p.advance(&mut bus, &mut regs, &mut pc);
3285 assert_eq!(p.ex_dc.pc, sentinel, "after 2 cycles, EX->DC");
3286
3287 p.advance(&mut bus, &mut regs, &mut pc);
3288 assert_eq!(p.dc_wb.pc, sentinel, "after 3 cycles, DC->WB");
3289
3290 // 5 stages => at least 5 PCycles per instruction (UM §4.1).
3291 assert_eq!(p.retired, 0, "nothing may retire before WB has run on it");
3292 p.advance(&mut bus, &mut regs, &mut pc);
3293 assert_eq!(p.retired, 1, "retires at WB on the 5th cycle, not sooner");
3294 }
3295
3296 /// **R-19: a branch whose delay slot faulted must NOT take, but STILL links.**
3297 ///
3298 /// The n64-systemtest `ExecuteTLBMappedMissInDelay` case hung the whole suite:
3299 /// a JALR whose target is its own address, with a delay slot in an unmapped
3300 /// page. When the delay-slot fetch faults, the exception has already pointed
3301 /// `next_pc` at the vector; if the branch then applies its redirect it clobbers
3302 /// the vector, re-fetches itself, and loops forever. The fix suppresses the
3303 /// redirect when the delay slot (in `ic_rf`) carries an abort, while still
3304 /// writing the link from `pc + 8`.
3305 ///
3306 /// Mutation guard: the control case (no delay-slot fault) asserts the redirect
3307 /// *is* applied and the link is the live `next_pc`, so removing the fault check
3308 /// turns exactly one of the two assertions red.
3309 #[test]
3310 fn a_branch_with_a_faulting_delay_slot_links_but_does_not_take() {
3311 // JALR $6, $3 (funct 9) -- the exact instruction from the hung test.
3312 let jalr = decode(0x0060_3009);
3313 assert!(jalr.op.has_delay_slot(), "JALR must have a delay slot");
3314 let redirect = Redirect {
3315 target: 0x1234_4FFC, // the JALR jumps to its own address
3316 nullify_delay_slot: false,
3317 };
3318 let branch_pc = 0x1234_4FFC;
3319 let vector = 0xFFFF_FFFF_8000_0180; // where the exception already sent us
3320
3321 // Faulting case: the delay slot in `ic_rf` carries an abort.
3322 {
3323 let mut p = Pipeline::new();
3324 p.ic_rf = Latch {
3325 occupied: true,
3326 in_delay_slot: true,
3327 abort: Some(Exception::AddressError { store: false }),
3328 ..Latch::default()
3329 };
3330 let mut out = Latch {
3331 occupied: true,
3332 pc: branch_pc,
3333 decoded: jalr,
3334 ..Latch::default()
3335 };
3336 let mut next_pc = vector;
3337 p.resolve_branch_control(&mut out, Some(6), Some(redirect), &mut next_pc);
3338 assert_eq!(
3339 next_pc, vector,
3340 "the exception vector must survive -- no take"
3341 );
3342 assert_eq!(
3343 out.write_back,
3344 WriteBack::Gpr {
3345 dest: 6,
3346 value: branch_pc + 8,
3347 },
3348 "the link is written from pc + 8, not the clobbered next_pc",
3349 );
3350 }
3351
3352 // Control case: the delay slot did NOT fault -> the branch takes normally
3353 // and links from the live next_pc.
3354 {
3355 let mut p = Pipeline::new();
3356 p.ic_rf = Latch {
3357 occupied: true,
3358 in_delay_slot: true,
3359 abort: None,
3360 ..Latch::default()
3361 };
3362 let mut out = Latch {
3363 occupied: true,
3364 pc: branch_pc,
3365 decoded: jalr,
3366 ..Latch::default()
3367 };
3368 let mut next_pc = branch_pc + 8;
3369 p.resolve_branch_control(&mut out, Some(6), Some(redirect), &mut next_pc);
3370 assert_eq!(
3371 next_pc, redirect.target,
3372 "an unfaulted branch takes its target"
3373 );
3374 assert_eq!(
3375 out.write_back,
3376 WriteBack::Gpr {
3377 dest: 6,
3378 value: branch_pc + 8,
3379 },
3380 "the link is the live next_pc (which was pc + 8 here)",
3381 );
3382 }
3383 }
3384
3385 /// **`Cause.BD` and `EPC` still come out right when a stall separates the
3386 /// branch from its delay slot.**
3387 ///
3388 /// The companion test below pins that the flag stays *attached* to its
3389 /// instruction across a stall. That is not the same claim: the flag could
3390 /// travel correctly and still be read from the wrong place at dispatch,
3391 /// which is the reverse-cascade hazard this project has hit twice. So this
3392 /// one drives the flagged instruction into a real exception and reads the
3393 /// architectural result — `EPC` must name the **branch**, four bytes back,
3394 /// not the faulting instruction.
3395 #[test]
3396 fn a_delay_slot_exception_after_a_stall_still_reports_the_branch() {
3397 use crate::cop0::reg;
3398 let mut p = Pipeline::new();
3399 let mut regs = Regs::new();
3400 // An unaligned `LW` — offset 0x101 — so the delay-slot instruction
3401 // faults with AdEL rather than needing a crafted overflow.
3402 let mut bus = Ram::new(alloc::vec![addiu_zero(1, 1), ld_st(0o43, 0, 3, 0x101)]);
3403 p.cop0.set_hardware(reg::STATUS, 0);
3404 let mut pc = KSEG0_PROG;
3405
3406 // Advance until the FAULTING instruction (the second word) is the one
3407 // in `IC/RF`, then mark it as the delay slot. Flagging whatever happens
3408 // to be there after one step marks the first instruction instead, which
3409 // never faults -- and the test would then be asserting BD for an
3410 // exception that never came from a delay slot at all.
3411 let slot_pc = KSEG0_PROG.wrapping_add(4);
3412 let mut warm = 0;
3413 while p.ic_rf.pc != slot_pc || !p.ic_rf.occupied {
3414 p.advance(&mut bus, &mut regs, &mut pc);
3415 warm += 1;
3416 assert!(warm < 16, "the faulting instruction never reached IC/RF");
3417 }
3418 p.ic_rf.in_delay_slot = true;
3419
3420 // A `DDIV`-length interlock lands between the branch and its slot.
3421 p.stall_for(69, Interlock::Mci);
3422 let mut cycles = 0;
3423 while p.stalled_by() != Some(Interlock::Exception) {
3424 p.advance(&mut bus, &mut regs, &mut pc);
3425 cycles += 1;
3426 assert!(cycles < 128, "the delay-slot instruction never faulted");
3427 }
3428
3429 assert_ne!(
3430 p.cop0.read(reg::CAUSE) & (1 << 31),
3431 0,
3432 "BD must be set -- the faulting instruction was in a delay slot"
3433 );
3434 assert_eq!(
3435 p.cop0.read(reg::EPC),
3436 slot_pc.wrapping_sub(4),
3437 "EPC must name the branch, so the handler re-evaluates it"
3438 );
3439 }
3440
3441 /// **The delay-slot invariant** — the Phase 1 exit criterion.
3442 ///
3443 /// A multi-cycle stall between a branch and its delay slot must not
3444 /// desynchronize the flag. A global `in_delay_slot` bool passes the naive
3445 /// test and fails this one, which is why the flag rides in the latch.
3446 #[test]
3447 fn delay_slot_flag_survives_a_multi_cycle_stall() {
3448 let mut p = Pipeline::new();
3449 let mut pc = 0xFFFF_FFFF_8000_0000;
3450 let mut regs = Regs::new();
3451 let mut bus = quiet();
3452
3453 // Two instructions in flight; mark the younger as the delay slot.
3454 p.advance(&mut bus, &mut regs, &mut pc);
3455 p.ic_rf.in_delay_slot = true;
3456 let slot_pc = p.ic_rf.pc;
3457
3458 // A long interlock lands between them (e.g. DDIV = 69 PCycles).
3459 p.stall_for(69, Interlock::Mci);
3460 for _ in 0..69 {
3461 p.advance(&mut bus, &mut regs, &mut pc);
3462 }
3463 assert!(p.stalled_by().is_none(), "the stall should have expired");
3464
3465 // The flag must still be attached to the SAME instruction.
3466 assert!(
3467 p.ic_rf.in_delay_slot && p.ic_rf.pc == slot_pc,
3468 "the delay-slot flag detached from its instruction across the stall"
3469 );
3470
3471 // And it must travel with it, not stay behind at a fixed boundary.
3472 p.advance(&mut bus, &mut regs, &mut pc);
3473 assert!(
3474 p.rf_ex.in_delay_slot && p.rf_ex.pc == slot_pc,
3475 "the flag failed to travel with the instruction"
3476 );
3477 assert!(
3478 !p.ic_rf.in_delay_slot,
3479 "the flag was left behind on the boundary instead of moving"
3480 );
3481 }
3482
3483 /// A stall holds every latch in place — nothing advances, nothing retires.
3484 #[test]
3485 fn a_stall_freezes_the_pipeline() {
3486 let mut p = Pipeline::new();
3487 let mut pc = 0xFFFF_FFFF_8000_0000;
3488 let mut regs = Regs::new();
3489 let mut bus = quiet();
3490 for _ in 0..4 {
3491 p.advance(&mut bus, &mut regs, &mut pc);
3492 }
3493 let before = (p.ic_rf, p.rf_ex, p.ex_dc, p.dc_wb, p.retired);
3494
3495 p.stall_for(3, Interlock::Dcm);
3496 for _ in 0..3 {
3497 p.advance(&mut bus, &mut regs, &mut pc);
3498 assert_eq!(
3499 (p.ic_rf, p.rf_ex, p.ex_dc, p.dc_wb, p.retired),
3500 before,
3501 "a stalled cycle must not advance any latch"
3502 );
3503 }
3504 assert!(p.stalled_by().is_none());
3505 p.advance(&mut bus, &mut regs, &mut pc);
3506 assert_ne!(
3507 p.dc_wb, before.3,
3508 "the pipeline must resume after the stall"
3509 );
3510 }
3511
3512 /// **The interrupt gate** (UM §4.7.1): an interrupt is accepted only if the
3513 /// *previous* `PCycle` was a run cycle. The cycle right after a stall is not.
3514 #[test]
3515 fn interrupt_is_not_accepted_on_the_cycle_after_a_stall() {
3516 /// `IE` set, `IM2` (the RCP line) unmasked, `EXL`/`ERL` clear.
3517 ///
3518 /// Needed since T-12-003: an asserted line is no longer sufficient on
3519 /// its own, and cold reset leaves `ERL` **set**, which alone blocks
3520 /// every interrupt.
3521 const IRQ_READY: u64 = 1 | (1 << 10);
3522
3523 let mut p = Pipeline::new();
3524 p.cop0.set_hardware(crate::cop0::reg::STATUS, IRQ_READY);
3525 let mut pc = 0xFFFF_FFFF_8000_0000;
3526 let mut regs = Regs::new();
3527 let mut bus = NullBus { irq: true };
3528
3529 // Warm up so `prev_was_run` is true AND a real instruction has reached
3530 // DC, then confirm an IRQ IS taken.
3531 //
3532 // This used to advance exactly twice, which accepted the interrupt while
3533 // `ex_dc` was still an unoccupied fill bubble — so `EPC` was charged to
3534 // address 0. The test never noticed because it asserted only the abort
3535 // *flag* and never `EPC`: an assertion the broken machine satisfied just
3536 // as well as a correct one. That blind spot is half of ledger **R-18**
3537 // (see `an_interrupt_across_an_eret_is_charged_to_the_target_not_the_eret`),
3538 // so the loop now waits for a real instruction and `EPC` is asserted too.
3539 p.advance(&mut bus, &mut regs, &mut pc);
3540 assert!(p.prev_cycle_was_run());
3541 let mut accepted = false;
3542 for _ in 0..8 {
3543 p.advance(&mut bus, &mut regs, &mut pc);
3544 if p.ex_dc.abort == Some(Exception::Interrupt) {
3545 accepted = true;
3546 break;
3547 }
3548 }
3549 assert!(
3550 accepted,
3551 "an interrupt should be accepted after a run cycle once a real \
3552 instruction reaches DC"
3553 );
3554 assert_ne!(
3555 p.cop0.read(crate::cop0::reg::EPC),
3556 0,
3557 "EPC must name the interrupted instruction, not a fill bubble"
3558 );
3559
3560 // Now stall, and check the very next cycle refuses it.
3561 let mut p = Pipeline::new();
3562 p.cop0.set_hardware(crate::cop0::reg::STATUS, IRQ_READY);
3563 let mut pc = 0xFFFF_FFFF_8000_0000;
3564 let mut regs = Regs::new();
3565 p.advance(&mut bus, &mut regs, &mut pc);
3566 p.stall_for(1, Interlock::Dcb);
3567 p.advance(&mut bus, &mut regs, &mut pc); // the stalled cycle
3568 assert!(
3569 !p.prev_cycle_was_run(),
3570 "a stalled cycle must not count as a run cycle"
3571 );
3572 let ex_dc_before = p.ex_dc.abort;
3573 p.advance(&mut bus, &mut regs, &mut pc); // the cycle immediately after
3574 assert_eq!(
3575 p.ex_dc.abort, ex_dc_before,
3576 "an interrupt must NOT be accepted when the previous PCycle stalled"
3577 );
3578 }
3579
3580 /// **An abort must survive the cascade**, not merely be present the instant
3581 /// it is stamped.
3582 ///
3583 /// The shallow version of this test asserted latch state immediately after
3584 /// `abort_from` and never advanced — so it could not tell a real flush from
3585 /// one that gets overwritten by the reverse cascade in the same cycle. This
3586 /// version steps the pipeline and follows the consequences.
3587 #[test]
3588 fn an_abort_survives_the_cascade() {
3589 let mut p = Pipeline::new();
3590 let mut pc = 0xFFFF_FFFF_8000_0000;
3591 let mut regs = Regs::new();
3592 let mut bus = quiet();
3593 for _ in 0..4 {
3594 p.advance(&mut bus, &mut regs, &mut pc);
3595 }
3596
3597 // Abort at DC: the instruction in DC plus everything younger.
3598 p.abort_from(Stage::Dc, Exception::AddressError { store: false });
3599 let aborted_pc = p.ex_dc.pc;
3600 p.advance(&mut bus, &mut regs, &mut pc);
3601
3602 // The causing instruction carried its abort forward into WB's latch...
3603 assert_eq!(
3604 (p.dc_wb.abort, p.dc_wb.pc),
3605 (Some(Exception::AddressError { store: false }), aborted_pc),
3606 "the aborting instruction lost its flag while advancing"
3607 );
3608 // ...and the younger ones kept theirs rather than having them
3609 // overwritten by the latch moves.
3610 assert_eq!(
3611 p.ex_dc.abort,
3612 Some(Exception::AddressError { store: false }),
3613 "a younger instruction's abort was overwritten by the cascade"
3614 );
3615
3616 // The younger instruction that was already in flight kept its abort as it
3617 // advanced, rather than having it overwritten by the latch move.
3618 assert_eq!(
3619 p.rf_ex.abort,
3620 Some(Exception::AddressError { store: false }),
3621 "an in-flight younger instruction lost its abort while advancing"
3622 );
3623
3624 // And the fetch issued during the aborting cycle is a bubble, not a live
3625 // wrong-path instruction that would escape the flush entirely.
3626 assert!(
3627 !p.ic_rf.occupied,
3628 "the instruction fetched during the abort escaped the flush"
3629 );
3630 }
3631
3632 /// A zero-cycle stall request is ignored — recording it would burn a cycle
3633 /// and suppress interrupt acceptance on the next one, with no visible cause.
3634 #[test]
3635 fn a_zero_cycle_stall_is_not_a_stall() {
3636 let mut p = Pipeline::new();
3637 let mut pc = 0xFFFF_FFFF_8000_0000;
3638 let mut regs = Regs::new();
3639 let mut bus = quiet();
3640 p.advance(&mut bus, &mut regs, &mut pc);
3641
3642 p.stall_for(0, Interlock::Ldi);
3643 assert!(p.stalled_by().is_none(), "a 0-cycle request is not a stall");
3644
3645 let before = p.ic_rf.pc;
3646 p.advance(&mut bus, &mut regs, &mut pc);
3647 assert_ne!(before, p.ic_rf.pc, "the cycle was silently consumed");
3648 assert!(
3649 p.prev_cycle_was_run(),
3650 "a non-stall must still count as a run cycle, or the interrupt gate \
3651 is wrongly suppressed on the following cycle"
3652 );
3653 }
3654
3655 /// An abort kills its own stage and everything younger, never anything older.
3656 #[test]
3657 fn abort_kills_younger_instructions_only() {
3658 let mut p = Pipeline::new();
3659 let mut pc = 0xFFFF_FFFF_8000_0000;
3660 let mut regs = Regs::new();
3661 let mut bus = quiet();
3662 for _ in 0..4 {
3663 p.advance(&mut bus, &mut regs, &mut pc);
3664 }
3665 p.abort_from(Stage::Ex, Exception::Overflow);
3666 assert_eq!(p.rf_ex.abort, Some(Exception::Overflow), "EX's own latch");
3667 assert_eq!(p.ic_rf.abort, Some(Exception::Overflow), "younger: killed");
3668 assert_eq!(p.ex_dc.abort, None, "older instruction must survive");
3669 assert_eq!(p.dc_wb.abort, None, "older instruction must survive");
3670 }
3671
3672 /// An aborted instruction must not retire.
3673 #[test]
3674 fn an_aborted_instruction_does_not_retire() {
3675 let mut p = Pipeline::new();
3676 let mut pc = 0xFFFF_FFFF_8000_0000;
3677 let mut regs = Regs::new();
3678 let mut bus = quiet();
3679 for _ in 0..4 {
3680 p.advance(&mut bus, &mut regs, &mut pc);
3681 }
3682 p.dc_wb.abort = Some(Exception::AddressError { store: false });
3683 let retired = p.retired;
3684 p.advance(&mut bus, &mut regs, &mut pc);
3685 assert_eq!(p.retired, retired, "an aborted instruction retired anyway");
3686 }
3687
3688 /// **End to end**: a real program, fetched from a bus, decoded, executed and
3689 /// committed to the register file through all five stages.
3690 ///
3691 /// Until this passed, every other test in the crate exercised a piece in
3692 /// isolation. This is the one that says the CPU *runs*.
3693 #[test]
3694 fn a_program_executes_through_the_whole_pipeline() {
3695 /// A bus holding a program at 0, returning `NOP` past its end.
3696 struct Rom(alloc::vec::Vec<u32>);
3697 impl Bus for Rom {
3698 fn read_u8(&mut self, _addr: u32) -> u8 {
3699 0
3700 }
3701 fn write_u8(&mut self, _addr: u32, _val: u8) {}
3702 fn read_u32(&mut self, addr: u32) -> u32 {
3703 self.0.get((addr / 4) as usize).copied().unwrap_or(0)
3704 }
3705 }
3706 const fn r(funct: u32, rs: u32, rt: u32, rd: u32, sa: u32) -> u32 {
3707 (rs << 21) | (rt << 16) | (rd << 11) | (sa << 6) | funct
3708 }
3709 const fn i(opcode: u32, rs: u32, rt: u32, imm: u16) -> u32 {
3710 (opcode << 26) | (rs << 21) | (rt << 16) | imm as u32
3711 }
3712
3713 // ADDIU $1, $0, 6 ; $1 = 6
3714 // ADDIU $2, $0, 7 ; $2 = 7
3715 // MULT $1, $2 ; HI:LO = 42 (stalls 5 PCycles)
3716 // MFLO $3 ; $3 = 42
3717 // ADDU $4, $1, $2 ; $4 = 13
3718 // SLL $5, $2, 2 ; $5 = 28
3719 let program = alloc::vec![
3720 i(0o11, 0, 1, 6),
3721 i(0o11, 0, 2, 7),
3722 r(0o30, 1, 2, 0, 0),
3723 r(0o22, 0, 0, 3, 0),
3724 r(0o41, 1, 2, 4, 0),
3725 r(0o00, 0, 2, 5, 2),
3726 ];
3727 let mut bus = Rom(program);
3728 let mut regs = Regs::new();
3729 let mut p = Pipeline::new();
3730 let mut pc = 0xFFFF_FFFF_8000_0000u64;
3731
3732 // Generous budget: 6 instructions, 5 stages deep, plus the MULT stall.
3733 for _ in 0..64 {
3734 p.advance(&mut bus, &mut regs, &mut pc);
3735 }
3736
3737 assert_eq!(regs.read(1), 6, "ADDIU $1");
3738 assert_eq!(regs.read(2), 7, "ADDIU $2");
3739 assert_eq!(regs.lo, 42, "MULT wrote LO");
3740 assert_eq!(regs.hi, 0, "MULT wrote HI");
3741 assert_eq!(regs.read(3), 42, "MFLO read LO into $3");
3742 assert_eq!(regs.read(4), 13, "ADDU $1 + $2");
3743 assert_eq!(regs.read(5), 28, "SLL $2 << 2");
3744 assert_eq!(regs.read(0), 0, "$zero stayed zero");
3745 assert!(p.retired >= 6, "all six instructions retired");
3746 }
3747
3748 /// `$zero` must survive an instruction that nominally targets it — software
3749 /// depends on that, and `ADDIU $0, $0, 5` is a legal encoding.
3750 #[test]
3751 fn writes_to_zero_are_discarded_by_the_pipeline() {
3752 struct Ones;
3753 impl Bus for Ones {
3754 fn read_u8(&mut self, _addr: u32) -> u8 {
3755 0
3756 }
3757 fn write_u8(&mut self, _addr: u32, _val: u8) {}
3758 fn read_u32(&mut self, _addr: u32) -> u32 {
3759 // ADDIU $0, $0, 5 -- targets $zero, forever.
3760 (0o11 << 26) | 5
3761 }
3762 }
3763 let mut bus = Ones;
3764 let mut regs = Regs::new();
3765 let mut p = Pipeline::new();
3766 let mut pc = 0xFFFF_FFFF_8000_0000u64;
3767 for _ in 0..32 {
3768 p.advance(&mut bus, &mut regs, &mut pc);
3769 }
3770 assert_eq!(regs.read(0), 0, "$zero was written");
3771 assert_eq!(regs.gpr[0], 0, "$zero was written through the raw array");
3772 }
3773
3774 /// An overflowing `ADD` must abort rather than commit, and the destination
3775 /// register must be left untouched.
3776 #[test]
3777 fn an_overflow_trap_prevents_the_write_back() {
3778 struct Overflowing;
3779 impl Bus for Overflowing {
3780 fn read_u8(&mut self, _addr: u32) -> u8 {
3781 0
3782 }
3783 fn write_u8(&mut self, _addr: u32, _val: u8) {}
3784 fn read_u32(&mut self, addr: u32) -> u32 {
3785 match addr / 4 {
3786 // LUI $1, 0x7FFF ; ORI $1, $1, 0xFFFF => $1 = i32::MAX
3787 0 => (0o17 << 26) | (1 << 16) | 0x7FFF,
3788 1 => (0o15 << 26) | (1 << 21) | (1 << 16) | 0xFFFF,
3789 // ADDIU $2, $0, 1
3790 2 => (0o11 << 26) | (2 << 16) | 1,
3791 // ADD $3, $1, $2 -> overflows
3792 3 => (1 << 21) | (2 << 16) | (3 << 11) | 0o40,
3793 _ => 0,
3794 }
3795 }
3796 }
3797 let mut bus = Overflowing;
3798 let mut regs = Regs::new();
3799 let mut p = Pipeline::new();
3800 let mut pc = 0xFFFF_FFFF_8000_0000u64;
3801 for _ in 0..48 {
3802 p.advance(&mut bus, &mut regs, &mut pc);
3803 }
3804 assert_eq!(regs.read(1), 0x7FFF_FFFF, "LUI+ORI built i32::MAX");
3805 assert_eq!(regs.read(3), 0, "the overflowing ADD must not commit");
3806 }
3807
3808 /// An unaligned instruction fetch raises an address error instead of
3809 /// fetching, and must not silently realign the PC — that would convert a
3810 /// raised exception into a wrong answer on a path hardware never takes.
3811 #[test]
3812 fn an_unaligned_fetch_raises_address_error_without_realigning() {
3813 struct Watch {
3814 fetched: bool,
3815 }
3816 impl Bus for Watch {
3817 fn read_u8(&mut self, _addr: u32) -> u8 {
3818 0
3819 }
3820 fn write_u8(&mut self, _addr: u32, _val: u8) {}
3821 fn read_u32(&mut self, _addr: u32) -> u32 {
3822 self.fetched = true;
3823 0
3824 }
3825 }
3826 let mut bus = Watch { fetched: false };
3827 let mut regs = Regs::new();
3828 let mut p = Pipeline::new();
3829 let mut pc = 0xFFFF_FFFF_8000_0002; // deliberately unaligned
3830
3831 p.advance(&mut bus, &mut regs, &mut pc);
3832
3833 assert_eq!(
3834 p.ic_rf.abort,
3835 Some(Exception::AddressError { store: false }),
3836 "an unaligned fetch must raise AdEL"
3837 );
3838 assert!(
3839 !bus.fetched,
3840 "the bus must not be accessed for a bad address"
3841 );
3842 // The PC is not silently realigned -- it is *vectored*, which is the
3843 // architectural response. Before T-12-002 nothing dispatched and this
3844 // asserted the PC stayed at the bad address; that was the absence of
3845 // dispatch, not a rule.
3846 assert_eq!(
3847 pc, 0xFFFF_FFFF_BFC0_0380,
3848 "the BEV=1 general vector -- cold reset leaves BEV set (UM §6.4.4), \
3849 so a fresh CPU vectors into the boot ROM, not into RDRAM"
3850 );
3851 assert_eq!(
3852 p.cop0.read(crate::cop0::reg::BAD_VADDR),
3853 0xFFFF_FFFF_8000_0002,
3854 "and BadVAddr holds the unaligned address, un-realigned"
3855 );
3856 assert_eq!(
3857 (p.cop0.read(crate::cop0::reg::CAUSE) >> 2) & 0x1F,
3858 crate::exception::exc_code::ADEL,
3859 "an instruction fetch is a load: AdEL, not AdES"
3860 );
3861 // EPC must name the FAULTING fetch. `abort_with` captures its context
3862 // out of `ic_rf`, so raising before populating that latch would silently
3863 // record the previous fetch's PC here -- which is exactly what this code
3864 // did until review caught it.
3865 assert_eq!(
3866 p.cop0.read(crate::cop0::reg::EPC),
3867 0xFFFF_FFFF_8000_0002,
3868 "EPC names the faulting fetch, not the previous one"
3869 );
3870
3871 // And the faulting instruction must never retire. Bounded to the
3872 // epilogue stall on purpose: past that the CPU is running the *handler*,
3873 // whose instructions retire legitimately, so a longer window would be
3874 // asserting that exception handling does not work.
3875 assert_eq!(p.retired, 0, "the faulting fetch retired");
3876 assert_eq!(
3877 p.stalled_by(),
3878 Some(Interlock::Exception),
3879 "the 2-PCycle epilogue stall is in progress (UM §4.7 p.114)"
3880 );
3881 for _ in 0..crate::exception::EPILOGUE_STALL {
3882 p.advance(&mut bus, &mut regs, &mut pc);
3883 assert_eq!(p.retired, 0, "nothing retires while the pipe drains");
3884 }
3885 assert_eq!(p.stalled_by(), None, "the stall was exactly 2 PCycles");
3886 }
3887
3888 /// A RAM-backed bus, so loads and stores can be exercised end to end.
3889 struct Ram {
3890 prog: alloc::vec::Vec<u32>,
3891 data: alloc::vec::Vec<u8>,
3892 }
3893 impl Ram {
3894 fn new(prog: alloc::vec::Vec<u32>) -> Self {
3895 Self {
3896 prog,
3897 data: alloc::vec![0; 0x1000],
3898 }
3899 }
3900 }
3901 impl Bus for Ram {
3902 fn read_u8(&mut self, addr: u32) -> u8 {
3903 self.data.get(addr as usize).copied().unwrap_or(0)
3904 }
3905 fn write_u8(&mut self, addr: u32, val: u8) {
3906 if let Some(b) = self.data.get_mut(addr as usize) {
3907 *b = val;
3908 }
3909 }
3910 fn read_u32(&mut self, addr: u32) -> u32 {
3911 // Instructions live above 0x800; data below it.
3912 if addr >= 0x800 {
3913 self.prog
3914 .get(((addr - 0x800) / 4) as usize)
3915 .copied()
3916 .unwrap_or(0)
3917 } else {
3918 u32::from_be_bytes([
3919 self.read_u8(addr),
3920 self.read_u8(addr + 1),
3921 self.read_u8(addr + 2),
3922 self.read_u8(addr + 3),
3923 ])
3924 }
3925 }
3926 }
3927 const fn ld_st(opcode: u32, base: u32, rt: u32, off: u16) -> u32 {
3928 (opcode << 26) | (base << 21) | (rt << 16) | off as u32
3929 }
3930
3931 /// A store followed by a load round-trips through real memory, at every
3932 /// width, with the sign/zero-extension rules applied.
3933 #[test]
3934 fn stores_and_loads_round_trip_through_memory() {
3935 // LUI $1, 0x8000 ; KSEG0 base
3936 // ADDIU $2, $0, -2 ; value = 0xFFFF_FFFF_FFFF_FFFE
3937 // SW $2, 0x100($1)
3938 // LW $3, 0x100($1) ; sign-extended -> 0xFFFF_FFFF_FFFF_FFFE
3939 // LWU $4, 0x100($1) ; zero-extended -> 0x0000_0000_FFFF_FFFE
3940 // LBU $5, 0x100($1) ; big-endian MSB -> 0xFF
3941 let prog = alloc::vec![
3942 lui_kseg0(1),
3943 (0o11 << 26) | (2 << 16) | 0xFFFE,
3944 ld_st(0o53, 1, 2, 0x100),
3945 ld_st(0o43, 1, 3, 0x100),
3946 ld_st(0o47, 1, 4, 0x100),
3947 ld_st(0o44, 1, 5, 0x100),
3948 ];
3949 let mut bus = Ram::new(prog);
3950 let mut regs = Regs::new();
3951 let mut p = Pipeline::new();
3952 let mut pc = KSEG0_PROG;
3953 for _ in 0..80 {
3954 p.advance(&mut bus, &mut regs, &mut pc);
3955 }
3956 assert_eq!(regs.read(3), 0xFFFF_FFFF_FFFF_FFFE, "LW sign-extends");
3957 assert_eq!(regs.read(4), 0x0000_0000_FFFF_FFFE, "LWU zero-extends");
3958 assert_eq!(regs.read(5), 0xFF, "LBU reads the big-endian MSB");
3959 }
3960
3961 /// An unaligned `LW` raises an address error and does not commit — the whole
3962 /// reason the `LWL`/`LWR` family exists.
3963 #[test]
3964 fn an_unaligned_load_raises_address_error() {
3965 // ADDIU $1, $0, 0x101 ; deliberately unaligned
3966 // LW $2, 0($1)
3967 let prog = alloc::vec![(0o11 << 26) | (1 << 16) | 0x101, ld_st(0o43, 1, 2, 0)];
3968 let mut bus = Ram::new(prog);
3969 let mut regs = Regs::new();
3970 let mut p = Pipeline::new();
3971 let mut pc = KSEG0_PROG;
3972 for _ in 0..40 {
3973 p.advance(&mut bus, &mut regs, &mut pc);
3974 }
3975 assert_eq!(regs.read(2), 0, "an unaligned LW must not commit");
3976 }
3977
3978 /// **The load-delay interlock** (UM §4.6.5) — it finally has something to
3979 /// interlock against. A load followed by an instruction naming the loaded
3980 /// register stalls one cycle, and the dependent instruction must still see
3981 /// the loaded value.
3982 #[test]
3983 fn a_load_followed_by_its_use_interlocks_and_still_reads_the_value() {
3984 // LUI $1, 0x8000 ; KSEG0 base -- unmapped, so no TLB entry needed
3985 // ADDIU $2, $0, 0x55
3986 // SW $2, 0x100($1)
3987 // LW $3, 0x100($1) ; load
3988 // ADDIU $4, $3, 1 ; uses $3 immediately -> LDI stall
3989 let prog = alloc::vec![
3990 lui_kseg0(1),
3991 (0o11 << 26) | (2 << 16) | 0x55,
3992 ld_st(0o53, 1, 2, 0x100),
3993 ld_st(0o43, 1, 3, 0x100),
3994 (0o11 << 26) | (3 << 21) | (4 << 16) | 1,
3995 ];
3996 let mut bus = Ram::new(prog);
3997 let mut regs = Regs::new();
3998 let mut p = Pipeline::new();
3999 let mut pc = KSEG0_PROG;
4000 let mut saw_ldi = false;
4001 for _ in 0..80 {
4002 p.advance(&mut bus, &mut regs, &mut pc);
4003 if p.stalled_by() == Some(Interlock::Ldi) {
4004 saw_ldi = true;
4005 }
4006 }
4007 assert!(saw_ldi, "the load-delay interlock never fired");
4008 assert_eq!(regs.read(3), 0x55, "LW loaded the stored value");
4009 assert_eq!(regs.read(4), 0x56, "the dependent instruction saw it");
4010 }
4011
4012 /// The `LWL`/`LWR` pair assembles an unaligned word from memory — the
4013 /// end-to-end version of the unit tests in `mem`.
4014 #[test]
4015 fn the_unaligned_pair_assembles_a_word_from_real_memory() {
4016 // ADDIU $1, $0, 0x100
4017 // LWL $3, 1($1)
4018 // LWR $3, 4($1)
4019 let prog = alloc::vec![
4020 lui_kseg0(1),
4021 ld_st(0o42, 1, 3, 0x101),
4022 ld_st(0o46, 1, 3, 0x104),
4023 ];
4024 let mut bus = Ram::new(prog);
4025 // Memory at 0x100: 00 11 22 33 44 55 66 77
4026 for (k, b) in [0x00u8, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77]
4027 .into_iter()
4028 .enumerate()
4029 {
4030 bus.write_u8(0x100 + k as u32, b);
4031 }
4032 let mut regs = Regs::new();
4033 let mut p = Pipeline::new();
4034 let mut pc = KSEG0_PROG;
4035 for _ in 0..60 {
4036 p.advance(&mut bus, &mut regs, &mut pc);
4037 }
4038 assert_eq!(
4039 regs.read(3),
4040 crate::alu::sext32(0x1122_3344),
4041 "LWL+LWR must assemble the unaligned word at 0x101"
4042 );
4043 }
4044
4045 /// **The branch delay slot executes before the target.** This is the single
4046 /// most load-bearing property of MIPS control flow: the instruction *after* a
4047 /// branch runs whether or not the branch is taken.
4048 #[test]
4049 fn the_delay_slot_executes_before_the_branch_target() {
4050 // 0x800: ADDIU $1, $0, 1
4051 // 0x804: BEQ $0, $0, +2 ; taken, to 0x810
4052 // 0x808: ADDIU $2, $0, 2 ; DELAY SLOT -- must execute
4053 // 0x80C: ADDIU $3, $0, 3 ; skipped
4054 // 0x810: ADDIU $4, $0, 4 ; target
4055 let prog = alloc::vec![
4056 (0o11 << 26) | (1 << 16) | 1,
4057 (0o04 << 26) | 2,
4058 (0o11 << 26) | (2 << 16) | 2,
4059 (0o11 << 26) | (3 << 16) | 3,
4060 (0o11 << 26) | (4 << 16) | 4,
4061 ];
4062 let mut bus = Ram::new(prog);
4063 let mut regs = Regs::new();
4064 let mut p = Pipeline::new();
4065 let mut pc = KSEG0_PROG;
4066 for _ in 0..60 {
4067 p.advance(&mut bus, &mut regs, &mut pc);
4068 }
4069 assert_eq!(regs.read(1), 1, "before the branch");
4070 assert_eq!(regs.read(2), 2, "the DELAY SLOT must execute");
4071 assert_eq!(regs.read(3), 0, "the instruction after the slot is skipped");
4072 assert_eq!(regs.read(4), 4, "the target must execute");
4073 }
4074
4075 /// **Branch-likely nullifies its delay slot when NOT taken**; an ordinary
4076 /// branch does not. Confusing the two silently runs or skips one instruction
4077 /// per untaken branch, which is invisible until a loop's trip count is wrong.
4078 #[test]
4079 fn branch_likely_nullifies_its_delay_slot_but_an_ordinary_branch_does_not() {
4080 // BNEL $0, $0, +1 ; NOT taken (0 == 0), so the slot is nullified
4081 // ADDIU $2, $0, 2 ; DELAY SLOT -- must be squashed
4082 // ADDIU $3, $0, 3
4083 let likely = alloc::vec![
4084 (0o25 << 26) | 1,
4085 (0o11 << 26) | (2 << 16) | 2,
4086 (0o11 << 26) | (3 << 16) | 3,
4087 ];
4088 let mut bus = Ram::new(likely);
4089 let mut regs = Regs::new();
4090 let mut p = Pipeline::new();
4091 let mut pc = KSEG0_PROG;
4092 for _ in 0..50 {
4093 p.advance(&mut bus, &mut regs, &mut pc);
4094 }
4095 assert_eq!(
4096 regs.read(2),
4097 0,
4098 "BNEL not taken must NULLIFY its delay slot"
4099 );
4100 assert_eq!(regs.read(3), 3, "execution continues after the slot");
4101
4102 // The same shape with the ordinary BNE: the slot DOES execute.
4103 let ordinary = alloc::vec![
4104 (0o05 << 26) | 1,
4105 (0o11 << 26) | (2 << 16) | 2,
4106 (0o11 << 26) | (3 << 16) | 3,
4107 ];
4108 let mut bus = Ram::new(ordinary);
4109 let mut regs = Regs::new();
4110 let mut p = Pipeline::new();
4111 let mut pc = KSEG0_PROG;
4112 for _ in 0..50 {
4113 p.advance(&mut bus, &mut regs, &mut pc);
4114 }
4115 assert_eq!(
4116 regs.read(2),
4117 2,
4118 "BNE not taken must still RUN its delay slot"
4119 );
4120 }
4121
4122 /// `JAL` links the address *after* the delay slot, so a returning `JR $31`
4123 /// resumes past it rather than re-executing it.
4124 #[test]
4125 fn jal_links_past_the_delay_slot_and_jr_returns_there() {
4126 // 0x800: JAL 0x204 ; -> 0x810, links $31 = 0x808
4127 // 0x804: ADDIU $1, $0, 1 ; DELAY SLOT
4128 // 0x808: ADDIU $2, $0, 2 ; where JR $31 must return to
4129 // 0x80C: ADDIU $9, $0, 9 ; must NOT run before the return
4130 // 0x810: JR $31
4131 // 0x814: ADDIU $3, $0, 3 ; the callee's DELAY SLOT
4132 let prog = alloc::vec![
4133 (0o03 << 26) | 0x204,
4134 (0o11 << 26) | (1 << 16) | 1,
4135 (0o11 << 26) | (2 << 16) | 2,
4136 (0o11 << 26) | (9 << 16) | 9,
4137 (31 << 21) | 0o10,
4138 (0o11 << 26) | (3 << 16) | 3,
4139 ];
4140 let mut bus = Ram::new(prog);
4141 let mut regs = Regs::new();
4142 let mut p = Pipeline::new();
4143 let mut pc = KSEG0_PROG;
4144 for _ in 0..80 {
4145 p.advance(&mut bus, &mut regs, &mut pc);
4146 }
4147 assert_eq!(
4148 regs.read(31),
4149 KSEG0_PROG + 8,
4150 "JAL links PC+8, past the delay slot"
4151 );
4152 assert_eq!(regs.read(1), 1, "JAL's delay slot ran");
4153 assert_eq!(regs.read(3), 3, "JR's delay slot ran");
4154 assert_eq!(regs.read(2), 2, "JR $31 returned to the linked address");
4155 }
4156
4157 /// A trap whose condition holds raises an exception and does not commit.
4158 #[test]
4159 fn a_taken_trap_raises_and_an_untaken_one_does_not() {
4160 // ADDIU $1, $0, 5
4161 // TEQ $1, $1 ; equal -> traps
4162 // ADDIU $2, $0, 2 ; must not commit
4163 let prog = alloc::vec![
4164 (0o11 << 26) | (1 << 16) | 5,
4165 (1 << 21) | (1 << 16) | 0o64,
4166 (0o11 << 26) | (2 << 16) | 2,
4167 ];
4168 let mut bus = Ram::new(prog);
4169 let mut regs = Regs::new();
4170 let mut p = Pipeline::new();
4171 let mut pc = KSEG0_PROG;
4172 let mut trapped = false;
4173 for _ in 0..50 {
4174 p.advance(&mut bus, &mut regs, &mut pc);
4175 if p.ex_dc.abort == Some(Exception::Trap) || p.dc_wb.abort == Some(Exception::Trap) {
4176 trapped = true;
4177 }
4178 }
4179 assert!(trapped, "TEQ with equal operands must trap");
4180
4181 // TNE with equal operands does NOT trap.
4182 let prog = alloc::vec![
4183 (0o11 << 26) | (1 << 16) | 5,
4184 (1 << 21) | (1 << 16) | 0o66,
4185 (0o11 << 26) | (2 << 16) | 2,
4186 ];
4187 let mut bus = Ram::new(prog);
4188 let mut regs = Regs::new();
4189 let mut p = Pipeline::new();
4190 let mut pc = KSEG0_PROG;
4191 for _ in 0..50 {
4192 p.advance(&mut bus, &mut regs, &mut pc);
4193 }
4194 assert_eq!(
4195 regs.read(2),
4196 2,
4197 "an untaken trap must not disturb execution"
4198 );
4199 }
4200
4201 /// `SYSCALL` and `BREAK` raise their own exceptions.
4202 #[test]
4203 fn syscall_and_break_raise_their_exceptions() {
4204 for (funct, want) in [(0o14u32, Exception::Syscall), (0o15, Exception::Breakpoint)] {
4205 let mut bus = Ram::new(alloc::vec![funct]);
4206 let mut regs = Regs::new();
4207 let mut p = Pipeline::new();
4208 let mut pc = KSEG0_PROG;
4209 let mut seen = false;
4210 for _ in 0..40 {
4211 p.advance(&mut bus, &mut regs, &mut pc);
4212 if p.ex_dc.abort == Some(want) || p.dc_wb.abort == Some(want) {
4213 seen = true;
4214 }
4215 }
4216 assert!(seen, "funct {funct:o} should raise {want:?}");
4217 }
4218 }
4219
4220 /// **`in_delay_slot` must actually be set**, and only on the instruction
4221 /// after a branch or jump.
4222 ///
4223 /// This test exists because mutation-testing found the flag was **not yet
4224 /// load-bearing**: forcing it to `false` broke nothing, since its only
4225 /// consumer is `Cause.BD`/`EPC` at exception time and COP0 arrives in
4226 /// Sprint 2. A field that is written and never read is the exact pattern
4227 /// this crate has twice deleted (`poll_irq_at_phase`, `Stall.resume`).
4228 ///
4229 /// Rather than delete it — it is genuinely needed, and it must ride in the
4230 /// latch rather than be recomputed later — it is pinned here so it is
4231 /// verified from the moment it is written.
4232 #[test]
4233 fn in_delay_slot_is_set_on_exactly_the_instruction_after_a_branch() {
4234 // 0x800: ADDIU $1, $0, 1 ; not a delay slot
4235 // 0x804: BEQ $0, $0, +2 ; a branch, not itself a delay slot
4236 // 0x808: ADDIU $2, $0, 2 ; IS the delay slot
4237 // 0x810: ADDIU $4, $0, 4 ; target, not a delay slot
4238 let prog = alloc::vec![
4239 (0o11 << 26) | (1 << 16) | 1,
4240 (0o04 << 26) | 2,
4241 (0o11 << 26) | (2 << 16) | 2,
4242 (0o11 << 26) | (3 << 16) | 3,
4243 (0o11 << 26) | (4 << 16) | 4,
4244 ];
4245 let mut bus = Ram::new(prog);
4246 let mut regs = Regs::new();
4247 let mut p = Pipeline::new();
4248 let mut pc = KSEG0_PROG;
4249
4250 let mut flagged = alloc::vec::Vec::new();
4251 for _ in 0..8 {
4252 p.advance(&mut bus, &mut regs, &mut pc);
4253 if p.ic_rf.occupied && p.ic_rf.in_delay_slot {
4254 flagged.push(p.ic_rf.pc);
4255 }
4256 }
4257 assert_eq!(
4258 flagged,
4259 alloc::vec![KSEG0_PROG + 8],
4260 "exactly one instruction -- the one after the branch -- must be \
4261 flagged as a delay slot"
4262 );
4263 }
4264
4265 /// The load interlock reproduces the hardware's documented imprecision.
4266 /// Emulating *precise* behavior here is the bug.
4267 #[test]
4268 fn load_interlock_is_imprecise_exactly_as_hardware_is() {
4269 // Matches on rt: the ordinary true positive.
4270 assert!(load_interlocks(8, 0, 8, true));
4271 // Matches on rs.
4272 assert!(load_interlocks(8, 8, 0, true));
4273 // False positive hardware really has: LUI's unused rs field matching.
4274 assert!(
4275 load_interlocks(8, 8, 9, true),
4276 "hardware stalls even when the field is not used as a source"
4277 );
4278 // Two consecutive loads into the same register also stall.
4279 assert!(load_interlocks(8, 0, 8, true));
4280 // $zero is exempt -- a load into it can never be depended on.
4281 assert!(!load_interlocks(0, 0, 0, true));
4282 // GPR loads do not interlock with float instructions, or vice versa.
4283 assert!(!load_interlocks(8, 8, 8, false));
4284 // No overlap at all.
4285 assert!(!load_interlocks(8, 9, 10, true));
4286 }
4287
4288 // --- LL / SC (UM §16 pp. 453, 487; §3.1; §5.4.7) ------------------------
4289
4290 use crate::mem::{LoadKind, StoreKind};
4291
4292 /// Where [`Ram`] keeps test programs, addressed through **KSEG0**.
4293 ///
4294 /// KSEG0 is unmapped, so it reaches physical `0x800` without a TLB entry —
4295 /// which is how real code runs and, since T-12-004, the only way a test can
4296 /// fetch at all without installing a mapping. Fetching from a bare `0x800`
4297 /// is a KUSEG address and now correctly raises a TLB refill.
4298 const KSEG0_PROG: u64 = 0xFFFF_FFFF_8000_0800;
4299
4300 /// A bus that fetches `NOP`s and holds its interrupt line asserted.
4301 ///
4302 /// Reads return 0, which decodes to `SLL $0, $0, 0` — the canonical `NOP` —
4303 /// so the pipeline runs without any instruction interfering with what these
4304 /// tests observe.
4305 struct AlwaysIrq;
4306 impl Bus for AlwaysIrq {
4307 fn read_u8(&mut self, _a: u32) -> u8 {
4308 0
4309 }
4310 fn write_u8(&mut self, _a: u32, _v: u8) {}
4311 fn read_u32(&mut self, _a: u32) -> u32 {
4312 0
4313 }
4314 fn poll_irq(&mut self) -> bool {
4315 true
4316 }
4317 }
4318
4319 /// The synchronization tests need only the data half of [`Ram`].
4320 fn ram() -> Ram {
4321 Ram::new(alloc::vec![])
4322 }
4323
4324 /// Push the dirty D-cache line covering `vaddr` out to the bus.
4325 ///
4326 /// A store to a cached segment now lands in the write-back D-cache and RAM
4327 /// does not see it until something forces it out. That is the whole point of
4328 /// a write-back cache, and it is what real software does before handing a
4329 /// buffer to DMA -- so a test that asserts on RAM without this would be
4330 /// asserting the cache does not exist.
4331 fn writeback<B: Bus>(p: &mut Pipeline, bus: &mut B, vaddr: u64) {
4332 p.access(
4333 bus,
4334 MemOp::Cache {
4335 addr: vaddr,
4336 op: 25,
4337 },
4338 )
4339 .expect("Hit_WriteBack on a mapped address cannot fault");
4340 }
4341
4342 /// Without a preceding `LL` the store must not happen, and `rt` must still
4343 /// be written — with 0. A `Store`-shaped implementation writes memory
4344 /// unconditionally; a `Load`-shaped one never writes `rt` on failure.
4345 #[test]
4346 fn sc_without_ll_stores_nothing_and_reports_failure() {
4347 let mut p = Pipeline::new();
4348 let mut bus = ram();
4349 let wb = p
4350 .access(
4351 &mut bus,
4352 MemOp::ConditionalStore {
4353 kind: StoreKind::Word,
4354 addr: 0xFFFF_FFFF_8000_0000 | 0x40,
4355 value: 0xDEAD_BEEF,
4356 dest: 9,
4357 },
4358 )
4359 .expect("aligned");
4360 assert_eq!(
4361 wb,
4362 WriteBack::Gpr { dest: 9, value: 0 },
4363 "failure is reported in rt as 0"
4364 );
4365 assert_eq!(bus.read_u32(0x40), 0, "memory untouched");
4366 }
4367
4368 /// The ordinary success path, and the `LLAddr` side effect.
4369 #[test]
4370 fn ll_arms_the_link_and_records_the_physical_address() {
4371 let mut p = Pipeline::new();
4372 let mut bus = ram();
4373 bus.data[0x40..0x44].copy_from_slice(&0x1234_5678u32.to_be_bytes());
4374
4375 // KSEG0, so `translate` has to strip the segment for LLAddr to be right.
4376 let wb = p
4377 .access(
4378 &mut bus,
4379 MemOp::LinkedLoad {
4380 kind: LoadKind::SignedWord,
4381 addr: 0xFFFF_FFFF_8000_0040,
4382 dest: 8,
4383 },
4384 )
4385 .expect("aligned");
4386 assert_eq!(
4387 wb,
4388 WriteBack::Gpr {
4389 dest: 8,
4390 value: 0x1234_5678
4391 }
4392 );
4393 assert!(p.ll_bit(), "LL arms the link bit");
4394 assert_eq!(
4395 p.ll_addr(),
4396 0x40 >> 4,
4397 "LLAddr holds PA(31:4) of the PHYSICAL address, not the virtual one"
4398 );
4399
4400 let wb = p
4401 .access(
4402 &mut bus,
4403 MemOp::ConditionalStore {
4404 kind: StoreKind::Word,
4405 addr: 0xFFFF_FFFF_8000_0040,
4406 value: 0xA5A5_A5A5,
4407 dest: 9,
4408 },
4409 )
4410 .expect("aligned");
4411 assert_eq!(wb, WriteBack::Gpr { dest: 9, value: 1 });
4412 writeback(&mut p, &mut bus, 0xFFFF_FFFF_8000_0040);
4413 assert_eq!(bus.read_u32(0x40), 0xA5A5_A5A5, "the store happened");
4414 }
4415
4416 /// The manual lists exactly what clears `LLbit`: *"set by the LL
4417 /// instruction, cleared by an ERET, and tested by the SC instruction"*
4418 /// (UM §3.1). `SC` is a *tester*, not a clearer.
4419 ///
4420 /// This is the assertion that fails if someone "tidies up" by clearing the
4421 /// link in `SC` — which looks right, matches several other architectures,
4422 /// and would make a second `SC` spuriously fail.
4423 #[test]
4424 fn sc_does_not_clear_the_link_bit() {
4425 let mut p = Pipeline::new();
4426 let mut bus = ram();
4427 p.access(
4428 &mut bus,
4429 MemOp::LinkedLoad {
4430 kind: LoadKind::SignedWord,
4431 addr: 0xFFFF_FFFF_8000_0000 | 0x40,
4432 dest: 8,
4433 },
4434 )
4435 .expect("aligned");
4436
4437 for round in 0..3 {
4438 let wb = p
4439 .access(
4440 &mut bus,
4441 MemOp::ConditionalStore {
4442 kind: StoreKind::Word,
4443 addr: 0xFFFF_FFFF_8000_0000 | 0x40,
4444 value: 1,
4445 dest: 9,
4446 },
4447 )
4448 .expect("aligned");
4449 assert_eq!(
4450 wb,
4451 WriteBack::Gpr { dest: 9, value: 1 },
4452 "SC #{round} must still succeed -- nothing has cleared LLbit"
4453 );
4454 assert!(p.ll_bit(), "and the bit is still armed after SC #{round}");
4455 }
4456 }
4457
4458 /// *"If this instruction both fails and causes an exception, the exception
4459 /// takes precedence"* (UM §16 p. 487) — so a misaligned `SC` must raise,
4460 /// not quietly report failure in `rt`.
4461 #[test]
4462 fn misaligned_sc_raises_rather_than_reporting_failure() {
4463 let mut p = Pipeline::new();
4464 let mut bus = ram();
4465 let err = p
4466 .access(
4467 &mut bus,
4468 MemOp::ConditionalStore {
4469 kind: StoreKind::Word,
4470 addr: 0xFFFF_FFFF_8000_0000 | 0x42,
4471 value: 1,
4472 dest: 9,
4473 },
4474 )
4475 .expect_err("misaligned");
4476 assert_eq!(
4477 err,
4478 Exception::AddressError { store: true },
4479 "SC is a store, so AdES not AdEL"
4480 );
4481 }
4482
4483 /// A misaligned `LL` must not arm the link — the instruction did not
4484 /// complete, so a following `SC` has nothing to succeed against.
4485 #[test]
4486 fn misaligned_ll_does_not_arm_the_link() {
4487 let mut p = Pipeline::new();
4488 let mut bus = ram();
4489 let err = p
4490 .access(
4491 &mut bus,
4492 MemOp::LinkedLoad {
4493 kind: LoadKind::SignedWord,
4494 addr: 0xFFFF_FFFF_8000_0000 | 0x42,
4495 dest: 8,
4496 },
4497 )
4498 .expect_err("misaligned");
4499 assert_eq!(
4500 err,
4501 Exception::AddressError { store: false },
4502 "LL is a load, so AdEL not AdES"
4503 );
4504 assert!(!p.ll_bit(), "a faulted LL leaves the link disarmed");
4505 }
4506
4507 /// The doubleword forms share the path, but the width must actually differ.
4508 #[test]
4509 fn lld_and_scd_operate_on_eight_bytes() {
4510 let mut p = Pipeline::new();
4511 let mut bus = ram();
4512 bus.data[0x40..0x48].copy_from_slice(&0x0123_4567_89AB_CDEFu64.to_be_bytes());
4513
4514 let wb = p
4515 .access(
4516 &mut bus,
4517 MemOp::LinkedLoad {
4518 kind: LoadKind::Double,
4519 addr: 0xFFFF_FFFF_8000_0000 | 0x40,
4520 dest: 8,
4521 },
4522 )
4523 .expect("aligned");
4524 assert_eq!(
4525 wb,
4526 WriteBack::Gpr {
4527 dest: 8,
4528 value: 0x0123_4567_89AB_CDEF
4529 }
4530 );
4531
4532 p.access(
4533 &mut bus,
4534 MemOp::ConditionalStore {
4535 kind: StoreKind::Double,
4536 addr: 0xFFFF_FFFF_8000_0000 | 0x40,
4537 value: u64::MAX,
4538 dest: 9,
4539 },
4540 )
4541 .expect("aligned");
4542 writeback(&mut p, &mut bus, 0xFFFF_FFFF_8000_0040);
4543 assert_eq!(bus.read_u32(0x40), u32::MAX);
4544 assert_eq!(bus.read_u32(0x44), u32::MAX, "all eight bytes, not four");
4545 }
4546
4547 // --- COP0 access through the pipeline (T-12-001) -----------------------
4548
4549 /// Build a `COP0` instruction word: opcode 0o20, `rs` = form, `rt` = GPR,
4550 /// `rd` = COP0 register.
4551 const fn cop0_word(rs: u32, rt: u32, rd: u32) -> u32 {
4552 (0o20 << 26) | (rs << 21) | (rt << 16) | (rd << 11)
4553 }
4554
4555 /// `LUI rt, 0x8000` — put a **KSEG0** base in `rt`.
4556 ///
4557 /// Data addresses need this for the same reason instruction fetches need
4558 /// [`KSEG0_PROG`]: a bare low address is KUSEG, which is TLB-mapped and now
4559 /// correctly raises a refill rather than being silently masked.
4560 const fn lui_kseg0(rt: u32) -> u32 {
4561 (0o17 << 26) | (rt << 16) | 0x8000
4562 }
4563
4564 /// `ADDIU rt, $0, imm` — the constant-loading prologue these tests share.
4565 /// Written as a helper rather than inline so the `rs = $0` term does not
4566 /// have to be spelled as a no-op shift that clippy objects to.
4567 const fn addiu_zero(rt: u32, imm: u16) -> u32 {
4568 (0o11 << 26) | (rt << 16) | imm as u32
4569 }
4570
4571 /// `MTC0` then `MFC0` must round-trip through the real register file,
4572 /// exercising the WB-write / DC-read split rather than a direct call.
4573 #[test]
4574 fn mtc0_then_mfc0_round_trips_through_the_pipeline() {
4575 // ADDIU $1, $0, 0x18 ; a value to write
4576 // MTC0 $1, Compare ; COP0 write happens in WB
4577 // MFC0 $2, Compare ; COP0 read happens in DC
4578 let program = alloc::vec![
4579 addiu_zero(1, 0x18),
4580 cop0_word(0o04, 1, u32::from(crate::cop0::reg::COMPARE)),
4581 cop0_word(0o00, 2, u32::from(crate::cop0::reg::COMPARE)),
4582 ];
4583 let mut bus = Ram::new(program);
4584 let mut regs = Regs::new();
4585 let mut p = Pipeline::new();
4586 let mut pc = KSEG0_PROG;
4587
4588 for _ in 0..32 {
4589 p.advance(&mut bus, &mut regs, &mut pc);
4590 }
4591
4592 assert_eq!(
4593 p.cop0.read(crate::cop0::reg::COMPARE),
4594 0x18,
4595 "MTC0 reached the COP0 register file"
4596 );
4597 assert_eq!(regs.read(2), 0x18, "MFC0 brought it back into $2");
4598 }
4599
4600 /// `MTC0` must not be given a GPR destination by decode: it reads `rt` and
4601 /// writes COP0. If `dest` were set to `rt`, the instruction would clobber
4602 /// the very register it sourced its value from.
4603 #[test]
4604 fn mtc0_does_not_write_a_general_register() {
4605 let program = alloc::vec![
4606 addiu_zero(1, 0x55),
4607 cop0_word(0o04, 1, u32::from(crate::cop0::reg::COMPARE)),
4608 ];
4609 let mut bus = Ram::new(program);
4610 let mut regs = Regs::new();
4611 let mut p = Pipeline::new();
4612 let mut pc = KSEG0_PROG;
4613 for _ in 0..24 {
4614 p.advance(&mut bus, &mut regs, &mut pc);
4615 }
4616 assert_eq!(regs.read(1), 0x55, "$1 still holds the source value");
4617 assert_eq!(p.cop0.read(crate::cop0::reg::COMPARE), 0x55);
4618 }
4619
4620 /// The write-mask rules must survive the pipeline path, not just direct
4621 /// calls: `MTC0` to `Cause` may only touch IP1:IP0.
4622 #[test]
4623 fn a_pipelined_mtc0_still_respects_the_write_mask() {
4624 let program = alloc::vec![
4625 // ADDIU $1, $0, -1 => $1 = 0xFFFF_FFFF_FFFF_FFFF
4626 addiu_zero(1, 0xFFFF),
4627 cop0_word(0o04, 1, u32::from(crate::cop0::reg::CAUSE)),
4628 ];
4629 let mut bus = Ram::new(program);
4630 let mut regs = Regs::new();
4631 let mut p = Pipeline::new();
4632 // Move `Compare` off `Count`'s reset value first. Both reset undefined
4633 // (UM §6.4.4) and we choose a deterministic zero for each, so they match
4634 // at power-on and latch IP7 -- see accuracy-ledger D-3. Harmless, but it
4635 // would show up in `Cause` here and obscure what this test is about.
4636 p.cop0.mtc0(crate::cop0::reg::COMPARE, 0xFFFF);
4637 let mut pc = KSEG0_PROG;
4638 for _ in 0..24 {
4639 p.advance(&mut bus, &mut regs, &mut pc);
4640 }
4641 assert_eq!(
4642 p.cop0.read(crate::cop0::reg::CAUSE),
4643 0x0000_0300,
4644 "only the software interrupt bits took"
4645 );
4646 }
4647
4648 /// `LL` records `LLAddr`, which *is* COP0 register 17 — so a `MFC0` of it
4649 /// must see what `LL` wrote. Until COP0 existed, `LLAddr` lived on
4650 /// `Pipeline` as a second copy of the same architectural value; this test
4651 /// pins the fact that there is now only one.
4652 #[test]
4653 fn ll_writes_the_real_cop0_lladdr_register() {
4654 let mut p = Pipeline::new();
4655 let mut bus = Ram::new(alloc::vec![]);
4656 p.access(
4657 &mut bus,
4658 MemOp::LinkedLoad {
4659 kind: LoadKind::SignedWord,
4660 addr: 0xFFFF_FFFF_8000_0000 | 0x40,
4661 dest: 8,
4662 },
4663 )
4664 .expect("aligned");
4665 assert_eq!(p.ll_addr(), 0x04, "PA(31:4) of 0x40");
4666 assert_eq!(
4667 p.cop0.read(crate::cop0::reg::LL_ADDR),
4668 0x04,
4669 "the accessor and COP0 reg 17 are the same storage, not two copies"
4670 );
4671 assert_eq!(
4672 p.cop0.mfc0(crate::cop0::reg::LL_ADDR),
4673 0x04,
4674 "and software can read it back with MFC0"
4675 );
4676 }
4677
4678 // --- exception dispatch and ERET through the pipeline (T-12-002) --------
4679
4680 /// The stale-capture regression, with a *plausible* wrong answer available.
4681 ///
4682 /// The pipeline runs valid instructions first, so `ic_rf` holds a real PC
4683 /// when the unaligned fetch arrives. A capture taken before the latch is
4684 /// populated therefore reports that earlier PC — a value that looks entirely
4685 /// reasonable in `EPC`, which is what makes the bug survive inspection.
4686 #[test]
4687 fn an_unaligned_fetch_after_valid_ones_still_reports_its_own_address() {
4688 let program = alloc::vec![addiu_zero(1, 1), addiu_zero(2, 2), addiu_zero(3, 3)];
4689 let mut bus = Ram::new(program);
4690 let mut regs = Regs::new();
4691 let mut p = Pipeline::new();
4692 p.cop0.set_hardware(crate::cop0::reg::STATUS, 0);
4693 let mut pc = KSEG0_PROG;
4694
4695 // Let the pipeline fill with real instructions.
4696 for _ in 0..3 {
4697 p.advance(&mut bus, &mut regs, &mut pc);
4698 }
4699 assert!(
4700 p.ic_rf.pc >= 0x800,
4701 "a real PC is latched to be stale about"
4702 );
4703
4704 // Now fetch an unaligned address.
4705 pc = 0xFFFF_FFFF_8000_0006;
4706 p.advance(&mut bus, &mut regs, &mut pc);
4707
4708 assert_eq!(
4709 p.cop0.read(crate::cop0::reg::EPC),
4710 0xFFFF_FFFF_8000_0006,
4711 "EPC must be the unaligned fetch, not the last good one"
4712 );
4713 assert_eq!(
4714 p.cop0.read(crate::cop0::reg::BAD_VADDR),
4715 0xFFFF_FFFF_8000_0006,
4716 "and BadVAddr likewise"
4717 );
4718 }
4719
4720 /// `ERET` clears `LLbit`, completing the `LL`/`SC` contract that Sprint 1
4721 /// left open: until now **nothing** cleared the link, so a `LL`; `ERET`;
4722 /// `SC` sequence wrongly succeeded.
4723 #[test]
4724 fn eret_clears_the_link_bit_and_makes_a_following_sc_fail() {
4725 let mut p = Pipeline::new();
4726 let mut bus = ram();
4727 p.access(
4728 &mut bus,
4729 MemOp::LinkedLoad {
4730 kind: LoadKind::SignedWord,
4731 addr: 0xFFFF_FFFF_8000_0000 | 0x40,
4732 dest: 8,
4733 },
4734 )
4735 .expect("aligned");
4736 assert!(p.ll_bit(), "LL armed the link");
4737
4738 // ERET: opcode 0o20, rs = 0o20 (CO), funct = 0o30.
4739 let word = (0o20 << 26) | (0o20 << 21) | 0o30;
4740 assert_eq!(decode(word).op, crate::decode::Op::Eret);
4741
4742 p.cop0
4743 .set_hardware(crate::cop0::reg::STATUS, 1 << 1 /* EXL */);
4744 p.cop0
4745 .set_hardware(crate::cop0::reg::EPC, 0xFFFF_FFFF_8000_5000);
4746
4747 let mut regs = Regs::new();
4748 let mut prog = Ram::new(alloc::vec![word]);
4749 let mut pc = KSEG0_PROG;
4750 // The `LL` above filled the D-cache, whose stall (`M_DCACHE_FILL`, ledger
4751 // C-1) must drain before the ERET runs -- so more cycles than the bare
4752 // pipeline latency.
4753 for _ in 0..60 {
4754 p.advance(&mut prog, &mut regs, &mut pc);
4755 }
4756
4757 assert!(!p.ll_bit(), "ERET cleared the link (UM §3.1)");
4758 let wb = p
4759 .access(
4760 &mut bus,
4761 MemOp::ConditionalStore {
4762 kind: StoreKind::Word,
4763 addr: 0xFFFF_FFFF_8000_0000 | 0x40,
4764 value: 0xFFFF,
4765 dest: 9,
4766 },
4767 )
4768 .expect("aligned");
4769 assert_eq!(
4770 wb,
4771 WriteBack::Gpr { dest: 9, value: 0 },
4772 "SC after ERET must fail"
4773 );
4774 assert_eq!(bus.data[0x40..0x44], [0, 0, 0, 0], "and store nothing");
4775 }
4776
4777 /// `ERET` resumes at `EPC` and clears `EXL`, and it has **no delay slot** —
4778 /// the instruction after it must not execute.
4779 #[test]
4780 fn eret_resumes_at_epc_and_has_no_delay_slot() {
4781 let eret = (0o20 << 26) | (0o20 << 21) | 0o30;
4782 // ERET, then an instruction that would be a delay slot for any branch.
4783 // If it runs, $5 becomes 0x1234 -- which is the whole assertion.
4784 let program = alloc::vec![eret, addiu_zero(5, 0x1234)];
4785 let mut bus = Ram::new(program);
4786 let mut regs = Regs::new();
4787 let mut p = Pipeline::new();
4788 p.cop0.set_hardware(crate::cop0::reg::STATUS, 1 << 1);
4789 p.cop0
4790 .set_hardware(crate::cop0::reg::EPC, 0xFFFF_FFFF_8000_5000);
4791 let mut pc = KSEG0_PROG;
4792
4793 for _ in 0..12 {
4794 p.advance(&mut bus, &mut regs, &mut pc);
4795 }
4796
4797 assert_eq!(
4798 regs.read(5),
4799 0,
4800 "ERET has no delay slot -- the following instruction must be squashed"
4801 );
4802 assert_eq!(
4803 p.cop0.read(crate::cop0::reg::STATUS) & (1 << 1),
4804 0,
4805 "EXL cleared"
4806 );
4807 }
4808
4809 /// **An interrupt pending across an `ERET` is taken at the `ERET`'s TARGET,
4810 /// never at the `ERET` itself** — ledger R-18, and the defect that stopped
4811 /// Banjo-Tooie dead.
4812 ///
4813 /// `ERET` resolves in `EX`, where it clears `Status.EXL` and points
4814 /// `next_pc` at `EPC`. `DC` runs its interrupt check one cycle later, by
4815 /// which time `EXL` reads 0 and `interrupt_pending()` is true — so the
4816 /// interrupt was stamped onto the instruction sitting in `ex_dc`, **which is
4817 /// the `ERET`**. That overwrites `EPC` with the `ERET`'s own address, and
4818 /// the return address it was about to consume is gone. The handler then
4819 /// returns to the `ERET`, which resumes at itself: an architectural
4820 /// livelock, measured at 390,625 `ERET` retirements per frame with no other
4821 /// instruction ever reaching `WB`.
4822 ///
4823 /// The assertion is on `EPC`, not on "an interrupt happened". Both the
4824 /// broken and the fixed machine take the interrupt; they differ only in
4825 /// **which PC it is charged to**, so an assertion that merely observed the
4826 /// vector being entered would pass either way.
4827 #[test]
4828 fn an_interrupt_across_an_eret_is_charged_to_the_target_not_the_eret() {
4829 /// [`Ram`], but with the interrupt line held asserted.
4830 ///
4831 /// `dc_stage` re-samples `bus.poll_irq()` into `Cause.IP2` every cycle,
4832 /// so seeding `IP2` on the COP0 block directly is erased on the next
4833 /// cycle — the first version of this test did exactly that and passed
4834 /// having taken no interrupt at all.
4835 struct IrqRam(Ram);
4836 impl Bus for IrqRam {
4837 fn read_u8(&mut self, a: u32) -> u8 {
4838 self.0.read_u8(a)
4839 }
4840 fn write_u8(&mut self, a: u32, v: u8) {
4841 self.0.write_u8(a, v);
4842 }
4843 fn read_u32(&mut self, a: u32) -> u32 {
4844 self.0.read_u32(a)
4845 }
4846 fn poll_irq(&mut self) -> bool {
4847 true
4848 }
4849 }
4850
4851 const TARGET: u64 = 0xFFFF_FFFF_8000_5000;
4852 let eret = (0o20 << 26) | (0o20 << 21) | 0o30;
4853 let program = alloc::vec![eret, addiu_zero(5, 0x1234)];
4854 let mut bus = IrqRam(Ram::new(program));
4855 let mut regs = Regs::new();
4856 let mut p = Pipeline::new();
4857 // In a handler: EXL set, IE set, IM allowing IP2, and IP2 asserted. This
4858 // is the state libultra's interrupt epilogue actually reaches — the
4859 // handler unmasks and returns with an RCP interrupt still pending.
4860 //
4861 // `EXL` is **set**, which is what isolates this to the `ERET`. While a
4862 // handler runs, `interrupt_pending()` is false however hard the line is
4863 // asserted, so nothing can fire until the `ERET` itself clears `EXL` in
4864 // EX — which places the `ERET` in `ex_dc` on exactly the cycle the
4865 // interrupt becomes takeable. With `EXL` clear instead, the interrupt
4866 // fires during pipeline fill and is charged to a *bubble* (`EPC == 0`) —
4867 // a different defect that would mask this one.
4868 p.cop0
4869 .set_hardware(crate::cop0::reg::STATUS, (1 << 1) | (0xFF << 8) | 1);
4870 p.cop0.set_hardware(crate::cop0::reg::EPC, TARGET);
4871 p.cop0.set_ip(2, true);
4872 let mut pc = KSEG0_PROG;
4873
4874 // Witness BOTH events as they happen, because both end-state values this
4875 // test cares about are also their own starting values: `EXL` starts set
4876 // and `EPC` starts at `TARGET`. Checking either one at the end therefore
4877 // proves nothing on its own — a machine that executed nothing at all
4878 // satisfies both. (The first version of this test did exactly that, and
4879 // it took a review to notice.) So observe the transitions instead:
4880 // `EXL` going clear can only be the `ERET` retiring, and an `Interrupt`
4881 // abort appearing in a latch can only be the interrupt being taken.
4882 let mut eret_retired = false;
4883 let mut interrupt_taken = false;
4884 for _ in 0..12 {
4885 p.advance(&mut bus, &mut regs, &mut pc);
4886 if p.cop0.read(crate::cop0::reg::STATUS) & (1 << 1) == 0 {
4887 eret_retired = true;
4888 }
4889 if [p.dc_wb.abort, p.ex_dc.abort, p.rf_ex.abort, p.ic_rf.abort]
4890 .contains(&Some(Exception::Interrupt))
4891 {
4892 interrupt_taken = true;
4893 }
4894 }
4895 assert!(
4896 eret_retired,
4897 "the ERET never cleared EXL, so it never executed -- nothing under \
4898 test happened"
4899 );
4900 assert!(
4901 interrupt_taken,
4902 "no interrupt was ever taken (Cause={:#010X}) -- this test would \
4903 prove nothing",
4904 p.cop0.read(crate::cop0::reg::CAUSE) as u32
4905 );
4906
4907 let epc = p.cop0.read(crate::cop0::reg::EPC);
4908 assert_ne!(
4909 epc, KSEG0_PROG,
4910 "the interrupt was charged to the ERET itself ({epc:#018X}); ERET would \
4911 then resume at its own address forever (R-18 livelock)"
4912 );
4913 assert_eq!(
4914 epc, TARGET,
4915 "an interrupt taken across an ERET belongs to the instruction the ERET \
4916 returned to, so EPC must still be the target"
4917 );
4918 }
4919
4920 /// A `SYSCALL` executed through the pipeline must vector, record `EPC`, and
4921 /// set the right `ExcCode` — the whole epilogue, end to end.
4922 #[test]
4923 fn a_syscall_vectors_and_records_its_cause() {
4924 // SYSCALL is SPECIAL funct 0o14.
4925 let program = alloc::vec![0o14];
4926 let mut bus = Ram::new(program);
4927 let mut regs = Regs::new();
4928 let mut p = Pipeline::new();
4929 // BEV=0 so the vector is the RDRAM one, which is what a running game
4930 // uses; cold reset would otherwise send us to the boot ROM.
4931 p.cop0.set_hardware(crate::cop0::reg::STATUS, 0);
4932 let mut pc = KSEG0_PROG;
4933
4934 // Stop AT the dispatch cycle. Running on would be fine architecturally
4935 // -- the handler starts fetching -- but then `pc` has moved past the
4936 // vector and asserting on it would be asserting that nothing executes.
4937 let mut cycles = 0;
4938 while p.stalled_by() != Some(Interlock::Exception) {
4939 p.advance(&mut bus, &mut regs, &mut pc);
4940 cycles += 1;
4941 assert!(cycles < 8, "SYSCALL never dispatched");
4942 }
4943
4944 assert_eq!(
4945 (p.cop0.read(crate::cop0::reg::CAUSE) >> 2) & 0x1F,
4946 crate::exception::exc_code::SYS
4947 );
4948 assert_eq!(p.cop0.read(crate::cop0::reg::EPC), KSEG0_PROG);
4949 assert_eq!(pc, 0xFFFF_FFFF_8000_0180);
4950 assert_ne!(
4951 p.cop0.read(crate::cop0::reg::STATUS) & (1 << 1),
4952 0,
4953 "EXL set, so the handler runs in kernel mode with interrupts off"
4954 );
4955 }
4956
4957 /// The epilogue must not overwrite `EPC` when `EXL` is already set — tested
4958 /// **through the pipeline**, not just against `dispatch` directly, because
4959 /// this is the failure that only shows up when handlers nest.
4960 #[test]
4961 fn a_second_exception_in_a_handler_preserves_the_first_epc() {
4962 let program = alloc::vec![0o14, 0o14];
4963 let mut bus = Ram::new(program);
4964 let mut regs = Regs::new();
4965 let mut p = Pipeline::new();
4966 p.cop0.set_hardware(crate::cop0::reg::STATUS, 0);
4967 let mut pc = KSEG0_PROG;
4968
4969 for _ in 0..8 {
4970 p.advance(&mut bus, &mut regs, &mut pc);
4971 }
4972 let first_epc = p.cop0.read(crate::cop0::reg::EPC);
4973 assert_eq!(first_epc, KSEG0_PROG);
4974 assert_ne!(p.cop0.read(crate::cop0::reg::STATUS) & (1 << 1), 0);
4975
4976 // Now run a second SYSCALL while EXL is still set. Point the fetch back
4977 // at the program so it hits the second word.
4978 pc = KSEG0_PROG + 4;
4979 for _ in 0..8 {
4980 p.advance(&mut bus, &mut regs, &mut pc);
4981 }
4982 assert_eq!(
4983 p.cop0.read(crate::cop0::reg::EPC),
4984 first_epc,
4985 "the first handler's return address must survive (UM §6.3.7)"
4986 );
4987 }
4988
4989 // --- interrupts, Count/Compare (T-12-003) -------------------------------
4990
4991 /// Every term of the recognition predicate is load-bearing. Dropping the
4992 /// `EXL`/`ERL` terms is the classic bug: it works until an interrupt arrives
4993 /// inside a handler, and then re-enters it forever.
4994 #[test]
4995 fn every_term_of_the_interrupt_predicate_is_required() {
4996 use crate::cop0::reg;
4997 let ready = 1u64 | (1 << 10); // IE | IM2
4998
4999 let mut p = Pipeline::new();
5000 p.cop0.set_hardware(reg::STATUS, ready);
5001 p.cop0.set_ip(2, true);
5002 assert!(p.cop0.interrupt_pending(), "all four conditions met");
5003
5004 for (name, status) in [
5005 ("IE clear", ready & !1),
5006 ("EXL set", ready | (1 << 1)),
5007 ("ERL set", ready | (1 << 2)),
5008 ("IM2 masked", ready & !(1 << 10)),
5009 ] {
5010 let mut p = Pipeline::new();
5011 p.cop0.set_hardware(reg::STATUS, status);
5012 p.cop0.set_ip(2, true);
5013 assert!(
5014 !p.cop0.interrupt_pending(),
5015 "{name}: the interrupt must NOT be recognized"
5016 );
5017 }
5018 }
5019
5020 /// A masked interrupt must still be *visible* in `Cause.IP`, because
5021 /// software polls it. Folding assertion and recognition into one step makes
5022 /// a masked line invisible to `MFC0 Cause`.
5023 #[test]
5024 fn a_masked_interrupt_is_still_visible_in_cause() {
5025 use crate::cop0::reg;
5026 let mut p = Pipeline::new();
5027 let mut regs = Regs::new();
5028 let mut pc = KSEG0_PROG;
5029 let mut bus = AlwaysIrq;
5030 // IE set but IM2 MASKED, so nothing is recognized.
5031 p.cop0.set_hardware(reg::STATUS, 1);
5032
5033 p.advance(&mut bus, &mut regs, &mut pc);
5034 assert_ne!(
5035 p.cop0.read(reg::CAUSE) & (1 << 10),
5036 0,
5037 "IP2 is asserted even though it is masked"
5038 );
5039 assert!(!p.cop0.interrupt_pending(), "but not recognized");
5040 }
5041
5042 /// **NMI ignores every mask, and saves to `ErrorEPC` rather than `EPC`.**
5043 ///
5044 /// The interesting part is what it is *not*: `Status` here has `IE` clear,
5045 /// `EXL` set and `ERL` set, which is the state in which an ordinary
5046 /// interrupt is unconditionally ignored. The companion assertion below
5047 /// checks exactly that, so the two differ only in which line was raised.
5048 #[test]
5049 fn an_nmi_is_taken_regardless_of_ie_exl_and_erl() {
5050 use crate::cop0::reg;
5051 let mut p = Pipeline::new();
5052 let mut regs = Regs::new();
5053 let mut bus = Ram::new(alloc::vec![addiu_zero(1, 1); 8]);
5054 // IE clear, EXL set, ERL set -- every reason to refuse an interrupt.
5055 // EXL (bit 1) and ERL (bit 2).
5056 p.cop0.set_hardware(reg::STATUS, (1 << 1) | (1 << 2));
5057 p.cop0.set_hardware(reg::CAUSE, 0);
5058 let mut pc = KSEG0_PROG;
5059
5060 // Warm the pipeline first: the NMI is attributed to the instruction at
5061 // the boundary, and an empty `DC` latch has no PC to record.
5062 for _ in 0..5 {
5063 p.advance(&mut bus, &mut regs, &mut pc);
5064 }
5065 p.signal_nmi();
5066 // Stop at the redirect itself: past it, `pc` is the next fetch address
5067 // and has already walked on from the vector.
5068 let mut cycles = 0;
5069 while p.stalled_by() != Some(Interlock::Exception) {
5070 p.advance(&mut bus, &mut regs, &mut pc);
5071 cycles += 1;
5072 assert!(cycles < 16, "the NMI was never taken");
5073 }
5074
5075 assert_eq!(
5076 pc, 0xFFFF_FFFF_BFC0_0000,
5077 "NMI vectors to the Cold Reset location (UM §6.4.6)"
5078 );
5079 let status = p.cop0.read(reg::STATUS);
5080 assert_ne!(status & (1 << 20), 0, "SR set, to tell NMI from a reset");
5081 assert_ne!(status & (1 << 22), 0, "BEV set");
5082 assert_ne!(status & (1 << 2), 0, "ERL set");
5083 assert_eq!(status & (1 << 21), 0, "TS cleared");
5084 assert_eq!(
5085 p.cop0.read(reg::CAUSE),
5086 0,
5087 "NMI writes no Cause -- all registers are preserved but ErrorEPC \
5088 and those four Status bits"
5089 );
5090 assert_ne!(
5091 p.cop0.read(reg::ERROR_EPC),
5092 0,
5093 "the boundary PC is saved to ErrorEPC, not EPC"
5094 );
5095 assert_eq!(p.cop0.read(reg::EPC), 0, "and EPC is left alone");
5096 }
5097
5098 /// The control for the test above: in that same `Status`, an ordinary
5099 /// interrupt is refused. Without this, "NMI was taken" would be equally
5100 /// consistent with the masks simply not working.
5101 #[test]
5102 fn an_ordinary_interrupt_in_the_same_state_is_refused() {
5103 use crate::cop0::reg;
5104 let mut p = Pipeline::new();
5105 let mut regs = Regs::new();
5106 let mut bus = Ram::new(alloc::vec![addiu_zero(1, 1); 8]);
5107 // EXL (bit 1) and ERL (bit 2).
5108 p.cop0.set_hardware(reg::STATUS, (1 << 1) | (1 << 2));
5109 // Raise the timer line, the strongest ordinary interrupt available.
5110 p.cop0.set_ip(7, true);
5111 let mut pc = KSEG0_PROG;
5112
5113 for _ in 0..16 {
5114 p.advance(&mut bus, &mut regs, &mut pc);
5115 assert_ne!(
5116 p.stalled_by(),
5117 Some(Interlock::Exception),
5118 "a maskable interrupt must NOT be taken with EXL and ERL set"
5119 );
5120 }
5121 }
5122
5123 /// **A stall must not swallow the timer interrupt.**
5124 ///
5125 /// `Count` keeps advancing while the pipeline is stalled — it is derived
5126 /// from the master clock, not from retired instructions — so `Compare` can
5127 /// be passed entirely inside a multi-cycle interlock. An `MCI` stall for a
5128 /// 64-bit multiply is 69 `PCycles` (UM Table 3-12), far longer than the
5129 /// single cycle for which the equality holds.
5130 ///
5131 /// The failure this pins is not a *late* interrupt but a **lost** one: an
5132 /// edge test that only asks "is `Count == Compare` right now?" and is polled
5133 /// only from `DC` never sees the match at all, and `IP7` stays clear
5134 /// forever. Software waiting on the timer then hangs.
5135 #[test]
5136 fn the_timer_interrupt_survives_a_multi_cycle_stall() {
5137 use crate::cop0::reg;
5138 let mut p = Pipeline::new();
5139 let mut regs = Regs::new();
5140 let mut pc = KSEG0_PROG;
5141 let mut bus = Ram::new(alloc::vec![addiu_zero(1, 1)]);
5142 p.cop0.set_hardware(reg::STATUS, 0);
5143 // Compare sits in the middle of the stall window, never at its edges.
5144 p.cop0.mtc0(reg::COMPARE, 40);
5145
5146 p.advance_at(&mut bus, &mut regs, &mut pc, 0);
5147 // A 64-bit multiply's interlock, long enough to step over the match.
5148 p.stall_for(69, Interlock::Mci);
5149 for now in 1..=80 {
5150 p.advance_at(&mut bus, &mut regs, &mut pc, now);
5151 }
5152
5153 assert_ne!(
5154 p.cop0.read(reg::CAUSE) & (1 << 15),
5155 0,
5156 "Count passed Compare during the stall -- IP7 must still latch, \
5157 because the timer is tied to the clock and not to the pipeline"
5158 );
5159 }
5160
5161 /// `Count` reaching `Compare` raises `IP7`, and writing `Compare` clears it
5162 /// as a side effect (UM §6.3.4, p. 165).
5163 #[test]
5164 fn the_timer_interrupt_sets_ip7_and_a_compare_write_clears_it() {
5165 use crate::cop0::reg;
5166 let mut p = Pipeline::new();
5167 let mut regs = Regs::new();
5168 let mut pc = KSEG0_PROG;
5169 let mut bus = Ram::new(alloc::vec![addiu_zero(1, 1)]);
5170 p.cop0.set_hardware(reg::STATUS, 0);
5171 p.cop0.mtc0(reg::COMPARE, 3);
5172
5173 // Walk the timeline to the match.
5174 for now in 0..=3 {
5175 p.advance_at(&mut bus, &mut regs, &mut pc, now);
5176 }
5177 assert_ne!(
5178 p.cop0.read(reg::CAUSE) & (1 << 15),
5179 0,
5180 "IP7 set at Count==Compare"
5181 );
5182
5183 // It must PERSIST as Count runs past Compare. A level implementation
5184 // clears here, and with it drops any timer interrupt raised while the
5185 // CPU could not accept one.
5186 for now in 4..20 {
5187 p.advance_at(&mut bus, &mut regs, &mut pc, now);
5188 assert_ne!(
5189 p.cop0.read(reg::CAUSE) & (1 << 15),
5190 0,
5191 "IP7 must stay latched past the match (UM §6.4.18)"
5192 );
5193 }
5194
5195 // Only writing Compare clears it.
5196 p.cop0.mtc0(reg::COMPARE, 999);
5197 assert_eq!(
5198 p.cop0.read(reg::CAUSE) & (1 << 15),
5199 0,
5200 "writing Compare clears the timer interrupt"
5201 );
5202 p.advance_at(&mut bus, &mut regs, &mut pc, 20);
5203 assert_eq!(p.cop0.read(reg::CAUSE) & (1 << 15), 0, "and it stays clear");
5204 }
5205
5206 /// The convenience [`Pipeline::advance`] **holds** the `Count` timeline
5207 /// rather than guessing a rate for it.
5208 ///
5209 /// `Count` runs at half `PClock`, so stepping it once per `advance` would run
5210 /// the timer at double rate — and halving it with a parity bit would be a
5211 /// second incremented counter, exactly what ADR 0006 forbids. Anything that
5212 /// exercises `Count` must use `advance_at`.
5213 #[test]
5214 fn the_convenience_advance_holds_the_count_timeline() {
5215 let mut p = Pipeline::new();
5216 let mut regs = Regs::new();
5217 let mut pc = KSEG0_PROG;
5218 let mut bus = Ram::new(alloc::vec![]);
5219
5220 p.advance_at(&mut bus, &mut regs, &mut pc, 7);
5221 assert_eq!(p.cop0.read(crate::cop0::reg::COUNT), 7);
5222 for _ in 0..10 {
5223 p.advance(&mut bus, &mut regs, &mut pc);
5224 }
5225 assert_eq!(
5226 p.cop0.read(crate::cop0::reg::COUNT),
5227 7,
5228 "held, not advanced at PClock rate"
5229 );
5230 }
5231
5232 /// **Why `IP7` must latch.** A timer interrupt that fires while the CPU
5233 /// cannot accept one — `EXL` set, i.e. inside a handler — must still be
5234 /// waiting when the handler returns.
5235 ///
5236 /// With `IP7` modeled as a level tied to `Count == Compare`, the equality
5237 /// holds for a single tick, so the interrupt is silently **lost**. That is
5238 /// the failure the latch prevents, and it is invisible to any test that only
5239 /// checks the match cycle itself.
5240 #[test]
5241 fn a_timer_interrupt_raised_while_exl_is_set_survives_until_eret() {
5242 use crate::cop0::reg;
5243 let mut p = Pipeline::new();
5244 let mut regs = Regs::new();
5245 let mut pc = KSEG0_PROG;
5246 let mut bus = Ram::new(alloc::vec![]);
5247 // IE and IM7 set, but EXL set too: a handler is running.
5248 p.cop0.set_hardware(reg::STATUS, 1 | (1 << 15) | (1 << 1));
5249 p.cop0.mtc0(reg::COMPARE, 3);
5250
5251 for now in 0..=10 {
5252 p.advance_at(&mut bus, &mut regs, &mut pc, now);
5253 }
5254 assert!(
5255 !p.cop0.interrupt_pending(),
5256 "EXL blocks it, correctly, for now"
5257 );
5258 assert_ne!(
5259 p.cop0.read(reg::CAUSE) & (1 << 15),
5260 0,
5261 "but IP7 is still asserted, waiting"
5262 );
5263
5264 // The handler returns.
5265 let status = p.cop0.read(reg::STATUS);
5266 p.cop0.set_hardware(reg::STATUS, status & !(1 << 1));
5267 assert!(
5268 p.cop0.interrupt_pending(),
5269 "and the timer interrupt is taken now, not dropped"
5270 );
5271 }
5272
5273 /// An interrupt taken through the pipeline runs the whole epilogue: `EXL`
5274 /// set, `ExcCode` = 0, vectored.
5275 #[test]
5276 fn an_accepted_interrupt_vectors_with_exccode_zero() {
5277 use crate::cop0::reg;
5278 let mut p = Pipeline::new();
5279 let mut regs = Regs::new();
5280 let mut pc = KSEG0_PROG;
5281 let mut bus = AlwaysIrq;
5282 p.cop0.set_hardware(reg::STATUS, 1 | (1 << 10));
5283
5284 let mut cycles = 0;
5285 while p.stalled_by() != Some(Interlock::Exception) {
5286 p.advance(&mut bus, &mut regs, &mut pc);
5287 cycles += 1;
5288 assert!(cycles < 12, "the interrupt was never taken");
5289 }
5290 assert_eq!(
5291 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
5292 crate::exception::exc_code::INT
5293 );
5294 assert_ne!(p.cop0.read(reg::STATUS) & (1 << 1), 0, "EXL set");
5295 assert_eq!(pc, 0xFFFF_FFFF_8000_0180);
5296 // And now that EXL is set, the still-asserted line must NOT re-enter.
5297 assert!(
5298 !p.cop0.interrupt_pending(),
5299 "EXL blocks re-entry while the handler runs"
5300 );
5301 }
5302
5303 // --- the TLB through the pipeline (T-12-004) ---------------------------
5304
5305 /// Install a 4 KiB global mapping `vaddr` -> `pfn`, valid and writable.
5306 fn map(p: &mut Pipeline, index: u64, vaddr: u64, pfn: u64) {
5307 use crate::cop0::reg;
5308 p.cop0.set_hardware(reg::PAGE_MASK, 0);
5309 p.cop0.set_hardware(reg::ENTRY_HI, vaddr & 0xFFFF_E000);
5310 // V | D | C=3 | G, in both halves so the entry is global.
5311 p.cop0
5312 .set_hardware(reg::ENTRY_LO0, (pfn << 6) | (3 << 3) | 0b111);
5313 p.cop0
5314 .set_hardware(reg::ENTRY_LO1, ((pfn + 1) << 6) | (3 << 3) | 0b111);
5315 p.cop0.set_hardware(reg::INDEX, index);
5316 p.tlb.write_entry(index as usize, &p.cop0);
5317 }
5318
5319 /// A KUSEG access with no mapping raises a **refill**, which takes the
5320 /// refill vector — not the general one.
5321 #[test]
5322 fn an_unmapped_kuseg_access_takes_the_refill_vector() {
5323 use crate::cop0::reg;
5324 let mut p = Pipeline::new();
5325 let mut regs = Regs::new();
5326 let mut bus = Ram::new(alloc::vec![lui_kseg0(1), ld_st(0o43, 0, 3, 0x100)]);
5327 // BEV=0, EXL=0 so the refill vector is the RDRAM one.
5328 p.cop0.set_hardware(reg::STATUS, 0);
5329 let mut pc = KSEG0_PROG;
5330
5331 let mut cycles = 0;
5332 while p.stalled_by() != Some(Interlock::Exception) {
5333 p.advance(&mut bus, &mut regs, &mut pc);
5334 cycles += 1;
5335 assert!(cycles < 16, "no exception raised");
5336 }
5337 assert_eq!(
5338 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
5339 crate::exception::exc_code::TLBL,
5340 "a load miss is TLBL"
5341 );
5342 assert_eq!(
5343 pc, 0xFFFF_FFFF_8000_0000,
5344 "the REFILL vector (0x000), not the general one (0x180)"
5345 );
5346 assert_eq!(p.cop0.read(reg::BAD_VADDR), 0x100);
5347 }
5348
5349 /// The same miss, but with **64-bit addressing enabled**, must reach the
5350 /// **XTLB** refill vector at `0x080`.
5351 ///
5352 /// This is the end-to-end companion to
5353 /// `a_refill_in_64_bit_addressing_takes_the_xtlb_vector`, which only pins the
5354 /// exception-to-vector mapping. That mapping is satisfied just as well by a
5355 /// `wide` flag that is *never true in practice* — the plumbing from
5356 /// `Status.KX` through the faulting access to the exception is exactly what
5357 /// a mapping test cannot see. The two differ only in `Status`, so the
5358 /// vectors landing on different addresses is attributable to nothing else.
5359 #[test]
5360 fn an_unmapped_access_under_64_bit_addressing_takes_the_xtlb_vector() {
5361 use crate::cop0::reg;
5362 let mut p = Pipeline::new();
5363 let mut regs = Regs::new();
5364 let mut bus = Ram::new(alloc::vec![lui_kseg0(1), ld_st(0o43, 0, 3, 0x100)]);
5365 // Identical to the 32-bit case except for KX (bit 7), which turns on
5366 // 64-bit addressing for Kernel mode.
5367 p.cop0.set_hardware(reg::STATUS, 1 << 7);
5368 let mut pc = KSEG0_PROG;
5369
5370 let mut cycles = 0;
5371 while p.stalled_by() != Some(Interlock::Exception) {
5372 p.advance(&mut bus, &mut regs, &mut pc);
5373 cycles += 1;
5374 assert!(cycles < 16, "no exception raised");
5375 }
5376 assert_eq!(
5377 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
5378 crate::exception::exc_code::TLBL,
5379 "still TLBL -- the ExcCode does not change with the vector"
5380 );
5381 assert_eq!(
5382 pc, 0xFFFF_FFFF_8000_0080,
5383 "the XTLB refill vector (0x080), not the 32-bit one (0x000)"
5384 );
5385 }
5386
5387 /// A mapped, valid page translates end to end through a real load.
5388 #[test]
5389 fn a_mapped_page_translates_a_real_load() {
5390 let mut p = Pipeline::new();
5391 let mut regs = Regs::new();
5392 // Map KUSEG page-pair 0 with pfn 0 (even) / 1 (odd), so the even page is
5393 // an identity mapping onto the small test RAM. Note 0x1000 and 0x0000
5394 // are the SAME pair -- VPN2 tags at 8 KiB granularity, not 4 KiB.
5395 let mut bus = Ram::new(alloc::vec![ld_st(0o43, 0, 3, 0x100)]);
5396 bus.write_u8(0x100, 0xAB);
5397 bus.write_u8(0x101, 0xCD);
5398 bus.write_u8(0x102, 0xEF);
5399 bus.write_u8(0x103, 0x01);
5400 map(&mut p, 0, 0x1000, 0);
5401 p.cop0.set_hardware(crate::cop0::reg::STATUS, 0);
5402 let mut pc = KSEG0_PROG;
5403
5404 // Enough cycles to cover the D-cache fill stall the cached load now pays
5405 // (`M_DCACHE_FILL`, ledger C-1) on top of the pipeline latency.
5406 for _ in 0..100 {
5407 p.advance(&mut bus, &mut regs, &mut pc);
5408 }
5409 assert_eq!(
5410 regs.read(3),
5411 crate::alu::sext32(0xABCD_EF01),
5412 "the even page of pair 0 maps to physical 0x100 via the TLB"
5413 );
5414 }
5415
5416 /// A store to a page whose `D` bit is clear raises **Modified**, which takes
5417 /// the general vector — an entry was found, so there is nothing to refill.
5418 #[test]
5419 fn a_store_to_a_clean_page_raises_modified_at_the_general_vector() {
5420 use crate::cop0::reg;
5421 let mut p = Pipeline::new();
5422 let mut regs = Regs::new();
5423 let mut bus = Ram::new(alloc::vec![ld_st(0o53, 0, 0, 0x1100)]);
5424 // V | C=3 | G but NOT D -- readable, not writable.
5425 p.cop0.set_hardware(reg::PAGE_MASK, 0);
5426 p.cop0.set_hardware(reg::ENTRY_HI, 0x1000);
5427 p.cop0.set_hardware(reg::ENTRY_LO0, (3 << 3) | 0b011);
5428 p.cop0
5429 .set_hardware(reg::ENTRY_LO1, (1 << 6) | (3 << 3) | 0b011);
5430 p.tlb.write_entry(0, &p.cop0);
5431 p.cop0.set_hardware(reg::STATUS, 0);
5432 let mut pc = KSEG0_PROG;
5433
5434 let mut cycles = 0;
5435 while p.stalled_by() != Some(Interlock::Exception) {
5436 p.advance(&mut bus, &mut regs, &mut pc);
5437 cycles += 1;
5438 assert!(cycles < 16, "no exception raised");
5439 }
5440 assert_eq!(
5441 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
5442 crate::exception::exc_code::MOD
5443 );
5444 assert_eq!(
5445 pc, 0xFFFF_FFFF_8000_0180,
5446 "Modified takes the GENERAL vector"
5447 );
5448 }
5449
5450 /// A TLB exception fills `EntryHi`, `Context` and `XContext` — the refill
5451 /// handler reads `Context` as a ready-made page-table pointer, which is why
5452 /// hardware assembles it rather than leaving it to software.
5453 #[test]
5454 fn a_tlb_exception_assembles_entryhi_and_context() {
5455 use crate::cop0::reg;
5456 let mut p = Pipeline::new();
5457 let mut regs = Regs::new();
5458 let mut bus = Ram::new(alloc::vec![ld_st(0o43, 0, 3, 0x4000)]);
5459 p.cop0.set_hardware(reg::STATUS, 0);
5460 // A page-table base the handler would have set up.
5461 p.cop0.set_hardware(reg::CONTEXT, 0xFFFF_FFFF_8080_0000);
5462 let mut pc = KSEG0_PROG;
5463
5464 let mut cycles = 0;
5465 while p.stalled_by() != Some(Interlock::Exception) {
5466 p.advance(&mut bus, &mut regs, &mut pc);
5467 cycles += 1;
5468 assert!(cycles < 16, "no exception raised");
5469 }
5470 assert_eq!(
5471 p.cop0.read(reg::ENTRY_HI) & 0xFFFF_E000,
5472 0x4000,
5473 "EntryHi holds the faulting VPN2"
5474 );
5475 assert_eq!(
5476 p.cop0.read(reg::CONTEXT) & 0xFFFF_FFFF_FF80_0000,
5477 0xFFFF_FFFF_8080_0000,
5478 "PTEBase is preserved"
5479 );
5480 assert_eq!(
5481 (p.cop0.read(reg::CONTEXT) >> 4) & 0x7_FFFF,
5482 0x4000 >> 13,
5483 "BadVPN2 is filled in"
5484 );
5485 }
5486
5487 /// `TLBWI` writes the entry `Index` names; `TLBWR` writes the one `Random`
5488 /// names. Using the wrong register is a silent, hard-to-see swap.
5489 #[test]
5490 fn tlbwi_uses_index_and_tlbwr_uses_random() {
5491 use crate::cop0::reg;
5492 const TLBWI: u32 = (0o20 << 26) | (0o20 << 21) | 0o02;
5493 const TLBWR: u32 = (0o20 << 26) | (0o20 << 21) | 0o06;
5494
5495 let mut p = Pipeline::new();
5496 let mut regs = Regs::new();
5497 let mut bus = Ram::new(alloc::vec![TLBWI]);
5498 p.cop0.set_hardware(reg::STATUS, 0);
5499 p.cop0.set_hardware(reg::ENTRY_HI, 0x2000);
5500 p.cop0
5501 .set_hardware(reg::ENTRY_LO0, (7 << 6) | (3 << 3) | 0b111);
5502 p.cop0
5503 .set_hardware(reg::ENTRY_LO1, (8 << 6) | (3 << 3) | 0b111);
5504 p.cop0.set_hardware(reg::INDEX, 5);
5505 let mut pc = KSEG0_PROG;
5506 for _ in 0..16 {
5507 p.advance(&mut bus, &mut regs, &mut pc);
5508 }
5509 assert_eq!(p.tlb.entry(5).lo0.pfn, 7, "TLBWI wrote entry Index = 5");
5510
5511 // TLBWR with Random forced to a different index.
5512 let mut p = Pipeline::new();
5513 let mut regs = Regs::new();
5514 let mut bus = Ram::new(alloc::vec![TLBWR]);
5515 p.cop0.set_hardware(reg::STATUS, 0);
5516 p.cop0.set_hardware(reg::ENTRY_HI, 0x2000);
5517 p.cop0
5518 .set_hardware(reg::ENTRY_LO0, (9 << 6) | (3 << 3) | 0b111);
5519 p.cop0
5520 .set_hardware(reg::ENTRY_LO1, (10 << 6) | (3 << 3) | 0b111);
5521 p.cop0.set_hardware(reg::INDEX, 5);
5522 p.cop0.set_hardware(reg::RANDOM, 20);
5523 let mut pc = KSEG0_PROG;
5524 for _ in 0..16 {
5525 p.advance(&mut bus, &mut regs, &mut pc);
5526 }
5527 assert_eq!(p.tlb.entry(20).lo0.pfn, 9, "TLBWR wrote entry Random = 20");
5528 assert_ne!(p.tlb.entry(5).lo0.pfn, 9, "and NOT entry Index");
5529 }
5530
5531 /// `TLBWR` cannot reach a wired entry, because `Random` never goes below
5532 /// `Wired` — but **`TLBWI` can** (UM §5.4.4, p. 150). Guarding both is a
5533 /// natural-looking mistake that makes wired entries unwritable at all.
5534 #[test]
5535 fn tlbwi_can_overwrite_a_wired_entry_even_though_tlbwr_cannot() {
5536 use crate::cop0::reg;
5537 const TLBWI: u32 = (0o20 << 26) | (0o20 << 21) | 0o02;
5538 let mut p = Pipeline::new();
5539 let mut regs = Regs::new();
5540 let mut bus = Ram::new(alloc::vec![TLBWI]);
5541 p.cop0.set_hardware(reg::STATUS, 0);
5542 p.cop0.mtc0(reg::WIRED, 8);
5543 p.cop0.set_hardware(reg::ENTRY_HI, 0x2000);
5544 p.cop0
5545 .set_hardware(reg::ENTRY_LO0, (11 << 6) | (3 << 3) | 0b111);
5546 p.cop0
5547 .set_hardware(reg::ENTRY_LO1, (12 << 6) | (3 << 3) | 0b111);
5548 p.cop0.set_hardware(reg::INDEX, 3); // inside the wired range
5549 let mut pc = KSEG0_PROG;
5550 for _ in 0..16 {
5551 p.advance(&mut bus, &mut regs, &mut pc);
5552 }
5553 assert_eq!(
5554 p.tlb.entry(3).lo0.pfn,
5555 11,
5556 "TLBWI must be able to write a wired entry"
5557 );
5558
5559 // And Random's range protects those entries from TLBWR structurally,
5560 // rather than by a check.
5561 for _ in 0..200 {
5562 p.cop0.tick_random();
5563 assert!(
5564 p.cop0.read(reg::RANDOM) >= 8,
5565 "Random must never select a wired entry"
5566 );
5567 }
5568 }
5569
5570 /// A TLB fault on a sign-extended kernel address must record the **`R`**
5571 /// region in `EntryHi`, not just `VPN2`.
5572 ///
5573 /// Leaving `R` zero puts every such fault in region 0, so the handler's
5574 /// `TLBWR` installs an entry that can never match the address that faulted —
5575 /// an infinite refill loop, not a visibly wrong value.
5576 #[test]
5577 fn a_fault_on_a_sign_extended_address_records_its_region_in_entryhi() {
5578 use crate::cop0::reg;
5579 let mut p = Pipeline::new();
5580 let mut regs = Regs::new();
5581 // LW $3, 0($1) with $1 = 0xFFFF_FFFF_E000_0000 (KSEG3, mapped).
5582 let mut bus = Ram::new(alloc::vec![
5583 (0o17 << 26) | (1 << 16) | 0xE000, // LUI $1, 0xE000
5584 ld_st(0o43, 1, 3, 0),
5585 ]);
5586 p.cop0.set_hardware(reg::STATUS, 0);
5587 let mut pc = KSEG0_PROG;
5588
5589 let mut cycles = 0;
5590 while p.stalled_by() != Some(Interlock::Exception) {
5591 p.advance(&mut bus, &mut regs, &mut pc);
5592 cycles += 1;
5593 assert!(cycles < 20, "no TLB exception raised");
5594 }
5595 assert_eq!(
5596 p.cop0.read(reg::BAD_VADDR),
5597 0xFFFF_FFFF_E000_0000,
5598 "the full sign-extended address faulted"
5599 );
5600 assert_eq!(
5601 (p.cop0.read(reg::ENTRY_HI) >> 62) & 0b11,
5602 0b11,
5603 "EntryHi.R must carry the faulting region, not 0"
5604 );
5605 assert_eq!(
5606 p.cop0.read(reg::ENTRY_HI) & crate::tlb::VPN2_MASK,
5607 0xFFFF_FFFF_E000_0000 & crate::tlb::VPN2_MASK,
5608 "and VPN2 alongside it"
5609 );
5610 }
5611
5612 /// **`Random` advances as instructions retire** (UM §5.4.2, p. 147).
5613 ///
5614 /// It was implemented and never called from the pipeline, so it sat at 31
5615 /// forever and every `TLBWR` overwrote the same entry. A stuck counter is
5616 /// invisible to any test that calls `tick_random` itself — which is what the
5617 /// COP0 unit tests do — so this asserts it through `advance`.
5618 #[test]
5619 fn random_advances_as_instructions_retire() {
5620 use crate::cop0::reg;
5621 let mut p = Pipeline::new();
5622 let mut regs = Regs::new();
5623 let mut bus = Ram::new(alloc::vec![
5624 addiu_zero(1, 1),
5625 addiu_zero(2, 2),
5626 addiu_zero(3, 3),
5627 addiu_zero(4, 4),
5628 ]);
5629 p.cop0.set_hardware(reg::STATUS, 0);
5630 p.cop0.mtc0(reg::WIRED, 0);
5631 let start = p.cop0.read(reg::RANDOM);
5632 let mut pc = KSEG0_PROG;
5633 for _ in 0..24 {
5634 p.advance(&mut bus, &mut regs, &mut pc);
5635 }
5636 assert!(p.retired >= 4, "instructions retired");
5637 assert_ne!(
5638 p.cop0.read(reg::RANDOM),
5639 start,
5640 "Random must move as instructions retire -- a stuck Random makes \
5641 every TLBWR overwrite the same entry"
5642 );
5643 }
5644
5645 /// TLB shutdown must reach **`Status.TS`**, not just an internal flag —
5646 /// software polls `TS` precisely to discover that the TLB has died.
5647 #[test]
5648 fn tlb_shutdown_sets_status_ts() {
5649 use crate::cop0::reg;
5650 let mut p = Pipeline::new();
5651 let mut regs = Regs::new();
5652 let mut bus = Ram::new(alloc::vec![ld_st(0o43, 0, 3, 0x100)]);
5653 p.cop0.set_hardware(reg::STATUS, 0);
5654 // Two coinciding entries.
5655 map(&mut p, 0, 0x0000, 0);
5656 map(&mut p, 7, 0x0000, 4);
5657 assert_eq!(p.cop0.read(reg::STATUS) & (1 << 21), 0, "TS clear so far");
5658
5659 let mut pc = KSEG0_PROG;
5660 for _ in 0..24 {
5661 p.advance(&mut bus, &mut regs, &mut pc);
5662 }
5663 assert!(p.tlb.is_shutdown(), "the duplicate was noticed");
5664 assert_ne!(
5665 p.cop0.read(reg::STATUS) & (1 << 21),
5666 0,
5667 "Status.TS must be set (UM Fig. 6-6) -- an internal flag nobody can \
5668 read is worse than not tracking it"
5669 );
5670 }
5671
5672 /// The 3-PCycle micro-ITLB reload is charged **only when a reload happens**
5673 /// (UM §4.6.2). A fetch that misses both levels goes straight to its
5674 /// exception rather than paying for a reload that never occurred.
5675 ///
5676 /// **This test cannot currently observe the charge itself.** `stall_for`
5677 /// replaces any pending stall, so the exception's 2-PCycle stall supersedes
5678 /// a wrongly-charged 3-PCycle reload in the same cycle — mutating the guard
5679 /// away produces no behavioral difference today, which mutation testing
5680 /// duly reported. The guard is kept because it is what the manual says and
5681 /// because it becomes observable the moment stalls compose rather than
5682 /// replace; what is asserted here is the *decision*
5683 /// ([`Tlb::jtlb_has_match`]) and the absence of a reload, not the timing.
5684 #[test]
5685 fn a_fetch_missing_both_tlb_levels_is_not_charged_for_a_reload() {
5686 use crate::cop0::reg;
5687 let mut p = Pipeline::new();
5688 let mut regs = Regs::new();
5689 let mut bus = Ram::new(alloc::vec![]);
5690 p.cop0.set_hardware(reg::STATUS, 0);
5691 // Fetch from an unmapped KUSEG address: misses ITLB and JTLB alike.
5692 let mut pc = 0x0000_4000u64;
5693
5694 assert!(
5695 !p.tlb.jtlb_has_match(pc, 0),
5696 "the JTLB has nothing to reload the micro-TLB from"
5697 );
5698 p.advance(&mut bus, &mut regs, &mut pc);
5699 assert_eq!(
5700 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
5701 crate::exception::exc_code::TLBL,
5702 "it went straight to the refill exception"
5703 );
5704 }
5705
5706 // --- COP1 control and coprocessor usability (T-12-006) -----------------
5707
5708 /// The exact instruction n64-systemtest dies on: `CTC1 $rt, $31`, its fourth
5709 /// statement. If this does not work the suite reports nothing at all, and
5710 /// every COP0/TLB test in Sprint 2 is unreachable behind it.
5711 #[test]
5712 fn ctc1_to_fcr31_works_which_is_what_unblocks_the_oracle() {
5713 use crate::cop0::reg;
5714 // CTC1: opcode 0o21, rs = 0o06, rt = GPR, rd = fs.
5715 const fn ctc1(rt: u32, fs: u32) -> u32 {
5716 (0o21 << 26) | (0o06 << 21) | (rt << 16) | (fs << 11)
5717 }
5718 const fn cfc1(rt: u32, fs: u32) -> u32 {
5719 (0o21 << 26) | (0o02 << 21) | (rt << 16) | (fs << 11)
5720 }
5721 // LUI $1, 0x0100 ; bit 24 -- flush_denorm_to_zero
5722 // ORI $1, $1, 0x800 ; bit 11 -- enable_invalid_operation
5723 // CTC1 $1, $31
5724 // CFC1 $2, $31
5725 let prog = alloc::vec![
5726 (0o17 << 26) | (1 << 16) | 0x0100,
5727 (0o15 << 26) | (1 << 21) | (1 << 16) | 0x0800,
5728 ctc1(1, 31),
5729 cfc1(2, 31),
5730 ];
5731 let mut bus = Ram::new(prog);
5732 let mut regs = Regs::new();
5733 let mut p = Pipeline::new();
5734 // CU1 enabled, as IPL3 leaves it (Status = 0x3400_0000).
5735 p.cop0.set_hardware(reg::STATUS, 0x3400_0000);
5736 let mut pc = KSEG0_PROG;
5737 for _ in 0..40 {
5738 p.advance(&mut bus, &mut regs, &mut pc);
5739 }
5740
5741 let want = (1u64 << 24) | (1 << 11);
5742 assert_eq!(u64::from(p.cop1.fcsr()), want, "CTC1 reached FCR31");
5743 assert_eq!(regs.read(2), want, "and CFC1 read it back");
5744 assert!(p.cop1.flush_denorm_to_zero());
5745 }
5746
5747 /// With `CU1` clear, a COP1 instruction raises **Coprocessor Unusable** with
5748 /// `Cause.CE = 1` — not Reserved Instruction, which is the natural mistake
5749 /// for an unimplemented encoding.
5750 #[test]
5751 fn a_cop1_instruction_with_cu1_clear_raises_coprocessor_unusable() {
5752 use crate::cop0::reg;
5753 const CTC1: u32 = (0o21 << 26) | (0o06 << 21) | (1 << 16) | (31 << 11);
5754 let mut bus = Ram::new(alloc::vec![CTC1]);
5755 let mut regs = Regs::new();
5756 let mut p = Pipeline::new();
5757 // Kernel mode, but CU1 CLEAR.
5758 p.cop0.set_hardware(reg::STATUS, 0);
5759 let mut pc = KSEG0_PROG;
5760
5761 let mut cycles = 0;
5762 while p.stalled_by() != Some(Interlock::Exception) {
5763 p.advance(&mut bus, &mut regs, &mut pc);
5764 cycles += 1;
5765 assert!(cycles < 16, "no exception raised");
5766 }
5767 assert_eq!(
5768 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
5769 crate::exception::exc_code::CPU,
5770 "Coprocessor Unusable, not Reserved Instruction"
5771 );
5772 assert_eq!(
5773 (p.cop0.read(reg::CAUSE) >> 28) & 0b11,
5774 1,
5775 "Cause.CE names the offending unit"
5776 );
5777 assert_eq!(p.cop1.fcsr(), 0, "and the write did not take effect");
5778 }
5779
5780 /// **COP0 is usable from kernel mode regardless of `CU0`.** Otherwise the CPU
5781 /// could not run an exception handler before `Status` had been set up — a
5782 /// chicken-and-egg the hardware does not have.
5783 #[test]
5784 fn cop0_is_usable_in_kernel_mode_even_with_cu0_clear() {
5785 use crate::cop0::reg;
5786 const MTC0: u32 = (0o20 << 26) | (0o04 << 21) | (1 << 16) | ((reg::COMPARE as u32) << 11);
5787 let mut bus = Ram::new(alloc::vec![addiu_zero(1, 0x77), MTC0]);
5788 let mut regs = Regs::new();
5789 let mut p = Pipeline::new();
5790 // KSU = 0 (kernel), CU0 clear, EXL/ERL clear.
5791 p.cop0.set_hardware(reg::STATUS, 0);
5792 let mut pc = KSEG0_PROG;
5793 for _ in 0..24 {
5794 p.advance(&mut bus, &mut regs, &mut pc);
5795 }
5796 assert_eq!(
5797 p.cop0.read(reg::COMPARE),
5798 0x77,
5799 "MTC0 must work in kernel mode without CU0"
5800 );
5801 assert_eq!(
5802 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
5803 0,
5804 "and must not have raised"
5805 );
5806 }
5807
5808 /// In **user** mode with `CU0` clear, COP0 *is* unusable — otherwise the
5809 /// kernel-mode exemption would be a blanket bypass rather than a rule.
5810 #[test]
5811 fn cop0_is_unusable_in_user_mode_without_cu0() {
5812 use crate::cop0::reg;
5813 const MTC0: u32 = (0o20 << 26) | (0o04 << 21) | (1 << 16) | ((reg::COMPARE as u32) << 11);
5814 let mut bus = Ram::new(alloc::vec![MTC0]);
5815 let mut regs = Regs::new();
5816 let mut p = Pipeline::new();
5817 // KSU = 2 (user), CU0 clear, EXL/ERL clear.
5818 p.cop0.set_hardware(reg::STATUS, 0b10 << 3);
5819 // The program cannot live in KSEG0 here: that segment does not exist in
5820 // User mode, so the FETCH would raise AdEL and the test would pass on
5821 // the wrong exception. Map page-pair 0 identically instead and run from
5822 // USEG, which is the only place a user program can be.
5823 map(&mut p, 0, 0, 0);
5824 let mut pc = KSEG0_PROG & 0x1FFF_FFFF;
5825
5826 let mut cycles = 0;
5827 while p.stalled_by() != Some(Interlock::Exception) {
5828 p.advance(&mut bus, &mut regs, &mut pc);
5829 cycles += 1;
5830 assert!(cycles < 16, "no exception raised");
5831 }
5832 assert_eq!(
5833 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
5834 crate::exception::exc_code::CPU
5835 );
5836 assert_eq!((p.cop0.read(reg::CAUSE) >> 28) & 0b11, 0, "unit 0");
5837 }
5838
5839 /// A 64-bit operation is Reserved in 32-bit User or Supervisor mode, and
5840 /// usable everywhere else.
5841 ///
5842 /// All four rows are needed. Gating on the width bit alone reserves them for
5843 /// a 32-bit *kernel* — which is the mode every N64 boots into, so that
5844 /// mistake breaks everything and would be caught. Gating on the mode alone
5845 /// reserves them for a 64-bit *user* program, which nothing common does, so
5846 /// that mistake would sit unnoticed behind the rows that do pass.
5847 #[test]
5848 fn a_64_bit_operation_is_reserved_only_in_32_bit_non_kernel_mode() {
5849 use crate::cop0::reg;
5850 /// `DADD $2, $1, $1` — SPECIAL funct 0o54.
5851 const DADD: u32 = (1 << 21) | (1 << 16) | (2 << 11) | 0o54;
5852 /// `Status.KSU` = user, and `Status.UX`.
5853 const USER: u64 = 0b10 << 3;
5854 const UX: u64 = 1 << 5;
5855
5856 for (status, want_reserved, why) in [
5857 (0, false, "32-bit kernel: usable"),
5858 (1 << 7, false, "64-bit kernel: usable"),
5859 (USER, true, "32-bit user: reserved"),
5860 (USER | UX, false, "64-bit user: usable"),
5861 ] {
5862 let mut bus = Ram::new(alloc::vec![DADD]);
5863 let mut regs = Regs::new();
5864 let mut p = Pipeline::new();
5865 p.cop0.set_hardware(reg::STATUS, status);
5866 // User mode cannot fetch from KSEG0, so the program runs from USEG
5867 // through an identity mapping in both cases -- keeping the only
5868 // difference between the rows the one under test.
5869 map(&mut p, 0, 0, 0);
5870 let mut pc = KSEG0_PROG & 0x1FFF_FFFF;
5871
5872 let mut raised = false;
5873 for _ in 0..16 {
5874 p.advance(&mut bus, &mut regs, &mut pc);
5875 if p.stalled_by() == Some(Interlock::Exception) {
5876 raised = true;
5877 break;
5878 }
5879 }
5880 assert_eq!(raised, want_reserved, "{why}");
5881 if raised {
5882 assert_eq!(
5883 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
5884 crate::exception::exc_code::RI,
5885 "{why}: and it is Reserved Instruction, not something else"
5886 );
5887 }
5888 }
5889 }
5890
5891 /// A `BC1` immediately after a `C.cond.fmt` sees that compare's result.
5892 ///
5893 /// `BC1` resolves in `EX`; the compare commits `FCSR.C` in `WB`. Without a
5894 /// forwarding path the branch samples the **previous** condition, falls
5895 /// through, and nothing anywhere reports an error — the ROM emits exactly
5896 /// this pair with no separating instruction.
5897 ///
5898 /// Both directions are asserted. Testing only the taken case would pass
5899 /// against a bypass that returned `true` unconditionally.
5900 #[test]
5901 fn a_bc1_immediately_after_a_compare_sees_its_result() {
5902 use crate::cop0::reg;
5903 /// `C.EQ.S $f0, $f2` — fmt S, ft = 2, fs = 0, cond = EQ.
5904 const C_EQ: u32 = (0o21 << 26) | (0o20 << 21) | (2 << 16) | 0o62;
5905 /// `BC1T +2` — skips the `ORI` two instructions ahead.
5906 const BC1T: u32 = (0o21 << 26) | (0o10 << 21) | (1 << 16) | 2;
5907 /// `ORI $8, $0, 1` — runs only if the branch was NOT taken.
5908 const ORI: u32 = (0o15 << 26) | (8 << 16) | 1;
5909
5910 for (lo0, equal) in [(0x2222_3333u64, true), (0x4444_5555, false)] {
5911 let mut bus = Ram::new(alloc::vec![C_EQ, BC1T, 0, ORI, 0, 0, 0, 0, 0, 0]);
5912 let mut regs = Regs::new();
5913 let mut p = Pipeline::new();
5914 p.cop0.set_hardware(reg::STATUS, 1 << 29); // CU1
5915 // Upper halves differ deliberately: a single-precision compare must
5916 // ignore them, so a 64-bit comparison would report "not equal" for
5917 // the row that is supposed to match.
5918 p.fpr.write_d(0, true, lo0);
5919 p.fpr.write_d(2, true, 0x1111_1111_2222_3333);
5920 let mut pc = KSEG0_PROG;
5921 for _ in 0..14 {
5922 p.advance(&mut bus, &mut regs, &mut pc);
5923 }
5924 assert_eq!(
5925 regs.read(8) == 0,
5926 equal,
5927 "operands equal = {equal}: the branch must {} the ORI",
5928 if equal { "skip" } else { "run" }
5929 );
5930 }
5931 }
5932
5933 /// An unimplemented COP1 encoding with `CU1` **set** must not raise. Sprint 3
5934 /// then *adds* behavior rather than changing it — and an emulator that
5935 /// raised here would look correct until the FPU landed.
5936 #[test]
5937 fn an_unimplemented_cop1_encoding_does_not_raise_when_cu1_is_set() {
5938 use crate::cop0::reg;
5939 // COP1 `rs = 0o11` -- an unassigned encoding in the coprocessor's own
5940 // opcode space, so it is valid to *fetch* and does nothing.
5941 //
5942 // This test has now outlived three subjects: `ADD.S` until the S/D
5943 // arithmetic landed, `SQRT.S` until T-13-005, then `BC1F` until the
5944 // branch was wired. Each move is the test doing its job -- the point is
5945 // the *unimplemented* path, so the subject follows whatever is still on
5946 // it. When `rs = 0o11` acquires a meaning, move it again rather than
5947 // deleting it.
5948 const UNASSIGNED: u32 = (0o21 << 26) | (0o11 << 21);
5949 assert_eq!(
5950 decode(UNASSIGNED).op,
5951 crate::decode::Op::Cop1Unimplemented,
5952 "valid encoding, not Reserved"
5953 );
5954 let mut bus = Ram::new(alloc::vec![UNASSIGNED]);
5955 let mut regs = Regs::new();
5956 let mut p = Pipeline::new();
5957 p.cop0.set_hardware(reg::STATUS, 0x3400_0000); // CU1 set
5958 let mut pc = KSEG0_PROG;
5959 for _ in 0..16 {
5960 p.advance(&mut bus, &mut regs, &mut pc);
5961 }
5962 assert_eq!(
5963 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
5964 0,
5965 "no exception with CU1 set"
5966 );
5967 }
5968
5969 /// **`MOV.S` must actually move.** Decoding it is not enough — the failure
5970 /// this pins is a *silent no-op*, which is invisible to every test that
5971 /// only checks for an absent exception.
5972 ///
5973 /// The encoding is the one the correlated capture found in the delay slot
5974 /// of the failing n64-systemtest thunk's `jr $ra` (ledger C-10): with the
5975 /// move doing nothing, the callee's result never reached its caller and
5976 /// ~250 FP results were reported against a register the instruction under
5977 /// test never wrote.
5978 ///
5979 /// The destination is seeded with a value that differs from the source in
5980 /// **both halves**, so neither a no-op nor a half-width copy passes.
5981 #[test]
5982 fn mov_s_copies_the_low_word_and_a_no_op_would_fail_here() {
5983 use crate::cop0::reg;
5984 /// `MOV.S $f0, $f4` — fmt 16, fs 4, fd 0, funct 6.
5985 const MOV_S: u32 = 0x4600_2006;
5986
5987 let mut bus = Ram::new(alloc::vec![MOV_S]);
5988 let mut regs = Regs::new();
5989 let mut p = Pipeline::new();
5990 p.cop0.set_hardware(reg::STATUS, 0x3400_0000); // CU1 | FR
5991 p.fpr.write_raw(4, 0x0011_0011_4000_0000); // source: 2.0f
5992 p.fpr.write_raw(0, 0xDEAD_BEEF_1122_3344); // destination: junk
5993
5994 let mut pc = KSEG0_PROG;
5995 for _ in 0..16 {
5996 p.advance(&mut bus, &mut regs, &mut pc);
5997 }
5998
5999 // The WHOLE register, not just the low word. `MOV.S` is a 64-bit bit
6000 // move (C-10), so checking only the low half passes against a formatted
6001 // half-copy -- which is what the destination's junk upper word is here
6002 // to catch.
6003 assert_eq!(
6004 p.fpr.read_raw(0),
6005 0x0011_0011_4000_0000,
6006 "MOV.S copies fs entire, upper half included"
6007 );
6008 assert_eq!(
6009 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
6010 0,
6011 "MOV.S raises nothing"
6012 );
6013 }
6014
6015 /// **The documented FPU rates are actually charged.**
6016 ///
6017 /// `fpu::delay_cycles` transcribes UM Table 7-14 and its own test asserts
6018 /// the numbers, but a table nothing consults is inert — the failure mode
6019 /// here is a correct table that is simply never called. So this drives real
6020 /// instructions and reads the interlock back.
6021 ///
6022 /// The two cases are chosen to differ: `MUL.D` is 8 cycles and `MOV.S` is 1,
6023 /// i.e. no stall at all. A blanket stall on every COP1 op would pass a
6024 /// `MUL.D`-only test and fail this one.
6025 #[test]
6026 fn the_documented_fpu_rates_are_charged_as_stalls() {
6027 use crate::cop0::reg;
6028 /// fmt in bits 25:21, fs 4, ft 2, fd 0.
6029 const fn fp(fmt: u32, funct: u32) -> u32 {
6030 (0o21 << 26) | (fmt << 21) | (2 << 16) | (4 << 11) | funct
6031 }
6032 // MUL.D (fmt 17, funct 2) = 8 PCycles; MOV.S (fmt 16, funct 6) = 1, so
6033 // it must add nothing.
6034 for (word, want) in [(fp(0o21, 2), Some(Interlock::Mci)), (fp(0o20, 6), None)] {
6035 let mut bus = Ram::new(alloc::vec![word]);
6036 let mut regs = Regs::new();
6037 let mut p = Pipeline::new();
6038 p.cop0.set_hardware(reg::STATUS, 0x3400_0000);
6039 p.fpr.write_d(4, true, 2.0f64.to_bits());
6040 p.fpr.write_d(2, true, 3.0f64.to_bits());
6041 let mut pc = KSEG0_PROG;
6042
6043 // Run until the instruction reaches WB, where COP1 arithmetic
6044 // executes and the delay is charged.
6045 let mut seen = None;
6046 for _ in 0..8 {
6047 p.advance(&mut bus, &mut regs, &mut pc);
6048 if p.stalled_by() == Some(Interlock::Mci) {
6049 seen = Some(Interlock::Mci);
6050 break;
6051 }
6052 }
6053 assert_eq!(
6054 seen, want,
6055 "word {word:#010x}: a multi-cycle FPU op must stall and a \
6056 single-cycle one must not"
6057 );
6058 }
6059 }
6060
6061 /// `NEG.S` and `ABS.S` share the arm `MOV.S` was missing from, and are just
6062 /// as silent when absent: both leave a plausible-looking value behind.
6063 #[test]
6064 fn neg_s_and_abs_s_execute_rather_than_no_op() {
6065 use crate::cop0::reg;
6066 /// fmt 16, fs 4, fd 0.
6067 const fn fp(funct: u32) -> u32 {
6068 (0o21 << 26) | (0o20 << 21) | (4 << 11) | funct
6069 }
6070
6071 for (funct, input, want) in [
6072 (7u32, 0x4000_0000u32, 0xC000_0000u32), // NEG.S: 2.0 -> -2.0
6073 (5, 0xC000_0000, 0x4000_0000), // ABS.S: -2.0 -> 2.0
6074 ] {
6075 let mut bus = Ram::new(alloc::vec![fp(funct)]);
6076 let mut regs = Regs::new();
6077 let mut p = Pipeline::new();
6078 p.cop0.set_hardware(reg::STATUS, 0x3400_0000);
6079 p.fpr.write_s(4, true, input);
6080 p.fpr.write_s(0, true, 0x1122_3344);
6081 let mut pc = KSEG0_PROG;
6082 for _ in 0..16 {
6083 p.advance(&mut bus, &mut regs, &mut pc);
6084 }
6085 assert_eq!(p.fpr.read_s(0, true), want, "funct {funct} did not execute");
6086 }
6087 }
6088
6089 // --- Enabled FP traps (T-13-002) ----------------------------------------
6090
6091 /// `ADD.S $f4, $f0, $f2` — the encoding the FP-trap tests drive.
6092 const ADD_S_F4_F0_F2: u32 = 0x4602_0100;
6093 /// `FCSR.Enable` for Invalid Operation (bit 11).
6094 const ENABLE_INVALID: u32 = 1 << 11;
6095 /// `FCSR.Cause` for Invalid Operation (bit 16).
6096 const CAUSE_INVALID: u32 = 1 << 16;
6097 /// `FCSR.Flags` (sticky) for Invalid Operation (bit 6).
6098 const FLAG_INVALID: u32 = 1 << 6;
6099
6100 /// Run `ADD.S $f4, $f0, $f2` on `inf + (-inf)` — an Invalid Operation —
6101 /// with `FCSR` preloaded, and report what the machine ended up in.
6102 fn run_invalid_add_s(fcsr: u32) -> (Pipeline, u64) {
6103 use crate::cop0::reg;
6104 let mut bus = Ram::new(alloc::vec![ADD_S_F4_F0_F2]);
6105 let mut regs = Regs::new();
6106 let mut p = Pipeline::new();
6107 p.cop0.set_hardware(reg::STATUS, 0x3400_0000); // CU1 | FR
6108 p.cop1.ctc1(31, fcsr);
6109 p.fpr.write_s(0, true, 0x7F80_0000); // +inf
6110 p.fpr.write_s(2, true, 0xFF80_0000); // -inf
6111 p.fpr.write_raw(4, 0x1122_3344_5566_7788); // untouched-if-trapped marker
6112 let mut pc = KSEG0_PROG;
6113 for _ in 0..24 {
6114 p.advance(&mut bus, &mut regs, &mut pc);
6115 }
6116 let code = (p.cop0.read(reg::CAUSE) >> 2) & 0x1F;
6117 (p, code)
6118 }
6119
6120 /// With the Invalid enable **clear**, the operation completes: `fd` is
6121 /// written, both `Cause` and the sticky `Flags` record it, and nothing
6122 /// raises. This is the control for the trap test below — without it, a
6123 /// pipeline that raised on *every* invalid operation would pass that test.
6124 #[test]
6125 fn a_masked_fp_condition_completes_and_sets_both_cause_and_flags() {
6126 let (p, code) = run_invalid_add_s(0);
6127 assert_eq!(code, 0, "masked: no exception");
6128 assert_ne!(
6129 p.fpr.read_s(4, true),
6130 0x5566_7788,
6131 "fd must be written when no trap is taken"
6132 );
6133 let fcsr = p.cop1.fcsr();
6134 assert_ne!(fcsr & CAUSE_INVALID, 0, "Cause.V set");
6135 assert_ne!(fcsr & FLAG_INVALID, 0, "sticky Flags.V set");
6136 }
6137
6138 /// With the enable **set**, the same operation traps: `ExcCode` is 15, `fd`
6139 /// keeps its old value, and `Cause` records the condition.
6140 #[test]
6141 fn an_enabled_fp_condition_raises_and_leaves_the_destination_alone() {
6142 use crate::cop0::reg;
6143 let (p, code) = run_invalid_add_s(ENABLE_INVALID);
6144 assert_eq!(code, crate::exception::exc_code::FPE, "ExcCode 15 (FPE)");
6145 assert_eq!(
6146 p.fpr.read_raw(4),
6147 0x1122_3344_5566_7788,
6148 "a trapped operation must not write fd"
6149 );
6150 assert_ne!(p.cop1.fcsr() & CAUSE_INVALID, 0, "Cause.V set");
6151 assert_eq!(
6152 (p.cop0.read(reg::CAUSE) >> 28) & 0b11,
6153 0,
6154 "Cause.CE is 0 for an FP exception, not the coprocessor number"
6155 );
6156 }
6157
6158 /// **The sticky `Flags` field is NOT updated on a trap** — only `Cause` is.
6159 ///
6160 /// Split from the test above deliberately. Both come from the same
6161 /// `Flags::to_fcsr_bits` value, so writing the whole thing on the trap path
6162 /// is the natural implementation and is wrong; it passes every assertion
6163 /// about the exception itself and only shows up when `FCSR` is read back,
6164 /// which is exactly what n64-systemtest does.
6165 #[test]
6166 fn a_trapped_operation_does_not_accumulate_into_the_sticky_flags() {
6167 let (p, _) = run_invalid_add_s(ENABLE_INVALID);
6168 assert_eq!(
6169 p.cop1.fcsr() & FLAG_INVALID,
6170 0,
6171 "Flags must be left alone when the trap is taken"
6172 );
6173 }
6174
6175 /// A trapped FP operation does not retire, so it must not tick `Random`.
6176 ///
6177 /// `Random` is decremented in the retirement tail of `WB`, which is also
6178 /// where the FP write-back happens — so an implementation that raises the
6179 /// exception but falls through keeps counting an instruction that never
6180 /// completed.
6181 ///
6182 /// **Asserted on the trap cycle specifically.** Comparing total `retired`
6183 /// between a trapping and a non-trapping run was tried first and proved
6184 /// nothing: the trap flushes the pipe and redirects `PC`, so the totals
6185 /// differ over a fixed cycle budget whether or not the trapping instruction
6186 /// itself retired. That version passed with the fix removed.
6187 #[test]
6188 fn a_trapped_fp_operation_does_not_retire() {
6189 use crate::cop0::reg;
6190 let mut bus = Ram::new(alloc::vec![ADD_S_F4_F0_F2]);
6191 let mut regs = Regs::new();
6192 let mut p = Pipeline::new();
6193 p.cop0.set_hardware(reg::STATUS, 0x3400_0000);
6194 p.cop1.ctc1(31, ENABLE_INVALID);
6195 p.fpr.write_s(0, true, 0x7F80_0000);
6196 p.fpr.write_s(2, true, 0xFF80_0000);
6197
6198 let mut pc = KSEG0_PROG;
6199 let mut saw_trap = false;
6200 for _ in 0..24 {
6201 let retired_before = p.retired;
6202 let random_before = p.cop0.read(reg::RANDOM);
6203 p.advance(&mut bus, &mut regs, &mut pc);
6204 let code = (p.cop0.read(reg::CAUSE) >> 2) & 0x1F;
6205 if !saw_trap && code == crate::exception::exc_code::FPE {
6206 saw_trap = true;
6207 assert_eq!(
6208 p.retired, retired_before,
6209 "the trapping instruction must not retire on the trap cycle"
6210 );
6211 assert_eq!(
6212 p.cop0.read(reg::RANDOM),
6213 random_before,
6214 "and must not tick Random"
6215 );
6216 }
6217 }
6218 assert!(saw_trap, "no FP trap was taken -- the test proved nothing");
6219 }
6220
6221 /// **A `MOV.S` must not disturb `FCSR.Cause`.**
6222 ///
6223 /// This is a regression test for a real defect. `MOV`/`ABS`/`NEG` were
6224 /// first written to clear the `Cause` field, on no evidence — and because
6225 /// the compiler emits `MOV.fmt` to move an FP return value, a `MOV` almost
6226 /// always sits between an arithmetic operation and the `CFC1` that reads
6227 /// its result. It therefore erased the very bits the program was about to
6228 /// inspect, costing 112 n64-systemtest assertions.
6229 ///
6230 /// The signature was distinctive and worth recording: the suite reported
6231 /// `flags: inexact` with `causes: ""` — the sticky half surviving and the
6232 /// per-operation half gone. That shape means *a later instruction
6233 /// overwrote it*, not that the flag was never raised.
6234 ///
6235 /// The sequence below is exactly the shape a compiled FP call has:
6236 /// arithmetic, then a move of the result.
6237 #[test]
6238 fn a_following_mov_s_leaves_the_cause_field_of_a_previous_operation_intact() {
6239 use crate::cop0::reg;
6240 /// `ADD.S $f4, $f0, $f2` then `MOV.S $f6, $f4`.
6241 const MOV_S_F6_F4: u32 = (0o21 << 26) | (0o20 << 21) | (4 << 11) | (6 << 6) | 6;
6242 /// `FCSR.Cause` for Inexact (bit 12).
6243 const CAUSE_INEXACT: u32 = 1 << 12;
6244
6245 let mut bus = Ram::new(alloc::vec![ADD_S_F4_F0_F2, MOV_S_F6_F4]);
6246 let mut regs = Regs::new();
6247 let mut p = Pipeline::new();
6248 p.cop0.set_hardware(reg::STATUS, 0x3400_0000);
6249 // f32::MAX + 1.0 -- the value is unchanged and the operation is
6250 // inexact, which is the n64-systemtest case from ledger C-11.
6251 p.fpr.write_s(0, true, f32::MAX.to_bits());
6252 p.fpr.write_s(2, true, 1.0f32.to_bits());
6253
6254 let mut pc = KSEG0_PROG;
6255 for _ in 0..8 {
6256 p.advance(&mut bus, &mut regs, &mut pc);
6257 }
6258 assert_ne!(
6259 p.cop1.fcsr() & CAUSE_INEXACT,
6260 0,
6261 "ADD.S must raise Cause.Inexact in the first place"
6262 );
6263 for _ in 0..16 {
6264 p.advance(&mut bus, &mut regs, &mut pc);
6265 }
6266 assert_eq!(
6267 p.fpr.read_s(6, true),
6268 p.fpr.read_s(4, true),
6269 "the MOV.S did run"
6270 );
6271 assert_ne!(
6272 p.cop1.fcsr() & CAUSE_INEXACT,
6273 0,
6274 "and must not have cleared the ADD.S's Cause"
6275 );
6276 }
6277
6278 /// **A later COP1 operation clears a stale `Cause.E` (bit 17).**
6279 ///
6280 /// `Cause` is bits **17:12** and is replaced wholesale by each operation.
6281 /// The mask here originally covered only 16:12, so the unimplemented-
6282 /// operation bit — which has no `Enable` and no sticky `Flags` twin, and so
6283 /// is only ever cleared by that mask — stayed set forever once raised.
6284 /// Software reading `FCSR` after a perfectly good conversion would still
6285 /// see the previous failure.
6286 ///
6287 /// Found by a review bot, not by this suite, which had no case that raised
6288 /// bit 17 and then ran another COP1 instruction.
6289 #[test]
6290 fn a_later_operation_clears_a_stale_unimplemented_cause() {
6291 use crate::cop0::reg;
6292 /// `ADD.S $f4, $f0, $f2` — an ordinary, entirely successful operation.
6293 const ADD_S: u32 = 0x4602_0100;
6294 /// `FCSR.Cause.E`, bit 17.
6295 const CAUSE_E: u32 = 1 << 17;
6296
6297 let mut bus = Ram::new(alloc::vec![ADD_S]);
6298 let mut regs = Regs::new();
6299 let mut p = Pipeline::new();
6300 p.cop0.set_hardware(reg::STATUS, 0x3400_0000);
6301 // Pre-set the bit, as a previous unimplemented operation would have.
6302 p.cop1.ctc1(31, CAUSE_E);
6303 p.fpr.write_s(0, true, 1.0f32.to_bits());
6304 p.fpr.write_s(2, true, 2.0f32.to_bits());
6305
6306 let mut pc = KSEG0_PROG;
6307 for _ in 0..16 {
6308 p.advance(&mut bus, &mut regs, &mut pc);
6309 }
6310 assert_eq!(p.fpr.read_s(4, true), 3.0f32.to_bits(), "the ADD.S ran");
6311 assert_eq!(
6312 p.cop1.fcsr() & CAUSE_E,
6313 0,
6314 "a successful operation must clear the whole Cause field"
6315 );
6316 }
6317
6318 /// `C.cond.fmt` writes `FCSR.C` and **no FPR at all**.
6319 ///
6320 /// Both halves matter. A compare that also wrote `fd` would corrupt a
6321 /// register the program never named, and one that computed the right
6322 /// condition without storing it leaves every dependent branch wrong.
6323 #[test]
6324 fn a_compare_writes_the_fcsr_condition_and_leaves_the_registers_alone() {
6325 use crate::cop0::reg;
6326 /// `FCSR.C`, bit 23.
6327 const FCSR_C: u32 = 1 << 23;
6328 /// `C.EQ.S $f0, $f2` — fmt 16, funct 0o62 (cond 2 = EQ).
6329 const C_EQ_S: u32 = (0o21 << 26) | (0o20 << 21) | (2 << 16) | 0o62;
6330
6331 for (a, b, want) in [(1.0f32, 1.0f32, true), (1.0, 2.0, false)] {
6332 let mut bus = Ram::new(alloc::vec![C_EQ_S]);
6333 let mut regs = Regs::new();
6334 let mut p = Pipeline::new();
6335 p.cop0.set_hardware(reg::STATUS, 0x3400_0000);
6336 // Start with the condition at the OPPOSITE of the expected result,
6337 // so "wrote the right value" is distinguishable from "left it".
6338 p.cop1.ctc1(31, if want { 0 } else { FCSR_C });
6339 p.fpr.write_s(0, true, a.to_bits());
6340 p.fpr.write_s(2, true, b.to_bits());
6341 p.fpr.write_raw(4, 0xDEAD_BEEF_1122_3344);
6342
6343 let mut pc = KSEG0_PROG;
6344 for _ in 0..16 {
6345 p.advance(&mut bus, &mut regs, &mut pc);
6346 }
6347 assert_eq!(p.cop1.fcsr() & FCSR_C != 0, want, "{a} == {b}");
6348 assert_eq!(
6349 p.fpr.read_raw(4),
6350 0xDEAD_BEEF_1122_3344,
6351 "a compare must not write an FPR"
6352 );
6353 }
6354 }
6355
6356 /// **`TRUNC.W.S` takes its rounding mode from the OPCODE, not `FCSR.RM`.**
6357 ///
6358 /// This is the entire difference between the `ROUND`/`TRUNC`/`CEIL`/`FLOOR`
6359 /// family and `CVT.W`/`CVT.L`, and it is invisible whenever `RM` happens to
6360 /// agree with the opcode. So `FCSR.RM` is set to round-to-nearest and the
6361 /// input chosen where nearest and truncate disagree: `-1.5` truncates to
6362 /// `-1` and rounds to `-2`.
6363 ///
6364 /// `CVT.W.S` on the same input under the same `FCSR` must give `-2`,
6365 /// proving the two families really are wired differently rather than both
6366 /// happening to truncate.
6367 #[test]
6368 fn the_fixed_mode_conversions_ignore_fcsr_rm_and_cvt_w_honors_it() {
6369 use crate::cop0::reg;
6370 /// `TRUNC.W.S $f4, $f0` — fmt 16, `fs` 0 (the zero shift is elided),
6371 /// `fd` 4, funct 0o15.
6372 const TRUNC_W_S: u32 = (0o21 << 26) | (0o20 << 21) | (4 << 6) | 0o15;
6373 /// `CVT.W.S $f4, $f0` — fmt 16, `fs` 0, `fd` 4, funct 0o44.
6374 const CVT_W_S: u32 = (0o21 << 26) | (0o20 << 21) | (4 << 6) | 0o44;
6375
6376 for (word, want) in [(TRUNC_W_S, -1i32), (CVT_W_S, -2)] {
6377 let mut bus = Ram::new(alloc::vec![word]);
6378 let mut regs = Regs::new();
6379 let mut p = Pipeline::new();
6380 p.cop0.set_hardware(reg::STATUS, 0x3400_0000);
6381 p.cop1.ctc1(31, 0); // RM = 0, round to nearest even
6382 p.fpr.write_s(0, true, (-1.5f32).to_bits());
6383
6384 let mut pc = KSEG0_PROG;
6385 for _ in 0..16 {
6386 p.advance(&mut bus, &mut regs, &mut pc);
6387 }
6388 #[allow(clippy::cast_possible_wrap)] // reading the word back as signed
6389 let got = p.fpr.read_s(4, true) as i32;
6390 assert_eq!(got, want, "instruction {word:#010X} on -1.5");
6391 }
6392 }
6393
6394 /// `CVT.S.W` reads its source as a **32-bit integer**, which is a different
6395 /// format carried in the same `fmt` field.
6396 ///
6397 /// A decoder that admits only formats 16/17 leaves every integer-to-float
6398 /// conversion a silent no-op, and `fd` keeps whatever it had — which looks
6399 /// exactly like a plausible float.
6400 #[test]
6401 fn cvt_s_w_converts_an_integer_source() {
6402 use crate::cop0::reg;
6403 /// `CVT.S.W $f4, $f0` — fmt 20 (`.W`), `fs` 0, `fd` 4, funct 0o40.
6404 const CVT_S_W: u32 = (0o21 << 26) | (0o24 << 21) | (4 << 6) | 0o40;
6405
6406 let mut bus = Ram::new(alloc::vec![CVT_S_W]);
6407 let mut regs = Regs::new();
6408 let mut p = Pipeline::new();
6409 p.cop0.set_hardware(reg::STATUS, 0x3400_0000);
6410 p.fpr.write_s(0, true, 12345u32);
6411 p.fpr.write_s(4, true, 0x1122_3344);
6412
6413 let mut pc = KSEG0_PROG;
6414 for _ in 0..16 {
6415 p.advance(&mut bus, &mut regs, &mut pc);
6416 }
6417 // Compared as BITS, not as a float: 12345.0 is exactly representable,
6418 // so this is the stricter check and it also catches a wrong-signed
6419 // zero or a NaN payload that float equality would accept.
6420 assert_eq!(
6421 p.fpr.read_s(4, true),
6422 12345.0f32.to_bits(),
6423 "the integer source must be converted, not reinterpreted"
6424 );
6425 }
6426
6427 /// **`CTC1` can raise an FP exception by itself.** Writing `FCSR` with a
6428 /// Cause bit whose Enable is also set meets the trap condition
6429 /// immediately — no arithmetic has to run.
6430 ///
6431 /// The instruction must also not retire, exactly as a trapping arithmetic
6432 /// operation does not.
6433 #[test]
6434 fn ctc1_raises_when_it_writes_a_cause_bit_that_is_enabled() {
6435 use crate::cop0::reg;
6436 /// `CTC1 $1, $f31` — rs 0o06, rt 1, fs 31.
6437 const CTC1_F31: u32 = (0o21 << 26) | (0o06 << 21) | (1 << 16) | (31 << 11);
6438 /// `Cause.overflow` (bit 14) with `Enable.overflow` (bit 9).
6439 const OVERFLOW_ARMED: u64 = (1 << 14) | (1 << 9);
6440
6441 let mut bus = Ram::new(alloc::vec![CTC1_F31]);
6442 let mut regs = Regs::new();
6443 let mut p = Pipeline::new();
6444 p.cop0.set_hardware(reg::STATUS, 0x3400_0000);
6445 regs.write(1, OVERFLOW_ARMED);
6446 let mut pc = KSEG0_PROG;
6447 for _ in 0..24 {
6448 p.advance(&mut bus, &mut regs, &mut pc);
6449 }
6450 assert_eq!(
6451 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
6452 crate::exception::exc_code::FPE,
6453 "the CTC1 itself must raise"
6454 );
6455 assert_eq!(
6456 u64::from(p.cop1.fcsr()),
6457 OVERFLOW_ARMED,
6458 "and the written value stands"
6459 );
6460 }
6461
6462 /// The *enable* half is load-bearing: the same Cause bit with its Enable
6463 /// clear must NOT raise. Without this, an implementation that trapped on
6464 /// any non-zero Cause would pass the test above.
6465 #[test]
6466 fn ctc1_does_not_raise_when_the_matching_enable_is_clear() {
6467 use crate::cop0::reg;
6468 const CTC1_F31: u32 = (0o21 << 26) | (0o06 << 21) | (1 << 16) | (31 << 11);
6469
6470 let mut bus = Ram::new(alloc::vec![CTC1_F31]);
6471 let mut regs = Regs::new();
6472 let mut p = Pipeline::new();
6473 p.cop0.set_hardware(reg::STATUS, 0x3400_0000);
6474 // Cause.overflow set, Enable.overflow clear; an unrelated enable set.
6475 regs.write(1, (1 << 14) | (1 << 11));
6476 let mut pc = KSEG0_PROG;
6477 for _ in 0..24 {
6478 p.advance(&mut bus, &mut regs, &mut pc);
6479 }
6480 assert_eq!(
6481 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
6482 0,
6483 "a Cause bit whose Enable is clear must not trap"
6484 );
6485 }
6486
6487 /// **`DCFC1`/`DCTC1` and `DCFC2`/`DCTC2` decline in DIFFERENT ways.**
6488 ///
6489 /// The encodings are structurally identical — the doubleword control moves
6490 /// of their respective coprocessors — and it is tempting to give them one
6491 /// behavior. COP1's raise a *floating-point* exception with `FCSR.Cause`
6492 /// set to unimplemented-operation; COP2's raise *Reserved Instruction* with
6493 /// `Cause.CE = 2` and do not touch `FCSR` at all.
6494 ///
6495 /// Both are tested here together precisely because treating them uniformly
6496 /// is the natural mistake.
6497 #[test]
6498 fn the_doubleword_control_moves_decline_differently_per_coprocessor() {
6499 use crate::cop0::reg;
6500 /// `DCFC1 $1, $f0` — COP1, rs 0o03.
6501 const DCFC1: u32 = (0o21 << 26) | (0o03 << 21) | (1 << 16);
6502 /// `DCFC2 $1, $0` — COP2, rs 0o03.
6503 const DCFC2: u32 = (0o22 << 26) | (0o03 << 21) | (1 << 16);
6504
6505 // COP1: floating-point exception, FCSR.Cause = unimplemented ONLY.
6506 let mut bus = Ram::new(alloc::vec![DCFC1]);
6507 let mut regs = Regs::new();
6508 let mut p = Pipeline::new();
6509 p.cop0.set_hardware(reg::STATUS, 0x3400_0000); // CU1 set
6510 // Pre-load unrelated cause bits: they must be cleared, not merged.
6511 p.cop1.ctc1(31, 0x0001_F000);
6512 let mut pc = KSEG0_PROG;
6513 for _ in 0..24 {
6514 p.advance(&mut bus, &mut regs, &mut pc);
6515 }
6516 assert_eq!(
6517 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
6518 crate::exception::exc_code::FPE,
6519 "DCFC1 raises FPE"
6520 );
6521 assert_eq!(
6522 p.cop1.fcsr() & (0x3F << 12),
6523 crate::fpu::CAUSE_UNIMPLEMENTED,
6524 "and Cause is ONLY the unimplemented bit"
6525 );
6526
6527 // COP2: Reserved Instruction, and Cause.CE names the coprocessor.
6528 let mut bus = Ram::new(alloc::vec![DCFC2]);
6529 let mut regs = Regs::new();
6530 let mut p = Pipeline::new();
6531 // CU2 set (bit 30) as well as CU1, so this is not an unusable fault.
6532 p.cop0.set_hardware(reg::STATUS, 0x7400_0000);
6533 let mut pc = KSEG0_PROG;
6534 for _ in 0..24 {
6535 p.advance(&mut bus, &mut regs, &mut pc);
6536 }
6537 assert_eq!(
6538 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
6539 crate::exception::exc_code::RI,
6540 "DCFC2 raises Reserved Instruction, not FPE"
6541 );
6542 assert_eq!(
6543 (p.cop0.read(reg::CAUSE) >> 28) & 0b11,
6544 2,
6545 "and Cause.CE names COP2 -- a plain RI would leave it zero"
6546 );
6547 }
6548
6549 /// **A `JAL` inside another jump's delay slot links to the OUTER target
6550 /// + 4**, not to its own `pc + 8`.
6551 ///
6552 /// Its own delay slot never runs — the outer jump already redirected — so
6553 /// the instruction after it is the outer target. n64-systemtest states the
6554 /// rule in its assertion text: *"JAL in delay slot writes target address+4
6555 /// of original jump into delay slot"*.
6556 ///
6557 /// The ordinary case is asserted alongside it, because the two share one
6558 /// implementation and a test of only the nested case would pass with the
6559 /// link hard-wired to `next_pc` in a way that broke normal jumps.
6560 #[test]
6561 fn a_jal_in_a_delay_slot_links_past_the_outer_target() {
6562 /// `JAL <target>` — the target is a word index within the 256 MB region.
6563 const fn jal(target_word: u32) -> u32 {
6564 (0o03 << 26) | target_word
6565 }
6566 /// `J <target>`.
6567 const fn j(target_word: u32) -> u32 {
6568 (0o02 << 26) | target_word
6569 }
6570
6571 // Ordinary: JAL at KSEG0_PROG, delay slot after it, link = pc + 8.
6572 {
6573 let outer = u32::try_from(KSEG0_PROG & 0x0FFF_FFFF).unwrap() >> 2;
6574 let mut bus = Ram::new(alloc::vec![jal(outer + 8), 0, 0, 0, 0, 0, 0, 0, 0, 0]);
6575 let mut regs = Regs::new();
6576 let mut p = Pipeline::new();
6577 let mut pc = KSEG0_PROG;
6578 for _ in 0..24 {
6579 p.advance(&mut bus, &mut regs, &mut pc);
6580 }
6581 assert_eq!(regs.read(31), KSEG0_PROG + 8, "an ordinary JAL links pc+8");
6582 }
6583
6584 // Nested: J to T, with a JAL in its delay slot. The JAL must link to
6585 // T + 4, NOT to its own address + 8.
6586 {
6587 let base = u32::try_from(KSEG0_PROG & 0x0FFF_FFFF).unwrap() >> 2;
6588 // T is eight words along; the JAL sits in the J's delay slot.
6589 let t_word = base + 8;
6590 let prog = alloc::vec![j(t_word), jal(base + 4), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
6591 let mut bus = Ram::new(prog);
6592 let mut regs = Regs::new();
6593 let mut p = Pipeline::new();
6594 let mut pc = KSEG0_PROG;
6595 for _ in 0..24 {
6596 p.advance(&mut bus, &mut regs, &mut pc);
6597 }
6598 let outer_target = KSEG0_PROG + 8 * 4;
6599 assert_eq!(
6600 regs.read(31),
6601 outer_target + 4,
6602 "the nested JAL links to the OUTER target + 4"
6603 );
6604 assert_ne!(
6605 regs.read(31),
6606 KSEG0_PROG + 4 + 8,
6607 "and specifically NOT to its own pc + 8"
6608 );
6609 }
6610 }
6611
6612 /// **COP2 is one 64-bit latch, not a register file.**
6613 ///
6614 /// The register index is ignored entirely: n64-systemtest writes with one
6615 /// index and reads back with several others — including 30 and 31 — and
6616 /// gets the same value every time. `MTC2` writes all 64 bits despite being
6617 /// nominally a 32-bit move; `MFC2` returns the low half sign-extended and
6618 /// `DMFC2` the whole thing.
6619 ///
6620 /// The index-independence is the assertion that matters: a real 32-entry
6621 /// register file passes a write-then-read-same-index test perfectly.
6622 #[test]
6623 fn cop2_is_a_single_latch_whose_register_index_is_ignored() {
6624 use crate::cop0::reg;
6625 /// `MTC2 $1, $5` — COP2 rs 0o04, rt 1, rd 5.
6626 const MTC2_R1_TO_5: u32 = (0o22 << 26) | (0o04 << 21) | (1 << 16) | (5 << 11);
6627 /// `DMFC2 $2, $30` — rs 0o01, rt 2, rd 30. A DIFFERENT index.
6628 const DMFC2_30_TO_R2: u32 = (0o22 << 26) | (0o01 << 21) | (2 << 16) | (30 << 11);
6629 /// `MFC2 $3, $31` — rs 0o00 (so no `rs` term), rt 3, rd 31. Another
6630 /// different index again.
6631 const MFC2_31_TO_R3: u32 = (0o22 << 26) | (3 << 16) | (31 << 11);
6632
6633 let mut bus = Ram::new(alloc::vec![MTC2_R1_TO_5, DMFC2_30_TO_R2, MFC2_31_TO_R3]);
6634 let mut regs = Regs::new();
6635 let mut p = Pipeline::new();
6636 // CU2 usable, or these fault before reaching the latch.
6637 p.cop0.set_hardware(reg::STATUS, 0x5000_0000);
6638 regs.write(1, 0x0123_4567_89AB_CDEF);
6639
6640 let mut pc = KSEG0_PROG;
6641 for _ in 0..32 {
6642 p.advance(&mut bus, &mut regs, &mut pc);
6643 }
6644 assert_eq!(
6645 regs.read(2),
6646 0x0123_4567_89AB_CDEF,
6647 "DMFC2 from a DIFFERENT index reads all 64 bits MTC2 wrote"
6648 );
6649 assert_eq!(
6650 regs.read(3),
6651 0xFFFF_FFFF_89AB_CDEF,
6652 "MFC2 from another index reads the low half, sign-extended"
6653 );
6654 }
6655
6656 // --- Unimplemented operation on subnormals (T-13-004) -------------------
6657
6658 /// `ADD.S $f4, $f0, $f2`.
6659 const ADD_S_SUB: u32 = 0x4602_0100;
6660 /// `FCSR.Cause.E`, bit 17 — unmaskable unimplemented operation.
6661 const CAUSE_E: u32 = 1 << 17;
6662 /// `FCSR.FS`, bit 24 — flush denormals to zero.
6663 const FCSR_FS: u32 = 1 << 24;
6664
6665 /// Run `ADD.S $f4, $f0, $f2` with the given operands and `FCSR`.
6666 fn run_add_s(fcsr: u32, a: u32, b: u32) -> Pipeline {
6667 use crate::cop0::reg;
6668 let mut bus = Ram::new(alloc::vec![ADD_S_SUB]);
6669 let mut regs = Regs::new();
6670 let mut p = Pipeline::new();
6671 p.cop0.set_hardware(reg::STATUS, 0x3400_0000);
6672 p.cop1.ctc1(31, fcsr);
6673 p.fpr.write_s(0, true, a);
6674 p.fpr.write_s(2, true, b);
6675 p.fpr.write_raw(4, 0x1122_3344_5566_7788);
6676 let mut pc = KSEG0_PROG;
6677 for _ in 0..24 {
6678 p.advance(&mut bus, &mut regs, &mut pc);
6679 }
6680 p
6681 }
6682
6683 /// A **subnormal operand** raises unimplemented operation before the
6684 /// arithmetic is attempted — even with `FS` set, which does not rescue it.
6685 ///
6686 /// `FCSR` must end up with bit 17 and *nothing else*: no Invalid, no
6687 /// Inexact, and the sticky `Flags` untouched.
6688 #[test]
6689 fn a_subnormal_operand_raises_unimplemented_operation() {
6690 use crate::cop0::reg;
6691 let subnormal = 1u32; // the smallest positive subnormal
6692 for fcsr in [0, FCSR_FS] {
6693 let p = run_add_s(fcsr, subnormal, 2.0f32.to_bits());
6694 assert_eq!(
6695 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
6696 crate::exception::exc_code::FPE,
6697 "fcsr={fcsr:#X}"
6698 );
6699 assert_ne!(p.cop1.fcsr() & CAUSE_E, 0, "Cause.E set");
6700 assert_eq!(
6701 p.cop1.fcsr() & !(CAUSE_E | FCSR_FS),
6702 0,
6703 "bit 17 and nothing else -- no flags, no other causes"
6704 );
6705 assert_eq!(
6706 p.fpr.read_raw(4),
6707 0x1122_3344_5566_7788,
6708 "fd must be untouched"
6709 );
6710 }
6711 }
6712
6713 /// A **subnormal result** with `FS` clear is equally refused. The operands
6714 /// here are both normal, so this is the result path and not the operand
6715 /// one — the two are separate checks and a test using a subnormal input
6716 /// would pass with the result check deleted.
6717 #[test]
6718 fn a_subnormal_result_raises_unimplemented_when_fs_is_clear() {
6719 use crate::cop0::reg;
6720 let a = 1.528_510_4e-37f32;
6721 let b = -1.539_154_3e-37f32;
6722 assert!(!crate::fpu::is_subnormal_f32(a), "operands are normal");
6723 assert!(!crate::fpu::is_subnormal_f32(b));
6724
6725 let p = run_add_s(0, a.to_bits(), b.to_bits());
6726 assert_eq!(
6727 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
6728 crate::exception::exc_code::FPE
6729 );
6730 assert_ne!(p.cop1.fcsr() & CAUSE_E, 0);
6731 }
6732
6733 /// **An underflow that rounds past the subnormal grid to ZERO is refused
6734 /// too**, not just one that lands on a subnormal.
6735 ///
6736 /// `MIN_POSITIVE * MIN_POSITIVE` is about `1.4e-76`, far below `f32`'s
6737 /// smallest subnormal, so the rounded result is plain zero and
6738 /// `is_subnormal` is false for it. Testing only that condition let every
6739 /// such case through silently — worth 22 oracle assertions.
6740 ///
6741 /// The converse matters as much and is covered by
6742 /// `a_subnormal_result_raises_unimplemented_when_fs_is_clear`: IEEE signals
6743 /// underflow only when tiny **and inexact**, so an *exact* subnormal has
6744 /// `underflow` clear. Neither condition implies the other; the
6745 /// implementation needs both.
6746 #[test]
6747 fn an_underflow_that_reaches_zero_is_refused_as_well() {
6748 use crate::cop0::reg;
6749 /// `MUL.S $f4, $f0, $f2` — fmt 16, funct 2.
6750 const MUL_S: u32 = (0o21 << 26) | (0o20 << 21) | (2 << 16) | (4 << 6) | 2;
6751
6752 let mut bus = Ram::new(alloc::vec![MUL_S]);
6753 let mut regs = Regs::new();
6754 let mut p = Pipeline::new();
6755 p.cop0.set_hardware(reg::STATUS, 0x3400_0000);
6756 p.fpr.write_s(0, true, f32::MIN_POSITIVE.to_bits());
6757 p.fpr.write_s(2, true, f32::MIN_POSITIVE.to_bits());
6758
6759 let mut pc = KSEG0_PROG;
6760 for _ in 0..24 {
6761 p.advance(&mut bus, &mut regs, &mut pc);
6762 }
6763 assert_ne!(
6764 p.cop1.fcsr() & CAUSE_E,
6765 0,
6766 "an underflow to zero must raise unimplemented with FS clear"
6767 );
6768 }
6769
6770 /// With `FS` set, the same operation **flushes** — and where it flushes to
6771 /// depends on the rounding mode. These are n64-systemtest's own vectors.
6772 ///
6773 /// Round-to-nearest and toward-zero give a signed zero; a mode that rounds
6774 /// *away* from zero must give the smallest **normal** instead, because zero
6775 /// is on the wrong side of the true result. Getting that wrong yields `-0`
6776 /// in all four cases, which looks entirely reasonable.
6777 #[test]
6778 fn with_fs_set_a_subnormal_result_flushes_per_rounding_mode() {
6779 let a = 1.528_510_4e-37f32.to_bits();
6780 let b = (-1.539_154_3e-37f32).to_bits();
6781 // (RM, expected) -- the true result is a tiny NEGATIVE subnormal.
6782 for (rm, want) in [
6783 (0u32, (-0.0f32).to_bits()), // nearest
6784 (1, (-0.0f32).to_bits()), // toward zero
6785 (2, (-0.0f32).to_bits()), // toward +inf
6786 (3, (-f32::MIN_POSITIVE).to_bits()), // toward -inf: away from zero
6787 ] {
6788 let p = run_add_s(FCSR_FS | rm, a, b);
6789 assert_eq!(p.fpr.read_s(4, true), want, "RM={rm}");
6790 let fcsr = p.cop1.fcsr();
6791 assert_ne!(fcsr & (1 << 13), 0, "Cause.underflow, RM={rm}");
6792 assert_ne!(fcsr & (1 << 12), 0, "Cause.inexact, RM={rm}");
6793 assert_eq!(fcsr & CAUSE_E, 0, "not unimplemented, RM={rm}");
6794 }
6795 // ...and the mirrored operands flush the other way.
6796 let p = run_add_s(
6797 FCSR_FS | 2,
6798 (-1.528_510_4e-37f32).to_bits(),
6799 1.539_154_3e-37f32.to_bits(),
6800 );
6801 assert_eq!(
6802 p.fpr.read_s(4, true),
6803 f32::MIN_POSITIVE.to_bits(),
6804 "a positive tiny result under toward-+inf"
6805 );
6806 }
6807
6808 /// **`FS` set but underflow enabled is unimplemented, not a trap.** The
6809 /// processor cannot deliver a trapped underflow's defined result either.
6810 ///
6811 /// Easy to miss because it is the interaction of two features that each
6812 /// work: flushing works, and enabled traps work, but together they do not.
6813 #[test]
6814 fn fs_plus_an_enabled_underflow_is_unimplemented_rather_than_a_trap() {
6815 let a = 1.528_510_4e-37f32.to_bits();
6816 let b = (-1.539_154_3e-37f32).to_bits();
6817 // bit 8 = enable underflow, bit 7 = enable inexact.
6818 for enable in [1u32 << 8, 1 << 7] {
6819 let p = run_add_s(FCSR_FS | enable, a, b);
6820 assert_ne!(
6821 p.cop1.fcsr() & CAUSE_E,
6822 0,
6823 "enable={enable:#X} must give unimplemented"
6824 );
6825 }
6826 }
6827
6828 /// **The two NaN classes trap differently.** MSB clear (quiet by the
6829 /// VR4300's convention) is unimplemented; MSB set (signaling) is Invalid.
6830 ///
6831 /// Swapping them is invisible until `FCSR` is read back, and both are
6832 /// "the operation trapped", so a test asserting only that would pass either
6833 /// way. See ledger C-12.
6834 #[test]
6835 fn the_two_nan_classes_raise_different_causes() {
6836 let msb_clear = 0x7F80_0001u32; // unimplemented here
6837 let msb_set = 0x7FC0_0001u32; // signaling here -> Invalid
6838
6839 let p = run_add_s(0, msb_clear, 2.0f32.to_bits());
6840 assert_ne!(p.cop1.fcsr() & CAUSE_E, 0, "MSB clear -> unimplemented");
6841 assert_eq!(p.cop1.fcsr() & (1 << 16), 0, "and NOT invalid");
6842
6843 let p = run_add_s(0, msb_set, 2.0f32.to_bits());
6844 assert_ne!(p.cop1.fcsr() & (1 << 16), 0, "MSB set -> Cause.invalid");
6845 assert_eq!(p.cop1.fcsr() & CAUSE_E, 0, "and NOT unimplemented");
6846 }
6847
6848 /// **`ABS`/`NEG` classify their operand; `MOV` does not.** All three look
6849 /// like sign/bit manipulation, and only `MOV` actually is.
6850 ///
6851 /// n64-systemtest settles it by construction rather than description:
6852 /// `MOV.S` is driven through the cause-*preserving* harness while `ABS.S`
6853 /// and `NEG.S` go through the ordinary one. Treating all three alike was
6854 /// worth 52 assertions.
6855 #[test]
6856 fn abs_and_neg_refuse_a_subnormal_but_mov_moves_it() {
6857 use crate::cop0::reg;
6858 /// fmt 16, `fs` 0, `fd` 4.
6859 const fn unary(funct: u32) -> u32 {
6860 (0o21 << 26) | (0o20 << 21) | (4 << 6) | funct
6861 }
6862 let subnormal = 1u32;
6863
6864 for funct in [5u32, 7] {
6865 let mut bus = Ram::new(alloc::vec![unary(funct)]);
6866 let mut regs = Regs::new();
6867 let mut p = Pipeline::new();
6868 p.cop0.set_hardware(reg::STATUS, 0x3400_0000);
6869 p.fpr.write_s(0, true, subnormal);
6870 let mut pc = KSEG0_PROG;
6871 for _ in 0..24 {
6872 p.advance(&mut bus, &mut regs, &mut pc);
6873 }
6874 assert_ne!(
6875 p.cop1.fcsr() & CAUSE_E,
6876 0,
6877 "funct {funct} (ABS/NEG) must refuse a subnormal"
6878 );
6879 }
6880
6881 // MOV.S is a pure move: it transports the subnormal untouched and
6882 // raises nothing at all.
6883 let mut bus = Ram::new(alloc::vec![unary(6)]);
6884 let mut regs = Regs::new();
6885 let mut p = Pipeline::new();
6886 p.cop0.set_hardware(reg::STATUS, 0x3400_0000);
6887 p.fpr.write_s(0, true, subnormal);
6888 let mut pc = KSEG0_PROG;
6889 for _ in 0..24 {
6890 p.advance(&mut bus, &mut regs, &mut pc);
6891 }
6892 assert_eq!(
6893 p.fpr.read_s(4, true),
6894 subnormal,
6895 "MOV.S moves the subnormal"
6896 );
6897 assert_eq!(p.cop1.fcsr(), 0, "and raises nothing");
6898 }
6899
6900 /// **A `.L` conversion refuses a magnitude of `2^53` or more**, which is
6901 /// far narrower than `i64`.
6902 ///
6903 /// The threshold is bracketed rather than assumed: `9007198717870080`
6904 /// converts and `9007199254740992` (`2^53`) does not, and both are
6905 /// comfortably inside `i64`. A test using only a huge value would pass with
6906 /// the limit set anywhere between `2^53` and `i64::MAX`.
6907 ///
6908 /// The same limit applies to `.W`, where it is **unobservable**: `2^53` is
6909 /// far outside `i32`, so both paths end in unimplemented. Guarding it on
6910 /// the target width was tried and removed — a branch no test can
6911 /// distinguish is a branch that will rot.
6912 #[test]
6913 fn a_long_conversion_refuses_two_to_the_fifty_three_but_not_just_below() {
6914 use crate::cop0::reg;
6915 /// `CVT.L.D $f4, $f0` — fmt 17 (`.D`), `fs` 0, `fd` 4, funct 0o45.
6916 const CVT_L_D: u32 = (0o21 << 26) | (0o21 << 21) | (4 << 6) | 0o45;
6917
6918 for (src, want_unimplemented) in [
6919 (9_007_198_717_870_080.0f64, false),
6920 (9_007_199_254_740_992.0f64, true), // 2^53
6921 (-9_007_199_254_740_992.0f64, true),
6922 (-9_007_198_717_870_080.0f64, false),
6923 ] {
6924 let mut bus = Ram::new(alloc::vec![CVT_L_D]);
6925 let mut regs = Regs::new();
6926 let mut p = Pipeline::new();
6927 p.cop0.set_hardware(reg::STATUS, 0x3400_0000);
6928 p.fpr.write_d(0, true, src.to_bits());
6929 let mut pc = KSEG0_PROG;
6930 for _ in 0..24 {
6931 p.advance(&mut bus, &mut regs, &mut pc);
6932 }
6933 let raised = p.cop1.fcsr() & CAUSE_E != 0;
6934 assert_eq!(raised, want_unimplemented, "{src:e}");
6935 if !want_unimplemented {
6936 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
6937 let want = src as i64 as u64;
6938 assert_eq!(p.fpr.read_d(4, true), want, "{src:e} must convert");
6939 }
6940 }
6941 }
6942
6943 /// An out-of-range float-to-integer conversion is **unimplemented**, not
6944 /// Invalid. IEEE says Invalid; this processor declines instead, and
6945 /// `fpu::to_i32` reports the IEEE answer that must be translated.
6946 #[test]
6947 fn an_out_of_range_integer_conversion_is_unimplemented_not_invalid() {
6948 use crate::cop0::reg;
6949 /// `CVT.W.S $f4, $f0` — fmt 16, `fs` 0, `fd` 4, funct 0o44.
6950 const CVT_W_S: u32 = (0o21 << 26) | (0o20 << 21) | (4 << 6) | 0o44;
6951
6952 for src in [1e30f32, f32::INFINITY, f32::from_bits(0x7FC0_0000)] {
6953 let mut bus = Ram::new(alloc::vec![CVT_W_S]);
6954 let mut regs = Regs::new();
6955 let mut p = Pipeline::new();
6956 p.cop0.set_hardware(reg::STATUS, 0x3400_0000);
6957 p.fpr.write_s(0, true, src.to_bits());
6958 let mut pc = KSEG0_PROG;
6959 for _ in 0..24 {
6960 p.advance(&mut bus, &mut regs, &mut pc);
6961 }
6962 assert_ne!(p.cop1.fcsr() & CAUSE_E, 0, "{src:e} -> unimplemented");
6963 assert_eq!(p.cop1.fcsr() & (1 << 16), 0, "{src:e} -> NOT invalid");
6964 assert_eq!(
6965 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
6966 crate::exception::exc_code::FPE
6967 );
6968 }
6969 }
6970
6971 // --- CACHE (T-12-005) ---------------------------------------------------
6972
6973 /// `CACHE` must **not** raise. IPL3 and libdragon both issue it, so a
6974 /// reserved-instruction exception here blocks every real ROM — which is why
6975 /// this was called out as a hard blocker before it was implemented.
6976 #[test]
6977 fn cache_executes_instead_of_raising() {
6978 use crate::cop0::reg;
6979 // CACHE op=0, 0($1) with $1 = KSEG0 base.
6980 let prog = alloc::vec![lui_kseg0(1), ld_st(0o57, 1, 0, 0x100)];
6981 assert_eq!(decode(prog[1]).op, crate::decode::Op::Cache);
6982 let mut bus = Ram::new(prog);
6983 let mut regs = Regs::new();
6984 let mut p = Pipeline::new();
6985 p.cop0.set_hardware(reg::STATUS, 0);
6986 let mut pc = KSEG0_PROG;
6987 for _ in 0..24 {
6988 p.advance(&mut bus, &mut regs, &mut pc);
6989 }
6990 assert_eq!(
6991 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
6992 0,
6993 "CACHE must not raise"
6994 );
6995 assert_eq!(p.stalled_by(), None, "and must not be stuck in an epilogue");
6996 }
6997
6998 /// `CACHE`'s `rt` slot is the **operation selector**, not a destination.
6999 /// Decoding it as a load would clobber whichever GPR the cache-op encoding
7000 /// happens to name — a spectacularly confusing bug, since the register
7001 /// destroyed depends on which cache operation was requested.
7002 #[test]
7003 fn cache_writes_no_general_register() {
7004 // op = 0b10101 = 21, which as a destination would be $21.
7005 let word = ld_st(0o57, 1, 21, 0);
7006 let d = decode(word);
7007 assert_eq!(d.op, crate::decode::Op::Cache);
7008 assert_eq!(d.dest, 0, "rt is the cache operation, not a destination");
7009
7010 let prog = alloc::vec![lui_kseg0(1), addiu_zero(21, 0x33), word];
7011 let mut bus = Ram::new(prog);
7012 let mut regs = Regs::new();
7013 let mut p = Pipeline::new();
7014 p.cop0.set_hardware(crate::cop0::reg::STATUS, 0);
7015 let mut pc = KSEG0_PROG;
7016 for _ in 0..32 {
7017 p.advance(&mut bus, &mut regs, &mut pc);
7018 }
7019 assert_eq!(regs.read(21), 0x33, "$21 must survive CACHE op 21");
7020 }
7021
7022 /// An **`Index_*`** `CACHE` op addresses the cache by index and **must not
7023 /// translate**, so it cannot fault however unmapped the address is.
7024 ///
7025 /// This matters at boot: cache-initialization code walks every index with an
7026 /// arbitrary base address, and translating would raise a TLB refill on the
7027 /// first one — before any mapping exists to satisfy it.
7028 #[test]
7029 fn an_index_cache_op_never_faults_however_unmapped_the_address() {
7030 use crate::cop0::reg;
7031 for op in [0u16, 4, 8] {
7032 // op4..2 = 0, 1, 2 -> Index_Invalidate / Load_Tag / Store_Tag.
7033 assert!((op >> 2) < 3, "op {op} must be an Index form");
7034 let mut bus = Ram::new(alloc::vec![ld_st(0o57, 0, u32::from(op), 0x4000)]);
7035 let mut regs = Regs::new();
7036 let mut p = Pipeline::new();
7037 p.cop0.set_hardware(reg::STATUS, 0);
7038 let mut pc = KSEG0_PROG;
7039 for _ in 0..24 {
7040 p.advance(&mut bus, &mut regs, &mut pc);
7041 }
7042 assert_eq!(
7043 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
7044 0,
7045 "Index op {op} must not raise -- it never consults the TLB"
7046 );
7047 }
7048 }
7049
7050 /// A **`Hit_*`** `CACHE` op translates, so it raises a TLB fault on an
7051 /// unmapped address — it is defined in terms of *"the specified address"*.
7052 #[test]
7053 fn a_hit_cache_op_on_an_unmapped_address_faults() {
7054 use crate::cop0::reg;
7055 // op = 16 -> op4..2 = 4 = Hit_Invalidate.
7056 let mut bus = Ram::new(alloc::vec![ld_st(0o57, 0, 16, 0x4000)]);
7057 let mut regs = Regs::new();
7058 let mut p = Pipeline::new();
7059 p.cop0.set_hardware(reg::STATUS, 0);
7060 let mut pc = KSEG0_PROG;
7061
7062 let mut cycles = 0;
7063 while p.stalled_by() != Some(Interlock::Exception) {
7064 p.advance(&mut bus, &mut regs, &mut pc);
7065 cycles += 1;
7066 assert!(cycles < 16, "CACHE did not translate");
7067 }
7068 assert_eq!(
7069 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
7070 crate::exception::exc_code::TLBL
7071 );
7072 }
7073
7074 // --- FP register file, moves and loads/stores (T-13-001) ---------------
7075
7076 /// `MTC1` then `MFC1` round-trips through the real FP register file, and
7077 /// `MTC1` must **not** write a general register — `rt` is its source.
7078 #[test]
7079 fn mtc1_then_mfc1_round_trips_and_writes_no_gpr() {
7080 use crate::cop0::reg;
7081 const fn cop1(rs: u32, rt: u32, fs: u32) -> u32 {
7082 (0o21 << 26) | (rs << 21) | (rt << 16) | (fs << 11)
7083 }
7084 // ADDIU $1, $0, 0x55
7085 // MTC1 $1, $f4
7086 // MFC1 $2, $f4
7087 let prog = alloc::vec![addiu_zero(1, 0x55), cop1(0o04, 1, 4), cop1(0o00, 2, 4)];
7088 let mut bus = Ram::new(prog);
7089 let mut regs = Regs::new();
7090 let mut p = Pipeline::new();
7091 p.cop0.set_hardware(reg::STATUS, 0x3400_0000); // CU1 | FR
7092 let mut pc = KSEG0_PROG;
7093 for _ in 0..40 {
7094 p.advance(&mut bus, &mut regs, &mut pc);
7095 }
7096 assert_eq!(
7097 p.fpr.read_s(4, true),
7098 0x55,
7099 "MTC1 reached the FP register file"
7100 );
7101 assert_eq!(regs.read(2), 0x55, "and MFC1 read it back");
7102 assert_eq!(regs.read(1), 0x55, "$1 -- MTC1's source -- must survive");
7103 }
7104
7105 /// **`DMFC1`/`DMTC1` apply the `FR` view**, they do not move the physical
7106 /// register. UM Ch. 17's pseudocode: with `FR = 0` and an even `fs`,
7107 /// In half mode a 64-bit access addresses **FGR `fs & !1` in its
7108 /// entirety**, and the odd FGR is not part of it.
7109 ///
7110 /// This test previously asserted the *pair* model — low word in the even
7111 /// FGR, high word in the odd one — which round-trips through
7112 /// `DMTC1`/`DMFC1` perfectly and is still wrong: hardware never touches
7113 /// the odd register. See `fpr.rs` for the n64-systemtest vector that
7114 /// settles it.
7115 #[test]
7116 fn dmtc1_and_dmfc1_apply_the_fr_view_rather_than_moving_the_raw_fgr() {
7117 use crate::cop0::reg;
7118 const fn cop1(rs: u32, rt: u32, fs: u32) -> u32 {
7119 (0o21 << 26) | (rs << 21) | (rt << 16) | (fs << 11)
7120 }
7121 let mut p = Pipeline::new();
7122 // CU1 set, FR CLEAR -- the paired view.
7123 p.cop0.set_hardware(reg::STATUS, 1 << 29);
7124 // DMTC1 $1, $f2 with $1 = 0x1122_3344_5566_7788.
7125 let prog = alloc::vec![cop1(0o05, 1, 2), cop1(0o01, 2, 2)];
7126 let mut bus = Ram::new(prog);
7127 let mut regs = Regs::new();
7128 regs.write(1, 0x1122_3344_5566_7788);
7129 let mut pc = KSEG0_PROG;
7130 for _ in 0..32 {
7131 p.advance(&mut bus, &mut regs, &mut pc);
7132 }
7133
7134 // All 64 bits in FGR 2; FGR 3 is not involved.
7135 assert_eq!(
7136 p.fpr.read_raw(2),
7137 0x1122_3344_5566_7788,
7138 "the whole value lives in the even FGR"
7139 );
7140 assert_eq!(p.fpr.read_raw(3), 0, "the odd FGR is untouched");
7141 assert_eq!(regs.read(2), 0x1122_3344_5566_7788, "DMFC1 reassembles it");
7142 // And it agrees with the LDC1/SDC1 view of the same register.
7143 assert_eq!(p.fpr.read_d(2, false), 0x1122_3344_5566_7788);
7144 }
7145
7146 /// `SDC1` then `LDC1` round-trips a double through memory, and with
7147 /// `FR = 0` the value lives in an **FGR pair** — so this exercises the view
7148 /// that a direct-index register file gets wrong.
7149 #[test]
7150 fn ldc1_and_sdc1_round_trip_a_double_with_fr_clear() {
7151 use crate::cop0::reg;
7152 // LUI $1, 0x8000
7153 // SDC1 $f2, 0x100($1)
7154 // LDC1 $f4, 0x100($1)
7155 let prog = alloc::vec![
7156 lui_kseg0(1),
7157 ld_st(0o75, 1, 2, 0x100),
7158 ld_st(0o65, 1, 4, 0x100),
7159 ];
7160 let mut bus = Ram::new(prog);
7161 let mut regs = Regs::new();
7162 let mut p = Pipeline::new();
7163 // CU1 set, FR CLEAR -- the paired view.
7164 p.cop0.set_hardware(reg::STATUS, 1 << 29);
7165 p.fpr.write_d(2, false, 0x0123_4567_89AB_CDEF);
7166 let mut pc = KSEG0_PROG;
7167 for _ in 0..48 {
7168 p.advance(&mut bus, &mut regs, &mut pc);
7169 }
7170 assert_eq!(
7171 p.fpr.read_d(4, false),
7172 0x0123_4567_89AB_CDEF,
7173 "the double survived memory in the FR = 0 paired view"
7174 );
7175 writeback(&mut p, &mut bus, 0xFFFF_FFFF_8000_0100);
7176 assert_eq!(
7177 bus.read_u32(0x100),
7178 0x0123_4567,
7179 "big-endian high word first"
7180 );
7181 }
7182
7183 /// FP loads and stores obey the same alignment rules as the integer forms —
7184 /// `LDC1` needs 8-byte alignment, and a misaligned one raises `AdEL`.
7185 #[test]
7186 fn a_misaligned_ldc1_raises_an_address_error() {
7187 use crate::cop0::reg;
7188 let prog = alloc::vec![lui_kseg0(1), ld_st(0o65, 1, 4, 0x104)];
7189 let mut bus = Ram::new(prog);
7190 let mut regs = Regs::new();
7191 let mut p = Pipeline::new();
7192 p.cop0.set_hardware(reg::STATUS, 1 << 29);
7193 let mut pc = KSEG0_PROG;
7194
7195 let mut cycles = 0;
7196 while p.stalled_by() != Some(Interlock::Exception) {
7197 p.advance(&mut bus, &mut regs, &mut pc);
7198 cycles += 1;
7199 assert!(cycles < 24, "no address error raised");
7200 }
7201 assert_eq!(
7202 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
7203 crate::exception::exc_code::ADEL,
7204 "a misaligned LDC1 is a load address error"
7205 );
7206 }
7207
7208 /// The FP moves and loads need `CU1` like every other COP1 instruction.
7209 #[test]
7210 fn fp_loads_need_cu1() {
7211 use crate::cop0::reg;
7212 let prog = alloc::vec![lui_kseg0(1), ld_st(0o61, 1, 4, 0x100)];
7213 let mut bus = Ram::new(prog);
7214 let mut regs = Regs::new();
7215 let mut p = Pipeline::new();
7216 p.cop0.set_hardware(reg::STATUS, 0); // CU1 CLEAR
7217 let mut pc = KSEG0_PROG;
7218
7219 let mut cycles = 0;
7220 while p.stalled_by() != Some(Interlock::Exception) {
7221 p.advance(&mut bus, &mut regs, &mut pc);
7222 cycles += 1;
7223 assert!(cycles < 24, "no exception raised");
7224 }
7225 assert_eq!(
7226 (p.cop0.read(reg::CAUSE) >> 2) & 0x1F,
7227 crate::exception::exc_code::CPU
7228 );
7229 assert_eq!((p.cop0.read(reg::CAUSE) >> 28) & 0b11, 1, "unit 1");
7230 }
7231
7232 // ---------------------------------------------------------------------
7233 // The latch-independent primitives, tested directly (ADR 0013's seam)
7234 // ---------------------------------------------------------------------
7235 //
7236 // The stage tests above reach these through the cascade, which is the right
7237 // way to test the accurate path and the wrong way to pin a *contract*. These
7238 // call the primitives directly, because the instruction-granular path will
7239 // call them with different consequences and needs the decisions to hold on
7240 // their own.
7241
7242 /// A faulting fetch must report the fault **and leave the bus untouched** —
7243 /// the access itself is what is invalid.
7244 ///
7245 /// Distinct from `an_unaligned_fetch_raises_address_error_without_realigning`
7246 /// above, which checks what `ic_stage` *does* with the fault (stamps `ic_rf`,
7247 /// declines to realign). This checks the primitive's own contract, which is
7248 /// the half a second caller depends on.
7249 #[test]
7250 fn fetch_word_reports_a_fault_without_touching_the_bus() {
7251 struct Watch {
7252 touched: bool,
7253 }
7254 impl Bus for Watch {
7255 fn read_u8(&mut self, _addr: u32) -> u8 {
7256 self.touched = true;
7257 0
7258 }
7259 fn write_u8(&mut self, _addr: u32, _val: u8) {
7260 self.touched = true;
7261 }
7262 fn read_u32(&mut self, _addr: u32) -> u32 {
7263 self.touched = true;
7264 0
7265 }
7266 }
7267 let mut bus = Watch { touched: false };
7268 let mut p = Pipeline::new();
7269
7270 assert_eq!(
7271 p.fetch_word(&mut bus, 0xFFFF_FFFF_8000_0002),
7272 Err(Exception::AddressError { store: false }),
7273 "an unaligned fetch is an address error"
7274 );
7275 assert!(
7276 !bus.touched,
7277 "the bus was accessed for a fetch that is invalid before it happens"
7278 );
7279 }
7280
7281 /// The positive case, so the test above cannot pass against a `fetch_word`
7282 /// that simply always fails.
7283 ///
7284 /// The word is a distinctive sentinel rather than zero: zero is `NOP` and is
7285 /// also what a bus returning nothing produces, so it cannot tell a real fetch
7286 /// from a missing one.
7287 #[test]
7288 fn fetch_word_returns_the_word_at_an_aligned_pc() {
7289 struct Fixed(u32);
7290 impl Bus for Fixed {
7291 fn read_u8(&mut self, _addr: u32) -> u8 {
7292 0
7293 }
7294 fn write_u8(&mut self, _addr: u32, _val: u8) {}
7295 fn read_u32(&mut self, _addr: u32) -> u32 {
7296 self.0
7297 }
7298 }
7299 const SENTINEL: u32 = 0xDEAD_BEEF;
7300 let mut bus = Fixed(SENTINEL);
7301 let mut p = Pipeline::new();
7302
7303 // KSEG1 — uncached and unmapped, so this reaches the bus directly and
7304 // tests the fetch rather than the I-cache.
7305 assert_eq!(p.fetch_word(&mut bus, 0xFFFF_FFFF_A000_0000), Ok(SENTINEL));
7306 }
7307
7308 /// `ex_gate` refuses `DCFC1`/`DCTC1` by **replacing** `FCSR.Cause`, not by
7309 /// OR-ing into it.
7310 ///
7311 /// Asserted as an *effect*: `FCSR` is seeded with unrelated cause bits first,
7312 /// and they must be gone afterwards. A test that only checked bit 17 had
7313 /// arrived would pass against an implementation that OR-ed — which is the
7314 /// behavior n64-systemtest specifically pre-loads unrelated bits to catch.
7315 #[test]
7316 fn ex_gate_replaces_the_whole_fcsr_cause_field() {
7317 /// `FCSR.Cause`, bits 17:12 — the field under test.
7318 const CAUSE: u32 = 0x3F << 12;
7319 /// Unrelated cause bits (Inexact and Overflow), seeded to be cleared.
7320 const SEEDED: u32 = 0b0001_0100 << 12;
7321
7322 use crate::cop0::reg;
7323
7324 let mut p = Pipeline::new();
7325 p.cop1.ctc1(31, SEEDED);
7326 assert_eq!(p.cop1.fcsr() & CAUSE, SEEDED, "the seed took");
7327
7328 // COP1 opcode with rs = 3 is `DCFC1`, the usable-but-unimplemented
7329 // control move. Decoded rather than hand-built so the encoding stays
7330 // owned by `decode`.
7331 let d = decode((0o21 << 26) | (3 << 21));
7332 assert_eq!(
7333 d.op,
7334 crate::decode::Op::Cop1ReservedControl,
7335 "the encoding under test must actually be the reserved control move"
7336 );
7337
7338 // `CU1` must be SET, or the coprocessor-usability check refuses first and
7339 // this would test the wrong branch.
7340 p.cop0.set_hardware(reg::STATUS, 1 << 29);
7341 assert_eq!(p.ex_gate(d), Err(Exception::FloatingPoint));
7342 assert_eq!(
7343 p.cop1.fcsr() & CAUSE,
7344 crate::fpu::CAUSE_UNIMPLEMENTED,
7345 "the seeded cause bits survived: `Cause` was OR-ed, not replaced"
7346 );
7347 }
7348
7349 /// **`ex_gate`'s first two refusals are DISJOINT, so their order cannot be
7350 /// observed — and this test is what keeps that true.**
7351 ///
7352 /// The extraction started from the accurate path's comment, which says an
7353 /// unusable coprocessor is reported as such *"even when the encoding is also
7354 /// a 64-bit one"*. Mutation-checking that against n64-systemtest changed
7355 /// nothing, and the reason is stronger than "the suite misses it":
7356 ///
7357 /// - `Op::is_64_bit` covers only CPU integer and load/store operations
7358 /// (`Dadd` … `Sdr`);
7359 /// - `unusable_coprocessor` returns `Some` only for COP0/COP1/COP2 encodings.
7360 ///
7361 /// No encoding is in both sets, so **no input can reach the second check by
7362 /// way of the first**. The ordering is unobservable rather than merely
7363 /// untested, which is why the oracle was silent.
7364 ///
7365 /// Swept over a structured slice of the encoding space rather than asserted
7366 /// against a copy of either list: a duplicated list stops covering the thing
7367 /// it duplicates the moment one side grows, which is the failure this test
7368 /// exists to catch. If `Dmfc1`/`Dmtc1`/`Dmfc0`/`Dmtc0` are ever added to
7369 /// `is_64_bit` — they *are* 64-bit operations, and whether their omission is
7370 /// correct is an open question, not a claim made here — this fails, and
7371 /// whoever adds them has to decide what the precedence should be with the
7372 /// case in front of them.
7373 #[test]
7374 fn ex_gates_first_two_refusals_cannot_both_apply() {
7375 use crate::cop0::reg;
7376
7377 let mut p = Pipeline::new();
7378 // 32-bit USER mode with every `CU` bit clear, so *every* coprocessor
7379 // encoding is unusable and `sixty_four_bit_is_reserved` holds. Kernel
7380 // mode would exempt COP0 and hide half the question.
7381 p.cop0.set_hardware(reg::STATUS, 0b10 << 3);
7382 assert!(
7383 p.sixty_four_bit_is_reserved(),
7384 "the sweep is meaningless unless 64-bit really is reserved here"
7385 );
7386
7387 let mut coprocessor = 0u32;
7388 let mut sixty_four = 0u32;
7389 for opcode in 0..64u32 {
7390 for rs in 0..32u32 {
7391 for funct in 0..64u32 {
7392 let d = decode((opcode << 26) | (rs << 21) | funct);
7393 let cop = p.unusable_coprocessor(d).is_some();
7394 let wide = d.op.is_64_bit();
7395 coprocessor += u32::from(cop);
7396 sixty_four += u32::from(wide);
7397 assert!(
7398 !(cop && wide),
7399 "opcode {opcode:#o} rs {rs:#o} funct {funct:#o} decodes to \
7400 {:?}, which is BOTH 64-bit and on an unusable coprocessor \
7401 -- `ex_gate`'s first two checks are no longer disjoint, so \
7402 their order is now observable and has to be justified \
7403 against the manual rather than inherited",
7404 d.op
7405 );
7406 }
7407 }
7408 }
7409 // A sweep that decoded nothing interesting would pass just as
7410 // convincingly. Both populations must be non-empty for the disjointness
7411 // above to be a claim about anything.
7412 assert!(
7413 coprocessor > 0 && sixty_four > 0,
7414 "the sweep found {coprocessor} coprocessor and {sixty_four} 64-bit \
7415 encodings; with either at zero it proves nothing"
7416 );
7417 }
7418}