diff --git a/CHANGELOG.md b/CHANGELOG.md index 29b96e5d..983951c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * Support for Zip file in Web frontend +* Support for raw frame buffer +* Lazy evaluation of frame_buffer (on-demand) for DMG ### Changed diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs index 96835116..5f82603b 100644 --- a/frontends/sdl/src/main.rs +++ b/frontends/sdl/src/main.rs @@ -312,7 +312,7 @@ impl Emulator { fn save_image(&mut self, file_path: &str) { let width = self.system.display_width() as u32; let height = self.system.display_height() as u32; - let pixels = self.system.frame_buffer(); + let pixels = self.system.frame_buffer_raw(); let mut image_buffer: ImageBuffer, Vec> = ImageBuffer::new(width, height); diff --git a/src/gb.rs b/src/gb.rs index 000ff555..c86f13f8 100644 --- a/src/gb.rs +++ b/src/gb.rs @@ -589,6 +589,10 @@ impl GameBoy { self.frame_buffer().to_vec() } + pub fn frame_buffer_raw_eager(&mut self) -> Vec { + self.frame_buffer_raw().to_vec() + } + pub fn audio_buffer_eager(&mut self, clear: bool) -> Vec { let buffer = Vec::from(self.audio_buffer().clone()); if clear { @@ -962,7 +966,7 @@ impl GameBoy { } pub fn frame_buffer(&mut self) -> &[u8; FRAME_BUFFER_SIZE] { - &(self.ppu().frame_buffer) + self.ppu().frame_buffer() } pub fn frame_buffer_xrgb8888(&mut self) -> [u8; FRAME_BUFFER_XRGB8888_SIZE] { @@ -989,6 +993,10 @@ impl GameBoy { self.ppu().frame_buffer_rgb565_u16() } + pub fn frame_buffer_raw(&mut self) -> [u8; FRAME_BUFFER_SIZE] { + self.ppu().frame_buffer_raw() + } + pub fn audio_buffer(&mut self) -> &VecDeque { self.apu().audio_buffer() } diff --git a/src/ppu.rs b/src/ppu.rs index 61516362..38fed551 100644 --- a/src/ppu.rs +++ b/src/ppu.rs @@ -56,11 +56,16 @@ pub const DISPLAY_HEIGHT: usize = 144; /// The size in pixels of the display. pub const DISPLAY_SIZE: usize = DISPLAY_WIDTH * DISPLAY_HEIGHT; -/// The size to be used by the buffer of colors -/// for the Game Boy screen the values there should +/// The size to be used by the buffer of color ids +/// for the Game Boy screen, the values there should /// range from 0 to 3. pub const COLOR_BUFFER_SIZE: usize = DISPLAY_SIZE; +/// The size of the buffer that will hold the concrete shade +/// index (0 to 3) for each of the pixel in the screen, so that +/// it is possible to rebuild the pixel buffer from scratch. +pub const SHADE_BUFFER_SIZE: usize = DISPLAY_SIZE; + /// The size of the RGB frame buffer in bytes. pub const FRAME_BUFFER_SIZE: usize = DISPLAY_SIZE * RGB_SIZE; @@ -80,6 +85,8 @@ pub const FRAME_BUFFER_RGB565_SIZE: usize = DISPLAY_SIZE * RGB565_SIZE; /// custom palettes of the Game Boy. pub const PALETTE_COLORS: Palette = [[255, 255, 255], [192, 192, 192], [96, 96, 96], [0, 0, 0]]; +/// Default tile data to be used in the DMG compatibility +/// mode of tile processing (avoids algorithmic forking). pub const DEFAULT_TILE_ATTR: TileData = TileData { palette: 0, vram_bank: 0, @@ -88,6 +95,15 @@ pub const DEFAULT_TILE_ATTR: TileData = TileData { priority: false, }; +/// A basic palette for DMG with the typical high contrast +/// color characteristic of the Game Boy. +pub const BASIC_PALETTE: Palette = [ + [0xff, 0xff, 0xff], + [0xc0, 0xc0, 0xc0], + [0x60, 0x60, 0x60], + [0x00, 0x00, 0x00], +]; + /// Defines the Game Boy pixel type as a buffer /// with the size of RGB (3 bytes). pub type Pixel = [u8; RGB_SIZE]; @@ -311,9 +327,16 @@ pub struct Ppu { /// (from 0 to 3) for all the pixels in the screen. pub color_buffer: Box<[u8; COLOR_BUFFER_SIZE]>, - /// The 8 bit based RGB frame buffer with the - /// processed set of pixels ready to be displayed on screen. - pub frame_buffer: Box<[u8; FRAME_BUFFER_SIZE]>, + /// The shades buffer that is going to store the shade + /// tone value (0 to 3) for all the pixels in the screen (DMG only). + pub shade_buffer: Box<[u8; SHADE_BUFFER_SIZE]>, + + /// The 8 bit based RGB frame buffer with the processed + /// set of pixels ready to be displayed on screen. + /// This value may be lazy computed in case the DMG mode + /// is in use, the lazy evaluation is controlled by + /// the `frame_buffer_index` value. + frame_buffer: Box<[u8; FRAME_BUFFER_SIZE]>, /// The buffer that will control the background to OAM /// priority, allowing the background to be drawn over @@ -491,6 +514,11 @@ pub struct Ppu { /// the identifier wraps on the u16 edges. frame_index: u16, + // Index of the last frame that was rendered, this value is used + // to control the delayed rendering of the frame buffer and should + // avoid some resource usage + frame_buffer_index: u16, + stat_hblank: bool, stat_vblank: bool, stat_oam: bool, @@ -533,6 +561,7 @@ impl Ppu { pub fn new(mode: GameBoyMode, gbc: Rc>) -> Self { Self { color_buffer: Box::new([0u8; COLOR_BUFFER_SIZE]), + shade_buffer: Box::new([0u8; COLOR_BUFFER_SIZE]), frame_buffer: Box::new([0u8; FRAME_BUFFER_SIZE]), priority_buffer: Box::new([false; COLOR_BUFFER_SIZE]), vram: [0u8; VRAM_SIZE], @@ -576,6 +605,7 @@ impl Ppu { palette_address_obj: 0x0, first_frame: false, frame_index: 0, + frame_buffer_index: std::u16::MAX, stat_hblank: false, stat_vblank: false, stat_oam: false, @@ -590,6 +620,7 @@ impl Ppu { pub fn reset(&mut self) { self.color_buffer = Box::new([0u8; COLOR_BUFFER_SIZE]); + self.shade_buffer = Box::new([0u8; SHADE_BUFFER_SIZE]); self.frame_buffer = Box::new([0u8; FRAME_BUFFER_SIZE]); self.priority_buffer = Box::new([false; COLOR_BUFFER_SIZE]); self.vram = [0u8; VRAM_SIZE_CGB]; @@ -629,6 +660,7 @@ impl Ppu { self.palette_address_obj = 0x0; self.first_frame = false; self.frame_index = 0; + self.frame_buffer_index = std::u16::MAX; self.stat_hblank = false; self.stat_vblank = false; self.stat_oam = false; @@ -927,13 +959,35 @@ impl Ppu { } } - pub fn frame_buffer_xrgb8888(&self) -> [u8; FRAME_BUFFER_XRGB8888_SIZE] { + pub fn frame_buffer(&mut self) -> &[u8; FRAME_BUFFER_SIZE] { + if self.gb_mode != GameBoyMode::Dmg { + return &self.frame_buffer; + } + + if self.frame_index == self.frame_buffer_index { + return &self.frame_buffer; + } + + for (index, pixel) in self.frame_buffer.chunks_mut(RGB_SIZE).enumerate() { + let shade_index = self.shade_buffer[index]; + let color = &self.palette_colors[shade_index as usize]; + pixel[0] = color[0]; + pixel[1] = color[1]; + pixel[2] = color[2]; + } + + self.frame_buffer_index = self.frame_index; + &self.frame_buffer + } + + pub fn frame_buffer_xrgb8888(&mut self) -> [u8; FRAME_BUFFER_XRGB8888_SIZE] { + let frame_buffer = self.frame_buffer(); let mut buffer = [0u8; FRAME_BUFFER_XRGB8888_SIZE]; for index in 0..DISPLAY_SIZE { let (r, g, b) = ( - self.frame_buffer[index * RGB_SIZE], - self.frame_buffer[index * RGB_SIZE + 1], - self.frame_buffer[index * RGB_SIZE + 2], + frame_buffer[index * RGB_SIZE], + frame_buffer[index * RGB_SIZE + 1], + frame_buffer[index * RGB_SIZE + 2], ); buffer[index * XRGB8888_SIZE] = b; buffer[index * XRGB8888_SIZE + 1] = g; @@ -943,26 +997,28 @@ impl Ppu { buffer } - pub fn frame_buffer_xrgb8888_u32(&self) -> [u32; FRAME_BUFFER_SIZE] { + pub fn frame_buffer_xrgb8888_u32(&mut self) -> [u32; FRAME_BUFFER_SIZE] { + let frame_buffer = self.frame_buffer(); let mut buffer = [0u32; FRAME_BUFFER_SIZE]; for (index, pixel) in buffer.iter_mut().enumerate().take(DISPLAY_SIZE) { let (r, g, b) = ( - self.frame_buffer[index * RGB_SIZE], - self.frame_buffer[index * RGB_SIZE + 1], - self.frame_buffer[index * RGB_SIZE + 2], + frame_buffer[index * RGB_SIZE], + frame_buffer[index * RGB_SIZE + 1], + frame_buffer[index * RGB_SIZE + 2], ); *pixel = ((r as u32) << 16) | ((g as u32) << 8) | b as u32; } buffer } - pub fn frame_buffer_rgb1555(&self) -> [u8; FRAME_BUFFER_RGB1555_SIZE] { + pub fn frame_buffer_rgb1555(&mut self) -> [u8; FRAME_BUFFER_RGB1555_SIZE] { + let frame_buffer = self.frame_buffer(); let mut buffer = [0u8; FRAME_BUFFER_RGB1555_SIZE]; for index in 0..DISPLAY_SIZE { let (r, g, b) = ( - self.frame_buffer[index * RGB_SIZE], - self.frame_buffer[index * RGB_SIZE + 1], - self.frame_buffer[index * RGB_SIZE + 2], + frame_buffer[index * RGB_SIZE], + frame_buffer[index * RGB_SIZE + 1], + frame_buffer[index * RGB_SIZE + 2], ); let rgb1555 = Self::rgb888_to_rgb1555(r, g, b); buffer[index * RGB1555_SIZE] = rgb1555[0]; @@ -971,26 +1027,28 @@ impl Ppu { buffer } - pub fn frame_buffer_rgb1555_u16(&self) -> [u16; FRAME_BUFFER_SIZE] { + pub fn frame_buffer_rgb1555_u16(&mut self) -> [u16; FRAME_BUFFER_SIZE] { + let frame_buffer = self.frame_buffer(); let mut buffer = [0u16; FRAME_BUFFER_SIZE]; for (index, pixel) in buffer.iter_mut().enumerate().take(DISPLAY_SIZE) { let (r, g, b) = ( - self.frame_buffer[index * RGB_SIZE], - self.frame_buffer[index * RGB_SIZE + 1], - self.frame_buffer[index * RGB_SIZE + 2], + frame_buffer[index * RGB_SIZE], + frame_buffer[index * RGB_SIZE + 1], + frame_buffer[index * RGB_SIZE + 2], ); *pixel = Self::rgb888_to_rgb1555_u16(r, g, b); } buffer } - pub fn frame_buffer_rgb565(&self) -> [u8; FRAME_BUFFER_RGB565_SIZE] { + pub fn frame_buffer_rgb565(&mut self) -> [u8; FRAME_BUFFER_RGB565_SIZE] { + let frame_buffer = self.frame_buffer(); let mut buffer = [0u8; FRAME_BUFFER_RGB565_SIZE]; for index in 0..DISPLAY_SIZE { let (r, g, b) = ( - self.frame_buffer[index * RGB_SIZE], - self.frame_buffer[index * RGB_SIZE + 1], - self.frame_buffer[index * RGB_SIZE + 2], + frame_buffer[index * RGB_SIZE], + frame_buffer[index * RGB_SIZE + 1], + frame_buffer[index * RGB_SIZE + 2], ); let rgb565 = Self::rgb888_to_rgb565(r, g, b); buffer[index * RGB565_SIZE] = rgb565[0]; @@ -999,19 +1057,48 @@ impl Ppu { buffer } - pub fn frame_buffer_rgb565_u16(&self) -> [u16; FRAME_BUFFER_SIZE] { + pub fn frame_buffer_rgb565_u16(&mut self) -> [u16; FRAME_BUFFER_SIZE] { + let frame_buffer = self.frame_buffer(); let mut buffer = [0u16; FRAME_BUFFER_SIZE]; for (index, pixel) in buffer.iter_mut().enumerate().take(DISPLAY_SIZE) { let (r, g, b) = ( - self.frame_buffer[index * RGB_SIZE], - self.frame_buffer[index * RGB_SIZE + 1], - self.frame_buffer[index * RGB_SIZE + 2], + frame_buffer[index * RGB_SIZE], + frame_buffer[index * RGB_SIZE + 1], + frame_buffer[index * RGB_SIZE + 2], ); *pixel = Self::rgb888_to_rgb565_u16(r, g, b); } buffer } + /// Obtains the "raw" version of the frame buffer any custom + /// color palette operation applied to it. This is can be an + /// extremely slow operation (in DMG devices) and because of + /// that should be used carefully. + pub fn frame_buffer_raw(&self) -> [u8; FRAME_BUFFER_SIZE] { + self.frame_buffer_palette(&BASIC_PALETTE) + } + + /// Obtains the frame buffer with the colors mapped according + /// to the provided palette of colors. + /// This method is very slow and only useful for the DMG mode + /// which can have its simple colors mapped to palettes. + pub fn frame_buffer_palette(&self, palette_colors: &Palette) -> [u8; FRAME_BUFFER_SIZE] { + if self.gb_mode == GameBoyMode::Dmg { + let mut buffer = [0u8; FRAME_BUFFER_SIZE]; + for (index, pixel) in buffer.chunks_mut(RGB_SIZE).enumerate() { + let shade_index = self.shade_buffer[index]; + let color = &palette_colors[shade_index as usize]; + pixel[0] = color[0]; + pixel[1] = color[1]; + pixel[2] = color[2]; + } + buffer + } else { + *self.frame_buffer.clone() + } + } + pub fn vram(&self) -> &[u8; VRAM_SIZE] { &self.vram } @@ -1149,21 +1236,24 @@ impl Ppu { } /// Fills the frame buffer with pixels of the provided color, - /// this method must represent the fastest way of achieving + /// this method should represent the fastest way of achieving /// the fill background with color operation. - pub fn fill_frame_buffer(&mut self, color: Pixel) { + pub fn fill_frame_buffer(&mut self, shade_index: u8) { + let color = &self.palette_colors[shade_index as usize]; self.color_buffer.fill(0); - for index in (0..self.frame_buffer.len()).step_by(RGB_SIZE) { - self.frame_buffer[index] = color[0]; - self.frame_buffer[index + 1] = color[1]; - self.frame_buffer[index + 2] = color[2]; + self.shade_buffer.fill(shade_index); + self.frame_buffer_index = std::u16::MAX; + for pixel in self.frame_buffer.chunks_mut(RGB_SIZE) { + pixel[0] = color[0]; + pixel[1] = color[1]; + pixel[2] = color[2]; } } /// Clears the current frame buffer, setting the background color /// for all the pixels in the frame buffer. pub fn clear_frame_buffer(&mut self) { - self.fill_frame_buffer(self.palette_colors[0]); + self.fill_frame_buffer(0); } /// Prints the tile data information to the stdout, this is @@ -1390,6 +1480,10 @@ impl Ppu { &self.palette_bg }; + // obtains the current integer value (raw) for the background palette + // this is going to be used for shade index value computation (DMG only) + let palette_v = self.palettes[0]; + // obtains the values of both X and Y flips for the current tile // they will be applied by the get tile pixel method let mut xflip = tile_attr.xflip; @@ -1429,16 +1523,18 @@ impl Ppu { // to draw the background map, note that the initial index is used // to skip the drawing of the tiles that are not visible (WX) for _ in initial_index..DISPLAY_WIDTH { - // obtains the current pixel data from the tile and - // re-maps it according to the current palette + // obtains the current pixel data from the tile let pixel = tile.get_flipped(x, y, xflip, yflip); - let color = &palette[pixel as usize]; // updates the pixel in the color buffer, which stores - // the raw pixel color information (unmapped) + // the raw pixel color information (unmapped) and then + // updates the shade buffer with the shade index self.color_buffer[color_offset] = pixel; + self.shade_buffer[color_offset] = (palette_v >> (pixel * 2)) & 3; - // set the color pixel in the frame buffer + // re-maps the pixel according to the current palette + // and sets the color pixel in the frame buffer + let color = &palette[pixel as usize]; self.frame_buffer[frame_offset] = color[0]; self.frame_buffer[frame_offset + 1] = color[1]; self.frame_buffer[frame_offset + 2] = color[2]; @@ -1536,9 +1632,9 @@ impl Ppu { // which stores Game Boy colors from 0 to 3 let mut color_offset = self.ly as usize * DISPLAY_WIDTH; - // calculates the frame buffer offset position assuming the proper - // Game Boy screen width and RGB pixel (3 bytes) size - let mut frame_offset = self.ly as usize * DISPLAY_WIDTH * RGB_SIZE; + // obtains the current integer value (raw) for the background palette + // this is going to be used for shade index value computation (DMG only) + let palette_v = self.palettes[0]; // calculates both the current Y and X positions within the tiles // using the bitwise and operation as an effective modulus 8 @@ -1549,25 +1645,19 @@ impl Ppu { // allows us to position the background map properly in the display let initial_index = max(wx as i16 - 7, 0) as usize; color_offset += initial_index; - frame_offset += initial_index * RGB_SIZE; // iterates over all the pixels in the current line of the display // to draw the background map, note that the initial index is used // to skip the drawing of the tiles that are not visible (WX) for _ in initial_index..DISPLAY_WIDTH { - // obtains the current pixel data from the tile and - // re-maps it according to the current palette + // obtains the current pixel data from the tile let pixel = tile.get(x, y); - let color = &self.palette_bg[pixel as usize]; // updates the pixel in the color buffer, which stores - // the raw pixel color information (unmapped) + // the raw pixel color information (unmapped) and then + // updates the shade buffer with the shade index self.color_buffer[color_offset] = pixel; - - // 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]; + self.shade_buffer[color_offset] = (palette_v >> (pixel * 2)) & 3; // increments the current tile X position in drawing x += 1; @@ -1597,10 +1687,6 @@ impl Ppu { // increments the color offset by one, representing // the drawing of one pixel color_offset += 1; - - // increments the offset of the frame buffer by the - // size of an RGB pixel (which is 3 bytes) - frame_offset += RGB_SIZE; } } @@ -1661,26 +1747,30 @@ impl Ppu { continue; } - let palette = if self.gb_mode == GameBoyMode::Cgb { + let (palette, palette_index) = if self.gb_mode == GameBoyMode::Cgb { if self.dmg_compat { if obj.palette == 0 { - &self.palette_obj_0 + (&self.palette_obj_0, 0_u8) } else if obj.palette == 1 { - &self.palette_obj_1 + (&self.palette_obj_1, 0_u8) } else { panic!("Invalid object palette: {:02x}", obj.palette); } } else { - &self.palettes_color_obj[obj.palette_cgb as usize] + (&self.palettes_color_obj[obj.palette_cgb as usize], 0_u8) } } else if obj.palette == 0 { - &self.palette_obj_0 + (&self.palette_obj_0, 1_u8) } else if obj.palette == 1 { - &self.palette_obj_1 + (&self.palette_obj_1, 2_u8) } else { panic!("Invalid object palette: {:02x}", obj.palette); }; + // obtains the current integer value (raw) for the palette in use + // this is going to be used for shade index value computation (DMG only) + let palette_v = self.palettes[palette_index as usize]; + // calculates the offset in the color buffer (raw color information // from 0 to 3) for the sprit that is going to be drawn, this value // is kept as a signed integer to allow proper negative number math @@ -1776,15 +1866,15 @@ impl Ppu { // to be used in priority calculus index_buffer[x as usize] = obj.x; - // obtains the current pixel data from the tile row and - // re-maps it according to the object palette - let color = palette[pixel as usize]; - // updates the pixel in the color buffer, which stores - // the raw pixel color information (unmapped) + // the raw pixel color information (unmapped) and then + // updates the shade buffer with the shade index self.color_buffer[color_offset as usize] = pixel; + self.shade_buffer[color_offset as usize] = (palette_v >> (pixel * 2)) & 3; - // sets the color pixel in the frame buffer + // re-maps the pixel according to the object palette + // and then sets the color pixel in the frame buffer + let color = &palette[pixel as usize]; self.frame_buffer[frame_offset as usize] = color[0]; self.frame_buffer[frame_offset as usize + 1] = color[1]; self.frame_buffer[frame_offset as usize + 2] = color[2]; diff --git a/src/state.rs b/src/state.rs index cf503389..9aa6d3e8 100644 --- a/src/state.rs +++ b/src/state.rs @@ -430,7 +430,7 @@ impl Serialize for BosImageBuffer { impl State for BosImageBuffer { fn from_gb(gb: &mut GameBoy) -> Result { - Ok(Self::new(*gb.ppu_i().frame_buffer)) + Ok(Self::new(gb.ppu_i().frame_buffer_raw())) } fn to_gb(&self, _gb: &mut GameBoy) -> Result<(), String> {