refactor: split into gameboy-rust workspace with core, demos, and desktop crates

Move the emulator into crates/gba-core, the original demo programs into
crates/gba-demos, and the minifb frontend into crates/desktop. The binary
is now gameboy-rust. Imports change from gba_rust to gba_core/gba_demos;
no behavior changes.

gba-core has no dependencies. Its integration tests use gba-demos as a
development dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
0xrsydn 2026-10-05 16:07:32 +07:00
commit f8a0676748
113 changed files with 304 additions and 282 deletions

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,327 @@
use super::*;
use crate::memory::{BIOS_SIZE, ROM_START};
fn program(instruction: u32) -> Memory {
Memory::new(instruction.to_le_bytes().to_vec()).unwrap()
}
fn vectors(instruction: u32) -> Memory {
let bios = instruction.to_le_bytes().repeat(BIOS_SIZE / 4);
Memory::with_bios(vec![], bios).unwrap()
}
#[test]
fn exception_entry_sets_vector_link_masks_and_saved_status() {
for (exception, mode, vector, arm_offset, thumb_offset) in [
(Exception::UndefinedInstruction, Mode::Undefined, 4, 4, 2),
(Exception::SoftwareInterrupt, Mode::Supervisor, 8, 4, 2),
(Exception::PrefetchAbort, Mode::Abort, 12, 4, 4),
(Exception::DataAbort, Mode::Abort, 16, 8, 8),
(Exception::Irq, Mode::Irq, 24, 4, 4),
(Exception::Fiq, Mode::Fiq, 28, 4, 4),
] {
for thumb in [false, true] {
for fiq_mask in [0, 0x40] {
let mut cpu = Cpu::new(ROM_START);
let status = 0xb000_001f | if thumb { 0x20 } else { 0 } | fiq_mask;
cpu.apply_status(status, Mode::System);
cpu.registers[13] = 0x0300_0100;
cpu.registers[14] = 123;
cpu.enter_exception(exception);
assert_eq!(cpu.mode(), mode);
assert_eq!(cpu.pc(), vector);
assert_eq!(
cpu.registers[14],
ROM_START + if thumb { thumb_offset } else { arm_offset }
);
assert_eq!(cpu.registers[13], 0);
assert_eq!(cpu.spsr(), Some(status));
assert_eq!(
cpu.cpsr(),
0xb000_0080
| mode as u32
| if exception == Exception::Fiq {
0x40
} else {
fiq_mask
}
);
cpu.switch_mode(Mode::System);
assert_eq!(cpu.registers[13..15], [0x0300_0100, 123]);
}
}
}
}
#[test]
fn arm_and_thumb_swi_execute_and_movs_pc_restores_caller() {
for thumb in [false, true] {
for comment in [0, 1, 255] {
let rom = if thumb {
(0xdf00_u16 | comment).to_le_bytes().to_vec()
} else {
(0xefab_0000_u32 | u32::from(comment))
.to_le_bytes()
.to_vec()
};
let mut memory =
Memory::with_bios(rom, 0xe1b0_f00e_u32.to_le_bytes().repeat(BIOS_SIZE / 4))
.unwrap();
let mut cpu = Cpu::new(ROM_START);
let status = 0x6000_001f | if thumb { 0x20 } else { 0 };
cpu.apply_status(status, Mode::System);
cpu.registers[14] = 0x1234;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), 8);
assert_eq!(cpu.mode(), Mode::Supervisor);
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
// MOVS pc, lr restores the saved flags, not ALU-result flags.
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START + if thumb { 2 } else { 4 });
assert_eq!(cpu.cpsr(), status);
assert_eq!(cpu.registers[14], 0x1234);
}
}
}
#[test]
fn conditional_swi_can_be_skipped_without_entering_supervisor() {
let mut cpu = Cpu::new(ROM_START);
let mut expected = cpu.clone();
expected.registers[15] += 4;
cpu.step(&mut program(0x0fff_ffff)).unwrap();
assert_eq!(cpu, expected);
}
#[test]
fn swi_without_vector_image_enters_then_reports_fetch_error() {
let mut cpu = Cpu::new(ROM_START);
let mut memory = program(0xef00_0000);
cpu.step(&mut memory).unwrap();
let before = cpu.clone();
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(8)))
);
assert_eq!(cpu, before);
}
#[test]
fn interrupt_masks_and_fiq_priority_are_respected() {
for irq_mask in [false, true] {
for fiq_mask in [false, true] {
for irq in [false, true] {
for fiq in [false, true] {
let mut cpu = Cpu::new(ROM_START);
cpu.irq_disabled = irq_mask;
cpu.fiq_disabled = fiq_mask;
let before = cpu.clone();
let expected = if fiq && !fiq_mask {
Some(Mode::Fiq)
} else if irq && !irq_mask {
Some(Mode::Irq)
} else {
None
};
assert_eq!(cpu.take_interrupt(irq, fiq), expected.is_some());
if let Some(mode) = expected {
assert_eq!(cpu.mode(), mode);
assert_eq!(cpu.spsr(), Some(before.cpsr()));
} else {
assert_eq!(cpu, before);
}
}
}
}
}
}
#[test]
fn subs_exception_returns_resume_or_retry_the_correct_instruction() {
for (exception, subtract) in [
(Exception::Irq, 4),
(Exception::Fiq, 4),
(Exception::PrefetchAbort, 4),
(Exception::DataAbort, 8),
] {
for thumb in [false, true] {
let mut cpu = Cpu::new(ROM_START + 2 * u32::from(thumb));
let status = 0xa000_001f | if thumb { 0x20 } else { 0 };
cpu.apply_status(status, Mode::System);
let before = cpu.clone();
cpu.enter_exception(exception);
cpu.step(&mut vectors(0xe25e_f000 | subtract)).unwrap(); // SUBS pc, lr, #offset
assert_eq!(cpu.registers, before.registers);
assert_eq!(cpu.cpsr(), before.cpsr());
}
}
}
#[test]
fn nested_fiq_preserves_irq_saved_status_and_banked_registers() {
let mut cpu = Cpu::new(ROM_START);
cpu.registers[8] = 88;
cpu.enter_exception(Exception::Irq);
cpu.registers[13] = 0x0300_0200;
let irq_status = cpu.cpsr();
let irq_spsr = cpu.spsr();
let irq_link = cpu.registers[14];
assert!(cpu.take_interrupt(false, true));
cpu.registers[8] = 888;
cpu.step(&mut vectors(0xe25e_f004)).unwrap();
assert_eq!(cpu.pc(), 0x18);
assert_eq!(cpu.cpsr(), irq_status);
assert_eq!(cpu.spsr(), irq_spsr);
assert_eq!(cpu.registers[13..15], [0x0300_0200, irq_link]);
assert_eq!(cpu.registers[8], 88);
cpu.step(&mut vectors(0xe25e_f004)).unwrap();
assert_eq!(cpu.pc(), ROM_START);
assert_eq!(cpu.cpsr(), 0x1f);
cpu.switch_mode(Mode::Fiq);
assert_eq!(cpu.registers[8], 888);
}
#[test]
fn same_mode_exception_overwrites_link_and_saved_status() {
let mut cpu = Cpu::new(ROM_START);
cpu.enter_exception(Exception::SoftwareInterrupt);
cpu.registers[15] = 0x100;
let previous = cpu.cpsr();
cpu.enter_exception(Exception::SoftwareInterrupt);
assert_eq!(cpu.spsr(), Some(previous));
assert_eq!(cpu.registers[14], 0x104);
}
#[test]
fn return_alignment_uses_saved_state_not_target_bit_zero() {
for (status, expected) in [(0x1f, ROM_START), (0x3f, ROM_START + 2)] {
let mut cpu = Cpu::at_reset();
cpu.registers[15] = ROM_START;
cpu.registers[14] = ROM_START + 3;
cpu.set_spsr(status);
cpu.step(&mut program(0xe1b0_f00e)).unwrap();
assert_eq!(cpu.pc(), expected);
assert_eq!(cpu.cpsr(), status);
}
}
#[test]
fn invalid_saved_mode_rejects_return_without_partial_changes() {
for instruction in [0xe1b0_f00e, 0xe8fd_8001] {
let mut cpu = Cpu::at_reset();
cpu.registers[15] = ROM_START;
cpu.registers[13] = 0x0200_0000;
cpu.set_spsr(0); // An invalid mode
let before = cpu.clone();
assert!(cpu.step(&mut program(instruction)).is_err());
assert_eq!(cpu, before);
}
}
#[test]
fn ldm_exception_return_loads_outgoing_bank_before_restoring_status() {
let mut cpu = Cpu::new(ROM_START);
cpu.registers[13] = 0x0300_0100;
cpu.registers[14] = 123;
cpu.enter_exception(Exception::SoftwareInterrupt);
cpu.registers[15] = ROM_START;
cpu.registers[13] = 0x0200_0000;
cpu.set_spsr(0x9000_003f);
let mut memory = program(0xe8fd_c001); // LDMIA sp!, {r0, lr, pc}^
for (index, value) in [42, 456, ROM_START + 7].into_iter().enumerate() {
memory
.write32(0x0200_0000 + index as u32 * 4, value)
.unwrap();
}
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.cpsr(), 0x9000_003f);
assert_eq!(cpu.pc(), ROM_START + 6);
assert_eq!(cpu.registers[0], 42);
assert_eq!(cpu.registers[13..15], [0x0300_0100, 123]);
cpu.switch_mode(Mode::Supervisor);
assert_eq!(cpu.registers[13..15], [0x0200_000c, 456]);
}
#[test]
fn failed_ldm_return_preserves_every_bank_and_status() {
let mut cpu = Cpu::at_reset();
cpu.registers[15] = ROM_START;
cpu.registers[13] = 0x0400_0054;
cpu.set_spsr(0x3f);
let mut memory = program(0xe8fd_8001);
memory.write32(0x0400_0054, 42).unwrap();
let before = cpu.clone();
assert!(matches!(cpu.step(&mut memory), Err(CpuError::Memory(_))));
assert_eq!(cpu, before);
}
#[test]
fn user_bank_transfers_use_current_base_and_user_values() {
for mode in [Mode::System, Mode::Supervisor, Mode::Fiq] {
let mut cpu = Cpu::new(ROM_START);
for register in 0..15 {
cpu.registers[register] = 100 + register as u32;
}
cpu.switch_mode(mode);
cpu.registers[13] = 0x0200_0100;
let original = cpu.clone();
let mut memory = program(0xe8cd_7fff); // STMIA sp, {r0-r14}^
cpu.step(&mut memory).unwrap();
for register in 0..15 {
assert_eq!(
memory.read32(0x0200_0100 + register as u32 * 4).unwrap(),
original.user_register(register)
);
memory
.write32(0x0200_0100 + register as u32 * 4, 200 + register as u32)
.unwrap();
}
// Use RAM for the next instruction without changing the transfer base.
memory.write32(0x0300_0000, 0xe8dd_7fff).unwrap(); // LDMIA sp, {r0-r14}^
cpu.registers[15] = 0x0300_0000;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.cpsr(), original.cpsr());
if mode != Mode::System {
assert_eq!(cpu.registers[13..15], original.registers[13..15]);
}
if mode == Mode::Fiq {
assert_eq!(cpu.registers[8..13], original.registers[8..13]);
}
cpu.switch_mode(Mode::System);
for register in 0..15 {
assert_eq!(cpu.registers[register], 200 + register as u32);
}
}
}
#[test]
fn user_bank_store_pc_uses_pipeline_offset() {
let mut cpu = Cpu::at_reset();
cpu.registers[15] = ROM_START;
cpu.registers[0] = 0x0200_0000;
let mut memory = program(0xe8c0_8000); // STMIA r0, {pc}^
cpu.step(&mut memory).unwrap();
assert_eq!(memory.read32(0x0200_0000).unwrap(), ROM_START + 12);
}
#[test]
fn user_mode_cannot_request_user_bank_transfers_or_status_return() {
for instruction in [0xe8c0_0002, 0xe8d0_0002, 0xe8d0_8000, 0xe1b0_f00e] {
let mut cpu = Cpu::new(ROM_START);
cpu.switch_mode(Mode::User);
cpu.registers[0] = 0x0200_0000;
let before = cpu.clone();
assert!(cpu.step(&mut program(instruction)).is_err());
assert_eq!(cpu, before);
}
}
#[test]
fn failed_user_bank_load_preserves_hidden_registers() {
let mut cpu = Cpu::at_reset();
cpu.registers[15] = ROM_START;
cpu.registers[0] = 0x0400_0054;
let before = cpu.clone();
assert!(cpu.step(&mut program(0xe8d0_6000)).is_err()); // LDMIA r0, {sp, lr}^
assert_eq!(cpu, before);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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