Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92787a1036 | |||
| d0f748baa5 | |||
| b3024e66c8 |
54
Cargo.lock
generated
54
Cargo.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
493
src/lcd.rs
Normal file
493
src/lcd.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
424
src/main.rs
424
src/main.rs
@@ -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 => {
|
|
||||||
if self.memory[0xFF50] == 0x00 {
|
|
||||||
self.boot[address]
|
|
||||||
}else {
|
|
||||||
self.rom.read_byte(address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CART_BEGIN ..= CART_END => {
|
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.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) {
|
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;
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
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;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user