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;