Playing around with reading gameboy roms, and maybe emulation

cleanup

-229
Cargo.lock
···
[[package]]
name = "GameBoyPlayground"
version = "0.1.0"
-
dependencies = [
-
"minifb",
-
]
-
-
[[package]]
-
name = "bitflags"
-
version = "1.3.2"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
-
-
[[package]]
-
name = "bitflags"
-
version = "2.9.1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
-
-
[[package]]
-
name = "cc"
-
version = "1.2.30"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7"
-
dependencies = [
-
"shlex",
-
]
-
-
[[package]]
-
name = "cfg-if"
-
version = "1.0.1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
-
-
[[package]]
-
name = "gdi32-sys"
-
version = "0.1.2"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "8e3eb92c1107527888f86b6ebb0b7f82794777dbf172a932998660a0a2e26c11"
-
dependencies = [
-
"winapi 0.2.8",
-
"winapi-build",
-
]
-
-
[[package]]
-
name = "kernel32-sys"
-
version = "0.2.2"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
-
dependencies = [
-
"winapi 0.2.8",
-
"winapi-build",
-
]
-
-
[[package]]
-
name = "lazy_static"
-
version = "0.2.11"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73"
-
-
[[package]]
-
name = "lazy_static"
-
version = "1.5.0"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
-
-
[[package]]
-
name = "libc"
-
version = "0.2.174"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
-
-
[[package]]
-
name = "libredox"
-
version = "0.1.6"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0"
-
dependencies = [
-
"bitflags 2.9.1",
-
"libc",
-
"redox_syscall",
-
]
-
-
[[package]]
-
name = "minifb"
-
version = "0.10.7"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "8cebe353532532dd30eaab68ece91e624ccf6f97453f2da84042f5915450a137"
-
dependencies = [
-
"cc",
-
"gdi32-sys",
-
"kernel32-sys",
-
"orbclient",
-
"time",
-
"user32-sys",
-
"winapi 0.2.8",
-
"x11-dl",
-
]
-
-
[[package]]
-
name = "orbclient"
-
version = "0.3.48"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43"
-
dependencies = [
-
"libc",
-
"libredox",
-
"sdl2",
-
"sdl2-sys",
-
]
-
-
[[package]]
-
name = "pkg-config"
-
version = "0.3.32"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
-
-
[[package]]
-
name = "redox_syscall"
-
version = "0.5.15"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec"
-
dependencies = [
-
"bitflags 2.9.1",
-
]
-
-
[[package]]
-
name = "sdl2"
-
version = "0.35.2"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a"
-
dependencies = [
-
"bitflags 1.3.2",
-
"lazy_static 1.5.0",
-
"libc",
-
"sdl2-sys",
-
]
-
-
[[package]]
-
name = "sdl2-sys"
-
version = "0.35.2"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0"
-
dependencies = [
-
"cfg-if",
-
"libc",
-
"version-compare",
-
]
-
-
[[package]]
-
name = "shlex"
-
version = "1.3.0"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
-
-
[[package]]
-
name = "time"
-
version = "0.1.45"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
-
dependencies = [
-
"libc",
-
"wasi",
-
"winapi 0.3.9",
-
]
-
-
[[package]]
-
name = "user32-sys"
-
version = "0.1.3"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "e6b719983b952c04198829b51653c06af36f0e44c967fcc1a2bb397ceafbf80a"
-
dependencies = [
-
"winapi 0.2.8",
-
"winapi-build",
-
]
-
-
[[package]]
-
name = "version-compare"
-
version = "0.1.1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
-
-
[[package]]
-
name = "wasi"
-
version = "0.10.0+wasi-snapshot-preview1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
-
-
[[package]]
-
name = "winapi"
-
version = "0.2.8"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
-
-
[[package]]
-
name = "winapi"
-
version = "0.3.9"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-
dependencies = [
-
"winapi-i686-pc-windows-gnu",
-
"winapi-x86_64-pc-windows-gnu",
-
]
-
-
[[package]]
-
name = "winapi-build"
-
version = "0.1.1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
-
-
[[package]]
-
name = "winapi-i686-pc-windows-gnu"
-
version = "0.4.0"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
-
[[package]]
-
name = "winapi-x86_64-pc-windows-gnu"
-
version = "0.4.0"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-
-
[[package]]
-
name = "x11-dl"
-
version = "2.14.0"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "326c500cdc166fd7c70dd8c8a829cd5c0ce7be5a5d98c25817de2b9bdc67faf8"
-
dependencies = [
-
"lazy_static 0.2.11",
-
"libc",
-
"pkg-config",
-
]
-1
Cargo.toml
···
edition = "2024"
[dependencies]
-
minifb = "0.10.7"
+3 -9
src/cartridge_header.rs
···
pub struct CartridgeHeader {
//Should be 80 bytes (0x014F(335) - 0x0100(256)) + 1 to include the last address
-
pub buffer: [u8; 80],
+
pub _buffer: [u8; 80],
pub title: [char; 16],
pub manufacturer_code: [char; 4],
pub cgb_flag: CGBFlag,
···
for (i, true_logo_byte) in NINTENDO_LOGO.iter().enumerate() {
let rom_byte = nintendo_logo_from_rom[i];
if rom_byte != *true_logo_byte {
-
return Err(Error::CartridgeReadError);
+
return Err(Error::NotAValidRom);
}
}
···
]);
Ok(Self {
-
buffer: header_buffer
+
_buffer: header_buffer
.try_into()
.map_err(|_| Error::CartridgeReadError)?,
title: title_chars,
···
header_checksum,
global_checksum,
})
-
}
-
-
fn print_test(&self) {
-
for byte in self.buffer.iter() {
-
print!("{} ", *byte as char);
-
}
}
}
+1 -1
src/enums.rs
···
CartridgeHeaderStart = 0x0100,
CartridgeHeaderEnd = 0x014F,
// 0100-0103 — Entry point
-
EntryPointEnd = 0x0103,
+
_EntryPointEnd = 0x0103,
// 0104-0133 — Nintendo logo
NintendoLogoStart = 0x0104,
NintendoLogoEnd = 0x00133,
+3 -41
src/main.rs
···
mod cartridge_header;
mod enums;
-
mod tile_map;
+
use crate::cartridge_header::CartridgeHeader;
-
use crate::enums::CartridgeHeaderAddress::OldLicenseeCode;
-
use crate::enums::{
-
CGBFlag, CartridgeHeaderAddress, CartridgeType, DestinationCode, Error, RamSize, RomSize,
-
};
-
use crate::tile_map::{GPU, VRAM_BEGIN, VRAM_END};
-
use minifb::{Key, Window, WindowOptions};
+
use crate::enums::DestinationCode;
use std::fs::File;
use std::io::Read;
-
-
const WINDOW_DIMENSIONS: [usize; 2] = [(160 * 1), (144 * 1)];
// https://github.com/ISSOtm/gb-bootroms/blob/2dce25910043ce2ad1d1d3691436f2c7aabbda00/src/dmg.asm#L259-L269
// Each tile is encoded using 2 (!) bytes
···
rom_file.read_to_end(&mut rom_buffer)?;
let cart_header = match CartridgeHeader::parse(&*rom_buffer) {
Ok(header) => header,
-
Err(err) => {
+
Err(_err) => {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Rom failed to parse",
···
println!("Version: {:?}", cart_header.version);
println!("Header Checksum: {:#X}", cart_header.header_checksum);
println!("Global Checksum: {:#X}", cart_header.global_checksum);
-
-
let mut gpu = GPU::new();
-
let tile_map_buffer = &rom_buffer[VRAM_BEGIN as usize..VRAM_END as usize];
-
for (i, byte) in tile_map_buffer.iter().enumerate() {
-
gpu.write_vram(i, *byte);
-
}
-
gpu.render_tile_to_rgb(0);
-
// let range_of_tiles = 0..255;
-
// for tile_id in range_of_tiles {
-
// let idk = gpu.print_tile_ascii(tile_id);
-
// println!("{:?}", idk);
-
// }
-
let mut window = Window::new(
-
"DMG-01",
-
WINDOW_DIMENSIONS[0],
-
WINDOW_DIMENSIONS[1],
-
WindowOptions {
-
scale: minifb::Scale::X2,
-
..WindowOptions::default()
-
},
-
)
-
.unwrap();
-
let mut tile_ids: Vec<u8> = (0..100).collect();
-
let tile_map_buffer = gpu.render_background_to_rgb(true, true, 25, 25); // let idk = gpu.render_tile_to_rgb(1).unwrap();
-
let buffer_u32: Vec<u32> = tile_map_buffer
-
.iter()
-
.map(|(r, g, b)| ((*r as u32) << 16) | ((*g as u32) << 8) | (*b as u32))
-
.collect();
-
while window.is_open() && !window.is_key_down(Key::Escape) {
-
window.update_with_buffer(&buffer_u32).unwrap();
-
}
Ok(())
}
-274
src/tile_map.rs
···
-
pub const VRAM_BEGIN: usize = 0x8000;
-
pub const VRAM_END: usize = 0x9FFF;
-
pub const VRAM_SIZE: usize = VRAM_END - VRAM_BEGIN + 1;
-
-
// Tilemap locations in VRAM
-
pub const TILEMAP_0_START: usize = 0x1800; // $9800 - $8000 = 0x1800
-
pub const TILEMAP_1_START: usize = 0x1C00; // $9C00 - $8000 = 0x1C00
-
pub const TILEMAP_SIZE: usize = 32 * 32; // 1024 bytes
-
-
#[derive(Copy, Clone, Debug, PartialEq)]
-
pub enum TilePixelValue {
-
Zero,
-
One,
-
Two,
-
Three,
-
}
-
-
impl TilePixelValue {
-
/// Convert pixel value to grayscale color (0-255)
-
pub fn to_grayscale(&self) -> u8 {
-
match self {
-
TilePixelValue::Zero => 255, // White
-
TilePixelValue::One => 170, // Light gray (66% brightness)
-
TilePixelValue::Two => 85, // Dark gray (33% brightness)
-
TilePixelValue::Three => 0, // Black
-
}
-
}
-
-
/// Convert pixel value to RGB color tuple
-
pub fn to_rgb(&self) -> (u8, u8, u8) {
-
let gray = self.to_grayscale();
-
(gray, gray, gray)
-
}
-
-
/// Convert pixel value to classic Game Boy green colors
-
pub fn to_gameboy_green(&self) -> (u8, u8, u8) {
-
match self {
-
TilePixelValue::Zero => (224, 248, 208), // Lightest green
-
TilePixelValue::One => (136, 192, 112), // Light green
-
TilePixelValue::Two => (52, 104, 86), // Dark green
-
TilePixelValue::Three => (8, 24, 32), // Darkest green/black
-
}
-
}
-
}
-
-
type Tile = [[TilePixelValue; 8]; 8];
-
-
fn empty_tile() -> Tile {
-
[[TilePixelValue::Zero; 8]; 8]
-
}
-
-
pub struct GPU {
-
vram: [u8; VRAM_SIZE],
-
tile_set: [Tile; 384], // 384 tiles total (256 from first set + 128 from second set)
-
}
-
-
impl GPU {
-
pub fn new() -> Self {
-
Self {
-
vram: [0; VRAM_SIZE],
-
tile_set: [empty_tile(); 384],
-
}
-
}
-
-
pub fn read_vram(&self, address: usize) -> u8 {
-
self.vram[address]
-
}
-
-
pub fn write_vram(&mut self, index: usize, value: u8) {
-
self.vram[index] = value;
-
-
// If our index is greater than 0x1800, we're not writing to the tile set storage
-
// so we can just return.
-
if index >= 0x1800 {
-
return;
-
}
-
-
// Tiles rows are encoded in two bytes with the first byte always
-
// on an even address. Bitwise ANDing the address with 0xffe
-
// gives us the address of the first byte.
-
let normalized_index = index & 0xFFFE;
-
-
// First we need to get the two bytes that encode the tile row.
-
let byte1 = self.vram[normalized_index];
-
let byte2 = self.vram[normalized_index + 1];
-
-
// A tile is 8 rows tall. Since each row is encoded with two bytes a tile
-
// is therefore 16 bytes in total.
-
let tile_index = index / 16;
-
// Every two bytes is a new row
-
let row_index = (index % 16) / 2;
-
-
// Now we're going to loop 8 times to get the 8 pixels that make up a given row.
-
for pixel_index in 0..8 {
-
let mask = 1 << (7 - pixel_index);
-
let lsb = byte1 & mask;
-
let msb = byte2 & mask;
-
-
let value = match (lsb != 0, msb != 0) {
-
(true, true) => TilePixelValue::Three,
-
(false, true) => TilePixelValue::Two,
-
(true, false) => TilePixelValue::One,
-
(false, false) => TilePixelValue::Zero,
-
};
-
-
self.tile_set[tile_index][row_index][pixel_index] = value;
-
}
-
}
-
-
/// Get a tile by its index, handling Game Boy's two addressing modes
-
pub fn get_tile(&self, tile_index: u8, use_signed_addressing: bool) -> Option<&Tile> {
-
let actual_index = if use_signed_addressing {
-
// Signed addressing mode: $8800-$97FF
-
// Index 0-127 maps to tiles 256-383, index 128-255 maps to tiles 0-127
-
if tile_index < 128 {
-
256 + tile_index as usize
-
} else {
-
(tile_index as i8 as i16 + 256) as usize
-
}
-
} else {
-
// Unsigned addressing mode: $8000-$8FFF
-
tile_index as usize
-
};
-
-
if actual_index < self.tile_set.len() {
-
Some(&self.tile_set[actual_index])
-
} else {
-
None
-
}
-
}
-
-
/// Read tilemap data from VRAM
-
pub fn get_tilemap_data(&self, tilemap_select: bool) -> [u8; TILEMAP_SIZE] {
-
let start_addr = if tilemap_select {
-
TILEMAP_1_START
-
} else {
-
TILEMAP_0_START
-
};
-
-
let mut tilemap = [0u8; TILEMAP_SIZE];
-
for i in 0..TILEMAP_SIZE {
-
tilemap[i] = self.vram[start_addr + i];
-
}
-
tilemap
-
}
-
-
/// Render the entire tilemap to RGB (256x256 pixels)
-
pub fn render_full_tilemap_to_rgb(
-
&self,
-
tilemap_select: bool,
-
use_signed_addressing: bool,
-
) -> Vec<(u8, u8, u8)> {
-
let tilemap_data = self.get_tilemap_data(tilemap_select);
-
let total_pixels = 256 * 256; // 32x32 tiles, each 8x8 pixels
-
let mut color_buffer = vec![(0, 0, 0); total_pixels];
-
-
for tilemap_y in 0..32 {
-
for tilemap_x in 0..32 {
-
let tilemap_index = tilemap_y * 32 + tilemap_x;
-
let tile_id = tilemap_data[tilemap_index];
-
-
if let Some(tile) = self.get_tile(tile_id, use_signed_addressing) {
-
// Render this tile into the color buffer
-
for tile_row in 0..8 {
-
for tile_col in 0..8 {
-
let pixel_x = tilemap_x * 8 + tile_col;
-
let pixel_y = tilemap_y * 8 + tile_row;
-
let buffer_index = pixel_y * 256 + pixel_x;
-
-
if buffer_index < color_buffer.len() {
-
color_buffer[buffer_index] =
-
tile[tile_row][tile_col].to_gameboy_green();
-
}
-
}
-
}
-
}
-
}
-
}
-
-
color_buffer
-
}
-
-
/// Render a visible portion of the tilemap (160x144 pixels) with scrolling
-
pub fn render_background_to_rgb(
-
&self,
-
tilemap_select: bool,
-
use_signed_addressing: bool,
-
scroll_x: u8,
-
scroll_y: u8,
-
) -> Vec<(u8, u8, u8)> {
-
let tilemap_data = self.get_tilemap_data(tilemap_select);
-
let mut color_buffer = vec![(0, 0, 0); 160 * 144];
-
-
for screen_y in 0..144 {
-
for screen_x in 0..160 {
-
// Calculate the position in the 256x256 tilemap with wrapping
-
let bg_x = ((screen_x as u16 + scroll_x as u16) % 256) as u8;
-
let bg_y = ((screen_y as u16 + scroll_y as u16) % 256) as u8;
-
-
// Which tile are we in?
-
let tile_x = (bg_x / 8) as usize;
-
let tile_y = (bg_y / 8) as usize;
-
let tilemap_index = tile_y * 32 + tile_x;
-
-
// Which pixel within that tile?
-
let pixel_x = (bg_x % 8) as usize;
-
let pixel_y = (bg_y % 8) as usize;
-
-
let tile_id = tilemap_data[tilemap_index];
-
-
if let Some(tile) = self.get_tile(tile_id, use_signed_addressing) {
-
let buffer_index = screen_y * 160 + screen_x;
-
color_buffer[buffer_index] = tile[pixel_y][pixel_x].to_gameboy_green();
-
}
-
}
-
}
-
-
color_buffer
-
}
-
-
/// Render a tile to a color buffer (64 pixels as RGB values)
-
pub fn render_tile_to_rgb(&self, tile_index: usize) -> Option<[(u8, u8, u8); 64]> {
-
if tile_index >= self.tile_set.len() {
-
return None;
-
}
-
-
let tile = &self.tile_set[tile_index];
-
let mut color_buffer = [(0, 0, 0); 64];
-
-
for (row_idx, row) in tile.iter().enumerate() {
-
for (col_idx, &pixel) in row.iter().enumerate() {
-
let buffer_index = row_idx * 8 + col_idx;
-
color_buffer[buffer_index] = pixel.to_gameboy_green();
-
}
-
}
-
-
Some(color_buffer)
-
}
-
-
/// Debug function to print tilemap as hex values
-
pub fn print_tilemap_hex(&self, tilemap_select: bool) {
-
let tilemap_data = self.get_tilemap_data(tilemap_select);
-
println!("Tilemap {} contents:", if tilemap_select { 1 } else { 0 });
-
-
for row in 0..32 {
-
for col in 0..32 {
-
let index = row * 32 + col;
-
print!("{:02X} ", tilemap_data[index]);
-
}
-
println!();
-
}
-
}
-
-
/// Debug function to print a tile as ASCII art
-
pub fn print_tile_ascii(&self, tile_index: usize) {
-
if let Some(tile) = self.tile_set.get(tile_index) {
-
println!("Tile {}:", tile_index);
-
for row in tile {
-
for &pixel in row {
-
let char = match pixel {
-
TilePixelValue::Zero => '░', // Light
-
TilePixelValue::One => '▒', // Light gray
-
TilePixelValue::Two => '▓', // Dark gray
-
TilePixelValue::Three => '█', // Dark
-
};
-
print!("{}", char);
-
}
-
println!();
-
}
-
} else {
-
println!("Tile {} not found", tile_index);
-
}
-
}
-
}