Skip to main content

rustyn64_cpu/
mem.rs

1//! Load/store data shaping (T-11-003).
2//!
3//! Pure byte-level transforms: no bus, no registers, no addresses beyond the
4//! low bits that select alignment. The pipeline's `DC` stage performs the actual
5//! access and calls these to shape the result.
6//!
7//! # The unaligned family is the interesting part
8//!
9//! MIPS has no unaligned load instruction. Instead `LWL`/`LWR` (and `LDL`/`LDR`
10//! for doublewords) are used **as a pair** to assemble an unaligned value from
11//! two aligned accesses, each merging part of the addressed word into the
12//! destination register while preserving the rest of it.
13//!
14//! The N64 is **big-endian**, which decides the direction of every shift here.
15//! `LWL` takes the bytes from the addressed byte to the end of the containing
16//! word and places them at the *top* of the register; `LWR` takes the bytes from
17//! the start of the word up to the addressed byte and places them at the
18//! *bottom*. Getting the endianness backwards produces plausible-looking values
19//! that are wrong only for unaligned addresses, which is a miserable bug to find.
20
21use crate::alu::sext32;
22use serde::{Deserialize, Serialize};
23
24/// Width and signedness of an aligned load.
25#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
26pub enum LoadKind {
27    /// `LB` — signed byte.
28    SignedByte,
29    /// `LBU` — unsigned byte.
30    UnsignedByte,
31    /// `LH` — signed halfword.
32    SignedHalf,
33    /// `LHU` — unsigned halfword.
34    UnsignedHalf,
35    /// `LW` — signed word (sign-extended into the 64-bit register).
36    SignedWord,
37    /// `LWU` — unsigned word (zero-extended).
38    UnsignedWord,
39    /// `LD` — doubleword.
40    Double,
41}
42
43impl LoadKind {
44    /// Bytes accessed, which is also the required alignment.
45    #[must_use]
46    pub const fn width(self) -> u64 {
47        match self {
48            Self::SignedByte | Self::UnsignedByte => 1,
49            Self::SignedHalf | Self::UnsignedHalf => 2,
50            Self::SignedWord | Self::UnsignedWord => 4,
51            Self::Double => 8,
52        }
53    }
54
55    /// Is `addr` correctly aligned for this access?
56    ///
57    /// An unaligned access raises an address error rather than being fixed up —
58    /// that is what the `LWL`/`LWR` family exists for.
59    #[must_use]
60    pub const fn is_aligned(self, addr: u64) -> bool {
61        addr.is_multiple_of(self.width())
62    }
63
64    /// Shape raw big-endian bytes (right-justified in a `u64`) into the value the
65    /// register receives.
66    #[must_use]
67    pub const fn shape(self, raw: u64) -> u64 {
68        match self {
69            Self::SignedByte => raw as u8 as i8 as i64 as u64,
70            Self::UnsignedByte => raw as u8 as u64,
71            Self::SignedHalf => raw as u16 as i16 as i64 as u64,
72            Self::UnsignedHalf => raw as u16 as u64,
73            // LW sign-extends into the 64-bit register; LWU does not. Confusing
74            // this pair silently breaks any address above 2 GiB.
75            Self::SignedWord => sext32(raw as u32),
76            Self::UnsignedWord => raw as u32 as u64,
77            Self::Double => raw,
78        }
79    }
80}
81
82/// Width of an aligned store.
83#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
84pub enum StoreKind {
85    /// `SB` — byte.
86    Byte,
87    /// `SH` — halfword.
88    Half,
89    /// `SW` — word.
90    Word,
91    /// `SD` — doubleword.
92    Double,
93}
94
95impl StoreKind {
96    /// Bytes written, which is also the required alignment.
97    #[must_use]
98    pub const fn width(self) -> u64 {
99        match self {
100            Self::Byte => 1,
101            Self::Half => 2,
102            Self::Word => 4,
103            Self::Double => 8,
104        }
105    }
106
107    /// Is `addr` correctly aligned for this access?
108    #[must_use]
109    pub const fn is_aligned(self, addr: u64) -> bool {
110        addr.is_multiple_of(self.width())
111    }
112}
113
114/// `LWL` — merge the bytes from the addressed byte to the end of the containing
115/// word into the **top** of `rt`, preserving `rt`'s low bytes.
116///
117/// `word` is the aligned word containing `addr`; `byte` is `addr & 3`.
118/// Big-endian, so byte offset 0 selects the most-significant byte.
119#[must_use]
120pub const fn lwl(rt: u64, word: u32, byte: u64) -> u64 {
121    let shift = (byte as u32) * 8;
122    // Bits of `rt` that survive: the low `shift` bits.
123    let keep = if shift == 0 { 0 } else { (1u32 << shift) - 1 };
124    sext32((word << shift) | ((rt as u32) & keep))
125}
126
127/// `LWR` — merge the bytes from the start of the containing word up to the
128/// addressed byte into the **bottom** of `rt`, preserving `rt`'s high bytes.
129///
130/// Bits 63:32 diverge from `LWL` (ledger R-20). `LWL` always writes the word's
131/// most-significant byte, so its result is always sign-extended; a **partial**
132/// `LWR` (`byte < 3`) never writes bit 31, and the VR4300 then leaves bits 63:32
133/// of `rt` UNCHANGED rather than sign-extending. Only the full-word case
134/// (`byte == 3`) writes bit 31 and sign-extends. n64-systemtest's `tlb64` load
135/// battery pins this with a sentinel whose upper half is non-zero; the earlier
136/// unconditional `sext32` zeroed it on every partial offset.
137#[must_use]
138pub const fn lwr(rt: u64, word: u32, byte: u64) -> u64 {
139    let shift = (3 - byte as u32) * 8;
140    // Bits of `rt` that survive: everything above the loaded bytes.
141    let keep = if shift == 0 { 0 } else { !(u32::MAX >> shift) };
142    let lo = (word >> shift) | ((rt as u32) & keep);
143    if byte == 3 {
144        sext32(lo)
145    } else {
146        (rt & 0xFFFF_FFFF_0000_0000) | (lo as u64)
147    }
148}
149
150/// `LDL` — the doubleword form of [`lwl`]. `byte` is `addr & 7`.
151#[must_use]
152pub const fn ldl(rt: u64, dword: u64, byte: u64) -> u64 {
153    let shift = (byte as u32) * 8;
154    let keep = if shift == 0 { 0 } else { (1u64 << shift) - 1 };
155    (dword << shift) | (rt & keep)
156}
157
158/// `LDR` — the doubleword form of [`lwr`].
159#[must_use]
160pub const fn ldr(rt: u64, dword: u64, byte: u64) -> u64 {
161    let shift = (7 - byte as u32) * 8;
162    let keep = if shift == 0 { 0 } else { !(u64::MAX >> shift) };
163    (dword >> shift) | (rt & keep)
164}
165
166/// `SWL` — merge the **top** bytes of `rt` into the addressed word.
167///
168/// Returns the new value of the aligned word containing `addr`.
169#[must_use]
170pub const fn swl(rt: u64, word: u32, byte: u64) -> u32 {
171    let shift = (byte as u32) * 8;
172    let keep = if shift == 0 { 0 } else { !(u32::MAX >> shift) };
173    (word & keep) | ((rt as u32) >> shift)
174}
175
176/// `SWR` — merge the **bottom** bytes of `rt` into the addressed word.
177#[must_use]
178pub const fn swr(rt: u64, word: u32, byte: u64) -> u32 {
179    let shift = (3 - byte as u32) * 8;
180    let keep = if shift == 0 { 0 } else { (1u32 << shift) - 1 };
181    (word & keep) | ((rt as u32) << shift)
182}
183
184/// `SDL` — the doubleword form of [`swl`].
185#[must_use]
186pub const fn sdl(rt: u64, dword: u64, byte: u64) -> u64 {
187    let shift = (byte as u32) * 8;
188    let keep = if shift == 0 { 0 } else { !(u64::MAX >> shift) };
189    (dword & keep) | (rt >> shift)
190}
191
192/// `SDR` — the doubleword form of [`swr`].
193#[must_use]
194pub const fn sdr(rt: u64, dword: u64, byte: u64) -> u64 {
195    let shift = (7 - byte as u32) * 8;
196    let keep = if shift == 0 { 0 } else { (1u64 << shift) - 1 };
197    (dword & keep) | (rt << shift)
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn load_shaping_extends_by_width_and_signedness() {
206        assert_eq!(
207            LoadKind::SignedByte.shape(0xFF),
208            u64::MAX,
209            "LB sign-extends"
210        );
211        assert_eq!(LoadKind::UnsignedByte.shape(0xFF), 0xFF, "LBU does not");
212        assert_eq!(LoadKind::SignedHalf.shape(0x8000), 0xFFFF_FFFF_FFFF_8000);
213        assert_eq!(LoadKind::UnsignedHalf.shape(0x8000), 0x8000);
214        // The LW/LWU distinction: LW sign-extends into the 64-bit register.
215        assert_eq!(
216            LoadKind::SignedWord.shape(0x8000_0000),
217            0xFFFF_FFFF_8000_0000
218        );
219        assert_eq!(
220            LoadKind::UnsignedWord.shape(0x8000_0000),
221            0x0000_0000_8000_0000
222        );
223        assert_eq!(LoadKind::Double.shape(u64::MAX), u64::MAX);
224    }
225
226    #[test]
227    fn alignment_requirements_match_access_width() {
228        assert!(
229            LoadKind::SignedByte.is_aligned(1),
230            "bytes are always aligned"
231        );
232        assert!(!LoadKind::SignedHalf.is_aligned(1));
233        assert!(LoadKind::SignedHalf.is_aligned(2));
234        assert!(!LoadKind::SignedWord.is_aligned(2));
235        assert!(LoadKind::SignedWord.is_aligned(4));
236        assert!(!LoadKind::Double.is_aligned(4));
237        assert!(LoadKind::Double.is_aligned(8));
238        assert!(!StoreKind::Word.is_aligned(3));
239        assert!(StoreKind::Double.is_aligned(16));
240    }
241
242    /// The canonical use: `LWL` then `LWR` assembles an unaligned word from two
243    /// aligned accesses. This is the whole reason the family exists, so it is
244    /// tested as the *pair* rather than as two independent transforms.
245    ///
246    /// Memory (big-endian): `00 11 22 33 | 44 55 66 77`
247    /// An unaligned load at address 1 must yield `0x11223344`.
248    #[test]
249    fn lwl_plus_lwr_assembles_an_unaligned_word() {
250        let word0: u32 = 0x0011_2233;
251        let word1: u32 = 0x4455_6677;
252        let rt = 0xDEAD_BEEF_DEAD_BEEF;
253
254        // LWL rt, 1(base): addr 1 -> word0, byte 1.
255        let high_half = lwl(rt, word0, 1);
256        // LWR rt, 4(base): the pair's second access covers the next word.
257        let assembled = lwr(high_half, word1, 0);
258        assert_eq!(assembled, sext32(0x1122_3344));
259    }
260
261    /// At byte 0, `LWL` loads the whole word; at byte 3, `LWR` does. Those are
262    /// the degenerate ends of the family and the easiest places to be off by a
263    /// byte.
264    #[test]
265    fn the_unaligned_family_degenerates_to_a_full_word_at_the_ends() {
266        let w = 0x0011_2233u32;
267        assert_eq!(
268            lwl(0xFFFF_FFFF_FFFF_FFFF, w, 0),
269            sext32(w),
270            "LWL @0 = whole word"
271        );
272        assert_eq!(
273            lwr(0xFFFF_FFFF_FFFF_FFFF, w, 3),
274            sext32(w),
275            "LWR @3 = whole word"
276        );
277        // ...and at the other end each touches exactly one byte.
278        assert_eq!(
279            lwl(0, w, 3) as u32,
280            0x3300_0000,
281            "LWL @3 = one byte at the top"
282        );
283        assert_eq!(
284            lwr(0, w, 0) as u32,
285            0x0000_0000,
286            "LWR @0 = one byte at the bottom"
287        );
288    }
289
290    /// `LWL` preserves the low bytes of `rt` and `LWR` the high bytes — that
291    /// preservation is what makes the pair composable.
292    #[test]
293    fn the_unaligned_loads_preserve_the_untouched_half_of_rt() {
294        let rt = 0x0000_0000_AABB_CCDD;
295        // LWL at byte 2 loads 2 bytes into the top, keeping rt's low 2 bytes.
296        assert_eq!(lwl(rt, 0x1122_3344, 2) as u32 & 0xFFFF, 0xCCDD);
297        // LWR at byte 1 loads 2 bytes into the bottom, keeping rt's top 2 bytes.
298        assert_eq!(lwr(rt, 0x1122_3344, 1) as u32 >> 16, 0xAABB);
299    }
300
301    /// **A partial `LWR` leaves bits 63:32 of `rt` untouched and does NOT
302    /// sign-extend; only the full-word case (`byte == 3`) sign-extends** (ledger
303    /// R-20, the values are n64-systemtest's `tlb64` load battery).
304    ///
305    /// Mutation guard: the sentinel's upper half (`0xBEEF_0000`) is non-zero, so
306    /// the old unconditional `sext32` — which zeroed it here (`0xBADDECAF`'s
307    /// bit 31 clear at these offsets) — turns the three partial assertions red
308    /// while the full-word one stays green.
309    #[test]
310    fn a_partial_lwr_preserves_rt_upper_half_and_only_the_full_word_sign_extends() {
311        let rt = 0xBEEF_0000_0102_0304;
312        let word = 0xBADD_ECAF;
313        assert_eq!(
314            lwr(rt, word, 0),
315            0xBEEF_0000_0102_03BA,
316            "@0: 1 byte, upper preserved"
317        );
318        assert_eq!(
319            lwr(rt, word, 1),
320            0xBEEF_0000_0102_BADD,
321            "@1: 2 bytes, upper preserved"
322        );
323        assert_eq!(
324            lwr(rt, word, 2),
325            0xBEEF_0000_01BA_DDEC,
326            "@2: 3 bytes, upper preserved"
327        );
328        assert_eq!(
329            lwr(rt, word, 3),
330            0xFFFF_FFFF_BADD_ECAF,
331            "@3: whole word, sign-extended"
332        );
333    }
334
335    #[test]
336    fn the_doubleword_unaligned_family_mirrors_the_word_one() {
337        let d0 = 0x0011_2233_4455_6677u64;
338        let d1 = 0x8899_AABB_CCDD_EEFFu64;
339        // An unaligned doubleword load at byte 1 of d0.
340        let r = ldr(ldl(0, d0, 1), d1, 0);
341        assert_eq!(r, 0x1122_3344_5566_7788);
342        // Degenerate ends.
343        assert_eq!(ldl(u64::MAX, d0, 0), d0);
344        assert_eq!(ldr(u64::MAX, d0, 7), d0);
345    }
346
347    /// Stores are the inverse: `SWL`/`SWR` merge parts of `rt` into memory while
348    /// preserving the bytes outside the access.
349    #[test]
350    fn the_unaligned_stores_merge_into_memory_preserving_the_rest() {
351        let mem = 0xAABB_CCDDu32;
352        let rt = 0x1122_3344u64;
353        // SWL at byte 1 writes rt's top 3 bytes into mem's low 3 bytes.
354        assert_eq!(swl(rt, mem, 1), 0xAA11_2233);
355        // SWR at byte 2: rt's low-order bytes fill BACKWARD from the addressed
356        // byte to the word start -- mem[2]=0x44, mem[1]=0x33, mem[0]=0x22, and
357        // mem[3] is untouched. (My first assertion here had the direction
358        // backwards; `unaligned_store_then_load_round_trips` is what proves the
359        // shift directions actually agree with each other.)
360        assert_eq!(swr(rt, mem, 2), 0x2233_44DD);
361        // Degenerate ends write the whole word.
362        assert_eq!(swl(rt, mem, 0), rt as u32);
363        assert_eq!(swr(rt, mem, 3), rt as u32);
364        // Doubleword forms mirror it.
365        assert_eq!(sdl(0x0123_4567_89AB_CDEF, 0, 0), 0x0123_4567_89AB_CDEF);
366        assert_eq!(sdr(0x0123_4567_89AB_CDEF, 0, 7), 0x0123_4567_89AB_CDEF);
367    }
368
369    /// A store followed by the matching load pair must round-trip — the strongest
370    /// statement that the shift directions agree with each other.
371    #[test]
372    fn unaligned_store_then_load_round_trips() {
373        for byte in 0..4u64 {
374            let mut w0 = 0u32;
375            let mut w1 = 0u32;
376            let value = 0x1122_3344u64;
377            // SWL/SWR pair at `byte`.
378            w0 = swl(value, w0, byte);
379            if byte > 0 {
380                w1 = swr(value, w1, byte - 1);
381            }
382            // LWL/LWR pair reads it back.
383            let mut rt = 0u64;
384            rt = lwl(rt, w0, byte);
385            if byte > 0 {
386                rt = lwr(rt, w1, byte - 1);
387            }
388            assert_eq!(rt, sext32(value as u32), "round trip failed at byte {byte}");
389        }
390    }
391}