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#[derive(Copy, Clone, Debug, PartialEq)] 6pub enum TilePixelValue { 7 Zero, 8 One, 9 Two, 10 Three, 11} 12 13impl TilePixelValue { 14 /// Convert pixel value to grayscale color (0-255) 15 pub fn to_grayscale(&self) -> u8 { 16 match self { 17 TilePixelValue::Zero => 255, // White 18 TilePixelValue::One => 170, // Light gray (66% brightness) 19 TilePixelValue::Two => 85, // Dark gray (33% brightness) 20 TilePixelValue::Three => 0, // Black 21 } 22 } 23 24 /// Convert pixel value to RGB color tuple 25 pub fn to_rgb(&self) -> (u8, u8, u8) { 26 let gray = self.to_grayscale(); 27 (gray, gray, gray) 28 } 29 30 /// Convert pixel value to classic Game Boy green colors 31 pub fn to_gameboy_green(&self) -> (u8, u8, u8) { 32 match self { 33 TilePixelValue::Zero => (224, 248, 208), // Lightest green 34 TilePixelValue::One => (136, 192, 112), // Light green 35 TilePixelValue::Two => (52, 104, 86), // Dark green 36 TilePixelValue::Three => (8, 24, 32), // Darkest green/black 37 } 38 } 39} 40 41type Tile = [[TilePixelValue; 8]; 8]; 42 43fn empty_tile() -> Tile { 44 [[TilePixelValue::Zero; 8]; 8] 45} 46 47pub struct GPU { 48 vram: [u8; VRAM_SIZE], 49 tile_set: [Tile; 384], // 384 tiles total (256 from first set + 128 from second set) 50} 51 52impl GPU { 53 pub fn new() -> Self { 54 Self { 55 vram: [0; VRAM_SIZE], 56 tile_set: [empty_tile(); 384], 57 } 58 } 59 60 pub fn read_vram(&self, address: usize) -> u8 { 61 self.vram[address] 62 } 63 64 pub fn write_vram(&mut self, index: usize, value: u8) { 65 self.vram[index] = value; 66 67 // If our index is greater than 0x1800, we're not writing to the tile set storage 68 // so we can just return. 69 if index >= 0x1800 { 70 return; 71 } 72 73 // Tiles rows are encoded in two bytes with the first byte always 74 // on an even address. Bitwise ANDing the address with 0xffe 75 // gives us the address of the first byte. 76 let normalized_index = index & 0xFFFE; 77 78 // First we need to get the two bytes that encode the tile row. 79 let byte1 = self.vram[normalized_index]; 80 let byte2 = self.vram[normalized_index + 1]; 81 82 // A tile is 8 rows tall. Since each row is encoded with two bytes a tile 83 // is therefore 16 bytes in total. 84 let tile_index = index / 16; 85 // Every two bytes is a new row 86 let row_index = (index % 16) / 2; 87 88 // Now we're going to loop 8 times to get the 8 pixels that make up a given row. 89 for pixel_index in 0..8 { 90 let mask = 1 << (7 - pixel_index); 91 let lsb = byte1 & mask; 92 let msb = byte2 & mask; 93 94 let value = match (lsb != 0, msb != 0) { 95 (true, true) => TilePixelValue::Three, 96 (false, true) => TilePixelValue::Two, 97 (true, false) => TilePixelValue::One, 98 (false, false) => TilePixelValue::Zero, 99 }; 100 101 self.tile_set[tile_index][row_index][pixel_index] = value; 102 } 103 } 104 105 /// Get a tile by its index 106 pub fn get_tile(&self, tile_index: usize) -> Option<&Tile> { 107 if tile_index < self.tile_set.len() { 108 Some(&self.tile_set[tile_index]) 109 } else { 110 None 111 } 112 } 113 114 /// Render a tile to a color buffer (64 pixels as RGB values) 115 pub fn render_tile_to_rgb(&self, tile_index: usize) -> Option<[(u8, u8, u8); 64]> { 116 let tile = self.get_tile(tile_index)?; 117 let mut color_buffer = [(0, 0, 0); 64]; 118 119 for (row_idx, row) in tile.iter().enumerate() { 120 for (col_idx, &pixel) in row.iter().enumerate() { 121 let buffer_index = row_idx * 8 + col_idx; 122 color_buffer[buffer_index] = pixel.to_gameboy_green(); 123 } 124 } 125 126 Some(color_buffer) 127 } 128 129 /// Render a tile to grayscale buffer (64 pixels as grayscale values) 130 pub fn render_tile_to_grayscale(&self, tile_index: usize) -> Option<[u8; 64]> { 131 let tile = self.get_tile(tile_index)?; 132 let mut gray_buffer = [0u8; 64]; 133 134 for (row_idx, row) in tile.iter().enumerate() { 135 for (col_idx, &pixel) in row.iter().enumerate() { 136 let buffer_index = row_idx * 8 + col_idx; 137 gray_buffer[buffer_index] = pixel.to_grayscale(); 138 } 139 } 140 141 Some(gray_buffer) 142 } 143 144 /// Render multiple tiles in a grid pattern 145 pub fn render_tile_map( 146 &self, 147 tile_indices: &[u8], 148 map_width: usize, 149 map_height: usize, 150 ) -> Vec<(u8, u8, u8)> { 151 let total_pixels = map_width * 8 * map_height * 8; // 8x8 pixels per tile 152 let mut color_buffer = vec![(0, 0, 0); total_pixels]; 153 154 for (map_idx, &tile_idx) in tile_indices.iter().enumerate() { 155 if let Some(tile) = self.get_tile(tile_idx as usize) { 156 let tile_x = map_idx % map_width; 157 let tile_y = map_idx / map_width; 158 159 for (row_idx, row) in tile.iter().enumerate() { 160 for (col_idx, &pixel) in row.iter().enumerate() { 161 let pixel_x = tile_x * 8 + col_idx; 162 let pixel_y = tile_y * 8 + row_idx; 163 let buffer_index = pixel_y * (map_width * 8) + pixel_x; 164 165 if buffer_index < color_buffer.len() { 166 color_buffer[buffer_index] = pixel.to_gameboy_green(); 167 } 168 } 169 } 170 } 171 } 172 173 color_buffer 174 } 175 176 /// Debug function to print a tile as ASCII art 177 pub fn print_tile_ascii(&self, tile_index: usize) { 178 if let Some(tile) = self.get_tile(tile_index) { 179 // println!("Tile {}:", tile_index); 180 for row in tile { 181 for &pixel in row { 182 let char = match pixel { 183 TilePixelValue::Zero => '░', // Light 184 TilePixelValue::One => '▒', // Light gray 185 TilePixelValue::Two => '▓', // Dark gray 186 TilePixelValue::Three => '█', // Dark 187 }; 188 print!("{}", char); 189 } 190 // println!(); 191 } 192 } else { 193 println!("Tile {} not found", tile_index); 194 } 195 } 196}