1//! CPU emulation code. 2 3use crate::api::NesEmulatorConfig; 4use crate::apu::ApuState; 5use crate::bus::{CpuBus, PpuRegister}; 6use bincode::{Decode, Encode}; 7use mos6502_emu::Mos6502; 8use mos6502_emu::bus::BusInterface; 9 10const JOYPAD_REGISTERS: [u16; 2] = [0x4016, 0x4017]; 11 12#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 13enum OamDmaState { 14 Idle, 15 Pending, 16 ReadReady { address_low: u8 }, 17 WriteReady { address_low: u8, byte: u8 }, 18} 19 20impl OamDmaState { 21 #[must_use] 22 fn progress_noop(self) -> Self { 23 match self { 24 Self::Pending => Self::ReadReady { address_low: 0 }, 25 _ => self, 26 } 27 } 28} 29 30#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 31enum DmcDmaState { 32 Idle, 33 PendingLoad, 34 PendingReload, 35 Pending, 36 DummyCycle, 37 ReadReady, 38} 39 40impl DmcDmaState { 41 #[must_use] 42 fn progress_noop(self, cpu_halted: bool, dma_cycle: DmaCycle, still_needs_dma: bool) -> Self { 43 if !still_needs_dma { 44 return Self::Idle; 45 } 46 47 match (self, dma_cycle) { 48 (Self::PendingLoad, DmaCycle::Get) | (Self::PendingReload, DmaCycle::Put) => { 49 if cpu_halted { 50 Self::DummyCycle 51 } else { 52 Self::Pending 53 } 54 } 55 (Self::Pending, _) if cpu_halted => Self::DummyCycle, 56 (Self::DummyCycle, _) => Self::ReadReady, 57 _ => self, 58 } 59 } 60} 61 62#[derive(Debug, Clone, Copy, PartialEq, Eq)] 63enum DmaCycle { 64 Get, 65 Put, 66} 67 68#[derive(Debug, Clone, Encode, Decode)] 69pub struct CpuState { 70 cpu: Mos6502, 71 halted_cpu_address: Option<u16>, 72 oam_dma: OamDmaState, 73 dmc_dma: DmcDmaState, 74} 75 76impl CpuState { 77 pub fn new(bus: &mut CpuBus<'_>) -> Self { 78 let cpu = Mos6502::new_nes(bus); 79 80 Self { 81 cpu, 82 halted_cpu_address: None, 83 oam_dma: OamDmaState::Idle, 84 dmc_dma: DmcDmaState::Idle, 85 } 86 } 87} 88 89/// Run the CPU for 1 CPU cycle. 90pub fn tick( 91 state: &mut CpuState, 92 bus: &mut CpuBus<'_>, 93 apu: &mut ApuState, 94 config: &NesEmulatorConfig, 95) { 96 if state.cpu.frozen() { 97 return; 98 } 99 100 if bus.is_oamdma_dirty() { 101 bus.clear_oamdma_dirty(); 102 state.oam_dma = OamDmaState::Pending; 103 } 104 105 let needs_dmc_dma = apu.needs_dmc_dma(); 106 if needs_dmc_dma && state.dmc_dma == DmcDmaState::Idle { 107 state.dmc_dma = if apu.dmc_dma_initial_load() { 108 DmcDmaState::PendingLoad 109 } else { 110 DmcDmaState::PendingReload 111 }; 112 } 113 114 if state.oam_dma == OamDmaState::Idle && state.dmc_dma == DmcDmaState::Idle { 115 state.cpu.tick(bus); 116 state.halted_cpu_address = None; 117 return; 118 } 119 120 let dma_cycle = if apu.is_active_cycle() { DmaCycle::Put } else { DmaCycle::Get }; 121 let Some(halted_cpu_address) = state.halted_cpu_address else { 122 try_halt_cpu(state, dma_cycle, needs_dmc_dma, bus, config); 123 return; 124 }; 125 126 progress_dma(state, apu, halted_cpu_address, dma_cycle, bus, config); 127} 128 129#[derive(Debug, Clone, Copy, PartialEq, Eq)] 130enum CpuCycle { 131 Read { address: u16 }, 132 Write, 133} 134 135fn try_halt_cpu( 136 state: &mut CpuState, 137 dma_cycle: DmaCycle, 138 still_needs_dmc_dma: bool, 139 bus: &mut CpuBus<'_>, 140 config: &NesEmulatorConfig, 141) { 142 struct CapturingBus<'a, 'b, 'c>(&'a mut CpuBus<'b>, CpuCycle, &'c NesEmulatorConfig); 143 144 impl BusInterface for CapturingBus<'_, '_, '_> { 145 fn read(&mut self, address: u16) -> u8 { 146 self.1 = CpuCycle::Read { address }; 147 148 if self.2.dma_dummy_joy_reads || !JOYPAD_REGISTERS.contains(&address) { 149 self.0.read(address) 150 } else { 151 // Return whatever, CPU state won't be used anyway because it will be halted 152 0 153 } 154 } 155 156 fn write(&mut self, address: u16, value: u8) { 157 self.1 = CpuCycle::Write; 158 self.0.write(address, value); 159 } 160 161 fn nmi(&self) -> bool { 162 self.0.nmi() 163 } 164 165 fn acknowledge_nmi(&mut self) { 166 // NMIs are only acknowledged during write cycles, so let it go through 167 self.0.acknowledge_nmi(); 168 } 169 170 fn irq(&self) -> bool { 171 self.0.irq() 172 } 173 } 174 175 let mut cpu_clone = state.cpu.clone(); 176 let mut bus = CapturingBus(bus, CpuCycle::Write, config); 177 cpu_clone.tick(&mut bus); 178 179 let cpu_halted; 180 match bus.1 { 181 CpuCycle::Read { address } => { 182 // Halt succeeded 183 cpu_halted = true; 184 state.halted_cpu_address = Some(address); 185 state.oam_dma = state.oam_dma.progress_noop(); 186 } 187 CpuCycle::Write => { 188 // Halt failed; try again next cycle 189 cpu_halted = false; 190 state.cpu = cpu_clone; 191 } 192 } 193 194 state.dmc_dma = state.dmc_dma.progress_noop(cpu_halted, dma_cycle, still_needs_dmc_dma); 195 196 log::trace!( 197 "Attempted to halt CPU; CPU cycle was {:?}, OAM DMA state {:?}, DMC DMA state {:?}", 198 bus.1, 199 state.oam_dma, 200 state.dmc_dma 201 ); 202} 203 204fn progress_dma( 205 state: &mut CpuState, 206 apu: &mut ApuState, 207 halted_cpu_address: u16, 208 dma_cycle: DmaCycle, 209 bus: &mut CpuBus<'_>, 210 config: &NesEmulatorConfig, 211) { 212 log::trace!( 213 "Progressing DMA, current OAM DMA state {:?}, current DMC DMA state {:?}", 214 state.oam_dma, 215 state.dmc_dma 216 ); 217 218 let still_needs_dmc_dma = apu.needs_dmc_dma(); 219 220 match dma_cycle { 221 DmaCycle::Get => { 222 if state.dmc_dma == DmcDmaState::ReadReady { 223 // DMC DMA read cycle; takes priority over OAM DMA read if both are ready 224 apu.dmc_dma_read(bus, halted_cpu_address, config); 225 state.dmc_dma = DmcDmaState::Idle; 226 227 state.oam_dma = state.oam_dma.progress_noop(); 228 229 log::trace!(" DMC DMA read; OAM DMA state is now {:?}", state.oam_dma); 230 231 return; 232 } 233 234 if let OamDmaState::ReadReady { address_low } = state.oam_dma { 235 // OAM DMA read cycle 236 let address = u16::from_le_bytes([address_low, bus.read_oamdma_for_transfer()]); 237 let byte = bus.oam_dma_read(address, halted_cpu_address, config); 238 state.oam_dma = OamDmaState::WriteReady { address_low, byte }; 239 240 state.dmc_dma = state.dmc_dma.progress_noop(true, dma_cycle, still_needs_dmc_dma); 241 242 log::trace!( 243 " OAM DMA read; OAM DMA state is now {:?}, DMC DMA state is now {:?}", 244 state.oam_dma, 245 state.dmc_dma 246 ); 247 248 return; 249 } 250 } 251 DmaCycle::Put => { 252 if let OamDmaState::WriteReady { mut address_low, byte } = state.oam_dma { 253 // OAM DMA write cycle 254 bus.write(PpuRegister::OAMDATA.to_address(), byte); 255 256 let done; 257 (address_low, done) = address_low.overflowing_add(1); 258 state.oam_dma = 259 if done { OamDmaState::Idle } else { OamDmaState::ReadReady { address_low } }; 260 261 state.dmc_dma = state.dmc_dma.progress_noop(true, dma_cycle, still_needs_dmc_dma); 262 263 log::trace!( 264 " OAM DMA write; OAM DMA state is now {:?}, DMC DMA state is now {:?}", 265 state.oam_dma, 266 state.dmc_dma 267 ); 268 269 return; 270 } 271 } 272 } 273 274 // Neither DMA used the bus this cycle; progress no-op cycles and perform a dummy read 275 state.oam_dma = state.oam_dma.progress_noop(); 276 state.dmc_dma = state.dmc_dma.progress_noop(true, dma_cycle, still_needs_dmc_dma); 277 278 if config.dma_dummy_joy_reads || !JOYPAD_REGISTERS.contains(&halted_cpu_address) { 279 bus.read(halted_cpu_address); 280 } 281 282 log::trace!( 283 " DMA no-op cycle; OAM DMA state is now {:?}, DMC DMA state is now {:?}", 284 state.oam_dma, 285 state.dmc_dma 286 ); 287} 288 289/// Reset the CPU, as if the console's reset button was pressed. 290/// 291/// Reset does the following: 292/// * Immediately update the PC to point to the RESET vector, and abandon the currently-in-progress instruction (if any) 293/// * Subtract 3 from the stack pointer 294/// * Disable IRQs 295pub fn reset<B: BusInterface>(state: &mut CpuState, bus: &mut B) { 296 state.cpu.reset(bus); 297}