Skip to main content

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}