Skip to main content

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}