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>
This commit is contained in:
parent
a8944d1d5f
commit
f8a0676748
113 changed files with 304 additions and 282 deletions
327
crates/gba-core/src/cpu/exception_tests.rs
Normal file
327
crates/gba-core/src/cpu/exception_tests.rs
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue