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}