Some checks failed
CI / rust (push) Has been cancelled
Full NES emulation: CPU, PPU, APU, 47 mappers, iNES/NES 2.0 parsing. GTK4 desktop client with HeaderBar, pixel-perfect Cairo rendering, drag-and-drop ROM loading, and keyboard shortcuts. 187 tests covering core emulation, mappers, and runtime.
113 lines
3.0 KiB
Rust
113 lines
3.0 KiB
Rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum CpuError {
|
|
UnsupportedOpcode { opcode: u8, pc: u16 },
|
|
}
|
|
|
|
pub trait CpuBus {
|
|
fn read(&mut self, addr: u16) -> u8;
|
|
fn write(&mut self, addr: u16, value: u8);
|
|
fn poll_nmi(&mut self) -> bool {
|
|
false
|
|
}
|
|
fn poll_irq(&mut self) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
const FLAG_CARRY: u8 = 0b0000_0001;
|
|
const FLAG_ZERO: u8 = 0b0000_0010;
|
|
const FLAG_IRQ_DISABLE: u8 = 0b0000_0100;
|
|
const FLAG_DECIMAL: u8 = 0b0000_1000;
|
|
const FLAG_BREAK: u8 = 0b0001_0000;
|
|
const FLAG_UNUSED: u8 = 0b0010_0000;
|
|
const FLAG_OVERFLOW: u8 = 0b0100_0000;
|
|
const FLAG_NEGATIVE: u8 = 0b1000_0000;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct Cpu6502 {
|
|
pub a: u8,
|
|
pub x: u8,
|
|
pub y: u8,
|
|
pub sp: u8,
|
|
pub pc: u16,
|
|
pub p: u8,
|
|
pub halted: bool,
|
|
pub irq_delay: bool,
|
|
pub pending_nmi: bool,
|
|
pub pending_irq: bool,
|
|
}
|
|
|
|
impl Default for Cpu6502 {
|
|
fn default() -> Self {
|
|
Self {
|
|
a: 0,
|
|
x: 0,
|
|
y: 0,
|
|
sp: 0xFD,
|
|
pc: 0,
|
|
p: FLAG_IRQ_DISABLE | FLAG_UNUSED,
|
|
halted: false,
|
|
irq_delay: false,
|
|
pending_nmi: false,
|
|
pending_irq: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Cpu6502 {
|
|
pub fn reset<B: CpuBus>(&mut self, bus: &mut B) {
|
|
let lo = bus.read(0xFFFC) as u16;
|
|
let hi = bus.read(0xFFFD) as u16;
|
|
self.pc = (hi << 8) | lo;
|
|
self.sp = self.sp.wrapping_sub(3);
|
|
self.p = (self.p | FLAG_IRQ_DISABLE | FLAG_UNUSED) & !FLAG_BREAK;
|
|
self.halted = false;
|
|
self.irq_delay = false;
|
|
self.pending_nmi = false;
|
|
self.pending_irq = false;
|
|
}
|
|
|
|
pub fn step<B: CpuBus>(&mut self, bus: &mut B) -> Result<u8, CpuError> {
|
|
if self.halted {
|
|
return Ok(2);
|
|
}
|
|
if self.pending_nmi {
|
|
self.pending_nmi = false;
|
|
self.pending_irq = false;
|
|
self.service_interrupt(bus, 0xFFFA, false);
|
|
self.p |= FLAG_UNUSED;
|
|
return Ok(7);
|
|
}
|
|
let irq_delayed = self.irq_delay;
|
|
self.irq_delay = false;
|
|
if self.pending_irq && !irq_delayed && (self.p & FLAG_IRQ_DISABLE) == 0 {
|
|
self.pending_irq = false;
|
|
self.service_interrupt(bus, 0xFFFE, false);
|
|
self.p |= FLAG_UNUSED;
|
|
return Ok(7);
|
|
}
|
|
|
|
let pc_before = self.pc;
|
|
let opcode = self.fetch(bus);
|
|
let cycles = self.execute_opcode(bus, opcode, pc_before)?;
|
|
|
|
// Real 6502 polls IRQ/NMI near the end of the current instruction and
|
|
// services it before the next opcode fetch.
|
|
if bus.poll_nmi() {
|
|
self.pending_nmi = true;
|
|
self.pending_irq = false;
|
|
} else if !irq_delayed && (self.p & FLAG_IRQ_DISABLE) == 0 && bus.poll_irq() {
|
|
self.pending_irq = true;
|
|
}
|
|
Ok(cycles)
|
|
}
|
|
}
|
|
|
|
// Addressing modes, ALU helpers, stack and interrupt internals.
|
|
mod helpers;
|
|
// Opcode decoder/executor table split out from cpu.rs for readability.
|
|
mod opcodes;
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|