Controller works
This commit is contained in:
+7
-2
@@ -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
|
||||
|
||||
+31
-1
@@ -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
|
||||
|
||||
+70
-2
@@ -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 [<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();
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user