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}