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
363
crates/gba-core/tests/bios/reset.rs
Normal file
363
crates/gba-core/tests/bios/reset.rs
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
use super::*;
|
||||
use gba_core::{
|
||||
dma::DMA_BASE,
|
||||
io::{BG0CNT, DISPCNT, WAITCNT},
|
||||
memory::{OAM_START, PALETTE_START},
|
||||
};
|
||||
|
||||
const FLAG: u32 = 0x0300_7ffa;
|
||||
const CLEAR_START: u32 = 0x0300_7e00;
|
||||
const CLEAR_END: u32 = 0x0300_8000;
|
||||
|
||||
fn restart(m: &mut Machine, flag: u8) {
|
||||
m.memory_mut().write8(FLAG, flag).unwrap();
|
||||
m.step().unwrap(); // Enter the real SWI vector.
|
||||
reach(m, if flag == 0 { ROM_START } else { SOURCE }, 2000);
|
||||
}
|
||||
|
||||
fn assert_restart_state(m: &Machine, target: u32) {
|
||||
assert_eq!(m.cpu().pc(), target);
|
||||
assert_eq!(m.cpu().mode(), Mode::System);
|
||||
assert_eq!(m.cpu().instruction_set(), InstructionSet::Arm);
|
||||
assert_eq!(m.cpu().cpsr(), 0x9f); // Deterministic policy: IRQ masked, FIQ clear, NZCV clear.
|
||||
assert_eq!(&m.cpu().registers()[..13], &[0; 13]);
|
||||
assert_eq!(m.cpu().registers()[13], SYSTEM_STACK);
|
||||
assert_eq!(m.cpu().registers()[14], target);
|
||||
}
|
||||
|
||||
fn assert_cleared(m: &Machine) {
|
||||
for address in CLEAR_START..CLEAR_END {
|
||||
assert_eq!(m.memory().read8(address).unwrap(), 0, "{address:#x}");
|
||||
}
|
||||
}
|
||||
|
||||
// Inline literal with a branch over its data. No host register mutation.
|
||||
fn literal(code: &mut Vec<u32>, register: u32, value: u32) {
|
||||
code.extend([0xe59f_0000 | register << 12, 0xea00_0000, value]);
|
||||
}
|
||||
|
||||
fn dirty_sp(mode: u32) -> u32 {
|
||||
0x0300_7000 + mode * 32
|
||||
}
|
||||
|
||||
fn dirty_lr(mode: u32) -> u32 {
|
||||
0xa100_0000 | mode
|
||||
}
|
||||
|
||||
fn seeded_call(thumb: bool, status: u32, location: Option<u32>) -> Machine {
|
||||
let mut code = Vec::new();
|
||||
for mode in [0x11, 0x12, 0x13, 0x17, 0x1b] {
|
||||
code.push(0xe321_f0c0 | mode); // Switch bank with IRQ/FIQ masked.
|
||||
literal(&mut code, 13, dirty_sp(mode));
|
||||
literal(&mut code, 14, dirty_lr(mode));
|
||||
literal(&mut code, 0, 0xb000_00df);
|
||||
code.push(0xe169_f000); // MSR SPSR_fc,r0
|
||||
if mode == 0x11 {
|
||||
for r in 8..=12 {
|
||||
code.push(0xe3a0_00e0 | r << 12 | r); // Distinct FIQ high registers.
|
||||
}
|
||||
}
|
||||
}
|
||||
code.push(0xe321_f0df);
|
||||
literal(&mut code, 13, 0x0300_7dc0);
|
||||
for r in 0..=12 {
|
||||
code.push(0xe3a0_0080 | r << 12 | r);
|
||||
}
|
||||
code.extend([0xe328_f20f, 0xe321_f000 | status]); // NZCV set; caller mode/masks.
|
||||
let swi_pc = if let Some(address) = location {
|
||||
literal(&mut code, 14, address | u32::from(thumb));
|
||||
code.push(0xe12f_ff1e);
|
||||
address
|
||||
} else {
|
||||
code.extend([
|
||||
if thumb { 0xe28f_e001 } else { 0xe1a0_e00f },
|
||||
if thumb { 0xe12f_ff1e } else { 0xe1a0_0000 },
|
||||
]);
|
||||
let address = ROM_START + code.len() as u32 * 4;
|
||||
code.extend([if thumb { 0xe7fe_df00 } else { 0xef00_0000 }, 0xeaff_fffe]);
|
||||
address
|
||||
};
|
||||
let mut m = bios::boot(words(&code)).unwrap();
|
||||
if let Some(address) = location {
|
||||
m.memory_mut()
|
||||
.write32(address, if thumb { 0xe7fe_df00 } else { 0xef00_0000 })
|
||||
.unwrap();
|
||||
m.memory_mut().write32(address + 4, 0xeaff_fffe).unwrap();
|
||||
}
|
||||
reach(&mut m, swi_pc, 500);
|
||||
m
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_flag_byte_selects_rom_or_ram_from_arm_and_thumb() {
|
||||
for thumb in [false, true] {
|
||||
for flag in 0..=255 {
|
||||
let (mut m, _) = call(0, thumb, [0x123, 0x456, 0x789]);
|
||||
// Only the byte at 7ffa selects the target, not surrounding bytes.
|
||||
m.memory_mut().write32(IRQ_FLAGS, 0xffff_ffff).unwrap();
|
||||
restart(&mut m, flag);
|
||||
assert_restart_state(&m, if flag == 0 { ROM_START } else { SOURCE });
|
||||
assert_cleared(&m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clears_exactly_top_512_bytes_and_keeps_other_memory() {
|
||||
for thumb in [false, true] {
|
||||
let (mut m, _) = call(0, thumb, [0, 0, 0]);
|
||||
for address in (0x0300_0000..CLEAR_END).step_by(4) {
|
||||
m.memory_mut()
|
||||
.write32(address, address ^ 0xa55a_c33c)
|
||||
.unwrap();
|
||||
}
|
||||
let sentinels = [
|
||||
SOURCE,
|
||||
SOURCE + 0x3fffc,
|
||||
PALETTE_START,
|
||||
PALETTE_START + 0x3fc,
|
||||
VRAM_START,
|
||||
VRAM_START + 0x17ffc,
|
||||
OAM_START,
|
||||
OAM_START + 0x3fc,
|
||||
];
|
||||
for address in sentinels {
|
||||
m.memory_mut().write32(address, 0x1357_2468).unwrap();
|
||||
}
|
||||
let rom_first = m.memory().read32(ROM_START).unwrap();
|
||||
restart(&mut m, 0);
|
||||
assert_cleared(&m);
|
||||
for address in (0x0300_0000..CLEAR_START).step_by(4) {
|
||||
assert_eq!(m.memory().read32(address).unwrap(), address ^ 0xa55a_c33c);
|
||||
}
|
||||
for address in sentinels {
|
||||
assert_eq!(m.memory().read32(address).unwrap(), 0x1357_2468);
|
||||
}
|
||||
assert_eq!(m.memory().read32(ROM_START).unwrap(), rom_first);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resets_nonzero_registers_and_status_for_user_system_callers() {
|
||||
for thumb in [false, true] {
|
||||
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
|
||||
for flag in [0, 1] {
|
||||
let mut m = seeded_call(thumb, status, None);
|
||||
assert!(m.cpu().registers()[..13].iter().all(|r| *r != 0));
|
||||
assert_eq!(m.cpu().cpsr() & 0xf000_0000, 0xf000_0000);
|
||||
restart(&mut m, flag);
|
||||
assert_restart_state(&m, if flag == 0 { ROM_START } else { SOURCE });
|
||||
assert_cleared(&m);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reinitializes_supervisor_irq_stacks_links_and_saved_status() {
|
||||
for thumb in [false, true] {
|
||||
let mut m = seeded_call(thumb, 0x1f, None);
|
||||
for (i, mode) in [0xd3, 0xd2, 0xdf].into_iter().enumerate() {
|
||||
m.memory_mut()
|
||||
.write32(SOURCE + i as u32 * 4, 0xe321_f000 | mode)
|
||||
.unwrap();
|
||||
}
|
||||
restart(&mut m, 1);
|
||||
for (mode, stack) in [
|
||||
(Mode::Supervisor, bios::SVC_STACK),
|
||||
(Mode::Irq, bios::IRQ_STACK),
|
||||
] {
|
||||
m.step().unwrap();
|
||||
assert_eq!(m.cpu().mode(), mode);
|
||||
assert_eq!(m.cpu().registers()[13], stack);
|
||||
assert_eq!(m.cpu().registers()[14], 0);
|
||||
assert_eq!(m.cpu().spsr(), Some(0));
|
||||
}
|
||||
m.step().unwrap();
|
||||
assert_eq!(m.cpu().mode(), Mode::System);
|
||||
assert_eq!(m.cpu().registers()[13], SYSTEM_STACK);
|
||||
assert_eq!(m.cpu().registers()[14], SOURCE);
|
||||
assert_cleared(&m);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_banked_registers_and_saved_status_are_preserved() {
|
||||
let mut m = seeded_call(false, 0x1f, None);
|
||||
for (i, mode) in [0x11, 0x17, 0x1b].into_iter().enumerate() {
|
||||
m.memory_mut()
|
||||
.write32(SOURCE + i as u32 * 4, 0xe321_f0c0 | mode)
|
||||
.unwrap();
|
||||
}
|
||||
restart(&mut m, 1);
|
||||
for mode in [Mode::Fiq, Mode::Abort, Mode::Undefined] {
|
||||
m.step().unwrap();
|
||||
assert_eq!(m.cpu().mode(), mode);
|
||||
assert_eq!(m.cpu().registers()[13], dirty_sp(mode as u32));
|
||||
assert_eq!(m.cpu().registers()[14], dirty_lr(mode as u32));
|
||||
assert_eq!(m.cpu().spsr(), Some(0xb000_00df));
|
||||
if mode == Mode::Fiq {
|
||||
assert_eq!(&m.cpu().registers()[8..13], &[0xe8, 0xe9, 0xea, 0xeb, 0xec]);
|
||||
} else {
|
||||
assert_eq!(&m.cpu().registers()[8..13], &[0; 5]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_restart_from_work_ram_including_code_in_erased_area() {
|
||||
for thumb in [false, true] {
|
||||
for location in [SOURCE + 0x100, 0x0300_1000, CLEAR_START, 0x03ff_fe00] {
|
||||
let mut m = seeded_call(thumb, 0x1f, Some(location));
|
||||
restart(&mut m, 1);
|
||||
assert_restart_state(&m, SOURCE);
|
||||
assert_cleared(&m);
|
||||
if location < CLEAR_START {
|
||||
assert_eq!(
|
||||
m.memory().read32(location).unwrap(),
|
||||
if thumb { 0xe7fe_df00 } else { 0xef00_0000 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ram_restart_executes_arm_code_and_can_call_bios_again() {
|
||||
for thumb in [false, true] {
|
||||
let mut m = seeded_call(thumb, 0x10, None);
|
||||
// This program is only valid as ARM: Sqrt(81), then a self-branch.
|
||||
for (i, instruction) in [0xe3a0_0051, 0xef08_0000, 0xeaff_fffe]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
m.memory_mut()
|
||||
.write32(SOURCE + i as u32 * 4, instruction)
|
||||
.unwrap();
|
||||
}
|
||||
restart(&mut m, 1);
|
||||
reach(&mut m, SOURCE + 8, 1000);
|
||||
assert_eq!(m.cpu().registers()[0], 9);
|
||||
assert_eq!(m.cpu().registers()[13], SYSTEM_STACK);
|
||||
assert_eq!(m.cpu().registers()[14], SOURCE);
|
||||
assert_eq!(m.cpu().cpsr(), 0x9f);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_reset_io_or_device_clocks() {
|
||||
for thumb in [false, true] {
|
||||
let (mut m, _) = call(0, thumb, [0, 0, 0]);
|
||||
for (address, value) in [
|
||||
(DISPCNT, 0x0403),
|
||||
(BG0CNT, 0x0141),
|
||||
(WAITCNT, 0x4317),
|
||||
(DISPSTAT, 0x7b38),
|
||||
(IE, 0x1234),
|
||||
(IME, 1),
|
||||
] {
|
||||
m.memory_mut().write16(address, value).unwrap();
|
||||
}
|
||||
m.memory_mut().write32(TIMER_BASE, 0x0080_0000).unwrap();
|
||||
let timer_before = m.memory().read16(TIMER_BASE).unwrap();
|
||||
let cycles = m.cycles();
|
||||
restart(&mut m, 0);
|
||||
for (address, value) in [
|
||||
(DISPCNT, 0x0403),
|
||||
(BG0CNT, 0x0141),
|
||||
(WAITCNT, 0x4317),
|
||||
(IE, 0x1234),
|
||||
(IME, 1),
|
||||
(POSTFLG, 1),
|
||||
(TIMER_BASE + 2, 0x80),
|
||||
] {
|
||||
assert_eq!(m.memory().read16(address).unwrap(), value, "{address:#x}");
|
||||
}
|
||||
assert_eq!(m.memory().read16(DISPSTAT).unwrap() & 0xff38, 0x7b38);
|
||||
let elapsed = m.cycles() - cycles;
|
||||
assert!(elapsed > 512 && elapsed < 65536);
|
||||
assert_eq!(
|
||||
m.memory().read16(TIMER_BASE).unwrap(),
|
||||
timer_before.wrapping_add(elapsed as u16)
|
||||
);
|
||||
assert!(!m.halted());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dma_timers_and_pending_irq_continue_but_irq_stays_masked_after_restart() {
|
||||
for thumb in [false, true] {
|
||||
for ime in [0, 1] {
|
||||
let (mut m, _) = call(0, thumb, [0, 0, 0]);
|
||||
m.memory_mut().write8(FLAG, 1).unwrap();
|
||||
m.memory_mut().write32(SOURCE, 0xeaff_fffe).unwrap();
|
||||
m.step().unwrap();
|
||||
// Queue IRQ and DMA only after SWI entry; otherwise IRQ can win first.
|
||||
m.memory_mut().write16(IE, 8).unwrap();
|
||||
m.memory_mut().write16(IME, ime).unwrap();
|
||||
m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap();
|
||||
m.memory_mut().write32(DEST, 0x8765_4321).unwrap();
|
||||
m.memory_mut().write32(DMA_BASE, DEST).unwrap();
|
||||
m.memory_mut().write32(DMA_BASE + 4, DEST + 4).unwrap();
|
||||
m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap();
|
||||
let mut dma = 0;
|
||||
for _ in 0..2000 {
|
||||
if m.cpu().pc() == SOURCE {
|
||||
break;
|
||||
}
|
||||
let step = m.step().unwrap();
|
||||
assert_ne!(step, StepKind::IrqEntry);
|
||||
if matches!(step, StepKind::Dma { channel: 0 }) {
|
||||
dma += 1;
|
||||
}
|
||||
}
|
||||
assert_restart_state(&m, SOURCE);
|
||||
assert_eq!(dma, 1);
|
||||
assert_eq!(m.memory().read32(DEST + 4).unwrap(), 0x8765_4321);
|
||||
assert_eq!(m.memory().read16(IF).unwrap() & 8, 8);
|
||||
assert_eq!(m.memory().read16(IME).unwrap(), ime);
|
||||
assert_cleared(&m);
|
||||
assert_eq!(m.step().unwrap(), StepKind::Instruction);
|
||||
assert_eq!(m.cpu().pc(), SOURCE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_is_not_an_atomic_host_memory_clear() {
|
||||
let (mut m, _) = call(0, false, [0, 0, 0]);
|
||||
for address in (CLEAR_START..CLEAR_END).step_by(4) {
|
||||
m.memory_mut().write32(address, 0xa55a_c33c).unwrap();
|
||||
}
|
||||
m.memory_mut().write8(FLAG, 0).unwrap();
|
||||
m.step().unwrap();
|
||||
for _ in 0..1000 {
|
||||
if m.memory().read32(CLEAR_START).unwrap() == 0 {
|
||||
break;
|
||||
}
|
||||
m.step().unwrap();
|
||||
}
|
||||
assert_eq!(m.memory().read32(CLEAR_START).unwrap(), 0);
|
||||
assert_eq!(m.memory().read32(CLEAR_START + 4).unwrap(), 0xa55a_c33c);
|
||||
assert_eq!(m.memory().read32(IRQ_HANDLER).unwrap(), 0xa55a_c33c);
|
||||
assert!(m.cpu().pc() < BIOS_SIZE as u32);
|
||||
reach(&mut m, ROM_START, 2000);
|
||||
assert_cleared(&m);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exception_mode_callers_still_receive_a_diagnostic() {
|
||||
for status in [0x12, 0x13] {
|
||||
let (mut m, _) = call_status(0, false, [0, 0, 0], status);
|
||||
m.memory_mut().write32(CLEAR_START, 0x1357_2468).unwrap();
|
||||
let error = (0..100).find_map(|_| m.step().err()).unwrap();
|
||||
assert!(matches!(
|
||||
error,
|
||||
MachineError::Cpu(CpuError::UnsupportedInstruction {
|
||||
instruction: UNSUPPORTED_TRAP,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert_eq!(m.memory().read32(CLEAR_START).unwrap(), 0x1357_2468);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue