refactor: split into gameboy-rust workspace with core, demos, and desktop crates
Move the emulator into crates/gba-core, the original demo programs into crates/gba-demos, and the minifb frontend into crates/desktop. The binary is now gameboy-rust. Imports change from gba_rust to gba_core/gba_demos; no behavior changes. gba-core has no dependencies. Its integration tests use gba-demos as a development dependency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a8944d1d5f
commit
f8a0676748
113 changed files with 304 additions and 282 deletions
9
crates/gba-core/Cargo.toml
Normal file
9
crates/gba-core/Cargo.toml
Normal 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
422
crates/gba-core/src/bios.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
186
crates/gba-core/src/bios/affine.rs
Normal file
186
crates/gba-core/src/bios/affine.rs
Normal 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]
|
||||
);
|
||||
}
|
||||
}
|
||||
112
crates/gba-core/src/bios/angles.rs
Normal file
112
crates/gba-core/src/bios/angles.rs
Normal 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");
|
||||
}
|
||||
57
crates/gba-core/src/bios/arithmetic.rs
Normal file
57
crates/gba-core/src/bios/arithmetic.rs
Normal 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");
|
||||
}
|
||||
90
crates/gba-core/src/bios/bit_unpack.rs
Normal file
90
crates/gba-core/src/bios/bit_unpack.rs
Normal 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");
|
||||
}
|
||||
23
crates/gba-core/src/bios/decompression.rs
Normal file
23
crates/gba-core/src/bios/decompression.rs
Normal 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
|
||||
}
|
||||
67
crates/gba-core/src/bios/differential.rs
Normal file
67
crates/gba-core/src/bios/differential.rs
Normal 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");
|
||||
}
|
||||
88
crates/gba-core/src/bios/huffman.rs
Normal file
88
crates/gba-core/src/bios/huffman.rs
Normal 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");
|
||||
}
|
||||
82
crates/gba-core/src/bios/lz77.rs
Normal file
82
crates/gba-core/src/bios/lz77.rs
Normal 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");
|
||||
}
|
||||
119
crates/gba-core/src/bios/reset.rs
Normal file
119
crates/gba-core/src/bios/reset.rs
Normal 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));
|
||||
}
|
||||
64
crates/gba-core/src/bios/run_length.rs
Normal file
64
crates/gba-core/src/bios/run_length.rs
Normal 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
254
crates/gba-core/src/cpu.rs
Normal 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]
|
||||
}
|
||||
}
|
||||
}
|
||||
108
crates/gba-core/src/cpu/alu.rs
Normal file
108
crates/gba-core/src/cpu/alu.rs
Normal 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,
|
||||
},
|
||||
)
|
||||
}
|
||||
305
crates/gba-core/src/cpu/arm.rs
Normal file
305
crates/gba-core/src/cpu/arm.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
67
crates/gba-core/src/cpu/exception.rs
Normal file
67
crates/gba-core/src/cpu/exception.rs
Normal 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
|
||||
}
|
||||
}
|
||||
327
crates/gba-core/src/cpu/exception_tests.rs
Normal file
327
crates/gba-core/src/cpu/exception_tests.rs
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
use super::*;
|
||||
use crate::memory::{BIOS_SIZE, ROM_START};
|
||||
|
||||
fn program(instruction: u32) -> Memory {
|
||||
Memory::new(instruction.to_le_bytes().to_vec()).unwrap()
|
||||
}
|
||||
|
||||
fn vectors(instruction: u32) -> Memory {
|
||||
let bios = instruction.to_le_bytes().repeat(BIOS_SIZE / 4);
|
||||
Memory::with_bios(vec![], bios).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exception_entry_sets_vector_link_masks_and_saved_status() {
|
||||
for (exception, mode, vector, arm_offset, thumb_offset) in [
|
||||
(Exception::UndefinedInstruction, Mode::Undefined, 4, 4, 2),
|
||||
(Exception::SoftwareInterrupt, Mode::Supervisor, 8, 4, 2),
|
||||
(Exception::PrefetchAbort, Mode::Abort, 12, 4, 4),
|
||||
(Exception::DataAbort, Mode::Abort, 16, 8, 8),
|
||||
(Exception::Irq, Mode::Irq, 24, 4, 4),
|
||||
(Exception::Fiq, Mode::Fiq, 28, 4, 4),
|
||||
] {
|
||||
for thumb in [false, true] {
|
||||
for fiq_mask in [0, 0x40] {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
let status = 0xb000_001f | if thumb { 0x20 } else { 0 } | fiq_mask;
|
||||
cpu.apply_status(status, Mode::System);
|
||||
cpu.registers[13] = 0x0300_0100;
|
||||
cpu.registers[14] = 123;
|
||||
cpu.enter_exception(exception);
|
||||
assert_eq!(cpu.mode(), mode);
|
||||
assert_eq!(cpu.pc(), vector);
|
||||
assert_eq!(
|
||||
cpu.registers[14],
|
||||
ROM_START + if thumb { thumb_offset } else { arm_offset }
|
||||
);
|
||||
assert_eq!(cpu.registers[13], 0);
|
||||
assert_eq!(cpu.spsr(), Some(status));
|
||||
assert_eq!(
|
||||
cpu.cpsr(),
|
||||
0xb000_0080
|
||||
| mode as u32
|
||||
| if exception == Exception::Fiq {
|
||||
0x40
|
||||
} else {
|
||||
fiq_mask
|
||||
}
|
||||
);
|
||||
cpu.switch_mode(Mode::System);
|
||||
assert_eq!(cpu.registers[13..15], [0x0300_0100, 123]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_and_thumb_swi_execute_and_movs_pc_restores_caller() {
|
||||
for thumb in [false, true] {
|
||||
for comment in [0, 1, 255] {
|
||||
let rom = if thumb {
|
||||
(0xdf00_u16 | comment).to_le_bytes().to_vec()
|
||||
} else {
|
||||
(0xefab_0000_u32 | u32::from(comment))
|
||||
.to_le_bytes()
|
||||
.to_vec()
|
||||
};
|
||||
let mut memory =
|
||||
Memory::with_bios(rom, 0xe1b0_f00e_u32.to_le_bytes().repeat(BIOS_SIZE / 4))
|
||||
.unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
let status = 0x6000_001f | if thumb { 0x20 } else { 0 };
|
||||
cpu.apply_status(status, Mode::System);
|
||||
cpu.registers[14] = 0x1234;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.pc(), 8);
|
||||
assert_eq!(cpu.mode(), Mode::Supervisor);
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
|
||||
// MOVS pc, lr restores the saved flags, not ALU-result flags.
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.pc(), ROM_START + if thumb { 2 } else { 4 });
|
||||
assert_eq!(cpu.cpsr(), status);
|
||||
assert_eq!(cpu.registers[14], 0x1234);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_swi_can_be_skipped_without_entering_supervisor() {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
let mut expected = cpu.clone();
|
||||
expected.registers[15] += 4;
|
||||
cpu.step(&mut program(0x0fff_ffff)).unwrap();
|
||||
assert_eq!(cpu, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swi_without_vector_image_enters_then_reports_fetch_error() {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
let mut memory = program(0xef00_0000);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
let before = cpu.clone();
|
||||
assert_eq!(
|
||||
cpu.step(&mut memory),
|
||||
Err(CpuError::Memory(MemoryError::Unmapped(8)))
|
||||
);
|
||||
assert_eq!(cpu, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interrupt_masks_and_fiq_priority_are_respected() {
|
||||
for irq_mask in [false, true] {
|
||||
for fiq_mask in [false, true] {
|
||||
for irq in [false, true] {
|
||||
for fiq in [false, true] {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.irq_disabled = irq_mask;
|
||||
cpu.fiq_disabled = fiq_mask;
|
||||
let before = cpu.clone();
|
||||
let expected = if fiq && !fiq_mask {
|
||||
Some(Mode::Fiq)
|
||||
} else if irq && !irq_mask {
|
||||
Some(Mode::Irq)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
assert_eq!(cpu.take_interrupt(irq, fiq), expected.is_some());
|
||||
if let Some(mode) = expected {
|
||||
assert_eq!(cpu.mode(), mode);
|
||||
assert_eq!(cpu.spsr(), Some(before.cpsr()));
|
||||
} else {
|
||||
assert_eq!(cpu, before);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subs_exception_returns_resume_or_retry_the_correct_instruction() {
|
||||
for (exception, subtract) in [
|
||||
(Exception::Irq, 4),
|
||||
(Exception::Fiq, 4),
|
||||
(Exception::PrefetchAbort, 4),
|
||||
(Exception::DataAbort, 8),
|
||||
] {
|
||||
for thumb in [false, true] {
|
||||
let mut cpu = Cpu::new(ROM_START + 2 * u32::from(thumb));
|
||||
let status = 0xa000_001f | if thumb { 0x20 } else { 0 };
|
||||
cpu.apply_status(status, Mode::System);
|
||||
let before = cpu.clone();
|
||||
cpu.enter_exception(exception);
|
||||
cpu.step(&mut vectors(0xe25e_f000 | subtract)).unwrap(); // SUBS pc, lr, #offset
|
||||
assert_eq!(cpu.registers, before.registers);
|
||||
assert_eq!(cpu.cpsr(), before.cpsr());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_fiq_preserves_irq_saved_status_and_banked_registers() {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[8] = 88;
|
||||
cpu.enter_exception(Exception::Irq);
|
||||
cpu.registers[13] = 0x0300_0200;
|
||||
let irq_status = cpu.cpsr();
|
||||
let irq_spsr = cpu.spsr();
|
||||
let irq_link = cpu.registers[14];
|
||||
assert!(cpu.take_interrupt(false, true));
|
||||
cpu.registers[8] = 888;
|
||||
cpu.step(&mut vectors(0xe25e_f004)).unwrap();
|
||||
assert_eq!(cpu.pc(), 0x18);
|
||||
assert_eq!(cpu.cpsr(), irq_status);
|
||||
assert_eq!(cpu.spsr(), irq_spsr);
|
||||
assert_eq!(cpu.registers[13..15], [0x0300_0200, irq_link]);
|
||||
assert_eq!(cpu.registers[8], 88);
|
||||
cpu.step(&mut vectors(0xe25e_f004)).unwrap();
|
||||
assert_eq!(cpu.pc(), ROM_START);
|
||||
assert_eq!(cpu.cpsr(), 0x1f);
|
||||
cpu.switch_mode(Mode::Fiq);
|
||||
assert_eq!(cpu.registers[8], 888);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_mode_exception_overwrites_link_and_saved_status() {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.enter_exception(Exception::SoftwareInterrupt);
|
||||
cpu.registers[15] = 0x100;
|
||||
let previous = cpu.cpsr();
|
||||
cpu.enter_exception(Exception::SoftwareInterrupt);
|
||||
assert_eq!(cpu.spsr(), Some(previous));
|
||||
assert_eq!(cpu.registers[14], 0x104);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_alignment_uses_saved_state_not_target_bit_zero() {
|
||||
for (status, expected) in [(0x1f, ROM_START), (0x3f, ROM_START + 2)] {
|
||||
let mut cpu = Cpu::at_reset();
|
||||
cpu.registers[15] = ROM_START;
|
||||
cpu.registers[14] = ROM_START + 3;
|
||||
cpu.set_spsr(status);
|
||||
cpu.step(&mut program(0xe1b0_f00e)).unwrap();
|
||||
assert_eq!(cpu.pc(), expected);
|
||||
assert_eq!(cpu.cpsr(), status);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_saved_mode_rejects_return_without_partial_changes() {
|
||||
for instruction in [0xe1b0_f00e, 0xe8fd_8001] {
|
||||
let mut cpu = Cpu::at_reset();
|
||||
cpu.registers[15] = ROM_START;
|
||||
cpu.registers[13] = 0x0200_0000;
|
||||
cpu.set_spsr(0); // An invalid mode
|
||||
let before = cpu.clone();
|
||||
assert!(cpu.step(&mut program(instruction)).is_err());
|
||||
assert_eq!(cpu, before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ldm_exception_return_loads_outgoing_bank_before_restoring_status() {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[13] = 0x0300_0100;
|
||||
cpu.registers[14] = 123;
|
||||
cpu.enter_exception(Exception::SoftwareInterrupt);
|
||||
cpu.registers[15] = ROM_START;
|
||||
cpu.registers[13] = 0x0200_0000;
|
||||
cpu.set_spsr(0x9000_003f);
|
||||
let mut memory = program(0xe8fd_c001); // LDMIA sp!, {r0, lr, pc}^
|
||||
for (index, value) in [42, 456, ROM_START + 7].into_iter().enumerate() {
|
||||
memory
|
||||
.write32(0x0200_0000 + index as u32 * 4, value)
|
||||
.unwrap();
|
||||
}
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.cpsr(), 0x9000_003f);
|
||||
assert_eq!(cpu.pc(), ROM_START + 6);
|
||||
assert_eq!(cpu.registers[0], 42);
|
||||
assert_eq!(cpu.registers[13..15], [0x0300_0100, 123]);
|
||||
cpu.switch_mode(Mode::Supervisor);
|
||||
assert_eq!(cpu.registers[13..15], [0x0200_000c, 456]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_ldm_return_preserves_every_bank_and_status() {
|
||||
let mut cpu = Cpu::at_reset();
|
||||
cpu.registers[15] = ROM_START;
|
||||
cpu.registers[13] = 0x0400_0054;
|
||||
cpu.set_spsr(0x3f);
|
||||
let mut memory = program(0xe8fd_8001);
|
||||
memory.write32(0x0400_0054, 42).unwrap();
|
||||
let before = cpu.clone();
|
||||
assert!(matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))));
|
||||
assert_eq!(cpu, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_bank_transfers_use_current_base_and_user_values() {
|
||||
for mode in [Mode::System, Mode::Supervisor, Mode::Fiq] {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
for register in 0..15 {
|
||||
cpu.registers[register] = 100 + register as u32;
|
||||
}
|
||||
cpu.switch_mode(mode);
|
||||
cpu.registers[13] = 0x0200_0100;
|
||||
let original = cpu.clone();
|
||||
let mut memory = program(0xe8cd_7fff); // STMIA sp, {r0-r14}^
|
||||
cpu.step(&mut memory).unwrap();
|
||||
for register in 0..15 {
|
||||
assert_eq!(
|
||||
memory.read32(0x0200_0100 + register as u32 * 4).unwrap(),
|
||||
original.user_register(register)
|
||||
);
|
||||
memory
|
||||
.write32(0x0200_0100 + register as u32 * 4, 200 + register as u32)
|
||||
.unwrap();
|
||||
}
|
||||
// Use RAM for the next instruction without changing the transfer base.
|
||||
memory.write32(0x0300_0000, 0xe8dd_7fff).unwrap(); // LDMIA sp, {r0-r14}^
|
||||
cpu.registers[15] = 0x0300_0000;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.cpsr(), original.cpsr());
|
||||
if mode != Mode::System {
|
||||
assert_eq!(cpu.registers[13..15], original.registers[13..15]);
|
||||
}
|
||||
if mode == Mode::Fiq {
|
||||
assert_eq!(cpu.registers[8..13], original.registers[8..13]);
|
||||
}
|
||||
cpu.switch_mode(Mode::System);
|
||||
for register in 0..15 {
|
||||
assert_eq!(cpu.registers[register], 200 + register as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_bank_store_pc_uses_pipeline_offset() {
|
||||
let mut cpu = Cpu::at_reset();
|
||||
cpu.registers[15] = ROM_START;
|
||||
cpu.registers[0] = 0x0200_0000;
|
||||
let mut memory = program(0xe8c0_8000); // STMIA r0, {pc}^
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(memory.read32(0x0200_0000).unwrap(), ROM_START + 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_mode_cannot_request_user_bank_transfers_or_status_return() {
|
||||
for instruction in [0xe8c0_0002, 0xe8d0_0002, 0xe8d0_8000, 0xe1b0_f00e] {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.switch_mode(Mode::User);
|
||||
cpu.registers[0] = 0x0200_0000;
|
||||
let before = cpu.clone();
|
||||
assert!(cpu.step(&mut program(instruction)).is_err());
|
||||
assert_eq!(cpu, before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_user_bank_load_preserves_hidden_registers() {
|
||||
let mut cpu = Cpu::at_reset();
|
||||
cpu.registers[15] = ROM_START;
|
||||
cpu.registers[0] = 0x0400_0054;
|
||||
let before = cpu.clone();
|
||||
assert!(cpu.step(&mut program(0xe8d0_6000)).is_err()); // LDMIA r0, {sp, lr}^
|
||||
assert_eq!(cpu, before);
|
||||
}
|
||||
560
crates/gba-core/src/cpu/instruction_tests.rs
Normal file
560
crates/gba-core/src/cpu/instruction_tests.rs
Normal 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);
|
||||
}
|
||||
183
crates/gba-core/src/cpu/status.rs
Normal file
183
crates/gba-core/src/cpu/status.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
195
crates/gba-core/src/cpu/status_tests.rs
Normal file
195
crates/gba-core/src/cpu/status_tests.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
210
crates/gba-core/src/cpu/tests.rs
Normal file
210
crates/gba-core/src/cpu/tests.rs
Normal 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(), ®isters);
|
||||
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
|
||||
}
|
||||
);
|
||||
}
|
||||
314
crates/gba-core/src/cpu/thumb.rs
Normal file
314
crates/gba-core/src/cpu/thumb.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
821
crates/gba-core/src/cpu/thumb_tests.rs
Normal file
821
crates/gba-core/src/cpu/thumb_tests.rs
Normal 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);
|
||||
}
|
||||
205
crates/gba-core/src/cpu/timing.rs
Normal file
205
crates/gba-core/src/cpu/timing.rs
Normal 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
|
||||
}
|
||||
383
crates/gba-core/src/cpu/timing_tests.rs
Normal file
383
crates/gba-core/src/cpu/timing_tests.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
145
crates/gba-core/src/cpu/transfer.rs
Normal file
145
crates/gba-core/src/cpu/transfer.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
493
crates/gba-core/src/cpu/transfer_tests.rs
Normal file
493
crates/gba-core/src/cpu/transfer_tests.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
132
crates/gba-core/src/display.rs
Normal file
132
crates/gba-core/src/display.rs
Normal 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
259
crates/gba-core/src/dma.rs
Normal 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)
|
||||
}
|
||||
42
crates/gba-core/src/input.rs
Normal file
42
crates/gba-core/src/input.rs
Normal 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
383
crates/gba-core/src/io.rs
Normal 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)
|
||||
}
|
||||
12
crates/gba-core/src/lib.rs
Normal file
12
crates/gba-core/src/lib.rs
Normal 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;
|
||||
176
crates/gba-core/src/machine.rs
Normal file
176
crates/gba-core/src/machine.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
705
crates/gba-core/src/memory.rs
Normal file
705
crates/gba-core/src/memory.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
126
crates/gba-core/src/timing.rs
Normal file
126
crates/gba-core/src/timing.rs
Normal 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()));
|
||||
}
|
||||
}
|
||||
381
crates/gba-core/src/video.rs
Normal file
381
crates/gba-core/src/video.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
266
crates/gba-core/src/video/affine.rs
Normal file
266
crates/gba-core/src/video/affine.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
76
crates/gba-core/src/video/capture.rs
Normal file
76
crates/gba-core/src/video/capture.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
95
crates/gba-core/src/video/effects.rs
Normal file
95
crates/gba-core/src/video/effects.rs
Normal 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)
|
||||
})
|
||||
}
|
||||
190
crates/gba-core/src/video/mosaic.rs
Normal file
190
crates/gba-core/src/video/mosaic.rs
Normal 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.
|
||||
}
|
||||
}
|
||||
230
crates/gba-core/src/video/sprites.rs
Normal file
230
crates/gba-core/src/video/sprites.rs
Normal 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, (¤t, 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
|
||||
}
|
||||
}
|
||||
124
crates/gba-core/src/video/sprites/budget.rs
Normal file
124
crates/gba-core/src/video/sprites/budget.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
122
crates/gba-core/src/video/sprites/pipeline.rs
Normal file
122
crates/gba-core/src/video/sprites/pipeline.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
crates/gba-core/src/video/windows.rs
Normal file
133
crates/gba-core/src/video/windows.rs
Normal 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.
|
||||
}
|
||||
}
|
||||
168
crates/gba-core/src/video/windows/horizontal.rs
Normal file
168
crates/gba-core/src/video/windows/horizontal.rs
Normal 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.
|
||||
}
|
||||
}
|
||||
377
crates/gba-core/tests/affine_backgrounds.rs
Normal file
377
crates/gba-core/tests/affine_backgrounds.rs
Normal 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());
|
||||
}
|
||||
330
crates/gba-core/tests/affine_tracking.rs
Normal file
330
crates/gba-core/tests/affine_tracking.rs
Normal 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
|
||||
);
|
||||
}
|
||||
671
crates/gba-core/tests/bios.rs
Normal file
671
crates/gba-core/tests/bios.rs
Normal 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))
|
||||
);
|
||||
}
|
||||
684
crates/gba-core/tests/bios/affine.rs
Normal file
684
crates/gba-core/tests/bios/affine.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
377
crates/gba-core/tests/bios/angles.rs
Normal file
377
crates/gba-core/tests/bios/angles.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
191
crates/gba-core/tests/bios/arithmetic.rs
Normal file
191
crates/gba-core/tests/bios/arithmetic.rs
Normal 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.
|
||||
}
|
||||
418
crates/gba-core/tests/bios/bit_unpack.rs
Normal file
418
crates/gba-core/tests/bios/bit_unpack.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
411
crates/gba-core/tests/bios/differential.rs
Normal file
411
crates/gba-core/tests/bios/differential.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
473
crates/gba-core/tests/bios/huffman.rs
Normal file
473
crates/gba-core/tests/bios/huffman.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
417
crates/gba-core/tests/bios/lz77.rs
Normal file
417
crates/gba-core/tests/bios/lz77.rs
Normal 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);
|
||||
}
|
||||
492
crates/gba-core/tests/bios/ram_reset.rs
Normal file
492
crates/gba-core/tests/bios/ram_reset.rs
Normal 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);
|
||||
}
|
||||
363
crates/gba-core/tests/bios/reset.rs
Normal file
363
crates/gba-core/tests/bios/reset.rs
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
use super::*;
|
||||
use gba_core::{
|
||||
dma::DMA_BASE,
|
||||
io::{BG0CNT, DISPCNT, WAITCNT},
|
||||
memory::{OAM_START, PALETTE_START},
|
||||
};
|
||||
|
||||
const FLAG: u32 = 0x0300_7ffa;
|
||||
const CLEAR_START: u32 = 0x0300_7e00;
|
||||
const CLEAR_END: u32 = 0x0300_8000;
|
||||
|
||||
fn restart(m: &mut Machine, flag: u8) {
|
||||
m.memory_mut().write8(FLAG, flag).unwrap();
|
||||
m.step().unwrap(); // Enter the real SWI vector.
|
||||
reach(m, if flag == 0 { ROM_START } else { SOURCE }, 2000);
|
||||
}
|
||||
|
||||
fn assert_restart_state(m: &Machine, target: u32) {
|
||||
assert_eq!(m.cpu().pc(), target);
|
||||
assert_eq!(m.cpu().mode(), Mode::System);
|
||||
assert_eq!(m.cpu().instruction_set(), InstructionSet::Arm);
|
||||
assert_eq!(m.cpu().cpsr(), 0x9f); // Deterministic policy: IRQ masked, FIQ clear, NZCV clear.
|
||||
assert_eq!(&m.cpu().registers()[..13], &[0; 13]);
|
||||
assert_eq!(m.cpu().registers()[13], SYSTEM_STACK);
|
||||
assert_eq!(m.cpu().registers()[14], target);
|
||||
}
|
||||
|
||||
fn assert_cleared(m: &Machine) {
|
||||
for address in CLEAR_START..CLEAR_END {
|
||||
assert_eq!(m.memory().read8(address).unwrap(), 0, "{address:#x}");
|
||||
}
|
||||
}
|
||||
|
||||
// Inline literal with a branch over its data. No host register mutation.
|
||||
fn literal(code: &mut Vec<u32>, register: u32, value: u32) {
|
||||
code.extend([0xe59f_0000 | register << 12, 0xea00_0000, value]);
|
||||
}
|
||||
|
||||
fn dirty_sp(mode: u32) -> u32 {
|
||||
0x0300_7000 + mode * 32
|
||||
}
|
||||
|
||||
fn dirty_lr(mode: u32) -> u32 {
|
||||
0xa100_0000 | mode
|
||||
}
|
||||
|
||||
fn seeded_call(thumb: bool, status: u32, location: Option<u32>) -> Machine {
|
||||
let mut code = Vec::new();
|
||||
for mode in [0x11, 0x12, 0x13, 0x17, 0x1b] {
|
||||
code.push(0xe321_f0c0 | mode); // Switch bank with IRQ/FIQ masked.
|
||||
literal(&mut code, 13, dirty_sp(mode));
|
||||
literal(&mut code, 14, dirty_lr(mode));
|
||||
literal(&mut code, 0, 0xb000_00df);
|
||||
code.push(0xe169_f000); // MSR SPSR_fc,r0
|
||||
if mode == 0x11 {
|
||||
for r in 8..=12 {
|
||||
code.push(0xe3a0_00e0 | r << 12 | r); // Distinct FIQ high registers.
|
||||
}
|
||||
}
|
||||
}
|
||||
code.push(0xe321_f0df);
|
||||
literal(&mut code, 13, 0x0300_7dc0);
|
||||
for r in 0..=12 {
|
||||
code.push(0xe3a0_0080 | r << 12 | r);
|
||||
}
|
||||
code.extend([0xe328_f20f, 0xe321_f000 | status]); // NZCV set; caller mode/masks.
|
||||
let swi_pc = if let Some(address) = location {
|
||||
literal(&mut code, 14, address | u32::from(thumb));
|
||||
code.push(0xe12f_ff1e);
|
||||
address
|
||||
} else {
|
||||
code.extend([
|
||||
if thumb { 0xe28f_e001 } else { 0xe1a0_e00f },
|
||||
if thumb { 0xe12f_ff1e } else { 0xe1a0_0000 },
|
||||
]);
|
||||
let address = ROM_START + code.len() as u32 * 4;
|
||||
code.extend([if thumb { 0xe7fe_df00 } else { 0xef00_0000 }, 0xeaff_fffe]);
|
||||
address
|
||||
};
|
||||
let mut m = bios::boot(words(&code)).unwrap();
|
||||
if let Some(address) = location {
|
||||
m.memory_mut()
|
||||
.write32(address, if thumb { 0xe7fe_df00 } else { 0xef00_0000 })
|
||||
.unwrap();
|
||||
m.memory_mut().write32(address + 4, 0xeaff_fffe).unwrap();
|
||||
}
|
||||
reach(&mut m, swi_pc, 500);
|
||||
m
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_flag_byte_selects_rom_or_ram_from_arm_and_thumb() {
|
||||
for thumb in [false, true] {
|
||||
for flag in 0..=255 {
|
||||
let (mut m, _) = call(0, thumb, [0x123, 0x456, 0x789]);
|
||||
// Only the byte at 7ffa selects the target, not surrounding bytes.
|
||||
m.memory_mut().write32(IRQ_FLAGS, 0xffff_ffff).unwrap();
|
||||
restart(&mut m, flag);
|
||||
assert_restart_state(&m, if flag == 0 { ROM_START } else { SOURCE });
|
||||
assert_cleared(&m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clears_exactly_top_512_bytes_and_keeps_other_memory() {
|
||||
for thumb in [false, true] {
|
||||
let (mut m, _) = call(0, thumb, [0, 0, 0]);
|
||||
for address in (0x0300_0000..CLEAR_END).step_by(4) {
|
||||
m.memory_mut()
|
||||
.write32(address, address ^ 0xa55a_c33c)
|
||||
.unwrap();
|
||||
}
|
||||
let sentinels = [
|
||||
SOURCE,
|
||||
SOURCE + 0x3fffc,
|
||||
PALETTE_START,
|
||||
PALETTE_START + 0x3fc,
|
||||
VRAM_START,
|
||||
VRAM_START + 0x17ffc,
|
||||
OAM_START,
|
||||
OAM_START + 0x3fc,
|
||||
];
|
||||
for address in sentinels {
|
||||
m.memory_mut().write32(address, 0x1357_2468).unwrap();
|
||||
}
|
||||
let rom_first = m.memory().read32(ROM_START).unwrap();
|
||||
restart(&mut m, 0);
|
||||
assert_cleared(&m);
|
||||
for address in (0x0300_0000..CLEAR_START).step_by(4) {
|
||||
assert_eq!(m.memory().read32(address).unwrap(), address ^ 0xa55a_c33c);
|
||||
}
|
||||
for address in sentinels {
|
||||
assert_eq!(m.memory().read32(address).unwrap(), 0x1357_2468);
|
||||
}
|
||||
assert_eq!(m.memory().read32(ROM_START).unwrap(), rom_first);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resets_nonzero_registers_and_status_for_user_system_callers() {
|
||||
for thumb in [false, true] {
|
||||
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
|
||||
for flag in [0, 1] {
|
||||
let mut m = seeded_call(thumb, status, None);
|
||||
assert!(m.cpu().registers()[..13].iter().all(|r| *r != 0));
|
||||
assert_eq!(m.cpu().cpsr() & 0xf000_0000, 0xf000_0000);
|
||||
restart(&mut m, flag);
|
||||
assert_restart_state(&m, if flag == 0 { ROM_START } else { SOURCE });
|
||||
assert_cleared(&m);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reinitializes_supervisor_irq_stacks_links_and_saved_status() {
|
||||
for thumb in [false, true] {
|
||||
let mut m = seeded_call(thumb, 0x1f, None);
|
||||
for (i, mode) in [0xd3, 0xd2, 0xdf].into_iter().enumerate() {
|
||||
m.memory_mut()
|
||||
.write32(SOURCE + i as u32 * 4, 0xe321_f000 | mode)
|
||||
.unwrap();
|
||||
}
|
||||
restart(&mut m, 1);
|
||||
for (mode, stack) in [
|
||||
(Mode::Supervisor, bios::SVC_STACK),
|
||||
(Mode::Irq, bios::IRQ_STACK),
|
||||
] {
|
||||
m.step().unwrap();
|
||||
assert_eq!(m.cpu().mode(), mode);
|
||||
assert_eq!(m.cpu().registers()[13], stack);
|
||||
assert_eq!(m.cpu().registers()[14], 0);
|
||||
assert_eq!(m.cpu().spsr(), Some(0));
|
||||
}
|
||||
m.step().unwrap();
|
||||
assert_eq!(m.cpu().mode(), Mode::System);
|
||||
assert_eq!(m.cpu().registers()[13], SYSTEM_STACK);
|
||||
assert_eq!(m.cpu().registers()[14], SOURCE);
|
||||
assert_cleared(&m);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_banked_registers_and_saved_status_are_preserved() {
|
||||
let mut m = seeded_call(false, 0x1f, None);
|
||||
for (i, mode) in [0x11, 0x17, 0x1b].into_iter().enumerate() {
|
||||
m.memory_mut()
|
||||
.write32(SOURCE + i as u32 * 4, 0xe321_f0c0 | mode)
|
||||
.unwrap();
|
||||
}
|
||||
restart(&mut m, 1);
|
||||
for mode in [Mode::Fiq, Mode::Abort, Mode::Undefined] {
|
||||
m.step().unwrap();
|
||||
assert_eq!(m.cpu().mode(), mode);
|
||||
assert_eq!(m.cpu().registers()[13], dirty_sp(mode as u32));
|
||||
assert_eq!(m.cpu().registers()[14], dirty_lr(mode as u32));
|
||||
assert_eq!(m.cpu().spsr(), Some(0xb000_00df));
|
||||
if mode == Mode::Fiq {
|
||||
assert_eq!(&m.cpu().registers()[8..13], &[0xe8, 0xe9, 0xea, 0xeb, 0xec]);
|
||||
} else {
|
||||
assert_eq!(&m.cpu().registers()[8..13], &[0; 5]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_restart_from_work_ram_including_code_in_erased_area() {
|
||||
for thumb in [false, true] {
|
||||
for location in [SOURCE + 0x100, 0x0300_1000, CLEAR_START, 0x03ff_fe00] {
|
||||
let mut m = seeded_call(thumb, 0x1f, Some(location));
|
||||
restart(&mut m, 1);
|
||||
assert_restart_state(&m, SOURCE);
|
||||
assert_cleared(&m);
|
||||
if location < CLEAR_START {
|
||||
assert_eq!(
|
||||
m.memory().read32(location).unwrap(),
|
||||
if thumb { 0xe7fe_df00 } else { 0xef00_0000 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ram_restart_executes_arm_code_and_can_call_bios_again() {
|
||||
for thumb in [false, true] {
|
||||
let mut m = seeded_call(thumb, 0x10, None);
|
||||
// This program is only valid as ARM: Sqrt(81), then a self-branch.
|
||||
for (i, instruction) in [0xe3a0_0051, 0xef08_0000, 0xeaff_fffe]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
m.memory_mut()
|
||||
.write32(SOURCE + i as u32 * 4, instruction)
|
||||
.unwrap();
|
||||
}
|
||||
restart(&mut m, 1);
|
||||
reach(&mut m, SOURCE + 8, 1000);
|
||||
assert_eq!(m.cpu().registers()[0], 9);
|
||||
assert_eq!(m.cpu().registers()[13], SYSTEM_STACK);
|
||||
assert_eq!(m.cpu().registers()[14], SOURCE);
|
||||
assert_eq!(m.cpu().cpsr(), 0x9f);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_reset_io_or_device_clocks() {
|
||||
for thumb in [false, true] {
|
||||
let (mut m, _) = call(0, thumb, [0, 0, 0]);
|
||||
for (address, value) in [
|
||||
(DISPCNT, 0x0403),
|
||||
(BG0CNT, 0x0141),
|
||||
(WAITCNT, 0x4317),
|
||||
(DISPSTAT, 0x7b38),
|
||||
(IE, 0x1234),
|
||||
(IME, 1),
|
||||
] {
|
||||
m.memory_mut().write16(address, value).unwrap();
|
||||
}
|
||||
m.memory_mut().write32(TIMER_BASE, 0x0080_0000).unwrap();
|
||||
let timer_before = m.memory().read16(TIMER_BASE).unwrap();
|
||||
let cycles = m.cycles();
|
||||
restart(&mut m, 0);
|
||||
for (address, value) in [
|
||||
(DISPCNT, 0x0403),
|
||||
(BG0CNT, 0x0141),
|
||||
(WAITCNT, 0x4317),
|
||||
(IE, 0x1234),
|
||||
(IME, 1),
|
||||
(POSTFLG, 1),
|
||||
(TIMER_BASE + 2, 0x80),
|
||||
] {
|
||||
assert_eq!(m.memory().read16(address).unwrap(), value, "{address:#x}");
|
||||
}
|
||||
assert_eq!(m.memory().read16(DISPSTAT).unwrap() & 0xff38, 0x7b38);
|
||||
let elapsed = m.cycles() - cycles;
|
||||
assert!(elapsed > 512 && elapsed < 65536);
|
||||
assert_eq!(
|
||||
m.memory().read16(TIMER_BASE).unwrap(),
|
||||
timer_before.wrapping_add(elapsed as u16)
|
||||
);
|
||||
assert!(!m.halted());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dma_timers_and_pending_irq_continue_but_irq_stays_masked_after_restart() {
|
||||
for thumb in [false, true] {
|
||||
for ime in [0, 1] {
|
||||
let (mut m, _) = call(0, thumb, [0, 0, 0]);
|
||||
m.memory_mut().write8(FLAG, 1).unwrap();
|
||||
m.memory_mut().write32(SOURCE, 0xeaff_fffe).unwrap();
|
||||
m.step().unwrap();
|
||||
// Queue IRQ and DMA only after SWI entry; otherwise IRQ can win first.
|
||||
m.memory_mut().write16(IE, 8).unwrap();
|
||||
m.memory_mut().write16(IME, ime).unwrap();
|
||||
m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap();
|
||||
m.memory_mut().write32(DEST, 0x8765_4321).unwrap();
|
||||
m.memory_mut().write32(DMA_BASE, DEST).unwrap();
|
||||
m.memory_mut().write32(DMA_BASE + 4, DEST + 4).unwrap();
|
||||
m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap();
|
||||
let mut dma = 0;
|
||||
for _ in 0..2000 {
|
||||
if m.cpu().pc() == SOURCE {
|
||||
break;
|
||||
}
|
||||
let step = m.step().unwrap();
|
||||
assert_ne!(step, StepKind::IrqEntry);
|
||||
if matches!(step, StepKind::Dma { channel: 0 }) {
|
||||
dma += 1;
|
||||
}
|
||||
}
|
||||
assert_restart_state(&m, SOURCE);
|
||||
assert_eq!(dma, 1);
|
||||
assert_eq!(m.memory().read32(DEST + 4).unwrap(), 0x8765_4321);
|
||||
assert_eq!(m.memory().read16(IF).unwrap() & 8, 8);
|
||||
assert_eq!(m.memory().read16(IME).unwrap(), ime);
|
||||
assert_cleared(&m);
|
||||
assert_eq!(m.step().unwrap(), StepKind::Instruction);
|
||||
assert_eq!(m.cpu().pc(), SOURCE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_is_not_an_atomic_host_memory_clear() {
|
||||
let (mut m, _) = call(0, false, [0, 0, 0]);
|
||||
for address in (CLEAR_START..CLEAR_END).step_by(4) {
|
||||
m.memory_mut().write32(address, 0xa55a_c33c).unwrap();
|
||||
}
|
||||
m.memory_mut().write8(FLAG, 0).unwrap();
|
||||
m.step().unwrap();
|
||||
for _ in 0..1000 {
|
||||
if m.memory().read32(CLEAR_START).unwrap() == 0 {
|
||||
break;
|
||||
}
|
||||
m.step().unwrap();
|
||||
}
|
||||
assert_eq!(m.memory().read32(CLEAR_START).unwrap(), 0);
|
||||
assert_eq!(m.memory().read32(CLEAR_START + 4).unwrap(), 0xa55a_c33c);
|
||||
assert_eq!(m.memory().read32(IRQ_HANDLER).unwrap(), 0xa55a_c33c);
|
||||
assert!(m.cpu().pc() < BIOS_SIZE as u32);
|
||||
reach(&mut m, ROM_START, 2000);
|
||||
assert_cleared(&m);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exception_mode_callers_still_receive_a_diagnostic() {
|
||||
for status in [0x12, 0x13] {
|
||||
let (mut m, _) = call_status(0, false, [0, 0, 0], status);
|
||||
m.memory_mut().write32(CLEAR_START, 0x1357_2468).unwrap();
|
||||
let error = (0..100).find_map(|_| m.step().err()).unwrap();
|
||||
assert!(matches!(
|
||||
error,
|
||||
MachineError::Cpu(CpuError::UnsupportedInstruction {
|
||||
instruction: UNSUPPORTED_TRAP,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert_eq!(m.memory().read32(CLEAR_START).unwrap(), 0x1357_2468);
|
||||
}
|
||||
}
|
||||
395
crates/gba-core/tests/bios/run_length.rs
Normal file
395
crates/gba-core/tests/bios/run_length.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
442
crates/gba-core/tests/bitmap_modes.rs
Normal file
442
crates/gba-core/tests/bitmap_modes.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
177
crates/gba-core/tests/core.rs
Normal file
177
crates/gba-core/tests/core.rs
Normal 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);
|
||||
}
|
||||
449
crates/gba-core/tests/display.rs
Normal file
449
crates/gba-core/tests/display.rs
Normal 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);
|
||||
}
|
||||
860
crates/gba-core/tests/dma.rs
Normal file
860
crates/gba-core/tests/dma.rs
Normal 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);
|
||||
}
|
||||
61
crates/gba-core/tests/effects.rs
Normal file
61
crates/gba-core/tests/effects.rs
Normal 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();
|
||||
}
|
||||
172
crates/gba-core/tests/effects/colors.rs
Normal file
172
crates/gba-core/tests/effects/colors.rs
Normal 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));
|
||||
}
|
||||
166
crates/gba-core/tests/effects/demo.rs
Normal file
166
crates/gba-core/tests/effects/demo.rs
Normal 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));
|
||||
}
|
||||
176
crates/gba-core/tests/effects/windows.rs
Normal file
176
crates/gba-core/tests/effects/windows.rs
Normal 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));
|
||||
}
|
||||
89
crates/gba-core/tests/exceptions.rs
Normal file
89
crates/gba-core/tests/exceptions.rs
Normal 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);
|
||||
}
|
||||
297
crates/gba-core/tests/graphics.rs
Normal file
297
crates/gba-core/tests/graphics.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
687
crates/gba-core/tests/halt.rs
Normal file
687
crates/gba-core/tests/halt.rs
Normal 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());
|
||||
}
|
||||
272
crates/gba-core/tests/machine.rs
Normal file
272
crates/gba-core/tests/machine.rs
Normal 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());
|
||||
}
|
||||
87
crates/gba-core/tests/memory.rs
Normal file
87
crates/gba-core/tests/memory.rs
Normal 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))
|
||||
);
|
||||
}
|
||||
47
crates/gba-core/tests/mosaic.rs
Normal file
47
crates/gba-core/tests/mosaic.rs
Normal 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]
|
||||
}
|
||||
212
crates/gba-core/tests/mosaic/backgrounds.rs
Normal file
212
crates/gba-core/tests/mosaic/backgrounds.rs
Normal 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));
|
||||
}
|
||||
100
crates/gba-core/tests/mosaic/demo.rs
Normal file
100
crates/gba-core/tests/mosaic/demo.rs
Normal 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);
|
||||
}
|
||||
215
crates/gba-core/tests/mosaic/objects.rs
Normal file
215
crates/gba-core/tests/mosaic/objects.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
351
crates/gba-core/tests/mosaic_tracking.rs
Normal file
351
crates/gba-core/tests/mosaic_tracking.rs
Normal 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)));
|
||||
}
|
||||
}
|
||||
38
crates/gba-core/tests/scanlines.rs
Normal file
38
crates/gba-core/tests/scanlines.rs
Normal 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()
|
||||
}
|
||||
169
crates/gba-core/tests/scanlines/capture.rs
Normal file
169
crates/gba-core/tests/scanlines/capture.rs
Normal 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());
|
||||
}
|
||||
196
crates/gba-core/tests/scanlines/machine.rs
Normal file
196
crates/gba-core/tests/scanlines/machine.rs
Normal 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))
|
||||
);
|
||||
}
|
||||
}
|
||||
155
crates/gba-core/tests/scanlines/rendering.rs
Normal file
155
crates/gba-core/tests/scanlines/rendering.rs
Normal 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]);
|
||||
}
|
||||
306
crates/gba-core/tests/sprite_budget.rs
Normal file
306
crates/gba-core/tests/sprite_budget.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
288
crates/gba-core/tests/sprite_pipeline.rs
Normal file
288
crates/gba-core/tests/sprite_pipeline.rs
Normal 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));
|
||||
}
|
||||
370
crates/gba-core/tests/sprites.rs
Normal file
370
crates/gba-core/tests/sprites.rs
Normal 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);
|
||||
}
|
||||
285
crates/gba-core/tests/sprites/affine.rs
Normal file
285
crates/gba-core/tests/sprites/affine.rs
Normal 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));
|
||||
}
|
||||
479
crates/gba-core/tests/tiles.rs
Normal file
479
crates/gba-core/tests/tiles.rs
Normal 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);
|
||||
}
|
||||
360
crates/gba-core/tests/timers.rs
Normal file
360
crates/gba-core/tests/timers.rs
Normal 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);
|
||||
}
|
||||
169
crates/gba-core/tests/timing.rs
Normal file
169
crates/gba-core/tests/timing.rs
Normal 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);
|
||||
}
|
||||
228
crates/gba-core/tests/window_tracking.rs
Normal file
228
crates/gba-core/tests/window_tracking.rs
Normal 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);
|
||||
}
|
||||
209
crates/gba-core/tests/window_tracking/horizontal.rs
Normal file
209
crates/gba-core/tests/window_tracking/horizontal.rs
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue