added input base
This commit is contained in:
@@ -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
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user