Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[target.x86_64-pc-windows-msvc]
[target.'cfg(windows)']
rustflags = ["-C", "link-arg=/STACK:8388608"]
13 changes: 7 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 \
Expand Down Expand Up @@ -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

Expand Down
4 changes: 1 addition & 3 deletions src/emulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
66 changes: 0 additions & 66 deletions src/emulator/graphics.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
#![allow(dead_code)]
#[allow(unused_imports)]
use debug_print::debug_println;

use crate::emulator::mem::*;
use crate::types::*;

Expand All @@ -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);
Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 0 additions & 4 deletions src/emulator/joypad.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 0 additions & 9 deletions src/emulator/mem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Mutex<Memory>>;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
Expand All @@ -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));

Expand Down
30 changes: 30 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<Mutex<...>>`) 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;

Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#[cfg(feature = "gui")]
extern crate sdl2;

mod emulator;
Expand Down
Loading