Skip to main content

rustyn64_cpu/
exception.rs

1//! Exception dispatch — the epilogue and the vector table (T-12-002).
2//!
3//! Short, and every line of it is load-bearing. The hardware sequence is UM
4//! Fig. 6-14 (p. 201) for the general case and Fig. 6-15 (p. 203) for TLB
5//! refills.
6//!
7//! # The `EXL` gate is the whole point
8//!
9//! `EPC` and `Cause.BD` are written **only when `EXL` was already 0**. The
10//! flowchart's `EXL = 1?` test precedes the `EPC` write, commented *"Check for
11//! multiple exception"*, and UM §6.3.7 (p. 174) states the reason directly:
12//!
13//! > *"The EXL bit ... is set to 1 to keep the processor from overwriting the
14//! > address of the exception-causing instruction contained in the EPC
15//! > register in the event of another exception."*
16//!
17//! An implementation that always writes `EPC` passes **every** single-exception
18//! test and corrupts **every** nested one — and nesting is not exotic: UM §6.4.8
19//! (p. 188) describes a TLB refill handler taking a second TLB miss as the
20//! normal path.
21//!
22//! # The `0x080` trap
23//!
24//! UM Fig. 6-15 (p. 203) routes a refill with `EXL = 1` to offset `0x080`. That
25//! figure is **wrong**, and the manual contradicts it three times — see
26//! [`vector`] and accuracy-ledger S-3.
27
28use crate::cop0::{Cop0, reg};
29use crate::pipeline::Exception;
30use serde::{Deserialize, Serialize};
31
32/// `Cause.ExcCode` values (UM Table 6-2, p. 172).
33///
34/// The full architectural list, including the codes the N64 cannot reach, so
35/// that a value read out of `Cause` can always be named.
36pub mod exc_code {
37    /// Interrupt.
38    pub const INT: u64 = 0;
39    /// TLB modification.
40    pub const MOD: u64 = 1;
41    /// TLB miss on a load or instruction fetch.
42    pub const TLBL: u64 = 2;
43    /// TLB miss on a store.
44    pub const TLBS: u64 = 3;
45    /// Address error on a load or instruction fetch.
46    pub const ADEL: u64 = 4;
47    /// Address error on a store.
48    pub const ADES: u64 = 5;
49    /// Bus error on an instruction fetch.
50    pub const IBE: u64 = 6;
51    /// Bus error on a data reference.
52    pub const DBE: u64 = 7;
53    /// `SYSCALL`.
54    pub const SYS: u64 = 8;
55    /// `BREAK`.
56    pub const BP: u64 = 9;
57    /// Reserved instruction.
58    pub const RI: u64 = 10;
59    /// Coprocessor unusable.
60    pub const CPU: u64 = 11;
61    /// Arithmetic overflow.
62    pub const OV: u64 = 12;
63    /// Trap.
64    pub const TR: u64 = 13;
65    /// Floating-point exception.
66    pub const FPE: u64 = 15;
67    /// Watchpoint.
68    pub const WATCH: u64 = 23;
69}
70
71/// Which vector an exception uses.
72///
73/// Only three kinds exist, because the vector table has only three rows. Every
74/// exception that is not a TLB refill takes the general vector, including a TLB
75/// refill that arrives with `EXL` already set.
76#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
77pub enum VectorKind {
78    /// The common vector, offset `0x180`.
79    General,
80    /// 32-bit TLB refill, offset `0x000` — **only** with `EXL = 0`.
81    TlbRefill,
82    /// 64-bit TLB refill, offset `0x080` — **only** with `EXL = 0`.
83    XtlbRefill,
84    /// Cold reset, soft reset and NMI, which ignore `BEV` and go to
85    /// `0xBFC0_0000`.
86    Reset,
87}
88
89/// `Status.EXL`.
90const STATUS_EXL: u64 = 1 << 1;
91/// `Status.ERL`.
92const STATUS_ERL: u64 = 1 << 2;
93/// `Status.SR` (bit 20) — "Indicates a Soft Reset or NMI has occurred"
94/// (UM Fig. 6-6, the Self-Diagnostic Status Field).
95const STATUS_SR: u64 = 1 << 20;
96/// `Status.TS` (bit 21) — TLB shutdown, cleared by an NMI (UM Fig. 6-6).
97const STATUS_TS: u64 = 1 << 21;
98/// `Status.DS.BEV` (bit 22) — bootstrap exception vectors (UM Fig. 6-6).
99const STATUS_BEV: u64 = 1 << 22;
100
101/// The exception vector address (UM Tables 6-3/6-4, p. 181).
102///
103/// # The `EXL` rule, which is where implementations diverge
104///
105/// A TLB or XTLB refill uses its special vector **only when `EXL` is 0**. With
106/// `EXL` already set it takes the general vector at `0x180`. The manual says so
107/// three times:
108///
109/// - Tables 6-3/6-4 label the refill rows `TLB Miss, EXL=0` and
110///   `XTLB Miss, EXL=0`. There is no `EXL=1` refill row to select.
111/// - §6.4.8 (p. 187): *"All TLB Miss exceptions use these two special vectors
112///   when the EXL bit is set to 0 ... and they use the common exception vector
113///   when the EXL bit is set to 1."*
114/// - §6.4.8 (p. 188): *"This second exception goes to the common exception
115///   vector because the EXL bit of the Status register is set."*
116///
117/// **UM Fig. 6-15 (p. 203) disagrees and is wrong.** Its `EXL = 0? → No` arm
118/// leads to a box reading *"General Purpose Exception, Vec. Off. = 0x080"*,
119/// contradicting both tables, the prose twice, and Fig. 6-14 — which is the
120/// general-purpose handler and unconditionally uses `+ 0x180`. CEN64 routes to
121/// `0x180` with a source comment that `0x080` *"doesn't make any sense"*; it is
122/// right, and this is accuracy-ledger **S-3**.
123///
124/// Note also that UM p. 181's prose gives the `BEV = 1` general vector as
125/// `0x8000_0180`, which is a typo: per Table 6-4 the `BEV = 1` base is
126/// `0xBFC0_0200`, making the vector `0xBFC0_0380`. The 64-bit value in the same
127/// sentence is correct and proves it.
128#[must_use]
129pub const fn vector(status: u64, kind: VectorKind) -> u64 {
130    if matches!(kind, VectorKind::Reset) {
131        return 0xFFFF_FFFF_BFC0_0000;
132    }
133    let bev = status & STATUS_BEV != 0;
134    let exl = status & STATUS_EXL != 0;
135    let base: u64 = if bev {
136        0xFFFF_FFFF_BFC0_0200
137    } else {
138        0xFFFF_FFFF_8000_0000
139    };
140    // A refill with EXL already set is NOT a refill for vectoring purposes.
141    let offset: u64 = match kind {
142        VectorKind::TlbRefill if !exl => 0x000,
143        VectorKind::XtlbRefill if !exl => 0x080,
144        _ => 0x180,
145    };
146    base.wrapping_add(offset)
147}
148
149/// The `ExcCode` an exception reports.
150#[must_use]
151pub const fn exc_code_of(exc: Exception) -> u64 {
152    match exc {
153        // NMI writes no `Cause` at all -- `dispatch` intercepts it before this
154        // is consulted. It shares `INT` only to keep the match total; nothing
155        // reads it, and a test pins that `Cause` survives an NMI untouched.
156        Exception::Nmi | Exception::Interrupt => exc_code::INT,
157        // The direction matters: AdEL and AdES are different codes, and a
158        // handler distinguishes them.
159        Exception::AddressError { store: false } => exc_code::ADEL,
160        Exception::AddressError { store: true } => exc_code::ADES,
161        Exception::Overflow => exc_code::OV,
162        Exception::Syscall => exc_code::SYS,
163        Exception::Breakpoint => exc_code::BP,
164        Exception::Trap => exc_code::TR,
165        Exception::ReservedInstruction | Exception::CoprocessorReserved { .. } => exc_code::RI,
166        // Refill and Invalid share an ExcCode -- the handler tells them apart by
167        // which vector it was entered through, not by Cause.
168        Exception::TlbRefill { store: false, .. } | Exception::TlbInvalid { store: false } => {
169            exc_code::TLBL
170        }
171        Exception::TlbRefill { store: true, .. } | Exception::TlbInvalid { store: true } => {
172            exc_code::TLBS
173        }
174        Exception::TlbModified => exc_code::MOD,
175        Exception::CoprocessorUnusable { .. } => exc_code::CPU,
176        Exception::FloatingPoint => exc_code::FPE,
177    }
178}
179
180/// Which vector kind an exception takes.
181///
182/// Every exception this crate can currently raise takes the general vector; the
183/// refill kinds arrive with the TLB in T-12-004. Written as a function rather
184/// than assumed, so that adding a TLB exception forces a decision here.
185#[must_use]
186pub const fn vector_kind_of(exc: Exception) -> VectorKind {
187    match exc {
188        // NMI never reaches `vector_kind_of` through `dispatch` (it is
189        // intercepted first), but the match must stay total and Reset is the
190        // vector it would name.
191        Exception::Nmi => VectorKind::Reset,
192        Exception::Interrupt
193        | Exception::AddressError { .. }
194        | Exception::Overflow
195        | Exception::Syscall
196        | Exception::Breakpoint
197        | Exception::Trap
198        | Exception::ReservedInstruction
199        | Exception::CoprocessorReserved { .. }
200        // Invalid and Modified take the GENERAL vector: an entry was found, so
201        // there is nothing for a refill handler to refill.
202        | Exception::TlbInvalid { .. }
203        | Exception::CoprocessorUnusable { .. }
204        | Exception::FloatingPoint
205        | Exception::TlbModified => VectorKind::General,
206        // Only a genuine miss takes a refill vector, and only with EXL clear.
207        // Which of the two it is depends on the addressing width of the access
208        // that missed: 64-bit addressing has its own refill handler at 0x080,
209        // because the page-table walk it runs is a different one (it reads
210        // XContext, not Context).
211        Exception::TlbRefill { wide: true, .. } => VectorKind::XtlbRefill,
212        Exception::TlbRefill { .. } => VectorKind::TlbRefill,
213    }
214}
215
216/// Does this exception write `BadVAddr`?
217///
218/// **Only address errors and TLB exceptions.** UM §6.3.2 (p. 164) carries an
219/// explicit Caution that a Bus Error does *not* write it, because a bus error is
220/// not an address error — the address was fine, the transaction failed.
221#[must_use]
222pub const fn writes_bad_vaddr(exc: Exception) -> bool {
223    matches!(
224        exc,
225        Exception::AddressError { .. }
226            | Exception::TlbRefill { .. }
227            | Exception::TlbInvalid { .. }
228            | Exception::TlbModified
229    )
230}
231
232/// The result of dispatching an exception.
233#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
234pub struct Dispatch {
235    /// Where to fetch next.
236    pub vector: u64,
237    /// `PCycle`s the pipeline stalls.
238    ///
239    /// **2**, and documented rather than fitted: *"When a pipeline exception
240    /// condition occurs, the pipeline stalls for 2 `PCycles` and the instruction
241    /// causing the exception as well as all those that follow it in the pipeline
242    /// are aborted"* (UM §4.7, p. 114). Three files in this project once
243    /// recorded this as undocumented; see `docs/engineering-lessons.md` §3.3b.
244    pub stall_cycles: u32,
245}
246
247/// The documented stall on taking an exception (UM §4.7, p. 114).
248pub const EPILOGUE_STALL: u32 = 2;
249
250/// Perform the exception epilogue and return where to vector.
251///
252/// The order is the flowchart's (UM Fig. 6-14, p. 201), and it matters:
253///
254/// 1. `Cause.ExcCode` and `Cause.CE`.
255/// 2. `BadVAddr` — address errors and TLB exceptions only.
256/// 3. `EntryHi` / `Context` / `XContext` — TLB exceptions only (T-12-004).
257/// 4. **If `EXL` was 0**: `Cause.BD` and `EPC`. Otherwise both are left alone.
258/// 5. `EXL ← 1`.
259/// 6. `PC ← vector`.
260///
261/// `pc` is the faulting instruction's address and `in_delay_slot` says whether
262/// it sits in a branch delay slot; when it does, `EPC` gets **`pc - 4`** — the
263/// branch, not the delay-slot instruction — because that is where a handler must
264/// resume for the branch to be re-evaluated.
265/// The **NMI** exception (UM §6.4.6, p. 185).
266///
267/// Kept out of [`dispatch`] because almost none of that function applies. NMI
268/// writes `ErrorEPC`, not `EPC`; it sets `ERL`, not `EXL`; and it writes no
269/// `Cause` at all — *"the contents of all registers are preserved except for"*
270/// `ErrorEPC` and four `Status` bits. Routing it through the general path would
271/// overwrite `Cause.ExcCode` with a code NMI does not have.
272///
273/// It is **not maskable**: *"it occurs regardless of the settings of the EXL,
274/// ERL, and the IE bits in the Status register"*. The one condition it does
275/// respect is the instruction boundary — unlike Cold and Soft Reset, *"NMI is
276/// taken only at instruction boundaries"*, and the caches and memory are
277/// preserved.
278///
279/// `SR` is what distinguishes it from a reset afterwards: the manual notes
280/// there is otherwise *"no indication from the processor to differentiate
281/// between NMI & Soft Reset"*.
282pub fn nmi(cop0: &mut Cop0, pc: u64) -> Dispatch {
283    // The PC of the instruction boundary, so the handler can report where the
284    // machine was. No delay-slot adjustment: NMI is not attributed to an
285    // instruction the way a synchronous exception is.
286    cop0.set_hardware(reg::ERROR_EPC, pc);
287    let status = cop0.read(reg::STATUS);
288    cop0.set_hardware(
289        reg::STATUS,
290        (status & !STATUS_TS) | STATUS_ERL | STATUS_SR | STATUS_BEV,
291    );
292    Dispatch {
293        // Same location as Cold Reset, and unmapped/uncached so neither the
294        // cache nor the TLB needs to be valid to service it.
295        vector: vector(status, VectorKind::Reset),
296        stall_cycles: EPILOGUE_STALL,
297    }
298}
299
300/// Perform the exception epilogue and return where to vector.
301///
302/// The order is the flowchart's (UM Fig. 6-14, p. 201), and it matters:
303///
304/// 1. `Cause.ExcCode` and `Cause.CE`.
305/// 2. `BadVAddr` — address errors and TLB exceptions only.
306/// 3. `EntryHi` / `Context` / `XContext` — TLB exceptions only (T-12-004).
307/// 4. **If `EXL` was 0**: `Cause.BD` and `EPC`. Otherwise both are left alone.
308/// 5. `EXL ← 1`.
309/// 6. `PC ← vector`.
310///
311/// `pc` is the faulting instruction's address and `in_delay_slot` says whether
312/// it sits in a branch delay slot; when it does, `EPC` gets **`pc - 4`** — the
313/// branch, not the delay-slot instruction — because that is where a handler must
314/// resume for the branch to be re-evaluated.
315///
316/// [`nmi`] is intercepted at the top: none of the six steps above describe it.
317pub fn dispatch(
318    cop0: &mut Cop0,
319    exc: Exception,
320    pc: u64,
321    in_delay_slot: bool,
322    bad_vaddr: u64,
323) -> Dispatch {
324    if matches!(exc, Exception::Nmi) {
325        return nmi(cop0, pc);
326    }
327    let status = cop0.read(reg::STATUS);
328    let exl_was_set = status & STATUS_EXL != 0;
329
330    // 1. Cause.ExcCode. Written through `set_hardware` because ExcCode is
331    //    read-only to software -- routing it through `write` would need the
332    //    Cause write mask widened, which would also let MTC0 forge it.
333    let cause = cop0.read(reg::CAUSE);
334    let mut new_cause = (cause & !0x7C) | (exc_code_of(exc) << 2);
335    // `Cause.CE` (29:28) names the coprocessor for a Coprocessor Unusable
336    // exception. It is written on **every** exception -- the unit for a
337    // Coprocessor Unusable, zero otherwise -- not left alone for the others.
338    //
339    // This previously skipped the write for non-CpU exceptions, reasoning that
340    // `CE` is meaningless for them and that clearing it would erase a value the
341    // handler had not read yet. That reasoning is plausible and wrong: it leaves
342    // `CE` **stale**, so an exception following any Coprocessor Unusable reports
343    // that old unit number. n64-systemtest catches it exactly -- `TLB: Read
344    // after 4k page, expect TLBL` reads `Cause = 0x2000_0008` where hardware
345    // gives `0x8`, the difference being a `CE = 2` left over from the COP2
346    // usability check that ran earlier in the suite.
347    // `Cause.CE` names the coprocessor for BOTH kinds of coprocessor fault:
348    // unusable, and a reserved encoding inside a usable one. Only the first is
349    // obvious, and n64-systemtest compares the whole `Cause` register, so a
350    // missing `CE` on the second reads as a wrong exception.
351    let ce = match exc {
352        Exception::CoprocessorUnusable { unit } | Exception::CoprocessorReserved { unit } => {
353            u64::from(unit) & 0b11
354        }
355        _ => 0,
356    };
357    new_cause = (new_cause & !0x3000_0000) | (ce << 28);
358
359    // 4. EPC and Cause.BD, ONLY if EXL was clear. This is the gate; see the
360    //    module docs. Note it also governs BD, not just EPC -- a stale BD with a
361    //    fresh ExcCode would misreport the *first* exception's delay-slot state.
362    if exl_was_set {
363        // Deliberately nothing. The first exception's EPC is what the handler
364        // will eventually return to, and it must survive.
365    } else {
366        new_cause = if in_delay_slot {
367            new_cause | (1 << 31)
368        } else {
369            new_cause & !(1 << 31)
370        };
371        let epc = if in_delay_slot {
372            pc.wrapping_sub(4)
373        } else {
374            pc
375        };
376        cop0.set_hardware(reg::EPC, epc);
377    }
378    cop0.set_hardware(reg::CAUSE, new_cause);
379
380    // 2. BadVAddr -- and with it Context/XContext -- for the exceptions that
381    //    define a faulting address.
382    //
383    //    `Context.BadVPN2` and `XContext.BadVPN2` are NOT TLB-only. Hardware
384    //    fills them from the same latch that feeds `BadVAddr`, so an **address
385    //    error** updates them too, even though no TLB lookup took place.
386    //    n64-systemtest pins this directly: on an unaligned `LW` it expects
387    //    `Context = 0x0052_0000` for `BadVAddr = 0xA400_1A42`, which is exactly
388    //    `(BadVAddr >> 13) << 4`. Gating these on the TLB exceptions leaves
389    //    `Context` at zero and fails every unaligned-access test.
390    if writes_bad_vaddr(exc) {
391        cop0.set_hardware(reg::BAD_VADDR, bad_vaddr);
392        // Context: PTEBase (63:23) kept, BadVPN2 (22:4) = VA(31:13).
393        let ctx = cop0.read(reg::CONTEXT);
394        cop0.set_hardware(
395            reg::CONTEXT,
396            (ctx & 0xFFFF_FFFF_FF80_0000) | ((bad_vaddr >> 13) & 0x7_FFFF) << 4,
397        );
398        // XContext: PTEBase (63:33) kept, R (32:31) = VA(63:62),
399        // BadVPN2 (30:4) = VA(39:13).
400        let xctx = cop0.read(reg::XCONTEXT);
401        cop0.set_hardware(
402            reg::XCONTEXT,
403            (xctx & 0xFFFF_FFFE_0000_0000)
404                | (((bad_vaddr >> 62) & 0b11) << 31)
405                | (((bad_vaddr >> 13) & 0x7FF_FFFF) << 4),
406        );
407    }
408
409    // 3. EntryHi -- written whenever BadVAddr is, address errors INCLUDED. The UM
410    //    (§6.4.7, p. 186 / Fig. 6-14) calls it "undefined" on an address error,
411    //    but the hardware oracle pins a definite value: n64-systemtest's `tlb64`
412    //    group triggers an AdEL from a 64-bit load and asserts
413    //    `EntryHi == (VPN2 << 13) | (R << 62)` from the faulting address (ledger
414    //    R-20). "Undefined" in the manual is a license, not a guarantee of
415    //    non-writing, and the oracle wins (ADR 0005). This matches how Context /
416    //    XContext are already filled on an address error two blocks up -- EntryHi
417    //    was the one register wrongly held back. The old worry that a stale
418    //    EntryHi corrupts a later `TLBWR` is unfounded: a real TLB miss reloads
419    //    EntryHi before its handler writes the entry.
420    if writes_bad_vaddr(exc) {
421        let vpn2 = bad_vaddr & crate::tlb::VPN2_MASK;
422        let hi = cop0.read(reg::ENTRY_HI);
423        // The `R` field (63:62) comes from the faulting address too, not just
424        // `VPN2` -- leaving it zero puts every sign-extended kernel fault in
425        // region 0.
426        let region = bad_vaddr & 0xC000_0000_0000_0000;
427        // ASID is preserved; VPN2 and R are replaced.
428        cop0.set_hardware(reg::ENTRY_HI, (hi & crate::tlb::ASID_MASK) | vpn2 | region);
429    }
430
431    // 5. EXL. Setting it puts the CPU in Kernel mode with interrupts disabled,
432    //    which is what makes the handler's own first instructions safe.
433    cop0.set_hardware(reg::STATUS, status | STATUS_EXL);
434
435    Dispatch {
436        vector: vector(status, vector_kind_of(exc)),
437        stall_cycles: EPILOGUE_STALL,
438    }
439}
440
441/// `ERET` — return from an exception (UM Ch. 16, p. 434).
442///
443/// Returns where to resume. Three rules, all easy to half-implement:
444///
445/// - If `Status.ERL` is set, resume at `ErrorEPC` and clear **`ERL`**; otherwise
446///   resume at `EPC` and clear **`EXL`**. Clearing the wrong one leaves the CPU
447///   stuck in kernel mode or returns to the wrong address.
448/// - **`LLbit` is always cleared**, which is the *only* thing besides cache
449///   invalidation that clears it (UM §3.1) — the other half of the `LL`/`SC`
450///   contract implemented in Sprint 1.
451/// - `ERET` has **no delay slot** and must not itself be placed in one.
452///
453/// The caller clears the link bit; this function reports that it must.
454#[must_use]
455pub fn eret(cop0: &mut Cop0) -> u64 {
456    let status = cop0.read(reg::STATUS);
457    if status & STATUS_ERL != 0 {
458        cop0.set_hardware(reg::STATUS, status & !STATUS_ERL);
459        cop0.read(reg::ERROR_EPC)
460    } else {
461        cop0.set_hardware(reg::STATUS, status & !STATUS_EXL);
462        cop0.read(reg::EPC)
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469
470    /// Baseline vectoring, both `BEV` values.
471    #[test]
472    fn the_vector_table_matches_the_manual() {
473        let bev0 = 0;
474        let bev1 = STATUS_BEV;
475        assert_eq!(vector(bev0, VectorKind::General), 0xFFFF_FFFF_8000_0180);
476        assert_eq!(vector(bev0, VectorKind::TlbRefill), 0xFFFF_FFFF_8000_0000);
477        assert_eq!(vector(bev0, VectorKind::XtlbRefill), 0xFFFF_FFFF_8000_0080);
478        assert_eq!(
479            vector(bev1, VectorKind::General),
480            0xFFFF_FFFF_BFC0_0380,
481            "UM p.181's prose says 0x8000_0180 here and is a typo; Table 6-4 wins"
482        );
483        assert_eq!(vector(bev1, VectorKind::TlbRefill), 0xFFFF_FFFF_BFC0_0200);
484        assert_eq!(vector(bev1, VectorKind::XtlbRefill), 0xFFFF_FFFF_BFC0_0280);
485        assert_eq!(
486            vector(bev1, VectorKind::Reset),
487            0xFFFF_FFFF_BFC0_0000,
488            "reset ignores BEV"
489        );
490    }
491
492    /// Accuracy-ledger **S-3**. A refill arriving with `EXL` already set takes
493    /// the **general** vector, not `0x080`.
494    ///
495    /// UM Fig. 6-15 (p. 203) says `0x080` and is wrong; Tables 6-3/6-4 and
496    /// §6.4.8 (twice) say otherwise, and Fig. 6-14 unconditionally uses `0x180`.
497    /// This test exists so nobody "fixes" it back to the figure.
498    #[test]
499    fn a_refill_with_exl_already_set_uses_the_general_vector() {
500        let exl = STATUS_EXL;
501        assert_eq!(
502            vector(exl, VectorKind::TlbRefill),
503            0xFFFF_FFFF_8000_0180,
504            "ledger S-3: NOT 0x8000_0000"
505        );
506        assert_eq!(
507            vector(exl, VectorKind::XtlbRefill),
508            0xFFFF_FFFF_8000_0180,
509            "ledger S-3: NOT 0x8000_0080 -- this is the value UM Fig. 6-15 gives"
510        );
511        // And with BEV too, since the two rules compose.
512        assert_eq!(
513            vector(exl | STATUS_BEV, VectorKind::XtlbRefill),
514            0xFFFF_FFFF_BFC0_0380
515        );
516    }
517
518    /// The ordinary epilogue: `EPC` gets the faulting PC, `BD` is clear, `EXL`
519    /// is set, and the `ExcCode` is right.
520    #[test]
521    fn a_first_exception_records_epc_and_sets_exl() {
522        let mut c = Cop0::new();
523        c.set_hardware(reg::STATUS, 0);
524        let d = dispatch(&mut c, Exception::Overflow, 0x8000_1000, false, 0);
525        assert_eq!(c.read(reg::EPC), 0x8000_1000);
526        assert_eq!((c.read(reg::CAUSE) >> 2) & 0x1F, exc_code::OV);
527        assert_eq!(c.read(reg::CAUSE) & (1 << 31), 0, "BD clear");
528        assert_ne!(c.read(reg::STATUS) & STATUS_EXL, 0, "EXL set");
529        assert_eq!(d.vector, 0xFFFF_FFFF_8000_0180);
530        assert_eq!(d.stall_cycles, 2, "UM §4.7 p.114");
531    }
532
533    /// In a delay slot, `EPC` points at the **branch** and `BD` is set.
534    #[test]
535    fn a_delay_slot_exception_reports_the_branch_address() {
536        let mut c = Cop0::new();
537        c.set_hardware(reg::STATUS, 0);
538        dispatch(&mut c, Exception::Overflow, 0x8000_1004, true, 0);
539        assert_eq!(
540            c.read(reg::EPC),
541            0x8000_1000,
542            "EPC = pc - 4, the branch itself"
543        );
544        assert_ne!(c.read(reg::CAUSE) & (1 << 31), 0, "BD set");
545    }
546
547    /// **The nested case, and the one that separates a correct epilogue from one
548    /// that merely passes.** With `EXL` already set, `EPC` and `BD` must not be
549    /// touched — otherwise the first exception's return address is destroyed.
550    ///
551    /// An implementation that always writes `EPC` passes every other test here.
552    #[test]
553    fn a_nested_exception_does_not_overwrite_epc_or_bd() {
554        let mut c = Cop0::new();
555        c.set_hardware(reg::STATUS, 0);
556
557        // First exception, in a delay slot: EPC = branch, BD = 1.
558        dispatch(&mut c, Exception::Overflow, 0x8000_1004, true, 0);
559        assert_eq!(c.read(reg::EPC), 0x8000_1000);
560        assert_ne!(c.read(reg::CAUSE) & (1 << 31), 0);
561
562        // Second exception while EXL is still set, NOT in a delay slot and at a
563        // completely different address. Both EPC and BD must survive.
564        dispatch(
565            &mut c,
566            Exception::AddressError { store: true },
567            0x8000_9999,
568            false,
569            0xDEAD,
570        );
571        assert_eq!(
572            c.read(reg::EPC),
573            0x8000_1000,
574            "the FIRST exception's EPC must survive (UM §6.3.7)"
575        );
576        assert_ne!(
577            c.read(reg::CAUSE) & (1 << 31),
578            0,
579            "and so must its BD -- a stale ExcCode with a fresh BD misreports it"
580        );
581        // ExcCode and BadVAddr, by contrast, DO update: they describe the
582        // current exception, not the return path.
583        assert_eq!((c.read(reg::CAUSE) >> 2) & 0x1F, exc_code::ADES);
584        assert_eq!(c.read(reg::BAD_VADDR), 0xDEAD);
585    }
586
587    /// **Refill and Invalid share an `ExcCode` and differ only in vector.** The
588    /// handler tells them apart by which entry point it was reached through, so
589    /// getting the vector wrong sends a page-protection fault to the refill
590    /// handler — which would refill a mapping that already exists.
591    ///
592    /// This is the one distinction `Cause` cannot express, so nothing but the
593    /// vector check can catch it.
594    #[test]
595    fn refill_and_invalid_share_an_exccode_but_not_a_vector() {
596        for store in [false, true] {
597            let refill = Exception::TlbRefill { store, wide: false };
598            let invalid = Exception::TlbInvalid { store };
599            assert_eq!(
600                exc_code_of(refill),
601                exc_code_of(invalid),
602                "the ExcCode cannot distinguish them"
603            );
604
605            assert_eq!(vector_kind_of(refill), VectorKind::TlbRefill);
606            assert_eq!(
607                vector_kind_of(invalid),
608                VectorKind::General,
609                "an entry WAS found, so there is nothing to refill"
610            );
611            assert_eq!(vector(0, refill_kind(store)), 0xFFFF_FFFF_8000_0000);
612            assert_eq!(
613                vector(0, vector_kind_of(invalid)),
614                0xFFFF_FFFF_8000_0180,
615                "Invalid must NOT reach the refill vector"
616            );
617        }
618        assert_eq!(
619            vector_kind_of(Exception::TlbModified),
620            VectorKind::General,
621            "Modified likewise -- the mapping exists, it is just not writable"
622        );
623    }
624
625    /// Helper for the test above: the refill kind, independent of direction.
626    const fn refill_kind(store: bool) -> VectorKind {
627        vector_kind_of(Exception::TlbRefill { store, wide: false })
628    }
629
630    /// **64-bit addressing selects the XTLB refill vector, at `0x080`.**
631    ///
632    /// The two refills carry the same `ExcCode` and differ only in entry point,
633    /// exactly as refill and invalid do -- and the handler behind `0x080` walks
634    /// a different page table (it reads `XContext`, not `Context`). Sending a
635    /// 64-bit miss to `0x000` runs the 32-bit walker over a 64-bit address.
636    #[test]
637    fn a_refill_in_64_bit_addressing_takes_the_xtlb_vector() {
638        for store in [false, true] {
639            let narrow = Exception::TlbRefill { store, wide: false };
640            let wide = Exception::TlbRefill { store, wide: true };
641            assert_eq!(
642                exc_code_of(narrow),
643                exc_code_of(wide),
644                "the ExcCode cannot distinguish them either"
645            );
646            assert_eq!(vector_kind_of(narrow), VectorKind::TlbRefill);
647            assert_eq!(vector_kind_of(wide), VectorKind::XtlbRefill);
648            assert_eq!(vector(0, vector_kind_of(narrow)), 0xFFFF_FFFF_8000_0000);
649            assert_eq!(vector(0, vector_kind_of(wide)), 0xFFFF_FFFF_8000_0080);
650        }
651    }
652
653    /// `AdEL` and `AdES` are different codes; conflating them loses information
654    /// a handler uses.
655    #[test]
656    fn address_error_reports_its_direction() {
657        assert_eq!(
658            exc_code_of(Exception::AddressError { store: false }),
659            exc_code::ADEL
660        );
661        assert_eq!(
662            exc_code_of(Exception::AddressError { store: true }),
663            exc_code::ADES
664        );
665        assert_ne!(exc_code::ADEL, exc_code::ADES);
666    }
667
668    /// `BadVAddr` is written for address errors and **not** for anything that is
669    /// not an addressing failure (UM §6.3.2 p. 164 Caution).
670    #[test]
671    fn bad_vaddr_is_written_only_for_addressing_failures() {
672        let mut c = Cop0::new();
673        c.set_hardware(reg::STATUS, 0);
674        dispatch(&mut c, Exception::Syscall, 0x8000_1000, false, 0x1234);
675        assert_eq!(
676            c.read(reg::BAD_VADDR),
677            0,
678            "SYSCALL is not an addressing failure"
679        );
680
681        let mut c = Cop0::new();
682        c.set_hardware(reg::STATUS, 0);
683        dispatch(
684            &mut c,
685            Exception::AddressError { store: false },
686            0x8000_1000,
687            false,
688            0x1234,
689        );
690        assert_eq!(c.read(reg::BAD_VADDR), 0x1234);
691    }
692
693    /// `ERET` clears `EXL` and returns to `EPC` in the ordinary case.
694    #[test]
695    fn eret_returns_to_epc_and_clears_exl() {
696        let mut c = Cop0::new();
697        c.set_hardware(reg::STATUS, STATUS_EXL);
698        c.set_hardware(reg::EPC, 0x8000_2000);
699        assert_eq!(eret(&mut c), 0x8000_2000);
700        assert_eq!(c.read(reg::STATUS) & STATUS_EXL, 0, "EXL cleared");
701    }
702
703    /// With `ERL` set, `ERET` uses `ErrorEPC` and clears **`ERL`**, leaving
704    /// `EXL` alone. Clearing the wrong bit is the classic version of this bug.
705    #[test]
706    fn eret_prefers_error_epc_when_erl_is_set() {
707        let mut c = Cop0::new();
708        c.set_hardware(reg::STATUS, STATUS_ERL | STATUS_EXL);
709        c.set_hardware(reg::EPC, 0x8000_2000);
710        c.set_hardware(reg::ERROR_EPC, 0x8000_3000);
711        assert_eq!(eret(&mut c), 0x8000_3000, "ErrorEPC wins when ERL is set");
712        let s = c.read(reg::STATUS);
713        assert_eq!(s & STATUS_ERL, 0, "ERL cleared");
714        assert_ne!(s & STATUS_EXL, 0, "EXL untouched -- clearing it is the bug");
715    }
716
717    /// Cold reset leaves `ERL` set (UM §6.4.4), so an `ERET` immediately after
718    /// reset takes the `ErrorEPC` path. Worth pinning because it is the state
719    /// the machine actually boots in.
720    #[test]
721    fn a_freshly_reset_cop0_erets_through_error_epc() {
722        let mut c = Cop0::new();
723        assert_ne!(c.read(reg::STATUS) & STATUS_ERL, 0, "reset sets ERL");
724        c.set_hardware(reg::ERROR_EPC, 0xBFC0_0000);
725        assert_eq!(eret(&mut c), 0xBFC0_0000);
726    }
727
728    /// **`Context.BadVPN2` is written on an ADDRESS ERROR, not only on a TLB
729    /// exception.** Hardware fills it from the same latch that feeds `BadVAddr`,
730    /// so no TLB lookup need have happened.
731    ///
732    /// n64-systemtest pins the exact value: an unaligned `LW` at
733    /// `0xA400_1A42` must leave `Context = 0x0052_0000`, which is
734    /// `(BadVAddr >> 13) << 4`. Gating this on the TLB exceptions leaves it at
735    /// zero and fails every unaligned-access test.
736    #[test]
737    fn an_address_error_writes_context_even_though_no_tlb_lookup_occurred() {
738        let mut c = Cop0::new();
739        c.set_hardware(reg::STATUS, 0);
740        c.set_hardware(reg::CONTEXT, 0);
741        dispatch(
742            &mut c,
743            Exception::AddressError { store: false },
744            0x8000_1000,
745            false,
746            0xFFFF_FFFF_A400_1A42,
747        );
748        assert_eq!(
749            c.read(reg::CONTEXT),
750            0x0052_0000,
751            "BadVPN2 = (BadVAddr >> 13) << 4"
752        );
753    }
754
755    /// `Context`'s `PTEBase` (63:23) is **preserved** across the update -- it is
756    /// software's page-table pointer, and clobbering it would send the refill
757    /// handler to the wrong table.
758    #[test]
759    fn an_address_error_preserves_the_context_pte_base() {
760        let mut c = Cop0::new();
761        c.set_hardware(reg::STATUS, 0);
762        c.set_hardware(reg::CONTEXT, 0xFFFF_FFFF_FF80_0000);
763        dispatch(
764            &mut c,
765            Exception::AddressError { store: false },
766            0x8000_1000,
767            false,
768            0xFFFF_FFFF_A400_1A42,
769        );
770        assert_eq!(
771            c.read(reg::CONTEXT),
772            0xFFFF_FFFF_FFD2_0000,
773            "PTEBase kept, BadVPN2 replaced"
774        );
775    }
776
777    /// **`EntryHi`'s VPN2/R IS written on an address error** (ledger R-20), the
778    /// same as `Context`/`XContext` -- the UM calls it "undefined" but the
779    /// hardware oracle (n64-systemtest `tlb64`) pins `(VPN2 << 13) | (R << 62)`
780    /// from the faulting address. ASID is preserved.
781    ///
782    /// Mutation guard: the address is chosen so both `VPN2` (bits 39:13) and the
783    /// region (bits 63:62) are non-zero, and the ASID is pre-seeded, so gating
784    /// `EntryHi` back to TLB-only, dropping the region, or clobbering the ASID
785    /// each turns the single assertion red.
786    #[test]
787    fn an_address_error_writes_entry_hi_vpn2_and_region() {
788        let mut c = Cop0::new();
789        c.set_hardware(reg::STATUS, 0);
790        // Pre-seed a non-zero ASID (low 8 bits) that must survive.
791        c.set_hardware(reg::ENTRY_HI, 0x5A);
792        let bad = 0xC000_00FF_0020_FFF4u64; // region 3, VPN2 non-zero, unaligned low bits irrelevant
793        dispatch(
794            &mut c,
795            Exception::AddressError { store: false },
796            0x8000_1000,
797            false,
798            bad,
799        );
800        let vpn = (bad >> 13) & 0x7FF_FFFF; // M27
801        let r = (bad >> 62) & 0b11;
802        let expected = (vpn << 13) | (r << 62) | 0x5A;
803        assert_eq!(
804            c.read(reg::ENTRY_HI),
805            expected,
806            "EntryHi = (VPN2 << 13) | (R << 62), ASID preserved",
807        );
808    }
809
810    /// **`Cause.CE` is written on every exception, not only on Coprocessor
811    /// Unusable.** Leaving it alone lets a stale unit number survive into an
812    /// unrelated exception.
813    ///
814    /// n64-systemtest reads `Cause = 0x2000_0008` for a `TLBL` where hardware
815    /// gives `0x8` — a `CE = 2` left over from an earlier COP2 usability check.
816    #[test]
817    fn a_later_exception_does_not_inherit_a_stale_cause_ce() {
818        let mut c = Cop0::new();
819        c.set_hardware(reg::STATUS, 0);
820        // A COP2 usability fault sets CE = 2.
821        dispatch(
822            &mut c,
823            Exception::CoprocessorUnusable { unit: 2 },
824            0x8000_1000,
825            false,
826            0,
827        );
828        assert_eq!(c.read(reg::CAUSE) & 0x3000_0000, 0x2000_0000, "CE = 2");
829
830        // Clear EXL so the next dispatch is a fresh exception, then take a TLB
831        // fault: CE must be back to zero.
832        c.set_hardware(reg::STATUS, 0);
833        dispatch(
834            &mut c,
835            Exception::TlbInvalid { store: false },
836            0x8000_2000,
837            false,
838            0x1234,
839        );
840        assert_eq!(
841            c.read(reg::CAUSE) & 0x3000_0000,
842            0,
843            "a TLBL must not inherit the COP2 unit number"
844        );
845        assert_eq!(c.read(reg::CAUSE) & 0x7C, 2 << 2, "and still reports TLBL");
846    }
847}