Separate Running, Halt, and Stop states. Complete the entering instruction before freezing CPU, DMA, timers, display, and externally supplied clock advances. Wake STOP through the live enabled keypad condition without creating a clock-off IF request. Implement original ARM SWI 0x03, preserve caller state, and expose zero-cycle StopIdle plus stopped frame-runner results. Retain DMA state, timer phase, and captured frames across sleep. Document keypad-only wake and unverified oscillator/edge timing. Add core, BIOS, and demo regression tests; preserve BIOS-only access and failed-store isolation. Validated on Darwin arm64 with debug/release workspace and core/demo suites, Clippy, rustdoc, ten native window smoke tests, and terminal demos.
673 lines
23 KiB
Rust
673 lines
23 KiB
Rust
use gba_core::{
|
|
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/affine.rs"]
|
|
mod affine;
|
|
#[path = "bios/angles.rs"]
|
|
mod angles;
|
|
#[path = "bios/arithmetic.rs"]
|
|
mod arithmetic;
|
|
#[path = "bios/bit_unpack.rs"]
|
|
mod bit_unpack;
|
|
#[path = "bios/differential.rs"]
|
|
mod differential;
|
|
#[path = "bios/huffman.rs"]
|
|
mod huffman;
|
|
#[path = "bios/lz77.rs"]
|
|
mod lz77;
|
|
#[path = "bios/ram_reset.rs"]
|
|
mod ram_reset;
|
|
#[path = "bios/reset.rs"]
|
|
mod reset;
|
|
#[path = "bios/run_length.rs"]
|
|
mod run_length;
|
|
#[path = "bios/stop.rs"]
|
|
mod stop;
|
|
|
|
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 [0x0d, 0x19, 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))
|
|
);
|
|
}
|