rustysnes_cart/coproc/armv3/regs.rs
1//! The ARM register file (with mode banking) and the 3-stage instruction pipeline.
2//!
3//! Clean-room port of the relevant pieces of Mesen2's `ArmV3Cpu`/`ArmV3CpuState` (see the parent
4//! module doc). This is step 2+3 of the build order in `docs/st018-arm-notes.md`: register
5//! banking and the pipeline's implicit "PC reads as address+8" timing, both landed before any
6//! instruction decode/execute exists (nothing here depends on it, and the pipeline model is the
7//! single highest-risk fidelity point every later instruction depends on getting right).
8
9use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
10
11use crate::coproc::armv3::primitives::Flags;
12
13/// The 7 real ARM processor modes.
14///
15/// A `u8`, not a Rust enum, because [`Regs::switch_mode`] deliberately mirrors the source's own
16/// `default:`-falls-back-to-User/System behavior for any OTHER 5-bit pattern reachable via `MSR`
17/// — a real ARM CPU accepts an out-of-range mode field as "reserved/UNPREDICTABLE," and this
18/// port's fallback (treat it like User/System for banking purposes) matches Mesen2's own choice
19/// rather than rejecting it outright.
20pub mod mode {
21 /// Bit 4 is always set on every real mode value; ORed into `switch_mode`'s input
22 /// unconditionally (matching the source), so a caller never needs to set it explicitly.
23 pub const BIT: u8 = 0b1_0000;
24 /// Unprivileged mode — the normal running state; no SPSR, no banked `R13`/`R14` of its own
25 /// (shares the same bank as [`SYSTEM`]).
26 pub const USER: u8 = 0b1_0000;
27 /// Fast interrupt mode — the only mode with a fully private `R8-R14` bank.
28 pub const FIQ: u8 = 0b1_0001;
29 /// Normal interrupt mode.
30 pub const IRQ: u8 = 0b1_0010;
31 /// Entered on `SWI` (software interrupt).
32 pub const SUPERVISOR: u8 = 0b1_0011;
33 /// Entered on a memory abort.
34 pub const ABORT: u8 = 0b1_0111;
35 /// Entered on an undefined-instruction trap.
36 pub const UNDEFINED: u8 = 0b1_1011;
37 /// Privileged mode that shares [`USER`]'s register bank (no SPSR of its own either).
38 pub const SYSTEM: u8 = 0b1_1111;
39}
40
41/// A `u8`-backed ARM mode value — see the [`mode`] module for the 7 real constants and the
42/// fallback-to-User/System posture for anything else.
43pub type Mode = u8;
44
45/// The full CPSR (or an SPSR): mode + interrupt-mask bits + the [`Flags`] condition codes.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct Cpsr {
48 /// The active processor mode (see the [`mode`] module).
49 pub mode: Mode,
50 /// FIQ (fast-interrupt) mask — set to disable FIQ.
51 pub fiq_disable: bool,
52 /// IRQ mask — set to disable normal interrupts.
53 pub irq_disable: bool,
54 /// The N/Z/C/V condition-code flags.
55 pub flags: Flags,
56}
57
58impl Default for Cpsr {
59 fn default() -> Self {
60 Self {
61 mode: mode::USER,
62 fiq_disable: false,
63 irq_disable: false,
64 flags: Flags::default(),
65 }
66 }
67}
68
69impl Cpsr {
70 /// Pack into the 32-bit CPSR/SPSR register layout (Mesen2 `ArmV3CpuFlags::ToInt32`) — used by
71 /// `MRS` and the (not-yet-ported) exception-entry path. `self.mode` is masked to its real
72 /// 5-bit width here too (belt-and-suspenders on top of [`Regs::switch_mode`]'s own mask, so a
73 /// stray high bit can never leak into the adjacent interrupt-mask/reserved bits regardless of
74 /// how `mode` was set).
75 #[must_use]
76 pub const fn to_u32(self) -> u32 {
77 ((self.flags.n as u32) << 31)
78 | ((self.flags.z as u32) << 30)
79 | ((self.flags.c as u32) << 29)
80 | ((self.flags.v as u32) << 28)
81 | ((self.irq_disable as u32) << 7)
82 | ((self.fiq_disable as u32) << 6)
83 | ((self.mode & 0x1F) as u32)
84 }
85
86 /// The inverse of [`Self::to_u32`] — used only by save-state restore (never by real ARM code,
87 /// which always goes through [`Regs::switch_mode`]/the SPSR-restore path instead of an
88 /// arbitrary 32-bit value). `mode` is masked to 5 bits and OR'd with [`mode::BIT`], matching
89 /// [`Regs::switch_mode`]'s own defensive mask so a corrupted save-state byte can't leak a
90 /// stray high bit into later bank-switch decisions.
91 #[must_use]
92 const fn from_u32(packed: u32) -> Self {
93 Self {
94 #[allow(clippy::cast_possible_truncation)]
95 mode: (packed as u8 & 0x1F) | mode::BIT,
96 fiq_disable: packed & (1 << 6) != 0,
97 irq_disable: packed & (1 << 7) != 0,
98 flags: Flags {
99 n: packed & (1 << 31) != 0,
100 z: packed & (1 << 30) != 0,
101 c: packed & (1 << 29) != 0,
102 v: packed & (1 << 28) != 0,
103 },
104 }
105 }
106
107 fn save_state(self, w: &mut SaveWriter) {
108 w.write_u32(self.to_u32());
109 }
110
111 fn load_state(r: &mut SaveReader) -> Result<Self, SaveStateError> {
112 Ok(Self::from_u32(r.read_u32()?))
113 }
114}
115
116/// The ARM register file: `R0-R15` plus every mode's banked registers and SPSR.
117///
118/// Banking follows real ARM hardware (and Mesen2's `SwitchMode`) exactly: `R8-R12` are shared by
119/// every mode EXCEPT FIQ (which gets its own private `R8-R12`); `R13`/`R14` are banked separately
120/// per mode, INCLUDING a distinct "User" bank from the four privileged non-FIQ modes' banks. This
121/// project's other CPU cores don't need this pattern (the 65C816 and SA-1 have no register
122/// banking), so it has no existing precedent to crib the shape from — ported straight from the
123/// reference `memcpy`-based save/restore sequence as explicit slice copies.
124#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
125pub struct Regs {
126 /// `R0-R15` (R15 = PC) — the currently-banked-in view every instruction reads/writes.
127 pub r: [u32; 16],
128 /// The live CPSR (current mode + interrupt masks + condition flags).
129 pub cpsr: Cpsr,
130
131 /// `R8-R14` (7 slots) for User/System mode; ALSO the shared source of `R8-R12` (only the
132 /// first 5 slots) when banking into/out of Irq/Supervisor/Abort/Undefined — see
133 /// [`Self::switch_mode`]'s doc for exactly how the two roles interact.
134 user_bank: [u32; 7],
135 /// `R8-R14` (7 slots) for FIQ mode — the only mode with its own private `R8-R12`.
136 fiq_bank: [u32; 7],
137 /// `R13-R14` (2 slots) private to IRQ.
138 irq_bank: [u32; 2],
139 /// `R13-R14` (2 slots) private to Supervisor.
140 svc_bank: [u32; 2],
141 /// `R13-R14` (2 slots) private to Abort.
142 abt_bank: [u32; 2],
143 /// `R13-R14` (2 slots) private to Undefined.
144 und_bank: [u32; 2],
145
146 fiq_spsr: Cpsr,
147 irq_spsr: Cpsr,
148 svc_spsr: Cpsr,
149 abt_spsr: Cpsr,
150 und_spsr: Cpsr,
151}
152
153impl Regs {
154 /// Switch the active processor mode, banking `R8-R14` in/out exactly like real ARM hardware
155 /// (Mesen2 `SwitchMode`).
156 ///
157 /// `new_mode` is masked to its real 5-bit hardware width THEN OR'd with [`mode::BIT`]
158 /// unconditionally (bit 4 is always set on real hardware — every caller, including `MSR`'s
159 /// raw mode field, relies on this rather than validating it themselves). The mask keeps
160 /// `self.cpsr.mode` a clean 5-bit value everywhere it's read (in particular
161 /// [`Cpsr::to_u32`]'s packed layout, where a stray high bit would corrupt the adjacent
162 /// interrupt-mask/reserved bits) even if some future caller passes an unmasked byte. A no-op
163 /// if the mode is already active (matches the source's early-return, and avoids a redundant
164 /// full 7-register bank round-trip on every same-mode `MSR`).
165 pub fn switch_mode(&mut self, new_mode: Mode) {
166 let new_mode = (new_mode & 0x1F) | mode::BIT;
167 if self.cpsr.mode == new_mode {
168 return;
169 }
170 let org_mode = self.cpsr.mode;
171
172 // Save the OUTGOING mode's banked registers. FIQ banks all 7 (R8-R14) into its own
173 // array; the four other privileged modes bank only their private R13-R14 (2) into their
174 // own array, and ALSO refresh the shared `user_bank[0..5]` (R8-R12) — since those 5 are
175 // visible from every non-FIQ mode, not just User/System. Leaving User/System (or any
176 // unrecognized mode, matching the source's `default:` fallback) banks the full 7 into
177 // `user_bank`, INCLUDING User mode's own private R13-R14 in slots 5-6.
178 match org_mode {
179 mode::FIQ => self.fiq_bank.copy_from_slice(&self.r[8..15]),
180 mode::IRQ => {
181 self.user_bank[0..5].copy_from_slice(&self.r[8..13]);
182 self.irq_bank.copy_from_slice(&self.r[13..15]);
183 }
184 mode::SUPERVISOR => {
185 self.user_bank[0..5].copy_from_slice(&self.r[8..13]);
186 self.svc_bank.copy_from_slice(&self.r[13..15]);
187 }
188 mode::ABORT => {
189 self.user_bank[0..5].copy_from_slice(&self.r[8..13]);
190 self.abt_bank.copy_from_slice(&self.r[13..15]);
191 }
192 mode::UNDEFINED => {
193 self.user_bank[0..5].copy_from_slice(&self.r[8..13]);
194 self.und_bank.copy_from_slice(&self.r[13..15]);
195 }
196 _ => self.user_bank.copy_from_slice(&self.r[8..15]),
197 }
198
199 self.cpsr.mode = new_mode;
200
201 // Load the INCOMING mode's banked registers, the mirror image of the save above.
202 if new_mode == mode::FIQ {
203 self.r[8..15].copy_from_slice(&self.fiq_bank);
204 } else {
205 self.r[8..15].copy_from_slice(&self.user_bank);
206 match new_mode {
207 mode::IRQ => self.r[13..15].copy_from_slice(&self.irq_bank),
208 mode::SUPERVISOR => self.r[13..15].copy_from_slice(&self.svc_bank),
209 mode::ABORT => self.r[13..15].copy_from_slice(&self.abt_bank),
210 mode::UNDEFINED => self.r[13..15].copy_from_slice(&self.und_bank),
211 _ => {}
212 }
213 }
214 }
215
216 /// The current mode's SPSR (Mesen2 `GetSpsr`). User/System (and any unrecognized mode,
217 /// matching the source's `default:` fallback) have no real SPSR; reading/writing it there is
218 /// architecturally UNPREDICTABLE, and this port follows the source's own choice of aliasing
219 /// the live CPSR itself as a harmless, safe fallback rather than inventing new behavior.
220 #[must_use]
221 pub const fn spsr(&self) -> Cpsr {
222 match self.cpsr.mode {
223 mode::FIQ => self.fiq_spsr,
224 mode::IRQ => self.irq_spsr,
225 mode::SUPERVISOR => self.svc_spsr,
226 mode::ABORT => self.abt_spsr,
227 mode::UNDEFINED => self.und_spsr,
228 _ => self.cpsr,
229 }
230 }
231
232 /// Mutable access to the current mode's SPSR — see [`Self::spsr`].
233 pub const fn spsr_mut(&mut self) -> &mut Cpsr {
234 match self.cpsr.mode {
235 mode::FIQ => &mut self.fiq_spsr,
236 mode::IRQ => &mut self.irq_spsr,
237 mode::SUPERVISOR => &mut self.svc_spsr,
238 mode::ABORT => &mut self.abt_spsr,
239 mode::UNDEFINED => &mut self.und_spsr,
240 _ => &mut self.cpsr,
241 }
242 }
243
244 /// Serialize every register/bank/SPSR (the board wrapper's `Board::save_state` composes this
245 /// with its own handshake/ROM/RAM state — see `docs/st018-arm-notes.md` step 9).
246 pub(crate) fn save_state(&self, w: &mut SaveWriter) {
247 for v in self.r {
248 w.write_u32(v);
249 }
250 self.cpsr.save_state(w);
251 for v in self.user_bank {
252 w.write_u32(v);
253 }
254 for v in self.fiq_bank {
255 w.write_u32(v);
256 }
257 for v in self.irq_bank {
258 w.write_u32(v);
259 }
260 for v in self.svc_bank {
261 w.write_u32(v);
262 }
263 for v in self.abt_bank {
264 w.write_u32(v);
265 }
266 for v in self.und_bank {
267 w.write_u32(v);
268 }
269 self.fiq_spsr.save_state(w);
270 self.irq_spsr.save_state(w);
271 self.svc_spsr.save_state(w);
272 self.abt_spsr.save_state(w);
273 self.und_spsr.save_state(w);
274 }
275
276 /// The inverse of [`Self::save_state`].
277 ///
278 /// # Errors
279 /// Propagates [`SaveReader`]'s own truncation error; every field here is a plain integer/bool
280 /// with no invalid-discriminant risk (unlike an enum), so there is nothing else to reject.
281 pub(crate) fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
282 for v in &mut self.r {
283 *v = r.read_u32()?;
284 }
285 self.cpsr = Cpsr::load_state(r)?;
286 for v in &mut self.user_bank {
287 *v = r.read_u32()?;
288 }
289 for v in &mut self.fiq_bank {
290 *v = r.read_u32()?;
291 }
292 for v in &mut self.irq_bank {
293 *v = r.read_u32()?;
294 }
295 for v in &mut self.svc_bank {
296 *v = r.read_u32()?;
297 }
298 for v in &mut self.abt_bank {
299 *v = r.read_u32()?;
300 }
301 for v in &mut self.und_bank {
302 *v = r.read_u32()?;
303 }
304 self.fiq_spsr = Cpsr::load_state(r)?;
305 self.irq_spsr = Cpsr::load_state(r)?;
306 self.svc_spsr = Cpsr::load_state(r)?;
307 self.abt_spsr = Cpsr::load_state(r)?;
308 self.und_spsr = Cpsr::load_state(r)?;
309 Ok(())
310 }
311}
312
313/// One fetched-but-not-yet-executed instruction word, tagged with the address it was fetched
314/// from (Mesen2 `ArmV3InstructionData`).
315#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
316pub struct Insn {
317 /// The byte address this word was fetched from.
318 pub address: u32,
319 /// The raw 32-bit instruction word.
320 pub opcode: u32,
321}
322
323/// The 3-stage Fetch/Decode/Execute pipeline (Mesen2 `ArmV3CpuPipeline`).
324///
325/// This is the ENTIRE mechanism behind ARM's well-known "PC reads as address+8" quirk: `r15`
326/// (owned by [`Regs`], threaded through [`Self::process`] rather than duplicated here) tracks the
327/// FETCH stage's address, two stages ahead of whatever instruction is currently in Execute. No
328/// `+8` constant exists anywhere in this port — it falls straight out of this stage timing, which
329/// is exactly why getting this model right BEFORE porting any instruction that reads R15 as an
330/// operand matters (see `docs/st018-arm-notes.md`'s pipeline section).
331///
332/// Bus-side access-mode bits (`Sequential`/`Word`/`Byte`/`Prefetch`, used by the board wrapper for
333/// its own cycle-timing bookkeeping) are deliberately NOT modeled here yet — they don't affect
334/// address/opcode sequencing, only the caller's `read_code` timing side effects, and land with
335/// the board wrapper (`docs/st018-arm-notes.md` step 9).
336#[derive(Debug, Clone, Copy, Default)]
337pub struct Pipeline {
338 /// The most recently fetched, not-yet-decoded word.
339 pub fetch: Insn,
340 /// The word decoded on the prior step, about to become `execute` on the next one.
341 pub decode: Insn,
342 /// The instruction actually running on the current step.
343 pub execute: Insn,
344 reload_requested: bool,
345}
346
347impl Pipeline {
348 /// Request a full pipeline flush + refill on the next [`Self::process`] call — set whenever
349 /// an instruction writes R15 (a taken branch, a data-processing `MOV PC, ...`, an `LDR PC,
350 /// ...`, an exception entry, etc.). Not yet wired to any register-write path (that lands with
351 /// instruction execute); exposed now so the pipeline model itself is independently testable.
352 pub const fn request_reload(&mut self) {
353 self.reload_requested = true;
354 }
355
356 /// Word-align `r15`, then fetch twice — landing the branch target in Decode and the
357 /// instruction after it in Fetch (Mesen2 `ReloadPipeline`). Leaves `execute` holding whatever
358 /// was in `decode` before the reload (stale, discarded); [`Self::process`]'s own unconditional
359 /// shift immediately after this call promotes the real target into `execute` before anything
360 /// reads it, so the staleness is never observable.
361 fn reload(&mut self, r15: &mut u32, read_code: &mut impl FnMut(u32) -> u32) {
362 self.reload_requested = false;
363 *r15 &= !0x3;
364 // The source writes this fetch into `self.fetch` first, then immediately shifts it into
365 // `self.decode` (and shifts the stale prior `decode` into `execute`) before this
366 // function returns. Both intermediate writes are dead: `process`'s own unconditional
367 // shift, right after this call returns, overwrites `execute` again before anything reads
368 // it. Skip straight to the observable end state instead of replaying the redundant steps.
369 self.decode = Insn {
370 address: *r15,
371 opcode: read_code(*r15),
372 };
373 *r15 = r15.wrapping_add(4);
374 self.fetch = Insn {
375 address: *r15,
376 opcode: read_code(*r15),
377 };
378 }
379
380 /// Advance the pipeline by one stage (Mesen2 `ProcessPipeline`): reload first if requested,
381 /// then unconditionally shift Execute←Decode←Fetch←(a fresh fetch at `r15+4`). Call this once
382 /// per CPU step, AFTER the current Execute-stage instruction has run.
383 pub fn process(&mut self, r15: &mut u32, mut read_code: impl FnMut(u32) -> u32) {
384 if self.reload_requested {
385 self.reload(r15, &mut read_code);
386 }
387 self.execute = self.decode;
388 self.decode = self.fetch;
389 *r15 = r15.wrapping_add(4);
390 self.fetch = Insn {
391 address: *r15,
392 opcode: read_code(*r15),
393 };
394 }
395
396 pub(crate) fn save_state(&self, w: &mut SaveWriter) {
397 self.fetch.save_state(w);
398 self.decode.save_state(w);
399 self.execute.save_state(w);
400 w.write_bool(self.reload_requested);
401 }
402
403 /// # Errors
404 /// Propagates [`SaveReader`]'s own truncation error.
405 pub(crate) fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
406 self.fetch = Insn::load_state(r)?;
407 self.decode = Insn::load_state(r)?;
408 self.execute = Insn::load_state(r)?;
409 self.reload_requested = r.read_bool()?;
410 Ok(())
411 }
412}
413
414impl Insn {
415 fn save_state(self, w: &mut SaveWriter) {
416 w.write_u32(self.address);
417 w.write_u32(self.opcode);
418 }
419
420 fn load_state(r: &mut SaveReader) -> Result<Self, SaveStateError> {
421 Ok(Self {
422 address: r.read_u32()?,
423 opcode: r.read_u32()?,
424 })
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 fn r8_to_r12_are_shared_across_every_non_fiq_mode() {
434 let mut regs = Regs::default();
435 regs.switch_mode(mode::IRQ);
436 regs.r[8] = 0xAAAA_AAAA;
437 regs.switch_mode(mode::SUPERVISOR);
438 // R8 must still read the value written under IRQ -- R8-R12 are the SAME physical
439 // registers in every non-FIQ mode, not independently banked per mode.
440 assert_eq!(regs.r[8], 0xAAAA_AAAA);
441 regs.switch_mode(mode::ABORT);
442 assert_eq!(regs.r[8], 0xAAAA_AAAA);
443 regs.switch_mode(mode::UNDEFINED);
444 assert_eq!(regs.r[8], 0xAAAA_AAAA);
445 regs.switch_mode(mode::USER);
446 assert_eq!(regs.r[8], 0xAAAA_AAAA);
447 }
448
449 #[test]
450 fn fiq_has_its_own_private_r8_to_r12() {
451 let mut regs = Regs::default();
452 regs.r[8] = 0x1111_1111; // written while in the power-on default (User) mode
453 regs.switch_mode(mode::FIQ);
454 // FIQ's R8 is a DIFFERENT physical register -- starts at the power-on default (0), not
455 // the User-mode value just written.
456 assert_eq!(regs.r[8], 0);
457 regs.r[8] = 0x2222_2222;
458 regs.switch_mode(mode::USER);
459 // Back in User mode, R8 reads the ORIGINAL User-mode value -- FIQ's write didn't leak.
460 assert_eq!(regs.r[8], 0x1111_1111);
461 }
462
463 #[test]
464 fn r13_r14_are_privately_banked_per_mode_including_a_distinct_user_bank() {
465 let mut regs = Regs::default();
466 regs.r[13] = 0x1000; // User mode's own SP
467 regs.r[14] = 0x1004;
468
469 regs.switch_mode(mode::IRQ);
470 regs.r[13] = 0x2000;
471 regs.r[14] = 0x2004;
472
473 regs.switch_mode(mode::SUPERVISOR);
474 // Supervisor gets its OWN R13/R14 bank, starting fresh (0), not IRQ's values.
475 assert_eq!(regs.r[13], 0);
476 assert_eq!(regs.r[14], 0);
477 regs.r[13] = 0x3000;
478 regs.r[14] = 0x3004;
479
480 regs.switch_mode(mode::IRQ);
481 assert_eq!(regs.r[13], 0x2000, "IRQ's own bank must round-trip");
482 assert_eq!(regs.r[14], 0x2004);
483
484 regs.switch_mode(mode::USER);
485 assert_eq!(
486 regs.r[13], 0x1000,
487 "User mode's bank is distinct from every privileged mode's"
488 );
489 assert_eq!(regs.r[14], 0x1004);
490
491 regs.switch_mode(mode::SUPERVISOR);
492 assert_eq!(
493 regs.r[13], 0x3000,
494 "Supervisor's own bank must also round-trip"
495 );
496 assert_eq!(regs.r[14], 0x3004);
497 }
498
499 #[test]
500 fn system_mode_shares_the_user_bank() {
501 // System mode uses the SAME R13/R14 bank as User (both are `_ => user_bank` in the
502 // source's switch); confirm a value written under User is visible under System.
503 let mut regs = Regs::default();
504 regs.r[13] = 0x5000;
505 regs.switch_mode(mode::SYSTEM);
506 assert_eq!(regs.r[13], 0x5000);
507 }
508
509 #[test]
510 fn switching_to_the_same_mode_is_a_no_op() {
511 let mut regs = Regs::default();
512 regs.switch_mode(mode::IRQ);
513 regs.r[13] = 0xDEAD_BEEF;
514 regs.switch_mode(mode::IRQ); // already active -- must not clobber via a spurious bank swap
515 assert_eq!(regs.r[13], 0xDEAD_BEEF);
516 }
517
518 #[test]
519 fn mode_bit_4_is_always_forced_set() {
520 let mut regs = Regs::default();
521 // Pass a raw mode value with bit 4 clear -- switch_mode must OR it in, matching every
522 // real mode constant, per the source's unconditional `mode | 0x10`.
523 regs.switch_mode(0b0001);
524 assert_eq!(regs.cpsr.mode, mode::FIQ);
525 }
526
527 #[test]
528 fn mode_is_masked_to_5_bits_even_when_the_caller_passes_stray_high_bits() {
529 // A high bit above the real 5-bit mode field must never survive into `cpsr.mode` --
530 // otherwise it would corrupt the adjacent interrupt-mask/reserved bits once packed by
531 // `Cpsr::to_u32`.
532 let mut regs = Regs::default();
533 regs.switch_mode(0xE1); // 0xE1 & 0x1F == 0x01, | mode::BIT == mode::FIQ
534 assert_eq!(regs.cpsr.mode, mode::FIQ);
535 assert_eq!(
536 regs.cpsr.to_u32() & 0xFF,
537 u32::from(mode::FIQ),
538 "no stray high bits leak into the packed CPSR's interrupt-mask/reserved bits"
539 );
540 }
541
542 #[test]
543 fn unrecognized_mode_values_bank_like_user_system() {
544 // A raw 5-bit pattern matching none of the 7 real modes (e.g. 0b10100) is architecturally
545 // reserved/UNPREDICTABLE on real hardware; this port's fallback treats it like User/System
546 // for banking purposes, matching the source's own `default:` case.
547 let mut regs = Regs::default();
548 regs.r[13] = 0x9000;
549 regs.switch_mode(0b10100);
550 assert_eq!(regs.r[13], 0x9000, "falls back to the User/System bank");
551 }
552
553 #[test]
554 fn spsr_routes_to_the_current_privileged_modes_own_register() {
555 let mut regs = Regs::default();
556 regs.switch_mode(mode::IRQ);
557 regs.spsr_mut().mode = mode::SUPERVISOR;
558 regs.switch_mode(mode::SUPERVISOR);
559 // A DIFFERENT physical SPSR from IRQ's -- starts at the power-on default.
560 assert_eq!(regs.spsr().mode, mode::USER);
561 regs.switch_mode(mode::IRQ);
562 assert_eq!(
563 regs.spsr().mode,
564 mode::SUPERVISOR,
565 "IRQ's SPSR must round-trip"
566 );
567 }
568
569 #[test]
570 fn spsr_in_user_or_system_mode_aliases_the_live_cpsr() {
571 // User/System have no real SPSR; the source's fallback aliases CPSR itself.
572 let mut regs = Regs::default();
573 regs.cpsr.flags.n = true;
574 assert!(regs.spsr().flags.n);
575 }
576
577 #[test]
578 fn pipeline_reload_lands_the_branch_target_in_execute_after_one_process_call() {
579 // `process` calls `reload` (which lands the target in Decode via its own internal
580 // shift), then IMMEDIATELY does its own unconditional shift on top -- so by the time
581 // `process` returns, the target has already been promoted one stage further, into
582 // Execute, ready to run on the very next `Exec` call. Decode/Fetch sit one/two words past
583 // it, exactly the normal steady-state spacing.
584 let mut pipeline = Pipeline::default();
585 let mut r15 = 0x1000u32;
586 pipeline.request_reload();
587 pipeline.process(&mut r15, |addr| addr);
588
589 assert_eq!(pipeline.execute.address, 0x1000, "the reload target itself");
590 assert_eq!(pipeline.decode.address, 0x1004);
591 assert_eq!(pipeline.fetch.address, 0x1008);
592 assert_eq!(
593 r15, 0x1008,
594 "r15 tracks the Fetch stage, execute.address + 8"
595 );
596 }
597
598 #[test]
599 fn pc_plus_8_falls_out_of_pipeline_timing_with_no_explicit_constant() {
600 // Reproduce power-on (Mesen2 PowerOn -> ProcessPipeline with ReloadRequested already set)
601 // starting at address 0, then confirm the invariant holds continuously across further
602 // steps: whenever an instruction sits in Execute, r15 == that instruction's own address + 8.
603 let mut pipeline = Pipeline::default();
604 let mut r15 = 0u32;
605 pipeline.request_reload();
606 pipeline.process(&mut r15, |addr| addr);
607
608 assert_eq!(pipeline.execute.address, 0);
609 assert_eq!(
610 r15, 8,
611 "r15 == execute.address + 8, immediately after power-on"
612 );
613
614 // Advance further WITHOUT another reload (normal sequential execution) -- the invariant
615 // must keep holding for every subsequent instruction that reaches Execute.
616 pipeline.process(&mut r15, |addr| addr);
617 assert_eq!(pipeline.execute.address, 4);
618 assert_eq!(
619 r15, 12,
620 "r15 == execute.address + 8 again, one instruction later"
621 );
622
623 pipeline.process(&mut r15, |addr| addr);
624 assert_eq!(pipeline.execute.address, 8);
625 assert_eq!(r15, 16);
626 }
627
628 #[test]
629 fn a_taken_branch_forces_a_full_refetch_not_an_incremental_step() {
630 let mut pipeline = Pipeline::default();
631 let mut r15 = 0u32;
632 pipeline.request_reload();
633 pipeline.process(&mut r15, |addr| addr); // power-on refill
634
635 // Simulate the branch instruction at Execute (address 0) jumping to 0x8000.
636 pipeline.request_reload();
637 let mut branch_target = 0x8000u32;
638 pipeline.process(&mut branch_target, |addr| addr);
639
640 assert_eq!(
641 pipeline.execute.address, 0x8000,
642 "the branch target is in Execute the very next step, not delayed further"
643 );
644 assert_eq!(
645 branch_target, 0x8008,
646 "r15 re-establishes +8 at the new target"
647 );
648 }
649
650 #[test]
651 fn fetch_reload_alignment_masks_to_a_word_boundary() {
652 let mut pipeline = Pipeline::default();
653 let mut r15 = 0x1003u32; // deliberately misaligned
654 pipeline.request_reload();
655 pipeline.process(&mut r15, |addr| addr);
656 assert_eq!(
657 pipeline.execute.address, 0x1000,
658 "misaligned reload target is word-aligned"
659 );
660 }
661}