gameboy-rust/crates/gba-core/src/cpu/exception_tests.rs
0xrsydn f8a0676748 refactor: split into gameboy-rust workspace with core, demos, and desktop crates
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>
2026-10-05 16:09:46 +07:00

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);
}