Skip to main content

rustyn64_cpu/
cop1.rs

1//! COP1 **control** registers (T-12-006).
2//!
3//! `CTC1` / `CFC1` on `FCR31` (the FCSR) and `FCR0` (the revision register), and
4//! nothing else. **FPU arithmetic is Sprint 3** — this module exists for one
5//! reason, stated plainly so it does not quietly grow:
6//!
7//! n64-systemtest's `entrypoint()` calls `set_fcsr(...)` — which is
8//! `ctc1::<31>` — as its **fourth statement**
9//! (`ref-proj/n64-systemtest/src/main.rs`). Without COP1 control the suite dies
10//! three statements after entry and reports nothing at all, so every COP0 and
11//! TLB test in Sprint 2 is unreachable behind it.
12//!
13//! # Scope discipline
14//!
15//! FCSR needs *storage* with correct bit semantics, not *behavior*: nothing
16//! acts on the rounding mode or the enable bits until COP1 arithmetic lands.
17//! Adding an arithmetic path here would make this ticket Sprint 3 by stealth.
18
19use serde::{Deserialize, Serialize};
20
21/// `FCR0` — the FPU implementation/revision register.
22///
23/// Read-only. `Imp` (bits 15:8) is **`0x0A`**, and the revision half is `0x00`.
24///
25/// # `Imp` is NOT the same as `PRId`'s
26///
27/// This was `0x0B00` on the reasoning that the FPU's implementation number
28/// matches the CPU's — and the N64brew Wiki says so outright: *"All VR4300
29/// units will report 0x0B (11) for the implementation number"*
30/// (`n64brew_wiki/markdown/VR4300.md`). That is **wrong**, and it is wrong in
31/// this project's designated primary hardware reference, which is worth knowing
32/// before trusting the wiki on a single-value claim.
33///
34/// Two independent sources give `0x0A00`:
35///
36/// - n64-systemtest asserts it directly, and it runs on real hardware.
37/// - cen64 hardcodes `0xa00` with the comment *"fpu version of both 0xb22 and
38///   0xb10 N64s"* — i.e. checked against two console revisions.
39///
40/// `PRId.Imp` really is `0x0B`; the two registers identify different units, and
41/// the near-identical values are what makes the conflation easy. Accuracy
42/// ledger **S-4**.
43pub const FCR0_REVISION: u32 = 0x0A00;
44
45/// The writable bits of `FCR31` (FCSR).
46///
47/// Bits 25 and 22..=18 are unused on the VR4300 and read zero. Everything else
48/// is software-writable, including the `Cause` bits — software clears them by
49/// writing, which is how an FP exception handler acknowledges.
50///
51/// | Bits | Field |
52/// | --- | --- |
53/// | 24 | `FS` — flush denormals to zero |
54/// | 23 | `C` — condition |
55/// | 17..=12 | `Cause` (unimplemented, invalid, div0, overflow, underflow, inexact) |
56/// | 11..=7 | `Enable` |
57/// | 6..=2 | `Flags` |
58/// | 1..=0 | `RM` — rounding mode |
59pub const FCSR_MASK: u32 = 0x0183_FFFF;
60
61/// COP1 control-register state.
62///
63/// Deliberately **only** the control registers: the 32 floating-point data
64/// registers arrive with the arithmetic in Sprint 3, and putting them here now
65/// would be state nothing reads.
66#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
67pub struct Cop1Control {
68    /// `FCR31`, the Floating-point Control/Status Register.
69    fcsr: u32,
70}
71
72impl Cop1Control {
73    /// Power-on state: all zero.
74    ///
75    /// The manual does not define a reset value for `FCSR`; ADR 0004 requires
76    /// reproducibility, so it is a documented zero. Software sets it up — which
77    /// is exactly what n64-systemtest does on its fourth instruction.
78    #[must_use]
79    pub const fn new() -> Self {
80        Self { fcsr: 0 }
81    }
82
83    /// `CFC1 rt, fs` — read a control register.
84    ///
85    /// Only `FCR0` and `FCR31` exist. Every other `fs` reads **zero**, which is
86    /// a choice rather than a documented fact — the manual does not say — and is
87    /// recorded as such.
88    #[must_use]
89    pub const fn cfc1(&self, fs: u8) -> u32 {
90        match fs {
91            0 => FCR0_REVISION,
92            31 => self.fcsr,
93            _ => 0,
94        }
95    }
96
97    /// `CTC1 rt, fs` — write a control register.
98    ///
99    /// `FCR0` is read-only, so a write to it is discarded rather than stored.
100    pub const fn ctc1(&mut self, fs: u8, value: u32) {
101        if fs == 31 {
102            self.fcsr = value & FCSR_MASK;
103        }
104    }
105
106    /// The raw `FCSR` value, for the arithmetic path in Sprint 3.
107    #[must_use]
108    pub const fn fcsr(&self) -> u32 {
109        self.fcsr
110    }
111
112    /// `FS` — flush denormals to zero (bit 24).
113    ///
114    /// Read by nothing yet. Exposed because n64-systemtest **sets** it during
115    /// startup, so an implementation that silently dropped the bit would look
116    /// fine until the first denormal.
117    #[must_use]
118    pub const fn flush_denorm_to_zero(&self) -> bool {
119        self.fcsr & (1 << 24) != 0
120    }
121
122    /// `RM` — the rounding mode (bits 1..=0).
123    #[must_use]
124    pub const fn rounding_mode(&self) -> u8 {
125        (self.fcsr & 0b11) as u8
126    }
127
128    /// The `Enable` field (bits 11..=7).
129    #[must_use]
130    pub const fn enables(&self) -> u32 {
131        (self.fcsr >> 7) & 0x1F
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    /// The exact sequence n64-systemtest performs on its fourth statement:
140    /// `FCSR::new().with_flush_denorm_to_zero(true).with_enable_invalid_operation(true)`.
141    ///
142    /// If this does not round-trip, the suite cannot start and Sprint 2 has no
143    /// oracle at all.
144    #[test]
145    fn the_n64_systemtest_startup_fcsr_round_trips() {
146        let mut c = Cop1Control::new();
147        // bit 24 = flush_denorm_to_zero, bit 11 = enable_invalid_operation.
148        let want = (1 << 24) | (1 << 11);
149        c.ctc1(31, want);
150        assert_eq!(c.cfc1(31), want, "CTC1 then CFC1 must round-trip");
151        assert!(c.flush_denorm_to_zero());
152        assert_eq!(c.enables(), 1 << 4, "enable_invalid_operation");
153    }
154
155    /// `FCR0` is read-only and reports the VR4300 implementation number.
156    #[test]
157    fn fcr0_is_read_only_and_reports_the_implementation() {
158        let mut c = Cop1Control::new();
159        assert_eq!(c.cfc1(0), FCR0_REVISION);
160        assert_eq!(
161            (c.cfc1(0) >> 8) & 0xFF,
162            0x0A,
163            "FCR0.Imp is 0x0A -- NOT PRId's 0x0B, and not the 0x0B the wiki claims"
164        );
165        c.ctc1(0, 0xFFFF_FFFF);
166        assert_eq!(c.cfc1(0), FCR0_REVISION, "writes are discarded");
167    }
168
169    /// The unused bits read zero rather than storing what was written.
170    #[test]
171    fn the_unused_fcsr_bits_read_zero() {
172        let mut c = Cop1Control::new();
173        c.ctc1(31, 0xFFFF_FFFF);
174        let v = c.cfc1(31);
175        assert_eq!(v & (1 << 25), 0, "bit 25 is unused");
176        assert_eq!(v & 0x007C_0000, 0, "bits 22..=18 are unused");
177        assert_eq!(v, FCSR_MASK, "everything else is writable");
178    }
179
180    /// The `Cause` bits are software-writable — that is how a handler
181    /// acknowledges an FP exception. Making them read-only looks defensive and
182    /// leaves the handler unable to clear them.
183    #[test]
184    fn the_cause_bits_are_software_writable() {
185        let mut c = Cop1Control::new();
186        c.ctc1(31, 0x0003_F000);
187        assert_eq!(c.cfc1(31), 0x0003_F000, "all six Cause bits took");
188        c.ctc1(31, 0);
189        assert_eq!(c.cfc1(31), 0, "and can be cleared again");
190    }
191
192    /// Rounding mode is stored, though nothing acts on it until Sprint 3.
193    #[test]
194    fn the_rounding_mode_is_stored_even_though_nothing_reads_it_yet() {
195        let mut c = Cop1Control::new();
196        for rm in 0..4u32 {
197            c.ctc1(31, rm);
198            assert_eq!(u32::from(c.rounding_mode()), rm);
199        }
200    }
201}