cpu.rsannotatedcpu.rssource297 lines · 8.3 KB · raw
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}