pub const EMBEDDED_BIOS: &[u8; 16384] = include_bytes!("bios.bin");
#[cfg(test)]
mod tests {
use super::*;
use crate::gba::Gba;
use crate::gba::bus::memory::BIOS_SIZE;
use crate::gba::cartridge::header::{
COMPLEMENT_CHECK_OFFSET, FIXED_BYTE_OFFSET, FIXED_BYTE_VALUE, compute_complement_check,
};
use crate::gba::cpu::bus::Bus;
use crate::platform::app_context::AppContext;
use crate::platform::config::Config;
use crate::platform::emulator::Emulator;
#[test]
fn embedded_bios_has_correct_size() {
assert_eq!(EMBEDDED_BIOS.len(), BIOS_SIZE);
}
#[test]
fn embedded_bios_has_valid_reset_vector() {
let first_word = u32::from_le_bytes([
EMBEDDED_BIOS[0],
EMBEDDED_BIOS[1],
EMBEDDED_BIOS[2],
EMBEDDED_BIOS[3],
]);
assert_eq!(first_word >> 24, 0xEA, "reset vector should be a branch");
}
#[test]
fn embedded_bios_has_valid_swi_vector() {
let swi_word = u32::from_le_bytes([
EMBEDDED_BIOS[0x08],
EMBEDDED_BIOS[0x09],
EMBEDDED_BIOS[0x0A],
EMBEDDED_BIOS[0x0B],
]);
assert_eq!(swi_word >> 24, 0xEA, "SWI vector should be a branch");
}
#[test]
fn embedded_bios_has_valid_irq_vector() {
let irq_word = u32::from_le_bytes([
EMBEDDED_BIOS[0x18],
EMBEDDED_BIOS[0x19],
EMBEDDED_BIOS[0x1A],
EMBEDDED_BIOS[0x1B],
]);
assert_eq!(irq_word >> 24, 0xEA, "IRQ vector should be a branch");
}
fn make_test_rom(arm_code: &[u32]) -> Vec<u8> {
let code_bytes = arm_code.len() * 4;
let rom_size = (0xC0 + code_bytes).max(0x100);
let mut rom = vec![0u8; rom_size];
for (i, &word) in arm_code.iter().enumerate() {
let offset = i * 4;
rom[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
}
rom[FIXED_BYTE_OFFSET] = FIXED_BYTE_VALUE;
rom[COMPLEMENT_CHECK_OFFSET] = compute_complement_check(&rom);
rom
}
fn boot_with_embedded_bios(arm_code: &[u32]) -> Gba {
let mut config = Config::default();
let tmp = std::env::temp_dir().join("neser_bios_test_nonexistent");
config.gba.bios_path = Some(tmp.to_string_lossy().into_owned());
let mut gba = Gba::new(AppContext::new_with_config(config));
let rom = make_test_rom(arm_code);
gba.load_rom(&rom, "bios-test.gba")
.expect("test ROM should load with embedded BIOS");
gba.bus_mut().write8(0x03007FFC, 1);
let mut cycles = 0u64;
while cycles < 10_000 {
let pc = gba.cpu_pc();
if pc >= 0x0800_0000 {
break;
}
let tick = gba.run_tick_for_tests() as u64;
if tick == 0 {
break;
}
cycles += tick;
}
assert!(
gba.cpu_pc() >= 0x0800_0000,
"BIOS should boot to cartridge entry point, got PC={:#010X}",
gba.cpu_pc()
);
gba
}
fn run_until_idle(gba: &mut Gba, max_cycles: u64) {
let mut cycles = 0u64;
let mut last_pc = None;
while cycles < max_cycles {
let tick = gba.run_tick_for_tests() as u64;
if tick == 0 {
break;
}
cycles += tick;
let pc = gba.cpu_pc();
if Some(pc) == last_pc {
break;
}
last_pc = Some(pc);
}
}
const ARM_IDLE: u32 = 0xEAFF_FFFE;
const fn arm_mov_imm(rd: u32, imm8: u32) -> u32 {
0xE3A0_0000 | (rd << 12) | (imm8 & 0xFF)
}
const fn arm_swi(swi_num: u32) -> u32 {
0xEF00_0000 | ((swi_num & 0xFF) << 16)
}
const fn arm_mvn_imm(rd: u32, imm8: u32) -> u32 {
0xE3E0_0000 | (rd << 12) | (imm8 & 0xFF)
}
const fn arm_mov_imm_rot(rd: u32, imm8: u32, rot: u32) -> u32 {
0xE3A0_0000 | (rd << 12) | ((rot & 0xF) << 8) | (imm8 & 0xFF)
}
const fn arm_orr_imm_rot(rd: u32, rn: u32, imm8: u32, rot: u32) -> u32 {
0xE380_0000 | (rn << 16) | (rd << 12) | ((rot & 0xF) << 8) | (imm8 & 0xFF)
}
const fn arm_str(rd: u32, rn: u32, imm12: u32) -> u32 {
0xE580_0000 | (rn << 16) | (rd << 12) | (imm12 & 0xFFF)
}
fn arm_load_const(rd: u32, value: u32) -> Vec<u32> {
let mut instrs = Vec::new();
let byte0 = value & 0xFF;
let byte1 = (value >> 8) & 0xFF;
let byte2 = (value >> 16) & 0xFF;
let byte3 = (value >> 24) & 0xFF;
instrs.push(arm_mov_imm(rd, byte0));
if byte1 != 0 {
instrs.push(arm_orr_imm_rot(rd, rd, byte1, 12));
}
if byte2 != 0 {
instrs.push(arm_orr_imm_rot(rd, rd, byte2, 8));
}
if byte3 != 0 {
instrs.push(arm_orr_imm_rot(rd, rd, byte3, 4));
}
instrs
}
#[test]
fn bios_div_positive_values() {
let code = &[
arm_mov_imm(0, 7), arm_mov_imm(1, 3), arm_swi(0x06), ARM_IDLE, ];
let mut gba = boot_with_embedded_bios(code);
run_until_idle(&mut gba, 100_000);
assert_eq!(gba.cpu_reg(0), 2, "quotient of 7/3");
assert_eq!(gba.cpu_reg(1), 1, "remainder of 7/3");
assert_eq!(gba.cpu_reg(3), 2, "abs(quotient) of 7/3");
}
#[test]
fn bios_div_negative_numerator() {
let code = &[
arm_mvn_imm(0, 6), arm_mov_imm(1, 3), arm_swi(0x06), ARM_IDLE,
];
let mut gba = boot_with_embedded_bios(code);
run_until_idle(&mut gba, 100_000);
assert_eq!(gba.cpu_reg(0) as i32, -2, "quotient of -7/3");
assert_eq!(gba.cpu_reg(1) as i32, -1, "remainder of -7/3");
assert_eq!(gba.cpu_reg(3), 2, "abs(quotient) of -7/3");
}
#[test]
fn bios_div_exact() {
let code = &[
arm_mov_imm(0, 10),
arm_mov_imm(1, 5),
arm_swi(0x06),
ARM_IDLE,
];
let mut gba = boot_with_embedded_bios(code);
run_until_idle(&mut gba, 100_000);
assert_eq!(gba.cpu_reg(0), 2, "quotient of 10/5");
assert_eq!(gba.cpu_reg(1), 0, "remainder of 10/5");
assert_eq!(gba.cpu_reg(3), 2, "abs(quotient) of 10/5");
}
#[test]
fn bios_div_large_dividend_does_not_hang() {
let code = &[
arm_mov_imm_rot(0, 0x02, 1), arm_mov_imm(1, 1), arm_swi(0x06), ARM_IDLE,
];
let mut gba = boot_with_embedded_bios(code);
run_until_idle(&mut gba, 500_000);
assert_eq!(
gba.cpu_reg(0) as i32,
-2_147_483_648i32,
"quotient of 0x80000000/1"
);
assert_eq!(gba.cpu_reg(1), 0, "remainder of 0x80000000/1");
}
#[test]
fn bios_div_arm_swaps_operands() {
let code = &[
arm_mov_imm(0, 3), arm_mov_imm(1, 7), arm_swi(0x07), ARM_IDLE,
];
let mut gba = boot_with_embedded_bios(code);
run_until_idle(&mut gba, 100_000);
assert_eq!(gba.cpu_reg(0), 2, "quotient of 7/3 via DivArm");
assert_eq!(gba.cpu_reg(1), 1, "remainder of 7/3 via DivArm");
assert_eq!(gba.cpu_reg(3), 2, "abs(quotient) of 7/3 via DivArm");
}
#[test]
fn bios_sqrt_perfect_square() {
let code = &[arm_mov_imm(0, 16), arm_swi(0x08), ARM_IDLE];
let mut gba = boot_with_embedded_bios(code);
run_until_idle(&mut gba, 100_000);
assert_eq!(gba.cpu_reg(0), 4, "sqrt(16)");
}
#[test]
fn bios_sqrt_non_perfect() {
let code = &[arm_mov_imm(0, 10), arm_swi(0x08), ARM_IDLE];
let mut gba = boot_with_embedded_bios(code);
run_until_idle(&mut gba, 100_000);
assert_eq!(gba.cpu_reg(0), 3, "floor(sqrt(10))");
}
#[test]
fn bios_sqrt_zero() {
let code = &[arm_mov_imm(0, 0), arm_swi(0x08), ARM_IDLE];
let mut gba = boot_with_embedded_bios(code);
run_until_idle(&mut gba, 100_000);
assert_eq!(gba.cpu_reg(0), 0, "sqrt(0)");
}
#[test]
fn bios_sqrt_one() {
let code = &[arm_mov_imm(0, 1), arm_swi(0x08), ARM_IDLE];
let mut gba = boot_with_embedded_bios(code);
run_until_idle(&mut gba, 100_000);
assert_eq!(gba.cpu_reg(0), 1, "sqrt(1)");
}
#[test]
fn bios_checksum_returns_gba_checksum() {
let code = &[arm_swi(0x0D), ARM_IDLE];
let mut gba = boot_with_embedded_bios(code);
run_until_idle(&mut gba, 100_000);
assert_eq!(
gba.cpu_reg(0),
0xBAAE_187F,
"BiosChecksum should match the original GBA/GBA SP BIOS checksum for game compatibility"
);
}
#[test]
fn bios_boot_sets_postflg() {
let code = &[ARM_IDLE];
let mut gba = boot_with_embedded_bios(code);
run_until_idle(&mut gba, 100_000);
let postflg = gba.bus_mut().read8(0x04000300);
assert_eq!(postflg, 1, "POSTFLG should be 1 after BIOS boot");
}
#[test]
fn bios_arctan_zero() {
let code = &[arm_mov_imm(0, 0), arm_swi(0x09), ARM_IDLE];
let mut gba = boot_with_embedded_bios(code);
run_until_idle(&mut gba, 200_000);
assert_eq!(gba.cpu_reg(0), 0, "ArcTan(0) result");
assert_eq!(gba.cpu_reg(1) as i32, 0, "ArcTan(0) intermediate a");
assert_eq!(gba.cpu_reg(3), 0xA2F9, "ArcTan(0) coefficient b");
}
#[test]
fn bios_arctan_quarter() {
let mut code = arm_load_const(0, 0x4000);
code.push(arm_swi(0x09));
code.push(ARM_IDLE);
let mut gba = boot_with_embedded_bios(&code);
run_until_idle(&mut gba, 200_000);
assert_eq!(gba.cpu_reg(0), 0x2000, "ArcTan(0x4000) result");
assert_eq!(gba.cpu_reg(1), 0xFFFFC000, "ArcTan(0x4000) intermediate a");
assert_eq!(gba.cpu_reg(3), 0x8000, "ArcTan(0x4000) coefficient b");
}
#[test]
fn bios_arctan2_zero_zero() {
let code = &[
arm_mov_imm(0, 0),
arm_mov_imm(1, 0),
arm_swi(0x0A),
ARM_IDLE,
];
let mut gba = boot_with_embedded_bios(code);
run_until_idle(&mut gba, 200_000);
assert_eq!(gba.cpu_reg(0), 0, "ArcTan2(0,0) angle");
assert_eq!(gba.cpu_reg(3), 0x170, "ArcTan2(0,0) r3 clobber");
}
#[test]
fn bios_arctan2_equal_positive() {
let mut code = arm_load_const(0, 0x4000);
code.extend(arm_load_const(1, 0x4000));
code.push(arm_swi(0x0A));
code.push(ARM_IDLE);
let mut gba = boot_with_embedded_bios(&code);
run_until_idle(&mut gba, 200_000);
assert_eq!(gba.cpu_reg(0), 0x2000, "ArcTan2(0x4000,0x4000) angle");
assert_eq!(gba.cpu_reg(3), 0x170, "ArcTan2(0x4000,0x4000) r3");
}
#[test]
fn bios_arctan2_negative_x_zero_y() {
let mut code = arm_load_const(0, 0xFFFF0000);
code.push(arm_mov_imm(1, 0));
code.push(arm_swi(0x0A));
code.push(ARM_IDLE);
let mut gba = boot_with_embedded_bios(&code);
run_until_idle(&mut gba, 200_000);
assert_eq!(gba.cpu_reg(0), 0x8000, "ArcTan2(neg,0) angle");
assert_eq!(gba.cpu_reg(3), 0x170, "ArcTan2(neg,0) r3");
}
fn boot_and_setup_memory(arm_code: &[u32], mem_setup: &[(u32, &[u8])]) -> Gba {
let mut gba = boot_with_embedded_bios(arm_code);
for &(addr, data) in mem_setup {
for (i, &byte) in data.iter().enumerate() {
gba.bus_mut().write8(addr + i as u32, byte);
}
}
gba
}
#[test]
fn bios_cpu_set_copies_halfwords() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_mov_imm(2, 4)); code.push(arm_swi(0x0B));
code.push(ARM_IDLE);
let halfwords: [u16; 4] = [0x1234, 0x5678, 0x9ABC, 0xDEF0];
let src_data: Vec<u8> = halfwords.iter().flat_map(|v| v.to_le_bytes()).collect();
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
for (i, &expected) in halfwords.iter().enumerate() {
assert_eq!(
gba.bus_mut().read16(dst_addr + (i as u32) * 2),
expected,
"CpuSet halfword {i}"
);
}
}
#[test]
fn bios_cpu_set_copies_halfwords_from_unaligned_source() {
let src_addr: u32 = 0x0200_0101;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_mov_imm(2, 2));
code.push(arm_swi(0x0B));
code.push(ARM_IDLE);
let src_data = 0xFEED_FACEu32.to_le_bytes();
let mut gba = boot_and_setup_memory(&code, &[(src_addr & !1, &src_data)]);
run_until_idle(&mut gba, 500_000);
assert_eq!(
gba.bus_mut().read32(dst_addr),
0x00FE_00FA,
"CpuSet halfword copy should preserve odd source byte lane"
);
}
#[test]
fn bios_cpu_set_fills_halfwords_from_unaligned_source() {
let src_addr: u32 = 0x0200_0101;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
let mut r2_code = arm_load_const(2, 2 | (1 << 24));
code.append(&mut r2_code);
code.push(arm_swi(0x0B));
code.push(ARM_IDLE);
let src_data = 0xFEED_FACEu32.to_le_bytes();
let mut gba = boot_and_setup_memory(&code, &[(src_addr & !1, &src_data)]);
run_until_idle(&mut gba, 500_000);
assert_eq!(
gba.bus_mut().read32(dst_addr),
0x00FA_00FA,
"CpuSet halfword fill should preserve odd source byte lane"
);
}
#[test]
fn bios_cpu_set_copies_words() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
let mut r2_code = arm_load_const(2, 2 | (1 << 26));
code.append(&mut r2_code);
code.push(arm_swi(0x0B));
code.push(ARM_IDLE);
let mut src_data = Vec::new();
src_data.extend_from_slice(&0xDEAD_BEEFu32.to_le_bytes());
src_data.extend_from_slice(&0xCAFE_BABEu32.to_le_bytes());
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
assert_eq!(gba.bus_mut().read32(dst_addr), 0xDEAD_BEEF, "CpuSet word 0");
assert_eq!(
gba.bus_mut().read32(dst_addr + 4),
0xCAFE_BABE,
"CpuSet word 1"
);
}
#[test]
fn bios_cpu_set_copies_words_from_unaligned_sram_source() {
let dst_addr: u32 = 0x0200_0200;
for base in [0x0E00_0000, 0x0F00_0000] {
for (offset, expected) in [(1, 0x6161_6161), (2, 0x6D6D_6D6D), (3, 0x6565_6565)] {
let src_addr = base + offset;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
let mut r2_code = arm_load_const(2, 1 | (1 << 26));
code.append(&mut r2_code);
code.push(arm_swi(0x0B));
code.push(ARM_IDLE);
let mut gba = boot_and_setup_memory(&code, &[(base, b"Game")]);
run_until_idle(&mut gba, 500_000);
assert_eq!(
gba.bus_mut().read32(dst_addr),
expected,
"CpuSet word copy should preserve SRAM source lane at {src_addr:#010X}"
);
}
}
}
#[test]
fn bios_cpu_set_copies_words_to_unaligned_sram_destination() {
let src_addr: u32 = 0x0200_0100;
let src_data = 0x6666_6666u32.to_le_bytes();
for base in [0x0E00_0000, 0x0F00_0000] {
for offset in 1..=3 {
let dst_addr = base + offset;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
let mut r2_code = arm_load_const(2, 1 | (1 << 26));
code.append(&mut r2_code);
code.push(arm_swi(0x0B));
code.push(ARM_IDLE);
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
for lane in 1..=3 {
gba.bus_mut().write8(base + lane, 0xD8);
}
run_until_idle(&mut gba, 500_000);
assert_eq!(
gba.bus_mut().read32(dst_addr),
0x6666_6666,
"CpuSet word copy should preserve SRAM destination lane at {dst_addr:#010X}"
);
}
}
}
#[test]
fn bios_cpu_set_fill_mode() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
let mut r2_code = arm_load_const(2, 4 | (1 << 24) | (1 << 26));
code.append(&mut r2_code);
code.push(arm_swi(0x0B));
code.push(ARM_IDLE);
let src_data = 0xA5A5_A5A5u32.to_le_bytes();
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
for i in 0u32..4 {
assert_eq!(
gba.bus_mut().read32(dst_addr + i * 4),
0xA5A5_A5A5,
"CpuSet fill word {i}"
);
}
}
#[test]
fn bios_cpu_fast_set_copies_words() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_mov_imm(2, 8)); code.push(arm_swi(0x0C));
code.push(ARM_IDLE);
let mut src_data = Vec::new();
for i in 0u32..8 {
src_data.extend_from_slice(&(0x1000_0000 + i).to_le_bytes());
}
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
for i in 0u32..8 {
assert_eq!(
gba.bus_mut().read32(dst_addr + i * 4),
0x1000_0000 + i,
"CpuFastSet word {i}"
);
}
}
#[test]
fn bios_cpu_fast_set_copies_words_from_unaligned_sram_source() {
let dst_addr: u32 = 0x0200_0200;
for base in [0x0E00_0000, 0x0F00_0000] {
for (offset, expected) in [(1, 0x6161_6161), (2, 0x6D6D_6D6D), (3, 0x6565_6565)] {
let src_addr = base + offset;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_mov_imm(2, 8));
code.push(arm_swi(0x0C));
code.push(ARM_IDLE);
let mut gba = boot_and_setup_memory(&code, &[(base, b"Game")]);
run_until_idle(&mut gba, 500_000);
assert_eq!(
gba.bus_mut().read32(dst_addr),
expected,
"CpuFastSet word copy should preserve SRAM source lane at {src_addr:#010X}"
);
}
}
}
#[test]
fn bios_cpu_fast_set_copies_words_to_unaligned_sram_destination() {
let src_addr: u32 = 0x0200_0100;
let src_data = 0x6666_6666u32.to_le_bytes().repeat(8);
for base in [0x0E00_0000, 0x0F00_0000] {
for offset in 1..=3 {
let dst_addr = base + offset;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_mov_imm(2, 8));
code.push(arm_swi(0x0C));
code.push(ARM_IDLE);
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
for lane in 1..=3 {
gba.bus_mut().write8(base + lane, 0xD8);
}
run_until_idle(&mut gba, 500_000);
assert_eq!(
gba.bus_mut().read32(dst_addr),
0x6666_6666,
"CpuFastSet word copy should preserve SRAM destination lane at {dst_addr:#010X}"
);
}
}
}
#[test]
fn bios_cpu_fast_set_fill_mode() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
let mut r2_code = arm_load_const(2, 3 | (1 << 24));
code.append(&mut r2_code);
code.push(arm_swi(0x0C));
code.push(ARM_IDLE);
let src_data = 0xBEEF_CAFEu32.to_le_bytes();
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
for i in 0u32..8 {
assert_eq!(
gba.bus_mut().read32(dst_addr + i * 4),
0xBEEF_CAFE,
"CpuFastSet fill word {i}"
);
}
}
#[test]
fn bios_bg_affine_set_identity() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_mov_imm(2, 1));
code.push(arm_swi(0x0E));
code.push(ARM_IDLE);
let mut src_data = Vec::new();
src_data.extend_from_slice(&0i32.to_le_bytes()); src_data.extend_from_slice(&0i32.to_le_bytes()); src_data.extend_from_slice(&0i16.to_le_bytes()); src_data.extend_from_slice(&0i16.to_le_bytes()); src_data.extend_from_slice(&0x0100i16.to_le_bytes()); src_data.extend_from_slice(&0x0100i16.to_le_bytes()); src_data.extend_from_slice(&0u16.to_le_bytes());
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 1_000_000);
let pa = gba.bus_mut().read16(dst_addr) as i16;
let pb = gba.bus_mut().read16(dst_addr + 2) as i16;
let pc = gba.bus_mut().read16(dst_addr + 4) as i16;
let pd = gba.bus_mut().read16(dst_addr + 6) as i16;
let x0 = gba.bus_mut().read32(dst_addr + 8) as i32;
let y0 = gba.bus_mut().read32(dst_addr + 12) as i32;
assert_eq!(pa, 0x0100, "pa should be 1.0 (0x100)");
assert_eq!(pb, 0, "pb should be 0");
assert_eq!(pc, 0, "pc should be 0");
assert_eq!(pd, 0x0100, "pd should be 1.0 (0x100)");
assert_eq!(x0, 0, "x0 should be 0");
assert_eq!(y0, 0, "y0 should be 0");
}
#[test]
fn bios_obj_affine_set_identity() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_mov_imm(2, 1));
code.push(arm_mov_imm(3, 2));
code.push(arm_swi(0x0F));
code.push(ARM_IDLE);
let mut src_data = Vec::new();
src_data.extend_from_slice(&0x0100i16.to_le_bytes()); src_data.extend_from_slice(&0x0100i16.to_le_bytes()); src_data.extend_from_slice(&0u16.to_le_bytes());
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 1_000_000);
let pa = gba.bus_mut().read16(dst_addr) as i16;
let pb = gba.bus_mut().read16(dst_addr + 2) as i16;
let pc = gba.bus_mut().read16(dst_addr + 4) as i16;
let pd = gba.bus_mut().read16(dst_addr + 6) as i16;
assert_eq!(pa, 0x0100, "PA should be 1.0 (0x100)");
assert_eq!(pb, 0, "PB should be 0");
assert_eq!(pc, 0, "PC should be 0");
assert_eq!(pd, 0x0100, "PD should be 1.0 (0x100)");
}
#[test]
fn bios_bit_unpack_1bpp_to_4bpp() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let info_addr: u32 = 0x0200_0300;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.extend(arm_load_const(2, info_addr));
code.push(arm_swi(0x10));
code.push(ARM_IDLE);
let src_data: Vec<u8> = vec![0b1011_0001];
let mut info_data = Vec::new();
info_data.extend_from_slice(&1u16.to_le_bytes()); info_data.push(1); info_data.push(4); info_data.extend_from_slice(&1u32.to_le_bytes());
let mut gba =
boot_and_setup_memory(&code, &[(src_addr, &src_data), (info_addr, &info_data)]);
run_until_idle(&mut gba, 500_000);
let result = gba.bus_mut().read32(dst_addr);
assert_eq!(result, 0x2022_0002, "BitUnPack 1bpp→4bpp with offset=1");
}
#[test]
fn bios_lz77_wram_all_literals() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_swi(0x11));
code.push(ARM_IDLE);
let header: u32 = (4 << 8) | (1 << 4);
let mut src_data = header.to_le_bytes().to_vec();
src_data.push(0x00);
src_data.extend_from_slice(&[0x41, 0x42, 0x43, 0x44]);
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
let expected = [0x41u8, 0x42, 0x43, 0x44];
for (i, &exp) in expected.iter().enumerate() {
assert_eq!(
gba.bus_mut().read8(dst_addr + i as u32),
exp,
"LZ77 literal byte {i}"
);
}
}
#[test]
fn bios_lz77_wram_back_reference() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_swi(0x11));
code.push(ARM_IDLE);
let header: u32 = (6 << 8) | (1 << 4);
let mut src_data = header.to_le_bytes().to_vec();
src_data.push(0x10);
src_data.extend_from_slice(&[0x41, 0x42, 0x43]);
src_data.push(0x00);
src_data.push(0x02);
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
let expected = [0x41, 0x42, 0x43, 0x41, 0x42, 0x43];
for (i, &exp) in expected.iter().enumerate() {
assert_eq!(
gba.bus_mut().read8(dst_addr + i as u32),
exp,
"LZ77 back-ref byte {i}"
);
}
}
#[test]
fn bios_lz77_wram_repeated_pattern() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_swi(0x11));
code.push(ARM_IDLE);
let header: u32 = (10 << 8) | (1 << 4);
let mut src_data = header.to_le_bytes().to_vec();
src_data.push(0x20);
src_data.extend_from_slice(&[0x41, 0x42]);
src_data.push(0x50); src_data.push(0x01);
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
let expected = [0x41, 0x42, 0x41, 0x42, 0x41, 0x42, 0x41, 0x42, 0x41, 0x42];
for (i, &exp) in expected.iter().enumerate() {
assert_eq!(
gba.bus_mut().read8(dst_addr + i as u32),
exp,
"LZ77 repeat byte {i}"
);
}
}
#[test]
fn bios_lz77_vram_basic() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_swi(0x12));
code.push(ARM_IDLE);
let header: u32 = (4 << 8) | (1 << 4);
let mut src_data = header.to_le_bytes().to_vec();
src_data.push(0x00); src_data.extend_from_slice(&[0x41, 0x42, 0x43, 0x44]);
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
assert_eq!(gba.bus_mut().read16(dst_addr), 0x4241, "LZ77 VRAM hw 0");
assert_eq!(gba.bus_mut().read16(dst_addr + 2), 0x4443, "LZ77 VRAM hw 1");
}
#[test]
fn bios_huffman_8bit_two_symbols() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_swi(0x13));
code.push(ARM_IDLE);
let header: u32 = (4 << 8) | (2 << 4) | 8;
let mut src_data = header.to_le_bytes().to_vec();
src_data.push(1);
src_data.push(0xC0);
src_data.push(0x41);
src_data.push(0x42);
src_data.extend_from_slice(&0x30000000u32.to_le_bytes());
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
assert_eq!(gba.bus_mut().read8(dst_addr), 0x41, "Huffman 8bit byte 0");
assert_eq!(
gba.bus_mut().read8(dst_addr + 1),
0x41,
"Huffman 8bit byte 1"
);
assert_eq!(
gba.bus_mut().read8(dst_addr + 2),
0x42,
"Huffman 8bit byte 2"
);
assert_eq!(
gba.bus_mut().read8(dst_addr + 3),
0x42,
"Huffman 8bit byte 3"
);
}
#[test]
fn bios_huffman_8bit_deeper_tree() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_swi(0x13));
code.push(ARM_IDLE);
let header: u32 = (4 << 8) | (2 << 4) | 8;
let mut src_data = header.to_le_bytes().to_vec();
src_data.push(2);
src_data.push(0x80);
src_data.push(0x41);
src_data.push(0xC0);
src_data.push(0x42);
src_data.push(0x43);
src_data.push(0x00);
src_data.push(0x00);
src_data.extend_from_slice(&0x5C000000u32.to_le_bytes());
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
assert_eq!(gba.bus_mut().read8(dst_addr), 0x41, "Huffman deep byte 0");
assert_eq!(
gba.bus_mut().read8(dst_addr + 1),
0x42,
"Huffman deep byte 1"
);
assert_eq!(
gba.bus_mut().read8(dst_addr + 2),
0x43,
"Huffman deep byte 2"
);
assert_eq!(
gba.bus_mut().read8(dst_addr + 3),
0x42,
"Huffman deep byte 3"
);
}
#[test]
fn bios_huffman_4bit() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_swi(0x13));
code.push(ARM_IDLE);
let header: u32 = (4 << 8) | (2 << 4) | 4;
let mut src_data = header.to_le_bytes().to_vec();
src_data.push(1);
src_data.push(0xC0);
src_data.push(0x03);
src_data.push(0x07);
src_data.extend_from_slice(&0x55000000u32.to_le_bytes());
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
assert_eq!(gba.bus_mut().read8(dst_addr), 0x73, "Huffman 4bit byte 0");
assert_eq!(
gba.bus_mut().read8(dst_addr + 1),
0x73,
"Huffman 4bit byte 1"
);
assert_eq!(
gba.bus_mut().read8(dst_addr + 2),
0x73,
"Huffman 4bit byte 2"
);
assert_eq!(
gba.bus_mut().read8(dst_addr + 3),
0x73,
"Huffman 4bit byte 3"
);
}
#[test]
fn bios_rle_wram_mixed() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_swi(0x14));
code.push(ARM_IDLE);
let header: u32 = (6 << 8) | (3 << 4);
let mut src_data = header.to_le_bytes().to_vec();
src_data.push(0x81);
src_data.push(0x41);
src_data.push(0x01);
src_data.extend_from_slice(&[0x42, 0x43]);
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
let expected = [0x41, 0x41, 0x41, 0x41, 0x42, 0x43];
for (i, &exp) in expected.iter().enumerate() {
assert_eq!(
gba.bus_mut().read8(dst_addr + i as u32),
exp,
"RLE mixed byte {i}"
);
}
}
#[test]
fn bios_rle_wram_all_compressed() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_swi(0x14));
code.push(ARM_IDLE);
let header: u32 = (8 << 8) | (3 << 4);
let mut src_data = header.to_le_bytes().to_vec();
src_data.push(0x85);
src_data.push(0x58);
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
for i in 0u32..8 {
assert_eq!(
gba.bus_mut().read8(dst_addr + i),
0x58,
"RLE all-compressed byte {i}"
);
}
}
#[test]
fn bios_rle_wram_all_literal() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_swi(0x14));
code.push(ARM_IDLE);
let header: u32 = (4 << 8) | (3 << 4);
let mut src_data = header.to_le_bytes().to_vec();
src_data.push(0x03);
src_data.extend_from_slice(&[0x41, 0x42, 0x43, 0x44]);
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
let expected = [0x41, 0x42, 0x43, 0x44];
for (i, &exp) in expected.iter().enumerate() {
assert_eq!(
gba.bus_mut().read8(dst_addr + i as u32),
exp,
"RLE literal byte {i}"
);
}
}
#[test]
fn bios_rle_vram_basic() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_swi(0x15));
code.push(ARM_IDLE);
let header: u32 = (4 << 8) | (3 << 4);
let mut src_data = header.to_le_bytes().to_vec();
src_data.push(0x81);
src_data.push(0x41);
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
assert_eq!(gba.bus_mut().read16(dst_addr), 0x4141, "RLE VRAM hw 0");
assert_eq!(gba.bus_mut().read16(dst_addr + 2), 0x4141, "RLE VRAM hw 1");
}
#[test]
fn bios_diff8_unfilter_wram_ascending() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_swi(0x16));
code.push(ARM_IDLE);
let header: u32 = (4 << 8) | (8 << 4) | 1;
let mut src_data = header.to_le_bytes().to_vec();
src_data.extend_from_slice(&[10, 5, 3, 2]);
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
let expected = [10u8, 15, 18, 20];
for (i, &exp) in expected.iter().enumerate() {
assert_eq!(
gba.bus_mut().read8(dst_addr + i as u32),
exp,
"Diff8 WRAM byte {i}"
);
}
}
#[test]
fn bios_diff8_unfilter_wram_negative_deltas() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_swi(0x16));
code.push(ARM_IDLE);
let header: u32 = (4 << 8) | (8 << 4) | 1;
let mut src_data = header.to_le_bytes().to_vec();
src_data.extend_from_slice(&[100, 246, 20, 226]);
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
let expected = [100u8, 90, 110, 80];
for (i, &exp) in expected.iter().enumerate() {
assert_eq!(
gba.bus_mut().read8(dst_addr + i as u32),
exp,
"Diff8 neg delta byte {i}"
);
}
}
#[test]
fn bios_diff8_unfilter_wram_wrapping() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_swi(0x16));
code.push(ARM_IDLE);
let header: u32 = (2 << 8) | (8 << 4) | 1;
let mut src_data = header.to_le_bytes().to_vec();
src_data.extend_from_slice(&[200, 100]);
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
assert_eq!(gba.bus_mut().read8(dst_addr), 200, "Diff8 wrap byte 0");
assert_eq!(gba.bus_mut().read8(dst_addr + 1), 44, "Diff8 wrap byte 1");
}
#[test]
fn bios_diff8_unfilter_vram() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_swi(0x17));
code.push(ARM_IDLE);
let header: u32 = (4 << 8) | (8 << 4) | 1;
let mut src_data = header.to_le_bytes().to_vec();
src_data.extend_from_slice(&[10, 5, 3, 2]);
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
assert_eq!(gba.bus_mut().read16(dst_addr), 0x0F0A, "Diff8 VRAM hw 0");
assert_eq!(
gba.bus_mut().read16(dst_addr + 2),
0x1412,
"Diff8 VRAM hw 1"
);
}
#[test]
fn bios_diff16_unfilter_ascending() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_swi(0x18));
code.push(ARM_IDLE);
let header: u32 = (6 << 8) | (8 << 4) | 2;
let mut src_data = header.to_le_bytes().to_vec();
src_data.extend_from_slice(&100u16.to_le_bytes());
src_data.extend_from_slice(&50u16.to_le_bytes());
src_data.extend_from_slice(&30u16.to_le_bytes());
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
assert_eq!(gba.bus_mut().read16(dst_addr), 100, "Diff16 hw 0");
assert_eq!(gba.bus_mut().read16(dst_addr + 2), 150, "Diff16 hw 1");
assert_eq!(gba.bus_mut().read16(dst_addr + 4), 180, "Diff16 hw 2");
}
#[test]
fn bios_diff16_unfilter_negative_deltas() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let mut code = arm_load_const(0, src_addr);
code.extend(arm_load_const(1, dst_addr));
code.push(arm_swi(0x18));
code.push(ARM_IDLE);
let header: u32 = (6 << 8) | (8 << 4) | 2;
let mut src_data = header.to_le_bytes().to_vec();
src_data.extend_from_slice(&1000u16.to_le_bytes());
src_data.extend_from_slice(&(-200i16 as u16).to_le_bytes());
src_data.extend_from_slice(&500u16.to_le_bytes());
let mut gba = boot_and_setup_memory(&code, &[(src_addr, &src_data)]);
run_until_idle(&mut gba, 500_000);
assert_eq!(gba.bus_mut().read16(dst_addr), 1000, "Diff16 neg hw 0");
assert_eq!(gba.bus_mut().read16(dst_addr + 2), 800, "Diff16 neg hw 1");
assert_eq!(gba.bus_mut().read16(dst_addr + 4), 1300, "Diff16 neg hw 2");
}
#[test]
fn bios_sound_bias_set_high() {
let src_addr: u32 = 0x0200_0100;
let dst_addr: u32 = 0x0200_0200;
let _ = (src_addr, dst_addr);
let mut code = vec![arm_mov_imm(0, 1)]; code.push(arm_swi(0x19));
code.push(ARM_IDLE);
let mut gba = boot_with_embedded_bios(&code);
run_until_idle(&mut gba, 2_000_000);
let bias = gba.bus_mut().read16(0x0400_0088);
assert_eq!(bias & 0x3FF, 0x200, "SoundBias should set bias to 0x200");
}
#[test]
fn bios_sound_bias_set_low() {
let mut code = vec![arm_mov_imm(0, 0)]; code.push(arm_swi(0x19));
code.push(ARM_IDLE);
let mut gba = boot_with_embedded_bios(&code);
run_until_idle(&mut gba, 2_000_000);
let bias = gba.bus_mut().read16(0x0400_0088);
assert_eq!(bias & 0x3FF, 0x000, "SoundBias should set bias to 0x000");
}
#[test]
fn bios_midi_key2freq_a4() {
let wa_addr: u32 = 0x0200_0100;
let mut wa_data: Vec<u8> = Vec::new();
wa_data.extend_from_slice(&0u16.to_le_bytes()); wa_data.extend_from_slice(&0u16.to_le_bytes()); wa_data.extend_from_slice(&7040u32.to_le_bytes()); wa_data.extend_from_slice(&0u32.to_le_bytes()); wa_data.extend_from_slice(&0u32.to_le_bytes());
let mut code = arm_load_const(0, wa_addr); code.extend(arm_load_const(1, 69)); code.extend(arm_load_const(2, 0)); code.push(arm_swi(0x1F));
let result_addr: u32 = 0x0200_0300;
code.extend(arm_load_const(4, result_addr));
code.push(arm_str(0, 4, 0)); code.push(ARM_IDLE);
let mut gba = boot_and_setup_memory(&code, &[(wa_addr, &wa_data)]);
run_until_idle(&mut gba, 500_000);
let result = gba.bus_mut().read32(result_addr);
assert!(
result > 0 && result < 1000,
"MidiKey2Freq should return a small positive value for high key offset, got {result}"
);
}
#[test]
fn bios_midi_key2freq_key_180() {
let wa_addr: u32 = 0x0200_0100;
let mut wa_data: Vec<u8> = Vec::new();
wa_data.extend_from_slice(&0u16.to_le_bytes()); wa_data.extend_from_slice(&0u16.to_le_bytes()); wa_data.extend_from_slice(&8000u32.to_le_bytes()); wa_data.extend_from_slice(&0u32.to_le_bytes()); wa_data.extend_from_slice(&0u32.to_le_bytes());
let mut code = arm_load_const(0, wa_addr);
code.extend(arm_load_const(1, 180)); code.extend(arm_load_const(2, 0)); code.push(arm_swi(0x1F));
let result_addr: u32 = 0x0200_0300;
code.extend(arm_load_const(4, result_addr));
code.push(arm_str(0, 4, 0));
code.push(ARM_IDLE);
let mut gba = boot_and_setup_memory(&code, &[(wa_addr, &wa_data)]);
run_until_idle(&mut gba, 500_000);
let result = gba.bus_mut().read32(result_addr);
assert_eq!(
result, 8000,
"MidiKey2Freq at key 180 should return base freq"
);
}
#[test]
fn bios_multiboot_returns_failure() {
let result_addr: u32 = 0x0200_0300;
let mut code = arm_load_const(0, 0x0200_0100); code.extend(arm_load_const(1, 0)); code.push(arm_swi(0x25));
code.extend(arm_load_const(4, result_addr));
code.push(arm_str(0, 4, 0)); code.push(ARM_IDLE);
let mut gba = boot_with_embedded_bios(&code);
run_until_idle(&mut gba, 500_000);
let result = gba.bus_mut().read32(result_addr);
assert_eq!(result, 1, "MultiBoot stub should return r0=1 (failure)");
}
#[test]
fn bios_sound_driver_stubs_return() {
for swi_num in [0x1A, 0x1B, 0x1C, 0x1D, 0x1E] {
let mut code = vec![arm_mov_imm(0, 0)];
code.push(arm_swi(swi_num));
code.push(ARM_IDLE);
let mut gba = boot_with_embedded_bios(&code);
run_until_idle(&mut gba, 500_000);
let pc = gba.cpu_reg(15);
assert!(
pc >= 0x0800_0000,
"SWI 0x{swi_num:02X} stub should return to ROM"
);
}
}
#[test]
fn bios_sound_vsync_off_on_stubs_return() {
for swi_num in [0x28, 0x29, 0x2A] {
let mut code = vec![arm_mov_imm(0, 0)];
code.push(arm_swi(swi_num));
code.push(ARM_IDLE);
let mut gba = boot_with_embedded_bios(&code);
run_until_idle(&mut gba, 500_000);
let pc = gba.cpu_reg(15);
assert!(
pc >= 0x0800_0000,
"SWI 0x{swi_num:02X} stub should return to ROM"
);
}
}
#[test]
fn bios_boot_writes_undocumented_0x04000410() {
let code = &[ARM_IDLE];
let mut gba = boot_with_embedded_bios(code);
run_until_idle(&mut gba, 100_000);
let val = gba.bus_mut().read8(0x04000410);
assert_eq!(
val, 0xFF,
"BIOS should write 0xFF to undocumented 0x04000410"
);
}
#[test]
fn bios_boot_ramps_soundbias_to_0x200() {
let code = &[ARM_IDLE];
let mut gba = boot_with_embedded_bios(code);
run_until_idle(&mut gba, 100_000);
let soundbias = gba.bus_mut().read16(0x04000088);
assert_eq!(
soundbias & 0x3FF,
0x200,
"SOUNDBIAS should be 0x200 after boot"
);
}
#[test]
fn bios_boot_with_invalid_header_locks_up() {
let mut rom = make_test_rom(&[ARM_IDLE]);
rom[COMPLEMENT_CHECK_OFFSET] = rom[COMPLEMENT_CHECK_OFFSET].wrapping_add(1);
let mut config = Config::default();
let tmp = std::env::temp_dir().join("neser_bios_test_nonexistent");
config.gba.bios_path = Some(tmp.to_string_lossy().into_owned());
let mut gba = Gba::new(AppContext::new_with_config(config));
gba.load_rom(&rom, "bad-header.gba")
.expect("ROM should still load");
let mut cycles = 0u64;
while cycles < 50_000 {
let tick = gba.run_tick_for_tests() as u64;
if tick == 0 {
break;
}
cycles += tick;
}
assert!(
gba.cpu_pc() < 0x0800_0000,
"BIOS should lock up with invalid header, but PC reached {:#010X}",
gba.cpu_pc()
);
}
#[test]
fn bios_boot_with_invalid_fixed_byte_locks_up() {
let mut rom = make_test_rom(&[ARM_IDLE]);
rom[FIXED_BYTE_OFFSET] = 0x00; rom[COMPLEMENT_CHECK_OFFSET] = compute_complement_check(&rom);
let mut config = Config::default();
let tmp = std::env::temp_dir().join("neser_bios_test_nonexistent");
config.gba.bios_path = Some(tmp.to_string_lossy().into_owned());
let mut gba = Gba::new(AppContext::new_with_config(config));
gba.load_rom(&rom, "bad-fixed-byte.gba")
.expect("ROM should still load");
let mut cycles = 0u64;
while cycles < 50_000 {
let tick = gba.run_tick_for_tests() as u64;
if tick == 0 {
break;
}
cycles += tick;
}
assert!(
gba.cpu_pc() < 0x0800_0000,
"BIOS should lock up with invalid fixed byte, but PC reached {:#010X}",
gba.cpu_pc()
);
}
#[test]
fn bios_warm_boot_redirects_to_debug_vector() {
let code = &[ARM_IDLE];
let mut config = Config::default();
let tmp = std::env::temp_dir().join("neser_bios_test_nonexistent");
config.gba.bios_path = Some(tmp.to_string_lossy().into_owned());
let mut gba = Gba::new(AppContext::new_with_config(config));
let rom = make_test_rom(code);
gba.load_rom(&rom, "warm-boot.gba")
.expect("ROM should load");
gba.bus_mut().write8(0x04000300, 1);
let mut cycles = 0u64;
while cycles < 1_000 {
let pc = gba.cpu_pc();
if pc >= 0x0800_0000 {
break;
}
let tick = gba.run_tick_for_tests() as u64;
if tick == 0 {
break;
}
cycles += tick;
}
let pc = gba.cpu_pc();
assert!(
pc < 0x0800_0000,
"Warm boot should redirect to debug vector, not cartridge. PC={pc:#010X}"
);
}
fn boot_with_full_intro(arm_code: &[u32]) -> Option<Gba> {
let mut config = Config::default();
let tmp = std::env::temp_dir().join("neser_bios_test_nonexistent");
config.gba.bios_path = Some(tmp.to_string_lossy().into_owned());
let mut gba = Gba::new(AppContext::new_with_config(config));
let rom = make_test_rom(arm_code);
gba.load_rom(&rom, "bios-full-boot.gba")
.expect("test ROM should load");
let max_cycles: u64 = 84_000_000;
let mut cycles = 0u64;
while cycles < max_cycles {
let pc = gba.cpu_pc();
if pc >= 0x0800_0000 {
return Some(gba);
}
let tick = gba.run_tick_for_tests() as u64;
if tick == 0 {
return None;
}
cycles += tick;
}
None
}
#[test]
fn bios_full_boot_reaches_cartridge() {
let gba = boot_with_full_intro(&[ARM_IDLE]);
assert!(
gba.is_some(),
"Full boot with intro should eventually reach cartridge"
);
}
#[test]
fn bios_full_boot_enables_apu() {
let mut gba = boot_with_full_intro(&[ARM_IDLE]).expect("Full boot should reach cartridge");
run_until_idle(&mut gba, 100_000);
let soundcnt_x = gba.bus_mut().read16(0x04000084);
assert_eq!(
soundcnt_x & 0x80,
0x80,
"APU should be enabled after full boot (SOUNDCNT_X bit 7)"
);
}
#[test]
fn bios_full_boot_sets_sound_routing() {
let mut gba = boot_with_full_intro(&[ARM_IDLE]).expect("Full boot should reach cartridge");
run_until_idle(&mut gba, 100_000);
let soundcnt_l = gba.bus_mut().read16(0x04000080);
assert_ne!(
soundcnt_l, 0,
"SOUNDCNT_L should have routing configured after boot jingle"
);
}
#[test]
fn bios_skip_flag_bypasses_intro() {
let mut config = Config::default();
let tmp = std::env::temp_dir().join("neser_bios_test_nonexistent");
config.gba.bios_path = Some(tmp.to_string_lossy().into_owned());
let mut gba = Gba::new(AppContext::new_with_config(config));
let rom = make_test_rom(&[ARM_IDLE]);
gba.load_rom(&rom, "skip-test.gba")
.expect("ROM should load");
gba.bus_mut().write8(0x03007FFC, 1);
let mut cycles = 0u64;
while cycles < 10_000 {
let pc = gba.cpu_pc();
if pc >= 0x0800_0000 {
break;
}
let tick = gba.run_tick_for_tests() as u64;
if tick == 0 {
break;
}
cycles += tick;
}
assert!(
gba.cpu_pc() >= 0x0800_0000,
"Skip flag should bypass intro. PC={:#010X}",
gba.cpu_pc()
);
}
#[test]
fn bios_logo_vram_no_byte_write_bleed() {
let mut config = Config::default();
let tmp = std::env::temp_dir().join("neser_bios_test_nonexistent");
config.gba.bios_path = Some(tmp.to_string_lossy().into_owned());
let mut gba = Gba::new(AppContext::new_with_config(config));
let rom = make_test_rom(&[ARM_IDLE]);
gba.load_rom(&rom, "vram-bleed-test.gba")
.expect("ROM should load");
let target_cycles: u64 = 16_780_000;
let mut cycles = 0u64;
while cycles < target_cycles {
let tick = gba.run_tick_for_tests() as u64;
if tick == 0 {
break;
}
cycles += tick;
}
assert_eq!(
gba.bus().debug_read_vram(16630),
1,
"Pixel at span start (col 70) should be palette entry 1"
);
assert_eq!(
gba.bus().debug_read_vram(16632),
1,
"Pixel at span end (col 72) should be palette entry 1"
);
assert_eq!(
gba.bus().debug_read_vram(16633),
0,
"Pixel after span end (col 73) should be 0 — STRB bleed bug if not"
);
}
#[test]
fn bios_full_boot_produces_visible_framebuffer() {
let mut config = Config::default();
let tmp = std::env::temp_dir().join("neser_bios_test_nonexistent");
config.gba.bios_path = Some(tmp.to_string_lossy().into_owned());
let mut gba = Gba::new(AppContext::new_with_config(config));
let rom = make_test_rom(&[ARM_IDLE]);
gba.load_rom(&rom, "display-test.gba")
.expect("ROM should load");
let target_cycles: u64 = 33_000_000;
let mut cycles = 0u64;
let mut any_non_black_frame = false;
while cycles < target_cycles {
let tick = gba.run_tick_for_tests() as u64;
if tick == 0 {
break;
}
cycles += tick;
if gba.is_ready_to_render() {
let fb = gba.screen_snapshot();
let has_non_black = fb
.chunks_exact(3)
.any(|px| px[0] != 0 || px[1] != 0 || px[2] != 0);
if has_non_black {
any_non_black_frame = true;
break;
}
gba.clear_ready_to_render();
}
}
assert!(
any_non_black_frame,
"Boot intro should produce visible (non-black) pixels in the framebuffer"
);
}
#[test]
fn bios_full_boot_produces_audio_samples() {
let mut config = Config::default();
let tmp = std::env::temp_dir().join("neser_bios_test_nonexistent");
config.gba.bios_path = Some(tmp.to_string_lossy().into_owned());
let mut gba = Gba::new(AppContext::new_with_config(config));
let rom = make_test_rom(&[ARM_IDLE]);
gba.load_rom(&rom, "audio-test.gba")
.expect("ROM should load");
let target_cycles: u64 = 50_000_000;
let mut cycles = 0u64;
let mut any_non_zero_sample = false;
while cycles < target_cycles {
let tick = gba.run_tick_for_tests() as u64;
if tick == 0 {
break;
}
cycles += tick;
if gba.sample_ready()
&& let Some(sample) = gba.get_sample()
&& sample.abs() > 0.001
{
any_non_zero_sample = true;
break;
}
}
assert!(
any_non_zero_sample,
"Boot jingle should produce non-zero audio samples"
);
}
}