Playing around with reading gameboy roms, and maybe emulation
1pub const VRAM_BEGIN: usize = 0x8000; 2pub const VRAM_END: usize = 0x9FFF; 3pub const VRAM_SIZE: usize = VRAM_END - VRAM_BEGIN + 1; 4 5// Tilemap locations in VRAM 6pub const TILEMAP_0_START: usize = 0x1800; // $9800 - $8000 = 0x1800 7pub const TILEMAP_1_START: usize = 0x1C00; // $9C00 - $8000 = 0x1C00 8pub const TILEMAP_SIZE: usize = 32 * 32; // 1024 bytes 9 10#[derive(Copy, Clone, Debug, PartialEq)] 11pub enum TilePixelValue { 12 Zero, 13 One, 14 Two, 15 Three, 16} 17 18impl TilePixelValue { 19 /// Convert pixel value to grayscale color (0-255) 20 pub fn to_grayscale(&self) -> u8 { 21 match self { 22 TilePixelValue::Zero => 255, // White 23 TilePixelValue::One => 170, // Light gray (66% brightness) 24 TilePixelValue::Two => 85, // Dark gray (33% brightness) 25 TilePixelValue::Three => 0, // Black 26 } 27 } 28 29 /// Convert pixel value to RGB color tuple 30 pub fn to_rgb(&self) -> (u8, u8, u8) { 31 let gray = self.to_grayscale(); 32 (gray, gray, gray) 33 } 34 35 /// Convert pixel value to classic Game Boy green colors 36 pub fn to_gameboy_green(&self) -> (u8, u8, u8) { 37 match self { 38 TilePixelValue::Zero => (224, 248, 208), // Lightest green 39 TilePixelValue::One => (136, 192, 112), // Light green 40 TilePixelValue::Two => (52, 104, 86), // Dark green 41 TilePixelValue::Three => (8, 24, 32), // Darkest green/black 42 } 43 } 44} 45 46type Tile = [[TilePixelValue; 8]; 8]; 47 48fn empty_tile() -> Tile { 49 [[TilePixelValue::Zero; 8]; 8] 50} 51 52pub struct GPU { 53 vram: [u8; VRAM_SIZE], 54 tile_set: [Tile; 384], // 384 tiles total (256 from first set + 128 from second set) 55} 56 57impl GPU { 58 pub fn new() -> Self { 59 Self { 60 vram: [0; VRAM_SIZE], 61 tile_set: [empty_tile(); 384], 62 } 63 } 64 65 pub fn read_vram(&self, address: usize) -> u8 { 66 self.vram[address] 67 } 68 69 pub fn write_vram(&mut self, index: usize, value: u8) { 70 self.vram[index] = value; 71 72 // If our index is greater than 0x1800, we're not writing to the tile set storage 73 // so we can just return. 74 if index >= 0x1800 { 75 return; 76 } 77 78 // Tiles rows are encoded in two bytes with the first byte always 79 // on an even address. Bitwise ANDing the address with 0xffe 80 // gives us the address of the first byte. 81 let normalized_index = index & 0xFFFE; 82 83 // First we need to get the two bytes that encode the tile row. 84 let byte1 = self.vram[normalized_index]; 85 let byte2 = self.vram[normalized_index + 1]; 86 87 // A tile is 8 rows tall. Since each row is encoded with two bytes a tile 88 // is therefore 16 bytes in total. 89 let tile_index = index / 16; 90 // Every two bytes is a new row 91 let row_index = (index % 16) / 2; 92 93 // Now we're going to loop 8 times to get the 8 pixels that make up a given row. 94 for pixel_index in 0..8 { 95 let mask = 1 << (7 - pixel_index); 96 let lsb = byte1 & mask; 97 let msb = byte2 & mask; 98 99 let value = match (lsb != 0, msb != 0) { 100 (true, true) => TilePixelValue::Three, 101 (false, true) => TilePixelValue::Two, 102 (true, false) => TilePixelValue::One, 103 (false, false) => TilePixelValue::Zero, 104 }; 105 106 self.tile_set[tile_index][row_index][pixel_index] = value; 107 } 108 } 109 110 /// Get a tile by its index, handling Game Boy's two addressing modes 111 pub fn get_tile(&self, tile_index: u8, use_signed_addressing: bool) -> Option<&Tile> { 112 let actual_index = if use_signed_addressing { 113 // Signed addressing mode: $8800-$97FF 114 // Index 0-127 maps to tiles 256-383, index 128-255 maps to tiles 0-127 115 if tile_index < 128 { 116 256 + tile_index as usize 117 } else { 118 (tile_index as i8 as i16 + 256) as usize 119 } 120 } else { 121 // Unsigned addressing mode: $8000-$8FFF 122 tile_index as usize 123 }; 124 125 if actual_index < self.tile_set.len() { 126 Some(&self.tile_set[actual_index]) 127 } else { 128 None 129 } 130 } 131 132 /// Read tilemap data from VRAM 133 pub fn get_tilemap_data(&self, tilemap_select: bool) -> [u8; TILEMAP_SIZE] { 134 let start_addr = if tilemap_select { 135 TILEMAP_1_START 136 } else { 137 TILEMAP_0_START 138 }; 139 140 let mut tilemap = [0u8; TILEMAP_SIZE]; 141 for i in 0..TILEMAP_SIZE { 142 tilemap[i] = self.vram[start_addr + i]; 143 } 144 tilemap 145 } 146 147 /// Render the entire tilemap to RGB (256x256 pixels) 148 pub fn render_full_tilemap_to_rgb( 149 &self, 150 tilemap_select: bool, 151 use_signed_addressing: bool, 152 ) -> Vec<(u8, u8, u8)> { 153 let tilemap_data = self.get_tilemap_data(tilemap_select); 154 let total_pixels = 256 * 256; // 32x32 tiles, each 8x8 pixels 155 let mut color_buffer = vec![(0, 0, 0); total_pixels]; 156 157 for tilemap_y in 0..32 { 158 for tilemap_x in 0..32 { 159 let tilemap_index = tilemap_y * 32 + tilemap_x; 160 let tile_id = tilemap_data[tilemap_index]; 161 162 if let Some(tile) = self.get_tile(tile_id, use_signed_addressing) { 163 // Render this tile into the color buffer 164 for tile_row in 0..8 { 165 for tile_col in 0..8 { 166 let pixel_x = tilemap_x * 8 + tile_col; 167 let pixel_y = tilemap_y * 8 + tile_row; 168 let buffer_index = pixel_y * 256 + pixel_x; 169 170 if buffer_index < color_buffer.len() { 171 color_buffer[buffer_index] = 172 tile[tile_row][tile_col].to_gameboy_green(); 173 } 174 } 175 } 176 } 177 } 178 } 179 180 color_buffer 181 } 182 183 /// Render a visible portion of the tilemap (160x144 pixels) with scrolling 184 pub fn render_background_to_rgb( 185 &self, 186 tilemap_select: bool, 187 use_signed_addressing: bool, 188 scroll_x: u8, 189 scroll_y: u8, 190 ) -> Vec<(u8, u8, u8)> { 191 let tilemap_data = self.get_tilemap_data(tilemap_select); 192 let mut color_buffer = vec![(0, 0, 0); 160 * 144]; 193 194 for screen_y in 0..144 { 195 for screen_x in 0..160 { 196 // Calculate the position in the 256x256 tilemap with wrapping 197 let bg_x = ((screen_x as u16 + scroll_x as u16) % 256) as u8; 198 let bg_y = ((screen_y as u16 + scroll_y as u16) % 256) as u8; 199 200 // Which tile are we in? 201 let tile_x = (bg_x / 8) as usize; 202 let tile_y = (bg_y / 8) as usize; 203 let tilemap_index = tile_y * 32 + tile_x; 204 205 // Which pixel within that tile? 206 let pixel_x = (bg_x % 8) as usize; 207 let pixel_y = (bg_y % 8) as usize; 208 209 let tile_id = tilemap_data[tilemap_index]; 210 211 if let Some(tile) = self.get_tile(tile_id, use_signed_addressing) { 212 let buffer_index = screen_y * 160 + screen_x; 213 color_buffer[buffer_index] = tile[pixel_y][pixel_x].to_gameboy_green(); 214 } 215 } 216 } 217 218 color_buffer 219 } 220 221 /// Render a tile to a color buffer (64 pixels as RGB values) 222 pub fn render_tile_to_rgb(&self, tile_index: usize) -> Option<[(u8, u8, u8); 64]> { 223 if tile_index >= self.tile_set.len() { 224 return None; 225 } 226 227 let tile = &self.tile_set[tile_index]; 228 let mut color_buffer = [(0, 0, 0); 64]; 229 230 for (row_idx, row) in tile.iter().enumerate() { 231 for (col_idx, &pixel) in row.iter().enumerate() { 232 let buffer_index = row_idx * 8 + col_idx; 233 color_buffer[buffer_index] = pixel.to_gameboy_green(); 234 } 235 } 236 237 Some(color_buffer) 238 } 239 240 /// Debug function to print tilemap as hex values 241 pub fn print_tilemap_hex(&self, tilemap_select: bool) { 242 let tilemap_data = self.get_tilemap_data(tilemap_select); 243 println!("Tilemap {} contents:", if tilemap_select { 1 } else { 0 }); 244 245 for row in 0..32 { 246 for col in 0..32 { 247 let index = row * 32 + col; 248 print!("{:02X} ", tilemap_data[index]); 249 } 250 println!(); 251 } 252 } 253 254 /// Debug function to print a tile as ASCII art 255 pub fn print_tile_ascii(&self, tile_index: usize) { 256 if let Some(tile) = self.tile_set.get(tile_index) { 257 println!("Tile {}:", tile_index); 258 for row in tile { 259 for &pixel in row { 260 let char = match pixel { 261 TilePixelValue::Zero => '░', // Light 262 TilePixelValue::One => '▒', // Light gray 263 TilePixelValue::Two => '▓', // Dark gray 264 TilePixelValue::Three => '█', // Dark 265 }; 266 print!("{}", char); 267 } 268 println!(); 269 } 270 } else { 271 println!("Tile {} not found", tile_index); 272 } 273 } 274}