Controller works

This commit is contained in:
2026-09-26 18:43:49 -05:00
parent ce554c68ac
commit 2e3e7e5bf4
3 changed files with 108 additions and 5 deletions
+7 -2
View File
@@ -68,8 +68,13 @@ impl Bus for NesBus {
0x0000..=0x1FFF => self.ram[(address & 0x07FF) as usize] = value, 0x0000..=0x1FFF => self.ram[(address & 0x07FF) as usize] = value,
0x2000..=0x3FFF => self.ppu.write(&mut self.cart, address, value), 0x2000..=0x3FFF => self.ppu.write(&mut self.cart, address, value),
0x4014 => self.ppu.oam_dma(&self.ram, value), 0x4014 => self.ppu.oam_dma(&self.ram, value),
0x4016 => self.controller.write_strobe(value), 0x4016 => {
0x4017 => self.controller2.write_strobe(value), // 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), 0x6000..=0x7FFF => self.cart.write_prg_ram(address, value),
0x8000..=0xFFFF => self.cart.write_prg(address, value), 0x8000..=0xFFFF => self.cart.write_prg(address, value),
_ => {} // Everything else dropped for now _ => {} // Everything else dropped for now
+31 -1
View File
@@ -20,6 +20,11 @@ pub struct Cpu {
pub cycles: u64, pub cycles: u64,
irq_pending: bool, irq_pending: bool,
nmi_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 { impl Cpu {
@@ -45,7 +50,8 @@ impl Cpu {
// New and reset methods // New and reset methods
pub fn new() -> Self { 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) { pub fn reset(&mut self, bus: &mut impl Bus) {
@@ -104,6 +110,13 @@ impl Cpu {
pub fn sp(&self) -> u8 { self.sp } pub fn sp(&self) -> u8 { self.sp }
pub fn p(&self) -> u8 { self.p } pub fn p(&self) -> u8 { self.p }
pub fn cycles(&self) -> u64 { self.cycles } 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 // shared interrumpt function
fn interrupt(&mut self, bus: &mut impl Bus, vector: u16) -> u8 { fn interrupt(&mut self, bus: &mut impl Bus, vector: u16) -> u8 {
@@ -130,6 +143,23 @@ impl Cpu {
return cycles; 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 opcode: u8 = self.fetch(bus);
let cycles = match opcode { let cycles = match opcode {
// Jumps & subroutines // Jumps & subroutines
+70 -2
View File
@@ -13,6 +13,7 @@ use crate::cartridge::Cartridge;
use crate::cpu::Cpu; use crate::cpu::Cpu;
use crate::render::{PpmRenderer, Renderer}; use crate::render::{PpmRenderer, Renderer};
use eframe::egui; use eframe::egui;
use eframe::egui::debug_text::print;
use crate::render::WindowRenderer; use crate::render::WindowRenderer;
@@ -23,8 +24,9 @@ fn main() {
3 if args[1] == "blargg" => run_blargg(&args[2]), 3 if args[1] == "blargg" => run_blargg(&args[2]),
3 if args[1] == "dbg" => run_dbg(&args[2]), 3 if args[1] == "dbg" => run_dbg(&args[2]),
3 if args[1] == "gui" => run_gui(&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]), 2 => run_cart(&args[1]),
_ => eprintln!("usage: rnes [<rom> | blargg <rom> | dbg <rom> | gui <rom>]"), _ => eprintln!("usage: rnes [<rom> | blargg <rom> | dbg <rom> | gui <rom> | autotest <rom>]"),
} }
} }
@@ -103,6 +105,50 @@ fn run_one_frame(bus: &mut NesBus, cpu: &mut Cpu) {
bus.begin_frame(); 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 { struct GuiApp {
bus: NesBus, bus: NesBus,
cpu: Cpu, cpu: Cpu,
@@ -123,9 +169,31 @@ impl eframe::App for GuiApp {
// Non-UI work: emulation runs here, before painting. // Non-UI work: emulation runs here, before painting.
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.renderer.poll_input(self.bus.controller_mut()); 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); run_one_frame(&mut self.bus, &mut self.cpu);
self.renderer.present(self.bus.framebuffer(), 256, 240); 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)); ctx.request_repaint_after(std::time::Duration::from_millis(16));
} }