Playing around with reading gameboy roms, and maybe emulation

it's printing something

+463 -2
+229
Cargo.lock
··· 5 5 [[package]] 6 6 name = "GameBoyPlayground" 7 7 version = "0.1.0" 8 + dependencies = [ 9 + "minifb", 10 + ] 11 + 12 + [[package]] 13 + name = "bitflags" 14 + version = "1.3.2" 15 + source = "registry+https://github.com/rust-lang/crates.io-index" 16 + checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 17 + 18 + [[package]] 19 + name = "bitflags" 20 + version = "2.9.1" 21 + source = "registry+https://github.com/rust-lang/crates.io-index" 22 + checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 23 + 24 + [[package]] 25 + name = "cc" 26 + version = "1.2.30" 27 + source = "registry+https://github.com/rust-lang/crates.io-index" 28 + checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" 29 + dependencies = [ 30 + "shlex", 31 + ] 32 + 33 + [[package]] 34 + name = "cfg-if" 35 + version = "1.0.1" 36 + source = "registry+https://github.com/rust-lang/crates.io-index" 37 + checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 38 + 39 + [[package]] 40 + name = "gdi32-sys" 41 + version = "0.1.2" 42 + source = "registry+https://github.com/rust-lang/crates.io-index" 43 + checksum = "8e3eb92c1107527888f86b6ebb0b7f82794777dbf172a932998660a0a2e26c11" 44 + dependencies = [ 45 + "winapi 0.2.8", 46 + "winapi-build", 47 + ] 48 + 49 + [[package]] 50 + name = "kernel32-sys" 51 + version = "0.2.2" 52 + source = "registry+https://github.com/rust-lang/crates.io-index" 53 + checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 54 + dependencies = [ 55 + "winapi 0.2.8", 56 + "winapi-build", 57 + ] 58 + 59 + [[package]] 60 + name = "lazy_static" 61 + version = "0.2.11" 62 + source = "registry+https://github.com/rust-lang/crates.io-index" 63 + checksum = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" 64 + 65 + [[package]] 66 + name = "lazy_static" 67 + version = "1.5.0" 68 + source = "registry+https://github.com/rust-lang/crates.io-index" 69 + checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 70 + 71 + [[package]] 72 + name = "libc" 73 + version = "0.2.174" 74 + source = "registry+https://github.com/rust-lang/crates.io-index" 75 + checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 76 + 77 + [[package]] 78 + name = "libredox" 79 + version = "0.1.6" 80 + source = "registry+https://github.com/rust-lang/crates.io-index" 81 + checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" 82 + dependencies = [ 83 + "bitflags 2.9.1", 84 + "libc", 85 + "redox_syscall", 86 + ] 87 + 88 + [[package]] 89 + name = "minifb" 90 + version = "0.10.7" 91 + source = "registry+https://github.com/rust-lang/crates.io-index" 92 + checksum = "8cebe353532532dd30eaab68ece91e624ccf6f97453f2da84042f5915450a137" 93 + dependencies = [ 94 + "cc", 95 + "gdi32-sys", 96 + "kernel32-sys", 97 + "orbclient", 98 + "time", 99 + "user32-sys", 100 + "winapi 0.2.8", 101 + "x11-dl", 102 + ] 103 + 104 + [[package]] 105 + name = "orbclient" 106 + version = "0.3.48" 107 + source = "registry+https://github.com/rust-lang/crates.io-index" 108 + checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" 109 + dependencies = [ 110 + "libc", 111 + "libredox", 112 + "sdl2", 113 + "sdl2-sys", 114 + ] 115 + 116 + [[package]] 117 + name = "pkg-config" 118 + version = "0.3.32" 119 + source = "registry+https://github.com/rust-lang/crates.io-index" 120 + checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 121 + 122 + [[package]] 123 + name = "redox_syscall" 124 + version = "0.5.15" 125 + source = "registry+https://github.com/rust-lang/crates.io-index" 126 + checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" 127 + dependencies = [ 128 + "bitflags 2.9.1", 129 + ] 130 + 131 + [[package]] 132 + name = "sdl2" 133 + version = "0.35.2" 134 + source = "registry+https://github.com/rust-lang/crates.io-index" 135 + checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a" 136 + dependencies = [ 137 + "bitflags 1.3.2", 138 + "lazy_static 1.5.0", 139 + "libc", 140 + "sdl2-sys", 141 + ] 142 + 143 + [[package]] 144 + name = "sdl2-sys" 145 + version = "0.35.2" 146 + source = "registry+https://github.com/rust-lang/crates.io-index" 147 + checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0" 148 + dependencies = [ 149 + "cfg-if", 150 + "libc", 151 + "version-compare", 152 + ] 153 + 154 + [[package]] 155 + name = "shlex" 156 + version = "1.3.0" 157 + source = "registry+https://github.com/rust-lang/crates.io-index" 158 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 159 + 160 + [[package]] 161 + name = "time" 162 + version = "0.1.45" 163 + source = "registry+https://github.com/rust-lang/crates.io-index" 164 + checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 165 + dependencies = [ 166 + "libc", 167 + "wasi", 168 + "winapi 0.3.9", 169 + ] 170 + 171 + [[package]] 172 + name = "user32-sys" 173 + version = "0.1.3" 174 + source = "registry+https://github.com/rust-lang/crates.io-index" 175 + checksum = "e6b719983b952c04198829b51653c06af36f0e44c967fcc1a2bb397ceafbf80a" 176 + dependencies = [ 177 + "winapi 0.2.8", 178 + "winapi-build", 179 + ] 180 + 181 + [[package]] 182 + name = "version-compare" 183 + version = "0.1.1" 184 + source = "registry+https://github.com/rust-lang/crates.io-index" 185 + checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" 186 + 187 + [[package]] 188 + name = "wasi" 189 + version = "0.10.0+wasi-snapshot-preview1" 190 + source = "registry+https://github.com/rust-lang/crates.io-index" 191 + checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 192 + 193 + [[package]] 194 + name = "winapi" 195 + version = "0.2.8" 196 + source = "registry+https://github.com/rust-lang/crates.io-index" 197 + checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 198 + 199 + [[package]] 200 + name = "winapi" 201 + version = "0.3.9" 202 + source = "registry+https://github.com/rust-lang/crates.io-index" 203 + checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 204 + dependencies = [ 205 + "winapi-i686-pc-windows-gnu", 206 + "winapi-x86_64-pc-windows-gnu", 207 + ] 208 + 209 + [[package]] 210 + name = "winapi-build" 211 + version = "0.1.1" 212 + source = "registry+https://github.com/rust-lang/crates.io-index" 213 + checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 214 + 215 + [[package]] 216 + name = "winapi-i686-pc-windows-gnu" 217 + version = "0.4.0" 218 + source = "registry+https://github.com/rust-lang/crates.io-index" 219 + checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 220 + 221 + [[package]] 222 + name = "winapi-x86_64-pc-windows-gnu" 223 + version = "0.4.0" 224 + source = "registry+https://github.com/rust-lang/crates.io-index" 225 + checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 226 + 227 + [[package]] 228 + name = "x11-dl" 229 + version = "2.14.0" 230 + source = "registry+https://github.com/rust-lang/crates.io-index" 231 + checksum = "326c500cdc166fd7c70dd8c8a829cd5c0ce7be5a5d98c25817de2b9bdc67faf8" 232 + dependencies = [ 233 + "lazy_static 0.2.11", 234 + "libc", 235 + "pkg-config", 236 + ]
+1
Cargo.toml
··· 4 4 edition = "2024" 5 5 6 6 [dependencies] 7 + minifb = "0.10.7"
+37 -2
src/main.rs
··· 1 1 mod cartridge_header; 2 2 mod enums; 3 - 3 + mod tile_map; 4 4 use crate::cartridge_header::CartridgeHeader; 5 5 use crate::enums::CartridgeHeaderAddress::OldLicenseeCode; 6 6 use crate::enums::{ 7 7 CGBFlag, CartridgeHeaderAddress, CartridgeType, DestinationCode, Error, RamSize, RomSize, 8 8 }; 9 + use crate::tile_map::{GPU, VRAM_BEGIN, VRAM_END}; 10 + use minifb::{Key, Window, WindowOptions}; 9 11 use std::fs::File; 10 12 use std::io::Read; 11 13 14 + const WINDOW_DIMENSIONS: [usize; 2] = [(160 * 1), (144 * 1)]; 15 + 12 16 // https://github.com/ISSOtm/gb-bootroms/blob/2dce25910043ce2ad1d1d3691436f2c7aabbda00/src/dmg.asm#L259-L269 13 17 // Each tile is encoded using 2 (!) bytes 14 18 // The tiles are represented below ··· 43 47 )); 44 48 } 45 49 }; 46 - // cart_header.print_test(); 47 50 48 51 let title: String = String::from_iter(cart_header.title); 49 52 println!("Title: {}", title); ··· 70 73 println!("Version: {:?}", cart_header.version); 71 74 println!("Header Checksum: {:#X}", cart_header.header_checksum); 72 75 println!("Global Checksum: {:#X}", cart_header.global_checksum); 76 + 77 + let mut gpu = GPU::new(); 78 + let tile_map_buffer = &rom_buffer[VRAM_BEGIN as usize..VRAM_END as usize]; 79 + for (i, byte) in tile_map_buffer.iter().enumerate() { 80 + gpu.write_vram(i, *byte); 81 + } 82 + gpu.render_tile_to_rgb(0); 83 + // let range_of_tiles = 0..255; 84 + // for tile_id in range_of_tiles { 85 + // let idk = gpu.print_tile_ascii(tile_id); 86 + // println!("{:?}", idk); 87 + // } 88 + let mut window = Window::new( 89 + "DMG-01", 90 + WINDOW_DIMENSIONS[0], 91 + WINDOW_DIMENSIONS[1], 92 + WindowOptions { 93 + scale: minifb::Scale::X2, 94 + ..WindowOptions::default() 95 + }, 96 + ) 97 + .unwrap(); 98 + let mut tile_ids: Vec<u8> = (0..100).collect(); 99 + let tile_map_buffer = gpu.render_tile_map(&tile_ids, 166, 144); 100 + // let idk = gpu.render_tile_to_rgb(1).unwrap(); 101 + let buffer_u32: Vec<u32> = tile_map_buffer 102 + .iter() 103 + .map(|(r, g, b)| ((*r as u32) << 16) | ((*g as u32) << 8) | (*b as u32)) 104 + .collect(); 105 + while window.is_open() && !window.is_key_down(Key::Escape) { 106 + window.update_with_buffer(&buffer_u32).unwrap(); 107 + } 73 108 74 109 Ok(()) 75 110 }
+196
src/tile_map.rs
··· 1 + pub const VRAM_BEGIN: usize = 0x8000; 2 + pub const VRAM_END: usize = 0x9FFF; 3 + pub const VRAM_SIZE: usize = VRAM_END - VRAM_BEGIN + 1; 4 + 5 + #[derive(Copy, Clone, Debug, PartialEq)] 6 + pub enum TilePixelValue { 7 + Zero, 8 + One, 9 + Two, 10 + Three, 11 + } 12 + 13 + impl 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 + 41 + type Tile = [[TilePixelValue; 8]; 8]; 42 + 43 + fn empty_tile() -> Tile { 44 + [[TilePixelValue::Zero; 8]; 8] 45 + } 46 + 47 + pub 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 + 52 + impl 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 + }