Initial commit: NES emulator with GTK4 desktop frontend
Some checks failed
CI / rust (push) Has been cancelled
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.
This commit is contained in:
112
src/native_core/cpu.rs
Normal file
112
src/native_core/cpu.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
#[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;
|
||||
Reference in New Issue
Block a user