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.
This commit is contained in:
54
Cargo.lock
generated
54
Cargo.lock
generated
@@ -2,6 +2,18 @@
|
||||
# It is not intended for manual editing.
|
||||
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]]
|
||||
name = "glob"
|
||||
version = "0.3.2"
|
||||
@@ -14,6 +26,18 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
@@ -44,6 +68,29 @@ version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
@@ -98,5 +145,12 @@ name = "untitled"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"sdl2",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
|
||||
|
||||
@@ -8,3 +8,4 @@ edition = "2021"
|
||||
[dependencies]
|
||||
serde_json = "1.0.140"
|
||||
glob = "0.3.2"
|
||||
sdl2 = "0.35.2"
|
||||
|
||||
666
src/lcd.rs
Normal file
666
src/lcd.rs
Normal file
@@ -0,0 +1,666 @@
|
||||
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) {
|
||||
// Debug: Track update calls
|
||||
static mut UPDATE_COUNTER: u32 = 0;
|
||||
static mut FORCE_REFRESH_COUNTER: u32 = 0;
|
||||
unsafe {
|
||||
UPDATE_COUNTER += 1;
|
||||
FORCE_REFRESH_COUNTER += 1;
|
||||
|
||||
if UPDATE_COUNTER % 10000 == 0 {
|
||||
println!("GPU update called {} times, cycles={}", UPDATE_COUNTER, cycles);
|
||||
}
|
||||
|
||||
// Force a complete refresh of the screen buffer periodically
|
||||
if FORCE_REFRESH_COUNTER >= 1_000_000 {
|
||||
println!("Forcing complete screen refresh");
|
||||
|
||||
// Force render all scanlines
|
||||
for scanline in 0..SCREEN_HEIGHT {
|
||||
self.ly = scanline as u8;
|
||||
self.render_scanline();
|
||||
}
|
||||
|
||||
// Reset counter
|
||||
FORCE_REFRESH_COUNTER = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Store the old LY value for change detection
|
||||
let old_ly = self.ly;
|
||||
|
||||
// Check if we're stuck at LY < 144
|
||||
static mut STUCK_COUNTER: u32 = 0;
|
||||
unsafe {
|
||||
if self.ly < 144 {
|
||||
STUCK_COUNTER += 1;
|
||||
// If we've been stuck for a while, force LY to increment
|
||||
if STUCK_COUNTER > 10000 {
|
||||
println!("Forcing LY increment from {} to {}", self.ly, self.ly + 1);
|
||||
self.ly += 1;
|
||||
if self.ly == 144 {
|
||||
self.mode = LCDMode::VBlank;
|
||||
println!("Forced transition to VBlank at LY = 144");
|
||||
}
|
||||
STUCK_COUNTER = 0;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
STUCK_COUNTER = 0;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Debug print for HBlank mode when approaching VBlank
|
||||
if self.ly >= 140 {
|
||||
println!("HBlank: LY = {} (approaching VBlank)", self.ly);
|
||||
}
|
||||
|
||||
if self.ly == SCREEN_HEIGHT as u8 {
|
||||
// Enter V-Blank
|
||||
self.mode = LCDMode::VBlank;
|
||||
println!("Entering VBlank mode at LY = {}", self.ly);
|
||||
// TODO: Request V-Blank interrupt
|
||||
} else {
|
||||
// Start next scanline
|
||||
self.mode = LCDMode::OAMSearch;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're stuck in HBlank mode
|
||||
static mut HBLANK_STUCK_COUNTER: u32 = 0;
|
||||
unsafe {
|
||||
HBLANK_STUCK_COUNTER += 1;
|
||||
// If we've been stuck in HBlank for a while, force transition to next mode
|
||||
if HBLANK_STUCK_COUNTER > 50000 {
|
||||
println!("Forcing transition from HBlank at LY = {}", self.ly);
|
||||
self.ly += 1;
|
||||
|
||||
if self.ly == SCREEN_HEIGHT as u8 {
|
||||
// Enter V-Blank
|
||||
self.mode = LCDMode::VBlank;
|
||||
println!("Forced transition to VBlank at LY = {}", self.ly);
|
||||
} else {
|
||||
// Start next scanline
|
||||
self.mode = LCDMode::OAMSearch;
|
||||
println!("Forced transition to OAMSearch at LY = {}", self.ly);
|
||||
}
|
||||
|
||||
HBLANK_STUCK_COUNTER = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're stuck in VBlank mode
|
||||
static mut VBLANK_STUCK_COUNTER: u32 = 0;
|
||||
unsafe {
|
||||
VBLANK_STUCK_COUNTER += 1;
|
||||
// If we've been stuck in VBlank for a while, force transition to next frame
|
||||
if VBLANK_STUCK_COUNTER > 100000 {
|
||||
println!("Forcing end of VBlank, LY was {}", self.ly);
|
||||
self.ly = 0;
|
||||
self.mode = LCDMode::OAMSearch;
|
||||
VBLANK_STUCK_COUNTER = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
LCDMode::OAMSearch => {
|
||||
if self.mode_clock >= MODE_2_CYCLES {
|
||||
self.mode_clock = 0;
|
||||
self.mode = LCDMode::PixelTransfer;
|
||||
|
||||
// Debug print for OAMSearch mode when approaching VBlank
|
||||
if self.ly >= 140 {
|
||||
println!("OAMSearch -> PixelTransfer at LY = {}", self.ly);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're stuck in OAMSearch mode
|
||||
static mut OAMSEARCH_STUCK_COUNTER: u32 = 0;
|
||||
unsafe {
|
||||
OAMSEARCH_STUCK_COUNTER += 1;
|
||||
// If we've been stuck in OAMSearch for a while, force transition to PixelTransfer
|
||||
if OAMSEARCH_STUCK_COUNTER > 50000 {
|
||||
println!("Forcing transition from OAMSearch to PixelTransfer at LY = {}", self.ly);
|
||||
self.mode = LCDMode::PixelTransfer;
|
||||
OAMSEARCH_STUCK_COUNTER = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
LCDMode::PixelTransfer => {
|
||||
if self.mode_clock >= MODE_3_CYCLES {
|
||||
self.mode_clock = 0;
|
||||
self.mode = LCDMode::HBlank;
|
||||
|
||||
// Debug print for PixelTransfer mode when approaching VBlank
|
||||
if self.ly >= 140 {
|
||||
println!("PixelTransfer -> HBlank at LY = {}", self.ly);
|
||||
}
|
||||
|
||||
// Render scanline
|
||||
self.render_scanline();
|
||||
}
|
||||
|
||||
// Check if we're stuck in PixelTransfer mode
|
||||
static mut PIXELTRANSFER_STUCK_COUNTER: u32 = 0;
|
||||
unsafe {
|
||||
PIXELTRANSFER_STUCK_COUNTER += 1;
|
||||
// If we've been stuck in PixelTransfer for a while, force transition to HBlank
|
||||
if PIXELTRANSFER_STUCK_COUNTER > 50000 {
|
||||
println!("Forcing transition from PixelTransfer to HBlank at LY = {}", self.ly);
|
||||
self.mode = LCDMode::HBlank;
|
||||
PIXELTRANSFER_STUCK_COUNTER = 0;
|
||||
// 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.ly % 20 == 0 {
|
||||
println!(" Background and window display disabled!");
|
||||
}
|
||||
}
|
||||
|
||||
if self.lcd_control.object_enable {
|
||||
self.render_sprites();
|
||||
}
|
||||
|
||||
// Debug: Check if any pixels are set in this scanline
|
||||
if self.ly % 20 == 0 {
|
||||
let mut non_zero_pixels = 0;
|
||||
for x in 0..SCREEN_WIDTH {
|
||||
if self.screen_buffer[self.ly as usize][x] > 0 {
|
||||
non_zero_pixels += 1;
|
||||
}
|
||||
}
|
||||
println!(" Non-zero pixels in scanline {}: {}/{}", self.ly, non_zero_pixels, SCREEN_WIDTH);
|
||||
|
||||
// Print a sample of the screen buffer for this scanline
|
||||
if non_zero_pixels > 0 {
|
||||
println!(" Sample of screen buffer for scanline {}:", self.ly);
|
||||
for x in 0..min(20, SCREEN_WIDTH) { // Print first 20 pixels
|
||||
print!("{} ", self.screen_buffer[self.ly as usize][x]);
|
||||
if (x + 1) % 10 == 0 {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// Debug: Print detailed palette mapping for a few pixels
|
||||
if self.ly == 80 && x < 5 {
|
||||
println!(" Pixel mapping: value={}, palette=0x{:02X}, shift={}, color={}",
|
||||
pixel, self.bg_palette, pixel * 2, color);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Debug print for the first few pixels of a specific scanline
|
||||
if self.ly == 80 && x < 5 {
|
||||
println!(" Pixel at ({}, {}): value={}, color={}", x, self.ly, pixel, color);
|
||||
println!(" Tile index: 0x{:02X}, Tile data addr: 0x{:04X}", tile_index, tile_data_addr);
|
||||
println!(" Byte1: 0x{:02X}, Byte2: 0x{:02X}, Bit: {}", byte1, byte2, bit);
|
||||
}
|
||||
}
|
||||
|
||||
// Debug print for non-zero pixels
|
||||
if self.ly == 80 {
|
||||
println!(" Non-zero pixels in scanline {}: {}/{}", self.ly, non_zero_pixels, SCREEN_WIDTH);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
520
src/main.rs
520
src/main.rs
@@ -1,11 +1,19 @@
|
||||
mod registers;
|
||||
mod instructions;
|
||||
mod lcd;
|
||||
|
||||
use glob::glob;
|
||||
use serde_json::Value;
|
||||
use crate::instructions::{Condition, Target, LoadTarget, TargetRegister, TargetU16Register, Instruction, parse_instruction};
|
||||
use crate::registers::FlagsRegister;
|
||||
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)]
|
||||
@@ -129,82 +137,12 @@ struct CPU {
|
||||
|
||||
const BOOT_BEGIN: usize = 0x0000;
|
||||
const BOOT_END: usize = 0x00FF;
|
||||
const CART_BEGIN: usize = 0x0100;
|
||||
const CART_BEGIN: usize = 0x0000;
|
||||
const CART_END: usize = 0x7FFF;
|
||||
const VRAM_BEGIN: usize = 0x8000;
|
||||
const VRAM_END: usize = 0x9FFF;
|
||||
|
||||
#[derive(Debug)]
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
// GPU implementation moved to lcd.rs
|
||||
|
||||
struct ExecutionReturn {
|
||||
pc: u16,
|
||||
@@ -225,22 +163,37 @@ impl MemoryBus {
|
||||
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 => {
|
||||
self.gpu.read_vram(address - VRAM_BEGIN)
|
||||
}
|
||||
BOOT_BEGIN..=BOOT_END => {
|
||||
if self.memory[0xFF50] == 0x00 {
|
||||
self.boot[address]
|
||||
}else {
|
||||
self.rom.read_byte(address)
|
||||
}
|
||||
}
|
||||
CART_BEGIN ..= CART_END => {
|
||||
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]
|
||||
} 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.memory[address]
|
||||
}
|
||||
LCDC_ADDR => {u8::from(self.gpu.lcd_control)}
|
||||
LY_ADDR => {self.gpu.ly}
|
||||
_ => self.memory[address]
|
||||
};
|
||||
|
||||
result
|
||||
}
|
||||
fn write_byte(&mut self, address: u16, value: u8) {
|
||||
if self.flat_ram {
|
||||
@@ -248,6 +201,13 @@ impl MemoryBus {
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
VRAM_BEGIN ..= VRAM_END => {
|
||||
self.gpu.write_vram(address - VRAM_BEGIN, value)
|
||||
@@ -278,6 +238,99 @@ impl CPU {
|
||||
println!("{l}");
|
||||
println!("CPU init");
|
||||
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 {
|
||||
@@ -306,7 +359,7 @@ impl CPU {
|
||||
sp: test["sp"].as_u64().unwrap() as u16,
|
||||
bus:MemoryBus{
|
||||
memory,
|
||||
gpu:GPU{ vram, tile_set: [[[TilePixelValue::Zero; 8]; 8]; 384] },
|
||||
gpu: GPU::new(vram),
|
||||
rom: GameRom{
|
||||
data:game_rom,
|
||||
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)));
|
||||
// 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);
|
||||
self.pc = result.pc;
|
||||
|
||||
// Update the LCD based on the number of cycles
|
||||
self.bus.gpu.update(result.cycles as u32);
|
||||
|
||||
result.cycles
|
||||
}
|
||||
|
||||
@@ -641,6 +731,18 @@ impl CPU {
|
||||
ExecutionReturn{pc: self.pc.wrapping_add(1), cycles: 1}
|
||||
}
|
||||
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}
|
||||
}
|
||||
|
||||
@@ -1026,8 +1128,8 @@ impl CPU {
|
||||
|
||||
|
||||
fn main() {
|
||||
run_instruction_tests()
|
||||
// run_gameboy()
|
||||
// run_instruction_tests()
|
||||
run_gameboy()
|
||||
}
|
||||
|
||||
fn run_instruction_tests() {
|
||||
@@ -1061,17 +1163,46 @@ fn run_instruction_tests() {
|
||||
}
|
||||
}
|
||||
fn run_gameboy() {
|
||||
let boot_rom = std::fs::read("boot/dmg.bin").unwrap();
|
||||
let game_rom= GameRom::load("cpu_instrs/cpu_instrs.gb");
|
||||
// Initialize SDL2
|
||||
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("gb240p.gb");
|
||||
|
||||
let mut gameboy = CPU{
|
||||
registers:Registers{a:0,b:0,c:0,d:0,e:0,f:FlagsRegister::from(0),h:0,l:0},
|
||||
pc:0,
|
||||
bus:MemoryBus{
|
||||
registers: Registers{a:0,b:0,c:0,d:0,e:0,f:FlagsRegister::from(0),h:0,l:0},
|
||||
pc: 0,
|
||||
bus: MemoryBus{
|
||||
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,
|
||||
boot: boot_rom,
|
||||
flat_ram: false
|
||||
@@ -1080,12 +1211,209 @@ fn run_gameboy() {
|
||||
interrupts_enabled: false,
|
||||
};
|
||||
gameboy.init();
|
||||
let mut count =0;
|
||||
loop {
|
||||
count += 1;
|
||||
if count % 1_000_000 == 0 {
|
||||
println!("PC: {:04X}, {count}", gameboy.pc);
|
||||
// Set up event handling
|
||||
let mut event_pump = sdl_context.event_pump().unwrap();
|
||||
|
||||
// Main loop
|
||||
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;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the screen buffer has changed
|
||||
unsafe {
|
||||
if buffer_hash != LAST_BUFFER_HASH {
|
||||
println!("Screen buffer changed at count={} (after {} frames)",
|
||||
count, FRAMES_SINCE_CHANGE);
|
||||
LAST_BUFFER_HASH = buffer_hash;
|
||||
FRAMES_SINCE_CHANGE = 0;
|
||||
LAST_CHANGE_COUNT = count;
|
||||
} else {
|
||||
FRAMES_SINCE_CHANGE += 1;
|
||||
if FRAMES_SINCE_CHANGE % 60 == 0 { // Log every second if no change
|
||||
println!("No change in screen buffer for {} frames (last change at count={})",
|
||||
FRAMES_SINCE_CHANGE, LAST_CHANGE_COUNT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print debug info about the screen buffer
|
||||
if count % 60 == 0 { // Print once per second at 60 FPS
|
||||
println!("Screen buffer has {}/{} non-zero pixels",
|
||||
non_zero_pixels, SCREEN_WIDTH * SCREEN_HEIGHT);
|
||||
|
||||
// Print a sample of the screen buffer
|
||||
println!("Sample of screen buffer:");
|
||||
for y in 70..80 { // Print 10 rows around the middle
|
||||
let mut row_str = String::new();
|
||||
for x in 0..10 { // Print first 10 columns
|
||||
row_str.push_str(&format!("{} ", gameboy.bus.gpu.screen_buffer[y][x]));
|
||||
}
|
||||
println!("Row {}: {}", y, row_str);
|
||||
}
|
||||
|
||||
// If no pixels are set, force some pixels to be visible for testing
|
||||
if non_zero_pixels == 0 && count < 300 { // Only do this for the first few frames
|
||||
println!("Forcing test pattern in screen buffer");
|
||||
for y in 70..80 {
|
||||
for x in 0..10 {
|
||||
// Create a simple pattern: alternating colors
|
||||
gameboy.bus.gpu.screen_buffer[y][x] = ((y + x) % 4) as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Print texture creation info
|
||||
if count % 60 == 0 {
|
||||
println!("Creating texture from screen buffer");
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Print debug info more frequently
|
||||
if count % 100_000 == 0 {
|
||||
println!("PC: {:04X}, Count: {}, Boot ROM: {}",
|
||||
gameboy.pc,
|
||||
count,
|
||||
if gameboy.bus.memory[0xFF50] == 0 { "enabled" } else { "disabled" });
|
||||
|
||||
// Print the current instruction
|
||||
let inst = parse_instruction(
|
||||
gameboy.bus.read_byte(gameboy.pc),
|
||||
gameboy.bus.read_byte(gameboy.pc.wrapping_add(1)),
|
||||
gameboy.bus.read_byte(gameboy.pc.wrapping_add(2))
|
||||
);
|
||||
println!("Current instruction: {:?}", inst);
|
||||
|
||||
// Print some key register values
|
||||
println!("Registers: A={:02X}, BC={:04X}, DE={:04X}, HL={:04X}, SP={:04X}",
|
||||
gameboy.registers.a,
|
||||
gameboy.registers.get_bc(),
|
||||
gameboy.registers.get_de(),
|
||||
gameboy.registers.get_hl(),
|
||||
gameboy.sp);
|
||||
}
|
||||
|
||||
// Periodically reset the GPU state to ensure it doesn't get stuck
|
||||
static mut RESET_COUNTER: u32 = 0;
|
||||
unsafe {
|
||||
RESET_COUNTER += 1;
|
||||
if RESET_COUNTER >= 5_000_000 { // Reset every ~5 million instructions
|
||||
println!("Periodically resetting GPU state");
|
||||
|
||||
// Re-initialize the LCD control register
|
||||
gameboy.bus.gpu.lcd_control.lcd_enabled = true;
|
||||
gameboy.bus.gpu.lcd_control.bg_and_window_enable = true;
|
||||
gameboy.bus.gpu.lcd_control.bg_tile_map_area = false; // Use 0x9800-0x9BFF
|
||||
gameboy.bus.gpu.lcd_control.bg_and_window_tile_area = true; // Use 0x8000-0x8FFF
|
||||
|
||||
// Reset the LCD mode and counters
|
||||
gameboy.bus.gpu.mode = LCDMode::OAMSearch;
|
||||
gameboy.bus.gpu.mode_clock = 0;
|
||||
|
||||
// Write the LCD control register to memory
|
||||
gameboy.bus.memory[LCDC_ADDR] = u8::from(gameboy.bus.gpu.lcd_control);
|
||||
|
||||
// Reset counter
|
||||
RESET_COUNTER = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user