From 3372559c190a2b91d2d4c8d437db58d95b6135b9 Mon Sep 17 00:00:00 2001 From: Matthew Pomes Date: Tue, 10 Feb 2026 22:17:51 -0600 Subject: [PATCH] Working Sprite implementation --- src/main.rs | 7 +- src/ppu.rs | 268 +++++++++++++++++++-- src/test_roms/input_test.s | 335 +++++++++++++++++++++++++++ src/test_roms/mod.rs | 63 +++++ src/test_roms/ppu.rs | 5 + src/test_roms/ppu_fine_x_scrolling.s | 306 ++++++++++++++++++++++++ src/test_roms/scrolling.s | 306 ++++++++++++++++++++++++ src/test_roms/sprites.s | 321 +++++++++++++++++++++++++ 8 files changed, 1585 insertions(+), 26 deletions(-) create mode 100644 src/test_roms/input_test.s create mode 100644 src/test_roms/ppu_fine_x_scrolling.s create mode 100644 src/test_roms/scrolling.s create mode 100644 src/test_roms/sprites.s diff --git a/src/main.rs b/src/main.rs index 637af55..9ae34e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,7 +35,10 @@ use tracing_subscriber::EnvFilter; // const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "int_nmi_exit_timing.nes"); // const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "render-updating.nes"); // const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "ppu_palette_shared.nes"); -const ROM_FILE: &str = "./Super Mario Bros. (World).nes"; +// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "input_test.nes"); +// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "scrolling.nes"); +const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "sprites.nes"); +// const ROM_FILE: &str = "./Super Mario Bros. (World).nes"; // const ROM_FILE: &str = "./cpu_timing_test.nes"; // const ROM_FILE: &str = "../nes-test-roms/instr_test-v5/official_only.nes"; @@ -178,7 +181,7 @@ impl Emulator { nes, windows: HashMap::from_iter([ (win, WindowType::Main), - (win_2, WindowType::Memory(MemoryTy::OAM, HexView {})) + (win_2, WindowType::Debugger) ]), debugger: DebuggerState::new(), main_win_size: Size::new(0., 0.), diff --git a/src/ppu.rs b/src/ppu.rs index 3b35e9d..95ea349 100644 --- a/src/ppu.rs +++ b/src/ppu.rs @@ -88,11 +88,205 @@ pub enum PPUMMRegisters { Palette, } +bitfield::bitfield! { + #[derive(Default, Clone, Copy, PartialEq, Eq, Hash)] + pub struct SpriteAttrs(u8); + impl Debug; + vflip, set_vflip: 7; + hflip, set_hflip: 6; + priority, set_priority: 5; + palette, set_palette: 1, 0; +} + +#[derive(Debug, Clone, Default)] +struct SpriteOutputUnit { + high: u8, + low: u8, + x_pos: u8, + y_pos: u8, + tile: u8, + attrs: SpriteAttrs, +} + +#[derive(Debug, Clone)] +enum OamState { + ReadY, + ReadTile, + ReadAttrs, + ReadX, + OverflowScan, + Wait, +} + #[derive(Debug, Clone)] pub struct OAM { mem: Vec, + secondary: Vec, addr: u8, + count: u8, + state: OamState, + oam_read_buffer: u8, edit_ver: usize, + sprite_output_units: Vec, + overflow: bool, + sprite_offset_0x1000: bool, +} + +impl OAM { + fn new() -> Self { + Self { + mem: vec![0u8; 256], + addr: 0, + count: 0, + edit_ver: 0, + secondary: vec![0xFF; 32], + sprite_output_units: vec![SpriteOutputUnit::default(); 8], + state: OamState::ReadY, + oam_read_buffer: 0, + overflow: false, + sprite_offset_0x1000: false, + } + } + + fn ppu_cycle(&mut self, pixel: usize, line: usize, mem: &mut PpuMem<'_>) { + if pixel < 64 { + } else if pixel == 64 { + self.secondary.fill(0xFF); + self.state = OamState::ReadY; + self.addr = 0; + self.count = 0; + } else if pixel <= 256 { + // Fetch/evalute sprites for next line + if pixel % 2 == 1 && !matches!(self.state, OamState::Wait) { + self.oam_read_buffer = self.mem[self.addr as usize]; + if self.addr == 255 { + self.addr = 0; + } else { + self.addr += 1; + } + } else { + match self.state { + OamState::ReadY => { + let l = self.oam_read_buffer as usize; + if self.count < 8 { + if l <= line && line < l + 8 { + self.secondary[self.count as usize * 4] = self.oam_read_buffer; + self.state = OamState::ReadTile; + } else { + if self.addr < 0xFD { + self.addr += 3; + } else { + self.state = OamState::Wait; // Should be Overflow scan... + } + } + } else { + self.state = OamState::Wait; // Should be Overflow scan... + } + } + OamState::ReadTile => { + self.secondary[self.count as usize * 4 + 1] = self.oam_read_buffer; + self.state = OamState::ReadAttrs; + } + OamState::ReadAttrs => { + self.secondary[self.count as usize * 4 + 2] = self.oam_read_buffer; + self.state = OamState::ReadX; + } + OamState::ReadX => { + self.secondary[self.count as usize * 4 + 3] = self.oam_read_buffer; + self.state = OamState::ReadY; + self.count += 1; + if self.count == 8 { + self.state = OamState::Wait; // Should be Overflow scan... + } + } + OamState::OverflowScan => todo!(), + OamState::Wait => (), + } + } + } else if pixel <= 320 { + if pixel == 257 { + // Reset output units + self.sprite_output_units.fill(SpriteOutputUnit::default()); + } + let run = pixel - 257; + if run / 8 < self.count as usize { + if run % 8 == 0 { + self.sprite_output_units[run / 8].y_pos = self.secondary[(run / 8) * 4]; + } else if run % 8 == 1 { + self.sprite_output_units[run / 8].tile = self.secondary[(run / 8) * 4 + 1]; + } else if run % 8 == 2 { + self.sprite_output_units[run / 8].attrs.0 = self.secondary[(run / 8) * 4 + 2]; + } else if run % 8 == 3 { + self.sprite_output_units[run / 8].x_pos = self.secondary[(run / 8) * 4 + 3]; + } else if run % 8 == 4 { + } else if run % 8 == 5 { + let off = line - self.sprite_output_units[run / 8].y_pos as usize; + let off = if self.sprite_output_units[run / 8].attrs.vflip() { + 7 - off + } else { + off + }; + let addr = if self.sprite_offset_0x1000 { + 0x1000 + } else { + 0x0000 + } + 16 * self.sprite_output_units[run / 8].tile as u16 + + off as u16; + self.sprite_output_units[run / 8].low = mem.read(addr).reg_map(|_, _| todo!()); + } else if run % 8 == 6 { + } else if run % 8 == 7 { + let off = line - self.sprite_output_units[run / 8].y_pos as usize; + let off = if self.sprite_output_units[run / 8].attrs.vflip() { + 7 - off + } else { + off + }; + let addr = if self.sprite_offset_0x1000 { + 0x1000 + } else { + 0x0000 + } + 16 * self.sprite_output_units[run / 8].tile as u16 + + off as u16 + + 8; + self.sprite_output_units[run / 8].high = mem.read(addr).reg_map(|_, _| todo!()); + } + } + } + } + + /// Returns Some((palette index, above background)) + fn pixel(&mut self, pixel: usize, _line: usize) -> Option<(u8, bool, bool)> { + self.sprite_output_units + .iter() + .enumerate() + .filter_map(|(i, s)| { + if pixel >= s.x_pos as usize && pixel < s.x_pos as usize + 8 && s.x_pos < 0xFF { + let bit_pos = pixel - s.x_pos as usize; + let bit_pos = if s.attrs.hflip() { + bit_pos + } else { + 7 - bit_pos + }; + // println!("Shifting: 0b{:08b} by {}", s.high, 8 - bit_pos); + let hi = (s.high >> bit_pos) & 1; + // println!("Shifting: 0b{:08b} by {}", s.low, bit_pos); + let lo = (s.low >> bit_pos) & 1; + let idx = (hi << 1) | lo; + Some(( + if idx != 0 { + idx + s.attrs.palette() * 4 + 0x10 + } else { + 0 + }, + !s.attrs.priority(), + i == 0, + )) + } else { + None + } + }) + .next() + } } #[derive(Debug, Clone)] @@ -463,9 +657,9 @@ pub struct Palette { } impl Palette { - pub fn color(&self, idx: u8, palette: u8) -> Color { + pub fn color(&self, idx: u8) -> Color { debug_assert!(idx < 0x20, "Palette index out of range"); - self.colors[(self.ram[idx as usize + palette as usize * 4] & 0x3F) as usize] + self.colors[(self.ram[idx as usize] & 0x3F) as usize] } pub fn ram(&self, offset: u8) -> u8 { @@ -487,6 +681,7 @@ pub struct PPU { pub mask: Mask, pub vblank: bool, + sprite_zero_hit: bool, pub palette: Palette, pub background: Background, @@ -542,6 +737,7 @@ impl PPU { ], }, vblank: false, + sprite_zero_hit: false, frame_count: 0, nmi_enabled: false, // nmi_waiting: false, @@ -566,11 +762,7 @@ impl PPU { next_attr: 0, next_attr_2: 0, }, - oam: OAM { - mem: vec![0u8; 256], - addr: 0, - edit_ver: 0, - }, + oam: OAM::new(), vram_buffer: 0, } } @@ -587,7 +779,9 @@ impl PPU { 0 => panic!("ppuctrl is write-only"), 1 => panic!("ppumask is write-only"), 2 => { - let tmp = if self.vblank { 0b1000_0000 } else { 0 }; + let tmp = if self.vblank { 0b1000_0000 } else { 0 } + | if self.sprite_zero_hit { 0b0100_0000 } else { 0 } + | if self.oam.overflow { 0b0010_0000 } else { 0 }; self.vblank = false; self.background.w = false; tmp @@ -605,10 +799,11 @@ impl PPU { let val = self.vram_buffer; self.vram_buffer = v; val - }, - Value::Register { reg: PPUMMRegisters::Palette, offset } => { - self.palette.ram[offset as usize] - }, + } + Value::Register { + reg: PPUMMRegisters::Palette, + offset, + } => self.palette.ram[offset as usize], }; // .reg_map(|a, off| match a { // PPUMMRegisters::Palette => self.palette.ram[off as usize], @@ -626,9 +821,10 @@ impl PPU { 0x00 => { self.nmi_enabled = val & 0b1000_0000 != 0; self.background.t = - (self.background.t & 0b0001_1000_0000_0000) | (((val & 0b11) as u16) << 10); + (self.background.t & !0b0001_1000_0000_0000) | (((val & 0b11) as u16) << 10); self.background.vram_column = val & 0b0000_0100 != 0; self.background.second_pattern = val & 0b0001_0000 != 0; + self.oam.sprite_offset_0x1000 = val & 0b0000_1000 != 0; // TODO: other control fields } 0x01 => { @@ -752,31 +948,54 @@ impl PPU { self.background.cur_shift_high <<= 1; self.background.cur_shift_low <<= 1; } + if self.scanline != 261 { + self.oam.ppu_cycle(self.pixel, self.scanline, mem); + } // TODO if self.pixel == 0 { + if self.scanline < 9 { + // dbg!((self.scanline, &self.oam.sprite_output_units[0])); + // dbg!(&self.oam.secondary); + } // self.dbg_int = true; // idle cycle } else if self.pixel < 257 || self.pixel > 320 { // self.dbg_int = true; - const POS: u32 = 1 << 15; + // const POS: u32 = 1 << 15; + let bit_pos = 15 - self.background.x; + // let pos: u32 = 1 << (15 + self.background.x*0); // TODO: handle this correctly // Determine background color - let a = self.background.cur_shift_high & POS; - let b = self.background.cur_shift_low & POS; - let val = (a >> 14) | (b >> 15); + let a = (self.background.cur_shift_high >> bit_pos) & 1; + let b = (self.background.cur_shift_low >> bit_pos) & 1; + let val = (a << 1) | b; debug_assert!(val < 4); let h_off = ((self.pixel - 1) / 16) % 2; let v_off = (self.scanline / 16) % 2; let off = v_off * 4 + h_off * 2; let palette = (self.background.cur_attr >> off) & 0x3; + let color = val as u8 + if val != 0 { palette * 4 } else { 0 }; if self.scanline < 240 && self.pixel < 257 { + let color = if let Some((sp_color, above, is_sprite_zero)) = + self.oam.pixel(self.pixel, self.scanline) + { + if is_sprite_zero && sp_color != 0 && color != 0 { + self.sprite_zero_hit = true; + } + if sp_color == 0 || (color != 0 && !above) { + color + } else { + sp_color + } + } else { + color + }; // Write to screen self.render_buffer.write( self.scanline, self.pixel - 1, - self.palette - .color(val as u8, if val != 0 { palette } else { 0 }), // self.palette.colors[val as usize], + self.palette.color(color), // self.palette.colors[val as usize], ); // TODO: this should come from shift registers } if self.pixel < 337 { @@ -868,7 +1087,8 @@ impl PPU { } if self.scanline == 261 && self.pixel == 1 { self.vblank = false; - // TODO: clear sprite 0 & sprite overflow + self.sprite_zero_hit = false; + self.oam.overflow = false; } if self.scanline == 241 && self.pixel == 1 { self.vblank = true; @@ -930,10 +1150,10 @@ impl PPU { ), Size::new(1., 1.), match (low & (1 << bit) != 0, high & (1 << bit) != 0) { - (false, false) => self.palette.color(0, 0), - (true, false) => self.palette.color(1, palette), - (false, true) => self.palette.color(2, palette), - (true, true) => self.palette.color(3, palette), + (false, false) => self.palette.color(0), + (true, false) => self.palette.color(1 + 4 * palette), + (false, true) => self.palette.color(2 + 4 * palette), + (true, true) => self.palette.color(3 + 4 * palette), // (false, false) => Color { r: 0, g: 0, b: 0 }, // (true, false) => Color { // r: 64, diff --git a/src/test_roms/input_test.s b/src/test_roms/input_test.s new file mode 100644 index 0000000..513a22c --- /dev/null +++ b/src/test_roms/input_test.s @@ -0,0 +1,335 @@ + +; Verifies that reset doesn't alter any RAM. + +.include "testing.s" +; patterns_bin "pat.bin" +.macro chr b,s +.repeat s +.byte b +.endrepeat +.endmacro +.pushseg +.segment "CHARS" +T_EMPTY = 0 +.repeat 2 ; Empty 0 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.endrepeat + +T_TOP_LEFT = 1 +.repeat 2 ; Top Left 1 +.byte %11111111 +.byte %11111111 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.endrepeat +T_TOP_RIGHT = 2 +.repeat 2 ; Top Right 2 +.byte %11111111 +.byte %11111111 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.endrepeat +T_BOTTOM_RIGHT = 3 +.repeat 2 ; Bottom Right 3 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %11111111 +.byte %11111111 +.endrepeat +T_BOTTOM_LEFT = 4 +.repeat 2 ; Bottom Left 4 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11111111 +.byte %11111111 +.endrepeat +T_FORWARD = 5 +.repeat 2 ; FW 5 +.byte %00000011 +.byte %00000111 +.byte %00001110 +.byte %00011100 +.byte %00111000 +.byte %01110000 +.byte %11100000 +.byte %11000000 +.endrepeat +T_BACKWARD = 6 +.repeat 2 ; BW 6 +.byte %11000000 +.byte %11100000 +.byte %01110000 +.byte %00111000 +.byte %00011100 +.byte %00001110 +.byte %00000111 +.byte %00000011 +.endrepeat + +T_PILL_L = 7 +.repeat 2 ; Pill L 7 +.byte %00111111 +.byte %01111111 +.byte %11100000 +.byte %11000000 +.byte %11000000 +.byte %11100000 +.byte %01111111 +.byte %00111111 +.endrepeat +T_PILL_R = 8 +.repeat 2 ; Pill R 8 +.byte %11111100 +.byte %11111110 +.byte %00000111 +.byte %00000011 +.byte %00000011 +.byte %00000111 +.byte %11111110 +.byte %11111100 +.endrepeat + +.popseg + +zp_res TEMP + +.macro load_ppu_addr addr + lda #.hibyte(addr) + sta PPUADDR + lda #.lobyte(addr) + sta PPUADDR ; Load $2000 +.endmacro + +reset: + sei + cld + ldx #$FF + txs + + ; Init PPU + bit PPUSTATUS +vwait1: + bit PPUSTATUS + bpl vwait1 +vwait2: + bit PPUSTATUS + bpl vwait2 + + lda #$00 + sta PPUCTRL ; NMI off, PPU slave, Small sprites, 0 addresses + + ; Fill NT0 + load_ppu_addr $2000 + ldx #$00 + ldy #$04 + lda #$00 +fill_loop: + sta PPUDATA + dex + bne fill_loop + dey + bne fill_loop + ; Set specific tiles + load_ppu_addr $2184 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_TOP_RIGHT + sta PPUDATA + load_ppu_addr $21A4 + lda #T_BACKWARD + sta PPUDATA + lda #T_FORWARD + sta PPUDATA + + load_ppu_addr $21C2 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_BACKWARD + sta PPUDATA + load_ppu_addr $21E2 + lda #T_BOTTOM_LEFT + sta PPUDATA + lda #T_FORWARD + sta PPUDATA + + load_ppu_addr $21C6 + lda #T_FORWARD + sta PPUDATA + lda #T_TOP_RIGHT + sta PPUDATA + load_ppu_addr $21E6 + lda #T_BACKWARD + sta PPUDATA + lda #T_BOTTOM_RIGHT + sta PPUDATA + + load_ppu_addr $2204 + lda #T_FORWARD + sta PPUDATA + lda #T_BACKWARD + sta PPUDATA + load_ppu_addr $2224 + lda #T_BOTTOM_LEFT + sta PPUDATA + lda #T_BOTTOM_RIGHT + sta PPUDATA + + load_ppu_addr $21EA + lda #T_PILL_L + sta PPUDATA + lda #T_PILL_R + sta PPUDATA + lda #$00 + sta PPUDATA + sta PPUDATA + + lda #T_PILL_L + sta PPUDATA + lda #T_PILL_R + sta PPUDATA + + load_ppu_addr $21D2 + lda #T_FORWARD + sta PPUDATA + lda #T_BACKWARD + sta PPUDATA + load_ppu_addr $21F2 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_TOP_RIGHT + sta PPUDATA + + load_ppu_addr $21D6 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_PILL_R + sta PPUDATA + load_ppu_addr $21F6 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_PILL_R + sta PPUDATA + + load_ppu_addr $3F00 + lda #$0f + sta PPUDATA + lda #$20 + sta PPUDATA + sta PPUDATA + sta PPUDATA + lda #$0f + sta PPUDATA + lda #$09 + sta PPUDATA + sta PPUDATA + sta PPUDATA + + lda #$00 + sta PPUSCROLL + sta PPUSCROLL + lda #%00001110 + sta PPUMASK + lda #$80 + sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses +loop: + jmp loop + +.macro shl_a count +.scope + ldx #count + a_l:asl A + dex + bne a_l +.endscope +.endmacro + +nmi: + lda PPUSTATUS + lda #%00000110 + sta PPUMASK ; Force blank + lda #$01 + sta JOY1 + lda #$00 + sta JOY1 + + load_ppu_addr $23DC ; A + lda JOY1 + and #$01 + shl_a 6 + sta PPUDATA + + load_ppu_addr $23DD ; B + lda JOY1 + and #$01 + shl_a 6 + sta PPUDATA + + load_ppu_addr $23DA ; Select + lda JOY1 + and #$01 + shl_a 6 + sta PPUDATA + + load_ppu_addr $23DB ; Start + lda JOY1 + and #$01 + shl_a 6 + sta PPUDATA + + lda JOY1 ; Up + and #$01 + sta TEMP + + load_ppu_addr $23E1 ; Down + lda JOY1 + and #$01 + sta PPUDATA + + load_ppu_addr $23D8 ; Left + lda JOY1 + and #$01 + shl_a 6 + sta PPUDATA + + load_ppu_addr $23D9 ; Right + lda JOY1 + and #$01 + shl_a 6 + ora TEMP + sta PPUDATA + + lda #$00 + sta PPUSCROLL + sta PPUSCROLL + + lda #%00001110 + sta PPUMASK ; Enable rendering + rti +exit: + stp + +irq: + stp diff --git a/src/test_roms/mod.rs b/src/test_roms/mod.rs index cd34c71..d2e6d48 100644 --- a/src/test_roms/mod.rs +++ b/src/test_roms/mod.rs @@ -36,6 +36,8 @@ macro_rules! rom_test { pub(crate) use rom_test; +use crate::{Break, NES}; + rom_test!(basic_cpu, "basic-cpu.nes", |nes| { assert_eq!(nes.last_instruction(), "0x8001 HLT :2 []"); assert_eq!(nes.cpu_cycle(), 10); @@ -136,6 +138,67 @@ rom_test!(even_odd, "even_odd.nes", |nes| { assert_eq!(nes.cpu.y, 0x00); }); +fn run_frame(nes: &mut NES) { + nes.run_one_clock_cycle(&Break::default()); + while nes.ppu().scanline != 241 || nes.ppu().pixel != 0 { + nes.run_one_clock_cycle(&Break::default()); + } +} + +rom_test!(input_test, "input_test.nes", timeout = 86964*3, |nes| { + const A: u16 = 0x23DC; // 0 || 0b01000000 + const B: u16 = 0x23DD; // 0 || 0b01000000 + const SELECT: u16 = 0x23DA; // 0 || 0b01000000 + const START: u16 = 0x23DB; // 0 || 0b01000000 + const DOWN: u16 = 0x23E1; // 0 || 1 + const LEFT: u16 = 0x23D8; // 0 || 0b01000000 + const UP_RIGHT: u16 = 0x23D9; // 0 || 0b01000000 | 1 + run_frame(&mut nes); + assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0); + nes.controller_1().set_a(true); + run_frame(&mut nes); + assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0x40); + assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0); + nes.controller_1().set_b(true); + run_frame(&mut nes); + assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0x40); + assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0x40); + assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0); + nes.controller_1().0 = 0xFF; + run_frame(&mut nes); + assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0x40); + assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0x40); + assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0x40); + assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0x40); + assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0x01); + assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0x40); + assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0x41); + nes.controller_1().0 = 0; + run_frame(&mut nes); + assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0); + assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0); +}); + // rom_test!(even_odd, "even_odd.nes", |nes| { // assert_eq!(nes.last_instruction(), "0x8023 HLT :2 []"); // assert_eq!(nes.cpu_cycle(), 57182); diff --git a/src/test_roms/ppu.rs b/src/test_roms/ppu.rs index ef7ecd0..b8b53c8 100644 --- a/src/test_roms/ppu.rs +++ b/src/test_roms/ppu.rs @@ -94,3 +94,8 @@ rom_test!(ppu_palette_shared, "ppu_palette_shared.nes", |nes| { }); + +// Sets up an image, and scrolls a specific number of pixels over +rom_test!(ppu_scrolling, "ppu_fine_x_scrolling.nes", timeout = 86964*4, |nes| { + assert_eq!(nes.image().read(0, 0), Color { r: 0xFF, g: 0xFE, b: 0xFF }); +}); diff --git a/src/test_roms/ppu_fine_x_scrolling.s b/src/test_roms/ppu_fine_x_scrolling.s new file mode 100644 index 0000000..1e4c35a --- /dev/null +++ b/src/test_roms/ppu_fine_x_scrolling.s @@ -0,0 +1,306 @@ +.include "testing.s" +; patterns_bin "pat.bin" +.macro chr b,s +.repeat s +.byte b +.endrepeat +.endmacro +.pushseg +.segment "CHARS" +T_EMPTY = 0 +.repeat 2 ; Empty 0 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.endrepeat + +T_TOP_LEFT = 1 +.repeat 2 ; Top Left 1 +.byte %11111111 +.byte %11111111 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.endrepeat +T_TOP_RIGHT = 2 +.repeat 2 ; Top Right 2 +.byte %11111111 +.byte %11111111 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.endrepeat +T_BOTTOM_RIGHT = 3 +.repeat 2 ; Bottom Right 3 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %11111111 +.byte %11111111 +.endrepeat +T_BOTTOM_LEFT = 4 +.repeat 2 ; Bottom Left 4 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11111111 +.byte %11111111 +.endrepeat +T_FORWARD = 5 +.repeat 2 ; FW 5 +.byte %00000011 +.byte %00000111 +.byte %00001110 +.byte %00011100 +.byte %00111000 +.byte %01110000 +.byte %11100000 +.byte %11000000 +.endrepeat +T_BACKWARD = 6 +.repeat 2 ; BW 6 +.byte %11000000 +.byte %11100000 +.byte %01110000 +.byte %00111000 +.byte %00011100 +.byte %00001110 +.byte %00000111 +.byte %00000011 +.endrepeat + +T_PILL_L = 7 +.repeat 2 ; Pill L 7 +.byte %00111111 +.byte %01111111 +.byte %11100000 +.byte %11000000 +.byte %11000000 +.byte %11100000 +.byte %01111111 +.byte %00111111 +.endrepeat +T_PILL_R = 8 +.repeat 2 ; Pill R 8 +.byte %11111100 +.byte %11111110 +.byte %00000111 +.byte %00000011 +.byte %00000011 +.byte %00000111 +.byte %11111110 +.byte %11111100 +.endrepeat + +T_LINE = 9 +.repeat 2 ; Empty 0 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.endrepeat + +.popseg + +zp_res Y_SCROLL +zp_res X_SCROLL +zp_res CTRL_BYTE + +.macro load_ppu_addr addr + lda #.hibyte(addr) + sta PPUADDR + lda #.lobyte(addr) + sta PPUADDR ; Load $2000 +.endmacro + +reset: + sei + cld + ldx #$FF + txs + + ; Init PPU + bit PPUSTATUS +vwait1: + bit PPUSTATUS + bpl vwait1 +vwait2: + bit PPUSTATUS + bpl vwait2 + + lda #$00 + sta PPUCTRL ; NMI off, PPU slave, Small sprites, 0 addresses + + ; Fill NT0 + load_ppu_addr $2000 + ldx #$00 + ldy #$04 + lda #$00 +fill_loop: + sta PPUDATA + dex + bne fill_loop + dey + bne fill_loop + ; Set specific tiles + load_ppu_addr $2184 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_TOP_RIGHT + sta PPUDATA + load_ppu_addr $21A4 + lda #T_BACKWARD + sta PPUDATA + lda #T_FORWARD + sta PPUDATA + + load_ppu_addr $21C2 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_BACKWARD + sta PPUDATA + load_ppu_addr $21E2 + lda #T_BOTTOM_LEFT + sta PPUDATA + lda #T_FORWARD + sta PPUDATA + + load_ppu_addr $21C6 + lda #T_FORWARD + sta PPUDATA + lda #T_TOP_RIGHT + sta PPUDATA + load_ppu_addr $21E6 + lda #T_BACKWARD + sta PPUDATA + lda #T_BOTTOM_RIGHT + sta PPUDATA + + load_ppu_addr $2204 + lda #T_FORWARD + sta PPUDATA + lda #T_BACKWARD + sta PPUDATA + load_ppu_addr $2224 + lda #T_BOTTOM_LEFT + sta PPUDATA + lda #T_BOTTOM_RIGHT + sta PPUDATA + + load_ppu_addr $21EA + lda #T_PILL_L + sta PPUDATA + lda #T_PILL_R + sta PPUDATA + lda #$00 + sta PPUDATA + sta PPUDATA + + lda #T_PILL_L + sta PPUDATA + lda #T_PILL_R + sta PPUDATA + + load_ppu_addr $21D2 + lda #T_FORWARD + sta PPUDATA + lda #T_BACKWARD + sta PPUDATA + load_ppu_addr $21F2 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_TOP_RIGHT + sta PPUDATA + + load_ppu_addr $21D6 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_PILL_R + sta PPUDATA + load_ppu_addr $21F6 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_PILL_R + sta PPUDATA + + load_ppu_addr $3F00 + lda #$0f + sta PPUDATA + lda #$20 + sta PPUDATA + sta PPUDATA + sta PPUDATA + lda #$0f + sta PPUDATA + lda #$09 + sta PPUDATA + sta PPUDATA + sta PPUDATA + + load_ppu_addr $2001 + lda #T_LINE + sta PPUDATA + + lda #$00 + sta PPUSCROLL + sta PPUSCROLL + lda #%00001110 + sta PPUMASK + lda #$80 + sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses + sta CTRL_BYTE +loop: + jmp loop + +nmi: + lda PPUSTATUS + lda #%00000110 + sta PPUMASK ; Force blank + + lda #$0A + ; adc X_SCROLL + ; sta X_SCROLL + sta PPUSCROLL +; bcc y_scroll +; lda #$01 +; eor CTRL_BYTE +; sta CTRL_BYTE +; sta PPUCTRL +; y_scroll: +; clc + lda #$00 + ; adc Y_SCROLL + ; sta Y_SCROLL + sta PPUSCROLL + + lda CTRL_BYTE + sta PPUCTRL + lda #%00001110 + sta PPUMASK ; Enable rendering + rti +exit: + stp + +irq: + stp diff --git a/src/test_roms/scrolling.s b/src/test_roms/scrolling.s new file mode 100644 index 0000000..aa3b546 --- /dev/null +++ b/src/test_roms/scrolling.s @@ -0,0 +1,306 @@ +.include "testing.s" +; patterns_bin "pat.bin" +.macro chr b,s +.repeat s +.byte b +.endrepeat +.endmacro +.pushseg +.segment "CHARS" +T_EMPTY = 0 +.repeat 2 ; Empty 0 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.endrepeat + +T_TOP_LEFT = 1 +.repeat 2 ; Top Left 1 +.byte %11111111 +.byte %11111111 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.endrepeat +T_TOP_RIGHT = 2 +.repeat 2 ; Top Right 2 +.byte %11111111 +.byte %11111111 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.endrepeat +T_BOTTOM_RIGHT = 3 +.repeat 2 ; Bottom Right 3 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %11111111 +.byte %11111111 +.endrepeat +T_BOTTOM_LEFT = 4 +.repeat 2 ; Bottom Left 4 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11111111 +.byte %11111111 +.endrepeat +T_FORWARD = 5 +.repeat 2 ; FW 5 +.byte %00000011 +.byte %00000111 +.byte %00001110 +.byte %00011100 +.byte %00111000 +.byte %01110000 +.byte %11100000 +.byte %11000000 +.endrepeat +T_BACKWARD = 6 +.repeat 2 ; BW 6 +.byte %11000000 +.byte %11100000 +.byte %01110000 +.byte %00111000 +.byte %00011100 +.byte %00001110 +.byte %00000111 +.byte %00000011 +.endrepeat + +T_PILL_L = 7 +.repeat 2 ; Pill L 7 +.byte %00111111 +.byte %01111111 +.byte %11100000 +.byte %11000000 +.byte %11000000 +.byte %11100000 +.byte %01111111 +.byte %00111111 +.endrepeat +T_PILL_R = 8 +.repeat 2 ; Pill R 8 +.byte %11111100 +.byte %11111110 +.byte %00000111 +.byte %00000011 +.byte %00000011 +.byte %00000111 +.byte %11111110 +.byte %11111100 +.endrepeat + +T_LINE = 9 +.repeat 2 ; Empty 0 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.endrepeat + +.popseg + +zp_res Y_SCROLL +zp_res X_SCROLL +zp_res CTRL_BYTE + +.macro load_ppu_addr addr + lda #.hibyte(addr) + sta PPUADDR + lda #.lobyte(addr) + sta PPUADDR ; Load $2000 +.endmacro + +reset: + sei + cld + ldx #$FF + txs + + ; Init PPU + bit PPUSTATUS +vwait1: + bit PPUSTATUS + bpl vwait1 +vwait2: + bit PPUSTATUS + bpl vwait2 + + lda #$00 + sta PPUCTRL ; NMI off, PPU slave, Small sprites, 0 addresses + + ; Fill NT0 + load_ppu_addr $2000 + ldx #$00 + ldy #$04 + lda #$00 +fill_loop: + sta PPUDATA + dex + bne fill_loop + dey + bne fill_loop + ; Set specific tiles + load_ppu_addr $2184 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_TOP_RIGHT + sta PPUDATA + load_ppu_addr $21A4 + lda #T_BACKWARD + sta PPUDATA + lda #T_FORWARD + sta PPUDATA + + load_ppu_addr $21C2 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_BACKWARD + sta PPUDATA + load_ppu_addr $21E2 + lda #T_BOTTOM_LEFT + sta PPUDATA + lda #T_FORWARD + sta PPUDATA + + load_ppu_addr $21C6 + lda #T_FORWARD + sta PPUDATA + lda #T_TOP_RIGHT + sta PPUDATA + load_ppu_addr $21E6 + lda #T_BACKWARD + sta PPUDATA + lda #T_BOTTOM_RIGHT + sta PPUDATA + + load_ppu_addr $2204 + lda #T_FORWARD + sta PPUDATA + lda #T_BACKWARD + sta PPUDATA + load_ppu_addr $2224 + lda #T_BOTTOM_LEFT + sta PPUDATA + lda #T_BOTTOM_RIGHT + sta PPUDATA + + load_ppu_addr $21EA + lda #T_PILL_L + sta PPUDATA + lda #T_PILL_R + sta PPUDATA + lda #$00 + sta PPUDATA + sta PPUDATA + + lda #T_PILL_L + sta PPUDATA + lda #T_PILL_R + sta PPUDATA + + load_ppu_addr $21D2 + lda #T_FORWARD + sta PPUDATA + lda #T_BACKWARD + sta PPUDATA + load_ppu_addr $21F2 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_TOP_RIGHT + sta PPUDATA + + load_ppu_addr $21D6 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_PILL_R + sta PPUDATA + load_ppu_addr $21F6 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_PILL_R + sta PPUDATA + + load_ppu_addr $3F00 + lda #$0f + sta PPUDATA + lda #$20 + sta PPUDATA + sta PPUDATA + sta PPUDATA + lda #$0f + sta PPUDATA + lda #$09 + sta PPUDATA + sta PPUDATA + sta PPUDATA + + load_ppu_addr $2001 + lda #T_LINE + sta PPUDATA + + lda #$00 + sta PPUSCROLL + sta PPUSCROLL + lda #%00001110 + sta PPUMASK + lda #$80 + sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses + sta CTRL_BYTE +loop: + jmp loop + +nmi: + lda PPUSTATUS + lda #%00000110 + sta PPUMASK ; Force blank + + lda #$0A + adc X_SCROLL + sta X_SCROLL + sta PPUSCROLL + bcc y_scroll + lda #$01 + eor CTRL_BYTE + sta CTRL_BYTE + sta PPUCTRL +y_scroll: + clc + lda #$00 + adc Y_SCROLL + sta Y_SCROLL + sta PPUSCROLL + + lda CTRL_BYTE + sta PPUCTRL + lda #%00001110 + sta PPUMASK ; Enable rendering + rti +exit: + stp + +irq: + stp diff --git a/src/test_roms/sprites.s b/src/test_roms/sprites.s new file mode 100644 index 0000000..c3ccae5 --- /dev/null +++ b/src/test_roms/sprites.s @@ -0,0 +1,321 @@ +.include "testing.s" + +.pushseg +.segment "CHARS" +T_EMPTY = 0 +.repeat 2 ; Empty 0 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.byte %00000000 +.endrepeat + +T_TOP_LEFT = 1 +.repeat 2 ; Top Left 1 +.byte %11111111 +.byte %11111111 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.endrepeat +T_TOP_RIGHT = 2 +.repeat 2 ; Top Right 2 +.byte %11111111 +.byte %11111111 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.endrepeat +T_BOTTOM_RIGHT = 3 +.repeat 2 ; Bottom Right 3 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %00000011 +.byte %11111111 +.byte %11111111 +.endrepeat +T_BOTTOM_LEFT = 4 +.repeat 2 ; Bottom Left 4 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11000000 +.byte %11111111 +.byte %11111111 +.endrepeat +T_FORWARD = 5 +.repeat 2 ; FW 5 +.byte %00000011 +.byte %00000111 +.byte %00001110 +.byte %00011100 +.byte %00111000 +.byte %01110000 +.byte %11100000 +.byte %11000000 +.endrepeat +T_BACKWARD = 6 +.repeat 2 ; BW 6 +.byte %11000000 +.byte %11100000 +.byte %01110000 +.byte %00111000 +.byte %00011100 +.byte %00001110 +.byte %00000111 +.byte %00000011 +.endrepeat + +T_PILL_L = 7 +.repeat 2 ; Pill L 7 +.byte %00111111 +.byte %01111111 +.byte %11100000 +.byte %11000000 +.byte %11000000 +.byte %11100000 +.byte %01111111 +.byte %00111111 +.endrepeat +T_PILL_R = 8 +.repeat 2 ; Pill R 8 +.byte %11111100 +.byte %11111110 +.byte %00000111 +.byte %00000011 +.byte %00000011 +.byte %00000111 +.byte %11111110 +.byte %11111100 +.endrepeat + +T_LINE = 9 +.repeat 2 ; Empty 0 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.byte %00100000 +.endrepeat + +.popseg + +zp_res Y_SCROLL +zp_res X_SCROLL +zp_res CTRL_BYTE + +.macro load_ppu_addr addr + lda #.hibyte(addr) + sta PPUADDR + lda #.lobyte(addr) + sta PPUADDR ; Load $2000 +.endmacro + +reset: + sei + cld + ldx #$FF + txs + + ; Init PPU + bit PPUSTATUS +vwait1: + bit PPUSTATUS + bpl vwait1 +vwait2: + bit PPUSTATUS + bpl vwait2 + + lda #$00 + sta PPUCTRL ; NMI off, PPU slave, Small sprites, 0 addresses + + ; Fill NT0 + load_ppu_addr $2000 + ldx #$00 + ldy #$04 + lda #$00 +fill_loop: + sta PPUDATA + dex + bne fill_loop + dey + bne fill_loop + ; Set specific tiles + load_ppu_addr $2184 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_TOP_RIGHT + sta PPUDATA + load_ppu_addr $21A4 + lda #T_BACKWARD + sta PPUDATA + lda #T_FORWARD + sta PPUDATA + + load_ppu_addr $21C2 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_BACKWARD + sta PPUDATA + load_ppu_addr $21E2 + lda #T_BOTTOM_LEFT + sta PPUDATA + lda #T_FORWARD + sta PPUDATA + + load_ppu_addr $21C6 + lda #T_FORWARD + sta PPUDATA + lda #T_TOP_RIGHT + sta PPUDATA + load_ppu_addr $21E6 + lda #T_BACKWARD + sta PPUDATA + lda #T_BOTTOM_RIGHT + sta PPUDATA + + load_ppu_addr $2204 + lda #T_FORWARD + sta PPUDATA + lda #T_BACKWARD + sta PPUDATA + load_ppu_addr $2224 + lda #T_BOTTOM_LEFT + sta PPUDATA + lda #T_BOTTOM_RIGHT + sta PPUDATA + + load_ppu_addr $21EA + lda #T_PILL_L + sta PPUDATA + lda #T_PILL_R + sta PPUDATA + lda #$00 + sta PPUDATA + sta PPUDATA + + lda #T_PILL_L + sta PPUDATA + lda #T_PILL_R + sta PPUDATA + + load_ppu_addr $21D2 + lda #T_FORWARD + sta PPUDATA + lda #T_BACKWARD + sta PPUDATA + load_ppu_addr $21F2 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_TOP_RIGHT + sta PPUDATA + + load_ppu_addr $21D6 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_PILL_R + sta PPUDATA + load_ppu_addr $21F6 + lda #T_TOP_LEFT + sta PPUDATA + lda #T_PILL_R + sta PPUDATA + + load_ppu_addr $3F00 + lda #$0f + sta PPUDATA + lda #$20 + sta PPUDATA + sta PPUDATA + sta PPUDATA + lda #$0f + sta PPUDATA + lda #$09 + sta PPUDATA + sta PPUDATA + sta PPUDATA + + load_ppu_addr $2001 + lda #T_LINE + sta PPUDATA + + ldx #$00 +sprite_loop: + lda sprite_data,x + sta $300,x + inx + cpx #(sprite_data_end - sprite_data) + bne sprite_loop + + lda #$00 + sta PPUSCROLL + sta PPUSCROLL + lda #%00001110 + sta PPUMASK + lda #$80 + sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses + sta CTRL_BYTE +loop: + jmp loop + +nmi: + lda PPUSTATUS + lda #%00000110 + sta PPUMASK ; Force blank + + lda #$01 + adc X_SCROLL + sta X_SCROLL + sta PPUSCROLL + bcc y_scroll + lda #$01 + eor CTRL_BYTE + sta CTRL_BYTE + sta PPUCTRL +y_scroll: + clc + lda #$00 + adc Y_SCROLL + sta Y_SCROLL + sta PPUSCROLL + + lda #$00 + sta SPRADDR + lda #$03 + sta SPRDMA + + lda CTRL_BYTE + sta PPUCTRL + lda #%00011110 + sta PPUMASK ; Enable rendering + rti +exit: + stp + +irq: + stp + +sprite_data: + .byte $00 + .byte T_PILL_R + .byte $00 + .byte $00 +sprite_data_end: