diff --git a/STATE.md b/STATE.md index 622fba3..28afb06 100644 --- a/STATE.md +++ b/STATE.md @@ -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 - 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 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) - 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): 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): - - 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) - - Controller struct (concrete, NOT a trait - fixed hardware protocol): holds Box, strobe latch + shift register for $4016/$4017 semantics. read/write protocol implemented once here - - 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: + 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) + 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 + 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: - 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 + - 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) - New run_gui 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) diff --git a/src/bus.rs b/src/bus.rs index 6ba7635..a35e264 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -1,5 +1,7 @@ use crate::{cartridge::Cartridge, ppu::Ppu}; +use crate::input::{Controller, MaskInput}; + pub trait Bus { fn read(&mut self, address: u16) -> u8; fn write(&mut self, address: u16, value: u8); @@ -10,11 +12,14 @@ pub struct NesBus { cart: Cartridge, ppu: Ppu, openbus: u8, // last byte on the CPU data bus + controller: Controller, } impl NesBus { 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) { @@ -42,6 +47,7 @@ impl Bus for NesBus { let result = match address { 0x0000..=0x1FFF => self.ram[(address & 0x07FF) as usize], // Zero-Page my beloved 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 0x4020..=0x5FFF => self.openbus, // expansion: open bus 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, 0x2000..=0x3FFF => self.ppu.write(&mut self.cart, address, 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), 0x8000..=0xFFFF => self.cart.write_prg(address, value), _ => {} // Everything else dropped for now diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..d31d2ec --- /dev/null +++ b/src/input.rs @@ -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, + strobe: bool, + shift: u8, +} + +impl Controller { + pub fn new(source: Box) -> 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 + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index ac70189..3b2df1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod palette; mod render; mod ppu; mod dump; +mod input; use crate::bus::{Bus, NesBus}; use crate::cartridge::Cartridge;