+3
-9
src/cartridge_header.rs
+3
-9
src/cartridge_header.rs
···
5
5
6
6
pub struct CartridgeHeader {
7
7
//Should be 80 bytes (0x014F(335) - 0x0100(256)) + 1 to include the last address
8
-
pub buffer: [u8; 80],
8
+
pub _buffer: [u8; 80],
9
9
pub title: [char; 16],
10
10
pub manufacturer_code: [char; 4],
11
11
pub cgb_flag: CGBFlag,
···
49
49
for (i, true_logo_byte) in NINTENDO_LOGO.iter().enumerate() {
50
50
let rom_byte = nintendo_logo_from_rom[i];
51
51
if rom_byte != *true_logo_byte {
52
-
return Err(Error::CartridgeReadError);
52
+
return Err(Error::NotAValidRom);
53
53
}
54
54
}
55
55
···
97
97
]);
98
98
99
99
Ok(Self {
100
-
buffer: header_buffer
100
+
_buffer: header_buffer
101
101
.try_into()
102
102
.map_err(|_| Error::CartridgeReadError)?,
103
103
title: title_chars,
···
115
115
global_checksum,
116
116
})
117
117
}
118
-
119
-
fn print_test(&self) {
120
-
for byte in self.buffer.iter() {
121
-
print!("{} ", *byte as char);
122
-
}
123
-
}
124
118
}
+1
-1
src/enums.rs
+1
-1
src/enums.rs
+3
-41
src/main.rs
+3
-41
src/main.rs
···
1
1
mod cartridge_header;
2
2
mod enums;
3
-
mod tile_map;
3
+
4
4
use crate::cartridge_header::CartridgeHeader;
5
-
use crate::enums::CartridgeHeaderAddress::OldLicenseeCode;
6
-
use crate::enums::{
7
-
CGBFlag, CartridgeHeaderAddress, CartridgeType, DestinationCode, Error, RamSize, RomSize,
8
-
};
9
-
use crate::tile_map::{GPU, VRAM_BEGIN, VRAM_END};
10
-
use minifb::{Key, Window, WindowOptions};
5
+
use crate::enums::DestinationCode;
11
6
use std::fs::File;
12
7
use std::io::Read;
13
8
14
-
const WINDOW_DIMENSIONS: [usize; 2] = [(160 * 1), (144 * 1)];
15
-
16
9
// https://github.com/ISSOtm/gb-bootroms/blob/2dce25910043ce2ad1d1d3691436f2c7aabbda00/src/dmg.asm#L259-L269
17
10
// Each tile is encoded using 2 (!) bytes
18
11
// The tiles are represented below
···
40
33
rom_file.read_to_end(&mut rom_buffer)?;
41
34
let cart_header = match CartridgeHeader::parse(&*rom_buffer) {
42
35
Ok(header) => header,
43
-
Err(err) => {
36
+
Err(_err) => {
44
37
return Err(std::io::Error::new(
45
38
std::io::ErrorKind::Other,
46
39
"Rom failed to parse",
···
74
67
println!("Header Checksum: {:#X}", cart_header.header_checksum);
75
68
println!("Global Checksum: {:#X}", cart_header.global_checksum);
76
69
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_background_to_rgb(true, true, 25, 25); // let idk = gpu.render_tile_to_rgb(1).unwrap();
100
-
let buffer_u32: Vec<u32> = tile_map_buffer
101
-
.iter()
102
-
.map(|(r, g, b)| ((*r as u32) << 16) | ((*g as u32) << 8) | (*b as u32))
103
-
.collect();
104
-
while window.is_open() && !window.is_key_down(Key::Escape) {
105
-
window.update_with_buffer(&buffer_u32).unwrap();
106
-
}
107
-
108
70
Ok(())
109
71
}
-274
src/tile_map.rs
-274
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
-
// Tilemap locations in VRAM
6
-
pub const TILEMAP_0_START: usize = 0x1800; // $9800 - $8000 = 0x1800
7
-
pub const TILEMAP_1_START: usize = 0x1C00; // $9C00 - $8000 = 0x1C00
8
-
pub const TILEMAP_SIZE: usize = 32 * 32; // 1024 bytes
9
-
10
-
#[derive(Copy, Clone, Debug, PartialEq)]
11
-
pub enum TilePixelValue {
12
-
Zero,
13
-
One,
14
-
Two,
15
-
Three,
16
-
}
17
-
18
-
impl 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
-
46
-
type Tile = [[TilePixelValue; 8]; 8];
47
-
48
-
fn empty_tile() -> Tile {
49
-
[[TilePixelValue::Zero; 8]; 8]
50
-
}
51
-
52
-
pub 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
-
57
-
impl 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
-
}
+8
-1
README.md
+8
-1
README.md
···
1
-
# GameBoyPlayground
1
+
# DMG Playground
2
+
3
+
Just some experiments with Gameboy emulation. Right now it just reads
4
+
the [Cartridge Header](https://gbdev.io/pandocs/The_Cartridge_Header.html) of a GB rom and parses it
5
+
out
6
+
7
+
1. Obtain a `LegallyObtainedRom.gb` and name it as such in the root of the project
8
+
2. `cargo run` You should see it list out things like the name of the game, etc