Skip to main content

rustysnes_cpu/
lib.rs

1//! `rustysnes-cpu` — WDC 65C816 (cpu).
2//!
3//! 16-bit 65C816 main CPU; emulation/native modes; region-variable (FastROM/SlowROM) access
4//! speed. The CPU borrows `&mut impl Bus` for the duration of an instruction (the RustyNES
5//! "Bus owns everything mutable" rule — the CPU never owns the PPU/APU/cart). The concrete
6//! `Bus` impl lives in `rustysnes-core`; this crate only sees the narrow [`Bus`] trait.
7//!
8//! Part of the one-directional chip-crate graph (see `docs/architecture.md`): this crate
9//! does NOT depend on the other chip crates. `#![no_std]` + alloc so it cross-compiles to a
10//! bare-metal target; only the frontend carries `std` + `unsafe`.
11//!
12//! # Cycle-count unit
13//!
14//! [`Cpu::step`] returns a count of **CPU cycles** (one per bus byte access plus internal I/O
15//! cycles the instruction consumed), per the standard 65C816 timing tables and the
16//! variable-timing rules in `docs/cpu.md`. It does **not** apply the per-access master-clock
17//! speed weighting (6/8/12) — the CPU asks the Bus for that via `Bus::access_cycles` and drives
18//! it with `Bus::advance`. The value equals the increment of [`Cpu::cycles`] across the call.
19
20#![no_std]
21#![forbid(unsafe_code)]
22extern crate alloc;
23
24pub mod addr;
25pub mod bus;
26pub mod disasm;
27pub mod exec;
28pub mod regs;
29
30pub use addr::{Effective, Mode};
31pub use bus::Bus;
32pub use regs::{Regs, Status};
33
34use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
35
36/// Native-mode interrupt/exception vector addresses (bank `0`).
37pub mod vectors {
38    /// Native COP software interrupt vector (`$00FFE4`).
39    pub const COP_NATIVE: u32 = 0x00_FFE4;
40    /// Native BRK vector (`$00FFE6`).
41    pub const BRK_NATIVE: u32 = 0x00_FFE6;
42    /// Native ABORT vector (`$00FFE8`).
43    pub const ABORT_NATIVE: u32 = 0x00_FFE8;
44    /// Native NMI vector (`$00FFEA`).
45    pub const NMI_NATIVE: u32 = 0x00_FFEA;
46    /// Native IRQ vector (`$00FFEE`).
47    pub const IRQ_NATIVE: u32 = 0x00_FFEE;
48    /// Emulation COP vector (`$00FFF4`).
49    pub const COP_EMU: u32 = 0x00_FFF4;
50    /// Emulation ABORT vector (`$00FFF8`).
51    pub const ABORT_EMU: u32 = 0x00_FFF8;
52    /// Emulation NMI vector (`$00FFFA`).
53    pub const NMI_EMU: u32 = 0x00_FFFA;
54    /// RESET vector (always emulation-table; `$00FFFC`).
55    pub const RESET: u32 = 0x00_FFFC;
56    /// Emulation IRQ/BRK vector (`$00FFFE`).
57    pub const IRQ_BRK_EMU: u32 = 0x00_FFFE;
58}
59
60/// WDC 65C816 CPU core.
61///
62/// Holds the architectural [`Regs`] register file plus bookkeeping. The model is driven one
63/// instruction at a time via [`Cpu::step`]; the bus is borrowed for the call's duration.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct Cpu {
66    /// Architectural register file (A/X/Y/S/D/DBR/PBR/PC/P/E).
67    pub regs: Regs,
68    /// Cumulative CPU cycles consumed across all instructions (one per bus access or I/O cycle).
69    pub cycles: u64,
70    /// `WAI` latch: the CPU is waiting for an interrupt; cleared when one is taken.
71    pub waiting: bool,
72    /// `STP` latch: the CPU has been stopped and resumes only on reset.
73    pub stopped: bool,
74    /// Per-instruction CPU-cycle accumulator (reset at the start of each [`Cpu::step`]).
75    cyc: u32,
76}
77
78impl Default for Cpu {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84impl Cpu {
85    /// Construct at power-on (emulation mode). Call [`Cpu::reset`] to load `PC` from the
86    /// reset vector before stepping.
87    #[must_use]
88    pub const fn new() -> Self {
89        Self {
90            regs: Regs::new(),
91            cycles: 0,
92            waiting: false,
93            stopped: false,
94            cyc: 0,
95        }
96    }
97
98    /// Power-on / reset: force emulation mode (`E=1`, `M=1`, `X=1`, `I=1`, `D=0`), park the
99    /// stack at `$01FF`, clear the `WAI`/`STP` latches, and load `PC` from the emulation
100    /// RESET vector at `$00FFFC/$00FFFD`. `cycles` is left as-is (cumulative counter).
101    pub fn reset(&mut self, bus: &mut impl Bus) {
102        self.regs = Regs::new();
103        self.waiting = false;
104        self.stopped = false;
105        self.cyc = 0;
106        let lo = self.bus_read8(bus, vectors::RESET);
107        let hi = self.bus_read8(bus, vectors::RESET + 1);
108        self.regs.pc = u16::from(lo) | (u16::from(hi) << 8);
109        self.regs.pbr = 0;
110    }
111
112    /// Write the full architectural register file plus the `WAI`/`STP` latches and cumulative
113    /// cycle counter into a `"CPU0"` section. `cyc` (the per-instruction accumulator, reset at
114    /// the start of every [`Cpu::step`]) is included too even though a save-state can only ever
115    /// be taken between instructions (where it's always `0`) — it costs one `u32` to round-trip
116    /// exactly rather than assume that invariant holds for every future caller of this format.
117    pub fn save_state(&self, w: &mut SaveWriter) {
118        w.section(*b"CPU0", |s| {
119            s.write_u16(self.regs.a);
120            s.write_u16(self.regs.x);
121            s.write_u16(self.regs.y);
122            s.write_u16(self.regs.s);
123            s.write_u16(self.regs.d);
124            s.write_u8(self.regs.dbr);
125            s.write_u8(self.regs.pbr);
126            s.write_u16(self.regs.pc);
127            s.write_u8(self.regs.p.bits());
128            s.write_bool(self.regs.emulation);
129            s.write_u64(self.cycles);
130            s.write_bool(self.waiting);
131            s.write_bool(self.stopped);
132            s.write_u32(self.cyc);
133        });
134    }
135
136    /// The inverse of [`Self::save_state`].
137    ///
138    /// # Errors
139    /// [`SaveStateError`] on truncated/corrupt input or a section with unconsumed trailing
140    /// bytes. `p` is restored via [`Status::from_bits_truncate`], which silently drops any bit
141    /// outside the flag set instead of erroring — `Status` covers all 8 bits of the real
142    /// hardware register, so no encoding of a save-state byte is actually invalid here. `x`/`y`/
143    /// `s`/`pbr` are masked to the widths `emulation`/`Status::X` force during normal operation
144    /// (8-bit `X`/`Y` with the high byte forced to zero, `S` confined to page `$01`, `PBR` forced
145    /// to `0`, per `docs/cpu.md`'s emulation-mode rules) — the same "apply the engine's own
146    /// normal-operation invariant on load" reasoning already applied elsewhere in this project.
147    pub fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
148        let mut s = r.expect_section(*b"CPU0")?;
149        self.regs.a = s.read_u16()?;
150        self.regs.x = s.read_u16()?;
151        self.regs.y = s.read_u16()?;
152        self.regs.s = s.read_u16()?;
153        self.regs.d = s.read_u16()?;
154        self.regs.dbr = s.read_u8()?;
155        self.regs.pbr = s.read_u8()?;
156        self.regs.pc = s.read_u16()?;
157        self.regs.p = Status::from_bits_truncate(s.read_u8()?);
158        self.regs.emulation = s.read_bool()?;
159        self.cycles = s.read_u64()?;
160        self.waiting = s.read_bool()?;
161        self.stopped = s.read_bool()?;
162        self.cyc = s.read_u32()?;
163        if s.remaining() != 0 {
164            return Err(SaveStateError::Invalid(alloc::format!(
165                "CPU0 section has {} trailing byte(s)",
166                s.remaining()
167            )));
168        }
169        if self.regs.emulation {
170            self.regs.x &= 0x00FF;
171            self.regs.y &= 0x00FF;
172            self.regs.s = 0x0100 | (self.regs.s & 0x00FF);
173            self.regs.pbr = 0;
174        } else if self.regs.p.contains(Status::X) {
175            self.regs.x &= 0x00FF;
176            self.regs.y &= 0x00FF;
177        }
178        Ok(())
179    }
180}
181
182#[cfg(test)]
183mod tests;