From a1ff5f280fd1c459bd93bd2161891d1a248d8272 Mon Sep 17 00:00:00 2001 From: JakeBreath Date: Sat, 8 Aug 2026 14:42:38 -0500 Subject: [PATCH] Added 10 more opcodes into the CPU interpreter --- STATE.md | 51 +++++++++++++++++++++++++++--- ours.log | 5 ++- src/cpu.rs | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 140 insertions(+), 9 deletions(-) diff --git a/STATE.md b/STATE.md index bd22ab7..219a829 100644 --- a/STATE.md +++ b/STATE.md @@ -10,18 +10,61 @@ - read_prg/read_chr/write_chr with debug_assert guards - Split from_file (thin I/O wrapper) / parse_data (pure parser) for testability - 8 unit tests, all passing (header, PRG mapping, error cases, CHR-RAM roundtrip) +- Bus module done (src/bus.rs): + - Bus trait (read/write) so the CPU is decoupled from the concrete memory layout + - NesBus: 2KB mirrored CPU RAM, cartridge PRG routing, PPU/APU/SRAM/expansion regions stubbed (return 0) +- CPU skeleton done (src/cpu.rs): + - Flag bit constants, registers (A/X/Y/SP/PC/P), set/clear/flag helpers, set_zp_flags + - Memory helpers: fetch, fetch16, read, write, read16, push, pop + - All 10 addressing mode helpers, including the JMP ($xxxx) page-wrap bug and page-cross cycle penalties + - First 3 opcodes implemented (JMP abs, LDX imm, STX zp) + - step dispatch shell + trace in nestest format + - set_test_start for the nestest harness +- nestest harness working (main.rs): loads nestest.nes, runs from $C000 with CYC:7, dumps trace to ours.log +- First 5 trace lines match the nestest reference log exactly (PC, regs, P, SP, CYC), then it panics on the next unimplemented opcode (JSR 0x20), which names the next target ## Next -- CPU (6502 core) - biggest remaining piece for this milestone -- Then bus to connect CPU to RAM/cartridge -- Verify CPU with nestest log diff + blargg suite before any graphics -- GUI (egui + winit) and PPU come after the CPU milestone +Finish the official CPU. Nestest analysis findings: the full 8991-line log tests illegal opcodes too (225 distinct opcodes total; first illegal is 0x04 at line 5004). So the official-only CPU is verified against lines 1-5003, then panics on 0x04 by design. Official opcodes never seen in the log: BRK 0x00, CLI 0x58. + +CPU implementation batches (by family, ordered by nestest log frequency): + +- Batch 1 - Subroutines + stack: JSR 0x20 (push ret-1 high then low), RTS 0x60 (pop low then high), PHA 0x48, PLA 0x68, PHP 0x08, PLP 0x28 (B-flag on push rule), RTI 0x40, BRK 0x00 +- Batch 2 - Branches (all 8): BEQ F0, BNE D0, BCC 90, BCS B0, BPL 10, BMI 30, BVC 50, BVS 70. One shared method per flag. +1 cycle if taken, +1 more if target crosses page +- Batch 3 - Loads: LDA 8 modes, LDX 5 modes, LDY 5 modes (share load + set_zp_flags) +- Batch 4 - Stores: STA 7 modes, STX 3, STY 3 (share store core, no flag changes) +- Batch 5 - Compares: CMP 8 modes, CPX 3, CPY 3 (set C/Z/N, no store) +- Batch 6 - Arithmetic: ADC 8 modes, SBC 8 modes (N/V/C/Z, decimal flag inert), BIT 2 modes +- Batch 7 - Logic: AND, ORA, EOR (8 modes each) +- Batch 8 - Inc/Dec: INC/DEC 4 modes each (RMW), INX, INY, DEX, DEY +- Batch 9 - Shifts: ASL, LSR, ROL, ROR (accumulator + 4 memory modes each, RMW) +- Batch 10 - Flags + misc: CLC 18, SEC 38, CLI 58, SEI 78, CLD D8, SED F8, CLV B8, NOP EA, JMP abs 4C, JMP ind 6C, transfers TAX/TAY/TXA/TYA/TSX/TXS + +Rules for each batch: +- Add the opcode's length to opcode_len in the same edit (never let them drift) +- Re-diff after each batch (cargo run > ours.log + awk column compare) +- Check base cycles per mode, feed page-cross extra into the cycle total + +Verification for this phase: +- All 151 official opcodes implemented +- Trace matches lines 1-5003 field-for-field (PC/A/X/Y/P/SP/CYC) +- cargo build clean +- Commit + update STATE.md + +After official CPU is done (follow-ups): +- Add MMC1 (mapper 1) to cartridge so blargg official_only.nes / all_instrs.nes can run (authoritative official check) +- Run blargg mapper-0 suite (coredump, cpu_timing_test, ram_after_reset, registers) +- Then the illegal-opcode phase for the full 8991-line nestest pass +- Then bus/PPU/timing, GUI (egui + winit) come after the CPU milestone ## Decisions - No external dependencies yet, keeping it pure std until the GUI milestone - GUI choice: egui + winit (decided, not yet used) - Scope: cartridge + CPU first, verified headless before graphics +- Bus is a trait (impl Bus) so the CPU works against any memory layout (real bus, test bus, debug bus) +- Flags stored as a raw u8 with named bit-mask constants (FLAG_CARRY etc.) +- opcode length is a growing match in opcode_len() (returns u8); migrate to a full 256-entry table when it gets big +- trace diff compares PC + A/X/Y/P/SP/CYC, ignoring disassembly/PPU columns - Mapper 0 first, other mappers (MMC1, CNROM) implemented after CPU works - Test ROMs are homebrew/open, no ethics issue; game backups stay in gitignored roms/backup diff --git a/ours.log b/ours.log index 9d1a90e..1a1fbce 100644 --- a/ours.log +++ b/ours.log @@ -3,4 +3,7 @@ C5F5 A2 00 A:00 X:00 Y:00 P:24 SP:FD CYC:10 C5F7 86 00 A:00 X:00 Y:00 P:26 SP:FD CYC:12 C5F9 86 10 A:00 X:00 Y:00 P:26 SP:FD CYC:15 C5FB 86 11 A:00 X:00 Y:00 P:26 SP:FD CYC:18 -C5FD 20 A:00 X:00 Y:00 P:26 SP:FD CYC:21 +C5FD 20 2D C7 A:00 X:00 Y:00 P:26 SP:FD CYC:21 +C72D EA A:00 X:00 Y:00 P:26 SP:FB CYC:27 +C72E 38 A:00 X:00 Y:00 P:26 SP:FB CYC:29 +C72F B0 A:00 X:00 Y:00 P:27 SP:FB CYC:31 diff --git a/src/cpu.rs b/src/cpu.rs index f623181..747e930 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -99,6 +99,16 @@ impl Cpu { 0x4C => self.jmp_abs(bus), 0xA2 => self.ldx_imm(bus), 0x86 => self.stx_zp(bus), + 0x20 => self.jsr_abs(bus), + 0x60 => self.rts_imp(bus), + 0x48 => self.pha_imp(bus), + 0x68 => self.pla_imp(bus), + 0x08 => self.php_imp(bus), + 0x28 => self.plp_imp(bus), + 0x40 => self.rti_imp(bus), + 0x00 => self.brk_imp(bus), + 0xEA => self.nop_imp(bus), + 0x38 => self.sec_imp(bus), _ => panic!("illegal/unkown opcode {opcode:#04X} at {:#06X}", self.pc.wrapping_sub(1)), }; self.cycles += cycles as u64; @@ -182,30 +192,105 @@ impl Cpu { } // NES 6502 opcode -> functions - fn jmp_abs(&mut self, bus: &mut impl Bus) -> u8 { // 0x4C, 3 cycles + fn jmp_abs(&mut self, bus: &mut impl Bus) -> u8 { // 0x4C, 3 bytes, 3 cycles self.pc = self.fetch16(bus); 3 } - fn ldx_imm(&mut self, bus: &mut impl Bus) -> u8 { // 0xA2, 2 cycles + fn ldx_imm(&mut self, bus: &mut impl Bus) -> u8 { // 0xA2, 2 bytes, 2 cycles self.x = self.fetch(bus); self.set_zp_flags(self.x); 2 } - fn stx_zp(&mut self, bus: &mut impl Bus) -> u8 { // 0x86, 3 cycles + fn stx_zp(&mut self, bus: &mut impl Bus) -> u8 { // 0x86, 2 bytes, 3 cycles let (addr, _) = self.address_zp(bus); self.write(bus, addr, self.x); 3 } + fn jsr_abs(&mut self, bus: &mut impl Bus) -> u8 { // 0x20, 3 bytes, 6 cycles + let target = self.fetch16(bus); + let ret = self.pc.wrapping_sub(1); + self.push(bus, (ret >> 8) as u8); + self.push(bus, (ret &0xFF) as u8); + self.pc = target; + 6 + } + + fn rts_imp(&mut self, bus: &mut impl Bus) -> u8 { // 0x60, 1 byte, 6 cycles + let low = self.pop(bus) as u16; + let high = self.pop(bus) as u16; + self.pc = (high << 8 | low).wrapping_add(1); + 6 + } + + fn pha_imp(&mut self, bus: &mut impl Bus) -> u8 { // 0x48, 1 byte, 3 cycles + self.push(bus, self.a); + 3 + } + + fn pla_imp(&mut self, bus: &mut impl Bus) -> u8 { // 0x68, 1 byte, 4 cycles + self.a = self.pop(bus); + self.set_zp_flags(self.a); + 4 + } + + fn php_imp(&mut self, bus: &mut impl Bus) -> u8 { // 0x08, 1 byte, 3 cycles; B-flag model: B exists only in the pushed copy + self.push(bus, self.p | FLAG_BREAK); + 3 + } + + fn plp_imp(&mut self, bus: &mut impl Bus) -> u8 { // 0x28, 1 byte, 4 cycles + self.p = (self.pop(bus) & !FLAG_BREAK) | FLAG_UNUSED; + 4 + } + + fn rti_imp(&mut self, bus: &mut impl Bus) -> u8 { // 0x40, 1 byte, 6 cycles + self.p = (self.pop(bus) & !FLAG_BREAK) | FLAG_UNUSED; + let low: u16 = self.pop(bus) as u16; + let high: u16 = self.pop(bus) as u16; + self.pc = high << 8 | low; + 6 + } + + fn brk_imp(&mut self, bus: &mut impl Bus) -> u8 { // 0x00, 1 byte, 7 cycles + self.pc = self.pc.wrapping_add(1); // Skip padding byte + let ret = self.pc; + self.push(bus, (ret >> 8) as u8); + self.push(bus, (ret & 0xFF) as u8); + self.push(bus, self.p | FLAG_BREAK); + self.set_flag(FLAG_INTERRUPT); + self.pc = self.read16(bus, 0xFFFE); // IRQ vector + 7 + } + + fn nop_imp(&mut self, _bus: &mut impl Bus) -> u8 { // 0xEA, 1 byte, 2 cycles + 2 + } + + fn sec_imp(&mut self, _bus: &mut impl Bus) -> u8 { // 0x38, 1 byte, 2 cycles + self.set_flag(FLAG_CARRY); + 2 + } + // NES 6502 opcode length table/matcher fn opcode_len(opcode: u8) -> u8 { match opcode { 0x4C => 3, // JMP abs 0xA2 => 2, // LDX imm 0x86 => 2, // STX zp - _ => 1, // fallback, would show the invalid/unknown opcode only + 0x20 => 3, // JSR abs + 0x60 => 1, // RTS imp + 0x48 => 1, // PHA imp + 0x68 => 1, // PLA imp + 0x08 => 1, // PHP imp + 0x28 => 1, // PLP imp + 0x40 => 1, // RTI imp + 0x00 => 1, // BRK imp + 0xEA => 1, // NOP + 0x38 => 1, // SEC imp + _ => 1, // fallback, would show the invalid/unknown opcode only not its parameters } }