rustyn64_cpu/fpr.rs
1//! The floating-point register file (T-13-001).
2//!
3//! 32 physical 64-bit **FGRs**. How software sees them depends on
4//! `Status.FR` (UM §6.3.5, and the FPR/FGR figures in Ch. 7):
5//!
6//! | `FR` | Register `n` addresses |
7//! | --- | --- |
8//! | 1 | FGR *n* — 32 independent 64-bit registers |
9//! | 0 | FGR *n & !1* — **odd FGRs are not addressable at all** |
10//!
11//! With `FR = 0` there are 16 usable 64-bit registers, and a 32-bit access
12//! picks a half of one of them: an **even** register number is the low half,
13//! an **odd** register number is the **high** half of its even partner.
14//!
15//! # This is not the "FGR pair" model, and the difference is observable
16//!
17//! It is natural to read "`FR = 0` uses register pairs" as *the value is
18//! `FGR[n+1]:FGR[n]`, assembled from two registers' low halves*. This module
19//! had exactly that, and it is wrong: it makes `MTC1 $1` write FGR1, where
20//! hardware writes the upper half of FGR0 and leaves FGR1 untouched.
21//!
22//! n64-systemtest pins it directly. In half mode, after `MTC1 $1`:
23//!
24//! ```text
25//! DMFC1(0) == 0x01234567_89ABCDEF <- the write landed in FGR0's HIGH half
26//! DMFC1(1) == 0x44445555_66667777 <- unchanged
27//! ```
28//!
29//! # Three write behaviors, not one
30//!
31//! - [`Fpr::write_s`] — `MTC1`/`LWC1`: deposit 32 bits, **preserve** the other
32//! half of the register.
33//! - [`Fpr::write_s_arith`] — a single-precision arithmetic result: **clear**
34//! the other half. The suite's *"Upper bits of 32 bit operation"* reads the
35//! destination back with `DMFC1` after an `ADD.S` and expects zero there.
36//! - [`Fpr::write_d`] — a 64-bit value, whole register.
37//!
38//! Collapsing the first two is invisible until something reads the register at
39//! a different width, which is precisely what those tests do.
40
41use serde::{Deserialize, Serialize};
42
43/// The 32 physical floating-point general registers.
44#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
45pub struct Fpr {
46 /// Raw FGR storage. Prefer the accessors: they apply the `FR` view, which
47 /// direct indexing silently gets wrong for `FR = 0` doubles.
48 fgr: [u64; 32],
49}
50
51impl Default for Fpr {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57impl Fpr {
58 /// Power-on state.
59 ///
60 /// The manual does not define one; ADR 0004 requires reproducibility, so it
61 /// is a documented zero.
62 #[must_use]
63 pub const fn new() -> Self {
64 Self { fgr: [0; 32] }
65 }
66
67 /// Read a 32-bit value from FPR `n` under the current `FR` view.
68 ///
69 /// | `FR` | Register `n` maps to |
70 /// | --- | --- |
71 /// | 1 | the **low** half of FGR *n* |
72 /// | 0 | FGR *n & !1* — its **low** half for even `n`, its **HIGH** half for odd `n` |
73 ///
74 /// The `FR = 0` row is the whole subtlety, and it is not what "the pair
75 /// `FGR[n+1]:FGR[n]`" suggests: an **odd** register in half mode is the
76 /// upper 32 bits of its *even partner*, and odd FGRs are not addressable
77 /// at all. n64-systemtest pins it directly — after `MTC1 $1` in half mode,
78 /// `DMFC1(0)` shows the written value in its high half while `DMFC1(1)` is
79 /// **unchanged**.
80 #[must_use]
81 pub const fn read_s(&self, n: u8, fr: bool) -> u32 {
82 let i = (n & 31) as usize;
83 if fr {
84 self.fgr[i] as u32
85 } else if i & 1 == 1 {
86 (self.fgr[i & !1] >> 32) as u32
87 } else {
88 self.fgr[i] as u32
89 }
90 }
91
92 /// Write a 32-bit value to FPR `n` under the current `FR` view.
93 ///
94 /// The other half of the 64-bit register is **preserved** — this models
95 /// `MTC1`/`LWC1`, which deposit 32 bits and leave the rest alone. An
96 /// arithmetic `.S` result does not behave this way; see
97 /// [`Fpr::write_s_arith`].
98 ///
99 /// See [`Fpr::read_s`] for the `FR = 0` mapping.
100 pub const fn write_s(&mut self, n: u8, fr: bool, v: u32) {
101 let i = (n & 31) as usize;
102 if !fr && i & 1 == 1 {
103 let e = i & !1;
104 self.fgr[e] = (self.fgr[e] & 0xFFFF_FFFF) | ((v as u64) << 32);
105 } else {
106 let e = if fr { i } else { i & !1 };
107 self.fgr[e] = (self.fgr[e] & 0xFFFF_FFFF_0000_0000) | v as u64;
108 }
109 }
110
111 /// Write a single-precision **arithmetic result**, which clears the other
112 /// half of the destination rather than preserving it.
113 ///
114 /// This is what separates an arithmetic write-back from `MTC1`.
115 /// n64-systemtest's *"Upper bits of 32 bit operation"* reads the
116 /// destination back with `DMFC1` after an `ADD.S` and expects the upper
117 /// 32 bits to be **zero**, not the register's previous contents.
118 ///
119 /// The destination index is used **as-is in both `FR` modes** — unlike
120 /// [`Fpr::write_s`], which under `FR = 0` reaches an even partner. `ADD.S $1`
121 /// in half mode leaves its result in FGR1, upper half cleared, which is what
122 /// the suite reads back.
123 pub const fn write_s_arith(&mut self, n: u8, _fr: bool, v: u32) {
124 self.fgr[(n & 31) as usize] = v as u64;
125 }
126
127 /// Read the **`fs`** operand of a floating-point *arithmetic* instruction.
128 ///
129 /// Under `FR = 0` an odd index reads its **even partner's** value — the low
130 /// bit of the field is simply dropped. This is *not* the same mapping as
131 /// [`Fpr::read_s`], which models `MTC1`/`LWC1` and reaches the partner's
132 /// **high** half. Two different mappings for two different instruction
133 /// classes is surprising, and it is what the hardware does.
134 ///
135 /// # Why this is measured rather than looked up
136 ///
137 /// The manual declines to specify it: *"If the FR bit is 0, an odd-numbered
138 /// register cannot be specified"*, and for the arithmetic instructions
139 /// *"If an odd number is specified, the operation is undefined"* (UM §7.5.3,
140 /// §16). Undefined in the manual is still deterministic in silicon, and
141 /// n64-systemtest measures it — so the ROM's table is the oracle here, and
142 /// the accuracy ledger records it as such rather than as documentation.
143 #[must_use]
144 pub const fn read_s_fs(&self, n: u8, fr: bool) -> u32 {
145 let i = (n & 31) as usize;
146 self.fgr[if fr { i } else { i & !1 }] as u32
147 }
148
149 /// Read the **`ft`** operand of a floating-point arithmetic instruction.
150 ///
151 /// Unlike [`Fpr::read_s_fs`], the index is used **as-is** in both `FR`
152 /// modes: an odd `ft` reads the odd FGR's own low half.
153 ///
154 /// The asymmetry is the whole point of having two accessors. It is pinned by
155 /// a pair of rows that disagree under any single rule: with `FR = 0`,
156 /// `SQRT.S $13, $31` yields `sqrt(16)` — so `fs = 31` read FGR30 — while
157 /// `ADD.S $2, $28, $31` yields `-10 + -16` — so `ft = 31` read FGR31. One
158 /// shared mapping cannot satisfy both.
159 #[must_use]
160 pub const fn read_s_ft(&self, n: u8, _fr: bool) -> u32 {
161 self.fgr[(n & 31) as usize] as u32
162 }
163
164 /// Read the `fs` operand of a **double**-precision arithmetic instruction.
165 #[must_use]
166 pub const fn read_d_fs(&self, n: u8, fr: bool) -> u64 {
167 let i = (n & 31) as usize;
168 self.fgr[if fr { i } else { i & !1 }]
169 }
170
171 /// Read the `ft` operand of a double-precision arithmetic instruction.
172 #[must_use]
173 pub const fn read_d_ft(&self, n: u8, _fr: bool) -> u64 {
174 self.fgr[(n & 31) as usize]
175 }
176
177 /// Write a **double**-precision arithmetic result.
178 ///
179 /// Like [`Fpr::write_s_arith`], the destination index is used as-is in both
180 /// modes. `ADD.D $1` under `FR = 0` leaves the result in FGR1, which
181 /// n64-systemtest checks by observing that FGR1 does *not* keep its
182 /// preloaded value.
183 pub const fn write_d_arith(&mut self, n: u8, _fr: bool, v: u64) {
184 self.fgr[(n & 31) as usize] = v;
185 }
186
187 /// Read a 64-bit value from FPR `n` under the current `FR` view.
188 ///
189 /// With `FR = 0` the register number is forced even and the **whole**
190 /// 64-bit FGR is the value — not an assembly of two FGRs' low halves,
191 /// which is the shape this originally had and which disagreed with
192 /// hardware on every odd index.
193 #[must_use]
194 pub const fn read_d(&self, n: u8, fr: bool) -> u64 {
195 let i = (n & 31) as usize;
196 self.fgr[if fr { i } else { i & !1 }]
197 }
198
199 /// Write a 64-bit value to FPR `n` under the current `FR` view.
200 pub const fn write_d(&mut self, n: u8, fr: bool, v: u64) {
201 let i = (n & 31) as usize;
202 self.fgr[if fr { i } else { i & !1 }] = v;
203 }
204
205 /// Read a raw FGR, ignoring `FR`.
206 ///
207 /// **Not for any instruction.** `DMFC1` looked like a user of this and is
208 /// not: it is a *formatted* 64-bit access and goes through [`Fpr::read_d`]
209 /// (accuracy ledger U-7). This exists for tests and for save-state
210 /// serialization, which want the physical file.
211 #[must_use]
212 pub const fn read_raw(&self, n: u8) -> u64 {
213 self.fgr[(n & 31) as usize]
214 }
215
216 /// Write a raw FGR, ignoring `FR`. See [`Fpr::read_raw`].
217 pub const fn write_raw(&mut self, n: u8, v: u64) {
218 self.fgr[(n & 31) as usize] = v;
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 /// With `FR = 1`, an FPR **is** an FGR: 32 independent 64-bit registers.
227 #[test]
228 fn fr_set_gives_thirty_two_independent_registers() {
229 let mut f = Fpr::new();
230 for n in 0..32u8 {
231 f.write_d(n, true, 0xDEAD_0000_0000_0000 | u64::from(n));
232 }
233 for n in 0..32u8 {
234 assert_eq!(
235 f.read_d(n, true),
236 0xDEAD_0000_0000_0000 | u64::from(n),
237 "FPR {n} must be independent"
238 );
239 }
240 }
241
242 /// **The n64-systemtest `MTC1` half-mode sequence, verbatim.**
243 ///
244 /// This is the vector that showed the old "FGR pair" model was wrong, so it
245 /// is transcribed rather than paraphrased: writing an **odd** register in
246 /// half mode lands in the **high half of its even partner**, and the odd
247 /// FGR is left completely alone.
248 #[test]
249 fn a_half_mode_odd_index_writes_the_high_half_of_its_even_partner() {
250 let mut f = Fpr::new();
251 f.write_raw(0, 0x0000_1111_2222_3333);
252 f.write_raw(1, 0x4444_5555_6666_7777);
253 f.write_raw(2, 0x8888_9999_AAAA_BBBB);
254
255 // MTC1 $0 in half mode -> low half of FGR0, high half preserved.
256 f.write_s(0, false, 0x89AB_CDEF);
257 assert_eq!(f.read_d(0, true), 0x0000_1111_89AB_CDEF);
258
259 // MTC1 $1 in half mode -> HIGH half of FGR0. FGR1 is untouched.
260 f.write_s(1, false, 0x0123_4567);
261 assert_eq!(
262 f.read_d(0, true),
263 0x0123_4567_89AB_CDEF,
264 "landed in FGR0's high half"
265 );
266 assert_eq!(
267 f.read_d(1, true),
268 0x4444_5555_6666_7777,
269 "FGR1 must be untouched -- the old pair model wrote here"
270 );
271
272 // MTC1 $2 -> low half of FGR2; MTC1 $3 -> high half of FGR2.
273 f.write_s(2, false, 0x1234_5678);
274 assert_eq!(f.read_d(2, true), 0x8888_9999_1234_5678);
275 f.write_s(3, false, 0x9ABC_DEF0);
276 assert_eq!(f.read_d(3, false), 0x9ABC_DEF0_1234_5678);
277 }
278
279 /// Reading mirrors writing: an odd index in half mode reads the high half.
280 #[test]
281 fn a_half_mode_odd_index_reads_the_high_half() {
282 let mut f = Fpr::new();
283 f.write_raw(0, 0x0123_4567_89AB_CDEF);
284 f.write_raw(1, 0xFFFF_FFFF_FFFF_FFFF);
285 assert_eq!(f.read_s(0, false), 0x89AB_CDEF, "even -> low half");
286 assert_eq!(f.read_s(1, false), 0x0123_4567, "odd -> HIGH half of FGR0");
287 // ...and under FR = 1 the same index is a different register entirely.
288 assert_eq!(f.read_s(1, true), 0xFFFF_FFFF, "FR=1 -> FGR1's low half");
289 }
290
291 /// A 64-bit access in half mode is the **whole** even register, not two
292 /// registers' low halves assembled.
293 #[test]
294 fn a_half_mode_64_bit_access_is_one_whole_register() {
295 let mut f = Fpr::new();
296 f.write_d(2, false, 0x1122_3344_5566_7788);
297 assert_eq!(f.read_raw(2), 0x1122_3344_5566_7788, "all 64 bits in FGR2");
298 assert_eq!(f.read_raw(3), 0, "FGR3 is not part of it");
299 assert_eq!(
300 f.read_d(3, false),
301 0x1122_3344_5566_7788,
302 "odd aliases its partner"
303 );
304 }
305
306 /// **`MTC1` preserves the other half; an arithmetic result clears it.**
307 ///
308 /// Both write 32 bits to the same place, so a single `write_s` for both is
309 /// the natural implementation — and it is wrong. n64-systemtest reads the
310 /// destination back with `DMFC1` after an `ADD.S` and expects zero above.
311 #[test]
312 fn an_arithmetic_write_clears_the_other_half_but_mtc1_preserves_it() {
313 let mut f = Fpr::new();
314 f.write_raw(4, 0xAAAA_BBBB_CCCC_DDDD);
315 f.write_s(4, true, 0x1234_5678);
316 assert_eq!(f.read_raw(4), 0xAAAA_BBBB_1234_5678, "MTC1 preserves");
317
318 f.write_raw(4, 0xAAAA_BBBB_CCCC_DDDD);
319 f.write_s_arith(4, true, 0x1234_5678);
320 assert_eq!(f.read_raw(4), 0x0000_0000_1234_5678, "arithmetic clears");
321
322 // Half mode, odd destination: the arithmetic result goes to the ODD
323 // FGR itself, upper half cleared, and the even partner is untouched.
324 //
325 // This is where an arithmetic write parts company with `MTC1`, which
326 // *would* reach FGR4's high half. `ADD.S $1` in half mode leaves its
327 // result in FGR1 -- n64-systemtest sees FGR1 lose its preloaded value,
328 // which cannot happen if the write is folded into the even partner.
329 f.write_raw(4, 0xAAAA_BBBB_CCCC_DDDD);
330 f.write_raw(5, 0x1111_2222_3333_4444);
331 f.write_s_arith(5, false, 0x1234_5678);
332 assert_eq!(f.read_raw(5), 0x0000_0000_1234_5678, "odd destination");
333 assert_eq!(f.read_raw(4), 0xAAAA_BBBB_CCCC_DDDD, "partner untouched");
334 }
335
336 /// `FR = 0` addresses only even FGRs, so the odd ones are unreachable
337 /// through every accessor. Pinned because the old model used them as
338 /// storage.
339 #[test]
340 fn half_mode_never_touches_an_odd_fgr() {
341 let mut f = Fpr::new();
342 for n in 0..32u8 {
343 f.write_raw(n, 0x5A5A_5A5A_5A5A_5A5A);
344 }
345 for n in 0..32u8 {
346 f.write_s(n, false, 0x1111_2222);
347 f.write_d(n, false, 0x3333_4444_5555_6666);
348 }
349 for n in (1..32u8).step_by(2) {
350 assert_eq!(
351 f.read_raw(n),
352 0x5A5A_5A5A_5A5A_5A5A,
353 "FGR {n} is odd and must be untouched in half mode"
354 );
355 }
356 }
357}