From 4cbfb5635e3be55de0cd6ac9b699bf2bdb4259e4 Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 26 Jul 2023 19:57:39 +0200 Subject: [PATCH 1/2] Add a fast mode (WIP) --- .vscode/settings.json | 4 +- 3ds/src/main.rs | 44 +-- Cargo.lock | 119 --------- src/nes.rs | 8 +- src/ppu/mod.rs | 580 ++++++++++++++++++++++++++++++++++++---- src/ppu/registers.rs | 41 +++ web/src/lib.rs | 10 +- web/ui/src/main.ts | 22 +- web/ui/src/ui/screen.ts | 8 +- web/ui/src/ui/ui.ts | 4 +- web/ui/src/webgl.ts | 4 +- 11 files changed, 613 insertions(+), 231 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e156b8a..2d9e3ef 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,5 @@ { "rust-analyzer.linkedProjects": [ - "./crate/Cargo.toml", - "./crate/Cargo.toml", - "./crate/Cargo.toml" + "Cargo.toml", ] } \ No newline at end of file diff --git a/3ds/src/main.rs b/3ds/src/main.rs index 14ce41b..8d147be 100644 --- a/3ds/src/main.rs +++ b/3ds/src/main.rs @@ -14,14 +14,13 @@ const TOP_SCREEN_HEIGHT: usize = 240; // p const BOTTOM_SCREEN_WIDTH: usize = 320; // px const BOTTOM_SCREEN_HEIGHT: usize = 240; // px const SAMPLE_RATE: f64 = 44_100.0; // Hz -const ROM_BYTES: &[u8] = include_bytes!("../assets/Kirby's Adventure.nes"); +const ROM_BYTES: &[u8] = include_bytes!("../assets/Super Mario Bros.nes"); const LEFT_X_OFFSET_TOP: usize = (TOP_SCREEN_WIDTH - NES_SCREEN_WIDTH) / 2; const LEFT_X_OFFSET_BOTTOM: usize = (BOTTOM_SCREEN_WIDTH - NES_SCREEN_WIDTH) / 2; -static LOGO: &[u8] = include_bytes!("../assets/logo.rgb"); #[inline] fn is_key_active(hid: &Hid, key: KeyPad) -> bool { - hid.keys_down().contains(key) || hid.keys_held().contains(key) + hid.keys_held().contains(key) } fn main() { @@ -31,27 +30,18 @@ fn main() { let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let apt = Apt::new().expect("Couldn't obtain APT controller"); - // let _console = Console::new(gfx.bottom_screen.borrow_mut()); + let _console = Console::new(gfx.bottom_screen.borrow_mut()); - // println!("\x1b[0;3HPress L + R to exit."); + println!("\x1b[0;3HPress L + R to exit."); let mut top_screen = gfx.top_screen.borrow_mut(); - let mut bottom_screen = gfx.bottom_screen.borrow_mut(); - bottom_screen.set_double_buffering(false); - bottom_screen.swap_buffers(); - - // We don't need double buffering in this example. - // In this way we can draw our image only once on screen. top_screen.set_double_buffering(false); + top_screen.swap_buffers(); let rom = ROM::new(ROM_BYTES.to_vec()).expect("Couldn't load ROM"); let mut nes = Nes::new(rom, SAMPLE_RATE); let mut nes_frame_buffer = [0u8; FRAME_BUFFER_BYTE_SIZE]; let mut top_frame_buffer = [0u8; TOP_SCREEN_WIDTH * TOP_SCREEN_HEIGHT * 3]; - let mut bottom_frame_buffer = [0u8; BOTTOM_SCREEN_WIDTH * BOTTOM_SCREEN_HEIGHT * 3]; - - let bottom_offset = LEFT_X_OFFSET_BOTTOM * BOTTOM_SCREEN_HEIGHT * 3; - bottom_frame_buffer[bottom_offset..(LOGO.len() + bottom_offset)].copy_from_slice(LOGO); // Main loop while apt.main_loop() { @@ -72,33 +62,35 @@ fn main() { joypad1_state.insert(JoypadStatus::SELECT); } - if is_key_active(&hid, KeyPad::A) { + if is_key_active(&hid, KeyPad::A) || is_key_active(&hid, KeyPad::Y) { joypad1_state.insert(JoypadStatus::A); } - if is_key_active(&hid, KeyPad::B) { + if is_key_active(&hid, KeyPad::B) || is_key_active(&hid, KeyPad::X) { joypad1_state.insert(JoypadStatus::B); } - if is_key_active(&hid, KeyPad::DPAD_UP) { + if is_key_active(&hid, KeyPad::DPAD_UP) || is_key_active(&hid, KeyPad::CPAD_UP) { joypad1_state.insert(JoypadStatus::UP); } - if is_key_active(&hid, KeyPad::DPAD_DOWN) { + if is_key_active(&hid, KeyPad::DPAD_DOWN) || is_key_active(&hid, KeyPad::CPAD_DOWN) { joypad1_state.insert(JoypadStatus::DOWN); } - if is_key_active(&hid, KeyPad::DPAD_LEFT) { + if is_key_active(&hid, KeyPad::DPAD_LEFT) || is_key_active(&hid, KeyPad::CPAD_LEFT) { joypad1_state.insert(JoypadStatus::LEFT); } - if is_key_active(&hid, KeyPad::DPAD_RIGHT) { + if is_key_active(&hid, KeyPad::DPAD_RIGHT) || is_key_active(&hid, KeyPad::CPAD_RIGHT) { joypad1_state.insert(JoypadStatus::RIGHT); } nes.get_joypad1_mut().update(joypad1_state.bits()); - + let t0 = std::time::Instant::now(); nes.next_frame(); + let frame_time = t0.elapsed().as_millis() as usize; + println!("{frame_time}ms"); nes_frame_buffer.copy_from_slice(&nes.get_frame()); // rotate the frame buffer 90 degrees @@ -119,17 +111,9 @@ fn main() { .raw_framebuffer() .ptr .copy_from(top_frame_buffer.as_ptr(), top_frame_buffer.len()); - - bottom_screen - .raw_framebuffer() - .ptr - .copy_from(bottom_frame_buffer.as_ptr(), bottom_frame_buffer.len()); } - // Flush and swap framebuffers top_screen.flush_buffers(); - bottom_screen.flush_buffers(); - // top_screen.swap_buffers(); //Wait for VBlank gfx.wait_for_vblank(); diff --git a/Cargo.lock b/Cargo.lock index 1dfa693..02925b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,28 +17,12 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bumpalo" -version = "3.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" - [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - [[package]] name = "cpufeatures" version = "0.2.9" @@ -84,44 +68,12 @@ version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" -[[package]] -name = "log" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" - [[package]] name = "nessy" version = "0.1.0" dependencies = [ "bitflags", - "console_error_panic_hook", "sha2", - "wasm-bindgen", -] - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "proc-macro2" -version = "1.0.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" -dependencies = [ - "proc-macro2", ] [[package]] @@ -135,85 +87,14 @@ dependencies = [ "digest", ] -[[package]] -name = "syn" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "typenum" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" -[[package]] -name = "unicode-ident" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" - [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wasm-bindgen" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" diff --git a/src/nes.rs b/src/nes.rs index 2dd6c2e..d1d5e45 100644 --- a/src/nes.rs +++ b/src/nes.rs @@ -103,8 +103,8 @@ impl Nes { } #[inline] - pub fn get_frame(&self) -> &[u8] { - self.cpu.bus.ppu.get_frame() + pub fn get_frame(&self, buffer: &mut [u8]) { + self.cpu.bus.ppu.get_frame(buffer); } pub fn save_state(&self) -> SaveState { @@ -113,6 +113,10 @@ impl Nes { state } + pub fn get_updated_tiles_count(&self) -> usize { + self.cpu.bus.ppu.get_updated_tiles_count() + } + pub fn load_state(&mut self, data: &[u8]) -> Result<(), SaveStateError> { let mut state = SaveState::decode(data)?; diff --git a/src/ppu/mod.rs b/src/ppu/mod.rs index 2e90de6..d11c1d1 100644 --- a/src/ppu/mod.rs +++ b/src/ppu/mod.rs @@ -1,15 +1,21 @@ mod registers; -use self::registers::{Ctrl, Registers, SpriteSize, Status}; +use self::registers::{Ctrl, Mask, Registers, SpriteSize, Status}; use crate::{ cpu::rom::{Mirroring, ROM}, savestate::{self, SaveStateError}, }; +const PIXELS_PER_TILE: usize = 8; const BYTES_PER_PALLETE: usize = 4; +const TILES_PER_NAMETABLE_BYTE: usize = 4; +const TILES_PER_NAMETABLE: usize = 32 * 30; +const BYTES_PER_NAMETABLE: usize = 1024; +const ATTRIBUTES_PER_NAMETABLE: usize = 64; const SPRITE_PALETTES_OFFSET: usize = 0x11; const WIDTH: usize = 256; const HEIGHT: usize = 240; +const FAST_MODE: bool = true; #[rustfmt::skip] pub static COLOR_PALETTE: [(u8, u8, u8); 64] = [ @@ -37,14 +43,19 @@ struct SpriteData { behind_background: bool, } +#[derive(Clone, Copy)] +struct CachedTile { + chr: [u8; 64], +} + #[allow(clippy::upper_case_acronyms)] pub struct PPU { pub rom: ROM, regs: Registers, open_bus: u8, - vram: [u8; 2 * 1024], + vram: [u8; 2 * BYTES_PER_NAMETABLE], palette: [u8; 32], - attributes: [u8; 64 * 4], + attributes: [u8; ATTRIBUTES_PER_NAMETABLE * 4], pub cycle: u16, scanline: u16, frame: u64, @@ -60,8 +71,10 @@ pub struct PPU { pattern_table_high_byte: u8, scanline_sprites: [SpriteData; 8], visible_sprites_count: u8, - frame_buffer: [u8; WIDTH * HEIGHT * 3], - frame_buffer_complete: Box<[u8; WIDTH * HEIGHT * 3]>, // avoid stack overflow in WASM + background_buffer: Box<[u8; WIDTH * HEIGHT * 3 * 2]>, // both nametables + frame_buffer_complete: Box<[u8; WIDTH * HEIGHT * 3 * 2]>, // avoid stack overflow in WASM + nt_cache: Box<[Option; BYTES_PER_NAMETABLE * 2]>, + updated_bg_tiles: Box<[bool; BYTES_PER_NAMETABLE * 2]>, } impl PPU { @@ -95,8 +108,10 @@ impl PPU { chr: [0; 8], }; 8], visible_sprites_count: 0, - frame_buffer: [0; WIDTH * HEIGHT * 3], - frame_buffer_complete: Box::new([0; WIDTH * HEIGHT * 3]), + background_buffer: Box::new([0; WIDTH * HEIGHT * 3 * 2]), + frame_buffer_complete: Box::new([0; WIDTH * HEIGHT * 3 * 2]), + nt_cache: Box::new([None; BYTES_PER_NAMETABLE * 2]), + updated_bg_tiles: Box::new([true; BYTES_PER_NAMETABLE * 2]), }; ppu.reset(); @@ -127,6 +142,10 @@ impl PPU { self.cycle += 1; if self.cycle > 340 { + if FAST_MODE && self.is_sprite_zero_hit() { + self.regs.status.insert(Status::SPRITE_ZERO_HIT); + } + self.cycle = 0; self.scanline += 1; @@ -153,50 +172,52 @@ impl PPU { let fetch_cycle = pre_fetch_cycle || visible_cycle; // background logic - if self.regs.show_background() { - if visible_line && visible_cycle { - self.render_pixel(); - } - - if render_line && fetch_cycle { - self.tile_data <<= 4; - - match self.cycle & 7 { - 1 => self.fetch_nametable_byte(), - 3 => self.fetch_attribute_table_byte(), - // 5 => self.fetch_pattern_table_low_byte(), - // 7 => self.fetch_pattern_table_high_byte(), - 7 => self.fetch_pattern_table_bytes(), - 0 => self.store_background_tile_data(), - _ => {} + if !FAST_MODE { + if self.regs.show_background() { + if visible_line && visible_cycle { + self.render_pixel(); } - } - - if preline && self.cycle >= 280 && self.cycle <= 304 { - self.regs.copy_y(); - } - if render_line { - if fetch_cycle && self.cycle & 7 == 0 { - self.regs.increment_x(); + if render_line && fetch_cycle { + self.tile_data <<= 4; + + match self.cycle & 7 { + 1 => self.fetch_nametable_byte(), + 3 => self.fetch_attribute_table_byte(), + // 5 => self.fetch_pattern_table_low_byte(), + // 7 => self.fetch_pattern_table_high_byte(), + 7 => self.fetch_pattern_table_bytes(), + 0 => self.store_background_tile_data(), + _ => {} + } } - if self.cycle == 256 { - self.regs.increment_y(); + if preline && self.cycle >= 280 && self.cycle <= 304 { + self.regs.copy_y(); } - if self.cycle == 257 { - self.regs.copy_x(); + if render_line { + if fetch_cycle && self.cycle & 7 == 0 { + self.regs.increment_x(); + } + + if self.cycle == 256 { + self.regs.increment_y(); + } + + if self.cycle == 257 { + self.regs.copy_x(); + } } } - } - if self.regs.show_sprites() && self.cycle == 257 { - if visible_line { - self.fetch_next_scanline_sprites(); - } else { - // clear secondary OAM - self.visible_sprites_count = 0; + if self.regs.show_sprites() && self.cycle == 257 { + if visible_line { + self.fetch_next_scanline_sprites(); + } else { + // clear secondary OAM + self.visible_sprites_count = 0; + } } } @@ -205,7 +226,12 @@ impl PPU { self.frame_complete = true; self.regs.status.insert(Status::VBLANK_STARTED); self.detect_nmi_edge(); - self.transfer_frame_buffer(); + + if FAST_MODE { + self.render_frame(); + } + + // self.transfer_frame_buffer(); } if preline && self.cycle == 1 { @@ -216,12 +242,363 @@ impl PPU { } } - #[inline] - fn transfer_frame_buffer(&mut self) { + fn render_frame(&mut self) { + // self.frame_buffer1.fill(0); + // self.frame_buffer2.fill(0); + + let base_nametable_addr = self.regs.ctrl.base_nametable_addr(); + let scroll_x = self.regs.scroll.x as usize; + let scroll_y = self.regs.scroll.y as usize; + + let (nametable1, nametable2): (usize, usize) = match self.rom.cart.mirroring { + Mirroring::Vertical => match base_nametable_addr { + 0x2000 | 0x2800 => (0, 0x400), + 0x2400 | 0x2c00 => (0x400, 0), + _ => unreachable!(), + }, + Mirroring::Horizontal => match base_nametable_addr { + 0x2000 | 0x2400 => (0, 0x400), + 0x2800 | 0x2c00 => (0x400, 0), + _ => unreachable!(), + }, + Mirroring::OneScreenLowerBank => (0, 0), + Mirroring::OneScreenUpperBank => (0x400, 0x400), + Mirroring::FourScreen => match base_nametable_addr { + 0x2000 => (0x000, 0x400), + 0x2400 => (0x400, 0x800), + 0x2800 => (0x800, 0xc00), + 0x2c00 => (0xc00, 0x800), + _ => unreachable!(), + }, + }; + + if self.regs.show_background() { + self.render_background(nametable1, 0); + self.render_background(nametable2, WIDTH); + } + + // for i in 0..HEIGHT { + // self.set_pixel(scroll_x, i, (255, 0, 0)); + // self.set_pixel((scroll_x + WIDTH) % (2 * WIDTH), i, (0, 255, 0)); + // } + + // for i in 0..WIDTH { + // self.set_pixel(i, scroll_y, (0, 0, 255)); + // self.set_pixel(i, (scroll_y + HEIGHT) % (2 * HEIGHT), (255, 255, 0)); + // } + + // if scroll_x > 0 { + // self.render_background( + // nametable2, + // BoundingBox { + // x_min: 0, + // x_max: scroll_x, + // y_min: 0, + // y_max: HEIGHT, + // }, + // (WIDTH - scroll_x) as isize, + // 0, + // ); + // } else if scroll_y > 0 { + // self.render_background( + // nametable2, + // BoundingBox { + // x_min: 0, + // x_max: WIDTH, + // y_min: 0, + // y_max: scroll_y, + // }, + // 0, + // (HEIGHT - scroll_y) as isize, + // ); + // } + self.frame_buffer_complete - .copy_from_slice(&self.frame_buffer); + .copy_from_slice(self.background_buffer.as_slice()); + + if self.regs.show_sprites() { + self.render_sprites(scroll_x, scroll_y); + } + } + + fn render_background(&mut self, nt_offset: usize, x_offset: usize) { + let chr_bank_offset = self.regs.ctrl.background_chr_offset(); + + for i in 0..0x03c0 { + let tile_col = i & 31; // i % 32 + let tile_row = i / 32; + + self.render_background_tile( + chr_bank_offset, + nt_offset, + i, + tile_col, + tile_row, + x_offset, + ); + } } + fn get_nametable_tile( + &mut self, + nt_offset: usize, + nth: usize, + chr_bank_offset: u16, + ) -> [u8; 64] { + let tile_index = nt_offset + nth; + + match self.nt_cache[tile_index] { + // Some(tile) => tile.chr, + _ => { + let mut tile = [0u8; 16]; + self.rom + .mapper + .get_tile(&mut self.rom.cart, chr_bank_offset, nth, &mut tile); + + let mut chr = [0u8; 64]; + + for y in 0..PIXELS_PER_TILE { + let mut plane1 = tile[y]; + let mut plane2 = tile[y + 8]; + + for x in (0..PIXELS_PER_TILE).rev() { + let bit0 = plane1 & 1; + let bit1 = plane2 & 1; + let color_idx = (bit1 << 1) | bit0; + + plane1 >>= 1; + plane2 >>= 1; + + chr[y * PIXELS_PER_TILE + x] = color_idx; + } + } + + self.nt_cache[tile_index] = Some(CachedTile { chr }); + chr + } + } + } + + #[allow(clippy::too_many_arguments)] + fn render_background_tile( + &mut self, + chr_bank_offset: u16, + nt_offset: usize, + nth: usize, + tile_col: usize, + tile_row: usize, + x_offset: usize, + ) { + let tile_idx = nt_offset + nth; + + if !self.updated_bg_tiles[tile_idx] { + return; + } + + let tile = + self.get_nametable_tile(nt_offset, self.vram[tile_idx] as usize, chr_bank_offset); + + for y in 0..PIXELS_PER_TILE { + for x in 0..PIXELS_PER_TILE { + let pixel_x = tile_col * PIXELS_PER_TILE + x; + let pixel_y = tile_row * PIXELS_PER_TILE + y; + let color_idx = tile[y * PIXELS_PER_TILE + x]; + + let rgb = + self.background_color_at(nt_offset, tile_col, tile_row, color_idx as usize); + + PPU::set_pixel( + self.background_buffer.as_mut_slice(), + x_offset + pixel_x, + pixel_y, + rgb, + ); + } + } + + self.updated_bg_tiles[tile_idx] = false; + } + + fn render_sprites(&mut self, shift_x: usize, shift_y: usize) { + let sprite_size = self.regs.ctrl.sprite_size(); + + // Sprites with lower OAM indices are drawn in front + for i in (0..WIDTH).step_by(4).rev() { + let sprite_y = self.attributes[i] as usize + 1; + let sprite_tile_idx = match sprite_size { + SpriteSize::Sprite8x8 => self.attributes[i + 1] as usize, + SpriteSize::Sprite8x16 => (self.attributes[i + 1]) as usize, + }; + let sprite_attr = self.attributes[i + 2]; + let sprite_x = self.attributes[i + 3] as usize; + + let palette_idx = sprite_attr & 0b11; + let behind_background = sprite_attr & 0b0010_0000 != 0; + let flip_horizontally = sprite_attr & 0b0100_0000 != 0; + let flip_vertically = sprite_attr & 0b1000_0000 != 0; + let palette = self.sprite_palette(palette_idx); + let chr_bank_offset: u16 = match sprite_size { + SpriteSize::Sprite8x8 => { + if !self.regs.ctrl.contains(Ctrl::SPRITE_PATTERN_ADDR) { + 0 + } else { + 0x1000 + } + } + SpriteSize::Sprite8x16 => (sprite_tile_idx as u16 & 1) * 0x1000, + }; + + let (top_tile_idx, bot_tile_idx) = { + use SpriteSize::*; + match (sprite_size, flip_vertically) { + (Sprite8x8, _) => (sprite_tile_idx, None), + (Sprite8x16, false) => (sprite_tile_idx, Some(sprite_tile_idx + 1)), + (Sprite8x16, true) => (sprite_tile_idx + 1, Some(sprite_tile_idx)), + } + }; + + self.render_sprite_tile( + chr_bank_offset, + top_tile_idx, + sprite_x, + sprite_y, + behind_background, + flip_horizontally, + flip_vertically, + palette, + shift_x, + shift_y, + ); + + if let Some(idx) = bot_tile_idx { + self.render_sprite_tile( + chr_bank_offset, + idx, + sprite_x, + sprite_y + 8, + behind_background, + flip_horizontally, + flip_vertically, + palette, + shift_x, + shift_y, + ); + } + } + } + + // https://www.nesdev.org/wiki/PPU_palettes + fn sprite_palette(&self, palette_idx: u8) -> [Option<(u8, u8, u8)>; 4] { + let palette_offset = SPRITE_PALETTES_OFFSET + palette_idx as usize * BYTES_PER_PALLETE; + + [ + None, // transparent + Some(COLOR_PALETTE[self.palette[palette_offset] as usize]), + Some(COLOR_PALETTE[self.palette[palette_offset + 1] as usize]), + Some(COLOR_PALETTE[self.palette[palette_offset + 2] as usize]), + ] + } + + #[allow(clippy::too_many_arguments)] + fn render_sprite_tile( + &mut self, + chr_bank_offset: u16, + nth: usize, + tile_x: usize, + tile_y: usize, + behind_background: bool, + flip_horizontally: bool, + flip_vertically: bool, + palette: [Option<(u8, u8, u8)>; 4], + shift_x: usize, + shift_y: usize, + ) { + let mut tile = [0u8; 16]; + self.rom + .mapper + .get_tile(&mut self.rom.cart, chr_bank_offset, nth, &mut tile); + + for y in 0..PIXELS_PER_TILE { + let mut plane1 = tile[y]; + let mut plane2 = tile[y + 8]; + + for x in (0..PIXELS_PER_TILE).rev() { + let bit0 = plane1 & 1; + let bit1 = plane2 & 1; + let color_idx = (bit1 << 1) | bit0; + + plane1 >>= 1; + plane2 >>= 1; + + if let Some(rgb) = palette[color_idx as usize] { + let (x, y) = match (flip_horizontally, flip_vertically) { + (false, false) => (x, y), + (true, false) => (7 - x, y), + (false, true) => (x, 7 - y), + (true, true) => (7 - x, 7 - y), + }; + + let pixel_x = tile_x + x + shift_x; + let pixel_y = tile_y + y + shift_y; + let is_bg_opaque = false; + if !behind_background || !is_bg_opaque { + PPU::set_pixel( + self.frame_buffer_complete.as_mut_slice(), + pixel_x, + pixel_y, + rgb, + ); + } + } + } + } + } + + #[inline] + fn is_sprite_zero_hit(&mut self) -> bool { + let y = self.attributes[0] as usize; + let x = self.attributes[3] as usize; + + y == self.scanline as usize + && x <= self.cycle as usize + && self.regs.mask.contains(Mask::SHOW_SPRITES) + } + + fn background_color_at( + &self, + nametable_offset: usize, + tile_x: usize, + tile_y: usize, + color_idx: usize, + ) -> (u8, u8, u8) { + let x = tile_x / TILES_PER_NAMETABLE_BYTE; // 4x4 tiles + let y = tile_y / TILES_PER_NAMETABLE_BYTE; + let nametable_idx = y * 8 + x; // 1 byte for color info of 4x4 tiles + let color_byte = self.vram[nametable_offset + 0x3c0 + nametable_idx]; + + let block_x = (tile_x % TILES_PER_NAMETABLE_BYTE) / 2; + let block_y = (tile_y % TILES_PER_NAMETABLE_BYTE) / 2; + + let palette_offset = 1 + 4 * match (block_x, block_y) { + (0, 0) => color_byte & 0b11, + (1, 0) => (color_byte >> 2) & 0b11, + (0, 1) => (color_byte >> 4) & 0b11, + (1, 1) => (color_byte >> 6) & 0b11, + _ => unreachable!(), + } as usize; + + COLOR_PALETTE[match color_idx { + 0 => self.palette[0], + _ => self.palette[palette_offset + color_idx - 1], + } as usize] + } + + // #[inline] + // fn transfer_frame_buffer(&mut self) { + // self.frame_buffer_complete + // .copy_from_slice(self.frame_buffer.as_slice()); + // } + fn detect_nmi_edge(&mut self) { let nmi = self.regs.ctrl.contains(Ctrl::GENERATE_NMI) && self.regs.status.contains(Status::VBLANK_STARTED); @@ -429,15 +806,21 @@ impl PPU { } } - self.set_pixel(x as usize, y as usize, color); + PPU::set_pixel( + self.background_buffer.as_mut_slice(), + x as usize, + y as usize, + color, + ); } - fn set_pixel(&mut self, x: usize, y: usize, (r, g, b): (u8, u8, u8)) { - if x < WIDTH && y < HEIGHT { - let offset = (y * WIDTH + x) * 3; - self.frame_buffer[offset] = r; - self.frame_buffer[offset + 1] = g; - self.frame_buffer[offset + 2] = b; + fn set_pixel(target: &mut [u8], x: usize, y: usize, (r, g, b): (u8, u8, u8)) { + let offset = (y * 2 * WIDTH + x) * 3; + + if offset < 2 * WIDTH * HEIGHT * 3 { + target[offset] = r; + target[offset + 1] = g; + target[offset + 2] = b; } } @@ -542,14 +925,48 @@ impl PPU { match addr { 0x0000..=0x1fff => self.rom.mapper.write(&mut self.rom.cart, addr, data), 0x2000..=0x2fff => { - self.vram[self.nametable_mirrored_addr(addr) as usize] = data; + let mirrored_addr = self.nametable_mirrored_addr(addr) as usize; + if self.vram[mirrored_addr] != data { + self.vram[mirrored_addr] = data; + + if mirrored_addr & 0x3ff < TILES_PER_NAMETABLE { + // nametable + self.updated_bg_tiles[mirrored_addr] = true; + } else { + // attributes + let attrib_idx = mirrored_addr & 63; + let meta_tile_x = attrib_idx & 31; + let meta_tile_y = attrib_idx / 32; + let base_nt_addr = + (mirrored_addr / BYTES_PER_NAMETABLE) * BYTES_PER_NAMETABLE; + + for x in 0..4 { + for y in 0..4 { + let tile_x = meta_tile_x * 4 + x; + let tile_y = meta_tile_y * 4 + y; + let tile_idx = base_nt_addr + tile_y * 32 + tile_x; + self.updated_bg_tiles[tile_idx] = true; + } + } + } + } } // 0x3000..=0x3eff => unreachable!(), 0x3f10 | 0x3f14 | 0x3f18 | 0x3f1c => { - self.palette[((addr - 0x3f10) & 31) as usize] = data; + let addr = ((addr - 0x3f10) & 31) as usize; + + if data != self.palette[addr] { + self.palette[addr] = data; + self.updated_bg_tiles.fill(true); + } } 0x3f00..=0x3fff => { - self.palette[((addr - 0x3f00) & 31) as usize] = data; + let addr = ((addr - 0x3f00) & 31) as usize; + + if data != self.palette[addr] { + self.palette[addr] = data; + self.updated_bg_tiles.fill(true); + } } _ => { // ignoring write to {addr:04X} @@ -608,10 +1025,59 @@ impl PPU { } } + // returns number of bytes copied + fn copy_rect( + &self, + offset: usize, + (x1, y1): (usize, usize), + (x2, y2): (usize, usize), + buffer: &mut [u8], + ) -> usize { + let width = x2 - x1; + let height = y2 - y1; + + // for y in 0..height { + // let src_offset = ((y1 + y) * WIDTH + x1) * 3; + // let dst_offset = offset + y * width * 3; + // buffer[dst_offset..(dst_offset + width * 3)] + // .copy_from_slice(&self.frame_buffer_complete[src_offset..(src_offset + width * 3)]); + // } + + let mut index = offset; + + for x in x1..x2 { + for y in y1..y2 { + let i = (y * 2 * WIDTH + x) * 3; + buffer[index] = self.frame_buffer_complete[i]; + index += 1; + } + } + + width * height * 3 + } + + pub fn get_frame(&self, buffer: &mut [u8]) { + // let scroll_x = self.regs.scroll.x as usize; + // self.copy_rect(0, (0, 0), (WIDTH, HEIGHT), buffer); + + // if scroll_x < WIDTH { + // self.copy_rect(0, (scroll_x, 0), (scroll_x + WIDTH, HEIGHT), buffer); + // } else { + // let offset2 = self.copy_rect(0, (scroll_x, 0), (2 * WIDTH, HEIGHT), buffer); + // self.copy_rect(offset2, (0, 0), (scroll_x, HEIGHT), buffer); + // } + + buffer.copy_from_slice(self.frame_buffer_complete.as_slice()); + } + #[inline] - pub fn get_frame(&self) -> &[u8] { + pub fn get_full_frame(&mut self) -> &[u8] { self.frame_buffer_complete.as_slice() } + + pub fn get_updated_tiles_count(&self) -> usize { + self.updated_bg_tiles.iter().filter(|&b| *b).count() + } } impl savestate::Save for SpriteData { diff --git a/src/ppu/registers.rs b/src/ppu/registers.rs index b246cb2..5c1809e 100644 --- a/src/ppu/registers.rs +++ b/src/ppu/registers.rs @@ -14,6 +14,7 @@ pub struct Registers { pub mask: Mask, pub status: Status, pub oam_addr: u8, + pub scroll: Scroll, } impl Registers { @@ -28,6 +29,11 @@ impl Registers { mask: Mask::empty(), status: Status::empty(), oam_addr: 0, + scroll: Scroll { + x: 0, + y: 0, + is_x: true, + }, } } @@ -96,6 +102,17 @@ impl Ctrl { } } + #[inline] + pub fn base_nametable_addr(&self) -> usize { + match self.bits() & 0b11 { + 0 => 0x2000, + 1 => 0x2400, + 2 => 0x2800, + 3 => 0x2c00, + _ => unreachable!(), + } + } + #[inline] pub fn sprite_chr_offset(&self) -> u16 { if !self.contains(Ctrl::SPRITE_PATTERN_ADDR) { @@ -248,12 +265,18 @@ impl Registers { self.t = (self.t & 0xFFE0) | ((data as u16) >> 3); self.x = data & 0b111; self.w = true; + + self.scroll.is_x = false; + self.scroll.x = data; } else { // t: FGH..AB CDE..... <- d: ABCDEFGH // w: <- 0 self.t = (self.t & 0x8FFF) | (((data as u16) & 0b111) << 12); self.t = (self.t & 0xFC1F) | (((data as u16) & 0b11111000) << 2); self.w = false; + + self.scroll.is_x = true; + self.scroll.y = data; } } @@ -328,6 +351,24 @@ impl Registers { } } +pub struct Scroll { + pub x: u8, + pub y: u8, + pub is_x: bool, +} + +impl Scroll { + pub fn write(&mut self, data: u8) { + if self.is_x { + self.x = data; + } else { + self.y = data; + } + + self.is_x = !self.is_x; + } +} + const PPU_REGS_SECTION_NAME: &str = "regs"; impl savestate::Save for Registers { diff --git a/web/src/lib.rs b/web/src/lib.rs index ab0269f..82ab6a7 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -70,9 +70,8 @@ impl WasmNes { } #[wasm_bindgen(js_name = nextFrame)] - pub fn next_frame(&mut self, buffer: &mut [u8]) { + pub fn next_frame(&mut self) { self.nes.next_frame(); - buffer.copy_from_slice(self.nes.get_frame()); } #[wasm_bindgen(js_name = nextSamples)] @@ -82,7 +81,12 @@ impl WasmNes { #[wasm_bindgen(js_name = fillFrameBuffer)] pub fn fill_frame_buffer(&self, buffer: &mut [u8]) { - buffer.copy_from_slice(self.nes.get_frame()) + self.nes.get_frame(buffer) + } + + #[wasm_bindgen(js_name = getUpdatedTilesCount)] + pub fn get_updated_tiles_count(&self) -> usize { + self.nes.get_updated_tiles_count() } #[wasm_bindgen(js_name = setJoypad1)] diff --git a/web/ui/src/main.ts b/web/ui/src/main.ts index 4dfaeb8..7f3316b 100644 --- a/web/ui/src/main.ts +++ b/web/ui/src/main.ts @@ -6,7 +6,7 @@ import { hooks } from './ui/hooks'; import { StoreData, createStore } from './ui/store'; import { createUI } from './ui/ui'; -const WIDTH = 256; // px +const WIDTH = 256 * 2; // px const HEIGHT = 240; // px type SyncMode = 0 | 1 | 2; const SYNC_VIDEO: SyncMode = 0; @@ -28,12 +28,12 @@ async function setup() { await init(); Nes.initPanicHook(); const store = await createStore(); - const ui = createUI(store); - const syncMode = SYNC_BOTH; + const ui = createUI(store, WIDTH, HEIGHT); + const syncMode = SYNC_VIDEO; const audioBufferSize = AUDIO_BUFFER_SIZE_MAPPING[syncMode]; const avoidUnderruns = syncMode === SYNC_BOTH; const canvas = document.querySelector('#screen')!; - const renderer = createWebglRenderer(canvas); + const renderer = createWebglRenderer(canvas, WIDTH, HEIGHT); let nes: Nes; const controller = createController(store); const frame = new Uint8Array(WIDTH * HEIGHT * 3); @@ -222,7 +222,8 @@ async function setup() { function renderState(state: Uint8Array, buffer: Uint8Array): void { const prevState = nes.saveState(); nes.loadState(state); - nes.nextFrame(buffer); + nes.nextFrame(); + nes.fillFrameBuffer(buffer); nes.loadState(prevState); } @@ -263,7 +264,8 @@ async function setup() { if (nes && store.ref.lastState != null) { nes.loadState(store.ref.lastState); - nes.nextFrame(backgroundFrame); + nes.nextFrame(); + nes.fillFrameBuffer(backgroundFrame); nes.loadState(store.ref.lastState); hooks.call('setBackground', { mode: 'current' }); @@ -285,7 +287,8 @@ async function setup() { // Generate the screenshot after 2 seconds for (let i = 0; i < 120; i++) { - titleScreenNes.nextFrame(titleScreenFrame); + titleScreenNes.nextFrame(); + titleScreenNes.fillFrameBuffer(titleScreenFrame); } await store.db.titleScreen.insert(hash, titleScreenFrame); @@ -326,6 +329,7 @@ async function setup() { store.save(); } + function run(): void { requestAnimationFrame(run); controller.tick(); @@ -335,7 +339,9 @@ async function setup() { renderer.render(frame); } else if (nes !== undefined) { if (syncMode !== SYNC_AUDIO) { - nes.nextFrame(frame); + nes.nextFrame(); + console.log(nes.getUpdatedTilesCount()); + nes.fillFrameBuffer(frame); renderer.render(frame); } } diff --git a/web/ui/src/ui/screen.ts b/web/ui/src/ui/screen.ts index 1b78271..1525853 100644 --- a/web/ui/src/ui/screen.ts +++ b/web/ui/src/ui/screen.ts @@ -15,18 +15,16 @@ const PALETTE = [ [0x99, 0xFF, 0xFC], [0xDD, 0xDD, 0xDD], [0x11, 0x11, 0x11], [0x11, 0x11, 0x11], ]; -const WIDTH = 256; -const HEIGHT = 240; const TILES_PER_ROW = 32; const TILES_PER_COL = 30; export type Tile = Uint8Array; export type Screen = ReturnType; -export const createScreen = () => { +export const createScreen = (width: number, height: number) => { const tiles: Tile[] = []; const blankTile: Tile = new Uint8Array(64).fill(0x00); - const background = new Uint8Array(WIDTH * HEIGHT * 3).fill(0); + const background = new Uint8Array(width * height * 3).fill(0); let opacity = 0; for (let i = 0; i < TILES_PER_ROW * TILES_PER_COL; i += 1) { @@ -66,7 +64,7 @@ export const createScreen = () => { for (let i = 0; i < 8; i += 1) { const colorIndex = tile[i + j * 8]; const color = PALETTE[colorIndex]; - const index = (x * 8 + i + (y * 8 + j) * WIDTH) * 3; + const index = (x * 8 + i + (y * 8 + j) * width) * 3; buffer[index + 0] = mix(color[0], background[index + 0]); buffer[index + 1] = mix(color[1], background[index + 1]); buffer[index + 2] = mix(color[2], background[index + 2]); diff --git a/web/ui/src/ui/ui.ts b/web/ui/src/ui/ui.ts index d8377a5..810c39f 100644 --- a/web/ui/src/ui/ui.ts +++ b/web/ui/src/ui/ui.ts @@ -42,8 +42,8 @@ const UI_ACTION_MAPPING: Record = { ' ': 'select', }; -export const createUI = (store: Store) => { - const screen = createScreen(); +export const createUI = (store: Store, width: number, height: number) => { + const screen = createScreen(width, height); const alerts: Alert[] = []; const subMenuMapping: Record & { prev(): void, diff --git a/web/ui/src/webgl.ts b/web/ui/src/webgl.ts index b0199b0..5c4d8e5 100644 --- a/web/ui/src/webgl.ts +++ b/web/ui/src/webgl.ts @@ -1,5 +1,5 @@ -export function createWebglRenderer(canvas: HTMLCanvasElement) { +export function createWebglRenderer(canvas: HTMLCanvasElement, width: number, height: number) { const gl = canvas.getContext('webgl')!; if (gl == null) { @@ -63,7 +63,7 @@ export function createWebglRenderer(canvas: HTMLCanvasElement) { const fragmentShader = createShader(gl.FRAGMENT_SHADER, fragmentShaderSource); const program = createProgram(vertexShader, fragmentShader); const posAttribLoc = gl.getAttribLocation(program, "a_position"); - gl.viewport(0, 0, 256, 240); + gl.viewport(0, 0, width, height); function createShader(type: number, source: string): WebGLShader { const shader = gl.createShader(type); From 9f4e9005d1d2460ac10e8370410947c3e9b503a1 Mon Sep 17 00:00:00 2001 From: nathsou Date: Mon, 31 Jul 2023 14:29:48 +0200 Subject: [PATCH 2/2] Implement faster scrolling --- .vscode/settings.json | 2 + 3ds/src/main.rs | 52 ++++++-- README.md | 1 + desktop/src/main.rs | 51 ++++--- src/bus/mod.rs | 4 +- src/cpu/instructions.rs | 4 +- src/nes.rs | 72 ++++++---- src/ppu/mod.rs | 288 +++++++++++++++++----------------------- src/ppu/registers.rs | 3 - 9 files changed, 255 insertions(+), 222 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 2d9e3ef..fa76483 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { "rust-analyzer.linkedProjects": [ "Cargo.toml", + "./desktop/Cargo.toml", + "./3ds/Cargo.toml" ] } \ No newline at end of file diff --git a/3ds/src/main.rs b/3ds/src/main.rs index 60c3995..efed05a 100644 --- a/3ds/src/main.rs +++ b/3ds/src/main.rs @@ -23,15 +23,15 @@ fn is_key_active(hid: &Hid, key: KeyPad) -> bool { } fn main() { - let mut nes_frame_buffer: [u8; FRAME_BUFFER_BYTE_SIZE] = [0; FRAME_BUFFER_BYTE_SIZE]; + let mut nes_frame_buffer = [0u8; FRAME_BUFFER_BYTE_SIZE]; ctru::use_panic_handler(); let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let apt = Apt::new().expect("Couldn't obtain APT controller"); - let _console = Console::new(gfx.bottom_screen.borrow_mut()); + // let _console = Console::new(gfx.bottom_screen.borrow_mut()); - println!("\x1b[0;3HPress L + R to exit."); + // println!("\x1b[0;3HPress L + R to exit."); let mut top_screen = gfx.top_screen.borrow_mut(); top_screen.set_double_buffering(false); @@ -39,11 +39,13 @@ fn main() { let rom = ROM::new(ROM_BYTES.to_vec()).expect("Couldn't load ROM"); let mut nes = Nes::new(rom, SAMPLE_RATE); - let mut nes_frame_buffer = [0u8; FRAME_BUFFER_BYTE_SIZE]; let mut top_frame_buffer = [0u8; TOP_SCREEN_WIDTH * TOP_SCREEN_HEIGHT * 3]; + // let mut last_frame = std::time::Instant::now(); // Main loop while apt.main_loop() { + // let frame_duration = last_frame.elapsed(); + // last_frame = std::time::Instant::now(); // Scan all the inputs. This should be done once for each frame hid.scan_input(); @@ -86,13 +88,22 @@ fn main() { } nes.get_joypad1_mut().update(joypad1_state.bits()); - let t0 = std::time::Instant::now(); - nes.next_frame(); - let frame_time = t0.elapsed().as_millis() as usize; - println!("{frame_time}ms"); - nes_frame_buffer.copy_from_slice(&nes.get_frame()); - - // rotate the frame buffer 90 degrees + // let t0 = std::time::Instant::now(); + let offset = LEFT_X_OFFSET_TOP * NES_SCREEN_HEIGHT * 3; + // nes.next_frame_inaccurate( + // &mut top_frame_buffer[offset..offset + NES_SCREEN_WIDTH * NES_SCREEN_HEIGHT * 3], + // ); + + nes.next_frame_inaccurate(&mut nes_frame_buffer); + + // let frame_time = t0.elapsed().as_millis() as usize; + // let d0 = t0.elapsed(); + // let t1 = std::time::Instant::now(); + // nes.get_frame(&mut nes_frame_buffer); + // let d1 = t1.elapsed(); + + // let t2 = std::time::Instant::now(); + // // rotate the frame buffer 90 degrees for y in 0..NES_SCREEN_HEIGHT { for x in 0..NES_SCREEN_WIDTH { let src_index = (y * NES_SCREEN_WIDTH + x) * 3; @@ -105,6 +116,12 @@ fn main() { } } + // top_frame_buffer[..(NES_SCREEN_WIDTH * NES_SCREEN_HEIGHT * 3)] + // .copy_from_slice(&nes_frame_buffer[..]); + + // let d2 = t2.elapsed(); + + // let t3 = std::time::Instant::now(); unsafe { top_screen .raw_framebuffer() @@ -114,7 +131,18 @@ fn main() { top_screen.flush_buffers(); + // let d3 = t3.elapsed(); + + // println!( + // "nf {} gf {}, rt {} after {} frame {}", + // d0.as_millis(), + // d1.as_millis(), + // d2.as_millis(), + // d3.as_millis(), + // frame_duration.as_millis() + // ); + //Wait for VBlank - gfx.wait_for_vblank(); + // gfx.wait_for_vblank(); } } diff --git a/README.md b/README.md index 6623ff3..d696b35 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Use the arrow keys to navigate the menus and press enter to validate - VR / 3D mode with sprites in front and bg tiles in the background? - Optimize! (JIT Compiler / frame by frame or scanline by scanline rendering instead of pixel by pixel) - Wide mode (for scrolling games, visualize the prefilled tiles in advance) +- Switch savestates to proto-buf? ## Embedding diff --git a/desktop/src/main.rs b/desktop/src/main.rs index 4085d84..b716c34 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -17,6 +17,8 @@ use sdl2::{ const SCALE_FACTOR: usize = 2; const SAMPLE_RATE: f64 = 44_100.0; +const WIDTH: usize = SCREEN_WIDTH; +const HEIGHT: usize = SCREEN_HEIGHT; fn build_controller_map() -> HashMap { let mut controller_map = HashMap::new(); @@ -33,6 +35,7 @@ fn build_controller_map() -> HashMap { struct APUCallback<'a> { nes: &'a mut Nes, + frame: &'a mut [u8], avoid_underruns: bool, } @@ -40,7 +43,8 @@ impl<'a> AudioCallback for APUCallback<'a> { type Channel = f32; fn callback(&mut self, out: &mut [f32]) { - self.nes.fill_audio_buffer(out, self.avoid_underruns); + self.nes + .fill_audio_buffer(out, self.frame, self.avoid_underruns); } } @@ -48,15 +52,22 @@ fn handle_events( event_pump: &mut EventPump, controller: &mut Joypad, controller_map: &HashMap, + paused: &mut bool, ) { for event in event_pump.poll_iter() { match event { - Event::Quit { .. } - | Event::KeyDown { + Event::Quit { .. } => { + std::process::exit(0); + } + Event::KeyDown { keycode: Some(Keycode::Escape), .. + } + | Event::KeyDown { + keycode: Some(Keycode::Tab), + .. } => { - std::process::exit(0); + *paused = !*paused; } Event::KeyDown { keycode, .. } => { if let Some(&button) = keycode.and_then(|k| controller_map.get(&k)) { @@ -96,8 +107,8 @@ fn main() { let window = video_subsystem .window( "nessy", - (SCREEN_WIDTH * SCALE_FACTOR) as u32, - (SCREEN_HEIGHT * SCALE_FACTOR) as u32, + (WIDTH * SCALE_FACTOR) as u32, + (HEIGHT * SCALE_FACTOR) as u32, ) .position_centered() .build() @@ -115,31 +126,41 @@ fn main() { let creator = canvas.texture_creator(); let mut texture = creator - .create_texture_target( - PixelFormatEnum::RGB24, - SCREEN_WIDTH as u32, - SCREEN_HEIGHT as u32, - ) + .create_texture_target(PixelFormatEnum::RGB24, WIDTH as u32, HEIGHT as u32) .unwrap(); let mut event_pump = sdl_context.event_pump().unwrap(); let controller_map = build_controller_map(); + let mut frame = [0; WIDTH * HEIGHT * 3]; let audio_device = audio_subsystem .open_playback(None, &desired_audio_spec, |_| APUCallback { nes: &mut nes, + frame: &mut frame, avoid_underruns: false, }) .unwrap(); audio_device.resume(); + let mut paused = false; + loop { - handle_events(&mut event_pump, nes.get_joypad1_mut(), &controller_map); - nes.next_frame(); - let frame = nes.get_frame(); - texture.update(None, frame, SCREEN_WIDTH * 3).unwrap(); + handle_events( + &mut event_pump, + nes.get_joypad1_mut(), + &controller_map, + &mut paused, + ); + + if !paused { + nes.next_frame_inaccurate(&mut frame); + } + + // nes.get_frame(&mut frame); + texture.update(None, &frame, WIDTH * 3).unwrap(); canvas.copy(&texture, None, None).unwrap(); + canvas.present(); } } diff --git a/src/bus/mod.rs b/src/bus/mod.rs index e3a977a..8f8a88b 100644 --- a/src/bus/mod.rs +++ b/src/bus/mod.rs @@ -68,11 +68,11 @@ impl Bus { } } - pub fn advance(&mut self, cpu_cycles: u32) { + pub fn advance(&mut self, cpu_cycles: u32, frame: &mut [u8]) { let ppu_cycles = cpu_cycles * 3; for _ in 0..ppu_cycles { - self.ppu.step(); + self.ppu.step(frame); } for _ in 0..cpu_cycles { diff --git a/src/cpu/instructions.rs b/src/cpu/instructions.rs index 52672c1..22b2891 100644 --- a/src/cpu/instructions.rs +++ b/src/cpu/instructions.rs @@ -189,7 +189,9 @@ impl CPU { self.irq(); } } - Interrupt::Nmi => self.nmi(), + Interrupt::Nmi => { + self.nmi(); + } } let op_code = self.next_byte(); diff --git a/src/nes.rs b/src/nes.rs index 60c0688..10415df 100644 --- a/src/nes.rs +++ b/src/nes.rs @@ -1,6 +1,7 @@ use crate::{ bus::{controller::Joypad, Bus}, cpu::{rom::ROM, CPU}, + ppu::registers::Status, savestate::{self, Save, SaveState, SaveStateError}, }; @@ -14,9 +15,9 @@ impl Nes { Nes { cpu: CPU::new(bus) } } - pub fn step(&mut self) { + pub fn step(&mut self, frame: &mut [u8]) { let cpu_cycles = self.cpu.step(); - self.cpu.bus.advance(cpu_cycles); + self.cpu.bus.advance(cpu_cycles, frame); } #[inline] @@ -24,16 +25,43 @@ impl Nes { self.cpu.bus.ppu.frame_complete = false; } - pub fn next_frame(&mut self) { + pub fn next_frame(&mut self, frame: &mut [u8]) { while !self.cpu.bus.ppu.frame_complete { - self.step(); + self.step(frame); } self.on_frame_complete(); } + pub fn next_frame_inaccurate(&mut self, frame: &mut [u8]) { + let mut cycles = 0; + + let (spzx, spzy) = self.cpu.bus.ppu.sprite_zero_coords(); + let sprite_zero_cycles = (spzy as f32 * 113.667 + spzx as f32 * 3.0) as u32; + + while cycles < sprite_zero_cycles { + cycles += self.cpu.step(); + } + + self.cpu.bus.ppu.regs.status.insert(Status::SPRITE_ZERO_HIT); + + while cycles < 27_508 { + cycles += self.cpu.step(); + } + + self.cpu.bus.ppu.render_frame(frame); + self.cpu.bus.ppu.start_vblank(); + self.cpu.bus.ppu.tick_inaccurate(); + + while cycles < 29_781 { + cycles += self.cpu.step(); + } + + self.cpu.bus.ppu.end_vblank(); + } + /// emulates enough cycles to fill the audio buffer, - pub fn next_samples(&mut self, audio_buffer: &mut [f32]) -> bool { + pub fn next_samples(&mut self, audio_buffer: &mut [f32], frame_buffer: &mut [u8]) -> bool { let mut count = 0; let mut new_frame = false; @@ -49,7 +77,7 @@ impl Nes { } break; } - None => self.step(), + None => self.step(frame_buffer), } } } @@ -57,7 +85,7 @@ impl Nes { new_frame } - pub fn wait_for_samples(&mut self, count: usize) { + pub fn wait_for_samples(&mut self, count: usize, frame: &mut [u8]) { let mut i = 0; while i < count { @@ -67,27 +95,32 @@ impl Nes { i += 1; break; } - None => self.step(), + None => self.step(frame), } } } } - pub fn fill_audio_buffer(&mut self, buffer: &mut [f32], avoid_underruns: bool) { + pub fn fill_audio_buffer( + &mut self, + audio_buffer: &mut [f32], + frame_buffer: &mut [u8], + avoid_underruns: bool, + ) { let remaining_samples_in_bufffer = self.cpu.bus.apu.remaining_samples() as usize; if avoid_underruns { // ensure that the buffer is filled with enough samples - if remaining_samples_in_bufffer < buffer.len() { - let wait_for = buffer.len() - remaining_samples_in_bufffer + 1; - self.wait_for_samples(wait_for); + if remaining_samples_in_bufffer < audio_buffer.len() { + let wait_for = audio_buffer.len() - remaining_samples_in_bufffer + 1; + self.wait_for_samples(wait_for, frame_buffer); } } - self.cpu.bus.apu.fill(buffer); + self.cpu.bus.apu.fill(audio_buffer); - for i in remaining_samples_in_bufffer..buffer.len() { - buffer[i] = 0.0; + for i in remaining_samples_in_bufffer..audio_buffer.len() { + audio_buffer[i] = 0.0; } } @@ -107,21 +140,12 @@ impl Nes { &mut self.cpu.bus.joypad2 } - #[inline] - pub fn get_frame(&self, buffer: &mut [u8]) { - self.cpu.bus.ppu.get_frame(buffer); - } - pub fn save_state(&self) -> SaveState { let mut state = SaveState::new(&self.cpu.bus.ppu.rom.cart.hash); self.save(state.get_root_mut()); state } - pub fn get_updated_tiles_count(&self) -> usize { - self.cpu.bus.ppu.get_updated_tiles_count() - } - pub fn load_state(&mut self, data: &[u8]) -> Result<(), SaveStateError> { let mut state = SaveState::decode(data)?; diff --git a/src/ppu/mod.rs b/src/ppu/mod.rs index 92d6f3a..1585596 100644 --- a/src/ppu/mod.rs +++ b/src/ppu/mod.rs @@ -1,4 +1,4 @@ -mod registers; +pub mod registers; use self::registers::{Ctrl, Mask, Registers, SpriteSize, Status}; use crate::{ @@ -11,7 +11,6 @@ const BYTES_PER_PALLETE: usize = 4; const TILES_PER_NAMETABLE_BYTE: usize = 4; const TILES_PER_NAMETABLE: usize = 32 * 30; const BYTES_PER_NAMETABLE: usize = 1024; -const ATTRIBUTES_PER_NAMETABLE: usize = 64; const SPRITE_PALETTES_OFFSET: usize = 0x11; const WIDTH: usize = 256; const HEIGHT: usize = 240; @@ -51,11 +50,11 @@ struct CachedTile { #[allow(clippy::upper_case_acronyms)] pub struct PPU { pub rom: ROM, - regs: Registers, + pub regs: Registers, open_bus: u8, vram: [u8; 2 * BYTES_PER_NAMETABLE], palette: [u8; 32], - attributes: [u8; ATTRIBUTES_PER_NAMETABLE * 4], + attributes: [u8; 64 * 4], pub cycle: u16, scanline: u16, frame: u64, @@ -71,10 +70,9 @@ pub struct PPU { pattern_table_high_byte: u8, scanline_sprites: [SpriteData; 8], visible_sprites_count: u8, - background_buffer: Box<[u8; WIDTH * HEIGHT * 3 * 2]>, // both nametables - frame_buffer_complete: Box<[u8; WIDTH * HEIGHT * 3 * 2]>, // avoid stack overflow in WASM nt_cache: Box<[Option; BYTES_PER_NAMETABLE * 2]>, updated_bg_tiles: Box<[bool; BYTES_PER_NAMETABLE * 2]>, + background: Box<[u8; WIDTH * HEIGHT * 3 * 2]>, } impl PPU { @@ -108,16 +106,25 @@ impl PPU { chr: [0; 8], }; 8], visible_sprites_count: 0, - background_buffer: Box::new([0; WIDTH * HEIGHT * 3 * 2]), - frame_buffer_complete: Box::new([0; WIDTH * HEIGHT * 3 * 2]), nt_cache: Box::new([None; BYTES_PER_NAMETABLE * 2]), updated_bg_tiles: Box::new([true; BYTES_PER_NAMETABLE * 2]), + background: Box::new([0; WIDTH * HEIGHT * 3 * 2]), }; ppu.reset(); ppu } + pub fn tick_inaccurate(&mut self) { + if self.should_trigger_nmi + && self.regs.ctrl.contains(Ctrl::GENERATE_NMI) + && self.regs.status.contains(Status::VBLANK_STARTED) + { + self.nmi_triggered = true; + self.should_trigger_nmi = false; + } + } + fn tick(&mut self) { // TODO: handle NMI delay @@ -161,7 +168,7 @@ impl PPU { } } - pub fn step(&mut self) { + pub fn step(&mut self, frame: &mut [u8]) { self.tick(); let preline = self.scanline == 261; @@ -175,7 +182,7 @@ impl PPU { if !FAST_MODE { if self.regs.show_background() { if visible_line && visible_cycle { - self.render_pixel(); + self.render_pixel(frame); } if render_line && fetch_cycle { @@ -221,35 +228,40 @@ impl PPU { } } + if preline && self.cycle == 1 { + self.end_vblank(); + } + // VBlank if self.scanline == 241 && self.cycle == 1 { - self.frame_complete = true; - self.regs.status.insert(Status::VBLANK_STARTED); - self.detect_nmi_edge(); + self.start_vblank(); + } + } - if FAST_MODE { - self.render_frame(); - } + pub fn start_vblank(&mut self) { + self.frame_complete = true; + self.regs.status.insert(Status::VBLANK_STARTED); + self.detect_nmi_edge(); - // self.transfer_frame_buffer(); - } + // if FAST_MODE { + // self.render_frame(); + // } - if preline && self.cycle == 1 { - self.regs.status.remove(Status::VBLANK_STARTED); - self.regs.status.remove(Status::SPRITE_ZERO_HIT); - self.regs.status.remove(Status::SPRITE_OVERFLOW); - self.detect_nmi_edge(); - } + // self.transfer_frame_buffer(); } -<<<<<<< HEAD - fn render_frame(&mut self) { - // self.frame_buffer1.fill(0); - // self.frame_buffer2.fill(0); + pub fn end_vblank(&mut self) { + self.frame_complete = false; + self.regs.status.remove(Status::VBLANK_STARTED); + self.regs.status.remove(Status::SPRITE_ZERO_HIT); + self.regs.status.remove(Status::SPRITE_OVERFLOW); + self.detect_nmi_edge(); + } + pub fn render_frame(&mut self, frame: &mut [u8]) { let base_nametable_addr = self.regs.ctrl.base_nametable_addr(); let scroll_x = self.regs.scroll.x as usize; - let scroll_y = self.regs.scroll.y as usize; + // let scroll_y = self.regs.scroll.y as usize; let (nametable1, nametable2): (usize, usize) = match self.rom.cart.mirroring { Mirroring::Vertical => match base_nametable_addr { @@ -274,58 +286,53 @@ impl PPU { }; if self.regs.show_background() { + frame.fill(0); self.render_background(nametable1, 0); - self.render_background(nametable2, WIDTH); - } - // for i in 0..HEIGHT { - // self.set_pixel(scroll_x, i, (255, 0, 0)); - // self.set_pixel((scroll_x + WIDTH) % (2 * WIDTH), i, (0, 255, 0)); - // } + if scroll_x > 0 { + self.render_background(nametable2, WIDTH); + } - // for i in 0..WIDTH { - // self.set_pixel(i, scroll_y, (0, 0, 255)); - // self.set_pixel(i, (scroll_y + HEIGHT) % (2 * HEIGHT), (255, 255, 0)); - // } + let mut offset = 0; + let len = WIDTH * 3; + for y in 0..HEIGHT { + // for x in 0..WIDTH { + // let xs = x + scroll_x; + // let bg_idx = (y * 2 * WIDTH + xs) * 3; + // let buf_idx = (y * WIDTH + x) * 3; + // frame[buf_idx] = self.background[bg_idx]; + // frame[buf_idx + 1] = self.background[bg_idx + 1]; + // frame[buf_idx + 2] = self.background[bg_idx + 2]; + // } + + let s = (y * 2 * WIDTH + scroll_x) * 3; + + frame[offset..offset + len] + .copy_from_slice(&self.background.as_slice()[s..s + len]); + offset += len; + } - // if scroll_x > 0 { - // self.render_background( - // nametable2, - // BoundingBox { - // x_min: 0, - // x_max: scroll_x, - // y_min: 0, - // y_max: HEIGHT, - // }, - // (WIDTH - scroll_x) as isize, - // 0, - // ); - // } else if scroll_y > 0 { - // self.render_background( - // nametable2, - // BoundingBox { - // x_min: 0, - // x_max: WIDTH, - // y_min: 0, - // y_max: scroll_y, - // }, - // 0, - // (HEIGHT - scroll_y) as isize, - // ); - // } + // frame.copy_from_slice(&self.background.as_slice()[..WIDTH * HEIGHT * 3]); + // self.copy_rect(0, (scroll_x, 0), (WIDTH + scroll_x, HEIGHT), frame); + // self.copy_rect( + // WIDTH - scroll_x, + // (WIDTH + scroll_x, 0), + // (2 * WIDTH, HEIGHT), + // frame, + // ); + } -======= - fn transfer_frame_buffer(&mut self) { ->>>>>>> main - self.frame_buffer_complete - .copy_from_slice(self.background_buffer.as_slice()); + // for y in 0..HEIGHT { + // PPU::set_pixel(frame, scroll_x, y, (0, 0, 255)); + // PPU::set_pixel(frame, (scroll_x + WIDTH) % (2 * WIDTH), y, (0, 0, 255)); + // } if self.regs.show_sprites() { - self.render_sprites(scroll_x, scroll_y); + self.render_sprites(frame); } } - fn render_background(&mut self, nt_offset: usize, x_offset: usize) { + fn render_background(&mut self, nt_offset: usize, offset_x: usize) { let chr_bank_offset = self.regs.ctrl.background_chr_offset(); for i in 0..0x03c0 { @@ -338,7 +345,7 @@ impl PPU { i, tile_col, tile_row, - x_offset, + offset_x, ); } } @@ -352,7 +359,7 @@ impl PPU { let tile_index = nt_offset + nth; match self.nt_cache[tile_index] { - // Some(tile) => tile.chr, + Some(tile) => tile.chr, _ => { let mut tile = [0u8; 16]; self.rom @@ -391,7 +398,7 @@ impl PPU { nth: usize, tile_col: usize, tile_row: usize, - x_offset: usize, + offset_x: usize, ) { let tile_idx = nt_offset + nth; @@ -404,26 +411,20 @@ impl PPU { for y in 0..PIXELS_PER_TILE { for x in 0..PIXELS_PER_TILE { - let pixel_x = tile_col * PIXELS_PER_TILE + x; - let pixel_y = tile_row * PIXELS_PER_TILE + y; let color_idx = tile[y * PIXELS_PER_TILE + x]; - let rgb = self.background_color_at(nt_offset, tile_col, tile_row, color_idx as usize); - PPU::set_pixel( - self.background_buffer.as_mut_slice(), - x_offset + pixel_x, - pixel_y, - rgb, - ); + let pixel_x = offset_x + tile_col * PIXELS_PER_TILE + x; + let pixel_y = tile_row * PIXELS_PER_TILE + y; + PPU::set_pixel(self.background.as_mut_slice(), pixel_x, pixel_y, rgb); } } self.updated_bg_tiles[tile_idx] = false; } - fn render_sprites(&mut self, shift_x: usize, shift_y: usize) { + fn render_sprites(&mut self, frame: &mut [u8]) { let sprite_size = self.regs.ctrl.sprite_size(); // Sprites with lower OAM indices are drawn in front @@ -470,8 +471,7 @@ impl PPU { flip_horizontally, flip_vertically, palette, - shift_x, - shift_y, + frame, ); if let Some(idx) = bot_tile_idx { @@ -484,8 +484,7 @@ impl PPU { flip_horizontally, flip_vertically, palette, - shift_x, - shift_y, + frame, ); } } @@ -514,8 +513,7 @@ impl PPU { flip_horizontally: bool, flip_vertically: bool, palette: [Option<(u8, u8, u8)>; 4], - shift_x: usize, - shift_y: usize, + frame: &mut [u8], ) { let mut tile = [0u8; 16]; self.rom @@ -542,16 +540,17 @@ impl PPU { (true, true) => (7 - x, 7 - y), }; - let pixel_x = tile_x + x + shift_x; - let pixel_y = tile_y + y + shift_y; + let pixel_x = tile_x + x; + let pixel_y = tile_y + y; let is_bg_opaque = false; if !behind_background || !is_bg_opaque { - PPU::set_pixel( - self.frame_buffer_complete.as_mut_slice(), - pixel_x, - pixel_y, - rgb, - ); + let offset = (pixel_y * WIDTH + pixel_x) * 3; + if offset < frame.len() { + frame[offset] = rgb.0; + frame[offset + 1] = rgb.1; + frame[offset + 2] = rgb.2; + // PPU::set_pixel(frame, pixel_x, pixel_y, rgb); + } } } } @@ -770,7 +769,7 @@ impl PPU { self.visible_sprites_count = count as u8; } - fn render_pixel(&mut self) { + fn render_pixel(&mut self, frame: &mut [u8]) { let x = self.cycle - 1; let y = self.scanline; let mut bg = self.get_background_pixel(); @@ -810,24 +809,24 @@ impl PPU { } } - PPU::set_pixel( - self.background_buffer.as_mut_slice(), - x as usize, - y as usize, - color, - ); + PPU::set_pixel(frame, x as usize, y as usize, color); } fn set_pixel(target: &mut [u8], x: usize, y: usize, (r, g, b): (u8, u8, u8)) { let offset = (y * 2 * WIDTH + x) * 3; - if offset < 2 * WIDTH * HEIGHT * 3 { + if x < 2 * WIDTH && y < HEIGHT && offset < target.len() { target[offset] = r; target[offset + 1] = g; target[offset + 2] = b; } } + #[inline] + pub fn sprite_zero_coords(&self) -> (u8, u8) { + (self.attributes[3], self.attributes[0]) + } + // https://www.nesdev.org/wiki/PPU_palettes fn sprite_color(&self, palette_idx: u8, color_idx: u8) -> Option<(u8, u8, u8)> { let palette_offset = SPRITE_PALETTES_OFFSET + palette_idx as usize * BYTES_PER_PALLETE; @@ -883,6 +882,21 @@ impl PPU { } pub fn write_ctrl_reg(&mut self, data: u8) { + // let bg_bank = self.regs.ctrl.bits() & 0b11; + // if self.regs.show_background() && data & 0b11 != bg_bank { + // match bg_bank { + // 0 => self.updated_bg_tiles[BYTES_PER_NAMETABLE..].fill(true), + // 1 => self.updated_bg_tiles[..BYTES_PER_NAMETABLE].fill(true), + // _ => {} + // } + // } + + if data & Ctrl::BACKROUND_PATTERN_ADDR.bits() + != self.regs.ctrl.bits() & Ctrl::BACKROUND_PATTERN_ADDR.bits() + { + self.nt_cache.fill(None); + } + self.regs.write_ctrl(data); // the PPU immediately triggers a NMI when the VBlank flag transitions from 0 to 1 during VBlank self.detect_nmi_edge(); @@ -1019,7 +1033,9 @@ impl PPU { match addr { 0x2000 => self.write_ctrl_reg(data), 0x2001 => self.regs.write_mask(data), - 0x2002 => panic!("PPU status register is read-only"), + 0x2002 => { + // panic!("PPU status register is read-only"); + } 0x2003 => self.regs.write_oam_address(data), 0x2004 => self.write_oam_data_reg(data), 0x2005 => self.regs.write_scroll(data), @@ -1028,64 +1044,6 @@ impl PPU { _ => unreachable!("invalid PPU register address"), } } - -<<<<<<< HEAD - // returns number of bytes copied - fn copy_rect( - &self, - offset: usize, - (x1, y1): (usize, usize), - (x2, y2): (usize, usize), - buffer: &mut [u8], - ) -> usize { - let width = x2 - x1; - let height = y2 - y1; - - // for y in 0..height { - // let src_offset = ((y1 + y) * WIDTH + x1) * 3; - // let dst_offset = offset + y * width * 3; - // buffer[dst_offset..(dst_offset + width * 3)] - // .copy_from_slice(&self.frame_buffer_complete[src_offset..(src_offset + width * 3)]); - // } - - let mut index = offset; - - for x in x1..x2 { - for y in y1..y2 { - let i = (y * 2 * WIDTH + x) * 3; - buffer[index] = self.frame_buffer_complete[i]; - index += 1; - } - } - - width * height * 3 - } - - pub fn get_frame(&self, buffer: &mut [u8]) { - // let scroll_x = self.regs.scroll.x as usize; - // self.copy_rect(0, (0, 0), (WIDTH, HEIGHT), buffer); - - // if scroll_x < WIDTH { - // self.copy_rect(0, (scroll_x, 0), (scroll_x + WIDTH, HEIGHT), buffer); - // } else { - // let offset2 = self.copy_rect(0, (scroll_x, 0), (2 * WIDTH, HEIGHT), buffer); - // self.copy_rect(offset2, (0, 0), (scroll_x, HEIGHT), buffer); - // } - - buffer.copy_from_slice(self.frame_buffer_complete.as_slice()); - } - - #[inline] - pub fn get_full_frame(&mut self) -> &[u8] { -======= - pub fn get_frame(&self) -> &[u8] { ->>>>>>> main - self.frame_buffer_complete.as_slice() - } - - pub fn get_updated_tiles_count(&self) -> usize { - self.updated_bg_tiles.iter().filter(|&b| *b).count() - } } impl savestate::Save for SpriteData { diff --git a/src/ppu/registers.rs b/src/ppu/registers.rs index ff46461..59906c9 100644 --- a/src/ppu/registers.rs +++ b/src/ppu/registers.rs @@ -99,7 +99,6 @@ impl Ctrl { } } -<<<<<<< HEAD #[inline] pub fn base_nametable_addr(&self) -> usize { match self.bits() & 0b11 { @@ -112,8 +111,6 @@ impl Ctrl { } #[inline] -======= ->>>>>>> main pub fn sprite_chr_offset(&self) -> u16 { if !self.contains(Ctrl::SPRITE_PATTERN_ADDR) { 0