gameboy-rust/tests/bios.rs
0xrsydn 0280563430 feat(bios): expand packed units with BitUnPack
Add SWI 0x10, packed-unit widths, conditional offsets, complete word stores, and independent bit-level tests.
2026-10-05 12:58:39 +07:00

659 lines
23 KiB
Rust

use gba_rust::{
bios::{self, IRQ_FLAGS, IRQ_HANDLER, SYSTEM_STACK, UNSUPPORTED_TRAP},
cpu::{CpuError, InstructionSet, Mode},
display::VBLANK_START,
io::{DISPSTAT, IE, IF, IME, POSTFLG, TIMER_BASE},
machine::{Machine, MachineError, StepKind},
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START, VRAM_START},
};
const SOURCE: u32 = 0x0200_0000;
const DEST: u32 = 0x0201_0000;
const CALLBACK: u32 = 0x0300_0100;
const ROM_DATA: u32 = ROM_START + 13 * 4;
#[path = "bios/arithmetic.rs"]
mod arithmetic;
#[path = "bios/bit_unpack.rs"]
mod bit_unpack;
#[path = "bios/lz77.rs"]
mod lz77;
#[path = "bios/run_length.rs"]
mod run_length;
fn words(code: &[u32]) -> Vec<u8> {
code.iter().flat_map(|word| word.to_le_bytes()).collect()
}
fn reach(machine: &mut Machine, pc: u32, max_steps: usize) {
for _ in 0..max_steps {
if machine.cpu().pc() == pc {
return;
}
machine.step().unwrap();
}
panic!(
"did not reach {pc:#x}; pc={:#x}, cpsr={:#x}",
machine.cpu().pc(),
machine.cpu().cpsr()
);
}
/// Boot, load operands and status through CPU instructions, then stop just before SWI.
fn call(service: u8, thumb: bool, operands: [u32; 3]) -> (Machine, u32) {
call_status(service, thumb, operands, 0x1f)
}
fn call_status(service: u8, thumb: bool, operands: [u32; 3], status: u32) -> (Machine, u32) {
call_status_data(service, thumb, operands, status, &[])
}
fn call_status_data(
service: u8,
thumb: bool,
operands: [u32; 3],
status: u32,
data: &[u8],
) -> (Machine, u32) {
let mut code = [
0xe59f_0020, // LDR r0,[pc,#32] (offset 40)
0xe59f_1020,
0xe59f_2020,
0xe3a0_30a3, // MOV r3,#0xa3
0xe3a0_c05c, // MOV r12,#0x5c
0xe328_f480, // MSR CPSR_f,#0x80000000
if thumb { 0xe28f_e001 } else { 0xe1a0_e00f }, // ADD lr,pc,#1 / MOV lr,pc
if thumb { 0xe12f_ff1e } else { 0xe1a0_0000 }, // BX lr / NOP
if thumb {
0xe7fe_0000 | 0xdf00 | u32::from(service)
} else {
0xef00_0000 | u32::from(service) << 16
},
0xeaff_fffe,
operands[0],
operands[1],
operands[2],
];
if status != 0x1f {
code[3] = 0xe321_f000 | status;
} // MSR CPSR_c,#status
let mut rom = words(&code);
assert_eq!(rom.len() as u32, ROM_DATA - ROM_START);
rom.extend_from_slice(data);
let mut machine = bios::boot(rom).unwrap();
reach(&mut machine, ROM_START + 32, 100);
assert_eq!(
machine.cpu().instruction_set(),
if thumb {
InstructionSet::Thumb
} else {
InstructionSet::Arm
}
);
(machine, ROM_START + if thumb { 34 } else { 36 })
}
fn finish(machine: &mut Machine, return_pc: u32) {
machine.step().unwrap(); // Execute SWI, not a host service call.
reach(machine, return_pc, 100_000);
}
/// Original ARM callback: acknowledge IF, record BIOS flags, then return to dispatcher.
fn callback(bus: &mut Memory) {
let code = [
0xe280_0c02, // ADD r0,r0,#0x200 (r0=I/O base from dispatcher)
0xe1d0_10b2, // LDRH r1,[r0,#2]
0xe1c0_10b2, // STRH r1,[r0,#2]
0xe3a0_0403, // MOV r0,#0x03000000
0xe280_0c7f, // ADD r0,r0,#0x7f00
0xe1d0_2fb8, // LDRH r2,[r0,#0xf8]
0xe182_2001, // ORR r2,r2,r1
0xe1c0_2fb8, // STRH r2,[r0,#0xf8]
0xe12f_ff1e, // BX lr
];
for (index, instruction) in code.into_iter().enumerate() {
bus.write32(CALLBACK + index as u32 * 4, instruction)
.unwrap();
}
bus.write32(IRQ_HANDLER, CALLBACK).unwrap();
}
#[test]
fn original_image_is_deterministic_and_boot_enters_arm_system_mode() {
assert_eq!(bios::image().len(), BIOS_SIZE);
assert_eq!(bios::image(), bios::image());
let mut machine = bios::boot(words(&[0xeaff_fffe])).unwrap();
assert_eq!(machine.cpu().mode(), Mode::Supervisor);
assert_eq!(machine.cpu().pc(), 0);
reach(&mut machine, ROM_START, 100);
assert_eq!(machine.cpu().mode(), Mode::System);
assert_eq!(machine.cpu().instruction_set(), InstructionSet::Arm);
assert_eq!(machine.cpu().cpsr(), 0x1f);
assert_eq!(machine.cpu().registers()[13], SYSTEM_STACK);
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 1);
assert_eq!(machine.memory().read32(IRQ_FLAGS).unwrap(), 0);
assert_eq!(machine.memory().read32(IRQ_HANDLER).unwrap(), 0);
assert!(machine.cycles() > 0);
}
#[test]
fn cpu_set_copies_both_widths_from_arm_and_thumb_and_preserves_caller() {
for thumb in [false, true] {
for word in [false, true] {
let (mut machine, return_pc) = call(
0x0b,
thumb,
[SOURCE, DEST, 4 | if word { 1 << 26 } else { 0 }],
);
for index in 0..4 {
machine
.memory_mut()
.write32(SOURCE + index * 4, 0x1234_5678 + index)
.unwrap();
}
let before = machine.cpu().clone();
finish(&mut machine, return_pc);
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
for offset in 0..if word { 16 } else { 8 } {
assert_eq!(
machine.memory().read8(DEST + offset).unwrap(),
machine.memory().read8(SOURCE + offset).unwrap()
);
}
}
}
}
#[test]
fn intr_wait_consumes_preexisting_selected_flags_without_sleep() {
for thumb in [false, true] {
let (mut machine, return_pc) = call(4, thumb, [0, 0x10, 0x1234]);
machine.memory_mut().write16(IRQ_FLAGS, 0x19).unwrap();
let before = machine.cpu().clone();
machine.step().unwrap();
for _ in 0..100 {
if machine.cpu().pc() == return_pc {
break;
}
assert_ne!(machine.step().unwrap(), StepKind::HaltIdle);
}
assert_eq!(machine.cpu().pc(), return_pc);
assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 9);
assert_eq!(machine.memory().read16(IME).unwrap(), 1);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
}
}
#[test]
fn vblank_wait_halts_dispatches_irq_and_returns_through_original_code() {
let (mut machine, return_pc) = call(5, false, [0x123, 0x456, 0x789]);
callback(machine.memory_mut());
machine.memory_mut().write16(DISPSTAT, 8).unwrap();
machine.memory_mut().write16(IE, 1).unwrap();
let before = machine.cpu().clone();
machine.step().unwrap();
let mut slept = false;
let mut irq = 0;
for _ in 0..1000 {
if machine.cpu().pc() == return_pc {
break;
}
match machine.step().unwrap() {
StepKind::HaltIdle => slept = true,
StepKind::IrqEntry => irq += 1,
_ => {}
}
}
assert!(slept);
assert_eq!(irq, 1);
assert_eq!(machine.cpu().pc(), return_pc);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 0);
assert_eq!(machine.memory().read16(IME).unwrap(), 1);
assert!(machine.cycles() > u64::from(VBLANK_START));
}
#[test]
fn unsupported_services_fail_at_an_explicit_instruction_trap() {
for service in [0, 1, 3, 9, 0x0a, 0x13, 0xff] {
let (mut machine, _) = call(service, false, [0, 0, 0]);
let error = (0..100)
.find_map(|_| machine.step().err())
.expect("unsupported SWI must fail");
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: UNSUPPORTED_TRAP,
..
})
));
}
}
#[test]
fn cpu_set_fill_reads_one_value_and_writes_exact_count() {
for thumb in [false, true] {
for word in [false, true] {
let control = 3 | (1 << 24) | if word { 1 << 26 } else { 0 };
let (mut machine, return_pc) = call(0x0b, thumb, [SOURCE, DEST, control]);
machine.memory_mut().write32(SOURCE, 0xaabb_ccdd).unwrap();
for offset in 0..5 {
machine
.memory_mut()
.write32(DEST + offset * 4, 0x9999_9999)
.unwrap();
}
finish(&mut machine, return_pc);
let bytes = if word { 12 } else { 6 };
for offset in 0..bytes {
assert_eq!(
machine.memory().read8(DEST + offset).unwrap(),
[0xdd, 0xcc, 0xbb, 0xaa][(offset % if word { 4 } else { 2 }) as usize]
);
}
assert_eq!(machine.memory().read16(DEST + bytes).unwrap(), 0x9999);
}
}
}
#[test]
fn cpu_fast_set_rounds_up_to_eight_words_for_copy_and_fill() {
for thumb in [false, true] {
for fill in [false, true] {
for count in [1_u32, 7, 8, 9, 15, 16, 17] {
let (mut machine, return_pc) = call(
0x0c,
thumb,
[SOURCE, DEST, count | if fill { 1 << 24 } else { 0 }],
);
let rounded = (count + 7) & !7;
for index in 0..rounded + 1 {
machine
.memory_mut()
.write32(SOURCE + index * 4, 0x1000 + index)
.unwrap();
machine
.memory_mut()
.write32(DEST + index * 4, 0xcccc_cccc)
.unwrap();
}
let before = machine.cpu().clone();
finish(&mut machine, return_pc);
for index in 0..rounded {
assert_eq!(
machine.memory().read32(DEST + index * 4).unwrap(),
0x1000 + if fill { 0 } else { index }
);
}
assert_eq!(
machine.memory().read32(DEST + rounded * 4).unwrap(),
0xcccc_cccc
);
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
}
}
}
}
#[test]
fn fast_copy_reads_each_eight_word_block_before_writing_it() {
let (mut machine, return_pc) = call(0x0c, false, [SOURCE, SOURCE + 4, 16]);
let mut expected: Vec<u32> = (0..17).map(|index| 0x1234 + index).collect();
for (index, value) in expected.iter().enumerate() {
machine
.memory_mut()
.write32(SOURCE + index as u32 * 4, *value)
.unwrap();
}
for start in [0, 8] {
let block = expected[start..start + 8].to_vec();
expected[start + 1..start + 9].copy_from_slice(&block);
}
finish(&mut machine, return_pc);
for (index, value) in expected.into_iter().enumerate() {
assert_eq!(
machine.memory().read32(SOURCE + index as u32 * 4).unwrap(),
value
);
}
}
#[test]
fn swi_callers_outside_user_and_system_modes_are_rejected() {
for mode in [0x11, 0x12, 0x13, 0x17, 0x1b] {
let (mut machine, _) = call_status(0x0b, false, [SOURCE, DEST, 0], mode);
let error = (0..100).find_map(|_| machine.step().err()).unwrap();
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: UNSUPPORTED_TRAP,
..
})
));
}
}
#[test]
fn zero_length_copies_do_not_read_source_or_write_destination() {
for service in [0x0b, 0x0c] {
for control in [0, 1 << 24, 1 << 26, (1 << 24) | (1 << 26), 0xfae0_0000] {
let (mut machine, return_pc) =
call(service, false, [0x0e00_0000, 0x0e00_0000, control]);
finish(&mut machine, return_pc); // Both addresses would fail if accessed.
}
}
}
#[test]
fn copy_count_ignores_reserved_bits_and_fast_set_always_uses_words() {
for service in [0x0b, 0x0c] {
let (mut machine, return_pc) = call(service, false, [SOURCE, DEST, 1 | 0xfae0_0000]);
machine.memory_mut().write32(SOURCE, 0x1234_abcd).unwrap();
finish(&mut machine, return_pc);
assert_eq!(
machine.memory().read32(DEST).unwrap(),
if service == 0x0b { 0xabcd } else { 0x1234_abcd }
);
}
}
#[test]
fn copying_to_video_ram_uses_halfword_and_word_bus_writes() {
for service in [0x0b, 0x0c] {
let (mut machine, return_pc) = call(service, false, [SOURCE, VRAM_START, 3]);
machine.memory_mut().write32(SOURCE, 0x03e0_001f).unwrap();
machine
.memory_mut()
.write32(SOURCE + 4, 0x7fff_7c00)
.unwrap();
finish(&mut machine, return_pc);
assert_eq!(machine.memory().read32(VRAM_START).unwrap(), 0x03e0_001f);
assert_eq!(machine.memory().read16(VRAM_START + 4).unwrap(), 0x7c00);
assert_eq!(
machine.memory().read16(VRAM_START + 6).unwrap(),
if service == 0x0b { 0 } else { 0x7fff }
);
}
}
#[test]
fn copy_addresses_align_down_and_overlaps_use_instruction_order() {
for word in [false, true] {
let (mut machine, return_pc) = call(
0x0b,
false,
[SOURCE + 3, DEST + 3, 1 | if word { 1 << 26 } else { 0 }],
);
machine.memory_mut().write32(SOURCE, 0x1234_abcd).unwrap();
finish(&mut machine, return_pc);
assert_eq!(
machine.memory().read32(DEST).unwrap(),
if word { 0x1234_abcd } else { 0x1234_0000 }
);
}
let (mut machine, return_pc) = call(0x0b, false, [SOURCE, SOURCE + 2, 4]);
machine.memory_mut().write32(SOURCE, 0xabcd_1234).unwrap();
finish(&mut machine, return_pc);
for offset in [0, 2, 4, 6, 8] {
assert_eq!(machine.memory().read16(SOURCE + offset).unwrap(), 0x1234);
}
}
#[test]
fn protected_or_wrapping_source_ranges_return_without_copying() {
for service in [0x0b, 0x0c] {
for source in [0, 0x3ffc, 0x0100_0000, 0x01ff_fffc, 0xffff_fffc] {
let (mut machine, return_pc) = call(service, false, [source, DEST, 2 | (1 << 26)]);
machine.memory_mut().write32(DEST, 0x1234_5678).unwrap();
finish(&mut machine, return_pc);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x1234_5678);
}
}
}
#[test]
fn copy_failure_keeps_earlier_writes_but_not_partial_instruction_changes() {
let (mut machine, _) = call(0x0b, false, [SOURCE, 0x07ff_fffc, 2 | (1 << 26)]);
machine.memory_mut().write32(SOURCE, 0xaabb_ccdd).unwrap();
let mut failed = false;
for _ in 0..100 {
let before = machine.cpu().clone();
let timing = machine.last_timing();
let cycles = machine.cycles();
if let Err(error) = machine.step() {
assert_eq!(
error,
MachineError::Cpu(MemoryError::ReadOnly(0x0800_0000).into())
);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.last_timing(), timing);
assert_eq!(machine.memory().read32(0x07ff_fffc).unwrap(), 0xaabb_ccdd);
failed = true;
break;
}
}
assert!(failed);
}
#[test]
fn halt_preserves_registers_and_ime_for_arm_and_thumb_callers() {
for thumb in [false, true] {
let (mut machine, return_pc) = call(2, thumb, [0x1234, 0x5678, 0x9abc]);
machine.memory_mut().write16(IE, 8).unwrap();
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_fc18)
.unwrap(); // IRQ after 1,000 cycles.
let before = machine.cpu().clone();
machine.step().unwrap();
let mut slept = false;
for _ in 0..200 {
if machine.cpu().pc() == return_pc {
break;
}
slept |= machine.step().unwrap() == StepKind::HaltIdle;
}
assert!(slept);
assert_eq!(machine.cpu().pc(), return_pc);
assert_eq!(machine.memory().read16(IME).unwrap(), 0);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
}
}
#[test]
fn halt_with_pending_irq_returns_without_an_idle_interval() {
let (mut machine, return_pc) = call(2, false, [1, 2, 3]);
request_timer(machine.memory_mut());
machine.memory_mut().write16(IE, 8).unwrap();
machine.step().unwrap();
for _ in 0..100 {
if machine.cpu().pc() == return_pc {
break;
}
assert_ne!(machine.step().unwrap(), StepKind::HaltIdle);
}
assert_eq!(machine.cpu().pc(), return_pc);
}
fn request_timer(bus: &mut Memory) {
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
bus.advance_cycles(1);
bus.write16(TIMER_BASE + 2, 0).unwrap();
}
#[test]
fn interrupt_wait_has_no_lost_wake_between_flag_check_and_halt() {
for injection_step in 0..100 {
let (mut machine, return_pc) = call(4, false, [0, 8, 0]);
callback(machine.memory_mut());
machine.memory_mut().write16(IE, 8).unwrap();
for step in 0..500 {
if step == injection_step {
request_timer(machine.memory_mut());
}
if machine.cpu().pc() == return_pc {
break;
}
machine.step().unwrap();
}
assert_eq!(
machine.cpu().pc(),
return_pc,
"injection step {injection_step}"
);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 0);
}
}
#[test]
fn discard_old_flags_waits_for_a_new_callback_and_retains_other_flags() {
for thumb in [false, true] {
let (mut machine, return_pc) = call(4, thumb, [1, 8, 0]);
callback(machine.memory_mut());
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().write16(IRQ_FLAGS, 9).unwrap();
for _ in 0..100 {
if machine.halted() {
break;
}
machine.step().unwrap();
}
assert!(machine.halted());
assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 1);
request_timer(machine.memory_mut());
reach(&mut machine, return_pc, 200);
assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 1);
}
}
#[test]
fn unrelated_irq_does_not_complete_a_wait_for_vblank() {
let (mut machine, return_pc) = call(4, false, [1, 1, 0]);
callback(machine.memory_mut());
machine.memory_mut().write16(IE, 9).unwrap();
machine.memory_mut().write16(DISPSTAT, 8).unwrap();
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_fc18)
.unwrap();
let mut irqs = 0;
for _ in 0..1000 {
if machine.cpu().pc() == return_pc {
break;
}
if machine.step().unwrap() == StepKind::IrqEntry {
irqs += 1;
machine.memory_mut().write16(TIMER_BASE + 2, 0).unwrap();
}
}
assert_eq!(machine.cpu().pc(), return_pc);
assert_eq!(irqs, 2);
assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 8);
assert!(machine.cycles() > u64::from(VBLANK_START));
}
#[test]
fn wait_services_restore_user_system_and_irq_masked_caller_status() {
for thumb in [false, true] {
for status in [0x10, 0x90, 0x9f, 0xdf] {
let (mut machine, return_pc) = call_status(4, thumb, [0, 8, 0], status);
callback(machine.memory_mut());
machine.memory_mut().write16(IE, 8).unwrap();
let before = machine.cpu().clone();
for _ in 0..100 {
if machine.halted() {
break;
}
machine.step().unwrap();
}
assert!(machine.halted());
request_timer(machine.memory_mut());
reach(&mut machine, return_pc, 200);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(machine.memory().read16(IME).unwrap(), 1);
}
}
}
#[test]
fn missing_or_misaligned_irq_callback_is_an_explicit_trap() {
for pointer in [0, CALLBACK + 1, CALLBACK + 2] {
let (mut machine, _) = call(5, false, [0, 0, 0]);
machine.memory_mut().write32(IRQ_HANDLER, pointer).unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().write16(IME, 1).unwrap();
request_timer(machine.memory_mut());
let error = (0..100).find_map(|_| machine.step().err()).unwrap();
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: UNSUPPORTED_TRAP,
..
})
));
}
}
#[test]
fn repeated_calls_restore_the_supervisor_stack() {
let mut machine = bios::boot(words(&[0xe3a0_2000, 0xef0b_0000, 0xeaff_fffd])).unwrap();
reach(&mut machine, ROM_START + 4, 100);
for _ in 0..100 {
machine.step().unwrap(); // SWI
assert_eq!(machine.cpu().registers()[13], bios::SVC_STACK);
reach(&mut machine, ROM_START + 8, 100);
assert_eq!(machine.cpu().registers()[13], SYSTEM_STACK);
machine.step().unwrap(); // Branch back to SWI.
assert_eq!(machine.cpu().pc(), ROM_START + 4);
}
}
#[test]
fn arm_swi_uses_upper_comment_byte_not_low_byte() {
let mut machine = bios::boot(words(&[0xe3a0_2000, 0xef0b_ff03, 0xeaff_fffe])).unwrap();
reach(&mut machine, ROM_START + 4, 100);
finish(&mut machine, ROM_START + 8); // CpuSet(count=0), not Stop(3).
}
#[test]
fn callback_must_record_bios_flags_not_only_acknowledge_hardware_if() {
let (mut machine, return_pc) = call(4, false, [0, 8, 0]);
callback(machine.memory_mut());
machine
.memory_mut()
.write32(CALLBACK + 12, 0xe12f_ff1e)
.unwrap(); // Return after IF acknowledgement, omit flags.
machine.memory_mut().write16(IE, 8).unwrap();
request_timer(machine.memory_mut());
for _ in 0..300 {
machine.step().unwrap();
}
assert_ne!(machine.cpu().pc(), return_pc);
assert!(machine.halted());
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 0);
}
#[test]
fn supplying_an_image_is_optional_and_does_not_change_memory_new() {
assert_eq!(
Memory::new(vec![]).unwrap().read32(0),
Err(MemoryError::Unmapped(0))
);
let mut bus = Memory::with_bios(vec![], bios::image()).unwrap();
assert_eq!(bus.write32(0, 1), Err(MemoryError::ReadOnly(0)));
assert_eq!(
bus.read8(BIOS_SIZE as u32),
Err(MemoryError::Unmapped(BIOS_SIZE as u32))
);
}