Initial commit: NES emulator with GTK4 desktop frontend
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:
2026-03-13 11:48:45 +03:00
commit bdf23de8db
143 changed files with 18501 additions and 0 deletions

View File

@@ -0,0 +1,245 @@
use super::*;
impl Cpu6502 {
pub(super) fn fetch<B: CpuBus>(&mut self, bus: &mut B) -> u8 {
let value = bus.read(self.pc);
self.pc = self.pc.wrapping_add(1);
value
}
pub(super) fn fetch_u16<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
let lo = self.fetch(bus) as u16;
let hi = self.fetch(bus) as u16;
(hi << 8) | lo
}
pub(super) fn read_u16<B: CpuBus>(&mut self, bus: &mut B, addr: u16) -> u16 {
let lo = bus.read(addr) as u16;
let hi = bus.read(addr.wrapping_add(1)) as u16;
(hi << 8) | lo
}
pub(super) fn read_u16_bug<B: CpuBus>(&mut self, bus: &mut B, addr: u16) -> u16 {
let lo = bus.read(addr) as u16;
let hi_addr = (addr & 0xFF00) | (addr.wrapping_add(1) & 0x00FF);
let hi = bus.read(hi_addr) as u16;
(hi << 8) | lo
}
pub(super) fn addr_zp<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
self.fetch(bus) as u16
}
pub(super) fn addr_zpx<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
self.fetch(bus).wrapping_add(self.x) as u16
}
pub(super) fn addr_zpy<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
self.fetch(bus).wrapping_add(self.y) as u16
}
pub(super) fn addr_abs<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
self.fetch_u16(bus)
}
pub(super) fn addr_abx<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
self.addr_abx_cross(bus).0
}
pub(super) fn addr_aby<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
self.addr_aby_cross(bus).0
}
pub(super) fn addr_indx<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
let ptr = self.fetch(bus).wrapping_add(self.x);
let lo = bus.read(ptr as u16) as u16;
let hi = bus.read(ptr.wrapping_add(1) as u16) as u16;
(hi << 8) | lo
}
pub(super) fn addr_indy<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
self.addr_indy_cross(bus).0
}
pub(super) fn addr_abx_cross<B: CpuBus>(&mut self, bus: &mut B) -> (u16, bool) {
let base = self.fetch_u16(bus);
let addr = base.wrapping_add(self.x as u16);
(addr, (base & 0xFF00) != (addr & 0xFF00))
}
pub(super) fn addr_aby_cross<B: CpuBus>(&mut self, bus: &mut B) -> (u16, bool) {
let base = self.fetch_u16(bus);
let addr = base.wrapping_add(self.y as u16);
(addr, (base & 0xFF00) != (addr & 0xFF00))
}
pub(super) fn addr_indy_cross<B: CpuBus>(&mut self, bus: &mut B) -> (u16, bool) {
let ptr = self.fetch(bus);
let lo = bus.read(ptr as u16) as u16;
let hi = bus.read(ptr.wrapping_add(1) as u16) as u16;
let base = (hi << 8) | lo;
let addr = base.wrapping_add(self.y as u16);
(addr, (base & 0xFF00) != (addr & 0xFF00))
}
pub(super) fn read_zp<B: CpuBus>(&mut self, bus: &mut B) -> u8 {
let addr = self.addr_zp(bus);
bus.read(addr)
}
pub(super) fn read_zpx<B: CpuBus>(&mut self, bus: &mut B) -> u8 {
let addr = self.addr_zpx(bus);
bus.read(addr)
}
pub(super) fn read_zpy<B: CpuBus>(&mut self, bus: &mut B) -> u8 {
let addr = self.addr_zpy(bus);
bus.read(addr)
}
pub(super) fn read_abs<B: CpuBus>(&mut self, bus: &mut B) -> u8 {
let addr = self.addr_abs(bus);
bus.read(addr)
}
pub(super) fn read_abx_cross<B: CpuBus>(&mut self, bus: &mut B) -> (u8, bool) {
let (addr, crossed) = self.addr_abx_cross(bus);
(bus.read(addr), crossed)
}
pub(super) fn read_aby_cross<B: CpuBus>(&mut self, bus: &mut B) -> (u8, bool) {
let (addr, crossed) = self.addr_aby_cross(bus);
(bus.read(addr), crossed)
}
pub(super) fn read_indx<B: CpuBus>(&mut self, bus: &mut B) -> u8 {
let addr = self.addr_indx(bus);
bus.read(addr)
}
pub(super) fn read_indy_cross<B: CpuBus>(&mut self, bus: &mut B) -> (u8, bool) {
let (addr, crossed) = self.addr_indy_cross(bus);
(bus.read(addr), crossed)
}
pub(super) fn push<B: CpuBus>(&mut self, bus: &mut B, value: u8) {
let addr = 0x0100 | self.sp as u16;
bus.write(addr, value);
self.sp = self.sp.wrapping_sub(1);
}
pub(super) fn pop<B: CpuBus>(&mut self, bus: &mut B) -> u8 {
self.sp = self.sp.wrapping_add(1);
let addr = 0x0100 | self.sp as u16;
bus.read(addr)
}
pub(super) fn set_zn(&mut self, value: u8) {
if value == 0 {
self.p |= FLAG_ZERO;
} else {
self.p &= !FLAG_ZERO;
}
if (value & 0x80) != 0 {
self.p |= FLAG_NEGATIVE;
} else {
self.p &= !FLAG_NEGATIVE;
}
}
pub(super) fn set_flag(&mut self, mask: u8, enabled: bool) {
if enabled {
self.p |= mask;
} else {
self.p &= !mask;
}
}
pub(super) fn compare(&mut self, lhs: u8, rhs: u8) {
let result = lhs.wrapping_sub(rhs);
self.set_flag(FLAG_CARRY, lhs >= rhs);
self.set_zn(result);
}
pub(super) fn bit(&mut self, value: u8) {
self.set_flag(FLAG_ZERO, (self.a & value) == 0);
self.set_flag(FLAG_OVERFLOW, (value & 0x40) != 0);
self.set_flag(FLAG_NEGATIVE, (value & 0x80) != 0);
}
pub(super) fn adc(&mut self, value: u8) {
let carry = u16::from((self.p & FLAG_CARRY) != 0);
let a = self.a as u16;
let b = value as u16;
let sum = a + b + carry;
let result = sum as u8;
self.set_flag(FLAG_CARRY, sum > 0xFF);
let overflow = ((self.a ^ result) & (value ^ result) & 0x80) != 0;
self.set_flag(FLAG_OVERFLOW, overflow);
self.a = result;
self.set_zn(self.a);
}
pub(super) fn asl(&mut self, value: u8) -> u8 {
self.set_flag(FLAG_CARRY, (value & 0x80) != 0);
let out = value << 1;
self.set_zn(out);
out
}
pub(super) fn lsr(&mut self, value: u8) -> u8 {
self.set_flag(FLAG_CARRY, (value & 0x01) != 0);
let out = value >> 1;
self.set_zn(out);
out
}
pub(super) fn rol(&mut self, value: u8) -> u8 {
let carry_in = u8::from((self.p & FLAG_CARRY) != 0);
self.set_flag(FLAG_CARRY, (value & 0x80) != 0);
let out = (value << 1) | carry_in;
self.set_zn(out);
out
}
pub(super) fn ror(&mut self, value: u8) -> u8 {
let carry_in = if (self.p & FLAG_CARRY) != 0 { 0x80 } else { 0 };
self.set_flag(FLAG_CARRY, (value & 0x01) != 0);
let out = (value >> 1) | carry_in;
self.set_zn(out);
out
}
pub(super) fn branch<B: CpuBus>(&mut self, bus: &mut B, condition: bool) -> u8 {
let offset = self.fetch(bus) as i8;
if !condition {
return 2;
}
let old_pc = self.pc;
self.pc = self.pc.wrapping_add_signed(offset as i16);
if (old_pc & 0xFF00) != (self.pc & 0xFF00) {
4
} else {
3
}
}
pub(super) fn service_interrupt<B: CpuBus>(
&mut self,
bus: &mut B,
vector_addr: u16,
break_flag: bool,
) {
self.push(bus, (self.pc >> 8) as u8);
self.push(bus, self.pc as u8);
let mut status = (self.p | FLAG_UNUSED) & !FLAG_BREAK;
if break_flag {
status |= FLAG_BREAK;
}
self.push(bus, status);
self.p = (self.p | FLAG_IRQ_DISABLE | FLAG_UNUSED) & !FLAG_BREAK;
self.pc = self.read_u16(bus, vector_addr);
}
}

View File

@@ -0,0 +1,29 @@
use super::*;
mod official;
mod ops;
mod undocumented;
impl Cpu6502 {
pub(super) fn execute_opcode<B: CpuBus>(
&mut self,
bus: &mut B,
opcode: u8,
pc_before: u16,
) -> Result<u8, CpuError> {
if let Some(cycles) = self.execute_undocumented(bus, opcode, pc_before) {
self.p |= FLAG_UNUSED;
return Ok(cycles);
}
if let Some(cycles) = self.execute_official(bus, opcode) {
self.p |= FLAG_UNUSED;
return Ok(cycles);
}
Err(CpuError::UnsupportedOpcode {
opcode,
pc: pc_before,
})
}
}

View File

@@ -0,0 +1,375 @@
use super::ops::{OperandReadMode, OperandWriteMode};
use super::*;
impl Cpu6502 {
pub(super) fn execute_official_alu<B: CpuBus>(
&mut self,
bus: &mut B,
opcode: u8,
) -> Option<u8> {
let cycles = match opcode {
// ADC
0x69 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
self.adc(value);
cycles
}
0x65 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
self.adc(value);
cycles
}
0x75 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpx, 4);
self.adc(value);
cycles
}
0x6D => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
self.adc(value);
cycles
}
0x7D => {
let (value, cycles) = self.op_read(bus, OperandReadMode::AbxCross, 4);
self.adc(value);
cycles
}
0x79 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
self.adc(value);
cycles
}
0x61 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Indx, 6);
self.adc(value);
cycles
}
0x71 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::IndyCross, 5);
self.adc(value);
cycles
}
// SBC
0xE9 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
self.adc(!value);
cycles
}
0xE5 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
self.adc(!value);
cycles
}
0xF5 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpx, 4);
self.adc(!value);
cycles
}
0xED => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
self.adc(!value);
cycles
}
0xFD => {
let (value, cycles) = self.op_read(bus, OperandReadMode::AbxCross, 4);
self.adc(!value);
cycles
}
0xF9 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
self.adc(!value);
cycles
}
0xE1 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Indx, 6);
self.adc(!value);
cycles
}
0xF1 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::IndyCross, 5);
self.adc(!value);
cycles
}
// AND
0x29 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
self.a &= value;
self.set_zn(self.a);
cycles
}
0x25 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
self.a &= value;
self.set_zn(self.a);
cycles
}
0x35 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpx, 4);
self.a &= value;
self.set_zn(self.a);
cycles
}
0x2D => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
self.a &= value;
self.set_zn(self.a);
cycles
}
0x3D => {
let (value, cycles) = self.op_read(bus, OperandReadMode::AbxCross, 4);
self.a &= value;
self.set_zn(self.a);
cycles
}
0x39 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
self.a &= value;
self.set_zn(self.a);
cycles
}
0x21 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Indx, 6);
self.a &= value;
self.set_zn(self.a);
cycles
}
0x31 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::IndyCross, 5);
self.a &= value;
self.set_zn(self.a);
cycles
}
// ORA
0x09 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
self.a |= value;
self.set_zn(self.a);
cycles
}
0x05 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
self.a |= value;
self.set_zn(self.a);
cycles
}
0x15 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpx, 4);
self.a |= value;
self.set_zn(self.a);
cycles
}
0x0D => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
self.a |= value;
self.set_zn(self.a);
cycles
}
0x19 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
self.a |= value;
self.set_zn(self.a);
cycles
}
0x1D => {
let (value, cycles) = self.op_read(bus, OperandReadMode::AbxCross, 4);
self.a |= value;
self.set_zn(self.a);
cycles
}
0x01 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Indx, 6);
self.a |= value;
self.set_zn(self.a);
cycles
}
0x11 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::IndyCross, 5);
self.a |= value;
self.set_zn(self.a);
cycles
}
// EOR
0x49 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
self.a ^= value;
self.set_zn(self.a);
cycles
}
0x45 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
self.a ^= value;
self.set_zn(self.a);
cycles
}
0x55 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpx, 4);
self.a ^= value;
self.set_zn(self.a);
cycles
}
0x4D => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
self.a ^= value;
self.set_zn(self.a);
cycles
}
0x5D => {
let (value, cycles) = self.op_read(bus, OperandReadMode::AbxCross, 4);
self.a ^= value;
self.set_zn(self.a);
cycles
}
0x59 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
self.a ^= value;
self.set_zn(self.a);
cycles
}
0x41 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Indx, 6);
self.a ^= value;
self.set_zn(self.a);
cycles
}
0x51 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::IndyCross, 5);
self.a ^= value;
self.set_zn(self.a);
cycles
}
// BIT
0x24 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
self.bit(value);
cycles
}
0x2C => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
self.bit(value);
cycles
}
// CMP
0xC9 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
self.compare(self.a, value);
cycles
}
0xC5 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
self.compare(self.a, value);
cycles
}
0xD5 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpx, 4);
self.compare(self.a, value);
cycles
}
0xCD => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
self.compare(self.a, value);
cycles
}
0xDD => {
let (value, cycles) = self.op_read(bus, OperandReadMode::AbxCross, 4);
self.compare(self.a, value);
cycles
}
0xD9 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
self.compare(self.a, value);
cycles
}
0xC1 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Indx, 6);
self.compare(self.a, value);
cycles
}
0xD1 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::IndyCross, 5);
self.compare(self.a, value);
cycles
}
// CPX / CPY
0xE0 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
self.compare(self.x, value);
cycles
}
0xE4 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
self.compare(self.x, value);
cycles
}
0xEC => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
self.compare(self.x, value);
cycles
}
0xC0 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
self.compare(self.y, value);
cycles
}
0xC4 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
self.compare(self.y, value);
cycles
}
0xCC => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
self.compare(self.y, value);
cycles
}
// ASL
0x0A => {
self.a = self.asl(self.a);
2
}
0x06 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| cpu.asl(value)),
0x16 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| cpu.asl(value)),
0x0E => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| cpu.asl(value)),
0x1E => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| cpu.asl(value)),
// LSR
0x4A => {
self.a = self.lsr(self.a);
2
}
0x46 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| cpu.lsr(value)),
0x56 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| cpu.lsr(value)),
0x4E => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| cpu.lsr(value)),
0x5E => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| cpu.lsr(value)),
// ROL
0x2A => {
self.a = self.rol(self.a);
2
}
0x26 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| cpu.rol(value)),
0x36 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| cpu.rol(value)),
0x2E => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| cpu.rol(value)),
0x3E => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| cpu.rol(value)),
// ROR
0x6A => {
self.a = self.ror(self.a);
2
}
0x66 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| cpu.ror(value)),
0x76 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| cpu.ror(value)),
0x6E => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| cpu.ror(value)),
0x7E => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| cpu.ror(value)),
_ => return None,
};
Some(cycles)
}
}

View File

@@ -0,0 +1,133 @@
use super::*;
impl Cpu6502 {
pub(super) fn execute_official_control<B: CpuBus>(
&mut self,
bus: &mut B,
opcode: u8,
) -> Option<u8> {
let cycles = match opcode {
0xEA => 2,
// Jumps / calls
0x4C => {
self.pc = self.addr_abs(bus);
3
}
0x6C => {
let ptr = self.addr_abs(bus);
self.pc = self.read_u16_bug(bus, ptr);
5
}
0x20 => {
let addr = self.addr_abs(bus);
let ret = self.pc.wrapping_sub(1);
self.push(bus, (ret >> 8) as u8);
self.push(bus, ret as u8);
self.pc = addr;
6
}
0x60 => {
let lo = self.pop(bus) as u16;
let hi = self.pop(bus) as u16;
self.pc = ((hi << 8) | lo).wrapping_add(1);
6
}
// Stack
0x48 => {
self.push(bus, self.a);
3
}
0x68 => {
self.a = self.pop(bus);
self.set_zn(self.a);
4
}
0x08 => {
self.push(bus, self.p | FLAG_BREAK | FLAG_UNUSED);
3
}
0x28 => {
let old_i = (self.p & FLAG_IRQ_DISABLE) != 0;
self.p = (self.pop(bus) | FLAG_UNUSED) & !FLAG_BREAK;
let new_i = (self.p & FLAG_IRQ_DISABLE) != 0;
if old_i && !new_i {
self.irq_delay = true;
}
4
}
// Flags
0x18 => {
self.p &= !FLAG_CARRY;
2
}
0x38 => {
self.p |= FLAG_CARRY;
2
}
0x58 => {
self.p &= !FLAG_IRQ_DISABLE;
self.irq_delay = true;
2
}
0x78 => {
self.p |= FLAG_IRQ_DISABLE;
2
}
0xD8 => {
self.p &= !FLAG_DECIMAL;
2
}
0xF8 => {
self.p |= FLAG_DECIMAL;
2
}
0xB8 => {
self.p &= !FLAG_OVERFLOW;
2
}
// BRK / RTI
0x00 => {
self.pc = self.pc.wrapping_add(1);
self.push(bus, (self.pc >> 8) as u8);
self.push(bus, self.pc as u8);
self.push(bus, self.p | FLAG_BREAK | FLAG_UNUSED);
self.p |= FLAG_IRQ_DISABLE;
// NMI can hijack BRK and force vector $FFFA while preserving B=1 on stack.
let vector = if bus.poll_nmi() { 0xFFFA } else { 0xFFFE };
self.pending_nmi = false;
self.pending_irq = false;
self.pc = self.read_u16(bus, vector);
7
}
0x40 => {
let old_i = (self.p & FLAG_IRQ_DISABLE) != 0;
self.p = (self.pop(bus) | FLAG_UNUSED) & !FLAG_BREAK;
let new_i = (self.p & FLAG_IRQ_DISABLE) != 0;
if old_i && !new_i {
self.irq_delay = true;
}
let lo = self.pop(bus) as u16;
let hi = self.pop(bus) as u16;
self.pc = (hi << 8) | lo;
6
}
// Branches
0xD0 => self.branch(bus, (self.p & FLAG_ZERO) == 0),
0xF0 => self.branch(bus, (self.p & FLAG_ZERO) != 0),
0x10 => self.branch(bus, (self.p & FLAG_NEGATIVE) == 0),
0x30 => self.branch(bus, (self.p & FLAG_NEGATIVE) != 0),
0x90 => self.branch(bus, (self.p & FLAG_CARRY) == 0),
0xB0 => self.branch(bus, (self.p & FLAG_CARRY) != 0),
0x50 => self.branch(bus, (self.p & FLAG_OVERFLOW) == 0),
0x70 => self.branch(bus, (self.p & FLAG_OVERFLOW) != 0),
_ => return None,
};
Some(cycles)
}
}

View File

@@ -0,0 +1,239 @@
use super::ops::{OperandReadMode, OperandWriteMode};
use super::*;
impl Cpu6502 {
pub(super) fn execute_official_load_store<B: CpuBus>(
&mut self,
bus: &mut B,
opcode: u8,
) -> Option<u8> {
let cycles = match opcode {
// LDA
0xA9 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
self.a = value;
self.set_zn(self.a);
cycles
}
0xA5 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
self.a = value;
self.set_zn(self.a);
cycles
}
0xB5 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpx, 4);
self.a = value;
self.set_zn(self.a);
cycles
}
0xAD => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
self.a = value;
self.set_zn(self.a);
cycles
}
0xBD => {
let (value, cycles) = self.op_read(bus, OperandReadMode::AbxCross, 4);
self.a = value;
self.set_zn(self.a);
cycles
}
0xB9 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
self.a = value;
self.set_zn(self.a);
cycles
}
0xA1 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Indx, 6);
self.a = value;
self.set_zn(self.a);
cycles
}
0xB1 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::IndyCross, 5);
self.a = value;
self.set_zn(self.a);
cycles
}
// LDX
0xA2 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
self.x = value;
self.set_zn(self.x);
cycles
}
0xA6 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
self.x = value;
self.set_zn(self.x);
cycles
}
0xB6 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpy, 4);
self.x = value;
self.set_zn(self.x);
cycles
}
0xAE => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
self.x = value;
self.set_zn(self.x);
cycles
}
0xBE => {
let (value, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
self.x = value;
self.set_zn(self.x);
cycles
}
// LDY
0xA0 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
self.y = value;
self.set_zn(self.y);
cycles
}
0xA4 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
self.y = value;
self.set_zn(self.y);
cycles
}
0xB4 => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpx, 4);
self.y = value;
self.set_zn(self.y);
cycles
}
0xAC => {
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
self.y = value;
self.set_zn(self.y);
cycles
}
0xBC => {
let (value, cycles) = self.op_read(bus, OperandReadMode::AbxCross, 4);
self.y = value;
self.set_zn(self.y);
cycles
}
// STA/STX/STY
0x85 => self.op_store(bus, OperandWriteMode::Zp, self.a, 3),
0x95 => self.op_store(bus, OperandWriteMode::Zpx, self.a, 4),
0x8D => self.op_store(bus, OperandWriteMode::Abs, self.a, 4),
0x9D => self.op_store(bus, OperandWriteMode::Abx, self.a, 5),
0x99 => self.op_store(bus, OperandWriteMode::Aby, self.a, 5),
0x81 => self.op_store(bus, OperandWriteMode::Indx, self.a, 6),
0x91 => self.op_store(bus, OperandWriteMode::Indy, self.a, 6),
0x86 => self.op_store(bus, OperandWriteMode::Zp, self.x, 3),
0x96 => self.op_store(bus, OperandWriteMode::Zpy, self.x, 4),
0x8E => self.op_store(bus, OperandWriteMode::Abs, self.x, 4),
0x84 => self.op_store(bus, OperandWriteMode::Zp, self.y, 3),
0x94 => self.op_store(bus, OperandWriteMode::Zpx, self.y, 4),
0x8C => self.op_store(bus, OperandWriteMode::Abs, self.y, 4),
// Transfers
0xAA => {
self.x = self.a;
self.set_zn(self.x);
2
}
0x8A => {
self.a = self.x;
self.set_zn(self.a);
2
}
0xA8 => {
self.y = self.a;
self.set_zn(self.y);
2
}
0x98 => {
self.a = self.y;
self.set_zn(self.a);
2
}
0xBA => {
self.x = self.sp;
self.set_zn(self.x);
2
}
0x9A => {
self.sp = self.x;
2
}
// Increments / decrements regs
0xE8 => {
self.x = self.x.wrapping_add(1);
self.set_zn(self.x);
2
}
0xC8 => {
self.y = self.y.wrapping_add(1);
self.set_zn(self.y);
2
}
0xCA => {
self.x = self.x.wrapping_sub(1);
self.set_zn(self.x);
2
}
0x88 => {
self.y = self.y.wrapping_sub(1);
self.set_zn(self.y);
2
}
// INC/DEC memory
0xE6 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| {
let value = value.wrapping_add(1);
cpu.set_zn(value);
value
}),
0xF6 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| {
let value = value.wrapping_add(1);
cpu.set_zn(value);
value
}),
0xEE => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| {
let value = value.wrapping_add(1);
cpu.set_zn(value);
value
}),
0xFE => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| {
let value = value.wrapping_add(1);
cpu.set_zn(value);
value
}),
0xC6 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| {
let value = value.wrapping_sub(1);
cpu.set_zn(value);
value
}),
0xD6 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| {
let value = value.wrapping_sub(1);
cpu.set_zn(value);
value
}),
0xCE => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| {
let value = value.wrapping_sub(1);
cpu.set_zn(value);
value
}),
0xDE => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| {
let value = value.wrapping_sub(1);
cpu.set_zn(value);
value
}),
_ => return None,
};
Some(cycles)
}
}

View File

@@ -0,0 +1,13 @@
use super::*;
mod alu;
mod control;
mod load_store;
impl Cpu6502 {
pub(super) fn execute_official<B: CpuBus>(&mut self, bus: &mut B, opcode: u8) -> Option<u8> {
self.execute_official_load_store(bus, opcode)
.or_else(|| self.execute_official_alu(bus, opcode))
.or_else(|| self.execute_official_control(bus, opcode))
}
}

View File

@@ -0,0 +1,100 @@
use super::*;
#[derive(Clone, Copy)]
pub(super) enum OperandReadMode {
Imm,
Zp,
Zpx,
Zpy,
Abs,
AbxCross,
AbyCross,
Indx,
IndyCross,
}
#[derive(Clone, Copy)]
pub(super) enum OperandWriteMode {
Zp,
Zpx,
Zpy,
Abs,
Abx,
Aby,
Indx,
Indy,
}
impl Cpu6502 {
pub(super) fn op_read<B: CpuBus>(
&mut self,
bus: &mut B,
mode: OperandReadMode,
base_cycles: u8,
) -> (u8, u8) {
match mode {
OperandReadMode::Imm => (self.fetch(bus), base_cycles),
OperandReadMode::Zp => (self.read_zp(bus), base_cycles),
OperandReadMode::Zpx => (self.read_zpx(bus), base_cycles),
OperandReadMode::Zpy => (self.read_zpy(bus), base_cycles),
OperandReadMode::Abs => (self.read_abs(bus), base_cycles),
OperandReadMode::AbxCross => {
let (value, crossed) = self.read_abx_cross(bus);
(value, base_cycles + u8::from(crossed))
}
OperandReadMode::AbyCross => {
let (value, crossed) = self.read_aby_cross(bus);
(value, base_cycles + u8::from(crossed))
}
OperandReadMode::Indx => (self.read_indx(bus), base_cycles),
OperandReadMode::IndyCross => {
let (value, crossed) = self.read_indy_cross(bus);
(value, base_cycles + u8::from(crossed))
}
}
}
pub(super) fn op_store<B: CpuBus>(
&mut self,
bus: &mut B,
mode: OperandWriteMode,
value: u8,
cycles: u8,
) -> u8 {
let addr = self.addr_for_write(bus, mode);
bus.write(addr, value);
cycles
}
pub(super) fn op_rmw<B: CpuBus, F>(
&mut self,
bus: &mut B,
mode: OperandWriteMode,
cycles: u8,
op: F,
) -> u8
where
F: FnOnce(&mut Self, u8) -> u8,
{
let addr = self.addr_for_write(bus, mode);
let old = bus.read(addr);
// 6502 RMW does a dummy write of the unmodified value before the final value.
bus.write(addr, old);
let value = op(self, old);
bus.write(addr, value);
cycles
}
fn addr_for_write<B: CpuBus>(&mut self, bus: &mut B, mode: OperandWriteMode) -> u16 {
match mode {
OperandWriteMode::Zp => self.addr_zp(bus),
OperandWriteMode::Zpx => self.addr_zpx(bus),
OperandWriteMode::Zpy => self.addr_zpy(bus),
OperandWriteMode::Abs => self.addr_abs(bus),
OperandWriteMode::Abx => self.addr_abx(bus),
OperandWriteMode::Aby => self.addr_aby(bus),
OperandWriteMode::Indx => self.addr_indx(bus),
OperandWriteMode::Indy => self.addr_indy(bus),
}
}
}

View File

@@ -0,0 +1,94 @@
use super::*;
impl Cpu6502 {
pub(super) fn execute_undocumented_combos<B: CpuBus>(
&mut self,
bus: &mut B,
opcode: u8,
) -> Option<u8> {
let cycles = match opcode {
// Undocumented immediate combos
0x0B | 0x2B => {
self.a &= self.fetch(bus);
self.set_zn(self.a);
self.set_flag(FLAG_CARRY, (self.a & 0x80) != 0);
2
}
0x4B => {
self.a &= self.fetch(bus);
self.a = self.lsr(self.a);
2
}
0x6B => {
let imm = self.fetch(bus);
self.a &= imm;
self.a = self.ror(self.a);
let bit5 = (self.a & 0x20) != 0;
let bit6 = (self.a & 0x40) != 0;
self.set_flag(FLAG_CARRY, bit6);
self.set_flag(FLAG_OVERFLOW, bit5 ^ bit6);
2
}
0xCB => {
let imm = self.fetch(bus);
let ax = self.a & self.x;
let v = ax.wrapping_sub(imm);
self.x = v;
self.set_flag(FLAG_CARRY, ax >= imm);
self.set_zn(self.x);
2
}
0xEB => {
let value = self.fetch(bus);
self.adc(!value);
2
}
0xBB => {
let (addr, crossed) = self.addr_aby_cross(bus);
let v = bus.read(addr) & self.sp;
self.a = v;
self.x = v;
self.sp = v;
self.set_zn(v);
4 + u8::from(crossed)
}
0x93 => {
let zp = self.fetch(bus);
let lo = bus.read(zp as u16) as u16;
let hi = bus.read(zp.wrapping_add(1) as u16) as u16;
let addr = ((hi << 8) | lo).wrapping_add(self.y as u16);
let hi_mask = ((addr >> 8) as u8).wrapping_add(1);
bus.write(addr, self.a & self.x & hi_mask);
6
}
0x9F => {
let addr = self.addr_aby(bus);
let hi_mask = ((addr >> 8) as u8).wrapping_add(1);
bus.write(addr, self.a & self.x & hi_mask);
5
}
0x9B => {
let addr = self.addr_aby(bus);
self.sp = self.a & self.x;
let hi_mask = ((addr >> 8) as u8).wrapping_add(1);
bus.write(addr, self.sp & hi_mask);
5
}
0x9C => {
let addr = self.addr_abx(bus);
let hi_mask = ((addr >> 8) as u8).wrapping_add(1);
bus.write(addr, self.y & hi_mask);
5
}
0x9E => {
let addr = self.addr_aby(bus);
let hi_mask = ((addr >> 8) as u8).wrapping_add(1);
bus.write(addr, self.x & hi_mask);
5
}
_ => return None,
};
Some(cycles)
}
}

View File

@@ -0,0 +1,18 @@
use super::*;
mod combos;
mod rmw;
mod system;
impl Cpu6502 {
pub(super) fn execute_undocumented<B: CpuBus>(
&mut self,
bus: &mut B,
opcode: u8,
pc_before: u16,
) -> Option<u8> {
self.execute_undocumented_system(bus, opcode, pc_before)
.or_else(|| self.execute_undocumented_rmw(bus, opcode))
.or_else(|| self.execute_undocumented_combos(bus, opcode))
}
}

View File

@@ -0,0 +1,258 @@
use super::ops::OperandWriteMode;
use super::*;
impl Cpu6502 {
pub(super) fn execute_undocumented_rmw<B: CpuBus>(
&mut self,
bus: &mut B,
opcode: u8,
) -> Option<u8> {
let cycles = match opcode {
// Undocumented SLO: ASL M then ORA M
0x03 => self.op_rmw(bus, OperandWriteMode::Indx, 8, |cpu, value| {
let v = cpu.asl(value);
cpu.a |= v;
cpu.set_zn(cpu.a);
v
}),
0x07 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| {
let v = cpu.asl(value);
cpu.a |= v;
cpu.set_zn(cpu.a);
v
}),
0x0F => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| {
let v = cpu.asl(value);
cpu.a |= v;
cpu.set_zn(cpu.a);
v
}),
0x13 => self.op_rmw(bus, OperandWriteMode::Indy, 8, |cpu, value| {
let v = cpu.asl(value);
cpu.a |= v;
cpu.set_zn(cpu.a);
v
}),
0x17 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| {
let v = cpu.asl(value);
cpu.a |= v;
cpu.set_zn(cpu.a);
v
}),
0x1B => self.op_rmw(bus, OperandWriteMode::Aby, 7, |cpu, value| {
let v = cpu.asl(value);
cpu.a |= v;
cpu.set_zn(cpu.a);
v
}),
0x1F => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| {
let v = cpu.asl(value);
cpu.a |= v;
cpu.set_zn(cpu.a);
v
}),
// Undocumented RLA: ROL M then AND M
0x23 => self.op_rmw(bus, OperandWriteMode::Indx, 8, |cpu, value| {
let v = cpu.rol(value);
cpu.a &= v;
cpu.set_zn(cpu.a);
v
}),
0x27 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| {
let v = cpu.rol(value);
cpu.a &= v;
cpu.set_zn(cpu.a);
v
}),
0x2F => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| {
let v = cpu.rol(value);
cpu.a &= v;
cpu.set_zn(cpu.a);
v
}),
0x33 => self.op_rmw(bus, OperandWriteMode::Indy, 8, |cpu, value| {
let v = cpu.rol(value);
cpu.a &= v;
cpu.set_zn(cpu.a);
v
}),
0x37 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| {
let v = cpu.rol(value);
cpu.a &= v;
cpu.set_zn(cpu.a);
v
}),
0x3B => self.op_rmw(bus, OperandWriteMode::Aby, 7, |cpu, value| {
let v = cpu.rol(value);
cpu.a &= v;
cpu.set_zn(cpu.a);
v
}),
0x3F => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| {
let v = cpu.rol(value);
cpu.a &= v;
cpu.set_zn(cpu.a);
v
}),
// Undocumented SRE: LSR M then EOR M
0x43 => self.op_rmw(bus, OperandWriteMode::Indx, 8, |cpu, value| {
let v = cpu.lsr(value);
cpu.a ^= v;
cpu.set_zn(cpu.a);
v
}),
0x47 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| {
let v = cpu.lsr(value);
cpu.a ^= v;
cpu.set_zn(cpu.a);
v
}),
0x4F => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| {
let v = cpu.lsr(value);
cpu.a ^= v;
cpu.set_zn(cpu.a);
v
}),
0x53 => self.op_rmw(bus, OperandWriteMode::Indy, 8, |cpu, value| {
let v = cpu.lsr(value);
cpu.a ^= v;
cpu.set_zn(cpu.a);
v
}),
0x57 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| {
let v = cpu.lsr(value);
cpu.a ^= v;
cpu.set_zn(cpu.a);
v
}),
0x5B => self.op_rmw(bus, OperandWriteMode::Aby, 7, |cpu, value| {
let v = cpu.lsr(value);
cpu.a ^= v;
cpu.set_zn(cpu.a);
v
}),
0x5F => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| {
let v = cpu.lsr(value);
cpu.a ^= v;
cpu.set_zn(cpu.a);
v
}),
// Undocumented RRA: ROR M then ADC M
0x63 => self.op_rmw(bus, OperandWriteMode::Indx, 8, |cpu, value| {
let v = cpu.ror(value);
cpu.adc(v);
v
}),
0x67 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| {
let v = cpu.ror(value);
cpu.adc(v);
v
}),
0x6F => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| {
let v = cpu.ror(value);
cpu.adc(v);
v
}),
0x73 => self.op_rmw(bus, OperandWriteMode::Indy, 8, |cpu, value| {
let v = cpu.ror(value);
cpu.adc(v);
v
}),
0x77 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| {
let v = cpu.ror(value);
cpu.adc(v);
v
}),
0x7B => self.op_rmw(bus, OperandWriteMode::Aby, 7, |cpu, value| {
let v = cpu.ror(value);
cpu.adc(v);
v
}),
0x7F => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| {
let v = cpu.ror(value);
cpu.adc(v);
v
}),
// Undocumented DCP: DEC M then CMP M
0xC3 => self.op_rmw(bus, OperandWriteMode::Indx, 8, |cpu, value| {
let v = value.wrapping_sub(1);
cpu.compare(cpu.a, v);
v
}),
0xC7 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| {
let v = value.wrapping_sub(1);
cpu.compare(cpu.a, v);
v
}),
0xCF => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| {
let v = value.wrapping_sub(1);
cpu.compare(cpu.a, v);
v
}),
0xD3 => self.op_rmw(bus, OperandWriteMode::Indy, 8, |cpu, value| {
let v = value.wrapping_sub(1);
cpu.compare(cpu.a, v);
v
}),
0xD7 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| {
let v = value.wrapping_sub(1);
cpu.compare(cpu.a, v);
v
}),
0xDB => self.op_rmw(bus, OperandWriteMode::Aby, 7, |cpu, value| {
let v = value.wrapping_sub(1);
cpu.compare(cpu.a, v);
v
}),
0xDF => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| {
let v = value.wrapping_sub(1);
cpu.compare(cpu.a, v);
v
}),
// Undocumented ISC/ISB: INC M then SBC M
0xE3 => self.op_rmw(bus, OperandWriteMode::Indx, 8, |cpu, value| {
let v = value.wrapping_add(1);
cpu.adc(!v);
v
}),
0xE7 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| {
let v = value.wrapping_add(1);
cpu.adc(!v);
v
}),
0xEF => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| {
let v = value.wrapping_add(1);
cpu.adc(!v);
v
}),
0xF3 => self.op_rmw(bus, OperandWriteMode::Indy, 8, |cpu, value| {
let v = value.wrapping_add(1);
cpu.adc(!v);
v
}),
0xF7 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| {
let v = value.wrapping_add(1);
cpu.adc(!v);
v
}),
0xFB => self.op_rmw(bus, OperandWriteMode::Aby, 7, |cpu, value| {
let v = value.wrapping_add(1);
cpu.adc(!v);
v
}),
0xFF => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| {
let v = value.wrapping_add(1);
cpu.adc(!v);
v
}),
_ => return None,
};
Some(cycles)
}
}

View File

@@ -0,0 +1,97 @@
use super::ops::{OperandReadMode, OperandWriteMode};
use super::*;
impl Cpu6502 {
pub(super) fn execute_undocumented_system<B: CpuBus>(
&mut self,
bus: &mut B,
opcode: u8,
pc_before: u16,
) -> Option<u8> {
let cycles = match opcode {
// Undocumented NOPs used by commercial ROMs.
0x1A | 0x3A | 0x5A | 0x7A | 0xDA | 0xFA => 2,
0x80 | 0x82 | 0x89 | 0xC2 | 0xE2 => self.op_read(bus, OperandReadMode::Imm, 2).1,
0x04 | 0x44 | 0x64 => self.op_read(bus, OperandReadMode::Zp, 3).1,
0x14 | 0x34 | 0x54 | 0x74 | 0xD4 | 0xF4 => self.op_read(bus, OperandReadMode::Zpx, 4).1,
0x0C => self.op_read(bus, OperandReadMode::Abs, 4).1,
0x1C | 0x3C | 0x5C | 0x7C | 0xDC | 0xFC => {
self.op_read(bus, OperandReadMode::AbxCross, 4).1
}
// JAM/KIL opcodes lock the real CPU until reset.
0x02 | 0x12 | 0x22 | 0x32 | 0x42 | 0x52 | 0x62 | 0x72 | 0x92 | 0xB2 | 0xD2 | 0xF2 => {
self.halted = true;
self.pc = pc_before;
2
}
// Undocumented LAX: A <- M, X <- A
0xAB => {
let (v, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
self.a = v;
self.x = v;
self.set_zn(v);
cycles
}
0xA3 => {
let (v, cycles) = self.op_read(bus, OperandReadMode::Indx, 6);
self.a = v;
self.x = v;
self.set_zn(v);
cycles
}
0xA7 => {
let (v, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
self.a = v;
self.x = v;
self.set_zn(v);
cycles
}
0xAF => {
let (v, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
self.a = v;
self.x = v;
self.set_zn(v);
cycles
}
0xB3 => {
let (v, cycles) = self.op_read(bus, OperandReadMode::IndyCross, 5);
self.a = v;
self.x = v;
self.set_zn(v);
cycles
}
0xB7 => {
let (v, cycles) = self.op_read(bus, OperandReadMode::Zpy, 4);
self.a = v;
self.x = v;
self.set_zn(v);
cycles
}
0xBF => {
let (v, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
self.a = v;
self.x = v;
self.set_zn(v);
cycles
}
// Undocumented SAX: M <- A & X
0x83 => self.op_store(bus, OperandWriteMode::Indx, self.a & self.x, 6),
0x87 => self.op_store(bus, OperandWriteMode::Zp, self.a & self.x, 3),
0x8F => self.op_store(bus, OperandWriteMode::Abs, self.a & self.x, 4),
0x97 => self.op_store(bus, OperandWriteMode::Zpy, self.a & self.x, 4),
0x8B => {
// XAA/ANE (unstable on hardware), practical emulation form.
let (v, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
self.a = self.x & v;
self.set_zn(self.a);
cycles
}
_ => return None,
};
Some(cycles)
}
}

View File

@@ -0,0 +1,10 @@
use super::{Cpu6502, CpuBus, FLAG_CARRY, FLAG_NEGATIVE, FLAG_ZERO};
use crate::native_core::test_support::{TestRamBus, cpu_setup_with_reset};
fn setup_cpu_with_reset(prog: &[u8]) -> (Cpu6502, TestRamBus) {
cpu_setup_with_reset(prog)
}
mod core;
mod interrupts;
mod property;

View File

@@ -0,0 +1,272 @@
use super::*;
#[test]
fn lda_sta_and_branch_work() {
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
0xA9, 0x42, // LDA #$42
0x8D, 0x34, 0x12, // STA $1234
0xC9, 0x42, // CMP #$42
0xF0, 0x02, // BEQ +2
0xA9, 0x00, // skipped
0xA9, 0x99, // LDA #$99
]);
for _ in 0..5 {
cpu.step(&mut bus).expect("opcode must be supported");
}
assert_eq!(bus.0[0x1234], 0x42);
assert_eq!(cpu.a, 0x99);
assert_eq!(cpu.p & FLAG_ZERO, 0);
}
#[test]
fn branch_not_taken_uses_two_cycles() {
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
0x38, // SEC
0x90, 0x7F, // BCC +$7F (not taken)
]);
assert_eq!(cpu.step(&mut bus).expect("SEC"), 2);
let cycles = cpu.step(&mut bus).expect("BCC");
assert_eq!(cycles, 2);
assert_eq!(cpu.pc, 0x8003);
}
#[test]
fn branch_taken_same_page_uses_three_cycles() {
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
0x18, // CLC
0x90, 0x02, // BCC +2
0xEA, // skipped
0xEA, // target
]);
assert_eq!(cpu.step(&mut bus).expect("CLC"), 2);
let cycles = cpu.step(&mut bus).expect("BCC");
assert_eq!(cycles, 3);
assert_eq!(cpu.pc, 0x8005);
}
#[test]
fn branch_taken_cross_page_uses_four_cycles() {
let (_cpu, mut bus) = setup_cpu_with_reset(&[]);
bus.0[0x80FD] = 0x90; // BCC
bus.0[0x80FE] = 0x02; // target $8101 (page-crossing from $80FF)
let mut cpu = Cpu6502 {
pc: 0x80FD,
..Cpu6502::default()
};
cpu.p &= !FLAG_CARRY;
let cycles = cpu.step(&mut bus).expect("BCC cross-page");
assert_eq!(cycles, 4);
assert_eq!(cpu.pc, 0x8101);
}
#[test]
fn jsr_rts_roundtrip() {
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
0x20, 0x06, 0x80, // JSR $8006
0xA9, 0x11, // LDA #$11
0xEA, // NOP
0xA9, 0x77, // LDA #$77
0x60, // RTS
]);
for _ in 0..4 {
cpu.step(&mut bus).expect("opcode must be supported");
}
assert_eq!(cpu.a, 0x11);
}
#[test]
fn adc_and_sbc_update_flags() {
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
0xA9, 0x01, // LDA #$01
0x18, // CLC
0x69, 0x01, // ADC #$01
0x38, // SEC
0xE9, 0x02, // SBC #$02
]);
for _ in 0..5 {
cpu.step(&mut bus).expect("opcode must be supported");
}
assert_eq!(cpu.a, 0x00);
assert_ne!(cpu.p & FLAG_CARRY, 0);
assert_ne!(cpu.p & FLAG_ZERO, 0);
assert_eq!(cpu.p & FLAG_NEGATIVE, 0);
}
#[test]
fn indirect_x_and_indirect_y_addressing() {
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
0xA2, 0x04, // LDX #$04
0xA1, 0x10, // LDA ($10,X) -> ptr at $14/$15
0xA0, 0x01, // LDY #$01
0xB1, 0x20, // LDA ($20),Y
]);
bus.0[0x0014] = 0x00;
bus.0[0x0015] = 0x90;
bus.0[0x9000] = 0x55;
bus.0[0x0020] = 0x10;
bus.0[0x0021] = 0x90;
bus.0[0x9011] = 0xAA;
cpu.step(&mut bus).expect("ldx");
cpu.step(&mut bus).expect("lda (ind,x)");
assert_eq!(cpu.a, 0x55);
cpu.step(&mut bus).expect("ldy");
cpu.step(&mut bus).expect("lda (ind),y");
assert_eq!(cpu.a, 0xAA);
}
#[test]
fn bit_and_shift_ops_work() {
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
0xA9, 0x40, // LDA #$40
0x24, 0x10, // BIT $10
0x0A, // ASL A (0x80)
0x4A, // LSR A (0x40)
0x38, // SEC
0x6A, // ROR A (0xA0)
]);
bus.0[0x0010] = 0xC0;
for _ in 0..6 {
cpu.step(&mut bus).expect("opcode must be supported");
}
assert_eq!(cpu.a, 0xA0);
assert_ne!(cpu.p & FLAG_NEGATIVE, 0);
assert_eq!(cpu.p & FLAG_ZERO, 0);
}
#[test]
fn rmw_instructions_perform_dummy_write_before_final_write() {
struct TraceBus {
ram: [u8; 0x10000],
writes: Vec<(u16, u8)>,
}
impl CpuBus for TraceBus {
fn read(&mut self, addr: u16) -> u8 {
self.ram[addr as usize]
}
fn write(&mut self, addr: u16, value: u8) {
self.ram[addr as usize] = value;
self.writes.push((addr, value));
}
}
let mut bus = TraceBus {
ram: [0; 0x10000],
writes: Vec::new(),
};
bus.ram[0xFFFC] = 0x00;
bus.ram[0xFFFD] = 0x80;
bus.ram[0x8000] = 0xEE; // INC $2000
bus.ram[0x8001] = 0x00;
bus.ram[0x8002] = 0x20;
bus.ram[0x2000] = 0x7F;
let mut cpu = Cpu6502::default();
cpu.reset(&mut bus);
cpu.step(&mut bus).expect("INC should execute");
let writes_to_target: Vec<_> = bus
.writes
.iter()
.copied()
.filter(|(addr, _)| *addr == 0x2000)
.collect();
assert_eq!(writes_to_target, vec![(0x2000, 0x7F), (0x2000, 0x80)]);
}
#[test]
fn nmi_interrupt_jumps_to_vector() {
struct NmiBus {
ram: [u8; 0x10000],
nmi_pending: bool,
}
impl CpuBus for NmiBus {
fn read(&mut self, addr: u16) -> u8 {
self.ram[addr as usize]
}
fn write(&mut self, addr: u16, value: u8) {
self.ram[addr as usize] = value;
}
fn poll_nmi(&mut self) -> bool {
let out = self.nmi_pending;
self.nmi_pending = false;
out
}
}
let mut bus = NmiBus {
ram: [0; 0x10000],
nmi_pending: true,
};
bus.ram[0xFFFC] = 0x00;
bus.ram[0xFFFD] = 0x80;
bus.ram[0xFFFA] = 0x34;
bus.ram[0xFFFB] = 0x12;
let mut cpu = Cpu6502::default();
cpu.reset(&mut bus);
let cycles = cpu.step(&mut bus).expect("nmi should be serviced");
assert_eq!(cycles, 7);
assert_eq!(cpu.pc, 0x1234);
}
#[test]
fn undocumented_nop_da_is_accepted() {
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
0xDA, // undocumented NOP
0xA9, 0x42, // LDA #$42
]);
cpu.step(&mut bus).expect("0xDA should be supported");
cpu.step(&mut bus).expect("LDA should execute after 0xDA");
assert_eq!(cpu.a, 0x42);
}
#[test]
fn undocumented_slo_03_is_accepted() {
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
0xA2, 0x00, // LDX #$00
0x03, 0x10, // SLO ($10,X)
]);
bus.0[0x0010] = 0x00;
bus.0[0x0011] = 0x90;
bus.0[0x9000] = 0x40;
cpu.step(&mut bus).expect("LDX");
cpu.step(&mut bus).expect("0x03 should be supported");
assert_eq!(bus.0[0x9000], 0x80);
assert_eq!(cpu.a, 0x80);
}
#[test]
fn jam_opcode_halts_cpu_until_reset() {
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
0x02, // JAM/KIL
0xA9, 0x77, // LDA #$77 (must not execute while halted)
]);
let cycles = cpu.step(&mut bus).expect("jam opcode should decode");
assert_eq!(cycles, 2);
assert!(cpu.halted);
assert_eq!(cpu.pc, 0x8000);
let cycles2 = cpu.step(&mut bus).expect("halted cpu step should be valid");
assert_eq!(cycles2, 2);
assert_eq!(cpu.pc, 0x8000);
assert_eq!(cpu.a, 0x00);
cpu.reset(&mut bus);
assert!(!cpu.halted);
assert_eq!(cpu.pc, 0x8000);
}

View File

@@ -0,0 +1,242 @@
use super::*;
#[test]
fn cli_delays_irq_acceptance_by_one_instruction() {
struct IrqBus {
ram: [u8; 0x10000],
irq_level: bool,
}
impl CpuBus for IrqBus {
fn read(&mut self, addr: u16) -> u8 {
self.ram[addr as usize]
}
fn write(&mut self, addr: u16, value: u8) {
self.ram[addr as usize] = value;
}
fn poll_irq(&mut self) -> bool {
self.irq_level
}
}
let mut bus = IrqBus {
ram: [0; 0x10000],
irq_level: true,
};
bus.ram[0xFFFC] = 0x00;
bus.ram[0xFFFD] = 0x80;
bus.ram[0xFFFE] = 0x00;
bus.ram[0xFFFF] = 0x90; // IRQ vector -> $9000
bus.ram[0x8000] = 0x58; // CLI
bus.ram[0x8001] = 0xEA; // NOP
let mut cpu = Cpu6502::default();
cpu.reset(&mut bus);
let c1 = cpu.step(&mut bus).expect("CLI");
assert_eq!(c1, 2);
assert_eq!(cpu.pc, 0x8001);
let c2 = cpu.step(&mut bus).expect("NOP executes before IRQ");
assert_eq!(c2, 2);
assert_eq!(cpu.pc, 0x8002);
let c3 = cpu.step(&mut bus).expect("IRQ taken now");
assert_eq!(c3, 7);
assert_eq!(cpu.pc, 0x9000);
}
#[test]
fn plp_delays_irq_acceptance_by_one_instruction_when_clearing_i() {
struct IrqBus {
ram: [u8; 0x10000],
irq_level: bool,
}
impl CpuBus for IrqBus {
fn read(&mut self, addr: u16) -> u8 {
self.ram[addr as usize]
}
fn write(&mut self, addr: u16, value: u8) {
self.ram[addr as usize] = value;
}
fn poll_irq(&mut self) -> bool {
self.irq_level
}
}
let mut bus = IrqBus {
ram: [0; 0x10000],
irq_level: true,
};
bus.ram[0xFFFC] = 0x00;
bus.ram[0xFFFD] = 0x80;
bus.ram[0xFFFE] = 0x00;
bus.ram[0xFFFF] = 0x90;
bus.ram[0x8000] = 0xA9; // LDA #$20
bus.ram[0x8001] = 0x20;
bus.ram[0x8002] = 0x48; // PHA
bus.ram[0x8003] = 0x28; // PLP -> I clears
bus.ram[0x8004] = 0xEA; // NOP
let mut cpu = Cpu6502::default();
cpu.reset(&mut bus);
cpu.step(&mut bus).expect("LDA");
cpu.step(&mut bus).expect("PHA");
let c3 = cpu.step(&mut bus).expect("PLP");
assert_eq!(c3, 4);
assert_eq!(cpu.pc, 0x8004);
let c4 = cpu.step(&mut bus).expect("NOP executes before IRQ");
assert_eq!(c4, 2);
assert_eq!(cpu.pc, 0x8005);
let c5 = cpu.step(&mut bus).expect("IRQ taken");
assert_eq!(c5, 7);
assert_eq!(cpu.pc, 0x9000);
}
#[test]
fn jam_halt_ignores_nmi_and_irq() {
struct InterruptBus {
ram: [u8; 0x10000],
nmi: bool,
irq: bool,
}
impl CpuBus for InterruptBus {
fn read(&mut self, addr: u16) -> u8 {
self.ram[addr as usize]
}
fn write(&mut self, addr: u16, value: u8) {
self.ram[addr as usize] = value;
}
fn poll_nmi(&mut self) -> bool {
let out = self.nmi;
self.nmi = false;
out
}
fn poll_irq(&mut self) -> bool {
self.irq
}
}
let mut bus = InterruptBus {
ram: [0; 0x10000],
nmi: false,
irq: false,
};
bus.ram[0xFFFC] = 0x00;
bus.ram[0xFFFD] = 0x80;
bus.ram[0xFFFA] = 0x00;
bus.ram[0xFFFB] = 0x90;
bus.ram[0xFFFE] = 0x00;
bus.ram[0xFFFF] = 0xA0;
bus.ram[0x8000] = 0x02; // JAM
let mut cpu = Cpu6502::default();
cpu.reset(&mut bus);
cpu.step(&mut bus).expect("jam");
assert!(cpu.halted);
bus.nmi = true;
bus.irq = true;
let c = cpu.step(&mut bus).expect("halted step");
assert_eq!(c, 2);
assert_eq!(cpu.pc, 0x8000);
}
#[test]
fn rti_delays_irq_acceptance_by_one_instruction_when_clearing_i() {
struct IrqBus {
ram: [u8; 0x10000],
irq_level: bool,
}
impl CpuBus for IrqBus {
fn read(&mut self, addr: u16) -> u8 {
self.ram[addr as usize]
}
fn write(&mut self, addr: u16, value: u8) {
self.ram[addr as usize] = value;
}
fn poll_irq(&mut self) -> bool {
self.irq_level
}
}
let mut bus = IrqBus {
ram: [0; 0x10000],
irq_level: true,
};
bus.ram[0xFFFC] = 0x00;
bus.ram[0xFFFD] = 0x80;
bus.ram[0xFFFE] = 0x00;
bus.ram[0xFFFF] = 0x90; // IRQ vector
bus.ram[0x8000] = 0x40; // RTI
bus.ram[0x8001] = 0xEA; // NOP
// After reset SP is $FA, so RTI pops from $01FB/$01FC/$01FD.
bus.ram[0x01FB] = 0x20; // P (I clear)
bus.ram[0x01FC] = 0x01; // PC low
bus.ram[0x01FD] = 0x80; // PC high
let mut cpu = Cpu6502::default();
cpu.reset(&mut bus);
let c1 = cpu.step(&mut bus).expect("RTI");
assert_eq!(c1, 6);
assert_eq!(cpu.pc, 0x8001);
let c2 = cpu.step(&mut bus).expect("NOP before IRQ");
assert_eq!(c2, 2);
assert_eq!(cpu.pc, 0x8002);
let c3 = cpu.step(&mut bus).expect("IRQ now taken");
assert_eq!(c3, 7);
assert_eq!(cpu.pc, 0x9000);
}
#[test]
fn brk_is_hijacked_by_nmi_vector_when_nmi_arrives_during_brk() {
struct NmiBus {
ram: [u8; 0x10000],
nmi_now: bool,
}
impl CpuBus for NmiBus {
fn read(&mut self, addr: u16) -> u8 {
self.ram[addr as usize]
}
fn write(&mut self, addr: u16, value: u8) {
self.ram[addr as usize] = value;
}
fn poll_nmi(&mut self) -> bool {
let out = self.nmi_now;
self.nmi_now = false;
out
}
}
let mut bus = NmiBus {
ram: [0; 0x10000],
nmi_now: false,
};
bus.ram[0xFFFC] = 0x00;
bus.ram[0xFFFD] = 0x80;
bus.ram[0xFFFA] = 0x34; // NMI vector
bus.ram[0xFFFB] = 0x12;
bus.ram[0xFFFE] = 0x78; // IRQ/BRK vector
bus.ram[0xFFFF] = 0x56;
bus.ram[0x8000] = 0x00; // BRK
let mut cpu = Cpu6502::default();
cpu.reset(&mut bus);
bus.nmi_now = true;
let cycles = cpu.step(&mut bus).expect("BRK");
assert_eq!(cycles, 7);
assert_eq!(cpu.pc, 0x1234, "NMI vector should hijack BRK");
}

View File

@@ -0,0 +1,56 @@
use super::*;
#[test]
fn property_all_256_opcodes_decode_without_error() {
for opcode in 0u16..=255 {
let mut bus = TestRamBus::new();
bus.0[0xFFFC] = 0x00;
bus.0[0xFFFD] = 0x80;
bus.0[0xFFFE] = 0x00;
bus.0[0xFFFF] = 0x80;
bus.0[0xFFFA] = 0x00;
bus.0[0xFFFB] = 0x80;
bus.0[0x8000] = opcode as u8;
bus.0[0x8001] = 0x00;
bus.0[0x8002] = 0x00;
let mut cpu = Cpu6502::default();
cpu.reset(&mut bus);
let res = cpu.step(&mut bus);
assert!(res.is_ok(), "opcode {:02X} must be supported", opcode);
}
}
#[test]
fn property_lda_abs_x_adds_cycle_on_page_cross() {
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
0xA2, 0x01, // LDX #$01
0xBD, 0xFF, 0x12, // LDA $12FF,X -> crosses to $1300
]);
bus.0[0x1300] = 0x77;
let c1 = cpu.step(&mut bus).expect("LDX");
let c2 = cpu.step(&mut bus).expect("LDA abs,X");
assert_eq!(c1, 2);
assert_eq!(c2, 5);
assert_eq!(cpu.a, 0x77);
}
#[test]
fn property_lda_ind_y_adds_cycle_on_page_cross() {
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
0xA0, 0x01, // LDY #$01
0xB1, 0x10, // LDA ($10),Y
]);
bus.0[0x0010] = 0xFF;
bus.0[0x0011] = 0x12;
bus.0[0x1300] = 0x55;
let c1 = cpu.step(&mut bus).expect("LDY");
let c2 = cpu.step(&mut bus).expect("LDA ind,Y");
assert_eq!(c1, 2);
assert_eq!(c2, 6);
assert_eq!(cpu.a, 0x55);
}