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:
0xrsydn 2026-10-05 16:07:32 +07:00
commit f8a0676748
113 changed files with 304 additions and 282 deletions

View file

@ -0,0 +1,9 @@
[package]
name = "gba-core"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dev-dependencies]
# Integration tests exercise the core through the original demo programs.
gba-demos = { path = "../gba-demos" }

422
crates/gba-core/src/bios.rs Normal file
View file

@ -0,0 +1,422 @@
//! Original, optional ARM BIOS replacement. No Nintendo firmware is included.
//! Supports SoftReset, selective RegisterRamReset, interrupt waits, arithmetic, affine matrices,
//! memory copy/fill, bit unpacking, LZ77/run-length/Huffman decoding, and differential filters.
//! This is a functional subset, not a complete boot ROM or a timing-compatible BIOS.
use std::collections::BTreeMap;
mod affine;
mod angles;
mod arithmetic;
mod bit_unpack;
mod decompression;
mod differential;
mod huffman;
mod lz77;
mod reset;
mod run_length;
use crate::{
cpu::Cpu,
io::{HALTCNT, IME, POSTFLG},
machine::Machine,
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START},
};
pub const SYSTEM_STACK: u32 = 0x0300_7f00;
pub const IRQ_STACK: u32 = 0x0300_7fa0;
pub const SVC_STACK: u32 = 0x0300_7fe0;
pub const IRQ_FLAGS: u32 = 0x0300_7ff8;
pub const IRQ_HANDLER: u32 = 0x0300_7ffc;
/// An intentional undefined instruction used for unsupported SWIs/vectors and invalid IRQ pointers.
pub const UNSUPPORTED_TRAP: u32 = 0xe7f0_00f0;
/// A diagnostic trap for invalid service arguments, including unsupported reset flags.
pub const INVALID_ARGUMENT_TRAP: u32 = 0xe7f0_00f1;
/// Start at reset with our optional firmware. Call Machine::step to execute boot.
/// Boot initializes three stacks, POSTFLG, and the IRQ communication words, then
/// enters ROM_START in ARM System mode. It does not validate a cartridge header,
/// reproduce Nintendo's boot process, or provide file loading.
pub fn boot(rom: Vec<u8>) -> Result<Machine, MemoryError> {
Ok(Machine::new(
Cpu::at_reset(),
Memory::with_bios(rom, image())?,
))
}
/// Build a deterministic 16 KiB image from original ARM instructions and a
/// mathematically generated sine table. No Nintendo firmware bytes are included.
/// SWIs use bits 16–23 of the ARM immediate, or the Thumb immediate byte.
/// Services support User/System callers. Returning services preserve caller status
/// and registers other than each arithmetic service's documented outputs.
/// SoftReset instead clears BIOS work RAM, resets specified CPU banks, and enters
/// ROM or RAM in ARM System mode with IRQ masked. It does not reset devices.
/// RegisterRamReset clears selected RAM and supported I/O registers. Serial/sound
/// reset flags reach a diagnostic before display or reset writes.
/// IRQ callbacks must preserve r4–r11, acknowledge IF, update IRQ_FLAGS for wait
/// services, and return with BX lr. Nested IRQs and SWIs from callbacks are unsupported.
/// Service routines use the Supervisor stack; exact firmware stack layout and
/// undocumented BIOS side effects are not reproduced.
pub fn image() -> Vec<u8> {
let mut a = ArmImage::default();
for vector in [
"reset",
"unsupported",
"swi",
"unsupported",
"unsupported",
"unsupported",
"irq",
"unsupported",
] {
a.branch(14, vector);
}
a.label("reset");
a.emit(0xe321_f0d3); // MSR CPSR_c,#SVC|I|F
a.literal(13, SVC_STACK);
a.emit(0xe321_f0d2); // MSR CPSR_c,#IRQ|I|F
a.literal(13, IRQ_STACK);
a.emit(0xe321_f0df); // MSR CPSR_c,#System|I|F
a.literal(13, SYSTEM_STACK);
a.emit(0xe3a0_0000); // MOV r0,#0
a.literal(1, IRQ_FLAGS);
a.emit(0xe581_0000); // STR r0,[r1] (flags and reserved upper halfword)
a.emit(0xe581_0004); // STR r0,[r1,#4] (callback pointer)
a.literal(1, IME);
a.emit(0xe581_0000); // STR r0,[r1] (disable IRQ delivery during minimal boot)
a.literal(1, POSTFLG);
a.emit(0xe3a0_0001); // MOV r0,#1
a.emit(0xe5c1_0000); // STRB r0,[r1]
a.emit(0xe321_f01f); // MSR CPSR_c,#System
a.literal(15, ROM_START); // LDR pc,=ROM_START
a.label("irq");
a.emit(0xe92d_500f); // STMDB sp!,{r0-r3,r12,lr}
a.literal(1, IRQ_HANDLER);
a.emit(0xe591_1000); // LDR r1,[r1]
a.emit(0xe351_0000); // CMP r1,#0
a.branch(0, "unsupported");
a.emit(0xe311_0003); // TST r1,#3 (hardware callback is word-aligned ARM)
a.branch(1, "unsupported");
a.emit(0xe3a0_0301); // MOV r0,#0x04000000 (callback convention)
a.emit(0xe1a0_e00f); // MOV lr,pc (return after BX)
a.emit(0xe12f_ff11); // BX r1
a.emit(0xe8bd_500f); // LDMIA sp!,{r0-r3,r12,lr}
a.emit(0xe25e_f004); // SUBS pc,lr,#4
a.label("swi");
a.emit(0xe92d_500f); // STMDB sp!,{r0-r3,r12,lr}
a.emit(0xe14f_3000); // MRS r3,SPSR
a.emit(0xe92d_0008); // STMDB sp!,{r3} (save caller status across IRQs)
// Keep stack usage bounded within the default 64-byte Supervisor area.
// In particular, reject a nested SWI from an IRQ callback during a wait.
a.emit(0xe203_c01f); // AND r12,r3,#mode mask
a.emit(0xe35c_0010); // CMP r12,#User
a.branch(0, "decode_swi");
a.emit(0xe35c_001f); // CMP r12,#System
a.branch(1, "unsupported");
a.label("decode_swi");
a.emit(0xe313_0020); // TST r3,#Thumb
a.emit(0x115e_c0b2); // LDRHNE r12,[lr,#-2]
a.emit(0x051e_c004); // LDREQ r12,[lr,#-4]
a.emit(0x01a0_c82c); // MOVEQ r12,r12,LSR #16
a.emit(0xe20c_c0ff); // AND r12,r12,#0xff
for (number, target) in [
(0, "soft_reset"),
(1, "register_ram_reset"),
(2, "halt"),
(4, "intr_wait"),
(5, "vblank_wait"),
(6, "div"),
(7, "div_arm"),
(8, "sqrt"),
(9, "arctan"),
(0x0a, "arctan2"),
(0x0b, "cpu_set"),
(0x0c, "cpu_fast_set"),
(0x0e, "bg_affine"),
(0x0f, "obj_affine"),
(0x10, "bit_unpack"),
(0x11, "lz_wram"),
(0x12, "lz_vram"),
(0x13, "huffman"),
(0x14, "rl_wram"),
(0x15, "rl_vram"),
(0x16, "diff8_wram"),
(0x17, "diff8_vram"),
(0x18, "diff16"),
] {
a.emit(0xe35c_0000 | number); // CMP r12,#service
a.branch(0, target);
}
a.branch(14, "unsupported");
a.label("halt");
a.literal(1, HALTCNT);
a.emit(0xe3a0_0000); // MOV r0,#0
a.emit(0xe5c1_0000); // STRB r0,[r1]
a.branch(14, "return");
a.label("vblank_wait");
a.emit(0xe3a0_0001); // MOV r0,#1 (discard old flags)
a.emit(0xe3a0_1001); // MOV r1,#VBlank
a.label("intr_wait");
a.literal(12, IME);
a.emit(0xe3a0_3001); // MOV r3,#1
a.emit(0xe58c_3000); // STR r3,[r12] (IME=1)
a.literal(2, IRQ_FLAGS);
a.emit(0xe350_0000); // CMP r0,#0
a.branch(0, "wait_unmask");
a.emit(0xe1d2_30b0); // LDRH r3,[r2]
a.emit(0xe1c3_3001); // BIC r3,r3,r1
a.emit(0xe1c2_30b0); // STRH r3,[r2]
a.label("wait_unmask");
a.emit(0xe10f_3000); // MRS r3,CPSR
a.emit(0xe3c3_3080); // BIC r3,r3,#I
a.emit(0xe121_f003); // MSR CPSR_c,r3
a.label("wait_loop");
// Mask IME while checking flags and entering HALT. A new hardware request
// still wakes HALT, but cannot be acknowledged between the check and sleep.
a.emit(0xe3a0_3000); // MOV r3,#0
a.emit(0xe58c_3000); // STR r3,[r12] (IME=0)
a.emit(0xe1d2_30b0); // LDRH r3,[r2]
a.emit(0xe113_0001); // TST r3,r1
a.branch(1, "wait_done");
a.literal(0, HALTCNT);
a.emit(0xe3a0_3000); // MOV r3,#0
a.emit(0xe5c0_3000); // STRB r3,[r0] (HALT)
a.emit(0xe3a0_3001); // MOV r3,#1
a.emit(0xe58c_3000); // STR r3,[r12] (IME=1, allow IRQ callback to record flags)
a.branch(14, "wait_loop");
a.label("wait_done");
a.emit(0xe1c3_3001); // BIC r3,r3,r1 (consume selected flags only)
a.emit(0xe1c2_30b0); // STRH r3,[r2]
a.emit(0xe3a0_3001); // MOV r3,#1
a.emit(0xe58c_3000); // STR r3,[r12]
a.branch(14, "return");
a.label("cpu_set");
emit_count(&mut a);
a.emit(0xe312_0301); // TST r2,#0x04000000 (word width)
a.branch(1, "copy_word");
a.branch(14, "copy_halfword");
a.label("cpu_fast_set");
emit_count(&mut a);
a.emit(0xe283_3007); // ADD r3,r3,#7
a.emit(0xe3c3_3007); // BIC r3,r3,#7 (round up to eight words)
emit_copy_checks(&mut a, true);
a.emit(0xe92d_0ff0); // STMDB sp!,{r4-r11} (60 bytes total SVC stack use)
a.emit(0xe312_0401); // TST r2,#0x01000000
a.branch(0, "fast_copy_loop");
a.emit(0xe590_4000); // LDR r4,[r0] (read fill value once)
for register in 5..=11 {
a.emit(0xe1a0_0004 | register << 12);
} // MOV rN,r4
a.label("fast_fill_loop");
a.emit(0xe8a1_0ff0); // STMIA r1!,{r4-r11}
a.emit(0xe253_3008); // SUBS r3,r3,#8
a.branch(1, "fast_fill_loop");
a.branch(14, "fast_done");
a.label("fast_copy_loop");
a.emit(0xe8b0_0ff0); // LDMIA r0!,{r4-r11}
a.emit(0xe8a1_0ff0); // STMIA r1!,{r4-r11}
a.emit(0xe253_3008); // SUBS r3,r3,#8
a.branch(1, "fast_copy_loop");
a.label("fast_done");
a.emit(0xe8bd_0ff0); // LDMIA sp!,{r4-r11}
a.branch(14, "return");
emit_copy(
&mut a,
true,
["copy_word", "word_loop", "word_fill", "word_fill_loop"],
);
emit_copy(
&mut a,
false,
["copy_halfword", "half_loop", "half_fill", "half_fill_loop"],
);
// The preceding routine ends with an unconditional branch. Place a literal
// pool here so reset/dispatcher loads stay within ARM's 4 KiB reach.
a.flush_literals();
reset::emit(&mut a);
// Both reset routines end without fallthrough; keep their literals local.
a.flush_literals();
arithmetic::emit(&mut a);
affine::emit(&mut a);
angles::emit(&mut a);
bit_unpack::emit(&mut a);
lz77::emit(&mut a);
run_length::emit(&mut a);
differential::emit(&mut a);
huffman::emit(&mut a);
decompression::emit_writer(&mut a);
a.label("return");
a.emit(0xe10f_3000); // MRS r3,CPSR
a.emit(0xe383_3080); // ORR r3,r3,#I (protect status/stack restoration)
a.emit(0xe121_f003); // MSR CPSR_c,r3
a.emit(0xe8bd_0008); // LDMIA sp!,{r3}
a.emit(0xe169_f003); // MSR SPSR_fc,r3
a.emit(0xe8bd_500f); // LDMIA sp!,{r0-r3,r12,lr}
a.emit(0xe1b0_f00e); // MOVS pc,lr
a.label("unsupported");
a.emit(UNSUPPORTED_TRAP);
a.label("invalid_argument");
a.emit(INVALID_ARGUMENT_TRAP);
let mut image = a.finish(affine::TABLE_ADDRESS as usize);
affine::write_table(&mut image);
image
}
fn emit_count(a: &mut ArmImage) {
a.emit(0xe1a0_3582); // MOV r3,r2,LSL #11
a.emit(0xe1a0_35a3); // MOV r3,r3,LSR #11 (low 21 bits)
}
fn emit_copy_checks(a: &mut ArmImage, word: bool) {
a.emit(0xe353_0000); // CMP r3,#0
a.branch(0, "return");
a.emit(if word { 0xe3c0_0003 } else { 0xe3c0_0001 }); // BIC r0,r0,#alignment mask
a.emit(if word { 0xe3c1_1003 } else { 0xe3c1_1001 }); // BIC r1,r1,#alignment mask
// Reject protected source ranges and wrapping end addresses before writing.
a.emit(0xe350_0402); // CMP r0,#0x02000000
a.branch(3, "return"); // BLO
a.emit(if word { 0xe090_c103 } else { 0xe090_c083 }); // ADDS r12,r0,r3,LSL #2/#1
a.branch(2, "return"); // BCS (address overflow)
a.emit(0xe35c_0402); // CMP r12,#0x02000000
a.branch(3, "return");
}
fn emit_copy(a: &mut ArmImage, word: bool, labels: [&'static str; 4]) {
let [entry, copy_loop, fill, fill_loop] = labels;
a.label(entry);
emit_copy_checks(a, word);
a.emit(0xe312_0401); // TST r2,#0x01000000 (fixed source)
a.branch(1, fill);
a.label(copy_loop);
a.emit(if word { 0xe490_2004 } else { 0xe0d0_20b2 }); // LDR/LDRH r2,[r0],#width
a.emit(if word { 0xe481_2004 } else { 0xe0c1_20b2 }); // STR/STRH r2,[r1],#width
a.emit(0xe253_3001); // SUBS r3,r3,#1
a.branch(1, copy_loop);
a.branch(14, "return");
a.label(fill);
a.emit(if word { 0xe590_2000 } else { 0xe1d0_20b0 }); // Read fixed source once.
a.label(fill_loop);
a.emit(if word { 0xe481_2004 } else { 0xe0c1_20b2 });
a.emit(0xe253_3001);
a.branch(1, fill_loop);
a.branch(14, "return");
}
/// Small image builder for branch/literal fixups. Not a general ARM assembler.
#[derive(Default)]
struct ArmImage {
code: Vec<u32>,
labels: BTreeMap<&'static str, usize>,
branches: Vec<(usize, u32, &'static str, bool)>,
literals: Vec<(usize, u32, u32)>,
}
impl ArmImage {
fn emit(&mut self, instruction: u32) {
self.code.push(instruction);
}
fn label(&mut self, name: &'static str) {
assert!(
self.labels.insert(name, self.code.len()).is_none(),
"duplicate BIOS label"
);
}
fn branch(&mut self, condition: u32, target: &'static str) {
self.branches
.push((self.code.len(), condition, target, false));
self.emit(0);
}
fn call(&mut self, target: &'static str) {
self.branches.push((self.code.len(), 14, target, true));
self.emit(0);
}
fn literal(&mut self, register: u32, value: u32) {
self.literals.push((self.code.len(), register, value));
self.emit(0);
}
// Call only after an unconditional branch/return/trap, never on a fallthrough path.
fn flush_literals(&mut self) {
for (index, register, value) in std::mem::take(&mut self.literals) {
let offset = (self.code.len() as i32 - index as i32 - 2) * 4;
assert!(offset.unsigned_abs() <= 0xfff);
self.code[index] = (if offset < 0 { 0xe51f_0000 } else { 0xe59f_0000 })
| register << 12
| offset.unsigned_abs();
self.code.push(value);
}
}
fn finish(mut self, code_limit: usize) -> Vec<u8> {
self.flush_literals();
for (index, condition, target, link) in self.branches {
let offset = self.labels[target] as i32 - index as i32 - 2;
assert!((-0x80_0000..0x80_0000).contains(&offset));
self.code[index] = condition << 28
| 0x0a00_0000
| (u32::from(link) << 24)
| (offset as u32 & 0x00ff_ffff);
}
let mut bytes: Vec<u8> = self.code.into_iter().flat_map(u32::to_le_bytes).collect();
assert!(
bytes.len() <= code_limit,
"BIOS code overlaps reserved data"
);
assert!(code_limit <= BIOS_SIZE);
bytes.resize(BIOS_SIZE, 0);
bytes
}
}
#[cfg(test)]
mod image_tests {
use super::*;
#[test]
fn multiple_literal_pools_preserve_pc_offsets_and_branch_fixups() {
let mut a = ArmImage::default();
a.literal(0, 0x1122_3344);
a.branch(14, "after_pool");
a.flush_literals();
a.label("after_pool");
a.literal(1, 0x5566_7788);
a.emit(0xe12f_ff1e); // BX lr
let image = a.finish(BIOS_SIZE);
let actual: Vec<_> = image[..24]
.chunks_exact(4)
.map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap()))
.collect();
assert_eq!(
actual,
[
0xe59f_0000,
0xea00_0000,
0x1122_3344,
0xe59f_1000,
0xe12f_ff1e,
0x5566_7788
]
);
}
#[test]
#[should_panic(expected = "BIOS code overlaps reserved data")]
fn code_cannot_overlap_reserved_tables() {
let mut a = ArmImage::default();
a.emit(0xe1a0_0000);
a.emit(0xe1a0_0000);
a.finish(4);
}
}

View file

@ -0,0 +1,186 @@
//! Original ARM affine-matrix services with a mathematically generated sine table.
//! Scaling uses signed 8.8 values and the upper angle byte. Matrix intermediates
//! narrow to signed halfwords before background-origin calculation.
use super::ArmImage;
pub(super) const TABLE_ADDRESS: u32 = 0x3e00;
const UNIT: i128 = 1i128 << 48;
// floor(pi * 2^48), not data extracted from a firmware image.
const PI: i128 = 884_279_719_003_555;
const fn quarter_sine(index: usize) -> i16 {
if index == 64 {
return 16384;
}
let x = PI * index as i128 / 128;
let mut term = x;
let mut sum = x;
let mut n = 1;
while n <= 10 {
term = -((term * x / UNIT) * x / UNIT) / ((2 * n) * (2 * n + 1));
sum += term;
n += 1;
}
(sum * 16384 / UNIT) as i16
}
const fn sine_table() -> [i16; 256] {
let mut table = [0; 256];
let mut index = 0;
while index < 256 {
let phase = index & 63;
table[index] = match index / 64 {
0 => quarter_sine(phase),
1 => quarter_sine(64 - phase),
2 => -quarter_sine(phase),
_ => -quarter_sine(64 - phase),
};
index += 1;
}
table
}
const SINE: [i16; 256] = sine_table();
pub(super) fn write_table(image: &mut [u8]) {
for (index, value) in SINE.iter().enumerate() {
let address = TABLE_ADDRESS as usize + index * 2;
image[address..address + 2].copy_from_slice(&value.to_le_bytes());
}
}
pub(super) fn emit(a: &mut ArmImage) {
a.label("bg_affine");
entry(a, true);
a.label("bg_affine_record");
a.emit(0xe1d0_40fc); // LDRSH r4,[r0,#12] (SX)
a.emit(0xe1d0_50fe); // LDRSH r5,[r0,#14] (SY)
a.emit(0xe1d0_61b0); // LDRH r6,[r0,#16] (angle)
a.call("affine_matrix");
a.emit(0xe590_8000); // LDR r8,[r0] (texture center X, signed 24.8)
a.emit(0xe590_9004); // LDR r9,[r0,#4] (texture center Y)
a.emit(0xe1d0_a0f8); // LDRSH r10,[r0,#8] (display center X)
a.emit(0xe1d0_b0fa); // LDRSH r11,[r0,#10] (display center Y)
a.emit(0xe00c_0a94); // MUL r12,r4,r10
a.emit(0xe048_800c); // SUB r8,r8,r12
a.emit(0xe00c_0b95); // MUL r12,r5,r11
a.emit(0xe088_800c); // ADD r8,r8,r12 (X = centerX - PA*displayX + SXsin*displayY)
a.emit(0xe00c_0a96); // MUL r12,r6,r10
a.emit(0xe049_900c); // SUB r9,r9,r12
a.emit(0xe00c_0b97); // MUL r12,r7,r11
a.emit(0xe049_900c); // SUB r9,r9,r12 (Y = centerY - PC*displayX - PD*displayY)
a.emit(0xe265_5000); // RSB r5,r5,#0 (PB: negate after signed shift/narrowing)
a.emit(0xe1c1_40b0); // STRH r4,[r1]
a.emit(0xe1c1_50b2); // STRH r5,[r1,#2]
a.emit(0xe1c1_60b4); // STRH r6,[r1,#4]
a.emit(0xe1c1_70b6); // STRH r7,[r1,#6]
a.emit(0xe581_8008); // STR r8,[r1,#8]
a.emit(0xe581_900c); // STR r9,[r1,#12]
a.emit(0xe280_0014); // ADD r0,r0,#20 (skip trailing two-byte source padding)
a.emit(0xe281_1010); // ADD r1,r1,#16
a.emit(0xe252_2001); // SUBS r2,r2,#1
a.branch(1, "bg_affine_record");
a.branch(14, "affine_done");
a.label("obj_affine");
entry(a, false);
a.label("obj_affine_record");
a.emit(0xe1d0_40f0); // LDRSH r4,[r0] (SX)
a.emit(0xe1d0_50f2); // LDRSH r5,[r0,#2] (SY)
a.emit(0xe1d0_60b4); // LDRH r6,[r0,#4] (angle)
a.call("affine_matrix");
a.emit(0xe265_5000); // RSB r5,r5,#0 (PB)
for register in 4..=7 {
a.emit(0xe1c1_00b0 | register << 12); // STRH rN,[r1]
a.emit(0xe081_1003); // ADD r1,r1,r3 (caller-supplied coefficient stride)
}
a.emit(0xe280_0008); // ADD r0,r0,#8 (skip trailing two-byte padding)
a.emit(0xe252_2001); // SUBS r2,r2,#1
a.branch(1, "obj_affine_record");
a.label("affine_done");
a.emit(0xe8bd_0ff0); // LDMIA sp!,{r4-r11}
a.branch(14, "return");
// Leaf kernel preserves argument pointers/count/stride. r4-r7 return PA,
// pre-negation SXsin, PC, PD. r8-r11 are scratch; no additional stack use.
a.label("affine_matrix");
a.emit(0xe1a0_6426); // MOV r6,r6,LSR #8 (discard fractional angle byte)
a.literal(10, TABLE_ADDRESS);
a.emit(0xe08a_b086); // ADD r11,r10,r6,LSL #1
a.emit(0xe1db_80f0); // LDRSH r8,[r11] (sine in signed 2.14)
a.emit(0xe286_6040); // ADD r6,r6,#64
a.emit(0xe206_60ff); // AND r6,r6,#255
a.emit(0xe08a_b086); // ADD r11,r10,r6,LSL #1
a.emit(0xe1db_90f0); // LDRSH r9,[r11] (cosine)
a.emit(0xe006_0895); // MUL r6,r5,r8 (SYsin)
a.emit(0xe007_0995); // MUL r7,r5,r9 (SYcos)
a.emit(0xe005_0894); // MUL r5,r4,r8 (SXsin)
a.emit(0xe008_0994); // MUL r8,r4,r9 (SXcos)
a.emit(0xe1a0_4748); // MOV r4,r8,ASR #14
for register in 5..=7 {
a.emit(0xe1a0_0740 | register << 12 | register); // MOV rN,rN,ASR #14
}
for register in 4..=7 {
a.emit(0xe1a0_0800 | register << 12 | register); // MOV rN,rN,LSL #16
a.emit(0xe1a0_0840 | register << 12 | register); // MOV rN,rN,ASR #16
}
a.emit(0xe12f_ff1e); // BX lr
}
fn entry(a: &mut ArmImage, background: bool) {
a.emit(0xe352_0000); // CMP r2,#0
a.branch(0, "return"); // Zero count does not validate or access buffers.
if !background {
// SWI dispatch uses r3 for caller status. Recover the fourth argument
// from the common frame before adding our local register save.
a.emit(0xe59d_3010); // LDR r3,[sp,#16] (original coefficient stride)
}
a.emit(0xe92d_0ff0); // STMDB sp!,{r4-r11} (60 bytes total SVC stack use)
a.emit(0xe180_c001); // ORR r12,r0,r1
a.emit(if background { 0xe31c_0003 } else { 0xe31c_0001 }); // TST alignment
a.branch(1, "invalid_argument");
a.emit(0xe350_0402); // CMP r0,#0x02000000
a.branch(3, "invalid_argument");
if !background {
a.emit(0xe313_0001); // TST r3,#1 (even coefficient stride)
a.branch(1, "invalid_argument");
a.emit(0xe353_0002); // CMP r3,#2
a.branch(3, "invalid_argument");
a.emit(0xe313_0103); // TST r3,#0xc0000000 (4*stride must not wrap)
a.branch(1, "invalid_argument");
}
a.emit(if background { 0xe3a0_6014 } else { 0xe3a0_6008 }); // MOV r6,#source stride
check_span(a, 0);
a.emit(if background { 0xe3a0_6010 } else { 0xe1a0_6103 }); // MOV r6,#16 / r3,LSL #2
check_span(a, 1);
}
fn check_span(a: &mut ArmImage, pointer: u32) {
a.emit(0xe085_4692); // UMULL r4,r5,r2,r6 (full unsigned count*stride)
a.emit(0xe355_0000); // CMP r5,#0
a.branch(1, "invalid_argument");
a.emit(0xe090_4004 | pointer << 16); // ADDS r4,rPointer,r4
a.branch(2, "invalid_argument");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generated_table_matches_independent_trigonometry_for_every_phase() {
for (phase, actual) in SINE.iter().enumerate() {
let angle = phase as f64 * std::f64::consts::TAU / 256.0;
assert_eq!(
*actual,
(angle.sin() * 16384.0).trunc() as i16,
"phase={phase}"
);
}
assert_eq!(
[SINE[0], SINE[64], SINE[128], SINE[192]],
[0, 16384, 0, -16384]
);
}
}

View file

@ -0,0 +1,112 @@
//! Original ARM implementations of the BIOS fixed-point angle services.
//! Inputs must be sign-extended signed 16-bit values. Only r0 is returned;
//! undocumented scratch-register results and original BIOS timing are not modeled.
use super::ArmImage;
pub(super) fn emit(a: &mut ArmImage) {
a.label("arctan");
validate_input(a, 0);
a.call("atan_polynomial");
a.emit(0xe58d_0004); // STR r0,[sp,#4] (saved caller r0)
a.branch(14, "return");
a.label("arctan2");
validate_input(a, 0);
validate_input(a, 1);
a.emit(0xe92d_0070); // STMDB sp!,{r4-r6} (40 bytes total SVC stack use)
a.emit(0xe351_0000); // CMP r1,#0 (Y)
a.branch(0, "atan2_horizontal_axis");
a.emit(0xe350_0000); // CMP r0,#0 (X)
a.branch(0, "atan2_vertical_axis");
a.emit(0xe1a0_4000); // MOV r4,r0 (original X)
a.emit(0xe1a0_5001); // MOV r5,r1 (original Y)
a.emit(0xe020_6001); // EOR r6,r0,r1 (ratio sign in bit31)
a.emit(0xe350_0000); // CMP r0,#0
a.emit(0x4260_0000); // RSBMI r0,r0,#0 (|X|, including -32768)
a.emit(0xe351_0000); // CMP r1,#0
a.emit(0x4261_1000); // RSBMI r1,r1,#0 (|Y|)
a.emit(0xe150_0001); // CMP r0,r1
a.branch(3, "atan2_vertical_ratio"); // BLO |X| < |Y|
// Horizontal sectors: ArcTan(Y/X) plus 0 or half a turn.
a.emit(0xe1a0_2000); // MOV r2,r0
a.emit(0xe1a0_0701); // MOV r0,r1,LSL #14 (unsigned numerator)
a.emit(0xe1a0_1002); // MOV r1,r2 (unsigned denominator)
a.emit(0xe354_0000); // CMP r4,#0 (original X)
a.emit(0xa3a0_4000); // MOVGE r4,#0
a.emit(0xb3a0_4902); // MOVLT r4,#0x8000
a.emit(0xe3a0_5000); // MOV r5,#0 (add angle to base)
a.branch(14, "atan2_divide");
a.label("atan2_vertical_ratio");
// Vertical sectors: quarter/three-quarter turn minus ArcTan(X/Y).
a.emit(0xe1a0_0700); // MOV r0,r0,LSL #14
a.emit(0xe355_0000); // CMP r5,#0 (original Y)
a.emit(0xa3a0_4901); // MOVGE r4,#0x4000
a.emit(0xb3a0_4903); // MOVLT r4,#0xc000
a.emit(0xe3a0_5001); // MOV r5,#1 (subtract angle from base)
a.label("atan2_divide");
// Binary long division of (smaller magnitude << 14) by larger magnitude.
// Axis cases excluded zero denominators; the quotient is at most 0x4000.
a.emit(0xe3a0_2000); // MOV r2,#0 (quotient)
a.emit(0xe3a0_3000); // MOV r3,#0 (remainder)
a.emit(0xe3a0_c020); // MOV r12,#32
a.label("atan2_divide_bit");
a.emit(0xe1b0_0080); // MOVS r0,r0,LSL #1
a.emit(0xe0a3_3003); // ADC r3,r3,r3
a.emit(0xe153_0001); // CMP r3,r1
a.emit(0x2043_3001); // SUBCS r3,r3,r1
a.emit(0xe0a2_2002); // ADC r2,r2,r2
a.emit(0xe25c_c001); // SUBS r12,r12,#1
a.branch(1, "atan2_divide_bit");
a.emit(0xe1a0_0002); // MOV r0,r2
a.emit(0xe316_0102); // TST r6,#0x80000000
a.emit(0x1260_0000); // RSBNE r0,r0,#0 (signed ratio, truncated toward zero)
a.call("atan_polynomial");
a.emit(0xe355_0000); // CMP r5,#0
a.emit(0x0084_0000); // ADDEQ r0,r4,r0
a.emit(0x1060_0004); // RSBNE r0,r0,r4
a.branch(14, "atan2_done");
a.label("atan2_horizontal_axis");
a.emit(0xe350_0000); // CMP r0,#0
a.emit(0xa3a0_0000); // MOVGE r0,#0 (includes X=Y=0)
a.emit(0xb3a0_0902); // MOVLT r0,#0x8000
a.branch(14, "atan2_done");
a.label("atan2_vertical_axis");
a.emit(0xe351_0000); // CMP r1,#0
a.emit(0xa3a0_0901); // MOVGE r0,#0x4000
a.emit(0xb3a0_0903); // MOVLT r0,#0xc000
a.label("atan2_done");
a.emit(0xe1a0_0800); // MOV r0,r0,LSL #16
a.emit(0xe1a0_0820); // MOV r0,r0,LSR #16 (unsigned turn angle)
a.emit(0xe58d_0010); // STR r0,[sp,#16] (saved caller r0 below local frame)
a.emit(0xe8bd_0070); // LDMIA sp!,{r4-r6}
a.branch(14, "return");
// Shared leaf kernel: r0 is signed Q2.14; r1-r3 are scratch.
// Every multiply keeps its low 32 bits before the arithmetic shift.
a.label("atan_polynomial");
a.emit(0xe001_0090); // MUL r1,r0,r0
a.emit(0xe1a0_1741); // MOV r1,r1,ASR #14
a.emit(0xe261_1000); // RSB r1,r1,#0 (negative squared ratio)
a.emit(0xe3a0_20a9); // MOV r2,#0xa9
for coefficient in [0x390, 0x91c, 0xfb6, 0x16aa, 0x2081, 0x3651, 0xa2f9] {
a.emit(0xe003_0192); // MUL r3,r2,r1
a.emit(0xe1a0_2743); // MOV r2,r3,ASR #14
a.literal(3, coefficient);
a.emit(0xe082_2003); // ADD r2,r2,r3
}
a.emit(0xe003_0290); // MUL r3,r0,r2
a.emit(0xe1a0_0843); // MOV r0,r3,ASR #16 (sign-extended 16-bit angle)
a.emit(0xe12f_ff1e); // BX lr
}
fn validate_input(a: &mut ArmImage, register: u32) {
a.emit(0xe1a0_2800 | register); // MOV r2,rN,LSL #16
a.emit(0xe1a0_2842); // MOV r2,r2,ASR #16
a.emit(0xe150_0002 | register << 16); // CMP rN,r2
a.branch(1, "invalid_argument");
}

View file

@ -0,0 +1,57 @@
//! Original integer algorithms emitted as ARM instructions, not host calculations.
use super::ArmImage;
pub(super) fn emit(a: &mut ArmImage) {
a.label("div_arm");
a.emit(0xe1a0_2000); // MOV r2,r0
a.emit(0xe1a0_0001); // MOV r0,r1
a.emit(0xe1a0_1002); // MOV r1,r2 (exchange input operands)
a.label("div");
a.emit(0xe351_0000); // CMP r1,#0
a.branch(0, "invalid_argument"); // Diagnostic instead of firmware's divide-by-zero hang.
a.emit(0xe92d_0070); // STMDB sp!,{r4-r6} (40 bytes total SVC stack use)
a.emit(0xe1a0_4fc0); // MOV r4,r0,ASR #31 (numerator sign)
a.emit(0xe020_5001); // EOR r5,r0,r1
a.emit(0xe1a0_5fc5); // MOV r5,r5,ASR #31 (quotient sign)
a.emit(0xe350_0000); // CMP r0,#0
a.emit(0x4260_0000); // RSBMI r0,r0,#0 (unsigned magnitude, including INT_MIN)
a.emit(0xe351_0000); // CMP r1,#0
a.emit(0x4261_1000); // RSBMI r1,r1,#0
a.emit(0xe3a0_2000); // MOV r2,#0 (quotient magnitude)
a.emit(0xe3a0_3000); // MOV r3,#0 (remainder magnitude)
a.emit(0xe3a0_6020); // MOV r6,#32
a.label("div_bit");
a.emit(0xe1b0_0080); // MOVS r0,r0,LSL #1 (next numerator bit into C)
a.emit(0xe0a3_3003); // ADC r3,r3,r3 (remainder * 2 + bit)
a.emit(0xe153_0001); // CMP r3,r1 (unsigned)
a.emit(0x2043_3001); // SUBCS r3,r3,r1 (preserve comparison carry)
a.emit(0xe0a2_2002); // ADC r2,r2,r2 (quotient * 2 + comparison carry)
a.emit(0xe256_6001); // SUBS r6,r6,#1
a.branch(1, "div_bit");
// Patch only the defined outputs in the dispatcher's saved register frame.
a.emit(0xe58d_201c); // STR r2,[sp,#28] (saved r3 = unsigned quotient magnitude)
a.emit(0xe355_0000); // CMP r5,#0
a.emit(0x1262_2000); // RSBNE r2,r2,#0 (signed quotient)
a.emit(0xe354_0000); // CMP r4,#0
a.emit(0x1263_3000); // RSBNE r3,r3,#0 (remainder follows numerator sign)
a.emit(0xe58d_2010); // STR r2,[sp,#16] (saved r0)
a.emit(0xe58d_3014); // STR r3,[sp,#20] (saved r1)
a.emit(0xe8bd_0070); // LDMIA sp!,{r4-r6}
a.branch(14, "return");
// Restoring base-four square root: exactly 16 iterations for any u32 input.
a.label("sqrt");
a.emit(0xe3a0_1000); // MOV r1,#0 (root)
a.emit(0xe3a0_2101); // MOV r2,#0x40000000 (highest power of four)
a.label("sqrt_bit");
a.emit(0xe081_3002); // ADD r3,r1,r2 (trial)
a.emit(0xe150_0003); // CMP r0,r3
a.emit(0x2040_0003); // SUBCS r0,r0,r3
a.emit(0xe1a0_10a1); // MOV r1,r1,LSR #1 (comparison carry unchanged)
a.emit(0x2081_1002); // ADDCS r1,r1,r2
a.emit(0xe1b0_2122); // MOVS r2,r2,LSR #2
a.branch(1, "sqrt_bit");
a.emit(0xe58d_1004); // STR r1,[sp,#4] (saved r0 = floor(sqrt(input)))
a.branch(14, "return");
}

View file

@ -0,0 +1,90 @@
//! Original BitUnPack routine emitted as ARM instructions.
//! This subset requires widening/equal widths and complete output words.
//! Invalid parameters or overflowing offset values produce development diagnostics.
use super::ArmImage;
pub(super) fn emit(a: &mut ArmImage) {
a.label("bit_unpack");
a.emit(0xe92d_0ff0); // STMDB sp!,{r4-r11} (60 bytes total SVC stack use)
a.emit(0xe181_c002); // ORR r12,r1,r2
a.emit(0xe31c_0003); // TST r12,#3 (word-aligned destination and info)
a.branch(1, "invalid_argument");
a.emit(0xe350_0402); // CMP r0,#0x02000000 (protected source range)
a.branch(3, "invalid_argument");
a.emit(0xe1d2_30b0); // LDRH r3,[r2] (source byte count)
a.emit(0xe5d2_4002); // LDRB r4,[r2,#2] (source width)
a.emit(0xe5d2_5003); // LDRB r5,[r2,#3] (destination width)
a.emit(0xe592_6004); // LDR r6,[r2,#4] (offset and zero-data flag)
a.emit(0xe354_0000); // CMP r4,#0
a.branch(0, "invalid_argument");
a.emit(0xe354_0008); // CMP r4,#8
a.branch(8, "invalid_argument");
a.emit(0xe244_c001); // SUB r12,r4,#1
a.emit(0xe114_000c); // TST r4,r12 (power of two)
a.branch(1, "invalid_argument");
a.emit(0xe155_0004); // CMP r5,r4 (reject narrowing, including zero width)
a.branch(3, "invalid_argument");
a.emit(0xe355_0020); // CMP r5,#32
a.branch(8, "invalid_argument");
a.emit(0xe245_c001); // SUB r12,r5,#1
a.emit(0xe115_000c); // TST r5,r12
a.branch(1, "invalid_argument");
// Output bytes = source bytes * destination width / source width.
// Validated powers of two make the division exact, with at most three shifts.
a.emit(0xe00c_0593); // MUL r12,r3,r5
a.emit(0xe3a0_2001); // MOV r2,#1
a.label("unpack_size");
a.emit(0xe152_0004); // CMP r2,r4
a.branch(0, "unpack_validate_end");
a.emit(0xe1a0_c0ac); // MOV r12,r12,LSR #1
a.emit(0xe1a0_2082); // MOV r2,r2,LSL #1
a.branch(14, "unpack_size");
a.label("unpack_validate_end");
a.emit(0xe31c_0003); // TST r12,#3 (no partial output word)
a.branch(1, "invalid_argument");
a.emit(0xe091_200c); // ADDS r2,r1,r12
a.branch(2, "invalid_argument"); // Output address wrap.
a.emit(0xe090_2003); // ADDS r2,r0,r3
a.branch(2, "invalid_argument"); // Source address wrap.
a.emit(0xe353_0000); // CMP r3,#0
a.branch(0, "unpack_done");
a.emit(0xe3a0_7001); // MOV r7,#1
a.emit(0xe1a0_7417); // MOV r7,r7,LSL r4
a.emit(0xe247_7001); // SUB r7,r7,#1 (source mask)
a.emit(0xe3a0_9000); // MOV r9,#0 (pending word)
a.emit(0xe3a0_a000); // MOV r10,#0 (output bit position)
a.label("unpack_byte");
a.emit(0xe4d0_8001); // LDRB r8,[r0],#1
a.emit(0xe3a0_b008); // MOV r11,#8 (remaining input bits)
a.label("unpack_unit");
a.emit(0xe008_c007); // AND r12,r8,r7
a.emit(0xe35c_0000); // CMP r12,#0
a.branch(1, "unpack_offset");
a.emit(0xe316_0102); // TST r6,#0x80000000 (also offset zero units?)
a.branch(0, "unpack_value");
a.label("unpack_offset");
a.emit(0xe3c6_2102); // BIC r2,r6,#0x80000000
a.emit(0xe08c_c002); // ADD r12,r12,r2
a.label("unpack_value");
a.emit(0xe1a0_253c); // MOV r2,r12,LSR r5 (register shift by 32 gives zero)
a.emit(0xe352_0000); // CMP r2,#0
a.branch(1, "invalid_argument"); // Converted value must fit destination width.
a.emit(0xe189_9a1c); // ORR r9,r9,r12,LSL r10
a.emit(0xe08a_a005); // ADD r10,r10,r5
a.emit(0xe35a_0020); // CMP r10,#32
a.emit(0x0481_9004); // STREQ r9,[r1],#4
a.emit(0x03a0_9000); // MOVEQ r9,#0
a.emit(0x03a0_a000); // MOVEQ r10,#0
a.emit(0xe1a0_8438); // MOV r8,r8,LSR r4
a.emit(0xe05b_b004); // SUBS r11,r11,r4
a.branch(1, "unpack_unit");
a.emit(0xe253_3001); // SUBS r3,r3,#1
a.branch(1, "unpack_byte");
a.label("unpack_done");
a.emit(0xe8bd_0ff0); // LDMIA sp!,{r4-r11}
a.branch(14, "return");
}

View file

@ -0,0 +1,23 @@
//! Shared output routine for original ARM decompression and differential filters.
use super::ArmImage;
/// Emit a leaf routine called with BL. r5 holds a zero-extended byte;
/// r1 is the destination, r2 the remaining output count, and r12 the write mode.
/// Halfword mode uses r9 as a pending value and r10 as bit position (0 or 8).
/// The routine changes flags, r1/r2, and r9/r10, but preserves the input byte.
pub(super) fn emit_writer(a: &mut ArmImage) {
a.label("decompress_store_byte");
a.emit(0xe35c_0000); // CMP r12,#0
a.emit(0x05c1_5000); // STRBEQ r5,[r1] (normal byte-bus rules apply)
a.branch(0, "decompress_stored");
a.emit(0xe189_9a15); // ORR r9,r9,r5,LSL r10
a.emit(0xe22a_a008); // EOR r10,r10,#8
a.emit(0xe35a_0000); // CMP r10,#0
a.emit(0x0141_90b1); // STRHEQ r9,[r1,#-1] (commit the completed halfword)
a.emit(0x03a0_9000); // MOVEQ r9,#0
a.label("decompress_stored");
a.emit(0xe281_1001); // ADD r1,r1,#1
a.emit(0xe242_2001); // SUB r2,r2,#1
a.emit(0xe12f_ff1e); // BX lr
}

View file

@ -0,0 +1,67 @@
//! Original differential filters emitted as ARM instructions.
//! Byte or halfword deltas reconstruct samples through modular addition.
//! Invalid headers/alignment produce diagnostics, not firmware-quirk emulation.
use super::ArmImage;
pub(super) fn emit(a: &mut ArmImage) {
a.label("diff8_wram");
a.emit(0xe3a0_c000); // MOV r12,#0 (byte input and output)
a.branch(14, "diff_start");
a.label("diff8_vram");
a.emit(0xe3a0_c001); // MOV r12,#1 (byte input, buffered halfword output)
a.branch(14, "diff_start");
a.label("diff16");
a.emit(0xe3a0_c002); // MOV r12,#2 (halfword input and output)
a.label("diff_start");
a.emit(0xe92d_0ff0); // STMDB sp!,{r4-r11} (60 bytes total SVC stack use)
a.emit(0xe310_0003); // TST r0,#3 (word-aligned header)
a.branch(1, "invalid_argument");
a.emit(0xe350_0402); // CMP r0,#0x02000000 (protected source range)
a.branch(3, "invalid_argument");
a.emit(0xe490_2004); // LDR r2,[r0],#4
a.emit(0xe202_b0ff); // AND r11,r2,#0xff
a.emit(0xe35c_0002); // CMP r12,#2
a.emit(0x03a0_3082); // MOVEQ r3,#0x82 (16-bit differential header)
a.emit(0x13a0_3081); // MOVNE r3,#0x81 (8-bit differential header)
a.emit(0xe15b_0003); // CMP r11,r3
a.branch(1, "invalid_argument");
a.emit(0xe1a0_2422); // MOV r2,r2,LSR #8 (24-bit output byte count)
a.emit(0xe35c_0000); // CMP r12,#0
a.branch(0, "diff_validate_end");
a.emit(0xe181_b002); // ORR r11,r1,r2 (even output address and byte count)
a.emit(0xe31b_0001); // TST r11,#1
a.branch(1, "invalid_argument");
a.label("diff_validate_end");
a.emit(0xe091_b002); // ADDS r11,r1,r2
a.branch(2, "invalid_argument"); // Reject destination address wrap.
a.emit(0xe090_b002); // ADDS r11,r0,r2
a.branch(2, "invalid_argument"); // Reject source payload address wrap.
a.emit(0xe352_0000); // CMP r2,#0
a.branch(0, "diff_done");
a.emit(0xe3a0_4000); // MOV r4,#0 (first delta is the first original sample)
a.emit(0xe35c_0002); // CMP r12,#2
a.branch(0, "diff_halfword");
a.emit(0xe3a0_9000); // MOV r9,#0 (pending output halfword)
a.emit(0xe3a0_a000); // MOV r10,#0 (pending byte position)
a.label("diff_byte");
a.emit(0xe4d0_5001); // LDRB r5,[r0],#1
a.emit(0xe084_4005); // ADD r4,r4,r5
a.emit(0xe204_40ff); // AND r4,r4,#255 (modulo 256)
a.emit(0xe1a0_5004); // MOV r5,r4
a.call("decompress_store_byte");
a.emit(0xe352_0000); // CMP r2,#0 (writer consumed one byte)
a.branch(1, "diff_byte");
a.branch(14, "diff_done");
a.label("diff_halfword");
a.emit(0xe0d0_50b2); // LDRH r5,[r0],#2
a.emit(0xe084_4005); // ADD r4,r4,r5
a.emit(0xe0c1_40b2); // STRH r4,[r1],#2 (low 16 bits give modulo 65536)
a.emit(0xe252_2002); // SUBS r2,r2,#2 (header length counts bytes, not samples)
a.branch(1, "diff_halfword");
a.label("diff_done");
a.emit(0xe8bd_0ff0); // LDMIA sp!,{r4-r11}
a.branch(14, "return");
}

View file

@ -0,0 +1,88 @@
//! Original Huffman decoder emitted as ARM instructions.
//! Supports 4/8-bit symbols and complete aligned output words.
//! Only traversed tree edges/leaves are validated; invalid inputs are diagnostics.
use super::ArmImage;
pub(super) fn emit(a: &mut ArmImage) {
a.label("huffman");
// The common SWI frame already saved lr. This leaf routine uses it as scratch.
a.emit(0xe92d_0ff0); // STMDB sp!,{r4-r11} (60 bytes total SVC stack use)
a.emit(0xe180_b001); // ORR r11,r0,r1
a.emit(0xe31b_0003); // TST r11,#3 (word-aligned source and destination)
a.branch(1, "invalid_argument");
a.emit(0xe350_0402); // CMP r0,#0x02000000
a.branch(3, "invalid_argument");
a.emit(0xe490_2004); // LDR r2,[r0],#4 (header)
a.emit(0xe202_b0ff); // AND r11,r2,#0xff
a.emit(0xe35b_0024); // CMP r11,#0x24
a.branch(0, "huff_header");
a.emit(0xe35b_0028); // CMP r11,#0x28
a.branch(1, "invalid_argument");
a.label("huff_header");
a.emit(0xe20b_300f); // AND r3,r11,#15 (symbol width)
a.emit(0xe1a0_2422); // MOV r2,r2,LSR #8 (output byte count)
a.emit(0xe312_0003); // TST r2,#3 (complete output words)
a.branch(1, "invalid_argument");
a.emit(0xe091_b002); // ADDS r11,r1,r2
a.branch(2, "invalid_argument"); // Output address wrap.
a.emit(0xe352_0000); // CMP r2,#0
a.branch(0, "huff_done"); // No tree or bitstream access for zero output.
a.emit(0xe4d0_c001); // LDRB r12,[r0],#1 (tree size byte)
a.emit(0xe1a0_4000); // MOV r4,r0 (root at source+5)
a.emit(0xe1a0_c08c); // MOV r12,r12,LSL #1
a.emit(0xe28c_c001); // ADD r12,r12,#1 (table bytes excluding size byte)
a.emit(0xe094_500c); // ADDS r5,r4,r12 (exclusive tree end / bitstream start)
a.branch(2, "invalid_argument");
a.emit(0xe315_0003); // TST r5,#3 (padded tree must give aligned input words)
a.branch(1, "invalid_argument");
a.emit(0xe1a0_0005); // MOV r0,r5 (bitstream cursor)
a.emit(0xe1a0_6004); // MOV r6,r4 (current node)
a.emit(0xe3a0_8000); // MOV r8,#0 (remaining input bits)
a.emit(0xe3a0_9000); // MOV r9,#0 (pending output word)
a.emit(0xe3a0_a000); // MOV r10,#0 (output bit position)
a.label("huff_bit");
a.emit(0xe358_0000); // CMP r8,#0
a.emit(0x0490_7004); // LDREQ r7,[r0],#4 (little-endian input word)
a.emit(0x03a0_8020); // MOVEQ r8,#32
a.emit(0xe5d6_b000); // LDRB r11,[r6] (current node descriptor)
a.emit(0xe20b_c03f); // AND r12,r11,#63
a.emit(0xe28c_c001); // ADD r12,r12,#1
a.emit(0xe3c6_e001); // BIC lr,r6,#1
a.emit(0xe09e_c08c); // ADDS r12,lr,r12,LSL #1 (left child address)
a.branch(2, "invalid_argument"); // Reject child address wrap.
a.emit(0xe1b0_7087); // MOVS r7,r7,LSL #1 (old bit31 becomes carry)
a.emit(0x228c_c001); // ADDCS r12,r12,#1 (right child)
a.emit(0x33a0_e080); // MOVCC lr,#0x80 (left leaf flag)
a.emit(0x23a0_e040); // MOVCS lr,#0x40 (right leaf flag)
a.emit(0xe248_8001); // SUB r8,r8,#1 (consume the branch bit)
a.emit(0xe155_000c); // CMP r5,r12
a.branch(9, "invalid_argument"); // BLS: child >= exclusive tree end.
// Children always advance, so bounds checks also bound traversal depth.
a.emit(0xe11b_000e); // TST r11,lr (selected child is a leaf?)
a.branch(0, "huff_node");
a.emit(0xe5dc_b000); // LDRB r11,[r12] (symbol)
a.emit(0xe1a0_e33b); // MOV lr,r11,LSR r3
a.emit(0xe35e_0000); // CMP lr,#0 (reject upper bits in 4-bit leaves)
a.branch(1, "invalid_argument");
a.emit(0xe189_9a1b); // ORR r9,r9,r11,LSL r10
a.emit(0xe08a_a003); // ADD r10,r10,r3
a.emit(0xe1a0_6004); // MOV r6,r4 (restart at root after each symbol)
a.emit(0xe35a_0020); // CMP r10,#32
a.branch(1, "huff_bit");
a.emit(0xe481_9004); // STR r9,[r1],#4 (commit completed word)
a.emit(0xe252_2004); // SUBS r2,r2,#4
a.branch(0, "huff_done");
a.emit(0xe3a0_9000); // MOV r9,#0
a.emit(0xe3a0_a000); // MOV r10,#0
a.branch(14, "huff_bit");
a.label("huff_node");
a.emit(0xe1a0_600c); // MOV r6,r12
a.branch(14, "huff_bit");
a.label("huff_done");
a.emit(0xe8bd_0ff0); // LDMIA sp!,{r4-r11}
a.branch(14, "return");
}

View file

@ -0,0 +1,82 @@
//! Original LZ77 decoder emitted as ARM instructions.
//! Invalid streams trap after any earlier successful writes. VRAM output requires
//! an even size and halfword alignment; distance-one references are rejected.
use super::ArmImage;
pub(super) fn emit(a: &mut ArmImage) {
a.label("lz_wram");
a.emit(0xe3a0_c000); // MOV r12,#0 (byte stores)
a.branch(14, "lz_start");
a.label("lz_vram");
a.emit(0xe3a0_c001); // MOV r12,#1 (buffer bytes into halfword stores)
a.label("lz_start");
a.emit(0xe92d_0ff0); // STMDB sp!,{r4-r11} (60 bytes total SVC stack use)
a.emit(0xe310_0003); // TST r0,#3 (header must be word-aligned)
a.branch(1, "invalid_argument");
a.emit(0xe350_0402); // CMP r0,#0x02000000 (do not decompress protected BIOS bytes)
a.branch(3, "invalid_argument");
a.emit(0xe490_2004); // LDR r2,[r0],#4 (header)
a.emit(0xe202_b0ff); // AND r11,r2,#0xff
a.emit(0xe35b_0010); // CMP r11,#0x10 (LZ77 type, reserved bits zero)
a.branch(1, "invalid_argument");
a.emit(0xe1a0_2422); // MOV r2,r2,LSR #8 (24-bit output byte count)
a.emit(0xe35c_0000); // CMP r12,#0
a.branch(0, "lz_validate_end");
a.emit(0xe181_b002); // ORR r11,r1,r2 (both destination and size must be even)
a.emit(0xe31b_0001); // TST r11,#1
a.branch(1, "invalid_argument");
a.label("lz_validate_end");
a.emit(0xe091_b002); // ADDS r11,r1,r2
a.branch(2, "invalid_argument"); // Reject output address wrap.
a.emit(0xe352_0000); // CMP r2,#0
a.branch(0, "lz_done");
a.emit(0xe1a0_8001); // MOV r8,r1 (start of produced output)
a.emit(0xe3a0_9000); // MOV r9,#0 (pending halfword)
a.emit(0xe3a0_a000); // MOV r10,#0 (bit position 0 or 8)
a.label("lz_group");
a.emit(0xe4d0_3001); // LDRB r3,[r0],#1 (flags, most significant bit first)
a.emit(0xe3a0_4080); // MOV r4,#0x80
a.label("lz_token");
a.emit(0xe113_0004); // TST r3,r4
a.branch(1, "lz_reference");
a.emit(0xe4d0_5001); // LDRB r5,[r0],#1 (literal)
a.call("decompress_store_byte");
a.branch(14, "lz_next_token");
a.label("lz_reference");
a.emit(0xe4d0_5001); // LDRB r5,[r0],#1 (length nibble and high displacement bits)
a.emit(0xe1a0_7225); // MOV r7,r5,LSR #4
a.emit(0xe287_7003); // ADD r7,r7,#3 (length 3..18)
a.emit(0xe205_600f); // AND r6,r5,#0xf
a.emit(0xe4d0_5001); // LDRB r5,[r0],#1 (low displacement bits)
a.emit(0xe185_6406); // ORR r6,r5,r6,LSL #8
a.emit(0xe286_6001); // ADD r6,r6,#1 (distance 1..4096)
a.emit(0xe041_b008); // SUB r11,r1,r8 (bytes already produced)
a.emit(0xe156_000b); // CMP r6,r11
a.branch(8, "invalid_argument"); // BHI: reference before start of output.
a.emit(0xe157_0002); // CMP r7,r2
a.branch(8, "invalid_argument"); // BHI: run would exceed the declared size.
a.emit(0xe35c_0000); // CMP r12,#0
a.branch(0, "lz_distance_ok");
a.emit(0xe356_0001); // CMP r6,#1
a.branch(0, "invalid_argument"); // VRAM firmware cannot use its uncommitted previous byte.
a.label("lz_distance_ok");
a.emit(0xe041_6006); // SUB r6,r1,r6 (reference pointer)
a.label("lz_reference_byte");
a.emit(0xe4d6_5001); // LDRB r5,[r6],#1 (overlaps observe newly produced data)
a.call("decompress_store_byte");
a.emit(0xe257_7001); // SUBS r7,r7,#1
a.branch(1, "lz_reference_byte");
a.label("lz_next_token");
a.emit(0xe352_0000); // CMP r2,#0
a.branch(0, "lz_done");
a.emit(0xe1b0_40a4); // MOVS r4,r4,LSR #1
a.branch(1, "lz_token");
a.branch(14, "lz_group");
a.label("lz_done");
a.emit(0xe8bd_0ff0); // LDMIA sp!,{r4-r11}
a.branch(14, "return");
}

View file

@ -0,0 +1,119 @@
//! Original ARM SoftReset and selective RegisterRamReset services.
//! Serial/sound reset requests are diagnostics until those devices are modeled.
use super::{ArmImage, IRQ_STACK, ROM_START, SVC_STACK, SYSTEM_STACK};
use crate::{
dma::DMA_BASE,
io::{DISPCNT, IE, IME, TIMER_BASE},
};
pub(super) fn emit(a: &mut ArmImage) {
a.label("soft_reset");
// Read the byte before clearing its containing RAM and the common SWI frame.
// No stack accesses follow: this service never returns to its caller.
a.literal(0, 0x0300_7ffa);
a.emit(0xe5d0_2000); // LDRB r2,[r0] (zero: ROM, nonzero: EWRAM)
a.literal(0, 0x0300_7e00);
a.emit(0xe3a0_1080); // MOV r1,#128 (512 bytes)
a.emit(0xe3a0_3000); // MOV r3,#0
a.label("soft_reset_clear");
a.emit(0xe480_3004); // STR r3,[r0],#4
a.emit(0xe251_1001); // SUBS r1,r1,#1
a.branch(1, "soft_reset_clear");
a.emit(0xe169_f003); // MSR SPSR_fc,r3 (SVC saved status = 0)
a.emit(0xe1a0_e003); // MOV lr,r3 (SVC link = 0)
a.literal(13, SVC_STACK);
a.emit(0xe321_f0d2); // MSR CPSR_c,#IRQ|I|F
a.emit(0xe169_f003); // MSR SPSR_fc,r3 (IRQ saved status = 0)
a.emit(0xe1a0_e003); // MOV lr,r3 (IRQ link = 0)
a.literal(13, IRQ_STACK);
a.emit(0xe321_f0df); // MSR CPSR_c,#System|I|F
a.literal(13, SYSTEM_STACK);
a.emit(0xe352_0000); // CMP r2,#0
a.literal(14, ROM_START);
a.emit(0x13a0_e402); // MOVNE lr,#0x02000000
for register in 0..=12 {
a.emit(0xe3a0_0000 | register << 12); // MOV rN,#0
}
// Deterministic subset policy: clear NZCV and F, keep I set. Do not change
// IME, IE, IF, devices, or unrelated CPU banks. IRQ stays masked throughout.
a.emit(0xe329_f09f); // MSR CPSR_fc,#System|I
a.emit(0xe12f_ff1e); // BX lr (ARM target, no return through erased SWI frame)
emit_ram_reset(a);
}
fn emit_ram_reset(a: &mut ArmImage) {
a.label("register_ram_reset");
// Validate unsupported devices before forced blank or any reset writes.
// Higher flag bits are ignored, as in the documented low-byte flag format.
a.emit(0xe310_0060); // TST r0,#serial|sound
a.branch(1, "invalid_argument");
a.literal(1, DISPCNT);
a.emit(0xe3a0_2080); // MOV r2,#forced blank (also clear other DISPCNT bits)
a.emit(0xe1c1_20b0); // STRH r2,[r1]
a.emit(0xe3a0_3000); // MOV r3,#0 (shared fill value)
for (flag, start, bytes, next) in [
(1, 0x0200_0000, 0x40000, "ram_reset_iwram"),
(2, 0x0300_0000, 0x7e00, "ram_reset_palette"),
(4, 0x0500_0000, 0x400, "ram_reset_vram"),
(8, 0x0600_0000, 0x18000, "ram_reset_oam"),
(16, 0x0700_0000, 0x400, "ram_reset_io"),
] {
a.emit(0xe310_0000 | flag); // TST r0,#flag
a.branch(0, next);
clear_words(a, start, bytes);
a.label(next);
}
a.emit(0xe310_0080); // TST r0,#other registers
a.branch(0, "return");
// IRQ remains CPU-masked throughout. Disable DMA/timers before clearing
// their address/count/reload registers. These are bus writes, not host resets.
a.literal(1, IME);
store_halfword(a, 0); // IME = 0
a.literal(1, DMA_BASE);
for offset in [10, 22, 34, 46] {
store_halfword(a, offset); // Disable each channel before clearing its registers.
}
clear_words(a, DMA_BASE, 48);
a.literal(1, TIMER_BASE);
for offset in [2, 6, 10, 14] {
store_halfword(a, offset); // Stop each timer; the stopped counter remains latched.
}
clear_words(a, TIMER_BASE, 16); // Clear reload/control, not hidden counter/clock state.
clear_words(a, DISPCNT + 4, 0x54); // Display controls, offsets, origins, windows/effects.
a.literal(1, DISPCNT);
a.emit(0xe3a0_3c01); // MOV r3,#256 (identity affine PA/PD for BG2 and BG3)
for offset in [0x20, 0x26, 0x30, 0x36] {
store_halfword(a, offset);
}
a.emit(0xe3a0_3000); // MOV r3,#0
a.literal(1, IE);
store_halfword(a, 0); // IE = 0
store_halfword(a, 4); // WAITCNT = 0
a.literal(3, 0xffff);
store_halfword(a, 2); // Acknowledge IF after stopping timer/display IRQ sources.
a.branch(14, "return");
// Leaf routine; only r1/r2 change. The common 28-byte SWI frame saves lr.
a.label("ram_reset_words");
a.emit(0xe481_3004); // STR r3,[r1],#4
a.emit(0xe252_2004); // SUBS r2,r2,#4
a.branch(1, "ram_reset_words");
a.emit(0xe12f_ff1e); // BX lr
}
fn clear_words(a: &mut ArmImage, address: u32, bytes: u32) {
debug_assert!(bytes != 0 && bytes % 4 == 0);
a.literal(1, address);
a.literal(2, bytes);
a.call("ram_reset_words");
}
// Store r3 through r1 with an ARM immediate halfword offset.
fn store_halfword(a: &mut ArmImage, offset: u32) {
debug_assert!(offset <= 255 && offset % 2 == 0);
a.emit(0xe1c1_30b0 | (offset & 0xf0) << 4 | (offset & 15));
}

View file

@ -0,0 +1,64 @@
//! Original run-length decoder emitted as ARM instructions.
//! Runs must fit the declared output size. Halfword output requires an even
//! destination and size; malformed-input diagnostics do not emulate BIOS quirks.
use super::ArmImage;
pub(super) fn emit(a: &mut ArmImage) {
a.label("rl_wram");
a.emit(0xe3a0_c000); // MOV r12,#0 (byte stores)
a.branch(14, "rl_start");
a.label("rl_vram");
a.emit(0xe3a0_c001); // MOV r12,#1 (buffered halfword stores)
a.label("rl_start");
a.emit(0xe92d_0ff0); // STMDB sp!,{r4-r11} (60 bytes total SVC stack use)
a.emit(0xe310_0003); // TST r0,#3 (word-aligned header)
a.branch(1, "invalid_argument");
a.emit(0xe350_0402); // CMP r0,#0x02000000 (reject protected source region)
a.branch(3, "invalid_argument");
a.emit(0xe490_2004); // LDR r2,[r0],#4
a.emit(0xe202_b0ff); // AND r11,r2,#0xff
a.emit(0xe35b_0030); // CMP r11,#0x30 (run-length type, reserved bits zero)
a.branch(1, "invalid_argument");
a.emit(0xe1a0_2422); // MOV r2,r2,LSR #8 (24-bit output byte count)
a.emit(0xe35c_0000); // CMP r12,#0
a.branch(0, "rl_validate_end");
a.emit(0xe181_b002); // ORR r11,r1,r2
a.emit(0xe31b_0001); // TST r11,#1 (even destination and length)
a.branch(1, "invalid_argument");
a.label("rl_validate_end");
a.emit(0xe091_b002); // ADDS r11,r1,r2
a.branch(2, "invalid_argument"); // Reject destination address wrap.
a.emit(0xe352_0000); // CMP r2,#0
a.branch(0, "rl_done");
a.emit(0xe3a0_9000); // MOV r9,#0 (pending halfword)
a.emit(0xe3a0_a000); // MOV r10,#0 (bit position 0 or 8)
a.label("rl_block");
a.emit(0xe4d0_3001); // LDRB r3,[r0],#1 (block control)
a.emit(0xe203_707f); // AND r7,r3,#0x7f
a.emit(0xe313_0080); // TST r3,#0x80 (repeat flag)
a.emit(0x0287_7001); // ADDEQ r7,r7,#1 (1..128 literal bytes)
a.emit(0x1287_7003); // ADDNE r7,r7,#3 (3..130 repeated bytes)
a.emit(0xe157_0002); // CMP r7,r2
a.branch(8, "invalid_argument"); // Reject a block exceeding remaining output.
a.emit(0xe313_0080); // TST r3,#0x80
a.branch(0, "rl_literal");
a.emit(0xe4d0_5001); // LDRB r5,[r0],#1 (read repeated value only once)
a.label("rl_repeat");
a.call("decompress_store_byte");
a.emit(0xe257_7001); // SUBS r7,r7,#1
a.branch(1, "rl_repeat");
a.branch(14, "rl_next");
a.label("rl_literal");
a.emit(0xe4d0_5001); // LDRB r5,[r0],#1
a.call("decompress_store_byte");
a.emit(0xe257_7001); // SUBS r7,r7,#1
a.branch(1, "rl_literal");
a.label("rl_next");
a.emit(0xe352_0000); // CMP r2,#0
a.branch(1, "rl_block");
a.label("rl_done");
a.emit(0xe8bd_0ff0); // LDMIA sp!,{r4-r11}
a.branch(14, "return");
}

254
crates/gba-core/src/cpu.rs Normal file
View file

@ -0,0 +1,254 @@
use std::{error::Error, fmt};
use crate::memory::{Memory, MemoryError};
mod alu;
mod arm;
mod exception;
mod status;
mod thumb;
mod timing;
pub use exception::Exception;
pub use status::Mode;
mod transfer;
#[cfg(test)]
mod exception_tests;
#[cfg(test)]
mod instruction_tests;
#[cfg(test)]
mod status_tests;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod thumb_tests;
#[cfg(test)]
mod timing_tests;
#[cfg(test)]
mod transfer_tests;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CpuError {
Memory(MemoryError),
UnsupportedInstruction { address: u32, instruction: u32 },
UnsupportedThumbInstruction { address: u32, instruction: u16 },
}
impl fmt::Display for CpuError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Memory(error) => write!(f, "{error}"),
Self::UnsupportedThumbInstruction {
address,
instruction,
} => {
write!(
f,
"unsupported Thumb instruction {instruction:#06x} at {address:#010x}"
)
}
Self::UnsupportedInstruction {
address,
instruction,
} => {
write!(
f,
"unsupported instruction {instruction:#010x} at {address:#010x}"
)
}
}
}
}
impl Error for CpuError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Memory(error) => Some(error),
Self::UnsupportedInstruction { .. } | Self::UnsupportedThumbInstruction { .. } => None,
}
}
}
impl From<MemoryError> for CpuError {
fn from(error: MemoryError) -> Self {
Self::Memory(error)
}
}
/// Arithmetic status flags from the current program status register.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Flags {
pub negative: bool,
pub zero: bool,
/// For subtraction, carry means no unsigned borrow occurred.
pub carry: bool,
pub overflow: bool,
}
impl Flags {
fn condition_passed(self, condition: u32) -> bool {
match condition {
0x0 => self.zero, // EQ
0x1 => !self.zero, // NE
0x2 => self.carry, // CS/HS
0x3 => !self.carry, // CC/LO
0x4 => self.negative, // MI
0x5 => !self.negative, // PL
0x6 => self.overflow, // VS
0x7 => !self.overflow, // VC
0x8 => self.carry && !self.zero, // HI
0x9 => !self.carry || self.zero, // LS
0xa => self.negative == self.overflow, // GE
0xb => self.negative != self.overflow, // LT
0xc => !self.zero && self.negative == self.overflow, // GT
0xd => self.zero || self.negative != self.overflow, // LE
0xe => true, // AL
_ => false, // Reserved condition; rejected by Cpu::step.
}
}
}
/// Instruction encoding selected by the processor's T bit.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstructionSet {
Arm,
Thumb,
}
impl InstructionSet {
fn width(self) -> u32 {
match self {
Self::Arm => 4,
Self::Thumb => 2,
}
}
fn align(self, address: u32) -> u32 {
address & !(self.width() - 1)
}
}
/// ARM/Thumb interpreter with register banks and optional nominal cycle costs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cpu {
registers: [u32; 16],
flags: Flags,
instruction_set: InstructionSet,
mode: Mode,
irq_disabled: bool,
fiq_disabled: bool,
banks: status::Banks,
}
impl Cpu {
/// Start directly at ARM-state test code in System mode, with interrupts enabled.
/// This does not perform a GBA reset.
pub fn new(entry_point: u32) -> Self {
let mut registers = [0; 16];
registers[15] = entry_point;
Self {
registers,
flags: Flags::default(),
instruction_set: InstructionSet::Arm,
mode: Mode::System,
irq_disabled: false,
fiq_disabled: false,
banks: status::Banks::default(),
}
}
/// r15 stores the next instruction address, not the pipeline-adjusted value.
pub fn registers(&self) -> &[u32; 16] {
&self.registers
}
pub fn pc(&self) -> u32 {
self.registers[15]
}
pub fn flags(&self) -> Flags {
self.flags
}
pub fn instruction_set(&self) -> InstructionSet {
self.instruction_set
}
/// Execute one ARM instruction or one 16-bit Thumb instruction.
/// Each half of a Thumb BL is a separate step.
/// This CPU-only API does not execute DMA, advance clocks, or sample device interrupts.
/// It records HALTCNT writes but does not enforce HALT. Use Machine::step for integration.
/// Unsupported operations and memory errors leave CPU state unchanged.
/// These errors are development diagnostics, not emulated CPU exceptions.
pub fn step(&mut self, memory: &mut Memory) -> Result<(), CpuError> {
let instruction = self.fetch(memory)?;
self.execute_fetched(instruction, memory)
}
fn fetch(&self, memory: &Memory) -> Result<u32, CpuError> {
Ok(match self.instruction_set {
InstructionSet::Arm => memory.read32(self.pc())?,
InstructionSet::Thumb => u32::from(memory.read16(self.pc())?),
})
}
fn execute_fetched(&mut self, instruction: u32, memory: &mut Memory) -> Result<(), CpuError> {
// Record the executing instruction, not an operand's pipelined PC value.
// Always clear the access context, including on diagnostic errors.
memory.begin_cpu_access(self.pc());
let result = self.execute_instruction(instruction, memory);
memory.end_cpu_access();
result
}
fn execute_instruction(
&mut self,
instruction: u32,
memory: &mut Memory,
) -> Result<(), CpuError> {
if self.instruction_set == InstructionSet::Thumb {
return self.execute_thumb(instruction as u16, memory);
}
let condition = instruction >> 28;
// Reserved in ARMv4. Do not decode ARMv5 unconditional extensions.
if condition == 0xf {
return Err(self.unsupported(instruction));
}
if !self.flags.condition_passed(condition) {
self.registers[15] = self.pc().wrapping_add(4);
return Ok(());
}
self.execute_arm(instruction, memory)
}
fn unsupported(&self, instruction: u32) -> CpuError {
match self.instruction_set {
InstructionSet::Arm => CpuError::UnsupportedInstruction {
address: self.pc(),
instruction,
},
InstructionSet::Thumb => CpuError::UnsupportedThumbInstruction {
address: self.pc(),
instruction: instruction as u16,
},
}
}
fn branch_exchange(&mut self, target: u32) {
self.instruction_set = if target & 1 == 0 {
InstructionSet::Arm
} else {
InstructionSet::Thumb
};
self.registers[15] = self.instruction_set.align(target);
}
fn operand_register(&self, register: usize, pc_offset: u32) -> u32 {
if register == 15 {
self.pc().wrapping_add(pc_offset)
} else {
self.registers[register]
}
}
}

View file

@ -0,0 +1,108 @@
use super::Flags;
#[derive(Clone, Copy, Debug)]
pub(super) enum Shift {
Lsl,
Lsr,
Asr,
Ror,
}
impl Shift {
pub(super) fn decode(bits: u32) -> Self {
match bits & 3 {
0 => Self::Lsl,
1 => Self::Lsr,
2 => Self::Asr,
_ => Self::Ror,
}
}
}
/// ARM barrel shifter. Register shifts use only the low eight amount bits.
pub(super) fn shift(
value: u32,
kind: Shift,
amount: u32,
carry: bool,
by_register: bool,
) -> (u32, bool) {
let mut amount = if by_register { amount & 0xff } else { amount };
if amount == 0 {
if by_register || matches!(kind, Shift::Lsl) {
return (value, carry);
}
if matches!(kind, Shift::Ror) {
return ((u32::from(carry) << 31) | (value >> 1), value & 1 != 0); // RRX
}
amount = 32; // Immediate LSR/ASR #0 encodes a shift by 32.
}
match kind {
Shift::Lsl => match amount {
1..=31 => (value << amount, (value >> (32 - amount)) & 1 != 0),
32 => (0, value & 1 != 0),
_ => (0, false),
},
Shift::Lsr => match amount {
1..=31 => (value >> amount, (value >> (amount - 1)) & 1 != 0),
32 => (0, value >> 31 != 0),
_ => (0, false),
},
Shift::Asr => {
let sign = value >> 31 != 0;
if amount >= 32 {
(if sign { u32::MAX } else { 0 }, sign)
} else {
(
((value as i32) >> amount) as u32,
(value >> (amount - 1)) & 1 != 0,
)
}
}
Shift::Ror => {
let result = value.rotate_right(amount);
(result, result >> 31 != 0)
}
}
}
/// Addition is also used for subtraction: a - b = a + !b + 1.
fn add_with_carry(a: u32, b: u32, carry: bool) -> (u32, bool, bool) {
let wide = u64::from(a) + u64::from(b) + u64::from(carry);
let result = wide as u32;
let overflow = (!(a ^ b) & (a ^ result)) >> 31 != 0;
(result, wide > u64::from(u32::MAX), overflow)
}
pub(super) fn execute(
opcode: u32,
a: u32,
b: u32,
shifter_carry: bool,
previous: Flags,
) -> (u32, Flags) {
let (result, carry, overflow) = match opcode {
0x0 | 0x8 => (a & b, shifter_carry, previous.overflow),
0x1 | 0x9 => (a ^ b, shifter_carry, previous.overflow),
0x2 | 0xa => add_with_carry(a, !b, true),
0x3 => add_with_carry(b, !a, true),
0x4 | 0xb => add_with_carry(a, b, false),
0x5 => add_with_carry(a, b, previous.carry),
0x6 => add_with_carry(a, !b, previous.carry),
0x7 => add_with_carry(b, !a, previous.carry),
0xc => (a | b, shifter_carry, previous.overflow),
0xd => (b, shifter_carry, previous.overflow),
0xe => (a & !b, shifter_carry, previous.overflow),
0xf => (!b, shifter_carry, previous.overflow),
_ => unreachable!("opcode comes from a four-bit instruction field"),
};
(
result,
Flags {
negative: result >> 31 != 0,
zero: result == 0,
carry,
overflow,
},
)
}

View file

@ -0,0 +1,305 @@
use super::{
alu::{self, Shift},
Cpu, CpuError,
};
use crate::memory::Memory;
impl Cpu {
pub(super) fn execute_arm(
&mut self,
instruction: u32,
memory: &mut Memory,
) -> Result<(), CpuError> {
if instruction & 0x0f00_0000 == 0x0f00_0000 {
self.enter_exception(super::Exception::SoftwareInterrupt);
return Ok(());
}
if instruction & 0x0fbf_0fff == 0x010f_0000 {
return self.read_status(instruction);
}
if instruction & 0x0fb0_fff0 == 0x0120_f000 || instruction & 0x0fb0_f000 == 0x0320_f000 {
return self.write_status(instruction);
}
if instruction & 0x0fff_fff0 == 0x012f_ff10 {
let target = self.operand_register((instruction & 15) as usize, 8);
self.branch_exchange(target);
return Ok(());
}
if instruction & 0x0e00_0000 == 0x0a00_0000 {
let address = self.pc();
let offset = (((instruction & 0x00ff_ffff) << 8) as i32 >> 6) as u32;
if instruction & (1 << 24) != 0 {
self.registers[14] = address.wrapping_add(4); // BL return address
}
self.registers[15] = address.wrapping_add(8).wrapping_add(offset);
return Ok(());
}
if instruction & 0x0e00_0000 == 0x0800_0000 {
return self.block_transfer(instruction, memory);
}
// SWP shares the 1001 low bits with multiply and halfword transfers.
if instruction & 0x0fb0_0ff0 == 0x0100_0090 {
return self.swap(instruction, memory);
}
if instruction & 0x0f80_00f0 == 0x0080_0090 {
return self.multiply_long(instruction);
}
if instruction & 0x0fc0_00f0 == 0x0000_0090 {
return self.multiply(instruction);
}
if instruction & 0x0e00_0090 == 0x0000_0090 {
return self.halfword_transfer(instruction, memory);
}
if instruction & 0x0c00_0000 == 0x0400_0000 {
return self.single_transfer(instruction, memory);
}
if instruction & 0x0c00_0000 == 0 {
return self.data_processing(instruction);
}
Err(self.unsupported(instruction))
}
fn data_processing(&mut self, instruction: u32) -> Result<(), CpuError> {
let opcode = (instruction >> 21) & 15;
let set_flags = instruction & (1 << 20) != 0;
let test = (8..=11).contains(&opcode);
let source = ((instruction >> 16) & 15) as usize;
let destination = ((instruction >> 12) & 15) as usize;
// S=0 in the test/compare opcode range is PSR transfer or an extension.
if test && !set_flags {
return Err(self.unsupported(instruction));
}
let restore = if !test && destination == 15 && set_flags {
Some(self.return_status(instruction)?)
} else {
None
};
let immediate = instruction & (1 << 25) != 0;
let register_shift = !immediate && instruction & (1 << 4) != 0;
let pc_offset = if register_shift { 12 } else { 8 };
let (operand, carry) = if immediate {
let rotation = ((instruction >> 8) & 15) * 2;
let value = (instruction & 255).rotate_right(rotation);
(
value,
if rotation == 0 {
self.flags.carry
} else {
value >> 31 != 0
},
)
} else {
let amount = if register_shift {
let shift_register = ((instruction >> 8) & 15) as usize;
if instruction & (1 << 7) != 0 || shift_register == 15 {
return Err(self.unsupported(instruction));
}
self.registers[shift_register] & 255
} else {
(instruction >> 7) & 31
};
alu::shift(
self.operand_register((instruction & 15) as usize, pc_offset),
Shift::decode(instruction >> 5),
amount,
self.flags.carry,
register_shift,
)
};
let (result, flags) = alu::execute(
opcode,
self.operand_register(source, pc_offset),
operand,
carry,
self.flags,
);
if let Some((status, mode)) = restore {
self.apply_status(status, mode);
} else if set_flags {
self.flags = flags;
}
let next = if !test && destination == 15 {
self.instruction_set.align(result)
} else {
self.pc().wrapping_add(4)
};
if !test && destination != 15 {
self.registers[destination] = result;
}
self.registers[15] = next;
Ok(())
}
fn single_transfer(&mut self, instruction: u32, memory: &mut Memory) -> Result<(), CpuError> {
let base_register = ((instruction >> 16) & 15) as usize;
let destination = ((instruction >> 12) & 15) as usize;
let pre = instruction & (1 << 24) != 0;
let write_bit = instruction & (1 << 21) != 0;
let write_back = !pre || write_bit;
let byte = instruction & (1 << 22) != 0;
let load = instruction & (1 << 20) != 0;
if (!pre && write_bit) // LDRT/STRT access semantics are not implemented.
|| (write_back && (base_register == 15 || (load && base_register == destination)))
|| (byte && destination == 15)
{
return Err(self.unsupported(instruction));
}
let offset = if instruction & (1 << 25) != 0 {
let rm = (instruction & 15) as usize;
if instruction & (1 << 4) != 0 || rm == 15 {
return Err(self.unsupported(instruction));
}
alu::shift(
self.registers[rm],
Shift::decode(instruction >> 5),
(instruction >> 7) & 31,
self.flags.carry,
false,
)
.0
} else {
instruction & 0xfff
};
let base = self.operand_register(base_register, 8);
let adjusted = offset_address(base, offset, instruction);
let address = if pre { adjusted } else { base };
let next = self.pc().wrapping_add(4);
// All validation precedes memory writes. CPU changes follow successful access.
let value = if load {
if byte {
u32::from(memory.read8(address)?)
} else {
memory.read32(address & !3)?.rotate_right((address & 3) * 8)
}
} else {
let value = self.operand_register(destination, 12);
if byte {
memory.write8(address, value as u8)?;
} else {
memory.write32(address & !3, value)?;
}
value
};
if write_back {
self.registers[base_register] = adjusted;
}
if load && destination != 15 {
self.registers[destination] = value;
}
// ARMv4 LDR PC does not switch to Thumb, even if bit zero is set.
self.registers[15] = if load && destination == 15 {
value & !3
} else {
next
};
Ok(())
}
fn halfword_transfer(&mut self, instruction: u32, memory: &mut Memory) -> Result<(), CpuError> {
let kind = (instruction >> 5) & 3;
let load = instruction & (1 << 20) != 0;
let pre = instruction & (1 << 24) != 0;
let write_bit = instruction & (1 << 21) != 0;
let write_back = !pre || write_bit;
let base_register = ((instruction >> 16) & 15) as usize;
let destination = ((instruction >> 12) & 15) as usize;
if kind == 0
|| (!load && kind != 1)
|| (!pre && write_bit)
|| destination == 15
|| (write_back && (base_register == 15 || (load && base_register == destination)))
{
return Err(self.unsupported(instruction));
}
let offset = if instruction & (1 << 22) != 0 {
((instruction >> 4) & 0xf0) | (instruction & 15)
} else {
let rm = (instruction & 15) as usize;
if instruction & 0xf00 != 0 || rm == 15 {
return Err(self.unsupported(instruction));
}
self.registers[rm]
};
let base = self.operand_register(base_register, 8);
let adjusted = offset_address(base, offset, instruction);
let address = if pre { adjusted } else { base };
let value = if load {
match kind {
1 => u32::from(memory.read16(address & !1)?).rotate_right((address & 1) * 8),
2 => memory.read8(address)? as i8 as i32 as u32,
_ if address & 1 != 0 => memory.read8(address)? as i8 as i32 as u32,
_ => memory.read16(address)? as i16 as i32 as u32,
}
} else {
memory.write16(address & !1, self.registers[destination] as u16)?;
0
};
if write_back {
self.registers[base_register] = adjusted;
}
if load {
self.registers[destination] = value;
}
self.registers[15] = self.pc().wrapping_add(4);
Ok(())
}
fn multiply(&mut self, instruction: u32) -> Result<(), CpuError> {
let rd = ((instruction >> 16) & 15) as usize;
let rn = ((instruction >> 12) & 15) as usize;
let rs = ((instruction >> 8) & 15) as usize;
let rm = (instruction & 15) as usize;
let accumulate = instruction & (1 << 21) != 0;
if [rd, rn, rs, rm].contains(&15) || rd == rm || (!accumulate && rn != 0) {
return Err(self.unsupported(instruction));
}
let mut result = self.registers[rm].wrapping_mul(self.registers[rs]);
if accumulate {
result = result.wrapping_add(self.registers[rn]);
}
self.registers[rd] = result;
if instruction & (1 << 20) != 0 {
self.flags.negative = result >> 31 != 0;
self.flags.zero = result == 0;
// ARM7 carry is unspecified after multiply; preserve it deterministically.
}
self.registers[15] = self.pc().wrapping_add(4);
Ok(())
}
fn multiply_long(&mut self, instruction: u32) -> Result<(), CpuError> {
let hi = ((instruction >> 16) & 15) as usize;
let lo = ((instruction >> 12) & 15) as usize;
let rs = ((instruction >> 8) & 15) as usize;
let rm = (instruction & 15) as usize;
if [hi, lo, rs, rm].contains(&15) || hi == lo || hi == rm || lo == rm {
return Err(self.unsupported(instruction));
}
let mut result = if instruction & (1 << 22) != 0 {
(i64::from(self.registers[rm] as i32) * i64::from(self.registers[rs] as i32)) as u64
} else {
u64::from(self.registers[rm]) * u64::from(self.registers[rs])
};
if instruction & (1 << 21) != 0 {
let accumulator = (u64::from(self.registers[hi]) << 32) | u64::from(self.registers[lo]);
result = result.wrapping_add(accumulator);
}
self.registers[hi] = (result >> 32) as u32;
self.registers[lo] = result as u32;
if instruction & (1 << 20) != 0 {
self.flags.negative = result >> 63 != 0;
self.flags.zero = result == 0;
// Unspecified multiply C/V outputs are preserved, not hardware-verified.
}
self.registers[15] = self.pc().wrapping_add(4);
Ok(())
}
}
fn offset_address(base: u32, offset: u32, instruction: u32) -> u32 {
if instruction & (1 << 23) != 0 {
base.wrapping_add(offset)
} else {
base.wrapping_sub(offset)
}
}

View file

@ -0,0 +1,67 @@
//! Exception entry. Memory errors remain diagnostics rather than hardware aborts.
use super::{Cpu, InstructionSet, Mode};
/// ARM7 exception sources. GBA hardware does not expose every source.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Exception {
UndefinedInstruction,
SoftwareInterrupt,
PrefetchAbort,
DataAbort,
Irq,
Fiq,
}
impl Cpu {
/// Start at the reset vector in ARM Supervisor mode, with IRQ/FIQ disabled.
/// Registers and RAM are not initialized as a Nintendo BIOS would initialize them.
pub fn at_reset() -> Self {
let mut cpu = Self::new(0);
cpu.mode = Mode::Supervisor;
cpu.irq_disabled = true;
cpu.fiq_disabled = true;
cpu
}
/// Enter an exception explicitly, regardless of interrupt masks.
/// For synchronous exceptions, PC must identify the faulting instruction.
/// For IRQ/FIQ, PC must identify the next instruction, between CPU steps.
/// This does not execute a BIOS service or fetch the vector instruction.
pub fn enter_exception(&mut self, exception: Exception) {
let (mode, vector, offset) = match exception {
Exception::UndefinedInstruction => {
(Mode::Undefined, 0x04, self.instruction_set.width())
}
Exception::SoftwareInterrupt => (Mode::Supervisor, 0x08, self.instruction_set.width()),
Exception::PrefetchAbort => (Mode::Abort, 0x0c, 4),
Exception::DataAbort => (Mode::Abort, 0x10, 8),
Exception::Irq => (Mode::Irq, 0x18, 4),
Exception::Fiq => (Mode::Fiq, 0x1c, 4),
};
let previous = self.cpsr();
let link = self.pc().wrapping_add(offset);
self.switch_mode(mode);
self.set_spsr(previous);
self.registers[14] = link;
self.registers[15] = vector;
self.instruction_set = InstructionSet::Arm;
self.irq_disabled = true;
if exception == Exception::Fiq {
self.fiq_disabled = true;
}
}
/// Sample external interrupt lines between instructions. FIQ has priority.
/// Returns true when an unmasked interrupt was taken. Machine connects the GBA IRQ line.
pub fn take_interrupt(&mut self, irq: bool, fiq: bool) -> bool {
if fiq && !self.fiq_disabled {
self.enter_exception(Exception::Fiq);
} else if irq && !self.irq_disabled {
self.enter_exception(Exception::Irq);
} else {
return false;
}
true
}
}

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

View file

@ -0,0 +1,560 @@
use super::{
alu::{self, Shift},
*,
};
use crate::memory::ROM_START;
const RAM: u32 = 0x0200_0000;
fn program(words: &[u32]) -> Memory {
Memory::new(words.iter().flat_map(|word| word.to_le_bytes()).collect()).unwrap()
}
#[test]
fn register_shifts_match_a_bit_at_a_time_reference() {
for value in [0, 1, 0x8000_0000, 0x8000_0001, 0x1234_5678, u32::MAX] {
for kind in [Shift::Lsl, Shift::Lsr, Shift::Asr, Shift::Ror] {
for incoming_carry in [false, true] {
for amount in 0..=255 {
let mut expected = value;
let mut carry = incoming_carry;
for _ in 0..amount {
match kind {
Shift::Lsl => {
carry = expected >> 31 != 0;
expected <<= 1;
}
Shift::Lsr => {
carry = expected & 1 != 0;
expected >>= 1;
}
Shift::Asr => {
carry = expected & 1 != 0;
expected = ((expected as i32) >> 1) as u32;
}
Shift::Ror => {
carry = expected & 1 != 0;
expected = (expected >> 1) | (u32::from(carry) << 31);
}
}
}
assert_eq!(
alu::shift(value, kind, amount, incoming_carry, true),
(expected, carry),
"value={value:08x} kind={kind:?} amount={amount}"
);
}
}
}
}
}
#[test]
fn immediate_zero_shifts_have_arm_special_meanings() {
for carry in [false, true] {
let value = 0x8000_0001;
assert_eq!(
alu::shift(value, Shift::Lsl, 0, carry, false),
(value, carry)
);
assert_eq!(alu::shift(value, Shift::Lsr, 0, carry, false), (0, true));
assert_eq!(
alu::shift(value, Shift::Asr, 0, carry, false),
(u32::MAX, true)
);
assert_eq!(
alu::shift(value, Shift::Ror, 0, carry, false),
(0x4000_0000 | (u32::from(carry) << 31), true)
);
}
assert_eq!(alu::shift(1, Shift::Asr, 0, true, false), (0, false));
}
#[test]
fn register_shift_amount_uses_only_the_low_byte() {
assert_eq!(alu::shift(3, Shift::Lsl, 256, true, true), (3, true));
assert_eq!(alu::shift(3, Shift::Lsl, 257, true, true), (6, false));
let mut memory = program(&[0xe1b0_0311]); // MOVS r0,r1,LSL r3
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = 3;
cpu.registers[3] = 257;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], 6);
}
#[test]
fn all_data_processing_opcodes_decode_and_preserve_test_destinations() {
let expected = [
0x10,
0x26,
0x22,
0xffff_ffde,
0x46,
0x47,
0x22,
0xffff_ffde,
0x10,
0x26,
0x22,
0x46,
0x36,
0x12,
0x24,
0xffff_ffed,
];
for (opcode, result) in expected.into_iter().enumerate() {
// opcode{S} r2,r0,r1. Test instructions ignore the destination.
let instruction = 0xe010_2001 | ((opcode as u32) << 21);
let mut memory = program(&[instruction]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = 0x34;
cpu.registers[1] = 0x12;
cpu.registers[2] = 0xfeed_face;
cpu.flags.carry = true;
cpu.step(&mut memory).unwrap();
assert_eq!(
cpu.registers[2],
if (8..=11).contains(&opcode) {
0xfeed_face
} else {
result
}
);
assert_eq!(cpu.flags.negative, result >> 31 != 0);
assert_eq!(cpu.flags.zero, result == 0);
assert_eq!(cpu.pc(), ROM_START + 4);
}
}
#[test]
fn arithmetic_with_carry_matches_wide_signed_and_unsigned_reference() {
let values = [0, 1, 0x7fff_ffff, 0x8000_0000, 0x8000_0001, u32::MAX];
for opcode in [2, 3, 4, 5, 6, 7, 10, 11] {
let mut memory = program(&[0xe010_2001 | (opcode << 21)]);
for a in values {
for b in values {
for carry in [false, true] {
let subtract = matches!(opcode, 2 | 3 | 6 | 7 | 10);
let reverse = matches!(opcode, 3 | 7);
let (a, b) = if reverse { (b, a) } else { (a, b) };
let extra = match opcode {
5 => i64::from(carry),
6 | 7 => i64::from(carry) - 1,
_ => 0,
};
let wide = if subtract {
i64::from(a) - i64::from(b) + extra
} else {
i64::from(a) + i64::from(b) + extra
};
let signed = if subtract {
i64::from(a as i32) - i64::from(b as i32) + extra
} else {
i64::from(a as i32) + i64::from(b as i32) + extra
};
let mut cpu = Cpu::new(ROM_START);
(cpu.registers[0], cpu.registers[1]) = if reverse { (b, a) } else { (a, b) };
cpu.flags.carry = carry;
cpu.step(&mut memory).unwrap();
if opcode < 8 {
assert_eq!(cpu.registers[2], wide as u32);
}
assert_eq!(
cpu.flags,
Flags {
negative: (wide as u32) >> 31 != 0,
zero: wide as u32 == 0,
carry: if subtract {
wide >= 0
} else {
wide > i64::from(u32::MAX)
},
overflow: i32::try_from(signed).is_err(),
},
"opcode={opcode:x} a={a:08x} b={b:08x} carry={carry}"
);
}
}
}
}
}
#[test]
fn logical_ops_use_shifter_carry_and_preserve_overflow() {
for opcode in [0, 1, 8, 9, 12, 13, 14, 15] {
let mut memory = program(&[0xe010_2081 | (opcode << 21)]); // r1 LSL #1
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = 0x8000_0001;
cpu.flags.overflow = true;
cpu.step(&mut memory).unwrap();
assert!(cpu.flags.carry);
assert!(cpu.flags.overflow);
}
}
#[test]
fn arithmetic_uses_old_carry_not_shifter_carry() {
let mut memory = program(&[0xe0b0_2081]); // ADCS r2,r0,r1,LSL #1
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = 0x8000_0000; // shift produces carry=1 and value=0
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[2], 0);
assert!(!cpu.flags.carry);
assert!(cpu.flags.zero);
}
#[test]
fn non_flag_setting_register_ops_preserve_flags() {
for opcode in [0, 1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15] {
let mut memory = program(&[0xe000_2081 | (opcode << 21)]);
let mut cpu = Cpu::new(ROM_START);
cpu.flags = Flags {
negative: true,
zero: true,
carry: true,
overflow: true,
};
let before = cpu.flags;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.flags, before);
}
}
#[test]
fn register_specified_shifts_read_pc_plus_twelve_for_both_operands() {
for (instruction, expected) in [
(0xe1a0_021f, ROM_START + 12), // MOV r0,pc,LSL r2
(0xe08f_0211, ROM_START + 12), // ADD r0,pc,r1,LSL r2
(0xe1a0_000f, ROM_START + 8), // MOV r0,pc (immediate zero shift)
] {
let mut memory = program(&[instruction]);
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], expected);
}
}
#[test]
fn data_processing_pc_destination_aligns_without_switching_state() {
let mut memory = program(&[0xe1a0_f000]); // MOV pc,r0
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM + 3;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), RAM);
}
#[test]
fn branch_link_and_bx_return_execute_a_subroutine() {
let mut memory = program(&[
0xe3a0_0006, // MOV r0,#6
0xe3a0_1007, // MOV r1,#7
0xeb00_0001, // BL subroutine at +20
0xeaff_fffe, // B .
0xe1a0_0000, // NOP, skipped
0xe002_0190, // MUL r2,r0,r1
0xe12f_ff1e, // BX lr
]);
let mut cpu = Cpu::new(ROM_START);
for _ in 0..6 {
cpu.step(&mut memory).unwrap();
}
assert_eq!(cpu.registers[2], 42);
assert_eq!(cpu.registers[14], ROM_START + 12);
assert_eq!(cpu.pc(), ROM_START + 12);
}
#[test]
fn bx_thumb_target_switches_state_and_clears_only_bit_zero() {
let mut memory = program(&[0xe12f_ff10]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = ROM_START + 11;
let mut expected = cpu.registers;
expected[15] = ROM_START + 10;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
assert_eq!(cpu.registers, expected);
assert_eq!(cpu.flags, Flags::default());
}
#[test]
fn skipped_branch_link_does_not_change_lr() {
let mut memory = program(&[0x0b00_0000]); // BLEQ with Z clear
let mut cpu = Cpu::new(ROM_START);
cpu.registers[14] = 123;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[14], 123);
assert_eq!(cpu.pc(), ROM_START + 4);
}
#[test]
fn word_transfers_cover_pre_post_up_down_and_writeback() {
for pre in [false, true] {
for up in [false, true] {
for explicit_writeback in [false, true] {
if !pre && explicit_writeback {
continue;
} // T form is intentionally unsupported.
let fields = (u32::from(pre) << 24)
| (u32::from(up) << 23)
| (u32::from(explicit_writeback) << 21);
let mut memory = program(&[0xe400_1004 | fields, 0xe410_2004 | fields]);
let mut cpu = Cpu::new(ROM_START);
let base = RAM + 16;
let adjusted = if up { base + 4 } else { base - 4 };
let address = if pre { adjusted } else { base };
cpu.registers[0] = base;
cpu.registers[1] = 0x1234_5678;
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read32(address).unwrap(), 0x1234_5678);
assert_eq!(
cpu.registers[0],
if !pre || explicit_writeback {
adjusted
} else {
base
}
);
cpu.registers[0] = base;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[2], 0x1234_5678);
assert_eq!(
cpu.registers[0],
if !pre || explicit_writeback {
adjusted
} else {
base
}
);
}
}
}
}
#[test]
fn byte_transfers_zero_extend_and_preserve_neighbors() {
let mut memory = program(&[0xe5c0_1001, 0xe5d0_2001]); // STRB r1,[r0,#1]; LDRB r2,[r0,#1]
memory.write32(RAM, 0xffff_ffff).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.registers[1] = 0x1234_5680;
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[2], 0x80);
assert_eq!(memory.read32(RAM).unwrap(), 0xffff_80ff);
}
#[test]
fn word_loads_rotate_and_stores_align_down() {
for offset in 0..4 {
let mut memory = program(&[0xe590_1000, 0xe580_2000]);
memory.write32(RAM, 0x1234_5678).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM + offset;
cpu.registers[2] = 0xaabb_ccdd;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 0x1234_5678_u32.rotate_right(offset * 8));
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read32(RAM).unwrap(), 0xaabb_ccdd);
}
}
#[test]
fn register_offset_load_uses_shifter_without_changing_flags() {
let mut memory = program(&[0xe790_1102]); // LDR r1,[r0,r2,LSL #2]
memory.write32(RAM + 12, 42).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.registers[2] = 3;
cpu.flags.carry = true;
let flags = cpu.flags;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 42);
assert_eq!(cpu.flags, flags);
}
#[test]
fn pc_relative_load_and_pc_store_use_different_pipeline_offsets() {
let mut memory = program(&[0xe59f_1000, 0xe580_f000, 42]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 42);
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read32(RAM).unwrap(), ROM_START + 16);
}
#[test]
fn ldr_pc_aligns_target_without_thumb_exchange() {
let mut memory = program(&[0xe590_f000]);
memory.write32(RAM, ROM_START + 15).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START + 12);
}
#[test]
fn store_base_with_writeback_stores_original_base_value() {
let mut memory = program(&[0xe480_0004]); // STR r0,[r0],#4
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read32(RAM).unwrap(), RAM);
assert_eq!(cpu.registers[0], RAM + 4);
}
#[test]
fn halfword_and_signed_loads_handle_arm7_odd_addresses() {
for (instruction, offset, expected) in [
(0xe1d0_10b0, 0, 0x80fe), // LDRH
(0xe1d0_10b0, 1, 0xfe00_0080), // odd LDRH rotates in 32 bits
(0xe1d0_10d0, 0, 0xffff_fffe), // LDRSB
(0xe1d0_10f0, 0, 0xffff_80fe), // LDRSH
(0xe1d0_10f0, 1, 0xffff_ff80), // odd LDRSH becomes LDRSB
] {
let mut memory = program(&[instruction]);
memory.write16(RAM, 0x80fe).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM + offset;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], expected);
}
}
#[test]
fn halfword_store_aligns_down_and_updates_the_unaligned_base() {
let mut memory = program(&[0xe0c0_10b2]); // STRH r1,[r0],#2
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM + 1;
cpu.registers[1] = 0x1234_abcd;
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read16(RAM).unwrap(), 0xabcd);
assert_eq!(cpu.registers[0], RAM + 3);
}
#[test]
fn halfword_register_offsets_and_split_immediate_are_decoded() {
for (instruction, offset) in [(0xe190_10b2, 6), (0xe1d0_12b4, 0x24)] {
let mut memory = program(&[instruction]);
memory.write16(RAM + offset, 0x1234).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.registers[2] = 6;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 0x1234);
}
}
#[test]
fn multiply_and_accumulate_wrap_and_set_only_defined_flags() {
for (instruction, expected) in [(0xe012_0190, u32::MAX - 1), (0xe032_3190, 3)] {
let mut memory = program(&[instruction]); // MULS/MLAS r2,r0,r1[,r3]
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = u32::MAX;
cpu.registers[1] = 2;
cpu.registers[3] = 5;
cpu.flags.carry = true;
cpu.flags.overflow = true;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[2], expected);
assert_eq!(cpu.flags.negative, expected >> 31 != 0);
assert_eq!(cpu.flags.zero, expected == 0);
assert!(cpu.flags.carry); // Deterministic policy for unspecified output.
assert!(cpu.flags.overflow);
}
}
#[test]
fn long_multiply_supports_signed_unsigned_and_accumulation() {
for (instruction, expected) in [
(0xe093_2190, 0x0000_0001_ffff_fffe_u64), // UMULLS r2,r3,r0,r1
(0xe0b3_2190, 0x0000_0002_0000_0003), // UMLALS (+5)
(0xe0d3_2190, 0xffff_ffff_ffff_fffe), // SMULLS (-1 * 2)
(0xe0f3_2190, 3), // SMLALS (-2 + 5)
] {
let mut memory = program(&[instruction]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = u32::MAX;
cpu.registers[1] = 2;
cpu.registers[2] = 5;
cpu.step(&mut memory).unwrap();
assert_eq!(
(u64::from(cpu.registers[3]) << 32) | u64::from(cpu.registers[2]),
expected
);
assert_eq!(cpu.flags.negative, expected >> 63 != 0);
assert_eq!(cpu.flags.zero, expected == 0);
}
}
#[test]
fn long_multiply_zero_flag_uses_both_halves() {
let mut memory = program(&[0xe093_2190]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = 0x8000_0000;
cpu.registers[1] = 2;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[2], 0);
assert_eq!(cpu.registers[3], 1);
assert!(!cpu.flags.zero);
}
#[test]
fn invalid_or_unimplemented_encodings_do_not_change_state_or_memory() {
for instruction in [
0xe1a0_0f11, // Rs=pc (register shift)
0xe3b0_f001, // status restore via MOVS pc
0xe490_0004, // LDR r0,[r0],#4 (base/destination overlap)
0xe5bf_1000, // LDR with PC writeback
0xe4b0_1004, // LDRT user-mode transfer
0xe5d0_f000, // LDRB pc
0xe790_1012, // illegal register-specified memory shift
0xe790_100f, // memory offset register PC
0xe1c0_10d0, // ARMv5 doubleword load, not an ARM7 signed store
0xe000_0190, // MUL Rd=Rm
0xe082_2190, // UMULL RdHi=RdLo
0xe8f0_0001, // LDM user-bank transfer with writeback
0xe100_f090, // SWP with PC destination
0xe12f_ff30, // BLX, ARMv5 only
] {
let mut memory = program(&[instruction]);
memory.write32(RAM, 0xdead_beef).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.registers[1] = 123;
let before = cpu.registers;
let flags = cpu.flags;
assert!(
matches!(
cpu.step(&mut memory),
Err(CpuError::UnsupportedInstruction { .. })
),
"{instruction:08x}"
);
assert_eq!(cpu.registers, before);
assert_eq!(cpu.flags, flags);
assert_eq!(memory.read32(RAM).unwrap(), 0xdead_beef);
}
}
#[test]
fn failed_memory_access_does_not_commit_writeback_or_destination() {
for instruction in [0xe490_1004, 0xe480_1004, 0xe0d0_10b2, 0xe0c0_10b2] {
let mut memory = program(&[instruction]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = 123;
let before = cpu.registers;
assert!(matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))));
assert_eq!(cpu.registers, before);
}
}
#[test]
fn conditional_store_is_skipped_without_touching_memory() {
let mut memory = program(&[0x0580_1000]); // STREQ with Z clear
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.registers[1] = 123;
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read32(RAM).unwrap(), 0);
}

View file

@ -0,0 +1,183 @@
//! ARM7 processor modes, register banks, and status-register transfers.
use super::{Cpu, CpuError, Flags, InstructionSet};
/// ARMv4T processor mode. User and System share a register bank.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum Mode {
User = 0x10,
Fiq = 0x11,
Irq = 0x12,
Supervisor = 0x13,
Abort = 0x17,
Undefined = 0x1b,
System = 0x1f,
}
impl Mode {
fn decode(status: u32) -> Option<Self> {
Some(match status & 31 {
0x10 => Self::User,
0x11 => Self::Fiq,
0x12 => Self::Irq,
0x13 => Self::Supervisor,
0x17 => Self::Abort,
0x1b => Self::Undefined,
0x1f => Self::System,
_ => return None,
})
}
fn bank(self) -> usize {
match self {
Self::User | Self::System => 0,
Self::Fiq => 1,
Self::Irq => 2,
Self::Supervisor => 3,
Self::Abort => 4,
Self::Undefined => 5,
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(super) struct Banks {
high: [[u32; 5]; 2], // r8-r12: shared and FIQ
sp_lr: [[u32; 2]; 6],
spsr: [u32; 5],
}
impl Cpu {
pub fn mode(&self) -> Mode {
self.mode
}
/// Current program status register. Reserved bits read as zero.
pub fn cpsr(&self) -> u32 {
(u32::from(self.flags.negative) << 31)
| (u32::from(self.flags.zero) << 30)
| (u32::from(self.flags.carry) << 29)
| (u32::from(self.flags.overflow) << 28)
| (u32::from(self.irq_disabled) << 7)
| (u32::from(self.fiq_disabled) << 6)
| (u32::from(self.instruction_set == InstructionSet::Thumb) << 5)
| self.mode as u32
}
/// Saved program status register. User and System have no SPSR.
pub fn spsr(&self) -> Option<u32> {
self.mode
.bank()
.checked_sub(1)
.map(|bank| self.banks.spsr[bank])
}
pub(super) fn set_spsr(&mut self, value: u32) {
self.banks.spsr[self.mode.bank() - 1] = value;
}
pub(super) fn switch_mode(&mut self, mode: Mode) {
// Save the active bank before loading the new one. User/System share it.
self.banks.high[usize::from(self.mode == Mode::Fiq)]
.copy_from_slice(&self.registers[8..13]);
self.banks.sp_lr[self.mode.bank()].copy_from_slice(&self.registers[13..15]);
self.registers[8..13].copy_from_slice(&self.banks.high[usize::from(mode == Mode::Fiq)]);
self.registers[13..15].copy_from_slice(&self.banks.sp_lr[mode.bank()]);
self.mode = mode;
}
// Callers validate the mode before committing any CPU or memory changes.
pub(super) fn apply_status(&mut self, value: u32, mode: Mode) {
self.switch_mode(mode);
self.flags = Flags {
negative: value & (1 << 31) != 0,
zero: value & (1 << 30) != 0,
carry: value & (1 << 29) != 0,
overflow: value & (1 << 28) != 0,
};
self.irq_disabled = value & (1 << 7) != 0;
self.fiq_disabled = value & (1 << 6) != 0;
self.instruction_set = if value & (1 << 5) == 0 {
InstructionSet::Arm
} else {
InstructionSet::Thumb
};
}
pub(super) fn return_status(&self, instruction: u32) -> Result<(u32, Mode), CpuError> {
let value = self.spsr().ok_or_else(|| self.unsupported(instruction))?;
let mode = Mode::decode(value).ok_or_else(|| self.unsupported(instruction))?;
Ok((value, mode))
}
pub(super) fn user_register(&self, register: usize) -> u32 {
match register {
8..=12 if self.mode == Mode::Fiq => self.banks.high[0][register - 8],
13..=14 if self.mode.bank() != 0 => self.banks.sp_lr[0][register - 13],
_ => self.operand_register(register, 12),
}
}
pub(super) fn set_user_register(&mut self, register: usize, value: u32) {
match register {
8..=12 if self.mode == Mode::Fiq => self.banks.high[0][register - 8] = value,
13..=14 if self.mode.bank() != 0 => self.banks.sp_lr[0][register - 13] = value,
_ => self.registers[register] = value,
}
}
pub(super) fn read_status(&mut self, instruction: u32) -> Result<(), CpuError> {
let destination = ((instruction >> 12) & 15) as usize;
if destination == 15 {
return Err(self.unsupported(instruction));
}
let value = if instruction & (1 << 22) == 0 {
self.cpsr()
} else {
self.spsr().ok_or_else(|| self.unsupported(instruction))?
};
self.registers[destination] = value;
self.registers[15] = self.pc().wrapping_add(4);
Ok(())
}
pub(super) fn write_status(&mut self, instruction: u32) -> Result<(), CpuError> {
let value = if instruction & (1 << 25) != 0 {
(instruction & 255).rotate_right(((instruction >> 8) & 15) * 2)
} else {
let source = (instruction & 15) as usize;
if source == 15 {
return Err(self.unsupported(instruction));
}
self.registers[source]
};
let saved = instruction & (1 << 22) != 0;
let previous = if saved {
self.spsr().ok_or_else(|| self.unsupported(instruction))?
} else {
self.cpsr()
};
let mut mask = 0;
if instruction & (1 << 19) != 0 {
mask |= 0xf000_0000;
}
if instruction & (1 << 16) != 0 && (saved || self.mode != Mode::User) {
mask |= 0xff;
}
let next = (previous & !mask) | (value & mask);
if saved {
// Invalid mode bits can be stored, but cannot be used for a return.
self.set_spsr(next);
} else {
let mode = Mode::decode(next).ok_or_else(|| self.unsupported(instruction))?;
// ARM7 forbids changing T through MSR CPSR. Diagnose that form.
if (previous ^ next) & (1 << 5) != 0 {
return Err(self.unsupported(instruction));
}
self.apply_status(next, mode);
}
self.registers[15] = self.pc().wrapping_add(4);
Ok(())
}
}

View file

@ -0,0 +1,195 @@
use super::*;
use crate::memory::ROM_START;
const MODES: [Mode; 7] = [
Mode::User,
Mode::Fiq,
Mode::Irq,
Mode::Supervisor,
Mode::Abort,
Mode::Undefined,
Mode::System,
];
fn program(instruction: u32) -> Memory {
Memory::new(instruction.to_le_bytes().to_vec()).unwrap()
}
#[test]
fn direct_start_and_reset_have_explicit_status() {
let cpu = Cpu::new(ROM_START);
assert_eq!(cpu.mode(), Mode::System);
assert_eq!(cpu.cpsr(), 0x1f);
assert_eq!(cpu.spsr(), None);
let cpu = Cpu::at_reset();
assert_eq!(cpu.pc(), 0);
assert_eq!(cpu.cpsr(), 0xd3);
assert_eq!(cpu.spsr(), Some(0));
}
#[test]
fn every_mode_has_the_correct_shared_and_private_registers() {
let mut cpu = Cpu::new(ROM_START);
for register in 0..15 {
cpu.registers[register] = register as u32 + 100;
}
// Initialize all five exception banks independently.
for (index, mode) in MODES[1..6].iter().copied().enumerate() {
cpu.switch_mode(mode);
assert_eq!(
cpu.registers[0..8],
[100, 101, 102, 103, 104, 105, 106, 107]
);
if mode == Mode::Fiq {
assert_eq!(cpu.registers[8..13], [0; 5]);
cpu.registers[8..13].copy_from_slice(&[208, 209, 210, 211, 212]);
} else {
assert_eq!(cpu.registers[8..13], [108, 109, 110, 111, 112]);
}
assert_eq!(cpu.registers[13..15], [0, 0]);
cpu.registers[13] = 300 + index as u32;
cpu.registers[14] = 400 + index as u32;
cpu.set_spsr(500 + index as u32);
}
for (index, mode) in MODES[1..6].iter().copied().enumerate() {
cpu.switch_mode(mode);
assert_eq!(
cpu.registers[13..15],
[300 + index as u32, 400 + index as u32]
);
assert_eq!(cpu.spsr(), Some(500 + index as u32));
}
cpu.switch_mode(Mode::User);
assert_eq!(cpu.registers[8..15], [108, 109, 110, 111, 112, 113, 114]);
cpu.registers[13] = 999;
cpu.switch_mode(Mode::System);
assert_eq!(cpu.registers[13], 999);
assert_eq!(cpu.spsr(), None);
cpu.switch_mode(Mode::Fiq);
assert_eq!(cpu.registers[8..13], [208, 209, 210, 211, 212]);
}
#[test]
fn cpsr_round_trips_all_modes_flags_masks_and_states() {
let mut cpu = Cpu::new(ROM_START);
for mode in MODES {
for flags in 0..16 {
for control in 0..8 {
let value = (flags << 28) | (control << 5) | mode as u32;
cpu.apply_status(value, mode);
assert_eq!(cpu.cpsr(), value);
assert_eq!(cpu.mode(), mode);
}
}
}
}
#[test]
fn mrs_reads_current_and_saved_status() {
let mut cpu = Cpu::new(ROM_START);
cpu.apply_status(0xa000_00d3, Mode::Supervisor);
cpu.set_spsr(0x5000_0030);
let previous = cpu.cpsr();
cpu.step(&mut program(0xe10f_0000)).unwrap(); // MRS r0, CPSR
assert_eq!(cpu.registers[0], previous);
assert_eq!(cpu.cpsr(), previous);
cpu.registers[15] = ROM_START;
cpu.step(&mut program(0xe14f_1000)).unwrap(); // MRS r1, SPSR
assert_eq!(cpu.registers[1], 0x5000_0030);
assert_eq!(cpu.pc(), ROM_START + 4);
}
#[test]
fn msr_register_and_immediate_write_selected_fields_only() {
for fields in 0..16 {
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = 0xffff_ffd3; // reserved bits are ignored
cpu.step(&mut program(0xe120_f000 | (fields << 16)))
.unwrap();
let expected = if fields & 1 != 0 { 0xd3 } else { 0x1f }
| if fields & 8 != 0 { 0xf000_0000 } else { 0 };
assert_eq!(cpu.cpsr(), expected);
}
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut program(0xe328_f480)).unwrap(); // MSR CPSR_f, #0x80000000
assert_eq!(cpu.cpsr(), 0x8000_001f);
cpu.registers[15] = ROM_START;
cpu.step(&mut program(0xe321_f0d3)).unwrap(); // MSR CPSR_c, #0xd3
assert_eq!(cpu.cpsr(), 0x8000_00d3);
}
#[test]
fn msr_reads_old_banked_operand_before_switching_mode() {
let mut cpu = Cpu::new(ROM_START);
cpu.registers[13] = 0xd3;
cpu.step(&mut program(0xe121_f00d)).unwrap(); // MSR CPSR_c, sp
assert_eq!(cpu.mode(), Mode::Supervisor);
assert_eq!(cpu.registers[13], 0);
cpu.switch_mode(Mode::System);
assert_eq!(cpu.registers[13], 0xd3);
}
#[test]
fn user_msr_changes_flags_but_cannot_change_control() {
let mut cpu = Cpu::new(ROM_START);
cpu.switch_mode(Mode::User);
cpu.registers[0] = 0xf000_00ff; // Includes T, invalid mode, and interrupt masks
cpu.step(&mut program(0xe12f_f000)).unwrap();
assert_eq!(cpu.cpsr(), 0xf000_0010);
}
#[test]
fn msr_spsr_accepts_thumb_and_preserves_unselected_fields() {
for fields in 0..16 {
let mut cpu = Cpu::new(ROM_START);
cpu.switch_mode(Mode::Supervisor);
cpu.set_spsr(0x2000_001f);
cpu.registers[0] = 0x9fff_fff0;
cpu.step(&mut program(0xe160_f000 | (fields << 16)))
.unwrap();
assert_eq!(cpu.cpsr(), 0x13);
let expected = if fields & 1 != 0 { 0xf0 } else { 0x1f }
| if fields & 8 != 0 {
0x9000_0000
} else {
0x2000_0000
};
assert_eq!(cpu.spsr(), Some(expected));
}
let mut cpu = Cpu::at_reset();
cpu.registers[15] = ROM_START;
cpu.step(&mut program(0xe361_f03f)).unwrap(); // MSR SPSR_c, #0x3f
assert_eq!(cpu.spsr(), Some(0x3f));
}
#[test]
fn invalid_status_transfers_preserve_all_banks() {
for instruction in [
0xe10f_f000, // MRS pc, CPSR
0xe14f_0000, // MRS r0, SPSR without an SPSR
0xe161_f000, // MSR SPSR_c, r0 without an SPSR
0xe121_f00f, // MSR CPSR_c, pc
0xe321_f000, // Invalid mode
0xe321_f03f, // Attempt to change T
] {
let mut cpu = Cpu::new(ROM_START);
let before = cpu.clone();
assert!(
cpu.step(&mut program(instruction)).is_err(),
"{instruction:08x}"
);
assert_eq!(cpu, before);
}
}
#[test]
fn conditional_status_transfers_do_not_execute_when_condition_fails() {
let mut cpu = Cpu::new(ROM_START);
for instruction in [0x010f_0000, 0x0121_f000, 0x0161_f000] {
cpu.registers[15] = ROM_START;
let mut expected = cpu.clone();
expected.registers[15] += 4;
cpu.step(&mut program(instruction)).unwrap();
assert_eq!(cpu, expected);
}
}

View file

@ -0,0 +1,210 @@
use super::*;
use crate::memory::ROM_START;
fn program(words: &[u32]) -> Memory {
Memory::new(words.iter().flat_map(|word| word.to_le_bytes()).collect()).unwrap()
}
fn flags_from_bits(bits: u32) -> Flags {
Flags {
negative: bits & 8 != 0,
zero: bits & 4 != 0,
carry: bits & 2 != 0,
overflow: bits & 1 != 0,
}
}
#[test]
fn all_conditions_execute_or_skip_for_every_flag_combination() {
// Each bit marks a passing NZCV combination, indexed as N*8+Z*4+C*2+V.
// Conditions in order: EQ NE CS CC MI PL VS VC HI LS GE LT GT LE AL.
let truth_tables = [
0xf0f0_u16, 0x0f0f, 0xcccc, 0x3333, 0xff00, 0x00ff, 0xaaaa, 0x5555, 0x0c0c, 0xf3f3, 0xaa55,
0x55aa, 0x0a05, 0xf5fa, 0xffff,
];
for (condition, truth_table) in truth_tables.into_iter().enumerate() {
for instruction in [0x03a0_002a, 0x0a00_0000] {
// MOV r0, #42; B +8
let mut memory = program(&[instruction | ((condition as u32) << 28)]);
for bits in 0..16 {
let mut cpu = Cpu::new(ROM_START);
cpu.flags = flags_from_bits(bits);
let before = cpu.flags();
let passes = truth_table & (1 << bits) != 0;
cpu.step(&mut memory).unwrap();
let branch = instruction == 0x0a00_0000;
assert_eq!(cpu.registers()[0], if passes && !branch { 42 } else { 0 });
assert_eq!(cpu.pc(), ROM_START + if passes && branch { 8 } else { 4 });
assert_eq!(cpu.flags(), before);
}
}
}
}
#[test]
fn arithmetic_flags_match_wide_integer_reference() {
// Exercise every rotated-immediate encoding against boundary operands.
let operands = [
0,
1,
127,
255,
0x7fff_fffe,
0x7fff_ffff,
0x8000_0000,
0x8000_0001,
u32::MAX,
];
let address = 0x0200_0000;
let mut memory = Memory::new(vec![]).unwrap();
for opcode in [0x2, 0x4, 0xa] {
for encoding in 0..4096_u32 {
let immediate = (encoding & 0xff).rotate_right(((encoding >> 8) & 15) * 2);
// CMP has no destination; ADD/SUB write r1 from r0.
let destination = if opcode == 0xa { 0 } else { 1 };
let instruction = 0xe210_0000 | (opcode << 21) | (destination << 12) | encoding;
for (offset, byte) in instruction.to_le_bytes().into_iter().enumerate() {
memory.write8(address + offset as u32, byte).unwrap();
}
for operand in operands {
let mut cpu = Cpu::new(address);
cpu.registers[0] = operand;
cpu.registers[1] = 0x1234_5678;
cpu.flags = flags_from_bits(15);
let mut expected_registers = *cpu.registers();
let (unsigned_result, signed_result, carry) = if opcode == 0x4 {
let wide = u64::from(operand) + u64::from(immediate);
(
wide as u32,
i64::from(operand as i32) + i64::from(immediate as i32),
wide > u64::from(u32::MAX),
)
} else {
let wide = i64::from(operand) - i64::from(immediate);
(
wide as u32,
i64::from(operand as i32) - i64::from(immediate as i32),
operand >= immediate,
)
};
if opcode != 0xa {
expected_registers[1] = unsigned_result;
}
expected_registers[15] += 4;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers(), &expected_registers);
assert_eq!(
cpu.flags(),
Flags {
negative: unsigned_result >> 31 != 0,
zero: unsigned_result == 0,
carry,
overflow: i32::try_from(signed_result).is_err(),
},
"opcode={opcode:x} operand={operand:08x} encoding={encoding:03x}"
);
}
}
}
}
#[test]
fn movs_uses_shifter_carry_and_preserves_overflow() {
for (instruction, result, shifted_carry) in [
(0xe3b0_0000, 0, None), // MOVS r0, #0, no rotation
(0xe3b0_00ff, 255, None), // MOVS r0, #255, no rotation
(0xe3b0_0480, 0x8000_0000, Some(true)), // rotated immediate
(0xe3b0_0401, 0x0100_0000, Some(false)),
(0xe3b0_0400, 0, Some(false)), // rotated zero still clears carry
] {
let mut memory = program(&[instruction]);
for bits in 0..16 {
let mut cpu = Cpu::new(ROM_START);
cpu.flags = flags_from_bits(bits);
let before = cpu.flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[0], result);
assert_eq!(
cpu.flags(),
Flags {
negative: result >> 31 != 0,
zero: result == 0,
carry: shifted_carry.unwrap_or(before.carry),
overflow: before.overflow,
}
);
}
}
}
#[test]
fn instructions_without_s_preserve_all_flags() {
for instruction in [0xe3a0_0480, 0xe280_0001, 0xe240_0001] {
let mut memory = program(&[instruction]);
for bits in 0..16 {
let mut cpu = Cpu::new(ROM_START);
cpu.flags = flags_from_bits(bits);
let before = cpu.flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.flags(), before);
}
}
}
#[test]
fn failed_condition_skips_flag_updates_and_unsupported_opcodes() {
for instruction in [0x03b0_0000, 0x0f00_0000, 0x0350_0000] {
// EQ is false, so MOVS, SWI, and CMP must all be skipped.
let mut memory = program(&[instruction]);
let mut cpu = Cpu::new(ROM_START);
cpu.flags = flags_from_bits(11); // N=1 Z=0 C=1 V=1
let before = cpu.flags();
let mut registers = *cpu.registers();
registers[15] += 4;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers(), &registers);
assert_eq!(cpu.flags(), before);
}
}
#[test]
fn errors_preserve_nonzero_flags_and_registers() {
for instruction in [0xe3b0_f001, 0xe10f_f000, 0xf3a0_0001, 0xe340_0001] {
let mut memory = program(&[instruction]);
let mut cpu = Cpu::new(ROM_START);
cpu.flags = flags_from_bits(15);
cpu.registers[0] = 123;
let before = *cpu.registers();
let flags = cpu.flags();
assert!(matches!(
cpu.step(&mut memory),
Err(CpuError::UnsupportedInstruction { .. })
));
assert_eq!(cpu.registers(), &before);
assert_eq!(cpu.flags(), flags);
}
}
#[test]
fn compare_controls_a_conditional_move_without_changing_source() {
let mut memory = program(&[
0xe3a0_0005, // MOV r0, #5
0xe350_0005, // CMP r0, #5
0x03a0_102a, // MOVEQ r1, #42
0x13a0_1063, // MOVNE r1, #99, skipped
]);
let mut cpu = Cpu::new(ROM_START);
for _ in 0..4 {
cpu.step(&mut memory).unwrap();
}
assert_eq!(&cpu.registers()[..2], &[5, 42]);
assert_eq!(
cpu.flags(),
Flags {
negative: false,
zero: true,
carry: true,
overflow: false
}
);
}

View file

@ -0,0 +1,314 @@
//! ARMv4T Thumb instructions, including software interrupts.
use super::{
alu::{self, Shift},
Cpu, CpuError,
};
use crate::memory::Memory;
#[derive(Clone, Copy)]
enum Transfer {
StoreWord,
StoreHalf,
StoreByte,
LoadSignedByte,
LoadWord,
LoadHalf,
LoadByte,
LoadSignedHalf,
}
impl Cpu {
pub(super) fn execute_thumb(
&mut self,
instruction: u16,
memory: &mut Memory,
) -> Result<(), CpuError> {
let instruction = u32::from(instruction);
let next_pc = self.pc().wrapping_add(2);
match instruction {
0x0000..=0x17ff => {
let source = ((instruction >> 3) & 7) as usize;
let destination = (instruction & 7) as usize;
let (value, carry) = alu::shift(
self.registers[source],
Shift::decode(instruction >> 11),
(instruction >> 6) & 31,
self.flags.carry,
false,
);
self.thumb_result(destination, 0xd, 0, value, carry);
}
0x1800..=0x1fff => {
let source = ((instruction >> 3) & 7) as usize;
let destination = (instruction & 7) as usize;
let operand = (instruction >> 6) & 7;
let operand = if instruction & (1 << 10) != 0 {
operand
} else {
self.registers[operand as usize]
};
let opcode = if instruction & (1 << 9) != 0 { 2 } else { 4 };
self.thumb_result(
destination,
opcode,
self.registers[source],
operand,
self.flags.carry,
);
}
0x2000..=0x3fff => {
let destination = ((instruction >> 8) & 7) as usize;
let opcode = [0xd, 0xa, 4, 2][((instruction >> 11) & 3) as usize];
self.thumb_result(
destination,
opcode,
self.registers[destination],
instruction & 255,
self.flags.carry,
);
}
0x4000..=0x43ff => self.thumb_register_alu(instruction),
0x4400..=0x47ff => return self.thumb_high_register(instruction),
0x4800..=0x4fff => {
let address =
(self.pc().wrapping_add(4) & !3).wrapping_add((instruction & 255) * 4);
self.thumb_transfer(
memory,
Transfer::LoadWord,
((instruction >> 8) & 7) as usize,
address,
)?;
}
0x5000..=0x5fff => {
let base = self.registers[((instruction >> 3) & 7) as usize];
let offset = self.registers[((instruction >> 6) & 7) as usize];
let kind = [
Transfer::StoreWord,
Transfer::StoreHalf,
Transfer::StoreByte,
Transfer::LoadSignedByte,
Transfer::LoadWord,
Transfer::LoadHalf,
Transfer::LoadByte,
Transfer::LoadSignedHalf,
][((instruction >> 9) & 7) as usize];
self.thumb_transfer(
memory,
kind,
(instruction & 7) as usize,
base.wrapping_add(offset),
)?;
}
0x6000..=0x7fff => {
let byte = instruction & (1 << 12) != 0;
let load = instruction & (1 << 11) != 0;
let offset = ((instruction >> 6) & 31) * if byte { 1 } else { 4 };
let base = self.registers[((instruction >> 3) & 7) as usize];
let kind = match (load, byte) {
(false, false) => Transfer::StoreWord,
(false, true) => Transfer::StoreByte,
(true, false) => Transfer::LoadWord,
(true, true) => Transfer::LoadByte,
};
self.thumb_transfer(
memory,
kind,
(instruction & 7) as usize,
base.wrapping_add(offset),
)?;
}
0x8000..=0x8fff => {
let kind = if instruction & (1 << 11) != 0 {
Transfer::LoadHalf
} else {
Transfer::StoreHalf
};
let base = self.registers[((instruction >> 3) & 7) as usize];
let address = base.wrapping_add(((instruction >> 6) & 31) * 2);
self.thumb_transfer(memory, kind, (instruction & 7) as usize, address)?;
}
0x9000..=0x9fff => {
let kind = if instruction & (1 << 11) != 0 {
Transfer::LoadWord
} else {
Transfer::StoreWord
};
let address = self.registers[13].wrapping_add((instruction & 255) * 4);
self.thumb_transfer(memory, kind, ((instruction >> 8) & 7) as usize, address)?;
}
0xa000..=0xafff => {
let base = if instruction & (1 << 11) != 0 {
self.registers[13]
} else {
self.pc().wrapping_add(4) & !3
};
self.registers[((instruction >> 8) & 7) as usize] =
base.wrapping_add((instruction & 255) * 4);
}
0xb000..=0xb0ff => {
let offset = (instruction & 127) * 4;
self.registers[13] = if instruction & 128 != 0 {
self.registers[13].wrapping_sub(offset)
} else {
self.registers[13].wrapping_add(offset)
};
}
0xb400..=0xb5ff | 0xbc00..=0xbdff => {
let pop = instruction & (1 << 11) != 0;
let extra = if instruction & (1 << 8) != 0 {
1 << if pop { 15 } else { 14 }
} else {
0
};
// Shared block logic uses Thumb PC alignment and pipeline offsets.
let arm = if pop { 0xe8bd_0000 } else { 0xe92d_0000 };
return self.block_transfer(arm | extra | (instruction & 255), memory);
}
0xc000..=0xcfff => {
let arm = 0xe8a0_0000
| ((instruction & 0x800) << 9)
| (((instruction >> 8) & 7) << 16)
| (instruction & 255);
return self.block_transfer(arm, memory);
}
0xdf00..=0xdfff => {
self.enter_exception(super::Exception::SoftwareInterrupt);
return Ok(());
}
0xd000..=0xddff => {
if self.flags.condition_passed((instruction >> 8) & 15) {
let offset = (instruction as u8 as i8 as i32 * 2) as u32;
self.registers[15] = self.pc().wrapping_add(4).wrapping_add(offset);
return Ok(());
}
}
0xe000..=0xe7ff => {
let offset = (((instruction & 0x7ff) << 21) as i32 >> 20) as u32;
self.registers[15] = self.pc().wrapping_add(4).wrapping_add(offset);
return Ok(());
}
0xf000..=0xf7ff => {
// BL prefix: sign-extend eleven bits, then shift left twelve.
let offset = (((instruction & 0x7ff) << 21) as i32 >> 9) as u32;
self.registers[14] = self.pc().wrapping_add(4).wrapping_add(offset);
}
0xf800..=0xffff => {
// A standalone suffix is legal on ARM7; it uses the existing LR.
let target = self.registers[14].wrapping_add((instruction & 0x7ff) * 2);
self.registers[14] = next_pc | 1;
self.registers[15] = target & !1;
return Ok(());
}
// Includes undefined condition 0xE and ARMv5+ extensions.
_ => return Err(self.unsupported(instruction)),
}
self.registers[15] = next_pc;
Ok(())
}
fn thumb_result(&mut self, destination: usize, opcode: u32, a: u32, b: u32, carry: bool) {
let (result, flags) = alu::execute(opcode, a, b, carry, self.flags);
self.flags = flags;
if !(8..=11).contains(&opcode) {
self.registers[destination] = result;
}
}
fn thumb_register_alu(&mut self, instruction: u32) {
let destination = (instruction & 7) as usize;
let source = ((instruction >> 3) & 7) as usize;
let opcode = (instruction >> 6) & 15;
let a = self.registers[destination];
let b = self.registers[source];
match opcode {
2 | 3 | 4 | 7 => {
let kind = match opcode {
2 => Shift::Lsl,
3 => Shift::Lsr,
4 => Shift::Asr,
_ => Shift::Ror,
};
let (value, carry) = alu::shift(a, kind, b, self.flags.carry, true);
self.thumb_result(destination, 0xd, 0, value, carry);
}
9 => self.thumb_result(destination, 2, 0, b, self.flags.carry), // NEG
13 => {
let result = a.wrapping_mul(b);
self.registers[destination] = result;
self.flags.negative = result >> 31 != 0;
self.flags.zero = result == 0;
// Preserve unspecified multiply carry, matching the ARM core policy.
}
_ => self.thumb_result(destination, opcode, a, b, self.flags.carry),
}
}
fn thumb_high_register(&mut self, instruction: u32) -> Result<(), CpuError> {
let opcode = (instruction >> 8) & 3;
let source = ((instruction >> 3) & 15) as usize;
let destination = ((instruction & 7) | ((instruction >> 4) & 8)) as usize;
if opcode == 3 {
// Bit 7 selects BLX on ARMv5; low destination bits must be zero.
if instruction & 0x87 != 0 {
return Err(self.unsupported(instruction));
}
self.branch_exchange(self.operand_register(source, 4));
return Ok(());
}
// ARMv4T requires at least one high register in this instruction format.
if instruction & 0xc0 == 0 {
return Err(self.unsupported(instruction));
}
let a = self.operand_register(destination, 4);
let b = self.operand_register(source, 4);
let value = match opcode {
0 => a.wrapping_add(b),
1 => {
self.flags = alu::execute(0xa, a, b, self.flags.carry, self.flags).1;
self.registers[15] = self.pc().wrapping_add(2);
return Ok(());
}
_ => b,
};
if destination == 15 {
self.registers[15] = value & !1; // ADD/MOV PC stay in Thumb state.
} else {
self.registers[destination] = value;
self.registers[15] = self.pc().wrapping_add(2);
}
Ok(())
}
fn thumb_transfer(
&mut self,
memory: &mut Memory,
kind: Transfer,
register: usize,
address: u32,
) -> Result<(), CpuError> {
let value = match kind {
Transfer::StoreWord => {
return Ok(memory.write32(address & !3, self.registers[register])?)
}
Transfer::StoreHalf => {
return Ok(memory.write16(address & !1, self.registers[register] as u16)?)
}
Transfer::StoreByte => {
return Ok(memory.write8(address, self.registers[register] as u8)?)
}
Transfer::LoadWord => memory.read32(address & !3)?.rotate_right((address & 3) * 8),
Transfer::LoadHalf => {
u32::from(memory.read16(address & !1)?).rotate_right((address & 1) * 8)
}
Transfer::LoadByte => u32::from(memory.read8(address)?),
Transfer::LoadSignedByte => memory.read8(address)? as i8 as i32 as u32,
Transfer::LoadSignedHalf if address & 1 != 0 => {
memory.read8(address)? as i8 as i32 as u32
}
Transfer::LoadSignedHalf => memory.read16(address)? as i16 as i32 as u32,
};
self.registers[register] = value;
Ok(())
}
}

View file

@ -0,0 +1,821 @@
use super::*;
use crate::memory::ROM_START;
const RAM: u32 = 0x0200_0000;
fn program(instructions: &[u16]) -> Memory {
Memory::new(
instructions
.iter()
.flat_map(|word| word.to_le_bytes())
.collect(),
)
.unwrap()
}
fn thumb_cpu() -> Cpu {
let mut cpu = Cpu::new(ROM_START);
cpu.instruction_set = InstructionSet::Thumb;
cpu
}
fn flags() -> Flags {
Flags {
negative: true,
zero: true,
carry: true,
overflow: true,
}
}
#[test]
fn fetches_one_halfword_and_advances_two_bytes() {
let mut memory = program(&[0x202a]); // MOV r0,#42, no trailing halfword
let mut cpu = thumb_cpu();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], 42);
assert_eq!(cpu.pc(), ROM_START + 2);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
}
#[test]
fn immediate_shift_formats_use_arm7_zero_amount_rules() {
for (instruction, expected, carry) in [
(0x0008, 0x8000_0001, false), // LSL r0,r1,#0
(0x0048, 2, true), // LSL #1
(0x0808, 0, true), // LSR #32
(0x0848, 0x4000_0000, true), // LSR #1
(0x1008, u32::MAX, true), // ASR #32
(0x1048, 0xc000_0000, true), // ASR #1
] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.registers[1] = 0x8000_0001;
cpu.flags.overflow = true;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], expected);
assert_eq!(
cpu.flags,
Flags {
negative: expected >> 31 != 0,
zero: expected == 0,
carry,
overflow: true
}
);
}
}
#[test]
fn three_register_and_small_immediate_add_subtract_formats() {
for (instruction, expected) in [
(0x1888, 12), // ADD r0,r1,r2
(0x1a88, 8), // SUB r0,r1,r2
(0x1cc8, 13), // ADD r0,r1,#3
(0x1ec8, 7), // SUB r0,r1,#3
] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.registers[1] = 10;
cpu.registers[2] = 2;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], expected);
assert!(!cpu.flags.overflow);
assert_eq!(cpu.flags.carry, instruction & 0x200 != 0);
}
}
#[test]
fn add_subtract_and_compare_set_signed_overflow_and_borrow() {
for (instruction, initial, expected, expected_flags) in [
(
0x3001,
0x7fff_ffff,
0x8000_0000,
Flags {
negative: true,
zero: false,
carry: false,
overflow: true,
},
),
(
0x3801,
0x8000_0000,
0x7fff_ffff,
Flags {
negative: false,
zero: false,
carry: true,
overflow: true,
},
),
(
0x3801,
0,
u32::MAX,
Flags {
negative: true,
zero: false,
carry: false,
overflow: false,
},
),
(
0x2801,
1,
1,
Flags {
negative: false,
zero: true,
carry: true,
overflow: false,
},
),
] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.registers[0] = initial;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], expected);
assert_eq!(cpu.flags, expected_flags);
}
}
#[test]
fn immediate_mov_preserves_carry_and_overflow() {
let mut memory = program(&[0x2700]); // MOV r7,#0
let mut cpu = thumb_cpu();
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(
cpu.flags,
Flags {
negative: false,
zero: true,
carry: true,
overflow: true
}
);
}
#[test]
fn all_sixteen_register_alu_opcodes_decode_correctly() {
let results = [
0,
0x36,
0xd0,
0xd,
0xd,
0x37,
0x32,
0xd,
0,
0xffff_fffe,
0x32,
0x36,
0x36,
0x68,
0x34,
0xffff_fffd,
];
for (opcode, result) in results.into_iter().enumerate() {
let mut memory = program(&[0x4008 | ((opcode as u16) << 6)]); // op r0,r1
let mut cpu = thumb_cpu();
cpu.registers[0] = 0x34;
cpu.registers[1] = 2;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(
cpu.registers[0],
if [8, 10, 11].contains(&opcode) {
0x34
} else {
result
},
"opcode {opcode}"
);
assert_eq!(cpu.flags.negative, result >> 31 != 0);
assert_eq!(cpu.flags.zero, result == 0);
assert_eq!(cpu.flags.overflow, ![5, 6, 9, 10, 11].contains(&opcode));
assert_eq!(cpu.flags.carry, ![2, 3, 4, 5, 7, 9, 11].contains(&opcode));
}
}
#[test]
fn register_shift_amounts_include_zero_32_and_255_and_use_low_byte() {
for (opcode, amount, expected, carry) in [
(2, 0, 0x8000_0001, true),
(2, 32, 0, true),
(2, 33, 0, false),
(3, 32, 0, true),
(3, 255, 0, false),
(4, 255, u32::MAX, true),
(7, 32, 0x8000_0001, true),
(7, 256, 0x8000_0001, true),
(2, 257, 2, true),
] {
let mut memory = program(&[0x4008 | (opcode << 6)]);
let mut cpu = thumb_cpu();
cpu.registers[0] = 0x8000_0001;
cpu.registers[1] = amount;
cpu.flags.carry = true;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], expected);
assert_eq!(cpu.flags.carry, carry);
}
}
#[test]
fn adc_sbc_and_neg_handle_boundary_values() {
for (instruction, a, b, carry, expected, out_carry, overflow) in [
(0x4148, u32::MAX, 0, true, 0, true, false),
(0x4148, 0x7fff_ffff, 0, true, 0x8000_0000, false, true),
(0x4188, 0, 0, false, u32::MAX, false, false),
(0x4188, 0x8000_0000, 0, false, 0x7fff_ffff, true, true),
(0x4248, 123, 0x8000_0000, false, 0x8000_0000, false, true),
] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.registers[0] = a;
cpu.registers[1] = b;
cpu.flags.carry = carry;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], expected);
assert_eq!(cpu.flags.carry, out_carry);
assert_eq!(cpu.flags.overflow, overflow);
}
}
#[test]
fn multiply_wraps_and_preserves_the_unspecified_carry_policy() {
let mut memory = program(&[0x4348]); // MUL r0,r1
let mut cpu = thumb_cpu();
cpu.registers[0] = 0x8000_0000;
cpu.registers[1] = 2;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], 0);
assert_eq!(
cpu.flags,
Flags {
negative: false,
zero: true,
carry: true,
overflow: true
}
);
}
#[test]
fn high_register_add_and_mov_preserve_flags_while_cmp_updates_them() {
let mut memory = program(&[
0x4680, // MOV r8,r0
0x4448, // ADD r0,r9
0x4540, // CMP r0,r8
]);
let mut cpu = thumb_cpu();
cpu.registers[0] = 10;
cpu.registers[9] = 5;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[8], 10);
assert_eq!(cpu.flags, flags());
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], 15);
assert_eq!(cpu.flags, flags());
cpu.step(&mut memory).unwrap();
assert_eq!(
cpu.flags,
Flags {
negative: false,
zero: false,
carry: true,
overflow: false
}
);
}
#[test]
fn high_register_pc_reads_use_pc_plus_four_without_word_alignment() {
for instruction in [0x4678, 0x4478] {
// MOV/ADD r0,pc
let mut memory = program(&[0x46c0, instruction]);
let mut cpu = thumb_cpu();
cpu.registers[15] += 2;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], ROM_START + 6);
}
}
#[test]
fn high_register_pc_writes_remain_thumb_and_keep_bit_one() {
for instruction in [0x4687, 0x4487] {
// MOV/ADD pc,r0
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.registers[0] = if instruction == 0x4687 {
ROM_START + 11
} else {
7
};
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START + 10);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
assert_eq!(cpu.flags, flags());
}
}
#[test]
fn thumb_bx_selects_instruction_set_from_bit_zero() {
for (target, expected, state) in [
(ROM_START + 11, ROM_START + 10, InstructionSet::Thumb),
(ROM_START + 10, ROM_START + 8, InstructionSet::Arm),
] {
let mut memory = program(&[0x4700]); // BX r0
let mut cpu = thumb_cpu();
cpu.registers[0] = target;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), expected);
assert_eq!(cpu.instruction_set(), state);
assert_eq!(cpu.flags, flags());
}
}
#[test]
fn bx_pc_switches_to_arm_with_word_alignment() {
let mut memory = program(&[0x46c0, 0x4778]); // BX pc at halfword boundary
let mut cpu = thumb_cpu();
cpu.registers[15] += 2;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START + 4);
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
}
#[test]
fn literal_load_and_adr_align_pc_plus_four() {
for instruction in [0x4801, 0xa001] {
let mut memory = program(&[0x46c0, instruction, 0, 0, 0x5678, 0x1234]);
let mut cpu = thumb_cpu();
cpu.registers[15] += 2;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(
cpu.registers[0],
if instruction == 0x4801 {
0x1234_5678
} else {
ROM_START + 8
}
);
assert_eq!(cpu.flags, flags());
}
}
#[test]
fn all_register_offset_memory_formats_decode_and_handle_odd_addresses() {
let loaded = [
0,
0,
0,
0xffff_ff80,
0xfe12_3480,
0xfe00_0080,
0x80,
0xffff_ff80,
];
let stored = [0xaabb_ccdd, 0x1234_ccdd, 0x1234_ddfe];
for opcode in 0..8 {
let mut memory = program(&[0x5081 | (opcode << 9)]); // Rd=r1,Rb=r0,Ro=r2
memory.write32(RAM, 0x1234_80fe).unwrap();
let mut cpu = thumb_cpu();
cpu.registers[0] = RAM;
cpu.registers[1] = 0xaabb_ccdd;
cpu.registers[2] = 1;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
if opcode < 3 {
assert_eq!(memory.read32(RAM).unwrap(), stored[opcode as usize]);
assert_eq!(cpu.registers[1], 0xaabb_ccdd);
} else {
assert_eq!(cpu.registers[1], loaded[opcode as usize]);
assert_eq!(memory.read32(RAM).unwrap(), 0x1234_80fe);
}
assert_eq!(cpu.flags, flags());
}
}
#[test]
fn immediate_memory_offsets_scale_by_transfer_size() {
for (store, load, address, expected) in [
(0x6041, 0x6842, RAM + 4, 0x1234_abcd),
(0x7041, 0x7842, RAM + 1, 0xcd),
(0x8041, 0x8842, RAM + 2, 0xabcd),
] {
let mut memory = program(&[store, load]);
let mut cpu = thumb_cpu();
cpu.registers[0] = RAM;
cpu.registers[1] = 0x1234_abcd;
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[2], expected);
assert_eq!(memory.read8(address).unwrap(), 0xcd);
}
}
#[test]
fn sp_relative_memory_and_address_operations_preserve_flags() {
let mut memory = program(&[
0x9001, // STR r0,[sp,#4]
0x9901, // LDR r1,[sp,#4]
0xaa02, // ADD r2,sp,#8
0xb001, // ADD sp,#4
0xb081, // SUB sp,#4
]);
let mut cpu = thumb_cpu();
cpu.registers[13] = RAM;
cpu.registers[0] = 42;
cpu.flags = flags();
for _ in 0..5 {
cpu.step(&mut memory).unwrap();
}
assert_eq!(cpu.registers[1], 42);
assert_eq!(cpu.registers[2], RAM + 8);
assert_eq!(cpu.registers[13], RAM);
assert_eq!(cpu.flags, flags());
}
#[test]
fn memory_offsets_wrap_and_sp_addition_wraps_without_panicking() {
let mut memory = program(&[0x5081, 0xb001]); // STR r1,[r0,r2]; ADD sp,#4
let mut cpu = thumb_cpu();
cpu.registers[0] = 0xffff_fffc;
cpu.registers[1] = 42;
cpu.registers[2] = RAM + 4;
cpu.registers[13] = u32::MAX - 3;
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read32(RAM).unwrap(), 42);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[13], 0);
}
#[test]
fn signed_halfword_aligned_load_sign_extends_sixteen_bits() {
let mut memory = program(&[0x5e81]);
memory.write16(RAM, 0x8001).unwrap();
let mut cpu = thumb_cpu();
cpu.registers[0] = RAM;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 0xffff_8001);
}
#[test]
fn push_and_pop_restore_registers_and_pc_without_arm_exchange() {
for return_address in [ROM_START + 10, ROM_START + 11] {
let mut memory = program(&[0xb505, 0x2000, 0x2200, 0xbd05]); // PUSH/POP {r0,r2,lr/pc}
let mut cpu = thumb_cpu();
cpu.registers[0] = 11;
cpu.registers[2] = 22;
cpu.registers[13] = RAM + 0x100;
cpu.registers[14] = return_address;
for _ in 0..4 {
cpu.step(&mut memory).unwrap();
}
assert_eq!(cpu.registers[0], 11);
assert_eq!(cpu.registers[2], 22);
assert_eq!(cpu.registers[13], RAM + 0x100);
assert_eq!(cpu.pc(), ROM_START + 10);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
assert_eq!(memory.read32(RAM + 0xf4).unwrap(), 11);
assert_eq!(memory.read32(RAM + 0xf8).unwrap(), 22);
assert_eq!(memory.read32(RAM + 0xfc).unwrap(), return_address);
}
}
#[test]
fn push_pop_without_lr_or_pc_advance_by_two() {
let mut memory = program(&[0xb401, 0xbc02]); // PUSH {r0}; POP {r1}
let mut cpu = thumb_cpu();
cpu.registers[0] = 123;
cpu.registers[13] = RAM + 16;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 123);
assert_eq!(cpu.registers[13], RAM + 16);
assert_eq!(cpu.pc(), ROM_START + 4);
assert_eq!(cpu.flags, flags());
}
#[test]
fn thumb_multiple_transfers_use_low_register_base_and_ascending_addresses() {
let mut memory = program(&[0xc305, 0xcb30]); // STMIA r3!,{r0,r2}; LDMIA r3!,{r4,r5}
let mut cpu = thumb_cpu();
cpu.registers[0] = 11;
cpu.registers[2] = 22;
cpu.registers[3] = RAM + 1;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[3], RAM + 9);
cpu.registers[3] = RAM + 1;
cpu.step(&mut memory).unwrap();
assert_eq!(&cpu.registers[4..6], &[11, 22]);
assert_eq!(cpu.registers[3], RAM + 9);
assert_eq!(cpu.pc(), ROM_START + 4);
assert_eq!(cpu.flags, flags());
}
#[test]
fn thumb_multiple_transfer_base_overlap_matches_arm7() {
for (instruction, old_base_stored) in [(0xc103, false), (0xc106, true)] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.registers[1] = RAM;
cpu.step(&mut memory).unwrap();
assert_eq!(
memory
.read32(if old_base_stored { RAM } else { RAM + 4 })
.unwrap(),
if old_base_stored { RAM } else { RAM + 8 }
);
}
let mut memory = program(&[0xc903]); // LDMIA r1!,{r0,r1}
memory.write32(RAM, 11).unwrap();
memory.write32(RAM + 4, 22).unwrap();
let mut cpu = thumb_cpu();
cpu.registers[1] = RAM;
cpu.step(&mut memory).unwrap();
assert_eq!(&cpu.registers[..2], &[11, 22]);
}
#[test]
fn empty_thumb_multiple_lists_transfer_pc_with_sixty_four_byte_writeback() {
for instruction in [0xc000, 0xc800] {
let mut memory = program(&[instruction]);
memory.write32(RAM, ROM_START + 11).unwrap();
let mut cpu = thumb_cpu();
cpu.registers[0] = RAM;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], RAM + 64);
if instruction == 0xc000 {
assert_eq!(memory.read32(RAM).unwrap(), ROM_START + 4);
assert_eq!(cpu.pc(), ROM_START + 2);
} else {
assert_eq!(cpu.pc(), ROM_START + 10);
}
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
}
}
#[test]
fn empty_push_pop_follow_arm7_block_transfer_rules() {
let mut memory = program(&[0xb400, 0xbc00]);
let mut cpu = thumb_cpu();
cpu.registers[13] = RAM + 0x100;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[13], RAM + 0xc0);
assert_eq!(memory.read32(RAM + 0xc0).unwrap(), ROM_START + 4);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[13], RAM + 0x100);
assert_eq!(cpu.pc(), ROM_START + 4);
}
#[test]
fn conditional_branches_cover_every_condition_and_flag_combination() {
let tables = [
0xf0f0_u16, 0x0f0f, 0xcccc, 0x3333, 0xff00, 0x00ff, 0xaaaa, 0x5555, 0x0c0c, 0xf3f3, 0xaa55,
0x55aa, 0x0a05, 0xf5fa,
];
for (condition, table) in tables.into_iter().enumerate() {
let mut memory = program(&[0xd001 | ((condition as u16) << 8)]);
for bits in 0..16 {
let mut cpu = thumb_cpu();
cpu.flags = Flags {
negative: bits & 8 != 0,
zero: bits & 4 != 0,
carry: bits & 2 != 0,
overflow: bits & 1 != 0,
};
let before = cpu.flags;
cpu.step(&mut memory).unwrap();
assert_eq!(
cpu.pc(),
ROM_START + if table & (1 << bits) != 0 { 6 } else { 2 }
);
assert_eq!(cpu.flags, before);
}
}
}
#[test]
fn branch_offsets_are_signed_and_relative_to_pc_plus_four() {
for (instruction, target) in [
(0xe3ff, ROM_START + 2050),
(0xe400, ROM_START - 2044),
(0xe7fe, ROM_START),
(0xd080, ROM_START - 252),
(0xd07f, ROM_START + 258),
] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.flags.zero = true;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), target);
}
}
#[test]
fn bl_prefix_and_suffix_execute_as_separate_halfwords() {
let mut memory = program(&[0xf000, 0xf802, 0xe7fe, 0x46c0, 0x202a, 0x4770]);
let mut cpu = thumb_cpu();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[14], ROM_START + 4);
assert_eq!(cpu.pc(), ROM_START + 2);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START + 8);
assert_eq!(cpu.registers[14], ROM_START + 5);
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap(); // BX lr
assert_eq!(cpu.registers[0], 42);
assert_eq!(cpu.pc(), ROM_START + 4);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
}
#[test]
fn bl_prefix_sign_extends_and_suffix_can_execute_alone() {
for (instruction, expected) in [
(0xf3ff, ROM_START + 4 + 0x3ff000),
(0xf400, ROM_START + 4 - 0x400000),
(0xf7ff, ROM_START + 4 - 0x1000),
] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[14], expected);
}
let mut memory = program(&[0xf801]);
let mut cpu = thumb_cpu();
cpu.registers[14] = ROM_START + 9;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START + 10);
assert_eq!(cpu.registers[14], ROM_START + 3);
}
#[test]
fn arm_thumb_arm_round_trip_fetches_the_correct_instruction_width() {
let mut bytes: Vec<u8> = [0xe12f_ff10_u32, 0xe3a0_2063]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect();
bytes.extend([0x212a_u16, 0x4718].into_iter().flat_map(u16::to_le_bytes)); // MOV r1,#42; BX r3
let mut memory = Memory::new(bytes).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = ROM_START + 9;
cpu.registers[3] = ROM_START + 4;
for _ in 0..4 {
cpu.step(&mut memory).unwrap();
}
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
assert_eq!(cpu.registers[1], 42);
assert_eq!(cpu.registers[2], 99);
assert_eq!(cpu.pc(), ROM_START + 8);
}
#[test]
fn unsupported_thumb_instructions_preserve_registers_flags_and_state() {
for instruction in [
0xde00, 0xdeff, 0xbe00, 0xe800, 0x4780, 0x4701, 0x4400, 0x4500, 0x4600, 0xb100, 0xb200,
0xb600, 0xbf00,
] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.flags = flags();
let before = cpu.registers;
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::UnsupportedThumbInstruction {
address: ROM_START,
instruction
})
);
assert_eq!(cpu.registers, before);
assert_eq!(cpu.flags, flags());
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
}
}
#[test]
fn thumb_memory_errors_leave_state_unchanged() {
for instruction in [
0x6801, 0x6001, 0x8801, 0x8001, 0x7801, 0x7001, 0x5e81, 0x9801, 0x9001, 0x4801,
] {
let mut memory = program(&[instruction]);
let mut cpu = thumb_cpu();
cpu.flags = flags();
cpu.registers[1] = 123;
let before = cpu.registers;
assert!(
matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))),
"{instruction:04x}"
);
assert_eq!(cpu.registers, before);
assert_eq!(cpu.flags, flags());
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
}
}
#[test]
fn failed_thumb_store_to_rom_does_not_modify_rom_or_cpu() {
let mut memory = program(&[0x6001]);
let mut cpu = thumb_cpu();
cpu.registers[0] = ROM_START;
let before = cpu.registers;
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::ReadOnly(ROM_START)))
);
assert_eq!(cpu.registers, before);
assert_eq!(memory.read16(ROM_START).unwrap(), 0x6001);
}
#[test]
fn late_thumb_stack_and_block_errors_do_not_partially_commit() {
for instruction in [0xb403, 0xbc03, 0xc003, 0xc803] {
let mut memory = Memory::new(vec![]).unwrap();
memory.write16(0x0300_0000, instruction).unwrap();
memory.write32(0x07ff_fffc, 0x1234_5678).unwrap();
let mut cpu = thumb_cpu();
cpu.registers[15] = 0x0300_0000;
cpu.registers[0] = 0x07ff_fffc;
cpu.registers[13] = if instruction == 0xb403 {
0x0800_0004
} else {
0x07ff_fffc
};
let before = cpu.registers;
assert!(matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))));
assert_eq!(cpu.registers, before);
assert_eq!(memory.read32(0x07ff_fffc).unwrap(), 0x1234_5678);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
}
}
#[test]
fn skipped_arm_bx_does_not_switch_to_thumb() {
let mut memory = Memory::new(0x012f_ff10_u32.to_le_bytes().to_vec()).unwrap(); // BXEQ r0
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = ROM_START + 9;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
assert_eq!(cpu.pc(), ROM_START + 4);
}
#[test]
fn failing_fetch_after_bl_prefix_preserves_the_completed_prefix() {
let mut memory = program(&[0xf000]);
let mut cpu = thumb_cpu();
cpu.step(&mut memory).unwrap();
let before = cpu.registers;
assert!(matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))));
assert_eq!(cpu.registers, before);
assert_eq!(cpu.registers[14], ROM_START + 4);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
}
#[test]
fn every_thumb_encoding_executes_or_returns_a_diagnostic_without_panicking() {
let mut memory = Memory::new(vec![]).unwrap();
for instruction in 0..=u16::MAX {
memory.write16(RAM, instruction).unwrap();
let mut cpu = thumb_cpu();
for register in 0..15 {
cpu.registers[register] = RAM + 0x100 + register as u32 * 4;
}
cpu.registers[15] = RAM;
cpu.flags = flags();
let before = cpu.clone();
match cpu.step(&mut memory) {
Ok(()) => assert_eq!(cpu.pc() & (cpu.instruction_set.width() - 1), 0),
Err(_) => assert_eq!(cpu, before, "encoding {instruction:04x}"),
}
}
}
#[test]
fn truncated_or_misaligned_thumb_fetches_do_not_advance_pc() {
let mut memory = Memory::new(vec![0x20]).unwrap();
let mut cpu = thumb_cpu();
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(ROM_START + 1)))
);
assert_eq!(cpu.pc(), ROM_START);
cpu.registers[15] += 1;
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unaligned(ROM_START + 1)))
);
assert_eq!(cpu.pc(), ROM_START + 1);
}

View file

@ -0,0 +1,205 @@
//! Nominal ARM7 instruction-cycle summaries, separate from instruction semantics.
//! Data costs come from actual bus accesses. No speculative pipeline reads occur.
//! PC writes use target-region code costs, following GBATEK's cycle summary.
use super::{Cpu, CpuError, InstructionSet};
use crate::{
memory::Memory,
timing::{bus_cycles, refill_cycles, AccessKind, AccessWidth, StepTiming},
};
#[derive(Default)]
struct Summary {
code_kind: AccessKind,
refill: bool,
internal: u32,
}
impl Summary {
fn branch() -> Self {
Self {
refill: true,
..Self::default()
}
}
fn transfer(load: bool, pc: bool) -> Self {
Self {
code_kind: if load {
AccessKind::Sequential
} else {
AccessKind::NonSequential
},
refill: load && pc,
internal: u32::from(load),
}
}
}
impl InstructionSet {
fn access_width(self) -> AccessWidth {
match self {
Self::Arm => AccessWidth::Word,
Self::Thumb => AccessWidth::Halfword,
}
}
}
impl Cpu {
/// Execute one instruction and return its nominal cycle cost. Like Cpu::step,
/// this does not advance device time or sample interrupts. Machine does that.
/// Failed instructions discard their timing trace and retain existing diagnostics.
pub fn step_timed(&mut self, memory: &mut Memory) -> Result<StepTiming, CpuError> {
let instruction = self.fetch(memory)?;
let summary = match self.instruction_set {
InstructionSet::Arm => self.arm_summary(instruction),
InstructionSet::Thumb => self.thumb_summary(instruction),
};
let pc = self.pc();
let width = self.instruction_set.access_width();
// A WAITCNT store affects subsequent instructions, not this code access.
let waitcnt = memory.waitcnt();
memory.begin_data_timing();
let result = self.execute_fetched(instruction, memory);
let data_cycles = memory.end_data_timing();
result?;
let code_cycles = if summary.refill {
refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width())
} else {
bus_cycles(waitcnt, pc, width, summary.code_kind)
};
Ok(StepTiming {
code_cycles,
data_cycles,
internal_cycles: summary.internal,
idle_cycles: 0,
})
}
/// Nominal refill cost after explicit exception entry. No vector bytes are read.
pub(crate) fn exception_timing(&self, memory: &Memory) -> StepTiming {
StepTiming {
code_cycles: refill_cycles(
memory.waitcnt(),
self.pc(),
self.instruction_set.access_width(),
),
..StepTiming::default()
}
}
fn arm_summary(&self, instruction: u32) -> Summary {
if !self.flags.condition_passed(instruction >> 28) {
return Summary::default();
}
if instruction & 0x0f00_0000 == 0x0f00_0000
|| instruction & 0x0fff_fff0 == 0x012f_ff10
|| instruction & 0x0e00_0000 == 0x0a00_0000
{
return Summary::branch();
}
if instruction & 0x0fbf_0fff == 0x010f_0000
|| instruction & 0x0fb0_fff0 == 0x0120_f000
|| instruction & 0x0fb0_f000 == 0x0320_f000
{
return Summary::default(); // MRS/MSR overlap the data-processing space.
}
if instruction & 0x0e00_0000 == 0x0800_0000 {
let list = instruction & 0xffff;
return Summary::transfer(
instruction & (1 << 20) != 0,
list == 0 || list & 0x8000 != 0,
);
}
if instruction & 0x0fb0_0ff0 == 0x0100_0090 {
return Summary {
internal: 1,
..Summary::default()
}; // SWP: two N data accesses
}
if instruction & 0x0f80_00f0 == 0x0080_0090 {
let signed = instruction & (1 << 22) != 0;
let multiplier = self.registers[((instruction >> 8) & 15) as usize];
return Summary {
internal: multiply_cycles(multiplier, signed)
+ 1
+ u32::from(instruction & (1 << 21) != 0),
..Summary::default()
};
}
if instruction & 0x0fc0_00f0 == 0x0000_0090 {
let multiplier = self.registers[((instruction >> 8) & 15) as usize];
return Summary {
internal: multiply_cycles(multiplier, true)
+ u32::from(instruction & (1 << 21) != 0),
..Summary::default()
};
}
if instruction & 0x0e00_0090 == 0x0000_0090 {
return Summary::transfer(instruction & (1 << 20) != 0, false);
}
if instruction & 0x0c00_0000 == 0x0400_0000 {
return Summary::transfer(instruction & (1 << 20) != 0, (instruction >> 12) & 15 == 15);
}
let test = (8..=11).contains(&((instruction >> 21) & 15));
Summary {
refill: !test && (instruction >> 12) & 15 == 15,
internal: u32::from(instruction & (1 << 25) == 0 && instruction & (1 << 4) != 0),
..Summary::default()
}
}
fn thumb_summary(&self, instruction: u32) -> Summary {
match instruction {
0x4000..=0x43ff => {
let opcode = (instruction >> 6) & 15;
let internal = match opcode {
2 | 3 | 4 | 7 => 1,
// Thumb MUL maps the incoming destination to ARM's Rs.
13 => multiply_cycles(self.registers[(instruction & 7) as usize], true),
_ => 0,
};
Summary {
internal,
..Summary::default()
}
}
0x4400..=0x47ff => {
let opcode = (instruction >> 8) & 3;
let destination = (instruction & 7) | ((instruction >> 4) & 8);
if opcode == 3 || (opcode != 1 && destination == 15) {
Summary::branch()
} else {
Summary::default()
}
}
0x4800..=0x4fff => Summary::transfer(true, false),
0x5000..=0x5fff => Summary::transfer((instruction >> 9) & 7 >= 3, false),
0x6000..=0x9fff => Summary::transfer(instruction & (1 << 11) != 0, false),
0xb400..=0xb5ff | 0xbc00..=0xbdff => Summary::transfer(
instruction & (1 << 11) != 0,
instruction & 0x100 != 0 || instruction & 0xff == 0,
),
0xc000..=0xcfff => {
Summary::transfer(instruction & (1 << 11) != 0, instruction & 255 == 0)
}
0xd000..=0xddff if self.flags.condition_passed((instruction >> 8) & 15) => {
Summary::branch()
}
0xdf00..=0xdfff | 0xe000..=0xe7ff | 0xf800..=0xffff => Summary::branch(),
// Thumb BL prefix is 1S in the old region; its suffix is 2S+1N at the target.
_ => Summary::default(),
}
}
}
fn multiply_cycles(value: u32, signed: bool) -> u32 {
for bytes in 1..=3 {
let mask = u32::MAX << (bytes * 8);
let high = value & mask;
if high == 0 || signed && high == mask {
return bytes;
}
}
4
}

View file

@ -0,0 +1,383 @@
use super::*;
use crate::{io::WAITCNT, memory::ROM_START, timing::StepTiming};
const CODE: u32 = 0x0300_0000;
const DATA: u32 = 0x0200_0100;
fn arm(instruction: u32) -> (Cpu, Memory) {
let mut memory = Memory::new(vec![0; 0x40000]).unwrap();
memory.write32(CODE, instruction).unwrap();
(Cpu::new(CODE), memory)
}
fn thumb(instruction: u16) -> (Cpu, Memory) {
let (mut cpu, mut memory) = arm(0);
cpu.instruction_set = InstructionSet::Thumb;
memory.write16(CODE, instruction).unwrap();
(cpu, memory)
}
fn expected(code: u32, data: u32, internal: u32) -> StepTiming {
StepTiming {
code_cycles: code,
data_cycles: data,
internal_cycles: internal,
idle_cycles: 0,
}
}
#[test]
fn arm_alu_immediate_shifts_register_shifts_and_status_transfers() {
for opcode in 0..16 {
for operand in [0x0200_0001, 0x0000_0082, 0x0000_0312] {
// r0 destination, r1 first operand; S set makes TST/TEQ/CMP/CMN valid.
let instruction = 0xe011_0000 | (opcode << 21) | operand;
let (mut cpu, mut memory) = arm(instruction);
cpu.registers[1] = 123;
cpu.registers[2] = 12;
cpu.registers[3] = 0; // Register shift still costs I when amount is zero.
let cycles = cpu.step_timed(&mut memory).unwrap();
assert_eq!(cycles, expected(1, 0, u32::from(operand == 0x312)));
assert_eq!(memory.cycles(), 0); // Timed CPU API returns costs; it does not run devices.
}
}
for instruction in [0xe10f_0000, 0xe328_f480, 0xe128_f000, 0xe321_f013] {
let (mut cpu, mut memory) = arm(instruction);
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 0, 0));
}
}
#[test]
fn conditional_skips_do_not_charge_memory_multiply_or_branch_work() {
for instruction in [
0x0590_1000,
0x0580_1000,
0x0a00_0000,
0x0f00_0000,
0x0000_0291,
] {
let (mut cpu, mut memory) = arm(instruction);
cpu.registers[2] = 0x1234_5678;
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 0, 0));
assert_eq!(cpu.pc(), CODE + 4);
}
}
#[test]
fn branch_and_pc_write_refill_even_when_target_equals_fallthrough() {
for instruction in [0xeaff_ffff, 0xe1a0_f000, 0xe1a0_f110] {
let (mut cpu, mut memory) = arm(instruction);
cpu.registers[0] = CODE + 4;
let timing = cpu.step_timed(&mut memory).unwrap();
assert_eq!(
timing,
expected(3, 0, u32::from(instruction == 0xe1a0_f110))
);
assert_eq!(cpu.pc(), CODE + 4);
}
}
#[test]
fn cross_region_bx_uses_destination_width_and_wait_states() {
let (mut cpu, mut memory) = arm(0xe12f_ff10);
cpu.registers[0] = 0x0a00_0101; // Thumb in WS1: 5N + 5S + 5S
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(15, 0, 0));
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
let (mut cpu, mut memory) = thumb(0x4700);
cpu.registers[0] = ROM_START + 0x100; // ARM in WS0: 8N + 6S + 6S
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(20, 0, 0));
}
#[test]
fn arm_single_load_store_widths_charge_one_actual_bus_access() {
for (instruction, data_cycles, internal) in [
(0xe590_1000, 6, 1), // LDR
(0xe580_1000, 6, 0), // STR
(0xe5d0_1000, 3, 1), // LDRB
(0xe5c0_1000, 3, 0), // STRB
(0xe1d0_10b0, 3, 1), // LDRH
(0xe1c0_10b0, 3, 0), // STRH
(0xe1d0_10d0, 3, 1), // LDRSB
(0xe1d0_10f0, 3, 1), // LDRSH
] {
for offset in 0..4 {
let (mut cpu, mut memory) = arm(instruction);
cpu.registers[0] = DATA + offset;
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
expected(1, data_cycles, internal)
);
}
}
}
#[test]
fn actual_pre_post_and_shifted_addresses_determine_load_costs() {
for (instruction, base, offset) in [
(0xe790_1102, 0x0100_0100, 0x0080_0000), // LDR r1,[r0,r2,LSL #2] -> IWRAM
(0xe490_1004, 0x0300_0100, 0), // post-index uses old base
(0xe510_1100, 0x0300_0200, 0), // negative immediate -> IWRAM
] {
let (mut cpu, mut memory) = arm(instruction);
cpu.registers[0] = base;
cpu.registers[2] = offset;
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 1, 1));
}
}
#[test]
fn load_pc_adds_refill_and_does_not_count_fetch_as_data() {
let (mut cpu, mut memory) = arm(0xe590_f000);
cpu.registers[0] = DATA;
memory.write32(DATA, ROM_START + 0x100).unwrap();
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(20, 6, 1));
}
#[test]
fn block_transfers_count_words_and_apply_refill_only_for_load_pc() {
for list in [0, 1, 0x000f, 0x1fff, 0xffff] {
for load in [false, true] {
let (mut cpu, mut memory) = arm(0xe880_0000 | (u32::from(load) << 20) | list);
cpu.registers[0] = CODE + 0x100;
let count = if list == 0 { 1 } else { list.count_ones() };
let pc = load && (list == 0 || list & 0x8000 != 0);
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
expected(if pc { 3 } else { 1 }, count, u32::from(load))
);
}
}
}
#[test]
fn rom_block_reads_use_n_then_s_and_restart_at_128_kib_boundary() {
let (mut cpu, mut memory) = arm(0xe890_001e); // LDMIA r0, {r1-r4}
cpu.registers[0] = ROM_START + 0x1fff8;
// Word accesses: N+S=8, S+S=6, forced N+S=8, S+S=6.
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 28, 1));
let (mut cpu, mut memory) = arm(0xe590_1000);
cpu.registers[0] = ROM_START + 0x100;
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 8, 1));
}
#[test]
fn swap_uses_two_nonsequential_accesses_and_one_internal_cycle() {
for (instruction, data_cycles) in [(0xe100_1092, 12), (0xe140_1092, 6)] {
let (mut cpu, mut memory) = arm(instruction);
cpu.registers[0] = DATA;
cpu.registers[2] = 123;
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
expected(1, data_cycles, 1)
);
}
}
#[test]
fn multiply_early_termination_uses_rs_and_signedness_before_execution() {
for (value, signed_m, unsigned_m) in [
(0, 1, 1),
(255, 1, 1),
(256, 2, 2),
(65535, 2, 2),
(65536, 3, 3),
(0x00ff_ffff, 3, 3),
(0x0100_0000, 4, 4),
(0xffff_ffff, 1, 4),
(0xffff_ff00, 1, 4),
(0xffff_0000, 2, 4),
(0xff00_0000, 3, 4),
(0x8000_0000, 4, 4),
] {
for (instruction, internal) in [
(0xe000_0291, signed_m), // MUL r0,r1,r2
(0xe020_3291, signed_m + 1), // MLA r0,r1,r2,r3
(0xe081_0392, unsigned_m + 1), // UMULL r0,r1,r2,r3
(0xe0a1_0392, unsigned_m + 2), // UMLAL
(0xe0c1_0392, signed_m + 1), // SMULL
(0xe0e1_0392, signed_m + 2), // SMLAL
] {
let (mut cpu, mut memory) = arm(instruction);
cpu.registers[1] = 0x1234_5678;
cpu.registers[2] = value;
cpu.registers[3] = value;
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
expected(1, 0, internal),
"{instruction:08x}, {value:08x}"
);
}
}
}
#[test]
fn thumb_alu_shifts_and_multiply_use_correct_internal_cycles() {
for opcode in 0..16 {
for value in [7, 0x1234_5678, 0xffff_ffff] {
let (mut cpu, mut memory) = thumb(0x4008 | opcode << 6);
cpu.registers[0] = value;
cpu.registers[1] = 0; // Multiplication timing must not use this source.
let internal = match opcode {
2 | 3 | 4 | 7 => 1,
13 if value == 0x1234_5678 => 4,
13 => 1,
_ => 0,
};
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
expected(1, 0, internal)
);
}
}
}
#[test]
fn thumb_transfers_literals_sp_and_register_offsets_use_actual_bus_widths() {
for (instruction, data_cycles, internal) in [
(0x4800, 1, 1), // literal LDR from IWRAM
(0x5081, 6, 0),
(0x5281, 3, 0),
(0x5481, 3, 0),
(0x5681, 3, 1),
(0x5881, 6, 1),
(0x5a81, 3, 1),
(0x5c81, 3, 1),
(0x5e81, 3, 1),
(0x6001, 6, 0),
(0x6801, 6, 1),
(0x7001, 3, 0),
(0x7801, 3, 1),
(0x8001, 3, 0),
(0x8801, 3, 1),
(0x9000, 6, 0),
(0x9800, 6, 1),
] {
let (mut cpu, mut memory) = thumb(instruction);
cpu.registers[0] = DATA;
cpu.registers[13] = DATA;
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
expected(1, data_cycles, internal),
"{instruction:04x}"
);
}
}
#[test]
fn thumb_branches_bl_halves_and_high_pc_writes_refill_only_when_taken() {
for (instruction, code) in [
(0xd000, 1),
(0xd100, 3),
(0xe000, 3),
(0x4687, 3),
(0x4487, 3),
(0x4587, 1),
] {
let (mut cpu, mut memory) = thumb(instruction);
cpu.registers[0] = CODE + 4;
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(code, 0, 0));
}
let (mut cpu, mut memory) = thumb(0xf000);
memory.write16(CODE + 2, 0xf800).unwrap();
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 0, 0));
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(3, 0, 0));
}
#[test]
fn thumb_push_pop_and_multiple_transfers_charge_each_word() {
for (instruction, data, internal, code) in [
(0xb403, 12, 0, 1),
(0xb503, 18, 0, 1),
(0xbc03, 12, 1, 1),
(0xbd03, 18, 1, 3),
(0xc003, 12, 0, 1),
(0xc803, 12, 1, 1),
(0xc800, 6, 1, 3),
(0xbc00, 6, 1, 3),
] {
let (mut cpu, mut memory) = thumb(instruction);
cpu.registers[0] = DATA;
cpu.registers[13] = DATA;
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
expected(code, data, internal)
);
}
}
#[test]
fn software_interrupts_and_exception_returns_use_new_code_region_and_state() {
for thumb_state in [false, true] {
let (mut cpu, mut memory) = if thumb_state {
thumb(0xdf00)
} else {
arm(0xef00_0000)
};
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(3, 0, 0));
assert_eq!(cpu.mode(), Mode::Supervisor);
// Return via MOVS pc,lr, executing from IWRAM without a BIOS image.
memory.write32(CODE + 0x100, 0xe1b0_f00e).unwrap();
cpu.registers[15] = CODE + 0x100;
cpu.registers[14] = ROM_START + 0x100;
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
expected(if thumb_state { 11 } else { 20 }, 0, 0)
);
}
}
#[test]
fn waitcnt_store_charges_old_code_settings_and_new_settings_apply_next_step() {
let mut memory = Memory::new(
[0xe580_1000_u32, 0xe1a0_0000]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect(),
)
.unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = WAITCNT;
cpu.registers[1] = 0x18; // N=3 and S=2 per halfword
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(8, 1, 0));
assert_eq!(memory.waitcnt(), 0x18);
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(4, 0, 0));
}
#[test]
fn failed_data_access_discards_trace_before_retry() {
let (mut cpu, mut memory) = arm(0xe8b0_0006); // LDMIA r0!,{r1,r2}
cpu.registers[0] = 0x0400_0054;
let before = cpu.clone();
assert!(cpu.step_timed(&mut memory).is_err());
assert_eq!(cpu, before);
assert_eq!(memory.cycles(), 0);
cpu.registers[0] = DATA;
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(1, 12, 1));
}
#[test]
fn timing_preserves_semantics_for_every_thumb_encoding() {
let mut timed_memory = Memory::new(vec![]).unwrap();
let mut plain_memory = Memory::new(vec![]).unwrap();
for instruction in 0..=u16::MAX {
timed_memory.write16(CODE, instruction).unwrap();
plain_memory.write16(CODE, instruction).unwrap();
let mut plain = Cpu::new(CODE);
plain.instruction_set = InstructionSet::Thumb;
for register in 0..15 {
plain.registers[register] = DATA + register as u32 * 4;
}
let mut timed = plain.clone();
let cost = timed.step_timed(&mut timed_memory);
if let Ok(cost) = cost {
assert!(cost.total() > 0);
}
assert_eq!(
cost.map(|_| ()),
plain.step(&mut plain_memory),
"{instruction:04x}"
);
assert_eq!(timed, plain, "{instruction:04x}");
assert_eq!(timed_memory.cycles(), 0);
}
}

View file

@ -0,0 +1,145 @@
//! Block transfers shared by ARM and Thumb, plus ARM swaps.
//! Data aborts and per-access device event timing are not modeled.
use super::{Cpu, CpuError, InstructionSet};
use crate::memory::Memory;
impl Cpu {
pub(super) fn block_transfer(
&mut self,
instruction: u32,
memory: &mut Memory,
) -> Result<(), CpuError> {
let base_register = ((instruction >> 16) & 15) as usize;
if base_register == 15 {
return Err(self.unsupported(instruction));
}
let encoded_list = instruction & 0xffff;
// ARM7's empty list transfers PC, but calculates addresses/writeback
// as if sixteen registers were selected.
let (list, span) = if encoded_list == 0 {
(1 << 15, 64)
} else {
(encoded_list, encoded_list.count_ones() * 4)
};
let pre = instruction & (1 << 24) != 0;
let up = instruction & (1 << 23) != 0;
let write_back = instruction & (1 << 21) != 0;
let load = instruction & (1 << 20) != 0;
let special = instruction & (1 << 22) != 0;
let user_bank = special && !(load && list & (1 << 15) != 0);
// Diagnose unpredictable user-bank forms and the unverified S+empty case.
if special && (self.mode == super::Mode::User || encoded_list == 0)
|| user_bank && write_back
{
return Err(self.unsupported(instruction));
}
let restore = if special && !user_bank {
Some(self.return_status(instruction)?)
} else {
None
};
let base = self.registers[base_register];
let adjusted = if up {
base.wrapping_add(span)
} else {
base.wrapping_sub(span)
};
let start = match (up, pre) {
(true, false) => base, // IA
(true, true) => base.wrapping_add(4), // IB
(false, false) => adjusted.wrapping_add(4), // DA
(false, true) => adjusted, // DB
};
// Unlike LDR, block loads align down without rotating the loaded word.
// The low base bits are retained in writeback.
let mut address = start & !3;
let mut next = self.registers;
next[15] = self.pc().wrapping_add(self.instruction_set.width());
let base_in_list = list & (1 << base_register) != 0;
if write_back && !(load && base_in_list) {
next[base_register] = adjusted;
}
let mut writes = [(0, 0); 16];
let mut write_count = 0;
for (register, destination) in next.iter_mut().enumerate() {
if list & (1 << register) == 0 {
continue;
}
if load {
let value = memory.read32(address)?;
*destination = if register == 15 && restore.is_none() {
self.instruction_set.align(value)
} else {
value
};
} else {
let value = if write_back
&& register == base_register
&& register as u32 != list.trailing_zeros()
{
// ARM7 stores the updated base when it is not first in Rlist.
adjusted
} else if user_bank {
self.user_register(register)
} else {
let pc_offset = if self.instruction_set == InstructionSet::Arm {
12
} else {
4
};
self.operand_register(register, pc_offset)
};
writes[write_count] = (address, value);
write_count += 1;
}
address = address.wrapping_add(4);
}
if !load {
memory.write_words(&writes[..write_count])?;
}
// Commit only after every access succeeds. This keeps diagnostics retryable.
if user_bank && load {
for (register, value) in next.into_iter().enumerate() {
if list & (1 << register) != 0 {
self.set_user_register(register, value);
}
}
self.registers[15] = next[15];
} else {
self.registers = next;
}
if let Some((status, mode)) = restore {
// Save loaded banked registers and writeback in the outgoing bank.
self.apply_status(status, mode);
self.registers[15] = self.instruction_set.align(self.pc());
}
Ok(())
}
pub(super) fn swap(&mut self, instruction: u32, memory: &mut Memory) -> Result<(), CpuError> {
let base_register = ((instruction >> 16) & 15) as usize;
let destination = ((instruction >> 12) & 15) as usize;
let source = (instruction & 15) as usize;
if [base_register, destination, source].contains(&15) {
return Err(self.unsupported(instruction));
}
// Snapshot operands so source/destination/base aliases use old values.
let address = self.registers[base_register];
let value = self.registers[source];
let previous = if instruction & (1 << 22) != 0 {
let previous = u32::from(memory.read8(address)?);
memory.write8(address, value as u8)?;
previous
} else {
let previous = memory.read32(address & !3)?.rotate_right((address & 3) * 8);
memory.write32(address & !3, value)?;
previous
};
self.registers[destination] = previous;
self.registers[15] = self.pc().wrapping_add(4);
Ok(())
}
}

View file

@ -0,0 +1,493 @@
use super::*;
use crate::memory::ROM_START;
const RAM: u32 = 0x0200_0000;
const BASE: u32 = RAM + 0x100;
fn program(words: &[u32]) -> Memory {
Memory::new(words.iter().flat_map(|word| word.to_le_bytes()).collect()).unwrap()
}
fn flags() -> Flags {
Flags {
negative: true,
zero: true,
carry: true,
overflow: true,
}
}
fn block(mode: u32, load: bool, write_back: bool, base: u32, list: u32) -> u32 {
0xe800_0000
| mode
| (u32::from(load) << 20)
| (u32::from(write_back) << 21)
| (base << 16)
| list
}
// Explicit address fixtures for five-register transfers with BASE as Rn.
const MODES: [(u32, u32, u32); 4] = [
(0x0080_0000, BASE, BASE + 20), // IA
(0x0180_0000, BASE + 4, BASE + 20), // IB
(0x0000_0000, BASE - 16, BASE - 20), // DA
(0x0100_0000, BASE - 20, BASE - 20), // DB
];
#[test]
fn stm_all_addressing_modes_store_sparse_registers_in_ascending_order() {
let list = (1 << 0) | (1 << 3) | (1 << 7) | (1 << 14) | (1 << 15);
for (mode, first, updated) in MODES {
for write_back in [false, true] {
let mut memory = program(&[block(mode, false, write_back, 10, list)]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = 0x11;
cpu.registers[3] = 0x22;
cpu.registers[7] = 0x33;
cpu.registers[14] = 0x44;
cpu.registers[10] = BASE;
cpu.flags = flags();
memory.write32(first - 4, 0xdead_beef).unwrap();
memory.write32(first + 20, 0xcafe_babe).unwrap();
cpu.step(&mut memory).unwrap();
for (offset, value) in [0x11, 0x22, 0x33, 0x44, ROM_START + 12]
.into_iter()
.enumerate()
{
assert_eq!(memory.read32(first + offset as u32 * 4).unwrap(), value);
}
assert_eq!(memory.read32(first - 4).unwrap(), 0xdead_beef);
assert_eq!(memory.read32(first + 20).unwrap(), 0xcafe_babe);
assert_eq!(cpu.registers[10], if write_back { updated } else { BASE });
assert_eq!(cpu.flags, flags());
assert_eq!(cpu.pc(), ROM_START + 4);
}
}
}
#[test]
fn ldm_all_addressing_modes_load_sparse_registers_and_align_pc() {
let registers = [0, 3, 7, 14, 15];
let list = registers
.iter()
.fold(0, |list, register| list | (1 << register));
for (mode, first, updated) in MODES {
for write_back in [false, true] {
let mut memory = program(&[block(mode, true, write_back, 10, list)]);
for (offset, value) in [11, 22, 33, 44, ROM_START + 0x47].into_iter().enumerate() {
memory.write32(first + offset as u32 * 4, value).unwrap();
}
let mut cpu = Cpu::new(ROM_START);
cpu.registers[10] = BASE;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
for (register, value) in registers
.into_iter()
.zip([11, 22, 33, 44, ROM_START + 0x44])
{
assert_eq!(cpu.registers[register], value);
}
assert_eq!(cpu.registers[10], if write_back { updated } else { BASE });
assert_eq!(cpu.flags, flags());
}
}
}
#[test]
fn stm_base_in_list_stores_old_base_only_when_first_with_writeback() {
// The five-entry list is r1,r3,r5,r7,r9. Exercise each possible base position.
let registers = [1, 3, 5, 7, 9];
let list = registers
.iter()
.fold(0, |list, register| list | (1 << register));
for (mode, first, updated) in MODES {
for write_back in [false, true] {
for (position, base_register) in registers.into_iter().enumerate() {
let mut memory = program(&[block(mode, false, write_back, base_register, list)]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[base_register as usize] = BASE;
cpu.step(&mut memory).unwrap();
let stored = memory.read32(first + position as u32 * 4).unwrap();
assert_eq!(
stored,
if write_back && position != 0 {
updated
} else {
BASE
}
);
assert_eq!(
cpu.registers[base_register as usize],
if write_back { updated } else { BASE }
);
}
}
}
}
#[test]
fn ldm_base_in_list_keeps_loaded_value_instead_of_writeback() {
let registers = [1, 3, 5, 7, 9];
let list = registers
.iter()
.fold(0, |list, register| list | (1 << register));
for (mode, first, _) in MODES {
for write_back in [false, true] {
for base_register in registers {
let mut memory = program(&[block(mode, true, write_back, base_register, list)]);
for (offset, register) in registers.into_iter().enumerate() {
memory
.write32(first + offset as u32 * 4, 100 + register)
.unwrap();
}
let mut cpu = Cpu::new(ROM_START);
cpu.registers[base_register as usize] = BASE;
cpu.step(&mut memory).unwrap();
for register in registers {
assert_eq!(cpu.registers[register as usize], 100 + register);
}
}
}
}
}
#[test]
fn a_single_base_register_in_the_list_uses_arm7_writeback_rules() {
for load in [false, true] {
let mut memory = program(&[block(0x0080_0000, load, true, 2, 1 << 2)]);
memory.write32(BASE, 0x1234_5678).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[2] = BASE;
cpu.step(&mut memory).unwrap();
if load {
assert_eq!(cpu.registers[2], 0x1234_5678);
} else {
assert_eq!(cpu.registers[2], BASE + 4);
assert_eq!(memory.read32(BASE).unwrap(), BASE);
}
}
}
#[test]
fn full_register_list_loads_all_registers_including_the_base_and_pc() {
let mut memory = program(&[block(0x0080_0000, true, true, 10, 0xffff)]);
for register in 0..16 {
memory
.write32(BASE + register * 4, 0x1000 + register * 4)
.unwrap();
}
let mut cpu = Cpu::new(ROM_START);
cpu.registers[10] = BASE;
cpu.step(&mut memory).unwrap();
for register in 0..16 {
assert_eq!(cpu.registers[register], 0x1000 + register as u32 * 4);
}
}
#[test]
fn full_register_list_stores_pc_and_updated_base_without_array_overflow() {
let mut memory = program(&[block(0x0080_0000, false, true, 10, 0xffff)]);
let mut cpu = Cpu::new(ROM_START);
for register in 0..15 {
cpu.registers[register] = register as u32;
}
cpu.registers[10] = BASE;
cpu.step(&mut memory).unwrap();
for register in 0..16 {
let expected = match register {
10 => BASE + 64,
15 => ROM_START + 12,
_ => register,
};
assert_eq!(memory.read32(BASE + register * 4).unwrap(), expected);
}
}
#[test]
fn empty_list_transfers_only_pc_with_a_sixty_four_byte_span() {
let modes = [
(0x0080_0000, BASE, BASE + 64),
(0x0180_0000, BASE + 4, BASE + 64),
(0x0000_0000, BASE - 60, BASE - 64),
(0x0100_0000, BASE - 64, BASE - 64),
];
for (mode, address, updated) in modes {
for write_back in [false, true] {
for load in [false, true] {
let mut memory = program(&[block(mode, load, write_back, 1, 0)]);
memory.write32(address - 4, 0xfeed_face).unwrap();
memory.write32(address, ROM_START + 0x47).unwrap();
memory.write32(address + 4, 0xdead_beef).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = BASE;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], if write_back { updated } else { BASE });
assert_eq!(cpu.pc(), ROM_START + if load { 0x44 } else { 4 });
assert_eq!(
memory.read32(address).unwrap(),
if load {
ROM_START + 0x47
} else {
ROM_START + 12
}
);
assert_eq!(memory.read32(address - 4).unwrap(), 0xfeed_face);
assert_eq!(memory.read32(address + 4).unwrap(), 0xdead_beef);
}
}
}
}
#[test]
fn unaligned_block_transfers_align_words_without_rotation_and_preserve_base_low_bits() {
for offset in 1..4 {
let mut memory = program(&[
block(0x0080_0000, false, true, 0, 0x6),
block(0x0100_0000, true, true, 0, 0x18),
]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = BASE + offset;
cpu.registers[1] = 0x1234_5678;
cpu.registers[2] = 0x90ab_cdef;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], BASE + offset + 8);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[0], BASE + offset);
assert_eq!(cpu.registers[3], 0x1234_5678);
assert_eq!(cpu.registers[4], 0x90ab_cdef);
}
}
#[test]
fn stack_push_and_pop_restore_registers_and_return_through_pc() {
let mut memory = program(&[
0xe92d_4010, // STMDB sp!,{r4,lr}
0xe3a0_4000, // MOV r4,#0
0xe8bd_8010, // LDMIA sp!,{r4,pc}
]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[4] = 42;
cpu.registers[13] = BASE;
cpu.registers[14] = ROM_START + 20;
for _ in 0..3 {
cpu.step(&mut memory).unwrap();
}
assert_eq!(cpu.registers[4], 42);
assert_eq!(cpu.registers[13], BASE);
assert_eq!(cpu.pc(), ROM_START + 20);
assert_eq!(memory.read32(BASE - 8).unwrap(), 42);
assert_eq!(memory.read32(BASE - 4).unwrap(), ROM_START + 20);
}
#[test]
fn block_transfer_can_cross_ram_regions() {
let mut memory = program(&[block(0x0080_0000, false, true, 0, 0x6)]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = 0x02ff_fffc;
cpu.registers[1] = 11;
cpu.registers[2] = 22;
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read32(0x0203_fffc).unwrap(), 11); // External RAM mirror
assert_eq!(memory.read32(0x0300_0000).unwrap(), 22); // Internal RAM
assert_eq!(cpu.registers[0], 0x0300_0004);
}
#[test]
fn block_diagnostics_do_not_commit_partial_register_or_ram_changes() {
for load in [false, true] {
let mut memory = Memory::new(vec![]).unwrap();
memory
.write32(0x0300_0000, block(0x0080_0000, load, true, 2, 0x8001))
.unwrap();
memory.write32(0x07ff_fffc, 0xdead_beef).unwrap();
let mut cpu = Cpu::new(0x0300_0000);
cpu.registers[0] = 42;
cpu.registers[2] = 0x07ff_fffc; // Second transfer reaches absent/read-only ROM.
cpu.flags = flags();
let before = cpu.registers;
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(if load {
MemoryError::Unmapped(ROM_START)
} else {
MemoryError::ReadOnly(ROM_START)
}))
);
assert_eq!(cpu.registers, before);
assert_eq!(cpu.flags, flags());
assert_eq!(memory.read32(0x07ff_fffc).unwrap(), 0xdead_beef);
}
}
#[test]
fn block_load_can_read_rom_and_store_reports_read_only_memory() {
for load in [false, true] {
let mut memory = program(&[block(0x0080_0000, load, true, 0, 0x6), 11, 22]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = ROM_START + 4;
if load {
cpu.step(&mut memory).unwrap();
assert_eq!(&cpu.registers[1..3], &[11, 22]);
assert_eq!(cpu.registers[0], ROM_START + 12);
} else {
let before = cpu.registers;
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::ReadOnly(ROM_START + 4)))
);
assert_eq!(cpu.registers, before);
assert_eq!(memory.read32(ROM_START + 4).unwrap(), 11);
}
}
}
#[test]
fn truncated_rom_does_not_partially_load_a_register_list() {
let mut memory = program(&[block(0x0080_0000, true, true, 0, 0x6), 11]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = ROM_START + 4;
let before = cpu.registers;
assert!(matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))));
assert_eq!(cpu.registers, before);
}
#[test]
fn swap_word_rotates_loads_and_aligns_stores_at_all_offsets() {
for offset in 0..4 {
let mut memory = program(&[0xe100_1092]); // SWP r1,r2,[r0]
memory.write32(BASE, 0x1234_5678).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = BASE + offset;
cpu.registers[2] = 0xaabb_ccdd;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 0x1234_5678_u32.rotate_right(offset * 8));
assert_eq!(memory.read32(BASE).unwrap(), 0xaabb_ccdd);
assert_eq!(cpu.registers[0], BASE + offset);
assert_eq!(cpu.registers[2], 0xaabb_ccdd);
assert_eq!(cpu.flags, flags());
assert_eq!(cpu.pc(), ROM_START + 4);
}
}
#[test]
fn swap_byte_zero_extends_and_preserves_neighboring_bytes() {
let mut memory = program(&[0xe140_1092]); // SWPB r1,r2,[r0]
memory.write32(BASE, 0x1234_8078).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = BASE + 1;
cpu.registers[2] = 0xaabb_ccdd;
cpu.flags = flags();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 0x80);
assert_eq!(memory.read32(BASE).unwrap(), 0x1234_dd78);
assert_eq!(cpu.flags, flags());
}
#[test]
fn swap_snapshots_aliased_source_destination_and_base_registers() {
for byte in [false, true] {
for (destination, source) in [(1, 2), (1, 1), (1, 0), (0, 1), (0, 0)] {
let instruction = 0xe100_0090 | (u32::from(byte) << 22) | (destination << 12) | source;
let mut memory = program(&[instruction]);
memory.write32(BASE, 0x1234_5678).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = BASE;
cpu.registers[1] = 0x1122_3344;
cpu.registers[2] = 0x5566_7788;
let old_source = cpu.registers[source as usize];
cpu.step(&mut memory).unwrap();
assert_eq!(
cpu.registers[destination as usize],
if byte { 0x78 } else { 0x1234_5678 }
);
assert_eq!(
memory.read32(BASE).unwrap(),
if byte {
0x1234_5600 | (old_source & 255)
} else {
old_source
}
);
}
}
}
#[test]
fn swap_write_failure_preserves_destination_and_memory() {
for byte in [false, true] {
let mut memory = program(&[0xe100_1092 | (u32::from(byte) << 22), 0x1234_5678]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = ROM_START + 4;
cpu.registers[1] = 123;
cpu.registers[2] = 456;
let before = cpu.registers;
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::ReadOnly(ROM_START + 4)))
);
assert_eq!(cpu.registers, before);
assert_eq!(memory.read32(ROM_START + 4).unwrap(), 0x1234_5678);
}
}
#[test]
fn swap_read_failure_preserves_cpu_state() {
let mut memory = program(&[0xe100_1092]);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = 42;
let before = cpu.registers;
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(0)))
);
assert_eq!(cpu.registers, before);
}
#[test]
fn skipped_block_transfers_and_swaps_do_not_touch_unmapped_memory() {
for instruction in [0x08a0_0006, 0x08b0_0006, 0x0100_1092, 0x0140_1092] {
let mut memory = program(&[instruction]); // EQ while Z is clear
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = 42;
let mut expected = cpu.registers;
expected[15] += 4;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers, expected);
}
}
#[test]
fn unsupported_transfer_variants_preserve_registers_and_memory() {
for instruction in [
0xe8ef_0001, // PC base plus S bit
0xe89f_0001, // PC base without S bit
0xe8e0_0001, // STM user-bank transfer with writeback
0xe8f0_0001, // LDM user-bank transfer with writeback
0xe8fd_8000, // LDM with status restore
0xe8c0_0000, // Empty-list STM with S bit
0xe10f_1092, // SWP PC base
0xe100_f092, // SWP PC destination
0xe100_109f, // SWP PC source
0xe110_1092, // Reserved SWP bit 20
0xe100_1192, // Reserved SWP bits 11..8
] {
let mut memory = program(&[instruction]);
memory.write32(BASE, 0xdead_beef).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = BASE;
cpu.registers[13] = BASE;
cpu.flags = flags();
let before = cpu.registers;
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::UnsupportedInstruction {
address: ROM_START,
instruction
})
);
assert_eq!(cpu.registers, before);
assert_eq!(cpu.flags, flags());
assert_eq!(memory.read32(BASE).unwrap(), 0xdead_beef);
}
}

View file

@ -0,0 +1,132 @@
//! GBA display clock and status, independent of pixel rendering.
//! Events use supplied clock cycles. Device updates occur at instruction, DMA,
//! or HALT-idle boundaries; display bus contention and IRQ synchronization delays
//! are not modeled. Forced blank does not stop this clock.
pub const CPU_HZ: u32 = 16_777_216;
pub const CYCLES_PER_LINE: u32 = 1232;
pub const HBLANK_START: u32 = 1006;
pub const VISIBLE_LINES: u32 = 160;
pub const LINES_PER_FRAME: u32 = 228;
pub const CYCLES_PER_FRAME: u32 = CYCLES_PER_LINE * LINES_PER_FRAME;
pub const VBLANK_START: u32 = CYCLES_PER_LINE * VISIBLE_LINES;
/// Display position after the latest clock advance. Event counters wrap at u64::MAX.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DisplayPosition {
pub scanline: u16,
pub line_cycle: u16,
/// Number of completed 228-line frames (increments on entry to line zero).
pub frames: u64,
/// Number of entries to line 160, independent of local interrupt enables.
pub vblanks: u64,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(crate) struct Display {
phase: u32,
control: u16,
frames: u64,
vblanks: u64,
}
impl Display {
pub(crate) fn position(&self) -> DisplayPosition {
DisplayPosition {
scanline: (self.phase / CYCLES_PER_LINE) as u16,
line_cycle: (self.phase % CYCLES_PER_LINE) as u16,
frames: self.frames,
vblanks: self.vblanks,
}
}
pub(crate) fn status(&self) -> u16 {
let position = self.position();
// Line 227 is in the non-visible region, but the VBlank flag is clear.
let vblank = (160..227).contains(&position.scanline);
let hblank = u32::from(position.line_cycle) >= HBLANK_START;
let matched = position.scanline == self.control >> 8;
self.control | u16::from(vblank) | (u16::from(hblank) << 1) | (u16::from(matched) << 2)
}
/// Write a DISPSTAT byte. Status bits and unused GBA bits are read-only.
/// Changing the comparator can generate a rising match event immediately.
/// Enabling an IRQ while its status is already set does not synthesize an edge.
pub(crate) fn write_status(&mut self, high: bool, value: u8) -> u16 {
let previously_matched = self.status() & 4 != 0;
let shift = u32::from(high) * 8;
self.control = ((self.control & !(0xff << shift)) | (u16::from(value) << shift)) & 0xff38;
if !previously_matched && self.status() & 4 != 0 && self.control & 0x20 != 0 {
4
} else {
0
}
}
/// Next HBlank entry or scanline start. Both are strictly in the future.
pub(crate) fn next_event_cycles(&self) -> u32 {
let cycle = self.phase % CYCLES_PER_LINE;
if cycle < HBLANK_START {
HBLANK_START - cycle
} else {
CYCLES_PER_LINE - cycle
}
}
/// Display DMA requests are independent of DISPSTAT interrupt enables.
/// Unlike HBlank IRQs, HBlank DMA requests occur only on visible lines.
pub(crate) fn dma_events(&self, cycles: u32) -> (bool, bool) {
let vblank = crossings(self.phase, cycles, CYCLES_PER_FRAME, VBLANK_START) != 0;
let visible_hblanks = |time: u64| {
let frame = u64::from(CYCLES_PER_FRAME);
let line = u64::from(CYCLES_PER_LINE);
let visible = u64::from(VISIBLE_LINES);
time / frame * visible
+ ((time % frame + line - u64::from(HBLANK_START)) / line).min(visible)
};
let begin = u64::from(self.phase);
let hblank = visible_hblanks(begin + u64::from(cycles)) != visible_hblanks(begin);
(vblank, hblank)
}
/// Return IRQ request bits crossed during the interval (old time, new time].
/// Periodic arithmetic handles large batches without skipping events or
/// looping per cycle/scanline. Status derives from the final clock position.
pub(crate) fn advance(&mut self, cycles: u32) -> u16 {
let vblanks = crossings(self.phase, cycles, CYCLES_PER_FRAME, VBLANK_START);
let frames = crossings(self.phase, cycles, CYCLES_PER_FRAME, 0);
let mut requests = 0;
if self.control & 8 != 0 && vblanks != 0 {
requests |= 1;
}
// HBlank status and IRQ events occur on all 228 lines, including VBlank.
if self.control & 0x10 != 0
&& crossings(self.phase, cycles, CYCLES_PER_LINE, HBLANK_START) != 0
{
requests |= 2;
}
let compare = u32::from(self.control >> 8);
if self.control & 0x20 != 0
&& compare < LINES_PER_FRAME
&& crossings(
self.phase,
cycles,
CYCLES_PER_FRAME,
compare * CYCLES_PER_LINE,
) != 0
{
requests |= 4;
}
self.phase =
((u64::from(self.phase) + u64::from(cycles)) % u64::from(CYCLES_PER_FRAME)) as u32;
self.vblanks = self.vblanks.wrapping_add(vblanks);
self.frames = self.frames.wrapping_add(frames);
requests
}
}
fn crossings(start: u32, elapsed: u32, period: u32, offset: u32) -> u64 {
let shift = u64::from(period - offset);
let begin = u64::from(start) + shift;
(begin + u64::from(elapsed)) / u64::from(period) - begin / u64::from(period)
}

259
crates/gba-core/src/dma.rs Normal file
View file

@ -0,0 +1,259 @@
//! Four-channel GBA direct memory access (DMA) controller.
//! Transfers are scheduled one data unit at a time. Startup/resumption delays,
//! open-bus latches, sound FIFO, video capture, and Game Pak DRQ are not modeled.
use std::{error::Error, fmt};
use crate::{memory::MemoryError, timing::AccessWidth};
pub const DMA_BASE: u32 = 0x0400_00b0;
pub const DMA_STRIDE: u32 = 12;
pub(crate) const DMA_END: u32 = DMA_BASE + 4 * DMA_STRIDE - 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DmaError {
Memory { channel: usize, error: MemoryError },
UnsupportedControl { channel: usize, control: u16 },
UnsupportedSource { channel: usize, address: u32 },
RegisterDestination { channel: usize, address: u32 },
PowerControlDestination { channel: usize, address: u32 },
}
impl fmt::Display for DmaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Memory { channel, error } => write!(f, "DMA{channel}: {error}"),
Self::UnsupportedControl { channel, control } => {
write!(f, "DMA{channel}: unsupported control {control:#06x}")
}
Self::UnsupportedSource { channel, address } => {
write!(f, "DMA{channel}: unsupported source {address:#010x}")
}
Self::PowerControlDestination { channel, address } => {
write!(
f,
"DMA{channel}: power-control writes are not supported ({address:#010x})"
)
}
Self::RegisterDestination { channel, address } => {
write!(
f,
"DMA{channel}: writes to DMA registers are not supported ({address:#010x})"
)
}
}
}
}
impl Error for DmaError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Memory { error, .. } => Some(error),
_ => None,
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct Channel {
source: u32,
destination: u32,
count: u16,
control: u16,
current_source: u32,
current_destination: u32,
remaining: u32,
active: bool,
first: bool,
}
impl Channel {
fn enabled(&self) -> bool {
self.control & 0x8000 != 0
}
fn start_timing(&self) -> u16 {
(self.control >> 12) & 3
}
fn unsupported(&self) -> bool {
self.start_timing() == 3 || self.control & 0x800 != 0 || self.control & 0x180 == 0x180
}
fn width(&self) -> AccessWidth {
if self.control & 0x400 != 0 {
AccessWidth::Word
} else {
AccessWidth::Halfword
}
}
fn initial_count(&self, index: usize) -> u32 {
if self.count != 0 {
u32::from(self.count)
} else if index == 3 {
0x1_0000
} else {
0x4000
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(crate) struct Dma {
channels: [Channel; 4],
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Transfer {
pub channel: usize,
pub source: u32,
pub destination: u32,
pub width: AccessWidth,
pub first: bool,
}
impl Dma {
/// Address/count registers are write-only. Zero is a diagnostic placeholder,
/// not hardware open-bus behavior. Only CNT_H has meaningful readback.
pub(crate) fn read8(&self, address: u32) -> u8 {
let offset = address - DMA_BASE;
let channel = &self.channels[(offset / DMA_STRIDE) as usize];
match offset % DMA_STRIDE {
10 => channel.control as u8,
11 => (channel.control >> 8) as u8,
_ => 0,
}
}
pub(crate) fn write8(&mut self, address: u32, value: u8) {
let offset = address - DMA_BASE;
let index = (offset / DMA_STRIDE) as usize;
let channel = &mut self.channels[index];
let register_byte = offset % DMA_STRIDE;
match register_byte {
0..=3 => {
channel.source = replace_byte(channel.source, register_byte, value)
& if index == 0 { 0x07ff_fffe } else { 0x0fff_fffe };
}
4..=7 => {
channel.destination = replace_byte(channel.destination, register_byte - 4, value)
& if index == 3 { 0x0fff_fffe } else { 0x07ff_fffe };
}
8..=9 => {
channel.count = replace_byte(u32::from(channel.count), register_byte - 8, value)
as u16
& if index == 3 { 0xffff } else { 0x3fff };
}
10..=11 => {
let was_enabled = channel.enabled();
channel.control =
replace_byte(u32::from(channel.control), register_byte - 10, value) as u16
& if index == 3 { 0xffe0 } else { 0xf7e0 };
if !channel.enabled() {
channel.active = false;
} else if !was_enabled {
let mask = !(channel.width().bytes() - 1);
channel.current_source = channel.source & mask;
channel.current_destination = channel.destination & mask;
channel.remaining = channel.initial_count(index);
channel.first = true;
channel.active = channel.start_timing() == 0;
}
}
_ => unreachable!("DMA register byte"),
}
}
/// Blanking requests are edges, not queued jobs. An active channel ignores
/// further edges until its current block completes.
pub(crate) fn trigger(&mut self, vblank: bool, hblank: bool) {
for channel in &mut self.channels {
if channel.enabled()
&& !channel.active
&& ((vblank && channel.start_timing() == 1)
|| (hblank && channel.start_timing() == 2))
{
channel.active = true;
}
}
}
/// Select the highest-priority ready channel without changing its state.
/// Unsupported enabled modes report a diagnostic rather than silently waiting.
pub(crate) fn next(&self) -> Result<Option<Transfer>, DmaError> {
for (index, channel) in self.channels.iter().enumerate() {
if !channel.enabled() {
continue;
}
if channel.unsupported() {
return Err(DmaError::UnsupportedControl {
channel: index,
control: channel.control,
});
}
if channel.active {
return Ok(Some(Transfer {
channel: index,
source: channel.current_source,
destination: channel.current_destination,
width: channel.width(),
first: channel.first,
}));
}
}
Ok(None)
}
/// Commit one successful data unit after its bus cycles. Return completion IRQ bits.
pub(crate) fn complete_unit(&mut self, index: usize) -> u16 {
let channel = &mut self.channels[index];
let width = channel.width().bytes();
// The Game Pak source counter increments regardless of source control.
let source_mode = if (0x08..=0x0d).contains(&(channel.current_source >> 24)) {
0
} else {
(channel.control >> 7) & 3
};
channel.current_source = advance_address(channel.current_source, width, source_mode);
channel.current_destination = advance_address(
channel.current_destination,
width,
(channel.control >> 5) & 3,
);
channel.remaining -= 1;
channel.first = false;
if channel.remaining != 0 {
return 0;
}
channel.active = false;
if channel.control & 0x200 != 0 && channel.start_timing() != 0 {
channel.remaining = channel.initial_count(index);
channel.first = true;
if channel.control & 0x60 == 0x60 {
channel.current_destination = channel.destination & !(width - 1);
}
} else {
channel.control &= !0x8000;
}
if channel.control & 0x4000 != 0 {
1 << (8 + index)
} else {
0
}
}
}
fn advance_address(address: u32, width: u32, mode: u16) -> u32 {
match mode {
0 | 3 => address.wrapping_add(width),
1 => address.wrapping_sub(width),
2 => address,
_ => unreachable!("two-bit address control"),
}
}
fn replace_byte(previous: u32, byte: u32, value: u8) -> u32 {
let shift = byte * 8;
(previous & !(0xff << shift)) | (u32::from(value) << shift)
}

View file

@ -0,0 +1,42 @@
//! Platform-independent GBA button state. Bits are pressed-high here;
//! the KEYINPUT register exposes their active-low hardware representation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum Button {
A = 1 << 0,
B = 1 << 1,
Select = 1 << 2,
Start = 1 << 3,
Right = 1 << 4,
Left = 1 << 5,
Up = 1 << 6,
Down = 1 << 7,
R = 1 << 8,
L = 1 << 9,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Buttons(u16);
impl Buttons {
pub fn from_bits(bits: u16) -> Self {
Self(bits & 0x03ff)
}
pub fn bits(self) -> u16 {
self.0
}
pub fn with(self, button: Button, pressed: bool) -> Self {
Self(if pressed {
self.0 | button as u16
} else {
self.0 & !(button as u16)
})
}
pub(crate) fn keyinput(self) -> u16 {
!self.0 & 0x03ff
}
}

383
crates/gba-core/src/io.rs Normal file
View file

@ -0,0 +1,383 @@
//! GBA I/O subset: DMA, timers, interrupts, WAITCNT, display, and polled input.
//! Timer phase resets on enable or clock-source changes. Hardware startup delays,
//! shared prescaler phase, audio events, and interrupt delivery delays are not modeled.
use crate::{
display::{Display, DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
dma::{Dma, DmaError, Transfer, DMA_BASE, DMA_END},
input::Buttons,
};
pub const DISPCNT: u32 = 0x0400_0000;
pub const GREENSWAP: u32 = 0x0400_0002;
pub const DISPSTAT: u32 = 0x0400_0004;
pub const VCOUNT: u32 = 0x0400_0006;
pub const BG0CNT: u32 = 0x0400_0008;
pub const BG1CNT: u32 = 0x0400_000a;
pub const BG2CNT: u32 = 0x0400_000c;
pub const BG3CNT: u32 = 0x0400_000e;
pub const BG0HOFS: u32 = 0x0400_0010;
pub const BG0VOFS: u32 = 0x0400_0012;
pub const BG1HOFS: u32 = 0x0400_0014;
pub const BG1VOFS: u32 = 0x0400_0016;
pub const BG2HOFS: u32 = 0x0400_0018;
pub const BG2VOFS: u32 = 0x0400_001a;
pub const BG3HOFS: u32 = 0x0400_001c;
pub const BG3VOFS: u32 = 0x0400_001e;
pub const BG2PA: u32 = 0x0400_0020;
pub const BG2PB: u32 = 0x0400_0022;
pub const BG2PC: u32 = 0x0400_0024;
pub const BG2PD: u32 = 0x0400_0026;
pub const BG2X: u32 = 0x0400_0028;
pub const BG2Y: u32 = 0x0400_002c;
pub const BG3PA: u32 = 0x0400_0030;
pub const BG3PB: u32 = 0x0400_0032;
pub const BG3PC: u32 = 0x0400_0034;
pub const BG3PD: u32 = 0x0400_0036;
pub const BG3X: u32 = 0x0400_0038;
pub const BG3Y: u32 = 0x0400_003c;
pub const WIN0H: u32 = 0x0400_0040;
pub const WIN1H: u32 = 0x0400_0042;
pub const WIN0V: u32 = 0x0400_0044;
pub const WIN1V: u32 = 0x0400_0046;
pub const WININ: u32 = 0x0400_0048;
pub const WINOUT: u32 = 0x0400_004a;
pub const MOSAIC: u32 = 0x0400_004c;
pub const BLDCNT: u32 = 0x0400_0050;
pub const BLDALPHA: u32 = 0x0400_0052;
pub const BLDY: u32 = 0x0400_0054;
pub const KEYINPUT: u32 = 0x0400_0130;
pub const TIMER_BASE: u32 = 0x0400_0100;
pub const IE: u32 = 0x0400_0200;
pub const IF: u32 = 0x0400_0202;
pub const WAITCNT: u32 = 0x0400_0204;
pub const IME: u32 = 0x0400_0208;
pub const POSTFLG: u32 = 0x0400_0300;
pub const HALTCNT: u32 = 0x0400_0301;
const IRQ_MASK: u16 = 0x3fff;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct Timer {
reload: u16,
counter: u16,
control: u8,
phase: u32,
}
impl Timer {
fn enabled(&self) -> bool {
self.control & 0x80 != 0
}
fn count_up(&self) -> bool {
self.control & 4 != 0
}
fn set_control(&mut self, value: u8, index: usize) {
let value = value & if index == 0 { 0xc3 } else { 0xc7 };
let start = !self.enabled() && value & 0x80 != 0;
if start {
self.counter = self.reload;
}
if start || (self.control ^ value) & 7 != 0 {
self.phase = 0;
}
self.control = value;
}
/// Next independently clocked overflow. Cascaded timers receive their
/// pulses at a predecessor's event and cannot overflow earlier.
fn next_overflow(&self) -> Option<u32> {
if !self.enabled() || self.count_up() {
return None;
}
let divisor = [1, 64, 256, 1024][usize::from(self.control & 3)];
Some((0x1_0000 - u32::from(self.counter)) * divisor - self.phase)
}
/// Advance without looping once per cycle or overflow. Return the number of
/// overflows, not just a boolean, so cascaded timers receive every pulse.
fn advance(&mut self, cycles: u32, previous_overflows: u64) -> u64 {
if !self.enabled() {
return 0;
}
let ticks = if self.count_up() {
previous_overflows
} else {
let divisor = [1, 64, 256, 1024][usize::from(self.control & 3)];
let elapsed = u64::from(self.phase) + u64::from(cycles);
self.phase = (elapsed % divisor) as u32;
elapsed / divisor
};
let until_overflow = 0x1_0000 - u64::from(self.counter);
if ticks < until_overflow {
self.counter += ticks as u16;
return 0;
}
let remaining = ticks - until_overflow;
let period = 0x1_0000 - u64::from(self.reload);
self.counter = (u64::from(self.reload) + remaining % period) as u16;
1 + remaining / period
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(crate) struct Io {
timers: [Timer; 4],
enable: u16,
pending: u16,
master_enable: bool,
waitcnt: u16,
dispcnt: u16,
greenswap: bool,
mosaic: u16,
vertical_mosaic: crate::video::mosaic::VerticalMosaic,
backgrounds: [crate::video::Background; 4],
effects: crate::video::effects::Effects,
window_edges: crate::video::windows::WindowEdges,
horizontal_windows: crate::video::windows::horizontal::HorizontalWindows,
buttons: Buttons,
display: Display,
dma: Dma,
postflg: bool,
halted: bool,
}
impl Io {
pub(crate) fn mapped(address: u32) -> bool {
matches!(address, 0x0400_0000..=0x0400_0057 | DMA_BASE..=DMA_END | 0x0400_0100..=0x0400_010f | 0x0400_0130..=0x0400_0131 | 0x0400_0200..=0x0400_020b | POSTFLG..=0x0400_0303)
}
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
if (DMA_BASE..=DMA_END).contains(&address) {
return Some(self.dma.read8(address));
}
let value = match address & !1 {
DISPCNT => self.dispcnt,
GREENSWAP => u16::from(self.greenswap),
DISPSTAT => self.display.status(),
VCOUNT => self.display.position().scanline,
BG0CNT..=BG3CNT => self.backgrounds[((address - BG0CNT) / 2) as usize].control,
BG0HOFS..=0x0400_003e => 0, // Scroll/affine registers are write-only; not open bus.
WIN0H..=WIN1V | MOSAIC | 0x0400_004e | BLDY | 0x0400_0056 => 0, // Write-only/unused; no open bus yet.
WININ => self.effects.inside,
WINOUT => self.effects.outside,
BLDCNT => self.effects.control,
BLDALPHA => self.effects.alpha,
KEYINPUT => self.buttons.keyinput(),
0x0400_0100..=0x0400_010e => {
let timer = &self.timers[((address - TIMER_BASE) / 4) as usize];
if address & 2 == 0 {
timer.counter
} else {
u16::from(timer.control)
}
}
IE => self.enable,
IF => self.pending,
WAITCNT => self.waitcnt,
0x0400_0206 => 0,
IME => u16::from(self.master_enable),
0x0400_020a => 0,
POSTFLG => u16::from(self.postflg), // HALTCNT reads zero, not an open-bus model.
0x0400_0302 => 0,
_ => return None,
};
Some(value.to_le_bytes()[(address & 1) as usize])
}
/// The memory bus validates every byte before calling this method.
pub(crate) fn write8(&mut self, address: u32, value: u8) {
debug_assert!(Self::mapped(address));
if (DMA_BASE..=DMA_END).contains(&address) {
self.dma.write8(address, value);
return;
}
match address & !1 {
DISPCNT => self.dispcnt = replace_byte(self.dispcnt, address, value) & 0xfff7,
GREENSWAP if address & 1 == 0 => self.greenswap = value & 1 != 0,
DISPSTAT => self.pending |= self.display.write_status(address & 1 != 0, value),
VCOUNT | KEYINPUT => {} // Read-only; writes are ignored.
BG0CNT..=BG3CNT => {
let index = ((address - BG0CNT) / 2) as usize;
let control = &mut self.backgrounds[index].control;
// Overflow applies to affine BG2/BG3 only; bits 4 and 5 are unused.
*control = replace_byte(*control, address, value)
& if index < 2 { 0xdfcf } else { 0xffcf };
}
BG0HOFS..=BG3VOFS => {
let bg = &mut self.backgrounds[((address - BG0HOFS) / 4) as usize];
let offset = if address & 2 == 0 {
&mut bg.x
} else {
&mut bg.y
};
*offset = replace_byte(*offset, address, value) & 0x1ff;
}
BG2PA..=0x0400_003e => {
let index = 2 + ((address - BG2PA) / 16) as usize;
let position = self.display.position();
let after_row = u32::from(position.line_cycle) >= HBLANK_START
|| u32::from(position.scanline) >= VISIBLE_LINES;
self.backgrounds[index]
.affine
.write8((address & 15) as usize, value, after_row);
}
WIN0H..=WIN1V => {
let index = ((address - WIN0H) / 2) as usize;
let bounds = if index < 2 {
&mut self.effects.horizontal[index]
} else {
&mut self.effects.vertical[index - 2]
};
*bounds = replace_byte(*bounds, address, value);
}
MOSAIC => self.mosaic = replace_byte(self.mosaic, address, value),
WININ | WINOUT | BLDCNT | BLDALPHA | BLDY => {
let (latch, mask) = match address & !1 {
WININ => (&mut self.effects.inside, 0x3f3f),
WINOUT => (&mut self.effects.outside, 0x3f3f),
BLDCNT => (&mut self.effects.control, 0x3fff),
BLDALPHA => (&mut self.effects.alpha, 0x1f1f),
_ => (&mut self.effects.brightness, 0x1f),
};
*latch = replace_byte(*latch, address, value) & mask;
}
0x0400_0100..=0x0400_010e => {
let index = ((address - TIMER_BASE) / 4) as usize;
let timer = &mut self.timers[index];
if address & 2 == 0 {
// Merge into the reload latch, never the visible counter.
timer.reload = replace_byte(timer.reload, address, value);
} else if address & 1 == 0 {
timer.set_control(value, index);
}
}
IE => self.enable = replace_byte(self.enable, address, value) & IRQ_MASK,
WAITCNT => self.waitcnt = replace_byte(self.waitcnt, address, value) & 0x5fff,
IF => self.pending &= !(u16::from(value) << ((address & 1) * 8)),
IME if address & 1 == 0 => self.master_enable = value & 1 != 0,
POSTFLG if address == POSTFLG => self.postflg = value & 1 != 0,
POSTFLG => {
// The bus has already rejected STOP and enforced CPU write access.
debug_assert_eq!(value & 0x80, 0);
self.halted = true;
}
_ => {} // Unused I/O bytes ignore writes.
}
self.wake_if_requested();
}
pub(crate) fn halted(&self) -> bool {
self.halted
}
fn wake_if_requested(&mut self) {
// Wake-up ignores IME and CPSR.I. It does not acknowledge IF.
if self.enable & self.pending != 0 {
self.halted = false;
}
}
/// Bound an idle batch by the next display edge or independent timer overflow.
/// Display edges keep this finite even without an enabled wake source.
pub(crate) fn next_event_cycles(&self) -> u32 {
self.timers
.iter()
.filter_map(Timer::next_overflow)
.fold(self.display.next_event_cycles(), u32::min)
}
pub(crate) fn display_position(&self) -> DisplayPosition {
self.display.position()
}
pub(crate) fn set_buttons(&mut self, buttons: Buttons) {
self.buttons = buttons;
}
pub(crate) fn display_control(&self) -> u16 {
self.dispcnt
}
pub(crate) fn video_settings(&self, captured: bool) -> crate::video::RenderSettings<'_> {
crate::video::RenderSettings {
control: self.dispcnt,
prepared_objects: None,
internal_affine: captured,
vertical_windows: captured.then_some(self.window_edges.active),
horizontal_windows: captured.then_some(&self.horizontal_windows),
green_swap: self.greenswap,
mosaic: self.mosaic,
vertical_mosaic: captured.then_some(self.vertical_mosaic),
backgrounds: &self.backgrounds,
effects: &self.effects,
}
}
pub(crate) fn waitcnt(&self) -> u16 {
self.waitcnt
}
pub(crate) fn irq_pending(&self) -> bool {
self.master_enable && self.enable & self.pending != 0
}
pub(crate) fn next_dma(&self) -> Result<Option<Transfer>, DmaError> {
self.dma.next()
}
pub(crate) fn complete_dma_unit(&mut self, channel: usize) {
self.pending |= self.dma.complete_unit(channel);
self.wake_if_requested();
}
pub(crate) fn advance(&mut self, cycles: u32) {
// Hardware state tracks time even when host-side frame capture is disabled.
let position = self.display.position();
let phase = u32::from(position.scanline) * CYCLES_PER_LINE + u32::from(position.line_cycle);
self.window_edges
.advance(phase, cycles, self.effects.vertical);
self.horizontal_windows
.advance(phase, cycles, self.effects.horizontal);
let mode = self.dispcnt & 7;
for index in 2..4 {
let active = self.dispcnt & (0x100 << index) != 0
&& if index == 2 {
(1..=5).contains(&mode)
} else {
mode == 2
};
let (height, counter) = if self.backgrounds[index].control & 0x40 != 0 {
(
u32::from((self.mosaic >> 4) & 15) + 1,
self.vertical_mosaic.background,
)
} else {
(1, 0)
};
self.backgrounds[index]
.affine
.advance(phase, cycles, active, height, counter);
}
self.vertical_mosaic.advance(phase, cycles, self.mosaic);
let (vblank, hblank) = self.display.dma_events(cycles);
self.dma.trigger(vblank, hblank);
self.pending |= self.display.advance(cycles);
let mut overflows = 0;
for (index, timer) in self.timers.iter_mut().enumerate() {
overflows = timer.advance(cycles, overflows);
if overflows != 0 && timer.control & 0x40 != 0 {
// Pending flags latch even if IE, IME, or CPSR.I blocks delivery.
self.pending |= 1 << (3 + index);
}
}
self.wake_if_requested();
}
}
fn replace_byte(previous: u16, address: u32, value: u8) -> u16 {
let shift = (address & 1) * 8;
(previous & !(0xff << shift)) | (u16::from(value) << shift)
}

View file

@ -0,0 +1,12 @@
//! An educational GBA starting point, not a game-compatible emulator.
pub mod bios;
pub mod cpu;
pub mod display;
pub mod dma;
pub mod input;
pub mod io;
pub mod machine;
pub mod memory;
pub mod timing;
pub mod video;

View file

@ -0,0 +1,176 @@
//! CPU/device integration with nominal ARM7 instruction and GBA bus costs.
//! Device clocks update at instruction, DMA-unit, or HALT-idle boundaries, not per bus access.
use std::{error::Error, fmt};
use crate::{
cpu::{Cpu, CpuError},
dma::DmaError,
memory::Memory,
timing::StepTiming,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepKind {
Instruction,
IrqEntry,
Dma {
channel: usize,
},
/// Advance device clocks to the next event while the CPU stays paused.
HaltIdle,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MachineError {
Cpu(CpuError),
Dma(DmaError),
}
impl fmt::Display for MachineError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cpu(error) => write!(f, "{error}"),
Self::Dma(error) => write!(f, "{error}"),
}
}
}
impl Error for MachineError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Cpu(error) => Some(error),
Self::Dma(error) => Some(error),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FrameRunError {
Cpu(CpuError),
Dma(DmaError),
StepLimit(usize),
}
impl fmt::Display for FrameRunError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cpu(error) => write!(f, "{error}"),
Self::Dma(error) => write!(f, "{error}"),
Self::StepLimit(limit) => write!(f, "no VBlank entry within {limit} machine steps"),
}
}
}
impl Error for FrameRunError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Cpu(error) => Some(error),
Self::Dma(error) => Some(error),
Self::StepLimit(_) => None,
}
}
}
/// Owns the CPU and memory bus. Use this instead of Cpu::step to advance device
/// clocks, execute DMA, honor HALT, and deliver interrupts. No audio or STOP yet.
pub struct Machine {
cpu: Cpu,
memory: Memory,
last_timing: StepTiming,
}
impl Machine {
pub fn new(cpu: Cpu, memory: Memory) -> Self {
Self {
cpu,
memory,
last_timing: StepTiming::default(),
}
}
pub fn cpu(&self) -> &Cpu {
&self.cpu
}
pub fn memory(&self) -> &Memory {
&self.memory
}
/// Host setup and inspection. Bus writes do not automatically advance time.
pub fn memory_mut(&mut self) -> &mut Memory {
&mut self.memory
}
pub fn halted(&self) -> bool {
self.memory.halted()
}
pub fn cycles(&self) -> u64 {
self.memory.cycles()
}
/// Execute through the next entry to scanline 160, even when already in VBlank.
/// Stops at the first instruction, DMA-unit, or HALT-idle boundary after the event.
/// DMA requested by that event may still be pending. Prior successful
/// steps remain committed on error or limit exhaustion. Zero steps always fails.
pub fn run_until_vblank(&mut self, max_steps: usize) -> Result<usize, FrameRunError> {
let previous = self.memory.display_position().vblanks;
for steps in 1..=max_steps {
self.step().map_err(|error| match error {
MachineError::Cpu(error) => FrameRunError::Cpu(error),
MachineError::Dma(error) => FrameRunError::Dma(error),
})?;
if self.memory.display_position().vblanks != previous {
return Ok(steps);
}
}
Err(FrameRunError::StepLimit(max_steps))
}
/// Cost of the last successful step. Failed steps leave this unchanged.
pub fn last_timing(&self) -> StepTiming {
self.last_timing
}
/// Service one ready DMA unit before sampling IRQ or executing CPU code.
/// Lower channel numbers take priority; higher-priority requests can preempt
/// between units. The CPU and its registers remain paused throughout DMA.
/// If HALT is still waiting, advance to the next display edge or timer overflow.
/// Wake-up ignores IME and CPSR.I; IRQ delivery still respects both masks.
/// Sample IRQ before executing the next instruction. GBA devices do not
/// generate FIQ. IRQ entry consumes a separate step and does not clear IF.
/// CPU/DMA work uses nominal costs; HALT advances exactly to the next device event.
/// CPU writes take effect before this bulk device update. Within-instruction
/// register timing, prefetch, and IRQ synchronization delays are not modeled.
/// CPU/DMA diagnostics leave CPU, devices, and the clock unchanged for this step.
/// Row-capture diagnostics are deferred to Memory::present_frame instead.
pub fn step(&mut self) -> Result<StepKind, MachineError> {
if let Some((channel, timing)) = self.memory.step_dma().map_err(MachineError::Dma)? {
self.last_timing = timing;
return Ok(StepKind::Dma { channel });
}
if self.memory.halted() {
let timing = StepTiming {
idle_cycles: self.memory.next_event_cycles(),
..StepTiming::default()
};
self.memory.advance_cycles(timing.total());
self.last_timing = timing;
return Ok(StepKind::HaltIdle);
}
let (kind, timing) = if self.cpu.take_interrupt(self.memory.irq_pending(), false) {
(StepKind::IrqEntry, self.cpu.exception_timing(&self.memory))
} else {
(
StepKind::Instruction,
self.cpu
.step_timed(&mut self.memory)
.map_err(MachineError::Cpu)?,
)
};
self.memory.advance_cycles(timing.total());
self.last_timing = timing;
Ok(kind)
}
}

View file

@ -0,0 +1,705 @@
use std::{cell::Cell, error::Error, fmt};
use crate::{
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
dma::{DmaError, DMA_BASE, DMA_END},
input::Buttons,
io::{Io, HALTCNT, POSTFLG},
timing::{bus_cycles, AccessKind, AccessWidth, DataTiming, StepTiming},
video::{self, sprites::pipeline, Framebuffer, VideoError},
};
pub const ROM_START: u32 = 0x0800_0000;
const ROM_CAPACITY: usize = 32 * 1024 * 1024;
pub const BIOS_SIZE: usize = 16 * 1024;
pub const PALETTE_START: u32 = 0x0500_0000;
pub const VRAM_START: u32 = 0x0600_0000;
pub const OAM_START: u32 = 0x0700_0000;
const VRAM_SIZE: usize = 96 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MemoryError {
Unmapped(u32),
ReadOnly(u32),
Unaligned(u32),
RomTooLarge(usize),
InvalidBiosSize(usize),
UnsupportedStop,
}
impl fmt::Display for MemoryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnsupportedStop => write!(f, "HALTCNT STOP mode is not implemented"),
Self::Unmapped(address) => write!(f, "unmapped memory at {address:#010x}"),
Self::ReadOnly(address) => write!(f, "read-only memory at {address:#010x}"),
Self::Unaligned(address) => write!(f, "unaligned memory access at {address:#010x}"),
Self::RomTooLarge(size) => write!(f, "ROM size {size} exceeds 32 MiB"),
Self::InvalidBiosSize(size) => {
write!(f, "BIOS size {size} must be exactly {BIOS_SIZE} bytes")
}
}
}
}
impl Error for MemoryError {}
fn vram_index(address: u32) -> usize {
let offset = (address & 0x1ffff) as usize;
if offset >= 0x18000 {
offset - 0x8000
} else {
offset
}
}
/// Work/video/palette RAM, OAM, cartridge ROM, optional BIOS, and supported I/O.
/// BIOS read protection and open-bus behavior are not modeled.
pub struct Memory {
external_ram: Vec<u8>,
internal_ram: Vec<u8>,
palette_ram: Vec<u8>,
video_ram: Vec<u8>,
oam: Vec<u8>,
rom: Vec<u8>,
bios: Option<Vec<u8>>,
io: Io,
cycles: u64,
data_timing: Cell<Option<DataTiming>>,
cpu_access: Option<u32>,
scanline_capture: Option<video::capture::Capture>,
sprite_pipeline: pipeline::SpritePipeline,
}
impl Memory {
pub fn new(rom: Vec<u8>) -> Result<Self, MemoryError> {
if rom.len() > ROM_CAPACITY {
return Err(MemoryError::RomTooLarge(rom.len()));
}
Ok(Self {
external_ram: vec![0; 256 * 1024],
internal_ram: vec![0; 32 * 1024],
palette_ram: vec![0; 1024],
video_ram: vec![0; VRAM_SIZE],
oam: vec![0; 1024],
rom,
bios: None,
io: Io::default(),
cycles: 0,
data_timing: Cell::new(None),
cpu_access: None,
scanline_capture: None,
sprite_pipeline: pipeline::SpritePipeline::default(),
})
}
/// Map a caller-supplied 16 KiB image at the ARM exception vectors.
/// No Nintendo BIOS is included. The image is read-only and is not mirrored.
pub fn with_bios(rom: Vec<u8>, bios: Vec<u8>) -> Result<Self, MemoryError> {
if bios.len() != BIOS_SIZE {
return Err(MemoryError::InvalidBiosSize(bios.len()));
}
let mut memory = Self::new(rom)?;
memory.bios = Some(bios);
Ok(memory)
}
/// Total supplied clock cycles, wrapping at u64::MAX. Reads/writes alone
/// consume no cycles. Machine stepping or the caller must advance time.
pub fn cycles(&self) -> u64 {
self.cycles
}
/// Advance devices and optional row capture. Multiple unserviced DMA requests
/// coalesce. No CPU or DMA work is executed by this method.
pub fn advance_cycles(&mut self, cycles: u32) {
if cycles != 0 && !self.sprite_pipeline.started {
// Synthetic reset begins on row0, with no preceding row227 to prepare it.
self.prepare_sprite_row(0);
self.sprite_pipeline.started = true;
}
if self.scanline_capture.is_none() {
let position = self.io.display_position();
let phase =
u32::from(position.scanline) * CYCLES_PER_LINE + u32::from(position.line_cycle);
let mut advanced = 0;
for (offset, row) in pipeline::latest_events(phase, cycles).into_iter().flatten() {
self.io.advance(offset - advanced);
self.prepare_sprite_row(row);
advanced = offset;
}
self.io.advance(cycles - advanced);
} else {
// Split at display and sprite-preparation edges. CPU/DMA writes have
// already committed before these nominal clocks; no per-access timing.
let mut remaining = cycles;
while remaining != 0 {
let position = self.io.display_position();
let cycle = u32::from(position.line_cycle);
let display_edge = if cycle < HBLANK_START {
HBLANK_START - cycle
} else {
CYCLES_PER_LINE - cycle
};
let until =
display_edge.min(pipeline::next_event(position.scanline, position.line_cycle));
let step = remaining.min(until);
self.io.advance(step);
remaining -= step;
if step == until {
let position = self.io.display_position();
if u32::from(position.line_cycle) == pipeline::PREPARE_CYCLE {
if let Some(row) = pipeline::target(position.scanline) {
self.prepare_sprite_row(row);
}
} else if u32::from(position.line_cycle) == HBLANK_START
&& u32::from(position.scanline) < VISIBLE_LINES
{
let row = usize::from(position.scanline);
if self.scanline_capture.as_ref().unwrap().wants_row(row) {
let mut settings = self.io.video_settings(true);
settings.prepared_objects = Some(self.sprite_pipeline.row(row));
let pixels = video::render_rows(
settings,
&self.video_ram,
&self.palette_ram,
&self.oam,
row..row + 1,
);
self.scanline_capture.as_mut().unwrap().record(row, pixels);
}
} else if position.line_cycle == 0
&& u32::from(position.scanline) == VISIBLE_LINES
{
self.scanline_capture
.as_mut()
.unwrap()
.publish(position.vblanks);
}
}
}
}
self.cycles = self.cycles.wrapping_add(u64::from(cycles));
}
fn prepare_sprite_row(&mut self, row: usize) {
let settings = self.io.video_settings(true);
let prepared = video::sprites::prepare(
settings.control,
settings.mosaic,
settings.vertical_mosaic.map(|v| v.object),
&self.oam,
&self.video_ram,
row..row + 1,
);
self.sprite_pipeline.store(row, prepared);
}
/// Enable row capture without changing clocks or hardware registers. Disabled
/// by default to keep CPU-only tests and large clock batches inexpensive.
/// Repeating the current setting preserves captured data. Disabling drops it.
pub fn set_scanline_rendering(&mut self, enabled: bool) {
if enabled && self.scanline_capture.is_none() {
self.scanline_capture = Some(video::capture::Capture::default());
} else if !enabled {
self.scanline_capture = None;
}
}
/// VBlank number of the latest full captured frame, including failed frames.
/// None means capture is disabled or no full frame has reached VBlank yet.
pub fn captured_vblank(&self) -> Option<u64> {
self.scanline_capture
.as_ref()
.and_then(video::capture::Capture::vblank)
}
/// Copy the latest captured frame. False means no complete frame is available.
/// Rendering errors are reported here, not as CPU/DMA failures. On errors or
/// false, the output is unchanged. Presenting does not advance time or consume
/// the frame. Writes after capture cannot change its pixels.
pub fn present_frame(&self, output: &mut Framebuffer) -> Result<bool, VideoError> {
match &self.scanline_capture {
Some(capture) => capture.present(output),
None => Ok(false),
}
}
/// Execute at most one DMA unit. Validate before side effects, then advance
/// device clocks with the CPU paused. Earlier units remain committed on error.
pub(crate) fn step_dma(&mut self) -> Result<Option<(usize, StepTiming)>, DmaError> {
let Some(transfer) = self.io.next_dma()? else {
return Ok(None);
};
let channel = transfer.channel;
if (POSTFLG..=POSTFLG + 3).contains(&transfer.destination) {
return Err(DmaError::PowerControlDestination {
channel,
address: transfer.destination,
});
}
if transfer.source < 0x0200_0000 {
return Err(DmaError::UnsupportedSource {
channel,
address: transfer.source,
});
}
if (DMA_BASE..=DMA_END).contains(&transfer.destination) {
return Err(DmaError::RegisterDestination {
channel,
address: transfer.destination,
});
}
let map_error = |error| DmaError::Memory { channel, error };
// Reads have no side effects. A failed destination leaves the unit pending.
let value = match transfer.width {
AccessWidth::Halfword => {
self.write_index::<2>(transfer.destination)
.map_err(map_error)?;
u32::from(self.read16(transfer.source).map_err(map_error)?)
}
AccessWidth::Word => {
self.write_index::<4>(transfer.destination)
.map_err(map_error)?;
self.read32(transfer.source).map_err(map_error)?
}
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
};
let kind = if transfer.first {
AccessKind::NonSequential
} else {
AccessKind::Sequential
};
let timing = StepTiming {
code_cycles: 0,
data_cycles: bus_cycles(self.waitcnt(), transfer.source, transfer.width, kind)
+ bus_cycles(self.waitcnt(), transfer.destination, transfer.width, kind),
internal_cycles: if transfer.first { 2 } else { 0 },
idle_cycles: 0,
};
match transfer.width {
AccessWidth::Halfword => self.write16(transfer.destination, value as u16),
AccessWidth::Word => self.write32(transfer.destination, value),
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
}
.map_err(map_error)?;
// Keep this channel active during its cycles: another edge must not
// queue a second block while the current block is still transferring.
self.advance_cycles(timing.total());
self.io.complete_dma_unit(channel);
Ok(Some((channel, timing)))
}
/// Whether HALT is waiting for IE & IF to become nonzero.
/// IME and the CPU's interrupt mask do not affect this state.
pub fn halted(&self) -> bool {
self.io.halted()
}
pub(crate) fn next_event_cycles(&self) -> u32 {
self.io.next_event_cycles()
}
pub(crate) fn begin_cpu_access(&mut self, pc: u32) {
debug_assert!(self.cpu_access.is_none());
self.cpu_access = Some(pc);
}
pub(crate) fn end_cpu_access(&mut self) {
self.cpu_access = None;
}
fn can_write_power_control(&self) -> bool {
// Bare memory writes are host/debug setup. CPU code must execute in BIOS.
self.cpu_access.is_none_or(|pc| pc < BIOS_SIZE as u32)
}
/// Current scanline position and frame/VBlank event counters.
pub fn display_position(&self) -> DisplayPosition {
self.io.display_position()
}
/// The GBA IRQ line after IE/IF/IME gating, before the CPU's interrupt mask.
pub fn irq_pending(&self) -> bool {
self.io.irq_pending()
}
/// Replace the current pressed-button state. This does not advance time.
/// KEYCNT and keypad interrupts are not implemented yet.
pub fn set_buttons(&mut self, buttons: Buttons) {
self.io.set_buttons(buttons);
}
/// Render a Mode 0–5 snapshot. No emulated cycles are consumed.
/// Uses programmed affine origins, geometric window bounds, and screen-grid mosaic.
/// Ignores captured history. Use present_frame for latched scanline state.
pub fn render_frame(&self, frame: &mut Framebuffer) -> Result<(), VideoError> {
video::render(
self.io.video_settings(false),
&self.video_ram,
&self.palette_ram,
&self.oam,
frame,
)
}
/// Current Game Pak wait-state control. Prefetch is not implemented.
pub fn waitcnt(&self) -> u16 {
self.io.waitcnt()
}
pub(crate) fn begin_data_timing(&self) {
debug_assert!(self.data_timing.get().is_none());
self.data_timing.set(Some(DataTiming::default()));
}
pub(crate) fn end_data_timing(&self) -> u32 {
self.data_timing.take().map_or(0, |trace| trace.cycles)
}
fn record_access(&self, address: u32, width: AccessWidth) {
if let Some(mut trace) = self.data_timing.get() {
trace.access(self.waitcnt(), address, width);
self.data_timing.set(Some(trace));
}
}
pub fn read8(&self, address: u32) -> Result<u8, MemoryError> {
let value = self.read_byte(address)?;
self.record_access(address, AccessWidth::Byte);
Ok(value)
}
// Multi-byte reads use this helper to avoid charging each byte as a bus access.
fn read_byte(&self, address: u32) -> Result<u8, MemoryError> {
match address >> 24 {
0x00 => self
.bios
.as_ref()
.and_then(|bios| bios.get(address as usize))
.copied()
.ok_or(MemoryError::Unmapped(address)),
0x02 => Ok(self.external_ram[(address & 0x3ffff) as usize]),
0x03 => Ok(self.internal_ram[(address & 0x7fff) as usize]),
0x04 => self.io.read8(address).ok_or(MemoryError::Unmapped(address)),
0x05 => Ok(self.palette_ram[(address & 0x3ff) as usize]),
0x06 => Ok(self.video_ram[vram_index(address)]),
0x07 => Ok(self.oam[(address & 0x3ff) as usize]),
0x08..=0x0d => self
.rom
.get((address & 0x01ff_ffff) as usize)
.copied()
.ok_or(MemoryError::Unmapped(address)),
_ => Err(MemoryError::Unmapped(address)),
}
}
pub fn write8(&mut self, address: u32, value: u8) -> Result<(), MemoryError> {
self.write_aligned(address, [value])
}
pub fn write16(&mut self, address: u32, value: u16) -> Result<(), MemoryError> {
self.write_aligned(address, value.to_le_bytes())
}
pub fn write32(&mut self, address: u32, value: u32) -> Result<(), MemoryError> {
self.write_aligned(address, value.to_le_bytes())
}
// Validate the complete access before any RAM or I/O side effects.
// Timer reload bytes precede control bytes in a combined 32-bit write.
fn write_aligned<const N: usize>(
&mut self,
address: u32,
bytes: [u8; N],
) -> Result<(), MemoryError> {
let index = self.validate_write(address, &bytes)?;
self.record_access(
address,
match N {
1 => AccessWidth::Byte,
2 => AccessWidth::Halfword,
4 => AccessWidth::Word,
_ => unreachable!("unsupported bus width"),
},
);
if address >> 24 == 0x04 {
for (offset, byte) in bytes.into_iter().enumerate() {
let byte_address = address + offset as u32;
if matches!(byte_address, POSTFLG | HALTCNT) && !self.can_write_power_control() {
continue; // BIOS-only CPU writes; ignored elsewhere, including STOP requests.
}
self.io.write8(byte_address, byte);
}
return Ok(());
}
if N == 1 && address >> 24 == 0x07 {
return Ok(()); // OAM ignores byte writes; halfword/word writes remain valid.
}
if N == 1 && matches!(address >> 24, 0x05 | 0x06) {
let bg_limit = if (3..=5).contains(&(self.io.display_control() & 7)) {
0x14000
} else {
0x10000
};
// Byte writes to OBJ VRAM are ignored. BG/palette writes duplicate
// the byte into both halves of the aligned 16-bit location.
if address >> 24 == 0x06 && index >= bg_limit {
return Ok(());
}
let ram = if address >> 24 == 0x05 {
&mut self.palette_ram
} else {
&mut self.video_ram
};
let aligned = index & !1;
ram[aligned..aligned + 2].fill(bytes[0]);
return Ok(());
}
let ram = match address >> 24 {
0x02 => &mut self.external_ram,
0x03 => &mut self.internal_ram,
0x05 => &mut self.palette_ram,
0x06 => &mut self.video_ram,
0x07 => &mut self.oam,
_ => unreachable!("validated writable region"),
};
ram[index..index + N].copy_from_slice(&bytes);
Ok(())
}
fn validate_write<const N: usize>(
&self,
address: u32,
bytes: &[u8; N],
) -> Result<usize, MemoryError> {
let index = self.write_index::<N>(address)?;
if self.can_write_power_control() {
for (offset, byte) in bytes.iter().enumerate() {
if address + offset as u32 == HALTCNT && byte & 0x80 != 0 {
return Err(MemoryError::UnsupportedStop);
}
}
}
Ok(index)
}
fn write_index<const N: usize>(&self, address: u32) -> Result<usize, MemoryError> {
if address & (N as u32 - 1) != 0 {
return Err(MemoryError::Unaligned(address));
}
match address >> 24 {
0x00 if self.bios.is_some() && address < BIOS_SIZE as u32 => {
Err(MemoryError::ReadOnly(address))
}
0x02 => Ok((address & 0x3ffff) as usize),
0x03 => Ok((address & 0x7fff) as usize),
0x05 => Ok((address & 0x3ff) as usize),
0x06 => Ok(vram_index(address)),
0x07 => Ok((address & 0x3ff) as usize),
0x04 => {
for offset in 0..N as u32 {
if !Io::mapped(address + offset) {
return Err(MemoryError::Unmapped(address + offset));
}
}
Ok(0) // I/O does not use a RAM index.
}
0x08..=0x0d => Err(MemoryError::ReadOnly(address)),
_ => Err(MemoryError::Unmapped(address)),
}
}
/// Validate a batch before any RAM or I/O writes. Diagnostic errors cannot
/// leave a partial block store. This is not a model of hardware data aborts.
/// Mapped I/O reads currently have no side effects; writes cannot fail after validation.
pub(crate) fn write_words(&mut self, writes: &[(u32, u32)]) -> Result<(), MemoryError> {
for &(address, value) in writes {
self.validate_write(address, &value.to_le_bytes())?;
}
for &(address, value) in writes {
// The map cannot change between validation and these writes.
self.write32(address, value)?;
}
Ok(())
}
pub fn read16(&self, address: u32) -> Result<u16, MemoryError> {
if address & 1 != 0 {
return Err(MemoryError::Unaligned(address));
}
let value = u16::from_le_bytes([self.read_byte(address)?, self.read_byte(address + 1)?]);
self.record_access(address, AccessWidth::Halfword);
Ok(value)
}
/// The bus requires alignment; the CPU applies ARM7TDMI load rotation.
pub fn read32(&self, address: u32) -> Result<u32, MemoryError> {
if address & 3 != 0 {
return Err(MemoryError::Unaligned(address));
}
let value = u32::from_le_bytes([
self.read_byte(address)?,
self.read_byte(address + 1)?,
self.read_byte(address + 2)?,
self.read_byte(address + 3)?,
]);
self.record_access(address, AccessWidth::Word);
Ok(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
display::CYCLES_PER_LINE,
io::{DISPSTAT, IF},
};
#[test]
fn affine_state_tracks_without_capture_and_survives_failed_batch_writes() {
use crate::io::{BG2PA, BG2PB, BG2PD, BG2X, DISPCNT};
let mut bulk = Memory::new(vec![]).unwrap();
let mut captured = Memory::new(vec![]).unwrap();
captured.set_scanline_rendering(true);
for m in [&mut bulk, &mut captured] {
m.write16(DISPCNT, 0x403).unwrap();
m.write16(BG2PA, 256).unwrap();
m.write16(BG2PB, (-181i16) as u16).unwrap();
m.write16(BG2PD, 91).unwrap();
m.write32(BG2X, 12345).unwrap();
}
for cycles in [0, 1006, 226, 7319, 280896, 1232 * 317] {
for m in [&mut bulk, &mut captured] {
m.advance_cycles(cycles);
}
assert_eq!(
bulk.io.video_settings(true).backgrounds[2].affine,
captured.io.video_settings(true).backgrounds[2].affine
);
}
let before = bulk.io.video_settings(true).backgrounds[2].affine;
assert!(bulk.write_words(&[(BG2X, 0), (0x0400_0058, 0)]).is_err());
assert_eq!(bulk.io.video_settings(true).backgrounds[2].affine, before);
bulk.set_scanline_rendering(true);
bulk.set_scanline_rendering(false);
assert_eq!(bulk.io.video_settings(true).backgrounds[2].affine, before);
}
#[test]
fn window_flags_match_batched_and_captured_clocks_and_survive_failed_writes() {
use crate::io::{WIN0V, WIN1V};
let mut bulk = Memory::new(vec![]).unwrap();
let mut captured = Memory::new(vec![]).unwrap();
captured.set_scanline_rendering(true);
for (cycles, first, second) in [
(1006, 0x0010, 0xc808),
(226, 0x0010, 0xc808),
(1232 * 5, 0x0508, 0x0014),
(280896 * 3, 0xffff, 0xc808),
(1232 * 500 + 100, 0x00ff, 0xff00),
] {
for m in [&mut bulk, &mut captured] {
m.write16(WIN0V, first).unwrap();
m.write16(WIN1V, second).unwrap();
m.advance_cycles(cycles);
}
assert_eq!(
bulk.io.video_settings(true).vertical_windows,
captured.io.video_settings(true).vertical_windows
);
}
let before = bulk.io.clone();
assert!(bulk.write_words(&[(WIN0V, 0), (0x0400_0058, 0)]).is_err());
assert_eq!(bulk.io, before);
bulk.set_scanline_rendering(true);
bulk.set_scanline_rendering(false);
assert_eq!(bulk.io, before);
}
#[test]
fn mosaic_phase_matches_batched_capture_and_survives_writes_and_capture_toggles() {
use crate::io::{BG2CNT, BG2PD, DISPCNT, MOSAIC};
let mut bulk = Memory::new(vec![]).unwrap();
let mut captured = Memory::new(vec![]).unwrap();
captured.set_scanline_rendering(true);
for m in [&mut bulk, &mut captured] {
m.write16(DISPCNT, 0x403).unwrap();
m.write16(BG2CNT, 0x40).unwrap();
m.write16(BG2PD, 91).unwrap();
}
for (cycles, sizes) in [
(6 * CYCLES_PER_LINE, 0x7070),
(9 * CYCLES_PER_LINE, 0x1010),
(1006, 0x3010),
(226, 0x0030),
(280896 * 3 + 1232 * 5, 0x9030),
] {
for m in [&mut bulk, &mut captured] {
m.write16(MOSAIC, sizes).unwrap();
m.advance_cycles(cycles);
}
assert_eq!(bulk.io, captured.io);
}
let before = bulk.io.clone();
assert!(bulk.write_words(&[(MOSAIC, 0), (0x0400_0058, 0)]).is_err());
assert_eq!(bulk.io, before);
bulk.set_scanline_rendering(true);
bulk.set_scanline_rendering(false);
assert_eq!(bulk.io, before);
let counters = bulk.io.video_settings(true).vertical_mosaic;
bulk.write16(MOSAIC, 0).unwrap();
bulk.advance_cycles(0);
assert_eq!(bulk.io.video_settings(true).vertical_mosaic, counters);
}
#[test]
fn horizontal_history_matches_capture_clock_splits_and_survives_failed_writes() {
use crate::io::WIN0H;
let mut bulk = Memory::new(vec![]).unwrap();
let mut captured = Memory::new(vec![]).unwrap();
captured.set_scanline_rendering(true);
for (cycles, bounds) in [
(81, 0x0a50),
(3, 0x1450),
(922, 0x2828),
(14, 0xff0a),
(1, 0xff0a),
(211, 0xff0a),
(280896 * 3 + 100, 0xf014),
] {
for m in [&mut bulk, &mut captured] {
m.write32(WIN0H, bounds | ((bounds ^ 0x371b) << 16))
.unwrap();
m.advance_cycles(cycles);
}
assert_eq!(bulk.io, captured.io);
}
let before = bulk.io.clone();
assert!(bulk.write_words(&[(WIN0H, 0), (0x0400_0058, 0)]).is_err());
assert_eq!(bulk.io, before);
bulk.advance_cycles(0);
bulk.set_scanline_rendering(true);
bulk.set_scanline_rendering(false);
let mut snapshot = Framebuffer::default();
bulk.render_frame(&mut snapshot).unwrap();
assert_eq!(bulk.io, before);
}
#[test]
fn failed_batch_does_not_change_comparison_or_latch_match_irq() {
let mut bus = Memory::new(vec![]).unwrap();
bus.advance_cycles(7 * CYCLES_PER_LINE);
let before = bus.display_position();
// This batch is no longer reachable with one CPU STM: the mapped
// display register area now exceeds its sixteen-word transfer limit.
assert_eq!(
bus.write_words(&[(DISPSTAT, 0x720), (0x0400_0058, 0)]),
Err(MemoryError::Unmapped(0x0400_0058))
);
assert_eq!(bus.display_position(), before);
assert_eq!(bus.read16(DISPSTAT).unwrap(), 0);
assert_eq!(bus.read16(IF).unwrap(), 0);
}
}

View file

@ -0,0 +1,126 @@
//! Nominal GBA bus costs. These calculations do not perform memory accesses.
//! Game Pak prefetch, display-bus contention, and configurable EWRAM timing are not modeled.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessWidth {
Byte,
Halfword,
Word,
}
impl AccessWidth {
pub(crate) fn bytes(self) -> u32 {
match self {
Self::Byte => 1,
Self::Halfword => 2,
Self::Word => 4,
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum AccessKind {
NonSequential,
#[default]
Sequential,
}
/// Timing breakdown for one instruction, exception entry, DMA unit, or HALT batch.
/// DMA has zero code cycles and charges two internal cycles on its first unit.
/// This is a nominal estimate, not a cycle-accurate bus trace.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct StepTiming {
pub code_cycles: u32,
pub data_cycles: u32,
pub internal_cycles: u32,
/// Device-clock cycles while the CPU is halted, without CPU or DMA bus work.
pub idle_cycles: u32,
}
impl StepTiming {
pub fn total(self) -> u32 {
self.code_cycles + self.data_cycles + self.internal_cycles + self.idle_cycles
}
}
/// Compute an access cost from a WAITCNT snapshot. This does not validate the
/// address against the memory map. Unmapped regions retain a one-cycle fallback
/// so a branch can finish before the next instruction fetch reports a diagnostic.
/// WAITCNT's prefetch and PHI bits are stored but do not affect this calculation.
pub fn bus_cycles(waitcnt: u16, address: u32, width: AccessWidth, kind: AccessKind) -> u32 {
match address >> 24 {
0x02 => {
if width == AccessWidth::Word {
6
} else {
3
}
}
0x05 | 0x06 => {
if width == AccessWidth::Word {
2
} else {
1
}
}
0x08..=0x0d => {
let window = ((address >> 25) - 4) as usize;
let first_shift = [2, 5, 8][window];
let second_shift = [4, 7, 10][window];
let first = 1 + [4, 3, 2, 8][usize::from((waitcnt >> first_shift) & 3)];
let second = 1 + if waitcnt & (1 << second_shift) != 0 {
1
} else {
[2, 4, 8][window]
};
// The Game Pak bus forces N timing at each 128 KiB boundary.
let sequential = kind == AccessKind::Sequential && address & 0x1fffe != 0;
let cycles = if sequential { second } else { first };
// Each word is two halfwords. The second halfword always uses S timing.
cycles
+ if width == AccessWidth::Word {
second
} else {
0
}
}
// BIOS, IWRAM, I/O, and OAM use a 32-bit bus. Other areas remain unmapped.
_ => 1,
}
}
pub(crate) fn refill_cycles(waitcnt: u16, target: u32, width: AccessWidth) -> u32 {
bus_cycles(waitcnt, target, width, AccessKind::NonSequential)
+ bus_cycles(
waitcnt,
target.wrapping_add(width.bytes()),
width,
AccessKind::Sequential,
)
+ bus_cycles(
waitcnt,
target.wrapping_add(2 * width.bytes()),
width,
AccessKind::Sequential,
)
}
/// A data-only trace. Instruction fetches and branch refills are charged by the
/// CPU timing summary, not here. Actual CPU bus calls supply addresses and widths.
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct DataTiming {
pub cycles: u32,
next: Option<u32>,
}
impl DataTiming {
pub fn access(&mut self, waitcnt: u16, address: u32, width: AccessWidth) {
let kind = if self.next == Some(address) {
AccessKind::Sequential
} else {
AccessKind::NonSequential
};
self.cycles += bus_cycles(waitcnt, address, width, kind);
self.next = Some(address.wrapping_add(width.bytes()));
}
}

View file

@ -0,0 +1,381 @@
//! Platform-independent Mode 0–5 rendering for snapshots and row capture.
//! Includes window masks, mosaic, and color effects. Horizontal window history
//! has four-cycle timing. Sprites prepare one row ahead; backgrounds, palettes,
//! and color effects still sample once per displayed row.
mod affine;
pub(crate) mod capture;
pub(crate) mod effects;
pub(crate) mod mosaic;
pub(crate) mod sprites;
pub(crate) mod windows;
use std::{error::Error, fmt};
pub const WIDTH: usize = 240;
pub const HEIGHT: usize = 160;
/// Convert GBA RGB555 (red in bits 0..4) into packed 0x00RRGGBB.
/// Bit 15 is unused. Replication expands five-bit channels to eight bits.
pub fn rgb555_to_rgb888(color: u16) -> u32 {
let expand = |channel: u16| u32::from((channel << 3) | (channel >> 2));
let red = expand(color & 31);
let green = expand((color >> 5) & 31);
let blue = expand((color >> 10) & 31);
(red << 16) | (green << 8) | blue
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VideoError {
UnsupportedMode(u8),
UnsupportedLayers(u16),
UnsupportedTileAddress(usize),
UnsupportedMapAddress(usize),
UnsupportedObject { index: usize, reason: &'static str },
}
impl fmt::Display for VideoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnsupportedMode(mode) => write!(
f,
"unsupported display mode {mode}; only Modes 0 through 5 are rendered"
),
Self::UnsupportedTileAddress(address) => write!(
f,
"text background tile address {address:#x} exceeds the supported 64 KiB BG area"
),
Self::UnsupportedMapAddress(address) => write!(
f,
"background map address {address:#x} exceeds the supported 64 KiB BG area"
),
Self::UnsupportedObject { index, reason } => write!(f, "OBJ{index}: {reason}"),
Self::UnsupportedLayers(control) => {
write!(f, "unsupported display layers in DISPCNT {control:#06x}")
}
}
}
}
impl Error for VideoError {}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Background {
pub control: u16,
pub x: u16,
pub y: u16,
pub affine: affine::AffineBackground,
}
pub(crate) struct RenderSettings<'a> {
pub control: u16,
pub prepared_objects: Option<&'a Result<sprites::PreparedObjects, VideoError>>,
pub internal_affine: bool,
pub vertical_windows: Option<[bool; 2]>,
pub horizontal_windows: Option<&'a windows::horizontal::HorizontalWindows>,
pub green_swap: bool,
pub mosaic: u16,
pub vertical_mosaic: Option<mosaic::VerticalMosaic>,
pub backgrounds: &'a [Background; 4],
pub effects: &'a effects::Effects,
}
#[derive(Clone, Copy)]
struct Pixel {
color: u16,
layer: u8, // BG0..3, OBJ=4, backdrop=5.
priority: u8,
semi_transparent: bool,
}
impl Pixel {
fn order(self) -> (u8, u8) {
// OBJ wins a tie against BG; lower BG number wins other ties.
(
self.priority,
if self.layer == 4 { 0 } else { self.layer + 1 },
)
}
}
/// Snapshot the current registers and memory. Errors leave the output unchanged.
/// Memory owns fixed-size 96 KiB VRAM, 1 KiB palette, and 1 KiB OAM buffers.
pub(crate) fn render(
settings: RenderSettings<'_>,
vram: &[u8],
palette: &[u8],
oam: &[u8],
frame: &mut Framebuffer,
) -> Result<(), VideoError> {
frame.pixels = render_rows(settings, vram, palette, oam, 0..HEIGHT)?;
Ok(())
}
/// Render the requested rows from current registers and optional scanline state.
/// Row capture uses prepared sprites, affine origins, window history, and mosaic counters.
/// Debug snapshots use programmed origins, geometric window bounds, and screen-grid mosaic.
pub(crate) fn render_rows(
settings: RenderSettings<'_>,
vram: &[u8],
palette: &[u8],
oam: &[u8],
rows: std::ops::Range<usize>,
) -> Result<Vec<u32>, VideoError> {
debug_assert!(rows.start <= rows.end && rows.end <= HEIGHT);
let length = rows.len() * WIDTH;
let RenderSettings {
control,
prepared_objects,
internal_affine,
vertical_windows,
horizontal_windows,
green_swap,
mosaic,
vertical_mosaic,
backgrounds,
effects,
} = settings;
if control & 0x80 != 0 {
return Ok(vec![0xffffff; length]);
}
let backdrop = Pixel {
color: halfword(palette, 0),
layer: 5,
priority: 4,
semi_transparent: false,
};
let mut output = vec![0; length];
let mode = control & 7;
let unsupported = match mode {
0 => 0,
1 => 0x800, // BG0/1 text, BG2 affine.
2 => 0x300, // BG2/3 affine.
3..=5 => 0xb00, // BG2 bitmap.
_ => return Err(VideoError::UnsupportedMode(mode as u8)),
};
if control & unsupported != 0 {
return Err(VideoError::UnsupportedLayers(control));
}
let mut layers: Vec<usize> = (0..4).filter(|bg| control & (0x100 << bg) != 0).collect();
// Lower priority number wins. BG0 wins ties, then BG1, BG2, BG3.
layers.sort_by_key(|&bg| (backgrounds[bg].control & 3, bg));
let objects = if control & 0x1000 == 0 {
sprites::Objects {
pixels: vec![None; length],
window: vec![false; length],
}
} else if let Some(prepared) = prepared_objects {
debug_assert_eq!(rows.len(), 1);
prepared
.as_ref()
.map_err(Clone::clone)?
.compose(mosaic, palette)
} else {
sprites::prepare(control, mosaic, None, oam, vram, rows.clone())?.compose(mosaic, palette)
};
let mosaic_width = usize::from(mosaic & 15) + 1;
let mosaic_height = usize::from((mosaic >> 4) & 15) + 1;
for y in rows.clone() {
for x in 0..WIDTH {
let offset = (y - rows.start) * WIDTH + x;
let mask = effects.mask(
control,
x,
y,
objects.window[offset],
vertical_windows,
horizontal_windows,
);
let mut top = backdrop;
let mut below = None;
let mut insert = |pixel: Pixel| {
if pixel.order() < top.order() {
below = Some(top);
top = pixel;
} else if below.is_none_or(|second: Pixel| pixel.order() < second.order()) {
below = Some(pixel);
}
};
for &bg in &layers {
// Validate enabled background fetches even when a window hides them.
// Mosaic samples before scroll or affine mapping. Captured vertical
// phase comes from a counter; horizontal blocks remain screen-aligned.
// Window selection still uses the actual output coordinate.
let (sample_x, sample_y) = if backgrounds[bg].control & 0x40 != 0 {
let counter =
vertical_mosaic.map_or(y % mosaic_height, |v| usize::from(v.background));
(x - x % mosaic_width, y - counter)
} else {
(x, y)
};
let color = if mode == 0 || (mode == 1 && bg < 2) {
text_pixel(backgrounds[bg], vram, palette, sample_x, sample_y)?
} else {
affine::pixel(
backgrounds[bg],
vram,
palette,
sample_x,
sample_y,
control,
internal_affine,
)?
};
if let Some(color) = color.filter(|_| mask & (1 << bg) != 0) {
insert(Pixel {
color,
layer: bg as u8,
priority: (backgrounds[bg].control & 3) as u8,
semi_transparent: false,
});
}
}
if let Some(object) = objects.pixels[offset].filter(|_| mask & 0x10 != 0) {
insert(object);
}
output[offset] = rgb555_to_rgb888(effects.apply(top, below, mask & 0x20 != 0));
}
}
if green_swap {
for pair in output.chunks_exact_mut(2) {
let green_difference = (pair[0] ^ pair[1]) & 0x0000_ff00;
pair[0] ^= green_difference;
pair[1] ^= green_difference;
}
}
Ok(output)
}
fn halfword(bytes: &[u8], offset: usize) -> u16 {
u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
}
fn text_pixel(
bg: Background,
vram: &[u8],
palette: &[u8],
x: usize,
y: usize,
) -> Result<Option<u16>, VideoError> {
let control = usize::from(bg.control);
let width = if control & 0x4000 != 0 { 512 } else { 256 };
let height = if control & 0x8000 != 0 { 512 } else { 256 };
// Text backgrounds always wrap; the affine overflow bit has no effect.
let x = (x + usize::from(bg.x)) % width;
let y = (y + usize::from(bg.y)) % height;
let block = x / 256 + (y / 256) * (width / 256);
let map = ((control >> 8) & 31) * 0x800 + block * 0x800 + ((y / 8 % 32) * 32 + x / 8 % 32) * 2;
if map >= 0x10000 {
return Err(VideoError::UnsupportedMapAddress(map));
}
let entry = usize::from(halfword(vram, map));
let tx = (x & 7) ^ if entry & 0x400 != 0 { 7 } else { 0 };
let ty = (y & 7) ^ if entry & 0x800 != 0 { 7 } else { 0 };
let eight_bit = control & 0x80 != 0;
let pixel = ty * 8 + tx;
let address = ((control >> 2) & 3) * 0x4000
+ (entry & 0x3ff) * if eight_bit { 64 } else { 32 }
+ if eight_bit { pixel } else { pixel / 2 };
// The real PPU has special behavior outside BG VRAM. Do not read OBJ tiles
// as ordinary BG data or silently wrap this unsupported case.
if address >= 0x10000 {
return Err(VideoError::UnsupportedTileAddress(address));
}
let index = if eight_bit {
usize::from(vram[address])
} else {
usize::from((vram[address] >> ((pixel & 1) * 4)) & 15)
};
if index == 0 {
return Ok(None);
}
let bank = if eight_bit { 0 } else { (entry >> 12) * 16 };
Ok(Some(halfword(palette, (bank + index) * 2)))
}
pub struct Framebuffer {
pixels: Vec<u32>,
}
impl Default for Framebuffer {
fn default() -> Self {
Self {
pixels: vec![0; WIDTH * HEIGHT],
}
}
}
impl Framebuffer {
/// Packed 0x00RRGGBB pixels in row-major order.
pub fn pixels(&self) -> &[u32] {
&self.pixels
}
pub fn clear(&mut self, color: u16) {
self.pixels.fill(rgb555_to_rgb888(color));
}
/// Write a GBA RGB555 color. Out-of-bounds coordinates return false.
pub fn set_pixel(&mut self, x: usize, y: usize, color: u16) -> bool {
if x >= WIDTH || y >= HEIGHT {
return false;
}
self.pixels[y * WIDTH + x] = rgb555_to_rgb888(color);
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converts_gba_channel_order_and_ignores_unused_bit() {
for (color, expected) in [
(0, 0),
(0x001f, 0x00ff_0000),
(0x03e0, 0x0000_ff00),
(0x7c00, 0x0000_00ff),
(0x7fff, 0x00ff_ffff),
(0xffff, 0x00ff_ffff),
(0x8000, 0),
(0x4210, 0x0084_8484),
] {
assert_eq!(rgb555_to_rgb888(color), expected);
}
}
#[test]
fn starts_with_a_black_gba_sized_buffer() {
let frame = Framebuffer::default();
assert_eq!(frame.pixels().len(), 240 * 160);
assert!(frame.pixels().iter().all(|pixel| *pixel == 0));
}
#[test]
fn writes_row_major_pixels_including_last_pixel() {
let mut frame = Framebuffer::default();
assert!(frame.set_pixel(1, 2, 0x001f));
assert!(frame.set_pixel(WIDTH - 1, HEIGHT - 1, 0x03e0));
assert_eq!(frame.pixels()[2 * WIDTH + 1], 0x00ff_0000);
assert_eq!(frame.pixels()[WIDTH * HEIGHT - 1], 0x0000_ff00);
assert_eq!(frame.pixels()[0], 0);
}
#[test]
fn clips_invalid_coordinates_without_changing_pixels() {
let mut frame = Framebuffer::default();
for (x, y) in [(WIDTH, 0), (0, HEIGHT), (usize::MAX, usize::MAX)] {
assert!(!frame.set_pixel(x, y, 0x7fff));
}
assert!(frame.pixels().iter().all(|pixel| *pixel == 0));
}
#[test]
fn clear_replaces_every_pixel() {
let mut frame = Framebuffer::default();
frame.set_pixel(0, 0, 0x001f);
frame.clear(0x7c00);
assert!(frame.pixels().iter().all(|pixel| *pixel == 0x0000_00ff));
}
}

View file

@ -0,0 +1,266 @@
//! Programmed affine BG registers and sampling for snapshots and row capture.
//! Internal origins advance at line boundaries. Pixel-fetch and per-access timing
//! remain outside this row-level model.
use super::{halfword, Background, VideoError, HEIGHT, WIDTH};
#[cfg(test)]
use crate::display::{CYCLES_PER_FRAME, CYCLES_PER_LINE};
const REFERENCE_MASK: u32 = 0x0fff_ffff;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) struct AffineBackground {
matrix: [u16; 4],
reference: [u32; 2],
internal: [u32; 2],
// An HBlank write sets the next row's origin and overrides that row's increment.
reload_next: [bool; 2],
}
impl AffineBackground {
/// Merge into write-only latches, never into their zero readback placeholders.
pub(crate) fn write8(&mut self, offset: usize, value: u8, after_row: bool) {
if offset < 8 {
let word = &mut self.matrix[offset / 2];
let shift = (offset & 1) * 8;
*word = (*word & !(0xff << shift)) | (u16::from(value) << shift);
} else {
let axis = (offset - 8) / 4;
let word = &mut self.reference[axis];
let shift = (offset & 3) * 8;
*word = ((*word & !(0xff << shift)) | (u32::from(value) << shift)) & REFERENCE_MASK;
self.internal[axis] = *word;
self.reload_next[axis] = after_row;
}
}
/// Advance across line ends with fixed registers. Reload at the next visible
/// frame's line-zero boundary, so arbitrarily large batches remain O(1).
pub(crate) fn advance(
&mut self,
phase: u32,
cycles: u32,
active: bool,
mosaic_height: u32,
counter: u8,
) {
let progress = super::mosaic::progress(phase, cycles, counter, mosaic_height);
if progress.frame_reloaded {
self.internal = self.reference;
self.reload_next = [false; 2];
}
if !progress.crossed_line {
return;
}
// A mosaic-enabled affine origin advances only when the shared BG
// counter becomes zero. Height1/counter0 models non-mosaic backgrounds.
for (axis, coefficient) in [self.matrix[1], self.matrix[3]].into_iter().enumerate() {
if active {
let count =
progress.resets - u32::from(self.reload_next[axis] && progress.first_reset);
let delta = i32::from(coefficient as i16) * (count * mosaic_height) as i32;
self.internal[axis] =
self.internal[axis].wrapping_add(delta as u32) & REFERENCE_MASK;
}
self.reload_next[axis] = false;
}
}
fn source(self, x: usize, y: usize, internal: bool) -> (i32, i32) {
let [a, b, c, d] = self.matrix.map(|v| i32::from(v as i16));
let reference = if internal {
self.internal
} else {
self.reference
};
let [rx, ry] = reference.map(|v| ((v << 4) as i32) >> 4);
// Internal origins already include vertical progress.
let y = if internal { 0 } else { y };
// Products and sums fit i32 for a 240x160 output. Model the 28-bit
// fixed-point range, then floor with an arithmetic shift by eight.
let coordinate = |v: i32| ((v as u32) << 4) as i32 >> 12;
(
coordinate(rx + a * x as i32 + b * y as i32),
coordinate(ry + c * x as i32 + d * y as i32),
)
}
}
pub(super) fn pixel(
bg: Background,
vram: &[u8],
palette: &[u8],
x: usize,
y: usize,
display_control: u16,
internal: bool,
) -> Result<Option<u16>, VideoError> {
let (mut tx, mut ty) = bg.affine.source(x, y, internal);
let mode = display_control & 7;
if (3..=5).contains(&mode) {
let (width, height) = if mode == 5 {
(160, 128)
} else {
(WIDTH, HEIGHT)
};
// Bitmap overflow never wraps, even when BGCNT bit 13 is set.
if tx < 0 || ty < 0 || tx >= width as i32 || ty >= height as i32 {
return Ok(None);
}
// Mode 3 ignores the page bit. Modes 4/5 share the same 40 KiB page stride.
let page = if mode != 3 && display_control & 0x10 != 0 {
0xa000
} else {
0
};
let offset = ty as usize * width + tx as usize;
return Ok(if mode == 4 {
let index = usize::from(vram[page + offset]);
if index == 0 {
None
} else {
Some(halfword(palette, index * 2))
}
} else {
// Modes 3/5 store opaque RGB555, including black. Bit 15 is unused.
Some(halfword(vram, page + offset * 2))
});
}
let control = usize::from(bg.control);
let size = 128_i32 << (control >> 14);
if control & 0x2000 != 0 {
tx = tx.rem_euclid(size);
ty = ty.rem_euclid(size);
} else if tx < 0 || ty < 0 || tx >= size || ty >= size {
return Ok(None);
}
let (tx, ty) = (tx as usize, ty as usize);
let map = ((control >> 8) & 31) * 0x800 + (ty / 8) * (size as usize / 8) + tx / 8;
if map >= 0x10000 {
return Err(VideoError::UnsupportedMapAddress(map));
}
// Affine maps have byte-sized indices and no flip/palette-bank attributes.
// Every tile is 8bpp, regardless of BGCNT bit 7.
let address =
((control >> 2) & 3) * 0x4000 + usize::from(vram[map]) * 64 + (ty % 8) * 8 + tx % 8;
let index = usize::from(vram[address]); // Maximum address is 0xffff.
Ok(if index == 0 {
None
} else {
Some(halfword(palette, index * 2))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn batched_tracking_matches_an_independent_line_boundary_model() {
let mut seed = 0x92ad_1037u32;
for _ in 0..500 {
let mut next = || {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
seed
};
let mut bg = AffineBackground {
matrix: [next() as u16, next() as u16, next() as u16, next() as u16],
reference: [next() & REFERENCE_MASK, next() & REFERENCE_MASK],
internal: [next() & REFERENCE_MASK, next() & REFERENCE_MASK],
reload_next: [next() & 1 != 0, next() & 2 != 0],
};
let phase = next() % CYCLES_PER_FRAME;
let cycles = next() % (CYCLES_PER_FRAME * 7);
let height = next() % 16 + 1;
let counter = (next() & 15) as u8;
let mut expected_counter = counter;
let active = next() & 8 != 0;
let mut expected = bg.internal.map(i64::from);
let mut pending = bg.reload_next;
let first = phase / CYCLES_PER_LINE + 1;
let last = (phase + cycles) / CYCLES_PER_LINE;
for boundary in first..=last {
let row = boundary % 228;
if row == 0 {
expected = bg.reference.map(i64::from);
expected_counter = 0;
} else if row <= 160 {
let next = u32::from(expected_counter) + 1;
expected_counter = if row == 160 || next == height {
0
} else {
(next & 15) as u8
};
for axis in 0..2 {
if active && expected_counter == 0 && !pending[axis] {
let coefficient = i64::from(bg.matrix[axis * 2 + 1] as i16);
expected[axis] = (expected[axis] + coefficient * i64::from(height))
.rem_euclid(1 << 28);
}
}
}
pending = [false; 2];
}
bg.advance(phase, cycles, active, height, counter);
assert_eq!(
bg.internal.map(i64::from),
expected,
"phase={phase} cycles={cycles} height={height} active={active}"
);
assert_eq!(bg.reload_next, pending);
}
}
#[test]
fn signed_accumulators_wrap_and_extreme_batches_reload_without_iteration() {
let mut bg = AffineBackground {
matrix: [256, 1, 0, (-1i16) as u16],
reference: [REFERENCE_MASK, 0],
internal: [REFERENCE_MASK, 0],
..Default::default()
};
bg.advance(0, CYCLES_PER_LINE, true, 1, 0);
assert_eq!(bg.internal, [0, REFERENCE_MASK]);
assert_eq!(bg.source(0, 0, true), (0, -1));
assert_eq!(bg.source(0, 0, false), (-1, 0));
let phase = 17 * CYCLES_PER_LINE + 413;
bg.advance(phase, u32::MAX, true, 1, 0);
let lines = ((u64::from(phase) + u64::from(u32::MAX)) % u64::from(CYCLES_PER_FRAME)
/ u64::from(CYCLES_PER_LINE))
.min(160);
assert_eq!(
bg.internal[0],
((i64::from(REFERENCE_MASK) + lines as i64) % (1 << 28)) as u32
);
assert_eq!(bg.internal[1], (-(lines as i64)).rem_euclid(1 << 28) as u32);
}
#[test]
fn reference_byte_writes_merge_programmed_values_not_accumulated_values() {
let mut bg = AffineBackground {
matrix: [256, 256, 0, 256],
reference: [0x1234500, 0x2000],
internal: [0x1234500, 0x2000],
..Default::default()
};
bg.advance(0, 10 * CYCLES_PER_LINE, true, 1, 0);
let y = bg.internal[1];
bg.write8(8, 0x7f, false);
assert_eq!(bg.reference[0], 0x123457f);
assert_eq!(bg.internal[0], 0x123457f);
assert_eq!(bg.internal[1], y);
bg.write8(11, 0xff, true);
assert_eq!(bg.reference[0], 0x0f23457f);
bg.advance(
10 * CYCLES_PER_LINE + 1006,
CYCLES_PER_LINE - 1006,
true,
1,
0,
);
assert_eq!(bg.internal[0], 0x0f23457f); // HBlank reload overrides this transition.
assert_eq!(bg.internal[1], y + 256); // Other axis still increments.
bg.write8(2, 0, false); // PB byte write must not reload X.
assert_eq!(bg.internal[0], 0x0f23457f);
}
}

View file

@ -0,0 +1,76 @@
//! Optional scanline frame capture. Rows sample at HBlank entry; publication
//! happens at VBlank entry. This is not a pixel fetch pipeline.
use super::{Framebuffer, VideoError, HEIGHT, WIDTH};
pub(crate) struct Capture {
drawing: Vec<u32>,
completed: Vec<u32>,
rows: usize,
drawing_error: Option<VideoError>,
completed_error: Option<VideoError>,
vblank: Option<u64>,
}
impl Default for Capture {
fn default() -> Self {
Self {
drawing: vec![0; WIDTH * HEIGHT],
completed: vec![0; WIDTH * HEIGHT],
rows: 0,
drawing_error: None,
completed_error: None,
vblank: None,
}
}
}
impl Capture {
/// Starting capture in the middle of a frame must not publish missing rows.
pub(crate) fn wants_row(&self, row: usize) -> bool {
row == 0 || (self.rows != 0 && row == self.rows)
}
pub(crate) fn record(&mut self, row: usize, pixels: Result<Vec<u32>, VideoError>) {
if row == 0 {
self.rows = 0;
self.drawing_error = None;
}
debug_assert_eq!(self.rows, row);
match pixels {
Ok(pixels) => self.drawing[row * WIDTH..(row + 1) * WIDTH].copy_from_slice(&pixels),
Err(error) => {
self.drawing_error.get_or_insert(error);
}
}
self.rows += 1;
}
pub(crate) fn publish(&mut self, vblank: u64) {
if self.rows != HEIGHT {
return;
}
self.completed_error = self.drawing_error.take();
if self.completed_error.is_none() {
std::mem::swap(&mut self.drawing, &mut self.completed);
}
self.vblank = Some(vblank);
self.rows = 0;
}
pub(crate) fn vblank(&self) -> Option<u64> {
self.vblank
}
/// Errors leave the caller's last displayed image unchanged.
pub(crate) fn present(&self, output: &mut Framebuffer) -> Result<bool, VideoError> {
if self.vblank.is_none() {
return Ok(false);
}
if let Some(error) = &self.completed_error {
return Err(error.clone());
}
output.pixels.copy_from_slice(&self.completed);
Ok(true)
}
}

View file

@ -0,0 +1,95 @@
//! Window masks and five-bit color arithmetic for snapshots and row capture.
//! Snapshots use half-open, wrapping bounds. Row capture supplies vertical flags
//! and horizontal comparator history. Layer/effect selection remains row-sampled.
use super::Pixel;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Effects {
pub horizontal: [u16; 2],
pub vertical: [u16; 2],
pub inside: u16,
pub outside: u16,
pub control: u16,
pub alpha: u16,
pub brightness: u16,
}
impl Effects {
pub(super) fn mask(
&self,
display: u16,
x: usize,
y: usize,
object: bool,
vertical: Option<[bool; 2]>,
horizontal: Option<&super::windows::horizontal::HorizontalWindows>,
) -> u16 {
if display & 0xe000 == 0 {
return 0x3f;
}
for window in 0..2 {
if display & (0x2000 << window) != 0
&& horizontal.map_or_else(
|| contains(self.horizontal[window], x),
|state| state.pixels[window][x],
)
&& vertical
.map_or_else(|| contains(self.vertical[window], y), |flags| flags[window])
{
return (self.inside >> (window * 8)) & 0x3f;
}
}
if display & 0x9000 == 0x9000 && object {
(self.outside >> 8) & 0x3f
} else {
self.outside & 0x3f
}
}
pub(super) fn apply(&self, top: Pixel, below: Option<Pixel>, enabled: bool) -> u16 {
if !enabled {
return top.color;
}
let mode = (self.control >> 6) & 3;
let first = self.control & (1 << top.layer) != 0;
if top.semi_transparent || (mode == 1 && first) {
if let Some(second) = below.filter(|p| self.control & (0x100 << p.layer) != 0) {
let a = (self.alpha & 31).min(16);
let b = ((self.alpha >> 8) & 31).min(16);
return channels(top.color, second.color, |x, y| {
((x * a + y * b) >> 4).min(31)
});
}
}
// Semi-transparent OBJ falls back to a selected brightness effect only
// when no eligible second target is directly below it.
if first && mode >= 2 {
let coefficient = (self.brightness & 31).min(16);
return channels(top.color, 0, |x, _| {
if mode == 2 {
x + (((31 - x) * coefficient) >> 4)
} else {
x - ((x * coefficient) >> 4)
}
});
}
top.color
}
}
fn contains(bounds: u16, coordinate: usize) -> bool {
let start = usize::from(bounds >> 8);
let end = usize::from(bounds & 255);
if start > end {
coordinate >= start || coordinate < end
} else {
(start..end).contains(&coordinate)
}
}
fn channels(first: u16, second: u16, operation: impl Fn(u16, u16) -> u16) -> u16 {
[0, 5, 10].into_iter().fold(0, |color, shift| {
color | (operation((first >> shift) & 31, (second >> shift) & 31) << shift)
})
}

View file

@ -0,0 +1,190 @@
//! Vertical mosaic phase for row capture. Size writes preserve four-bit counters.
//! Background phase follows the displayed row. OBJ phase runs one row ahead
//! for sprite preparation. Per-access size latching is not modeled.
use crate::display::{CYCLES_PER_FRAME, CYCLES_PER_LINE, VISIBLE_LINES};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) struct VerticalMosaic {
pub background: u8,
pub object: u8,
started: bool,
}
impl VerticalMosaic {
pub fn advance(&mut self, phase: u32, cycles: u32, sizes: u16) {
if cycles == 0 {
return;
}
let object_height = u32::from(sizes >> 12) + 1;
if !self.started {
// Reset has no preceding row227. Row0 uses phase0; row1 uses the next phase.
self.object = u8::from(object_height != 1);
self.started = true;
}
self.background = progress(
phase,
cycles,
self.background,
u32::from((sizes >> 4) & 15) + 1,
)
.counter;
self.object = progress(
(phase + CYCLES_PER_LINE) % CYCLES_PER_FRAME,
cycles,
self.object,
object_height,
)
.counter;
}
}
pub(super) struct Progress {
pub counter: u8,
/// Number of zero transitions since the last frame reload, if any.
pub resets: u32,
pub first_reset: bool,
pub frame_reloaded: bool,
pub crossed_line: bool,
}
/// Summarize fixed-size line transitions without iterating over lines or frames.
/// A frame wrap discards earlier history because affine origins reload at row0.
pub(super) fn progress(phase: u32, cycles: u32, counter: u8, height: u32) -> Progress {
debug_assert!(counter < 16 && (1..=16).contains(&height));
let end = u64::from(phase) + u64::from(cycles);
let wrapped = end >= u64::from(CYCLES_PER_FRAME);
let first = if wrapped { 0 } else { phase / CYCLES_PER_LINE };
let last = (end % u64::from(CYCLES_PER_FRAME)) as u32 / CYCLES_PER_LINE;
let initial = if wrapped { 0 } else { u32::from(counter) };
let first_reset = first < VISIBLE_LINES
&& (first == VISIBLE_LINES - 1 || initial + 1 == height || initial == 15);
// Normal increments enter rows1..159. Entry to row160 unconditionally resets.
let lines = last.min(VISIBLE_LINES - 1) - first.min(VISIBLE_LINES - 1);
let until_zero = if initial < height {
height - initial
} else {
16 - initial
};
let (mut current, mut resets) = if lines < until_zero {
(initial + lines, 0)
} else {
let remaining = lines - until_zero;
(remaining % height, 1 + remaining / height)
};
if first < VISIBLE_LINES && last >= VISIBLE_LINES {
current = 0;
resets += 1;
}
Progress {
counter: current as u8,
resets,
first_reset,
frame_reloaded: wrapped,
crossed_line: wrapped || first != last,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_counter_height_and_visible_line_matches_iterated_transitions() {
for counter in 0..16 {
for height in 1..=16 {
for first in 0..160 {
let mut expected = counter;
let mut resets = 0;
for last in first + 1..=160 {
let next = expected + 1;
expected = if last == 160 || next == height {
0
} else {
next & 15
};
resets += u32::from(expected == 0);
let p = progress(
first * CYCLES_PER_LINE,
(last - first) * CYCLES_PER_LINE,
counter as u8,
height,
);
assert_eq!(
u32::from(p.counter),
expected,
"counter={counter} height={height} first={first} last={last}"
);
assert_eq!(p.resets, resets);
assert_eq!(
p.first_reset,
first == 159 || counter + 1 == height || counter == 15
);
}
}
}
}
}
#[test]
fn random_clock_batches_and_maximum_batch_match_line_iteration() {
let mut seed = 0x3712_bc09u32;
for case in 0..500 {
let mut next = || {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
seed
};
let phase = next() % CYCLES_PER_FRAME;
let cycles = if case == 0 {
u32::MAX
} else {
next() % (7 * CYCLES_PER_FRAME)
};
let height = next() % 16 + 1;
let counter = if phase / CYCLES_PER_LINE >= 160 {
0
} else {
(next() & 15) as u8
};
let mut expected = counter;
let mut resets = 0;
for boundary in u64::from(phase / CYCLES_PER_LINE) + 1
..=(u64::from(phase) + u64::from(cycles)) / u64::from(CYCLES_PER_LINE)
{
let row = boundary % 228;
if row == 0 {
expected = 0;
resets = 0;
} else if row <= 160 {
let value = u32::from(expected) + 1;
expected = if row == 160 || value == height {
0
} else {
(value & 15) as u8
};
resets += u32::from(expected == 0);
}
}
let p = progress(phase, cycles, counter, height);
assert_eq!(p.counter, expected, "phase={phase} cycles={cycles}");
assert_eq!(p.resets, resets);
}
}
#[test]
fn size_shrink_preserves_counter_until_four_bit_wrap_and_zero_cycles_do_nothing() {
let mut mosaic = VerticalMosaic::default();
mosaic.advance(0, 6 * CYCLES_PER_LINE, 0x3070); // BG8, OBJ4.
assert_eq!((mosaic.background, mosaic.object), (6, 3)); // OBJ prepares row7.
mosaic.advance(6 * CYCLES_PER_LINE, 0, 0);
assert_eq!((mosaic.background, mosaic.object), (6, 3));
mosaic.advance(6 * CYCLES_PER_LINE, 9 * CYCLES_PER_LINE, 0x1010); // BG2, OBJ2.
assert_eq!((mosaic.background, mosaic.object), (15, 12));
mosaic.advance(15 * CYCLES_PER_LINE, CYCLES_PER_LINE, 0x1010);
assert_eq!((mosaic.background, mosaic.object), (0, 13));
mosaic.advance(16 * CYCLES_PER_LINE, 144 * CYCLES_PER_LINE, 0xffff);
assert_eq!((mosaic.background, mosaic.object), (0, 0));
mosaic.advance(160 * CYCLES_PER_LINE, 68 * CYCLES_PER_LINE, 0xffff);
assert_eq!((mosaic.background, mosaic.object), (0, 1)); // Row1 phase, row0 already prepared.
}
}

View file

@ -0,0 +1,230 @@
//! Regular and affine OBJ sampling with a nominal per-row work allowance.
//! Individual fetch timing and OAM contention are not modeled.
mod budget;
pub(crate) mod pipeline;
use super::{halfword, Pixel, VideoError, WIDTH};
const SIZES: [[(usize, usize); 4]; 3] = [
[(8, 8), (16, 16), (32, 32), (64, 64)],
[(16, 8), (32, 8), (32, 16), (64, 32)],
[(8, 16), (8, 32), (16, 32), (32, 64)],
];
pub(super) struct Objects {
pub pixels: Vec<Option<Pixel>>,
pub window: Vec<bool>,
}
pub(crate) struct PreparedObjects {
samples: Vec<Sample>,
window: Vec<bool>,
}
#[derive(Clone, Copy)]
struct ObjectPixel {
palette_index: u16,
semi_transparent: bool,
}
// Transparent texels can change priority and mosaic without replacing color.
#[derive(Clone, Copy, Default)]
struct Sample {
pixel: Option<ObjectPixel>,
priority: u8,
mosaic: bool,
}
pub(crate) fn prepare(
control: u16,
mosaic: u16,
vertical_counter: Option<u8>,
oam: &[u8],
vram: &[u8],
rows: std::ops::Range<usize>,
) -> Result<PreparedObjects, VideoError> {
// Lower priority numbers win; equal priorities retain the earlier OAM entry.
// Color, priority, and mosaic metadata are tracked separately.
let mut output = PreparedObjects {
samples: vec![Sample::default(); WIDTH * rows.len()],
window: vec![false; WIDTH * rows.len()],
};
if control & 0x1000 == 0 {
return Ok(output);
}
let samples = &mut output.samples;
let mut budgets = vec![budget::Budget::new(control); rows.len()];
let mosaic_height = usize::from(mosaic >> 12) + 1;
let one_dimensional = control & 0x40 != 0;
for index in 0..128 {
let a = usize::from(halfword(oam, index * 8));
let b = usize::from(halfword(oam, index * 8 + 2));
let c = usize::from(halfword(oam, index * 8 + 4));
if a & 0x300 == 0x200 {
for budget in &mut budgets {
budget.skip();
}
continue; // Disabled regular OBJ still consumes nominal OAM inspection.
}
let reason = if a & 0xc00 == 0xc00 {
Some("prohibited OBJ mode")
} else if a >> 14 == 3 {
Some("prohibited OBJ shape")
} else {
None
};
if let Some(reason) = reason {
return Err(VideoError::UnsupportedObject { index, reason });
}
let window = a & 0xc00 == 0x800;
// Window coverage is prepared independently of its later display mask.
// OBJ-window coverage does not use either mosaic dimension.
let mosaic_enabled = a & 0x1000 != 0 && !window;
let (width, height) = SIZES[a >> 14][b >> 14];
let affine = a & 0x100 != 0;
let doubled = affine && a & 0x200 != 0;
let canvas_width = width << usize::from(doubled);
let canvas_height = height << usize::from(doubled);
let matrix = if affine {
let base = ((b >> 9) & 31) * 32 + 6;
[0, 8, 16, 24].map(|offset| i32::from(halfword(oam, base + offset) as i16))
} else {
[0; 4]
};
let eight_bit = a & 0x2000 != 0;
let mut tile = c & 0x3ff;
// Restricted bitmap tiles produce no pixels, but active sprite work
// still uses the allowance. They are not disabled OAM entries.
let tile_visible = !(control & 7 >= 3 && tile < 512);
if eight_bit && !one_dimensional {
tile &= !1; // 2D 8bpp tiles are aligned to pairs of 32-byte slots.
}
let slot_width = if eight_bit { 2 } else { 1 };
let priority = ((c >> 10) & 3) as u8;
for y in rows.clone() {
let budget = &mut budgets[y - rows.start];
let sy = (y + 256 - (a & 255)) & 255;
let left = b & 511;
let intersects_screen = left < WIDTH || left + canvas_width > 512;
if sy >= canvas_height || !intersects_screen {
budget.skip();
continue;
}
// Charge before texel lookup or priority/window masking. Transparent,
// hidden, and out-of-texture samples still require preparation work.
let columns = budget.draw(canvas_width, affine);
if !tile_visible {
continue;
}
// Preparation latches the ahead-of-display OBJ counter; snapshots
// use the screen grid.
// Clamp partial first blocks to the object's first row, without
// extending its canvas. OBJ-window sampling bypasses this counter.
let sample_y = if mosaic_enabled {
sy.saturating_sub(vertical_counter.map_or(y % mosaic_height, usize::from))
} else {
sy
};
for sx in 0..columns {
let x = ((b & 511) + sx) & 511;
if x >= WIDTH {
continue;
}
let offset = (y - rows.start) * WIDTH + x;
let (tx, ty) = if affine {
// Signed 8.8 inverse mapping from canvas center to texture center.
// Keep fractional products until the final arithmetic shift. Negative
// coordinates round down, not toward zero. Outside texels are transparent.
let dx = sx as i32 - canvas_width as i32 / 2;
let dy = sample_y as i32 - canvas_height as i32 / 2;
let tx = (matrix[0] * dx + matrix[1] * dy + ((width as i32) << 7)) >> 8;
let ty = (matrix[2] * dx + matrix[3] * dy + ((height as i32) << 7)) >> 8;
if tx < 0 || ty < 0 || tx >= width as i32 || ty >= height as i32 {
continue;
}
(tx as usize, ty as usize)
} else {
(
if b & 0x1000 != 0 { width - 1 - sx } else { sx },
if b & 0x2000 != 0 {
height - 1 - sample_y
} else {
sample_y
},
)
};
let slot = if one_dimensional {
tile + (ty / 8 * (width / 8) + tx / 8) * slot_width
} else {
// A 2D row is 32 slots wide. Horizontal overflow wraps within
// that row; vertical overflow wraps within 32 KiB OBJ VRAM.
(tile & !31) + (ty / 8) * 32 + ((tile + tx / 8 * slot_width) & 31)
};
let pixel = (ty % 8) * 8 + tx % 8;
let address =
0x10000 + ((slot * 32 + if eight_bit { pixel } else { pixel / 2 }) & 0x7fff);
let color_index = if eight_bit {
usize::from(vram[address])
} else {
usize::from(vram[address] >> ((pixel & 1) * 4) & 15)
};
if window && color_index != 0 {
output.window[offset] = true;
} else {
let sample = &mut samples[offset];
if sample.pixel.is_none() || priority < sample.priority {
if color_index != 0 {
let bank = if eight_bit { 0 } else { (c >> 12) * 16 };
sample.pixel = Some(ObjectPixel {
palette_index: (bank + color_index) as u16,
semi_transparent: a & 0xc00 == 0x400,
});
}
sample.priority = priority;
sample.mosaic = mosaic_enabled;
}
}
}
}
}
Ok(output)
}
impl PreparedObjects {
/// Resolve buffered indices through the current palette. Horizontal mosaic
/// belongs to composition, not the previous line's OAM/VRAM preparation.
pub(super) fn compose(&self, mosaic: u16, palette: &[u8]) -> Objects {
let mut output = Objects {
pixels: vec![None; self.samples.len()],
window: self.window.clone(),
};
let mosaic_width = usize::from((mosaic >> 8) & 15) + 1;
// Horizontal OBJ mosaic latches the selected object sample, not each
// object's texture coordinate. Reset at each scanline and at transitions
// to/from non-mosaic samples or a numerically lower priority.
for (source, destination) in self
.samples
.chunks_exact(WIDTH)
.zip(output.pixels.chunks_exact_mut(WIDTH))
{
let mut latched = Sample::default();
for (x, (&current, pixel)) in source.iter().zip(destination).enumerate() {
if x % mosaic_width == 0
|| !current.mosaic
|| !latched.mosaic
|| current.priority < latched.priority
{
latched = current;
}
*pixel = latched.pixel.map(|p| Pixel {
color: halfword(palette, 0x200 + usize::from(p.palette_index) * 2),
layer: 4,
priority: latched.priority,
semi_transparent: p.semi_transparent,
});
}
}
output
}
}

View file

@ -0,0 +1,124 @@
//! Nominal per-row OBJ work allowance, not a timed OAM/VRAM fetch scheduler.
//! GBATEK gives 1210/954 cycles and costs of width or 10+2*width.
//! Inactive entries cost two inspection cycles, following emulator references.
//! Active costs include that inspection; clipped active canvases cost full width.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct Budget {
remaining: usize,
}
impl Budget {
pub fn new(control: u16) -> Self {
Self {
remaining: if control & 0x20 != 0 { 954 } else { 1210 },
}
}
pub fn skip(&mut self) {
self.remaining = self.remaining.saturating_sub(2);
}
/// Return the prepared prefix of the drawing canvas. Unfinished work consumes
/// the remaining allowance; it cannot be reused by a later, cheaper object.
pub fn draw(&mut self, width: usize, affine: bool) -> usize {
if self.remaining < 2 {
self.remaining = 0;
return 0;
}
let (setup, per_pixel) = if affine { (10, 2) } else { (0, 1) };
let pixels = (self.remaining.saturating_sub(setup) / per_pixel).min(width);
self.remaining = self.remaining.saturating_sub(setup + per_pixel * width);
pixels
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_allowance_and_width_matches_independent_cycle_consumption() {
for initial in 0usize..=1210 {
for width in [8, 16, 32, 64, 128] {
for affine in [false, true] {
let mut expected = initial;
let mut pixels = 0;
if expected < 2 {
expected = 0;
} else {
if affine {
for _ in 0..10 {
expected = expected.saturating_sub(1);
}
}
for _ in 0..width {
let needed = if affine { 2 } else { 1 };
if expected < needed {
expected = 0;
break;
}
expected -= needed;
pixels += 1;
}
}
let mut budget = Budget { remaining: initial };
assert_eq!(
budget.draw(width, affine),
pixels,
"initial={initial} width={width} affine={affine}"
);
assert_eq!(budget.remaining, expected);
}
}
}
}
#[test]
fn inactive_scan_and_active_costs_do_not_double_charge_inspection() {
let mut normal = Budget::new(0);
assert_eq!(normal.draw(64, false), 64);
assert_eq!(normal.remaining, 1210 - 64);
normal.skip();
assert_eq!(normal.remaining, 1210 - 66);
assert_eq!(normal.draw(128, true), 128);
assert_eq!(normal.remaining, 1210 - 66 - 266);
let mut short = Budget::new(0x20);
for _ in 0..119 {
assert_eq!(short.draw(8, false), 8);
}
assert_eq!(short.draw(8, false), 2);
assert_eq!(short.draw(8, false), 0);
}
#[test]
fn random_mixed_work_matches_an_independent_remaining_time_model() {
let mut seed = 0x8321_602bu32;
for control in [0, 0x20] {
for _ in 0..1000 {
let mut budget = Budget::new(control);
let mut expected: i32 = if control == 0 { 1210 } else { 954 };
for _ in 0..128 {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
let width = 8 << ((seed >> 4) % 5);
let affine = seed & 8 != 0;
if seed & 3 == 0 {
budget.skip();
expected = (expected - 2).max(0);
} else {
let columns = if expected < 2 {
0
} else if affine {
((expected - 10).max(0) / 2).min(width)
} else {
expected.min(width)
};
assert_eq!(budget.draw(width as usize, affine), columns as usize);
expected = (expected - if affine { 10 + 2 * width } else { width }).max(0);
}
assert_eq!(budget.remaining, expected as usize);
}
}
}
}
}

View file

@ -0,0 +1,122 @@
//! Row-at-once approximation of the one-line-ahead OBJ pipeline.
//! OAM and VRAM sample at cycle40 of the preceding line. This does not model
//! individual fetches, fetch budgets, or OAM/VRAM contention.
use super::PreparedObjects;
use crate::{display::CYCLES_PER_LINE, video::VideoError};
pub(crate) const PREPARE_CYCLE: u32 = 40;
#[derive(Default)]
pub(crate) struct SpritePipeline {
rows: [Option<(usize, Result<PreparedObjects, VideoError>)>; 2],
pub started: bool,
}
impl SpritePipeline {
pub fn store(&mut self, row: usize, objects: Result<PreparedObjects, VideoError>) {
self.rows[row % 2] = Some((row, objects));
}
pub fn row(&self, row: usize) -> &Result<PreparedObjects, VideoError> {
let (prepared_row, objects) = self.rows[row % 2]
.as_ref()
.expect("sprite row prepared before capture");
assert_eq!(*prepared_row, row);
objects
}
}
/// Only rows0..158 and227 prepare the following visible row.
pub(crate) fn target(scanline: u16) -> Option<usize> {
match scanline {
0..=158 => Some(usize::from(scanline) + 1),
227 => Some(0),
_ => None,
}
}
pub(crate) fn next_event(scanline: u16, cycle: u16) -> u32 {
let (line, cycle) = (u32::from(scanline), u32::from(cycle));
if target(scanline).is_some() && cycle < PREPARE_CYCLE {
return PREPARE_CYCLE - cycle;
}
let lines = if (158..227).contains(&line) {
227 - line
} else {
1
};
lines * CYCLES_PER_LINE + PREPARE_CYCLE - cycle
}
/// Latest two events suffice when capture is disabled: fixed memory/registers
/// cannot change inside a clock batch, and only two prepared rows are retained.
/// Offsets are relative to the supplied starting phase, in chronological order.
pub(crate) fn latest_events(phase: u32, cycles: u32) -> [Option<(u32, usize)>; 2] {
let start = u64::from(phase);
let end = start + u64::from(cycles);
let latest = |end: u64| {
let mut line = end.checked_sub(u64::from(PREPARE_CYCLE))? / u64::from(CYCLES_PER_LINE);
let row = line % 228;
if (159..227).contains(&row) {
line -= row - 158;
}
let time = line * u64::from(CYCLES_PER_LINE) + u64::from(PREPARE_CYCLE);
(time > start).then_some((time, ((line + 1) % 228) as usize))
};
let Some((last, row)) = latest(end) else {
return [None; 2];
};
let previous = latest(last - 1).map(|(time, row)| ((time - start) as u32, row));
[previous, Some(((last - start) as u32, row))]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::display::CYCLES_PER_FRAME;
#[test]
fn event_selection_matches_independent_line_iteration() {
let mut seed = 0x6328_93abu32;
for case in 0..1000 {
let mut next = || {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
seed
};
let phase = next() % CYCLES_PER_FRAME;
let cycles = if case == 0 {
u32::MAX
} else {
next() % (CYCLES_PER_FRAME * 5)
};
let end = u64::from(phase) + u64::from(cycles);
let mut expected = [None; 2];
for line in u64::from(phase / CYCLES_PER_LINE)..=end / u64::from(CYCLES_PER_LINE) {
let time = line * u64::from(CYCLES_PER_LINE) + 40;
if time > u64::from(phase) && time <= end && (line % 228 < 159 || line % 228 == 227)
{
expected[0] = expected[1];
expected[1] = Some((
(time - u64::from(phase)) as u32,
((line + 1) % 228) as usize,
));
}
}
assert_eq!(latest_events(phase, cycles), expected);
}
}
#[test]
fn next_event_handles_visible_end_hidden_lines_and_exact_boundary() {
for line in 0..228 {
for cycle in [0, 39, 40, 1006, 1231] {
let phase = u32::from(line) * CYCLES_PER_LINE + u32::from(cycle);
let distance = next_event(line, cycle);
assert!(distance > 0);
assert_eq!(latest_events(phase, distance - 1), [None; 2]);
assert!(latest_events(phase, distance)[1].is_some());
}
}
}
}

View file

@ -0,0 +1,133 @@
//! Vertical WIN0/WIN1 comparators. State survives frame wrap and register writes.
//! Horizontal comparators retain per-column history separately from vertical state.
pub(crate) mod horizontal;
use crate::display::{CYCLES_PER_LINE, LINES_PER_FRAME};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) struct WindowEdges {
pub active: [bool; 2],
started: bool,
}
impl WindowEdges {
/// Evaluate each crossed line start, including hidden lines. With fixed bounds,
/// only the latest matching edge matters, so large clock batches remain O(1).
pub fn advance(&mut self, phase: u32, cycles: u32, bounds: [u16; 2]) {
if cycles == 0 {
return;
}
if !self.started {
// Allow host setup before the first clock advance at reset's line zero.
// Later writes at an already-entered line never replay its comparator.
let row = phase / CYCLES_PER_LINE;
for (window, bounds) in bounds.into_iter().enumerate() {
if row == u32::from(bounds >> 8) {
self.active[window] = true;
}
if row == u32::from(bounds & 255) {
self.active[window] = false;
}
}
self.started = true;
}
let first = u64::from(phase / CYCLES_PER_LINE) + 1;
let last = (u64::from(phase) + u64::from(cycles)) / u64::from(CYCLES_PER_LINE);
let period = u64::from(LINES_PER_FRAME);
let latest = |row: u16| {
if u32::from(row) >= LINES_PER_FRAME {
return None; // VCOUNT never reaches values 228..255.
}
let distance = (last + period - u64::from(row)) % period;
last.checked_sub(distance).filter(|&edge| edge >= first)
};
for (window, bounds) in bounds.into_iter().enumerate() {
let top = latest(bounds >> 8);
let bottom = latest(bounds & 255);
match (top, bottom) {
(Some(top), Some(bottom)) => self.active[window] = top > bottom,
(Some(_), None) => self.active[window] = true,
(None, Some(_)) => self.active[window] = false,
(None, None) => {} // No edge: retain state, even across frames.
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::display::CYCLES_PER_FRAME;
fn step(active: &mut bool, bounds: u16, row: u32) {
if row == u32::from(bounds >> 8) {
*active = true;
}
if row == u32::from(bounds & 255) {
*active = false;
}
}
#[test]
fn all_bounds_match_independent_edge_iteration_across_two_frames() {
for bounds in 0..=u16::MAX {
let mut edges = WindowEdges::default();
let mut expected = false;
for row in 0..=LINES_PER_FRAME * 2 {
step(&mut expected, bounds, row % LINES_PER_FRAME);
}
edges.advance(0, 2 * CYCLES_PER_FRAME, [bounds; 2]);
assert_eq!(edges.active, [expected; 2], "bounds={bounds:#06x}");
}
}
#[test]
fn random_batches_and_extreme_batch_match_independent_edge_iteration() {
let mut seed = 0x60c3_273bu32;
for case in 0..500 {
let mut next = || {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
seed
};
let bounds = [next() as u16, next() as u16];
let phase = next() % CYCLES_PER_FRAME;
let cycles = if case == 0 {
u32::MAX
} else {
next() % (7 * CYCLES_PER_FRAME)
};
let mut edges = WindowEdges {
active: [next() & 4 != 0, next() & 8 != 0],
started: true,
};
let mut expected = edges.active;
for boundary in u64::from(phase / CYCLES_PER_LINE) + 1
..=(u64::from(phase) + u64::from(cycles)) / u64::from(CYCLES_PER_LINE)
{
for window in 0..2 {
step(
&mut expected[window],
bounds[window],
(boundary % u64::from(LINES_PER_FRAME)) as u32,
);
}
}
edges.advance(phase, cycles, bounds);
assert_eq!(edges.active, expected, "phase={phase} cycles={cycles}");
}
}
#[test]
fn zero_cycles_and_writes_do_not_replay_an_entered_line() {
let mut edges = WindowEdges::default();
edges.advance(0, 0, [0x0010, 0x0020]);
assert!(!edges.started);
edges.advance(0, 1, [0x0010, 0x0020]);
assert_eq!(edges.active, [true; 2]);
edges.advance(1, CYCLES_PER_LINE - 1, [0x0110, 0x0101]);
assert_eq!(edges.active, [true, false]); // Bottom wins when edges coincide.
edges.advance(CYCLES_PER_LINE, 1, [0x0101, 0x0110]);
assert_eq!(edges.active, [true, false]); // The row1 comparator already ran.
}
}

View file

@ -0,0 +1,168 @@
//! Horizontal comparator history only, not pixel-fetch or color-register timing.
//! Sample X=0..255 at line cycles 0,4,..1020; right-edge matches clear after left.
use crate::{display::CYCLES_PER_LINE, video::WIDTH};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct HorizontalWindows {
active: [bool; 2],
pub pixels: [[bool; WIDTH]; 2],
}
impl Default for HorizontalWindows {
fn default() -> Self {
Self {
active: [false; 2],
pixels: [[false; WIDTH]; 2],
}
}
}
impl HorizontalWindows {
/// Consume the half-open clock interval [phase, phase+cycles). A write at
/// cycle 4*x therefore precedes X's comparison; a write at 4*x+1 is too late.
/// Process at most three line segments, even across arbitrarily many frames.
pub fn advance(&mut self, phase: u32, cycles: u32, bounds: [u16; 2]) {
if cycles == 0 {
return;
}
let start = phase % CYCLES_PER_LINE;
let end = u64::from(start) + u64::from(cycles);
let last_line = (end - 1) / u64::from(CYCLES_PER_LINE);
if last_line == 0 {
self.segment(start, end as u32, bounds);
return;
}
self.segment(start, CYCLES_PER_LINE, bounds);
if last_line > 1 {
if last_line > 2 {
// Two or more full intervening lines: both edges have fired,
// so the last full line starts in the stable wrapping state.
self.active = bounds.map(|b| (b >> 8) > (b & 255));
}
// Retain unvisited pixels from the previous line, just as repeated
// single-cycle advancement does. Never clear history on line entry.
self.segment(0, CYCLES_PER_LINE, bounds);
}
let tail = ((end - 1) % u64::from(CYCLES_PER_LINE) + 1) as u32;
self.segment(0, tail, bounds);
}
fn segment(&mut self, start: u32, end: u32, bounds: [u16; 2]) {
let first = start.div_ceil(4).min(256) as usize;
let last = end.div_ceil(4).min(256) as usize;
if first == last {
return;
}
for (window, bounds) in bounds.into_iter().enumerate() {
let left = usize::from(bounds >> 8);
let right = usize::from(bounds & 255);
let edges = if left <= right {
[(left, true), (right, false)]
} else {
[(right, false), (left, true)]
};
let mut from = first;
for (edge, state) in edges {
if (first..last).contains(&edge) {
self.pixels[window][from.min(WIDTH)..edge.min(WIDTH)].fill(self.active[window]);
self.active[window] = state;
from = edge;
}
}
self.pixels[window][from.min(WIDTH)..last.min(WIDTH)].fill(self.active[window]);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn reference(state: &mut HorizontalWindows, start: u32, cycles: u32, bounds: [u16; 2]) {
// Independent dot iteration, including offscreen columns and hidden lines.
let end = u64::from(start) + u64::from(cycles);
let mut tick = u64::from(start.div_ceil(4)) * 4;
while tick < end {
let x = (tick % u64::from(CYCLES_PER_LINE)) / 4;
if x < 256 {
for (window, bounds) in bounds.into_iter().enumerate() {
if x == u64::from(bounds >> 8) {
state.active[window] = true;
}
if x == u64::from(bounds & 255) {
state.active[window] = false;
}
if x < WIDTH as u64 {
state.pixels[window][x as usize] = state.active[window];
}
}
}
tick += 4;
}
}
#[test]
fn all_bounds_match_dot_iteration_for_startup_and_wrapped_next_line() {
for bounds in 0..=u16::MAX {
let mut actual = HorizontalWindows::default();
let mut expected = actual.clone();
for phase in [0, CYCLES_PER_LINE] {
actual.advance(phase, CYCLES_PER_LINE, [bounds; 2]);
reference(&mut expected, phase, CYCLES_PER_LINE, [bounds; 2]);
assert_eq!(actual, expected, "bounds={bounds:#x} phase={phase}");
}
}
}
#[test]
fn arbitrary_partial_lines_and_bound_writes_match_dot_iteration() {
let mut seed = 0x371b_c321u32;
let mut next = || {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
seed
};
let mut actual = HorizontalWindows::default();
let mut expected = actual.clone();
let mut phase = 0;
for _ in 0..2000 {
let bounds = [next() as u16, next() as u16];
let cycles = next() % (9 * CYCLES_PER_LINE);
actual.advance(phase, cycles, bounds);
reference(&mut expected, phase, cycles, bounds);
assert_eq!(
actual, expected,
"phase={phase} cycles={cycles} bounds={bounds:?}"
);
phase = (phase + cycles) % (228 * CYCLES_PER_LINE);
}
}
#[test]
fn largest_batch_has_same_state_as_short_batch_with_same_final_phase() {
for start in [0, 1, 79, 955, 1006, 1021, 1231] {
let mut actual = HorizontalWindows::default();
let mut expected = actual.clone();
let cycles = u32::MAX;
// After several complete fixed-bound lines all earlier history is replaced.
let short = 4 * CYCLES_PER_LINE + cycles % CYCLES_PER_LINE;
actual.advance(start, cycles, [0xf00a, 0x1428]);
reference(&mut expected, start, short, [0xf00a, 0x1428]);
assert_eq!(actual, expected);
}
}
#[test]
fn zero_cycles_do_not_sample_or_replay_edges() {
let mut state = HorizontalWindows::default();
state.advance(0, 0, [0x0010; 2]);
assert_eq!(state, HorizontalWindows::default());
state.advance(0, 1, [0x0010; 2]);
assert!(state.active[0] && state.pixels[0][0]);
let before = state.clone();
state.advance(1, 0, [0; 2]);
assert_eq!(state, before);
state.advance(1, 3, [0; 2]);
assert_eq!(state, before); // X0 already passed, X1 has not started.
}
}

View file

@ -0,0 +1,377 @@
use gba_core::{
io::*,
memory::{Memory, MemoryError, OAM_START as OAM, PALETTE_START as PAL, VRAM_START as VRAM},
video::{rgb555_to_rgb888, Framebuffer, VideoError, WIDTH},
};
fn setup(bg: u32, size: u16, wrap: bool) -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.write16(DISPCNT, 2 | (0x100 << bg)).unwrap();
m.write16(
BG0CNT + bg * 2,
0x1000 | (size << 14) | if wrap { 0x2000 } else { 0 },
)
.unwrap();
transform(&mut m, bg, [256, 0, 0, 256], [0, 0]);
m.write16(PAL, 0x4000).unwrap();
for i in 1..=255 {
m.write16(PAL + i * 2, i as u16 * 31).unwrap();
}
// All 256 byte-sized tile numbers, with an asymmetric indexed pattern.
for tile in 0..256 {
for y in 0..8 {
for x in (0..8).step_by(2) {
let lo = index(tile, x, y);
let hi = index(tile, x + 1, y);
m.write16(
VRAM + tile * 64 + y * 8 + x,
u16::from(lo) | (u16::from(hi) << 8),
)
.unwrap();
}
}
}
let tiles = 16_u32 << size;
for y in 0..tiles {
for x in (0..tiles).step_by(2) {
m.write16(
VRAM + 0x8000 + y * tiles + x,
u16::from(tile_number(x, y)) | (u16::from(tile_number(x + 1, y)) << 8),
)
.unwrap();
}
}
m
}
fn tile_number(x: u32, y: u32) -> u8 {
(x.wrapping_mul(17) + y.wrapping_mul(31)) as u8
}
fn index(tile: u32, x: u32, y: u32) -> u8 {
(tile + x + 3 * y) as u8
}
fn transform(m: &mut Memory, bg: u32, coeff: [i16; 4], origin: [i32; 2]) {
let base = BG2PA + (bg - 2) * 16;
for (i, v) in coeff.into_iter().enumerate() {
m.write16(base + i as u32 * 2, v as u16).unwrap();
}
for (i, v) in origin.into_iter().enumerate() {
m.write32(base + 8 + i as u32 * 4, v as u32).unwrap();
}
}
fn frame(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
m.render_frame(&mut f).unwrap();
f
}
fn pixel(m: &Memory, x: usize, y: usize) -> u32 {
frame(m).pixels()[y * WIDTH + x]
}
fn reference(m: &Memory, size: u16, wrap: bool, coeff: [i16; 4], origin: [i32; 2]) {
let f = frame(m);
let size = 128_i64 << size;
for y in 0..160 {
for x in 0..240 {
// Wide-integer reference with explicit 28-bit wrapping and floor division.
let coord = |r: i32, a: i16, b: i16| {
let v = i64::from(r) + i64::from(a) * x as i64 + i64::from(b) * y as i64;
let signed = (v + (1 << 27)).rem_euclid(1 << 28) - (1 << 27);
signed.div_euclid(256)
};
let mut tx = coord(origin[0], coeff[0], coeff[1]);
let mut ty = coord(origin[1], coeff[2], coeff[3]);
let mut color = 0x4000;
if wrap || (tx >= 0 && ty >= 0 && tx < size && ty < size) {
tx = tx.rem_euclid(size);
ty = ty.rem_euclid(size);
let t = tile_number(tx as u32 / 8, ty as u32 / 8);
let i = index(u32::from(t), tx as u32 % 8, ty as u32 % 8);
if i != 0 {
color = u16::from(i) * 31;
}
}
assert_eq!(
f.pixels()[y * WIDTH + x],
rgb555_to_rgb888(color),
"pixel {x},{y}, size {size}, origin={origin:?}, matrix={coeff:?}"
);
}
}
}
#[test]
fn affine_matrices_sizes_wrapping_and_signed_origins_match_wide_reference() {
for bg in [2, 3] {
for size in 0..4 {
for wrap in [false, true] {
let mut m = setup(bg, size, wrap);
for (coeff, origin) in [
([256, 0, 0, 256], [0, 0]),
([0, 256, -256, 0], [-1, 128 * 256]),
([181, 181, -181, 181], [-173, 17551]),
([128, 0, 0, 512], [127, -129]),
([256, 127, 1, -256], [0x07ff_ffff, 0x0800_0000]),
([32767, -32768, -32768, 32767], [-123456, 123456]),
([0; 4], [23 * 256, 17 * 256]),
] {
transform(&mut m, bg, coeff, origin);
reference(&m, size, wrap, coeff, origin);
}
}
}
}
}
#[test]
fn write_only_registers_merge_bytes_halfwords_and_words_and_mask_reference_top_bits() {
let mut m = setup(2, 1, true);
m.write32(BG2PA, 0xff80_0180).unwrap(); // A=1.5, B=-0.5
m.write8(BG2PA, 0x40).unwrap(); // A=1.25, retains high byte
m.write32(BG2PC, 0x0100_0000).unwrap();
m.write32(BG2X, 0xffff_ffff).unwrap();
m.write8(BG2X, 0x80).unwrap(); // -0.5
m.write16(BG2Y, 0x0101).unwrap();
m.write16(BG2Y + 2, 0xf000).unwrap(); // Unused high nibble must be zero internally.
reference(&m, 1, true, [320, -128, 0, 256], [-128, 257]);
for address in (BG2PA..=BG3Y).step_by(4) {
assert_eq!(m.read32(address).unwrap(), 0);
}
assert_eq!(
m.read16(0x0400_0058),
Err(MemoryError::Unmapped(0x0400_0058))
);
assert_eq!(m.cycles(), 0);
}
#[test]
fn affine_bg_ignores_text_scroll_and_color_depth_bit() {
let mut m = setup(2, 1, true);
let before = frame(&m);
m.write32(BG2HOFS, u32::MAX).unwrap();
m.write16(BG2CNT, 0x7080).unwrap(); // Same map/size/wrap, color bit set.
assert_eq!(frame(&m).pixels(), before.pixels());
}
#[test]
fn affine_byte_maps_are_flat_rows_use_tile255_and_have_no_flip_or_bank_bits() {
let mut m = setup(2, 2, false);
m.write16(VRAM + 0x8000 + 64, 0x00ff).unwrap(); // (tileX=0,tileY=1), not next text screen block.
assert_eq!(pixel(&m, 0, 8), rgb555_to_rgb888(255 * 31));
assert_eq!(pixel(&m, 1, 8), rgb555_to_rgb888(0x4000)); // Tile255 texel1 wraps to palette index0.
assert_eq!(pixel(&m, 8, 8), rgb555_to_rgb888(0x4000)); // Adjacent map byte selects tile0.
}
#[test]
fn affine_character_base_and_last_tile_reach_the_end_of_bg_vram() {
let mut m = setup(2, 0, false);
m.write16(BG2CNT, 0x100c).unwrap(); // Character base3, map16.
m.write16(VRAM + 0x8000, 0x00ff).unwrap();
m.write16(VRAM + 0xfffe, 0x0201).unwrap();
transform(&mut m, 2, [256, 0, 0, 256], [6 * 256, 7 * 256]);
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(31));
assert_eq!(pixel(&m, 1, 0), rgb555_to_rgb888(62));
}
#[test]
fn mode1_composes_two_text_layers_and_one_affine_layer_with_priority_ties() {
let mut m = setup(2, 1, true);
m.write16(DISPCNT, 0x701).unwrap();
m.write16(BG0CNT, 0x1804).unwrap(); // Text tile base1, map24, priority0.
m.write16(BG1CNT, 0x1904).unwrap();
m.write16(VRAM + 0x4000, 0x2222).unwrap();
m.write16(VRAM + 0xc800, 1).unwrap();
m.write16(VRAM + 0x4020, 0x3333).unwrap();
m.write16(VRAM, 0x0101).unwrap(); // BG2 affine nonzero index1.
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(2 * 31)); // BG0 wins tie.
m.write16(BG0CNT, 0x1807).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(3 * 31)); // BG1 wins tie over BG2.
m.write16(BG1CNT, 0x1907).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(31));
m.write16(VRAM, 0).unwrap(); // Affine palette index0 reveals text BG0.
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(2 * 31));
}
#[test]
fn mode2_bg3_has_independent_matrix_reference_and_priority() {
let mut m = setup(2, 1, true);
m.write16(DISPCNT, 0xc02).unwrap();
m.write16(BG3CNT, 0x7000).unwrap();
transform(&mut m, 2, [0; 4], [256, 0]);
transform(&mut m, 3, [0; 4], [512, 0]);
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(31)); // BG2 wins tie.
m.write16(BG2CNT, 0x7001).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(62));
m.write16(PAL + 4, 0).unwrap();
assert_eq!(pixel(&m, 0, 0), 0); // Nonzero index with black color is opaque.
}
#[test]
fn mode3_uses_affine_matrix_ignores_overflow_and_keeps_black_opaque() {
let mut m = Memory::new(vec![]).unwrap();
m.write16(DISPCNT, 0x403).unwrap();
m.write16(PAL, 0x03e0).unwrap();
m.write16(VRAM, 0).unwrap();
m.write16(VRAM + 2, 0x001f).unwrap();
m.write16(VRAM + (159 * 240 + 239) * 2, 0x7fff).unwrap();
transform(&mut m, 2, [256, 0, 0, 256], [-256, 0]);
m.write16(BG2CNT, 0x2000).unwrap(); // Ignored bitmap overflow flag.
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
assert_eq!(pixel(&m, 1, 0), 0);
assert_eq!(pixel(&m, 2, 0), 0xff0000);
transform(&mut m, 2, [-256, 0, 0, -256], [239 * 256, 159 * 256]);
assert_eq!(pixel(&m, 0, 0), 0xffffff);
assert_eq!(pixel(&m, 239, 159), 0);
// Hardware reset is a zero matrix, not an implicit identity.
transform(&mut m, 2, [0; 4], [256, 0]);
assert!(frame(&m).pixels().iter().all(|&p| p == 0xff0000));
}
#[test]
fn transformed_backgrounds_composite_with_regular_and_affine_sprites() {
for mode in [1, 2] {
let mut m = setup(2, 1, true);
m.write16(DISPCNT, mode | 0x1440).unwrap();
for i in 0..128 {
m.write16(OAM + i * 8, 0x200).unwrap();
}
m.write16(VRAM, 0x0101).unwrap();
m.write16(VRAM + 0x10000, 0x2222).unwrap();
m.write16(PAL + 0x204, 0x03e0).unwrap();
m.write16(OAM, 0).unwrap();
m.write16(OAM + 4, 0).unwrap();
assert_eq!(pixel(&m, 0, 0), 0x00ff00); // OBJ wins equal BG priority.
m.write16(OAM + 4, 0x400).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(31));
m.write16(OAM, 0x100).unwrap();
m.write16(OAM + 6, 256).unwrap();
m.write16(OAM + 30, 256).unwrap();
m.write16(OAM + 4, 0).unwrap();
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
}
}
#[test]
fn diagnostics_leave_output_unchanged_and_disabled_layers_skip_validation() {
let mut m = setup(2, 3, false);
let mut f = Framebuffer::default();
f.clear(0x001f);
for control in [0xc01, 0xd02, 0xe02] {
m.write16(DISPCNT, control).unwrap();
assert_eq!(
m.render_frame(&mut f),
Err(VideoError::UnsupportedLayers(control))
);
}
m.write16(DISPCNT, 0x402).unwrap();
m.write16(BG2CNT, 0xdf00).unwrap(); // Map31, 1024 pixels.
m.write32(BG2Y, 128 * 256).unwrap(); // Flat row starts outside first64K.
assert_eq!(
m.render_frame(&mut f),
Err(VideoError::UnsupportedMapAddress(0x10000))
);
assert!(f.pixels().iter().all(|&p| p == 0xff0000));
m.write16(DISPCNT, 2).unwrap();
m.render_frame(&mut f).unwrap();
assert!(f.pixels().iter().all(|&p| p == rgb555_to_rgb888(0x4000)));
m.write16(DISPCNT, 0xffff).unwrap();
m.render_frame(&mut f).unwrap();
assert!(f.pixels().iter().all(|&p| p == 0xffffff));
}
#[test]
fn cpu_demo_pans_rotates_zooms_clips_cancels_opposites_and_resets() {
use gba_core::input::{Button, Buttons};
use gba_demos::affine_demo::{AffineDemo, AFFINE_STATE};
let mut demo = AffineDemo::new().unwrap();
let mut f = Framebuffer::default();
assert_eq!(demo.machine().memory().read16(DISPCNT).unwrap(), 0);
assert_eq!(demo.machine().memory().read16(VRAM).unwrap(), 0);
// Signed negative panning and opposite-key cancellation.
demo.frame(
Buttons::default()
.with(Button::Left, true)
.with(Button::Up, true),
&mut f,
)
.unwrap();
demo.frame(Buttons::from_bits(0xf0), &mut f).unwrap();
assert_eq!(
demo.machine().memory().read32(AFFINE_STATE + 4).unwrap() as i32,
-2
);
assert_eq!(
demo.machine().memory().read32(AFFINE_STATE + 8).unwrap() as i32,
-2
);
for bits in 0..8 {
let rotated = bits & 1 != 0;
let zoomed = bits & 2 != 0;
let clipped = bits & 4 != 0;
let before = demo.machine().memory().display_position();
demo.frame(
Buttons::default()
.with(Button::L, rotated)
.with(Button::R, zoomed)
.with(Button::A, clipped),
&mut f,
)
.unwrap();
let a = f64::from(match (rotated, zoomed) {
(false, false) => 256,
(false, true) => 128,
(true, false) => 181,
(true, true) => 90,
}) / 256.0;
let b = if rotated { a } else { 0.0 };
for y in 0..160 {
for x in 0..240 {
let dx = x as f64 - 120.0;
let dy = y as f64 - 80.0;
let tx = (126.0 + a * dx + b * dy).floor() as i32;
let ty = (126.0 - b * dx + a * dy).floor() as i32;
let color = if clipped && (!(0..256).contains(&tx) || !(0..256).contains(&ty)) {
0x4000
} else {
let tx = tx.rem_euclid(256) as usize;
let ty = ty.rem_euclid(256) as usize;
[
0x0260, 0x03a0, 0x7d20, 0x7e80, 0x001f, 0x421f, 0x03ff, 0x7fff,
][((tx / 32 + 2 * (ty / 32)) % 4) * 2 + (tx / 2 + ty / 2) % 2]
};
assert_eq!(
f.pixels()[y * WIDTH + x],
rgb555_to_rgb888(color),
"input {bits}, pixel {x},{y}"
);
}
}
let m = demo.machine().memory();
assert_eq!(m.read32(AFFINE_STATE).unwrap(), bits + 3);
assert_eq!(m.read16(DISPCNT).unwrap(), 0x402);
assert_eq!(m.display_position().vblanks, before.vblanks + 1);
assert_eq!(m.display_position().scanline, 160);
}
demo.frame(
Buttons::default()
.with(Button::Start, true)
.with(Button::Right, true),
&mut f,
)
.unwrap();
assert_eq!(demo.machine().memory().read32(AFFINE_STATE + 4).unwrap(), 0);
assert_eq!(demo.machine().memory().read32(AFFINE_STATE + 8).unwrap(), 0);
}
#[test]
fn snapshot_uses_programmed_origin_without_advancing_or_accumulating_scanline_state() {
let mut m = setup(2, 1, true);
transform(&mut m, 2, [181, 181, -181, 181], [12345, -567]);
let first = frame(&m);
m.advance_cycles(1232 * 57);
let cycles = m.cycles();
assert_eq!(frame(&m).pixels(), first.pixels());
assert_eq!(m.cycles(), cycles);
m.write32(BG2X, 0).unwrap();
assert_ne!(frame(&m).pixels(), first.pixels());
}

View file

@ -0,0 +1,330 @@
use gba_core::{
cpu::Cpu,
display::{CYCLES_PER_FRAME, CYCLES_PER_LINE as LINE, HBLANK_START as HB, VBLANK_START as VB},
dma::DMA_BASE,
input::{Button, Buttons},
io::*,
machine::Machine,
memory::{Memory, PALETTE_START as PAL, ROM_START, VRAM_START as VRAM},
video::{rgb555_to_rgb888 as rgb, Framebuffer, HEIGHT, WIDTH},
};
use gba_demos::affine_raster_demo::{AffineRasterDemo, AFFINE_RASTER_STATE, AFFINE_RASTER_TABLE};
fn color(x: u32, y: u32) -> u16 {
((x & 31) | ((y & 31) << 5) | (((x + y) & 31) << 10)) as u16
}
fn bitmap() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.set_scanline_rendering(true);
m.write16(DISPCNT, 0x403).unwrap();
m.write16(BG2PA, 256).unwrap();
m.write16(BG2PB, 256).unwrap();
m.write16(BG2PD, 256).unwrap();
for y in 0..160 {
for x in 0..240 {
m.write16(VRAM + (y * 240 + x) * 2, color(x, y)).unwrap();
}
}
m
}
fn finish(m: &mut Memory) -> Framebuffer {
let p = m.display_position();
let phase = u32::from(p.scanline) * LINE + u32::from(p.line_cycle);
m.advance_cycles(VB - phase);
let mut f = Framebuffer::default();
assert!(m.present_frame(&mut f).unwrap());
f
}
fn row(f: &Framebuffer, y: usize, sx: u32, sy: u32) {
assert_eq!(f.pixels()[y * WIDTH], rgb(color(sx, sy)), "row={y}");
}
#[test]
fn coefficient_changes_accumulate_instead_of_multiplying_absolute_screen_y() {
let mut m = bitmap();
m.advance_cycles(2 * LINE + HB);
m.write16(BG2PB, 3 * 256).unwrap();
m.write16(BG2PD, 2 * 256).unwrap();
let f = finish(&mut m);
for (y, x, sy) in [(0, 0, 0), (1, 1, 1), (2, 2, 2), (3, 5, 4), (4, 8, 6)] {
row(&f, y, x, sy);
}
let mut snapshot = Framebuffer::default();
m.render_frame(&mut snapshot).unwrap();
row(&snapshot, 3, 9, 6); // Debug snapshots still use programmed origins.
}
#[test]
fn visible_reference_writes_replace_current_origin_and_only_the_written_axis() {
let mut m = bitmap();
m.advance_cycles(3 * LINE);
m.write32(BG2X, 10 * 256).unwrap();
m.advance_cycles(HB);
m.write32(BG2Y, 20 * 256).unwrap(); // HBlank: override next Y increment.
let f = finish(&mut m);
row(&f, 2, 2, 2);
row(&f, 3, 10, 3);
row(&f, 4, 11, 20);
row(&f, 5, 12, 21);
}
#[test]
fn partial_reference_writes_merge_programmed_not_accumulated_values() {
let mut m = bitmap();
m.write32(BG2X, 0x200).unwrap();
m.advance_cycles(5 * LINE);
m.write8(BG2X, 0x80).unwrap();
let f = finish(&mut m);
row(&f, 4, 6, 4);
row(&f, 5, 2, 5);
row(&f, 6, 3, 6);
assert_eq!(m.read32(BG2X).unwrap(), 0); // Still write-only.
}
#[test]
fn line_zero_reloads_programmed_origins_after_vblank_writes() {
let mut m = bitmap();
finish(&mut m);
m.write32(BG2X, 7 * 256).unwrap();
m.write32(BG2Y, 9 * 256).unwrap();
m.advance_cycles(CYCLES_PER_FRAME - VB);
let f = finish(&mut m);
row(&f, 0, 7, 9);
row(&f, 1, 8, 10);
m.advance_cycles(CYCLES_PER_FRAME - VB);
let f = finish(&mut m);
row(&f, 0, 7, 9); // Not the last frame's accumulated position.
}
#[test]
fn disabled_and_non_affine_modes_pause_tracking_but_forced_blank_does_not() {
for disabled in [3, 0x400, 0x483] {
let mut m = bitmap();
m.advance_cycles(2 * LINE);
m.write16(DISPCNT, disabled).unwrap();
m.advance_cycles(3 * LINE);
m.write16(DISPCNT, 0x403).unwrap();
let f = finish(&mut m);
let origin = if disabled == 0x483 { 5 } else { 2 };
row(&f, 5, origin, origin);
if disabled == 0x483 {
assert_eq!(f.pixels()[2 * WIDTH], 0xffffff);
}
}
}
#[test]
fn window_masks_do_not_pause_internal_tracking() {
let mut m = bitmap();
m.write16(DISPCNT, 0x2403).unwrap();
m.write16(WIN0H, 240).unwrap();
m.write16(WIN0V, 160).unwrap();
m.write16(WININ, 0).unwrap();
m.advance_cycles(5 * LINE);
m.write16(WININ, 4).unwrap();
let f = finish(&mut m);
assert_eq!(f.pixels()[4 * WIDTH], 0);
row(&f, 5, 5, 5);
}
#[test]
fn mosaic_holds_origin_then_uses_current_coefficient_for_block_increment() {
let mut m = bitmap();
m.write16(BG2CNT, 0x40).unwrap();
m.write16(MOSAIC, 0x30).unwrap(); // Four-row vertical blocks.
m.advance_cycles(2 * LINE + HB);
m.write16(BG2PB, 2 * 256).unwrap();
let f = finish(&mut m);
for y in 0..4 {
row(&f, y, 0, 0);
}
for y in 4..8 {
row(&f, y, 8, 4);
}
row(&f, 8, 16, 8);
}
#[test]
fn bg2_and_bg3_track_independently_in_supported_tiled_modes() {
for mode in [1, 2] {
let mut m = Memory::new(vec![]).unwrap();
m.set_scanline_rendering(true);
m.write16(DISPCNT, 0x400 | mode).unwrap();
m.write16(BG2CNT, 0x1000).unwrap();
m.write16(BG3CNT, 0x1000).unwrap();
for reg in [BG2PA, BG3PA] {
m.write16(reg, 256).unwrap();
}
m.write16(BG2PB, 256).unwrap();
m.write16(BG3PB, 2 * 256).unwrap();
for index in 1..=8 {
m.write16(PAL + index * 2, color(index, 0)).unwrap();
}
for y in 0..8 {
for x in (0..8).step_by(2) {
m.write16(VRAM + y * 8 + x, (x + 1) as u16 | (((x + 2) as u16) << 8))
.unwrap();
}
}
m.advance_cycles(3 * LINE);
m.write16(DISPCNT, if mode == 2 { 0x802 } else { 0x401 })
.unwrap();
let f = finish(&mut m);
row(&f, 2, 3, 0);
if mode == 2 {
row(&f, 3, 1, 0); // Disabled BG3 did not accumulate.
row(&f, 4, 3, 0);
row(&f, 5, 5, 0);
} else {
row(&f, 4, 5, 0); // BG2 remains affine in mode1.
}
}
}
#[test]
fn capture_enable_does_not_reset_affine_hardware_state() {
let mut m = bitmap();
m.set_scanline_rendering(false);
m.advance_cycles(CYCLES_PER_FRAME); // Batched clock path.
m.set_scanline_rendering(true);
m.advance_cycles(4 * LINE);
m.write32(BG2X, 9 * 256).unwrap();
let captured = finish(&mut m);
row(&captured, 4, 9, 4);
m.set_scanline_rendering(false);
m.advance_cycles(CYCLES_PER_FRAME - VB);
m.set_scanline_rendering(true);
let captured = finish(&mut m);
row(&captured, 0, 9, 0);
}
#[test]
fn hblank_dma_reference_writes_override_next_increment() {
let mut m = bitmap();
for y in 0..160 {
m.write32(0x0200_0000 + y * 4, (y % 16 + 10) * 256).unwrap();
}
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, BG2X).unwrap();
m.write32(DMA_BASE + 8, 0xa640_0001).unwrap(); // Word, fixed destination, repeat HBlank.
m.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
machine.run_until_vblank(1000).unwrap();
let mut f = Framebuffer::default();
assert!(machine.memory().present_frame(&mut f).unwrap());
row(&f, 0, 0, 0);
for y in 1..HEIGHT {
row(&f, y, (y as u32 - 1) % 16 + 10, y as u32);
}
}
#[test]
fn cpu_raster_demo_generates_dma_table_and_all_transform_combinations() {
let mut demo = AffineRasterDemo::new().unwrap();
let mut f = Framebuffer::default();
for index in 0..8 {
let rotated = index & 1 != 0;
let zoomed = index & 2 != 0;
let bypassed = index & 4 != 0;
let buttons = Buttons::default()
.with(Button::L, rotated)
.with(Button::R, zoomed)
.with(Button::A, bypassed)
.with(Button::Left, true)
.with(Button::Up, true);
demo.frame(buttons, &mut f).unwrap();
let m = demo.machine().memory();
let pan = -2 * (index + 1);
assert_eq!(m.read32(AFFINE_RASTER_STATE).unwrap(), index as u32 + 1);
assert_eq!(m.read32(AFFINE_RASTER_STATE + 4).unwrap() as i32, pan);
assert_eq!(m.read32(AFFINE_RASTER_STATE + 8).unwrap() as i32, pan);
assert_eq!(m.read16(DMA_BASE + 10).unwrap(), 0xa240);
assert_eq!(m.captured_vblank(), Some(m.display_position().vblanks));
let a = match (rotated, zoomed) {
(false, false) => 256,
(false, true) => 128,
(true, false) => 181,
(true, true) => 90,
};
let b = if rotated { a } else { 0 };
for y in 0..160 {
let offset = if bypassed {
0
} else if y & 16 == 0 {
512
} else {
-512
};
assert_eq!(
m.read16(AFFINE_RASTER_TABLE + y * 2).unwrap() as i16,
(b + offset) as i16
);
}
for y in 0..HEIGHT {
for x in 0..WIDTH {
let dx = x as i32 - 120;
let dy = y as i32 - 80;
let phase = y as i32 % 32;
let extra = if bypassed {
0
} else {
512 * phase.min(32 - phase)
};
let tx = ((128 + pan) * 256 + a * dx + b * dy + extra)
.div_euclid(256)
.rem_euclid(256) as usize;
let ty = ((128 + pan) * 256 - b * dx + a * dy)
.div_euclid(256)
.rem_euclid(256) as usize;
let tile = (tx / 32 + 2 * (ty / 32)) % 4;
let shade = (tx / 2 + ty / 2) % 2;
let expected = [
0x0260, 0x03a0, 0x7d20, 0x7e80, 0x001f, 0x421f, 0x03ff, 0x7fff,
][tile * 2 + shade];
assert_eq!(
f.pixels()[y * WIDTH + x],
rgb(expected),
"case={index} x={x} y={y}"
);
}
}
}
demo.frame(
Buttons::default()
.with(Button::Left, true)
.with(Button::Right, true),
&mut f,
)
.unwrap();
assert_eq!(
demo.machine()
.memory()
.read32(AFFINE_RASTER_STATE + 4)
.unwrap() as i32,
-16
);
demo.frame(
Buttons::default()
.with(Button::Start, true)
.with(Button::Right, true),
&mut f,
)
.unwrap();
assert_eq!(
demo.machine()
.memory()
.read32(AFFINE_RASTER_STATE + 4)
.unwrap(),
0
);
assert_eq!(
demo.machine()
.memory()
.read32(AFFINE_RASTER_STATE + 8)
.unwrap(),
0
);
}

View file

@ -0,0 +1,671 @@
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;
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 [3, 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))
);
}

View file

@ -0,0 +1,684 @@
use super::*;
use gba_core::{
bios::INVALID_ARGUMENT_TRAP,
dma::DMA_BASE,
io::{BG2PA, DISPCNT},
memory::{OAM_START as OAM, PALETTE_START as PAL},
video::{rgb555_to_rgb888 as rgb, Framebuffer},
};
const DATA: u32 = ROM_START + 23 * 4;
#[derive(Clone, Copy)]
struct Bg {
texture: [i32; 2],
screen: [i16; 2],
scale: [i16; 2],
angle: u16,
}
#[derive(Clone, Copy)]
struct Obj {
scale: [i16; 2],
angle: u16,
}
fn bg_bytes(records: &[Bg]) -> Vec<u8> {
records
.iter()
.flat_map(|r| {
let mut bytes = Vec::new();
for v in r.texture {
bytes.extend(v.to_le_bytes());
}
for v in r.screen.into_iter().chain(r.scale) {
bytes.extend(v.to_le_bytes());
}
bytes.extend(r.angle.to_le_bytes());
bytes.extend([0xa5, 0x5a]); // Ignored structure padding.
bytes
})
.collect()
}
fn obj_bytes(records: &[Obj]) -> Vec<u8> {
records
.iter()
.flat_map(|r| {
let mut bytes = Vec::new();
for v in r.scale {
bytes.extend(v.to_le_bytes());
}
bytes.extend(r.angle.to_le_bytes());
bytes.extend([0xa5, 0x5a]);
bytes
})
.collect()
}
// Four-argument call harness. Seed r4-r12 to check nonzero register preservation.
fn prepare(service: u8, thumb: bool, args: [u32; 4], status: u32, data: &[u8]) -> (Machine, u32) {
let mut code: Vec<u32> = (0..4).map(|r| 0xe59f_0044 | r << 12).collect();
for r in 4..=12 {
code.push(0xe3a0_0040 | r << 12 | r);
}
code.extend([
0xe321_f000 | status,
0xe328_f480, // MSR CPSR_f,#N
if thumb { 0xe28f_e001 } else { 0xe1a0_e00f },
if thumb { 0xe12f_ff1e } else { 0xe1a0_0000 },
if thumb {
0xe7fe_df00 | u32::from(service)
} else {
0xef00_0000 | u32::from(service) << 16
},
0xeaff_fffe,
]);
code.extend(args);
let mut rom = words(&code);
assert_eq!(rom.len() as u32 + ROM_START, DATA);
rom.extend(data);
let mut m = bios::boot(rom).unwrap();
reach(&mut m, ROM_START + 68, 100);
(m, ROM_START + if thumb { 70 } else { 72 })
}
fn complete(m: &mut Machine, pc: u32) {
let before = m.cpu().clone();
m.memory_mut()
.write32(bios::IRQ_STACK, 0x1234_5678)
.unwrap();
m.step().unwrap();
reach(m, pc, 3_000_000);
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
assert_eq!(m.memory().read32(bios::IRQ_STACK).unwrap(), 0x1234_5678);
}
// Independent floating-point table generation and wide-integer matrix arithmetic.
fn coefficients(scale: [i16; 2], angle: u16) -> [i16; 4] {
let phase = usize::from(angle >> 8);
let sine = |index: usize| {
((index as f64 * std::f64::consts::TAU / 256.0).sin() * 16384.0).trunc() as i64
};
let s = sine(phase);
let c = sine((phase + 64) % 256);
let convert = |scale: i16, v: i64| (i64::from(scale) * v).div_euclid(16384) as i16;
[
convert(scale[0], c),
convert(scale[0], s),
convert(scale[1], s),
convert(scale[1], c),
]
}
fn bg_reference(r: Bg) -> Vec<u8> {
let [pa, sxsin, pc, pd] = coefficients(r.scale, r.angle);
let x = i64::from(r.texture[0]) - i64::from(pa) * i64::from(r.screen[0])
+ i64::from(sxsin) * i64::from(r.screen[1]);
let y = i64::from(r.texture[1])
- i64::from(pc) * i64::from(r.screen[0])
- i64::from(pd) * i64::from(r.screen[1]);
let mut bytes: Vec<_> = [pa, sxsin.wrapping_neg(), pc, pd]
.into_iter()
.flat_map(i16::to_le_bytes)
.collect();
bytes.extend((x as u32).to_le_bytes());
bytes.extend((y as u32).to_le_bytes());
bytes
}
fn verify_bg(records: &[Bg], thumb: bool, dest: u32) {
let (mut m, pc) = prepare(
0x0e,
thumb,
[DATA, dest, records.len() as u32, 0xdead_beef],
0x1f,
&bg_bytes(records),
);
let end = dest + records.len() as u32 * 16;
for address in [dest - 4, end] {
m.memory_mut().write32(address, 0xa55a_1234).unwrap();
}
complete(&mut m, pc);
let expected: Vec<_> = records.iter().flat_map(|r| bg_reference(*r)).collect();
for (i, byte) in expected.iter().enumerate() {
assert_eq!(
m.memory().read8(dest + i as u32).unwrap(),
*byte,
"byte={i}"
);
}
for address in [dest - 4, end] {
assert_eq!(m.memory().read32(address).unwrap(), 0xa55a_1234);
}
}
fn verify_obj(records: &[Obj], thumb: bool, dest: u32, stride: u32) {
let data = obj_bytes(records);
let span = records.len() as u32 * stride * 4;
let (mut m, pc) = prepare(
0x0f,
thumb,
[DATA, dest, records.len() as u32, stride],
0x1f,
&data,
);
let mut expected = vec![0xa55au16; span as usize / 2];
for (i, r) in records.iter().enumerate() {
let [pa, sxsin, pc, pd] = coefficients(r.scale, r.angle);
for (j, value) in [pa, sxsin.wrapping_neg(), pc, pd].into_iter().enumerate() {
expected[(i * 4 + j) * stride as usize / 2] = value as u16;
}
}
for offset in (0..span).step_by(2) {
m.memory_mut().write16(dest + offset, 0xa55a).unwrap();
}
for address in [dest - 2, dest + span] {
m.memory_mut().write16(address, 0x1357).unwrap();
}
complete(&mut m, pc);
for (i, value) in expected.iter().enumerate() {
assert_eq!(
m.memory().read16(dest + i as u32 * 2).unwrap(),
*value,
"halfword={i}"
);
}
for address in [dest - 2, dest + span] {
assert_eq!(m.memory().read16(address).unwrap(), 0x1357);
}
}
fn fail(m: &mut Machine) -> MachineError {
for _ in 0..3000 {
let before = m.cpu().clone();
let cycles = m.cycles();
let timing = m.last_timing();
if let Err(error) = m.step() {
assert_eq!(m.cpu(), &before);
assert_eq!(m.cycles(), cycles);
assert_eq!(m.last_timing(), timing);
assert_eq!(m.step(), Err(error.clone()));
return error;
}
}
panic!("invalid affine call did not produce a bounded diagnostic");
}
fn assert_invalid(error: MachineError) {
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: INVALID_ARGUMENT_TRAP,
..
})
));
}
#[test]
fn background_identity_translates_texture_and_display_centers() {
let r = Bg {
texture: [0x12345, -0x12345],
screen: [12, -34],
scale: [256, 256],
angle: 0,
};
let expected = bg_reference(r);
assert_eq!(&expected[..8], &[0, 1, 0, 0, 0, 0, 0, 1]);
for thumb in [false, true] {
verify_bg(&[r], thumb, DEST);
}
}
#[test]
fn object_cardinal_rotations_write_packed_and_oam_matrices() {
let records: Vec<_> = [0, 0x4000, 0x8000, 0xc000]
.into_iter()
.map(|angle| Obj {
scale: [256, 512],
angle,
})
.collect();
for thumb in [false, true] {
verify_obj(&records, thumb, DEST, 2);
verify_obj(&records, thumb, OAM + 6, 8);
}
}
#[test]
fn every_angle_phase_matches_independent_matrix_and_origin_references() {
let records: Vec<_> = (0..256u16)
.map(|phase| Bg {
texture: [0x123456, -0x234567],
screen: [-123, 234],
scale: [257, -513],
angle: phase << 8,
})
.collect();
verify_bg(&records, false, DEST);
let objects: Vec<_> = records
.iter()
.map(|r| Obj {
scale: r.scale,
angle: r.angle,
})
.collect();
verify_obj(&objects, true, DEST, 8);
}
#[test]
fn low_angle_byte_and_record_padding_do_not_change_results() {
let records: Vec<_> = (0..256u16)
.map(|low| Bg {
texture: [-12345, 98765],
screen: [45, -67],
scale: [319, 711],
angle: 0x2300 | low,
})
.collect();
let expected = bg_reference(records[0]);
assert!(records.iter().all(|r| bg_reference(*r) == expected));
verify_bg(&records, true, DEST);
let objects: Vec<_> = records
.iter()
.map(|r| Obj {
scale: r.scale,
angle: r.angle,
})
.collect();
verify_obj(&objects, false, DEST, 2);
}
#[test]
fn signed_scales_zero_reflections_and_halfword_wrap_match_reference() {
let mut records = Vec::new();
for sx in [-32768, -32767, -257, -1, 0, 1, 127, 256, 32767] {
for sy in [-32768, -1, 0, 1, 256, 32767] {
for angle in [0, 0x100, 0x4000, 0x8000, 0xc000, 0xff00] {
records.push(Bg {
texture: [i32::MIN, i32::MAX],
screen: [i16::MIN, i16::MAX],
scale: [sx, sy],
angle,
});
}
}
}
verify_bg(&records, false, DEST);
let objects: Vec<_> = records
.iter()
.map(|r| Obj {
scale: r.scale,
angle: r.angle,
})
.collect();
verify_obj(&objects, true, DEST, 2);
}
#[test]
fn pb_negation_occurs_after_signed_rounding() {
assert_eq!(coefficients([1, 1], 0x100), [0, 0, 0, 0]);
assert_eq!(coefficients([-1, -1], 0x100), [-1, -1, -1, -1]);
let records = [
Obj {
scale: [1, 1],
angle: 0x100,
},
Obj {
scale: [-1, -1],
angle: 0x100,
},
];
verify_obj(&records, false, DEST, 2); // PB must be 0 then +1, not -1 then 0.
}
#[test]
fn seeded_origins_and_matrices_match_wide_integer_wraparound() {
let mut state = 0x817b_53e9u32;
let mut next = || {
state = state.wrapping_mul(1664525).wrapping_add(1013904223);
state
};
let records: Vec<_> = (0..512)
.map(|_| Bg {
texture: [next() as i32, next() as i32],
screen: [next() as i16, next() as i16],
scale: [next() as i16, next() as i16],
angle: next() as u16,
})
.collect();
verify_bg(&records, true, DEST);
}
#[test]
fn custom_even_strides_preserve_gaps_between_coefficients_and_records() {
let records = [
Obj {
scale: [256, 128],
angle: 0x1200,
},
Obj {
scale: [-257, 513],
angle: 0xa500,
},
Obj {
scale: [0, -1],
angle: 0xffff,
},
];
for stride in [2, 4, 6, 8, 16, 32, 1024] {
verify_obj(&records, false, DEST, stride);
}
}
#[test]
fn zero_count_skips_all_buffer_and_stride_validation() {
for service in [0x0e, 0x0f] {
let (mut m, pc) = prepare(service, true, [1, 0xffff_ffff, 0, 1], 0x1f, &[]);
complete(&mut m, pc);
}
}
#[test]
fn alignment_and_protected_sources_are_rejected_before_output() {
for service in [0x0e, 0x0f] {
for source in [0, 0x3ffc, 0x0100_0000, DATA + 1, DATA + 3] {
let (mut m, _) = prepare(service, false, [source, DEST, 1, 2], 0x1f, &[0; 20]);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
let (mut m, _) = prepare(service, false, [DATA, DEST + 1, 1, 2], 0x1f, &[0; 20]);
assert_invalid(fail(&mut m));
}
for (source, dest) in [(DATA + 2, DEST), (DATA, DEST + 2)] {
let (mut m, _) = prepare(0x0e, false, [source, dest, 1, 2], 0x1f, &[0; 24]);
assert_invalid(fail(&mut m));
}
let mut data = vec![0, 0];
data.extend(obj_bytes(&[Obj {
scale: [256, 256],
angle: 0,
}]));
let (mut m, pc) = prepare(0x0f, false, [DATA + 2, DEST + 2, 1, 2], 0x1f, &data);
complete(&mut m, pc); // OBJ only requires halfword alignment.
assert_eq!(m.memory().read16(DEST + 2).unwrap(), 256);
}
#[test]
fn invalid_stride_batch_sizes_and_wrapping_ranges_are_diagnostics() {
for stride in [0, 1, 3, 0x4000_0000, 0xffff_fffe] {
let (mut m, _) = prepare(0x0f, false, [DATA, DEST, 1, stride], 0x1f, &[]);
assert_invalid(fail(&mut m));
}
for service in [0x0e, 0x0f] {
for (source, dest, count, stride) in [
(DATA, DEST, u32::MAX, 2),
(0xffff_fff8, DEST, 1, 2),
(DATA, 0xffff_fff8, 1, 2),
(DATA, DEST, 0x1000_0000, 32),
] {
let (mut m, _) = prepare(service, false, [source, dest, count, stride], 0x1f, &[]);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
}
}
#[test]
fn last_record_padding_need_not_be_mapped() {
for service in [0x0e, 0x0f] {
let mut data = if service == 0x0e {
bg_bytes(&[Bg {
texture: [0, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}])
} else {
obj_bytes(&[Obj {
scale: [256, 256],
angle: 0,
}])
};
data.truncate(data.len() - 2);
let (mut m, pc) = prepare(service, false, [DATA, DEST, 1, 2], 0x1f, &data);
complete(&mut m, pc);
assert_eq!(m.memory().read16(DEST).unwrap(), 256);
}
}
#[test]
fn later_source_failure_preserves_first_record_but_not_partial_new_output() {
for service in [0x0e, 0x0f] {
let mut data = if service == 0x0e {
bg_bytes(
&[Bg {
texture: [0, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}; 2],
)
} else {
obj_bytes(
&[Obj {
scale: [256, 256],
angle: 0,
}; 2],
)
};
data.truncate(data.len() - 3); // Missing last angle byte in second record.
let span = if service == 0x0e { 16 } else { 8 };
let (mut m, _) = prepare(service, false, [DATA, DEST, 2, 2], 0x1f, &data);
m.memory_mut().write32(DEST + span, 0xcccc_cccc).unwrap();
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::Unmapped(DATA + data.len() as u32).into())
);
assert_eq!(m.memory().read16(DEST).unwrap(), 256);
assert_eq!(m.memory().read16(DEST + 6).unwrap(), 256);
assert_eq!(m.memory().read32(DEST + span).unwrap(), 0xcccc_cccc);
}
}
#[test]
fn destination_failure_keeps_preceding_halfword_stores() {
for service in [0x0e, 0x0f] {
let data = if service == 0x0e {
bg_bytes(&[Bg {
texture: [0, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}])
} else {
obj_bytes(&[Obj {
scale: [256, 256],
angle: 0,
}])
};
let (mut m, _) = prepare(service, false, [DATA, 0x07ff_fffc, 1, 2], 0x1f, &data);
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into())
);
assert_eq!(m.memory().read32(0x07ff_fffc).unwrap(), 256);
}
}
#[test]
fn both_work_ram_regions_can_supply_records() {
for service in [0x0e, 0x0f] {
let data = if service == 0x0e {
bg_bytes(&[Bg {
texture: [0, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}])
} else {
obj_bytes(&[Obj {
scale: [256, 256],
angle: 0,
}])
};
for source in [SOURCE, 0x0300_0100] {
let (mut m, pc) = prepare(service, true, [source, DEST, 1, 2], 0x1f, &[]);
for (i, byte) in data.iter().enumerate() {
m.memory_mut().write8(source + i as u32, *byte).unwrap();
}
complete(&mut m, pc);
assert_eq!(m.memory().read16(DEST).unwrap(), 256);
assert_eq!(m.memory().read16(DEST + 6).unwrap(), 256);
}
}
}
#[test]
fn user_system_status_masks_and_nonzero_caller_registers_are_preserved() {
for service in [0x0e, 0x0f] {
let data = if service == 0x0e {
bg_bytes(&[Bg {
texture: [-12345, 67890],
screen: [-123, 321],
scale: [-257, 519],
angle: 0xa501,
}])
} else {
obj_bytes(&[Obj {
scale: [-257, 519],
angle: 0xa501,
}])
};
for thumb in [false, true] {
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
let (mut m, pc) = prepare(service, thumb, [DATA, DEST, 1, 8], status, &data);
complete(&mut m, pc);
}
}
}
}
#[test]
fn background_service_can_write_live_affine_registers_and_render() {
let data = bg_bytes(&[Bg {
texture: [256, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}]);
let (mut m, pc) = prepare(0x0e, false, [DATA, BG2PA, 1, 0], 0x1f, &data);
m.memory_mut().write16(DISPCNT, 0x0403).unwrap();
m.memory_mut().write16(VRAM_START, 31).unwrap();
m.memory_mut().write16(VRAM_START + 2, 0x3e0).unwrap();
complete(&mut m, pc);
// Affine registers are write-only; verify their effect through rendering.
let mut f = Framebuffer::default();
m.memory().render_frame(&mut f).unwrap();
assert_eq!(f.pixels()[0], rgb(0x3e0));
}
#[test]
fn object_service_writes_oam_matrix_without_changing_sprite_attributes() {
let data = obj_bytes(&[Obj {
scale: [-256, 256],
angle: 0,
}]);
let (mut m, pc) = prepare(0x0f, true, [DATA, OAM + 6, 1, 8], 0x1f, &data);
m.memory_mut().write16(DISPCNT, 0x1040).unwrap();
m.memory_mut().write16(PAL, 0x7c00).unwrap();
m.memory_mut().write16(PAL + 0x202, 31).unwrap();
m.memory_mut().write16(PAL + 0x204, 0x3e0).unwrap();
for i in 0..128 {
m.memory_mut().write16(OAM + i * 8, 0x200).unwrap();
}
m.memory_mut().write16(OAM, 0x100).unwrap();
for y in 0..8 {
for x in (0..8).step_by(4) {
m.memory_mut()
.write16(
VRAM_START + 0x10000 + y * 4 + x / 2,
if x == 0 { 0x1111 } else { 0x2222 },
)
.unwrap();
}
}
complete(&mut m, pc);
assert_eq!(m.memory().read16(OAM).unwrap(), 0x100);
assert_eq!(m.memory().read16(OAM + 8).unwrap(), 0x200);
let mut f = Framebuffer::default();
m.memory().render_frame(&mut f).unwrap();
assert_eq!(f.pixels()[1], rgb(0x3e0));
assert_eq!(f.pixels()[7], rgb(31));
}
#[test]
fn output_uses_normal_halfword_and_word_video_bus_writes() {
verify_bg(
&[Bg {
texture: [0x1234, -0x1234],
screen: [0, 0],
scale: [256, 512],
angle: 0,
}],
false,
VRAM_START + 4,
);
verify_obj(
&[Obj {
scale: [256, 512],
angle: 0x1234,
}],
true,
PAL + 4,
2,
);
}
#[test]
fn dma_and_timers_progress_while_services_defer_irq_delivery() {
for service in [0x0e, 0x0f] {
let data = if service == 0x0e {
bg_bytes(&[Bg {
texture: [0, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}])
} else {
obj_bytes(&[Obj {
scale: [256, 256],
angle: 0,
}])
};
let (mut m, pc) = prepare(service, false, [DATA, DEST, 1, 2], 0x1f, &data);
m.step().unwrap();
m.memory_mut().write16(IE, 8).unwrap();
m.memory_mut().write16(IME, 1).unwrap();
m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap();
m.memory_mut().write32(SOURCE, 0x8765_4321).unwrap();
m.memory_mut().write32(DMA_BASE, SOURCE).unwrap();
m.memory_mut().write32(DMA_BASE + 4, SOURCE + 4).unwrap();
m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap();
let mut dma = 0;
for _ in 0..1000 {
if m.cpu().pc() == pc {
break;
}
let step = m.step().unwrap();
assert_ne!(step, StepKind::IrqEntry);
if matches!(step, StepKind::Dma { channel: 0 }) {
dma += 1;
}
}
assert_eq!(m.cpu().pc(), pc);
assert_eq!(dma, 1);
assert_eq!(m.memory().read32(SOURCE + 4).unwrap(), 0x8765_4321);
assert_eq!(m.memory().read16(IF).unwrap() & 8, 8);
assert_eq!(m.memory().read16(IME).unwrap(), 1);
assert_eq!(m.memory().read16(IRQ_FLAGS).unwrap(), 0);
assert_eq!(m.memory().read16(DEST).unwrap(), 256);
assert_eq!(m.step().unwrap(), StepKind::IrqEntry);
}
}

View file

@ -0,0 +1,377 @@
use super::*;
use gba_core::{bios::INVALID_ARGUMENT_TRAP, dma::DMA_BASE};
/// Wide-integer reference for signed low-word multiplication and floor division.
/// This avoids both host signed overflow and the emitted instruction sequence.
fn low_signed(value: i64) -> i64 {
let bits = value.rem_euclid(1i64 << 32);
if bits >= 1i64 << 31 {
bits - (1i64 << 32)
} else {
bits
}
}
fn atan_reference(tangent: i32) -> i32 {
let a = -low_signed(i64::from(tangent).pow(2)).div_euclid(16384);
let mut polynomial = 0xa9i64;
for coefficient in [0x390, 0x91c, 0xfb6, 0x16aa, 0x2081, 0x3651, 0xa2f9] {
polynomial = low_signed(low_signed(polynomial * a).div_euclid(16384) + coefficient);
}
low_signed(i64::from(tangent) * polynomial).div_euclid(65536) as i32
}
/// Eight-sector signed-coordinate reference; production uses absolute magnitudes.
fn atan2_reference(x: i32, y: i32) -> u32 {
if y == 0 {
return if x < 0 { 0x8000 } else { 0 };
}
if x == 0 {
return if y < 0 { 0xc000 } else { 0x4000 };
}
let ratio = |n: i32, d: i32| atan_reference((i64::from(n) * 16384 / i64::from(d)) as i32);
let angle = if y > 0 {
if x > 0 && x >= y {
ratio(y, x)
} else if x < 0 && -x >= y {
0x8000 + ratio(y, x)
} else {
0x4000 - ratio(x, y)
}
} else if x < 0 && -x > -y {
0x8000 + ratio(y, x)
} else if x > 0 && x >= -y {
0x10000 + ratio(y, x)
} else {
0xc000 - ratio(x, y)
};
(angle & 0xffff) as u32
}
fn run(service: u8, x: i32, y: i32, thumb: bool) -> u32 {
let (mut m, pc) = call(service, thumb, [x as u32, y as u32, 0xcafe_babe]);
let before = m.cpu().clone();
let guard = bios::SVC_STACK - if service == 9 { 32 } else { 44 };
m.memory_mut().write32(guard, 0xa55a_1234).unwrap();
finish(&mut m, pc);
assert_eq!(&m.cpu().registers()[1..15], &before.registers()[1..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
assert_eq!(m.memory().read32(guard).unwrap(), 0xa55a_1234);
let actual = m.cpu().registers()[0];
let expected = if service == 9 {
atan_reference(x) as u32
} else {
atan2_reference(x, y)
};
assert_eq!(actual, expected, "service={service}, x={x}, y={y}");
actual
}
fn check_direction(x: i32, y: i32, thumb: bool) {
let actual = run(0x0a, x, y, thumb);
if x == 0 && y == 0 {
return;
}
let ideal = f64::from(y).atan2(f64::from(x)) * 32768.0 / std::f64::consts::PI;
let error = (f64::from(actual) - ideal + 32768.0).rem_euclid(65536.0) - 32768.0;
assert!(
error.abs() < 3.0,
"({x},{y}) actual={actual} ideal={ideal} error={error}"
);
}
#[test]
fn arctan_known_fixed_point_values_include_signed_results() {
for (input, expected) in [
(-16384, -8192),
(-8192, -4836),
(-1, -1),
(0, 0),
(1, 0),
(8192, 4836),
(16384, 8192),
] {
for thumb in [false, true] {
assert_eq!(run(9, input, 0x12345678, thumb) as i32, expected);
}
}
}
#[test]
fn arctan_dense_unit_interval_grid_matches_polynomial_and_real_angle() {
for input in (-16384..=16384).step_by(16) {
let actual = run(9, input, 0, input & 16 != 0) as i32;
let ideal = (f64::from(input) / 16384.0).atan() * 32768.0 / std::f64::consts::PI;
assert!((f64::from(actual) - ideal).abs() < 2.0, "input={input}");
}
}
#[test]
fn arctan_full_signed_domain_uses_wrapped_products_not_host_float() {
for input in (-32768..=32767).step_by(257) {
run(9, input, 0, input & 1 != 0);
}
// Known out-of-unit-range accuracy problems are retained, not corrected with host atan.
assert_eq!(run(9, -32768, 0, false) as i32, -5795);
assert_eq!(run(9, 32767, 0, true), 5848);
}
#[test]
fn arctan_seeded_inputs_match_wide_integer_reference() {
let mut seed = 0x9a2b_14cdu32;
for index in 0..1024 {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
run(9, i32::from((seed >> 16) as i16), 0, index % 2 == 0);
}
}
#[test]
fn negative_arctan_rounds_down_instead_of_truncating_toward_zero() {
for input in [1, 2, 3, 127, 8191, 8192, 16383, 16384] {
let positive = run(9, input, 0, false) as i32;
let negative = run(9, -input, 0, true) as i32;
assert!([0, -1].contains(&(positive + negative)));
}
assert_eq!(run(9, -1, 0, true), u32::MAX);
}
#[test]
fn arctan2_axes_origin_and_diagonals_have_exact_turn_angles() {
for thumb in [false, true] {
for (x, y, expected) in [
(0, 0, 0),
(1, 0, 0),
(-32768, 0, 0x8000),
(0, 32767, 0x4000),
(0, -32768, 0xc000),
(1, 1, 0x2000),
(-1, 1, 0x6000),
(-1, -1, 0xa000),
(1, -1, 0xe000),
(-32768, -32768, 0xa000),
(32767, 32767, 0x2000),
] {
assert_eq!(run(0x0a, x, y, thumb), expected);
}
}
}
#[test]
fn arctan2_all_quadrants_and_signed_extremes_match_both_references() {
let values = [
-32768, -32767, -16384, -1024, -2, -1, 0, 1, 2, 1024, 16384, 32766, 32767,
];
for x in values {
for y in values {
check_direction(x, y, (x ^ y) & 1 != 0);
}
}
}
#[test]
fn arctan2_sector_boundaries_handle_equal_and_adjacent_magnitudes() {
for magnitude in [1, 2, 3, 127, 1024, 16384, 32766] {
for delta in [-1, 0, 1] {
for sx in [-1, 1] {
for sy in [-1, 1] {
check_direction(sx * magnitude, sy * (magnitude + delta), delta != 0);
}
}
}
}
}
#[test]
fn arctan2_seeded_signed_coordinates_match_integer_and_float_references() {
let mut seed = 0x5017_e3b9u32;
for index in 0..1024 {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
let x = i32::from((seed >> 16) as i16);
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
let y = i32::from((seed >> 16) as i16);
check_direction(x, y, index % 2 != 0);
}
}
#[test]
fn arctan2_preserves_direction_when_coordinates_scale_equally() {
for (x, y) in [
(3, 7),
(7, 3),
(-3, 7),
(-7, 3),
(-3, -7),
(-7, -3),
(3, -7),
(7, -3),
] {
let expected = run(0x0a, x, y, false);
for scale in [2, 7, 31, 4096] {
assert_eq!(run(0x0a, x * scale, y * scale, true), expected);
}
}
}
#[test]
fn user_system_modes_masks_and_all_nonresult_registers_are_preserved() {
for service in [9, 0x0a] {
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
for thumb in [false, true] {
let (mut m, pc) = call_status(
service,
thumb,
[(-1234i32) as u32, 2345, 0xabcd_ef01],
status,
);
let before = m.cpu().clone();
finish(&mut m, pc);
assert_eq!(&m.cpu().registers()[1..15], &before.registers()[1..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
}
}
}
}
#[test]
fn every_caller_flag_combination_survives_angle_calculation() {
for service in [9, 0x0au32] {
for flags in 0..16u32 {
let mut m = bios::boot(words(&[
0xe3a0_0a02, // MOV r0,#0x2000
0xe3a0_1901, // MOV r1,#0x4000
0xe328_f200 | flags, // MSR CPSR_f,#flags<<28
0xef00_0000 | service << 16,
0xeaff_fffe,
]))
.unwrap();
reach(&mut m, ROM_START + 12, 100);
assert_eq!(m.cpu().cpsr() >> 28, flags);
let before = m.cpu().clone();
finish(&mut m, ROM_START + 16);
assert_eq!(m.cpu().cpsr(), before.cpsr());
assert_eq!(&m.cpu().registers()[1..15], &before.registers()[1..15]);
assert_eq!(
m.cpu().registers()[0],
if service == 9 {
atan_reference(8192) as u32
} else {
atan2_reference(8192, 16384)
}
);
}
}
}
#[test]
fn noncanonical_signed_sixteen_bit_arguments_are_repeatable_diagnostics() {
for value in [
0x8000,
0xffff,
0x10000,
0xffff_7fff,
0x7fff_ffff,
0x8000_0000,
] {
for (service, args) in [
(9, [value, 0, 0]),
(0x0a, [value, 1, 0]),
(0x0a, [1, value, 0]),
] {
let (mut m, _) = call(service, false, args);
let mut failed = false;
for _ in 0..150 {
let before = m.cpu().clone();
let cycles = m.cycles();
let timing = m.last_timing();
if let Err(error) = m.step() {
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: INVALID_ARGUMENT_TRAP,
..
})
));
assert_eq!(m.cpu(), &before);
assert_eq!(m.cycles(), cycles);
assert_eq!(m.last_timing(), timing);
assert_eq!(m.step(), Err(error));
failed = true;
break;
}
}
assert!(failed);
}
}
}
#[test]
fn repeated_calls_restore_supervisor_stack_and_do_not_retain_ratio_state() {
for service in [9u32, 0x0a] {
let mut m = bios::boot(words(&[
0xe3a0_0a02, // MOV r0,#0x2000
0xe3a0_1901, // MOV r1,#0x4000
0xef00_0000 | service << 16,
0xeaff_fffb, // B ROM_START
]))
.unwrap();
reach(&mut m, ROM_START, 100);
for _ in 0..100 {
reach(&mut m, ROM_START + 8, 10);
m.step().unwrap();
assert_eq!(m.cpu().registers()[13], bios::SVC_STACK);
reach(&mut m, ROM_START + 12, 1000);
assert_eq!(m.cpu().registers()[13], SYSTEM_STACK);
assert_eq!(
m.cpu().registers()[0],
if service == 9 {
atan_reference(8192) as u32
} else {
atan2_reference(8192, 16384)
}
);
m.step().unwrap();
assert_eq!(m.cpu().pc(), ROM_START);
}
}
}
#[test]
fn devices_and_dma_progress_and_pending_irq_waits_until_return() {
for service in [9, 0x0a] {
let (mut m, pc) = call(service, false, [8192, 16384, 0]);
m.step().unwrap(); // Enter SWI before enabling IRQ sources.
m.memory_mut().write16(IE, 8).unwrap();
m.memory_mut().write16(IME, 1).unwrap();
m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap();
m.memory_mut().write32(SOURCE, 0x8765_4321).unwrap();
m.memory_mut().write32(DMA_BASE, SOURCE).unwrap();
m.memory_mut().write32(DMA_BASE + 4, SOURCE + 4).unwrap();
m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap();
let mut dma = 0;
for _ in 0..1000 {
if m.cpu().pc() == pc {
break;
}
let step = m.step().unwrap();
assert_ne!(step, StepKind::IrqEntry);
if matches!(step, StepKind::Dma { channel: 0 }) {
dma += 1;
}
}
assert_eq!(m.cpu().pc(), pc);
assert_eq!(dma, 1);
assert_eq!(m.memory().read32(SOURCE + 4).unwrap(), 0x8765_4321);
assert_eq!(m.memory().read16(IF).unwrap() & 8, 8);
assert_eq!(m.memory().read16(IME).unwrap(), 1);
assert_eq!(m.memory().read16(IRQ_FLAGS).unwrap(), 0);
assert_eq!(
m.cpu().registers()[0],
if service == 9 {
atan_reference(8192) as u32
} else {
atan2_reference(8192, 16384)
}
);
assert_eq!(m.step().unwrap(), StepKind::IrqEntry);
}
}

View file

@ -0,0 +1,191 @@
use super::*;
use gba_core::bios::INVALID_ARGUMENT_TRAP;
fn divide(number: i32, denominator: i32, thumb: bool, service: u8) {
let inputs = if service == 6 {
[number as u32, denominator as u32]
} else {
[denominator as u32, number as u32]
};
let (mut machine, return_pc) = call(service, thumb, [inputs[0], inputs[1], 0xcafe_babe]);
let before = machine.cpu().clone();
finish(&mut machine, return_pc);
// Widening avoids host overflow for INT_MIN / -1.
let quotient = i64::from(number) / i64::from(denominator);
let remainder = i64::from(number) % i64::from(denominator);
assert_eq!(
machine.cpu().registers()[0],
quotient as u32,
"{number}/{denominator}"
);
assert_eq!(machine.cpu().registers()[1], remainder as u32);
assert_eq!(machine.cpu().registers()[3], quotient.unsigned_abs() as u32);
assert_eq!(machine.cpu().registers()[2], before.registers()[2]);
assert_eq!(
&machine.cpu().registers()[4..15],
&before.registers()[4..15]
);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
}
#[test]
fn division_handles_signs_zero_numerator_and_signed_boundaries() {
for service in [6, 7] {
for thumb in [false, true] {
for number in [
i32::MIN,
i32::MIN + 1,
-1234,
-17,
-1,
0,
1,
17,
1234,
i32::MAX,
] {
for denominator in [i32::MIN, -123, -10, -1, 1, 10, 123, i32::MAX] {
divide(number, denominator, thumb, service);
}
}
}
}
}
#[test]
fn division_matches_wide_integer_reference_for_seeded_inputs() {
let mut state = 0x1a2b_3c4d_u32;
for index in 0..1024 {
state = state.wrapping_mul(1664525).wrapping_add(1013904223);
let number = state as i32;
state = state.wrapping_mul(1664525).wrapping_add(1013904223);
let denominator = (state | 1) as i32;
divide(
number,
denominator,
index & 1 != 0,
if index & 2 == 0 { 6 } else { 7 },
);
}
}
#[test]
fn division_by_zero_is_a_repeatable_diagnostic_not_an_endless_loop() {
for service in [6, 7] {
for thumb in [false, true] {
for number in [0, 1, u32::MAX, 0x8000_0000] {
let operands = if service == 6 {
[number, 0, 0]
} else {
[0, number, 0]
};
let (mut machine, _) = call(service, thumb, operands);
let error = (0..100)
.find_map(|_| machine.step().err())
.expect("bounded division diagnostic");
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: INVALID_ARGUMENT_TRAP,
..
})
));
let before = machine.cpu().clone();
let cycles = machine.cycles();
let timing = machine.last_timing();
assert_eq!(machine.step(), Err(error));
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.last_timing(), timing);
}
}
}
}
fn square_root(value: u32, thumb: bool) {
let (mut machine, return_pc) = call(8, thumb, [value, 0x1234_5678, 0xabcd_ef01]);
let before = machine.cpu().clone();
finish(&mut machine, return_pc);
let root = u64::from(machine.cpu().registers()[0]);
assert!(root * root <= u64::from(value));
assert!((root + 1) * (root + 1) > u64::from(value));
assert!(root <= 0xffff);
assert_eq!(
&machine.cpu().registers()[1..15],
&before.registers()[1..15]
);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
}
#[test]
fn sqrt_handles_perfect_squares_adjacent_values_and_u32_maximum() {
for thumb in [false, true] {
for root in [0_u32, 1, 2, 3, 255, 256, 32767, 32768, 65534, 65535] {
let square = root * root;
if square != 0 {
square_root(square - 1, thumb);
}
square_root(square, thumb);
square_root(square + 1, thumb);
}
square_root(u32::MAX, thumb);
}
}
#[test]
fn sqrt_result_satisfies_integer_bounds_for_seeded_inputs() {
let mut state = 0x9876_5432_u32;
for index in 0..512 {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
square_root(state, index & 1 != 0);
}
}
#[test]
fn arithmetic_services_preserve_user_mode_and_interrupt_masks() {
for service in [6, 7, 8] {
for status in [0x10, 0x90, 0x9f, 0xdf] {
for thumb in [false, true] {
let (mut machine, return_pc) =
call_status(service, thumb, [144, 12, 0x55aa], status);
let before = machine.cpu().clone();
finish(&mut machine, return_pc);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
assert_eq!(machine.cpu().registers()[2], before.registers()[2]);
assert_eq!(
&machine.cpu().registers()[4..15],
&before.registers()[4..15]
);
}
}
}
}
#[test]
fn arithmetic_executes_real_instructions_and_keeps_device_clocks_running() {
let (mut machine, return_pc) = call(6, false, [1234, 10, 0]);
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().write16(IME, 1).unwrap();
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_fff0)
.unwrap();
let start = machine.cycles();
let mut steps = 0;
loop {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
steps += 1;
if machine.cpu().pc() == return_pc {
break;
}
assert!(steps < 1000);
}
assert!(steps >= 32 * 7);
assert!(machine.cycles() > start + 16);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
assert_eq!(machine.cpu().registers()[0], 123);
assert_eq!(machine.cpu().registers()[1], 4);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); // Delivery deferred until service return.
}

View file

@ -0,0 +1,418 @@
use super::*;
use gba_core::{
bios::INVALID_ARGUMENT_TRAP,
dma::DMA_BASE,
memory::{OAM_START, PALETTE_START},
};
const INFO: u32 = SOURCE + 0x1000;
fn info(m: &mut Machine, length: u16, source_width: u8, dest_width: u8, offset: u32) {
m.memory_mut().write16(INFO, length).unwrap();
m.memory_mut().write8(INFO + 2, source_width).unwrap();
m.memory_mut().write8(INFO + 3, dest_width).unwrap();
m.memory_mut().write32(INFO + 4, offset).unwrap();
}
fn prepare(
thumb: bool,
destination: u32,
data: &[u8],
sw: u8,
dw: u8,
offset: u32,
) -> (Machine, u32) {
let (mut m, pc) = call_status_data(0x10, thumb, [ROM_DATA, destination, INFO], 0x1f, data);
info(&mut m, data.len().try_into().unwrap(), sw, dw, offset);
(m, pc)
}
/// Expand individual bits and repack words independently of the emitted ARM loops.
fn reference(data: &[u8], sw: u8, dw: u8, offset: u32) -> Vec<u32> {
let mut bits = Vec::new();
for byte in data {
for position in (0..8).step_by(sw as usize) {
let mut value = 0u64;
for bit in 0..sw {
value += u64::from((byte >> (position + bit)) & 1) << bit;
}
if value != 0 || offset & 0x8000_0000 != 0 {
value += u64::from(offset & 0x7fff_ffff);
}
assert!(value < (1u64 << dw));
for bit in 0..dw {
bits.push((value >> bit) & 1);
}
}
}
assert_eq!(bits.len() % 32, 0);
bits.chunks_exact(32)
.map(|chunk| {
chunk
.iter()
.enumerate()
.fold(0, |word, (bit, v)| word | (*v as u32) << bit)
})
.collect()
}
fn complete(m: &mut Machine, pc: u32) {
m.step().unwrap();
reach(m, pc, 3_000_000);
}
fn verify(thumb: bool, dest: u32, data: &[u8], sw: u8, dw: u8, offset: u32) {
let expected = reference(data, sw, dw, offset);
let (mut m, pc) = prepare(thumb, dest, data, sw, dw, offset);
let before = m.cpu().clone();
let end = dest + expected.len() as u32 * 4;
for address in [dest - 4, end, bios::IRQ_STACK] {
m.memory_mut().write32(address, 0xa55a_1234).unwrap();
}
complete(&mut m, pc);
for (index, word) in expected.iter().enumerate() {
assert_eq!(
m.memory().read32(dest + index as u32 * 4).unwrap(),
*word,
"word={index}, sw={sw}, dw={dw}"
);
}
for address in [dest - 4, end, bios::IRQ_STACK] {
assert_eq!(m.memory().read32(address).unwrap(), 0xa55a_1234);
}
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
}
fn fail(m: &mut Machine) -> MachineError {
for _ in 0..3000 {
let before = m.cpu().clone();
let cycles = m.cycles();
let timing = m.last_timing();
if let Err(error) = m.step() {
assert_eq!(m.cpu(), &before);
assert_eq!(m.cycles(), cycles);
assert_eq!(m.last_timing(), timing);
assert_eq!(m.step(), Err(error.clone()));
return error;
}
}
panic!("invalid unpack did not produce a bounded diagnostic");
}
fn assert_invalid(error: MachineError) {
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: INVALID_ARGUMENT_TRAP,
..
})
));
}
#[test]
fn all_supported_width_pairs_and_input_bytes_match_bit_reference() {
let data: Vec<_> = (0..=255).collect();
for sw in [1, 2, 4, 8] {
for dw in [1, 2, 4, 8, 16, 32].into_iter().filter(|dw| *dw >= sw) {
for thumb in [false, true] {
verify(
thumb,
if thumb { VRAM_START + 4 } else { DEST },
&data,
sw,
dw,
0,
);
}
}
}
}
#[test]
fn source_units_and_destination_units_are_least_significant_first() {
let (mut m, pc) = prepare(false, DEST, &[0b1011_0001], 1, 4, 0);
complete(&mut m, pc);
assert_eq!(m.memory().read32(DEST).unwrap(), 0x1011_0001);
let (mut m, pc) = prepare(true, DEST, &[0xe4], 2, 8, 0);
complete(&mut m, pc);
assert_eq!(m.memory().read32(DEST).unwrap(), 0x0302_0100);
}
#[test]
fn offsets_apply_to_nonzero_units_and_optionally_to_zero() {
for zero in [0, 0x8000_0000] {
for offset in [0, 1, 0x1234, 0x7fff_ffff] {
verify(false, DEST, &[0, 1, 0xfe, 0xff], 8, 32, offset | zero);
}
verify(true, VRAM_START + 4, &[0x10, 0x21], 4, 8, 0x80 | zero);
}
}
#[test]
fn seeded_width_offset_and_data_combinations_match_reference() {
let mut seed = 0x615c_ae39u32;
for case in 0..96 {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
let sw = 1u8 << (seed & 3);
let choices: Vec<u8> = [1, 2, 4, 8, 16, 32]
.into_iter()
.filter(|dw| *dw >= sw)
.collect();
let dw = choices[(seed >> 8) as usize % choices.len()];
let capacity = ((1u64 << dw) - (1u64 << sw)).min(0x7fff_ffff);
let bias =
((seed as u64) % (capacity + 1)) as u32 | if case % 2 == 0 { 0x8000_0000 } else { 0 };
let data: Vec<_> = (0..20)
.map(|_| {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
(seed >> 24) as u8
})
.collect();
verify(case % 2 != 0, DEST, &data, sw, dw, bias);
}
}
#[test]
fn maximum_source_byte_count_is_unsigned_and_fully_processed() {
let data: Vec<_> = (0..65535u32).map(|i| i as u8).collect();
// Keep the descriptor in IWRAM: the output fills almost all EWRAM.
let (mut m, pc) = call_status_data(0x10, false, [ROM_DATA, SOURCE, 0x0300_0100], 0x1f, &data);
m.memory_mut().write32(0x0300_0100, 0x2008_ffff).unwrap();
m.memory_mut().write32(0x0300_0104, 0).unwrap();
m.memory_mut()
.write32(SOURCE + 65535 * 4, 0xa55a_1234)
.unwrap();
complete(&mut m, pc);
for (index, byte) in data.iter().enumerate() {
assert_eq!(
m.memory().read32(SOURCE + index as u32 * 4).unwrap(),
u32::from(*byte)
);
}
assert_eq!(m.memory().read32(SOURCE + 65535 * 4).unwrap(), 0xa55a_1234);
}
#[test]
fn byte_aligned_sources_work_in_rom_and_both_ram_regions() {
for source in [
ROM_DATA + 1,
ROM_DATA + 2,
ROM_DATA + 3,
SOURCE + 1,
0x0300_0103,
] {
let mut data = vec![0xcc; 3];
data.extend([0x12, 0x34, 0x56, 0x78]);
let (mut m, pc) = call_status_data(0x10, true, [source, DEST, INFO], 0x1f, &data);
info(&mut m, 4, 8, 8, 0);
let expected = if source >= ROM_DATA {
&data[(source - ROM_DATA) as usize..(source - ROM_DATA) as usize + 4]
} else {
&[0x12, 0x34, 0x56, 0x78]
};
if source < ROM_DATA {
for (index, byte) in expected.iter().enumerate() {
m.memory_mut().write8(source + index as u32, *byte).unwrap();
}
}
complete(&mut m, pc);
assert_eq!(
m.memory().read32(DEST).unwrap(),
u32::from_le_bytes(expected.try_into().unwrap())
);
}
}
#[test]
fn unpack_descriptor_can_be_in_cartridge_rom() {
let data = [4, 0, 8, 8, 0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78];
let (mut m, pc) = call_status_data(0x10, false, [ROM_DATA + 8, DEST, ROM_DATA], 0x1f, &data);
complete(&mut m, pc);
assert_eq!(m.memory().read32(DEST).unwrap(), 0x7856_3412);
}
#[test]
fn caller_registers_status_and_stack_boundaries_are_preserved() {
for thumb in [false, true] {
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
let (mut m, pc) =
call_status_data(0x10, thumb, [ROM_DATA, DEST, INFO], status, &[0x12, 0x34]);
info(&mut m, 2, 4, 8, 0x10);
let before = m.cpu().clone();
m.memory_mut()
.write32(bios::IRQ_STACK, 0x1234_5678)
.unwrap();
complete(&mut m, pc);
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
assert_eq!(m.memory().read32(bios::IRQ_STACK).unwrap(), 0x1234_5678);
}
}
}
#[test]
fn zero_length_does_not_access_source_or_destination_after_validation() {
let (mut m, pc) = call(0x10, false, [0x0e00_0000, 0x0e00_0000, INFO]);
info(&mut m, 0, 1, 32, 0);
complete(&mut m, pc);
}
#[test]
fn invalid_widths_and_narrowing_produce_diagnostics() {
for width in 0..=255u8 {
if ![1, 2, 4, 8].contains(&width) {
let (mut m, _) = prepare(false, DEST, &[0; 4], width, 32, 0);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
if ![1, 2, 4, 8, 16, 32].contains(&width) {
let (mut m, _) = prepare(true, DEST, &[0; 4], 1, width, 0);
assert_invalid(fail(&mut m));
}
}
for (sw, dw) in [(2, 1), (4, 1), (4, 2), (8, 1), (8, 2), (8, 4)] {
let (mut m, _) = prepare(false, DEST, &[0; 4], sw, dw, 0);
assert_invalid(fail(&mut m));
}
}
#[test]
fn alignment_protected_source_and_wrapping_ranges_are_rejected() {
for (source, dest, descriptor) in [
(0, DEST, INFO),
(0x3fff, DEST, INFO),
(0x0100_0000, DEST, INFO),
(ROM_DATA, DEST + 1, INFO),
(ROM_DATA, DEST + 2, INFO),
(ROM_DATA, DEST + 3, INFO),
(ROM_DATA, DEST, INFO + 1),
(ROM_DATA, DEST, INFO + 2),
(ROM_DATA, DEST, INFO + 3),
(0xffff_fffe, DEST, INFO),
(ROM_DATA, 0xffff_fffc, INFO),
] {
let (mut m, _) = call_status_data(0x10, false, [source, dest, descriptor], 0x1f, &[1; 4]);
info(&mut m, 4, 8, 32, 0);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
}
#[test]
fn incomplete_output_words_are_rejected_before_source_reads() {
for (length, sw, dw) in [(1, 8, 8), (2, 8, 8), (3, 8, 8), (1, 4, 8), (6, 8, 8)] {
let (mut m, _) = call(0x10, false, [0x0e00_0000, DEST, INFO]);
info(&mut m, length, sw, dw, 0);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
}
#[test]
fn offset_overflow_keeps_completed_words_but_discards_pending_word() {
// First four values fit; a later 255+1 does not fit eight destination bits.
let (mut m, _) = prepare(false, DEST, &[1, 2, 3, 4, 5, 0xff, 7, 8], 8, 8, 1);
m.memory_mut().write32(DEST + 4, 0xcccc_cccc).unwrap();
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0x0504_0302);
assert_eq!(m.memory().read32(DEST + 4).unwrap(), 0xcccc_cccc);
// Offsets on transparent zero units are checked only when the flag requests them.
verify(true, DEST, &[0; 4], 8, 8, 256);
let (mut m, _) = prepare(true, DEST, &[0; 4], 8, 8, 0x8000_0100);
assert_invalid(fail(&mut m));
}
#[test]
fn truncated_descriptors_and_sources_return_memory_diagnostics() {
let descriptor = [4, 0, 8, 8, 0, 0, 0, 0];
for length in 0..8 {
let (mut m, _) = call_status_data(
0x10,
false,
[SOURCE, DEST, ROM_DATA],
0x1f,
&descriptor[..length],
);
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::Unmapped(ROM_DATA + length as u32).into())
);
}
for length in 0..8 {
let (mut m, _) = prepare(false, DEST, &vec![0x12; length], 8, 8, 0);
info(&mut m, 8, 8, 8, 0);
m.memory_mut().write32(DEST, 0xcccc_cccc).unwrap();
m.memory_mut().write32(DEST + 4, 0xcccc_cccc).unwrap();
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::Unmapped(ROM_DATA + length as u32).into())
);
assert_eq!(
m.memory().read32(DEST).unwrap(),
if length >= 4 {
0x1212_1212
} else {
0xcccc_cccc
}
);
assert_eq!(m.memory().read32(DEST + 4).unwrap(), 0xcccc_cccc);
}
}
#[test]
fn destination_failure_preserves_completed_word() {
let (mut m, _) = prepare(
false,
0x07ff_fffc,
&[0x12, 0x34, 0x56, 0x78, 1, 2, 3, 4],
8,
8,
0,
);
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into())
);
assert_eq!(m.memory().read32(0x07ff_fffc).unwrap(), 0x7856_3412);
}
#[test]
fn word_stores_support_palette_oam_and_video_memory() {
for dest in [PALETTE_START + 4, OAM_START + 4, VRAM_START + 4] {
verify(false, dest, &[0xe4, 0x1b], 2, 4, 0);
}
}
#[test]
fn timers_and_dma_continue_while_irq_delivery_is_masked() {
let (mut m, pc) = prepare(false, DEST, &[0x12; 16], 8, 32, 0);
m.step().unwrap(); // Enter SWI before enabling IRQ sources.
m.memory_mut().write16(IE, 8).unwrap();
m.memory_mut().write16(IME, 1).unwrap();
m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap();
m.memory_mut().write32(SOURCE, 0xa55a_1234).unwrap();
m.memory_mut().write32(DMA_BASE, SOURCE).unwrap();
m.memory_mut().write32(DMA_BASE + 4, SOURCE + 4).unwrap();
m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap();
let mut dma = 0;
for _ in 0..5000 {
if m.cpu().pc() == pc {
break;
}
let step = m.step().unwrap();
assert_ne!(step, StepKind::IrqEntry);
if matches!(step, StepKind::Dma { channel: 0 }) {
dma += 1;
}
}
assert_eq!(m.cpu().pc(), pc);
assert_eq!(dma, 1);
assert_eq!(m.memory().read32(SOURCE + 4).unwrap(), 0xa55a_1234);
assert_eq!(m.memory().read16(IF).unwrap() & 8, 8);
assert_eq!(m.memory().read16(IME).unwrap(), 1);
assert_eq!(m.memory().read16(IRQ_FLAGS).unwrap(), 0);
for index in 0..16 {
assert_eq!(m.memory().read32(DEST + index * 4).unwrap(), 0x12);
}
}

View file

@ -0,0 +1,411 @@
use super::*;
use gba_core::{bios::INVALID_ARGUMENT_TRAP, dma::DMA_BASE, memory::OAM_START};
fn filtered(service: u8, samples: &[u16]) -> (Vec<u8>, Vec<u8>) {
let halfword = service == 0x18;
let mask = if halfword { 65535i64 } else { 255 };
let length = samples.len() * if halfword { 2 } else { 1 };
let mut source = ((length as u32) << 8 | if halfword { 0x82 } else { 0x81 })
.to_le_bytes()
.to_vec();
let mut expected = Vec::new();
let mut previous = 0i64;
// Encode differences from original samples, rather than duplicating decoder accumulation.
for &sample in samples {
assert!(i64::from(sample) <= mask);
let difference = ((i64::from(sample) - previous) & mask) as u16;
if halfword {
source.extend(difference.to_le_bytes());
expected.extend(sample.to_le_bytes());
} else {
source.push(difference as u8);
expected.push(sample as u8);
}
previous = i64::from(sample);
}
(source, expected)
}
fn prepare(service: u8, thumb: bool, dest: u32, data: &[u8]) -> (Machine, u32) {
call_status_data(service, thumb, [ROM_DATA, dest, 0x55aa_1234], 0x1f, data)
}
fn complete(m: &mut Machine, pc: u32) {
m.step().unwrap();
reach(m, pc, 4_000_000);
}
fn verify(service: u8, thumb: bool, dest: u32, data: &[u8], expected: &[u8]) {
let (mut m, pc) = prepare(service, thumb, dest, data);
let before = m.cpu().clone();
let end = dest + expected.len() as u32;
for address in [(dest - 1) & !1, end & !1] {
m.memory_mut().write16(address, 0xa55a).unwrap();
}
m.memory_mut()
.write32(bios::IRQ_STACK, 0x5a5a_a5a5)
.unwrap();
complete(&mut m, pc);
for (index, byte) in expected.iter().enumerate() {
assert_eq!(
m.memory().read8(dest + index as u32).unwrap(),
*byte,
"service={service:x}, byte={index}"
);
}
for address in [dest - 1, end] {
assert_eq!(
m.memory().read8(address).unwrap(),
if address & 1 == 0 { 0x5a } else { 0xa5 }
);
}
assert_eq!(m.memory().read32(bios::IRQ_STACK).unwrap(), 0x5a5a_a5a5);
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
}
fn fail(m: &mut Machine) -> MachineError {
for _ in 0..2000 {
let before = m.cpu().clone();
let cycles = m.cycles();
let timing = m.last_timing();
if let Err(error) = m.step() {
assert_eq!(m.cpu(), &before);
assert_eq!(m.cycles(), cycles);
assert_eq!(m.last_timing(), timing);
assert_eq!(m.step(), Err(error.clone()));
return error;
}
}
panic!("invalid filter did not produce a bounded diagnostic");
}
fn assert_invalid(error: MachineError) {
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: INVALID_ARGUMENT_TRAP,
..
})
));
}
#[test]
fn first_unit_is_original_and_following_units_are_deltas() {
for service in [0x16, 0x17, 0x18] {
let (data, expected) = filtered(service, &[10, 11, 12, 13, 14, 15]);
if service != 0x18 {
assert_eq!(&data[4..], &[10, 1, 1, 1, 1, 1]);
}
for thumb in [false, true] {
verify(
service,
thumb,
if service == 0x16 {
DEST
} else {
VRAM_START + 4
},
&data,
&expected,
);
}
}
}
#[test]
fn byte_filters_cover_every_value_and_wrap_in_both_directions() {
let mut samples: Vec<_> = (0..=255u16).collect();
samples.extend((0..=255u16).rev());
samples.extend([255, 0, 255, 128, 0, 127, 0, 0]);
for service in [0x16, 0x17] {
let (data, expected) = filtered(service, &samples);
for thumb in [false, true] {
verify(
service,
thumb,
if service == 0x16 {
DEST + 1
} else {
VRAM_START + 4
},
&data,
&expected,
);
}
}
}
#[test]
fn halfword_filter_covers_every_sample_and_uses_byte_length() {
let samples: Vec<_> = (0..=65535u32)
.map(|i| i.wrapping_mul(40503) as u16)
.collect();
let (data, expected) = filtered(0x18, &samples);
assert_eq!(&data[..4], &[0x82, 0, 0, 2]); // 131072 bytes, not 65536 units.
verify(0x18, true, DEST, &data, &expected);
}
#[test]
fn modular_accumulation_survives_full_register_wrap_and_large_byte_lengths() {
for (service, length) in [(0x16, 65536usize), (0x17, 65536), (0x18, 65538 * 2)] {
let mut data = ((length as u32) << 8 | if service == 0x18 { 0x82 } else { 0x81 })
.to_le_bytes()
.to_vec();
data.extend(std::iter::repeat_n(255, length));
let expected: Vec<u8> = if service == 0x18 {
(1..=length / 2)
.flat_map(|i| (((i as u64 * 65535) % 65536) as u16).to_le_bytes())
.collect()
} else {
(1..=length)
.map(|i| ((i as u64 * 255) % 256) as u8)
.collect()
};
verify(service, false, DEST, &data, &expected);
}
}
#[test]
fn seeded_original_samples_round_trip_through_all_filters() {
let mut seed = 0x415c_a927u32;
for case in 0..64 {
for service in [0x16, 0x17, 0x18] {
let samples: Vec<_> = (0..(case + 1) * 2)
.map(|_| {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
(seed >> 16) as u16 & if service == 0x18 { 65535 } else { 255 }
})
.collect();
let (data, expected) = filtered(service, &samples);
verify(service, case % 2 == 0, DEST, &data, &expected);
}
}
}
#[test]
fn smallest_outputs_and_odd_byte_output_use_exact_boundaries() {
for samples in [vec![0x12], vec![0x12, 0x34, 0x56]] {
let (data, expected) = filtered(0x16, &samples);
verify(0x16, true, DEST + 1, &data, &expected);
}
let (data, expected) = filtered(0x17, &[0x12, 0x34]);
verify(0x17, false, DEST, &data, &expected);
let (data, expected) = filtered(0x18, &[0xabcd]);
verify(0x18, true, VRAM_START + 4, &data, &expected);
}
#[test]
fn sources_can_be_read_from_either_work_ram_region() {
for service in [0x16, 0x17, 0x18] {
let (data, expected) = filtered(service, &[0, 255, 128, 1]);
for source in [SOURCE, 0x0300_0100] {
let (mut m, pc) = call(service, false, [source, DEST, 0]);
for (index, byte) in data.iter().enumerate() {
m.memory_mut().write8(source + index as u32, *byte).unwrap();
}
complete(&mut m, pc);
for (index, byte) in expected.iter().enumerate() {
assert_eq!(m.memory().read8(DEST + index as u32).unwrap(), *byte);
}
}
}
}
#[test]
fn caller_status_masks_and_registers_are_restored() {
for service in [0x16, 0x17, 0x18] {
let (data, _) = filtered(service, &[0x12, 0x34, 0xff, 0]);
for thumb in [false, true] {
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
let (mut m, pc) =
call_status_data(service, thumb, [ROM_DATA, DEST, 0xa55a_1234], status, &data);
let before = m.cpu().clone();
complete(&mut m, pc);
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
}
}
}
}
#[test]
fn zero_length_reads_no_payload_and_does_not_access_destination() {
for service in [0x16, 0x17, 0x18] {
let (data, _) = filtered(service, &[]);
let (mut m, pc) = prepare(service, false, 0x0e00_0000, &data);
complete(&mut m, pc);
}
}
#[test]
fn all_incorrect_type_or_unit_headers_are_rejected_before_output() {
for service in [0x16, 0x17, 0x18] {
for header in 0u8..=255 {
if header == if service == 0x18 { 0x82 } else { 0x81 } {
continue;
}
let (mut m, _) = prepare(service, false, DEST, &[header, 4, 0, 0]);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
}
}
#[test]
fn protected_sources_alignment_odd_lengths_and_output_wrap_are_diagnostics() {
for service in [0x16, 0x17, 0x18] {
let (data, _) = filtered(service, &[0x12, 0x34]);
for source in [
0,
0x3ffc,
0x0100_0000,
ROM_DATA + 1,
ROM_DATA + 2,
ROM_DATA + 3,
] {
let (mut m, _) = call_status_data(service, false, [source, DEST, 0], 0x1f, &data);
assert_invalid(fail(&mut m));
}
let (mut m, _) = prepare(service, false, 0xffff_fffe, &data);
assert_invalid(fail(&mut m));
if service == 0x16 {
continue;
}
let (mut m, _) = prepare(service, false, DEST + 1, &data);
assert_invalid(fail(&mut m));
let mut odd = data.clone();
odd[1] = 3;
let (mut m, _) = prepare(service, true, DEST, &odd);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
}
#[test]
fn every_truncated_source_prefix_returns_a_memory_diagnostic() {
for service in [0x16, 0x17, 0x18] {
let (data, _) = filtered(service, &[0x12, 0x34, 0x56, 0x78]);
for length in 0..data.len() {
let (mut m, _) = prepare(service, false, DEST, &data[..length]);
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::Unmapped(ROM_DATA + length as u32).into()),
"service={service:x}, length={length}"
);
}
}
}
#[test]
fn failed_source_reads_keep_completed_units_but_not_pending_halfwords() {
for service in [0x16, 0x17, 0x18] {
let (mut data, _) = filtered(service, &[0x12, 0x34, 0x56, 0x78]);
// Three byte samples or one complete halfword plus one byte are available.
data.truncate(7);
let (mut m, _) = prepare(service, false, DEST, &data);
m.memory_mut().write32(DEST, 0xcccc_cccc).unwrap();
assert!(matches!(
fail(&mut m),
MachineError::Cpu(CpuError::Memory(_))
));
assert_eq!(
m.memory().read32(DEST).unwrap(),
match service {
0x16 => 0xcc56_3412,
0x17 => 0xcccc_3412,
_ => 0xcccc_0012,
}
);
}
}
#[test]
fn destination_failure_preserves_preceding_completed_halfword() {
for service in [0x17, 0x18] {
let (data, _) = filtered(service, &[0x12, 0x34, 0x56, 0x78]);
let (mut m, _) = prepare(service, false, 0x07ff_fffe, &data);
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into())
);
assert_eq!(
m.memory().read16(0x07ff_fffe).unwrap(),
if service == 0x17 { 0x3412 } else { 0x12 }
);
}
}
#[test]
fn output_uses_normal_byte_and_halfword_video_bus_rules() {
for service in [0x16, 0x17, 0x18] {
let (data, _) = filtered(
service,
if service == 0x18 {
&[0x3412]
} else {
&[0x12, 0x34]
},
);
for dest in [VRAM_START, OAM_START] {
let (mut m, pc) = prepare(service, false, dest, &data);
m.memory_mut().write16(dest, 0xcccc).unwrap();
complete(&mut m, pc);
assert_eq!(
m.memory().read16(dest).unwrap(),
if service != 0x16 {
0x3412
} else if dest == OAM_START {
0xcccc
} else {
0x3434
}
);
}
}
}
#[test]
fn declared_end_does_not_consume_trailing_payload() {
for service in [0x16, 0x17, 0x18] {
let (mut data, expected) = filtered(service, &[0x12, 0x34]);
data.extend([0xff; 8]);
verify(service, false, DEST, &data, &expected);
}
}
#[test]
fn dma_and_timers_continue_while_filter_masks_cpu_irq_delivery() {
for service in [0x16, 0x17, 0x18] {
let (data, expected) = filtered(service, &[0x55; 32]);
let (mut m, pc) = prepare(service, false, DEST, &data);
m.step().unwrap(); // Enter SWI before requesting IRQs.
m.memory_mut().write16(IE, 8).unwrap();
m.memory_mut().write16(IME, 1).unwrap();
m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap();
m.memory_mut().write32(SOURCE, 0x8765_4321).unwrap();
m.memory_mut().write32(DMA_BASE, SOURCE).unwrap();
m.memory_mut().write32(DMA_BASE + 4, SOURCE + 4).unwrap();
m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap();
let mut dma = 0;
for _ in 0..5000 {
if m.cpu().pc() == pc {
break;
}
let step = m.step().unwrap();
assert_ne!(step, StepKind::IrqEntry);
if matches!(step, StepKind::Dma { channel: 0 }) {
dma += 1;
}
}
assert_eq!(m.cpu().pc(), pc);
assert_eq!(dma, 1);
assert_eq!(m.memory().read32(SOURCE + 4).unwrap(), 0x8765_4321);
assert_eq!(m.memory().read16(IF).unwrap() & 8, 8);
assert_eq!(m.memory().read16(IME).unwrap(), 1);
assert_eq!(m.memory().read16(IRQ_FLAGS).unwrap(), 0);
for (index, byte) in expected.iter().enumerate() {
assert_eq!(m.memory().read8(DEST + index as u32).unwrap(), *byte);
}
}
}

View file

@ -0,0 +1,473 @@
use super::*;
use gba_core::{
bios::INVALID_ARGUMENT_TRAP,
dma::DMA_BASE,
memory::{OAM_START, PALETTE_START},
};
use std::collections::{BTreeMap, VecDeque};
#[derive(Clone)]
enum Tree {
Leaf(u8),
Branch(Box<Tree>, Box<Tree>),
}
fn branch(left: Tree, right: Tree) -> Tree {
Tree::Branch(Box::new(left), Box::new(right))
}
fn pair() -> Tree {
branch(Tree::Leaf(0x12), Tree::Leaf(0x34))
}
fn balanced(values: &[u8]) -> Tree {
if values.len() == 1 {
return Tree::Leaf(values[0]);
}
let half = values.len() / 2;
branch(balanced(&values[..half]), balanced(&values[half..]))
}
fn chain(depth: u8) -> Tree {
(0..depth).rev().fold(Tree::Leaf(depth), |next, value| {
branch(next, Tree::Leaf(value))
})
}
/// Test-only tree serializer and path encoder. Expected output comes directly
/// from requested symbols; no production traversal code is used to decode it.
fn encode(width: u8, tree: &Tree, symbols: &[u8]) -> (Vec<u8>, Vec<u8>) {
fn paths(tree: &Tree, prefix: &mut Vec<bool>, codes: &mut BTreeMap<u8, Vec<bool>>) {
match tree {
Tree::Leaf(value) => {
assert!(codes.insert(*value, prefix.clone()).is_none());
}
Tree::Branch(left, right) => {
prefix.push(false);
paths(left, prefix, codes);
prefix.pop();
prefix.push(true);
paths(right, prefix, codes);
prefix.pop();
}
}
}
let mut codes = BTreeMap::new();
paths(tree, &mut Vec::new(), &mut codes);
let mut table = vec![0, 0]; // Size byte and root.
let mut queue = VecDeque::from([(1usize, tree)]);
while let Some((index, node)) = queue.pop_front() {
match node {
Tree::Leaf(value) => table[index] = *value,
Tree::Branch(left, right) => {
let children = table.len();
let offset = (children - (index & !1)) / 2 - 1;
assert!(offset < 64);
table[index] = offset as u8
| if matches!(**left, Tree::Leaf(_)) {
0x80
} else {
0
}
| if matches!(**right, Tree::Leaf(_)) {
0x40
} else {
0
};
table.extend([0, 0]);
queue.push_back((children, left));
queue.push_back((children + 1, right));
}
}
}
while table.len() % 4 != 0 {
table.push(0);
}
assert!(table.len() <= 512);
table[0] = (table.len() / 2 - 1) as u8;
let expected: Vec<u8> = if width == 4 {
assert!(symbols.iter().all(|v| *v < 16));
assert_eq!(symbols.len() % 2, 0);
symbols
.chunks_exact(2)
.map(|pair| pair[0] | pair[1] << 4)
.collect()
} else {
symbols.to_vec()
};
assert_eq!(expected.len() % 4, 0);
let mut data = (((expected.len() as u32) << 8) | 0x20 | u32::from(width))
.to_le_bytes()
.to_vec();
data.extend(table);
let bits: Vec<bool> = symbols
.iter()
.flat_map(|v| codes[v].iter().copied())
.collect();
for chunk in bits.chunks(32) {
let word = chunk
.iter()
.enumerate()
.fold(0u32, |word, (i, bit)| word | u32::from(*bit) << (31 - i));
data.extend(word.to_le_bytes());
}
(data, expected)
}
fn prepare(thumb: bool, dest: u32, data: &[u8]) -> (Machine, u32) {
call_status_data(0x13, thumb, [ROM_DATA, dest, 0xa55a_1234], 0x1f, data)
}
fn complete(m: &mut Machine, pc: u32) {
m.step().unwrap();
reach(m, pc, 8_000_000);
}
fn verify(thumb: bool, dest: u32, data: &[u8], expected: &[u8]) {
let (mut m, pc) = prepare(thumb, dest, data);
let before = m.cpu().clone();
let end = dest + expected.len() as u32;
for address in [dest - 4, end, bios::IRQ_STACK] {
m.memory_mut().write32(address, 0xa55a_1234).unwrap();
}
complete(&mut m, pc);
for (index, byte) in expected.iter().enumerate() {
assert_eq!(
m.memory().read8(dest + index as u32).unwrap(),
*byte,
"byte={index}"
);
}
for address in [dest - 4, end, bios::IRQ_STACK] {
assert_eq!(m.memory().read32(address).unwrap(), 0xa55a_1234);
}
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
}
fn fail(m: &mut Machine) -> MachineError {
for _ in 0..10000 {
let before = m.cpu().clone();
let cycles = m.cycles();
let timing = m.last_timing();
if let Err(error) = m.step() {
assert_eq!(m.cpu(), &before);
assert_eq!(m.cycles(), cycles);
assert_eq!(m.last_timing(), timing);
assert_eq!(m.step(), Err(error.clone()));
return error;
}
}
panic!("invalid Huffman stream did not produce a bounded diagnostic");
}
fn assert_invalid(error: MachineError) {
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: INVALID_ARGUMENT_TRAP,
..
})
));
}
#[test]
fn documented_huff_example_decodes_msb_first_words_from_arm_and_thumb() {
let tree = branch(Tree::Leaf(b'f'), branch(Tree::Leaf(b'H'), Tree::Leaf(b'u')));
let (data, expected) = encode(8, &tree, b"Huff");
assert_eq!(
data,
[0x28, 4, 0, 0, 3, 0x80, b'f', 0xc0, b'H', b'u', 0, 0, 0, 0, 0, 0xb0]
);
for thumb in [false, true] {
verify(thumb, DEST, &data, &expected);
}
}
#[test]
fn nibble_symbols_pack_low_nibble_first_into_output_words() {
let tree = branch(Tree::Leaf(2), Tree::Leaf(13));
let (data, expected) = encode(4, &tree, &[2, 13, 13, 2, 2, 2, 13, 13]);
assert_eq!(expected, [0xd2, 0x2d, 0x22, 0xdd]);
assert_eq!(&data[8..], &0x6300_0000u32.to_le_bytes());
verify(true, VRAM_START + 4, &data, &expected);
}
#[test]
fn balanced_trees_cover_every_byte_and_nibble_value() {
for base in [0u8, 128] {
let symbols: Vec<_> = (0..128u8).map(|n| base + n).collect();
let (data, expected) = encode(8, &balanced(&symbols), &symbols);
verify(false, DEST, &data, &expected);
verify(true, VRAM_START + 4, &data, &expected);
}
let symbols: Vec<_> = (0..16u8).collect();
let (data, expected) = encode(4, &balanced(&symbols), &symbols);
verify(true, DEST, &data, &expected);
}
#[test]
fn maximum_tree_and_deep_paths_preserve_node_state_across_input_words() {
let (data, expected) = encode(8, &chain(255), &[255, 254, 0, 127]);
assert_eq!(data[4], 255); // 512-byte size/table section; 511 actual nodes.
verify(false, DEST, &data, &expected);
verify(true, VRAM_START + 4, &data, &expected);
}
#[test]
fn maximum_child_offset_and_unused_tree_padding_are_supported() {
let mut data = vec![0x28, 4, 0, 0];
let mut table = vec![0xff; 512]; // Unused bytes are not decoded or validated.
table[0] = 255;
table[1] = 0xff; // Both leaves, offset 63 -> section bytes 128 and 129.
table[128] = 0x12;
table[129] = 0x34;
data.extend(table);
data.extend(0x6000_0000u32.to_le_bytes()); // 0,1,1,0
verify(false, DEST, &data, &[0x12, 0x34, 0x34, 0x12]);
}
#[test]
fn seeded_variable_length_paths_match_original_symbols() {
let mut seed = 0x392b_175eu32;
for case in 0..48 {
let width = if case % 2 == 0 { 4 } else { 8 };
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
let depth = 2 + (seed % if width == 4 { 14 } else { 62 }) as u8;
let symbols: Vec<_> = (0..64)
.map(|_| {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
(seed % (u32::from(depth) + 1)) as u8
})
.collect();
let (data, expected) = encode(width, &chain(depth), &symbols);
verify(case % 3 == 0, DEST, &data, &expected);
}
}
#[test]
fn output_byte_length_uses_upper_header_bits_for_both_symbol_widths() {
for width in [4, 8] {
let tree = branch(Tree::Leaf(1), Tree::Leaf(2));
let symbols: Vec<_> = (0..65536 * (8 / width as usize))
.map(|i| if i % 3 == 0 { 1 } else { 2 })
.collect();
let (data, expected) = encode(width, &tree, &symbols);
assert_eq!(&data[..4], &[0x20 | width, 0, 0, 1]);
verify(false, DEST, &data, &expected);
}
}
#[test]
fn streams_can_be_read_from_both_work_ram_regions() {
let (data, expected) = encode(8, &pair(), &[0x12, 0x34, 0x12, 0x34]);
for source in [SOURCE, 0x0300_0100] {
let (mut m, pc) = call(0x13, true, [source, DEST, 0]);
for (index, byte) in data.iter().enumerate() {
m.memory_mut().write8(source + index as u32, *byte).unwrap();
}
complete(&mut m, pc);
for (index, byte) in expected.iter().enumerate() {
assert_eq!(m.memory().read8(DEST + index as u32).unwrap(), *byte);
}
}
}
#[test]
fn user_system_status_masks_and_caller_registers_are_restored() {
let (data, _) = encode(8, &pair(), &[0x12; 4]);
for thumb in [false, true] {
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
let (mut m, pc) =
call_status_data(0x13, thumb, [ROM_DATA, DEST, 0x1234_5678], status, &data);
let before = m.cpu().clone();
complete(&mut m, pc);
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
}
}
}
#[test]
fn zero_output_length_does_not_read_tree_or_bitstream() {
for header in [0x24, 0x28] {
let (mut m, pc) = prepare(false, 0x0e00_0000, &[header, 0, 0, 0]);
complete(&mut m, pc);
}
}
#[test]
fn unsupported_type_or_symbol_width_headers_are_rejected() {
for header in 0u8..=255 {
if [0x24, 0x28].contains(&header) {
continue;
}
let (mut m, _) = prepare(false, DEST, &[header, 4, 0, 0]);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
}
#[test]
fn source_destination_length_and_bitstream_alignment_are_checked() {
let (data, _) = encode(8, &pair(), &[0x12; 4]);
for (source, dest) in [
(0, DEST),
(0x3ffc, DEST),
(0x0100_0000, DEST),
(ROM_DATA + 1, DEST),
(ROM_DATA + 2, DEST),
(ROM_DATA + 3, DEST),
(ROM_DATA, DEST + 1),
(ROM_DATA, DEST + 2),
(ROM_DATA, DEST + 3),
(ROM_DATA, 0xffff_fffc),
] {
let (mut m, _) = call_status_data(0x13, false, [source, dest, 0], 0x1f, &data);
assert_invalid(fail(&mut m));
}
for length in [1, 2, 3, 5, 6, 7] {
let (mut m, _) = prepare(true, DEST, &[0x28, length, 0, 0]);
assert_invalid(fail(&mut m));
}
for size in [0, 2, 128, 254] {
let (mut m, _) = prepare(false, DEST, &[0x28, 4, 0, 0, size]);
assert_invalid(fail(&mut m)); // Table end must be aligned, not rounded silently.
}
}
#[test]
fn traversed_out_of_table_children_fail_without_reading_them() {
for root in [0xc1, 0xc2, 0xff, 0] {
// Size=1 permits only root + two bytes. Root=0 treats a child as another
// internal node; its next edge must also stay inside the declared table.
let data = [0x28, 4, 0, 0, 1, root, 0, 0, 0, 0, 0, 0];
let (mut m, _) = prepare(false, DEST, &data);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
}
#[test]
fn invalid_nibble_leaves_discard_pending_word_but_keep_prior_output() {
let mut data = vec![0x24, 8, 0, 0, 1, 0xc0, 2, 0x10];
data.extend(0x0040_0000u32.to_le_bytes()); // Eight left leaves, then left, then invalid right.
let (mut m, _) = prepare(true, DEST, &data);
m.memory_mut().write32(DEST + 4, 0xcccc_cccc).unwrap();
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0x2222_2222);
assert_eq!(m.memory().read32(DEST + 4).unwrap(), 0xcccc_cccc);
}
#[test]
fn unused_invalid_leaf_does_not_block_valid_decoding() {
let data = [0x24, 4, 0, 0, 1, 0xc0, 2, 0xff, 0, 0, 0, 0];
verify(false, DEST, &data, &[0x22; 4]);
}
#[test]
fn truncated_headers_tables_and_words_return_memory_diagnostics() {
let (data, _) = encode(8, &pair(), &[0x12; 4]);
for length in 0..data.len() {
let (mut m, _) = prepare(false, DEST, &data[..length]);
// Input words are loaded before the first node. Missing table storage
// therefore faults at the declared bitstream address, not at a tree pre-scan.
let address = ROM_DATA
+ if (5..8).contains(&length) {
8
} else {
length as u32
};
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::Unmapped(address).into()),
"length={length}"
);
}
}
#[test]
fn input_word_refill_failure_does_not_commit_incomplete_output() {
for width in [4, 8] {
let symbols = vec![3; if width == 4 { 24 } else { 12 }];
let (mut data, _) = encode(width, &chain(3), &symbols);
let bitstream = 4 + (usize::from(data[4]) + 1) * 2;
data.truncate(bitstream + 4); // Only 32 of the required branch bits exist.
let (mut m, _) = prepare(false, DEST, &data);
for offset in [0, 4, 8] {
m.memory_mut().write32(DEST + offset, 0xcccc_cccc).unwrap();
}
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::Unmapped(ROM_DATA + data.len() as u32).into())
);
let word = if width == 4 { 0x3333_3333 } else { 0x0303_0303 };
assert_eq!(m.memory().read32(DEST).unwrap(), word);
assert_eq!(
m.memory().read32(DEST + 4).unwrap(),
if width == 4 { 0xcccc_cccc } else { word }
);
assert_eq!(m.memory().read32(DEST + 8).unwrap(), 0xcccc_cccc);
}
}
#[test]
fn destination_failure_preserves_preceding_completed_word() {
let (data, _) = encode(8, &pair(), &[0x12; 8]);
let (mut m, _) = prepare(false, 0x07ff_fffc, &data);
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into())
);
assert_eq!(m.memory().read32(0x07ff_fffc).unwrap(), 0x1212_1212);
}
#[test]
fn word_output_works_in_video_palette_and_object_memory() {
let (data, expected) = encode(8, &pair(), &[0x12, 0x34, 0x34, 0x12]);
for dest in [VRAM_START + 4, PALETTE_START + 4, OAM_START + 4] {
verify(false, dest, &data, &expected);
}
}
#[test]
fn declared_output_end_ignores_remaining_bits_and_trailing_words() {
let (mut data, expected) = encode(8, &pair(), &[0x12; 4]);
data[8..12].copy_from_slice(&0x0fff_ffffu32.to_le_bytes());
data.extend([0xff; 4]);
verify(true, DEST, &data, &expected);
}
#[test]
fn device_clocks_and_dma_continue_while_huffman_masks_irq_delivery() {
let (data, expected) = encode(8, &pair(), &[0x12; 32]);
let (mut m, pc) = prepare(false, DEST, &data);
m.step().unwrap();
m.memory_mut().write16(IE, 8).unwrap();
m.memory_mut().write16(IME, 1).unwrap();
m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap();
m.memory_mut().write32(SOURCE, 0x8765_4321).unwrap();
m.memory_mut().write32(DMA_BASE, SOURCE).unwrap();
m.memory_mut().write32(DMA_BASE + 4, SOURCE + 4).unwrap();
m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap();
let mut dma = 0;
for _ in 0..5000 {
if m.cpu().pc() == pc {
break;
}
let step = m.step().unwrap();
assert_ne!(step, StepKind::IrqEntry);
if matches!(step, StepKind::Dma { channel: 0 }) {
dma += 1;
}
}
assert_eq!(m.cpu().pc(), pc);
assert_eq!(dma, 1);
assert_eq!(m.memory().read32(SOURCE + 4).unwrap(), 0x8765_4321);
assert_eq!(m.memory().read16(IF).unwrap() & 8, 8);
assert_eq!(m.memory().read16(IME).unwrap(), 1);
assert_eq!(m.memory().read16(IRQ_FLAGS).unwrap(), 0);
for (index, byte) in expected.iter().enumerate() {
assert_eq!(m.memory().read8(DEST + index as u32).unwrap(), *byte);
}
}

View file

@ -0,0 +1,417 @@
use super::*;
use gba_core::bios::INVALID_ARGUMENT_TRAP;
#[derive(Clone, Copy)]
enum Token {
Literal(u8),
Reference { distance: usize, length: usize },
}
/// Test-only token encoder and byte-vector reference. Neither decodes the emitted ARM code.
fn stream(tokens: &[Token]) -> (Vec<u8>, Vec<u8>) {
let mut compressed = vec![0x10, 0, 0, 0];
let mut expected = Vec::new();
for group in tokens.chunks(8) {
let flags_index = compressed.len();
compressed.push(0);
for (index, token) in group.iter().enumerate() {
match *token {
Token::Literal(value) => {
compressed.push(value);
expected.push(value);
}
Token::Reference { distance, length } => {
assert!((1..=4096).contains(&distance));
assert!((3..=18).contains(&length));
assert!(distance <= expected.len());
compressed[flags_index] |= 0x80 >> index;
compressed.push((((length - 3) << 4) | ((distance - 1) >> 8)) as u8);
compressed.push((distance - 1) as u8);
for _ in 0..length {
expected.push(expected[expected.len() - distance]);
}
}
}
}
}
let length = expected.len() as u32;
compressed[..4].copy_from_slice(&(length << 8 | 0x10).to_le_bytes());
(compressed, expected)
}
fn prepare(service: u8, thumb: bool, destination: u32, compressed: &[u8]) -> (Machine, u32) {
call_status_data(
service,
thumb,
[ROM_DATA, destination, 0x55aa_1234],
0x1f,
compressed,
)
}
fn complete(machine: &mut Machine, return_pc: u32) {
machine.step().unwrap();
reach(machine, return_pc, 500_000);
}
fn verify(service: u8, thumb: bool, destination: u32, compressed: &[u8], expected: &[u8]) {
let (mut machine, return_pc) = prepare(service, thumb, destination, compressed);
let before = machine.cpu().clone();
// Halfword sentinels also work in VRAM without byte-write duplication.
machine
.memory_mut()
.write16((destination - 2) & !1, 0xa55a)
.unwrap();
let after = (destination + expected.len() as u32 + 1) & !1;
machine.memory_mut().write16(after, 0xa55a).unwrap();
for address in [destination - 1, destination + expected.len() as u32] {
machine.memory_mut().write16(address & !1, 0xa55a).unwrap();
}
machine
.memory_mut()
.write32(bios::IRQ_STACK, 0x5a5a_a5a5)
.unwrap();
complete(&mut machine, return_pc);
for (index, value) in expected.iter().enumerate() {
assert_eq!(
machine.memory().read8(destination + index as u32).unwrap(),
*value,
"output byte {index}"
);
}
assert_eq!(
machine.memory().read16((destination - 2) & !1).unwrap(),
0xa55a
);
assert_eq!(machine.memory().read16(after).unwrap(), 0xa55a);
for address in [destination - 1, destination + expected.len() as u32] {
assert_eq!(
machine.memory().read8(address).unwrap(),
if address & 1 == 0 { 0x5a } else { 0xa5 }
);
}
assert_eq!(
machine.memory().read32(bios::IRQ_STACK).unwrap(),
0x5a5a_a5a5
);
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
}
fn fail(machine: &mut Machine) -> MachineError {
for _ in 0..1000 {
let before = machine.cpu().clone();
let cycles = machine.cycles();
let timing = machine.last_timing();
if let Err(error) = machine.step() {
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.last_timing(), timing);
assert_eq!(machine.step(), Err(error.clone()));
return error;
}
}
panic!("invalid stream did not produce a bounded diagnostic");
}
fn assert_invalid(error: MachineError) {
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: INVALID_ARGUMENT_TRAP,
..
})
));
}
#[test]
fn literals_cross_flag_groups_in_arm_and_thumb_for_both_write_widths() {
let tokens: Vec<_> = (0..34).map(|value| Token::Literal(value * 7)).collect();
let (compressed, expected) = stream(&tokens);
for thumb in [false, true] {
verify(0x11, thumb, DEST, &compressed, &expected);
verify(0x12, thumb, VRAM_START + 4, &compressed, &expected);
verify(0x12, thumb, DEST, &compressed, &expected); // Halfword variant also works in RAM.
}
}
#[test]
fn literal_and_reference_flags_are_read_most_significant_bit_first() {
let tokens = [
Token::Literal(b'A'),
Token::Literal(b'B'),
Token::Reference {
distance: 2,
length: 3,
},
Token::Literal(b'C'),
Token::Reference {
distance: 4,
length: 4,
},
Token::Literal(b'D'),
Token::Literal(b'E'),
Token::Reference {
distance: 3,
length: 6,
},
Token::Reference {
distance: 8,
length: 4,
},
Token::Literal(b'F'),
Token::Literal(b'G'),
];
let (compressed, expected) = stream(&tokens);
assert_eq!(compressed[4], 0x29); // References in token positions 2,4,7.
assert_eq!(expected.len() % 2, 0);
verify(0x11, false, DEST, &compressed, &expected);
verify(0x12, true, VRAM_START + 4, &compressed, &expected);
}
#[test]
fn overlapping_references_support_every_run_length() {
for length in 3..=18 {
let mut tokens = vec![
Token::Literal(0x12),
Token::Literal(0xab),
Token::Reference {
distance: 2,
length,
},
];
if length % 2 != 0 {
tokens.push(Token::Literal(0x99));
}
let (compressed, expected) = stream(&tokens);
verify(0x11, false, DEST + 1, &compressed, &expected); // Byte destination need not be aligned.
verify(0x12, true, VRAM_START + 4, &compressed, &expected);
}
}
#[test]
fn wram_variant_supports_distance_one_runs() {
let (compressed, expected) = stream(&[
Token::Literal(0x5a),
Token::Reference {
distance: 1,
length: 18,
},
]);
verify(0x11, false, DEST, &compressed, &expected);
verify(0x11, true, DEST + 1, &compressed, &expected);
}
#[test]
fn full_twelve_bit_displacements_reach_back_4096_bytes() {
for distance in [4095, 4096] {
let mut tokens: Vec<_> = (0..4096)
.map(|index| Token::Literal((index * 37 % 251) as u8))
.collect();
tokens.push(Token::Reference {
distance,
length: 18,
});
let (compressed, expected) = stream(&tokens);
verify(0x11, false, DEST, &compressed, &expected);
verify(0x12, true, VRAM_START + 4, &compressed, &expected);
}
}
#[test]
fn seeded_valid_streams_match_independent_token_expansion() {
let mut state = 0x1234_abcd_u32;
for seed in 0..16 {
let mut tokens = vec![Token::Literal(0x13), Token::Literal(0x57)];
let mut length = 2;
for _ in 0..40 {
state = state.wrapping_mul(1664525).wrapping_add(1013904223);
if state & 3 == 0 {
tokens.push(Token::Literal((state >> 16) as u8));
length += 1;
} else {
let run = 3 + (state >> 8) as usize % 16;
let distance = 2 + (state >> 16) as usize % (length - 1).min(4095);
tokens.push(Token::Reference {
distance,
length: run,
});
length += run;
}
}
if length % 2 != 0 {
tokens.push(Token::Literal(0));
}
let (compressed, expected) = stream(&tokens);
verify(0x11, seed & 1 == 0, DEST, &compressed, &expected);
verify(0x12, seed & 1 != 0, VRAM_START + 4, &compressed, &expected);
}
}
#[test]
fn compressed_sources_can_be_read_from_either_work_ram_region() {
let (compressed, expected) = stream(&[
Token::Literal(0x55),
Token::Literal(0xaa),
Token::Reference {
distance: 2,
length: 4,
},
]);
for source in [SOURCE, 0x0300_0100] {
for service in [0x11, 0x12] {
let (mut machine, return_pc) = call(service, false, [source, DEST, 0]);
for (offset, byte) in compressed.iter().enumerate() {
machine
.memory_mut()
.write8(source + offset as u32, *byte)
.unwrap();
}
complete(&mut machine, return_pc);
for (offset, byte) in expected.iter().enumerate() {
assert_eq!(machine.memory().read8(DEST + offset as u32).unwrap(), *byte);
}
}
}
}
#[test]
fn decompression_restores_user_mode_masks_and_all_caller_registers() {
let (compressed, _) = stream(&[
Token::Literal(0x55),
Token::Literal(0xaa),
Token::Reference {
distance: 2,
length: 4,
},
]);
for service in [0x11, 0x12] {
for thumb in [false, true] {
for status in [0x10, 0x90, 0x9f, 0xdf] {
let (mut machine, return_pc) = call_status_data(
service,
thumb,
[ROM_DATA, DEST, 0x12345678],
status,
&compressed,
);
let before = machine.cpu().clone();
complete(&mut machine, return_pc);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
}
}
}
}
#[test]
fn zero_output_length_does_not_read_flags_or_touch_destination() {
for service in [0x11, 0x12] {
let (mut machine, return_pc) = prepare(service, false, 0x0e00_0000, &[0x10, 0, 0, 0]);
complete(&mut machine, return_pc); // Destination and next source byte are unmapped.
}
}
#[test]
fn bad_headers_alignment_and_wrapping_destinations_are_rejected() {
for header in [0_u32, 0x0200, 0x0211, 0x0220, 0x02ff] {
let (mut machine, _) = prepare(0x11, false, DEST, &header.to_le_bytes());
assert_invalid(fail(&mut machine));
assert_eq!(machine.memory().read32(DEST).unwrap(), 0);
}
for source in [0, 0x3ffc, ROM_DATA + 1, ROM_DATA + 2, ROM_DATA + 3] {
let (mut machine, _) =
call_status_data(0x11, false, [source, DEST, 0], 0x1f, &[0x10, 4, 0, 0]);
assert_invalid(fail(&mut machine));
}
let (mut machine, _) = prepare(0x12, false, 0xffff_fffe, &[0x10, 4, 0, 0]);
assert_invalid(fail(&mut machine));
}
#[test]
fn references_before_output_or_beyond_declared_length_are_rejected() {
for compressed in [
vec![0x10, 3, 0, 0, 0x80, 0, 0], // No preceding output.
vec![0x10, 4, 0, 0, 0x40, b'A', 0, 1], // Only one preceding byte, distance two.
vec![0x10, 3, 0, 0, 0x40, b'A', 0, 0], // Length three exceeds two remaining bytes.
] {
let (mut machine, _) = prepare(0x11, false, DEST, &compressed);
machine.memory_mut().write32(DEST, 0xcccc_cccc).unwrap();
assert_invalid(fail(&mut machine));
assert_eq!(
machine.memory().read8(DEST).unwrap(),
if compressed[4] == 0x80 { 0xcc } else { b'A' }
);
assert_eq!(machine.memory().read8(DEST + 1).unwrap(), 0xcc);
}
}
#[test]
fn vram_variant_rejects_odd_sizes_odd_destinations_and_distance_one() {
for (destination, compressed) in [
(VRAM_START + 4, vec![0x10, 1, 0, 0, 0, 0x77]),
(VRAM_START + 5, vec![0x10, 2, 0, 0, 0, 0x77, 0x88]),
(VRAM_START + 4, vec![0x10, 4, 0, 0, 0x40, 0x77, 0, 0]),
] {
let (mut machine, _) = prepare(0x12, true, destination, &compressed);
machine
.memory_mut()
.write32(VRAM_START + 4, 0xcccc_cccc)
.unwrap();
assert_invalid(fail(&mut machine));
assert_eq!(
machine.memory().read32(VRAM_START + 4).unwrap(),
0xcccc_cccc
);
}
}
#[test]
fn truncated_rom_inputs_produce_memory_diagnostics() {
for compressed in [
vec![],
vec![0x10, 2],
vec![0x10, 2, 0, 0],
vec![0x10, 2, 0, 0, 0],
vec![0x10, 3, 0, 0, 0x80, 0],
] {
let (mut machine, _) = prepare(0x11, false, DEST, &compressed);
assert_eq!(
fail(&mut machine),
MachineError::Cpu(MemoryError::Unmapped(ROM_DATA + compressed.len() as u32).into())
);
}
}
#[test]
fn halfword_output_never_commits_a_lone_pending_byte_on_failure() {
for service in [0x11, 0x12] {
let compressed = [0x10, 2, 0, 0, 0, b'A']; // Second literal is missing.
let (mut machine, _) = prepare(service, false, DEST, &compressed);
machine.memory_mut().write16(DEST, 0xcccc).unwrap();
assert!(matches!(
fail(&mut machine),
MachineError::Cpu(CpuError::Memory(_))
));
assert_eq!(
machine.memory().read16(DEST).unwrap(),
if service == 0x11 { 0xcc41 } else { 0xcccc }
);
}
}
#[test]
fn destination_failure_preserves_preceding_completed_halfword() {
let (compressed, _) = stream(&[
Token::Literal(0x12),
Token::Literal(0x34),
Token::Literal(0x56),
Token::Literal(0x78),
]);
let (mut machine, _) = prepare(0x12, false, 0x07ff_fffe, &compressed);
assert_eq!(
fail(&mut machine),
MachineError::Cpu(MemoryError::ReadOnly(0x0800_0000).into())
);
assert_eq!(machine.memory().read16(0x07ff_fffe).unwrap(), 0x3412);
}

View file

@ -0,0 +1,492 @@
use super::*;
use gba_core::{
bios::INVALID_ARGUMENT_TRAP,
dma::{DmaError, DMA_BASE, DMA_STRIDE},
input::Buttons,
io::{
BG0CNT, BG2PA, BG2PD, BG3PA, BG3PD, BLDALPHA, BLDCNT, DISPCNT, GREENSWAP, KEYINPUT,
WAITCNT, WININ, WINOUT,
},
memory::{OAM_START, PALETTE_START},
video::Framebuffer,
};
const REGIONS: [(u32, u32, u32); 5] = [
(1, 0x0200_0000, 0x40000),
(2, 0x0300_0000, 0x7e00),
(4, PALETTE_START, 0x400),
(8, VRAM_START, 0x18000),
(16, OAM_START, 0x400),
];
const MARKER: u32 = 0xa55a_c33c;
fn literal(code: &mut Vec<u32>, register: u32, value: u32) {
code.extend([0xe59f_0000 | register << 12, 0xea00_0000, value]);
}
fn prepare(flags: u32, thumb: bool, status: u32) -> (Machine, u32) {
prepare_at(flags, thumb, status, None)
}
fn prepare_at(flags: u32, thumb: bool, status: u32, location: Option<u32>) -> (Machine, u32) {
let mut code = Vec::new();
literal(&mut code, 0, flags);
for r in 1..=12 {
code.push(0xe3a0_0080 | r << 12 | r);
}
code.extend([0xe328_f20f, 0xe321_f000 | status]);
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_df01 } else { 0xef01_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_df01 } else { 0xef01_0000 })
.unwrap();
m.memory_mut().write32(address + 4, 0xeaff_fffe).unwrap();
}
reach(&mut m, swi_pc, 200);
(m, swi_pc + if thumb { 2 } else { 4 })
}
fn complete(m: &mut Machine, pc: u32) {
let before = m.cpu().clone();
m.step().unwrap();
reach(m, pc, 400_000);
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
assert_eq!(m.memory().read16(DISPCNT).unwrap(), 0x80);
}
fn samples(start: u32, bytes: u32) -> [u32; 4] {
[start, start + 4, start + bytes / 2, start + bytes - 4]
}
fn seed_samples(m: &mut Machine) {
for (_, start, bytes) in REGIONS {
for address in samples(start, bytes) {
m.memory_mut().write32(address, MARKER).unwrap();
}
}
}
fn check_samples(m: &Machine, flags: u32) {
for (bit, start, bytes) in REGIONS {
for address in samples(start, bytes) {
assert_eq!(
m.memory().read32(address).unwrap(),
if flags & bit == 0 { MARKER } else { 0 },
"flags={flags:#x}, address={address:#x}"
);
}
}
}
#[test]
fn each_memory_flag_clears_its_entire_region_and_no_other_region() {
for thumb in [false, true] {
for (flag, _, _) in REGIONS {
let (mut m, pc) = prepare(flag, thumb, 0x1f);
for (_, start, bytes) in REGIONS {
for address in (start..start + bytes).step_by(4) {
m.memory_mut().write32(address, MARKER ^ address).unwrap();
}
}
complete(&mut m, pc);
for (bit, start, bytes) in REGIONS {
for address in (start..start + bytes).step_by(4) {
assert_eq!(
m.memory().read32(address).unwrap(),
if bit == flag { 0 } else { MARKER ^ address },
"flag={flag}, address={address:#x}"
);
}
}
}
}
}
#[test]
fn all_memory_flag_combinations_are_independent_from_arm_and_thumb() {
for thumb in [false, true] {
for flags in 0..32 {
let (mut m, pc) = prepare(flags, thumb, 0x1f);
seed_samples(&mut m);
complete(&mut m, pc);
check_samples(&m, flags);
}
}
}
#[test]
fn zero_flags_still_force_blank_and_clear_other_display_control_bits() {
for thumb in [false, true] {
let (mut m, pc) = prepare(0, thumb, 0x1f);
seed_samples(&mut m);
m.memory_mut().write16(DISPCNT, 0x1403).unwrap();
m.memory_mut().write16(BG0CNT, 0x1234).unwrap();
complete(&mut m, pc);
check_samples(&m, 0);
assert_eq!(m.memory().read16(BG0CNT).unwrap(), 0x1204);
let mut frame = Framebuffer::default();
m.memory().render_frame(&mut frame).unwrap();
assert!(frame.pixels().iter().all(|p| *p == 0xffffff));
}
}
#[test]
fn reserved_high_bits_are_ignored_and_original_argument_is_restored() {
for flags in [
0x8000_0000,
0xffff_ff00,
0x1234_5604,
0xffff_ff80,
0xffff_ff9f,
] {
for thumb in [false, true] {
let (mut m, pc) = prepare(flags, thumb, 0x1f);
seed_samples(&mut m);
complete(&mut m, pc);
check_samples(&m, flags);
}
}
}
#[test]
fn internal_ram_clear_preserves_bios_work_area_except_common_swi_frame() {
for thumb in [false, true] {
let (mut m, pc) = prepare(0x9f, thumb, 0x1f);
for address in (0x0300_7e00..0x0300_8000).step_by(4) {
m.memory_mut().write32(address, MARKER ^ address).unwrap();
}
complete(&mut m, pc);
for address in (0x0300_7e00..0x0300_8000).step_by(4) {
if (bios::SVC_STACK - 28..bios::SVC_STACK).contains(&address) {
continue;
}
assert_eq!(
m.memory().read32(address).unwrap(),
MARKER ^ address,
"{address:#x}"
);
}
assert_eq!(m.memory().read32(0x0300_7dfc).unwrap(), 0);
}
}
#[test]
fn all_serial_sound_requests_fail_before_forced_blank_or_memory_clear() {
for thumb in [false, true] {
for flags in (0..256).filter(|flags| flags & 0x60 != 0) {
let (mut m, _) = prepare(flags, thumb, 0x1f);
seed_samples(&mut m);
m.memory_mut().write16(DISPCNT, 0x0403).unwrap();
for _ in 0..1000 {
let before = m.cpu().clone();
let cycles = m.cycles();
let timing = m.last_timing();
if let Err(error) = m.step() {
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: INVALID_ARGUMENT_TRAP,
..
})
));
assert_eq!(m.cpu(), &before);
assert_eq!(m.cycles(), cycles);
assert_eq!(m.last_timing(), timing);
assert_eq!(m.step(), Err(error));
break;
}
}
assert!(m.step().is_err());
assert_eq!(m.memory().read16(DISPCNT).unwrap(), 0x0403);
check_samples(&m, 0);
}
}
}
#[test]
fn caller_registers_flags_modes_and_interrupt_masks_are_preserved() {
for thumb in [false, true] {
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
for flags in [0, 4, 0x80] {
let (mut m, pc) = prepare(flags, thumb, status);
assert!(m.cpu().registers()[1..13].iter().all(|r| *r != 0));
complete(&mut m, pc);
}
}
}
}
#[test]
fn io_reset_clears_readable_controls_but_keeps_input_postflg_and_green_swap() {
for flags in [0x80, 0x9f] {
let (mut m, pc) = prepare(flags, false, 0x9f);
m.memory_mut().set_buttons(Buttons::from_bits(0x251));
let keys = m.memory().read16(KEYINPUT).unwrap();
m.memory_mut().write16(GREENSWAP, 1).unwrap();
for address in [
BG0CNT,
BG0CNT + 2,
BG0CNT + 4,
BG0CNT + 6,
WININ,
WINOUT,
BLDCNT,
BLDALPHA,
WAITCNT,
IE,
] {
m.memory_mut().write16(address, 0xffff).unwrap();
}
m.memory_mut().write16(DISPSTAT, 0x7b38).unwrap();
m.memory_mut().write16(IME, 1).unwrap();
// Overflow queues IF, independently of CPU execution.
m.memory_mut().write32(TIMER_BASE, 0x00c0_ffff).unwrap();
m.memory_mut().advance_cycles(1);
assert_ne!(m.memory().read16(IF).unwrap(), 0);
let cycles = m.cycles();
complete(&mut m, pc);
for address in [
BG0CNT,
BG0CNT + 2,
BG0CNT + 4,
BG0CNT + 6,
WININ,
WINOUT,
BLDCNT,
BLDALPHA,
WAITCNT,
IE,
IF,
IME,
] {
assert_eq!(m.memory().read16(address).unwrap(), 0, "{address:#x}");
}
assert_eq!(m.memory().read16(DISPSTAT).unwrap() & 0xff38, 0);
assert_eq!(m.memory().read16(KEYINPUT).unwrap(), keys);
assert_eq!(m.memory().read16(POSTFLG).unwrap(), 1);
assert_eq!(m.memory().read16(GREENSWAP).unwrap(), 1);
assert!(m.cycles() > cycles);
assert!(!m.halted());
}
}
#[test]
fn io_reset_clears_write_only_display_state_and_restores_both_identity_matrices() {
let (mut m, pc) = prepare(0x80, true, 0x1f);
let mut reference = Memory::new(vec![]).unwrap();
for (_, start, bytes) in [REGIONS[2], REGIONS[3]] {
for offset in (0..bytes).step_by(2) {
let value = ((offset * 37 + 1) ^ (offset >> 4)) as u16;
m.memory_mut().write16(start + offset, value).unwrap();
reference.write16(start + offset, value).unwrap();
}
}
for address in (BG0CNT..0x0400_0058).step_by(2) {
m.memory_mut().write16(address, 0x5353).unwrap();
}
complete(&mut m, pc);
for address in [BG2PA, BG2PD, BG3PA, BG3PD] {
reference.write16(address, 256).unwrap();
}
for control in [
0x0100, 0x0200, 0x0400, 0x0800, 0x0402, 0x0802, 0x0403, 0x2403, 0x4403, 0x6403,
] {
for bus in [m.memory_mut(), &mut reference] {
bus.write16(DISPCNT, control).unwrap();
// Enable mosaic in each background without altering its offset/matrix.
for address in (BG0CNT..=BG0CNT + 6).step_by(2) {
bus.write16(address, 0x40).unwrap();
}
}
let mut actual = Framebuffer::default();
let mut expected = Framebuffer::default();
m.memory().render_frame(&mut actual).unwrap();
reference.render_frame(&mut expected).unwrap();
assert_eq!(actual.pixels(), expected.pixels(), "DISPCNT={control:#x}");
}
}
#[test]
fn timer_reset_stops_counters_clears_reload_and_does_not_replace_counter_state() {
let (mut m, pc) = prepare(0x80, false, 0x1f);
for timer in 0..4 {
m.memory_mut()
.write32(TIMER_BASE + timer * 4, 0x0080_4321)
.unwrap();
}
complete(&mut m, pc);
for timer in 0..4 {
let address = TIMER_BASE + timer * 4;
assert_eq!(m.memory().read16(address + 2).unwrap(), 0);
let counter = m.memory().read16(address).unwrap();
assert!(counter >= 0x4321);
m.memory_mut().advance_cycles(1000);
assert_eq!(m.memory().read16(address).unwrap(), counter);
m.memory_mut().write16(address + 2, 0x80).unwrap();
assert_eq!(m.memory().read16(address).unwrap(), 0); // Reload latch was cleared.
m.memory_mut().write16(address + 2, 0).unwrap();
}
}
#[test]
fn dma_reset_disables_channels_and_clears_write_only_address_and_count_latches() {
for channel in 0..4 {
let (mut m, pc) = prepare(0x80, false, 0x1f);
let base = DMA_BASE + channel as u32 * DMA_STRIDE;
m.memory_mut().write32(base, SOURCE).unwrap();
m.memory_mut().write32(base + 4, DEST).unwrap();
m.memory_mut().write32(base + 8, 0x9200_0001).unwrap(); // Repeating VBlank, count one.
complete(&mut m, pc);
assert_eq!(m.memory().read16(base + 10).unwrap(), 0);
// With only control re-enabled, the source must now be zero.
m.memory_mut().write16(base + 10, 0x8000).unwrap();
assert_eq!(
m.step(),
Err(MachineError::Dma(DmaError::UnsupportedSource {
channel,
address: 0
}))
);
m.memory_mut().write16(base + 10, 0).unwrap();
m.memory_mut().write32(base, SOURCE).unwrap();
m.memory_mut().write16(base + 10, 0x8000).unwrap();
assert_eq!(
m.step(),
Err(MachineError::Dma(DmaError::Memory {
channel,
error: MemoryError::ReadOnly(0)
}))
);
m.memory_mut().write16(base + 10, 0).unwrap();
m.memory_mut().write32(base + 4, DEST).unwrap();
m.memory_mut().write32(SOURCE, MARKER).unwrap();
m.memory_mut().write16(base + 10, 0x8500).unwrap(); // Fixed source, words; count remains zero.
assert_eq!(m.step().unwrap(), StepKind::Dma { channel });
assert_eq!(m.step().unwrap(), StepKind::Dma { channel });
assert_eq!(m.memory().read32(DEST).unwrap(), MARKER);
assert_eq!(m.memory().read32(DEST + 4).unwrap(), MARKER);
// Count zero expands to the channel maximum, not the stale count of one.
assert_ne!(m.memory().read16(base + 10).unwrap() & 0x8000, 0);
m.memory_mut().write16(base + 10, 0).unwrap();
}
}
#[test]
fn memory_only_reset_keeps_io_and_allows_dma_timers_and_deferred_irq() {
for thumb in [false, true] {
let (mut m, pc) = prepare(4, thumb, 0x1f);
let before = m.cpu().clone();
m.step().unwrap();
m.memory_mut().write16(BG0CNT, 0x41).unwrap();
m.memory_mut().write16(WAITCNT, 0x4317).unwrap();
m.memory_mut().write16(IE, 8).unwrap();
m.memory_mut().write16(IME, 1).unwrap();
m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap();
m.memory_mut().write32(SOURCE, MARKER).unwrap();
m.memory_mut().write32(DMA_BASE, SOURCE).unwrap();
m.memory_mut().write32(DMA_BASE + 4, DEST).unwrap();
m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap();
let mut dma = 0;
for _ in 0..4000 {
if m.cpu().pc() == pc {
break;
}
let step = m.step().unwrap();
assert_ne!(step, StepKind::IrqEntry);
if matches!(step, StepKind::Dma { channel: 0 }) {
dma += 1;
}
}
assert_eq!(m.cpu().pc(), pc);
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
assert_eq!(dma, 1);
assert_eq!(m.memory().read32(DEST).unwrap(), MARKER);
for (address, value) in [
(BG0CNT, 0x41),
(WAITCNT, 0x4317),
(IE, 8),
(IME, 1),
(TIMER_BASE + 2, 0xc0),
] {
assert_eq!(m.memory().read16(address).unwrap(), value);
}
assert_ne!(m.memory().read16(IF).unwrap() & 8, 0);
assert_eq!(m.step().unwrap(), StepKind::IrqEntry);
}
}
#[test]
fn clear_is_visible_one_word_at_a_time_and_uses_normal_video_bus_stores() {
for flag in [1, 2, 4, 8, 16] {
let (mut m, pc) = prepare(flag, false, 0x1f);
let (_, start, _) = REGIONS.into_iter().find(|r| r.0 == flag).unwrap();
m.memory_mut().write32(start, MARKER).unwrap();
m.memory_mut().write32(start + 4, MARKER).unwrap();
m.step().unwrap();
for _ in 0..1000 {
if m.memory().read32(start).unwrap() == 0 {
break;
}
m.step().unwrap();
}
assert_eq!(m.memory().read32(start).unwrap(), 0);
assert_eq!(m.memory().read32(start + 4).unwrap(), MARKER);
assert_eq!(m.memory().read16(DISPCNT).unwrap(), 0x80);
assert!(m.cpu().pc() < BIOS_SIZE as u32);
reach(&mut m, pc, 400_000);
assert_eq!(m.memory().read32(start + 4).unwrap(), 0);
}
}
#[test]
fn ram_callers_can_return_but_selected_ram_code_is_really_erased() {
for thumb in [false, true] {
for (location, clear_flag) in [(SOURCE + 0x100, 1), (0x0300_1000, 2)] {
for flags in [4, clear_flag] {
let (mut m, pc) = prepare_at(flags, thumb, 0x1f, Some(location));
complete(&mut m, pc);
assert_eq!(
m.memory().read32(location).unwrap(),
if flags == clear_flag {
0
} else if thumb {
0xe7fe_df01
} else {
0xef01_0000
}
);
}
}
}
}
#[test]
fn cleared_oam_is_zero_not_the_disabled_sprite_encoding() {
let (mut m, pc) = prepare(16, false, 0x1f);
m.memory_mut().write16(OAM_START, 0x200).unwrap();
m.memory_mut().write16(PALETTE_START + 0x202, 31).unwrap();
m.memory_mut()
.write16(VRAM_START + 0x10000, 0x1111)
.unwrap();
complete(&mut m, pc);
assert_eq!(m.memory().read16(OAM_START).unwrap(), 0);
m.memory_mut().write16(DISPCNT, 0x1000).unwrap();
let mut frame = Framebuffer::default();
m.memory().render_frame(&mut frame).unwrap();
assert_eq!(frame.pixels()[0], 0xff0000);
}

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

View file

@ -0,0 +1,395 @@
use super::*;
use gba_core::{bios::INVALID_ARGUMENT_TRAP, dma::DMA_BASE, memory::OAM_START};
#[derive(Clone)]
enum Block {
Literal(Vec<u8>),
Repeat(u8, usize),
}
/// Encode structured blocks and independently expand their expected bytes.
fn stream(blocks: &[Block]) -> (Vec<u8>, Vec<u8>) {
let mut encoded = vec![0x30, 0, 0, 0];
let mut expected = Vec::new();
for block in blocks {
match block {
Block::Literal(bytes) => {
assert!((1..=128).contains(&bytes.len()));
encoded.push((bytes.len() - 1) as u8);
encoded.extend(bytes);
expected.extend(bytes);
}
Block::Repeat(byte, count) => {
assert!((3..=130).contains(count));
encoded.extend([0x80 | (*count - 3) as u8, *byte]);
expected.extend(std::iter::repeat_n(*byte, *count));
}
}
}
encoded[..4].copy_from_slice(&((expected.len() as u32) << 8 | 0x30).to_le_bytes());
(encoded, expected)
}
fn prepare(service: u8, thumb: bool, destination: u32, data: &[u8]) -> (Machine, u32) {
call_status_data(
service,
thumb,
[ROM_DATA, destination, 0xa55a_1234],
0x1f,
data,
)
}
fn complete(m: &mut Machine, return_pc: u32) {
m.step().unwrap();
reach(m, return_pc, 2_000_000);
}
fn verify(service: u8, thumb: bool, destination: u32, data: &[u8], expected: &[u8]) {
let (mut m, return_pc) = prepare(service, thumb, destination, data);
let before = m.cpu().clone();
let end = destination + expected.len() as u32;
// Halfword sentinels support both RAM and video memory.
for address in [(destination - 1) & !1, end & !1] {
m.memory_mut().write16(address, 0xa55a).unwrap();
}
// The 60-byte SVC frame must not touch the IRQ stack boundary at SVC_SP-64.
m.memory_mut()
.write32(bios::IRQ_STACK, 0x5a5a_a5a5)
.unwrap();
complete(&mut m, return_pc);
for (index, byte) in expected.iter().enumerate() {
assert_eq!(
m.memory().read8(destination + index as u32).unwrap(),
*byte,
"byte={index}"
);
}
for address in [destination - 1, end] {
assert_eq!(
m.memory().read8(address).unwrap(),
if address & 1 == 0 { 0x5a } else { 0xa5 }
);
}
assert_eq!(m.memory().read32(bios::IRQ_STACK).unwrap(), 0x5a5a_a5a5);
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
}
fn fail(m: &mut Machine) -> MachineError {
for _ in 0..2000 {
let before = m.cpu().clone();
let cycles = m.cycles();
let timing = m.last_timing();
if let Err(error) = m.step() {
assert_eq!(m.cpu(), &before);
assert_eq!(m.cycles(), cycles);
assert_eq!(m.last_timing(), timing);
assert_eq!(m.step(), Err(error.clone()));
return error;
}
}
panic!("invalid stream did not reach a bounded diagnostic");
}
fn assert_invalid(error: MachineError) {
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: INVALID_ARGUMENT_TRAP,
..
})
));
}
#[test]
fn every_block_control_value_matches_reference_in_arm_and_thumb() {
for flag in 0u8..=255 {
let count = usize::from(flag & 127) + if flag & 128 == 0 { 1 } else { 3 };
let block = if flag & 128 == 0 {
Block::Literal((0..count).map(|i| (i * 37) as u8).collect())
} else {
Block::Repeat(flag, count)
};
let mut blocks = vec![block];
// The final literal can complete a halfword started by the prior block.
if count % 2 != 0 {
blocks.push(Block::Literal(vec![0x42]));
}
let (data, expected) = stream(&blocks);
assert_eq!(data[4], flag);
for thumb in [false, true] {
verify(0x14, thumb, DEST + 1, &data, &expected);
verify(0x15, thumb, VRAM_START + 4, &data, &expected);
}
}
}
#[test]
fn repeated_values_cover_all_bytes_and_cross_halfword_boundaries() {
let blocks: Vec<_> = (0..=255).map(|byte| Block::Repeat(byte, 3)).collect();
let (data, expected) = stream(&blocks);
verify(0x14, false, DEST, &data, &expected);
verify(0x15, true, DEST, &data, &expected);
verify(0x15, false, VRAM_START + 4, &data, &expected);
}
#[test]
fn seeded_mixed_streams_match_independent_block_expansion() {
let mut seed = 0x83ca_206bu32;
for case in 0..64 {
let mut blocks = Vec::new();
let mut length = 0;
for _ in 0..32 {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
let repeat = seed & 128 != 0;
let count = ((seed >> 8) & 127) as usize + if repeat { 3 } else { 1 };
blocks.push(if repeat {
Block::Repeat((seed >> 24) as u8, count)
} else {
Block::Literal(
(0..count)
.map(|i| (seed.wrapping_add(i as u32 * 71) >> 3) as u8)
.collect(),
)
});
length += count;
}
if length % 2 != 0 {
blocks.push(Block::Literal(vec![0xab]));
}
let (data, expected) = stream(&blocks);
verify(0x14, case % 2 == 0, DEST, &data, &expected);
verify(0x15, case % 2 != 0, VRAM_START + 4, &data, &expected);
}
}
#[test]
fn output_length_uses_all_twenty_four_header_bits() {
let mut blocks = vec![Block::Repeat(0x5a, 130); 504];
blocks.push(Block::Repeat(0xa5, 16)); // 65,536 bytes, crossing the 16-bit count boundary.
let (data, expected) = stream(&blocks);
assert_eq!(expected.len(), 65536);
assert_eq!(&data[..4], &[0x30, 0, 0, 1]);
verify(0x14, false, DEST, &data, &expected);
verify(0x15, true, DEST, &data, &expected);
}
#[test]
fn source_can_be_in_either_work_ram_region() {
let (data, expected) = stream(&[Block::Literal(vec![1, 2, 3]), Block::Repeat(0xaa, 3)]);
for source in [SOURCE, 0x0300_0100] {
for service in [0x14, 0x15] {
let (mut m, return_pc) = call(service, false, [source, DEST, 0]);
for (index, byte) in data.iter().enumerate() {
m.memory_mut().write8(source + index as u32, *byte).unwrap();
}
complete(&mut m, return_pc);
for (index, byte) in expected.iter().enumerate() {
assert_eq!(m.memory().read8(DEST + index as u32).unwrap(), *byte);
}
}
}
}
#[test]
fn user_system_status_and_all_caller_registers_are_restored() {
let (data, _) = stream(&[Block::Repeat(0x88, 3), Block::Literal(vec![0x12])]);
for service in [0x14, 0x15] {
for thumb in [false, true] {
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
let (mut m, return_pc) =
call_status_data(service, thumb, [ROM_DATA, DEST, 0x12345678], status, &data);
let before = m.cpu().clone();
complete(&mut m, return_pc);
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
}
}
}
}
#[test]
fn zero_length_reads_no_blocks_and_does_not_access_destination() {
for service in [0x14, 0x15] {
let (mut m, return_pc) = prepare(service, false, 0x0e00_0000, &[0x30, 0, 0, 0]);
complete(&mut m, return_pc);
}
}
#[test]
fn bad_type_source_alignment_protection_and_output_wrap_are_diagnostics() {
for service in [0x14, 0x15] {
for header in [0u32, 0x0410, 0x0420, 0x0431, 0x04ff] {
let (mut m, _) = prepare(service, false, DEST, &header.to_le_bytes());
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
for source in [
0,
0x3ffc,
0x0100_0000,
ROM_DATA + 1,
ROM_DATA + 2,
ROM_DATA + 3,
] {
let (mut m, _) =
call_status_data(service, false, [source, DEST, 0], 0x1f, &[0x30, 4, 0, 0]);
assert_invalid(fail(&mut m));
}
let (mut m, _) = prepare(service, false, 0xffff_fffe, &[0x30, 4, 0, 0]);
assert_invalid(fail(&mut m));
}
}
#[test]
fn halfword_variant_rejects_odd_size_or_address_before_writing() {
for (destination, data) in [
(VRAM_START + 4, vec![0x30, 3, 0, 0, 0x80, 0x77]),
(VRAM_START + 5, vec![0x30, 4, 0, 0, 0x81, 0x77]),
(VRAM_START + 5, vec![0x30, 0, 0, 0]),
] {
let (mut m, _) = prepare(0x15, true, destination, &data);
m.memory_mut().write32(VRAM_START + 4, 0xcccc_cccc).unwrap();
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(VRAM_START + 4).unwrap(), 0xcccc_cccc);
}
let (data, expected) = stream(&[Block::Repeat(0x77, 3)]);
verify(0x14, true, DEST + 1, &data, &expected);
}
#[test]
fn oversized_blocks_fail_before_payload_reads_or_writes() {
for service in [0x14, 0x15] {
for flag in [1, 0x7f, 0x80, 0xff] {
// One literal succeeds, then the next block exceeds one remaining byte.
// No payload follows the invalid block: its bounds check must fail first.
let data = [0x30, 2, 0, 0, 0, 0x12, flag];
let (mut m, _) = prepare(service, false, DEST, &data);
m.memory_mut().write16(DEST, 0xcccc).unwrap();
assert_invalid(fail(&mut m));
assert_eq!(
m.memory().read16(DEST).unwrap(),
if service == 0x14 { 0xcc12 } else { 0xcccc }
);
}
}
}
#[test]
fn every_truncated_input_prefix_returns_a_memory_diagnostic() {
let (data, _) = stream(&[
Block::Literal(vec![0x12, 0x34, 0x56]),
Block::Repeat(0x78, 3),
]);
for service in [0x14, 0x15] {
for length in 0..data.len() {
let (mut m, _) = prepare(service, false, DEST, &data[..length]);
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::Unmapped(ROM_DATA + length as u32).into()),
"service={service:x}, length={length}"
);
}
}
}
#[test]
fn incomplete_halfwords_are_not_committed_after_source_failure() {
for service in [0x14, 0x15] {
// Three of four literal bytes are available. One halfword can be committed.
let data = [0x30, 4, 0, 0, 3, 0x12, 0x34, 0x56];
let (mut m, _) = prepare(service, false, DEST, &data);
m.memory_mut().write32(DEST, 0xcccc_cccc).unwrap();
assert!(matches!(
fail(&mut m),
MachineError::Cpu(CpuError::Memory(_))
));
assert_eq!(
m.memory().read32(DEST).unwrap(),
if service == 0x14 {
0xcc56_3412
} else {
0xcccc_3412
}
);
}
}
#[test]
fn destination_failure_preserves_earlier_completed_writes() {
let (data, _) = stream(&[Block::Literal(vec![0x12, 0x34, 0x56, 0x78])]);
let (mut m, _) = prepare(0x15, false, 0x07ff_fffe, &data);
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into())
);
assert_eq!(m.memory().read16(0x07ff_fffe).unwrap(), 0x3412);
}
#[test]
fn declared_end_ignores_padding_and_trailing_invalid_blocks() {
let (mut data, expected) = stream(&[Block::Repeat(0xab, 4)]);
data.extend([0x7f, 0xff, 0x80]); // Invalid if read as another block.
for service in [0x14, 0x15] {
verify(service, true, DEST, &data, &expected);
}
}
#[test]
fn byte_and_halfword_variants_use_normal_video_bus_write_rules() {
let (data, _) = stream(&[Block::Literal(vec![0x12, 0x34])]);
for service in [0x14, 0x15] {
for destination in [VRAM_START, OAM_START] {
let (mut m, return_pc) = prepare(service, false, destination, &data);
m.memory_mut().write16(destination, 0xcccc).unwrap();
complete(&mut m, return_pc);
let expected = if service == 0x15 {
0x3412
} else if destination == OAM_START {
0xcccc
} else {
0x3434
};
assert_eq!(m.memory().read16(destination).unwrap(), expected);
}
}
}
#[test]
fn device_clocks_and_dma_continue_while_service_masks_irq_delivery() {
let (data, expected) = stream(&[Block::Repeat(0x55, 130)]);
for service in [0x14, 0x15] {
let (mut m, return_pc) = prepare(service, false, DEST, &data);
m.step().unwrap(); // Enter Supervisor mode before requesting any IRQ.
let start = m.cycles();
m.memory_mut().write16(IE, 8).unwrap();
m.memory_mut().write16(IME, 1).unwrap();
m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap();
m.memory_mut().write32(SOURCE, 0x8765_4321).unwrap();
m.memory_mut().write32(DMA_BASE, SOURCE).unwrap();
m.memory_mut().write32(DMA_BASE + 4, SOURCE + 4).unwrap();
m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap();
let mut dma = 0;
for _ in 0..5000 {
if m.cpu().pc() == return_pc {
break;
}
let step = m.step().unwrap();
assert_ne!(step, StepKind::IrqEntry);
if matches!(step, StepKind::Dma { channel: 0 }) {
dma += 1;
}
}
assert_eq!(m.cpu().pc(), return_pc);
assert_eq!(dma, 1);
assert!(m.cycles() - start > 16);
assert_eq!(m.memory().read32(SOURCE + 4).unwrap(), 0x8765_4321);
assert_eq!(m.memory().read16(IF).unwrap() & 8, 8);
assert_eq!(m.memory().read16(IME).unwrap(), 1);
assert_eq!(m.memory().read16(IRQ_FLAGS).unwrap(), 0);
for (index, byte) in expected.iter().enumerate() {
assert_eq!(m.memory().read8(DEST + index as u32).unwrap(), *byte);
}
}
}

View file

@ -0,0 +1,442 @@
use gba_core::{
io::{BG2CNT, BG2PA, BG2X, BG2Y, DISPCNT, GREENSWAP},
memory::{Memory, OAM_START as OAM, PALETTE_START as PAL, VRAM_START as VRAM},
video::{rgb555_to_rgb888, Framebuffer, VideoError, WIDTH},
};
fn setup(mode: u16) -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.write16(DISPCNT, 0x400 | mode).unwrap();
transform(&mut m, [256, 0, 0, 256], [0, 0]);
m.write16(PAL, 0x4000).unwrap();
for i in 1..256 {
m.write16(PAL + i * 2, (i as u16 * 123) & 0x7fff).unwrap();
}
m
}
fn transform(m: &mut Memory, values: [i16; 4], origin: [i32; 2]) {
for (i, v) in values.into_iter().enumerate() {
m.write16(BG2PA + i as u32 * 2, v as u16).unwrap();
}
m.write32(BG2X, origin[0] as u32).unwrap();
m.write32(BG2Y, origin[1] as u32).unwrap();
}
fn frame(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
m.render_frame(&mut f).unwrap();
f
}
fn pixel(m: &Memory, x: usize, y: usize) -> u32 {
frame(m).pixels()[y * WIDTH + x]
}
fn sprite(m: &mut Memory, mode: u16, priority: u16, affine: bool) {
for i in 0..128 {
m.write16(OAM + i * 8, 0x200).unwrap();
}
m.write16(OAM, if affine { 0x100 } else { 0 }).unwrap();
m.write16(OAM + 4, 512 | (priority << 10)).unwrap();
m.write16(OAM + 6, 256).unwrap();
m.write16(OAM + 30, 256).unwrap();
m.write16(VRAM + 0x14000, 0x1111).unwrap();
m.write16(PAL + 0x202, 0x03e0).unwrap();
m.write16(DISPCNT, 0x1440 | mode).unwrap();
}
#[test]
fn mode4_reads_adjacent_bytes_all_indices_and_both_page_boundaries() {
let mut m = setup(4);
for page in [0, 0xa000] {
m.write16(VRAM + page, 0xff01).unwrap();
m.write16(VRAM + page + 238, 0x0203).unwrap();
m.write16(VRAM + page + 240, 0x0405).unwrap();
m.write16(VRAM + page + 0x95fe, 0x0607).unwrap();
m.write16(DISPCNT, 0x404 | if page == 0 { 0 } else { 0x10 })
.unwrap();
for (x, y, index) in [
(0, 0, 1),
(1, 0, 255),
(239, 0, 2),
(0, 1, 5),
(239, 159, 6),
] {
assert_eq!(pixel(&m, x, y), rgb555_to_rgb888((index * 123) & 0x7fff));
}
}
for i in (0..256).step_by(2) {
m.write16(VRAM + i, (i as u16) | ((i as u16 + 1) << 8))
.unwrap();
}
m.write16(DISPCNT, 0x404).unwrap();
let f = frame(&m);
for i in 0..256 {
assert_eq!(
f.pixels()[i],
rgb555_to_rgb888(if i == 0 {
0x4000
} else {
(i as u16 * 123) & 0x7fff
})
);
}
}
#[test]
fn mode4_page_stride_is_40k_and_inactive_page_writes_do_not_change_output() {
let mut m = setup(4);
m.write16(VRAM, 0x0101).unwrap();
m.write16(VRAM + 0x9600, 0x0303).unwrap(); // Padding is not page1.
let before = frame(&m);
m.write16(VRAM + 0xa000, 0x0202).unwrap();
assert_eq!(frame(&m).pixels(), before.pixels());
m.write8(DISPCNT, 0x14).unwrap(); // Byte write selects page1 without losing BG2 enable.
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(246));
m.write16(PAL + 4, 0x001f).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000); // Palette is live, not cached per page.
m.write8(DISPCNT, 4).unwrap();
assert_eq!(frame(&m).pixels(), before.pixels());
}
#[test]
fn mode5_uses_160_by_128_rgb555_pages_and_leaves_remainder_transparent() {
let mut m = setup(5);
for (page, color) in [(0, 0x801f), (0xa000, 0x83e0)] {
m.write16(VRAM + page, color).unwrap();
m.write16(VRAM + page + 318, 0x7c00).unwrap();
m.write16(VRAM + page + 320, 0x7fff).unwrap();
m.write16(VRAM + page + 0x9ffe, color).unwrap();
}
for (control, color) in [(0x405, 0xff0000), (0x415, 0x00ff00)] {
m.write16(DISPCNT, control).unwrap();
let f = frame(&m);
assert_eq!(f.pixels()[0], color);
assert_eq!(f.pixels()[159], 0x0000ff);
assert_eq!(f.pixels()[WIDTH], 0xffffff);
assert_eq!(f.pixels()[127 * WIDTH + 159], color);
for y in 0..160 {
for x in 0..240 {
if x >= 160 || y >= 128 {
assert_eq!(f.pixels()[y * WIDTH + x], rgb555_to_rgb888(0x4000));
}
}
}
}
}
#[test]
fn mode4_zero_index_is_transparent_but_nonzero_black_is_opaque() {
for affine in [false, true] {
let mut m = setup(4);
sprite(&mut m, 4, 3, affine);
m.write16(VRAM, 0x0100).unwrap();
m.write16(PAL + 2, 0).unwrap();
assert_eq!(pixel(&m, 0, 0), 0x00ff00); // Index0 reveals lower-priority sprite.
assert_eq!(pixel(&m, 1, 0), 0); // Black index1 still covers it.
m.write16(BG2CNT, 3).unwrap();
assert_eq!(pixel(&m, 1, 0), 0x00ff00); // OBJ wins equal priority.
}
}
#[test]
fn mode5_black_is_opaque_and_ignores_palette_while_outside_pixels_reveal_objects() {
let mut m = setup(5);
sprite(&mut m, 5, 3, false);
m.write16(VRAM, 0).unwrap();
m.write16(PAL, 0x7fff).unwrap();
assert_eq!(pixel(&m, 0, 0), 0);
m.write32(BG2X, u32::MAX).unwrap(); // Source X=-1/256 => first output column outside.
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
assert_eq!(pixel(&m, 1, 0), 0);
}
#[test]
fn bitmap_sprites_use_upper_obj_area_independent_of_page() {
for mode in [4, 5] {
let mut m = setup(mode);
sprite(&mut m, mode, 0, false);
m.write16(VRAM + 0x10000, 0x1111).unwrap();
m.write16(OAM + 4, 0).unwrap();
assert_ne!(pixel(&m, 0, 0), 0x00ff00); // Lower512 OBJ tiles unavailable.
m.write16(OAM + 4, 512).unwrap();
for page in [0, 0x10] {
m.write16(DISPCNT, 0x1440 | mode | page).unwrap();
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
}
assert_eq!(m.read16(VRAM + 0x14000).unwrap(), 0x1111);
}
}
#[test]
fn bitmap_byte_writes_duplicate_pixels_but_never_modify_obj_tiles() {
for mode in [4, 5] {
let mut m = setup(mode);
for address in [VRAM, VRAM + 0xa001, VRAM + 0x13fff] {
m.write8(address, 0x5a).unwrap();
assert_eq!(m.read16(address & !1).unwrap(), 0x5a5a);
}
m.write16(VRAM + 0x14000, 0x1234).unwrap();
m.write8(VRAM + 0x1c000, 0xff).unwrap(); // OBJ mirror.
assert_eq!(m.read16(VRAM + 0x14000).unwrap(), 0x1234);
}
}
#[test]
fn affine_bitmap_sampling_matches_wide_integer_reference_on_both_pages() {
for mode in [4, 5] {
let mut m = setup(mode);
let (w, h) = if mode == 4 { (240, 160) } else { (160, 128) };
for page in 0..2_u32 {
if mode == 4 {
for offset in (0..w * h).step_by(2) {
let lo = ((offset * 13 + page * 73) & 255) as u16;
let hi = (((offset + 1) * 13 + page * 73) & 255) as u16;
m.write16(VRAM + page * 0xa000 + offset, lo | (hi << 8))
.unwrap();
}
} else {
for offset in 0..w * h {
m.write16(
VRAM + page * 0xa000 + offset * 2,
((offset * 37 + page * 127) & 0xffff) as u16,
)
.unwrap();
}
}
}
for page in 0..2_u32 {
m.write16(DISPCNT, 0x400 | mode | (page as u16 * 16))
.unwrap();
m.write16(BG2CNT, 0xff8f).unwrap(); // Ignored map/size/overflow/color fields; mosaic clear.
for (coeff, origin) in [
([256, 0, 0, 256], [0, 0]),
([181, 181, -181, 181], [-173, 20001]),
([128, 0, 0, 128], [127, -129]),
(
[-256, 0, 0, -256],
[(w as i32 - 1) * 256, (h as i32 - 1) * 256],
),
([256, 73, -129, 512], [-512, 127]),
([0; 4], [256, 256]),
] {
transform(&mut m, coeff, origin);
let f = frame(&m);
for y in 0..160 {
for x in 0..240 {
let tx = (i64::from(origin[0])
+ i64::from(coeff[0]) * x as i64
+ i64::from(coeff[1]) * y as i64)
.div_euclid(256);
let ty = (i64::from(origin[1])
+ i64::from(coeff[2]) * x as i64
+ i64::from(coeff[3]) * y as i64)
.div_euclid(256);
let color = if tx < 0 || ty < 0 || tx >= i64::from(w) || ty >= i64::from(h)
{
0x4000
} else {
let offset = ty as u32 * w + tx as u32;
if mode == 4 {
let i = ((offset * 13 + page * 73) & 255) as u16;
if i == 0 {
0x4000
} else {
(i * 123) & 0x7fff
}
} else {
((offset * 37 + page * 127) & 0x7fff) as u16
}
};
assert_eq!(
f.pixels()[y * WIDTH + x],
rgb555_to_rgb888(color),
"mode {mode}, page {page}, pixel {x},{y}"
);
}
}
}
}
}
}
#[test]
fn mode5_can_scale_to_full_lcd_without_implicit_stretching() {
let mut m = setup(5);
for offset in 0..160 * 128 {
m.write16(VRAM + offset * 2, 0x001f).unwrap();
}
assert_eq!(pixel(&m, 239, 159), rgb555_to_rgb888(0x4000));
transform(&mut m, [170, 0, 0, 204], [0, 0]); // Approximate 160/240 and 128/160 sampling steps.
assert!(frame(&m).pixels().iter().all(|&p| p == 0xff0000));
}
#[test]
fn bitmap_green_swap_disabled_bg_and_forced_blank_remain_consistent() {
for mode in [4, 5] {
let mut m = setup(mode);
m.write16(PAL + 2, 0x001f).unwrap();
m.write16(PAL + 4, 0x03e0).unwrap();
if mode == 4 {
m.write16(VRAM, 0x0201).unwrap();
} else {
m.write32(VRAM, 0x03e0_001f).unwrap();
}
m.write16(GREENSWAP, 1).unwrap();
assert_eq!(&frame(&m).pixels()[..2], &[0xffff00, 0]);
m.write16(DISPCNT, mode | 0x10).unwrap();
assert!(frame(&m)
.pixels()
.iter()
.all(|&p| p == rgb555_to_rgb888(0x4000)));
m.write16(DISPCNT, 0xffff).unwrap();
assert!(frame(&m).pixels().iter().all(|&p| p == 0xffffff));
}
}
fn assert_demo_frame(
f: &Framebuffer,
mode: gba_demos::bitmap_demo::BitmapMode,
pan: (i32, i32),
rotated: bool,
zoomed: bool,
page: usize,
) {
use gba_demos::bitmap_demo::BitmapMode;
let (w, h) = if mode == BitmapMode::Mode4 {
(240, 160)
} else {
(160, 128)
};
let a = f64::from(match (rotated, zoomed) {
(false, false) => 256,
(false, true) => 128,
(true, false) => 181,
(true, true) => 90,
}) / 256.0;
let b = if rotated { a } else { 0.0 };
for y in 0..160 {
for x in 0..240 {
let dx = x as f64 - 120.0;
let dy = y as f64 - 80.0;
let tx = (f64::from(w / 2 + pan.0) + a * dx + b * dy).floor() as i32;
let ty = (f64::from(h / 2 + pan.1) - b * dx + a * dy).floor() as i32;
let color = if tx < 0 || ty < 0 || tx >= w || ty >= h {
0x4000
} else {
let (tx, ty) = (tx as usize, ty as usize);
if mode == BitmapMode::Mode4 {
let i = (tx / 8 + 3 * (ty / 8) + page * 64) % 256;
if i == 0 {
0x4000
} else {
((i % 32) | ((i / 8 % 32) << 5) | ((i * 3 % 32) << 10)) as u16
}
} else {
(((tx / 5 + page * 11) % 32)
| (((ty / 4 + page * 7) % 32) << 5)
| (((tx / 8 + ty / 8 + page * 5) % 32) << 10)) as u16
}
};
assert_eq!(
f.pixels()[y * WIDTH + x],
rgb555_to_rgb888(color),
"{mode:?} page{page}, pixel{x},{y}, pan={pan:?}"
);
}
}
}
#[test]
fn cpu_demos_copy_both_pages_and_flip_during_vblank_every_32_updates() {
use gba_core::input::Buttons;
use gba_demos::bitmap_demo::{BitmapDemo, BitmapMode, BITMAP_STATE};
for mode in [BitmapMode::Mode4, BitmapMode::Mode5] {
let mut demo = BitmapDemo::new(mode).unwrap();
let mut f = Framebuffer::default();
assert_eq!(demo.machine().memory().read16(DISPCNT).unwrap(), 0);
assert_eq!(demo.machine().memory().read16(VRAM + 0xa000).unwrap(), 0);
for updates in 1..=65 {
let before = demo.machine().memory().display_position();
demo.frame(Buttons::default(), &mut f).unwrap();
let page = (updates / 32) % 2;
let m = demo.machine().memory();
assert_eq!(
m.read16(DISPCNT).unwrap(),
0x400 | mode as u16 | (page as u16 * 16)
);
assert_eq!(m.read32(BITMAP_STATE).unwrap(), updates as u32);
assert_eq!(m.display_position().scanline, 160);
if updates > 1 {
assert_eq!(m.display_position().vblanks, before.vblanks + 1);
}
assert_demo_frame(&f, mode, (0, 0), false, false, page);
}
}
}
#[test]
fn cpu_bitmap_controls_pan_rotate_zoom_force_page_and_reset() {
use gba_core::input::{Button, Buttons};
use gba_demos::bitmap_demo::{BitmapDemo, BitmapMode, BITMAP_STATE};
for mode in [BitmapMode::Mode4, BitmapMode::Mode5] {
let mut demo = BitmapDemo::new(mode).unwrap();
let mut f = Framebuffer::default();
demo.frame(
Buttons::default()
.with(Button::Left, true)
.with(Button::Up, true),
&mut f,
)
.unwrap();
demo.frame(Buttons::from_bits(0xf0), &mut f).unwrap();
for bits in 0..8 {
let rotated = bits & 1 != 0;
let zoomed = bits & 2 != 0;
let page = usize::from(bits & 4 != 0);
demo.frame(
Buttons::default()
.with(Button::L, rotated)
.with(Button::R, zoomed)
.with(Button::A, page != 0),
&mut f,
)
.unwrap();
assert_demo_frame(&f, mode, (-2, -2), rotated, zoomed, page);
assert_eq!(
demo.machine().memory().read32(BITMAP_STATE + 4).unwrap() as i32,
-2
);
assert_eq!(
demo.machine().memory().read32(BITMAP_STATE + 8).unwrap() as i32,
-2
);
}
demo.frame(
Buttons::default()
.with(Button::Start, true)
.with(Button::Right, true),
&mut f,
)
.unwrap();
assert_demo_frame(&f, mode, (0, 0), false, false, 0);
assert_eq!(demo.machine().memory().read32(BITMAP_STATE + 4).unwrap(), 0);
assert_eq!(demo.machine().memory().read32(BITMAP_STATE + 8).unwrap(), 0);
}
}
#[test]
fn unsupported_bitmap_layers_leave_frame_unchanged() {
for mode in [4, 5] {
let mut m = setup(mode);
let mut f = Framebuffer::default();
f.clear(0x001f);
for extra in [0x100, 0x200, 0x800] {
let control = 0x410 | mode | extra;
m.write16(DISPCNT, control).unwrap();
assert_eq!(
m.render_frame(&mut f),
Err(VideoError::UnsupportedLayers(control))
);
}
assert!(f.pixels().iter().all(|&p| p == 0xff0000));
assert_eq!(m.cycles(), 0);
}
}

View file

@ -0,0 +1,177 @@
use gba_core::{
cpu::{Cpu, CpuError, InstructionSet},
memory::{Memory, MemoryError, ROM_START},
};
use gba_demos::{demo_program, DEMO_STEPS};
fn program(words: &[u32]) -> Memory {
Memory::new(words.iter().flat_map(|word| word.to_le_bytes()).collect()).unwrap()
}
#[test]
fn demo_computes_values_and_loops() {
let mut memory = Memory::new(demo_program()).unwrap();
let mut cpu = Cpu::new(ROM_START);
let mut saw_thumb = false;
for _ in 0..DEMO_STEPS + 5 {
saw_thumb |= cpu.instruction_set() == InstructionSet::Thumb;
cpu.step(&mut memory).unwrap();
}
assert!(saw_thumb);
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
assert_eq!(&cpu.registers()[..3], &[42, 7, 42]);
assert_eq!(cpu.pc(), ROM_START + 96);
assert_eq!(cpu.registers()[3], 0x0200_0000);
assert_eq!(&cpu.registers()[4..8], &[42, 84, 3528, 42]);
assert_eq!(memory.read32(0x0200_0000).unwrap(), 3528);
assert_eq!(memory.read32(0x0200_0004).unwrap(), 42);
assert_eq!(cpu.registers()[13], 0x0300_0100);
assert_eq!(cpu.registers()[14], ROM_START + 109);
assert_eq!(memory.read32(0x0300_00f8).unwrap(), 42);
assert_eq!(memory.read32(0x0300_00fc).unwrap(), ROM_START + 109);
assert!(cpu.flags().zero);
assert!(cpu.flags().carry);
}
#[test]
fn memory_is_little_endian() {
let memory = program(&[0x1234_5678]);
assert_eq!(memory.read8(ROM_START).unwrap(), 0x78);
assert_eq!(memory.read32(ROM_START).unwrap(), 0x1234_5678);
}
#[test]
fn ram_is_zeroed_writable_and_mirrored() {
let mut memory = Memory::new(vec![]).unwrap();
for (base, size) in [(0x0200_0000, 0x40000), (0x0300_0000, 0x8000)] {
assert_eq!(memory.read32(base).unwrap(), 0);
memory.write8(base, 0x12).unwrap();
memory.write8(base + 1, 0x34).unwrap();
assert_eq!(memory.read32(base + size).unwrap(), 0x3412);
memory.write8(base + size - 1, 0xab).unwrap();
assert_eq!(memory.read8(base + size * 2 - 1).unwrap(), 0xab);
}
}
#[test]
fn cartridge_has_three_windows() {
let memory = program(&[0x1234_5678]);
for address in [0x0800_0000, 0x0a00_0000, 0x0c00_0000] {
assert_eq!(memory.read32(address).unwrap(), 0x1234_5678);
}
}
#[test]
fn invalid_memory_accesses_return_errors() {
let mut memory = program(&[0]);
assert_eq!(memory.read8(0), Err(MemoryError::Unmapped(0)));
assert_eq!(
memory.read32(ROM_START + 1),
Err(MemoryError::Unaligned(ROM_START + 1))
);
assert_eq!(
memory.read8(ROM_START + 4),
Err(MemoryError::Unmapped(ROM_START + 4))
);
assert_eq!(
memory.write8(ROM_START, 1),
Err(MemoryError::ReadOnly(ROM_START))
);
assert_eq!(memory.write8(0, 1), Err(MemoryError::Unmapped(0)));
assert_eq!(
memory.read32(0xffff_fffc),
Err(MemoryError::Unmapped(0xffff_fffc))
);
}
#[test]
fn oversized_rom_is_rejected() {
assert!(matches!(
Memory::new(vec![0; 32 * 1024 * 1024 + 1]),
Err(MemoryError::RomTooLarge(_))
));
}
#[test]
fn rotated_immediate_is_decoded() {
let mut memory = program(&[0xe3a0_0480]); // MOV r0, #0x80000000
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[0], 0x8000_0000);
}
#[test]
fn arithmetic_wraps_at_32_bits() {
let mut memory = program(&[
0xe240_0001, // SUB r0, r0, #1
0xe280_1001, // ADD r1, r0, #1
]);
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[0], u32::MAX);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[1], 0);
}
#[test]
fn reading_pc_as_operand_includes_pipeline_offset() {
let mut memory = program(&[0xe28f_0004]); // ADD r0, pc, #4
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[0], ROM_START + 12);
}
#[test]
fn branches_use_signed_displacement_and_pipeline_offset() {
let mut memory = program(&[
0xea00_0000, // B +8
0xe3a0_0063, // skipped
0xeaff_fffc, // B -8, back to start
]);
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START + 8);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START);
assert_eq!(cpu.registers()[0], 0);
}
#[test]
fn unsupported_instructions_do_not_change_cpu() {
for instruction in [
0xf3a0_0001, // reserved condition
0xe340_0001, // CMP encoding with S=0
0xe3b0_f001, // MOVS pc, #1 (status restore unsupported)
0xe10f_f000, // MRS pc, CPSR
0xe8f0_0001, // LDM user-bank transfer with writeback
0xe100_f090, // SWP with PC destination
0xe14f_0000, // MRS SPSR in System mode
0xe16f_0f10, // CLZ (ARMv5)
0xe12f_ff30, // BLX (ARMv5)
] {
let mut memory = program(&[instruction]);
let mut cpu = Cpu::new(ROM_START);
let before = *cpu.registers();
let flags_before = cpu.flags();
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::UnsupportedInstruction {
address: ROM_START,
instruction
}),
);
assert_eq!(cpu.registers(), &before);
assert_eq!(cpu.flags(), flags_before);
}
}
#[test]
fn fetch_errors_do_not_advance_pc() {
let mut memory = Memory::new(vec![]).unwrap();
let mut cpu = Cpu::new(ROM_START);
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(ROM_START)))
);
assert_eq!(cpu.pc(), ROM_START);
}

View file

@ -0,0 +1,449 @@
use gba_core::{
cpu::{Cpu, Mode},
display::{CYCLES_PER_FRAME, CYCLES_PER_LINE, HBLANK_START, LINES_PER_FRAME, VBLANK_START},
io::{DISPCNT, DISPSTAT, IE, IF, IME, TIMER_BASE, VCOUNT},
machine::{FrameRunError, Machine, StepKind},
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START},
};
fn memory() -> Memory {
Memory::new(vec![]).unwrap()
}
#[test]
fn display_starts_at_line_zero_with_comparison_match_but_no_irq() {
let bus = memory();
assert_eq!(bus.read16(DISPSTAT).unwrap(), 4);
assert_eq!(bus.read16(VCOUNT).unwrap(), 0);
assert_eq!(bus.read16(IF).unwrap(), 0);
assert_eq!(bus.display_position().scanline, 0);
assert_eq!(bus.display_position().line_cycle, 0);
assert_eq!(bus.display_position().frames, 0);
assert_eq!(bus.display_position().vblanks, 0);
assert_eq!(bus.cycles(), 0);
}
#[test]
fn hblank_status_starts_at_1006_not_at_the_last_visible_pixel() {
let mut bus = memory();
bus.advance_cycles(960);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 2, 0);
bus.advance_cycles(45);
assert_eq!(bus.display_position().line_cycle, 1005);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 2, 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 2, 2);
bus.advance_cycles(225);
assert_eq!(bus.read16(VCOUNT).unwrap(), 0);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 2, 2);
bus.advance_cycles(1);
assert_eq!(bus.read16(VCOUNT).unwrap(), 1);
assert_eq!(bus.display_position().line_cycle, 0);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 7, 0);
}
#[test]
fn vblank_starts_at_160_clears_at_227_and_line_count_wraps_at_228() {
let mut bus = memory();
bus.advance_cycles(VBLANK_START - 1);
assert_eq!(bus.read16(VCOUNT).unwrap(), 159);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 3, 2);
bus.advance_cycles(1);
assert_eq!(bus.read16(VCOUNT).unwrap(), 160);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 3, 1);
assert_eq!(bus.display_position().vblanks, 1);
bus.advance_cycles(67 * CYCLES_PER_LINE - 1);
assert_eq!(bus.read16(VCOUNT).unwrap(), 226);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 3, 3);
bus.advance_cycles(1);
assert_eq!(bus.read16(VCOUNT).unwrap(), 227);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 3, 0);
assert_eq!(bus.display_position().frames, 0);
bus.advance_cycles(CYCLES_PER_LINE);
assert_eq!(bus.read32(DISPSTAT).unwrap(), 4); // Compare zero matches again
assert_eq!(bus.display_position().frames, 1);
assert_eq!(bus.display_position().vblanks, 1);
assert_eq!(bus.cycles(), 280_896);
}
#[test]
fn hblank_irq_occurs_on_every_line_including_hidden_lines() {
let mut bus = memory();
bus.write16(DISPSTAT, 0x10).unwrap();
for line in 0..LINES_PER_FRAME {
bus.advance_cycles(HBLANK_START - 1);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(VCOUNT).unwrap(), line as u16);
assert_eq!(bus.read16(IF).unwrap(), 2);
bus.write16(IF, 2).unwrap();
bus.advance_cycles(CYCLES_PER_LINE - HBLANK_START);
assert_eq!(bus.read16(IF).unwrap(), 0);
}
assert_eq!(bus.display_position().frames, 1);
}
#[test]
fn vblank_and_compare_requests_latch_without_ie_or_ime_and_acknowledge_independently() {
let mut bus = memory();
bus.write16(DISPSTAT, 0xa028).unwrap(); // VBlank + VCount=160
bus.advance_cycles(VBLANK_START);
assert_eq!(bus.read16(IF).unwrap(), 5);
assert!(!bus.irq_pending());
bus.write16(IE, 5).unwrap();
assert!(!bus.irq_pending());
bus.write32(IME, 1).unwrap();
assert!(bus.irq_pending());
bus.write16(IF, 1).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 4);
assert!(bus.irq_pending());
bus.write16(IF, 4).unwrap();
assert!(!bus.irq_pending());
bus.advance_cycles(0);
bus.advance_cycles(10);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(CYCLES_PER_FRAME - 10);
assert_eq!(bus.read16(IF).unwrap(), 5);
}
#[test]
fn register_masks_read_only_fields_and_word_accesses_are_coherent() {
let mut bus = memory();
bus.advance_cycles(17 * CYCLES_PER_LINE + HBLANK_START);
bus.write32(DISPSTAT, u32::MAX).unwrap();
assert_eq!(bus.read16(DISPSTAT).unwrap(), 0xff3a);
assert_eq!(bus.read16(VCOUNT).unwrap(), 17);
assert_eq!(bus.read32(DISPSTAT).unwrap(), 0x0011_ff3a);
bus.write8(VCOUNT, 200).unwrap();
bus.write8(VCOUNT + 1, 0xff).unwrap();
assert_eq!(bus.read16(VCOUNT).unwrap(), 17);
bus.write8(DISPSTAT, 0).unwrap();
assert_eq!(bus.read16(DISPSTAT).unwrap(), 0xff02);
bus.write8(DISPSTAT + 1, 17).unwrap();
assert_eq!(bus.read16(DISPSTAT).unwrap(), 0x1106);
assert_eq!(bus.read8(DISPSTAT).unwrap(), 6);
assert_eq!(bus.read8(DISPSTAT + 1).unwrap(), 17);
assert_eq!(bus.read8(VCOUNT + 1).unwrap(), 0);
assert_eq!(bus.read16(IF).unwrap(), 0);
assert_eq!(
bus.write16(DISPSTAT + 1, 0),
Err(MemoryError::Unaligned(DISPSTAT + 1))
);
assert_eq!(bus.read16(DISPSTAT).unwrap(), 0x1106);
}
#[test]
fn writing_comparator_can_create_one_immediate_rising_match_request() {
let mut bus = memory();
bus.advance_cycles(7 * CYCLES_PER_LINE);
bus.write16(DISPSTAT, 0x0620).unwrap();
assert_eq!(bus.read16(DISPSTAT).unwrap() & 4, 0);
bus.write8(DISPSTAT + 1, 7).unwrap();
assert_eq!(bus.read16(DISPSTAT).unwrap() & 4, 4);
assert_eq!(bus.read16(IF).unwrap(), 4);
bus.write16(IF, 4).unwrap();
bus.write8(DISPSTAT + 1, 7).unwrap(); // Already matches
bus.write8(DISPSTAT, 0).unwrap();
bus.write8(DISPSTAT, 0x20).unwrap(); // Enabling alone does not create a match edge
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.write8(DISPSTAT + 1, 6).unwrap();
bus.write8(DISPSTAT + 1, 7).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 4);
}
#[test]
fn zero_comparator_irq_waits_for_the_next_line_zero_entry() {
let mut bus = memory();
bus.write16(DISPSTAT, 0x20).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(CYCLES_PER_FRAME - 1);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 4);
bus.write16(IF, 4).unwrap();
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 0);
}
#[test]
fn out_of_range_comparator_values_never_match() {
let mut bus = memory();
for compare in 228..=255 {
bus.write16(DISPSTAT, (compare << 8) | 0x20).unwrap();
bus.advance_cycles(CYCLES_PER_FRAME);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 4, 0);
assert_eq!(bus.read16(IF).unwrap(), 0);
}
}
#[test]
fn enabling_vblank_or_hblank_irq_does_not_replay_an_active_status() {
let mut bus = memory();
bus.advance_cycles(VBLANK_START);
bus.write16(DISPSTAT, 8).unwrap();
bus.advance_cycles(0);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(CYCLES_PER_FRAME - 1);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 1);
let mut bus = memory();
bus.advance_cycles(HBLANK_START);
bus.write16(DISPSTAT, 0x10).unwrap();
bus.advance_cycles(CYCLES_PER_LINE - 1);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 2);
}
#[test]
fn forced_blank_and_display_mode_changes_do_not_stop_or_reset_the_clock() {
let mut bus = memory();
bus.write16(DISPCNT, 0x80).unwrap();
bus.write16(DISPSTAT, 0x18).unwrap();
bus.advance_cycles(VBLANK_START);
let position = bus.display_position();
assert_eq!(bus.read16(IF).unwrap(), 3);
bus.write16(DISPCNT, 0x403).unwrap();
assert_eq!(bus.display_position(), position);
bus.advance_cycles(CYCLES_PER_FRAME);
assert_eq!(bus.display_position().vblanks, 2);
assert_eq!(bus.display_position().scanline, 160);
}
#[test]
fn timer_and_display_events_can_latch_in_the_same_clock_batch() {
let mut bus = memory();
let reload = 65536 - HBLANK_START;
bus.write32(TIMER_BASE, 0x00c0_0000 | reload).unwrap();
bus.write16(DISPSTAT, 0x10).unwrap();
bus.advance_cycles(HBLANK_START);
assert_eq!(bus.read16(IF).unwrap(), 0x0a);
bus.write16(IF, 2).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 8);
}
#[test]
fn bulk_clock_advances_match_a_cycle_by_cycle_reference() {
for compare in [0_u16, 1, 160, 226, 227, 255] {
let mut bus = memory();
bus.write16(DISPSTAT, (compare << 8) | 0x38).unwrap();
let mut line = 0_u16;
let mut cycle = 0_u16;
let mut frames = 0_u64;
let mut vblanks = 0_u64;
for chunk in [
0,
1,
959,
46,
226,
17_777,
CYCLES_PER_FRAME * 2 + 99,
1232,
77,
] {
let mut pending = 0;
for _ in 0..chunk {
cycle += 1;
if cycle == 1006 {
pending |= 2;
}
if cycle == 1232 {
cycle = 0;
line += 1;
if line == 228 {
line = 0;
frames += 1;
}
if line == 160 {
vblanks += 1;
pending |= 1;
}
if line == compare {
pending |= 4;
}
}
}
bus.advance_cycles(chunk);
assert_eq!(
bus.read16(IF).unwrap(),
pending,
"compare {compare}, chunk {chunk}"
);
let flags = u16::from((160..227).contains(&line))
| (u16::from(cycle >= 1006) << 1)
| (u16::from(line == compare) << 2);
assert_eq!(bus.read16(DISPSTAT).unwrap(), (compare << 8) | 0x38 | flags);
let position = bus.display_position();
assert_eq!(
(
position.scanline,
position.line_cycle,
position.frames,
position.vblanks
),
(line, cycle, frames, vblanks)
);
bus.write16(IF, 7).unwrap();
}
}
}
#[test]
fn maximum_batches_preserve_multi_frame_counts_and_do_not_overflow_u32() {
let mut bus = memory();
bus.write16(DISPSTAT, 0xa038).unwrap();
for multiplier in 1..=2_u64 {
bus.advance_cycles(u32::MAX);
let total = multiplier * u64::from(u32::MAX);
let phase = total % u64::from(CYCLES_PER_FRAME);
let position = bus.display_position();
assert_eq!(position.frames, total / u64::from(CYCLES_PER_FRAME));
assert_eq!(
position.vblanks,
(total + u64::from(CYCLES_PER_FRAME - VBLANK_START)) / u64::from(CYCLES_PER_FRAME)
);
assert_eq!(
u64::from(position.scanline),
phase / u64::from(CYCLES_PER_LINE)
);
assert_eq!(
u64::from(position.line_cycle),
phase % u64::from(CYCLES_PER_LINE)
);
assert_eq!(bus.read16(IF).unwrap(), 7);
assert_eq!(bus.cycles(), total);
bus.write16(IF, 7).unwrap();
}
}
#[test]
fn display_sources_enter_an_original_irq_handler_acknowledge_and_return() {
for (control, source, edge) in [
(8, 1, VBLANK_START),
(0x10, 2, HBLANK_START),
(0xa020, 4, VBLANK_START),
] {
let mut bios = vec![0; BIOS_SIZE];
for (index, instruction) in [0xe1c1_20b2_u32, 0xe280_0001, 0xe25e_f004]
.into_iter()
.enumerate()
{
// STRH r2,[r1,#2] (IF); ADD r0,r0,#1; SUBS pc,lr,#4
let offset = 0x18 + index * 4;
bios[offset..offset + 4].copy_from_slice(&instruction.to_le_bytes());
}
let rom = [
0xe3a0_1301_u32,
0xe281_1c02,
0xe3a0_2000 | source,
0xe283_3001,
0xeaff_fffe,
]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect();
let mut bus = Memory::with_bios(rom, bios).unwrap();
let mut cpu = Cpu::new(ROM_START);
for _ in 0..3 {
cpu.step(&mut bus).unwrap();
}
bus.write16(DISPSTAT, control).unwrap();
bus.write16(IE, source as u16).unwrap();
bus.write32(IME, 1).unwrap();
bus.advance_cycles(edge - 1);
let mut machine = Machine::new(cpu, bus);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[3], 1);
assert_eq!(machine.memory().read16(IF).unwrap(), source as u16);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().mode(), Mode::Irq);
assert_eq!(machine.cpu().pc(), 0x18);
assert_eq!(machine.cpu().registers()[14], ROM_START + 20);
for _ in 0..3 {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
assert_eq!(machine.cpu().registers()[0], 1);
assert_eq!(machine.cpu().mode(), Mode::System);
assert_eq!(machine.cpu().pc(), ROM_START + 16);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
}
}
#[test]
fn failed_block_store_does_not_change_window_controls_or_advance_display() {
let rom = [
0xe3a0_2301_u32, // MOV r2,#0x04000000
0xe282_2048, // ADD r2,r2,#0x48 (WININ/WINOUT)
0xe3a0_003f, // MOV r0,#0x3f
0xe3a0_1001, // MOV r1,#1
0xe8a2_001f, // STMIA r2!,{r0-r4}; last word is unmapped 0x04000058
]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect();
let mut bus = Memory::new(rom).unwrap();
let mut cpu = Cpu::new(ROM_START);
for _ in 0..4 {
cpu.step(&mut bus).unwrap();
}
bus.advance_cycles(7 * CYCLES_PER_LINE);
let before = cpu.clone();
let position = bus.display_position();
let mut machine = Machine::new(cpu, bus);
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.memory().display_position(), position);
assert_eq!(machine.memory().read32(gba_core::io::WININ).unwrap(), 0);
assert_eq!(machine.memory().read16(DISPSTAT).unwrap(), 0);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
}
#[test]
fn frame_runner_waits_for_next_vblank_event_not_an_already_set_flag() {
let mut bus = memory();
bus.write32(0x0300_0000, 0xeaff_fffe).unwrap(); // 3-cycle IWRAM self-branch
bus.advance_cycles(VBLANK_START - 2);
let mut machine = Machine::new(Cpu::new(0x0300_0000), bus);
assert_eq!(machine.run_until_vblank(1).unwrap(), 1);
assert_eq!(machine.memory().display_position().line_cycle, 1);
assert_eq!(machine.memory().display_position().vblanks, 1);
let steps = machine
.run_until_vblank((CYCLES_PER_FRAME / 3) as usize)
.unwrap();
assert_eq!(steps, (CYCLES_PER_FRAME / 3) as usize);
assert_eq!(machine.memory().display_position().line_cycle, 1);
assert_eq!(machine.memory().display_position().vblanks, 2);
}
#[test]
fn frame_runner_limits_and_cpu_errors_preserve_completed_progress_only() {
let mut bus = memory();
bus.write32(0x0300_0000, 0xeaff_fffe).unwrap();
let mut machine = Machine::new(Cpu::new(0x0300_0000), bus);
assert_eq!(
machine.run_until_vblank(0),
Err(FrameRunError::StepLimit(0))
);
assert_eq!(machine.cycles(), 0);
assert_eq!(
machine.run_until_vblank(2),
Err(FrameRunError::StepLimit(2))
);
assert_eq!(machine.cycles(), 6);
assert_eq!(machine.memory().display_position().line_cycle, 6);
machine
.memory_mut()
.write32(0x0300_0000, 0xf000_0000)
.unwrap();
let position = machine.memory().display_position();
assert!(matches!(
machine.run_until_vblank(10),
Err(FrameRunError::Cpu(_))
));
assert_eq!(machine.memory().display_position(), position);
assert_eq!(machine.cycles(), 6);
}

View file

@ -0,0 +1,860 @@
use gba_core::{
cpu::{Cpu, Mode},
display::{CYCLES_PER_FRAME, CYCLES_PER_LINE, HBLANK_START, VBLANK_START},
dma::{DmaError, DMA_BASE, DMA_STRIDE},
io::{DISPCNT, DISPSTAT, IE, IF, IME, TIMER_BASE, WAITCNT},
machine::{FrameRunError, Machine, MachineError, StepKind},
memory::{Memory, MemoryError, BIOS_SIZE, PALETTE_START, ROM_START, VRAM_START},
timing::{bus_cycles, AccessKind, AccessWidth},
};
const SOURCE: u32 = 0x0200_0000;
const DEST: u32 = 0x0300_0000;
fn words(values: &[u32]) -> Vec<u8> {
values
.iter()
.flat_map(|value| value.to_le_bytes())
.collect()
}
fn machine() -> Machine {
Machine::new(
Cpu::new(ROM_START),
Memory::new(words(&[0xeaff_fffe])).unwrap(),
)
}
fn base(channel: usize) -> u32 {
DMA_BASE + channel as u32 * DMA_STRIDE
}
fn configure(
bus: &mut Memory,
channel: usize,
source: u32,
destination: u32,
count: u16,
control: u16,
) {
bus.write32(base(channel), source).unwrap();
bus.write32(base(channel) + 4, destination).unwrap();
bus.write32(
base(channel) + 8,
u32::from(count) | (u32::from(control | 0x8000) << 16),
)
.unwrap();
}
fn advance_to(machine: &mut Machine, cycle: u64) {
let elapsed = (cycle - machine.cycles()) as u32;
machine.memory_mut().advance_cycles(elapsed);
}
fn enabled(machine: &Machine, channel: usize) -> bool {
machine.memory().read16(base(channel) + 10).unwrap() & 0x8000 != 0
}
fn units(machine: &mut Machine, channel: usize, count: usize) {
let cpu = machine.cpu().clone();
for _ in 0..count {
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel });
assert_eq!(machine.last_timing().code_cycles, 0);
assert_eq!(machine.cpu(), &cpu);
}
}
#[test]
fn reset_register_layout_masks_and_write_only_readback() {
let mut bus = Memory::new(vec![]).unwrap();
for channel in 0..4 {
for offset in 0..12 {
assert_eq!(bus.read8(base(channel) + offset).unwrap(), 0);
bus.write8(base(channel) + offset, 0xff).unwrap();
}
assert_eq!(bus.read32(base(channel)).unwrap(), 0);
assert_eq!(bus.read32(base(channel) + 4).unwrap(), 0);
assert_eq!(bus.read16(base(channel) + 8).unwrap(), 0);
assert_eq!(
bus.read16(base(channel) + 10).unwrap(),
if channel == 3 { 0xffe0 } else { 0xf7e0 }
);
assert_eq!(
bus.read32(base(channel) + 8).unwrap(),
if channel == 3 {
0xffe0_0000
} else {
0xf7e0_0000
}
);
bus.write8(base(channel) + 11, 0).unwrap();
assert_eq!(bus.read16(base(channel) + 10).unwrap(), 0xe0);
}
assert_eq!(
bus.read8(DMA_BASE - 1),
Err(MemoryError::Unmapped(DMA_BASE - 1))
);
assert_eq!(
bus.read8(DMA_BASE + 48),
Err(MemoryError::Unmapped(DMA_BASE + 48))
);
assert_eq!(
bus.read16(DMA_BASE + 1),
Err(MemoryError::Unaligned(DMA_BASE + 1))
);
assert_eq!(
bus.write32(DMA_BASE + 10, 1),
Err(MemoryError::Unaligned(DMA_BASE + 10))
);
assert_eq!(
bus.read16(DMA_BASE + 0x400),
Err(MemoryError::Unmapped(DMA_BASE + 0x400))
);
}
#[test]
fn immediate_halfword_and_word_copies_pause_cpu_until_completion() {
for channel in 0..4 {
for word in [false, true] {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0x4433_2211).unwrap();
machine
.memory_mut()
.write32(SOURCE + 4, 0x8877_6655)
.unwrap();
configure(
machine.memory_mut(),
channel,
SOURCE,
DEST,
if word { 2 } else { 4 },
if word { 0x400 } else { 0 },
);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0); // Register writes do not execute DMA.
assert_eq!(machine.cycles(), 0);
units(&mut machine, channel, if word { 2 } else { 4 });
assert!(!enabled(&machine, channel));
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x4433_2211);
assert_eq!(machine.memory().read32(DEST + 4).unwrap(), 0x8877_6655);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.cycles(), if word { 16 } else { 18 });
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
}
}
#[test]
fn source_and_destination_address_modes_match_reference_copies() {
for word in [false, true] {
let width = if word { 4 } else { 2 };
for source_mode in 0..3 {
for dest_mode in 0..4 {
let mut machine = machine();
let mut expected = [0_u32; 9];
for index in 0..9 {
machine
.memory_mut()
.write32(SOURCE + index * 4, 0x0123_0123 * (index + 1))
.unwrap();
}
let mut source = SOURCE + 16;
let mut destination = DEST + 16;
// Read the source and simulate the destination independently of DMA state.
let mut expected_bytes = [0_u8; 36];
for _ in 0..4 {
for byte in 0..width {
expected_bytes[(destination - DEST + byte) as usize] =
machine.memory().read8(source + byte).unwrap();
}
source = match source_mode {
0 => source + width,
1 => source - width,
_ => source,
};
destination = match dest_mode {
0 | 3 => destination + width,
1 => destination - width,
_ => destination,
};
}
for (index, chunk) in expected_bytes.chunks_exact(4).enumerate() {
expected[index] = u32::from_le_bytes(chunk.try_into().unwrap());
}
let control = source_mode << 7 | dest_mode << 5 | if word { 0x400 } else { 0 };
configure(machine.memory_mut(), 3, SOURCE + 16, DEST + 16, 4, control);
units(&mut machine, 3, 4);
for (index, value) in expected.into_iter().enumerate() {
assert_eq!(
machine.memory().read32(DEST + index as u32 * 4).unwrap(),
value
);
}
}
}
}
}
#[test]
fn addresses_mask_channel_bits_and_align_to_transfer_width() {
for channel in 0..4 {
for word in [false, true] {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0xaabb_ccdd).unwrap();
let source_high = if channel == 0 {
0xf800_0000
} else {
0xf000_0000
};
let dest_high = if channel == 3 {
0xf000_0000
} else {
0xf800_0000
};
configure(
machine.memory_mut(),
channel,
SOURCE | source_high | 3,
DEST | dest_high | 3,
1,
if word { 0x400 } else { 0 },
);
units(&mut machine, channel, 1);
assert_eq!(
machine.memory().read32(DEST).unwrap(),
if word { 0xaabb_ccdd } else { 0xaabb_0000 }
);
}
}
}
#[test]
fn zero_count_means_channel_maximum_and_count_high_bits_are_masked() {
for (channel, count, expected) in [
(0, 0, 0x4000),
(1, 0xc000, 0x4000),
(2, 0xc003, 3),
(3, 0, 0x10000),
] {
let mut machine = machine();
machine.memory_mut().write16(SOURCE, 0x5a5a).unwrap();
configure(machine.memory_mut(), channel, SOURCE, DEST, count, 0x140); // Both addresses fixed.
units(&mut machine, channel, expected - 1);
assert!(enabled(&machine, channel));
units(&mut machine, channel, 1);
assert!(!enabled(&machine, channel));
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x5a5a);
assert_eq!(machine.cycles(), expected as u64 * 4 + 2);
}
}
#[test]
fn immediate_repeat_completes_once_and_does_not_stall_forever() {
let mut machine = machine();
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, 0x200);
units(&mut machine, 0, 1);
assert!(!enabled(&machine, 0));
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
#[test]
fn address_and_count_latches_change_only_on_enable_edge() {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0x2222_1111).unwrap();
machine.memory_mut().write16(SOURCE + 16, 0x3333).unwrap();
configure(machine.memory_mut(), 2, SOURCE, DEST, 2, 0);
machine.memory_mut().write32(base(2), SOURCE + 16).unwrap();
machine
.memory_mut()
.write32(base(2) + 4, DEST + 16)
.unwrap();
machine.memory_mut().write16(base(2) + 8, 1).unwrap();
machine.memory_mut().write16(base(2) + 10, 0x8000).unwrap(); // Still enabled, not a new edge.
units(&mut machine, 2, 2);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x2222_1111);
assert_eq!(machine.memory().read16(DEST + 16).unwrap(), 0);
machine.memory_mut().write16(base(2) + 10, 0x8000).unwrap();
units(&mut machine, 2, 1);
assert_eq!(machine.memory().read16(DEST + 16).unwrap(), 0x3333);
assert!(!enabled(&machine, 2));
}
#[test]
fn disabling_cancels_pending_dma_and_reenable_reloads_latches() {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0xabcd_1234).unwrap();
configure(machine.memory_mut(), 1, SOURCE, DEST, 2, 0);
units(&mut machine, 1, 1);
machine.memory_mut().write8(base(1) + 11, 0).unwrap();
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.memory().read16(DEST + 2).unwrap(), 0);
machine.memory_mut().write8(base(1) + 11, 0x80).unwrap();
units(&mut machine, 1, 2);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0xabcd_1234);
}
#[test]
fn simultaneous_channels_run_in_priority_order_before_cpu_or_irq() {
let mut machine = machine();
machine.memory_mut().write16(IE, 0x0f00).unwrap();
machine.memory_mut().write16(IME, 1).unwrap();
for channel in (0..4).rev() {
machine
.memory_mut()
.write16(SOURCE + channel as u32 * 2, channel as u16 + 1)
.unwrap();
configure(
machine.memory_mut(),
channel,
SOURCE + channel as u32 * 2,
DEST,
1,
0x4000,
);
}
for channel in 0..4 {
units(&mut machine, channel, 1);
assert_eq!(machine.memory().read16(DEST).unwrap(), channel as u16 + 1);
assert_eq!(
machine.memory().read16(IF).unwrap(),
((1 << (channel + 1)) - 1) << 8
);
}
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
}
#[test]
fn display_request_preempts_lower_priority_dma_at_unit_boundary() {
let mut machine = machine();
machine.memory_mut().write16(SOURCE, 0x1111).unwrap();
machine.memory_mut().write16(SOURCE + 2, 0x3333).unwrap();
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, 0x2000); // HBlank.
configure(machine.memory_mut(), 3, SOURCE + 2, DEST, 3, 0x140); // Immediate fixed fill.
machine.memory_mut().advance_cycles(HBLANK_START - 1);
units(&mut machine, 3, 1); // Crosses HBlank with DMA0 pending.
units(&mut machine, 0, 1);
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x1111);
units(&mut machine, 3, 2); // Resumes the lower-priority block.
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x3333);
assert!(!enabled(&machine, 3));
}
#[test]
fn blank_requests_do_not_depend_on_dispstat_or_forced_blank() {
for (timing, edge) in [(0x1000, VBLANK_START), (0x2000, HBLANK_START)] {
let mut machine = machine();
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, timing);
machine.memory_mut().write16(DISPCNT, 0x80).unwrap();
machine.memory_mut().advance_cycles(edge - 1);
assert_eq!(machine.memory().read16(DISPSTAT).unwrap() & 0x38, 0);
assert_eq!(machine.step().unwrap(), StepKind::Instruction); // Clock crosses edge.
units(&mut machine, 0, 1);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
}
}
#[test]
fn enabling_during_blank_waits_for_next_edge_and_zero_advance_does_not_trigger() {
for (timing, edge, period) in [
(0x1000, VBLANK_START, CYCLES_PER_FRAME),
(0x2000, HBLANK_START, CYCLES_PER_LINE),
] {
let mut machine = machine();
machine.memory_mut().advance_cycles(edge);
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, timing);
machine.memory_mut().advance_cycles(0);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
let elapsed = (machine.cycles() - u64::from(edge)) as u32;
machine.memory_mut().advance_cycles(period - elapsed);
units(&mut machine, 0, 1);
}
}
#[test]
fn hblank_dma_runs_on_all_visible_lines_but_not_hidden_lines() {
let mut machine = machine();
configure(machine.memory_mut(), 1, SOURCE, DEST, 1, 0x2340); // Repeat HBlank, fixed addresses.
machine.memory_mut().write16(DISPSTAT, 0x10).unwrap();
for line in 0..228_u64 {
let edge = line * u64::from(CYCLES_PER_LINE) + u64::from(HBLANK_START);
advance_to(&mut machine, edge);
assert_eq!(machine.memory().read16(IF).unwrap(), 2); // HBlank IRQ still occurs on hidden lines.
machine.memory_mut().write16(IF, 2).unwrap();
if line < 160 {
units(&mut machine, 1, 1);
} else {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
}
let edge = u64::from(CYCLES_PER_FRAME + HBLANK_START);
advance_to(&mut machine, edge);
units(&mut machine, 1, 1);
}
#[test]
fn vblank_repeat_preserves_source_reloads_destination_and_count() {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0x2222_1111).unwrap();
machine
.memory_mut()
.write32(SOURCE + 4, 0x4444_3333)
.unwrap();
configure(machine.memory_mut(), 3, SOURCE, DEST, 2, 0x5260); // VBlank, repeat, dest reload, IRQ.
machine.memory_mut().advance_cycles(VBLANK_START);
units(&mut machine, 3, 2);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x2222_1111);
assert!(enabled(&machine, 3));
assert_eq!(machine.memory().read16(IF).unwrap(), 0x800);
machine.memory_mut().write16(IF, 0x800).unwrap();
assert_eq!(machine.step().unwrap(), StepKind::Instruction); // Same VBlank does not retrigger.
let next = u64::from(VBLANK_START + CYCLES_PER_FRAME);
advance_to(&mut machine, next);
units(&mut machine, 3, 2);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x4444_3333);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x800);
assert!(enabled(&machine, 3));
}
#[test]
fn repeated_increment_destination_continues_and_reload_uses_programmed_values() {
for reload in [false, true] {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0x2222_1111).unwrap();
machine
.memory_mut()
.write32(SOURCE + 4, 0x4444_3333)
.unwrap();
configure(
machine.memory_mut(),
0,
SOURCE,
DEST,
1,
0x1200 | if reload { 0x60 } else { 0 },
);
// Initial count/address are latched. Repeat reloads use newly programmed values.
machine.memory_mut().write16(base(0) + 8, 2).unwrap();
machine
.memory_mut()
.write32(base(0) + 4, DEST + 16)
.unwrap();
machine.memory_mut().advance_cycles(VBLANK_START);
units(&mut machine, 0, 1);
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x1111);
let next = u64::from(VBLANK_START + CYCLES_PER_FRAME);
advance_to(&mut machine, next);
units(&mut machine, 0, 2);
let address = if reload { DEST + 16 } else { DEST + 2 };
assert_eq!(machine.memory().read16(address).unwrap(), 0x2222);
assert_eq!(machine.memory().read16(address + 2).unwrap(), 0x3333);
}
}
#[test]
fn pending_requests_coalesce_and_busy_channel_does_not_queue_repeats() {
let mut machine = machine();
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, 0x1200);
machine.memory_mut().advance_cycles(CYCLES_PER_FRAME * 3);
units(&mut machine, 0, 1);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
let mut machine = self::machine();
configure(machine.memory_mut(), 0, SOURCE, DEST, 100, 0x2200); // Takes 402 cycles, beyond HBlank.
machine.memory_mut().advance_cycles(HBLANK_START);
units(&mut machine, 0, 99);
// Inject more display edges while the block is still active; no queued repeat.
machine.memory_mut().advance_cycles(CYCLES_PER_FRAME * 2);
units(&mut machine, 0, 1);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
#[test]
fn timer_and_display_clocks_continue_during_dma_and_latch_all_irqs() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_ffff)
.unwrap();
machine.memory_mut().write16(DISPSTAT, 0x10).unwrap();
machine.memory_mut().advance_cycles(HBLANK_START - 2);
machine.memory_mut().write16(IF, 0x3fff).unwrap();
configure(machine.memory_mut(), 2, SOURCE, DEST, 1, 0x4000);
let before = machine.cycles();
units(&mut machine, 2, 1);
assert_eq!(machine.cycles(), before + 6);
assert_eq!(
machine.memory().display_position().line_cycle,
HBLANK_START as u16 + 4
);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x40a);
assert!(!machine.memory().irq_pending()); // IE/IME are off, requests still latch.
machine.memory_mut().write16(IE, 0x400).unwrap();
machine.memory_mut().write16(IME, 1).unwrap();
assert!(machine.memory().irq_pending());
}
#[test]
fn dma_completion_irq_enters_handler_acknowledges_and_returns_without_skipping_cpu() {
for channel in 0..4 {
let mask = 1_u16 << (8 + channel);
let mut bios = vec![0; BIOS_SIZE];
bios[0x18..0x24].copy_from_slice(&words(&[
0xe1c1_20b2, // STRH r2,[r1,#2] (IF)
0xe280_0001, // ADD r0,r0,#1
0xe25e_f004, // SUBS pc,lr,#4
]));
let rom = words(&[
0xe59f_1008, // LDR r1,[pc,#8] (IE)
0xe59f_2008, // LDR r2,[pc,#8] (mask)
0xe283_3001, // ADD r3,r3,#1 (interrupted instruction)
0xeaff_fffe,
IE,
u32::from(mask),
]);
let mut bus = Memory::with_bios(rom, bios).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut bus).unwrap();
cpu.step(&mut bus).unwrap();
configure(&mut bus, channel, SOURCE, DEST, 2, 0x4000);
bus.write16(IE, mask).unwrap();
bus.write16(IME, 1).unwrap();
let mut machine = Machine::new(cpu, bus);
units(&mut machine, channel, 1);
assert_eq!(machine.memory().read16(IF).unwrap(), 0); // Not yet complete.
units(&mut machine, channel, 1);
assert_eq!(machine.memory().read16(IF).unwrap(), mask);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().mode(), Mode::Irq);
assert_eq!(machine.cpu().registers()[14], ROM_START + 12);
for _ in 0..3 {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.cpu().mode(), Mode::System);
assert_eq!(machine.cpu().pc(), ROM_START + 8);
assert_eq!(machine.cpu().registers()[0], 1);
assert_eq!(machine.cpu().registers()[3], 0);
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[3], 1);
}
}
#[test]
fn rom_dma_uses_waitcnt_sequential_costs_and_forces_source_increment() {
for channel in 1..4 {
for source_mode in 0..3 {
for (window, waitcnt) in [(0x0800_0000, 0), (0x0a00_0000, 0x7fff), (0x0c00_0000, 0)] {
for word in [false, true] {
let mut bus = Memory::new(words(&[0x2222_1111, 0x4444_3333])).unwrap();
bus.write16(WAITCNT, waitcnt).unwrap();
let width = if word {
AccessWidth::Word
} else {
AccessWidth::Halfword
};
let stride = if word { 4 } else { 2 };
configure(
&mut bus,
channel,
window,
DEST,
2,
source_mode << 7 | if word { 0x400 } else { 0 },
);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
for index in 0..2 {
units(&mut machine, channel, 1);
let expected = bus_cycles(
machine.memory().waitcnt(),
window + index * stride,
width,
if index == 0 {
AccessKind::NonSequential
} else {
AccessKind::Sequential
},
) + 1;
assert_eq!(machine.last_timing().data_cycles, expected);
assert_eq!(
machine.last_timing().internal_cycles,
if index == 0 { 2 } else { 0 }
);
}
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x2222_1111);
if word {
assert_eq!(machine.memory().read32(DEST + 4).unwrap(), 0x4444_3333);
}
}
}
}
}
}
#[test]
fn rom_boundary_forces_nonsequential_timing_in_middle_of_dma() {
let mut machine = Machine::new(
Cpu::new(ROM_START),
Memory::new(vec![0x5a; 0x20004]).unwrap(),
);
configure(machine.memory_mut(), 3, ROM_START + 0x1fffe, DEST, 3, 0);
for expected in [8, 6, 4] {
// First: 5+1+2; boundary: 5+1; then 3+1.
units(&mut machine, 3, 1);
assert_eq!(machine.last_timing().total(), expected);
}
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x5a5a_5a5a);
}
#[test]
fn overlapping_copies_read_each_unit_after_previous_write() {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0x2222_1111).unwrap();
configure(machine.memory_mut(), 0, SOURCE, SOURCE + 2, 3, 0);
units(&mut machine, 0, 3);
for offset in [0, 2, 4, 6] {
assert_eq!(machine.memory().read16(SOURCE + offset).unwrap(), 0x1111);
}
}
#[test]
fn video_palette_mirrors_and_mapped_io_use_the_normal_bus() {
for (destination, alias) in [
(VRAM_START + 0x18000, VRAM_START + 0x10000),
(PALETTE_START + 0x400, PALETTE_START),
(DISPCNT, DISPCNT),
] {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0x0001_0403).unwrap();
configure(machine.memory_mut(), 3, SOURCE, destination, 1, 0x400);
units(&mut machine, 3, 1);
assert_eq!(machine.memory().read32(alias).unwrap(), 0x0001_0403);
}
}
#[test]
fn unsupported_controls_are_explicit_and_leave_state_and_time_unchanged() {
for channel in 0..4 {
for control in [0x180, 0x3000, 0x3800] {
let mut machine = machine();
configure(machine.memory_mut(), channel, SOURCE, DEST, 1, control);
let before = machine.cpu().clone();
let stored = machine.memory().read16(base(channel) + 10).unwrap();
for _ in 0..2 {
assert_eq!(
machine.step(),
Err(MachineError::Dma(DmaError::UnsupportedControl {
channel,
control: stored
}))
);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.last_timing().total(), 0);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0);
assert!(enabled(&machine, channel));
}
}
}
let mut machine = machine();
configure(machine.memory_mut(), 3, SOURCE, DEST, 1, 0x800); // DRQ even without special timing.
assert!(matches!(
machine.step(),
Err(MachineError::Dma(DmaError::UnsupportedControl { .. }))
));
}
#[test]
fn invalid_memory_and_dma_register_destinations_are_diagnostics() {
for (source, destination, error) in [
(
0,
DEST,
DmaError::UnsupportedSource {
channel: 3,
address: 0,
},
),
(
0x0e00_0000,
DEST,
DmaError::Memory {
channel: 3,
error: MemoryError::Unmapped(0x0e00_0000),
},
),
(
SOURCE,
ROM_START,
DmaError::Memory {
channel: 3,
error: MemoryError::ReadOnly(ROM_START),
},
),
(
SOURCE,
0x0e00_0000,
DmaError::Memory {
channel: 3,
error: MemoryError::Unmapped(0x0e00_0000),
},
),
(
SOURCE,
DMA_BASE,
DmaError::RegisterDestination {
channel: 3,
address: DMA_BASE,
},
),
] {
let mut machine = machine();
configure(machine.memory_mut(), 3, source, destination, 1, 0x4000);
let before = machine.cpu().clone();
assert_eq!(machine.step(), Err(MachineError::Dma(error)));
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert!(enabled(&machine, 3));
}
}
#[test]
fn failing_later_unit_preserves_earlier_data_but_not_partial_current_unit() {
let mut machine = Machine::new(
Cpu::new(ROM_START),
Memory::new(vec![0x11, 0x22, 0x33]).unwrap(),
);
configure(machine.memory_mut(), 3, ROM_START, DEST, 2, 0x4000);
units(&mut machine, 3, 1);
let timing = machine.last_timing();
let cycles = machine.cycles();
let position = machine.memory().display_position();
for _ in 0..2 {
assert_eq!(
machine.step(),
Err(MachineError::Dma(DmaError::Memory {
channel: 3,
error: MemoryError::Unmapped(ROM_START + 3)
}))
);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x2211);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().display_position(), position);
assert_eq!(machine.last_timing(), timing);
assert!(enabled(&machine, 3));
}
}
#[test]
fn failed_io_write_does_not_partially_change_display_control() {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0xffff_ffff).unwrap();
configure(machine.memory_mut(), 3, SOURCE, 0x0400_0058, 1, 0x400);
assert!(machine.step().is_err());
assert_eq!(machine.memory().read16(DISPCNT).unwrap(), 0);
assert_eq!(machine.cycles(), 0);
}
#[test]
fn cpu_can_enable_dma_with_a_combined_count_control_store() {
let rom = words(&[
0xe59f_0010, // LDR r0,[pc,#16] (DMA3CNT)
0xe59f_1010, // LDR r1,[pc,#16] (count/control)
0xe580_1000, // STR r1,[r0]
0xe282_2001, // ADD r2,r2,#1 (must wait for transfer)
0xeaff_fffe,
0xe1a0_0000,
base(3) + 8,
0x8400_0002,
]);
let mut bus = Memory::new(rom).unwrap();
bus.write32(SOURCE, 0x1234_5678).unwrap();
bus.write32(base(3), SOURCE).unwrap();
bus.write32(base(3) + 4, DEST).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
for _ in 0..3 {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
assert_eq!(machine.cpu().pc(), ROM_START + 12);
units(&mut machine, 3, 2);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x1234_5678);
assert_eq!(machine.cpu().registers()[2], 0);
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[2], 1);
}
#[test]
fn failed_cpu_block_store_cannot_enable_dma_or_change_its_hidden_latches() {
let rom = words(&[
0xe59f_000c, // LDR r0,[pc,#12] (DMA3CNT)
0xe59f_100c, // LDR r1,[pc,#12] (count/control)
0xe8a0_0006, // STMIA r0!,{r1,r2}; second word unmapped.
0xeaff_fffe,
0xe1a0_0000,
base(3) + 8,
0x8400_0001,
]);
let mut bus = Memory::new(rom).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut bus).unwrap();
cpu.step(&mut bus).unwrap();
configure(&mut bus, 3, SOURCE, DEST, 2, 0);
bus.write16(base(3) + 10, 0).unwrap();
let before = cpu.clone();
let mut machine = Machine::new(cpu, bus);
assert!(matches!(machine.step(), Err(MachineError::Cpu(_))));
assert_eq!(machine.cpu(), &before);
assert!(!enabled(&machine, 3));
assert_eq!(machine.cycles(), 0);
machine.memory_mut().write16(base(3) + 10, 0x8000).unwrap();
units(&mut machine, 3, 2); // The count latch is still 2, not 1.
assert!(!enabled(&machine, 3));
}
#[test]
fn cpu_only_and_clock_only_calls_never_execute_dma() {
let mut bus = Memory::new(words(&[0xe280_0001])).unwrap();
bus.write16(SOURCE, 0x1234).unwrap();
configure(&mut bus, 0, SOURCE, DEST, 1, 0);
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut bus).unwrap();
bus.advance_cycles(CYCLES_PER_FRAME);
assert_eq!(cpu.registers()[0], 1);
assert_eq!(bus.read16(DEST).unwrap(), 0);
let mut machine = Machine::new(cpu, bus);
units(&mut machine, 0, 1);
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x1234);
}
#[test]
fn frame_runner_counts_dma_units_obeys_limits_and_reports_dma_errors() {
let mut machine = machine();
configure(machine.memory_mut(), 0, SOURCE, DEST, 3, 0);
assert_eq!(
machine.run_until_vblank(2),
Err(FrameRunError::StepLimit(2))
);
assert_eq!(machine.cpu().pc(), ROM_START);
assert_eq!(machine.cycles(), 10);
machine.memory_mut().advance_cycles(VBLANK_START - 11);
assert_eq!(machine.run_until_vblank(1).unwrap(), 1);
assert_eq!(machine.memory().display_position().scanline, 160);
assert_eq!(machine.cpu().pc(), ROM_START);
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, 0x3000);
assert!(matches!(
machine.run_until_vblank(1),
Err(FrameRunError::Dma(_))
));
}
#[test]
fn vblank_runner_stops_at_event_before_newly_requested_dma_executes() {
let mut machine = machine();
machine.memory_mut().write16(SOURCE, 0x1234).unwrap();
configure(machine.memory_mut(), 3, SOURCE, DEST, 1, 0x1000);
machine.memory_mut().advance_cycles(VBLANK_START - 1);
assert_eq!(machine.run_until_vblank(1).unwrap(), 1);
assert_eq!(machine.memory().read16(DEST).unwrap(), 0);
units(&mut machine, 3, 1);
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x1234);
}

View file

@ -0,0 +1,61 @@
use gba_core::{
io::*,
memory::{Memory, MemoryError, OAM_START as OAM, PALETTE_START as PAL, VRAM_START as VRAM},
video::{rgb555_to_rgb888 as rgb, Framebuffer, HEIGHT, WIDTH},
};
#[path = "effects/colors.rs"]
mod colors;
#[path = "effects/demo.rs"]
mod demo;
#[path = "effects/windows.rs"]
mod windows;
fn setup() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.write16(DISPCNT, 0x700).unwrap();
m.write16(PAL, 0x4210).unwrap();
for (bg, color) in [0x001f, 0x03e0, 0x7c00].into_iter().enumerate() {
let bg = bg as u32;
m.write16(PAL + (bg + 1) * 2, color).unwrap();
m.write16(BG0CNT + bg * 2, (((24 + bg) << 8) | (bg << 2) | bg) as u16)
.unwrap();
for offset in (0..32).step_by(2) {
m.write16(VRAM + bg * 0x4000 + offset, (bg as u16 + 1) * 0x1111)
.unwrap();
}
}
for obj in 0..128 {
m.write16(OAM + obj * 8, 0x200).unwrap();
}
for offset in (0..32).step_by(2) {
m.write16(VRAM + 0x10000 + offset, 0x1111).unwrap();
}
m.write16(PAL + 0x202, 0x7fff).unwrap();
m
}
fn render(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
m.render_frame(&mut f).unwrap();
f
}
fn pixel(m: &Memory, x: usize, y: usize) -> u32 {
render(m).pixels()[y * WIDTH + x]
}
fn object(m: &mut Memory, index: u32, a: u16, b: u16, c: u16) {
m.write16(OAM + index * 8, a).unwrap();
m.write16(OAM + index * 8 + 2, b).unwrap();
m.write16(OAM + index * 8 + 4, c).unwrap();
}
fn window(m: &mut Memory, h: u16, v: u16, inside: u16, outside: u16) {
m.write16(DISPCNT, m.read16(DISPCNT).unwrap() | 0x2000)
.unwrap();
m.write16(WIN0H, h).unwrap();
m.write16(WIN0V, v).unwrap();
m.write16(WININ, inside).unwrap();
m.write16(WINOUT, outside).unwrap();
}

View file

@ -0,0 +1,172 @@
use super::*;
#[test]
fn alpha_uses_five_bit_channels_clamps_coefficients_and_saturates_sums() {
let mut m = setup();
m.write16(BLDCNT, 0x0241).unwrap(); // BG0 with BG1.
for (coefficients, expected) in [
(0, 0),
(0x0808, 0x01ef),
(0x0010, 31),
(0x1000, 0x3e0),
(0x1f1f, 0x3ff),
] {
m.write16(BLDALPHA, coefficients).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(expected));
}
m.write16(PAL + 2, 0x7fff).unwrap();
m.write16(PAL + 4, 0x7fff).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x7fff));
m.write16(PAL + 2, 1).unwrap();
m.write16(PAL + 4, 1).unwrap();
m.write16(BLDALPHA, 0x0808).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(1)); // Sum fractional terms before rounding.
}
#[test]
fn alpha_requires_top_first_target_and_immediate_second_target() {
let mut m = setup();
m.write16(BLDALPHA, 0x0808).unwrap();
for control in [0x0441, 0x0242, 0x0142, 0x2041, 0x0240] {
m.write16(BLDCNT, control).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(31), "control={control:x}");
}
m.write16(BLDCNT, 0x0441).unwrap();
m.write16(VRAM + 0x4000, 0).unwrap(); // Transparent BG1 exposes BG2.
assert_eq!(pixel(&m, 0, 0), rgb(0x3c0f));
assert_eq!(pixel(&m, 4, 0), rgb(31));
}
#[test]
fn windows_remove_layers_before_selecting_blend_targets() {
let mut m = setup();
m.write16(BLDALPHA, 0x0808).unwrap();
m.write16(BLDCNT, 0x0441).unwrap();
window(&mut m, 0x0008, 0x0008, 0x25, 0x3f); // Hide BG1 only inside.
assert_eq!(pixel(&m, 0, 0), rgb(0x3c0f));
assert_eq!(pixel(&m, 8, 0), rgb(31));
}
#[test]
fn brightness_uses_selected_top_layer_and_clamped_write_only_coefficient() {
let mut m = setup();
m.write16(PAL + 2, 0x4210).unwrap();
for (control, coefficient, expected) in [
(0x81, 0, 0x4210),
(0x81, 8, 0x5ef7),
(0x81, 31, 0x7fff),
(0xc1, 8, 0x2108),
(0xc1, 16, 0),
(0xc1, 31, 0),
(0x82, 16, 0x4210),
(0x01, 16, 0x4210),
] {
m.write16(BLDCNT, control).unwrap();
m.write32(BLDY, coefficient).unwrap();
assert_eq!(m.read16(BLDY).unwrap(), 0);
assert_eq!(pixel(&m, 0, 0), rgb(expected));
}
m.write16(BLDCNT, 0x81).unwrap();
m.write8(BLDY, 8).unwrap();
m.write8(BLDY + 1, 0xff).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x5ef7));
}
#[test]
fn backdrop_is_a_target_but_cannot_blend_with_itself() {
let mut m = setup();
m.write16(DISPCNT, 0).unwrap();
m.write16(BLDCNT, 0x2060).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x4210));
m.write16(BLDCNT, 0xa0).unwrap();
m.write16(BLDY, 16).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x7fff));
m.write16(DISPCNT, 0x100).unwrap();
m.write16(BLDCNT, 0x2041).unwrap();
m.write16(BLDALPHA, 0x1000).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x4210)); // Backdrop is not pre-brightened.
}
#[test]
fn object_can_be_first_or_second_target_but_never_blends_with_another_object() {
let mut m = setup();
m.write16(DISPCNT, 0x1100).unwrap();
object(&mut m, 0, 0, 0, 0);
object(&mut m, 1, 0, 0, 0xc00); // Lower priority; never a second OBJ target.
m.write16(BLDALPHA, 0x0808).unwrap();
m.write16(BLDCNT, 0x0150).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x3dff)); // White OBJ + red BG0.
m.write16(BLDCNT, 0x1050).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x7fff)); // No OBJ-to-OBJ blend.
object(&mut m, 0, 0, 0, 0x400); // OBJ0 below BG0; later OBJ still excluded.
m.write16(BLDCNT, 0x1041).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x3dff));
}
#[test]
fn semi_transparent_obj_overrides_mode_and_first_target_selection() {
let mut m = setup();
m.write16(DISPCNT, 0x1100).unwrap();
object(&mut m, 0, 0x400, 0, 0);
m.write16(BLDALPHA, 0x0808).unwrap();
m.write16(BLDY, 16).unwrap();
for mode in 0..4 {
m.write16(BLDCNT, 0x100 | (mode << 6)).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x3dff));
}
window(&mut m, 0x0004, 0x0008, 0x11, 0x31);
assert_eq!(pixel(&m, 0, 0), rgb(0x7fff)); // Window can disable forced alpha.
assert_eq!(pixel(&m, 4, 0), rgb(0x3dff));
}
#[test]
fn semi_transparent_obj_falls_back_to_selected_brightness_without_second_target() {
let mut m = setup();
m.write16(DISPCNT, 0x1100).unwrap();
object(&mut m, 0, 0x400, 0, 0);
m.write16(BLDY, 16).unwrap();
for (control, expected) in [(0xc0, 0x7fff), (0xd0, 0), (0x10, 0x7fff), (0x1d0, 0)] {
m.write16(BLDCNT, control).unwrap();
// Last case has a second target; zero alpha coefficients produce black.
assert_eq!(pixel(&m, 0, 0), rgb(expected));
}
m.write16(BLDALPHA, 0x0808).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x3dff)); // Alpha takes precedence over brightness.
}
#[test]
fn color_effects_work_in_affine_and_all_bitmap_modes() {
for mode in 1..=5 {
let mut m = setup();
m.write16(DISPCNT, 0x400 | mode).unwrap();
m.write16(BG2CNT, 0).unwrap();
m.write16(BG2PA, 256).unwrap();
m.write16(BG2PD, 256).unwrap();
if mode <= 2 {
m.write16(BG2CNT, 0x1000).unwrap(); // Byte map at 0x8000, tile0 at zero.
m.write16(VRAM + 0x8000, 0).unwrap();
m.write16(VRAM, 0x0101).unwrap();
} else {
m.write16(VRAM, if mode == 4 { 0x0101 } else { 31 })
.unwrap();
}
m.write16(BLDCNT, 0x2044).unwrap();
m.write16(BLDALPHA, 0x1000).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x4210), "mode={mode}");
window(&mut m, 0x0001, 0x0001, 4, 0x24);
assert_eq!(pixel(&m, 0, 0), rgb(31));
}
}
#[test]
fn green_swap_follows_effects_and_forced_blank_bypasses_every_effect() {
let mut m = setup();
m.write16(PAL + 2, 0x3e0).unwrap();
m.write16(BLDCNT, 0xc1).unwrap();
m.write16(BLDY, 16).unwrap();
window(&mut m, 0x0001, 0x0001, 1, 0x21);
m.write16(GREENSWAP, 1).unwrap();
assert_eq!(&render(&m).pixels()[..2], &[0, 0xff00]);
m.write16(DISPCNT, 0xffff).unwrap();
assert!(render(&m).pixels().iter().all(|&p| p == 0xffffff));
}

View file

@ -0,0 +1,166 @@
use super::*;
use gba_core::input::{Button, Buttons};
use gba_demos::{
effects_demo::{EffectsDemo, EFFECTS_STATE},
tile_demo::TileDemo,
};
#[test]
fn cpu_demo_moves_wraps_cancels_resets_and_updates_effect_registers_during_vblank() {
let mut demo = EffectsDemo::new().unwrap();
let mut frame = Framebuffer::default();
assert_eq!(demo.machine().memory().read16(DISPCNT).unwrap(), 0);
let mut expected_x = 0u32;
let mut expected_y = 0u32;
let sequence = [
Buttons::default(),
Buttons::default()
.with(Button::Left, true)
.with(Button::Up, true),
Buttons::default()
.with(Button::Right, true)
.with(Button::Down, true),
Buttons::default()
.with(Button::Right, true)
.with(Button::Left, true),
Buttons::default().with(Button::A, true),
Buttons::default().with(Button::B, true),
Buttons::default()
.with(Button::A, true)
.with(Button::B, true),
Buttons::default()
.with(Button::Start, true)
.with(Button::Right, true),
Buttons::default(),
];
for (i, buttons) in sequence.into_iter().enumerate() {
let before = demo.machine().cycles();
demo.frame(buttons, &mut frame).unwrap();
let memory = demo.machine().memory();
// Explicit expected movements keep this independent of the ARM program.
if i == 1 {
expected_x = 510;
expected_y = 510;
}
if i == 2 || i == 7 {
expected_x = 0;
expected_y = 0;
}
assert_eq!(memory.read32(EFFECTS_STATE).unwrap(), i as u32 + 1);
assert_eq!(memory.read32(EFFECTS_STATE + 4).unwrap(), expected_x);
assert_eq!(memory.read32(EFFECTS_STATE + 8).unwrap(), expected_y);
assert_eq!(memory.read16(DISPCNT).unwrap(), 0x3340);
assert_eq!(memory.read16(WININ).unwrap(), 0x1f);
assert_eq!(memory.read16(WINOUT).unwrap(), 0x3f);
assert_eq!(memory.read16(BLDALPHA).unwrap(), 0x808);
assert_eq!(
memory.read16(BLDCNT).unwrap(),
match i {
4 | 6 => 0x251,
5 => 0xff,
_ => 0xbf,
}
);
assert_eq!(memory.display_position().scanline, 160);
assert!(demo.machine().cycles() > before);
// The center is in WIN0 and therefore unchanged by any effect.
assert_eq!(frame.pixels()[80 * WIDTH + 120], rgb(0x7fff));
}
}
#[test]
fn cpu_brightness_demo_matches_tile_scene_inside_and_outside_moving_window() {
let mut demo = EffectsDemo::new().unwrap();
let mut base = TileDemo::new().unwrap();
let mut actual = Framebuffer::default();
let mut original = Framebuffer::default();
// Include wrapped scroll, affine objects, and brightness changes.
for (index, buttons) in [
Buttons::default()
.with(Button::Left, true)
.with(Button::Up, true),
Buttons::default()
.with(Button::B, true)
.with(Button::L, true),
Buttons::default()
.with(Button::B, true)
.with(Button::R, true),
Buttons::default().with(Button::Start, true),
]
.into_iter()
.enumerate()
{
demo.frame(buttons, &mut actual).unwrap();
base.frame(buttons, &mut original).unwrap();
let (left, top) = if index < 3 { (62, 38) } else { (64, 40) };
for y in 0..HEIGHT {
for x in 0..WIDTH {
let old = original.pixels()[y * WIDTH + x];
// Recover the exact five-bit channels from RGB888.
let channels = [(old >> 19) & 31, (old >> 11) & 31, (old >> 3) & 31];
let mut expected = 0u16;
for (channel, shift) in channels.into_iter().zip([0, 5, 10]) {
let changed = if (left..left + 112).contains(&x) && (top..top + 80).contains(&y)
{
channel
} else if index == 1 || index == 2 {
channel - channel / 2
} else {
channel + (31 - channel) / 2
};
expected |= (changed as u16) << shift;
}
assert_eq!(
actual.pixels()[y * WIDTH + x],
rgb(expected),
"frame={index} pixel={x},{y}"
);
}
}
}
}
#[test]
fn cpu_window_demo_retains_active_region_when_bottom_moves_beyond_vcount_range() {
let mut demo = EffectsDemo::new().unwrap();
let mut frame = Framebuffer::default();
for index in 0..54 {
demo.frame(Buttons::default().with(Button::Down, true), &mut frame)
.unwrap();
// x=80 avoids foreground crosses and sprites. It stays inside WIN0's X bounds.
let sy = 2 * (index + 1);
let raw: u16 =
[0x260, 0x3a0, 0x7d20, 0x7e80][((80 / 32 + sy / 32) % 2) * 2 + (80 / 2 + sy / 2) % 2];
let expected = if index == 53 {
// New bottom=228 cannot match any line. Last frame's active flag survives.
raw
} else {
[0, 5, 10].into_iter().fold(0, |value, shift| {
let channel = (raw >> shift) & 31;
value | ((channel + (31 - channel) / 2) << shift)
})
};
assert_eq!(frame.pixels()[80], rgb(expected), "frame={index}");
}
let mut snapshot = Framebuffer::default();
demo.machine().memory().render_frame(&mut snapshot).unwrap();
assert_ne!(snapshot.pixels()[80], frame.pixels()[80]);
}
#[test]
fn dma_word_write_updates_selection_and_coefficients_together() {
use gba_core::{
cpu::Cpu,
dma::DMA_BASE,
machine::{Machine, StepKind},
};
let mut m = setup();
m.write32(0x0200_0000, 0x0808_0241).unwrap();
m.write32(DMA_BASE + 36, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 40, BLDCNT).unwrap();
m.write32(DMA_BASE + 44, 0x8400_0001).unwrap();
let mut machine = Machine::new(Cpu::new(0x0300_0000), m);
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
assert_eq!(machine.memory().read32(BLDCNT).unwrap(), 0x0808_0241);
assert_eq!(pixel(machine.memory(), 0, 0), rgb(0x1ef));
}

View file

@ -0,0 +1,176 @@
use super::*;
#[test]
fn register_masks_byte_merges_and_write_only_reads() {
let mut m = setup();
for (address, mask) in [
(WININ, 0x3f3f),
(WINOUT, 0x3f3f),
(BLDCNT, 0x3fff),
(BLDALPHA, 0x1f1f),
] {
assert_eq!(m.read16(address).unwrap(), 0);
m.write16(address, 0xffff).unwrap();
assert_eq!(m.read16(address).unwrap(), mask);
m.write8(address, 0).unwrap();
assert_eq!(m.read16(address).unwrap(), mask & 0xff00);
m.write8(address + 1, 0).unwrap();
assert_eq!(m.read16(address).unwrap(), 0);
}
for address in [WIN0H, WIN1H, WIN0V, WIN1V, BLDY] {
m.write16(address, 0xffff).unwrap();
assert_eq!(m.read16(address).unwrap(), 0);
}
m.write32(BLDY, u32::MAX).unwrap();
assert_eq!(m.read32(BLDY).unwrap(), 0);
assert_eq!(
m.read8(0x0400_0058),
Err(MemoryError::Unmapped(0x0400_0058))
);
assert_eq!(
m.read8(0x0401_0048),
Err(MemoryError::Unmapped(0x0401_0048))
);
assert_eq!(
m.write16(WININ + 1, 1),
Err(MemoryError::Unaligned(WININ + 1))
);
}
#[test]
fn no_enabled_windows_ignore_zero_masks_but_apply_effects() {
let mut m = setup();
m.write16(BLDCNT, 0xc1).unwrap();
m.write16(BLDY, 16).unwrap();
assert_eq!(pixel(&m, 0, 0), 0);
m.write16(DISPCNT, 0x2700).unwrap(); // Empty WIN0; outside mask is zero.
assert_eq!(pixel(&m, 0, 0), rgb(0x4210));
}
#[test]
fn bounds_are_half_open_and_byte_writes_preserve_other_bound() {
let mut m = setup();
window(&mut m, 0, 0, 1, 2);
m.write8(WIN0H, 30).unwrap();
m.write8(WIN0H + 1, 10).unwrap();
m.write8(WIN0V, 40).unwrap();
m.write8(WIN0V + 1, 20).unwrap();
let f = render(&m);
for y in 0..HEIGHT {
for x in 0..WIDTH {
let inside = (10..30).contains(&x) && (20..40).contains(&y);
assert_eq!(
f.pixels()[y * WIDTH + x],
rgb(if inside { 31 } else { 0x3e0 })
);
}
}
}
#[test]
fn inverted_bounds_wrap_equal_bounds_are_empty_and_offscreen_bounds_clip() {
let mut m = setup();
for (h, v) in [
(0xdc14, 0x960a),
(0xfafa, 0x00ff),
(0xf005, 0xa005),
(0x00ff, 0x00ff),
] {
window(&mut m, h, v, 1, 2);
let f = render(&m);
for y in 0..HEIGHT {
for x in 0..WIDTH {
// Independent modular-distance definition of a half-open interval.
let inside = (x as u8).wrapping_sub((h >> 8) as u8)
< (h as u8).wrapping_sub((h >> 8) as u8)
&& (y as u8).wrapping_sub((v >> 8) as u8)
< (v as u8).wrapping_sub((v >> 8) as u8);
assert_eq!(
f.pixels()[y * WIDTH + x],
rgb(if inside { 31 } else { 0x3e0 })
);
}
}
}
}
#[test]
fn win0_overrides_win1_without_combining_masks() {
let mut m = setup();
window(&mut m, 0x0a1e, 0x0a1e, 0x0200, 4); // WIN0 hides all; WIN1 selects BG1.
m.write16(WIN1H, 0x1428).unwrap();
m.write16(WIN1V, 0x1428).unwrap();
m.write16(DISPCNT, 0x6700).unwrap();
assert_eq!(pixel(&m, 25, 25), rgb(0x4210));
assert_eq!(pixel(&m, 35, 35), rgb(0x03e0));
assert_eq!(pixel(&m, 45, 45), rgb(0x7c00));
m.write16(DISPCNT, 0x6500).unwrap(); // Window cannot enable a globally disabled BG.
assert_eq!(pixel(&m, 35, 35), rgb(0x4210));
}
#[test]
fn window_effect_bit_is_independent_of_layer_bits_and_backdrop_is_always_visible() {
let mut m = setup();
window(&mut m, 0x0008, 0x0008, 1, 0x21);
m.write16(BLDCNT, 0xc1).unwrap();
m.write16(BLDY, 16).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(31));
assert_eq!(pixel(&m, 8, 0), 0);
m.write16(WINOUT, 0x20).unwrap();
m.write16(BLDCNT, 0xa0).unwrap(); // Brighten backdrop.
assert_eq!(pixel(&m, 8, 0), rgb(0x7fff));
}
#[test]
fn object_window_uses_nonzero_texels_not_color_priority_or_visible_obj_mask() {
let mut m = setup();
m.write16(DISPCNT, 0x9700).unwrap();
m.write16(WINOUT, 0x0102).unwrap(); // OBJ region BG0 only, outside BG1 only.
object(&mut m, 0, 0x800, 0, 0xfc00); // Palette15, priority3 are irrelevant.
m.write16(VRAM + 0x10000, 0x1010).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x3e0));
assert_eq!(pixel(&m, 1, 0), rgb(31));
assert_eq!(pixel(&m, 8, 0), rgb(0x3e0));
m.write16(DISPCNT, 0x8700).unwrap(); // OBJ master enable is also required.
assert_eq!(pixel(&m, 1, 0), rgb(0x3e0));
m.write16(DISPCNT, 0x1700).unwrap(); // Window OBJ never becomes a visible sprite.
assert_eq!(pixel(&m, 1, 0), rgb(31));
}
#[test]
fn visible_objects_do_not_occlude_object_window_and_rectangles_take_precedence() {
let mut m = setup();
m.write16(DISPCNT, 0x9700).unwrap();
m.write16(WINOUT, 0x0102).unwrap();
object(&mut m, 0, 0, 0, 0); // Occupies the same pixels as the later window OBJ.
object(&mut m, 127, 0x800, 0, 0);
assert_eq!(pixel(&m, 0, 0), rgb(31)); // OBJ mask hides color OBJ, not window coverage.
window(&mut m, 0x0002, 0x0008, 4, 0x0102);
assert_eq!(pixel(&m, 0, 0), rgb(0x7c00));
assert_eq!(pixel(&m, 2, 0), rgb(31));
m.write16(WIN1H, 0x0004).unwrap();
m.write16(WIN1V, 0x0008).unwrap();
m.write16(WININ, 0x0204).unwrap();
m.write16(DISPCNT, 0xf700).unwrap();
assert_eq!(pixel(&m, 2, 0), rgb(0x3e0));
assert_eq!(pixel(&m, 4, 0), rgb(31));
}
#[test]
fn object_window_supports_regular_flips_and_affine_sampling() {
let mut m = setup();
m.write16(DISPCNT, 0x9700).unwrap();
m.write16(WINOUT, 0x0102).unwrap();
for offset in (0..32).step_by(2) {
m.write16(VRAM + 0x10000 + offset, 0).unwrap();
}
m.write16(VRAM + 0x10000, 1).unwrap();
object(&mut m, 0, 0x800, 0x1000, 0);
assert_eq!(pixel(&m, 7, 0), rgb(31));
assert_eq!(pixel(&m, 0, 0), rgb(0x3e0));
object(&mut m, 0, 0x900, 0, 0);
m.write16(OAM + 6, 256).unwrap();
m.write16(OAM + 30, 256).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(31));
assert_eq!(pixel(&m, 1, 0), rgb(0x3e0));
}

View file

@ -0,0 +1,89 @@
use gba_core::{
cpu::{Cpu, InstructionSet, Mode},
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START},
};
use gba_demos::{demo_bios, exception_demo_program, EXCEPTION_DEMO_STEPS};
#[test]
fn original_demo_enters_supervisor_and_returns_to_arm_and_thumb() {
let mut memory = Memory::with_bios(exception_demo_program(), demo_bios()).unwrap();
let mut cpu = Cpu::new(ROM_START);
let expected = [
(8, Mode::Supervisor, InstructionSet::Arm),
(0x40, Mode::Supervisor, InstructionSet::Arm),
(0x44, Mode::Supervisor, InstructionSet::Arm),
(ROM_START + 4, Mode::System, InstructionSet::Arm),
(ROM_START + 8, Mode::System, InstructionSet::Arm),
(ROM_START + 12, Mode::System, InstructionSet::Thumb),
(8, Mode::Supervisor, InstructionSet::Arm),
(0x40, Mode::Supervisor, InstructionSet::Arm),
(0x44, Mode::Supervisor, InstructionSet::Arm),
(ROM_START + 14, Mode::System, InstructionSet::Thumb),
(ROM_START + 14, Mode::System, InstructionSet::Thumb),
];
assert_eq!(expected.len(), EXCEPTION_DEMO_STEPS);
for (pc, mode, state) in expected {
cpu.step(&mut memory).unwrap();
assert_eq!(
(cpu.pc(), cpu.mode(), cpu.instruction_set()),
(pc, mode, state)
);
}
assert_eq!(cpu.registers()[10], 2);
assert_eq!(cpu.registers()[14], 0); // Supervisor LR did not replace System LR
assert_eq!(cpu.cpsr(), 0x3f);
for _ in 0..5 {
cpu.step(&mut memory).unwrap();
}
assert_eq!(cpu.pc(), ROM_START + 14);
assert_eq!(cpu.registers()[10], 2);
}
#[test]
fn optional_bios_image_is_exactly_16_kib_read_only_and_not_mirrored() {
for size in [0, BIOS_SIZE - 1, BIOS_SIZE + 1] {
assert!(matches!(
Memory::with_bios(vec![], vec![0; size]),
Err(MemoryError::InvalidBiosSize(actual)) if actual == size
));
}
let bios: Vec<u8> = (0..BIOS_SIZE).map(|index| index as u8).collect();
let mut memory = Memory::with_bios(vec![], bios).unwrap();
assert_eq!(memory.read32(0).unwrap(), 0x0302_0100);
assert_eq!(memory.read16(0x3ffe).unwrap(), 0xfffe);
assert_eq!(memory.read8(0x3fff).unwrap(), 255);
for address in [0x4000, 0x0100_0000] {
assert_eq!(memory.read8(address), Err(MemoryError::Unmapped(address)));
assert_eq!(
memory.write8(address, 1),
Err(MemoryError::Unmapped(address))
);
}
assert_eq!(memory.write8(0x3fff, 0), Err(MemoryError::ReadOnly(0x3fff)));
assert_eq!(
memory.write16(0x3ffe, 0),
Err(MemoryError::ReadOnly(0x3ffe))
);
assert_eq!(memory.write32(0, 0), Err(MemoryError::ReadOnly(0)));
assert_eq!(memory.read32(0).unwrap(), 0x0302_0100);
assert_eq!(memory.read16(0x3ffe).unwrap(), 0xfffe);
}
#[test]
fn no_bios_is_mapped_by_default() {
let mut memory = Memory::new(vec![]).unwrap();
assert_eq!(memory.read32(0), Err(MemoryError::Unmapped(0)));
assert_eq!(memory.write32(0, 0), Err(MemoryError::Unmapped(0)));
}
#[test]
fn reset_can_fetch_caller_supplied_vector_code() {
let mut bios = vec![0; BIOS_SIZE];
bios[0..4].copy_from_slice(&0xe3a0_002a_u32.to_le_bytes()); // MOV r0, #42
let mut memory = Memory::with_bios(vec![], bios).unwrap();
let mut cpu = Cpu::at_reset();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[0], 42);
assert_eq!(cpu.pc(), 4);
assert_eq!(cpu.cpsr(), 0xd3);
}

View file

@ -0,0 +1,297 @@
use gba_core::{
input::{Button, Buttons},
io::{BG2PA, BG2PD, DISPCNT, GREENSWAP, KEYINPUT},
memory::{Memory, MemoryError, PALETTE_START, VRAM_START},
timing::{bus_cycles, AccessKind, AccessWidth},
video::{rgb555_to_rgb888, Framebuffer, VideoError, HEIGHT, WIDTH},
};
use gba_demos::graphics_demo::{GraphicsDemo, BACKGROUND, DEMO_STATE, SQUARE_SIZE};
fn mode3() -> Memory {
let mut memory = Memory::new(vec![]).unwrap();
memory.write16(DISPCNT, 0x403).unwrap();
memory.write16(BG2PA, 256).unwrap();
memory.write16(BG2PD, 256).unwrap();
memory
}
fn assert_demo_frame(demo: &GraphicsDemo, frame: &Framebuffer, x: usize, y: usize, color: u16) {
let memory = demo.machine().memory();
assert_eq!(memory.read32(DEMO_STATE + 4).unwrap(), x as u32);
assert_eq!(memory.read32(DEMO_STATE + 8).unwrap(), y as u32);
assert_eq!(memory.read32(DEMO_STATE + 12).unwrap(), u32::from(color));
assert_eq!(memory.read16(DISPCNT).unwrap(), 0x403);
for py in 0..HEIGHT {
for px in 0..WIDTH {
let expected =
if (x..x + SQUARE_SIZE).contains(&px) && (y..y + SQUARE_SIZE).contains(&py) {
color
} else {
BACKGROUND
};
assert_eq!(
frame.pixels()[py * WIDTH + px],
rgb555_to_rgb888(expected),
"pixel {px},{py}"
);
}
}
}
#[test]
fn cpu_program_draws_mode3_and_moves_from_keyinput_without_host_vram_writes() {
let mut demo = GraphicsDemo::new().unwrap();
let mut frame = Framebuffer::default();
assert_eq!(demo.machine().memory().read16(DISPCNT).unwrap(), 0);
assert_eq!(demo.machine().memory().read16(VRAM_START).unwrap(), 0);
let startup_steps = demo.frame(Buttons::default(), &mut frame).unwrap();
assert!(startup_steps > WIDTH * HEIGHT);
assert_demo_frame(&demo, &frame, 112, 72, 0x7fff);
let previous = demo.machine().memory().display_position();
let previous_cycles = demo.machine().cycles();
assert_eq!(previous.scanline, 160);
let steps = demo
.frame(
Buttons::default()
.with(Button::Right, true)
.with(Button::Up, true),
&mut frame,
)
.unwrap();
assert!(steps < startup_steps);
let position = demo.machine().memory().display_position();
assert_eq!(position.scanline, 160);
assert_eq!(position.vblanks, previous.vblanks + 1);
assert_eq!(position.frames, previous.frames + 1);
assert_eq!(
demo.machine().cycles() - previous_cycles,
u64::from(gba_core::display::CYCLES_PER_FRAME) + u64::from(position.line_cycle)
- u64::from(previous.line_cycle)
);
assert_demo_frame(&demo, &frame, 114, 70, 0x7fff);
assert_eq!(demo.machine().memory().read32(DEMO_STATE).unwrap(), 2);
}
#[test]
fn cpu_program_clamps_edges_cancels_opposites_changes_color_and_resets() {
let mut demo = GraphicsDemo::new().unwrap();
let mut frame = Framebuffer::default();
let upper_left = Buttons::default()
.with(Button::Left, true)
.with(Button::Up, true);
for _ in 0..65 {
demo.frame(upper_left, &mut frame).unwrap();
}
assert_demo_frame(&demo, &frame, 0, 0, 0x7fff);
let all_directions = Buttons::from_bits(0xf0);
demo.frame(all_directions, &mut frame).unwrap();
assert_demo_frame(&demo, &frame, 0, 0, 0x7fff);
let lower_right = Buttons::default()
.with(Button::Right, true)
.with(Button::Down, true);
for _ in 0..120 {
demo.frame(lower_right, &mut frame).unwrap();
}
assert_demo_frame(&demo, &frame, 224, 144, 0x7fff);
demo.frame(all_directions, &mut frame).unwrap();
assert_demo_frame(&demo, &frame, 224, 144, 0x7fff);
for (buttons, color) in [
(Buttons::from_bits(2), 0x3e0),
(Buttons::from_bits(3), 0x1f),
(Buttons::default(), 0x7fff),
] {
demo.frame(buttons, &mut frame).unwrap();
assert_demo_frame(&demo, &frame, 224, 144, color);
}
demo.frame(Buttons::default().with(Button::Start, true), &mut frame)
.unwrap();
assert_demo_frame(&demo, &frame, 112, 72, 0x7fff);
}
#[test]
fn mode3_renders_row_major_colors_and_ignores_page_and_pixel_top_bit() {
let mut memory = mode3();
let mut frame = Framebuffer::default();
for (index, color) in [
(0, 0x801f),
(1, 0x03e0),
(WIDTH, 0x7c00),
(WIDTH * HEIGHT - 1, 0x7fff),
] {
memory
.write16(VRAM_START + index as u32 * 2, color)
.unwrap();
}
memory.write16(DISPCNT, 0x413).unwrap(); // Page bit has no effect in Mode 3.
memory.render_frame(&mut frame).unwrap();
assert_eq!(frame.pixels()[0], 0xff0000);
assert_eq!(frame.pixels()[1], 0x00ff00);
assert_eq!(frame.pixels()[WIDTH], 0x0000ff);
assert_eq!(frame.pixels()[WIDTH * HEIGHT - 1], 0xffffff);
assert_eq!(frame.pixels()[2], 0);
assert_eq!(memory.cycles(), 0);
}
#[test]
fn forced_blank_is_white_and_disabled_bg2_uses_palette_backdrop() {
let mut memory = mode3();
let mut frame = Framebuffer::default();
memory.write16(PALETTE_START, 0x3e0).unwrap();
memory.write16(DISPCNT, 3).unwrap();
memory.render_frame(&mut frame).unwrap();
assert!(frame.pixels().iter().all(|&pixel| pixel == 0x00ff00));
memory.write16(DISPCNT, 0x403).unwrap();
memory.render_frame(&mut frame).unwrap();
assert!(frame.pixels().iter().all(|&pixel| pixel == 0)); // Mode 3 black is opaque.
memory.write16(DISPCNT, 0xffff).unwrap();
memory.render_frame(&mut frame).unwrap();
assert!(frame.pixels().iter().all(|&pixel| pixel == 0xffffff));
}
#[test]
fn unsupported_modes_and_layers_report_errors_without_changing_output() {
let mut memory = mode3();
let mut frame = Framebuffer::default();
frame.clear(0x001f);
for mode in [6, 7] {
memory.write16(DISPCNT, 0x400 | mode).unwrap();
assert_eq!(
memory.render_frame(&mut frame),
Err(VideoError::UnsupportedMode(mode as u8))
);
}
for layer in [0x100, 0x200, 0x800] {
memory.write16(DISPCNT, 0x403 | layer).unwrap();
assert_eq!(
memory.render_frame(&mut frame),
Err(VideoError::UnsupportedLayers(0x403 | layer))
);
}
assert!(frame.pixels().iter().all(|&pixel| pixel == 0xff0000));
}
#[test]
fn display_control_masks_cgb_bit_and_green_swap_exchanges_only_green() {
let mut memory = mode3();
memory.write32(DISPCNT, u32::MAX).unwrap();
assert_eq!(memory.read32(DISPCNT).unwrap(), 0x0001_fff7);
memory.write8(DISPCNT, 3).unwrap();
memory.write8(DISPCNT + 1, 4).unwrap();
memory.write8(GREENSWAP + 1, 0xff).unwrap();
assert_eq!(memory.read32(DISPCNT).unwrap(), 0x0001_0403);
memory.write16(VRAM_START, 0x001f).unwrap();
memory.write16(VRAM_START + 2, 0x7fe0).unwrap(); // Green + blue
let mut frame = Framebuffer::default();
memory.render_frame(&mut frame).unwrap();
assert_eq!(frame.pixels()[0], 0xffff00);
assert_eq!(frame.pixels()[1], 0x0000ff);
memory.write8(GREENSWAP, 0).unwrap();
memory.render_frame(&mut frame).unwrap();
assert_eq!(frame.pixels()[0], 0xff0000);
assert_eq!(frame.pixels()[1], 0x00ffff);
}
#[test]
fn palette_and_vram_mirrors_preserve_word_and_halfword_layouts() {
let mut memory = mode3();
memory.write32(PALETTE_START + 0x400, 0x1234_5678).unwrap();
assert_eq!(memory.read32(PALETTE_START).unwrap(), 0x1234_5678);
memory.write16(0x05ff_fffe, 0xabcd).unwrap();
assert_eq!(memory.read16(PALETTE_START + 0x3fe).unwrap(), 0xabcd);
for (mirror, physical) in [
(0x0602_0000, VRAM_START),
(0x0601_8000, VRAM_START + 0x10000),
(0x06ff_fffc, VRAM_START + 0x17ffc),
] {
memory.write32(mirror, 0x1234_5678).unwrap();
assert_eq!(memory.read32(physical).unwrap(), 0x1234_5678);
assert_eq!(memory.read8(mirror).unwrap(), 0x78);
assert_eq!(memory.read16(mirror + 2).unwrap(), 0x1234);
}
memory.write32(VRAM_START + 0x17ffc, 0xabcd_1234).unwrap();
assert_eq!(memory.read32(VRAM_START + 0x1fffc).unwrap(), 0xabcd_1234);
assert_eq!(memory.read32(0x0700_0000).unwrap(), 0); // OAM starts cleared.
}
#[test]
fn video_byte_writes_duplicate_bg_and_palette_but_ignore_obj_area() {
let mut memory = mode3();
for address in [
PALETTE_START,
PALETTE_START + 0x3ff,
VRAM_START,
VRAM_START + 1,
VRAM_START + 0x13fff,
] {
memory.write8(address, 0x5a).unwrap();
assert_eq!(memory.read16(address & !1).unwrap(), 0x5a5a);
}
for address in [
VRAM_START + 0x14000,
VRAM_START + 0x17fff,
VRAM_START + 0x1ffff,
] {
memory.write16(address & !1, 0x1234).unwrap();
memory.write8(address, 0xff).unwrap();
assert_eq!(memory.read16(address & !1).unwrap(), 0x1234);
}
memory.write16(DISPCNT, 0).unwrap(); // Tile-mode OBJ area starts at 64 KiB.
memory.write16(VRAM_START + 0x10000, 0xabcd).unwrap();
memory.write8(VRAM_START + 0x18000, 0).unwrap();
assert_eq!(memory.read16(VRAM_START + 0x10000).unwrap(), 0xabcd);
}
#[test]
fn misaligned_video_writes_leave_all_bytes_unchanged() {
let mut memory = mode3();
for address in [PALETTE_START, VRAM_START] {
memory.write32(address, 0x1234_5678).unwrap();
assert_eq!(
memory.write16(address + 1, 0),
Err(MemoryError::Unaligned(address + 1))
);
assert_eq!(
memory.write32(address + 2, 0),
Err(MemoryError::Unaligned(address + 2))
);
assert_eq!(memory.read32(address).unwrap(), 0x1234_5678);
}
}
#[test]
fn keyinput_exposes_all_ten_buttons_active_low_and_ignores_writes() {
let mut memory = mode3();
assert_eq!(memory.read16(KEYINPUT).unwrap(), 0x3ff);
for bits in 0..=0x3ff {
memory.set_buttons(Buttons::from_bits(bits));
assert_eq!(memory.read16(KEYINPUT).unwrap(), !bits & 0x3ff);
memory.write16(KEYINPUT, 0).unwrap();
assert_eq!(memory.read16(KEYINPUT).unwrap(), !bits & 0x3ff);
}
let buttons = Buttons::from_bits(0xffff)
.with(Button::A, false)
.with(Button::L, false);
assert_eq!(buttons.bits(), 0x1fe);
memory.set_buttons(buttons);
memory.write8(KEYINPUT + 1, 0).unwrap();
assert_eq!(memory.read8(KEYINPUT).unwrap(), 1);
assert_eq!(memory.read8(KEYINPUT + 1).unwrap(), 2);
memory.set_buttons(Buttons::default());
assert_eq!(memory.read16(KEYINPUT).unwrap(), 0x3ff);
assert_eq!(memory.cycles(), 0);
assert_eq!(
memory.read16(KEYINPUT + 2),
Err(MemoryError::Unmapped(KEYINPUT + 2))
); // KEYCNT deferred.
}
#[test]
fn video_bus_widths_have_nominal_16_bit_bus_costs() {
for address in [PALETTE_START, VRAM_START, 0x06ff_fffc] {
for kind in [AccessKind::Sequential, AccessKind::NonSequential] {
assert_eq!(bus_cycles(0, address, AccessWidth::Byte, kind), 1);
assert_eq!(bus_cycles(0, address, AccessWidth::Halfword, kind), 1);
assert_eq!(bus_cycles(0, address, AccessWidth::Word, kind), 2);
}
}
}

View file

@ -0,0 +1,687 @@
use gba_core::{
cpu::{Cpu, InstructionSet, Mode},
display::{CYCLES_PER_FRAME, CYCLES_PER_LINE, HBLANK_START, VBLANK_START},
dma::{DmaError, DMA_BASE},
io::{DISPSTAT, HALTCNT, IE, IF, IME, POSTFLG, TIMER_BASE},
machine::{FrameRunError, Machine, MachineError, StepKind},
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START},
timing::StepTiming,
};
fn words(code: &[u32]) -> Vec<u8> {
code.iter().flat_map(|word| word.to_le_bytes()).collect()
}
fn machine() -> Machine {
Machine::new(
Cpu::new(ROM_START),
Memory::new(words(&[0xe280_0001, 0xeaff_fffe])).unwrap(),
)
}
fn pending_timer(bus: &mut Memory) {
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
bus.advance_cycles(1);
bus.write16(TIMER_BASE + 2, 0).unwrap();
}
fn halt(machine: &mut Machine) {
// Bare bus writes are explicit host/debug setup, not CPU-originated accesses.
machine.memory_mut().write8(HALTCNT, 0).unwrap();
}
fn dma(bus: &mut Memory, control: u16, count: u16) {
bus.write32(DMA_BASE, 0x0200_0000).unwrap();
bus.write32(DMA_BASE + 4, 0x0300_0000).unwrap();
bus.write32(
DMA_BASE + 8,
u32::from(control | 0x8000) << 16 | u32::from(count),
)
.unwrap();
}
// Prepare a real store at entry+8 with operands loaded by CPU instructions.
fn store_machine(
entry: u32,
instruction: u32,
address: u32,
value: u32,
timed_setup: bool,
) -> Machine {
let program = words(&[
0xe59f_1008,
0xe59f_0008,
instruction,
0xe282_2001,
address,
value,
]);
let mut bios = vec![0; BIOS_SIZE];
let mut bus = if entry < BIOS_SIZE as u32 {
bios[entry as usize..entry as usize + program.len()].copy_from_slice(&program);
Memory::with_bios(vec![], bios).unwrap()
} else if entry == ROM_START {
Memory::with_bios(program, bios).unwrap()
} else {
let mut bus = Memory::with_bios(vec![], bios).unwrap();
for (offset, value) in program.into_iter().enumerate() {
bus.write8(entry + offset as u32, value).unwrap();
}
bus
};
let mut cpu = Cpu::new(entry);
for _ in 0..2 {
if timed_setup {
cpu.step_timed(&mut bus).unwrap();
} else {
cpu.step(&mut bus).unwrap();
}
}
Machine::new(cpu, bus)
}
#[test]
fn reset_register_masks_widths_and_reserved_bytes() {
let mut bus = Memory::new(vec![]).unwrap();
assert!(!bus.halted());
assert_eq!(bus.read32(POSTFLG).unwrap(), 0);
bus.write8(POSTFLG, 0xff).unwrap();
assert_eq!(bus.read32(POSTFLG).unwrap(), 1);
assert!(!bus.halted());
bus.write8(POSTFLG, 0xfe).unwrap();
assert_eq!(bus.read8(POSTFLG).unwrap(), 0);
bus.write16(POSTFLG + 2, 0xffff).unwrap();
assert_eq!(bus.read16(POSTFLG + 2).unwrap(), 0);
assert!(!bus.halted());
bus.write16(POSTFLG, 0x7f01).unwrap();
assert!(bus.halted());
assert_eq!(bus.read16(POSTFLG).unwrap(), 1); // HALTCNT is write-only.
assert_eq!(bus.read16(HALTCNT), Err(MemoryError::Unaligned(HALTCNT)));
assert_eq!(
bus.read8(POSTFLG + 4),
Err(MemoryError::Unmapped(POSTFLG + 4))
);
assert_eq!(
bus.read8(POSTFLG + 0x400),
Err(MemoryError::Unmapped(POSTFLG + 0x400))
);
}
#[test]
fn all_halt_values_ignore_low_seven_bits() {
for value in 0..=0x7f {
let mut machine = machine();
machine.memory_mut().write8(HALTCNT, value).unwrap();
assert!(machine.halted());
assert_eq!(machine.memory().read8(HALTCNT).unwrap(), 0);
assert_eq!(machine.cycles(), 0);
}
}
#[test]
fn halt_idle_does_not_fetch_instructions_or_change_cpu_registers() {
let mut machine = Machine::new(Cpu::new(0xdead_beec), Memory::new(vec![]).unwrap());
halt(&mut machine);
let before = machine.cpu().clone();
for cycles in [HBLANK_START, CYCLES_PER_LINE - HBLANK_START, HBLANK_START] {
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.cpu(), &before);
assert_eq!(
machine.last_timing(),
StepTiming {
idle_cycles: cycles,
..StepTiming::default()
}
);
assert!(machine.halted());
}
assert_eq!(machine.cycles(), u64::from(CYCLES_PER_LINE + HBLANK_START));
}
#[test]
fn pending_enabled_request_prevents_sleep_regardless_of_ime() {
for ime in [0, 1] {
let mut machine = machine();
pending_timer(machine.memory_mut());
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().write16(IME, ime).unwrap();
halt(&mut machine);
assert!(!machine.halted());
assert_eq!(
machine.step().unwrap(),
if ime == 0 {
StepKind::Instruction
} else {
StepKind::IrqEntry
}
);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
}
}
#[test]
fn ie_gates_wake_but_ime_does_not_and_wake_is_not_reversed_by_acknowledgement() {
let mut machine = machine();
pending_timer(machine.memory_mut());
machine.memory_mut().write16(IE, 1).unwrap(); // Wrong source.
halt(&mut machine);
assert!(machine.halted());
machine.memory_mut().write16(IME, 1).unwrap();
assert!(machine.halted());
machine.memory_mut().write16(IE, 8).unwrap();
assert!(!machine.halted());
machine.memory_mut().write16(IF, 8).unwrap();
machine.memory_mut().write16(IE, 0).unwrap();
assert!(!machine.halted());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
#[test]
fn timer_wakes_at_exact_overflow_without_ime_and_resumes_next_instruction() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_fff6)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
halt(&mut machine);
let before = machine.cpu().clone();
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, 10);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 10);
assert!(!machine.halted());
assert!(!machine.memory().irq_pending());
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[0], 1);
assert_eq!(machine.cpu().pc(), ROM_START + 4);
}
#[test]
fn cpsr_irq_mask_blocks_handler_but_not_halt_wake() {
let mut bios = vec![0; BIOS_SIZE];
bios[0..4].copy_from_slice(&0xe280_0001_u32.to_le_bytes()); // ADD r0,r0,#1
let mut bus = Memory::with_bios(vec![], bios).unwrap();
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
bus.write16(IE, 8).unwrap();
bus.write16(IME, 1).unwrap();
bus.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::at_reset(), bus);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert!(!machine.halted());
assert!(machine.memory().irq_pending());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().mode(), Mode::Supervisor);
assert_eq!(machine.cpu().registers()[0], 1);
}
#[test]
fn unmasked_wake_enters_irq_before_executing_resumed_instruction() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_ffff)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().write16(IME, 1).unwrap();
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().mode(), Mode::Irq);
assert_eq!(machine.cpu().pc(), 0x18);
assert_eq!(machine.cpu().registers()[14], ROM_START + 4);
assert_eq!(machine.cpu().registers()[0], 0);
}
#[test]
fn wake_irq_returns_to_thumb_without_skipping_instruction() {
let mut rom = words(&[0xe28f_0001, 0xe12f_ff10]); // ADD r0,pc,#1; BX r0
rom.extend(0x2107_u16.to_le_bytes()); // MOV r1,#7
let bios = 0xe25e_f004_u32.to_le_bytes().repeat(BIOS_SIZE / 4); // SUBS pc,lr,#4
let mut bus = Memory::with_bios(rom, bios).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut bus).unwrap();
cpu.step(&mut bus).unwrap();
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
bus.write16(IE, 8).unwrap();
bus.write16(IME, 1).unwrap();
bus.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(cpu, bus);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().spsr(), Some(0x3f));
machine.memory_mut().write16(TIMER_BASE + 2, 0).unwrap();
machine.memory_mut().write16(IF, 8).unwrap();
machine.step().unwrap();
assert_eq!(machine.cpu().instruction_set(), InstructionSet::Thumb);
assert_eq!(machine.cpu().pc(), ROM_START + 8);
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[1], 7);
}
#[test]
fn all_display_interrupt_sources_can_wake_halt_at_their_edges() {
for (control, mask, edge) in [
(8, 1, VBLANK_START),
(0x10, 2, HBLANK_START),
(0x0120, 4, CYCLES_PER_LINE),
] {
let mut machine = machine();
machine.memory_mut().write16(DISPSTAT, control).unwrap();
machine.memory_mut().write16(IE, mask).unwrap();
machine.memory_mut().advance_cycles(edge - 1);
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, 1);
assert_eq!(machine.cycles(), u64::from(edge));
assert!(!machine.halted());
assert_eq!(machine.memory().read16(IF).unwrap(), mask);
}
}
#[test]
fn local_interrupt_enable_is_required_to_generate_a_wake_request() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, 0x0080_ffff)
.unwrap(); // IRQ off.
machine.memory_mut().write16(IE, 0x3fff).unwrap();
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, 1);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert!(machine.halted());
}
#[test]
fn idle_scheduling_accounts_for_timer_prescaler_phase() {
for (selection, divisor) in [1, 64, 256, 1024].into_iter().enumerate() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, ((0xc0 | selection as u32) << 16) | 0xffff)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().advance_cycles(divisor - 1);
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, 1);
assert_eq!(machine.cycles(), u64::from(divisor));
assert!(!machine.halted());
}
}
#[test]
fn cascaded_timer_wakes_only_on_predecessor_overflow() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, 0x0080_fff6)
.unwrap(); // Ten cycles per overflow, IRQ off.
machine
.memory_mut()
.write32(TIMER_BASE + 4, 0x00c4_fffe)
.unwrap(); // Two pulses, IRQ on.
machine.memory_mut().write16(IE, 0x10).unwrap();
halt(&mut machine);
for expected in [true, false] {
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, 10);
assert_eq!(machine.halted(), expected);
}
assert_eq!(machine.memory().read16(IF).unwrap(), 0x10);
assert_eq!(machine.cycles(), 20);
}
#[test]
fn disabled_predecessor_cannot_clock_a_cascaded_timer_during_halt() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE + 4, 0x00c4_ffff)
.unwrap();
machine.memory_mut().write16(IE, 0x10).unwrap();
halt(&mut machine);
for _ in 0..4 {
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert!(machine.halted());
assert_eq!(machine.memory().read16(TIMER_BASE + 4).unwrap(), 0xffff);
}
}
#[test]
fn idle_batches_match_cycle_by_cycle_display_and_timer_execution() {
for seed in 0..16_u32 {
let configure = || {
let mut bus = Memory::new(vec![]).unwrap();
bus.write16(DISPSTAT, ((seed as u16 * 17) << 8) | 0x38)
.unwrap();
for index in 0..4 {
let control = if index != 0 && (seed + index) % 3 == 0 {
0xc4
} else {
0xc0 | ((seed + index) & 3)
};
bus.write32(
TIMER_BASE + index * 4,
control << 16 | (0xffff - (seed * 13 + index * 7)),
)
.unwrap();
}
bus.advance_cycles(seed * 91);
bus.write16(IF, 0x3fff).unwrap();
bus.write8(HALTCNT, 0).unwrap(); // IE=0, so all events are observable without wake.
bus
};
let mut reference = configure();
let mut machine = Machine::new(Cpu::new(ROM_START), configure());
for _ in 0..40 {
machine.memory_mut().write16(IF, 0x3fff).unwrap();
reference.write16(IF, 0x3fff).unwrap();
let mut elapsed = 0;
loop {
reference.advance_cycles(1);
elapsed += 1;
let position = reference.display_position();
if position.line_cycle == 0
|| u32::from(position.line_cycle) == HBLANK_START
|| reference.read16(IF).unwrap() != 0
{
break;
}
assert!(elapsed < CYCLES_PER_LINE);
}
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, elapsed);
assert_eq!(
machine.memory().display_position(),
reference.display_position()
);
assert_eq!(machine.cycles(), reference.cycles());
assert_eq!(
machine.memory().read16(IF).unwrap(),
reference.read16(IF).unwrap()
);
for index in 0..4 {
assert_eq!(
machine.memory().read32(TIMER_BASE + index * 4).unwrap(),
reference.read32(TIMER_BASE + index * 4).unwrap()
);
}
}
}
}
#[test]
fn blank_triggered_dma_continues_during_halt_and_completion_can_wake() {
let mut machine = machine();
machine.memory_mut().write16(0x0200_0000, 0x1234).unwrap();
dma(machine.memory_mut(), 0x5000, 1); // VBlank and completion IRQ.
machine.memory_mut().write16(IE, 0x100).unwrap();
halt(&mut machine);
assert_eq!(machine.run_until_vblank(320).unwrap(), 320);
assert!(machine.halted()); // No display IRQ enabled; DMA has not run yet.
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert_eq!(machine.memory().read16(0x0300_0000).unwrap(), 0x1234);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x100);
assert!(!machine.halted());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
#[test]
fn dma_without_completion_irq_does_not_wake_halt() {
let mut machine = machine();
dma(machine.memory_mut(), 0x2000, 1); // HBlank, no IRQ.
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.cycles(), u64::from(HBLANK_START));
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert!(machine.halted());
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.cycles(), u64::from(CYCLES_PER_LINE));
}
#[test]
fn dma_finishes_before_irq_delivery_after_wake_during_transfer() {
let mut machine = machine();
dma(machine.memory_mut(), 0, 2);
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_ffff)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().write16(IME, 1).unwrap();
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert!(!machine.halted());
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert_eq!(machine.cpu().pc(), ROM_START);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
}
#[test]
fn unsupported_dma_power_control_write_preserves_halt_and_clock() {
let mut machine = machine();
dma(machine.memory_mut(), 0, 1);
machine.memory_mut().write16(DMA_BASE + 10, 0).unwrap();
machine.memory_mut().write32(DMA_BASE + 4, POSTFLG).unwrap();
machine.memory_mut().write16(DMA_BASE + 10, 0x8000).unwrap();
halt(&mut machine);
assert_eq!(
machine.step(),
Err(MachineError::Dma(DmaError::PowerControlDestination {
channel: 0,
address: POSTFLG
}))
);
assert!(machine.halted());
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0);
}
#[test]
fn byte_halfword_and_word_cpu_stores_enter_halt_only_from_bios() {
for timed in [false, true] {
for entry in [0x100, ROM_START, 0x0300_0000] {
for (instruction, address, value) in [
(0xe5c1_0000, HALTCNT, 0x7f), // STRB
(0xe1c1_00b0, POSTFLG, 1), // STRH
(0xe581_0000, POSTFLG, 0xffff_0001), // STR
] {
let mut machine = store_machine(entry, instruction, address, value, timed);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.halted(), entry < BIOS_SIZE as u32);
assert_eq!(machine.cpu().pc(), entry + 12);
assert_eq!(machine.cpu().registers()[2], 0);
let before = machine.cpu().clone();
if entry < BIOS_SIZE as u32 {
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.cpu(), &before);
} else {
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[2], 1);
}
}
}
}
}
#[test]
fn thumb_halt_stores_use_the_executing_address_for_bios_access() {
for entry in [0x100, ROM_START] {
let mut code = words(&[0xe28f_0001, 0xe12f_ff10]); // Enter Thumb at entry+8.
for instruction in [0x2000_u16, 0x4901, 0x7008, 0x3201] {
// MOV r0,#0; LDR r1,[pc,#4]; STRB r0,[r1]; ADD r2,#1.
code.extend(instruction.to_le_bytes());
}
code.extend(HALTCNT.to_le_bytes());
let mut bios = vec![0; BIOS_SIZE];
let mut bus = if entry < BIOS_SIZE as u32 {
bios[entry as usize..entry as usize + code.len()].copy_from_slice(&code);
Memory::with_bios(vec![], bios).unwrap()
} else {
Memory::new(code).unwrap()
};
let mut cpu = Cpu::new(entry);
for _ in 0..4 {
cpu.step(&mut bus).unwrap();
}
let mut machine = Machine::new(cpu, bus);
assert_eq!(machine.cpu().instruction_set(), InstructionSet::Thumb);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().pc(), entry + 14);
assert_eq!(machine.halted(), entry < BIOS_SIZE as u32);
}
}
#[test]
fn halt_store_finishes_and_can_wake_during_its_own_cycles() {
let mut machine = store_machine(0x100, 0xe5c1_0000, HALTCNT, 0, false);
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_ffff)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().pc(), 0x10c);
assert_eq!(machine.cycles(), 2);
assert!(!machine.halted());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[2], 1);
}
#[test]
fn stop_diagnostics_validate_entire_write_before_postflg_or_halt_changes() {
let mut bus = Memory::new(vec![]).unwrap();
for value in [0x80, 0xff] {
assert_eq!(
bus.write8(HALTCNT, value),
Err(MemoryError::UnsupportedStop)
);
assert_eq!(
bus.write16(POSTFLG, u16::from(value) << 8 | 1),
Err(MemoryError::UnsupportedStop)
);
assert_eq!(
bus.write32(POSTFLG, u32::from(value) << 8 | 1),
Err(MemoryError::UnsupportedStop)
);
assert_eq!(bus.read32(POSTFLG).unwrap(), 0);
assert!(!bus.halted());
assert_eq!(bus.cycles(), 0);
}
}
#[test]
fn bios_stop_error_preserves_cpu_timing_and_clears_access_context() {
let mut machine = store_machine(0x100, 0xe581_0000, POSTFLG, 0x8001, true);
let before = machine.cpu().clone();
assert_eq!(
machine.step(),
Err(MachineError::Cpu(MemoryError::UnsupportedStop.into()))
);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.last_timing(), StepTiming::default());
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0);
assert!(!machine.halted());
// A failed non-BIOS instruction must also clear its access context.
let mut machine = store_machine(ROM_START, 0xe581_0000, 0x0400_0058, 0, true);
assert!(machine.step().is_err());
machine.memory_mut().write8(POSTFLG, 1).unwrap();
halt(&mut machine);
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 1);
assert!(machine.halted());
}
#[test]
fn non_bios_stop_write_is_ignored_not_a_diagnostic() {
for entry in [ROM_START, 0x0300_0000] {
let mut machine = store_machine(entry, 0xe581_0000, POSTFLG, 0x8001, false);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert!(!machine.halted());
assert_eq!(machine.memory().read32(POSTFLG).unwrap(), 0);
}
}
#[test]
fn failed_block_store_cannot_partially_halt_or_change_postflg() {
// The first word is mapped; the second is not. Validation precedes all writes.
let mut machine = store_machine(0x100, 0xe8a1_0005, POSTFLG, 1, false); // STMIA r1!,{r0,r2}
let before = machine.cpu().clone();
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert!(!machine.halted());
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0);
assert_eq!(machine.cycles(), 0);
}
#[test]
fn cpu_only_apis_record_halt_but_do_not_idle_or_enforce_pause() {
for timed in [false, true] {
let mut bios = vec![0; BIOS_SIZE];
bios[..24].copy_from_slice(&words(&[
0xe59f_1008,
0xe3a0_0000,
0xe5c1_0000,
0xe282_2001,
HALTCNT,
0,
]));
let mut bus = Memory::with_bios(vec![], bios).unwrap();
let mut cpu = Cpu::new(0);
for _ in 0..3 {
if timed {
cpu.step_timed(&mut bus).unwrap();
} else {
cpu.step(&mut bus).unwrap();
}
}
assert!(bus.halted());
cpu.step(&mut bus).unwrap(); // CPU-only API intentionally bypasses machine scheduling.
assert_eq!(cpu.registers()[2], 1);
assert_eq!(bus.cycles(), 0);
assert!(bus.halted());
}
}
#[test]
fn frame_runner_remains_bounded_with_no_wake_source_and_reports_idle_cost() {
let mut machine = Machine::new(Cpu::new(ROM_START), Memory::new(vec![]).unwrap());
halt(&mut machine);
assert_eq!(
machine.run_until_vblank(0),
Err(FrameRunError::StepLimit(0))
);
assert_eq!(machine.cycles(), 0);
assert_eq!(
machine.run_until_vblank(319),
Err(FrameRunError::StepLimit(319))
);
assert!(machine.halted());
assert_eq!(machine.run_until_vblank(1).unwrap(), 1);
assert_eq!(machine.cycles(), u64::from(VBLANK_START));
assert_eq!(machine.memory().display_position().line_cycle, 0);
assert!(machine.halted());
assert_eq!(machine.run_until_vblank(456).unwrap(), 456);
assert_eq!(machine.cycles(), u64::from(VBLANK_START + CYCLES_PER_FRAME));
}
#[test]
fn externally_advanced_clock_can_wake_halt_without_cpu_execution() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_fff0)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
halt(&mut machine);
let before = machine.cpu().clone();
machine.memory_mut().advance_cycles(16);
assert!(!machine.halted());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.last_timing(), StepTiming::default());
}

View file

@ -0,0 +1,272 @@
use gba_core::{
cpu::{Cpu, CpuError, InstructionSet, Mode},
io::{IE, IF, IME, TIMER_BASE, WAITCNT},
machine::{Machine, MachineError, StepKind},
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START},
};
use gba_demos::timer_demo::{timer_demo, TIMER_DEMO_STEPS};
fn words(values: &[u32]) -> Vec<u8> {
values
.iter()
.flat_map(|value| value.to_le_bytes())
.collect()
}
fn timer(bus: &mut Memory) {
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
bus.write16(IE, 8).unwrap();
bus.write32(IME, 1).unwrap();
}
fn pending_timer(bus: &mut Memory) {
timer(bus);
bus.advance_cycles(1);
bus.write16(TIMER_BASE + 2, 0).unwrap();
}
// Load test operands through real instructions, without exposing CPU setters.
fn prepared(instruction: u32, operands: &[(usize, u32)]) -> Machine {
let offset = (operands.len() as u32 - 1) * 4;
let mut code: Vec<u32> = operands
.iter()
.map(|&(register, _)| 0xe59f_0000 | (register as u32) << 12 | offset)
.collect();
code.push(instruction);
code.extend(operands.iter().map(|&(_, value)| value));
let mut bus = Memory::new(words(&code)).unwrap();
let mut cpu = Cpu::new(ROM_START);
for _ in operands {
cpu.step(&mut bus).unwrap();
}
Machine::new(cpu, bus)
}
#[test]
fn timer_demo_configures_hardware_in_cpu_code_and_handles_exactly_one_irq() {
let mut machine = timer_demo().unwrap();
let mut entries = 0;
for step in 1..=TIMER_DEMO_STEPS {
let kind = machine.step().unwrap();
assert!(machine.last_timing().total() > 0);
if kind == StepKind::IrqEntry {
entries += 1;
assert_eq!(step, 13); // Nominal timing model, not a hardware measurement
assert_eq!(machine.cpu().pc(), 0x18);
assert_eq!(machine.cpu().spsr(), Some(0x1f));
assert_eq!(machine.cpu().registers()[14], ROM_START + 0x30);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
}
}
assert_eq!(entries, 1);
assert_eq!(machine.cycles(), 153);
assert_eq!(machine.cpu().mode(), Mode::System);
assert_eq!(machine.cpu().pc(), ROM_START + 0x2c);
assert_eq!(machine.cpu().registers()[10], 1);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.memory().read16(TIMER_BASE + 2).unwrap(), 0);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xfff4);
for _ in 0..100 {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
assert_eq!(machine.cpu().registers()[10], 1);
}
#[test]
fn overflow_after_instruction_is_sampled_on_next_step() {
let mut bus = Memory::new(words(&[0xe280_0001, 0xe280_0001])).unwrap();
timer(&mut bus);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[0], 1);
assert_eq!(machine.cpu().pc(), ROM_START + 4);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().registers()[0], 1); // Interrupted instruction did not execute
assert_eq!(machine.cpu().registers()[14], ROM_START + 8);
assert_eq!(machine.cycles(), 11); // First ROM access: 8; BIOS IRQ refill: 3
}
#[test]
fn missing_vector_errors_only_after_completed_irq_entry() {
let mut bus = Memory::new(vec![]).unwrap();
pending_timer(&mut bus);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); // No ROM fetch required
let before = machine.cpu().clone();
let cycles = machine.cycles();
assert_eq!(
machine.step(),
Err(MachineError::Cpu(CpuError::Memory(MemoryError::Unmapped(
0x18
))))
);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
}
#[test]
fn cpu_mask_defers_irq_until_msr_unmasks_it() {
let mut bios = vec![0; BIOS_SIZE];
bios[0..4].copy_from_slice(&0xe321_f013_u32.to_le_bytes()); // MSR CPSR_c, #SVC (clear I)
let mut bus = Memory::with_bios(vec![], bios).unwrap();
pending_timer(&mut bus);
let mut machine = Machine::new(Cpu::at_reset(), bus);
assert!(machine.memory().irq_pending());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().pc(), 4);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().registers()[14], 8);
assert_eq!(machine.cpu().spsr(), Some(0x13));
}
#[test]
fn ime_and_ie_gate_pending_device_requests_until_cpu_enables_them() {
for register in [IE, IME] {
let value = if register == IE { 8 } else { 1 };
let mut machine = prepared(0xe580_1000, &[(0, register), (1, value)]); // STR r1, [r0]
pending_timer(machine.memory_mut());
machine.memory_mut().write16(register, 0).unwrap();
assert!(!machine.memory().irq_pending());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert!(machine.memory().irq_pending());
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
}
}
#[test]
fn uncleared_request_reenters_after_irq_return_and_no_fiq_is_generated() {
let bios = 0xe25e_f004_u32.to_le_bytes().repeat(BIOS_SIZE / 4); // SUBS pc, lr, #4
let mut bus = Memory::with_bios(words(&[0xeaff_fffe]), bios).unwrap();
pending_timer(&mut bus);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
for _ in 0..3 {
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().mode(), Mode::Irq);
assert_eq!(machine.step().unwrap(), StepKind::Instruction); // I mask prevents immediate nesting
assert_eq!(machine.cpu().mode(), Mode::System);
assert_eq!(machine.cpu().pc(), ROM_START);
}
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
}
#[test]
fn timer_irq_returns_to_thumb_without_skipping_the_interrupted_instruction() {
let mut rom = words(&[0xe28f_0001, 0xe12f_ff10]); // ADD r0, pc, #1; BX r0
rom.extend(0x2107_u16.to_le_bytes()); // MOV r1, #7 at offset 8
let bios = 0xe25e_f004_u32.to_le_bytes().repeat(BIOS_SIZE / 4);
let mut bus = Memory::with_bios(rom, bios).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut bus).unwrap();
cpu.step(&mut bus).unwrap();
pending_timer(&mut bus);
let mut machine = Machine::new(cpu, bus);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().spsr(), Some(0x3f));
assert_eq!(machine.cpu().registers()[14], ROM_START + 12);
machine.memory_mut().write16(IF, 8).unwrap();
machine.step().unwrap();
assert_eq!(machine.cpu().instruction_set(), InstructionSet::Thumb);
assert_eq!(machine.cpu().pc(), ROM_START + 8);
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[1], 7);
assert_eq!(machine.cpu().pc(), ROM_START + 10);
}
#[test]
fn diagnostics_do_not_advance_timers_or_clock() {
for instruction in [0xf3a0_0001, 0xe590_1000] {
// Unsupported or unmapped LDR
let mut bus = Memory::new(words(&[instruction])).unwrap();
timer(&mut bus);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
let before = machine.cpu().clone();
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xffff);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
}
}
#[test]
fn late_block_store_error_does_not_change_timer_reload_or_enable() {
let mut machine = prepared(
0xe8a0_003e,
&[
(0, TIMER_BASE),
(1, 0x00c0_ffff),
(2, 0x00c0_ffff),
(3, 0x00c0_ffff),
(4, 0x00c0_ffff),
(5, 123),
],
); // STMIA r0!, {r1-r5}; fifth word is unmapped
let before = machine.cpu().clone();
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
for index in 0..4 {
let address = TIMER_BASE + index * 4;
assert_eq!(machine.memory().read32(address).unwrap(), 0);
machine.memory_mut().write16(address + 2, 0x80).unwrap();
assert_eq!(machine.memory().read16(address).unwrap(), 0); // Hidden reload also unchanged
}
}
#[test]
fn late_block_store_error_does_not_acknowledge_if_or_modify_ie() {
let mut machine = prepared(
0xe8a0_001e,
&[(0, IE), (1, 0x0008_ffff), (2, 123), (3, 1), (4, 42)],
);
// IE/IF, WAITCNT, IME are valid. The fourth word at 0x0400020c is not.
pending_timer(machine.memory_mut());
machine.memory_mut().write32(IME, 0).unwrap(); // Let the invalid store execute
let before = machine.cpu().clone();
let cycles = machine.cycles();
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().read32(IE).unwrap(), 0x0008_0008);
assert_eq!(machine.memory().read32(WAITCNT).unwrap(), 0);
assert_eq!(machine.memory().read32(IME).unwrap(), 0);
assert_eq!(machine.last_timing().total(), 0);
}
#[test]
fn late_block_load_error_preserves_cpu_and_devices() {
let mut machine = prepared(0xe8b0_0006, &[(0, IME)]); // LDMIA r0!, {r1, r2}
timer(machine.memory_mut());
let before = machine.cpu().clone();
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xffff);
}
#[test]
fn skipped_condition_charges_code_access_but_not_registers() {
let mut bus = Memory::new(words(&[0x0280_0001])).unwrap(); // ADDEQ r0, r0, #1; Z clear
timer(&mut bus);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[0], 0);
assert_eq!(machine.cpu().pc(), ROM_START + 4);
assert_eq!(machine.cycles(), 8);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
}
#[test]
fn cpu_only_step_does_not_advance_device_time_or_deliver_irq() {
let mut bus = Memory::new(words(&[0xe280_0001])).unwrap();
pending_timer(&mut bus);
let mut cpu = Cpu::new(ROM_START);
let cycles = bus.cycles();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers()[0], 1);
assert_eq!(cpu.mode(), Mode::System);
assert_eq!(bus.cycles(), cycles);
assert!(bus.irq_pending());
}

View file

@ -0,0 +1,87 @@
use gba_core::memory::{Memory, MemoryError, ROM_START};
#[test]
fn halfword_and_word_writes_are_little_endian() {
let mut memory = Memory::new(vec![]).unwrap();
for address in [0x0200_0000, 0x0300_0000] {
memory.write32(address, 0x1234_5678).unwrap();
assert_eq!(memory.read8(address).unwrap(), 0x78);
assert_eq!(memory.read16(address).unwrap(), 0x5678);
assert_eq!(memory.read16(address + 2).unwrap(), 0x1234);
memory.write16(address + 2, 0xabcd).unwrap();
assert_eq!(memory.read32(address).unwrap(), 0xabcd_5678);
}
}
#[test]
fn multi_byte_writes_respect_ram_mirrors_and_region_ends() {
let mut memory = Memory::new(vec![]).unwrap();
for (base, size, end) in [
(0x0200_0000, 0x40000, 0x02ff_ffff),
(0x0300_0000, 0x8000, 0x03ff_ffff),
] {
memory.write32(end - 3, 0xdead_beef).unwrap();
assert_eq!(memory.read32(base + size - 4).unwrap(), 0xdead_beef);
memory.write16(base + size, 0x1234).unwrap();
assert_eq!(memory.read16(base).unwrap(), 0x1234);
}
}
#[test]
fn unaligned_bus_writes_fail_without_modifying_memory() {
let mut memory = Memory::new(vec![]).unwrap();
let address = 0x0200_0000;
memory.write32(address, 0xdead_beef).unwrap();
assert_eq!(
memory.write16(address + 1, 0),
Err(MemoryError::Unaligned(address + 1))
);
for offset in 1..4 {
assert_eq!(
memory.write32(address + offset, 0),
Err(MemoryError::Unaligned(address + offset))
);
}
assert_eq!(memory.read32(address).unwrap(), 0xdead_beef);
assert_eq!(
memory.read16(address + 1),
Err(MemoryError::Unaligned(address + 1))
);
}
#[test]
fn rom_and_unmapped_writes_return_errors_without_partial_changes() {
let mut memory = Memory::new(vec![0x12, 0x34, 0x56, 0x78]).unwrap();
assert_eq!(
memory.write16(ROM_START, 0),
Err(MemoryError::ReadOnly(ROM_START))
);
assert_eq!(
memory.write32(ROM_START, 0),
Err(MemoryError::ReadOnly(ROM_START))
);
assert_eq!(memory.read32(ROM_START).unwrap(), 0x7856_3412);
for address in [0, 0x0400_0058, 0xffff_fffc] {
assert_eq!(
memory.write32(address, 0),
Err(MemoryError::Unmapped(address))
);
assert_eq!(
memory.write16(address, 0),
Err(MemoryError::Unmapped(address))
);
}
}
#[test]
fn truncated_rom_halfword_read_returns_an_error() {
let memory = Memory::new(vec![0x12]).unwrap();
assert_eq!(
memory.read16(ROM_START),
Err(MemoryError::Unmapped(ROM_START + 1))
);
assert_eq!(
memory.read16(0xffff_fffe),
Err(MemoryError::Unmapped(0xffff_fffe))
);
}

View file

@ -0,0 +1,47 @@
use gba_core::{
io::*,
memory::{Memory, MemoryError, OAM_START as OAM, PALETTE_START as PAL, VRAM_START as VRAM},
video::{rgb555_to_rgb888 as rgb, Framebuffer, HEIGHT, WIDTH},
};
#[path = "mosaic/backgrounds.rs"]
mod backgrounds;
#[path = "mosaic/demo.rs"]
mod demo;
#[path = "mosaic/objects.rs"]
mod objects;
fn memory() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
for index in 0..128 {
m.write16(OAM + index * 8, 0x200).unwrap();
}
for index in 0..256 {
let color = (index & 31) | (((index >> 3) & 31) << 5) | (((index * 7) & 31) << 10);
m.write16(PAL + index * 2, color as u16).unwrap();
m.write16(PAL + 0x200 + index * 2, color as u16).unwrap();
}
m
}
fn render(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
m.render_frame(&mut f).unwrap();
f
}
fn object(m: &mut Memory, index: u32, a: u16, b: u16, c: u16) {
m.write16(OAM + index * 8, a).unwrap();
m.write16(OAM + index * 8 + 2, b).unwrap();
m.write16(OAM + index * 8 + 4, c).unwrap();
}
fn fill(m: &mut Memory, address: u32, bytes: u32, value: u16) {
for offset in (0..bytes).step_by(2) {
m.write16(address + offset, value).unwrap();
}
}
fn at(frame: &Framebuffer, x: usize, y: usize) -> u32 {
frame.pixels()[y * WIDTH + x]
}

View file

@ -0,0 +1,212 @@
use super::*;
fn bitmap(mode: u16, page: u16) -> Memory {
let mut m = memory();
m.write16(DISPCNT, 0x400 | mode | (page * 16)).unwrap();
m.write16(BG2PA, 256).unwrap();
m.write16(BG2PD, 256).unwrap();
let (w, h) = if mode == 5 { (160, 128) } else { (240, 160) };
for y in 0..h {
for x in 0..w {
let offset = (y * w + x) as u32;
let index = (x * 3 + y * 7) as u16 & 255;
if mode == 4 {
if x % 2 == 0 {
let next = ((x + 1) * 3 + y * 7) as u16 & 255;
m.write16(VRAM + page as u32 * 0xa000 + offset, index | (next << 8))
.unwrap();
}
} else {
let color =
(x as u16 & 31) | ((y as u16 & 31) << 5) | (((x + y) as u16 & 31) << 10);
m.write16(VRAM + page as u32 * 0xa000 + offset * 2, color)
.unwrap();
}
}
}
m
}
#[test]
fn write_only_register_merges_bytes_and_ignores_upper_word_half() {
let mut m = bitmap(3, 0);
m.write16(BG2CNT, 0x40).unwrap();
m.write32(MOSAIC, 0xffff_0034).unwrap(); // BG 5x4, OBJ1x1.
assert_eq!(m.read32(MOSAIC).unwrap(), 0);
m.write8(MOSAIC + 1, 0xff).unwrap(); // Must retain BG dimensions.
let f = render(&m);
assert_eq!(at(&f, 4, 3), at(&f, 0, 0));
assert_ne!(at(&f, 5, 4), at(&f, 0, 0));
m.write8(MOSAIC, 0).unwrap();
assert_ne!(at(&render(&m), 1, 0), at(&f, 0, 0));
assert_eq!(m.read16(MOSAIC + 2).unwrap(), 0);
assert_eq!(
m.read8(MOSAIC + 0x10000),
Err(MemoryError::Unmapped(MOSAIC + 0x10000))
);
assert_eq!(
m.write16(MOSAIC + 1, 0),
Err(MemoryError::Unaligned(MOSAIC + 1))
);
assert_eq!(m.cycles(), 0);
}
#[test]
fn every_bg_size_and_bitmap_page_repeats_screen_grid_samples() {
for mode in [3, 4, 5] {
for page in 0..=u16::from(mode != 3) {
let mut m = bitmap(mode, page);
let base = render(&m);
m.write16(BG2CNT, 0x40).unwrap();
// Exercise every field value, including asymmetric blocks and partial LCD blocks.
for w in 1..=16 {
let h = 17 - w;
m.write16(MOSAIC, ((h - 1) * 16 + w - 1) as u16).unwrap();
let f = render(&m);
for y in 0..HEIGHT {
for x in 0..WIDTH {
assert_eq!(
at(&f, x, y),
at(&base, x / w * w, y / h * h),
"mode={mode} page={page} size={w}x{h} at={x},{y}"
);
}
}
}
}
}
}
#[test]
fn mosaic_flag_is_independent_of_dimensions_and_one_by_one_is_identity() {
let mut m = bitmap(3, 0);
let base = render(&m);
m.write16(MOSAIC, 0xffff).unwrap();
assert_eq!(render(&m).pixels(), base.pixels()); // Flag is off.
m.write16(BG2CNT, 0x40).unwrap();
m.write16(MOSAIC, 0xff00).unwrap(); // OBJ fields do not affect BG.
assert_eq!(render(&m).pixels(), base.pixels());
m.write16(MOSAIC, 0x000f).unwrap();
let f = render(&m);
assert_eq!(at(&f, 15, 1), at(&base, 0, 1));
assert_ne!(at(&f, 15, 1), at(&base, 0, 0));
}
#[test]
fn text_mosaic_samples_before_scroll_and_tile_flips_in_both_depths() {
for eight_bit in [false, true] {
let mut m = memory();
m.write16(DISPCNT, 0x100).unwrap();
let control = 0x1000 | if eight_bit { 0x80 } else { 0 };
m.write16(BG0CNT, control).unwrap();
m.write16(BG0HOFS, 7).unwrap();
m.write16(BG0VOFS, 5).unwrap();
fill(&mut m, VRAM + 0x8000, 0x800, 0x0c00); // Both tile flips.
for y in 0..8 {
for x in (0..8).step_by(if eight_bit { 2 } else { 4 }) {
let mut value = 0;
for i in 0..if eight_bit { 2 } else { 4 } {
let index = ((x + i + y * 3) % 15) as u16;
value |= index << (i * if eight_bit { 8 } else { 4 });
}
m.write16(
VRAM + (y * if eight_bit { 8 } else { 4 } + x / if eight_bit { 1 } else { 2 })
as u32,
value,
)
.unwrap();
}
}
let base = render(&m);
m.write16(BG0CNT, control | 0x40).unwrap();
m.write16(MOSAIC, 0x54).unwrap(); // 5x6 crosses tile boundaries.
let f = render(&m);
for y in 0..HEIGHT {
for x in 0..WIDTH {
assert_eq!(at(&f, x, y), at(&base, x / 5 * 5, y / 6 * 6));
}
}
}
}
#[test]
fn affine_mosaic_samples_screen_positions_before_signed_transform() {
for mode in [1, 2, 3, 4, 5] {
let mut m = if mode >= 3 { bitmap(mode, 0) } else { memory() };
m.write16(DISPCNT, 0x400 | mode).unwrap();
let control = if mode <= 2 { 0x3000 } else { 0 };
m.write16(BG2CNT, control).unwrap();
if mode <= 2 {
for offset in (0..64).step_by(2) {
m.write16(VRAM + offset, (offset as u16 * 0x101).wrapping_add(0x100))
.unwrap();
}
}
for (reg, value) in [
(BG2PA, 181),
(BG2PB, 91),
(BG2PC, (-181i16) as u16),
(BG2PD, 256),
] {
m.write16(reg, value).unwrap();
}
m.write32(BG2X, (-769i32) as u32).unwrap();
m.write32(BG2Y, 31 * 256 + 127).unwrap();
let base = render(&m);
m.write16(BG2CNT, control | 0x40).unwrap();
m.write16(MOSAIC, 0x76).unwrap();
let f = render(&m);
for y in 0..HEIGHT {
for x in 0..WIDTH {
assert_eq!(
at(&f, x, y),
at(&base, x / 7 * 7, y / 8 * 8),
"mode={mode} pixel={x},{y}"
);
}
}
}
}
#[test]
fn independent_background_flags_preserve_transparency_and_priority() {
let mut m = memory();
m.write16(DISPCNT, 0x300).unwrap();
m.write16(BG0CNT, 0x1040).unwrap();
m.write16(BG1CNT, 0x1105).unwrap();
m.write16(PAL + 2, 31).unwrap();
m.write16(PAL + 4, 0x3e0).unwrap();
fill(&mut m, VRAM, 32, 0x1010);
fill(&mut m, VRAM + 0x4000, 32, 0x2222);
m.write16(MOSAIC, 2).unwrap();
let f = render(&m);
for x in 0..18 {
assert_eq!(at(&f, x, 0), rgb(if x / 3 % 2 == 0 { 0x3e0 } else { 31 }));
}
m.write16(BG0CNT, 0x1000).unwrap();
m.write16(BG1CNT, 0x1145).unwrap();
let f = render(&m);
for x in 0..18 {
assert_eq!(at(&f, x, 0), rgb(if x % 2 == 0 { 0x3e0 } else { 31 }));
}
}
#[test]
fn windows_and_color_effects_use_output_coordinate_not_mosaic_sample() {
let mut m = bitmap(3, 0);
m.write16(BG2CNT, 0x40).unwrap();
m.write16(MOSAIC, 0x77).unwrap();
m.write16(DISPCNT, 0x2403).unwrap();
m.write16(WIN0H, 0x0306).unwrap();
m.write16(WIN0V, 0x0008).unwrap();
m.write16(WININ, 4).unwrap();
m.write16(WINOUT, 0x24).unwrap();
m.write16(BLDCNT, 0x84).unwrap();
m.write16(BLDY, 16).unwrap();
let f = render(&m);
for x in 0..8 {
assert_eq!(at(&f, x, 0), if (3..6).contains(&x) { 0 } else { 0xffffff });
}
m.write16(DISPCNT, 0xffff).unwrap();
assert!(render(&m).pixels().iter().all(|&p| p == 0xffffff));
}

View file

@ -0,0 +1,100 @@
use super::*;
use gba_core::input::{Button, Buttons};
use gba_demos::{
mosaic_demo::{MosaicDemo, MOSAIC_STATE},
tile_demo::TileDemo,
};
#[test]
fn cpu_demo_cycles_all_sizes_and_wraps_without_host_video_writes() {
let mut demo = MosaicDemo::new().unwrap();
let mut frame = Framebuffer::default();
assert_eq!(demo.machine().memory().read16(DISPCNT).unwrap(), 0);
assert_eq!(demo.machine().memory().read16(VRAM).unwrap(), 0);
for update in 1..=129 {
demo.frame(Buttons::default(), &mut frame).unwrap();
let m = demo.machine().memory();
assert_eq!(m.read32(MOSAIC_STATE).unwrap(), update);
assert_eq!(m.read16(DISPCNT).unwrap(), 0x1340);
assert_eq!(m.read16(BG0CNT).unwrap(), 0x1844);
assert_eq!(m.read16(BG1CNT).unwrap(), 0xd0c1);
assert_eq!(m.read16(OAM).unwrap(), 0x1048);
assert_eq!(m.read32(MOSAIC).unwrap(), 0);
assert_eq!(m.display_position().scanline, 160);
let size = (1 + (update / 8) % 16) as usize;
// Check terrain and foreground away from the sprite for every size.
for y in 0..48 {
for x in 0..WIDTH {
let sx = x / size * size;
let sy = y / size * size;
let cross = sx / 8 % 4 == 1 && sy / 8 % 4 == 1 && (sx % 8 == 3 || sy % 8 == 3);
let color = if cross {
0x3ff
} else {
[0x260, 0x3a0, 0x7d20, 0x7e80]
[((sx / 32 + sy / 32) % 2) * 2 + (sx / 2 + sy / 2) % 2]
};
assert_eq!(at(&frame, x, y), rgb(color), "update={update} at={x},{y}");
}
}
}
}
#[test]
fn both_bypasses_restore_original_scene_with_scroll_rotation_zoom_and_reset() {
let mut demo = MosaicDemo::new().unwrap();
let mut base = TileDemo::new().unwrap();
let mut f = Framebuffer::default();
let mut reference = Framebuffer::default();
for i in 0..32 {
let buttons = Buttons::default()
.with(Button::Left, i < 16)
.with(Button::Down, true)
.with(Button::Start, i == 31)
.with(Button::L, (8..24).contains(&i))
.with(Button::R, (16..28).contains(&i));
base.frame(buttons, &mut reference).unwrap();
demo.frame(buttons.with(Button::A, true).with(Button::B, true), &mut f)
.unwrap();
assert_eq!(f.pixels(), reference.pixels());
let m = demo.machine().memory();
assert_ne!(m.read16(OAM).unwrap() & 0x1000, 0);
assert_eq!(m.read16(OAM + 2).unwrap() & 0x1000, 0); // Z no longer flips.
assert_eq!(m.read16(OAM + 4).unwrap() & 0xc00, 0); // X no longer lowers priority.
assert_eq!(
m.read32(MOSAIC_STATE + 4).unwrap(),
base.machine().memory().read32(MOSAIC_STATE + 4).unwrap()
);
}
assert_eq!(demo.machine().memory().read32(MOSAIC_STATE + 4).unwrap(), 0);
assert_eq!(demo.machine().memory().read32(MOSAIC_STATE + 8).unwrap(), 0);
}
#[test]
fn dma_word_write_sets_bg_and_obj_fields_while_unused_halfword_ignores_writes() {
use gba_core::{
cpu::Cpu,
dma::DMA_BASE,
machine::{Machine, StepKind},
};
let mut m = memory();
m.write16(DISPCNT, 0x1140).unwrap();
m.write16(BG0CNT, 0x1040).unwrap();
fill(&mut m, VRAM, 32, 0x1212);
m.write16(PAL + 4, 31).unwrap();
m.write16(PAL + 2, 0x3e0).unwrap();
object(&mut m, 0, 0x1000, 0, 0);
fill(&mut m, VRAM + 0x10000, 32, 0x0101);
m.write16(PAL + 0x202, 0x7c00).unwrap();
m.write32(0x0200_0000, 0xffff_0302).unwrap(); // BG3x1, OBJ4x1.
m.write32(DMA_BASE + 36, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 40, MOSAIC).unwrap();
m.write32(DMA_BASE + 44, 0x8400_0001).unwrap();
let mut machine = Machine::new(Cpu::new(0x0300_0000), m);
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
let f = render(machine.memory());
assert_eq!(at(&f, 3, 0), rgb(0x7c00));
assert_eq!(at(&f, 8, 0), rgb(31)); // BG samples x6, not x8's independent phase.
assert_eq!(at(&f, 9, 0), rgb(0x3e0));
assert_eq!(machine.memory().read32(MOSAIC).unwrap(), 0);
}

View file

@ -0,0 +1,215 @@
use super::*;
fn scene(a: u16, b: u16) -> Memory {
let mut m = memory();
m.write16(DISPCNT, 0x1040).unwrap();
object(&mut m, 0, a | 7, b | 5 | 0x4000, 0); // 16x16 at (5,7).
for y in 0..16 {
for x in (0..16).step_by(2) {
let tile = y / 8 * 2 + x / 8;
let address = VRAM + 0x10000 + tile * 64 + y % 8 * 8 + x % 8;
let first = 1 + (x + y * 5) % 254;
let second = 1 + (x + 1 + y * 5) % 254;
m.write16(address, (first | second << 8) as u16).unwrap();
}
}
m
}
#[test]
fn regular_obj_uses_screen_grid_clamps_partial_first_blocks_and_preserves_flips() {
for flips in [0, 0x1000, 0x2000, 0x3000] {
let mut m = scene(0x2000, flips);
let base = render(&m);
m.write16(OAM, 0x3007).unwrap(); // 8bpp plus mosaic, y7.
for width in 1..=16 {
let height = 17 - width;
m.write16(MOSAIC, (((height - 1) << 12) | ((width - 1) << 8)) as u16)
.unwrap();
let f = render(&m);
for y in 0..32 {
for x in 0..32 {
let expected = if (5..21).contains(&x) && (7..23).contains(&y) {
at(
&base,
(x / width * width).max(5),
(y / height * height).max(7),
)
} else {
0
};
assert_eq!(
at(&f, x, y),
expected,
"flip={flips:x} size={width}x{height} at={x},{y}"
);
}
}
}
}
}
#[test]
fn affine_and_double_size_mosaic_repeat_transformed_samples() {
for doubled in [false, true] {
let mut m = scene(0x2100 | if doubled { 0x200 } else { 0 }, 0);
let matrix: [i16; 4] = if doubled {
[64, 32, -32, 64]
} else {
[128, 64, -64, 128]
};
for (i, value) in matrix.into_iter().enumerate() {
m.write16(OAM + 6 + i as u32 * 8, value as u16).unwrap();
}
let base = render(&m);
m.write16(OAM, 0x3107 | if doubled { 0x200 } else { 0 })
.unwrap();
m.write16(MOSAIC, 0x4500).unwrap(); // OBJ6x5; BG1x1.
let f = render(&m);
let size = if doubled { 32 } else { 16 };
for y in 0..48 {
for x in 0..48 {
let expected = if (5..5 + size).contains(&x) && (7..7 + size).contains(&y) {
at(&base, (x / 6 * 6).max(5), (y / 5 * 5).max(7))
} else {
0
};
assert_eq!(at(&f, x, y), expected, "double={doubled} at={x},{y}");
}
}
}
}
#[test]
fn obj_flag_and_high_byte_are_independent_and_vertical_sampling_wraps_at_screen_top() {
let mut m = scene(0x2000, 0);
let base = render(&m);
m.write16(MOSAIC, 0xffff).unwrap();
assert_eq!(render(&m).pixels(), base.pixels()); // Flag disabled.
m.write16(OAM, 0x3007).unwrap();
m.write8(MOSAIC + 1, 0).unwrap(); // BG15 bits do not affect OBJ.
assert_eq!(render(&m).pixels(), base.pixels());
object(&mut m, 0, 0x20fc, 0x41fc, 0); // 16x16, wraps from (-4,-4).
let base = render(&m);
m.write16(OAM, 0x30fc).unwrap();
m.write16(MOSAIC, 0x3400).unwrap();
let f = render(&m);
for y in 0..12 {
for x in 0..12 {
assert_eq!(at(&f, x, y), at(&base, x / 5 * 5, y / 4 * 4));
}
}
assert_eq!(at(&f, 12, 0), 0);
assert_eq!(at(&f, 0, 12), 0);
}
#[test]
fn transparent_samples_repeat_but_non_mosaic_samples_restart_the_latch() {
let mut m = memory();
m.write16(DISPCNT, 0x1040).unwrap();
m.write16(MOSAIC, 0x0300).unwrap(); // OBJ4x1.
m.write16(PAL + 0x202, 31).unwrap();
fill(&mut m, VRAM + 0x10000, 32, 0x1010); // First sample transparent, second red.
object(&mut m, 0, 0x1000, 0, 0);
assert!(render(&m).pixels().iter().all(|&p| p == 0));
m.write16(VRAM + 0x10000, 0x0101).unwrap();
let f = render(&m);
for x in 0..4 {
assert_eq!(at(&f, x, 0), rgb(31));
}
// A lower-priority non-mosaic sprite fills transparent texels before mosaic.
// Its non-mosaic metadata makes those pixels latch immediately.
fill(&mut m, VRAM + 0x10020, 32, 0x2222);
m.write16(PAL + 0x204, 0x3e0).unwrap();
object(&mut m, 1, 0, 0, 0x401);
let f = render(&m);
assert_eq!(at(&f, 0, 0), rgb(31));
assert_eq!(at(&f, 1, 0), rgb(0x3e0));
}
#[test]
fn horizontal_latch_restarts_for_non_mosaic_and_higher_priority_pixels() {
let mut m = memory();
m.write16(DISPCNT, 0x1040).unwrap();
m.write16(MOSAIC, 0x0f00).unwrap();
for (tile, color) in [(0, 31), (1, 0x3e0), (2, 0x7c00)] {
fill(
&mut m,
VRAM + 0x10000 + tile * 32,
32,
(tile as u16 + 1) * 0x1111,
);
m.write16(PAL + 0x202 + tile * 2, color).unwrap();
}
object(&mut m, 0, 0x1000, 1, 0x800);
object(&mut m, 1, 0x1000, 3, 0x401);
object(&mut m, 2, 0, 5, 2);
let f = render(&m);
assert_eq!(at(&f, 0, 0), 0);
assert_eq!(at(&f, 1, 0), rgb(31));
assert_eq!(at(&f, 3, 0), rgb(0x3e0));
assert_eq!(at(&f, 5, 0), rgb(0x7c00));
assert_eq!(at(&f, 13, 0), 0); // Leaving the canvas resets the latch.
}
#[test]
fn transparent_higher_priority_obj_changes_metadata_without_replacing_color() {
let mut m = memory();
m.write16(DISPCNT, 0x1140).unwrap();
m.write16(BG0CNT, 0x1001).unwrap(); // BG priority1.
fill(&mut m, VRAM, 32, 0x3333);
m.write16(PAL + 6, 0x7c00).unwrap();
fill(&mut m, VRAM + 0x10000, 32, 0x1111);
m.write16(PAL + 0x202, 31).unwrap();
object(&mut m, 0, 0x400, 0, 0x800); // Semi-transparent red, priority2.
object(&mut m, 1, 0x1000, 0, 0x401); // Transparent tile1, priority1, mosaic.
fill(&mut m, VRAM + 0x10040, 32, 0x2222);
object(&mut m, 2, 0, 0, 0x402); // Same priority as metadata; cannot replace red.
m.write16(MOSAIC, 0x0300).unwrap();
m.write16(BLDCNT, 0x100).unwrap();
m.write16(BLDALPHA, 0x0808).unwrap();
assert_eq!(at(&render(&m), 0, 0), rgb(0x3c0f)); // Alpha flag retained, priority now ties BG.
}
#[test]
fn object_window_ignores_mosaic_and_window_edges_do_not_quantize() {
let mut m = memory();
m.write16(DISPCNT, 0x9100).unwrap();
m.write16(BG0CNT, 0x1000).unwrap();
fill(&mut m, VRAM, 32, 0x1111);
m.write16(PAL + 2, 31).unwrap();
fill(&mut m, VRAM + 0x10000, 32, 0x1010);
object(&mut m, 0, 0x1800, 0, 0);
m.write16(MOSAIC, 0xffff).unwrap();
m.write16(WINOUT, 0x0100).unwrap();
let f = render(&m);
for x in 0..8 {
assert_eq!(at(&f, x, 0), if x % 2 == 0 { 0 } else { rgb(31) });
}
m.write16(VRAM + 0x10004, 0x0101).unwrap(); // Next source row remains distinct.
assert_eq!(at(&render(&m), 0, 1), rgb(31));
}
#[test]
fn mosaic_latch_precedes_window_masking_and_semi_transparent_blending() {
let mut m = memory();
m.write16(DISPCNT, 0x3100).unwrap();
m.write16(BG0CNT, 0x1000).unwrap();
fill(&mut m, VRAM, 32, 0x2222);
m.write16(PAL + 4, 0x7c00).unwrap();
m.write16(PAL + 0x202, 31).unwrap();
fill(&mut m, VRAM + 0x10000, 32, 0x0101);
object(&mut m, 0, 0x1400, 0, 0);
m.write16(MOSAIC, 0x0300).unwrap();
m.write16(WIN0H, 0x0001).unwrap();
m.write16(WIN0V, 0x0008).unwrap();
m.write16(WININ, 1).unwrap(); // First pixel masks OBJ but must not reset its latch.
m.write16(WINOUT, 0x31).unwrap();
m.write16(BLDCNT, 0x100).unwrap();
m.write16(BLDALPHA, 0x0808).unwrap();
let f = render(&m);
assert_eq!(at(&f, 0, 0), rgb(0x7c00));
for x in 1..4 {
assert_eq!(at(&f, x, 0), rgb(0x3c0f));
}
}

View file

@ -0,0 +1,351 @@
use gba_core::{
cpu::Cpu,
display::{
CYCLES_PER_FRAME as FRAME, CYCLES_PER_LINE as LINE, HBLANK_START as HB, VBLANK_START as VB,
},
dma::DMA_BASE,
io::*,
machine::Machine,
memory::{Memory, OAM_START as OAM, PALETTE_START as PAL, ROM_START, VRAM_START as VRAM},
video::{rgb555_to_rgb888 as rgb, Framebuffer, HEIGHT, WIDTH},
};
fn color(row: usize) -> u16 {
((row % 64 + 1) * 0x101) as u16
}
fn scene() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.set_scanline_rendering(true);
m.write16(DISPCNT, 0x1140).unwrap();
m.write16(BG0CNT, 0x10c0).unwrap(); // 8bpp, mosaic, map block16.
for index in 0..64 {
m.write16(PAL + (index + 1) * 2, color(index as usize))
.unwrap();
m.write16(PAL + 0x200 + (index + 1) * 2, color(index as usize))
.unwrap();
}
// Eight text tiles, each with a distinct color per row. The map repeats vertically.
for y in 0..64 {
for x in (0..8).step_by(2) {
m.write16(VRAM + y * 8 + x, (y as u16 + 1) * 0x101).unwrap();
}
}
for y in 0..32 {
for x in 0..32 {
m.write16(VRAM + 0x8000 + (y * 32 + x) * 2, (y % 8) as u16)
.unwrap();
}
}
for index in 0..128 {
m.write16(OAM + index * 8, 0x200).unwrap();
}
m.write16(OAM, 0x3000).unwrap(); // Mosaic 8bpp sprite at (32,0), 16x16.
m.write16(OAM + 2, 0x4020).unwrap();
for y in 0..16 {
for x in (0..16).step_by(2) {
let tile = y / 8 * 2 + x / 8;
m.write16(
VRAM + 0x10000 + tile * 64 + y % 8 * 8 + x % 8,
(y as u16 + 1) * 0x101,
)
.unwrap();
}
}
m
}
fn bitmap() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.set_scanline_rendering(true);
m.write16(DISPCNT, 0x403).unwrap();
m.write16(BG2CNT, 0x40).unwrap();
m.write16(BG2PA, 256).unwrap();
m.write16(BG2PD, 256).unwrap();
for y in 0..160 {
for x in 0..240 {
m.write16(VRAM + (y * 240 + x) * 2, color(y as usize))
.unwrap();
}
}
m
}
fn present(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
assert!(m.present_frame(&mut f).unwrap());
f
}
fn finish(m: &mut Memory) -> Framebuffer {
let p = m.display_position();
m.advance_cycles(VB - u32::from(p.scanline) * LINE - u32::from(p.line_cycle));
present(m)
}
fn at(f: &Framebuffer, x: usize, y: usize) -> u32 {
f.pixels()[y * WIDTH + x]
}
#[test]
fn all_static_heights_match_snapshots_with_independent_background_and_sprite_sizes() {
for height in 1..=16 {
let mut m = scene();
m.write16(MOSAIC, ((height - 1) << 4) | ((16 - height) << 12))
.unwrap();
let mut expected = Framebuffer::default();
m.render_frame(&mut expected).unwrap();
assert_eq!(
finish(&mut m).pixels(),
expected.pixels(),
"height={height}"
);
}
}
#[test]
fn growing_a_block_preserves_counter_instead_of_realigning_to_screen_y() {
let mut m = scene();
m.write16(MOSAIC, 0x30).unwrap(); // BG4, OBJ1.
m.advance_cycles(5 * LINE + HB);
m.write8(MOSAIC, 0x70).unwrap(); // BG8: phase1 remains phase1.
let f = finish(&mut m);
for y in 0..24 {
let source = if y < 4 {
0
} else if y < 12 {
4
} else if y < 20 {
12
} else {
20
};
assert_eq!(at(&f, 0, y), rgb(color(source)), "row={y}");
if y < 16 {
assert_eq!(at(&f, 32, y), rgb(color(y)));
}
}
let mut snapshot = Framebuffer::default();
m.render_frame(&mut snapshot).unwrap();
assert_eq!(at(&snapshot, 0, 8), rgb(color(8)));
assert_eq!(at(&f, 0, 8), rgb(color(4)));
assert_eq!(present(&m).pixels(), f.pixels());
}
#[test]
fn shrinking_below_current_phase_waits_for_four_bit_wrap() {
let mut m = scene();
m.write16(MOSAIC, 0x70).unwrap();
m.advance_cycles(6 * LINE);
m.write8(MOSAIC, 0x10).unwrap(); // Phase6 > new height2; no immediate reset.
m.advance_cycles(0);
let f = finish(&mut m);
for y in 0..24 {
let source = if y < 16 { 0 } else { y / 2 * 2 };
assert_eq!(at(&f, 0, y), rgb(color(source)), "row={y}");
}
}
#[test]
fn affine_mosaic_advances_by_current_height_when_counter_wraps_not_by_elapsed_rows() {
let mut m = bitmap();
m.write16(MOSAIC, 0x70).unwrap();
m.advance_cycles(6 * LINE + HB);
m.write8(MOSAIC, 0x10).unwrap();
let f = finish(&mut m);
for y in 0..24 {
let source = if y < 16 { 0 } else { 2 + (y - 16) / 2 * 2 };
assert_eq!(at(&f, 0, y), rgb(color(source)), "row={y}");
}
}
#[test]
fn affine_reference_writes_and_coefficients_do_not_reset_shared_mosaic_phase() {
let mut m = bitmap();
m.write16(MOSAIC, 0x70).unwrap();
m.advance_cycles(5 * LINE);
m.write32(BG2Y, 20 * 256).unwrap();
m.write16(BG2PD, 2 * 256).unwrap();
m.write8(MOSAIC, 0x10).unwrap();
let f = finish(&mut m);
for y in 0..20 {
let source = if y < 5 {
0
} else if y < 16 {
20
} else {
24 + (y - 16) / 2 * 4
};
assert_eq!(at(&f, 0, y), rgb(color(source)), "row={y}");
}
}
#[test]
fn mosaic_flags_and_display_enable_do_not_gate_counters() {
for disabled in [false, true] {
let mut m = scene();
m.write16(MOSAIC, 0x7070).unwrap();
m.write16(BG0CNT, 0x1080).unwrap();
m.write16(OAM, 0x2000).unwrap();
if disabled {
m.write16(DISPCNT, 0).unwrap();
}
m.advance_cycles(6 * LINE);
m.write16(MOSAIC, 0x1010).unwrap();
m.write16(BG0CNT, 0x10c0).unwrap();
m.write16(OAM, 0x3000).unwrap();
m.write16(DISPCNT, 0x1140).unwrap();
let f = finish(&mut m);
for y in 6..16 {
assert_eq!(at(&f, 0, y), rgb(color(0)));
// Row6 was prepared on row5, before the attribute/enable write.
let source = if y == 6 && !disabled { 6 } else { 0 };
assert_eq!(at(&f, 32, y), rgb(color(source)));
}
}
}
#[test]
fn forced_blank_does_not_pause_counters() {
let mut m = scene();
m.write16(MOSAIC, 0x7070).unwrap();
m.write16(DISPCNT, 0x11c0).unwrap();
m.advance_cycles(6 * LINE);
m.write16(MOSAIC, 0x1010).unwrap();
m.write16(DISPCNT, 0x1140).unwrap();
let f = finish(&mut m);
assert!(f.pixels()[..6 * WIDTH].iter().all(|&p| p == 0xffffff));
for y in 6..16 {
assert_eq!(at(&f, 0, y), rgb(color(0)));
}
}
#[test]
fn sprite_counter_is_independent_and_clamps_partial_first_blocks_before_transform() {
for affine in [false, true] {
let mut m = scene();
m.write16(OAM, if affine { 0x3103 } else { 0x3003 })
.unwrap(); // y3.
if affine {
m.write16(OAM + 6, 256).unwrap();
m.write16(OAM + 30, 256).unwrap();
}
m.write16(MOSAIC, 0x7030).unwrap(); // BG4, OBJ8.
m.advance_cycles(6 * LINE);
m.write8(MOSAIC + 1, 0x10).unwrap(); // OBJ2 only.
let f = finish(&mut m);
for y in 0..24 {
assert_eq!(at(&f, 0, y), rgb(color(y / 4 * 4)));
let source = if !(3..19).contains(&y) {
y / 4 * 4
} else if y < 16 {
0
} else if y < 18 {
13
} else {
15
};
assert_eq!(at(&f, 32, y), rgb(color(source)), "affine={affine} row={y}");
}
}
}
#[test]
fn object_window_ignores_live_mosaic_phase() {
let mut m = scene();
m.write16(DISPCNT, 0x9140).unwrap();
m.write16(WINOUT, 0x0100).unwrap(); // Show BG only inside OBJ window.
m.write16(BG0CNT, 0x1080).unwrap();
m.write16(OAM, 0x3800).unwrap(); // Mosaic-enabled window sprite.
for y in (1..16).step_by(2) {
for x in (0..16).step_by(2) {
let tile = y / 8 * 2 + x / 8;
m.write16(VRAM + 0x10000 + tile * 64 + y % 8 * 8 + x % 8, 0)
.unwrap();
}
}
m.write16(MOSAIC, 0x7000).unwrap();
m.advance_cycles(6 * LINE);
m.write16(MOSAIC, 0x1000).unwrap();
let f = finish(&mut m);
for y in 0..24 {
assert_eq!(
at(&f, 32, y),
if y < 16 && y % 2 == 0 {
rgb(color(y))
} else {
0
}
);
}
}
#[test]
fn vertical_hold_uses_live_palette_but_sprite_vram_changes_wait_for_preparation() {
let mut m = scene();
m.write16(MOSAIC, 0x7070).unwrap();
m.advance_cycles(3 * LINE + HB);
m.write16(PAL + 2, 31).unwrap();
m.write16(VRAM + 0x10000, 0x0202).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 3), rgb(color(0)));
assert_eq!(at(&f, 0, 4), rgb(31));
assert_eq!(at(&f, 32, 3), rgb(color(0)));
assert_eq!(at(&f, 32, 4), rgb(color(0))); // Prepared before row3 HBlank.
assert_eq!(at(&f, 32, 5), rgb(color(1)));
}
#[test]
fn hblank_dma_size_writes_preserve_phase_and_affect_next_line_transition() {
let mut m = scene();
m.write16(MOSAIC, 0x7070).unwrap();
for y in 0..160 {
m.write16(0x0200_0000 + y * 2, if y < 6 { 0x7070 } else { 0x3010 })
.unwrap();
}
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, MOSAIC).unwrap();
m.write32(DMA_BASE + 8, 0xa240_0001).unwrap();
m.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
machine.run_until_vblank(1000).unwrap();
let f = present(machine.memory());
// Independent recurrence, driven by the table value written after each row.
let (mut bg, mut obj) = (0usize, 0usize);
for y in 0..HEIGHT {
assert_eq!(at(&f, 0, y), rgb(color(y - bg)), "row={y}");
if y < 16 {
assert_eq!(at(&f, 32, y), rgb(color(y - obj)));
}
let (bg_height, obj_height) = if y < 6 { (8, 8) } else { (2, 4) };
bg = if bg + 1 == bg_height {
0
} else {
(bg + 1) % 16
};
obj = if obj + 1 == obj_height {
0
} else {
(obj + 1) % 16
};
}
assert!(machine.halted());
}
#[test]
fn vblank_resets_counters_and_capture_toggles_do_not_reset_them() {
let mut m = scene();
m.set_scanline_rendering(false);
m.write16(MOSAIC, 0x7070).unwrap();
m.advance_cycles(6 * LINE);
m.write16(MOSAIC, 0x1010).unwrap();
m.advance_cycles(VB - 6 * LINE);
m.write16(MOSAIC, 0x5050).unwrap(); // New size during VBlank.
m.advance_cycles(FRAME - VB);
m.set_scanline_rendering(true);
let f = finish(&mut m);
for y in 0..16 {
assert_eq!(at(&f, 0, y), rgb(color(y / 6 * 6)));
assert_eq!(at(&f, 32, y), rgb(color(y / 6 * 6)));
}
}

View file

@ -0,0 +1,38 @@
use gba_core::{
cpu::Cpu,
display::{CYCLES_PER_FRAME, CYCLES_PER_LINE, HBLANK_START, VBLANK_START},
dma::DMA_BASE,
input::{Button, Buttons},
io::*,
machine::{Machine, StepKind},
memory::{Memory, OAM_START as OAM, PALETTE_START as PAL, ROM_START, VRAM_START as VRAM},
video::{rgb555_to_rgb888 as rgb, Framebuffer, VideoError, HEIGHT, WIDTH},
};
use gba_demos::raster_demo::{RasterDemo, RASTER_STATE, RASTER_TABLE};
#[path = "scanlines/capture.rs"]
mod capture;
#[path = "scanlines/machine.rs"]
mod machine;
#[path = "scanlines/rendering.rs"]
mod rendering;
fn memory() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.set_scanline_rendering(true);
m
}
fn present(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
assert!(m.present_frame(&mut f).unwrap());
f
}
fn at(f: &Framebuffer, x: usize, y: usize) -> u32 {
f.pixels()[y * WIDTH + x]
}
fn word_rom(code: &[u32]) -> Vec<u8> {
code.iter().flat_map(|v| v.to_le_bytes()).collect()
}

View file

@ -0,0 +1,169 @@
use super::*;
#[test]
fn capture_is_optional_and_snapshot_api_remains_independent() {
let mut m = Memory::new(vec![]).unwrap();
m.write16(PAL, 31).unwrap();
m.advance_cycles(CYCLES_PER_FRAME * 2);
let mut f = Framebuffer::default();
f.clear(0x3e0);
assert!(!m.present_frame(&mut f).unwrap());
assert!(f.pixels().iter().all(|&p| p == rgb(0x3e0)));
assert_eq!(m.captured_vblank(), None);
m.render_frame(&mut f).unwrap();
assert!(f.pixels().iter().all(|&p| p == rgb(31)));
let cycles = m.cycles();
m.set_scanline_rendering(true);
assert_eq!(m.cycles(), cycles);
m.advance_cycles(VBLANK_START);
assert_eq!(m.captured_vblank(), Some(3));
assert_eq!(present(&m).pixels(), f.pixels());
}
#[test]
fn hblank_captures_old_row_and_later_palette_writes_do_not_recolor_it() {
let mut m = memory();
m.write16(PAL, 31).unwrap();
m.advance_cycles(HBLANK_START);
m.write16(PAL, 0x3e0).unwrap();
m.advance_cycles(VBLANK_START - HBLANK_START);
let f = present(&m);
for y in 0..HEIGHT {
assert!(f.pixels()[y * WIDTH..(y + 1) * WIDTH]
.iter()
.all(|&p| p == rgb(if y == 0 { 31 } else { 0x3e0 })));
}
m.write16(PAL, 0x7c00).unwrap();
assert_eq!(present(&m).pixels(), f.pixels());
let cycles = m.cycles();
let mut snapshot = Framebuffer::default();
m.render_frame(&mut snapshot).unwrap();
assert!(snapshot.pixels().iter().all(|&p| p == rgb(0x7c00)));
assert_eq!(m.cycles(), cycles);
}
#[test]
fn last_row_capture_does_not_publish_before_vblank_entry() {
let mut m = memory();
m.write16(PAL, 31).unwrap();
let mut f = Framebuffer::default();
f.clear(0x3e0);
m.advance_cycles(159 * CYCLES_PER_LINE + HBLANK_START);
assert!(!m.present_frame(&mut f).unwrap());
m.advance_cycles(CYCLES_PER_LINE - HBLANK_START - 1);
assert!(!m.present_frame(&mut f).unwrap());
assert!(f.pixels().iter().all(|&p| p == rgb(0x3e0)));
m.advance_cycles(1);
assert!(m.present_frame(&mut f).unwrap());
assert_eq!(m.captured_vblank(), Some(1));
assert!(f.pixels().iter().all(|&p| p == rgb(31)));
}
#[test]
fn enabling_mid_frame_discards_partial_frame_and_repeated_enable_preserves_progress() {
for start in [HBLANK_START, 40 * CYCLES_PER_LINE, VBLANK_START - 1] {
let mut m = Memory::new(vec![]).unwrap();
m.advance_cycles(start);
m.set_scanline_rendering(true);
m.write16(PAL, 31).unwrap();
m.advance_cycles(VBLANK_START - start);
assert_eq!(m.captured_vblank(), None);
m.advance_cycles(CYCLES_PER_FRAME - VBLANK_START + HBLANK_START);
m.set_scanline_rendering(true); // Must not reset row0.
m.advance_cycles(VBLANK_START - HBLANK_START);
assert_eq!(m.captured_vblank(), Some(2));
assert!(present(&m).pixels().iter().all(|&p| p == rgb(31)));
m.set_scanline_rendering(false);
assert_eq!(m.captured_vblank(), None);
assert!(!m.present_frame(&mut Framebuffer::default()).unwrap());
}
}
#[test]
fn enable_before_first_hblank_and_during_vblank_can_capture_next_complete_image() {
for start in [0, HBLANK_START - 1, VBLANK_START, CYCLES_PER_FRAME - 1] {
let mut m = Memory::new(vec![]).unwrap();
m.advance_cycles(start);
m.set_scanline_rendering(true);
let target = if start < HBLANK_START {
VBLANK_START
} else {
CYCLES_PER_FRAME + VBLANK_START
};
m.advance_cycles(target - start);
assert_eq!(
m.captured_vblank(),
Some(if start < HBLANK_START { 1 } else { 2 })
);
assert!(m.present_frame(&mut Framebuffer::default()).unwrap());
}
}
#[test]
fn large_batches_publish_latest_frame_and_zero_cycles_have_no_effect() {
let mut m = memory();
m.write16(PAL, 0x4210).unwrap();
m.advance_cycles(CYCLES_PER_FRAME * 3 + VBLANK_START);
assert_eq!(m.captured_vblank(), Some(4));
let before = m.display_position();
let f = present(&m);
m.advance_cycles(0);
m.write16(PAL, 0).unwrap();
assert_eq!(m.display_position(), before);
assert_eq!(present(&m).pixels(), f.pixels());
assert!(f.pixels().iter().all(|&p| p == rgb(0x4210)));
}
#[test]
fn first_render_error_is_reported_at_publication_without_partial_output_then_recovers() {
let mut m = memory();
m.write16(PAL, 31).unwrap();
m.advance_cycles(VBLANK_START);
let mut f = present(&m);
m.advance_cycles(CYCLES_PER_FRAME - VBLANK_START);
m.write16(DISPCNT, 6).unwrap();
m.advance_cycles(HBLANK_START);
m.write16(DISPCNT, 7).unwrap();
m.advance_cycles(CYCLES_PER_LINE);
m.write16(DISPCNT, 0).unwrap();
assert!(m.present_frame(&mut f).unwrap()); // Previous frame remains available while drawing.
m.advance_cycles(VBLANK_START - HBLANK_START - CYCLES_PER_LINE);
assert_eq!(m.captured_vblank(), Some(2));
assert_eq!(m.present_frame(&mut f), Err(VideoError::UnsupportedMode(6)));
assert!(f.pixels().iter().all(|&p| p == rgb(31)));
m.write16(PAL, 0x3e0).unwrap();
m.advance_cycles(CYCLES_PER_FRAME);
assert_eq!(m.captured_vblank(), Some(3));
assert!(m.present_frame(&mut f).unwrap());
assert!(f.pixels().iter().all(|&p| p == rgb(0x3e0)));
}
#[test]
fn split_clock_advance_matches_single_cycle_reference_and_disabled_capture_devices() {
let mut bulk = memory();
let mut single = memory();
let mut disabled = Memory::new(vec![]).unwrap();
for m in [&mut bulk, &mut single, &mut disabled] {
m.write16(DISPSTAT, 0x5038).unwrap();
m.write32(TIMER_BASE, 0x00c0_fff1).unwrap();
m.write16(IE, 15).unwrap();
m.write8(HALTCNT, 0).unwrap();
m.write16(PAL, 0x4210).unwrap();
}
let cycles = CYCLES_PER_FRAME + VBLANK_START + 73;
bulk.advance_cycles(cycles);
disabled.advance_cycles(cycles);
for _ in 0..cycles {
single.advance_cycles(1);
}
for m in [&single, &disabled] {
assert_eq!(bulk.display_position(), m.display_position());
assert_eq!(bulk.cycles(), m.cycles());
assert_eq!(bulk.halted(), m.halted());
for register in [DISPSTAT, IF, TIMER_BASE] {
assert_eq!(bulk.read16(register), m.read16(register));
}
}
assert_eq!(present(&bulk).pixels(), present(&single).pixels());
assert_eq!(bulk.captured_vblank(), single.captured_vblank());
}

View file

@ -0,0 +1,196 @@
use super::*;
#[test]
fn hblank_dma_updates_next_row_while_cpu_stays_halted() {
let mut m = memory();
m.write16(PAL, 31).unwrap();
for i in 0..160 {
m.write16(0x0200_0000 + i * 2, ((i + 1) as u16 & 31) << 5)
.unwrap();
}
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, PAL).unwrap();
m.write32(DMA_BASE + 8, 0xa240_0001).unwrap();
m.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
let cpu = machine.cpu().clone();
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(
machine.memory().display_position().line_cycle,
HBLANK_START as u16
);
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
machine.run_until_vblank(1000).unwrap();
assert_eq!(machine.cpu(), &cpu);
assert!(machine.halted());
let f = present(machine.memory());
for y in 0..HEIGHT {
let color = if y == 0 { 31 } else { (y as u16 & 31) << 5 };
assert!(f.pixels()[y * WIDTH..(y + 1) * WIDTH]
.iter()
.all(|&p| p == rgb(color)));
}
assert_eq!(machine.memory().read16(PAL).unwrap(), 0); // Row159 HBlank DMA ran after its capture.
}
#[test]
fn vblank_dma_cannot_change_the_frame_just_published() {
let mut m = memory();
m.write16(PAL, 31).unwrap();
m.write16(0x0200_0000, 0x3e0).unwrap();
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, PAL).unwrap();
m.write32(DMA_BASE + 8, 0x9000_0001).unwrap();
m.advance_cycles(VBLANK_START);
let mut machine = Machine::new(Cpu::new(ROM_START), m);
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert!(present(machine.memory())
.pixels()
.iter()
.all(|&p| p == rgb(31)));
machine.memory_mut().advance_cycles(CYCLES_PER_FRAME);
assert!(present(machine.memory())
.pixels()
.iter()
.all(|&p| p == rgb(0x3e0)));
}
#[test]
fn cpu_polling_vcount_can_split_the_frame_without_host_video_writes() {
let rom = word_rom(&[
0xe3a0_0301, // MOV r0,#0x04000000
0xe1d0_10b6, // LDRH r1,[r0,#6] (VCOUNT)
0xe351_0050, // CMP r1,#80
0x1aff_fffc, // BNE poll
0xe3a0_2405, // MOV r2,#0x05000000
0xe3a0_3c7c, // MOV r3,#0x7c00
0xe1c2_30b0, // STRH r3,[r2]
0xeaff_fffe,
]);
let mut m = Memory::new(rom).unwrap();
m.set_scanline_rendering(true);
m.write16(PAL, 31).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
machine.run_until_vblank(100_000).unwrap();
let f = present(machine.memory());
for y in 0..HEIGHT {
let color = if y < 80 { 31 } else { 0x7c00 };
assert!(f.pixels()[y * WIDTH..(y + 1) * WIDTH]
.iter()
.all(|&p| p == rgb(color)));
}
}
#[test]
fn instruction_crossing_hblank_uses_committed_write_for_entire_row() {
let rom = word_rom(&[
0xe3a0_0405, // MOV r0,#0x05000000
0xe3a0_1e3e, // MOV r1,#0x3e0
0xe1c0_10b0, // STRH r1,[r0]
]);
let mut m = Memory::new(rom).unwrap();
m.set_scanline_rendering(true);
m.write16(PAL, 31).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
machine.step().unwrap();
machine.step().unwrap();
let elapsed = machine.cycles() as u32;
machine
.memory_mut()
.advance_cycles(HBLANK_START - 1 - elapsed);
machine.step().unwrap();
assert!(machine.cycles() > u64::from(HBLANK_START));
let remaining = VBLANK_START - machine.cycles() as u32;
machine.memory_mut().advance_cycles(remaining);
// This pins the nominal whole-instruction policy, not pixel-accurate timing.
assert_eq!(at(&present(machine.memory()), 0, 0), rgb(0x3e0));
}
#[test]
fn failed_cpu_step_does_not_advance_or_discard_capture_progress() {
let mut m = Memory::new(word_rom(&[0xf000_0000])).unwrap();
m.set_scanline_rendering(true);
m.write16(PAL, 31).unwrap();
m.advance_cycles(HBLANK_START);
let mut machine = Machine::new(Cpu::new(ROM_START), m);
let before = machine.memory().display_position();
let cycles = machine.cycles();
assert!(machine.step().is_err());
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().display_position(), before);
machine.memory_mut().write16(PAL, 0x3e0).unwrap();
machine
.memory_mut()
.advance_cycles(VBLANK_START - HBLANK_START);
let f = present(machine.memory());
assert_eq!(at(&f, 0, 0), rgb(31));
assert_eq!(at(&f, 0, 1), rgb(0x3e0));
}
#[test]
fn cpu_raster_demo_copies_table_and_rearms_hblank_dma_across_frames() {
let mut demo = RasterDemo::new().unwrap();
let mut f = Framebuffer::default();
assert_eq!(demo.machine().memory().read16(RASTER_TABLE).unwrap(), 0);
for update in 1..=40 {
demo.frame(Buttons::default().with(Button::Right, true), &mut f)
.unwrap();
let phase = update % 32;
let m = demo.machine().memory();
assert_eq!(m.read32(RASTER_STATE).unwrap(), update);
assert_eq!(m.read32(RASTER_STATE + 4).unwrap(), phase);
assert_eq!(m.read16(DMA_BASE + 10).unwrap(), 0xa240);
assert_eq!(m.captured_vblank(), Some(m.display_position().vblanks));
assert_eq!(m.display_position().scanline, 160);
for y in 0..HEIGHT {
let n = (y as u16 + phase as u16) % 32;
let color = n + (31 - n) * 32 + (n / 2) * 1024;
assert!(
f.pixels()[y * WIDTH..(y + 1) * WIDTH]
.iter()
.all(|&p| p == rgb(color)),
"update={update} row={y}"
);
}
let mut snapshot = Framebuffer::default();
m.render_frame(&mut snapshot).unwrap();
assert_ne!(snapshot.pixels(), f.pixels()); // Same memory, but no row history in a snapshot.
}
assert_eq!(
demo.machine().memory().read16(RASTER_TABLE).unwrap(),
31 * 32
);
}
#[test]
fn raster_controls_wrap_cancel_opposites_and_reset() {
let mut demo = RasterDemo::new().unwrap();
let mut f = Framebuffer::default();
for (buttons, phase) in [
(Buttons::default().with(Button::Left, true), 31),
(
Buttons::default()
.with(Button::Left, true)
.with(Button::Right, true),
31,
),
(Buttons::default().with(Button::Right, true), 0),
(Buttons::default().with(Button::Right, true), 1),
(
Buttons::default()
.with(Button::Start, true)
.with(Button::Right, true),
0,
),
] {
demo.frame(buttons, &mut f).unwrap();
assert_eq!(
demo.machine().memory().read32(RASTER_STATE + 4).unwrap(),
phase
);
assert_eq!(
at(&f, 0, 0),
rgb(phase as u16 | ((31 - phase as u16) << 5) | ((phase as u16 / 2) << 10))
);
}
}

View file

@ -0,0 +1,155 @@
use super::*;
#[test]
fn static_capture_matches_snapshots_in_every_mode_with_objects_mosaic_windows_and_effects() {
for mode in 0..=5 {
let mut m = memory();
for i in 0..512 {
m.write16(PAL + i * 2, (i as u16).wrapping_mul(0x421))
.unwrap();
}
for i in 0..128 {
m.write16(OAM + i * 8, 0x200).unwrap();
}
for offset in (0..0x14000).step_by(2) {
let value = if mode < 3 {
if offset < 0x4000 {
0x1111
} else {
0
}
} else {
(offset as u16).wrapping_mul(17)
};
m.write16(VRAM + offset, value).unwrap();
}
m.write16(DISPCNT, 0x3440 | mode).unwrap();
m.write16(BG2CNT, 0x1041).unwrap();
m.write16(BG2PA, 181).unwrap();
m.write16(BG2PB, 91).unwrap();
m.write16(BG2PC, (-91i16) as u16).unwrap();
m.write16(BG2PD, 256).unwrap();
m.write32(BG2X, 513).unwrap();
m.write32(BG2Y, 7 * 256).unwrap();
m.write16(OAM, 0x1003).unwrap();
m.write16(OAM + 2, 5).unwrap();
m.write16(OAM + 4, if mode >= 3 { 512 } else { 0 }).unwrap();
let tiles = if mode >= 3 { 0x14000 } else { 0x10000 };
for offset in (0..32).step_by(2) {
m.write16(VRAM + tiles + offset, 0x1111).unwrap();
}
m.write16(MOSAIC, 0x3243).unwrap();
m.write16(WIN0H, 0x0a3c).unwrap();
m.write16(WIN0V, 0x0432).unwrap();
m.write16(WININ, 0x1f).unwrap();
m.write16(WINOUT, 0x3f).unwrap();
m.write16(BLDCNT, 0xbf).unwrap();
m.write16(BLDY, 5).unwrap();
m.write16(GREENSWAP, 1).unwrap();
let mut expected = Framebuffer::default();
m.render_frame(&mut expected).unwrap();
m.advance_cycles(VBLANK_START);
assert_eq!(present(&m).pixels(), expected.pixels(), "mode={mode}");
}
}
#[test]
fn page_palette_vram_and_oam_writes_only_change_rows_not_yet_captured() {
let mut m = memory();
m.write16(DISPCNT, 0x404).unwrap();
m.write16(BG2PA, 256).unwrap();
m.write16(BG2PD, 256).unwrap();
for i in 0..128 {
m.write16(OAM + i * 8, 0x200).unwrap();
}
for offset in (0..38400).step_by(2) {
m.write16(VRAM + offset, 0x0101).unwrap();
m.write16(VRAM + 0xa000 + offset, 0x0202).unwrap();
}
for (index, color) in [(1, 31), (2, 0x3e0), (3, 0x7fff)] {
m.write16(PAL + index * 2, color).unwrap();
}
m.advance_cycles(HBLANK_START); // Row0 page0 red.
m.write16(DISPCNT, 0x414).unwrap();
m.advance_cycles(CYCLES_PER_LINE); // Row1 page1 green.
m.write16(PAL + 4, 0x7c00).unwrap();
m.advance_cycles(CYCLES_PER_LINE); // Row2 same index, now blue.
m.write16(VRAM + 0xa000 + 3 * 240, 0x0303).unwrap();
m.advance_cycles(CYCLES_PER_LINE); // Row3 first two pixels now white.
m.write16(DISPCNT, 0x1454).unwrap();
m.write16(OAM, 0).unwrap();
m.write16(OAM + 4, 512).unwrap();
for offset in (0..32).step_by(2) {
m.write16(VRAM + 0x14000 + offset, 0x1111).unwrap();
}
m.write16(PAL + 0x202, 0).unwrap(); // Opaque black OBJ.
m.advance_cycles(VBLANK_START - (3 * CYCLES_PER_LINE + HBLANK_START));
let f = present(&m);
// Row4's sprites were prepared before the row3 HBlank writes; row5 sees them.
for (y, color) in [
(0, 31),
(1, 0x3e0),
(2, 0x7c00),
(3, 0x7fff),
(4, 0x7c00),
(5, 0),
] {
assert_eq!(at(&f, 0, y), rgb(color));
}
assert_eq!(at(&f, 8, 4), rgb(0x7c00));
assert_eq!(at(&f, 0, 8), rgb(0x7c00));
}
#[test]
fn scroll_mosaic_window_effect_and_forced_blank_changes_apply_per_row() {
let mut m = memory();
m.write16(DISPCNT, 0x100).unwrap();
m.write16(BG0CNT, 0x1000).unwrap();
m.write16(PAL, 0x4210).unwrap();
m.write16(PAL + 2, 31).unwrap();
m.write16(PAL + 4, 0x3e0).unwrap();
for offset in (0..32).step_by(2) {
m.write16(VRAM + offset, 0x2121).unwrap();
}
m.advance_cycles(HBLANK_START);
m.write16(BG0HOFS, 1).unwrap();
m.advance_cycles(CYCLES_PER_LINE);
m.write16(BG0CNT, 0x1040).unwrap();
m.write16(MOSAIC, 1).unwrap();
m.advance_cycles(CYCLES_PER_LINE);
m.write16(DISPCNT, 0x2100).unwrap();
m.write16(WIN0H, 1).unwrap();
// Set the next row's top edge; a missed row-zero edge cannot open the window.
m.write16(WIN0V, 0x03a0).unwrap();
m.write16(WININ, 0).unwrap();
m.write16(WINOUT, 0x21).unwrap();
m.write16(BLDCNT, 0xc1).unwrap();
m.write16(BLDY, 8).unwrap();
m.advance_cycles(CYCLES_PER_LINE);
m.write16(DISPCNT, 0x80).unwrap();
m.advance_cycles(VBLANK_START - (3 * CYCLES_PER_LINE + HBLANK_START));
let f = present(&m);
assert_eq!(&f.pixels()[..2], &[rgb(31), rgb(0x3e0)]);
assert_eq!(&f.pixels()[WIDTH..WIDTH + 2], &[rgb(0x3e0), rgb(31)]);
assert_eq!(&f.pixels()[2 * WIDTH..2 * WIDTH + 2], &[rgb(0x3e0); 2]);
assert_eq!(at(&f, 0, 3), rgb(0x4210));
assert_eq!(at(&f, 1, 3), rgb(0x200));
assert!(f.pixels()[4 * WIDTH..].iter().all(|&p| p == 0xffffff));
}
#[test]
fn green_swap_setting_is_captured_per_row_after_color_composition() {
let mut m = memory();
m.write16(DISPCNT, 0x403).unwrap();
m.write16(BG2PA, 256).unwrap();
m.write16(BG2PD, 256).unwrap();
for row in 0..2 {
m.write32(VRAM + row * 480, 0x03e0_001f).unwrap();
}
m.advance_cycles(HBLANK_START);
m.write16(GREENSWAP, 1).unwrap();
m.advance_cycles(VBLANK_START - HBLANK_START);
let f = present(&m);
assert_eq!(&f.pixels()[..2], &[0xff0000, 0xff00]);
assert_eq!(&f.pixels()[WIDTH..WIDTH + 2], &[0xffff00, 0]);
}

View file

@ -0,0 +1,306 @@
//! Regression tests for the nominal allowance, not hardware fetch cutoffs.
use gba_core::{
cpu::Cpu,
display::{CYCLES_PER_LINE as LINE, VBLANK_START as VB},
dma::DMA_BASE,
io::*,
machine::Machine,
memory::{Memory, OAM_START as OAM, PALETTE_START as PAL, ROM_START, VRAM_START as VRAM},
video::{rgb555_to_rgb888 as rgb, Framebuffer, VideoError, WIDTH},
};
fn scene(free: bool) -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.set_scanline_rendering(true);
m.write16(DISPCNT, if free { 0x1060 } else { 0x1040 })
.unwrap();
m.write16(PAL, 0x7c00).unwrap();
m.write16(PAL + 0x202, 31).unwrap();
m.write16(PAL + 0x204, 0x3e0).unwrap();
for i in 0..128 {
obj(&mut m, i, 0x200, 0, 0);
}
// Tile zero is transparent. Separate solid 64x64 textures use indices 1/2.
for offset in (0..2048).step_by(2) {
m.write16(VRAM + 0x11000 + offset, 0x1111).unwrap();
m.write16(VRAM + 0x12000 + offset, 0x2222).unwrap();
m.write16(VRAM + 0x14000 + offset, 0x2222).unwrap();
}
m
}
fn obj(m: &mut Memory, index: u32, a: u16, b: u16, c: u16) {
m.write16(OAM + index * 8, a).unwrap();
m.write16(OAM + index * 8 + 2, b).unwrap();
m.write16(OAM + index * 8 + 4, c).unwrap();
}
fn fillers(m: &mut Memory, count: u32) {
for i in 0..count {
obj(m, i, 0, 0xc000, 0);
}
}
fn snapshot(m: &Memory) -> Framebuffer {
let before = (m.cycles(), m.display_position());
let mut f = Framebuffer::default();
m.render_frame(&mut f).unwrap();
assert_eq!((m.cycles(), m.display_position()), before);
f
}
fn finish(m: &mut Memory) -> Framebuffer {
let p = m.display_position();
m.advance_cycles(VB - u32::from(p.scanline) * LINE - u32::from(p.line_cycle));
let mut f = Framebuffer::default();
assert!(m.present_frame(&mut f).unwrap());
f
}
fn at(f: &Framebuffer, x: usize, y: usize) -> u32 {
f.pixels()[y * WIDTH + x]
}
fn prefix(f: &Framebuffer, y: usize, left: usize, width: usize, visible: usize) {
for x in left..left + width {
assert_eq!(
at(f, x, y),
rgb(if x < left + visible { 0x3e0 } else { 0x7c00 }),
"pixel={x},{y}, prefix={visible}"
);
}
}
#[test]
fn regular_prefix_exhausts_allowance_and_omits_later_entries() {
for free in [false, true] {
let mut m = scene(free);
let count = if free { 14 } else { 18 };
fillers(&mut m, count);
obj(&mut m, count, 0, 0xc000 | 80, 256 | 0xc00);
obj(&mut m, count + 1, 0, 0xc000 | 160, 256);
let f = snapshot(&m);
prefix(&f, 0, 80, 64, 58);
prefix(&f, 0, 160, 64, 0);
assert_eq!(finish(&mut m).pixels(), f.pixels());
}
}
#[test]
fn small_sprites_have_no_fixed_thirty_two_object_limit() {
for (free, count, visible) in [(false, 127, 8), (true, 119, 2), (true, 120, 0)] {
let mut m = scene(free);
for i in 0..count {
obj(&mut m, i, 0, 0, 0);
}
obj(&mut m, count, 0, 80, 256);
prefix(&snapshot(&m), 0, 80, 8, visible);
}
}
#[test]
fn affine_setup_and_double_size_use_drawing_canvas_width() {
for free in [false, true] {
for doubled in [false, true] {
let mut m = scene(free);
let count = if free { 14 } else { 18 };
fillers(&mut m, count); // 58 cycles remain.
// Zero matrices sample the opaque center for the entire canvas.
obj(&mut m, count, 0x100, 80, 256); // 10+2*8 = 26.
obj(
&mut m,
count + 1,
if doubled { 0x300 } else { 0x100 },
if doubled { 112 } else { 0x4000 | 112 },
256,
); // 32 cycles remain: setup plus 11 canvas pixels.
let f = snapshot(&m);
prefix(&f, 0, 80, 8, 8);
prefix(&f, 0, 112, 16, 11);
}
}
}
#[test]
fn unfinished_affine_setup_does_not_refund_work_to_regular_objects() {
for free in [false, true] {
let mut m = scene(free);
let count = if free { 14 } else { 18 };
fillers(&mut m, count);
obj(&mut m, count, 0, 0x8000, 0); // 32 cycles.
obj(&mut m, count + 1, 0, 0x4000, 0); // 16 cycles.
obj(&mut m, count + 2, 0x100, 80, 256); // Only setup fits.
obj(&mut m, count + 3, 0, 96, 256);
let f = snapshot(&m);
prefix(&f, 0, 80, 8, 0);
prefix(&f, 0, 96, 8, 0);
}
}
#[test]
fn disabled_off_row_and_fully_offscreen_entries_cost_inspection_only() {
for free in [false, true] {
for (a, b) in [(0x200, 0), (80, 0xc000), (0, 0xc000 | 240)] {
let mut m = scene(free);
for i in 0..100 {
obj(&mut m, i, a, b, 0);
}
let count = if free { 11 } else { 15 };
for i in 100..100 + count {
obj(&mut m, i, 0, 0xc000, 0);
}
obj(&mut m, 100 + count, 0, 0xc000 | 80, 256);
prefix(&snapshot(&m), 0, 80, 64, 50);
}
}
}
#[test]
fn partially_clipped_objects_charge_full_canvas_in_this_nominal_model() {
let mut m = scene(false);
fillers(&mut m, 18);
obj(&mut m, 18, 0, 0xc000 | 508, 256); // Left four pixels are clipped.
prefix(&snapshot(&m), 0, 0, 60, 54);
let mut m = scene(false);
fillers(&mut m, 17);
obj(&mut m, 17, 0, 0xc000 | 238, 256); // Only two pixels are on screen.
obj(&mut m, 18, 0, 0xc000 | 80, 256);
let f = snapshot(&m);
prefix(&f, 0, 238, 2, 2);
prefix(&f, 0, 80, 64, 58);
}
#[test]
fn transparent_semitransparent_and_disabled_obj_window_pixels_still_cost_work() {
for a in [0, 0x400, 0x800] {
let mut m = scene(false);
for i in 0..18 {
obj(&mut m, i, a, 0xc000, if a == 0x800 { 128 } else { 0 });
}
obj(&mut m, 18, 0, 0xc000 | 80, 256);
prefix(&snapshot(&m), 0, 80, 64, 58);
}
}
#[test]
fn oam_order_not_display_priority_allocates_work_even_for_occluded_objects() {
let mut m = scene(false);
for i in 0..18 {
obj(&mut m, i, 0, 0xc000 | 80, 128 | 0xc00);
}
obj(&mut m, 18, 0, 0xc000 | 80, 256);
let f = snapshot(&m);
for x in 80..144 {
assert_eq!(at(&f, x, 0), rgb(if x < 138 { 0x3e0 } else { 31 }));
}
}
#[test]
fn large_affine_and_out_of_texture_samples_use_full_canvas_cost() {
for doubled in [false, true] {
let mut m = scene(false);
let count = if doubled { 4 } else { 8 };
for i in 0..count {
obj(&mut m, i, if doubled { 0x300 } else { 0x100 }, 0xc000, 0);
}
// Most samples are outside the texture. This does not refund work.
m.write16(OAM + 6, 0x7fff).unwrap();
m.write16(OAM + 30, 0x7fff).unwrap();
obj(&mut m, count, 0, 0xc000 | 16, 256);
obj(&mut m, count + 1, 0, 0xc000 | 80, 256);
obj(&mut m, count + 2, 0, 0xc000 | 144, 256);
let f = snapshot(&m);
prefix(&f, 0, 16, 64, 64);
prefix(&f, 0, 80, 64, if doubled { 64 } else { 42 });
prefix(&f, 0, 144, 64, if doubled { 18 } else { 0 });
}
}
#[test]
fn bitmap_restricted_tiles_do_not_refund_sprite_work() {
let mut m = scene(false);
m.write16(DISPCNT, 0x1043).unwrap();
fillers(&mut m, 18); // Tile zero is unavailable to OBJ in bitmap modes.
obj(&mut m, 18, 0, 0xc000 | 80, 512);
prefix(&snapshot(&m), 0, 80, 64, 58);
}
#[test]
fn each_row_gets_an_independent_allowance() {
let mut m = scene(false);
for i in 0..18 {
obj(&mut m, i, 1, 0xc000, 0);
}
obj(&mut m, 18, 0, 0xc000 | 80, 256);
let f = snapshot(&m);
prefix(&f, 0, 80, 64, 64);
prefix(&f, 1, 80, 64, 58);
assert_eq!(finish(&mut m).pixels(), f.pixels());
}
#[test]
fn hblank_free_is_sampled_at_preparation_not_composition() {
for elapsed in [39, 40, 41] {
let mut m = scene(false);
fillers(&mut m, 18);
obj(&mut m, 18, 0, 0xc000 | 80, 256);
m.advance_cycles(elapsed);
m.write16(DISPCNT, 0x1060).unwrap();
prefix(&snapshot(&m), 0, 80, 64, 0);
let f = finish(&mut m);
prefix(&f, 0, 80, 64, 58);
prefix(&f, 1, 80, 64, if elapsed < 40 { 0 } else { 58 });
prefix(&f, 2, 80, 64, 0);
}
}
#[test]
fn hblank_dma_updates_allowance_for_the_following_preparation() {
let mut m = scene(false);
fillers(&mut m, 18);
obj(&mut m, 18, 0, 0xc000 | 80, 256);
for row in 0..160 {
m.write16(
0x0200_0000 + row * 2,
if row % 2 == 0 { 0x1060 } else { 0x1040 },
)
.unwrap();
}
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, DISPCNT).unwrap();
m.write32(DMA_BASE + 8, 0xa240_0001).unwrap();
m.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
machine.run_until_vblank(1000).unwrap();
let mut f = Framebuffer::default();
assert!(machine.memory().present_frame(&mut f).unwrap());
for y in 0..64 {
prefix(&f, y, 80, 64, if y < 2 || y % 2 == 1 { 58 } else { 0 });
}
assert!(machine.halted());
}
#[test]
fn exhausted_work_preserves_development_diagnostics_and_output_atomicity() {
let mut m = scene(false);
fillers(&mut m, 19);
obj(&mut m, 127, 0xc00, 0, 0);
let mut f = Framebuffer::default();
f.clear(0x3e0);
assert!(matches!(
m.render_frame(&mut f),
Err(VideoError::UnsupportedObject { index: 127, .. })
));
assert!(f.pixels().iter().all(|&p| p == rgb(0x3e0)));
m.advance_cycles(VB);
assert!(matches!(
m.present_frame(&mut f),
Err(VideoError::UnsupportedObject { index: 127, .. })
));
assert!(f.pixels().iter().all(|&p| p == rgb(0x3e0)));
for control in [0x40, 0x10c0] {
m.write16(DISPCNT, control).unwrap();
m.render_frame(&mut f).unwrap();
}
}

View file

@ -0,0 +1,288 @@
use gba_core::{
cpu::Cpu,
display::{CYCLES_PER_FRAME as FRAME, CYCLES_PER_LINE as LINE, VBLANK_START as VB},
dma::DMA_BASE,
io::*,
machine::Machine,
memory::{Memory, OAM_START as OAM, PALETTE_START as PAL, ROM_START, VRAM_START as VRAM},
video::{rgb555_to_rgb888 as rgb, Framebuffer, VideoError, WIDTH},
};
fn scene() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.set_scanline_rendering(true);
m.write16(DISPCNT, 0x1040).unwrap();
m.write16(PAL, 0x7c00).unwrap();
m.write16(PAL + 0x202, 31).unwrap();
m.write16(PAL + 0x204, 0x3e0).unwrap();
for i in 0..128 {
m.write16(OAM + i * 8, 0x200).unwrap();
}
m.write16(OAM, 0).unwrap();
for offset in (0..32).step_by(2) {
m.write16(VRAM + 0x10000 + offset, 0x1111).unwrap();
}
m
}
fn finish(m: &mut Memory) -> Framebuffer {
let p = m.display_position();
m.advance_cycles(VB - u32::from(p.scanline) * LINE - u32::from(p.line_cycle));
present(m)
}
fn present(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
assert!(m.present_frame(&mut f).unwrap());
f
}
fn at(f: &Framebuffer, x: usize, y: usize) -> u32 {
f.pixels()[y * WIDTH + x]
}
#[test]
fn oam_position_writes_obey_preceding_lines_cycle40_boundary() {
for elapsed in [39, 40, 41] {
let mut m = scene();
m.advance_cycles(elapsed);
m.write16(OAM + 2, 16).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 0), rgb(31)); // Reset row0 was already prepared.
let x = if elapsed < 40 { 16 } else { 0 };
assert_eq!(at(&f, x, 1), rgb(31));
assert_eq!(at(&f, 16 - x, 1), rgb(0x7c00));
assert_eq!(at(&f, 16, 2), rgb(31));
}
}
#[test]
fn buffered_indices_keep_vram_history_but_resolve_current_palette() {
let mut m = scene();
m.advance_cycles(40);
for offset in (0..32).step_by(2) {
m.write16(VRAM + 0x10000 + offset, 0x2222).unwrap();
}
m.write16(PAL + 0x202, 0x7fff).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 0), rgb(0x7fff));
assert_eq!(at(&f, 0, 1), rgb(0x7fff));
assert_eq!(at(&f, 0, 2), rgb(0x3e0));
let mut snapshot = Framebuffer::default();
m.render_frame(&mut snapshot).unwrap();
assert_eq!(at(&snapshot, 0, 1), rgb(0x3e0));
assert_eq!(present(&m).pixels(), f.pixels());
}
#[test]
fn affine_matrix_and_tile_samples_are_frozen_at_preparation() {
let mut m = scene();
m.write16(OAM, 0x2100).unwrap(); // Affine, 8bpp.
m.write16(OAM + 6, 256).unwrap();
m.write16(OAM + 30, 256).unwrap();
for y in 0..8 {
for x in (0..8).step_by(2) {
m.write16(
VRAM + 0x10000 + y * 8 + x,
if x < 4 { 0x0101 } else { 0x0202 },
)
.unwrap();
}
}
m.advance_cycles(40);
m.write16(OAM + 6, 0).unwrap(); // PA0 samples the texture center for every X.
let f = finish(&mut m);
assert_eq!(at(&f, 0, 1), rgb(31));
assert_eq!(at(&f, 0, 2), rgb(0x3e0));
}
#[test]
fn semitransparency_is_buffered_but_blend_coefficients_remain_live() {
let mut m = scene();
m.write16(OAM, 0x400).unwrap();
m.advance_cycles(40);
m.write16(OAM, 0).unwrap();
m.write16(BLDCNT, 0x2000).unwrap(); // Backdrop as second target.
m.write16(BLDALPHA, 0x0808).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 1), rgb(0x3c0f));
assert_eq!(at(&f, 0, 2), rgb(31));
}
#[test]
fn obj_window_coverage_is_buffered_even_before_its_display_mask_is_enabled() {
let mut m = scene();
m.write16(OAM, 0x800).unwrap();
m.advance_cycles(40);
m.write16(OAM + 2, 16).unwrap();
m.write16(DISPCNT, 0x9040).unwrap();
m.write16(WINOUT, 0x2000).unwrap();
m.write16(BLDCNT, 0xa0).unwrap();
m.write16(BLDY, 16).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 1), rgb(0x7fff));
assert_eq!(at(&f, 16, 1), rgb(0x7c00));
assert_eq!(at(&f, 0, 2), rgb(0x7c00));
assert_eq!(at(&f, 16, 2), rgb(0x7fff));
}
#[test]
fn horizontal_mosaic_uses_current_size_and_prepared_sprite_metadata() {
let mut m = scene();
m.write16(OAM, 0x1000).unwrap();
for offset in (0..32).step_by(2) {
m.write16(VRAM + 0x10000 + offset, 0x2121).unwrap();
}
m.advance_cycles(40);
m.write16(OAM, 0).unwrap();
m.write16(MOSAIC, 0x0300).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 1, 1), rgb(31));
assert_eq!(at(&f, 1, 2), rgb(0x3e0));
}
#[test]
fn global_enable_gates_preparation_and_current_composition() {
let mut m = scene();
m.write16(DISPCNT, 0x40).unwrap();
m.advance_cycles(40);
m.write16(DISPCNT, 0x1040).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 0), rgb(0x7c00));
assert_eq!(at(&f, 0, 1), rgb(0x7c00));
assert_eq!(at(&f, 0, 2), rgb(31));
let mut m = scene();
m.advance_cycles(40);
m.write16(DISPCNT, 0x40).unwrap();
assert_eq!(at(&finish(&mut m), 0, 1), rgb(0x7c00));
}
#[test]
fn forced_blank_does_not_stop_preparation_in_this_row_model() {
let mut m = scene();
m.write16(DISPCNT, 0x10c0).unwrap();
m.advance_cycles(40);
m.write16(OAM + 2, 16).unwrap();
m.write16(DISPCNT, 0x1040).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 1), rgb(31));
assert_eq!(at(&f, 16, 2), rgb(31));
}
#[test]
fn hblank_dma_oam_changes_wait_until_a_later_preparation_event() {
let mut m = scene();
for row in 0..160 {
m.write16(0x0200_0000 + row * 2, ((row + 1) % 20 * 8) as u16)
.unwrap();
}
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, OAM + 2).unwrap();
m.write32(DMA_BASE + 8, 0xa240_0001).unwrap();
m.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
machine.run_until_vblank(1000).unwrap();
let f = present(machine.memory());
for y in 0usize..8 {
let left = y.saturating_sub(1) * 8;
for x in 0..WIDTH {
assert_eq!(
at(&f, x, y),
rgb(if (left..left + 8).contains(&x) {
31
} else {
0x7c00
}),
"pixel={x},{y}"
);
}
}
assert!(machine.halted());
}
#[test]
fn final_hidden_line_prepares_row_zero_before_frame_wrap() {
for cycle in [39, 40] {
let mut m = scene();
finish(&mut m);
m.advance_cycles(227 * LINE + cycle - VB);
m.write16(OAM + 2, 16).unwrap();
m.advance_cycles(FRAME - 227 * LINE - cycle);
let f = finish(&mut m);
assert_eq!(at(&f, if cycle < 40 { 16 } else { 0 }, 0), rgb(31));
assert_eq!(at(&f, 16, 1), rgb(31));
}
}
#[test]
fn capture_toggles_preserve_prepared_rows_after_large_capture_disabled_batches() {
for frames in [0, 3] {
let mut m = scene();
m.set_scanline_rendering(false);
m.advance_cycles(frames * FRAME + 41);
m.write16(OAM + 2, 16).unwrap();
m.set_scanline_rendering(true);
m.set_scanline_rendering(false);
m.set_scanline_rendering(true);
let f = finish(&mut m);
assert_eq!(at(&f, 0, 0), rgb(31));
assert_eq!(at(&f, 0, 1), rgb(31));
assert_eq!(at(&f, 16, 2), rgb(31));
}
}
#[test]
fn prepared_errors_are_deferred_until_visible_composition_and_recover_next_frame() {
let mut m = scene();
m.advance_cycles(1); // Bootstrap row0 remains valid.
m.write16(OAM, 0xc00).unwrap();
m.advance_cycles(39); // Prepare invalid row1; clock advancement itself succeeds.
m.write16(OAM, 0).unwrap();
m.advance_cycles(VB - 40);
let mut f = Framebuffer::default();
f.clear(0x3e0);
assert!(matches!(
m.present_frame(&mut f),
Err(VideoError::UnsupportedObject { index: 0, .. })
));
assert!(f.pixels().iter().all(|&p| p == rgb(0x3e0)));
m.advance_cycles(FRAME);
assert!(m.present_frame(&mut f).unwrap());
assert_eq!(at(&f, 0, 1), rgb(31));
}
#[test]
fn hidden_preparation_errors_do_not_bypass_global_disable_or_forced_blank() {
for control in [0x40, 0x10c0] {
let mut m = scene();
m.write16(OAM, 0xc00).unwrap();
m.advance_cycles(40);
m.write16(DISPCNT, control).unwrap();
let f = finish(&mut m);
assert_eq!(
at(&f, 0, 1),
if control & 0x80 != 0 {
0xffffff
} else {
rgb(0x7c00)
}
);
}
}
#[test]
fn zero_cycle_advances_and_failed_cpu_steps_do_not_prepare_rows() {
let mut m = scene();
m.advance_cycles(0);
m.write16(OAM + 2, 16).unwrap();
m.advance_cycles(39);
m.write32(0x0300_0000, 0xf000_0000).unwrap();
let mut machine = Machine::new(Cpu::new(0x0300_0000), m);
assert!(machine.step().is_err());
assert_eq!(machine.cycles(), 39);
machine.memory_mut().write16(OAM + 2, 24).unwrap();
let f = finish(machine.memory_mut());
assert_eq!(at(&f, 16, 0), rgb(31));
assert_eq!(at(&f, 24, 1), rgb(31));
}

View file

@ -0,0 +1,370 @@
use gba_core::{
io::{BG0CNT, BG2CNT, DISPCNT, GREENSWAP},
memory::{Memory, MemoryError, OAM_START as OAM, PALETTE_START as PAL, VRAM_START as VRAM},
video::{rgb555_to_rgb888, Framebuffer, VideoError, WIDTH},
};
#[path = "sprites/affine.rs"]
mod affine;
fn setup() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.write16(DISPCNT, 0x1040).unwrap(); // OBJ, 1D, Mode 0
for i in 0..128 {
m.write16(OAM + i * 8, 0x200).unwrap();
}
for (i, color) in [0x4210, 0x001f, 0x03e0, 0x7c00, 0x7fff]
.into_iter()
.enumerate()
{
m.write16(PAL + 0x200 + i as u32 * 2, color).unwrap();
}
m.write16(PAL, 0x4210).unwrap();
m
}
fn obj(m: &mut Memory, index: u32, a: u16, b: u16, c: u16) {
m.write16(OAM + index * 8, a).unwrap();
m.write16(OAM + index * 8 + 2, b).unwrap();
m.write16(OAM + index * 8 + 4, c).unwrap();
}
fn fill(m: &mut Memory, slot: u32, bytes: u32, value: u16) {
for offset in (0..bytes).step_by(2) {
m.write16(VRAM + 0x10000 + ((slot * 32 + offset) & 0x7fff), value)
.unwrap();
}
}
fn render(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
m.render_frame(&mut f).unwrap();
f
}
fn pixel(m: &Memory, x: usize, y: usize) -> u32 {
render(m).pixels()[y * WIDTH + x]
}
#[test]
fn oam_mirrors_reads_all_bytes_and_ignores_only_byte_writes() {
let mut m = setup();
m.write32(OAM, 0xabcd_1234).unwrap();
assert_eq!(m.read32(OAM + 0x400).unwrap(), 0xabcd_1234);
assert_eq!(m.read8(OAM + 1).unwrap(), 0x12);
m.write8(OAM, 0xff).unwrap();
m.write8(OAM + 3, 0xff).unwrap();
assert_eq!(m.read32(OAM).unwrap(), 0xabcd_1234);
m.write32(0x07ff_fffc, 0x7654_3210).unwrap();
assert_eq!(m.read32(OAM + 0x3fc).unwrap(), 0x7654_3210);
m.write16(OAM + 6, 0xface).unwrap(); // Affine parameter slots are ordinary memory.
assert_eq!(m.read16(OAM + 0x406).unwrap(), 0xface);
assert_eq!(m.write16(OAM + 1, 0), Err(MemoryError::Unaligned(OAM + 1)));
assert_eq!(m.write32(OAM + 2, 0), Err(MemoryError::Unaligned(OAM + 2)));
assert_eq!(m.read32(OAM).unwrap(), 0xabcd_1234);
}
#[test]
fn four_bit_sprite_nibbles_banks_zero_transparency_and_opaque_black() {
let mut m = setup();
obj(&mut m, 0, 0, 0, 0xf000);
fill(&mut m, 0, 2, 0x0321);
m.write16(PAL + 0x200 + 241 * 2, 0x001f).unwrap();
m.write16(PAL + 0x200 + 242 * 2, 0x03e0).unwrap();
m.write16(PAL + 0x200 + 243 * 2, 0).unwrap();
m.write16(PAL + 0x200 + 240 * 2, 0x7fff).unwrap();
assert_eq!(
&render(&m).pixels()[..4],
&[0xff0000, 0x00ff00, 0, rgb555_to_rgb888(0x4210)]
);
}
#[test]
fn eight_bit_sprite_ignores_bank_and_uses_obj_palette_not_bg_palette() {
let mut m = setup();
obj(&mut m, 0, 0x2000, 0, 0xf000);
fill(&mut m, 0, 2, 0x00ff);
m.write16(PAL + 0x3fe, 0x03e0).unwrap();
m.write16(PAL + 0x1fe, 0x001f).unwrap();
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
assert_eq!(pixel(&m, 1, 0), rgb555_to_rgb888(0x4210));
}
#[test]
fn every_shape_and_size_covers_exact_rectangle() {
let sizes = [
[(8, 8), (16, 16), (32, 32), (64, 64)],
[(16, 8), (32, 8), (32, 16), (64, 32)],
[(8, 16), (8, 32), (16, 32), (32, 64)],
];
for (shape, dimensions) in sizes.iter().enumerate() {
for (size, &(w, h)) in dimensions.iter().enumerate() {
let mut m = setup();
fill(&mut m, 0, 0x8000, 0x1111);
obj(
&mut m,
0,
((shape as u16) << 14) | 5,
((size as u16) << 14) | 7,
0,
);
let f = render(&m);
for y in 0..160 {
for x in 0..240 {
let expected = if (7..7 + w).contains(&x) && (5..5 + h).contains(&y) {
0xff0000
} else {
rgb555_to_rgb888(0x4210)
};
assert_eq!(
f.pixels()[y * WIDTH + x],
expected,
"shape {shape}, size {size}, pixel {x},{y}"
);
}
}
}
}
}
#[test]
fn tile_row_strides_differ_between_1d_and_2d_in_both_depths() {
for eight in [false, true] {
for one in [false, true] {
let mut m = setup();
m.write16(DISPCNT, if one { 0x1040 } else { 0x1000 })
.unwrap();
obj(&mut m, 0, if eight { 0x2000 } else { 0 }, 0x4000, 0);
let slots = if eight { 2 } else { 1 };
for (slot, index) in [
(0, 1),
(slots, 2),
(if one { 2 * slots } else { 32 }, 3),
(if one { 3 * slots } else { 32 + slots }, 4),
] {
fill(
&mut m,
slot,
slots * 32,
if eight { index * 0x101 } else { index * 0x1111 },
);
}
let f = render(&m);
for (x, y, color) in [
(0, 0, 0xff0000),
(8, 0, 0x00ff00),
(0, 8, 0x0000ff),
(8, 8, 0xffffff),
] {
assert_eq!(f.pixels()[y * WIDTH + x], color, "8bit={eight} 1D={one}");
}
}
}
}
#[test]
fn eight_bit_odd_base_is_preserved_in_1d_but_aligned_in_2d() {
let mut m = setup();
fill(&mut m, 0, 32, 0x0101);
fill(&mut m, 1, 32, 0x0202);
obj(&mut m, 0, 0x2000, 0, 1);
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
m.write16(DISPCNT, 0x1000).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000);
}
#[test]
fn mapping_wraps_horizontal_2d_rows_and_the_32k_obj_area() {
let mut m = setup();
obj(&mut m, 0, 0x4000, 0, 1023); // 16x8
fill(&mut m, 1023, 32, 0x1111);
fill(&mut m, 0, 32, 0x2222);
fill(&mut m, 992, 32, 0x3333);
assert_eq!(pixel(&m, 8, 0), 0x00ff00); // 1D wraps entire area.
m.write16(DISPCNT, 0x1000).unwrap();
assert_eq!(pixel(&m, 8, 0), 0x0000ff); // 2D wraps within last row.
obj(&mut m, 0, 0, 0x4000, 992); // 16x16
assert_eq!(pixel(&m, 0, 8), 0x00ff00); // Next row wraps to first row.
}
#[test]
fn flips_reverse_whole_multi_tile_sprite_not_each_tile() {
for eight in [false, true] {
let mut m = setup();
let slots = if eight { 2 } else { 1 };
for i in 0..4 {
fill(
&mut m,
i * slots,
32 * slots,
if eight {
(i as u16 + 1) * 0x101
} else {
(i as u16 + 1) * 0x1111
},
);
}
for (flips, color) in [
(0, 0xff0000),
(0x1000, 0x00ff00),
(0x2000, 0x0000ff),
(0x3000, 0xffffff),
] {
obj(&mut m, 0, if eight { 0x2000 } else { 0 }, 0x4000 | flips, 0);
assert_eq!(pixel(&m, 0, 0), color);
}
}
}
#[test]
fn screen_coordinates_wrap_and_clip_at_each_edge() {
let mut m = setup();
fill(&mut m, 0, 32, 0x1111);
for (ox, oy) in [(508, 252), (238, 158), (250, 170)] {
obj(&mut m, 0, oy, ox, 0);
let f = render(&m);
for y in 0..160 {
for x in 0..240 {
let inside =
((x + 512 - ox as usize) & 511) < 8 && ((y + 256 - oy as usize) & 255) < 8;
assert_eq!(
f.pixels()[y * WIDTH + x],
if inside {
0xff0000
} else {
rgb555_to_rgb888(0x4210)
}
);
}
}
}
}
#[test]
fn object_priority_wins_then_oam_index_and_transparent_pixels_expose_later_objs() {
let mut m = setup();
fill(&mut m, 0, 32, 0x1111);
fill(&mut m, 1, 32, 0x2222);
obj(&mut m, 0, 0, 0, 0xc00); // First OBJ has lowest BG priority.
obj(&mut m, 127, 0, 0, 1); // Later OBJ has highest BG priority.
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
obj(&mut m, 127, 0, 0, 0xc01); // Equal priority: first OAM entry wins.
assert_eq!(pixel(&m, 0, 0), 0xff0000);
// BG0 priority2 covers both priority3 objects.
m.write16(DISPCNT, 0x1140).unwrap();
m.write16(BG0CNT, 0x1002).unwrap();
m.write16(VRAM, 0x3333).unwrap();
m.write16(PAL + 6, 0x7c00).unwrap();
assert_eq!(pixel(&m, 0, 0), 0x0000ff);
obj(&mut m, 0, 0, 0, 0);
obj(&mut m, 127, 0, 0, 1);
fill(&mut m, 0, 2, 0x0010); // Zero only at first pixel.
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
assert_eq!(pixel(&m, 1, 0), 0xff0000);
}
#[test]
fn sprite_beats_bg_on_equal_priority_but_not_higher_priority_bg() {
let mut m = setup();
obj(&mut m, 0, 0, 0, 0x400);
fill(&mut m, 0, 32, 0x1111);
m.write16(DISPCNT, 0x1140).unwrap();
m.write16(BG0CNT, 0x1001).unwrap();
m.write16(VRAM, 0x2222).unwrap();
m.write16(PAL + 4, 0x03e0).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000);
m.write16(BG0CNT, 0x1000).unwrap();
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
m.write16(VRAM, 0).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000); // Transparent BG reveals sprite.
}
#[test]
fn mode3_uses_bg2_priority_and_only_upper_obj_tile_numbers() {
let mut m = setup();
m.write16(DISPCNT, 0x1443).unwrap();
m.write16(VRAM, 0x03e0).unwrap();
fill(&mut m, 0, 32, 0x1111);
fill(&mut m, 512, 32, 0x1111);
obj(&mut m, 0, 0, 0, 0);
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
obj(&mut m, 0, 0, 0, 512);
assert_eq!(pixel(&m, 0, 0), 0xff0000);
obj(&mut m, 0, 0, 0, 512 | 0x400);
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
m.write16(BG2CNT, 1).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000);
m.write16(DISPCNT, 0x1043).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000); // BG2 disabled.
}
#[test]
fn disabled_objects_ignore_unsupported_bits_and_global_disable_skips_all_objects() {
let mut m = setup();
obj(&mut m, 0, 0xfe00, 0xffff, 0xffff); // Disabled, despite mode/mosaic/shape bits.
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(0x4210));
obj(&mut m, 0, 0xc00, 0, 0); // Prohibited OBJ mode.
m.write16(DISPCNT, 0).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(0x4210));
}
#[test]
fn unsupported_objects_leave_output_unchanged_and_forced_blank_bypasses_errors() {
let mut m = setup();
fill(&mut m, 0, 32, 0x2222);
obj(&mut m, 0, 0, 0, 0);
let mut frame = Framebuffer::default();
frame.clear(0x001f);
for attr in [0xc00, 0xc000, 0xd00, 0x1c00, 0xc300] {
obj(&mut m, 127, attr, 0, 0);
assert!(matches!(
m.render_frame(&mut frame),
Err(VideoError::UnsupportedObject { index: 127, .. })
));
assert!(frame.pixels().iter().all(|&p| p == 0xff0000));
}
m.write16(DISPCNT, 0x1080).unwrap();
m.render_frame(&mut frame).unwrap();
assert!(frame.pixels().iter().all(|&p| p == 0xffffff));
}
#[test]
fn green_swap_applies_after_sprite_composition() {
let mut m = setup();
obj(&mut m, 0, 0, 0, 0);
fill(&mut m, 0, 2, 0x21);
m.write16(GREENSWAP, 1).unwrap();
assert_eq!(&render(&m).pixels()[..2], &[0xffff00, 0]);
}
#[test]
fn oam_bus_uses_one_cycle_for_every_width() {
use gba_core::timing::{bus_cycles, AccessKind, AccessWidth};
for kind in [AccessKind::Sequential, AccessKind::NonSequential] {
for width in [AccessWidth::Byte, AccessWidth::Halfword, AccessWidth::Word] {
assert_eq!(bus_cycles(0, OAM, width, kind), 1);
}
}
}
#[test]
fn dma_copies_words_to_oam_and_reads_them_back() {
use gba_core::{
cpu::Cpu,
dma::DMA_BASE,
machine::{Machine, StepKind},
};
let mut machine = Machine::new(Cpu::new(0x0300_0000), setup());
machine
.memory_mut()
.write32(0x0200_0000, 0x1234_abcd)
.unwrap();
for (source, dest) in [(0x0200_0000, OAM + 0x400), (OAM, 0x0200_0004)] {
let m = machine.memory_mut();
m.write32(DMA_BASE + 36, source).unwrap();
m.write32(DMA_BASE + 40, dest).unwrap();
m.write32(DMA_BASE + 44, 0x8400_0001).unwrap();
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
}
assert_eq!(machine.memory().read32(0x0200_0004).unwrap(), 0x1234_abcd);
}

View file

@ -0,0 +1,285 @@
use super::*;
fn matrix(m: &mut Memory, group: u32, values: [i16; 4]) {
for (i, value) in values.into_iter().enumerate() {
m.write16(OAM + group * 32 + 6 + i as u32 * 8, value as u16)
.unwrap();
}
}
fn texel(x: usize, y: usize) -> u16 {
if (x + y) % 7 == 0 {
0
} else {
((x + 3 * y) % 15 + 1) as u16
}
}
fn pattern(m: &mut Memory, w: usize, h: usize, eight: bool, one: bool) {
let mut bytes = [0_u8; 0x8000];
for y in 0..h {
for x in 0..w {
let slot = (y / 8)
* if one {
w / 8 * if eight { 2 } else { 1 }
} else {
32
}
+ (x / 8) * if eight { 2 } else { 1 };
let p = (y % 8) * 8 + x % 8;
let address = slot * 32 + if eight { p } else { p / 2 };
bytes[address] |= (texel(x, y) as u8) << if eight { 0 } else { (p % 2) * 4 };
}
}
for (i, pair) in bytes.chunks_exact(2).enumerate() {
m.write16(
VRAM + 0x10000 + i as u32 * 2,
u16::from_le_bytes([pair[0], pair[1]]),
)
.unwrap();
}
for i in 1..16 {
m.write16(PAL + 0x200 + i * 2, i as u16 * 0x421).unwrap();
}
}
// Independent floating-point reference. Every coefficient is an exact multiple
// of 1/256, so these small products are exact in f64. Floor defines negative rounding.
fn check_reference(
m: &Memory,
dimensions: (usize, usize),
origin: (usize, usize),
doubled: bool,
values: [i16; 4],
) {
let (w, h) = dimensions;
let (cw, ch) = if doubled { (w * 2, h * 2) } else { (w, h) };
let f = render(m);
for y in 0..160 {
for x in 0..240 {
let lx = (x + 512 - origin.0) % 512;
let ly = (y + 256 - origin.1) % 256;
let mut color = rgb555_to_rgb888(0x4210);
if lx < cw && ly < ch {
let dx = lx as f64 - cw as f64 / 2.0;
let dy = ly as f64 - ch as f64 / 2.0;
let tx = (f64::from(values[0]) / 256.0 * dx
+ f64::from(values[1]) / 256.0 * dy
+ w as f64 / 2.0)
.floor();
let ty = (f64::from(values[2]) / 256.0 * dx
+ f64::from(values[3]) / 256.0 * dy
+ h as f64 / 2.0)
.floor();
if tx >= 0.0 && ty >= 0.0 && tx < w as f64 && ty < h as f64 {
let index = texel(tx as usize, ty as usize);
if index != 0 {
color = rgb555_to_rgb888(index * 0x421);
}
}
}
assert_eq!(
f.pixels()[y * WIDTH + x],
color,
"pixel {x},{y}, size {w}x{h}, double={doubled}, matrix={values:?}"
);
}
}
}
#[test]
fn identity_matches_regular_sprites_for_all_shapes_sizes_depths_and_layouts() {
let sizes = [
[(8, 8), (16, 16), (32, 32), (64, 64)],
[(16, 8), (32, 8), (32, 16), (64, 32)],
[(8, 16), (8, 32), (16, 32), (32, 64)],
];
for (shape, row) in sizes.iter().enumerate() {
for (size, &(w, h)) in row.iter().enumerate() {
for eight in [false, true] {
for one in [false, true] {
let mut m = setup();
m.write16(DISPCNT, if one { 0x1040 } else { 0x1000 })
.unwrap();
pattern(&mut m, w, h, eight, one);
let a = (shape as u16) << 14 | if eight { 0x2000 } else { 0 } | 9;
let b = (size as u16) << 14 | 11;
obj(&mut m, 0, a, b, 0);
let regular = render(&m);
matrix(&mut m, 31, [256, 0, 0, 256]);
obj(&mut m, 0, a | 0x100, b | (31 << 9), 0); // High group bits are not flip flags.
assert_eq!(render(&m).pixels(), regular.pixels());
}
}
}
}
}
#[test]
fn rotations_shears_reflections_and_fractional_scales_match_reference() {
let transforms = [
[0, 256, -256, 0],
[0, -256, 256, 0],
[-256, 0, 0, -256],
[181, 181, -181, 181],
[256, 128, 0, 256],
[-256, 0, 0, 256],
[128, 0, 0, 128],
[512, 0, 0, 512],
[512, 0, 0, 128],
[255, 1, -1, 257],
];
for eight in [false, true] {
for one in [false, true] {
for doubled in [false, true] {
let mut m = setup();
m.write16(DISPCNT, if one { 0x1040 } else { 0x1000 })
.unwrap();
pattern(&mut m, 16, 8, eight, one);
obj(
&mut m,
0,
0x4100 | if doubled { 0x200 } else { 0 } | if eight { 0x2000 } else { 0 } | 17,
23,
0,
);
for values in transforms {
matrix(&mut m, 0, values);
check_reference(&m, (16, 8), (23, 17), doubled, values);
}
}
}
}
}
#[test]
fn double_size_changes_canvas_and_center_without_changing_texture_size() {
let mut m = setup();
pattern(&mut m, 8, 8, false, true);
for doubled in [false, true] {
obj(
&mut m,
0,
0x100 | if doubled { 0x200 } else { 0 } | 20,
30,
0,
);
for values in [[256, 0, 0, 256], [128, 0, 0, 128]] {
matrix(&mut m, 0, values);
check_reference(&m, (8, 8), (30, 20), doubled, values);
}
}
// Identity double-size: texture starts four pixels inside the enlarged box.
matrix(&mut m, 0, [256, 0, 0, 256]);
assert_eq!(pixel(&m, 30, 20), rgb555_to_rgb888(0x4210));
assert_eq!(pixel(&m, 35, 24), rgb555_to_rgb888(texel(1, 0) * 0x421));
}
#[test]
fn zero_singular_and_extreme_signed_matrices_do_not_overflow_or_invert() {
let mut m = setup();
pattern(&mut m, 64, 64, true, true);
obj(&mut m, 0, 0x2300, 0xc000, 0);
for values in [
[0; 4],
[256, 0, 0, 0],
[32767, -32768, -32768, 32767],
[-1, 1, 1, -1],
[1, 0, 0, 1],
] {
matrix(&mut m, 0, values);
check_reference(&m, (64, 64), (0, 0), true, values);
}
}
#[test]
fn all_32_matrix_groups_are_addressed_independently_and_can_be_shared() {
let mut m = setup();
pattern(&mut m, 8, 8, false, true);
for group in 0..32 {
// Distinct coefficients detect both incorrect matrix stride and component stride.
let values = [
256 + group as i16,
group as i16 * 3,
-(group as i16) * 2,
128,
];
matrix(&mut m, group, values);
}
for group in 0..32 {
let values = [
256 + group as i16,
group as i16 * 3,
-(group as i16) * 2,
128,
];
obj(&mut m, 0, 0x100, (group as u16) << 9, 0);
check_reference(&m, (8, 8), (0, 0), false, values);
}
matrix(&mut m, 31, [256, 0, 0, 256]);
obj(&mut m, 0, 0x100, 31 << 9, 0);
obj(&mut m, 127, 0x100, (31 << 9) | 16, 0);
let f = render(&m);
for y in 0..8 {
for x in 0..8 {
assert_eq!(f.pixels()[y * WIDTH + x], f.pixels()[y * WIDTH + x + 16]);
}
}
// Changes to one shared matrix are visible to both sprites on the next snapshot.
matrix(&mut m, 31, [0; 4]);
let f = render(&m);
for y in 0..8 {
for x in 0..8 {
assert_eq!(f.pixels()[y * WIDTH + x], f.pixels()[y * WIDTH + x + 16]);
}
}
}
#[test]
fn affine_canvas_wraps_coordinates_and_clips_without_restarting_transform() {
let mut m = setup();
pattern(&mut m, 32, 64, false, true);
let values = [181, 181, -181, 181];
matrix(&mut m, 0, values);
for (x, y) in [(500, 240), (220, 145), (255, 170)] {
obj(&mut m, 0, 0x8300 | y as u16, 0xc000 | x as u16, 0);
check_reference(&m, (32, 64), (x, y), true, values);
}
}
#[test]
fn affine_transparent_edges_reveal_later_objects_and_obey_bg_priority() {
let mut m = setup();
fill(&mut m, 0, 32, 0x1111);
fill(&mut m, 1, 32, 0x2222);
matrix(&mut m, 0, [512, 0, 0, 512]);
obj(&mut m, 0, 0x100, 0, 0x400);
obj(&mut m, 1, 0, 0, 0x401); // Equal priority retains the first opaque OBJ.
assert_eq!(pixel(&m, 0, 0), 0x00ff00); // Outside transformed texture, later OBJ visible.
assert_eq!(pixel(&m, 4, 4), 0xff0000);
m.write16(DISPCNT, 0x1140).unwrap();
m.write16(BG0CNT, 0x1000).unwrap();
for address in (0..32).step_by(2) {
m.write16(VRAM + address, 0x3333).unwrap();
}
m.write16(PAL + 6, 0x7c00).unwrap();
assert_eq!(pixel(&m, 4, 4), 0x0000ff); // BG priority0 covers selected OBJ priority1.
assert_eq!(pixel(&m, 0, 0), 0x0000ff);
m.write16(BG0CNT, 0x1001).unwrap();
assert_eq!(pixel(&m, 0, 0), 0x00ff00); // OBJ ties BG and wins at transparent edge.
}
#[test]
fn mode3_affine_sprites_keep_bitmap_tile_restrictions() {
let mut m = setup();
m.write16(DISPCNT, 0x1043).unwrap();
matrix(&mut m, 0, [128, 0, 0, 128]);
fill(&mut m, 0, 32, 0x1111);
fill(&mut m, 512, 32, 0x2222);
obj(&mut m, 0, 0x300, 0, 0);
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(0x4210));
obj(&mut m, 0, 0x300, 0, 512);
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
assert_eq!(pixel(&m, 15, 15), 0x00ff00);
assert_eq!(pixel(&m, 16, 16), rgb555_to_rgb888(0x4210));
}

View file

@ -0,0 +1,479 @@
use gba_core::{
io::*,
memory::{Memory, MemoryError, PALETTE_START as PAL, VRAM_START as VRAM},
video::{rgb555_to_rgb888, Framebuffer, VideoError, WIDTH},
};
fn setup() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.write16(DISPCNT, 0x100).unwrap();
m.write16(BG0CNT, 0x1000).unwrap(); // Map at 0x8000, tiles at zero.
for (i, color) in [0x4210, 0x001f, 0x03e0, 0x7c00, 0x7fff]
.into_iter()
.enumerate()
{
m.write16(PAL + i as u32 * 2, color).unwrap();
}
m
}
fn pixel(m: &Memory, x: usize, y: usize) -> u32 {
let mut frame = Framebuffer::default();
m.render_frame(&mut frame).unwrap();
frame.pixels()[y * WIDTH + x]
}
fn tile(m: &mut Memory, address: u32, value: u16, bytes: u32) {
for i in (0..bytes).step_by(2) {
m.write16(VRAM + address + i, value).unwrap();
}
}
#[test]
fn controls_mask_unused_bits_and_merge_byte_and_word_writes() {
let mut m = setup();
m.write32(BG0CNT, u32::MAX).unwrap();
m.write32(BG2CNT, u32::MAX).unwrap();
assert_eq!(m.read32(BG0CNT).unwrap(), 0xdfcf_dfcf);
assert_eq!(m.read32(BG2CNT).unwrap(), 0xffcf_ffcf);
m.write8(BG0CNT, 0x81).unwrap();
m.write8(BG0CNT + 1, 0x10).unwrap();
assert_eq!(m.read16(BG0CNT).unwrap(), 0x1081);
for reg in (BG0HOFS..=BG3VOFS).step_by(2) {
m.write16(reg, 0xffff).unwrap();
assert_eq!(m.read16(reg).unwrap(), 0); // Write-only placeholder, not open bus.
}
assert_eq!(
m.read16(0x0400_0058),
Err(MemoryError::Unmapped(0x0400_0058))
);
}
#[test]
fn four_bit_nibble_order_palette_banks_and_transparent_zero() {
let mut m = setup();
m.write16(VRAM, 0x0321).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000);
assert_eq!(pixel(&m, 1, 0), 0x00ff00);
assert_eq!(pixel(&m, 2, 0), 0x0000ff);
assert_eq!(pixel(&m, 3, 0), rgb555_to_rgb888(0x4210));
m.write16(VRAM + 0x8000, 0xf000).unwrap();
m.write16(PAL + 241 * 2, 0x7fff).unwrap();
m.write16(PAL + 240 * 2, 0x001f).unwrap(); // Bank zero is still transparent.
assert_eq!(pixel(&m, 0, 0), 0xffffff);
assert_eq!(pixel(&m, 3, 0), rgb555_to_rgb888(0x4210));
}
#[test]
fn eight_bit_tiles_use_full_index_and_ignore_palette_bank() {
let mut m = setup();
m.write16(BG0CNT, 0x1080).unwrap();
m.write16(VRAM + 0x8000, 0xf001).unwrap();
m.write16(VRAM + 64, 0x00ff).unwrap();
m.write16(PAL + 510, 0x001f).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000);
assert_eq!(pixel(&m, 1, 0), rgb555_to_rgb888(0x4210));
}
#[test]
fn horizontal_and_vertical_flips_apply_in_both_color_depths() {
for eight in [false, true] {
let mut m = setup();
m.write16(BG0CNT, 0x1000 | if eight { 0x80 } else { 0 })
.unwrap();
let (top, bottom) = if eight {
(0x0201, 0x0403)
} else {
(0x21, 0x43)
};
m.write16(VRAM, top).unwrap();
m.write16(VRAM + if eight { 56 } else { 28 }, bottom)
.unwrap();
for (flip, x, y, expected) in [
(0, 0, 0, 0xff0000),
(0x400, 7, 0, 0xff0000),
(0x800, 0, 0, 0x0000ff),
(0xc00, 6, 0, 0xffffff),
] {
m.write16(VRAM + 0x8000, flip).unwrap();
assert_eq!(pixel(&m, x, y), expected);
}
}
}
#[test]
fn all_map_sizes_use_screen_blocks_not_flat_rows_and_wrap() {
for size in 0..4_u16 {
let mut m = setup();
for i in 1..=4 {
tile(&mut m, i * 32, 0x1111 * i as u16, 32);
}
for block in 0..4 {
tile(&mut m, 0x8000 + block * 0x800, block as u16 + 1, 0x800);
}
m.write16(BG0CNT, 0x1000 | (size << 14)).unwrap();
let width = if size & 1 != 0 { 512 } else { 256 };
let height = if size & 2 != 0 { 512 } else { 256 };
for y in [0, 255, 256, 511] {
for x in [0, 255, 256, 511] {
m.write16(BG0HOFS, x).unwrap();
m.write16(BG0VOFS, y).unwrap();
let block = (x % width) / 256 + (y % height) / 256 * (width / 256);
let colors = [0x001f, 0x03e0, 0x7c00, 0x7fff];
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(colors[block as usize]));
}
}
}
}
#[test]
fn scroll_byte_writes_merge_latches_mask_nine_bits_and_move_subtile_pixels() {
let mut m = setup();
m.write16(BG0CNT, 0x1080).unwrap();
m.write16(VRAM + 2, 0x0201).unwrap();
m.write16(BG0HOFS, 0xffff).unwrap();
m.write8(BG0HOFS, 2).unwrap(); // latch becomes 0x102; 256-wide map wraps to 2.
assert_eq!(pixel(&m, 0, 0), 0xff0000);
m.write8(BG0HOFS + 1, 0xfe).unwrap(); // Only bit zero survives.
assert_eq!(pixel(&m, 1, 0), 0x00ff00);
m.write16(VRAM + 8 + 2, 0x0403).unwrap();
m.write32(BG0HOFS, 0xfe01_fe02).unwrap(); // X=2, Y=1
assert_eq!(pixel(&m, 0, 0), 0x0000ff);
assert_eq!(m.cycles(), 0);
}
#[test]
fn each_background_uses_its_own_scroll_and_control() {
for bg in 0..4 {
let mut m = setup();
m.write16(DISPCNT, 0x100 << bg).unwrap();
m.write16(BG0CNT + bg * 2, 0x1080).unwrap();
m.write16(VRAM + 10, 0x0201).unwrap();
m.write16(BG0HOFS + bg * 4, 2).unwrap();
m.write16(BG0VOFS + bg * 4, 1).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000);
}
}
#[test]
fn priorities_ties_transparency_and_opaque_black_composite_all_four_layers() {
let mut m = setup();
m.write16(DISPCNT, 0xf00).unwrap();
for bg in 0..4 {
m.write16(BG0CNT + bg * 2, ((16 + bg) << 8) as u16).unwrap();
tile(&mut m, 0x8000 + bg * 0x800, bg as u16 + 1, 0x800);
tile(&mut m, (bg + 1) * 32, 0x1111 * (bg as u16 + 1), 32);
}
for bg in 0..4 {
let expected = [0xff0000, 0x00ff00, 0x0000ff, 0xffffff][bg as usize];
assert_eq!(pixel(&m, 0, 0), expected);
// Lower the winning layer's priority so the next wins the tie.
m.write16(BG0CNT + bg * 2, ((16 + bg) << 8) as u16 | 3)
.unwrap();
}
assert_eq!(pixel(&m, 0, 0), 0xff0000); // All priority 3, BG0 wins.
m.write16(PAL + 2, 0).unwrap();
assert_eq!(pixel(&m, 0, 0), 0); // Nonzero index with black palette color is opaque.
tile(&mut m, 32, 0, 32);
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
m.write16(DISPCNT, 0).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(0x4210));
}
#[test]
fn character_base_and_high_tile_numbers_cross_character_blocks() {
for eight in [false, true] {
let mut m = setup();
m.write16(BG0CNT, 0x1004 | if eight { 0x80 } else { 0 })
.unwrap();
m.write16(VRAM + 0x8000, 1023).unwrap();
let address = 0x4000 + 1023 * if eight { 64 } else { 32 };
if eight {
assert!(matches!(
m.render_frame(&mut Framebuffer::default()),
Err(VideoError::UnsupportedTileAddress(_))
));
} else {
tile(&mut m, address, 0x1111, 32);
assert_eq!(pixel(&m, 0, 0), 0xff0000);
}
}
}
#[test]
fn screen_map_outside_supported_bg_area_reports_error_without_wrapping() {
let mut m = setup();
m.write16(BG0CNT, 0x5f04).unwrap(); // Map block31, wide; tile base1.
m.write16(BG0HOFS, 256).unwrap(); // Second block lies outside the BG area.
let mut frame = Framebuffer::default();
frame.clear(0x001f);
assert_eq!(
m.render_frame(&mut frame),
Err(VideoError::UnsupportedMapAddress(0x10000))
);
assert!(frame.pixels().iter().all(|&p| p == 0xff0000));
}
#[test]
fn unsupported_features_leave_output_unchanged_and_forced_blank_bypasses_them() {
let mut m = setup();
let mut frame = Framebuffer::default();
frame.clear(0x001f);
m.write16(DISPCNT, 0x100).unwrap();
m.write16(BG0CNT, 0x100c).unwrap();
m.write16(VRAM + 0x8002, 1023).unwrap(); // Error after some valid pixels.
assert!(matches!(
m.render_frame(&mut frame),
Err(VideoError::UnsupportedTileAddress(_))
));
assert!(frame.pixels().iter().all(|&p| p == 0xff0000));
m.write16(DISPCNT, 0xf180).unwrap();
m.render_frame(&mut frame).unwrap();
assert!(frame.pixels().iter().all(|&p| p == 0xffffff));
}
fn assert_scene(
frame: &Framebuffer,
sx: usize,
sy: usize,
flipped: bool,
behind: bool,
rotated: bool,
zoomed: bool,
) {
for y in 0..160 {
for x in 0..240 {
let fx = (x + sx / 2) % 256;
let fy = (y + sy / 2) % 256;
let bx = (x + sx) % 512;
let by = (y + sy) % 512;
let cross = fx / 8 % 4 == 1 && fy / 8 % 4 == 1 && (fx % 8 == 3 || fy % 8 == 3);
let mut color = if cross {
0x03ff
} else {
[0x0260, 0x03a0, 0x7d20, 0x7e80]
[((bx / 32 + by / 32) % 2) * 2 + (bx / 2 + by / 2) % 2]
};
let affine = rotated || zoomed;
let source = if affine && (104..136).contains(&x) && (64..96).contains(&y) {
let a = f64::from(if rotated {
if zoomed {
90
} else {
181
}
} else {
128
});
let b = if rotated { a } else { 0.0 };
let dx = x as f64 - 120.0;
let dy = y as f64 - 80.0;
Some((
((a * dx + b * dy) / 256.0 + 8.0).floor() as i32,
((-b * dx + a * dy) / 256.0 + 8.0).floor() as i32,
))
} else if !affine && (112..128).contains(&x) && (72..88).contains(&y) {
Some((
if flipped {
127 - x as i32
} else {
x as i32 - 112
},
y as i32 - 72,
))
} else {
None
};
if let Some((tx, ty)) = source.filter(|&(tx, ty)| {
(2..14).contains(&tx) && (1..15).contains(&ty) && !(cross && behind)
}) {
color = if tx == 10 && ty == 7 {
0
} else if tx == 12 && (10..13).contains(&ty) {
0x03ff
} else if (5..10).contains(&ty) {
0x7fff
} else {
0x001f
};
}
assert_eq!(
frame.pixels()[y * WIDTH + x],
rgb555_to_rgb888(color),
"pixel {x},{y} scroll {sx},{sy}"
);
}
}
}
#[test]
fn cpu_tile_demo_copies_assets_scrolls_wraps_cancels_opposites_and_resets() {
use gba_core::input::{Button, Buttons};
use gba_demos::tile_demo::{TileDemo, TILE_STATE};
let mut demo = TileDemo::new().unwrap();
let mut frame = Framebuffer::default();
assert_eq!(demo.machine().memory().read16(VRAM).unwrap(), 0);
assert_eq!(demo.machine().memory().read16(DISPCNT).unwrap(), 0);
let mut updates = 0;
for (buttons, x, y) in [
(Buttons::default(), 0, 0),
(
Buttons::default()
.with(Button::Left, true)
.with(Button::Up, true),
510,
510,
),
(Buttons::from_bits(0xf0), 510, 510),
(
Buttons::default()
.with(Button::Right, true)
.with(Button::Down, true),
0,
0,
),
(
Buttons::default()
.with(Button::Right, true)
.with(Button::Down, true),
2,
2,
),
(
Buttons::default()
.with(Button::Start, true)
.with(Button::Right, true),
0,
0,
),
] {
let before = demo.machine().memory().display_position();
demo.frame(buttons, &mut frame).unwrap();
updates += 1;
assert_scene(&frame, x, y, false, false, false, false);
let m = demo.machine().memory();
assert_eq!(m.read32(TILE_STATE).unwrap(), updates);
assert_eq!(m.read32(TILE_STATE + 4).unwrap(), x as u32);
assert_eq!(m.read32(TILE_STATE + 8).unwrap(), y as u32);
assert_eq!(m.read16(DISPCNT).unwrap(), 0x1340);
assert_eq!(m.display_position().scanline, 160);
if updates > 1 {
assert_eq!(m.display_position().vblanks, before.vblanks + 1);
}
}
}
#[test]
fn cpu_sprite_controls_flip_and_change_priority_through_oam() {
use gba_core::{
input::{Button, Buttons},
memory::OAM_START,
};
use gba_demos::tile_demo::TileDemo;
let mut demo = TileDemo::new().unwrap();
let mut frame = Framebuffer::default();
for _ in 0..16 {
demo.frame(Buttons::default().with(Button::Right, true), &mut frame)
.unwrap();
}
for (flipped, behind) in [
(false, false),
(true, false),
(true, true),
(false, true),
(false, false),
] {
demo.frame(
Buttons::default()
.with(Button::A, flipped)
.with(Button::B, behind),
&mut frame,
)
.unwrap();
assert_scene(&frame, 32, 0, flipped, behind, false, false);
let m = demo.machine().memory();
assert_eq!(m.read16(OAM_START).unwrap(), 72);
assert_eq!(
m.read16(OAM_START + 2).unwrap(),
0x4070 | if flipped { 0x1000 } else { 0 }
);
assert_eq!(
m.read16(OAM_START + 4).unwrap(),
if behind { 0x400 } else { 0 }
);
for i in 1..128 {
assert_eq!(m.read16(OAM_START + i * 8).unwrap(), 0x200);
}
}
}
#[test]
fn cpu_affine_controls_write_matrices_preserve_center_and_return_to_regular_mode() {
use gba_core::{
input::{Button, Buttons},
memory::OAM_START,
};
use gba_demos::tile_demo::TileDemo;
let mut demo = TileDemo::new().unwrap();
let mut frame = Framebuffer::default();
// Put foreground crosses over the sprite so X also tests affine composition.
for _ in 0..16 {
demo.frame(Buttons::default().with(Button::Right, true), &mut frame)
.unwrap();
}
for bits in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0] {
let flipped = bits & 1 != 0;
let behind = bits & 2 != 0;
let rotated = bits & 4 != 0;
let zoomed = bits & 8 != 0;
let buttons = Buttons::default()
.with(Button::A, flipped)
.with(Button::B, behind)
.with(Button::L, rotated)
.with(Button::R, zoomed);
let before = demo.machine().memory().display_position();
demo.frame(buttons, &mut frame).unwrap();
assert_scene(&frame, 32, 0, flipped, behind, rotated, zoomed);
let m = demo.machine().memory();
let affine = rotated || zoomed;
assert_eq!(
m.read16(OAM_START).unwrap(),
if affine { 0x340 } else { 72 }
);
assert_eq!(
m.read16(OAM_START + 2).unwrap(),
if affine {
0x4068
} else {
0x4070 | if flipped { 0x1000 } else { 0 }
}
);
let a: i16 = match (rotated, zoomed) {
(false, false) => 256,
(false, true) => 128,
(true, false) => 181,
(true, true) => 90,
};
let b = if rotated { a } else { 0 };
for (i, value) in [a, b, -b, a].into_iter().enumerate() {
assert_eq!(
m.read16(OAM_START + 6 + i as u32 * 8).unwrap(),
value as u16
);
}
for i in 1..128 {
assert_eq!(m.read16(OAM_START + i * 8).unwrap(), 0x200);
}
assert_eq!(m.display_position().vblanks, before.vblanks + 1);
assert_eq!(m.display_position().scanline, 160);
}
}
#[test]
fn green_swap_runs_after_tile_compositing() {
let mut m = setup();
m.write16(VRAM, 0x21).unwrap();
m.write16(GREENSWAP, 1).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xffff00);
assert_eq!(pixel(&m, 1, 0), 0);
}

View file

@ -0,0 +1,360 @@
use gba_core::{
io::{IE, IF, IME, TIMER_BASE},
memory::{Memory, MemoryError},
};
fn memory() -> Memory {
Memory::new(vec![]).unwrap()
}
#[test]
fn io_reset_values_masks_and_unused_bytes() {
let mut bus = memory();
for address in (TIMER_BASE..TIMER_BASE + 16)
.chain(IE..IE + 4)
.chain(IME..IME + 4)
{
assert_eq!(bus.read8(address).unwrap(), 0);
}
bus.write16(IE, 0xffff).unwrap();
assert_eq!(bus.read16(IE).unwrap(), 0x3fff);
bus.write32(IME, u32::MAX).unwrap();
assert_eq!(bus.read32(IME).unwrap(), 1);
bus.write32(IME, 0xffff_fffe).unwrap();
assert_eq!(bus.read32(IME).unwrap(), 0);
for index in 0..4 {
let address = TIMER_BASE + index * 4 + 2;
bus.write16(address, 0xffff).unwrap();
assert_eq!(
bus.read16(address).unwrap(),
if index == 0 { 0xc3 } else { 0xc7 }
);
}
}
#[test]
fn timer_data_reads_counter_not_reload_and_start_loads_reload() {
let mut bus = memory();
bus.write16(TIMER_BASE, 0x1234).unwrap();
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0);
bus.advance_cycles(100);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0);
bus.write16(TIMER_BASE + 2, 0x80).unwrap();
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1234);
bus.advance_cycles(5);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1239);
bus.write16(TIMER_BASE, 0xabcd).unwrap();
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1239);
bus.write16(TIMER_BASE + 2, 0x80).unwrap(); // No rising enable edge
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1239);
bus.write16(TIMER_BASE + 2, 0).unwrap();
bus.advance_cycles(100);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1239);
bus.write16(TIMER_BASE + 2, 0x80).unwrap();
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xabcd);
}
#[test]
fn combined_word_write_uses_new_reload_before_enabling() {
let mut bus = memory();
for index in 0..4 {
let address = TIMER_BASE + index * 4;
bus.write32(address, 0x0080_abcd).unwrap();
assert_eq!(bus.read32(address).unwrap(), 0x0080_abcd);
}
bus.advance_cycles(1);
for index in 0..4 {
assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xabce);
}
}
#[test]
fn reload_byte_writes_merge_with_hidden_latch_not_counter() {
let mut bus = memory();
bus.write16(TIMER_BASE, 0x1234).unwrap();
bus.write8(TIMER_BASE, 0x56).unwrap();
bus.write8(TIMER_BASE + 2, 0x80).unwrap();
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1256);
bus.advance_cycles(1000);
bus.write8(TIMER_BASE + 1, 0xab).unwrap();
bus.write8(TIMER_BASE + 2, 0).unwrap();
bus.write8(TIMER_BASE + 2, 0x80).unwrap();
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xab56);
}
#[test]
fn all_prescalers_retain_fractional_cycles_between_advances() {
for (selection, divisor) in [1, 64, 256, 1024].into_iter().enumerate() {
let mut bus = memory();
bus.write32(TIMER_BASE, ((0x80 | selection as u32) << 16) | 123)
.unwrap();
bus.advance_cycles(divisor - 1);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 123);
bus.advance_cycles(1);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 124);
bus.advance_cycles(divisor * 3 + divisor - 1);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 127);
bus.advance_cycles(1);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 128);
}
}
#[test]
fn clock_source_changes_and_restart_reset_provisional_phase() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x0081_0000).unwrap(); // /64
bus.advance_cycles(63);
bus.write16(TIMER_BASE + 2, 0xc1).unwrap(); // IRQ change preserves phase
bus.advance_cycles(1);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 1);
bus.advance_cycles(63);
bus.write16(TIMER_BASE + 2, 0x82).unwrap(); // /256 resets phase
bus.advance_cycles(255);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 1);
bus.advance_cycles(1);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 2);
bus.write16(TIMER_BASE + 2, 0).unwrap();
bus.advance_cycles(255);
bus.write16(TIMER_BASE + 2, 0x82).unwrap();
bus.advance_cycles(255);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0);
}
#[test]
fn overflow_reloads_and_latches_request_without_delivery_enables() {
let mut bus = memory();
for index in 0..4 {
bus.write32(TIMER_BASE + index * 4, 0x00c0_fffe).unwrap();
}
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 0x78);
assert!(!bus.irq_pending()); // IE and IME remain zero
for index in 0..4 {
assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xfffe);
}
bus.advance_cycles(5);
assert_eq!(bus.read16(IF).unwrap(), 0x78);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xffff);
}
#[test]
fn overflow_uses_updated_reload_without_resetting_current_counter() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x00c0_fffe).unwrap();
bus.write16(TIMER_BASE, 0x1234).unwrap();
bus.advance_cycles(1);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xffff);
bus.advance_cycles(1);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1234);
assert_eq!(bus.read16(IF).unwrap(), 8);
}
#[test]
fn overflow_with_local_irq_disabled_does_not_set_request() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x0080_ffff).unwrap();
bus.advance_cycles(100);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.write16(TIMER_BASE + 2, 0xc0).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 0); // No retroactive request
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 8);
}
#[test]
fn cascades_count_all_overflows_including_multiple_levels() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x00c0_fffe).unwrap(); // Overflow every 2 cycles
for index in 1..4 {
bus.write32(TIMER_BASE + index * 4, 0x00c7_fffe).unwrap(); // /1024 ignored
}
bus.advance_cycles(15);
assert_eq!(bus.read16(IF).unwrap(), 0x38);
for index in 0..4 {
assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xffff);
}
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 0x78);
for index in 0..4 {
assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xfffe);
}
}
#[test]
fn disabled_cascade_stage_blocks_downstream_pulses() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x0080_ffff).unwrap();
bus.write32(TIMER_BASE + 4, 0x0004_ffff).unwrap(); // Disabled timer 1
bus.write32(TIMER_BASE + 8, 0x00c4_1234).unwrap();
bus.advance_cycles(100_000);
assert_eq!(bus.read16(TIMER_BASE + 8).unwrap(), 0x1234);
assert_eq!(bus.read16(IF).unwrap(), 0);
}
#[test]
fn normal_timer_breaks_cascade_dependency_and_timer_zero_ignores_count_up() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x0084_ffff).unwrap(); // Bit 2 ignored
bus.write32(TIMER_BASE + 8, 0x0080_fffe).unwrap(); // Independent timer 2
bus.write32(TIMER_BASE + 12, 0x00c4_ffff).unwrap();
bus.advance_cycles(2);
assert_eq!(bus.read16(TIMER_BASE + 2).unwrap(), 0x80);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xffff);
assert_eq!(bus.read16(IF).unwrap(), 0x40);
}
#[test]
fn bulk_advance_handles_maximum_cycle_count_and_period_one() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
for index in 1..4 {
bus.write32(TIMER_BASE + index * 4, 0x00c4_ffff).unwrap();
}
bus.advance_cycles(u32::MAX);
assert_eq!(bus.cycles(), u64::from(u32::MAX));
for index in 0..4 {
assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xffff);
}
assert_eq!(bus.read16(IF).unwrap(), 0x78);
bus.advance_cycles(u32::MAX);
assert_eq!(bus.cycles(), 2 * u64::from(u32::MAX));
}
#[test]
fn bulk_advance_matches_independent_cycle_by_cycle_reference() {
// A small literal reference: count prescaler ticks and propagate one pulse
// per cycle. Unlike the implementation, this does not use overflow division.
for selection in 0..4 {
for cascade_mask in 0..8 {
let mut bus = memory();
let reloads = [0xfffd_u16, 0xfffe, 0xffff, 0xfffc];
let mut counters = reloads;
let mut phases = [0_u32; 4];
let mut pending = 0_u16;
for (index, reload) in reloads.into_iter().enumerate() {
let cascade = index > 0 && cascade_mask & (1 << (index - 1)) != 0;
let control = 0xc0 | selection | if cascade { 4 } else { 0 };
bus.write32(
TIMER_BASE + index as u32 * 4,
(control << 16) | u32::from(reload),
)
.unwrap();
}
for chunk in [0, 1, 17, 63, 1000, 8193] {
bus.advance_cycles(chunk);
for _ in 0..chunk {
let mut overflow = false;
for index in 0..4 {
let cascade = index > 0 && cascade_mask & (1 << (index - 1)) != 0;
let tick = if cascade {
overflow
} else {
phases[index] += 1;
if phases[index] == [1, 64, 256, 1024][selection as usize] {
phases[index] = 0;
true
} else {
false
}
};
overflow = tick && counters[index] == u16::MAX;
if overflow {
counters[index] = reloads[index];
pending |= 1 << (index + 3);
} else if tick {
counters[index] += 1;
}
}
}
for (index, counter) in counters.into_iter().enumerate() {
assert_eq!(bus.read16(TIMER_BASE + index as u32 * 4).unwrap(), counter);
}
assert_eq!(bus.read16(IF).unwrap(), pending);
}
}
}
}
#[test]
fn interrupt_delivery_requires_both_ie_and_ime_without_clearing_if() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
bus.advance_cycles(1);
for (enable, master, expected) in [
(0, 0, false),
(8, 0, false),
(0, 1, false),
(16, 1, false),
(8, 1, true),
] {
bus.write16(IE, enable).unwrap();
bus.write32(IME, master).unwrap();
assert_eq!(bus.irq_pending(), expected);
assert_eq!(bus.read16(IF).unwrap(), 8);
}
}
#[test]
fn interrupt_acknowledgement_is_write_one_to_clear_for_all_widths() {
let mut bus = memory();
for index in 0..4 {
bus.write32(TIMER_BASE + index * 4, 0x00c0_ffff).unwrap();
}
bus.advance_cycles(1);
bus.write16(IF, 0).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 0x78);
bus.write8(IF + 1, 0xff).unwrap(); // High byte must not clear low flags
assert_eq!(bus.read16(IF).unwrap(), 0x78);
bus.write8(IF, 8).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 0x70);
bus.write16(IF, 0x20).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 0x50);
bus.write32(IE, 0x0010_0078).unwrap(); // IE=0x78, clear only Timer 1
assert_eq!(bus.read32(IE).unwrap(), 0x0040_0078);
bus.write16(IF, 0xffff).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 0x78); // Later events relatch
}
#[test]
fn high_byte_writes_do_not_reset_control_or_modify_low_enable_bits() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x0080_1234).unwrap();
bus.write8(TIMER_BASE + 3, 0xff).unwrap();
bus.write8(IE, 8).unwrap();
bus.write8(IE + 1, 0xff).unwrap();
assert_eq!(bus.read16(IE).unwrap(), 0x3f08);
bus.write8(IME, 1).unwrap();
for offset in 1..4 {
bus.write8(IME + offset, 0xff).unwrap();
}
assert_eq!(bus.read32(IME).unwrap(), 1);
bus.advance_cycles(1);
assert_eq!(bus.read32(TIMER_BASE).unwrap(), 0x0080_1235);
}
#[test]
fn unknown_io_and_mirrors_stay_unmapped_and_unaligned_writes_have_no_effect() {
let mut bus = memory();
for address in [0x0400_0058, TIMER_BASE + 16, IE + 12, IME + 4, 0x0401_0100] {
assert_eq!(bus.read8(address), Err(MemoryError::Unmapped(address)));
assert_eq!(
bus.write32(address, u32::MAX),
Err(MemoryError::Unmapped(address))
);
}
assert_eq!(
bus.write32(TIMER_BASE + 2, u32::MAX),
Err(MemoryError::Unaligned(TIMER_BASE + 2))
);
assert_eq!(
bus.write16(IF + 1, u16::MAX),
Err(MemoryError::Unaligned(IF + 1))
);
assert_eq!(bus.read32(TIMER_BASE).unwrap(), 0);
assert_eq!(bus.read32(IE).unwrap(), 0);
assert_eq!(bus.cycles(), 0);
}

View file

@ -0,0 +1,169 @@
use gba_core::{
cpu::Cpu,
io::{IE, IF, IME, TIMER_BASE, WAITCNT},
machine::{Machine, StepKind},
memory::{Memory, MemoryError, ROM_START},
timing::{bus_cycles, AccessKind, AccessWidth},
};
#[test]
fn waitcnt_masks_unused_bits_and_supports_all_bus_widths() {
let mut memory = Memory::new(vec![]).unwrap();
assert_eq!(memory.read32(WAITCNT).unwrap(), 0);
memory.write32(WAITCNT, u32::MAX).unwrap();
assert_eq!(memory.read32(WAITCNT).unwrap(), 0x5fff);
memory.write8(WAITCNT, 0x17).unwrap();
memory.write8(WAITCNT + 1, 0x43).unwrap();
assert_eq!(memory.waitcnt(), 0x4317);
memory.write16(WAITCNT + 2, 0xffff).unwrap();
assert_eq!(memory.read32(WAITCNT).unwrap(), 0x4317);
assert_eq!(memory.read16(IE).unwrap(), 0);
assert_eq!(memory.read16(IF).unwrap(), 0);
assert_eq!(memory.cycles(), 0);
assert_eq!(
memory.write32(WAITCNT + 2, 0),
Err(MemoryError::Unaligned(WAITCNT + 2))
);
assert_eq!(memory.waitcnt(), 0x4317);
}
#[test]
fn every_rom_wait_setting_has_the_documented_halfword_and_word_costs() {
for window in 0..3 {
for first in 0..4 {
for second in 0..2 {
let waitcnt = (first << [2, 5, 8][window]) | (second << [4, 7, 10][window]);
let n = [5, 4, 3, 9][first as usize];
let s = if second == 1 { 2 } else { [3, 5, 9][window] };
for mirror in [0, 0x0100_0000] {
let address = ROM_START + window as u32 * 0x0200_0000 + mirror + 0x100;
for (kind, initial) in
[(AccessKind::NonSequential, n), (AccessKind::Sequential, s)]
{
assert_eq!(
bus_cycles(waitcnt, address, AccessWidth::Byte, kind),
initial
);
assert_eq!(
bus_cycles(waitcnt, address, AccessWidth::Halfword, kind),
initial
);
assert_eq!(
bus_cycles(waitcnt, address, AccessWidth::Word, kind),
initial + s
);
}
}
}
}
}
}
#[test]
fn rom_128_kib_boundaries_force_nonsequential_first_halfwords() {
for address in [
0x0800_0000,
0x0802_0000,
0x09fe_0000,
0x0a02_0000,
0x0c02_0000,
] {
let window = ((address >> 25) - 4) as usize;
let s = [3, 5, 9][window];
assert_eq!(
bus_cycles(0, address, AccessWidth::Halfword, AccessKind::Sequential),
5
);
assert_eq!(
bus_cycles(0, address + 1, AccessWidth::Byte, AccessKind::Sequential),
5
);
assert_eq!(
bus_cycles(0, address, AccessWidth::Word, AccessKind::Sequential),
5 + s
);
assert_eq!(
bus_cycles(
0,
address + 2,
AccessWidth::Halfword,
AccessKind::Sequential
),
s
);
}
}
#[test]
fn internal_regions_and_ewram_use_fixed_width_specific_costs() {
for kind in [AccessKind::NonSequential, AccessKind::Sequential] {
for width in [AccessWidth::Byte, AccessWidth::Halfword, AccessWidth::Word] {
for address in [0, 0x0300_0000, 0x03ff_fffc, WAITCNT] {
assert_eq!(bus_cycles(0x5fff, address, width, kind), 1);
}
for address in [0x0200_0000, 0x02ff_fffc] {
assert_eq!(
bus_cycles(0x5fff, address, width, kind),
if width == AccessWidth::Word { 6 } else { 3 }
);
}
}
}
}
#[test]
fn stored_prefetch_phi_and_sram_settings_do_not_change_supported_bus_costs() {
// Explicit limitation: no prefetch queue, PHI output, or SRAM mapping yet.
for width in [AccessWidth::Halfword, AccessWidth::Word] {
for address in [ROM_START, ROM_START + 4, 0x0a00_0004, 0x0c00_0004] {
for kind in [AccessKind::NonSequential, AccessKind::Sequential] {
assert_eq!(
bus_cycles(0, address, width, kind),
bus_cycles(0x5803, address, width, kind)
);
}
}
}
}
#[test]
fn wait_states_change_when_a_timer_interrupt_is_sampled() {
for (waitcnt, entry_step, instructions, cycles) in [(0, 4, 3, 23), (0x18, 6, 5, 24)] {
let mut memory = Memory::new(0xe280_0001_u32.to_le_bytes().repeat(64)).unwrap();
memory.write16(WAITCNT, waitcnt).unwrap();
memory.write32(TIMER_BASE, 0x00c0_ffec).unwrap(); // 20 cycles
memory.write16(IE, 8).unwrap();
memory.write32(IME, 1).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), memory);
assert_eq!(machine.last_timing().total(), 0);
for step in 1..=entry_step {
let event = machine.step().unwrap();
assert_eq!(
event,
if step == entry_step {
StepKind::IrqEntry
} else {
StepKind::Instruction
}
);
}
assert_eq!(machine.cpu().registers()[0], instructions);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.last_timing().code_cycles, 3);
}
}
#[test]
fn failed_machine_step_preserves_previous_timing_breakdown() {
let mut memory = Memory::new(0xe1a0_0000_u32.to_le_bytes().to_vec()).unwrap();
memory.write32(TIMER_BASE, 0x0080_0000).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), memory);
machine.step().unwrap();
let timing = machine.last_timing();
let cycles = machine.cycles();
let counter = machine.memory().read16(TIMER_BASE).unwrap();
assert!(machine.step().is_err());
assert_eq!(machine.last_timing(), timing);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), counter);
}

View file

@ -0,0 +1,228 @@
use gba_core::{
cpu::Cpu,
display::{
CYCLES_PER_FRAME as FRAME, CYCLES_PER_LINE as LINE, HBLANK_START as HB, VBLANK_START as VB,
},
dma::DMA_BASE,
io::*,
machine::Machine,
memory::{Memory, PALETTE_START as PAL, ROM_START},
video::{rgb555_to_rgb888 as rgb, Framebuffer, HEIGHT, WIDTH},
};
#[path = "window_tracking/horizontal.rs"]
mod horizontal;
fn setup(bounds: u16) -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.set_scanline_rendering(true);
m.write16(DISPCNT, 0x2000).unwrap();
m.write16(PAL, 31).unwrap();
m.write16(WIN0H, 240).unwrap();
m.write16(WIN1H, 240).unwrap();
m.write16(WIN0V, bounds).unwrap();
m.write16(WININ, 0x2020).unwrap();
m.write16(WINOUT, 0).unwrap();
m.write16(BLDCNT, 0xe0).unwrap(); // Darken backdrop only when effects are enabled.
m.write16(BLDY, 16).unwrap();
m
}
fn present(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
assert!(m.present_frame(&mut f).unwrap());
f
}
fn finish(m: &mut Memory) -> Framebuffer {
let position = m.display_position();
let phase = u32::from(position.scanline) * LINE + u32::from(position.line_cycle);
m.advance_cycles(VB - phase);
present(m)
}
fn check_rows(f: &Framebuffer, inside: impl Fn(usize) -> bool) {
for y in 0..HEIGHT {
let expected = if inside(y) { 0 } else { rgb(31) };
assert!(
f.pixels()[y * WIDTH..(y + 1) * WIDTH]
.iter()
.all(|&p| p == expected),
"row={y}"
);
}
}
#[test]
fn ordinary_static_windows_match_debug_snapshots() {
for bounds in [0x00a0, 0x0130, 0x0405, 0x9fa0, 0x0000, 0x0505, 0x00ff] {
let mut m = setup(bounds);
let mut expected = Framebuffer::default();
m.render_frame(&mut expected).unwrap();
assert_eq!(
finish(&mut m).pixels(),
expected.pixels(),
"bounds={bounds:#x}"
);
}
}
#[test]
fn moving_top_after_entry_does_not_close_an_active_window() {
let mut m = setup(0x0210);
m.advance_cycles(4 * LINE + HB);
m.write8(WIN0V + 1, 12).unwrap();
let f = finish(&mut m);
check_rows(&f, |y| (2..16).contains(&y));
let mut snapshot = Framebuffer::default();
m.render_frame(&mut snapshot).unwrap();
check_rows(&snapshot, |y| (12..16).contains(&y));
assert_eq!(present(&m).pixels(), f.pixels()); // Snapshot rendering has no latch side effects.
}
#[test]
fn moving_top_behind_vcount_does_not_open_a_window() {
let mut m = setup(0x0a14);
m.advance_cycles(5 * LINE + HB);
m.write8(WIN0V + 1, 2).unwrap();
check_rows(&finish(&mut m), |_| false);
m.advance_cycles(FRAME - VB);
check_rows(&finish(&mut m), |y| (2..20).contains(&y));
}
#[test]
fn missing_bottom_keeps_window_open_across_frame_wrap() {
let mut m = setup(0x0214);
m.advance_cycles(5 * LINE + HB);
m.write8(WIN0V, 4).unwrap();
check_rows(&finish(&mut m), |y| y >= 2);
m.advance_cycles(FRAME - VB);
check_rows(&finish(&mut m), |y| y < 4); // No unconditional line-zero reset.
}
#[test]
fn writes_before_line_start_take_effect_but_writes_after_it_do_not_replay_edges() {
for before in [false, true] {
let mut m = setup(0x1420);
m.advance_cycles(5 * LINE - u32::from(before));
m.write16(WIN0V, 0x0508).unwrap();
m.advance_cycles(0); // Does not trigger a comparator.
check_rows(&finish(&mut m), |y| before && (5..8).contains(&y));
}
}
#[test]
fn hblank_writes_cannot_replay_the_current_rows_comparator() {
let mut m = setup(0x0208);
m.advance_cycles(3 * LINE + HB);
m.write16(WIN0V, 0x0203).unwrap(); // Bottom already missed this row.
check_rows(&finish(&mut m), |y| y >= 2);
}
#[test]
fn hidden_line_edges_run_and_unreachable_edges_do_not_fire() {
for top in [160, 200, 227, 228, 255] {
let mut m = setup((top << 8) | 8);
check_rows(&finish(&mut m), |_| false); // Reset starts outside; top not reached yet.
m.advance_cycles(FRAME - VB);
check_rows(&finish(&mut m), |y| top < 228 && y < 8);
}
}
#[test]
fn equal_bounds_clear_only_at_the_matching_edge_and_bottom_wins() {
let mut m = setup(0x0214);
m.advance_cycles(4 * LINE);
m.write16(WIN0V, 0x0808).unwrap();
check_rows(&finish(&mut m), |y| (2..8).contains(&y));
let mut m = setup(0x0214);
m.advance_cycles(4 * LINE);
m.write16(WIN0V, 0xffff).unwrap(); // Neither edge is reachable by VCOUNT.
check_rows(&finish(&mut m), |y| y >= 2);
m.advance_cycles(FRAME - VB);
check_rows(&finish(&mut m), |_| true);
}
#[test]
fn window_disable_and_forced_blank_do_not_pause_comparators() {
for control in [0x4000, 0x2080] {
let mut m = setup(0x0210);
m.write16(DISPCNT, control).unwrap();
m.advance_cycles(5 * LINE);
m.write16(DISPCNT, 0x2000).unwrap();
let f = finish(&mut m);
for y in 0..HEIGHT {
let expected = if y < 5 && control & 0x80 != 0 {
0xffffff
} else if (5..16).contains(&y) {
0
} else {
rgb(31)
};
assert_eq!(
f.pixels()[y * WIDTH],
expected,
"control={control:#x} row={y}"
);
}
}
}
#[test]
fn win0_and_win1_keep_independent_state_and_win0_keeps_precedence() {
let mut m = setup(0x0206);
m.write32(WIN0V, 0x0408_0206).unwrap(); // Independent byte merging for both windows.
m.write16(DISPCNT, 0x6000).unwrap();
m.write16(WININ, 0x2000).unwrap(); // WIN0 allows red; WIN1 darkens to black.
m.advance_cycles(3 * LINE + HB);
m.write8(WIN0V + 1, 5).unwrap(); // Does not close WIN0 before row5.
m.advance_cycles(LINE);
m.write8(WIN1V + 1, 7).unwrap(); // Does not close WIN1 before row7.
check_rows(&finish(&mut m), |y| (6..8).contains(&y));
assert_eq!(m.read32(WIN0V).unwrap(), 0); // Registers remain write-only.
}
#[test]
fn hblank_bounds_and_mask_writes_apply_to_subsequent_rows() {
let mut m = setup(0x020a);
m.advance_cycles(4 * LINE + HB);
m.write16(WIN0H, 0x0a14).unwrap();
m.advance_cycles(LINE);
m.write16(WININ, 0).unwrap();
let f = finish(&mut m);
for y in 0..HEIGHT {
for x in 0..WIDTH {
let dark = (2..5).contains(&y) || (y == 5 && (10..20).contains(&x));
assert_eq!(f.pixels()[y * WIDTH + x], if dark { 0 } else { rgb(31) });
}
}
}
#[test]
fn hblank_dma_programs_future_edges_without_recomputing_interval_membership() {
let mut m = setup(0x1020);
for y in 0..160 {
m.write16(0x0200_0000 + y * 2, if y == 0 { 0x0104 } else { 0xc804 })
.unwrap();
}
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, WIN0V).unwrap();
m.write32(DMA_BASE + 8, 0xa240_0001).unwrap();
m.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
machine.run_until_vblank(1000).unwrap();
check_rows(&present(machine.memory()), |y| (1..4).contains(&y));
assert!(machine.halted());
assert_eq!(machine.memory().read16(DMA_BASE + 10).unwrap(), 0xa240);
}
#[test]
fn capture_disabled_still_tracks_hidden_edges_and_enabling_does_not_reset_state() {
let mut m = setup(0xc808);
m.set_scanline_rendering(false);
m.advance_cycles(FRAME); // Hidden row200 opens the window, retained at row0.
m.set_scanline_rendering(true);
m.set_scanline_rendering(true);
check_rows(&finish(&mut m), |y| y < 8);
}

View file

@ -0,0 +1,209 @@
use super::*;
fn scene(horizontal: u16) -> Memory {
let mut m = setup(160);
m.write16(WIN0H, horizontal).unwrap();
m
}
fn check_line(f: &Framebuffer, y: usize, inside: impl Fn(usize) -> bool) {
for x in 0..WIDTH {
assert_eq!(
f.pixels()[y * WIDTH + x],
if inside(x) { 0 } else { rgb(31) },
"pixel={x},{y}"
);
}
}
#[test]
fn moving_left_after_entry_preserves_earlier_pixels_and_active_state() {
let mut m = scene(0x0a50);
m.advance_cycles(4 * 20 + 1);
m.write8(WIN0H + 1, 40).unwrap();
let f = finish(&mut m);
check_line(&f, 0, |x| (10..80).contains(&x));
check_line(&f, 1, |x| (40..80).contains(&x));
let mut snapshot = Framebuffer::default();
m.render_frame(&mut snapshot).unwrap();
check_line(&snapshot, 0, |x| (40..80).contains(&x));
assert_eq!(present(&m).pixels(), f.pixels());
}
#[test]
fn moving_left_behind_current_column_does_not_open_an_inactive_window() {
let mut m = scene(0x6478);
m.advance_cycles(4 * 20);
m.write8(WIN0H + 1, 10).unwrap();
let f = finish(&mut m);
check_line(&f, 0, |_| false);
check_line(&f, 1, |x| (10..120).contains(&x));
}
#[test]
fn missing_right_keeps_window_open_into_the_next_line() {
let mut m = scene(0x0a50);
m.advance_cycles(4 * 30);
m.write8(WIN0H, 5).unwrap();
let f = finish(&mut m);
check_line(&f, 0, |x| x >= 10);
check_line(&f, 1, |x| !(5..10).contains(&x));
}
#[test]
fn exact_four_cycle_sampling_boundaries_and_zero_cycle_advances_are_observable() {
for elapsed in [79, 80, 81, 83] {
let mut m = scene(0x6450);
m.advance_cycles(elapsed);
m.write16(WIN0H, 0x1450).unwrap();
m.advance_cycles(0);
let f = finish(&mut m);
check_line(&f, 0, |x| elapsed <= 80 && (20..80).contains(&x));
check_line(&f, 1, |x| (20..80).contains(&x));
}
}
#[test]
fn equal_bounds_clear_at_the_edge_not_at_register_write() {
for equal in [20, 40] {
let mut m = scene(0x0ac8);
m.advance_cycles(4 * 30);
m.write16(WIN0H, equal * 0x101).unwrap();
let f = finish(&mut m);
check_line(&f, 0, |x| x >= 10 && (equal == 20 || x < 40));
check_line(&f, 1, |x| equal == 20 && x < 20);
check_line(&f, 2, |_| false);
}
}
#[test]
fn offscreen_columns_including_late_hblank_set_next_lines_wrapping_state() {
for left in [240, 251, 252, 255] {
let mut m = scene((left << 8) | 10);
let f = finish(&mut m);
check_line(&f, 0, |_| false); // No previous line at startup.
check_line(&f, 1, |x| x < 10);
check_line(&f, 159, |x| x < 10);
}
}
#[test]
fn hblank_writes_can_cancel_a_late_offscreen_edge_without_changing_captured_row() {
let mut m = scene(0xff0a);
m.advance_cycles(HB);
m.write8(WIN0H + 1, 20).unwrap(); // X255 not reached; X20 already passed.
let f = finish(&mut m);
check_line(&f, 0, |_| false);
check_line(&f, 1, |x| x >= 20);
check_line(&f, 2, |x| !(10..20).contains(&x));
}
#[test]
fn independent_window_histories_preserve_win0_precedence() {
let mut m = scene(0x0a50);
m.write16(WIN1H, 0x1464).unwrap();
m.write16(WIN1V, 160).unwrap();
m.write16(DISPCNT, 0x6000).unwrap();
m.write16(WININ, 0x2000).unwrap(); // WIN0 red, WIN1 black.
m.advance_cycles(4 * 30);
m.write32(WIN0H, 0x1464_3250).unwrap(); // Move WIN0's left edge only.
let f = finish(&mut m);
check_line(&f, 0, |x| (80..100).contains(&x));
check_line(&f, 1, |x| (20..50).contains(&x) || (80..100).contains(&x));
assert_eq!(m.read32(WIN0H).unwrap(), 0);
}
#[test]
fn disabled_windows_and_forced_blank_do_not_pause_horizontal_history() {
for control in [0x4000, 0x2080] {
let mut m = scene(0x0a50);
m.write16(DISPCNT, control).unwrap();
m.advance_cycles(4 * 30);
m.write16(DISPCNT, 0x2000).unwrap();
let f = finish(&mut m);
// Enable/blank bits still sample at HBlank, unlike WIN0H comparator history.
check_line(&f, 0, |x| (10..80).contains(&x));
}
}
#[test]
fn hblank_dma_can_set_an_offscreen_edge_before_the_next_line() {
let mut m = scene(0);
for row in 0..160 {
m.write16(0x0200_0000 + row * 2, 0xff0a).unwrap();
}
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, WIN0H).unwrap();
m.write32(DMA_BASE + 8, 0xa240_0001).unwrap();
m.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
machine.run_until_vblank(1000).unwrap();
let f = present(machine.memory());
check_line(&f, 0, |_| false);
for y in 1..HEIGHT {
check_line(&f, y, |x| x < 10);
}
assert!(machine.halted());
}
#[test]
fn cpu_store_crossing_a_comparator_uses_committed_bounds_not_final_instruction_time() {
for after in [false, true] {
let mut m = scene(0x6450);
let code = [
0xe3a0_0301, // MOV r0,#0x04000000
0xe3a0_1b05, // MOV r1,#0x1400
0xe381_1050, // ORR r1,r1,#0x50
0xe1c0_14b0, // STRH r1,[r0,#0x40] (WIN0H)
];
for (i, value) in code.into_iter().enumerate() {
m.write32(0x0300_0000 + i as u32 * 4, value).unwrap();
}
let mut machine = Machine::new(Cpu::new(0x0300_0000), m);
for _ in 0..3 {
machine.step().unwrap();
}
let cycles = machine.cycles() as u32;
machine
.memory_mut()
.advance_cycles(if after { 81 } else { 79 } - cycles);
machine.step().unwrap();
assert!(machine.cycles() > 80);
let f = finish(machine.memory_mut());
check_line(&f, 0, |x| !after && (20..80).contains(&x));
}
}
#[test]
fn pixel_colors_still_sample_at_hblank_while_window_edges_keep_history() {
let mut m = scene(0x0a50);
m.advance_cycles(4 * 30);
m.write8(WIN0H + 1, 40).unwrap();
m.write16(PAL, 0x7c00).unwrap();
let f = finish(&mut m);
for x in 0..WIDTH {
assert_eq!(
f.pixels()[x],
if (10..80).contains(&x) {
0
} else {
rgb(0x7c00)
}
);
}
}
#[test]
fn capture_enable_preserves_partial_line_history_and_hidden_lines_update_flags() {
let mut m = scene(0x0a50);
m.set_scanline_rendering(false);
m.advance_cycles(4 * 30);
m.write8(WIN0H + 1, 40).unwrap();
m.set_scanline_rendering(true); // Before first HBlank, this frame is still complete.
check_line(&finish(&mut m), 0, |x| (10..80).contains(&x));
m.set_scanline_rendering(false);
m.write16(WIN0H, 0xff0a).unwrap();
m.advance_cycles(FRAME - VB);
m.set_scanline_rendering(true);
check_line(&finish(&mut m), 0, |x| x < 10);
}