use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use std::path::{Path, PathBuf};
use crate::core::state::{MachineShape, Migrations, Source, StateReader, StateWriter};
use crate::machine::{Machine, catalog};
const GATE: &str = "RSEMU_CONFORMANCE";
const TESTDATA: &str = "RSEMU_TESTDATA";
const DEFAULT_FRAMES: u64 = 4000;
const QUANTA_PER_CHECK: u32 = 2048;
fn enabled() -> bool {
matches!(
std::env::var(GATE).as_deref(),
Ok("1") | Ok("true") | Ok("yes")
)
}
fn frame_limit() -> u64 {
std::env::var("RSEMU_GB_FRAMES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_FRAMES)
}
fn testdata_root() -> PathBuf {
match std::env::var_os(TESTDATA) {
Some(dir) => PathBuf::from(dir),
None => PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata"),
}
}
fn corpus(var: &str, default: &str, fetch: &str) -> Option<PathBuf> {
if !enabled() {
println!("SKIP {default}: set {GATE}=1 to run conformance suites");
return None;
}
let dir = match std::env::var_os(var) {
Some(d) => PathBuf::from(d),
None => testdata_root().join(default),
};
if !dir.is_dir() {
println!("SKIP {default}: corpus not found at {}", dir.display());
println!(" fetch it with: {fetch}");
return None;
}
Some(dir)
}
fn roms(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
collect(dir, &mut out);
out.sort();
out
}
fn collect(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect(&path, out);
} else if path.extension().is_some_and(|e| e == "gb") {
out.push(path);
}
}
}
fn label(root: &Path, rom: &Path) -> String {
rom.strip_prefix(root)
.unwrap_or(rom)
.to_string_lossy()
.to_string()
}
fn chunk_of(machine: &Machine, path: &str) -> Option<Vec<u8>> {
let entry = machine.device(path)?;
let class = entry.class();
let mut writer = StateWriter::new(MachineShape::new());
{
let mut chunk = writer.chunk(path, class.name, class.version).ok()?;
entry.device().save(&mut chunk).ok()?;
}
let bytes = writer.to_vec().ok()?;
let reader = StateReader::new(&bytes).ok()?;
let chunk = reader
.load(path, class.name, class.version, &Migrations::new())
.ok()?;
Some(chunk.into_data())
}
fn verdict_registers(machine: &Machine) -> Option<[u8; 6]> {
let data = chunk_of(machine, "cpu")?;
let mut r = crate::core::state::ChunkReader::new(&data);
let mut byte = || r.read_u8().ok();
let _a = byte()?;
let _f = byte()?;
Some([byte()?, byte()?, byte()?, byte()?, byte()?, byte()?])
}
fn serial_transcript(machine: &Machine) -> Option<String> {
let data = chunk_of(machine, "link")?;
let mut r = crate::core::state::ChunkReader::new(&data);
let _sb = r.read_u8().ok()?;
let _sc = r.read_u8().ok()?;
let _remaining = r.read_u64().ok()?;
let _tick = r.read_u64().ok()?;
let bytes = r.read_bytes().ok()?;
Some(bytes.iter().map(|b| *b as char).collect())
}
fn frames(machine: &Machine) -> Option<u64> {
let data = chunk_of(machine, "ppu")?;
let mut r = crate::core::state::ChunkReader::new(&data);
let _vram = r.read_bytes().ok()?;
let _oam = r.read_bytes().ok()?;
let _fb = r.read_bytes().ok()?;
for _ in 0..14 {
r.read_u8().ok()?;
}
let _window_active = r.read_bool().ok()?;
let _dot = r.read_u64().ok()?;
let _dots = r.read_u64().ok()?;
r.read_u64().ok()
}
fn machine_for(rom: &[u8]) -> Result<Machine, String> {
catalog::build_catalog("gameboy", &[("cart", rom)]).map_err(|e| e.to_string())
}
const BATCHES_PER_FRAME_LCD_OFF: u64 = 8;
fn run(machine: &mut Machine, limit_frames: u64, mut stop: impl FnMut(&Machine) -> bool) -> bool {
let start = frames(machine).unwrap_or(0);
let max_batches = limit_frames
.saturating_mul(BATCHES_PER_FRAME_LCD_OFF)
.max(1);
for _ in 0..max_batches {
for _ in 0..QUANTA_PER_CHECK {
if machine.run_quantum().is_err() {
return stop(machine);
}
}
if stop(machine) {
return true;
}
if frames(machine).unwrap_or(u64::MAX).saturating_sub(start) >= limit_frames {
return false;
}
}
false
}
const BLARGG_LEDGER: &[&str] = &["instr_timing.gb"];
#[test]
fn blargg_on_the_shipped_machine() {
let Some(dir) = corpus(
"RSEMU_GB_BLARGG_DIR",
"gb-blargg",
"scripts/fetch-testdata.sh gb-blargg",
) else {
return;
};
let roms = roms(&dir);
if roms.is_empty() {
println!("SKIP gb-blargg: no .gb files under {}", dir.display());
return;
}
let limit = frame_limit();
let mut passed = 0usize;
let mut failures = Vec::new();
for rom in &roms {
let name = label(&dir, rom);
let Ok(bytes) = std::fs::read(rom) else {
failures.push(format!("{name}: unreadable"));
continue;
};
let mut machine = match machine_for(&bytes) {
Ok(m) => m,
Err(e) => {
println!(" FAIL {name}: {e}");
failures.push(name);
continue;
}
};
machine.reset(crate::core::device::ResetKind::Cold);
run(&mut machine, limit, |m| {
let text = serial_transcript(m).unwrap_or_default();
text.contains("Passed") || text.contains("Failed")
});
let text = serial_transcript(&machine).unwrap_or_default();
if text.contains("Passed") {
passed += 1;
println!(" pass {name}");
} else if text.contains("Failed") {
let ledgered = BLARGG_LEDGER.iter().any(|l| name.ends_with(l));
let mark = if ledgered { "LDGR" } else { "FAIL" };
println!(" {mark} {name}: {}", text.trim().replace('\n', " / "));
if !ledgered {
failures.push(name);
}
} else {
println!(
" ???? {name}: no verdict in {limit} frames ({})",
text.trim().replace('\n', " / ")
);
failures.push(name);
}
}
println!(
"blargg (whole machine): {passed}/{} ROMs passed, {} ledgered",
roms.len(),
BLARGG_LEDGER.len()
);
assert!(
failures.is_empty(),
"blargg failures: {}",
failures.join(", ")
);
}
const MOONEYE_PASS: [u8; 6] = [3, 5, 8, 13, 21, 34];
const MOONEYE_FAIL: [u8; 6] = [0x42; 6];
fn targets_dmg(name: &str) -> bool {
let stem = name.rsplit('/').next().unwrap_or(name);
let stem = stem.strip_suffix(".gb").unwrap_or(stem);
match stem.rsplit_once('-') {
Some((_, suffix)) => matches!(suffix, "dmgABC" | "dmgABCmgb" | "GS"),
None => true,
}
}
#[test]
fn mooneye_acceptance_on_the_shipped_machine() {
let Some(dir) = corpus(
"RSEMU_GB_MOONEYE_DIR",
"gb-mooneye",
"scripts/fetch-testdata.sh gb-mooneye",
) else {
return;
};
let all = roms(&dir);
let roms: Vec<_> = all
.into_iter()
.filter(|p| targets_dmg(&label(&dir, p)))
.collect();
if roms.is_empty() {
println!("SKIP gb-mooneye: no DMG .gb files under {}", dir.display());
return;
}
let limit = frame_limit();
let mut passed = 0usize;
let mut failed = Vec::new();
for rom in &roms {
let name = label(&dir, rom);
let Ok(bytes) = std::fs::read(rom) else {
failed.push(format!("{name}: unreadable"));
continue;
};
let mut machine = match machine_for(&bytes) {
Ok(m) => m,
Err(e) => {
println!(" FAIL {name}: {e}");
failed.push(name);
continue;
}
};
machine.reset(crate::core::device::ResetKind::Cold);
run(&mut machine, limit, |m| {
matches!(
verdict_registers(m),
Some(MOONEYE_PASS) | Some(MOONEYE_FAIL)
)
});
match verdict_registers(&machine) {
Some(MOONEYE_PASS) => {
passed += 1;
println!(" pass {name}");
}
Some(MOONEYE_FAIL) => {
println!(" FAIL {name}");
failed.push(name);
}
Some(regs) => {
println!(
" TIME {name}: no verdict in {limit} frames \
(B={:02x} C={:02x} D={:02x} E={:02x} H={:02x} L={:02x})",
regs[0], regs[1], regs[2], regs[3], regs[4], regs[5]
);
failed.push(name);
}
None => {
println!(" ???? {name}: could not read the register file");
failed.push(name);
}
}
}
println!(
"mooneye acceptance (DMG subset): {passed}/{} ROMs passed",
roms.len()
);
println!(" {} still failing", failed.len());
}
#[test]
fn the_harness_reads_a_synthetic_roms_serial_output() {
let program: &[u8] = &[
0x3e, b'H', 0xe0, 0x01, 0x3e, 0x81, 0xe0, 0x02, 0x3e, b'i', 0xe0, 0x01, 0x3e, 0x81, 0xe0, 0x02, 0x18, 0xfe, ];
let rom = super::cart::synthetic_image(2, 0x00, 0x00, program);
let mut machine = machine_for(&rom).expect("the shipped machine builds");
machine.reset(crate::core::device::ResetKind::Cold);
let done = run(&mut machine, 4, |m| {
serial_transcript(m).unwrap_or_default() == "Hi"
});
assert!(done, "the transcript never reached `Hi`");
}
#[test]
fn the_harness_reads_the_register_file_and_the_frame_counter() {
let program: &[u8] = &[
0x06, 3, 0x0e, 5, 0x16, 8, 0x1e, 13, 0x26, 21, 0x2e, 34, 0x18, 0xfe, ];
let rom = super::cart::synthetic_image(2, 0x00, 0x00, program);
let mut machine = machine_for(&rom).expect("the shipped machine builds");
machine.reset(crate::core::device::ResetKind::Cold);
let done = run(&mut machine, 4, |m| {
verdict_registers(m) == Some(MOONEYE_PASS)
});
assert!(done, "the register pattern was never seen");
assert!(frames(&machine).is_some(), "the frame counter decodes");
}
#[test]
fn only_the_dmg_variants_of_the_suite_are_selected() {
assert!(targets_dmg("acceptance/timer/tim00.gb"));
assert!(targets_dmg("acceptance/boot_regs-dmgABC.gb"));
assert!(targets_dmg("acceptance/di_timing-GS.gb"));
assert!(!targets_dmg("acceptance/boot_regs-sgb.gb"));
assert!(!targets_dmg("acceptance/boot_div-dmg0.gb"));
assert!(!targets_dmg("acceptance/boot_hwio-S.gb"));
}