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}