added input base

This commit is contained in:
2026-08-09 23:36:45 -05:00
parent cda0a7ddb4
commit 5c24828842
4 changed files with 135 additions and 10 deletions
+5 -9
View File
@@ -51,6 +51,7 @@
3. Background pipeline timing (src/ppu.rs dot/bg_fetch, modeled after olcNES Part 4 reference): shifters must shift ONLY during fetch windows (cycles 2-257 and 321-337), with LoadBackgroundShifters + TransferAddressX at cycle 257 (preloads the NEXT scanline's first tile), IncrementScrollY at 256, idle NT fetches at 338/340, TransferAddressY on the pre-render line 280-304. Our code shifted every dot and copied horizontal at 255 -> each scanline started mid-tile, image shifted showing page 1 + page 2. Rewrote dot() with windowed shifters, (cycle-1)%8 fetch phases, load_shifters/update_shifters helpers, bit-mux fine-X (0x8000 >> x) in render_bg_pixel. Frame now renders pixel-correct with correct colors 3. Background pipeline timing (src/ppu.rs dot/bg_fetch, modeled after olcNES Part 4 reference): shifters must shift ONLY during fetch windows (cycles 2-257 and 321-337), with LoadBackgroundShifters + TransferAddressX at cycle 257 (preloads the NEXT scanline's first tile), IncrementScrollY at 256, idle NT fetches at 338/340, TransferAddressY on the pre-render line 280-304. Our code shifted every dot and copied horizontal at 255 -> each scanline started mid-tile, image shifted showing page 1 + page 2. Rewrote dot() with windowed shifters, (cycle-1)%8 fetch phases, load_shifters/update_shifters helpers, bit-mux fine-X (0x8000 >> x) in render_bg_pixel. Frame now renders pixel-correct with correct colors
- ACCEPTANCE MET: SMB renders the full frame correctly (background). Mario himself is a sprite and is NOT rendered yet - that's the next milestone (PPU-2 sprites + $4014 OAM DMA) - ACCEPTANCE MET: SMB renders the full frame correctly (background). Mario himself is a sprite and is NOT rendered yet - that's the next milestone (PPU-2 sprites + $4014 OAM DMA)
- PPU-2 3a SPRITES DONE: $4014 OAM DMA (bus.rs routes to Ppu::oam_dma, copies 256 bytes from CPU RAM page through the 2KB mirror), sprite evaluation at cycle 257 (8 sprites per scanline, overflow flag bit 5, sprite-0 tracking), sprite pattern fetch at cycle 340 (8x8 via ctrl bit 3, 8x16 via tile id bit 0, V-flip with 7-row inversion, H-flip via reverse_bits, hi plane +8), per-sprite shifters with X countdown (decrement X until 0 then shift, cycles 1-257), and render_pixel compositing (bg vs sprite priority via attr bit 5, sprites use palettes 4-7, sprite-0 hit sets status bit 6 at cycle>=9). Fixed one integration bug: the old bg-only pipeline block was left in dot() duplicating the new combined block (shifters shifted/fetched twice per dot) - deleted the old block. ACCEPTANCE MET: Mario visible and the coin sprite animates across frames - PPU-2 3a SPRITES DONE: $4014 OAM DMA (bus.rs routes to Ppu::oam_dma, copies 256 bytes from CPU RAM page through the 2KB mirror), sprite evaluation at cycle 257 (8 sprites per scanline, overflow flag bit 5, sprite-0 tracking), sprite pattern fetch at cycle 340 (8x8 via ctrl bit 3, 8x16 via tile id bit 0, V-flip with 7-row inversion, H-flip via reverse_bits, hi plane +8), per-sprite shifters with X countdown (decrement X until 0 then shift, cycles 1-257), and render_pixel compositing (bg vs sprite priority via attr bit 5, sprites use palettes 4-7, sprite-0 hit sets status bit 6 at cycle>=9). Fixed one integration bug: the old bg-only pipeline block was left in dot() duplicating the new combined block (shifters shifted/fetched twice per dot) - deleted the old block. ACCEPTANCE MET: Mario visible and the coin sprite animates across frames
- PPU-2 3b INPUT DONE (src/input.rs): InputSource trait (pluggable sources), MaskInput concrete source, Controller struct with strobe-latch + shift-out protocol ($4016/$4017, |0x80 filler so bits 9+ read 1), Key enum + key_to_button (Z=A, X=B, Shift=Select, Enter=Start, Arrows=D-pad). Bus wired. 3 unit tests pass. Fixed match-ordering bug: controller arms were below the $4000-$401F range arm (unreachable); specific arms must come before overlapping ranges
- Backup library mapper audit: mappers 0 (NROM) and 1 (MMC1) cover many games; still need mapper 2 (UNROM: Castlevania, Contra, Megaman 1), mapper 4 (MMC3: SMB2, SMB3, Lolo 2), mapper 7 (AOROM: Who Framed Roger Rabbit) - Backup library mapper audit: mappers 0 (NROM) and 1 (MMC1) cover many games; still need mapper 2 (UNROM: Castlevania, Contra, Megaman 1), mapper 4 (MMC3: SMB2, SMB3, Lolo 2), mapper 7 (AOROM: Who Framed Roger Rabbit)
- Finding: real commercial games rarely use illegal opcodes; none of the backup library needs them. Official-only CPU is sufficient for the goal of playing these games - Finding: real commercial games rarely use illegal opcodes; none of the backup library needs them. Official-only CPU is sufficient for the goal of playing these games
@@ -68,17 +69,12 @@ Phases in order:
3. NEXT: PPU-2 sprites + input + GUI window - makes SMB actually playable (Mario + enemies visible AND moveable). Detailed plan below. Four phases, sprites first (verifiable headless via PPM + dbg), then input logic (testable in isolation), then the window (first external deps): 3. NEXT: PPU-2 sprites + input + GUI window - makes SMB actually playable (Mario + enemies visible AND moveable). Detailed plan below. Four phases, sprites first (verifiable headless via PPM + dbg), then input logic (testable in isolation), then the window (first external deps):
3a. SPRITES - DONE (see Done section). Acceptance MET: Mario + coin sprite visible, coin animates 3a. SPRITES - DONE (see Done section). Acceptance MET: Mario + coin sprite visible, coin animates
3b. NEXT: INPUT (new src/input.rs, pure logic, testable without GUI): 3b. INPUT - DONE (see Done section). src/input.rs: InputSource trait + MaskInput + Controller (strobe latch, shift-out on read, |0x80 filler so bits 9+ read 1) + Key enum + key_to_button mapping. 3 unit tests pass (strobe repeats A, A-first shift-out order, B-second). Bus wired: $4016/$4017 read + write_strobe; caught a match-ordering bug (controller arms were below the 0x4000..=0x401F range arm so unreachable - specific arms must come first). $4017 APU frame-counter bits ignored for now (no APU yet)
- InputSource trait (the ONLY new trait): fn buttons(&mut self) -> u8, NES button bit order. Impls: KeyboardInput (winit later), BotInput for automated tests (e.g. "hold right 120 frames" to verify scrolling headlessly) 3c. BUS WIRING - DONE (done together with 3b): Controller in NesBus, $4016/$4017 read = shift out one bit, write = strobe. Open-bus handling for unused controller bits deferred
- Controller struct (concrete, NOT a trait - fixed hardware protocol): holds Box<dyn InputSource>, strobe latch + shift register for $4016/$4017 semantics. read/write protocol implemented once here 3d. NEXT: GUI WINDOW (src/render.rs + new deps): first external crates (egui + egui-winit + winit). main.rs owns the frame loop for ALL modes (nestest/blargg/cart/dbg/gui) - window is passive, renderer is dumb:
- Keyboard defaults: Z=A, X=B, Shift=Select, Enter=Start, Arrows=D-pad. Pure mapping fn, unit-testable
- One Controller per player ($4016 = P1, $4017 = P2); SMB1 is single-controller, two-player support for SMB2 later
3c. BUS WIRING (src/bus.rs): add Controller(s) to NesBus, $4016 write = strobe, $4016/$4017 read = shift out one bit (open-bus for unused bits). Bus stays a trait; controller wiring lives in the concrete NesBus impl like everything else
3d. GUI WINDOW (src/render.rs + new deps): first external crates (egui + egui-winit + winit). main.rs owns the frame loop for ALL modes (nestest/blargg/cart/dbg/gui) - window is passive, renderer is dumb:
- Renderer trait gains two default no-op methods (PPM inherits them): fn should_close(&self) -> bool { false }, fn poll_input(&mut self, controller: &mut Controller) {} - Renderer trait gains two default no-op methods (PPM inherits them): fn should_close(&self) -> bool { false }, fn poll_input(&mut self, controller: &mut Controller) {}
- WindowRenderer: egui + winit impl of Renderer. present() converts palette indices -> RGB via index_to_rgb, uploads as texture, draws. poll_input() maps winit key events -> Controller bits. should_close() from winit close request - WindowRenderer: egui + winit impl of Renderer. present() converts palette indices -> RGB via index_to_rgb, uploads as texture, draws. poll_input() maps winit key events -> Controller bits. should_close() from winit close request
- Keyboard defaults: Z=A, X=B, Shift=Select, Enter=Start, Arrows=D-pad (key_to_button already in input.rs, winit KeyCode -> Key glue needed)
- PPM stays as headless/dbg output; renders/ no longer written for normal GUI runs (was ~1MB/frame, heavy) - PPM stays as headless/dbg output; renders/ no longer written for normal GUI runs (was ~1MB/frame, heavy)
- New run_gui <rom> mode: event pump -> one emulated frame -> present, 60fps - New run_gui <rom> mode: event pump -> one emulated frame -> present, 60fps
- Acceptance: walk Mario in the window, screen scrolls correctly between nametables (this is how scrolling gets verified - the scroll logic itself is already fixed and correct) - Acceptance: walk Mario in the window, screen scrolls correctly between nametables (this is how scrolling gets verified - the scroll logic itself is already fixed and correct)
+8 -1
View File
@@ -1,5 +1,7 @@
use crate::{cartridge::Cartridge, ppu::Ppu}; use crate::{cartridge::Cartridge, ppu::Ppu};
use crate::input::{Controller, MaskInput};
pub trait Bus { pub trait Bus {
fn read(&mut self, address: u16) -> u8; fn read(&mut self, address: u16) -> u8;
fn write(&mut self, address: u16, value: u8); fn write(&mut self, address: u16, value: u8);
@@ -10,11 +12,14 @@ pub struct NesBus {
cart: Cartridge, cart: Cartridge,
ppu: Ppu, ppu: Ppu,
openbus: u8, // last byte on the CPU data bus openbus: u8, // last byte on the CPU data bus
controller: Controller,
} }
impl NesBus { impl NesBus {
pub fn new(cart: Cartridge) -> Self { pub fn new(cart: Cartridge) -> Self {
Self { ram: [0; 0x800], cart, ppu: Ppu::new(), openbus: 0 } Self { ram: [0; 0x800],
cart, ppu: Ppu::new(),
openbus: 0, controller: Controller::new(Box::new(MaskInput::new())), }
} }
pub fn tick_ppu(&mut self, cycles: u64) { pub fn tick_ppu(&mut self, cycles: u64) {
@@ -42,6 +47,7 @@ impl Bus for NesBus {
let result = match address { let result = match address {
0x0000..=0x1FFF => self.ram[(address & 0x07FF) as usize], // Zero-Page my beloved 0x0000..=0x1FFF => self.ram[(address & 0x07FF) as usize], // Zero-Page my beloved
0x2000..=0x3FFF => self.ppu.read(&mut self.cart, address, self.openbus), 0x2000..=0x3FFF => self.ppu.read(&mut self.cart, address, self.openbus),
0x4016 | 0x4017 => self.controller.read(),
0x4000..=0x401F => self.openbus, // APU regs: unmapped reads return bus contents 0x4000..=0x401F => self.openbus, // APU regs: unmapped reads return bus contents
0x4020..=0x5FFF => self.openbus, // expansion: open bus 0x4020..=0x5FFF => self.openbus, // expansion: open bus
0x6000..=0x7FFF => self.cart.read_prg_ram(address), // SRAM reads from the cartridge 0x6000..=0x7FFF => self.cart.read_prg_ram(address), // SRAM reads from the cartridge
@@ -56,6 +62,7 @@ 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 | 0x4017 => self.controller.write_strobe(value),
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
+121
View File
@@ -0,0 +1,121 @@
pub const BTN_A: u8 = 0b0000_0001;
pub const BTN_B: u8 = 0b0000_0010;
pub const BTN_SELECT: u8 = 0b0000_0100;
pub const BTN_START: u8 = 0b0000_1000;
pub const BTN_UP: u8 = 0b0001_0000;
pub const BTN_DOWN: u8 = 0b0010_0000;
pub const BTN_LEFT: u8 = 0b0100_0000;
pub const BTN_RIGHT: u8 = 0b1000_0000;
// Platform-agnostic key identity
pub enum Key {
A, B, Select, Start, Up, Down, Left, Right,
}
// Pure mapping, unit-testable without any GUI
pub fn key_to_button(key: Key) -> u8 {
match key {
Key::A => BTN_A,
Key::B => BTN_B,
Key::Select => BTN_SELECT,
Key::Start => BTN_START,
Key::Up => BTN_UP,
Key::Down => BTN_DOWN,
Key::Left => BTN_LEFT,
Key::Right => BTN_RIGHT,
}
}
pub trait InputSource {
fn buttons(&mut self) -> u8;
}
pub struct MaskInput {
state: u8,
}
impl MaskInput {
pub fn new() -> Self { Self { state: 0 } }
pub fn press(&mut self, button: u8) { self.state |= button; }
pub fn release(&mut self, button: u8) { self.state &= !button; }
}
impl InputSource for MaskInput {
fn buttons(&mut self) -> u8 { self.state }
}
// The fixed hardware protocol - ONE implementation, any InputSource behind it.
pub struct Controller {
source: Box<dyn InputSource>,
strobe: bool,
shift: u8,
}
impl Controller {
pub fn new(source: Box<dyn InputSource>) -> Self {
Self { source, strobe: false, shift: 0 }
}
// $4016/$4017 write: strobe pulse. On the falling edge, latch the buttons.
pub fn write_strobe(&mut self, value: u8) {
let strobe = value & 1 == 1;
if self.strobe && !strobe {
self.shift = self.source.buttons(); // falling edge: latch
}
self.strobe = strobe;
}
// $4016/$4017 read: while strobed, repeat A; else shift out one bit.
pub fn read(&mut self) -> u8 {
if self.strobe {
self.source.buttons() & 1
} else {
let bit = self.shift & 1;
// After 8 button bits the register reads as 1s (filler from the top)
self.shift = (self.shift >> 1) | 0x80;
bit
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strobe_repeats_a_while_high() {
let mut mask = MaskInput::new();
mask.press(BTN_A);
let mut pad = Controller::new(Box::new(mask));
pad.write_strobe(0x01); // strobe high
assert_eq!(pad.read(), 1); // A held -> repeats A
assert_eq!(pad.read(), 1);
}
#[test]
fn shift_out_order_a_first_then_filler_ones() {
let mut mask = MaskInput::new();
mask.press(BTN_A);
let mut pad = Controller::new(Box::new(mask));
pad.write_strobe(0x01);
pad.write_strobe(0x00); // falling edge latches
// A, B, Select, Start, Up, Down, Left, Right, then 1s forever
let bits = [1, 0, 0, 0, 0, 0, 0, 0, 1, 1];
for &b in &bits {
assert_eq!(pad.read(), b);
}
}
#[test]
fn shift_out_b_second() {
let mut mask = MaskInput::new();
mask.press(BTN_B);
let mut pad = Controller::new(Box::new(mask));
pad.write_strobe(0x01);
pad.write_strobe(0x00);
assert_eq!(pad.read(), 0); // A not pressed
assert_eq!(pad.read(), 1); // B pressed
assert_eq!(pad.read(), 0); // Select
assert_eq!(pad.read(), 0); // Start
}
}
+1
View File
@@ -6,6 +6,7 @@ mod palette;
mod render; mod render;
mod ppu; mod ppu;
mod dump; mod dump;
mod input;
use crate::bus::{Bus, NesBus}; use crate::bus::{Bus, NesBus};
use crate::cartridge::Cartridge; use crate::cartridge::Cartridge;