From 1d386c5503dfa114f23766494641b7d3a9109322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Magalh=C3=A3es?= <joamag@gmail.com> Date: Mon, 22 May 2023 00:23:07 +0100 Subject: [PATCH] chore: support for ROM unit testing Created infrastructure that allows the usage of the serial device to capture the result of the tests. Then the test is executed agains a pre-defined number of cycles and the result is compared against expectation. --- frontends/sdl/src/main.rs | 10 +++++-- src/devices/buffer.rs | 62 +++++++++++++++++++++++++++++++++++++++ src/devices/mod.rs | 1 + src/devices/printer.rs | 4 +++ src/devices/stdout.rs | 8 +++-- src/gb.rs | 5 ++-- src/lib.rs | 1 + src/serial.rs | 12 ++++++++ src/test.rs | 53 +++++++++++++++++++++++++++++++++ 9 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 src/devices/buffer.rs create mode 100644 src/test.rs diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs index c4137fda..5ab6c7fc 100644 --- a/frontends/sdl/src/main.rs +++ b/frontends/sdl/src/main.rs @@ -484,6 +484,8 @@ impl Emulator { } pub fn run_headless(&mut self, allowed_cycles: Option<u64>) { + let allowed_cycles = allowed_cycles.unwrap_or(u64::MAX); + // starts the variable that will control the number of cycles that // are going to move (because of overflow) from one tick to another let mut pending_cycles = 0u32; @@ -540,7 +542,7 @@ impl Emulator { // fot the current tick an in case the total number of cycles // exceeds the allowed cycles then the loop is broken total_cycles += cycle_limit as u64; - if total_cycles >= allowed_cycles.unwrap_or(u64::MAX) { + if total_cycles >= allowed_cycles { break; } @@ -635,7 +637,7 @@ fn main() { // creates a new Game Boy instance and loads both the boot ROM // and the initial game ROM to "start the engine" - let mut game_boy = GameBoy::new(mode); + let mut game_boy = GameBoy::new(Some(mode)); let device = build_device(&args.device); game_boy.set_ppu_enabled(!args.no_ppu); game_boy.set_apu_enabled(!args.no_apu); @@ -663,6 +665,10 @@ fn main() { emulator.start(SCREEN_SCALE); emulator.load_rom(Some(&args.rom_path)); emulator.toggle_palette(); + + // determines if the emulator should run in headless mode or + // not and runs it accordingly, note that if running in headless + // mode the number of cycles to be run may be specified if args.headless { emulator.run_headless(if args.cycles > 0 { Some(args.cycles) diff --git a/src/devices/buffer.rs b/src/devices/buffer.rs new file mode 100644 index 00000000..cf5707b7 --- /dev/null +++ b/src/devices/buffer.rs @@ -0,0 +1,62 @@ +use crate::serial::SerialDevice; + +use std::fmt::{self, Display, Formatter}; + +pub struct BufferDevice { + buffer: Vec<u8>, + callback: fn(image_buffer: &Vec<u8>), +} + +impl BufferDevice { + pub fn new() -> Self { + Self { + buffer: Vec::new(), + callback: |_| {}, + } + } + + pub fn set_callback(&mut self, callback: fn(image_buffer: &Vec<u8>)) { + self.callback = callback; + } + + pub fn buffer(&self) -> &Vec<u8> { + &self.buffer + } +} + +impl SerialDevice for BufferDevice { + fn send(&mut self) -> u8 { + 0xff + } + + fn receive(&mut self, byte: u8) { + self.buffer.push(byte); + let data = vec![byte]; + (self.callback)(&data); + } + + fn allow_slave(&self) -> bool { + false + } + + fn description(&self) -> String { + String::from("Buffer") + } + + fn state(&self) -> String { + let buffer = self.buffer.clone(); + String::from_utf8(buffer).unwrap() + } +} + +impl Default for BufferDevice { + fn default() -> Self { + Self::new() + } +} + +impl Display for BufferDevice { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "Buffer") + } +} diff --git a/src/devices/mod.rs b/src/devices/mod.rs index ada19f91..339782b8 100644 --- a/src/devices/mod.rs +++ b/src/devices/mod.rs @@ -1,2 +1,3 @@ +pub mod buffer; pub mod printer; pub mod stdout; diff --git a/src/devices/printer.rs b/src/devices/printer.rs index cf0161a8..dade2045 100644 --- a/src/devices/printer.rs +++ b/src/devices/printer.rs @@ -316,6 +316,10 @@ impl SerialDevice for PrinterDevice { fn description(&self) -> String { format!("Printer [{}]", self.command) } + + fn state(&self) -> String { + self.command.to_string() + } } impl Default for PrinterDevice { diff --git a/src/devices/stdout.rs b/src/devices/stdout.rs index 8a13eda0..75dbcd27 100644 --- a/src/devices/stdout.rs +++ b/src/devices/stdout.rs @@ -7,7 +7,7 @@ use std::{ pub struct StdoutDevice { flush: bool, - callback: fn(image_buffer: &Vec<u8>), + callback: fn(buffer: &Vec<u8>), } impl StdoutDevice { @@ -18,7 +18,7 @@ impl StdoutDevice { } } - pub fn set_callback(&mut self, callback: fn(image_buffer: &Vec<u8>)) { + pub fn set_callback(&mut self, callback: fn(buffer: &Vec<u8>)) { self.callback = callback; } } @@ -44,6 +44,10 @@ impl SerialDevice for StdoutDevice { fn description(&self) -> String { String::from("Stdout") } + + fn state(&self) -> String { + String::from("") + } } impl Default for StdoutDevice { diff --git a/src/gb.rs b/src/gb.rs index 073fa08c..66c37c44 100644 --- a/src/gb.rs +++ b/src/gb.rs @@ -346,7 +346,8 @@ pub struct GameBoy { #[cfg_attr(feature = "wasm", wasm_bindgen)] impl GameBoy { #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))] - pub fn new(mode: GameBoyMode) -> Self { + pub fn new(mode: Option<GameBoyMode>) -> Self { + let mode = mode.unwrap_or(GameBoyMode::Dmg); let gbc = Rc::new(RefCell::new(GameBoyConfig { mode, ppu_enabled: true, @@ -1052,7 +1053,7 @@ impl AudioProvider for GameBoy { impl Default for GameBoy { fn default() -> Self { - Self::new(GameBoyMode::Dmg) + Self::new(None) } } diff --git a/src/lib.rs b/src/lib.rs index d25f5ac6..0f8c9a49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,5 +14,6 @@ pub mod pad; pub mod ppu; pub mod rom; pub mod serial; +pub mod test; pub mod timer; pub mod util; diff --git a/src/serial.rs b/src/serial.rs index a8f522b7..a9bd6205 100644 --- a/src/serial.rs +++ b/src/serial.rs @@ -1,7 +1,11 @@ use crate::warnln; pub trait SerialDevice { + /// Sends a byte (u8) to the attached serial connection. fn send(&mut self) -> u8; + + /// Receives a byte (u8) from the attached serial connection, + /// can be either another device or the host. fn receive(&mut self, byte: u8); /// Whether the serial device "driver" supports slave mode @@ -11,6 +15,10 @@ pub trait SerialDevice { /// Returns a short description of the serial device. fn description(&self) -> String; + + /// Returns a string describing the current state of the + /// serial device. Could be used for debugging purposes. + fn state(&self) -> String; } pub struct Serial { @@ -222,6 +230,10 @@ impl SerialDevice for NullDevice { fn description(&self) -> String { String::from("Null") } + + fn state(&self) -> String { + String::from("") + } } impl Default for NullDevice { diff --git a/src/test.rs b/src/test.rs new file mode 100644 index 00000000..39dc1dba --- /dev/null +++ b/src/test.rs @@ -0,0 +1,53 @@ +use crate::{devices::buffer::BufferDevice, gb::GameBoy}; + +#[derive(Default)] +pub struct TestOptions { + ppu_enabled: Option<bool>, + apu_enabled: Option<bool>, + dma_enabled: Option<bool>, + timer_enabled: Option<bool>, +} + +pub fn build_test(options: TestOptions) -> GameBoy { + let device = Box::<BufferDevice>::default(); + let mut game_boy = GameBoy::new(None); + game_boy.set_ppu_enabled(options.ppu_enabled.unwrap_or(true)); + game_boy.set_apu_enabled(options.apu_enabled.unwrap_or(true)); + game_boy.set_dma_enabled(options.dma_enabled.unwrap_or(true)); + game_boy.set_timer_enabled(options.timer_enabled.unwrap_or(true)); + game_boy.attach_serial(device); + game_boy.load(true); + game_boy +} + +pub fn run_test(rom_path: &str, max_cycles: Option<u64>, options: TestOptions) -> String { + let mut cycles = 0u64; + let max_cycles = max_cycles.unwrap_or(u64::MAX); + + let mut game_boy = build_test(options); + game_boy.load_rom_file(rom_path); + + loop { + cycles += game_boy.clock() as u64; + if cycles >= max_cycles { + break; + } + } + + game_boy.serial().device().state() +} + +#[cfg(test)] +mod tests { + use super::{run_test, TestOptions}; + + #[test] + fn test_blargg_cpu_instrs() { + let result = run_test( + "res/roms/test/blargg/cpu/cpu_instrs.gb", + Some(300000000), + TestOptions::default(), + ); + assert_eq!(result, "cpu_instrs\n\n01:ok 02:ok 03:ok 04:ok 05:ok 06:ok 07:ok 08:ok 09:ok 10:ok 11:ok \n\nPassed all tests\n"); + } +} -- GitLab