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
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.
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue