From dbc385d0a4d84b3251d663e17aa22da354e2611c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Magalh=C3=A3es?= <joamag@gmail.com> Date: Thu, 7 Jul 2022 16:29:22 +0100 Subject: [PATCH] feat: initial sprite drawing support --- src/mmu.rs | 5 +- src/ppu.rs | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 135 insertions(+), 8 deletions(-) diff --git a/src/mmu.rs b/src/mmu.rs index 34292804..360d7f95 100644 --- a/src/mmu.rs +++ b/src/mmu.rs @@ -155,7 +155,10 @@ impl Mmu { | 0xa00 | 0xb00 | 0xc00 | 0xd00 => { self.ram[(addr & 0x1fff) as usize] = value; } - 0xe00 => self.ppu.oam[(addr & 0x009f) as usize] = value, + 0xe00 => { + self.ppu.oam[(addr & 0x009f) as usize] = value; + self.ppu.update_object(addr, value); + } 0xf00 => { if addr == 0xffff { self.ie = value; diff --git a/src/ppu.rs b/src/ppu.rs index cfa871f7..5a005537 100644 --- a/src/ppu.rs +++ b/src/ppu.rs @@ -12,11 +12,17 @@ pub const HRAM_SIZE: usize = 128; pub const OAM_SIZE: usize = 260; pub const PALETTE_SIZE: usize = 4; pub const RGB_SIZE: usize = 3; +pub const TILE_WIDTH: usize = 8; +pub const TILE_HEIGHT: usize = 8; /// The number of tiles that can be store in Game Boy's /// VRAM memory according to specifications. pub const TILE_COUNT: usize = 384; +/// The number of objects/sprites that can be handled at +/// the same time by the Game Boy. +pub const OBJ_COUNT: usize = 40; + /// The width of the Game Boy screen in pixels. pub const DISPLAY_WIDTH: usize = 160; @@ -42,14 +48,31 @@ pub struct Tile { buffer: [u8; 64], } +#[cfg_attr(feature = "wasm", wasm_bindgen)] +#[derive(Clone, Copy, PartialEq)] +pub struct ObjectData { + x: i16, + y: i16, + tile: u8, + palette: u8, + xflip: bool, + yflip: bool, + prio: u8, + num: u8, +} + #[cfg_attr(feature = "wasm", wasm_bindgen)] impl Tile { pub fn get(&self, x: usize, y: usize) -> u8 { - self.buffer[y * 8 + x] + self.buffer[y * TILE_WIDTH + x] } pub fn set(&mut self, x: usize, y: usize, value: u8) { - self.buffer[y * 8 + x] = value; + self.buffer[y * TILE_WIDTH + x] = value; + } + + pub fn get_row(&self, y: usize) -> &[u8] { + &self.buffer[y * TILE_WIDTH..(y + 1) * TILE_WIDTH] } pub fn buffer(&self) -> Vec<u8> { @@ -111,6 +134,10 @@ pub struct Ppu { /// PPU related structures. tiles: [Tile; TILE_COUNT], + /// The meta information about the sprites/objects that are going + /// to be drawn to the screen, + obj_data: [ObjectData; OBJ_COUNT], + /// The palette of colors that is currently loaded in Game Boy /// and used for background (tiles). palette: Palette, @@ -201,6 +228,16 @@ impl Ppu { hram: [0u8; HRAM_SIZE], oam: [0u8; OAM_SIZE], tiles: [Tile { buffer: [0u8; 64] }; TILE_COUNT], + obj_data: [ObjectData { + x: 0, + y: 0, + tile: 0, + palette: 0, + xflip: false, + yflip: false, + prio: 0, + num: 0, + }; OBJ_COUNT], palette: [[0u8; RGB_SIZE]; PALETTE_SIZE], palette_obj_0: [[0u8; RGB_SIZE]; PALETTE_SIZE], palette_obj_1: [[0u8; RGB_SIZE]; PALETTE_SIZE], @@ -273,9 +310,7 @@ impl Ppu { } PpuMode::VramRead => { if self.mode_clock >= 172 { - if self.switch_bg { - self.render_line(); - } + self.render_line(); self.mode_clock = 0; self.mode = PpuMode::HBlank; @@ -283,6 +318,8 @@ impl Ppu { } PpuMode::HBlank => { if self.mode_clock >= 204 { + // increments the register that holds the + // information about the current line in drawign self.ly += 1; // in case we've reached the end of the @@ -476,7 +513,7 @@ impl Ppu { let mut mask; - for x in 0..8 { + for x in 0..TILE_WIDTH { mask = 1 << (7 - x); tile.set( x, @@ -491,7 +528,37 @@ impl Ppu { } } + pub fn update_object(&mut self, addr: u16, value: u8) { + let addr = (addr & 0x01ff) as usize; + let obj_index = addr >> 2; + if obj_index >= OBJ_COUNT { + return; + } + let mut obj_data = self.obj_data[obj_index]; + match addr & 0x03 { + 0x00 => obj_data.y = value as i16 - 16, + 0x01 => obj_data.x = value as i16 - 16, + 0x02 => obj_data.tile = value, + 0x03 => { + obj_data.palette = if value & 0x10 == 0x10 { 1 } else { 0 }; + obj_data.xflip = if value & 0x20 == 0x20 { true } else { false }; + obj_data.yflip = if value & 0x40 == 0x40 { true } else { false }; + obj_data.prio = if value & 0x80 == 0x80 { 1 } else { 0 }; + } + _ => (), + } + } + fn render_line(&mut self) { + if self.switch_bg { + self.render_background(); + } + if self.switch_obj { + self.render_objects(); + } + } + + fn render_background(&mut self) { // obtains the base address of the background map using the bg map flag // that control which background map is going to be used let mut map_offset: usize = if self.bg_map { 0x1c00 } else { 0x1800 }; @@ -540,7 +607,7 @@ impl Ppu { // in case the end of tile width has been reached then // a new tile must be retrieved for plotting - if x == 8 { + if x == TILE_WIDTH { // resets the tile X position to the base value // as a new tile is going to be drawn x = 0; @@ -559,6 +626,63 @@ impl Ppu { } } + fn render_objects(&mut self) { + for index in 0..OBJ_COUNT { + let obj_data = self.obj_data[index]; + + // verifies if the sprite is currently located at the + // current line that is going to be drawn and skips it + // in case it's not + let is_contained = (obj_data.y <= self.ly as i16) + && ((obj_data.y + TILE_HEIGHT as i16) > self.ly as i16); + if !is_contained { + continue; + } + + let palette = if obj_data.palette == 0 { + self.palette_obj_0 + } else { + self.palette_obj_1 + }; + + // calculates the offset in the frame buffer for the sprite + // that is going to be drawn, this is going to be the starting + // point for the draw operation to be performed + let mut frame_offset = (self.ly as usize * DISPLAY_WIDTH + obj_data.x as usize) * RGB_SIZE; + + let tile_offset = self.ly as i16 - obj_data.y; + + let tile_row: &[u8]; + if obj_data.yflip { + tile_row = + self.tiles[obj_data.tile as usize].get_row((7 - tile_offset) as usize); + } else { + tile_row = self.tiles[obj_data.tile as usize].get_row((tile_offset) as usize); + } + + for x in 0..TILE_WIDTH { + let is_contained = (obj_data.x + x as i16 >= 0) && ((obj_data.x + x as i16) < DISPLAY_WIDTH as i16); + if !is_contained { + continue; + } + + //let is_visible = obj_data.prio || !scanrow[obj.x + x] // @todo must implement scanrown latter + + // obtains the current pixel data from the tile row and + // re-maps it according to the object palette + let pixel = tile_row[x]; + let color = palette[pixel as usize]; + + // set the color pixel in the frame buffer + self.frame_buffer[frame_offset] = color[0]; + self.frame_buffer[frame_offset + 1] = color[1]; + self.frame_buffer[frame_offset + 2] = color[2]; + + frame_offset += RGB_SIZE; + } + } + } + /// Obtains the current level of the LCD interrupt by /// checking the current PPU state in various sections. fn interrupt_level(&self) -> bool { -- GitLab