rustyn64_rsp/su.rs
1//! The **scalar unit** — the RSP's MIPS-like integer core (T-21-004).
2//!
3//! Close enough to a 32-bit R4000 that standard MIPS documentation covers most
4//! of it, so what this module records is the *differences* (N64brew *RSP CPU
5//! Core* §Scalar unit):
6//!
7//! - **No multiply/divide unit.** `MULT`, `MULTU`, `DIV`, `DIVU`, `MFHI`,
8//! `MFLO`, `MTHI`, `MTLO` do not exist, and neither does `HI`/`LO`.
9//! - **No 64-bit anything.** The registers are 32 bits, so every `D*` opcode is
10//! absent, as are `LD`/`SD`/`LDL`/`SDL`.
11//! - **No misaligned-access opcodes**, because none are needed: `LW` and `SW`
12//! work at *any* address. `LWL`/`LWR`/`SWL`/`SWR` are absent.
13//! - **No traps or exceptions at all** — no interrupts, no `SYSCALL`, no `TGE`
14//! family. `BREAK` exists and halts the core instead of raising anything.
15//! - **No likely branches** (`BEQL`, `BLEZL`, …).
16//!
17//! # The two rules that catch a MIPS core reused wholesale
18//!
19//! **The PC is 12 bits and wraps.** Every high bit of a branch or jump target
20//! is discarded, and running off the end of IMEM at `0xFFC` continues at
21//! `0x000` rather than faulting. n64-systemtest's `RSP Wrap around` places two
22//! `nop`s at `0xFF8` and a `BREAK` at `0x000`, runs from `0xFF8`, and expects
23//! to stop at `0x4`.
24//!
25//! **Misaligned data accesses are correct, not faults.** A `LW` at `0x001`
26//! returns the four bytes at `0x1..=0x4`; the address is masked to 12 bits and
27//! each byte wraps inside DMEM independently, so a word read at `0xFFE` takes
28//! two bytes from the end and two from the start. On the VR4300 the same access
29//! is an `AddressError`, which makes this the single easiest place to get the
30//! RSP wrong by reusing CPU code.
31
32use crate::Rsp;
33use crate::sp::{self, STATUS_INTBREAK};
34use serde::{Deserialize, Serialize};
35
36/// What one scalar step asked the rest of the machine to do.
37///
38/// The RSP cannot reach RDRAM or the MI itself — it does not own them — so it
39/// reports rather than acts, and `rustyn64-core::Bus` carries it out. This is
40/// the same shape the PI engine uses, and it is what lets the RSP be stepped in
41/// isolation.
42#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
43pub struct StepResult {
44 /// A transfer an `MTC0` to a length register started.
45 pub dma: Option<sp::Dma>,
46 /// A change to the MI's SP interrupt line: `Some(true)` raises it,
47 /// `Some(false)` acknowledges it, `None` leaves it alone.
48 ///
49 /// Three states rather than two, because the RSP acknowledges its **own**
50 /// interrupt by writing `CLR_INTR` through `MTC0` — with a plain `bool`,
51 /// "clear the line" and "this step said nothing about the line" are the
52 /// same value, and the acknowledgment is silently dropped.
53 pub interrupt_change: Option<bool>,
54 /// An `MTC0` to a DP command register (`c8`–`c15`), reported as a
55 /// **DPC word offset** `0`–`7` (`0`=`DP_START`, `1`=`DP_END`,
56 /// `2`=`DP_CURRENT`, `3`=`DP_STATUS`, `4`–`7` the clock/busy counters) and
57 /// the value.
58 ///
59 /// Those COP0 registers *are* the RDP's command registers, but the RSP
60 /// crate may not depend on `rustyn64-rdp` (the crate graph forbids that
61 /// chip→chip edge — `docs/architecture.md`). So the write is reported here
62 /// and `rustyn64-core::Bus` forwards it to `Rdp::dpc_write`, exactly as
63 /// `dma` and `interrupt_change` are carried out by the owner.
64 pub dp_write: Option<(u8, u32)>,
65}
66
67/// Decoded fields, named as the MIPS encoding names them.
68#[derive(Serialize, Deserialize)]
69struct Fields {
70 op: u32,
71 rs: usize,
72 rt: usize,
73 rd: usize,
74 sa: u32,
75 funct: u32,
76 imm: u32,
77 target: u32,
78}
79
80const fn decode(word: u32) -> Fields {
81 Fields {
82 op: word >> 26,
83 rs: ((word >> 21) & 31) as usize,
84 rt: ((word >> 16) & 31) as usize,
85 rd: ((word >> 11) & 31) as usize,
86 sa: (word >> 6) & 31,
87 funct: word & 63,
88 imm: word & 0xFFFF,
89 target: word & 0x03FF_FFFF,
90 }
91}
92
93/// Sign-extend a 16-bit immediate to 32 bits.
94const fn sext(imm: u32) -> u32 {
95 ((imm as u16).cast_signed() as i32).cast_unsigned()
96}
97
98impl Rsp {
99 /// Read one scalar register, with `r0` pinned to zero.
100 ///
101 /// Public so the vector load/store family can compute its base address —
102 /// those instructions live in [`crate::vu`] but address DMEM through a GPR.
103 #[must_use]
104 pub const fn r(&self, i: usize) -> u32 {
105 if i == 0 { 0 } else { self.su_regs[i] }
106 }
107
108 /// Write one scalar register; writes to `r0` are discarded.
109 ///
110 /// Public under this name so the VU's move instructions can reach it —
111 /// `MFC2` and `CFC2` write a GPR, and they live in [`crate::vu`].
112 pub const fn set_su(&mut self, i: usize, v: u32) {
113 self.set_r(i, v);
114 }
115
116 /// Write one register; writes to `r0` are discarded.
117 const fn set_r(&mut self, i: usize, v: u32) {
118 if i != 0 {
119 self.su_regs[i] = v;
120 }
121 }
122
123 /// Read a byte of DMEM. The address is 12 bits — everything above is
124 /// ignored, so a load never escapes the 4 KiB and never faults.
125 const fn dmem_read(&self, addr: u32) -> u8 {
126 self.dmem[(addr & 0xFFF) as usize]
127 }
128
129 /// Write a byte of DMEM, under the same 12-bit rule.
130 const fn dmem_write(&mut self, addr: u32, val: u8) {
131 self.dmem[(addr & 0xFFF) as usize] = val;
132 }
133
134 /// Read `n` bytes big-endian from DMEM, each byte wrapping independently.
135 ///
136 /// Byte-at-a-time rather than a word fetch because the access may be
137 /// misaligned *and* may straddle the end of DMEM — `LWU` from `0xFFD` takes
138 /// three bytes from the end and one from the start, which n64-systemtest
139 /// checks directly.
140 fn dmem_load(&self, addr: u32, n: u32) -> u32 {
141 let mut v = 0u32;
142 for i in 0..n {
143 v = (v << 8) | u32::from(self.dmem_read(addr.wrapping_add(i)));
144 }
145 v
146 }
147
148 /// Store the low `n` bytes of `val` big-endian into DMEM.
149 fn dmem_store(&mut self, addr: u32, n: u32, val: u32) {
150 for i in 0..n {
151 let shift = 8 * (n - 1 - i);
152 self.dmem_write(addr.wrapping_add(i), (val >> shift) as u8);
153 }
154 }
155
156 /// Fetch the instruction word at `pc` from IMEM.
157 ///
158 /// IMEM is a separate 4 KiB (the RSP is a Harvard machine), and the PC is
159 /// masked to 12 bits so the fetch wraps rather than running off the end.
160 fn imem_word(&self, pc: u32) -> u32 {
161 let base = (pc & 0xFFF) as usize;
162 u32::from_be_bytes([
163 self.imem[base],
164 self.imem[(base + 1) & 0xFFF],
165 self.imem[(base + 2) & 0xFFF],
166 self.imem[(base + 3) & 0xFFF],
167 ])
168 }
169
170 /// Execute one scalar instruction, if the core is running.
171 ///
172 /// Returns what the step asked the machine to do; see [`StepResult`].
173 pub fn su_step(&mut self) -> StepResult {
174 let mut out = StepResult::default();
175 if self.sp.halted() {
176 return out;
177 }
178 self.count_retired();
179
180 let pc = self.sp.pc();
181 let word = self.imem_word(pc);
182 // The branch target latched by the *previous* instruction. Taken now,
183 // because the instruction being executed is that branch's delay slot.
184 let after_delay = self.branch.take();
185 let sequential = pc.wrapping_add(4) & 0xFFC;
186
187 let d = decode(word);
188 let mut halt_at = None;
189
190 match d.op {
191 // SPECIAL — the register-form ALU, shifts, jumps and BREAK.
192 0 => halt_at = self.special(&d, sequential),
193 // REGIMM: the four conditional branches on rs's sign.
194 1 => {
195 let v = self.r(d.rs).cast_signed();
196 let take = match d.rt {
197 0o00 | 0o20 => v < 0,
198 0o01 | 0o21 => v >= 0,
199 _ => false,
200 };
201 // The AL forms link unconditionally, even when not taken.
202 if d.rt & 0o20 != 0 {
203 self.set_r(31, sequential.wrapping_add(4) & 0xFFC);
204 }
205 if take {
206 self.branch = Some(branch_target(sequential, d.imm));
207 }
208 }
209 0o02 => self.branch = Some((d.target << 2) & 0xFFC),
210 0o03 => {
211 self.set_r(31, sequential.wrapping_add(4) & 0xFFC);
212 self.branch = Some((d.target << 2) & 0xFFC);
213 }
214 0o04 => {
215 if self.r(d.rs) == self.r(d.rt) {
216 self.branch = Some(branch_target(sequential, d.imm));
217 }
218 }
219 0o05 => {
220 if self.r(d.rs) != self.r(d.rt) {
221 self.branch = Some(branch_target(sequential, d.imm));
222 }
223 }
224 0o06 => {
225 if self.r(d.rs).cast_signed() <= 0 {
226 self.branch = Some(branch_target(sequential, d.imm));
227 }
228 }
229 0o07 => {
230 if self.r(d.rs).cast_signed() > 0 {
231 self.branch = Some(branch_target(sequential, d.imm));
232 }
233 }
234 // ADDI and ADDIU likewise coincide: no overflow trap exists.
235 0o10 | 0o11 => self.set_r(d.rt, self.r(d.rs).wrapping_add(sext(d.imm))),
236 0o12 => self.set_r(
237 d.rt,
238 u32::from(self.r(d.rs).cast_signed() < sext(d.imm).cast_signed()),
239 ),
240 0o13 => self.set_r(d.rt, u32::from(self.r(d.rs) < sext(d.imm))),
241 0o14 => self.set_r(d.rt, self.r(d.rs) & d.imm),
242 0o15 => self.set_r(d.rt, self.r(d.rs) | d.imm),
243 0o16 => self.set_r(d.rt, self.r(d.rs) ^ d.imm),
244 0o17 => self.set_r(d.rt, d.imm << 16),
245 // COP0 — the SP and DP register files, reached by MFC0/MTC0.
246 0o20 => match d.rs {
247 0o00 => {
248 let v = self.cop0_read(d.rd as u32);
249 self.set_r(d.rt, v);
250 }
251 0o04 => out = self.cop0_write(d.rd as u32, self.r(d.rt)),
252 _ => {}
253 },
254 // Loads. `LW` and `LWU` are the same operation on a 32-bit machine:
255 // there is no upper half for the sign to extend into.
256 0o40 => self.set_r(
257 d.rt,
258 ((self.load(&d, 1) as u8).cast_signed() as i32).cast_unsigned(),
259 ),
260 0o41 => self.set_r(
261 d.rt,
262 ((self.load(&d, 2) as u16).cast_signed() as i32).cast_unsigned(),
263 ),
264 0o43 | 0o47 => self.set_r(d.rt, self.load(&d, 4)),
265 0o44 => self.set_r(d.rt, self.load(&d, 1)),
266 0o45 => self.set_r(d.rt, self.load(&d, 2)),
267 0o50 => self.store(&d, 1),
268 0o51 => self.store(&d, 2),
269 0o53 => self.store(&d, 4),
270 // COP2 — both the SU/VU moves and the computational vector
271 // instructions (bit 25 set) dispatch through `cop2` to the VU.
272 0o22 => self.cop2(&d),
273 // The vector load/store family. `opcode` is at 15..11 where `rd`
274 // sits, `element` at 10..7, and the offset is a **signed 7-bit**
275 // field -- not the 16-bit immediate an ordinary load carries.
276 0o62 | 0o72 => self.vector_memory(&d),
277 _ => {}
278 }
279
280 // `r0` is pinned; a write may have slipped through a path above.
281 self.su_regs[0] = 0;
282
283 if let Some(next) = halt_at {
284 // A BREAK in a *taken* branch's delay slot halts at the branch
285 // target, not the sequential address: the branch redirect that was
286 // already latched still wins. n64-systemtest pins this -- `beq`
287 // taken into a `break` leaves PC at the target (0x1C), while the
288 // same `break` after an untaken branch leaves it sequential (0x8).
289 self.sp.set_pc(after_delay.unwrap_or(next));
290 self.sp.set_halted(true);
291 self.sp.set_broke(true);
292 if self.sp.status() & STATUS_INTBREAK != 0 {
293 out.interrupt_change = Some(true);
294 }
295 return out;
296 }
297
298 self.sp.set_pc(after_delay.unwrap_or(sequential));
299 out
300 }
301
302 /// The `SPECIAL` opcode group: register-form ALU, shifts, `JR`/`JALR` and
303 /// `BREAK`. Split out so [`Rsp::su_step`] stays readable, not because the
304 /// group is separable — it is one arm of the same decode.
305 ///
306 /// Returns the PC to halt at when the instruction was a `BREAK`.
307 fn special(&mut self, d: &Fields, sequential: u32) -> Option<u32> {
308 let mut halt_at = None;
309 match d.funct {
310 0o00 => self.set_r(d.rd, self.r(d.rt) << d.sa),
311 0o02 => self.set_r(d.rd, self.r(d.rt) >> d.sa),
312 0o03 => self.set_r(d.rd, (self.r(d.rt).cast_signed() >> d.sa).cast_unsigned()),
313 0o04 => self.set_r(d.rd, self.r(d.rt) << (self.r(d.rs) & 31)),
314 0o06 => self.set_r(d.rd, self.r(d.rt) >> (self.r(d.rs) & 31)),
315 0o07 => self.set_r(
316 d.rd,
317 (self.r(d.rt).cast_signed() >> (self.r(d.rs) & 31)).cast_unsigned(),
318 ),
319 0o10 => self.branch = Some(self.r(d.rs) & 0xFFC),
320 0o11 => {
321 let target = self.r(d.rs) & 0xFFC;
322 self.set_r(d.rd, sequential.wrapping_add(4) & 0xFFC);
323 self.branch = Some(target);
324 }
325 // BREAK. Halts and latches BROKE; the interrupt is conditional.
326 0o15 => halt_at = Some(sequential),
327 // ADD and ADDU are the same instruction here: the RSP has no
328 // exceptions, so there is no overflow trap to distinguish them.
329 0o40 | 0o41 => self.set_r(d.rd, self.r(d.rs).wrapping_add(self.r(d.rt))),
330 0o42 | 0o43 => self.set_r(d.rd, self.r(d.rs).wrapping_sub(self.r(d.rt))),
331 0o44 => self.set_r(d.rd, self.r(d.rs) & self.r(d.rt)),
332 0o45 => self.set_r(d.rd, self.r(d.rs) | self.r(d.rt)),
333 0o46 => self.set_r(d.rd, self.r(d.rs) ^ self.r(d.rt)),
334 0o47 => self.set_r(d.rd, !(self.r(d.rs) | self.r(d.rt))),
335 0o52 => self.set_r(
336 d.rd,
337 u32::from(self.r(d.rs).cast_signed() < self.r(d.rt).cast_signed()),
338 ),
339 0o53 => self.set_r(d.rd, u32::from(self.r(d.rs) < self.r(d.rt))),
340 // Everything else in SPECIAL is one of the absent opcodes
341 // (multiply, divide, HI/LO, traps). They are not errors on this
342 // core -- there is no exception mechanism to report them with --
343 // so they retire doing nothing.
344 _ => {}
345 }
346 halt_at
347 }
348
349 /// `LWC2`/`SWC2` — the vector load/store family.
350 ///
351 /// `opcode` sits at 15..11 where `rd` is, `element` at 10..7, and the offset
352 /// is a **signed 7-bit** field rather than the 16-bit immediate an ordinary
353 /// load carries -- so reading `imm` as a whole would give a wildly wrong
354 /// address.
355 fn vector_memory(&mut self, d: &Fields) {
356 let element = ((d.sa >> 1) & 0xF) as usize;
357 self.vector_mem(d.op == 0o72, d.rd as u32, d.rs, d.rt, element, d.imm & 0x7F);
358 }
359
360 /// The COP2 escape: the SU/VU moves and the computational vector instructions.
361 ///
362 /// Bit 25 separates the two groups. When it is set the instruction is a
363 /// computational one, whose `element` field is a *broadcast modifier*; when
364 /// clear it is a move, whose element field is a **byte offset**. Conflating
365 /// them is the first thing to get wrong here.
366 fn cop2(&mut self, d: &Fields) {
367 // Bit 25 of the word is the top bit of the `rs` field, which is how the
368 // two groups share one opcode. The four moves are `rs` 0/2/4/6, all
369 // with it clear.
370 if d.rs & 0x10 != 0 {
371 // Computational. Note the operand fields are NOT in the usual MIPS
372 // positions: `vt` is at 20..16 (where `rt` normally sits), `vs` at
373 // 15..11 and `vd` at 10..6 -- so the natural reading of a MIPS
374 // R-type would swap the source and destination.
375 self.count_vu_funct(d.funct);
376 let element = d.rs as u32 & 0xF;
377 // The single-lane group reads `vs` as a destination *element*
378 // rather than a source register, so it is dispatched first.
379 if self.vu_single_lane(d.funct, element, d.rt, d.rd, d.sa as usize) {
380 return;
381 }
382 self.vu_compute(d.funct, element, d.rd, d.rt, d.sa as usize);
383 return;
384 }
385 // `vs_elem` is bits 10..=7 of the word, a byte offset into the register.
386 let elem = ((d.sa >> 1) & 0xF) as usize;
387 match d.rs {
388 0o00 => {
389 self.mfc2(d.rt, d.rd, elem);
390 }
391 0o02 => {
392 self.cfc2(d.rt, d.rd as u32);
393 }
394 0o04 => self.mtc2(self.r(d.rt), d.rd, elem),
395 0o06 => self.ctc2(self.r(d.rt), d.rd as u32),
396 _ => {}
397 }
398 }
399
400 /// Address for a load or store: `base + sign-extended offset`, 12 bits.
401 fn addr(&self, d: &Fields) -> u32 {
402 self.r(d.rs).wrapping_add(sext(d.imm)) & 0xFFF
403 }
404
405 fn load(&self, d: &Fields, n: u32) -> u32 {
406 self.dmem_load(self.addr(d), n)
407 }
408
409 fn store(&mut self, d: &Fields, n: u32) {
410 let addr = self.addr(d);
411 self.dmem_store(addr, n, self.r(d.rt));
412 }
413
414 /// `MFC0` — read an SP register, or a DP register.
415 ///
416 /// The `MFC0`/`MTC0` `rd` field is 5 bits, but the RSP has only **sixteen**
417 /// COP0 registers: `c0`–`c7` are the SP interface registers (the *same*
418 /// physical registers the CPU sees at `0x0404_0000`), and `c8`–`c15` are the
419 /// RDP's command registers, held in a [`shadow`](Rsp::dp) so a read sees a
420 /// prior write in the same run (the authoritative copy is in `rustyn64-rdp`).
421 /// `c16`–`c31` do not exist — they must **not** alias into `c8`–`c15` (that
422 /// would let a stray `MFC0 c16` read `DP_START`), so they read zero.
423 fn cop0_read(&mut self, index: u32) -> u32 {
424 match index {
425 0..=7 => self.sp.read(index),
426 8..=15 => self.dp[(index - 8) as usize],
427 _ => 0,
428 }
429 }
430
431 /// `MTC0` — write an SP register (possibly starting a DMA), or a DP command
432 /// register. DP writes update the [`shadow`](Rsp::dp) and are reported via
433 /// [`StepResult::dp_write`] for the Bus to forward to `Rdp::dpc_write`.
434 /// `c16`–`c31` do not exist and are ignored (see [`Self::cop0_read`]).
435 fn cop0_write(&mut self, index: u32, value: u32) -> StepResult {
436 let mut out = StepResult::default();
437 match index {
438 8..=15 => {
439 let off = (index - 8) as u8;
440 self.dp[off as usize] = value;
441 out.dp_write = Some((off, value));
442 return out;
443 }
444 16..=31 => return out,
445 _ => {}
446 }
447 if index == sp::reg::STATUS {
448 // Both directions propagate. The RSP acknowledging its own
449 // interrupt is a `CLR_INTR` write, and it must reach the MI.
450 out.interrupt_change = sp::SpRegs::interrupt_change(value);
451 }
452 out.dma = self.sp.write(index, value);
453 out
454 }
455}
456
457/// A branch target: PC-relative, from the delay slot, masked to 12 bits.
458const fn branch_target(sequential: u32, imm: u32) -> u32 {
459 sequential.wrapping_add(sext(imm) << 2) & 0xFFC
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 /// Assemble into IMEM and run until halted, returning the RSP.
467 fn run(program: &[u32], start: u32) -> Rsp {
468 let mut rsp = Rsp::new();
469 for (i, w) in program.iter().enumerate() {
470 let at = (start as usize + i * 4) & 0xFFF;
471 for (b, byte) in w.to_be_bytes().iter().enumerate() {
472 rsp.imem[(at + b) & 0xFFF] = *byte;
473 }
474 }
475 rsp.sp.set_pc(start);
476 rsp.sp.set_halted(false);
477 for _ in 0..10_000 {
478 rsp.su_step();
479 if rsp.sp.halted() {
480 break;
481 }
482 }
483 rsp
484 }
485
486 const NOP: u32 = 0;
487 const BREAK: u32 = 0o15;
488
489 const fn ori(rt: u32, rs: u32, imm: u32) -> u32 {
490 (0o15 << 26) | (rs << 21) | (rt << 16) | imm
491 }
492 const fn sw(rt: u32, base: u32, off: u32) -> u32 {
493 (0o53 << 26) | (base << 21) | (rt << 16) | off
494 }
495 const fn addu(rd: u32, rs: u32, rt: u32) -> u32 {
496 (rs << 21) | (rt << 16) | (rd << 11) | 0o41
497 }
498
499 /// **`BREAK` halts and latches `BROKE`, and the PC stops after it.**
500 ///
501 /// `SP_STATUS` reads `0x3` — both bits — which is what n64-systemtest's
502 /// `RSP Wrap around` expects.
503 #[test]
504 fn break_halts_and_sets_broke() {
505 let rsp = run(&[NOP, BREAK], 0);
506 assert!(rsp.sp.halted());
507 assert_eq!(rsp.sp.status(), 0x3, "HALTED | BROKE");
508 assert_eq!(rsp.sp.pc(), 0x8, "the PC sits past the BREAK");
509 }
510
511 /// **The 12-bit PC wraps at the end of IMEM instead of running off it.**
512 ///
513 /// n64-systemtest's own case: two `nop`s at `0xFF8`, a `BREAK` at `0x000`,
514 /// started at `0xFF8`, expecting to stop at `0x4` with status `0x3`. A core
515 /// that lets the PC grow past `0xFFF` fetches garbage and never halts.
516 #[test]
517 fn the_pc_wraps_from_the_end_of_imem_to_the_start() {
518 let mut rsp = Rsp::new();
519 for (at, w) in [(0xFF8u32, NOP), (0xFFC, NOP), (0x000, BREAK)] {
520 for (b, byte) in w.to_be_bytes().iter().enumerate() {
521 rsp.imem[(at as usize + b) & 0xFFF] = *byte;
522 }
523 }
524 rsp.sp.set_pc(0xFF8);
525 rsp.sp.set_halted(false);
526 for _ in 0..16 {
527 rsp.su_step();
528 if rsp.sp.halted() {
529 break;
530 }
531 }
532 assert_eq!(rsp.sp.pc(), 0x4, "wrapped to 0x000 and stopped after BREAK");
533 assert_eq!(rsp.sp.status(), 0x3);
534 }
535
536 /// **A misaligned word load is correct, not a fault.**
537 ///
538 /// The VR4300 raises `AddressError` for exactly this access, which is what
539 /// makes it the easiest thing to get wrong by reusing CPU code. Values from
540 /// n64-systemtest's `RSP LWU`, which seeds `BADDECAF01234567` at DMEM 0.
541 #[test]
542 fn a_misaligned_word_load_reads_across_the_boundary() {
543 let mut rsp = Rsp::new();
544 for (i, b) in 0xBADD_ECAF_0123_4567u64.to_be_bytes().iter().enumerate() {
545 rsp.dmem[i] = *b;
546 }
547 assert_eq!(rsp.dmem_load(0x000, 4), 0xBADD_ECAF);
548 assert_eq!(rsp.dmem_load(0x001, 4), 0xDDEC_AF01, "misaligned by one");
549 assert_eq!(rsp.dmem_load(0x003, 4), 0xAF01_2345);
550 }
551
552 /// A load that runs off the end of DMEM **wraps to the start**, because
553 /// every byte address is masked independently.
554 #[test]
555 fn a_load_at_the_end_of_dmem_wraps_to_the_beginning() {
556 let mut rsp = Rsp::new();
557 rsp.dmem[0xFFE] = 0xBC;
558 rsp.dmem[0xFFF] = 0xAD;
559 rsp.dmem[0x000] = 0x7E;
560 rsp.dmem[0x001] = 0x8F;
561 assert_eq!(rsp.dmem_load(0xFFE, 4), 0xBCAD_7E8F);
562 }
563
564 /// The integer core computes and stores: `ori` builds a value, `addu` sums
565 /// it, `sw` lands it in DMEM. Chosen so a no-op decode arm cannot pass —
566 /// the stored word depends on all three instructions.
567 #[test]
568 fn the_integer_core_computes_and_stores() {
569 let rsp = run(
570 &[
571 ori(1, 0, 0x1234),
572 ori(2, 0, 0x1111),
573 addu(3, 1, 2),
574 sw(3, 0, 0x20),
575 BREAK,
576 ],
577 0,
578 );
579 assert_eq!(rsp.dmem_load(0x20, 4), 0x2345, "0x1234 + 0x1111");
580 }
581
582 /// `r0` stays zero however hard an instruction tries to write it.
583 #[test]
584 fn register_zero_is_pinned() {
585 let rsp = run(&[ori(0, 0, 0xFFFF), sw(0, 0, 0x30), BREAK], 0);
586 assert_eq!(rsp.r(0), 0);
587 assert_eq!(rsp.dmem_load(0x30, 4), 0, "and it stores as zero");
588 }
589
590 /// A taken branch executes its **delay slot** before redirecting. The slot
591 /// here writes a value nothing else writes, so a core that skips it fails.
592 #[test]
593 fn a_branch_executes_its_delay_slot() {
594 // beq r0, r0, +2 / ori r1, 0x55 (delay slot) / ori r1, 0x99 (skipped)
595 // ... target: sw r1, 0x40 / break
596 let beq = (0o04 << 26) | 2;
597 let rsp = run(
598 &[beq, ori(1, 0, 0x55), ori(1, 0, 0x99), sw(1, 0, 0x40), BREAK],
599 0,
600 );
601 assert_eq!(
602 rsp.dmem_load(0x40, 4),
603 0x55,
604 "the delay slot ran and the skipped instruction did not"
605 );
606 }
607
608 /// **`BREAK` in a *taken* branch's delay slot halts at the branch target.**
609 /// The redirect the `beq` latched still wins over the sequential address:
610 /// n64-systemtest pins PC to `0x1C` (`0x4 + (6 << 2)`), not `0x8`. Reading
611 /// the sequential value instead is the natural mistake, and it is the exact
612 /// difference between this case and the untaken one below.
613 #[test]
614 fn break_in_a_taken_delay_slot_halts_at_the_branch_target() {
615 // beq r0, r0, +6 (always taken) / break (delay slot)
616 let beq = (0o04 << 26) | 6;
617 let rsp = run(&[beq, BREAK], 0);
618 assert_eq!(
619 rsp.sp.pc(),
620 0x1C,
621 "PC follows the branch, not sequential 0x8"
622 );
623 }
624
625 /// The mirror: a `BREAK` after an *untaken* branch halts sequentially. With
626 /// no redirect latched, `after_delay` is `None` and the PC is `0x8` — the
627 /// case that must NOT regress when the taken case is fixed.
628 #[test]
629 fn break_after_an_untaken_delay_slot_halts_sequentially() {
630 // bne r0, r0, +6 (never taken) / break (delay slot)
631 let bne = (0o05 << 26) | 6;
632 let rsp = run(&[bne, BREAK], 0);
633 assert_eq!(rsp.sp.pc(), 0x8, "no branch taken, so PC is sequential");
634 }
635
636 /// **The RSP can acknowledge its own interrupt.**
637 ///
638 /// `MTC0 SP_STATUS` with `CLR_INTR` must reach the MI as a *clear*. With a
639 /// plain `bool` on [`StepResult`], "clear the line" and "this step said
640 /// nothing about the line" are the same value, so the acknowledgment is
641 /// dropped and the CPU's `IP2` stays asserted for ever. The three cases are
642 /// asserted separately because that is exactly what a two-state flag cannot
643 /// express.
644 #[test]
645 fn the_rsp_can_raise_and_clear_its_own_interrupt() {
646 const SET_INTR: u32 = 1 << 4;
647 const CLR_INTR: u32 = 1 << 3;
648 let mut rsp = Rsp::new();
649
650 let out = rsp.cop0_write(sp::reg::STATUS, SET_INTR);
651 assert_eq!(out.interrupt_change, Some(true), "raise");
652
653 let out = rsp.cop0_write(sp::reg::STATUS, CLR_INTR);
654 assert_eq!(out.interrupt_change, Some(false), "acknowledge");
655
656 let out = rsp.cop0_write(sp::reg::STATUS, SET_INTR | CLR_INTR);
657 assert_eq!(out.interrupt_change, None, "both together: no change");
658
659 // A write that mentions neither must not disturb the line.
660 let out = rsp.cop0_write(sp::reg::STATUS, 1 << 10);
661 assert_eq!(out.interrupt_change, None, "unrelated flag write");
662 }
663
664 /// `BREAK` raises the line **only** when `INTBREAK` is set.
665 ///
666 /// Both configurations execute the same single `BREAK` and differ *only* in
667 /// that flag, so the differing `interrupt_change` is attributable to nothing
668 /// else. The disabled case asserts `None` rather than merely "not raised":
669 /// `BREAK` must leave a previously-raised line alone, and `Some(false)`
670 /// would clear it.
671 #[test]
672 fn break_raises_the_interrupt_only_when_enabled() {
673 /// Run one `BREAK` at IMEM 0 and return what the step reported.
674 fn break_once(intbreak: bool) -> (StepResult, u32) {
675 let mut rsp = Rsp::new();
676 for (b, byte) in BREAK.to_be_bytes().iter().enumerate() {
677 rsp.imem[b] = *byte;
678 }
679 if intbreak {
680 rsp.sp.write(sp::reg::STATUS, 1 << 8); // SET_INTBREAK
681 }
682 rsp.sp.set_pc(0);
683 rsp.sp.set_halted(false);
684 let out = rsp.su_step();
685 (out, rsp.sp.status())
686 }
687
688 let (out, status) = break_once(false);
689 assert_eq!(
690 out.interrupt_change, None,
691 "with INTBREAK clear, BREAK must not touch the line at all -- \
692 Some(false) would acknowledge an interrupt it never raised"
693 );
694 assert_eq!(status & 0x3, 0x3, "still HALTED | BROKE");
695
696 let (out, status) = break_once(true);
697 assert_eq!(out.interrupt_change, Some(true), "INTBREAK set");
698 assert_eq!(status & 0x3, 0x3, "and it halts either way");
699 }
700
701 /// `MFC0` reads the SP registers the CPU shares, and `MTC0` writes them.
702 #[test]
703 fn cop0_reaches_the_sp_registers() {
704 let mut rsp = Rsp::new();
705 rsp.sp.write(sp::reg::STATUS, 1 << 10); // SET_SIG0
706 let v = rsp.cop0_read(sp::reg::STATUS);
707 assert_ne!(v & sp::STATUS_SIG0, 0, "MFC0 sees the signal bit");
708
709 // c8–c15 are the RDP's; the shadow starts zeroed.
710 assert_eq!(rsp.cop0_read(9), 0);
711 }
712
713 /// `MTC0`/`MFC0` to `c8`–`c15` reach the DP shadow and report the write; the
714 /// non-existent `c16`–`c31` must **not** alias back into `c8`–`c15`.
715 #[test]
716 fn cop0_dp_registers_do_not_alias_beyond_c15() {
717 let mut rsp = Rsp::new();
718
719 // c9 = DP_END -> shadow[1], reported as offset 1.
720 let out = rsp.cop0_write(9, 0xABCD_0000);
721 assert_eq!(out.dp_write, Some((1, 0xABCD_0000)));
722 assert_eq!(
723 rsp.cop0_read(9),
724 0xABCD_0000,
725 "MFC0 reads the shadowed write"
726 );
727
728 // c16/c24 do not exist: writing them must neither report a DP write nor
729 // disturb any shadow register (the `(index-8) & 7` bug wrapped c16->c8).
730 let before = rsp.dp;
731 for idx in [16, 24, 31] {
732 let out = rsp.cop0_write(idx, 0xDEAD_BEEF);
733 assert_eq!(out.dp_write, None, "c{idx} is not a DP register");
734 assert_eq!(rsp.cop0_read(idx), 0, "c{idx} reads zero");
735 }
736 assert_eq!(
737 rsp.dp, before,
738 "c16–c31 writes must not touch the DP shadow"
739 );
740 }
741}