//! PPU (Picture Processing Unit) functions and structures. use core::fmt; use std::{ borrow::BorrowMut, cell::RefCell, cmp::max, fmt::{Display, Formatter}, rc::Rc, }; use crate::{ gb::{GameBoyConfig, GameBoyMode}, warnln, }; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; pub const VRAM_SIZE_DMG: usize = 8192; pub const VRAM_SIZE_CGB: usize = 16384; pub const VRAM_SIZE: usize = VRAM_SIZE_CGB; 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 RGBA_SIZE: usize = 4; pub const RGB888_SIZE: usize = 3; pub const XRGB8888_SIZE: usize = 4; pub const RGB1555_SIZE: usize = 2; pub const RGB565_SIZE: usize = 2; pub const TILE_WIDTH: usize = 8; pub const TILE_HEIGHT: usize = 8; pub const TILE_WIDTH_I: usize = 7; pub const TILE_HEIGHT_I: usize = 7; pub const TILE_DOUBLE_HEIGHT: usize = 16; pub const TILE_COUNT_DMG: usize = 384; pub const TILE_COUNT_CGB: usize = 768; /// The number of tiles that can be store in Game Boy's /// VRAM memory according to specifications. pub const TILE_COUNT: usize = TILE_COUNT_CGB; /// 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; /// The height of the Game Boy screen in pixels. 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 /// range from 0 to 3. pub const COLOR_BUFFER_SIZE: usize = DISPLAY_SIZE; /// The size of the RGB frame buffer in bytes. pub const FRAME_BUFFER_SIZE: usize = DISPLAY_SIZE * RGB_SIZE; /// The size of the RGB888 frame buffer in bytes. pub const FRAME_BUFFER_RGB888_SIZE: usize = DISPLAY_SIZE * RGB888_SIZE; /// The size of the XRGB8888 frame buffer in bytes. pub const FRAME_BUFFER_XRGB8888_SIZE: usize = DISPLAY_SIZE * XRGB8888_SIZE; /// The size of the RGB1555 frame buffer in bytes. pub const FRAME_BUFFER_RGB1555_SIZE: usize = DISPLAY_SIZE * RGB1555_SIZE; /// The size of the RGB565 frame buffer in bytes. pub const FRAME_BUFFER_RGB565_SIZE: usize = DISPLAY_SIZE * RGB565_SIZE; /// The base colors to be used to populate the /// custom palettes of the Game Boy. pub const PALETTE_COLORS: Palette = [[255, 255, 255], [192, 192, 192], [96, 96, 96], [0, 0, 0]]; pub const DEFAULT_TILE_ATTR: TileData = TileData { palette: 0, vram_bank: 0, xflip: false, yflip: false, priority: false, }; /// Defines the Game Boy pixel type as a buffer /// with the size of RGB (3 bytes). pub type Pixel = [u8; RGB_SIZE]; /// Defines a transparent Game Boy pixel type as a buffer /// with the size of RGBA (4 bytes). pub type PixelAlpha = [u8; RGBA_SIZE]; /// Defines a pixel with 5 bits per channel plus a padding /// bit at the beginning. pub type PixelRgb1555 = [u8; RGB1555_SIZE]; /// Defines a pixel with 5 bits per channel except for the /// green channel which uses 6 bits. pub type PixelRgb565 = [u8; RGB565_SIZE]; /// Defines a type that represents a color palette /// within the Game Boy context. pub type Palette = [Pixel; PALETTE_SIZE]; /// Defines a type that represents a color palette /// with alpha within the Game Boy context. pub type PaletteAlpha = [PixelAlpha; PALETTE_SIZE]; /// Represents a palette with the metadata that is /// associated with it. #[cfg_attr(feature = "wasm", wasm_bindgen)] #[derive(Clone, PartialEq, Eq)] pub struct PaletteInfo { name: String, colors: Palette, } impl PaletteInfo { pub fn new(name: &str, colors: Palette) -> Self { Self { name: String::from(name), colors, } } pub fn name(&self) -> &String { &self.name } pub fn colors(&self) -> &Palette { &self.colors } } /// Represents a tile within the Game Boy context, /// should contain the pixel buffer of the tile. /// The tiles are always 8x8 pixels in size. #[cfg_attr(feature = "wasm", wasm_bindgen)] #[derive(Clone, Copy, PartialEq, Eq)] pub struct Tile { /// The buffer for the tile, should contain a byte /// per each pixel of the tile with values ranging /// from 0 to 3 (4 colors). buffer: [u8; 64], } #[cfg_attr(feature = "wasm", wasm_bindgen)] impl Tile { pub fn get(&self, x: usize, y: usize) -> u8 { self.buffer[y * TILE_WIDTH + x] } pub fn get_flipped(&self, x: usize, y: usize, xflip: bool, yflip: bool) -> u8 { let x: usize = if xflip { TILE_WIDTH_I - x } else { x }; let y = if yflip { TILE_HEIGHT_I - y } else { y }; self.buffer[y * TILE_WIDTH + x] } pub fn set(&mut self, x: usize, y: usize, value: u8) { self.buffer[y * TILE_WIDTH + x] = value; } pub fn buffer(&self) -> Vec<u8> { self.buffer.to_vec() } } impl Tile { pub fn get_row(&self, y: usize) -> &[u8] { &self.buffer[y * TILE_WIDTH..(y + 1) * TILE_WIDTH] } } impl Tile { pub fn palette_buffer(&self, palette: Palette) -> Vec<u8> { self.buffer .iter() .flat_map(|p| palette[*p as usize]) .collect() } } impl Display for Tile { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let mut buffer = String::new(); for y in 0..8 { for x in 0..8 { buffer.push_str(format!("{}", self.get(x, y)).as_str()); } buffer.push('\n'); } write!(f, "{}", buffer) } } #[cfg_attr(feature = "wasm", wasm_bindgen)] #[derive(Clone, Copy, PartialEq, Eq)] pub struct ObjectData { x: i16, y: i16, tile: u8, palette_cgb: u8, tile_bank: u8, palette: u8, xflip: bool, yflip: bool, bg_over: bool, index: u8, } impl ObjectData { pub fn new() -> Self { Self { x: 0, y: 0, tile: 0, palette_cgb: 0, tile_bank: 0, palette: 0, xflip: false, yflip: false, bg_over: false, index: 0, } } } impl Default for ObjectData { fn default() -> Self { Self::new() } } impl Display for ObjectData { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!( f, "Index: {}, X: {}, Y: {}, Tile: {}", self.index, self.x, self.y, self.tile ) } } #[cfg_attr(feature = "wasm", wasm_bindgen)] #[derive(Clone, Copy, PartialEq, Eq)] pub struct TileData { palette: u8, vram_bank: u8, xflip: bool, yflip: bool, priority: bool, } impl TileData { pub fn new() -> Self { Self { palette: 0, vram_bank: 0, xflip: false, yflip: false, priority: false, } } } impl Default for TileData { fn default() -> Self { Self::new() } } impl Display for TileData { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!( f, "Palette: {}, VRAM Bank: {}, X Flip: {}, Y Flip: {}", self.palette, self.vram_bank, self.xflip, self.yflip ) } } pub struct PpuRegisters { pub scy: u8, pub scx: u8, pub wy: u8, pub wx: u8, pub ly: u8, pub lyc: u8, } /// Represents the Game Boy PPU (Pixel Processing Unit) and controls /// all of the logic behind the graphics processing and presentation. /// Should store both the VRAM and HRAM together with the internal /// graphic related registers. /// Outputs the screen as a RGB 8 bit frame buffer. /// /// # Basic usage /// ```rust /// use boytacean::ppu::Ppu; /// let mut ppu = Ppu::default(); /// ppu.clock(8); /// ``` pub struct Ppu { /// The color buffer that is going to store the colors /// (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 buffer that will control the background to OAM /// priority, allowing the background to be drawn over /// the sprites/objects if necessary. priority_buffer: Box<[bool; COLOR_BUFFER_SIZE]>, /// Video dedicated memory (VRAM) where both the tiles and /// the sprites/objects are going to be stored. vram: [u8; VRAM_SIZE], /// High RAM memory that should provide extra speed for regular /// operations. hram: [u8; HRAM_SIZE], /// OAM RAM (Sprite Attribute Table ) used for the storage of the /// sprite attributes for each of the 40 sprites of the Game Boy. oam: [u8; OAM_SIZE], /// The VRAM bank to be used in the read and write operation of /// the 0x8000-0x9FFF memory range (CGB only). vram_bank: u8, /// The offset to be used in the read and write operation of /// the VRAM, this value should be consistent with the VRAM bank /// that is currently selected (CGB only). vram_offset: u16, /// The current set of processed tiles that are store in the /// 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 base colors that are going to be used in the registration /// of the concrete palettes, this value basically controls the /// colors that are going to be shown for each of the four base /// values - 0x00, 0x01, 0x02, and 0x03. palette_colors: Palette, /// The palette of colors that is currently loaded in Game Boy /// and used for background (tiles) and window. palette_bg: Palette, /// The palette that is going to be used for sprites/objects #0. palette_obj_0: Palette, /// The palette that is going to be used for sprites/objects #1. palette_obj_1: Palette, /// The complete set of background palettes that are going to be /// used in CGB emulation to provide the full set of colors (CGB only). palettes_color_bg: [Palette; 8], /// The complete set of object/sprite palettes that are going to be /// used in CGB emulation to provide the full set of colors (CGB only). palettes_color_obj: [Palette; 8], /// The complete set of palettes in binary data so that they can /// be re-read if required by the system. palettes: [u8; 3], /// The raw binary information (64 bytes) for the color palettes, /// contains binary information for both the background and /// the objects palettes (CGB only). palettes_color: [[u8; 64]; 2], /// The complete list of attributes for the first background /// map that is located in 0x9800-0x9BFF (CGB only). bg_map_attrs_0: [TileData; 1024], /// The complete list of attributes for the second background /// map that is located in 0x9C00-0x9FFF (CGB only). bg_map_attrs_1: [TileData; 1024], /// The flag that controls if the object/sprite priority /// if set means that the priority mode to be used is the /// X coordinate otherwise the normal CGB OAM memory mode /// mode is used, the value of this flag is controlled by /// the OPRI register (CGB only) obj_priority: bool, /// The scroll Y register that controls the Y offset /// of the background. scy: u8, /// The scroll X register that controls the X offset /// of the background. scx: u8, /// The top most Y coordinate of the window, /// going to be used while drawing the window. wy: u8, /// The top most X coordinate of the window plus 7, /// going to be used while drawing the window. wx: u8, /// The current scan line in processing, should /// range between 0 (0x00) and 153 (0x99), representing /// the 154 lines plus 10 extra V-Blank lines. ly: u8, /// The line compare register that is going to be used /// in the STATE and associated interrupts. lyc: u8, /// The current execution mode of the PPU, should change /// between states over the drawing of a frame. mode: PpuMode, /// Internal clock counter used to control the time in ticks /// spent in each of the PPU modes. mode_clock: u16, /// Controls if the background is going to be drawn to screen. /// In CGB mode this flag controls the master priority instead /// enabling or disabling complex priority rules. switch_bg: bool, /// Controls if the sprites/objects are going to be drawn to screen. switch_obj: bool, /// Defines the size in pixels of the object (false=8x8, true=8x16). obj_size: bool, /// Controls the map that is going to be drawn to screen, the /// offset in VRAM will be adjusted according to this /// (false=0x9800, true=0x9c000). bg_map: bool, /// If the background tile set is active meaning that the /// negative based indexes are going to be used. bg_tile: bool, /// Controls if the window is meant to be drawn. switch_window: bool, /// Controls the offset of the map that is going to be drawn /// for the window section of the screen. window_map: bool, /// Flag that controls if the LCD screen is ON and displaying /// content. switch_lcd: bool, // Internal window counter value used to control the lines that // were effectively rendered as part of the window tile drawing process. // A line is only considered rendered when the WX and WY registers // are within the valid screen range and the window switch register // is valid. window_counter: u8, /// If the auto increment of the background color palette is enabled /// so that the next address is going to be set on every write. auto_increment_bg: bool, /// The current address in usage for the background color palettes. palette_address_bg: u8, /// If the auto increment of the object/sprite color palette is enabled /// so that the next address is going to be set on every write. auto_increment_obj: bool, /// The current address in usage for the object/sprite color palettes. palette_address_obj: u8, /// Flag that controls if the frame currently in rendering is the /// first one, preventing actions. first_frame: bool, /// Almost unique identifier of the frame that can be used to debug /// and uniquely identify the frame that is currently ind drawing, /// the identifier wraps on the u16 edges. frame_index: u16, stat_hblank: bool, stat_vblank: bool, stat_oam: bool, stat_lyc: bool, /// Boolean value set when the V-Blank interrupt should be handled /// by the next CPU clock operation. int_vblank: bool, /// Boolean value when the LCD STAT interrupt should be handled by /// the next CPU clock operation. int_stat: bool, /// Flag that controls if the DMG compatibility mode is /// enabled meaning that some of the PPU decisions will /// be made differently to address this special situation /// (CGB only). dmg_compat: bool, /// The current running mode of the emulator, this /// may affect many aspects of the emulation. gb_mode: GameBoyMode, /// The pointer to the parent configuration of the running /// Game Boy emulator, that can be used to control the behaviour /// of Game Boy emulation. gbc: Rc<RefCell<GameBoyConfig>>, } #[cfg_attr(feature = "wasm", wasm_bindgen)] #[derive(Clone, Copy, PartialEq, Eq)] pub enum PpuMode { HBlank = 0, VBlank = 1, OamRead = 2, VramRead = 3, } impl Ppu { pub fn new(mode: GameBoyMode, gbc: Rc<RefCell<GameBoyConfig>>) -> Self { Self { color_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], hram: [0u8; HRAM_SIZE], oam: [0u8; OAM_SIZE], vram_bank: 0x0, vram_offset: 0x0000, tiles: [Tile { buffer: [0u8; 64] }; TILE_COUNT], obj_data: [ObjectData::default(); OBJ_COUNT], palette_colors: PALETTE_COLORS, palette_bg: [[0u8; RGB_SIZE]; PALETTE_SIZE], palette_obj_0: [[0u8; RGB_SIZE]; PALETTE_SIZE], palette_obj_1: [[0u8; RGB_SIZE]; PALETTE_SIZE], palettes_color_bg: [[[0u8; RGB_SIZE]; PALETTE_SIZE]; 8], palettes_color_obj: [[[0u8; RGB_SIZE]; PALETTE_SIZE]; 8], palettes: [0u8; 3], palettes_color: [[0u8; 64]; 2], bg_map_attrs_0: [TileData::default(); 1024], bg_map_attrs_1: [TileData::default(); 1024], obj_priority: false, scy: 0x0, scx: 0x0, wy: 0x0, wx: 0x0, ly: 0x0, lyc: 0x0, mode: PpuMode::OamRead, mode_clock: 0, switch_bg: false, switch_obj: false, obj_size: false, bg_map: false, bg_tile: false, switch_window: false, window_map: false, switch_lcd: false, window_counter: 0x0, auto_increment_bg: false, palette_address_bg: 0x0, auto_increment_obj: false, palette_address_obj: 0x0, first_frame: false, frame_index: 0, stat_hblank: false, stat_vblank: false, stat_oam: false, stat_lyc: false, int_vblank: false, int_stat: false, dmg_compat: false, gb_mode: mode, gbc, } } pub fn reset(&mut self) { self.color_buffer = Box::new([0u8; COLOR_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]; self.hram = [0u8; HRAM_SIZE]; self.vram_bank = 0x0; self.vram_offset = 0x0000; self.tiles = [Tile { buffer: [0u8; 64] }; TILE_COUNT]; self.obj_data = [ObjectData::default(); OBJ_COUNT]; self.palette_bg = [[0u8; RGB_SIZE]; PALETTE_SIZE]; self.palette_obj_0 = [[0u8; RGB_SIZE]; PALETTE_SIZE]; self.palette_obj_1 = [[0u8; RGB_SIZE]; PALETTE_SIZE]; self.palettes_color_bg = [[[0u8; RGB_SIZE]; PALETTE_SIZE]; 8]; self.palettes_color_obj = [[[0u8; RGB_SIZE]; PALETTE_SIZE]; 8]; self.palettes = [0u8; 3]; self.palettes_color = [[0u8; 64]; 2]; self.bg_map_attrs_0 = [TileData::default(); 1024]; self.bg_map_attrs_1 = [TileData::default(); 1024]; self.obj_priority = false; self.scy = 0x0; self.scx = 0x0; self.ly = 0x0; self.lyc = 0x0; self.mode = PpuMode::OamRead; self.mode_clock = 0; self.switch_bg = false; self.switch_obj = false; self.obj_size = false; self.bg_map = false; self.bg_tile = false; self.switch_window = false; self.window_map = false; self.switch_lcd = false; self.window_counter = 0; self.auto_increment_bg = false; self.palette_address_bg = 0x0; self.auto_increment_obj = false; self.palette_address_obj = 0x0; self.first_frame = false; self.frame_index = 0; self.stat_hblank = false; self.stat_vblank = false; self.stat_oam = false; self.stat_lyc = false; self.int_vblank = false; self.int_stat = false; self.dmg_compat = false; } pub fn clock(&mut self, cycles: u16) { // in case the LCD is currently off then we skip the current // clock operation the PPU should not work if !self.switch_lcd { return; } // increments the current mode clock by the provided amount // of CPU cycles (probably coming from a previous CPU clock) self.mode_clock += cycles; match self.mode { PpuMode::OamRead => { if self.mode_clock >= 80 { self.mode = PpuMode::VramRead; self.mode_clock -= 80; } } PpuMode::VramRead => { if self.mode_clock >= 172 { self.render_line(); self.mode = PpuMode::HBlank; self.mode_clock -= 172; self.update_stat() } } PpuMode::HBlank => { if self.mode_clock >= 204 { // increments the window counter making sure that the // valid is only incremented when both the WX and WY // registers make sense (are within range), the window // switch is on and the line in drawing is above WY if self.switch_window && self.wx as i16 - 7 < DISPLAY_WIDTH as i16 && self.wy < DISPLAY_HEIGHT as u8 && self.ly >= self.wy { self.window_counter += 1; } // increments the register that holds the // information about the current line in drawing self.ly += 1; // in case we've reached the end of the // screen we're now entering the V-Blank if self.ly == 144 { self.int_vblank = true; self.mode = PpuMode::VBlank; } else { self.mode = PpuMode::OamRead; } self.mode_clock -= 204; self.update_stat() } } PpuMode::VBlank => { if self.mode_clock >= 456 { // increments the register that controls the line count, // notice that these represent the extra 10 horizontal // scanlines that are virtual and not real (off-screen) self.ly += 1; // in case the end of V-Blank has been reached then // we must jump again to the OAM read mode and reset // the scan line counter to the zero value if self.ly == 154 { self.mode = PpuMode::OamRead; self.ly = 0; self.window_counter = 0; self.first_frame = false; self.frame_index = self.frame_index.wrapping_add(1); self.update_stat() } self.mode_clock -= 456; } } } } pub fn read(&mut self, addr: u16) -> u8 { match addr { 0x8000..=0x9fff => self.vram[(self.vram_offset + (addr & 0x1fff)) as usize], 0xfe00..=0xfe9f => self.oam[(addr & 0x009f) as usize], // Not Usable 0xfea0..=0xfeff => 0xff, 0xff80..=0xfffe => self.hram[(addr & 0x007f) as usize], 0xff40 => { #[allow(clippy::bool_to_int_with_if)] (if self.switch_bg { 0x01 } else { 0x00 } | if self.switch_obj { 0x02 } else { 0x00 } | if self.obj_size { 0x04 } else { 0x00 } | if self.bg_map { 0x08 } else { 0x00 } | if self.bg_tile { 0x10 } else { 0x00 } | if self.switch_window { 0x20 } else { 0x00 } | if self.window_map { 0x40 } else { 0x00 } | if self.switch_lcd { 0x80 } else { 0x00 }) } 0xff41 => { (if self.stat_hblank { 0x08 } else { 0x00 } | if self.stat_vblank { 0x10 } else { 0x00 } | if self.stat_oam { 0x20 } else { 0x00 } | if self.stat_lyc { 0x40 } else { 0x00 } | if self.lyc == self.ly { 0x04 } else { 0x00 } | (self.mode as u8 & 0x03)) } 0xff42 => self.scy, 0xff43 => self.scx, 0xff44 => self.ly, 0xff45 => self.lyc, 0xff47 => self.palettes[0], 0xff48 => self.palettes[1], 0xff49 => self.palettes[2], 0xff4a => self.wy, 0xff4b => self.wx, // 0xFF4F — VBK (CGB only) 0xff4f => self.vram_bank | 0xfe, // 0xFF68 — BCPS/BGPI (CGB only) 0xff68 => self.palette_address_bg | if self.auto_increment_bg { 0x80 } else { 0x00 }, // 0xFF69 — BCPD/BGPD (CGB only) 0xff69 => self.palettes_color[0][self.palette_address_bg as usize], // 0xFF6A — OCPS/OBPI (CGB only) 0xff6a => self.palette_address_obj | if self.auto_increment_obj { 0x80 } else { 0x00 }, // 0xFF6B — OCPD/OBPD (CGB only) 0xff6b => self.palettes_color[1][self.palette_address_obj as usize], // 0xFF6C — OPRI (CGB only) 0xff6c => { #[allow(clippy::bool_to_int_with_if)] if self.obj_priority { 0x01 } else { 0x00 } } _ => { warnln!("Reading from unknown PPU location 0x{:04x}", addr); 0xff } } } pub fn write(&mut self, addr: u16, value: u8) { match addr { 0x8000..=0x9fff => { self.vram[(self.vram_offset + (addr & 0x1fff)) as usize] = value; if addr < 0x9800 { self.update_tile(addr, value); } else if self.vram_bank == 0x1 { self.update_bg_map_attrs(addr, value); } } 0xfe00..=0xfe9f => { self.oam[(addr & 0x009f) as usize] = value; self.update_object(addr, value); } // Not Usable 0xfea0..=0xfeff => (), 0xff80..=0xfffe => self.hram[(addr & 0x007f) as usize] = value, 0xff40 => { self.switch_bg = value & 0x01 == 0x01; self.switch_obj = value & 0x02 == 0x02; self.obj_size = value & 0x04 == 0x04; self.bg_map = value & 0x08 == 0x08; self.bg_tile = value & 0x10 == 0x10; self.switch_window = value & 0x20 == 0x20; self.window_map = value & 0x40 == 0x40; self.switch_lcd = value & 0x80 == 0x80; // in case the LCD is off takes the opportunity // to clear the screen, this is the expected // behaviour for this specific situation if !self.switch_lcd { self.mode = PpuMode::HBlank; self.mode_clock = 0; self.ly = 0; self.int_vblank = false; self.int_stat = false; self.first_frame = true; self.clear_frame_buffer(); } } 0xff41 => { self.stat_hblank = value & 0x08 == 0x08; self.stat_vblank = value & 0x10 == 0x10; self.stat_oam = value & 0x20 == 0x20; self.stat_lyc = value & 0x40 == 0x40; } 0xff42 => self.scy = value, 0xff43 => self.scx = value, 0xff45 => self.lyc = value, 0xff47 => { if value == self.palettes[0] { return; } if self.dmg_compat { Self::compute_palette(&mut self.palette_bg, &self.palettes_color_bg[0], value); } else { Self::compute_palette(&mut self.palette_bg, &self.palette_colors, value); } self.palettes[0] = value; } 0xff48 => { if value == self.palettes[1] { return; } if self.dmg_compat { Self::compute_palette( &mut self.palette_obj_0, &self.palettes_color_obj[0], value, ); } else { Self::compute_palette(&mut self.palette_obj_0, &self.palette_colors, value); } self.palettes[1] = value; } 0xff49 => { if value == self.palettes[2] { return; } if self.dmg_compat { Self::compute_palette( &mut self.palette_obj_1, &self.palettes_color_obj[1], value, ); } else { Self::compute_palette(&mut self.palette_obj_1, &self.palette_colors, value); } self.palettes[2] = value; } 0xff4a => self.wy = value, 0xff4b => self.wx = value, // 0xFF4F — VBK (CGB only) 0xff4f => { self.vram_bank = value & 0x01; self.vram_offset = self.vram_bank as u16 * 0x2000; } // 0xFF68 — BCPS/BGPI (CGB only) 0xff68 => { self.palette_address_bg = value & 0x3f; self.auto_increment_bg = value & 0x80 == 0x80; } // 0xFF69 — BCPD/BGPD (CGB only) 0xff69 => { let palette_index = self.palette_address_bg / 8; let color_index = (self.palette_address_bg % 8) / 2; let palette_color = &mut self.palettes_color[0]; palette_color[self.palette_address_bg as usize] = value; let palette = &mut self.palettes_color_bg[palette_index as usize]; Self::compute_palette_color(palette, palette_color, palette_index, color_index); if self.auto_increment_bg { self.palette_address_bg = (self.palette_address_bg + 1) & 0x3f; } } // 0xFF6A — OCPS/OBPI (CGB only) 0xff6a => { self.palette_address_obj = value & 0x3f; self.auto_increment_obj = value & 0x80 == 0x80; } // 0xFF6B — OCPD/OBPD (CGB only) 0xff6b => { let palette_index = self.palette_address_obj / 8; let color_index = (self.palette_address_obj % 8) / 2; let palette_color = &mut self.palettes_color[1]; palette_color[self.palette_address_obj as usize] = value; let palette = &mut self.palettes_color_obj[palette_index as usize]; Self::compute_palette_color(palette, palette_color, palette_index, color_index); if self.auto_increment_obj { self.palette_address_obj = (self.palette_address_obj + 1) & 0x3f; } } // 0xFF6C — OPRI (CGB only) 0xff6c => self.obj_priority = value & 0x01 == 0x01, 0xff7f => (), _ => warnln!("Writing in unknown PPU location 0x{:04x}", addr), } } pub fn frame_buffer_xrgb8888(&self) -> [u8; FRAME_BUFFER_XRGB8888_SIZE] { 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], ); buffer[index * XRGB8888_SIZE] = b; buffer[index * XRGB8888_SIZE + 1] = g; buffer[index * XRGB8888_SIZE + 2] = r; buffer[index * XRGB8888_SIZE + 3] = 0xff; } buffer } pub fn frame_buffer_xrgb8888_u32(&self) -> [u32; FRAME_BUFFER_SIZE] { 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], ); *pixel = ((r as u32) << 16) | ((g as u32) << 8) | b as u32; } buffer } pub fn frame_buffer_rgb1555(&self) -> [u8; FRAME_BUFFER_RGB1555_SIZE] { 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], ); let rgb1555 = Self::rgb888_to_rgb1555(r, g, b); buffer[index * RGB1555_SIZE] = rgb1555[0]; buffer[index * RGB1555_SIZE + 1] = rgb1555[1]; } buffer } pub fn frame_buffer_rgb1555_u16(&self) -> [u16; FRAME_BUFFER_SIZE] { 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], ); *pixel = Self::rgb888_to_rgb1555_u16(r, g, b); } buffer } pub fn frame_buffer_rgb565(&self) -> [u8; FRAME_BUFFER_RGB565_SIZE] { 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], ); let rgb565 = Self::rgb888_to_rgb565(r, g, b); buffer[index * RGB565_SIZE] = rgb565[0]; buffer[index * RGB565_SIZE + 1] = rgb565[1]; } buffer } pub fn frame_buffer_rgb565_u16(&self) -> [u16; FRAME_BUFFER_SIZE] { 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], ); *pixel = Self::rgb888_to_rgb565_u16(r, g, b); } buffer } pub fn vram(&self) -> &[u8; VRAM_SIZE] { &self.vram } pub fn hram(&self) -> &[u8; HRAM_SIZE] { &self.hram } pub fn tiles(&self) -> &[Tile; TILE_COUNT] { &self.tiles } pub fn set_palette_colors(&mut self, value: &Palette) { self.palette_colors = *value; self.compute_palettes() } pub fn palette_bg(&self) -> Palette { self.palette_bg } pub fn palette_obj_0(&self) -> Palette { self.palette_obj_0 } pub fn palette_obj_1(&self) -> Palette { self.palette_obj_1 } pub fn ly(&self) -> u8 { self.ly } pub fn mode(&self) -> PpuMode { self.mode } pub fn frame_index(&self) -> u16 { self.frame_index } #[inline(always)] pub fn int_vblank(&self) -> bool { self.int_vblank } #[inline(always)] pub fn set_int_vblank(&mut self, value: bool) { self.int_vblank = value; } #[inline(always)] pub fn ack_vblank(&mut self) { self.set_int_vblank(false); } #[inline(always)] pub fn int_stat(&self) -> bool { self.int_stat } #[inline(always)] pub fn set_int_stat(&mut self, value: bool) { self.int_stat = value; } #[inline(always)] pub fn ack_stat(&mut self) { self.set_int_stat(false); } pub fn dmg_compat(&self) -> bool { self.dmg_compat } pub fn set_dmg_compat(&mut self, value: bool) { self.dmg_compat = value; // if we're switching to the DMG compat mode // then we need to recompute the palettes so // that the colors are correct according to // the compat palettes set by the Boot ROM if value { self.compute_palettes(); } } pub fn gb_mode(&self) -> GameBoyMode { self.gb_mode } pub fn set_gb_mode(&mut self, value: GameBoyMode) { self.gb_mode = value; } pub fn set_gbc(&mut self, value: Rc<RefCell<GameBoyConfig>>) { self.gbc = value; } /// Fills the frame buffer with pixels of the provided color, /// this method must represent the fastest way of achieving /// the fill background with color operation. pub fn fill_frame_buffer(&mut self, color: Pixel) { 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]; } } /// 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]); } /// Prints the tile data information to the stdout, this is /// useful for debugging purposes. pub fn print_tile_stdout(&self, tile_index: usize) { println!("{}", self.tiles[tile_index]); } /// Updates the tile structure with the value that has /// just been written to a location on the VRAM associated /// with tiles. fn update_tile(&mut self, addr: u16, _value: u8) { let addr = (self.vram_offset + (addr & 0x1ffe)) as usize; let tile_index = ((addr >> 4) & 0x01ff) + (self.vram_bank as usize * TILE_COUNT_DMG); let tile = self.tiles[tile_index].borrow_mut(); let y = (addr >> 1) & 0x0007; let mut mask; for x in 0..TILE_WIDTH { mask = 1 << (TILE_WIDTH_I - x); #[allow(clippy::bool_to_int_with_if)] tile.set( x, y, if self.vram[addr] & mask > 0 { 0x1 } else { 0x0 } | if self.vram[addr + 1] & mask > 0 { 0x2 } else { 0x0 }, ); } } 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 obj = self.obj_data[obj_index].borrow_mut(); match addr & 0x03 { 0x00 => obj.y = value as i16 - 16, 0x01 => obj.x = value as i16 - 8, 0x02 => obj.tile = value, 0x03 => { obj.palette_cgb = value & 0x07; obj.tile_bank = (value & 0x08 == 0x08) as u8; obj.palette = (value & 0x10 == 0x10) as u8; obj.xflip = value & 0x20 == 0x20; obj.yflip = value & 0x40 == 0x40; obj.bg_over = value & 0x80 == 0x80; obj.index = obj_index as u8; } _ => (), } } fn update_bg_map_attrs(&mut self, addr: u16, value: u8) { let bg_map = addr >= 0x9c00; let tile_index = if bg_map { addr - 0x9c00 } else { addr - 0x9800 }; let bg_map_attrs = if bg_map { &mut self.bg_map_attrs_1 } else { &mut self.bg_map_attrs_0 }; let tile_data: &mut TileData = bg_map_attrs[tile_index as usize].borrow_mut(); tile_data.palette = value & 0x07; tile_data.vram_bank = (value & 0x08 == 0x08) as u8; tile_data.xflip = value & 0x20 == 0x20; tile_data.yflip = value & 0x40 == 0x40; tile_data.priority = value & 0x80 == 0x80; } pub fn registers(&self) -> PpuRegisters { PpuRegisters { scy: self.scy, scx: self.scx, wy: self.wy, wx: self.wx, ly: self.ly, lyc: self.lyc, } } fn render_line(&mut self) { if self.gb_mode == GameBoyMode::Dmg { self.render_line_dmg(); } else { self.render_line_cgb(); } } fn render_line_dmg(&mut self) { if self.first_frame { return; } if self.switch_bg { self.render_map_dmg(self.bg_map, self.scx, self.scy, 0, 0, self.ly); } if self.switch_bg && self.switch_window { self.render_map_dmg(self.window_map, 0, 0, self.wx, self.wy, self.window_counter); } if self.switch_obj { self.render_objects(); } } fn render_line_cgb(&mut self) { if self.first_frame { return; } let switch_bg_window = (self.gb_mode.is_cgb() && !self.dmg_compat) || self.switch_bg; if switch_bg_window { self.render_map(self.bg_map, self.scx, self.scy, 0, 0, self.ly); } if switch_bg_window && self.switch_window { self.render_map(self.window_map, 0, 0, self.wx, self.wy, self.window_counter); } if self.switch_obj { self.render_objects(); } } fn render_map(&mut self, map: bool, scx: u8, scy: u8, wx: u8, wy: u8, ld: u8) { // in case the target window Y position has not yet been reached // then there's nothing to be done, returns control flow immediately if self.ly < wy { return; } // selects the correct background attributes map based on the bg map flag // because the attributes are separated according to the map they represent // this is only relevant for CGB mode let bg_map_attrs = if map { self.bg_map_attrs_1 } else { self.bg_map_attrs_0 }; // obtains the base address of the background map using the bg map flag // that control which background map is going to be used let map_offset: usize = if map { 0x1c00 } else { 0x1800 }; // calculates the map row index for the tile by using the current line // index and the DY (scroll Y) divided by 8 (as the tiles are 8x8 pixels), // on top of that ensures that the result is modulus 32 meaning that the // drawing wraps around the Y axis let row_index = (((ld as usize + scy as usize) & 0xff) >> 3) % 32; // calculates the map offset by the row offset multiplied by the number // of tiles in each row (32) let row_offset = row_index * 32; // calculates the sprite line offset by using the SCX register // shifted by 3 meaning that the tiles are 8x8 let mut line_offset = (scx >> 3) as usize; // calculates the index of the initial tile in drawing, // if the tile data set in use is #1, the indexes are // signed, then calculates a real tile offset let mut tile_index = self.vram[map_offset + row_offset + line_offset] as usize; if !self.bg_tile && tile_index < 128 { tile_index += 256; } // obtains the reference to the attributes of the new tile in // drawing for meta processing (CGB only) let mut tile_attr = if self.dmg_compat { &DEFAULT_TILE_ATTR } else { &bg_map_attrs[row_offset + line_offset] }; // retrieves the proper palette for the current tile in drawing // taking into consideration if we're running in CGB mode or not let mut palette = if self.gb_mode == GameBoyMode::Cgb { if self.dmg_compat { &self.palette_bg } else { &self.palettes_color_bg[tile_attr.palette as usize] } } else { &self.palette_bg }; // 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; let mut yflip = tile_attr.yflip; // obtains the value the BG-to-OAM priority to be used in the computation // of the final pixel value (CGB only) let mut priority = tile_attr.priority; // increments the tile index value by the required offset for the VRAM // bank in which the tile is stored, this is only required for CGB mode tile_index += tile_attr.vram_bank as usize * TILE_COUNT_DMG; // obtains the reference to the tile that is going to be drawn let mut tile = &self.tiles[tile_index]; // calculates the offset that is going to be used in the update of the color buffer // 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; // calculates both the current Y and X positions within the tiles // using the bitwise and operation as an effective modulus 8 let y = (ld as usize + scy as usize) & 0x07; let mut x = (scx & 0x07) as usize; // calculates the initial tile X position in drawing, doing this // 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 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) 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]; // updates the priority buffer with the current pixel // the priority is only set in case the priority of // the background (over OAM) is set in the attributes // and the pixel is not transparent self.priority_buffer[color_offset] = priority && pixel != 0; // increments the current tile X position in drawing x += 1; // in case the end of tile width has been reached then // a new tile must be retrieved for rendering if x == TILE_WIDTH { // resets the tile X position to the base value // as a new tile is going to be drawn x = 0; // calculates the new line tile offset making sure that // the maximum of 32 is not overflown line_offset = (line_offset + 1) % 32; // calculates the tile index and makes sure the value // takes into consideration the bg tile value tile_index = self.vram[map_offset + row_offset + line_offset] as usize; if !self.bg_tile && tile_index < 128 { tile_index += 256; } // in case the current mode is CGB and the DMG compatibility // flag is not set then a series of tile values must be // updated according to the tile attributes field if self.gb_mode == GameBoyMode::Cgb && !self.dmg_compat { tile_attr = &bg_map_attrs[row_offset + line_offset]; palette = &self.palettes_color_bg[tile_attr.palette as usize]; xflip = tile_attr.xflip; yflip = tile_attr.yflip; priority = tile_attr.priority; tile_index += tile_attr.vram_bank as usize * TILE_COUNT_DMG; } // obtains the reference to the new tile in drawing tile = &self.tiles[tile_index]; } // 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; } } fn render_map_dmg(&mut self, map: bool, scx: u8, scy: u8, wx: u8, wy: u8, ld: u8) { // in case the target window Y position has not yet been reached // then there's nothing to be done, returns control flow immediately if self.ly < wy { return; } // obtains the base address of the background map using the bg map flag // that control which background map is going to be used let map_offset: usize = if map { 0x1c00 } else { 0x1800 }; // calculates the map row index for the tile by using the current line // index and the DY (scroll Y) divided by 8 (as the tiles are 8x8 pixels), // on top of that ensures that the result is modulus 32 meaning that the // drawing wraps around the Y axis let row_index = (((ld as usize + scy as usize) & 0xff) >> 3) % 32; // calculates the map offset by the row offset multiplied by the number // of tiles in each row (32) let row_offset = row_index * 32; // calculates the sprite line offset by using the SCX register // shifted by 3 meaning that the tiles are 8x8 let mut line_offset = (scx >> 3) as usize; // calculates the index of the initial tile in drawing, // if the tile data set in use is #1, the indexes are // signed, then calculates a real tile offset let mut tile_index = self.vram[map_offset + row_offset + line_offset] as usize; if !self.bg_tile && tile_index < 128 { tile_index += 256; } // obtains the reference to the tile that is going to be drawn let mut tile = &self.tiles[tile_index]; // calculates the offset that is going to be used in the update of the color buffer // 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; // calculates both the current Y and X positions within the tiles // using the bitwise and operation as an effective modulus 8 let y = (ld as usize + scy as usize) & 0x07; let mut x = (scx & 0x07) as usize; // calculates the initial tile X position in drawing, doing this // 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 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) 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]; // increments the current tile X position in drawing x += 1; // in case the end of tile width has been reached then // a new tile must be retrieved for rendering if x == TILE_WIDTH { // resets the tile X position to the base value // as a new tile is going to be drawn x = 0; // calculates the new line tile offset making sure that // the maximum of 32 is not overflown line_offset = (line_offset + 1) % 32; // calculates the tile index and makes sure the value // takes into consideration the bg tile value tile_index = self.vram[map_offset + row_offset + line_offset] as usize; if !self.bg_tile && tile_index < 128 { tile_index += 256; } // obtains the reference to the new tile in drawing tile = &self.tiles[tile_index]; } // 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; } } fn render_objects(&mut self) { // the mode in which the object priority should be computed // if true this means that the X coordinate priority mode will // be used otherwise the object priority will be defined according // to the object's index in the OAM memory, notice that this // control of priority is only present in the CGB and to be able // to offer retro-compatibility with DMG let obj_priority_mode = self.gb_mode != GameBoyMode::Cgb || self.obj_priority; // creates a local counter object to count the total number // of object that were drawn in the current line, this will // be used for instance to limit the total number of objects // to 10 per line (Game Boy limitation) let mut draw_count = 0u8; // allocates the buffer that is going to be used to determine // drawing priority for overlapping pixels between different // objects, in MBR mode the object that has the smallest X // coordinate takes priority in drawing the pixel let mut index_buffer = [-256i16; DISPLAY_WIDTH]; // determines if the object should always be placed over the // possible background, this is only required for CGB mode let always_over = if self.gb_mode == GameBoyMode::Cgb && !self.dmg_compat { !self.switch_bg } else { false }; // iterates over the complete set of available object to check // the ones that require drawing and draws them for index in 0..OBJ_COUNT { // in case the limit on the number of objects to be draw per // line has been reached breaks the loop avoiding more draws if draw_count == 10 { break; } // obtains the meta data of the object that is currently // under iteration to be checked for drawing let obj = &self.obj_data[index]; let obj_height = if self.obj_size { TILE_DOUBLE_HEIGHT } else { TILE_HEIGHT }; // 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.y <= self.ly as i16) && ((obj.y + obj_height as i16) > self.ly as i16); if !is_contained { continue; } let palette = if self.gb_mode == GameBoyMode::Cgb { if self.dmg_compat { if obj.palette == 0 { &self.palette_obj_0 } else if obj.palette == 1 { &self.palette_obj_1 } else { panic!("Invalid object palette: {:02x}", obj.palette); } } else { &self.palettes_color_obj[obj.palette_cgb as usize] } } else if obj.palette == 0 { &self.palette_obj_0 } else if obj.palette == 1 { &self.palette_obj_1 } else { panic!("Invalid object palette: {:02x}", obj.palette); }; // 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 let mut color_offset = self.ly as i32 * DISPLAY_WIDTH as i32 + obj.x as i32; // 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 i32 * DISPLAY_WIDTH as i32 + obj.x as i32) * RGB_SIZE as i32; // the relative title offset should range from 0 to 7 in 8x8 // objects and from 0 to 15 in 8x16 objects let mut tile_offset = self.ly as i16 - obj.y; // in case we're flipping the object we must recompute the // tile offset as an inverted value using the object's height if obj.yflip { tile_offset = obj_height as i16 - tile_offset - 1; } // saves some space for the reference to the tile that // is going to be used in the current operation let tile: &Tile; // "calculates" the index offset that is going to be applied // to the tile index to retrieve the proper tile taking into // consideration the VRAM in which the tile is stored let tile_bank_offset = if self.dmg_compat { 0 } else { obj.tile_bank as usize * TILE_COUNT_DMG }; // in case we're facing a 8x16 object then we must // differentiate between the handling of the top tile // and the bottom tile through bitwise manipulation // of the tile index if self.obj_size { if tile_offset < 8 { let tile_index = (obj.tile as usize & 0xfe) + tile_bank_offset; tile = &self.tiles[tile_index]; } else { let tile_index = (obj.tile as usize | 0x01) + tile_bank_offset; tile = &self.tiles[tile_index]; tile_offset -= 8; } } // otherwise we're facing a 8x8 sprite and we should grab // the tile directly from the object's tile index else { let tile_index = obj.tile as usize + tile_bank_offset; tile = &self.tiles[tile_index]; } let tile_row = tile.get_row(tile_offset as usize); // determines if the object should always be placed over the // previously placed background or window pixels let obj_over = always_over || !obj.bg_over; for tile_x in 0..TILE_WIDTH { let x = obj.x + tile_x as i16; let is_contained = (x >= 0) && (x < DISPLAY_WIDTH as i16); if is_contained { // the object is only considered visible if no background or // window should be drawn over or if the underlying pixel // is transparent (zero value) meaning there's no background // or window for the provided pixel let mut is_visible = obj_over || self.color_buffer[color_offset as usize] == 0; // additionally (in CCG mode) the object is only considered to // be visible if the priority buffer is not set for the current // pixel, this means that the background is capturing priority // by having the BG-to-OAM priority bit set in the bg map attributes is_visible &= always_over || !self.priority_buffer[color_offset as usize]; // determines if the current pixel has priority over a possible // one that has been drawn by a previous object, this happens // in case the current object has a small X coordinate according // to the MBR algorithm let has_priority = index_buffer[x as usize] == -256 || (obj_priority_mode && obj.x < index_buffer[x as usize]); let pixel = tile_row[if obj.xflip { TILE_WIDTH_I - tile_x } else { tile_x }]; if is_visible && has_priority && pixel != 0 { // marks the current pixel in iteration as "owned" // by the object with the defined X base position, // 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) self.color_buffer[color_offset as usize] = pixel; // sets the color pixel in the frame buffer 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]; } } // increment the color offset by one as this represents // the advance of one color 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 as i32; } // increments the counter so that we're able to keep // track on the number of object drawn draw_count += 1; } } /// Runs an update operation on the LCD STAT interrupt meaning /// that the flag that control will be updated in case the conditions /// required for the LCD STAT interrupt to be triggered are met. fn update_stat(&mut self) { self.int_stat = self.stat_level(); } /// Obtains the current level of the LCD STAT interrupt by /// checking the current PPU state in various sections. fn stat_level(&self) -> bool { self.stat_lyc && self.lyc == self.ly || self.stat_oam && self.mode == PpuMode::OamRead || self.stat_vblank && self.mode == PpuMode::VBlank || self.stat_hblank && self.mode == PpuMode::HBlank } /// Computes the values for all of the palettes, this method /// is useful to "flush" color computation whenever the base /// palette colors are changed. fn compute_palettes(&mut self) { if self.dmg_compat { Self::compute_palette( &mut self.palette_bg, &self.palettes_color_bg[0], self.palettes[0], ); Self::compute_palette( &mut self.palette_obj_0, &self.palettes_color_obj[0], self.palettes[1], ); Self::compute_palette( &mut self.palette_obj_1, &self.palettes_color_obj[1], self.palettes[2], ); } else { // re-computes the complete set of palettes according to // the currently set palette colors (that may have changed) Self::compute_palette(&mut self.palette_bg, &self.palette_colors, self.palettes[0]); Self::compute_palette( &mut self.palette_obj_0, &self.palette_colors, self.palettes[1], ); Self::compute_palette( &mut self.palette_obj_1, &self.palette_colors, self.palettes[2], ); } // clears the frame buffer to allow the new background // color to be used self.clear_frame_buffer(); } /// Static method used for the base logic of computation of RGB /// based palettes from the internal Game Boy color indexes. /// This method should be called whenever the palette indexes /// are changed. fn compute_palette(palette: &mut Palette, palette_colors: &Palette, value: u8) { for (index, palette_item) in palette.iter_mut().enumerate() { let color_index: usize = (value as usize >> (index * 2)) & 3; match color_index { 0..=3 => *palette_item = palette_colors[color_index], color_index => panic!("Invalid palette color index {:04x}", color_index), } } } /// Static method that computes an RGB888 color palette ready to /// be used for frame buffer operations from 4 (colors) x 2 bytes (RGB555) /// that represent an RGB555 set of colors. This method should be /// used for CGB mode where colors are represented using RGB555. fn compute_palette_color( palette: &mut Palette, palette_color: &[u8; 64], palette_index: u8, color_index: u8, ) { let palette_offset = (palette_index * 4 * 2) as usize; let color_offset = (color_index * 2) as usize; palette[color_index as usize] = Self::rgb555_to_rgb888( palette_color[palette_offset + color_offset], palette_color[palette_offset + color_offset + 1], ); } fn rgb555_to_rgb888(first: u8, second: u8) -> Pixel { let r = (first & 0x1f) << 3; let g = (((first & 0xe0) >> 5) | ((second & 0x03) << 3)) << 3; let b = ((second & 0x7c) >> 2) << 3; [r, g, b] } fn rgb888_to_rgb1555(first: u8, second: u8, third: u8) -> PixelRgb1555 { let pixel = Self::rgb888_to_rgb1555_u16(first, second, third); [pixel as u8, (pixel >> 8) as u8] } fn rgb888_to_rgb1555_u16(first: u8, second: u8, third: u8) -> u16 { let r = (first as u16 >> 3) & 0x1f; let g = (second as u16 >> 3) & 0x1f; let b = (third as u16 >> 3) & 0x1f; let a = 1; (a << 15) | (r << 10) | (g << 5) | b } fn rgb888_to_rgb565(first: u8, second: u8, third: u8) -> PixelRgb565 { let pixel = Self::rgb888_to_rgb565_u16(first, second, third); [pixel as u8, (pixel >> 8) as u8] } fn rgb888_to_rgb565_u16(first: u8, second: u8, third: u8) -> u16 { let r = (first as u16 >> 3) & 0x1f; let g = (second as u16 >> 2) & 0x3f; let b = (third as u16 >> 3) & 0x1f; (r << 11) | (g << 5) | b } } impl Default for Ppu { fn default() -> Self { Self::new( GameBoyMode::Dmg, Rc::new(RefCell::new(GameBoyConfig::default())), ) } } #[cfg(test)] mod tests { use super::Ppu; #[test] fn test_update_tile_simple() { let mut ppu = Ppu::default(); ppu.vram[0x0000] = 0xff; ppu.vram[0x0001] = 0xff; let result = ppu.tiles()[0].get(0, 0); assert_eq!(result, 0); ppu.update_tile(0x8000, 0x00); let result = ppu.tiles()[0].get(0, 0); assert_eq!(result, 3); } #[test] fn test_update_tile_upper() { let mut ppu = Ppu::default(); ppu.vram[0x1000] = 0xff; ppu.vram[0x1001] = 0xff; let result = ppu.tiles()[256].get(0, 0); assert_eq!(result, 0); ppu.update_tile(0x9000, 0x00); let result = ppu.tiles()[256].get(0, 0); assert_eq!(result, 3); } }