From b996b000fde68f7f1b82e4cc8c7546335f3af3e5 Mon Sep 17 00:00:00 2001 From: velllu <91963404+velllu@users.noreply.github.com> Date: Thu, 12 Sep 2024 16:05:13 +0200 Subject: [PATCH] Added experimental support for JSON tests --- Cargo.lock | 45 +++++++++ Cargo.toml | 6 +- examples/json.rs | 251 +++++++++++++++++++++++++++++++++++++++++++++++ src/bus.rs | 16 +-- src/flags.rs | 1 + src/lib.rs | 4 +- src/registers.rs | 1 + 7 files changed, 312 insertions(+), 12 deletions(-) create mode 100644 examples/json.rs diff --git a/Cargo.lock b/Cargo.lock index 911b86a..8d83c5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,6 +133,8 @@ version = "0.1.0" dependencies = [ "colored", "macroquad", + "serde", + "serde_json", ] [[package]] @@ -181,6 +183,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "lazy_static" version = "1.4.0" @@ -353,6 +361,43 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "simd-adler32" version = "0.3.7" diff --git a/Cargo.toml b/Cargo.toml index 9553a9a..3e62eb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,5 +5,7 @@ edition = "2021" # Stuff used for the examples [dev-dependencies] -colored = "2.0.4" -macroquad = "0.4.4" +colored = "2.0" +macroquad = "0.4" +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } diff --git a/examples/json.rs b/examples/json.rs new file mode 100644 index 0000000..638aaf9 --- /dev/null +++ b/examples/json.rs @@ -0,0 +1,251 @@ +//! This example is useful to run the +//! [gb json tests](https://github.com/SingleStepTests/sm83). This code is terrible, this +//! is just for testing purposes + +use std::{ + fs::{read_dir, read_to_string, File, ReadDir}, + io::Read, + process::exit, +}; + +use colored::{ColoredString, Colorize}; +use gameman::{ + consts::bus::{IO_SIZE, ROM_SIZE}, + flags::Flags, + registers::Registers, + GameBoy, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +struct RegisterSchema { + pc: u16, + sp: u16, + a: u8, + b: u8, + c: u8, + d: u8, + e: u8, + f: u8, + h: u8, + l: u8, + ime: u8, + ram: Vec<(u16, u8)>, +} + +#[derive(Serialize, Deserialize)] +struct JsonSchema { + name: String, + initial: RegisterSchema, + #[serde(rename = "final")] + final_: RegisterSchema, +} + +fn main() { + let args: Vec = std::env::args().collect(); + + if args.len() != 2 { + println!("You need to specify the JSON test file"); + std::process::exit(1); + } + + let json_path = args.last().unwrap(); + let json_file = read_json_file(json_path); + + match json_file { + Test::File(path) => { + let schemas = string_to_schemas(path); + run_test(schemas); + } + + Test::Folder(paths) => { + let mut paths: Vec<_> = paths.map(|r| r.unwrap()).collect(); + paths.sort_by_key(|dir| dir.path()); + + for path in paths { + let json_string = read_to_string(path.path()).unwrap(); + let schemas = string_to_schemas(json_string); + run_test(schemas); + } + } + } +} + +fn run_test(schemas: Vec) { + for schema in schemas { + print!("Running test {}...", schema.name.yellow()); + + // Creating gameboy + let mut gameboy = GameBoy::new_from_rom_array([0; ROM_SIZE]); + + // IO must be all zero during tests + gameboy.bus.io = [0; IO_SIZE]; + + // Setting values in ram + for (address, value) in &schema.initial.ram { + gameboy.bus[*address] = *value; + } + + // Setting registers + gameboy.registers = register_schema_to_registers(&schema.initial); + gameboy.flags = register_schema_to_flags(&schema.initial); + + // Running the test rom + loop { + gameboy.step(); + + // until we find a NOP instruction + if gameboy.bus[gameboy.registers.pc] == 0 { + break; + } + } + + let mut were_registers_wrong = false; + let mut were_flags_wrong = false; + + let final_registers = register_schema_to_registers(&schema.final_); + let final_flags = register_schema_to_flags(&schema.final_); + + if gameboy.registers == final_registers { + print!("{}", format!(" ✔ Registers are correct ").green()); + } else { + print!("{}", format!(" ✘ Registers are not correct ").red()); + were_registers_wrong = true; + } + + if gameboy.flags == final_flags { + print!("{}", format!("✔ Flags are correct").green()); + } else { + print!("{}", format!("✘ Flags are not correct").red()); + were_flags_wrong = true; + } + + println!(""); + + if were_flags_wrong || were_registers_wrong { + println!("\n"); + } + + if were_registers_wrong { + println!("Your registers:"); + println!( + "A: {}, B: {}, C: {}, D: {}, E: {}, H: {}, L: {}, SP: {}, PC: {}", + format!("{:x}", gameboy.registers.a).red(), + format!("{:x}", gameboy.registers.b).red(), + format!("{:x}", gameboy.registers.c).red(), + format!("{:x}", gameboy.registers.d).red(), + format!("{:x}", gameboy.registers.e).red(), + format!("{:x}", gameboy.registers.h).red(), + format!("{:x}", gameboy.registers.l).red(), + format!("{:x}", gameboy.registers.sp).red(), + format!("{:x}", gameboy.registers.pc).bold().blue(), + ); + + println!("\nCorrect registers:"); + println!( + "A: {}, B: {}, C: {}, D: {}, E: {}, H: {}, L: {}, SP: {}, PC: {}", + format!("{:x}", schema.final_.a).green(), + format!("{:x}", schema.final_.b).green(), + format!("{:x}", schema.final_.c).green(), + format!("{:x}", schema.final_.d).green(), + format!("{:x}", schema.final_.e).green(), + format!("{:x}", schema.final_.h).green(), + format!("{:x}", schema.final_.l).green(), + format!("{:x}", schema.final_.sp).green(), + format!("{:x}", schema.final_.pc).bold().blue(), + ); + + println!(""); + } + + if were_flags_wrong { + println!("Your flags:"); + println!( + "Zero: {}, Carry: {}", + bool_to_symbol(gameboy.flags.zero), + bool_to_symbol(gameboy.flags.carry) + ); + + println!("\nCorrect flags:"); + println!( + "Zero: {}, Carry: {}", + bool_to_symbol(final_flags.zero), + bool_to_symbol(final_flags.carry) + ); + + println!(""); + } + + if were_registers_wrong || were_flags_wrong { + print_opcodes_from_schema(&schema.initial); + exit(1); + } + } +} + +fn print_opcodes_from_schema(schema: &RegisterSchema) { + for (address, opcode) in &schema.ram { + println!( + "Address {}: {}", + format!("{:x}", address).yellow(), + format!("{:x}", opcode).green() + ); + } +} + +fn register_schema_to_registers(schema: &RegisterSchema) -> Registers { + Registers { + a: schema.a, + b: schema.b, + c: schema.c, + d: schema.d, + e: schema.e, + h: schema.h, + l: schema.l, + sp: schema.sp, + pc: schema.pc, + } +} + +fn register_schema_to_flags(schema: &RegisterSchema) -> Flags { + Flags { + zero: (schema.f >> 7) != 0, + carry: ((schema.f & 0b0001_0000) >> 4) != 0, + half_carry: false, + substraction: false, + } +} + +fn string_to_schemas(json_string: String) -> Vec { + let schema: Vec = serde_json::from_str(&json_string).unwrap(); + schema +} + +/// If the Test file given is a file or a folder of tests +enum Test { + File(String), + Folder(ReadDir), +} + +fn read_json_file(path: &str) -> Test { + let mut file = File::open(path).unwrap(); + let mut json_string = String::new(); + + if file.read_to_string(&mut json_string).is_ok() { + return Test::File(json_string); + } + + if let Ok(folder) = read_dir(path) { + return Test::Folder(folder); + } + + panic!("Could not read file or folder"); +} + +fn bool_to_symbol(boolean: bool) -> ColoredString { + match boolean { + true => String::from("✔️ ").green(), + false => String::from("❌").red(), + } + .bold() +} diff --git a/src/bus.rs b/src/bus.rs index c9469bd..19b7160 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -27,14 +27,14 @@ impl Display for BusError { /// ``` #[allow(unused)] pub struct Bus { - eom: [u8; EOM_SIZE], - external_ram: [u8; EXTERNAL_RAM_SIZE], - high_ram: [u8; HIGH_RAM_SIZE], - ie: u8, - io: [u8; IO_SIZE], - rom: [u8; ROM_SIZE], - video_ram: [u8; VIDEO_RAM_SIZE], - work_ram: [u8; WORK_RAM_SIZE], + pub eom: [u8; EOM_SIZE], + pub external_ram: [u8; EXTERNAL_RAM_SIZE], + pub high_ram: [u8; HIGH_RAM_SIZE], + pub ie: u8, + pub io: [u8; IO_SIZE], + pub rom: [u8; ROM_SIZE], + pub video_ram: [u8; VIDEO_RAM_SIZE], + pub work_ram: [u8; WORK_RAM_SIZE], /// TODO: This is not accurate to the gameboy, when a game writes to a ROM address, /// it should send commands to the cartridge, however, as of now, I just have a clone diff --git a/src/flags.rs b/src/flags.rs index f04acd9..5e70384 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -1,5 +1,6 @@ use crate::common::Bit; +#[derive(PartialEq, Eq)] pub struct Flags { /// This is set when the result is zero pub zero: bool, diff --git a/src/lib.rs b/src/lib.rs index f4a171f..678b00b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,10 +17,10 @@ mod bus; pub mod common; pub mod consts; mod cpu; -mod flags; +pub mod flags; pub mod gpu; mod joypad; -mod registers; +pub mod registers; pub struct GameBoy { pub bus: Bus, diff --git a/src/registers.rs b/src/registers.rs index e7b3c72..3530bb3 100644 --- a/src/registers.rs +++ b/src/registers.rs @@ -4,6 +4,7 @@ use crate::{ flags::Flags, }; +#[derive(PartialEq, Eq)] pub struct Registers { pub a: u8, pub b: u8,