rustysnes_cpu/regs.rs
1//! WDC 65C816 register file and processor-status flags.
2//!
3//! The 65C816 has variable-width data paths: the accumulator `A` is 8- or 16-bit per the
4//! `M` status flag, and the index registers `X`/`Y` are 8- or 16-bit per the `X` status
5//! flag. In 8-bit index mode the high byte of `X`/`Y` is forced to zero. The hidden `E`
6//! (emulation) latch forces `M`/`X` to the 8-bit width and confines the stack to page `$01`.
7//!
8//! See `docs/cpu.md` ("Registers and state", "Emulation vs native mode").
9
10// Taking the low byte of a 16-bit register to set 8-bit-width flags is a deliberate, ubiquitous
11// truncation in the width model; the cast-precision lints would flag every such narrowing.
12#![allow(clippy::cast_possible_truncation)]
13
14use bitflags::bitflags;
15
16bitflags! {
17 /// Processor status register `P` (8 bits) plus the hidden emulation latch is tracked
18 /// separately on [`Regs`]. Bit layout matches the hardware `P` register so `PHP`/`PLP`
19 /// round-trip exactly.
20 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
21 pub struct Status: u8 {
22 /// Carry.
23 const C = 0b0000_0001;
24 /// Zero.
25 const Z = 0b0000_0010;
26 /// IRQ disable (IRQ honored only when clear).
27 const I = 0b0000_0100;
28 /// Decimal mode (BCD arithmetic for `ADC`/`SBC`).
29 const D = 0b0000_1000;
30 /// Index-register width: set ⇒ 8-bit `X`/`Y`. In emulation mode this also doubles as
31 /// the 6502 `B` (break) flag on the stacked status byte.
32 const X = 0b0001_0000;
33 /// Memory/accumulator width: set ⇒ 8-bit `A`/memory.
34 const M = 0b0010_0000;
35 /// Overflow.
36 const V = 0b0100_0000;
37 /// Negative.
38 const N = 0b1000_0000;
39 }
40}
41
42/// The complete architectural register file of the 65C816.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct Regs {
45 /// Accumulator (full 16 bits stored; the active width is the `M` flag).
46 pub a: u16,
47 /// `X` index register (full 16 bits stored; the active width is the `X` flag).
48 pub x: u16,
49 /// `Y` index register (full 16 bits stored; the active width is the `X` flag).
50 pub y: u16,
51 /// Stack pointer (16-bit in native mode; forced to page `$01` in emulation mode).
52 pub s: u16,
53 /// Direct-page register `D`.
54 pub d: u16,
55 /// Data bank register `DBR`.
56 pub dbr: u8,
57 /// Program bank register `PBR` / `K`.
58 pub pbr: u8,
59 /// Program counter (16-bit; bank is `pbr`).
60 pub pc: u16,
61 /// Processor status flags `P`.
62 pub p: Status,
63 /// Hidden emulation-mode latch `E` (`true` at power-on).
64 pub emulation: bool,
65}
66
67impl Default for Regs {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl Regs {
74 /// Power-on register file: emulation mode (`E=1`), `M=1`, `X=1`, `I=1`, `D=0`, with the
75 /// stack pointer parked at `$01FF`. `PC` is loaded from the reset vector by
76 /// [`crate::Cpu::reset`].
77 #[must_use]
78 pub const fn new() -> Self {
79 Self {
80 a: 0,
81 x: 0,
82 y: 0,
83 s: 0x01FF,
84 d: 0,
85 dbr: 0,
86 pbr: 0,
87 pc: 0,
88 // I and the width flags are set at reset; emulation forces M/X high.
89 p: Status::from_bits_truncate(Status::M.bits() | Status::X.bits() | Status::I.bits()),
90 emulation: true,
91 }
92 }
93
94 /// Whether the accumulator / memory width is 8-bit (`M` flag set, or emulation mode).
95 #[must_use]
96 pub const fn m8(&self) -> bool {
97 self.emulation || self.p.contains(Status::M)
98 }
99
100 /// Whether the index registers are 8-bit (`X` flag set, or emulation mode).
101 #[must_use]
102 pub const fn x8(&self) -> bool {
103 self.emulation || self.p.contains(Status::X)
104 }
105
106 /// Read the accumulator at the active width (high byte masked off when 8-bit).
107 #[must_use]
108 pub const fn a_val(&self) -> u16 {
109 if self.m8() { self.a & 0x00FF } else { self.a }
110 }
111
112 /// Write the accumulator at the active width, preserving the hidden high byte (`B`) when
113 /// in 8-bit mode — matching hardware, where `A` and `B` are distinct halves.
114 pub const fn set_a(&mut self, val: u16) {
115 if self.m8() {
116 self.a = (self.a & 0xFF00) | (val & 0x00FF);
117 } else {
118 self.a = val;
119 }
120 }
121
122 /// Read `X` at the active index width.
123 #[must_use]
124 pub const fn x_val(&self) -> u16 {
125 if self.x8() { self.x & 0x00FF } else { self.x }
126 }
127
128 /// Read `Y` at the active index width.
129 #[must_use]
130 pub const fn y_val(&self) -> u16 {
131 if self.x8() { self.y & 0x00FF } else { self.y }
132 }
133
134 /// Write `X`; in 8-bit index mode the high byte is forced to zero (hardware behavior).
135 pub const fn set_x(&mut self, val: u16) {
136 self.x = if self.x8() { val & 0x00FF } else { val };
137 }
138
139 /// Write `Y`; in 8-bit index mode the high byte is forced to zero (hardware behavior).
140 pub const fn set_y(&mut self, val: u16) {
141 self.y = if self.x8() { val & 0x00FF } else { val };
142 }
143
144 /// Update `N` and `Z` from an accumulator-width result.
145 pub const fn set_nz_m(&mut self, val: u16) {
146 if self.m8() {
147 self.set_nz8(val as u8);
148 } else {
149 self.set_nz16(val);
150 }
151 }
152
153 /// Update `N` and `Z` from an index-width result.
154 pub const fn set_nz_x(&mut self, val: u16) {
155 if self.x8() {
156 self.set_nz8(val as u8);
157 } else {
158 self.set_nz16(val);
159 }
160 }
161
162 /// Update `N`/`Z` from an explicit 8-bit value.
163 pub const fn set_nz8(&mut self, val: u8) {
164 self.set_flag(Status::Z, val == 0);
165 self.set_flag(Status::N, val & 0x80 != 0);
166 }
167
168 /// Update `N`/`Z` from an explicit 16-bit value.
169 pub const fn set_nz16(&mut self, val: u16) {
170 self.set_flag(Status::Z, val == 0);
171 self.set_flag(Status::N, val & 0x8000 != 0);
172 }
173
174 /// Set or clear a single status flag.
175 pub const fn set_flag(&mut self, flag: Status, on: bool) {
176 if on {
177 self.p = Status::from_bits_truncate(self.p.bits() | flag.bits());
178 } else {
179 self.p = Status::from_bits_truncate(self.p.bits() & !flag.bits());
180 }
181 }
182}