diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5f4e56f..69498e3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,6 +22,6 @@ jobs: - name: Run rustfmt run: cargo fmt --check - name: Unit tests - run: cargo test -- --nocapture + run: cargo test -- --include-ignored --nocapture - name : Clippy run: cargo clippy --all-features diff --git a/Cargo.lock b/Cargo.lock index ab248db..68144ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -738,6 +738,8 @@ dependencies = [ "num-traits", "regex", "rstest", + "serde", + "serde_json", ] [[package]] @@ -1069,6 +1071,12 @@ version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "jni" version = "0.21.1" @@ -1683,6 +1691,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "same-file" version = "1.0.6" @@ -1730,6 +1744,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "simd-adler32" version = "0.3.7" diff --git a/README.md b/README.md index 759a747..2a60ee1 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,10 @@ RUSTFLAGS='--cfg=web_sys_unstable_apis' trunk serve --release `cargo test` +Run ignored tests: + +`cargo test -- --include-ignored` + ## References ### Opcodes diff --git a/fpt/Cargo.toml b/fpt/Cargo.toml index a7023df..bcab6b1 100644 --- a/fpt/Cargo.toml +++ b/fpt/Cargo.toml @@ -10,3 +10,6 @@ num-traits = "0.2" [dev-dependencies] rstest = "0.18" +[build-dependencies] +serde = {version = "1.0", features = ["derive"]} +serde_json = "1.0" diff --git a/fpt/build.rs b/fpt/build.rs new file mode 100644 index 0000000..c81bb4a --- /dev/null +++ b/fpt/build.rs @@ -0,0 +1,88 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::Path; +use std::process::Command; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +struct Suite { + tests: Vec, + name: String, +} + +#[derive(Serialize, Deserialize)] +struct Test { + id: u32, + path: String, + termination_address: String, + passing: Option, + enabled: Option, +} + +fn generate_rom_tests() { + println!("cargo:rerun-if-changed=tests"); + let out_dir = env::var("OUT_DIR").unwrap(); + let destination = Path::new(&out_dir).join("tests.rs"); + let mut test_file = File::create(destination).unwrap(); + + write_header(&mut test_file); + write_test(&mut test_file, "tests/rom_tests.json"); +} + +fn write_test(test_file: &mut File, directory: &str) { + let source = std::fs::read_to_string(directory).unwrap(); + let suite: Suite = serde_json::from_str(&source).unwrap(); + + for test in suite.tests { + if test.enabled.is_some() && !test.enabled.unwrap() { + continue; + } + let test_name = format!("{}_{}", suite.name, test.id); + + write!( + test_file, + include_str!("./tests/templates/test"), + name = test_name, + path = test.path, + termination_address = test.termination_address, + passing = if test.passing.unwrap_or(true) { + "true" + } else { + "false" + }, + ) + .unwrap(); + } +} + +fn write_header(test_file: &mut File) { + write!(test_file, include_str!("./tests/templates/header")).unwrap(); +} + +fn run_cmd(cmd: &str) -> String { + let output = Command::new("sh") + .arg("-c") + .arg(cmd) + .output() + .expect("failed to execute process"); + if !output.status.success() { + panic!("Command {} failed with exit code: {}", cmd, output.status); + } + String::from_utf8(output.stdout).expect("Failed to convert output to string") +} + +fn fetch_mooneye_test_roms() { + println!("cargo:rerun-if-changed=build.rs"); + run_cmd("curl -L --create-dirs --output-dir ../target/tmp -O https://gekkio.fi/files/mooneye-test-suite/mts-20240127-1204-74ae166/mts-20240127-1204-74ae166.tar.xz"); + run_cmd("mkdir -p ../target/test_roms"); + run_cmd("tar -xvf ../target/tmp/mts-20240127-1204-74ae166.tar.xz -C ../target/test_roms"); + run_cmd("rm -rf ../target/test_roms/mooneye"); + run_cmd("mv ../target/test_roms/mts-20240127-1204-74ae166 ../target/test_roms/mooneye"); +} + +fn main() { + generate_rom_tests(); + fetch_mooneye_test_roms(); +} diff --git a/fpt/src/debug_interface.rs b/fpt/src/debug_interface.rs index fb4a4e0..2b9997e 100644 --- a/fpt/src/debug_interface.rs +++ b/fpt/src/debug_interface.rs @@ -27,12 +27,25 @@ impl Watchpoint { } } +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Instrpoint { + pub opcode: u16, + pub triggered: bool, +} + +impl Instrpoint { + pub fn new(opcode: u16, triggered: bool) -> Self { + Self { opcode, triggered } + } +} + #[derive(Debug)] pub enum DebugCmd { Pause, Continue, Breakpoint(u16), Watchpoint(u16), + Instrpoint(u16), Load(String), ListBreakpoints, ListWatchpoints, @@ -43,10 +56,12 @@ pub enum DebugEvent { Continue, RegisterBreakpoint(u16), RegisterWatchpoint(u16), + RegisterInstrpoint(u16), ListBreakpoints(Vec), ListWatchpoints(Vec), Breakpoint(u16), Watchpoint(u16, u16), + Instrpoint(u16), } impl fmt::Display for DebugEvent { @@ -60,6 +75,9 @@ impl fmt::Display for DebugEvent { DebugEvent::RegisterWatchpoint(addr) => { writeln!(f, "Register watchpoint at address {:#04X}", addr) } + DebugEvent::RegisterInstrpoint(opcode) => { + writeln!(f, "Register instrpoint with opcode {:#04X}", opcode) + } DebugEvent::ListBreakpoints(breakpoints) => { writeln!(f, "breakpoints:")?; for (i, breakpoint) in breakpoints.iter().enumerate() { @@ -86,6 +104,10 @@ impl fmt::Display for DebugEvent { )?; Ok(()) } + DebugEvent::Instrpoint(opcode) => { + writeln!(f, "Hit instrpoint at {:#06X}", opcode)?; + Ok(()) + } } } } diff --git a/fpt/src/debugger.rs b/fpt/src/debugger.rs index 2cfce50..6efc720 100644 --- a/fpt/src/debugger.rs +++ b/fpt/src/debugger.rs @@ -1,11 +1,12 @@ use std::collections::VecDeque; -use crate::debug_interface::{Breakpoint, DebugCmd, DebugEvent, Watchpoint}; +use crate::debug_interface::{Breakpoint, DebugCmd, DebugEvent, Instrpoint, Watchpoint}; #[derive(Clone, PartialEq)] pub struct Debugger { breakpoints: Vec, watchpoints: Vec, + instrpoints: Vec, pub paused: bool, dbg_events: VecDeque, } @@ -21,6 +22,7 @@ impl Debugger { Self { breakpoints: Vec::new(), watchpoints: Vec::new(), + instrpoints: Vec::new(), paused: false, dbg_events: VecDeque::new(), } @@ -47,6 +49,13 @@ impl Debugger { self.watchpoints.push(Watchpoint { addr: *addr }); Some(DebugEvent::RegisterWatchpoint(*addr)) } + DebugCmd::Instrpoint(instruction) => { + self.instrpoints.push(Instrpoint { + opcode: *instruction, + triggered: false, + }); + Some(DebugEvent::RegisterInstrpoint(*instruction)) + } DebugCmd::ListBreakpoints => { Some(DebugEvent::ListBreakpoints(self.breakpoints.clone())) } @@ -57,6 +66,29 @@ impl Debugger { } } + pub fn match_instrpoint(&mut self, opcode: u16) -> bool { + let instrpoint = self.instrpoints.iter_mut().find(|i| i.opcode == opcode); + let is_instrpoint = instrpoint.is_some(); + + let triggered = false; + + if instrpoint.is_some() { + let instrpoint = instrpoint.unwrap(); + if instrpoint.triggered { + instrpoint.triggered = false; + } else { + instrpoint.triggered = true; + self.paused = true; + } + } + + if self.paused { + self.dbg_events.push_back(DebugEvent::Instrpoint(opcode)); + } + + is_instrpoint && !triggered + } + pub fn match_breakpoint(&mut self, pc: u16) -> bool { let breakpoint = self.breakpoints.iter_mut().find(|b| b.pc == pc); diff --git a/fpt/src/lib.rs b/fpt/src/lib.rs index a9652cc..978fb08 100644 --- a/fpt/src/lib.rs +++ b/fpt/src/lib.rs @@ -4,8 +4,7 @@ use std::collections::VecDeque; -use debug_interface::DebugEvent; -pub use debug_interface::{DebugCmd, DebugInterface}; +pub use debug_interface::{DebugCmd, DebugEvent, DebugInterface}; use lr35902::LR35902; use memory::{Bus, Buttons}; use ppu::{Frame, Ppu, DOTS_IN_ONE_FRAME}; diff --git a/fpt/src/lr35902.rs b/fpt/src/lr35902.rs index 7811eb4..bfd8214 100644 --- a/fpt/src/lr35902.rs +++ b/fpt/src/lr35902.rs @@ -643,6 +643,9 @@ impl LR35902 { if self.debugger.match_breakpoint(self.pc()) { return; } + if self.debugger.match_instrpoint(inst.opcode) { + return; + } self.execute(inst); if !self.mutated_pc() { self.set_pc(self.pc() + inst.size as u16); @@ -1251,7 +1254,7 @@ impl LR35902 { // HALT // Take care for halt bug: https://gbdev.io/pandocs/halt.html // https://rgbds.gbdev.io/docs/v0.6.1/gbz80.7/#HALT - //todo!("0x76 HALT") + todo!("0x76 HALT") } 0x77 => { // LD (HL),A diff --git a/fpt/src/timer.rs b/fpt/src/timer.rs index 8b9fd78..5bb590a 100644 --- a/fpt/src/timer.rs +++ b/fpt/src/timer.rs @@ -71,13 +71,6 @@ impl Timer { let tac = self.get_tac(); let tma = self.get_tma(); - if (div + 1) % 4 == 0 { - println!( - "self.div: {}, self.tima: {}, self.tac: {}, self.tma: {}", - div, tima, tac, tma - ); - } - self.sys = self.sys.overflowing_add(1).0; let enable = bw::test_bit8::<2>(tac); diff --git a/fpt/tests/rom_tests.json b/fpt/tests/rom_tests.json new file mode 100644 index 0000000..32718cb --- /dev/null +++ b/fpt/tests/rom_tests.json @@ -0,0 +1,335 @@ +{ + "name":"rom_tests", + "tests":[ + { + "id":0, + "path":"../target/test_roms/mooneye/acceptance/timer/tim00.gb", + "termination_address":"0x4ab4" + }, + { + "id":1, + "path":"../target/test_roms/mooneye/acceptance/timer/tim01.gb", + "termination_address":"0x4ab4" + }, + { + "id":2, + "path":"../target/test_roms/mooneye/acceptance/timer/div_write.gb", + "termination_address":"0x4ab4" + }, + { + "id":3, + "path":"../target/test_roms/mooneye/acceptance/timer/rapid_toggle.gb", + "termination_address":"0x4ab4", + "passing":false + }, + { + "id":4, + "path":"../target/test_roms/mooneye/acceptance/timer/tim00_div_trigger.gb", + "termination_address":"0x4ab4" + }, + { + "id":5, + "path":"../target/test_roms/mooneye/acceptance/timer/tim01_div_trigger.gb", + "termination_address":"0x4ab4" + }, + { + "id":6, + "path":"../target/test_roms/mooneye/acceptance/timer/tim10_div_trigger.gb", + "termination_address":"0x4ab4" + }, + { + "id":7, + "path":"../target/test_roms/mooneye/acceptance/timer/tim10.gb", + "termination_address":"0x4ab4" + }, + { + "id":8, + "path":"../target/test_roms/mooneye/acceptance/timer/tim11_div_trigger.gb", + "termination_address":"0x4ab4" + }, + { + "id":9, + "path":"../target/test_roms/mooneye/acceptance/timer/tim11.gb", + "termination_address":"0x4ab4" + }, + { + "id":10, + "path":"../target/test_roms/mooneye/acceptance/timer/tima_reload.gb", + "termination_address":"0x4ab4" + }, + { + "id":11, + "path":"../target/test_roms/mooneye/acceptance/timer/tima_write_reloading.gb", + "termination_address":"0x4ab4" + }, + { + "id":12, + "path":"../target/test_roms/mooneye/acceptance/timer/tma_write_reloading.gb", + "termination_address":"0x4ab4" + }, + { + "id":13, + "path":"../target/test_roms/mooneye/acceptance/bits/mem_oam.gb", + "termination_address":"0x4ab4" + }, + { + "id":14, + "path":"../target/test_roms/mooneye/acceptance/bits/reg_f.gb", + "termination_address":"0x4ab4" + }, + { + "id":15, + "path":"../target/test_roms/mooneye/acceptance/bits/unused_hwio-GS.gb", + "termination_address":"0x4ab4", + "passing":false + }, + { + "id":16, + "path":"../target/test_roms/mooneye/acceptance/instr/daa.gb", + "termination_address":"0x4ab4", + "passing":false, + "enabled":false + }, + { + "id":17, + "path":"../target/test_roms/mooneye/acceptance/interrupts/ie_push.gb", + "termination_address":"0x4ab4", + "passing":false, + "enabled":false + }, + { + "id":18, + "path":"../target/test_roms/mooneye/acceptance/add_sp_e_timing.gb", + "termination_address":"0x4ab4" + }, + { + "id":19, + "path":"../target/test_roms/mooneye/acceptance/boot_div2-S.gb", + "termination_address":"0x4ab4" + }, + { + "id":20, + "path":"../target/test_roms/mooneye/acceptance/boot_div-dmg0.gb", + "termination_address":"0x4ab4" + }, + { + "id":21, + "path":"../target/test_roms/mooneye/acceptance/boot_div-dmgABCmgb.gb", + "termination_address":"0x4ab4" + }, + { + "id":22, + "path":"../target/test_roms/mooneye/acceptance/boot_div-S.gb", + "termination_address":"0x4ab4" + }, + { + "id":23, + "path":"../target/test_roms/mooneye/acceptance/boot_hwio-dmg0.gb", + "termination_address":"0x4ab4", + "passing": false + }, + { + "id":24, + "path":"../target/test_roms/mooneye/acceptance/boot_hwio-dmgABCmgb.gb", + "termination_address":"0x4ab4", + "passing": false + }, + { + "id":25, + "path":"../target/test_roms/mooneye/acceptance/boot_hwio-S.gb", + "termination_address":"0x4ab4", + "passing": false + }, + { + "id":26, + "path":"../target/test_roms/mooneye/acceptance/boot_regs-dmg0.gb", + "termination_address":"0x4ab4" + }, + { + "id":27, + "path":"../target/test_roms/mooneye/acceptance/boot_regs-dmgABC.gb", + "termination_address":"0x4ab4" + }, + { + "id":28, + "path":"../target/test_roms/mooneye/acceptance/boot_regs-mgb.gb", + "termination_address":"0x4ab4" + }, + { + "id":29, + "path":"../target/test_roms/mooneye/acceptance/boot_regs-sgb2.gb", + "termination_address":"0x4ab4" + }, + { + "id":30, + "path":"../target/test_roms/mooneye/acceptance/boot_regs-sgb.gb", + "termination_address":"0x4ab4" + }, + { + "id":31, + "path":"../target/test_roms/mooneye/acceptance/call_cc_timing2.gb", + "termination_address":"0x4ab4" + }, + { + "id":32, + "path":"../target/test_roms/mooneye/acceptance/call_cc_timing.gb", + "termination_address":"0x4ab4", + "passing": false + }, + { + "id":34, + "path":"../target/test_roms/mooneye/acceptance/call_timing2.gb", + "termination_address":"0x4ab4" + }, + { + "id":35, + "path":"../target/test_roms/mooneye/acceptance/call_timing.gb", + "termination_address":"0x4ab4", + "passing": false + }, + { + "id":36, + "path":"../target/test_roms/mooneye/acceptance/di_timing-GS.gb", + "termination_address":"0x4ab4", + "passing": false, + "enabled": false + }, + { + "id":37, + "path":"../target/test_roms/mooneye/acceptance/div_timing.gb", + "termination_address":"0x4ab4" + }, + { + "id":38, + "path":"../target/test_roms/mooneye/acceptance/ei_sequence.gb", + "termination_address":"0x4ab4", + "passing": false, + "enabled": false + }, + { + "id":39, + "path":"../target/test_roms/mooneye/acceptance/ei_timing.gb", + "termination_address":"0x4ab4", + "passing": false, + "enabled": false + }, + { + "id":40, + "path":"../target/test_roms/mooneye/acceptance/halt_ime0_ei.gb", + "termination_address":"0x4ab4", + "passing": false, + "enabled": false + }, + { + "id":41, + "path":"../target/test_roms/mooneye/acceptance/halt_ime0_nointr_timing.gb", + "termination_address":"0x4ab4", + "passing": false, + "enabled": false + }, + { + "id":42, + "path":"../target/test_roms/mooneye/acceptance/halt_ime1_timing2-GS.gb", + "termination_address":"0x4ab4", + "passing": false, + "enabled": false + }, + { + "id":43, + "path":"../target/test_roms/mooneye/acceptance/halt_ime1_timing.gb", + "termination_address":"0x4ab4", + "passing": false, + "enabled": false + }, + { + "id":44, + "path":"../target/test_roms/mooneye/acceptance/if_ie_registers.gb", + "termination_address":"0x4ab4" + }, + { + "id":45, + "path":"../target/test_roms/mooneye/acceptance/intr_timing.gb", + "termination_address":"0x4ab4" + }, + { + "id":46, + "path":"../target/test_roms/mooneye/acceptance/jp_cc_timing.gb", + "termination_address":"0x4ab4", + "passing": false + }, + { + "id":47, + "path":"../target/test_roms/mooneye/acceptance/jp_timing.gb", + "termination_address":"0x4ab4", + "passing": false + }, + { + "id":48, + "path":"../target/test_roms/mooneye/acceptance/ld_hl_sp_e_timing.gb", + "termination_address":"0x4ab4", + "passing": false, + "enabled": false + }, + { + "id":49, + "path":"../target/test_roms/mooneye/acceptance/oam_dma_restart.gb", + "termination_address":"0x4ab4" + }, + { + "id":50, + "path":"../target/test_roms/mooneye/acceptance/oam_dma_start.gb", + "termination_address":"0x4ab4", + "passing": false, + "enabled": false + }, + { + "id":51, + "path":"../target/test_roms/mooneye/acceptance/oam_dma_timing.gb", + "termination_address":"0x4ab4" + }, + { + "id":52, + "path":"../target/test_roms/mooneye/acceptance/pop_timing.gb", + "termination_address":"0x4ab4" + }, + { + "id":53, + "path":"../target/test_roms/mooneye/acceptance/push_timing.gb", + "termination_address":"0x4ab4" + }, + { + "id":54, + "path":"../target/test_roms/mooneye/acceptance/rapid_di_ei.gb", + "termination_address":"0x4ab4", + "passing": false, + "enabled": false + }, + { + "id":55, + "path":"../target/test_roms/mooneye/acceptance/ret_cc_timing.gb", + "termination_address":"0x4ab4" + }, + { + "id":56, + "path":"../target/test_roms/mooneye/acceptance/reti_intr_timing.gb", + "termination_address":"0x4ab4", + "passing": false, + "enabled": false + }, + { + "id":57, + "path":"../target/test_roms/mooneye/acceptance/reti_timing.gb", + "termination_address":"0x4ab4" + }, + { + "id":58, + "path":"../target/test_roms/mooneye/acceptance/ret_timing.gb", + "termination_address":"0x4ab4" + }, + { + "id":59, + "path":"../target/test_roms/mooneye/acceptance/rst_timing.gb", + "termination_address":"0x4ab4" + } + ] +} diff --git a/fpt/tests/templates/header b/fpt/tests/templates/header new file mode 100644 index 0000000..84bc308 --- /dev/null +++ b/fpt/tests/templates/header @@ -0,0 +1,47 @@ +use fpt::{{DebugCmd, DebugEvent, Gameboy}}; + +fn check_registers(gb: &Gameboy) -> bool {{ + return gb.cpu().b() == 3 + && gb.cpu().c() == 5 + && gb.cpu().d() == 8 + && gb.cpu().e() == 13 + && gb.cpu().h() == 21 + && gb.cpu().l() == 34; +}} + +fn rom_test(rom_path: &str, termination_address: u16, passing: bool) {{ + let mut gb = Gameboy::new(); + gb.simulate_dmg0_bootrom_handoff_state(); + + let rom = std::fs::read(rom_path).unwrap(); + gb.load_rom(&rom); + + gb.debug_cmd(&DebugCmd::Instrpoint(0x40)); + gb.debug_cmd(&DebugCmd::Breakpoint(termination_address)); + + let mut success = true; + 'outer: loop {{ + gb.step(); + let debug_events = gb.get_debug_events(); + if !debug_events.is_empty() {{ + loop {{ + match debug_events.pop_back().unwrap() {{ + DebugEvent::Breakpoint(_) => {{ + break 'outer; + }} + DebugEvent::Instrpoint(_) => {{ + let check = check_registers(&gb); + success &= check; + if passing {{ + assert!(check == true); + }} + continue 'outer; + }} + _ => continue 'outer, + }} + }} + }} + }} + + assert!(success == passing); +}} diff --git a/fpt/tests/templates/test b/fpt/tests/templates/test new file mode 100644 index 0000000..5e0890f --- /dev/null +++ b/fpt/tests/templates/test @@ -0,0 +1,8 @@ +#[test] #[ignore] +fn {name}() {{ + rom_test( + "{path}", + {termination_address}, + {passing}, + ); +}} diff --git a/fpt/tests/test_roms.rs b/fpt/tests/test_roms.rs new file mode 100644 index 0000000..1429652 --- /dev/null +++ b/fpt/tests/test_roms.rs @@ -0,0 +1,2 @@ +// include tests generated by `build.rs`, one test per directory in tests/data +include!(concat!(env!("OUT_DIR"), "/tests.rs"));