Compare commits

3 Commits
master ... vibe

Author SHA1 Message Date
92787a1036 Remove debug logging and dead code for cleaner output.
This commit removes extensive debug logging, print statements, and unused code scattered across `main.rs` and `lcd.rs`. These changes streamline the codebase, reduce unnecessary noise, and improve maintainability without affecting functionality.
2025-05-19 17:01:55 +01:00
d0f748baa5 Add initial game binary and SDL2 dependencies
Introduce the main game binary `gb240p.gb` along with required SDL2 runtime (`SDL2.dll`) and development (`SDL2.lib`) files. These are essential for compiling and running the game.
2025-05-17 09:04:42 +01:00
b3024e66c8 Refactor and improve instruction execution behavior
Standardize instruction cycle handling and update return types for better clarity and accuracy. Add cycle count assertions in tests to ensure proper execution timings. Minor formatting fixes and typo corrections throughout the codebase.
2025-05-17 09:03:30 +01:00
7 changed files with 876 additions and 96 deletions

54
Cargo.lock generated
View File

@@ -2,6 +2,18 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "glob" name = "glob"
version = "0.3.2" version = "0.3.2"
@@ -14,6 +26,18 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[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.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.4" version = "2.7.4"
@@ -44,6 +68,29 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "sdl2"
version = "0.35.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a"
dependencies = [
"bitflags",
"lazy_static",
"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]] [[package]]
name = "serde" name = "serde"
version = "1.0.219" version = "1.0.219"
@@ -98,5 +145,12 @@ name = "untitled"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"glob", "glob",
"sdl2",
"serde_json", "serde_json",
] ]
[[package]]
name = "version-compare"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"

View File

@@ -8,3 +8,4 @@ edition = "2021"
[dependencies] [dependencies]
serde_json = "1.0.140" serde_json = "1.0.140"
glob = "0.3.2" glob = "0.3.2"
sdl2 = "0.35.2"

BIN
SDL2.dll Normal file

Binary file not shown.

BIN
SDL2.lib Normal file

Binary file not shown.

BIN
gb240p.gb Normal file

Binary file not shown.

493
src/lcd.rs Normal file
View File

@@ -0,0 +1,493 @@
use crate::registers::LCDControlRegister;
use std::cmp::min;
// LCD Mode represents the current mode of the LCD controller
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum LCDMode {
HBlank = 0, // Horizontal Blank
VBlank = 1, // Vertical Blank
OAMSearch = 2, // Searching OAM (Object Attribute Memory)
PixelTransfer = 3, // Transferring data to LCD driver
}
// LCD-related constants
pub const SCREEN_WIDTH: usize = 160;
pub const SCREEN_HEIGHT: usize = 144;
// LCD memory addresses
pub const LCDC_ADDR: usize = 0xFF40;
pub const STAT_ADDR: usize = 0xFF41;
pub const SCY_ADDR: usize = 0xFF42;
pub const SCX_ADDR: usize = 0xFF43;
pub const LY_ADDR: usize = 0xFF44;
pub const LYC_ADDR: usize = 0xFF45;
pub const DMA_ADDR: usize = 0xFF46;
pub const BGP_ADDR: usize = 0xFF47;
pub const OBP0_ADDR: usize = 0xFF48;
pub const OBP1_ADDR: usize = 0xFF49;
pub const WY_ADDR: usize = 0xFF4A;
pub const WX_ADDR: usize = 0xFF4B;
// LCD timing constants
pub const MODE_0_CYCLES: u32 = 204; // H-Blank
pub const MODE_1_CYCLES: u32 = 456 * 10; // V-Blank (10 scanlines)
pub const MODE_2_CYCLES: u32 = 80; // OAM Search
pub const MODE_3_CYCLES: u32 = 172; // Pixel Transfer
// OAM (Object Attribute Memory) constants
pub const OAM_SIZE: usize = 160; // 40 sprites * 4 bytes each
// GPU with LCD functionality
#[derive(Debug)]
pub struct GPU {
pub vram: Vec<u8>,
pub tile_set: [[[TilePixelValue; 8]; 8]; 384],
// LCD registers
pub lcd_control: LCDControlRegister,
pub lcd_status: u8,
pub scroll_y: u8,
pub scroll_x: u8,
pub ly: u8, // Current scanline
pub lyc: u8, // Scanline compare
pub window_y: u8,
pub window_x: u8,
pub bg_palette: u8,
pub obj_palette0: u8,
pub obj_palette1: u8,
// OAM (Object Attribute Memory) for sprites
pub oam: [u8; OAM_SIZE],
// LCD screen buffer (160x144 pixels)
pub screen_buffer: [[u8; SCREEN_WIDTH]; SCREEN_HEIGHT],
// LCD timing
pub mode_clock: u32,
pub mode: LCDMode,
}
#[derive(Copy, Clone, Debug)]
pub enum TilePixelValue { Zero, One, Two, Three }
impl GPU {
// Create a new GPU with default values
pub fn new(vram: Vec<u8>) -> Self {
GPU {
vram,
tile_set: [[[TilePixelValue::Zero; 8]; 8]; 384],
lcd_control: LCDControlRegister::from(0),
lcd_status: 0,
scroll_y: 0,
scroll_x: 0,
ly: 0,
lyc: 0,
window_y: 0,
window_x: 0,
bg_palette: 0,
obj_palette0: 0,
obj_palette1: 0,
oam: [0; OAM_SIZE],
screen_buffer: [[0; SCREEN_WIDTH]; SCREEN_HEIGHT],
mode_clock: 0,
mode: LCDMode::HBlank,
}
}
// Read from VRAM
pub fn read_vram(&self, address: usize) -> u8 {
self.vram[address]
}
// Write to VRAM and update tile data if necessary
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.
// For example, `12 & 0xFFFE == 12` and `13 & 0xFFFE == 12`
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];
// Tiles are 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 {
// To determine a pixel's value, we must first find the corresponding bit that encodes
// that pixel value.
let mask = 1 << (7 - pixel_index);
let lsb = byte1 & mask;
let msb = byte2 & mask;
// If the masked values are not 0, the masked bit must be 1. If they are 0, the masked
// bit must be 0.
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;
}
}
// Read from OAM
pub fn read_oam(&self, address: usize) -> u8 {
self.oam[address]
}
// Write to OAM
pub fn write_oam(&mut self, address: usize, value: u8) {
self.oam[address] = value;
}
// Update the LCD state
pub fn update(&mut self, cycles: u32) {
// If LCD is disabled, reset and return
if !self.lcd_control.lcd_enabled {
self.mode_clock = 0;
self.ly = 0;
self.mode = LCDMode::HBlank;
return;
}
self.mode_clock += cycles;
// Update LCD based on current mode
match self.mode {
LCDMode::HBlank => {
if self.mode_clock >= MODE_0_CYCLES {
self.mode_clock = 0;
self.ly += 1;
if self.ly == SCREEN_HEIGHT as u8 {
// Enter V-Blank
self.mode = LCDMode::VBlank;
// TODO: Request V-Blank interrupt
} else {
// Start next scanline
self.mode = LCDMode::OAMSearch;
}
}
},
LCDMode::VBlank => {
if self.mode_clock >= MODE_1_CYCLES / 10 {
self.mode_clock = 0;
self.ly += 1;
// Debug print for VBlank mode
println!("VBlank: LY = {}", self.ly);
if self.ly > 153 {
// End of V-Blank, start new frame
self.ly = 0;
self.mode = LCDMode::OAMSearch;
println!("End of VBlank, starting new frame");
}
}
},
LCDMode::OAMSearch => {
if self.mode_clock >= MODE_2_CYCLES {
self.mode_clock = 0;
self.mode = LCDMode::PixelTransfer;
}
},
LCDMode::PixelTransfer => {
if self.mode_clock >= MODE_3_CYCLES {
self.mode_clock = 0;
self.mode = LCDMode::HBlank;
// Render scanline
self.render_scanline();
}
}
}
// Update LCD status register
self.update_status();
// Print debug info when LY changes
// if self.ly != old_ly {
// println!("LY changed: {} -> {} (Mode: {:?})", old_ly, self.ly, self.mode);
// }
}
// Update the LCD status register
fn update_status(&mut self) {
// Update mode bits
self.lcd_status = (self.lcd_status & 0xFC) | (self.mode as u8);
// Check LYC=LY coincidence flag
if self.ly == self.lyc {
self.lcd_status |= 0x04;
// TODO: Request LYC=LY interrupt if enabled
} else {
self.lcd_status &= !0x04;
}
// TODO: Check for mode interrupts
}
// Render a single scanline
fn render_scanline(&mut self) {
// Debug print for rendering
// if self.ly % 20 == 0 { // Print every 20th scanline to avoid flooding the console
// println!("Rendering scanline {} (Mode: {:?})", self.ly, self.mode);
// println!(" LCD Control: {:?}", self.lcd_control);
// println!(" Background Palette: 0x{:02X}", self.bg_palette);
// }
if self.lcd_control.bg_and_window_enable {
self.render_background();
if self.lcd_control.window_enable {
self.render_window();
}
} else {
}
if self.lcd_control.object_enable {
self.render_sprites();
}
}
// Render the background for the current scanline
fn render_background(&mut self) {
let tile_map_area = if self.lcd_control.bg_tile_map_area { 0x9C00 } else { 0x9800 };
let tile_data_area = if self.lcd_control.bg_and_window_tile_area { 0x8000 } else { 0x8800 };
let signed_addressing = tile_data_area == 0x8800;
// // Debug print for rendering
// if self.ly == 80 { // Only print for a specific scanline to avoid flooding the console
// println!("Rendering background for scanline {}", self.ly);
// println!(" Tile map area: 0x{:04X}", tile_map_area);
// println!(" Tile data area: 0x{:04X}", tile_data_area);
// println!(" Signed addressing: {}", signed_addressing);
// println!(" Background palette: 0x{:02X}", self.bg_palette);
// println!(" LCD Control: {:?}", self.lcd_control);
// }
let y_pos = self.ly.wrapping_add(self.scroll_y);
let tile_row = (y_pos / 8) as usize;
// Track if any non-zero pixels are set
let mut non_zero_pixels = 0;
for x in 0..SCREEN_WIDTH {
let x_pos = (x as u8).wrapping_add(self.scroll_x);
let tile_col = (x_pos / 8) as usize;
// Get the tile index from the tile map
let tile_map_addr = tile_map_area - 0x8000 + tile_row * 32 + tile_col;
let tile_index = self.vram[tile_map_addr];
// Get the tile data
let tile_data_addr = if signed_addressing {
// 8800 method uses signed addressing
let signed_index = tile_index as i8;
// Calculate the offset in i16 to handle negative indices correctly
let offset = 0x1000i16 + (signed_index as i16 * 16);
// Ensure the result is non-negative before converting to usize
if offset < 0 {
// Handle the error case - use a default address or log an error
println!("Warning: Negative tile data address calculated in render_background: {}", offset);
0 // Use tile 0 as a fallback
} else {
offset as usize
}
} else {
// 8000 method uses unsigned addressing
(tile_data_area - 0x8000) + (tile_index as usize * 16)
};
// Get the specific row of the tile
let row = (y_pos % 8) as usize;
let row_addr = tile_data_addr + row * 2;
// Get the pixel data for the row
let byte1 = self.vram[row_addr];
let byte2 = self.vram[row_addr + 1];
// Get the specific pixel in the row
let bit = 7 - (x_pos % 8);
let pixel = ((byte1 >> bit) & 1) | (((byte2 >> bit) & 1) << 1);
// Map the pixel value through the palette
let color = (self.bg_palette >> (pixel * 2)) & 0x03;
// Set the pixel in the screen buffer
self.screen_buffer[self.ly as usize][x] = color;
// Count non-zero pixels
if color > 0 {
non_zero_pixels += 1;
}
}
}
// Render the window for the current scanline
fn render_window(&mut self) {
// Only render if the window is visible on this scanline
if self.window_y > self.ly {
return;
}
let tile_map_area = if self.lcd_control.window_tile_map_area { 0x9C00 } else { 0x9800 };
let tile_data_area = if self.lcd_control.bg_and_window_tile_area { 0x8000 } else { 0x8800 };
let signed_addressing = tile_data_area == 0x8800;
let y_pos = self.ly - self.window_y;
let tile_row = (y_pos / 8) as usize;
for x in 0..SCREEN_WIDTH {
// Window X position is offset by 7
let window_x = self.window_x.wrapping_sub(7);
// Only render if this pixel is within the window
if (x as u8) < window_x {
continue;
}
let x_pos = (x as u8) - window_x;
let tile_col = (x_pos / 8) as usize;
// Get the tile index from the tile map
let tile_map_addr = tile_map_area - 0x8000 + tile_row * 32 + tile_col;
let tile_index = self.vram[tile_map_addr];
// Get the tile data
let tile_data_addr = if signed_addressing {
// 8800 method uses signed addressing
let signed_index = tile_index as i8;
// Calculate the offset in i16 to handle negative indices correctly
let offset = 0x1000i16 + (signed_index as i16 * 16);
// Ensure the result is non-negative before converting to usize
if offset < 0 {
// Handle the error case - use a default address or log an error
println!("Warning: Negative tile data address calculated in render_window: {}", offset);
0 // Use tile 0 as a fallback
} else {
offset as usize
}
} else {
// 8000 method uses unsigned addressing
(tile_data_area - 0x8000) + (tile_index as usize * 16)
};
// Get the specific row of the tile
let row = (y_pos % 8) as usize;
let row_addr = tile_data_addr + row * 2;
// Get the pixel data for the row
let byte1 = self.vram[row_addr];
let byte2 = self.vram[row_addr + 1];
// Get the specific pixel in the row
let bit = 7 - (x_pos % 8);
let pixel = ((byte1 >> bit) & 1) | (((byte2 >> bit) & 1) << 1);
// Map the pixel value through the palette
let color = (self.bg_palette >> (pixel * 2)) & 0x03;
// Set the pixel in the screen buffer
self.screen_buffer[self.ly as usize][x] = color;
}
}
// Render sprites for the current scanline
fn render_sprites(&mut self) {
// Sprite height depends on the object size flag
let sprite_height = if self.lcd_control.object_size { 16 } else { 8 };
// We can have up to 10 sprites per scanline
let mut sprites_on_line = 0;
// Check all 40 sprites
for sprite_idx in 0..40 {
if sprites_on_line >= 10 {
break;
}
let sprite_addr = sprite_idx * 4;
let sprite_y = self.oam[sprite_addr].wrapping_sub(16);
let sprite_x = self.oam[sprite_addr + 1].wrapping_sub(8);
let tile_idx = self.oam[sprite_addr + 2];
let attributes = self.oam[sprite_addr + 3];
// Check if sprite is on this scanline
if self.ly < sprite_y || self.ly >= sprite_y.wrapping_add(sprite_height) {
continue;
}
sprites_on_line += 1;
// Get sprite flags
let palette = if attributes & 0x10 != 0 { self.obj_palette1 } else { self.obj_palette0 };
let x_flip = attributes & 0x20 != 0;
let y_flip = attributes & 0x40 != 0;
let priority = attributes & 0x80 != 0;
// Calculate the row of the sprite to use
let mut row = (self.ly - sprite_y) as usize;
if y_flip {
row = sprite_height as usize - 1 - row;
}
// For 8x16 sprites, the bottom half uses the next tile
let tile = if sprite_height == 16 && row >= 8 {
(tile_idx & 0xFE) + 1
} else {
tile_idx
};
// Get the tile data address
let tile_addr = (0x8000 - 0x8000) + (tile as usize * 16) + (row % 8) * 2;
// Get the pixel data for the row
let byte1 = self.vram[tile_addr];
let byte2 = self.vram[tile_addr + 1];
// Draw the sprite pixels
for x in 0..8 {
// Skip if sprite pixel is off-screen
if sprite_x.wrapping_add(x) >= SCREEN_WIDTH as u8 {
continue;
}
// Get the pixel bit (flipped if needed)
let bit = if x_flip { x } else { 7 - x };
let pixel = ((byte1 >> bit) & 1) | (((byte2 >> bit) & 1) << 1);
// Skip transparent pixels
if pixel == 0 {
continue;
}
// Check background priority
let screen_x = sprite_x.wrapping_add(x) as usize;
if priority && self.screen_buffer[self.ly as usize][screen_x] != 0 {
continue;
}
// Map the pixel value through the palette
let color = (palette >> (pixel * 2)) & 0x03;
// Set the pixel in the screen buffer
self.screen_buffer[self.ly as usize][screen_x] = color;
}
}
}
}

View File

@@ -1,11 +1,19 @@
mod registers; mod registers;
mod instructions; mod instructions;
mod lcd;
use glob::glob; use glob::glob;
use serde_json::Value; use serde_json::Value;
use crate::instructions::{Condition, Target, LoadTarget, TargetRegister, TargetU16Register, Instruction, parse_instruction}; use crate::instructions::{Condition, Target, LoadTarget, TargetRegister, TargetU16Register, Instruction, parse_instruction};
use crate::registers::FlagsRegister; use crate::registers::FlagsRegister;
use crate::registers::Registers; use crate::registers::Registers;
use crate::lcd::{GPU, LY_ADDR, LCDC_ADDR, SCREEN_WIDTH, SCREEN_HEIGHT, BGP_ADDR, LCDMode};
use sdl2::pixels::Color;
use sdl2::event::Event;
use sdl2::keyboard::Keycode;
use sdl2::rect::Rect;
use std::time::{Duration, Instant};
#[derive(Debug)] #[derive(Debug)]
@@ -129,82 +137,12 @@ struct CPU {
const BOOT_BEGIN: usize = 0x0000; const BOOT_BEGIN: usize = 0x0000;
const BOOT_END: usize = 0x00FF; const BOOT_END: usize = 0x00FF;
const CART_BEGIN: usize = 0x0100; const CART_BEGIN: usize = 0x0000;
const CART_END: usize = 0x7FFF; const CART_END: usize = 0x7FFF;
const VRAM_BEGIN: usize = 0x8000; const VRAM_BEGIN: usize = 0x8000;
const VRAM_END: usize = 0x9FFF; const VRAM_END: usize = 0x9FFF;
#[derive(Debug)] // GPU implementation moved to lcd.rs
struct GPU { vram: Vec<u8>, tile_set: [[[TilePixelValue; 8]; 8]; 384] }
#[derive(Copy, Clone, Debug)]
enum TilePixelValue { Three, Two, One, Zero }
impl GPU {
fn read_vram(&self, address: usize) -> u8 {
self.vram[address]
}
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.
// For example, `12 & 0xFFFE == 12` and `13 & 0xFFFE == 12`
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];
// Tiles are 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 {
// To determine a pixel's value, we must first find the corresponding bit that encodes
// that pixel value:
// 1111_1111
// 0123 4567
//
// As you can see, the bit that corresponds to the nth pixel is the bit in the nth
// position *from the left*. Bits are normally indexed from the right.
//
// To find the first pixel (a.k.a pixel 0) we find the left most bit (a.k.a bit 7). For
// the second pixel (a.k.a pixel 1) we first the second most left bit (a.k.a bit 6) and
// so on.
//
// We then create a mask with a 1 at that position and 0 s everywhere else.
//
// Bitwise ANDing this mask with our bytes will leave that particular bit with its
// original value and every other bit with a 0.
let mask = 1 << (7 - pixel_index);
let lsb = byte1 & mask;
let msb = byte2 & mask;
// If the masked values are not 0, the masked bit must be 1. If they are 0, the masked
// bit must be 0.
//
// Finally, we can tell which of the four tile values the pixel is. For example, if the least
// significant byte's bit is 1 and the most significant byte's bit is also 1, then we
// have tile value `Three`.
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;
}
}
}
struct ExecutionReturn { struct ExecutionReturn {
pc: u16, pc: u16,
@@ -225,22 +163,37 @@ impl MemoryBus {
return self.memory[address] return self.memory[address]
} }
match address { // Special logging for reads from 0xFF50 (boot ROM control)
if address == 0xFF50 {
println!("READ from 0xFF50: 0x{:02X} (Boot ROM is {})",
self.memory[0xFF50], if self.memory[0xFF50] == 0 { "enabled" } else { "disabled" });
}
let result = match address {
VRAM_BEGIN ..= VRAM_END => { VRAM_BEGIN ..= VRAM_END => {
self.gpu.read_vram(address - VRAM_BEGIN) self.gpu.read_vram(address - VRAM_BEGIN)
} }
BOOT_BEGIN..=BOOT_END => { CART_BEGIN ..= CART_END => {
if self.memory[0xFF50] == 0x00 { if address < 0x100 && self.memory[0xFF50] == 0x00 {
// Special logging for bootloader reads
if address == 0 {
println!("Reading from boot ROM at address 0x0000");
}
self.boot[address] self.boot[address]
}else { } else {
// Special logging for game ROM reads that would be bootloader if enabled
if address < 0x100 && self.memory[0xFF50] != 0x00 {
println!("Reading from game ROM at address 0x{:04X} (boot ROM disabled)", address);
}
self.rom.read_byte(address) self.rom.read_byte(address)
} }
} }
CART_BEGIN ..= CART_END => { LCDC_ADDR => {u8::from(self.gpu.lcd_control)}
self.rom.read_byte(address) LY_ADDR => {self.gpu.ly}
}
_ => self.memory[address] _ => self.memory[address]
} };
result
} }
fn write_byte(&mut self, address: u16, value: u8) { fn write_byte(&mut self, address: u16, value: u8) {
if self.flat_ram { if self.flat_ram {
@@ -248,6 +201,13 @@ impl MemoryBus {
return; return;
} }
let address = address as usize; let address = address as usize;
// Special logging for writes to 0xFF50 (boot ROM control)
if address == 0xFF50 {
println!("WRITE to 0xFF50: 0x{:02X} (Boot ROM will be {}) at address 0x{:04X}",
value, if value == 0 { "enabled" } else { "disabled" }, address);
}
match address { match address {
VRAM_BEGIN ..= VRAM_END => { VRAM_BEGIN ..= VRAM_END => {
self.gpu.write_vram(address - VRAM_BEGIN, value) self.gpu.write_vram(address - VRAM_BEGIN, value)
@@ -278,6 +238,99 @@ impl CPU {
println!("{l}"); println!("{l}");
println!("CPU init"); println!("CPU init");
self.bus.write_byte(0xFF50, 0x00); self.bus.write_byte(0xFF50, 0x00);
// Enable LCD and set all necessary LCD control flags
self.bus.gpu.lcd_control.lcd_enabled = true;
self.bus.gpu.lcd_control.bg_and_window_enable = true;
self.bus.gpu.lcd_control.bg_tile_map_area = false; // Use 0x9800-0x9BFF
self.bus.gpu.lcd_control.bg_and_window_tile_area = true; // Use 0x8000-0x8FFF
self.bus.gpu.lcd_control.window_enable = false; // Disable window for now
self.bus.gpu.lcd_control.object_enable = false; // Disable sprites for now
// Write the LCD control register to memory
self.bus.memory[LCDC_ADDR] = u8::from(self.bus.gpu.lcd_control);
println!("Set LCDC register to: 0x{:02X}", u8::from(self.bus.gpu.lcd_control));
// Set background palette (0b11100100)
// 11 = Darkest (color 3)
// 10 = Dark (color 2)
// 01 = Light (color 1)
// 00 = Lightest (color 0)
self.bus.gpu.bg_palette = 0xE4;
self.bus.memory[BGP_ADDR] = self.bus.gpu.bg_palette;
println!("Set BGP register to: 0x{:02X}", self.bus.gpu.bg_palette);
// Initialize VRAM with some test pattern to verify rendering
println!("Initializing VRAM with test pattern");
// Create a simple checkerboard pattern in the first tile
let tile_data = [
0xFF, 0x00, // Row 1: all pixels set to color 1
0xFF, 0xFF, // Row 2: all pixels set to color 3
0x00, 0xFF, // Row 3: all pixels set to color 2
0x00, 0x00, // Row 4: all pixels set to color 0
0xFF, 0x00, // Row 5: all pixels set to color 1
0xFF, 0xFF, // Row 6: all pixels set to color 3
0x00, 0xFF, // Row 7: all pixels set to color 2
0x00, 0x00, // Row 8: all pixels set to color 0
];
// Create a solid pattern for the second tile
let tile_data2 = [
0xFF, 0xFF, // Row 1: all pixels set to color 3
0xFF, 0xFF, // Row 2: all pixels set to color 3
0xFF, 0xFF, // Row 3: all pixels set to color 3
0xFF, 0xFF, // Row 4: all pixels set to color 3
0xFF, 0xFF, // Row 5: all pixels set to color 3
0xFF, 0xFF, // Row 6: all pixels set to color 3
0xFF, 0xFF, // Row 7: all pixels set to color 3
0xFF, 0xFF, // Row 8: all pixels set to color 3
];
// Create a striped pattern for the third tile
let tile_data3 = [
0xFF, 0x00, // Row 1: all pixels set to color 1
0x00, 0xFF, // Row 2: all pixels set to color 2
0xFF, 0x00, // Row 3: all pixels set to color 1
0x00, 0xFF, // Row 4: all pixels set to color 2
0xFF, 0x00, // Row 5: all pixels set to color 1
0x00, 0xFF, // Row 6: all pixels set to color 2
0xFF, 0x00, // Row 7: all pixels set to color 1
0x00, 0xFF, // Row 8: all pixels set to color 2
];
// Write the test patterns to the first three tiles in VRAM
for (i, &byte) in tile_data.iter().enumerate() {
self.bus.gpu.write_vram(i, byte);
}
for (i, &byte) in tile_data2.iter().enumerate() {
self.bus.gpu.write_vram(16 + i, byte); // Tile 1 starts at offset 16
}
for (i, &byte) in tile_data3.iter().enumerate() {
self.bus.gpu.write_vram(32 + i, byte); // Tile 2 starts at offset 32
}
// Fill the first 16x16 area of the background tile map with our test tiles
for y in 0..16 {
for x in 0..16 {
let tile_idx = (y % 3) * 1 + (x % 3); // Use tiles 0, 1, 2 in a pattern
let map_addr = 0x1800 + y * 32 + x; // Background map starts at 0x1800
self.bus.gpu.write_vram(map_addr, tile_idx as u8);
}
}
// Set scroll registers to center the display
// The test pattern is 16x16 tiles (128x128 pixels)
// The GameBoy screen is 160x144 pixels
// To center, we offset by (pattern_size - screen_size) / 2
self.bus.gpu.scroll_x = 0; // Center horizontally
self.bus.gpu.scroll_y = 0; // Center vertically
println!("VRAM initialized with test pattern");
println!("LCD Control: {:?}", self.bus.gpu.lcd_control);
println!("Background Palette: 0x{:02X}", self.bus.gpu.bg_palette);
} }
fn load_test(test: &Value) -> CPU { fn load_test(test: &Value) -> CPU {
@@ -306,7 +359,7 @@ impl CPU {
sp: test["sp"].as_u64().unwrap() as u16, sp: test["sp"].as_u64().unwrap() as u16,
bus:MemoryBus{ bus:MemoryBus{
memory, memory,
gpu:GPU{ vram, tile_set: [[[TilePixelValue::Zero; 8]; 8]; 384] }, gpu: GPU::new(vram),
rom: GameRom{ rom: GameRom{
data:game_rom, data:game_rom,
title: "test".parse().unwrap(), title: "test".parse().unwrap(),
@@ -455,8 +508,45 @@ impl CPU {
let inst = parse_instruction(self.bus.read_byte(self.pc), self.bus.read_byte(self.pc.wrapping_add(1)), self.bus.read_byte(self.pc.wrapping_add(2))); let inst = parse_instruction(self.bus.read_byte(self.pc), self.bus.read_byte(self.pc.wrapping_add(1)), self.bus.read_byte(self.pc.wrapping_add(2)));
// println!("{:x} {:?} {:?}", self.pc, inst, self.registers.f); // println!("{:x} {:?} {:?}", self.pc, inst, self.registers.f);
// Check if we're in a loop by detecting repeated PC values
static mut LAST_PC_VALUES: [u16; 10] = [0; 10];
static mut PC_INDEX: usize = 0;
unsafe {
// Check if we've seen this PC value multiple times in a row
let mut loop_detected = true;
for i in 0..10 {
if LAST_PC_VALUES[i] != self.pc {
loop_detected = false;
break;
}
}
if loop_detected {
println!("LOOP DETECTED at PC: {:04X}", self.pc);
println!("Boot ROM status: 0x{:02X}", self.bus.memory[0xFF50]);
// Print nearby memory for debugging
println!("Memory around 0xFF50:");
for i in 0xFF40..=0xFF60 {
print!("{:04X}: {:02X} ", i, self.bus.memory[i]);
if (i - 0xFF40 + 1) % 4 == 0 {
println!();
}
}
}
// Update the PC history
LAST_PC_VALUES[PC_INDEX] = self.pc;
PC_INDEX = (PC_INDEX + 1) % 10;
}
let result = self.execute(inst); let result = self.execute(inst);
self.pc = result.pc; self.pc = result.pc;
// Update the LCD based on the number of cycles
self.bus.gpu.update(result.cycles as u32);
result.cycles result.cycles
} }
@@ -641,6 +731,18 @@ impl CPU {
ExecutionReturn{pc: self.pc.wrapping_add(1), cycles: 1} ExecutionReturn{pc: self.pc.wrapping_add(1), cycles: 1}
} }
Instruction::HALT => { Instruction::HALT => {
// In a real GameBoy, HALT would wait for an interrupt
// Since we don't have proper interrupt handling yet, we'll just continue execution
// This should prevent the bootloader from getting stuck in a loop
println!("HALT instruction at PC: {:04X} - Continuing execution", self.pc);
// Force the boot ROM to be disabled if we're in the bootloader
// This is a workaround to allow the game to start
// if self.pc < 0x100 && self.bus.memory[0xFF50] == 0x00 {
// println!("Forcing boot ROM disable at PC: {:04X}", self.pc);
// self.bus.memory[0xFF50] = 0x01;
// }
ExecutionReturn{pc: self.pc.wrapping_add(1), cycles: 3} ExecutionReturn{pc: self.pc.wrapping_add(1), cycles: 3}
} }
@@ -1026,8 +1128,8 @@ impl CPU {
fn main() { fn main() {
run_instruction_tests() // run_instruction_tests()
// run_gameboy() run_gameboy()
} }
fn run_instruction_tests() { fn run_instruction_tests() {
@@ -1061,17 +1163,46 @@ fn run_instruction_tests() {
} }
} }
fn run_gameboy() { fn run_gameboy() {
let boot_rom = std::fs::read("boot/dmg.bin").unwrap(); // Initialize SDL2
let game_rom= GameRom::load("cpu_instrs/cpu_instrs.gb"); let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
// let game_rom = std::fs::read("cpu_instrs/cpu_instrs.gb").unwrap(); // Create a window larger than the scaled GameBoy screen to allow for centering
// Add 60 pixels of padding on each side (30 pixels on each edge)
let window_width = (SCREEN_WIDTH * 3 + 60) as u32;
let window_height = (SCREEN_HEIGHT * 3 + 60) as u32;
let window = video_subsystem.window("GameBoy Emulator", window_width, window_height)
.position_centered()
.build()
.unwrap();
// Create a renderer
let mut canvas = window.into_canvas().build().unwrap();
let texture_creator = canvas.texture_creator();
let mut texture = texture_creator.create_texture_streaming(
sdl2::pixels::PixelFormatEnum::RGB24,
SCREEN_WIDTH as u32,
SCREEN_HEIGHT as u32
).unwrap();
// Define the GameBoy color palette (from lightest to darkest)
let gb_colors = [
Color::RGB(155, 188, 15), // Light green
Color::RGB(139, 172, 15), // Medium green
Color::RGB(48, 98, 48), // Dark green
Color::RGB(15, 56, 15), // Darkest green
];
// Initialize the GameBoy
let boot_rom = std::fs::read("boot/dmg.bin").unwrap();
let game_rom = GameRom::load("dmg-acid2.gb");
let mut gameboy = CPU{ let mut gameboy = CPU{
registers:Registers{a:0,b:0,c:0,d:0,e:0,f:FlagsRegister::from(0),h:0,l:0}, registers: Registers{a:0,b:0,c:0,d:0,e:0,f:FlagsRegister::from(0),h:0,l:0},
pc:0, pc: 0,
bus:MemoryBus{ bus: MemoryBus{
memory: [0xFFu8; 0xFFFF+1], memory: [0xFFu8; 0xFFFF+1],
gpu:GPU{ vram: vec![0xFFu8;VRAM_END-VRAM_BEGIN+1], tile_set: [[[TilePixelValue::Zero; 8]; 8]; 384] }, gpu: GPU::new(vec![0xFFu8;VRAM_END-VRAM_BEGIN+1]),
rom: game_rom, rom: game_rom,
boot: boot_rom, boot: boot_rom,
flat_ram: false flat_ram: false
@@ -1080,12 +1211,113 @@ fn run_gameboy() {
interrupts_enabled: false, interrupts_enabled: false,
}; };
gameboy.init(); gameboy.init();
let mut count =0; // Set up event handling
loop { let mut event_pump = sdl_context.event_pump().unwrap();
count += 1;
if count % 1_000_000 == 0 { // Main loop
println!("PC: {:04X}, {count}", gameboy.pc); let mut count = 0;
let mut last_render_time = Instant::now();
let frame_duration = Duration::from_millis(16); // ~60 FPS
'running: loop {
// Handle events
for event in event_pump.poll_iter() {
match event {
Event::Quit {..} |
Event::KeyDown { keycode: Some(Keycode::Escape), .. } => {
break 'running;
},
_ => {}
}
} }
gameboy.execute_next_instruction();
// Execute instructions until it's time to render again
let now = Instant::now();
// Execute a minimum number of instructions per frame to ensure progress
let min_instructions_per_frame = 10000;
let mut instructions_this_frame = 0;
while now.duration_since(last_render_time) < frame_duration || instructions_this_frame < min_instructions_per_frame {
gameboy.execute_next_instruction();
count += 1;
instructions_this_frame += 1;
// Avoid getting stuck in this loop if it takes too long
if instructions_this_frame > min_instructions_per_frame * 2 {
println!("Warning: Executed {} instructions this frame, breaking loop", instructions_this_frame);
break;
}
}
// Log the number of instructions executed this frame
if count % 60 == 0 { // Log once per second
println!("Executed {} instructions this frame", instructions_this_frame);
}
// Render the screen
// Debug: Check if the screen buffer has any non-zero values
let mut non_zero_pixels = 0;
for y in 0..SCREEN_HEIGHT {
for x in 0..SCREEN_WIDTH {
if gameboy.bus.gpu.screen_buffer[y][x] > 0 {
non_zero_pixels += 1;
}
}
}
// Track changes in the screen buffer
static mut LAST_BUFFER_HASH: u64 = 0;
static mut FRAMES_SINCE_CHANGE: u32 = 0;
static mut LAST_CHANGE_COUNT: u32 = 0;
// Calculate a simple hash of the screen buffer
let mut buffer_hash: u64 = 0;
for y in 0..SCREEN_HEIGHT {
for x in 0..SCREEN_WIDTH {
buffer_hash = buffer_hash.wrapping_mul(31).wrapping_add(gameboy.bus.gpu.screen_buffer[y][x] as u64);
}
}
texture.with_lock(None, |buffer: &mut [u8], pitch: usize| {
for y in 0..SCREEN_HEIGHT {
for x in 0..SCREEN_WIDTH {
let color_idx = gameboy.bus.gpu.screen_buffer[y][x] as usize;
let color = gb_colors[color_idx];
let offset = y * pitch + x * 3;
buffer[offset] = color.r;
buffer[offset + 1] = color.g;
buffer[offset + 2] = color.b;
// Debug: Print color values for a few pixels
if count % 60 == 0 && y == 75 && x < 5 {
println!("Pixel at ({}, {}): color_idx={}, RGB=({}, {}, {})",
x, y, color_idx, color.r, color.g, color.b);
}
}
}
}).unwrap();
// Clear the canvas with a dark color for the border
canvas.set_draw_color(Color::RGB(24, 24, 24)); // Dark gray
canvas.clear();
// Calculate the position to center the texture in the window
let window_width = (SCREEN_WIDTH * 3 + 60) as u32;
let window_height = (SCREEN_HEIGHT * 3 + 60) as u32;
let x = (window_width - (SCREEN_WIDTH * 3) as u32) / 2;
let y = (window_height - (SCREEN_HEIGHT * 3) as u32) / 2;
canvas.copy(&texture, None, Some(Rect::new(x as i32, y as i32,
(SCREEN_WIDTH * 3) as u32,
(SCREEN_HEIGHT * 3) as u32))).unwrap();
canvas.present();
// Update timing
last_render_time = now;
} }
} }