diff --git a/.cargo/config.toml b/.cargo/config.toml index 6243706..3982f22 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,2 @@ -[target.x86_64-pc-windows-msvc] +[target.'cfg(windows)'] rustflags = ["-C", "link-arg=/STACK:8388608"] diff --git a/Cargo.toml b/Cargo.toml index 4af4326..ece2496 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,18 +13,19 @@ name = "rbgb" path = "src/lib.rs" [[bin]] -name = "emulator_executable" +name = "rbgb" path = "src/main.rs" +required-features = ["gui"] [dependencies] -debug_print = "1.0.0" -lazy_static = "1.5.0" -sdl2 = "0.37.0" -time = "0.3.41" +sdl2 = { version = "0.37.0", optional = true } + +[features] +gui = ["sdl2"] # Easiest to bundle on windows since theres not a super easy way to install [target.'cfg(windows)'.dependencies] -sdl2 = { version = "0.37.0", features = ["bundled"] } +sdl2 = { version = "0.37.0", features = ["bundled"], optional = true } [dev-dependencies] ntest = "0.9.3" diff --git a/README.md b/README.md index a27c11a..d53e90e 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ A simple Game Boy emulator written in Rust. - Shared Memory bank - Basic graphics rendering - ROM loading +- Dependency free (emulator lib) ## Repo Basics @@ -29,7 +30,7 @@ Before running this project ensure SDL2 is installed if you are not running with 3. Run the emulator: ```bash - cargo run --release + cargo run --release --features gui ``` 4. Select a ROM \ @@ -67,7 +68,7 @@ To build and run tests locally: ## Acknowledgements -Much of this was written and based off of [Codeslinger Gameboy](http://www.codeslinger.co.uk/pages/projects/gameboy/beginning.html). I needed this guide to get through most of this. Additionally, I learned about how the RZ80 worked from [RZ80](https://floooh.github.io/) and modified the CPU implementation of the LR35902 from [rboy](https://github.com/mvdnes/rboy). Getting the CPU emulation to work was the hardest part of this project. +Much of this was written and based off of [Codeslinger Gameboy](http://www.codeslinger.co.uk/pages/projects/gameboy/beginning.html). I needed this guide to get through most of this. Additionally, I learned about how the RZ80 worked from [RZ80](https://floooh.github.io/) and modified the CPU implementation of the LR35902 from [rboy](https://github.com/mvdnes/rboy). Getting the CPU emulation to work was the hardest part of this project. Additionally this project includes the `debug_println` macro from the [debug_print](https://docs.rs/debug_print/latest/debug_print/) crate in order to make it dependency free for the library. ## License diff --git a/src/emulator.rs b/src/emulator.rs index 470e2db..0830440 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -3,8 +3,8 @@ use std::{ time::{Duration, Instant}, }; +use crate::debug_println; use crate::types::{GameInput, KeyState}; -use debug_print::debug_println; mod cpu; mod graphics; @@ -76,10 +76,8 @@ impl Emulator { return; } let frame_start = Instant::now(); - // debug_println!("Main Loop"); let mut num_cycles: u32 = 0; while num_cycles < Self::MAXCYCLES { - //debug_println!("Program Counter: 0x{:X}", self.cpu.registers.val_pc()); let cycles = self.cpu.execute_next_opcode(false); num_cycles += cycles as u32; self.cpu.timers.update_timers(cycles as i32); diff --git a/src/emulator/graphics.rs b/src/emulator/graphics.rs index 5d96cf2..be5fd06 100644 --- a/src/emulator/graphics.rs +++ b/src/emulator/graphics.rs @@ -1,7 +1,3 @@ -#![allow(dead_code)] -#[allow(unused_imports)] -use debug_print::debug_println; - use crate::emulator::mem::*; use crate::types::*; @@ -26,70 +22,18 @@ impl Screen { } } - pub fn clear(&mut self, color: u8) { - let (r, g, b) = Self::color_to_rgb(color); - for chunk in self.buffer.chunks_mut(3) { - chunk[0] = r; - chunk[1] = g; - chunk[2] = b; - } - } - - pub fn set_pixel(&mut self, x: usize, y: usize, color: u8) { - if x >= SCREEN_WIDTH as usize || y >= SCREEN_HEIGHT as usize { - return; - } - let idx = (y * SCREEN_WIDTH as usize + x) * 3; - let (r, g, b) = Self::color_to_rgb(color); - self.buffer[idx] = r; - self.buffer[idx + 1] = g; - self.buffer[idx + 2] = b; - } - - pub fn get_pixel(&self, x: usize, y: usize) -> u8 { - if x >= SCREEN_WIDTH as usize || y >= SCREEN_HEIGHT as usize { - return 0; - } - let idx = (y * SCREEN_WIDTH as usize + x) * 3; - let r = self.buffer[idx]; - let g = self.buffer[idx + 1]; - let b = self.buffer[idx + 2]; - Self::rgb_to_color(r, g, b) - } - fn color_to_rgb(color: u8) -> (u8, u8, u8) { - match color { - 0 => (255, 255, 255), - 1 => (0xCC, 0xCC, 0xCC), - 2 => (0x77, 0x77, 0x77), - _ => (0, 0, 0), - } - } - - fn rgb_to_color(r: u8, g: u8, b: u8) -> u8 { - match (r, g, b) { - (255, 255, 255) => 0, - (0xCC, 0xCC, 0xCC) => 1, - (0x77, 0x77, 0x77) => 2, - _ => 3, - } - } - pub fn update_screen(&mut self, cycles: i32) { - // debug_println!("Screen update!"); - self.set_lcd_status(); if self.is_lcd_enabled() { self.scanline_counter -= cycles; } else { // LCD is not enabled so do nothing - //debug_println!("LCD Disabled"); return; } let mut mem = self.device_memory.lock().unwrap(); - // debug_println!("Check scanlines!"); if self.scanline_counter <= 0 { // Time to move onto the next scanline let scanline = mem.read_byte(CURRENT_SCANLINE).wrapping_add(1); @@ -98,7 +42,6 @@ impl Screen { self.scanline_counter = 456; // we are now in the vertical blank period - // debug_println!("Current Scanline is {scanline}"); if scanline == 144 { mem.request_interrupt(0); } @@ -132,7 +75,6 @@ impl Screen { } fn render_tiles(&mut self, control: Byte) { - // debug_println!("Rendering tile, control: {:b}", control); let mem = self.device_memory.lock().unwrap(); let mut unsigned = true; @@ -211,7 +153,6 @@ impl Screen { } let idx = (current_line as usize * SCREEN_WIDTH as usize + pixel as usize) * 3; - //debug_println!("Writing idx: {idx}"); self.buffer[idx] = red; self.buffer[idx + 1] = green; self.buffer[idx + 2] = blue; @@ -333,8 +274,6 @@ impl Screen { } fn set_lcd_status(&mut self) { - // debug_println!("Updating LCD Status!"); - let lcd_enabled = self.is_lcd_enabled(); // Gets lock on memory let mut mem = self.device_memory.lock().unwrap(); @@ -359,8 +298,6 @@ impl Screen { let mode; let mut require_interrupt = false; - // debug_println!("Current scanline is {current_line} and mode is {current_mode}"); - // in vblank so mode is set to 1 if current_line >= 144 { mode = 1; @@ -408,11 +345,8 @@ impl Screen { fn is_lcd_enabled(&mut self) -> bool { // Check bit 7 of LCD Control register (0xFF40) - // debug_println!("Checking if LCD is enabled"); let mem = self.device_memory.lock().unwrap(); - // debug_println!("Lock aquired"); mem.read_byte(LCD_CONTROL) & (1 << 7) != 0 - // debug_println!("Check complete"); } // TODO: Add more methods for drawing, sprites, etc. diff --git a/src/emulator/joypad.rs b/src/emulator/joypad.rs index d9c7ec4..f1cd3cb 100644 --- a/src/emulator/joypad.rs +++ b/src/emulator/joypad.rs @@ -1,7 +1,4 @@ //! Contains all code for interfacing io to the gameboy - -use debug_print::debug_println; - use crate::types::{GameInput, KeyState}; use super::mem::SharedMemory; @@ -34,7 +31,6 @@ impl Joypad { /// Takes the input and affects the associated memory value with what it should be pub fn log_input(&mut self, input: GameInput, val: KeyState) { - debug_println!("Registering input: {:?}", input); match input { // mode 0 buttons GameInput::A => self.a = val, diff --git a/src/emulator/mem.rs b/src/emulator/mem.rs index d1f7359..082af9e 100644 --- a/src/emulator/mem.rs +++ b/src/emulator/mem.rs @@ -5,8 +5,6 @@ use std::{ /// Functions and storage for operating on device memory use crate::types::*; -#[allow(unused_imports)] -use debug_print::debug_println; pub type SharedMemory = Arc>; @@ -229,10 +227,7 @@ impl Memory { //turns off the lower 5 bits of the banking mode let lower5: Byte = value & 31; let current = self.rom_banks.value(); - // debug_println!("Current Banking Type: {:#?}", current); - // debug_println!("Maked Lower 5: {:#?}", lower5); let masked = (current & 224) | lower5; - // debug_println!("Post Banking Type: {:#?}", masked); self.rom_banks = CurrentRomBank::from(masked); if self.rom_banks == CurrentRomBank::Bank(0) { self.rom_banks = CurrentRomBank::Bank(1); @@ -311,7 +306,6 @@ impl Memory { pub fn request_interrupt(&mut self, interrupt: Byte) { let mut request = self.read_byte(IF); request |= 1 << interrupt; // Sets the bit of the request - // debug_println!("Writing Interrupt {}", request); self.write_byte(IF, request); } @@ -404,7 +398,6 @@ impl Memory { // the existing enables. let mut enabled = self.read_byte(IE); enabled |= 1 << interrupt; // Sets the bit of the request - // debug_println!("Enabling Interrupt {}", enabled); self.write_byte(IE, enabled); } @@ -587,7 +580,6 @@ mod test { assert!(!mem.ram_write_enable); //Change rom bank - // debug_println!("\nCORRECTLY SET BANKS"); mem.write_byte(0x2001, 0x0); assert_eq!(mem.rom_banks, CurrentRomBank::Bank(1)); mem.write_byte(0x2001, 0x1); @@ -605,7 +597,6 @@ mod test { assert_eq!(mem.rom_banks, CurrentRomBank::Bank(35)); //Test banking set failure - // debug_println!("\nINCORRECTLY SET BANKS"); mem.write_byte(0x2001, 0x40); assert_eq!(mem.rom_banks, CurrentRomBank::Bank(32)); diff --git a/src/lib.rs b/src/lib.rs index cd85e39..5eb9aff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,33 @@ +//! Game Boy emulator core. +//! +//! This crate provides the core emulation loop and subsystems for a classic +//! Game Boy. The primary entry point is [`Emulator`], a struct that owns the +//! CPU, memory, graphics, and input devices and drives them per frame. +//! +//! High-level flow: +//! - Create an [`Emulator`] (starts paused). +//! - Load a ROM with [`Emulator::load_rom`], which resets memory and CPU state. +//! - Call [`Emulator::update`] once per frame to advance CPU, timers, and video. +//! - Provide input through [`Emulator::game_input`], and read pixels from +//! [`Emulator::get_display_buffer`]. +//! +//! The emulation state is backed by shared memory (`Arc>`) so the +//! CPU, graphics, and joypad stay in sync with memory-mapped I/O. +//! +//! # Example +//! ```no_run +//! use rbgb::Emulator; +//! +//! fn main() -> Result<(), String> { +//! let mut emu = Emulator::new(); +//! emu.load_rom("path/to/game.gb")?; +//! loop { +//! emu.update(); +//! let _pixels = emu.get_display_buffer(); +//! // Render pixels and handle input here. +//! } +//! } +//! ``` pub mod emulator; mod types; diff --git a/src/main.rs b/src/main.rs index fcd1616..cfcf7f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "gui")] extern crate sdl2; mod emulator; diff --git a/src/types.rs b/src/types.rs index 4ff29ab..50e79a0 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,16 +1,5 @@ -#![allow(dead_code)] - -use lazy_static::lazy_static; -use std::fs::File; -use std::io; -use std::io::Read; -use std::sync::Mutex; - pub type Byte = u8; -pub type SignedByte = i8; pub type Word = u16; -pub type SignedWord = i16; -pub type Cartridge = [Byte; FILESIZE]; pub type Ram = [Byte; MEM_SIZE]; #[allow(clippy::upper_case_acronyms)] pub type LCD = [Byte; (SCREEN_HEIGHT * SCREEN_WIDTH * 3) as usize]; @@ -20,42 +9,6 @@ pub const TIMA: Word = 0xFF05; pub const TMA: Word = 0xFF06; pub const TMC: Word = 0xFF07; pub const DIVIDER_REGISTER: Word = 0xFF04; -pub const CLOCKSPEED: u32 = 4194304; -/// CPU carry flag -pub const CF: Byte = 1 << 0; -/// CPU add/subtract flag -pub const NF: Byte = 1 << 1; -/// CPU overflow flag (same as parity) -pub const VF: Byte = 1 << 2; -/// CPU parity flag (same as overflow) -pub const PF: Byte = 1 << 2; -/// CPU undocumented 'X' flag -pub const XF: Byte = 1 << 3; -/// CPU half carry flag -pub const HF: Byte = 1 << 4; -/// CPU undocumented 'Y' flag -pub const YF: Byte = 1 << 5; -/// CPU zero flag -pub const ZF: Byte = 1 << 6; -/// CPU sign flag -pub const SF: Byte = 1 << 7; - -pub const BC: Byte = 0; -pub const DE: Byte = 2; -pub const HL: Byte = 4; -pub const AF: Byte = 6; -pub const IX: Byte = 8; -pub const IY: Byte = 10; -pub const SP: Byte = 12; -pub const WZ: Byte = 14; -pub const BC_: Byte = 16; -pub const DE_: Byte = 18; -pub const HL_: Byte = 20; -pub const AF_: Byte = 22; -pub const WZ_: Byte = 24; - -pub const SP_TABLE: [Byte; 4] = [BC, DE, HL, SP]; //TODO: Change to localize as global so compiler doesn't inline the table -pub const AF_TABLE: [Byte; 4] = [BC, DE, HL, AF]; // Interrupt Constants pub const IE: Word = 0xFFFF; // Interrupt enabled register @@ -72,9 +25,6 @@ pub const DMA_REG: Word = 0xFF46; pub const SPRITE_RAM: Word = 0xFE00; // from 0xFE00 to OxFE9F pub const MODE_2_BOUNDS: i32 = 456 - 80; pub const MODE_3_BOUNDS: i32 = MODE_2_BOUNDS - 172; -pub const MEMORY_REGION: Word = 0x8000; // Graphics memory location -pub const SIZE_OF_TILE_IN_MEMORY: i32 = 16; -pub const OFFSET: i32 = 128; // Input Constants pub const INPUT_REGISTER: Word = 0xFF00; @@ -112,23 +62,7 @@ pub const MEM_SIZE: usize = 0x10000; //Likely at some point will switch the RAM and ROM to be part of the Emulator struct -/// ROM Device memory -const FILESIZE: usize = 0x20000; -lazy_static! { - pub static ref CARTRIDGE_MEMORY: Mutex = Mutex::new([0; FILESIZE]); -} - -/// Loads the contents of the given file into the cartridge_memory array. -/// Returns the number of bytes read or an io::Error. -pub fn load_cartridge>(path: P) -> io::Result { - let mut file = File::open(path)?; - let mut memory = CARTRIDGE_MEMORY.lock().unwrap(); - let bytes_read = file.read(&mut *memory)?; - Ok(bytes_read) -} - #[derive(PartialEq, Debug)] - pub enum RomBankingType { MBC1, MBC2, @@ -164,36 +98,17 @@ pub enum CurrentRamBank { Bank3, } +/// Prints to the standard ouput only in debug build. +/// In release build this macro is not compiled thanks to `#[cfg(debug_assertions)]`. +/// see [https://doc.rust-lang.org/std/macro.println.html](https://doc.rust-lang.org/std/macro.println.html) for more info. +#[macro_export] +macro_rules! debug_println { + ($($arg:tt)*) => (#[cfg(debug_assertions)] println!($($arg)*)); +} + #[cfg(test)] mod test { use super::*; - use ntest::timeout; - use std::fs::{File, remove_file}; - use std::io::Write; - - #[test] - #[timeout(10)] - fn test_load_cartridge_success() { - let path = "test_cart.gb"; - { - let mut f = File::create(path).unwrap(); - f.write_all(&[1u8, 2, 3, 4]).unwrap(); - } - - let bytes = load_cartridge(path).unwrap(); - assert_eq!(bytes, 4); - let mem = CARTRIDGE_MEMORY.lock().unwrap(); - assert_eq!(&mem[..4], &[1, 2, 3, 4]); - drop(mem); - remove_file(path).unwrap(); - } - - #[test] - #[timeout(10)] - fn test_load_cartridge_missing() { - let res = load_cartridge("nonexistent.gb"); - assert!(res.is_err()); - } #[test] fn test_current_rom_bank_conversion() {