diff --git a/src/bus.rs b/src/bus.rs index ccfb63d..9a0bd55 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -68,8 +68,13 @@ impl Bus for NesBus { 0x0000..=0x1FFF => self.ram[(address & 0x07FF) as usize] = value, 0x2000..=0x3FFF => self.ppu.write(&mut self.cart, address, value), 0x4014 => self.ppu.oam_dma(&self.ram, value), - 0x4016 => self.controller.write_strobe(value), - 0x4017 => self.controller2.write_strobe(value), + 0x4016 => { + // Real hardware: the $4016 strobe line (OUT0) drives BOTH controller ports. + // Games only ever pulse $4016, so controller 2 must latch from it too. + self.controller.write_strobe(value); + self.controller2.write_strobe(value); + } + 0x4017 => {} // APU frame counter; it does not strobe the controllers 0x6000..=0x7FFF => self.cart.write_prg_ram(address, value), 0x8000..=0xFFFF => self.cart.write_prg(address, value), _ => {} // Everything else dropped for now diff --git a/src/cpu.rs b/src/cpu.rs index c1609ed..f7d4f82 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -20,6 +20,11 @@ pub struct Cpu { pub cycles: u64, irq_pending: bool, nmi_pending: bool, + pub hist: [(u16, u8, u8); 8], + pub hist_i: usize, + last_pc: u16, + entry: Option<(u16, u8, u8, u8, u8, u8, [(u16, u8, u8); 8])>, + check: Option<(u8, bool)>, // (A at $824F, BEQ $8255 taken) - SMB start-check probe } impl Cpu { @@ -45,7 +50,8 @@ impl Cpu { // New and reset methods pub fn new() -> Self { - Self { a: 0, x: 0, y: 0, sp: 0xFD, pc: 0, p: FLAG_UNUSED | FLAG_INTERRUPT, cycles: 0, irq_pending: false, nmi_pending: false } // kick stars a NES CPU + Self { a: 0, x: 0, y: 0, sp: 0xFD, pc: 0, p: FLAG_UNUSED | FLAG_INTERRUPT, cycles: 0, irq_pending: false, nmi_pending: false, + hist: [(0, 0, 0); 8], hist_i: 0, last_pc: 0, entry: None, check: None, } // kick stars a NES CPU } pub fn reset(&mut self, bus: &mut impl Bus) { @@ -104,6 +110,13 @@ impl Cpu { pub fn sp(&self) -> u8 { self.sp } pub fn p(&self) -> u8 { self.p } pub fn cycles(&self) -> u64 { self.cycles } + pub fn hist(&self) -> [(u16, u8, u8); 8] { self.hist } + pub fn take_entry(&mut self) -> Option<(u16, u8, u8, u8, u8, u8, [(u16, u8, u8); 8])> { + self.entry.take() + } + pub fn take_check(&mut self) -> Option<(u8, bool)> { + self.check.take() + } // shared interrumpt function fn interrupt(&mut self, bus: &mut impl Bus, vector: u16) -> u8 { @@ -130,6 +143,23 @@ impl Cpu { return cycles; } + let in_loop = (0x8227..=0x822E).contains(&self.pc); + if in_loop && !(0x8227..=0x822E).contains(&self.last_pc) && self.entry.is_none() { + let recent = core::array::from_fn( |i| { + let idx = self.hist_i.wrapping_sub(i + 1) & 7; + self.hist[idx] + }); + self.entry = Some((self.pc, self.a, self.x, self.y, self.sp, self.p, recent)); + } + // Diagnostic: SMB's title Start check is `$8247: LDA $06FC; ORA $06FD; CMP #$10; + // $824F: BEQ $8255` (StartGame). At $824F, A == $10 means the press was seen. + if self.pc == 0x824F && self.check.is_none() { + self.check = Some((self.a, self.a == 0x10)); + } + self.last_pc = self.pc; + self.hist[self.hist_i % 8] = (self.pc, self.y, self.sp); + self.hist_i += 1; + let opcode: u8 = self.fetch(bus); let cycles = match opcode { // Jumps & subroutines diff --git a/src/main.rs b/src/main.rs index 45f05e1..38897f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use crate::cartridge::Cartridge; use crate::cpu::Cpu; use crate::render::{PpmRenderer, Renderer}; use eframe::egui; +use eframe::egui::debug_text::print; use crate::render::WindowRenderer; @@ -23,8 +24,9 @@ fn main() { 3 if args[1] == "blargg" => run_blargg(&args[2]), 3 if args[1] == "dbg" => run_dbg(&args[2]), 3 if args[1] == "gui" => run_gui(&args[2]), + 3 if args[1] == "autotest" => run_autotest(&args[2]), 2 => run_cart(&args[1]), - _ => eprintln!("usage: rnes [ | blargg | dbg | gui ]"), + _ => eprintln!("usage: rnes [ | blargg | dbg | gui | autotest ]"), } } @@ -103,6 +105,50 @@ fn run_one_frame(bus: &mut NesBus, cpu: &mut Cpu) { bus.begin_frame(); } +// Headless scripted-input test: drives the controller exactly like the GUI does +// (Controller.set_buttons), so SMB's title Start handling can be observed without +// needing a window or a keyboard. Frame-accurate and reproducible. +fn run_autotest(path: &str) { + let cart = Cartridge::from_file(path).unwrap(); + let mut bus = NesBus::new(cart); + let mut cpu = Cpu::new(); + cpu.reset(&mut bus); + + let frames: u32 = 900; + let mut last_mode = 0u8; + + for f in 0..frames { + // hold Start frames 120..300, release 300..420, press and hold again from 420 + let buttons: u8 = if (120..300).contains(&f) || f >= 420 { + crate::input::BTN_START + } else { + 0 + }; + bus.controller_mut().set_buttons(buttons); + + run_one_frame(&mut bus, &mut cpu); + + let fc = bus.ram()[(0x06FC & 0x7FF) as usize]; + let fd = bus.ram()[(0x06FD & 0x7FF) as usize]; + let m7a = bus.ram()[(0x074A & 0x7FF) as usize]; + let tsk = bus.ram()[(0x0772 & 0x7FF) as usize]; + let dt = bus.ram()[(0x07A2 & 0x7FF) as usize]; + let mode = bus.ram()[(0x0770 & 0x7FF) as usize]; + + let check = match cpu.take_check() { + Some((a, taken)) => format!("CHECK A={a:02X} BEQ={:<5}", taken), + None => String::from("CHECK -- "), + }; + println!( + "f={f:03} {check} $06FC={fc:02X} $06FD={fd:02X} mask={m7a:02X} task={tsk:02X} demo={dt:02X} mode={mode:02X}" + ); + if mode != last_mode { + println!("f={f:03} *** MODE {last_mode:02X} -> {mode:02X} ***"); + last_mode = mode; + } + } +} + struct GuiApp { bus: NesBus, cpu: Cpu, @@ -123,9 +169,31 @@ impl eframe::App for GuiApp { // Non-UI work: emulation runs here, before painting. fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { self.renderer.poll_input(self.bus.controller_mut()); + + if let Some((pc, a, x, y, sp, p, recent)) = self.cpu.take_entry() { + print!("ENTRY: pc={pc:04X} A={a:02X} X={x:02X} Y={y:02X} SP={sp:02X} P={p:02X} | pre:"); + for &(rpc, ry, rsp) in &recent { print!(" {rpc:04X}(Y={ry:02X})"); } + println!(); + } + if let Some((a, taken)) = self.cpu.take_check() { + let tsk = self.bus.ram()[(0x0772 & 0x7FF) as usize]; + let dt = self.bus.ram()[(0x07A2 & 0x7FF) as usize]; + println!("CHECK: A={a:02X} BEQ={taken} task={tsk:02X} demoTimer={dt:02X}"); + } + // TEMP: watch SMB's joypad storage + game state + PC + let fc = self.bus.ram()[(0x06FC & 0x7FF) as usize]; + let m7a = self.bus.ram()[(0x074A & 0x7FF) as usize]; + let m70 = self.bus.ram()[(0x0770 & 0x7FF) as usize]; + let st = self.bus.ppu().status(); + let h = self.cpu.hist(); + println!("$06FC={fc:02X} mask=$074A={m7a:02X} $2002={st:02X} mode={m70:02X} \ + A={:02X} X={:02X} Y={:02X} SP={:02X} PC={:04X}", + self.cpu.a(), self.cpu.x(), self.cpu.y(), self.cpu.sp(), self.cpu.pc()); + print!(" hist(old->new): "); + for &(pc, y, sp) in &h { print!("{pc:04X}(Y={y:02X},SP={sp:02X})"); } + println!(); run_one_frame(&mut self.bus, &mut self.cpu); self.renderer.present(self.bus.framebuffer(), 256, 240); - // cap at 60fps: one emulated frame per 16.67ms, not "as fast as possible" ctx.request_repaint_after(std::time::Duration::from_millis(16)); }