Skip to main content

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}