Move the emulator into crates/gba-core, the original demo programs into crates/gba-demos, and the minifb frontend into crates/desktop. The binary is now gameboy-rust. Imports change from gba_rust to gba_core/gba_demos; no behavior changes. gba-core has no dependencies. Its integration tests use gba-demos as a development dependency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
327 lines
12 KiB
Rust
327 lines
12 KiB
Rust
use super::*;
|
|
use crate::memory::{BIOS_SIZE, ROM_START};
|
|
|
|
fn program(instruction: u32) -> Memory {
|
|
Memory::new(instruction.to_le_bytes().to_vec()).unwrap()
|
|
}
|
|
|
|
fn vectors(instruction: u32) -> Memory {
|
|
let bios = instruction.to_le_bytes().repeat(BIOS_SIZE / 4);
|
|
Memory::with_bios(vec![], bios).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn exception_entry_sets_vector_link_masks_and_saved_status() {
|
|
for (exception, mode, vector, arm_offset, thumb_offset) in [
|
|
(Exception::UndefinedInstruction, Mode::Undefined, 4, 4, 2),
|
|
(Exception::SoftwareInterrupt, Mode::Supervisor, 8, 4, 2),
|
|
(Exception::PrefetchAbort, Mode::Abort, 12, 4, 4),
|
|
(Exception::DataAbort, Mode::Abort, 16, 8, 8),
|
|
(Exception::Irq, Mode::Irq, 24, 4, 4),
|
|
(Exception::Fiq, Mode::Fiq, 28, 4, 4),
|
|
] {
|
|
for thumb in [false, true] {
|
|
for fiq_mask in [0, 0x40] {
|
|
let mut cpu = Cpu::new(ROM_START);
|
|
let status = 0xb000_001f | if thumb { 0x20 } else { 0 } | fiq_mask;
|
|
cpu.apply_status(status, Mode::System);
|
|
cpu.registers[13] = 0x0300_0100;
|
|
cpu.registers[14] = 123;
|
|
cpu.enter_exception(exception);
|
|
assert_eq!(cpu.mode(), mode);
|
|
assert_eq!(cpu.pc(), vector);
|
|
assert_eq!(
|
|
cpu.registers[14],
|
|
ROM_START + if thumb { thumb_offset } else { arm_offset }
|
|
);
|
|
assert_eq!(cpu.registers[13], 0);
|
|
assert_eq!(cpu.spsr(), Some(status));
|
|
assert_eq!(
|
|
cpu.cpsr(),
|
|
0xb000_0080
|
|
| mode as u32
|
|
| if exception == Exception::Fiq {
|
|
0x40
|
|
} else {
|
|
fiq_mask
|
|
}
|
|
);
|
|
cpu.switch_mode(Mode::System);
|
|
assert_eq!(cpu.registers[13..15], [0x0300_0100, 123]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn arm_and_thumb_swi_execute_and_movs_pc_restores_caller() {
|
|
for thumb in [false, true] {
|
|
for comment in [0, 1, 255] {
|
|
let rom = if thumb {
|
|
(0xdf00_u16 | comment).to_le_bytes().to_vec()
|
|
} else {
|
|
(0xefab_0000_u32 | u32::from(comment))
|
|
.to_le_bytes()
|
|
.to_vec()
|
|
};
|
|
let mut memory =
|
|
Memory::with_bios(rom, 0xe1b0_f00e_u32.to_le_bytes().repeat(BIOS_SIZE / 4))
|
|
.unwrap();
|
|
let mut cpu = Cpu::new(ROM_START);
|
|
let status = 0x6000_001f | if thumb { 0x20 } else { 0 };
|
|
cpu.apply_status(status, Mode::System);
|
|
cpu.registers[14] = 0x1234;
|
|
cpu.step(&mut memory).unwrap();
|
|
assert_eq!(cpu.pc(), 8);
|
|
assert_eq!(cpu.mode(), Mode::Supervisor);
|
|
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
|
|
// MOVS pc, lr restores the saved flags, not ALU-result flags.
|
|
cpu.step(&mut memory).unwrap();
|
|
assert_eq!(cpu.pc(), ROM_START + if thumb { 2 } else { 4 });
|
|
assert_eq!(cpu.cpsr(), status);
|
|
assert_eq!(cpu.registers[14], 0x1234);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn conditional_swi_can_be_skipped_without_entering_supervisor() {
|
|
let mut cpu = Cpu::new(ROM_START);
|
|
let mut expected = cpu.clone();
|
|
expected.registers[15] += 4;
|
|
cpu.step(&mut program(0x0fff_ffff)).unwrap();
|
|
assert_eq!(cpu, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn swi_without_vector_image_enters_then_reports_fetch_error() {
|
|
let mut cpu = Cpu::new(ROM_START);
|
|
let mut memory = program(0xef00_0000);
|
|
cpu.step(&mut memory).unwrap();
|
|
let before = cpu.clone();
|
|
assert_eq!(
|
|
cpu.step(&mut memory),
|
|
Err(CpuError::Memory(MemoryError::Unmapped(8)))
|
|
);
|
|
assert_eq!(cpu, before);
|
|
}
|
|
|
|
#[test]
|
|
fn interrupt_masks_and_fiq_priority_are_respected() {
|
|
for irq_mask in [false, true] {
|
|
for fiq_mask in [false, true] {
|
|
for irq in [false, true] {
|
|
for fiq in [false, true] {
|
|
let mut cpu = Cpu::new(ROM_START);
|
|
cpu.irq_disabled = irq_mask;
|
|
cpu.fiq_disabled = fiq_mask;
|
|
let before = cpu.clone();
|
|
let expected = if fiq && !fiq_mask {
|
|
Some(Mode::Fiq)
|
|
} else if irq && !irq_mask {
|
|
Some(Mode::Irq)
|
|
} else {
|
|
None
|
|
};
|
|
assert_eq!(cpu.take_interrupt(irq, fiq), expected.is_some());
|
|
if let Some(mode) = expected {
|
|
assert_eq!(cpu.mode(), mode);
|
|
assert_eq!(cpu.spsr(), Some(before.cpsr()));
|
|
} else {
|
|
assert_eq!(cpu, before);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn subs_exception_returns_resume_or_retry_the_correct_instruction() {
|
|
for (exception, subtract) in [
|
|
(Exception::Irq, 4),
|
|
(Exception::Fiq, 4),
|
|
(Exception::PrefetchAbort, 4),
|
|
(Exception::DataAbort, 8),
|
|
] {
|
|
for thumb in [false, true] {
|
|
let mut cpu = Cpu::new(ROM_START + 2 * u32::from(thumb));
|
|
let status = 0xa000_001f | if thumb { 0x20 } else { 0 };
|
|
cpu.apply_status(status, Mode::System);
|
|
let before = cpu.clone();
|
|
cpu.enter_exception(exception);
|
|
cpu.step(&mut vectors(0xe25e_f000 | subtract)).unwrap(); // SUBS pc, lr, #offset
|
|
assert_eq!(cpu.registers, before.registers);
|
|
assert_eq!(cpu.cpsr(), before.cpsr());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn nested_fiq_preserves_irq_saved_status_and_banked_registers() {
|
|
let mut cpu = Cpu::new(ROM_START);
|
|
cpu.registers[8] = 88;
|
|
cpu.enter_exception(Exception::Irq);
|
|
cpu.registers[13] = 0x0300_0200;
|
|
let irq_status = cpu.cpsr();
|
|
let irq_spsr = cpu.spsr();
|
|
let irq_link = cpu.registers[14];
|
|
assert!(cpu.take_interrupt(false, true));
|
|
cpu.registers[8] = 888;
|
|
cpu.step(&mut vectors(0xe25e_f004)).unwrap();
|
|
assert_eq!(cpu.pc(), 0x18);
|
|
assert_eq!(cpu.cpsr(), irq_status);
|
|
assert_eq!(cpu.spsr(), irq_spsr);
|
|
assert_eq!(cpu.registers[13..15], [0x0300_0200, irq_link]);
|
|
assert_eq!(cpu.registers[8], 88);
|
|
cpu.step(&mut vectors(0xe25e_f004)).unwrap();
|
|
assert_eq!(cpu.pc(), ROM_START);
|
|
assert_eq!(cpu.cpsr(), 0x1f);
|
|
cpu.switch_mode(Mode::Fiq);
|
|
assert_eq!(cpu.registers[8], 888);
|
|
}
|
|
|
|
#[test]
|
|
fn same_mode_exception_overwrites_link_and_saved_status() {
|
|
let mut cpu = Cpu::new(ROM_START);
|
|
cpu.enter_exception(Exception::SoftwareInterrupt);
|
|
cpu.registers[15] = 0x100;
|
|
let previous = cpu.cpsr();
|
|
cpu.enter_exception(Exception::SoftwareInterrupt);
|
|
assert_eq!(cpu.spsr(), Some(previous));
|
|
assert_eq!(cpu.registers[14], 0x104);
|
|
}
|
|
|
|
#[test]
|
|
fn return_alignment_uses_saved_state_not_target_bit_zero() {
|
|
for (status, expected) in [(0x1f, ROM_START), (0x3f, ROM_START + 2)] {
|
|
let mut cpu = Cpu::at_reset();
|
|
cpu.registers[15] = ROM_START;
|
|
cpu.registers[14] = ROM_START + 3;
|
|
cpu.set_spsr(status);
|
|
cpu.step(&mut program(0xe1b0_f00e)).unwrap();
|
|
assert_eq!(cpu.pc(), expected);
|
|
assert_eq!(cpu.cpsr(), status);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_saved_mode_rejects_return_without_partial_changes() {
|
|
for instruction in [0xe1b0_f00e, 0xe8fd_8001] {
|
|
let mut cpu = Cpu::at_reset();
|
|
cpu.registers[15] = ROM_START;
|
|
cpu.registers[13] = 0x0200_0000;
|
|
cpu.set_spsr(0); // An invalid mode
|
|
let before = cpu.clone();
|
|
assert!(cpu.step(&mut program(instruction)).is_err());
|
|
assert_eq!(cpu, before);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn ldm_exception_return_loads_outgoing_bank_before_restoring_status() {
|
|
let mut cpu = Cpu::new(ROM_START);
|
|
cpu.registers[13] = 0x0300_0100;
|
|
cpu.registers[14] = 123;
|
|
cpu.enter_exception(Exception::SoftwareInterrupt);
|
|
cpu.registers[15] = ROM_START;
|
|
cpu.registers[13] = 0x0200_0000;
|
|
cpu.set_spsr(0x9000_003f);
|
|
let mut memory = program(0xe8fd_c001); // LDMIA sp!, {r0, lr, pc}^
|
|
for (index, value) in [42, 456, ROM_START + 7].into_iter().enumerate() {
|
|
memory
|
|
.write32(0x0200_0000 + index as u32 * 4, value)
|
|
.unwrap();
|
|
}
|
|
cpu.step(&mut memory).unwrap();
|
|
assert_eq!(cpu.cpsr(), 0x9000_003f);
|
|
assert_eq!(cpu.pc(), ROM_START + 6);
|
|
assert_eq!(cpu.registers[0], 42);
|
|
assert_eq!(cpu.registers[13..15], [0x0300_0100, 123]);
|
|
cpu.switch_mode(Mode::Supervisor);
|
|
assert_eq!(cpu.registers[13..15], [0x0200_000c, 456]);
|
|
}
|
|
|
|
#[test]
|
|
fn failed_ldm_return_preserves_every_bank_and_status() {
|
|
let mut cpu = Cpu::at_reset();
|
|
cpu.registers[15] = ROM_START;
|
|
cpu.registers[13] = 0x0400_0054;
|
|
cpu.set_spsr(0x3f);
|
|
let mut memory = program(0xe8fd_8001);
|
|
memory.write32(0x0400_0054, 42).unwrap();
|
|
let before = cpu.clone();
|
|
assert!(matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))));
|
|
assert_eq!(cpu, before);
|
|
}
|
|
|
|
#[test]
|
|
fn user_bank_transfers_use_current_base_and_user_values() {
|
|
for mode in [Mode::System, Mode::Supervisor, Mode::Fiq] {
|
|
let mut cpu = Cpu::new(ROM_START);
|
|
for register in 0..15 {
|
|
cpu.registers[register] = 100 + register as u32;
|
|
}
|
|
cpu.switch_mode(mode);
|
|
cpu.registers[13] = 0x0200_0100;
|
|
let original = cpu.clone();
|
|
let mut memory = program(0xe8cd_7fff); // STMIA sp, {r0-r14}^
|
|
cpu.step(&mut memory).unwrap();
|
|
for register in 0..15 {
|
|
assert_eq!(
|
|
memory.read32(0x0200_0100 + register as u32 * 4).unwrap(),
|
|
original.user_register(register)
|
|
);
|
|
memory
|
|
.write32(0x0200_0100 + register as u32 * 4, 200 + register as u32)
|
|
.unwrap();
|
|
}
|
|
// Use RAM for the next instruction without changing the transfer base.
|
|
memory.write32(0x0300_0000, 0xe8dd_7fff).unwrap(); // LDMIA sp, {r0-r14}^
|
|
cpu.registers[15] = 0x0300_0000;
|
|
cpu.step(&mut memory).unwrap();
|
|
assert_eq!(cpu.cpsr(), original.cpsr());
|
|
if mode != Mode::System {
|
|
assert_eq!(cpu.registers[13..15], original.registers[13..15]);
|
|
}
|
|
if mode == Mode::Fiq {
|
|
assert_eq!(cpu.registers[8..13], original.registers[8..13]);
|
|
}
|
|
cpu.switch_mode(Mode::System);
|
|
for register in 0..15 {
|
|
assert_eq!(cpu.registers[register], 200 + register as u32);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn user_bank_store_pc_uses_pipeline_offset() {
|
|
let mut cpu = Cpu::at_reset();
|
|
cpu.registers[15] = ROM_START;
|
|
cpu.registers[0] = 0x0200_0000;
|
|
let mut memory = program(0xe8c0_8000); // STMIA r0, {pc}^
|
|
cpu.step(&mut memory).unwrap();
|
|
assert_eq!(memory.read32(0x0200_0000).unwrap(), ROM_START + 12);
|
|
}
|
|
|
|
#[test]
|
|
fn user_mode_cannot_request_user_bank_transfers_or_status_return() {
|
|
for instruction in [0xe8c0_0002, 0xe8d0_0002, 0xe8d0_8000, 0xe1b0_f00e] {
|
|
let mut cpu = Cpu::new(ROM_START);
|
|
cpu.switch_mode(Mode::User);
|
|
cpu.registers[0] = 0x0200_0000;
|
|
let before = cpu.clone();
|
|
assert!(cpu.step(&mut program(instruction)).is_err());
|
|
assert_eq!(cpu, before);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn failed_user_bank_load_preserves_hidden_registers() {
|
|
let mut cpu = Cpu::at_reset();
|
|
cpu.registers[15] = ROM_START;
|
|
cpu.registers[0] = 0x0400_0054;
|
|
let before = cpu.clone();
|
|
assert!(cpu.step(&mut program(0xe8d0_6000)).is_err()); // LDMIA r0, {sp, lr}^
|
|
assert_eq!(cpu, before);
|
|
}
|