From e5cbc7a70ccefc1665972a7d9274ae0f00ba1443 Mon Sep 17 00:00:00 2001 From: Jonathan Balls Date: Sun, 15 Sep 2024 12:32:01 +0200 Subject: [PATCH] introduce progressive scanline rendering --- src/cpu/mod.rs | 10 ++--- src/main.rs | 7 ++- src/mmu/ppu.rs | 115 +++++++++++++++++++++++++++++++++++++++++------- src/renderer.rs | 113 +++++------------------------------------------ 4 files changed, 118 insertions(+), 127 deletions(-) diff --git a/src/cpu/mod.rs b/src/cpu/mod.rs index 63690d7..0a74af4 100644 --- a/src/cpu/mod.rs +++ b/src/cpu/mod.rs @@ -169,11 +169,11 @@ impl CPU { // stat if mmu.ie & 2 > 0 && mmu.ppu.stat_irq { - //mmu.ppu.stat_irq = false; - //self.ime = false; - // - //self.push(mmu, return_pc); - //self.registers.pc = 0x48; + mmu.ppu.stat_irq = false; + self.ime = false; + + self.push(mmu, return_pc); + self.registers.pc = 0x48; } // timer diff --git a/src/main.rs b/src/main.rs index 115f3a6..18a00bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,6 @@ use std::thread; use gameboy::GameBoy; use minifb::Key; -use mmu::ppu::PPU; use renderer::window_loop; pub static DEBUG_MODE: AtomicBool = AtomicBool::new(false); @@ -48,7 +47,7 @@ fn main() { enable_gameboy_doctor(); } - let (tx, rx) = mpsc::channel::(); + let (tx, rx) = mpsc::channel::>(); let (tx_key, rx_key) = mpsc::channel::<(bool, Key)>(); let rom = read_file_to_bytes(rom_path.as_str()).unwrap(); let game_title = CartridgeHeader::new(&rom).unwrap().title(); @@ -57,7 +56,7 @@ fn main() { window_loop(rx, tx_key, &game_title); } -fn emulator_loop(rom: Vec, tx: Sender, rx: Receiver<(bool, Key)>) { +fn emulator_loop(rom: Vec, tx: Sender>, rx: Receiver<(bool, Key)>) { let mut gameboy = GameBoy::new(rom); ctrlc::set_handler(move || { @@ -84,7 +83,7 @@ fn emulator_loop(rom: Vec, tx: Sender, rx: Receiver<(bool, Key)>) { // Render window if gameboy.mmu.ppu.get_and_reset_frame_available() { - let _ = tx.send(gameboy.mmu.ppu.clone()); + let _ = tx.send(gameboy.mmu.ppu.frame_buffer.clone()); } // Handle joypad input diff --git a/src/mmu/ppu.rs b/src/mmu/ppu.rs index d9a6c03..7b30a2f 100644 --- a/src/mmu/ppu.rs +++ b/src/mmu/ppu.rs @@ -8,10 +8,27 @@ use crate::debugger::is_gameboy_doctor; const VRAM_SIZE: usize = 0x2000; const VOAM_SIZE: usize = 0xA0; +const SCREEN_WIDTH: usize = 160; +const SCREEN_HEIGHT: usize = 144; + +const TARGET_FPS: f64 = 30.0; + pub type Tile = [[u8; 8]; 8]; +fn palette(id: u8) -> u32 { + match id { + 0x0 => 0xFFFFFFFF, + 0x1 => 0xFF666666, + 0x2 => 0xFFBBBBBB, + 0x3 => 0xFF000000, + _ => unreachable!(), + } +} + #[derive(Clone)] pub struct PPU { + pub frame_buffer: Vec, + frame_available: bool, frame_number: u32, last_frame_time: Instant, @@ -41,6 +58,7 @@ pub struct PPU { impl PPU { pub fn new() -> PPU { PPU { + frame_buffer: vec![0; SCREEN_WIDTH * SCREEN_HEIGHT], frame_available: false, frame_number: 1, last_frame_time: Instant::now(), @@ -89,11 +107,15 @@ impl PPU { self.stat_irq = true; } + if self.ly < SCREEN_HEIGHT as u8 { + self.render_scanline(self.ly); + } + // Frame finished - flush to screen if self.ly == 0 { // Calculate how long to sleep let elapsed = self.last_frame_time.elapsed(); - let frame_duration = Duration::from_secs_f64(1.0 / 60.0); + let frame_duration = Duration::from_secs_f64(1.0 / TARGET_FPS); if elapsed < frame_duration { thread::sleep(frame_duration - elapsed); @@ -148,7 +170,7 @@ impl PPU { 0xFF48 => self.obj_palette_0, 0xFF49 => self.obj_palette_1, - 0xFe00..=0xFE9F => self.voam[(addr - 0xFE00) as usize], + 0xFE00..=0xFE9F => self.voam[(addr - 0xFE00) as usize], _ => { println!("tried to read {:#04x}", addr); @@ -195,14 +217,31 @@ impl PPU { return result; } - pub fn get_tile(&self, tile_index: u8) -> Tile { - let start_address = if self.lcdc & 0x10 > 0 { + pub fn get_tile_pixel( + &self, + tile_index: u8, + line_index: u16, + col_index: u16, + use_8000: bool, + ) -> u8 { + let start_address = if self.lcdc & 0x10 > 0 || use_8000 { 0x8000 + ((tile_index as u16) * 16) as u16 } else { let offset = ((tile_index as i8) as i16) * 16; 0x9000_u16.wrapping_add(offset as u16) }; + let byte_a = self.get_byte(start_address + (2 * line_index)); + let byte_b = self.get_byte(start_address + (2 * line_index) + 1); + + let bit1 = (byte_a >> 7 - col_index) & 1; + let bit2 = (byte_b >> 7 - col_index) & 1; + + return ((bit2 << 1) | bit1) as u8; + } + + pub fn get_object(&self, tile_index: u8) -> Tile { + let start_address = 0x8000 + ((tile_index as u16) * 16) as u16; let mut ret = [[0u8; 8]; 8]; for i in 0..8 { @@ -220,23 +259,67 @@ impl PPU { ret } - pub fn get_object(&self, tile_index: u8) -> Tile { - let start_address = 0x8000 + ((tile_index as u16) * 16) as u16; - let mut ret = [[0u8; 8]; 8]; + fn render_scanline(&mut self, line: u8) { + // calculate background scanline + let buffer_y_offset = (line as usize) * SCREEN_WIDTH; + let background_y = line.wrapping_add(self.scy); + let tile_map_row = background_y / 8; - for i in 0..8 { - let byte_a = self.get_byte(start_address + (2 * i)); - let byte_b = self.get_byte(start_address + (2 * i) + 1); + for buffer_x_offset in 0..SCREEN_WIDTH { + let background_x = (buffer_x_offset + self.scx as usize) % 256; - for j in 0..8 { - let bit1 = (byte_a >> 7 - j) & 1; - let bit2 = (byte_b >> 7 - j) & 1; + let tile_map_index = (tile_map_row as usize * 32) + (background_x / 8); + let tile_index = self.get_byte(0x9800 + tile_map_index as u16); - ret[j as usize][i as usize] = ((bit2 << 1) | bit1) as u8; - } + let tile_line = background_y % 8; + let tile_col = background_x % 8; + + let tile_pixel = + self.get_tile_pixel(tile_index, tile_line as u16, tile_col as u16, false); + + self.frame_buffer[buffer_y_offset + buffer_x_offset] = palette(tile_pixel); } - ret + // For each object we should + for i in 0..40 { + let start_position = 0xFE00 + i * 4; + + let object_y = self.get_byte(start_position); + let object_line = line.wrapping_sub(object_y).wrapping_add(16); + if object_line >= 8 { + continue; + } + let x_position = self.get_byte(start_position + 1); + let tile_index = self.get_byte(start_position + 2); + let flags = self.get_byte(start_position + 3); + + let x_flip = flags & 0x20 == 0x20; + let y_flip = flags & 0x40 == 0x40; + + for x_offset in 0..8 { + let x_position = if x_flip { + (x_position as usize).wrapping_sub(x_offset as usize) + } else { + (x_position as usize) + .wrapping_add(x_offset as usize) + .wrapping_sub(8) + }; + + if x_position >= SCREEN_WIDTH { + continue; + } + + let tile_line = if y_flip { 8 - object_line } else { object_line }; + + let tile_pixel = self.get_tile_pixel(tile_index, tile_line as u16, x_offset, true); + + if tile_pixel == 0 { + continue; + } + + self.frame_buffer[buffer_y_offset + x_position] = palette(tile_pixel); + } + } } } diff --git a/src/renderer.rs b/src/renderer.rs index 90b241d..6d77d8b 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,62 +1,14 @@ use minifb::{Key, KeyRepeat, Scale, Window, WindowOptions}; -use std::sync::mpsc::{Receiver, Sender, TryRecvError}; - -use crate::mmu::ppu::{Tile, PPU}; +use std::{ + collections::VecDeque, + sync::mpsc::{Receiver, Sender, TryRecvError}, + time::Instant, +}; const SCREEN_WIDTH: usize = 160; const SCREEN_HEIGHT: usize = 144; -fn palette(id: u8) -> u32 { - match id { - 0x0 => 0xFFFFFFFF, - 0x1 => 0xFF666666, - 0x2 => 0xFFBBBBBB, - 0x3 => 0xFF000000, - _ => unreachable!(), - } -} - -fn render_tile( - buffer: &mut Vec, - tile: Tile, - screen_x: u8, - screen_y: u8, - flags: u8, - transparency: bool, -) { - let x_flip = (flags & 0x20) > 0; - let y_flip = (flags & 0x40) > 0; - - for tile_y in 0..8 { - for tile_x in 0..8 { - let x_offset: usize = if x_flip { - screen_x.wrapping_add(8).wrapping_sub(tile_x) as usize - } else { - screen_x.wrapping_add(tile_x) as usize - }; - - let y_offset: usize = if y_flip { - let o = screen_y.wrapping_add(8).wrapping_sub(tile_y) as usize; - o * SCREEN_WIDTH - } else { - let o = screen_y.wrapping_add(tile_y) as usize; - o * SCREEN_WIDTH - }; - - if x_offset >= SCREEN_WIDTH || y_offset > (SCREEN_HEIGHT - 1) * SCREEN_WIDTH { - continue; - } - - if transparency && tile[tile_x as usize][tile_y as usize] == 0 { - continue; - } - - buffer[y_offset + x_offset] = palette(tile[tile_x as usize][tile_y as usize]); - } - } -} - -fn latest_ppu(rx: &Receiver) -> Option { +fn most_recent_frame(rx: &Receiver>) -> Option> { let mut latest_frame = None; loop { @@ -77,9 +29,7 @@ fn latest_ppu(rx: &Receiver) -> Option { latest_frame } -pub fn window_loop(rx: Receiver, tx: Sender<(bool, Key)>, game_title: &String) { - let mut buffer: Vec = vec![0; SCREEN_WIDTH as usize * SCREEN_HEIGHT as usize]; - +pub fn window_loop(rx: Receiver>, tx: Sender<(bool, Key)>, game_title: &String) { let mut window = Window::new( format!("Cowboy Emulator - {}", game_title).as_str(), SCREEN_WIDTH as usize, @@ -92,51 +42,10 @@ pub fn window_loop(rx: Receiver, tx: Sender<(bool, Key)>, game_title: &Stri .unwrap(); while window.is_open() && !window.is_key_down(Key::Escape) { - // We unwrap here as we want this code to exit if it fails. Real applications may want to - // handle this in a different way - window - .update_with_buffer(&buffer, SCREEN_WIDTH as usize, SCREEN_HEIGHT as usize) - .unwrap(); - - // Receive frame buffer from the emulator - if let Some(ppu) = latest_ppu(&rx) { - // Draw tiles - for i in 0..1024 { - let tile_index = ppu.get_byte(0x9800 + i); - - // Calculate location on the background map - let tile_x = ((i % 32) * 8) as u8; - let tile_y = ((i / 32) * 8) as u8; - - render_tile( - &mut buffer, - ppu.get_tile(tile_index), - tile_x.wrapping_sub(ppu.scx), // ensure wrapping - tile_y.wrapping_sub(ppu.scy), - 0x0, - false, - ) - } - - // Draw objects - for i in 0..40 { - let start_position = 0xFE00 + i * 4; - - let y_position = ppu.get_byte(start_position); - - let x_position = ppu.get_byte(start_position + 1); - let tile_index = ppu.get_byte(start_position + 2); - let flags = ppu.get_byte(start_position + 3); - - render_tile( - &mut buffer, - ppu.get_object(tile_index), - x_position.wrapping_sub(8), - y_position.wrapping_sub(16), - flags, - true, - ); - } + if let Some(frame_buffer) = most_recent_frame(&rx) { + window + .update_with_buffer(&frame_buffer, SCREEN_WIDTH as usize, SCREEN_HEIGHT as usize) + .unwrap(); } // dispatch unreleased keys