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}