From 0280563430e74722bf73a9550ee1f0299aa523b8 Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Mon, 5 Oct 2026 12:55:35 +0700 Subject: [PATCH] feat(bios): expand packed units with BitUnPack Add SWI 0x10, packed-unit widths, conditional offsets, complete word stores, and independent bit-level tests. --- src/bios.rs | 9 +- src/bios/bit_unpack.rs | 90 +++++++++ tests/bios.rs | 2 + tests/bios/bit_unpack.rs | 418 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 515 insertions(+), 4 deletions(-) create mode 100644 src/bios/bit_unpack.rs create mode 100644 tests/bios/bit_unpack.rs diff --git a/src/bios.rs b/src/bios.rs index 0fe2768..d014017 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -1,13 +1,12 @@ //! Original, optional ARM BIOS replacement. No Nintendo firmware is included. -//! Supports interrupt waits and memory copy/fill. -//! Includes integer arithmetic services. -//! Includes LZ77 decoding. -//! Includes run-length decoding. +//! Supports interrupt waits, integer arithmetic, memory copy/fill, bit unpacking, +//! and LZ77/run-length decoding. //! This is a functional subset, not a complete boot ROM or a timing-compatible BIOS. use std::collections::BTreeMap; mod arithmetic; +mod bit_unpack; mod decompression; mod lz77; mod run_length; @@ -122,6 +121,7 @@ pub fn image() -> Vec { (8, "sqrt"), (0x0b, "cpu_set"), (0x0c, "cpu_fast_set"), + (0x10, "bit_unpack"), (0x11, "lz_wram"), (0x12, "lz_vram"), (0x14, "rl_wram"), @@ -218,6 +218,7 @@ pub fn image() -> Vec { ); arithmetic::emit(&mut a); + bit_unpack::emit(&mut a); lz77::emit(&mut a); run_length::emit(&mut a); decompression::emit_writer(&mut a); diff --git a/src/bios/bit_unpack.rs b/src/bios/bit_unpack.rs new file mode 100644 index 0000000..b44943a --- /dev/null +++ b/src/bios/bit_unpack.rs @@ -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"); +} diff --git a/tests/bios.rs b/tests/bios.rs index 0f6dbbd..f5abc41 100644 --- a/tests/bios.rs +++ b/tests/bios.rs @@ -14,6 +14,8 @@ const ROM_DATA: u32 = ROM_START + 13 * 4; #[path = "bios/arithmetic.rs"] mod arithmetic; +#[path = "bios/bit_unpack.rs"] +mod bit_unpack; #[path = "bios/lz77.rs"] mod lz77; #[path = "bios/run_length.rs"] diff --git a/tests/bios/bit_unpack.rs b/tests/bios/bit_unpack.rs new file mode 100644 index 0000000..fd28350 --- /dev/null +++ b/tests/bios/bit_unpack.rs @@ -0,0 +1,418 @@ +use super::*; +use gba_rust::{ + bios::INVALID_ARGUMENT_TRAP, + dma::DMA_BASE, + memory::{OAM_START, PALETTE_START}, +}; + +const INFO: u32 = SOURCE + 0x1000; + +fn info(m: &mut Machine, length: u16, source_width: u8, dest_width: u8, offset: u32) { + m.memory_mut().write16(INFO, length).unwrap(); + m.memory_mut().write8(INFO + 2, source_width).unwrap(); + m.memory_mut().write8(INFO + 3, dest_width).unwrap(); + m.memory_mut().write32(INFO + 4, offset).unwrap(); +} + +fn prepare( + thumb: bool, + destination: u32, + data: &[u8], + sw: u8, + dw: u8, + offset: u32, +) -> (Machine, u32) { + let (mut m, pc) = call_status_data(0x10, thumb, [ROM_DATA, destination, INFO], 0x1f, data); + info(&mut m, data.len().try_into().unwrap(), sw, dw, offset); + (m, pc) +} + +/// Expand individual bits and repack words independently of the emitted ARM loops. +fn reference(data: &[u8], sw: u8, dw: u8, offset: u32) -> Vec { + let mut bits = Vec::new(); + for byte in data { + for position in (0..8).step_by(sw as usize) { + let mut value = 0u64; + for bit in 0..sw { + value += u64::from((byte >> (position + bit)) & 1) << bit; + } + if value != 0 || offset & 0x8000_0000 != 0 { + value += u64::from(offset & 0x7fff_ffff); + } + assert!(value < (1u64 << dw)); + for bit in 0..dw { + bits.push((value >> bit) & 1); + } + } + } + assert_eq!(bits.len() % 32, 0); + bits.chunks_exact(32) + .map(|chunk| { + chunk + .iter() + .enumerate() + .fold(0, |word, (bit, v)| word | (*v as u32) << bit) + }) + .collect() +} + +fn complete(m: &mut Machine, pc: u32) { + m.step().unwrap(); + reach(m, pc, 3_000_000); +} + +fn verify(thumb: bool, dest: u32, data: &[u8], sw: u8, dw: u8, offset: u32) { + let expected = reference(data, sw, dw, offset); + let (mut m, pc) = prepare(thumb, dest, data, sw, dw, offset); + let before = m.cpu().clone(); + let end = dest + expected.len() as u32 * 4; + for address in [dest - 4, end, bios::IRQ_STACK] { + m.memory_mut().write32(address, 0xa55a_1234).unwrap(); + } + complete(&mut m, pc); + for (index, word) in expected.iter().enumerate() { + assert_eq!( + m.memory().read32(dest + index as u32 * 4).unwrap(), + *word, + "word={index}, sw={sw}, dw={dw}" + ); + } + for address in [dest - 4, end, bios::IRQ_STACK] { + assert_eq!(m.memory().read32(address).unwrap(), 0xa55a_1234); + } + assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]); + assert_eq!(m.cpu().cpsr(), before.cpsr()); +} + +fn fail(m: &mut Machine) -> MachineError { + for _ in 0..3000 { + let before = m.cpu().clone(); + let cycles = m.cycles(); + let timing = m.last_timing(); + if let Err(error) = m.step() { + assert_eq!(m.cpu(), &before); + assert_eq!(m.cycles(), cycles); + assert_eq!(m.last_timing(), timing); + assert_eq!(m.step(), Err(error.clone())); + return error; + } + } + panic!("invalid unpack did not produce a bounded diagnostic"); +} + +fn assert_invalid(error: MachineError) { + assert!(matches!( + error, + MachineError::Cpu(CpuError::UnsupportedInstruction { + instruction: INVALID_ARGUMENT_TRAP, + .. + }) + )); +} + +#[test] +fn all_supported_width_pairs_and_input_bytes_match_bit_reference() { + let data: Vec<_> = (0..=255).collect(); + for sw in [1, 2, 4, 8] { + for dw in [1, 2, 4, 8, 16, 32].into_iter().filter(|dw| *dw >= sw) { + for thumb in [false, true] { + verify( + thumb, + if thumb { VRAM_START + 4 } else { DEST }, + &data, + sw, + dw, + 0, + ); + } + } + } +} + +#[test] +fn source_units_and_destination_units_are_least_significant_first() { + let (mut m, pc) = prepare(false, DEST, &[0b1011_0001], 1, 4, 0); + complete(&mut m, pc); + assert_eq!(m.memory().read32(DEST).unwrap(), 0x1011_0001); + let (mut m, pc) = prepare(true, DEST, &[0xe4], 2, 8, 0); + complete(&mut m, pc); + assert_eq!(m.memory().read32(DEST).unwrap(), 0x0302_0100); +} + +#[test] +fn offsets_apply_to_nonzero_units_and_optionally_to_zero() { + for zero in [0, 0x8000_0000] { + for offset in [0, 1, 0x1234, 0x7fff_ffff] { + verify(false, DEST, &[0, 1, 0xfe, 0xff], 8, 32, offset | zero); + } + verify(true, VRAM_START + 4, &[0x10, 0x21], 4, 8, 0x80 | zero); + } +} + +#[test] +fn seeded_width_offset_and_data_combinations_match_reference() { + let mut seed = 0x615c_ae39u32; + for case in 0..96 { + seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); + let sw = 1u8 << (seed & 3); + let choices: Vec = [1, 2, 4, 8, 16, 32] + .into_iter() + .filter(|dw| *dw >= sw) + .collect(); + let dw = choices[(seed >> 8) as usize % choices.len()]; + let capacity = ((1u64 << dw) - (1u64 << sw)).min(0x7fff_ffff); + let bias = + ((seed as u64) % (capacity + 1)) as u32 | if case % 2 == 0 { 0x8000_0000 } else { 0 }; + let data: Vec<_> = (0..20) + .map(|_| { + seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); + (seed >> 24) as u8 + }) + .collect(); + verify(case % 2 != 0, DEST, &data, sw, dw, bias); + } +} + +#[test] +fn maximum_source_byte_count_is_unsigned_and_fully_processed() { + let data: Vec<_> = (0..65535u32).map(|i| i as u8).collect(); + // Keep the descriptor in IWRAM: the output fills almost all EWRAM. + let (mut m, pc) = call_status_data(0x10, false, [ROM_DATA, SOURCE, 0x0300_0100], 0x1f, &data); + m.memory_mut().write32(0x0300_0100, 0x2008_ffff).unwrap(); + m.memory_mut().write32(0x0300_0104, 0).unwrap(); + m.memory_mut() + .write32(SOURCE + 65535 * 4, 0xa55a_1234) + .unwrap(); + complete(&mut m, pc); + for (index, byte) in data.iter().enumerate() { + assert_eq!( + m.memory().read32(SOURCE + index as u32 * 4).unwrap(), + u32::from(*byte) + ); + } + assert_eq!(m.memory().read32(SOURCE + 65535 * 4).unwrap(), 0xa55a_1234); +} + +#[test] +fn byte_aligned_sources_work_in_rom_and_both_ram_regions() { + for source in [ + ROM_DATA + 1, + ROM_DATA + 2, + ROM_DATA + 3, + SOURCE + 1, + 0x0300_0103, + ] { + let mut data = vec![0xcc; 3]; + data.extend([0x12, 0x34, 0x56, 0x78]); + let (mut m, pc) = call_status_data(0x10, true, [source, DEST, INFO], 0x1f, &data); + info(&mut m, 4, 8, 8, 0); + let expected = if source >= ROM_DATA { + &data[(source - ROM_DATA) as usize..(source - ROM_DATA) as usize + 4] + } else { + &[0x12, 0x34, 0x56, 0x78] + }; + if source < ROM_DATA { + for (index, byte) in expected.iter().enumerate() { + m.memory_mut().write8(source + index as u32, *byte).unwrap(); + } + } + complete(&mut m, pc); + assert_eq!( + m.memory().read32(DEST).unwrap(), + u32::from_le_bytes(expected.try_into().unwrap()) + ); + } +} + +#[test] +fn unpack_descriptor_can_be_in_cartridge_rom() { + let data = [4, 0, 8, 8, 0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78]; + let (mut m, pc) = call_status_data(0x10, false, [ROM_DATA + 8, DEST, ROM_DATA], 0x1f, &data); + complete(&mut m, pc); + assert_eq!(m.memory().read32(DEST).unwrap(), 0x7856_3412); +} + +#[test] +fn caller_registers_status_and_stack_boundaries_are_preserved() { + for thumb in [false, true] { + for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] { + let (mut m, pc) = + call_status_data(0x10, thumb, [ROM_DATA, DEST, INFO], status, &[0x12, 0x34]); + info(&mut m, 2, 4, 8, 0x10); + let before = m.cpu().clone(); + m.memory_mut() + .write32(bios::IRQ_STACK, 0x1234_5678) + .unwrap(); + complete(&mut m, pc); + assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]); + assert_eq!(m.cpu().cpsr(), before.cpsr()); + assert_eq!(m.memory().read32(bios::IRQ_STACK).unwrap(), 0x1234_5678); + } + } +} + +#[test] +fn zero_length_does_not_access_source_or_destination_after_validation() { + let (mut m, pc) = call(0x10, false, [0x0e00_0000, 0x0e00_0000, INFO]); + info(&mut m, 0, 1, 32, 0); + complete(&mut m, pc); +} + +#[test] +fn invalid_widths_and_narrowing_produce_diagnostics() { + for width in 0..=255u8 { + if ![1, 2, 4, 8].contains(&width) { + let (mut m, _) = prepare(false, DEST, &[0; 4], width, 32, 0); + assert_invalid(fail(&mut m)); + assert_eq!(m.memory().read32(DEST).unwrap(), 0); + } + if ![1, 2, 4, 8, 16, 32].contains(&width) { + let (mut m, _) = prepare(true, DEST, &[0; 4], 1, width, 0); + assert_invalid(fail(&mut m)); + } + } + for (sw, dw) in [(2, 1), (4, 1), (4, 2), (8, 1), (8, 2), (8, 4)] { + let (mut m, _) = prepare(false, DEST, &[0; 4], sw, dw, 0); + assert_invalid(fail(&mut m)); + } +} + +#[test] +fn alignment_protected_source_and_wrapping_ranges_are_rejected() { + for (source, dest, descriptor) in [ + (0, DEST, INFO), + (0x3fff, DEST, INFO), + (0x0100_0000, DEST, INFO), + (ROM_DATA, DEST + 1, INFO), + (ROM_DATA, DEST + 2, INFO), + (ROM_DATA, DEST + 3, INFO), + (ROM_DATA, DEST, INFO + 1), + (ROM_DATA, DEST, INFO + 2), + (ROM_DATA, DEST, INFO + 3), + (0xffff_fffe, DEST, INFO), + (ROM_DATA, 0xffff_fffc, INFO), + ] { + let (mut m, _) = call_status_data(0x10, false, [source, dest, descriptor], 0x1f, &[1; 4]); + info(&mut m, 4, 8, 32, 0); + assert_invalid(fail(&mut m)); + assert_eq!(m.memory().read32(DEST).unwrap(), 0); + } +} + +#[test] +fn incomplete_output_words_are_rejected_before_source_reads() { + for (length, sw, dw) in [(1, 8, 8), (2, 8, 8), (3, 8, 8), (1, 4, 8), (6, 8, 8)] { + let (mut m, _) = call(0x10, false, [0x0e00_0000, DEST, INFO]); + info(&mut m, length, sw, dw, 0); + assert_invalid(fail(&mut m)); + assert_eq!(m.memory().read32(DEST).unwrap(), 0); + } +} + +#[test] +fn offset_overflow_keeps_completed_words_but_discards_pending_word() { + // First four values fit; a later 255+1 does not fit eight destination bits. + let (mut m, _) = prepare(false, DEST, &[1, 2, 3, 4, 5, 0xff, 7, 8], 8, 8, 1); + m.memory_mut().write32(DEST + 4, 0xcccc_cccc).unwrap(); + assert_invalid(fail(&mut m)); + assert_eq!(m.memory().read32(DEST).unwrap(), 0x0504_0302); + assert_eq!(m.memory().read32(DEST + 4).unwrap(), 0xcccc_cccc); + // Offsets on transparent zero units are checked only when the flag requests them. + verify(true, DEST, &[0; 4], 8, 8, 256); + let (mut m, _) = prepare(true, DEST, &[0; 4], 8, 8, 0x8000_0100); + assert_invalid(fail(&mut m)); +} + +#[test] +fn truncated_descriptors_and_sources_return_memory_diagnostics() { + let descriptor = [4, 0, 8, 8, 0, 0, 0, 0]; + for length in 0..8 { + let (mut m, _) = call_status_data( + 0x10, + false, + [SOURCE, DEST, ROM_DATA], + 0x1f, + &descriptor[..length], + ); + assert_eq!( + fail(&mut m), + MachineError::Cpu(MemoryError::Unmapped(ROM_DATA + length as u32).into()) + ); + } + for length in 0..8 { + let (mut m, _) = prepare(false, DEST, &vec![0x12; length], 8, 8, 0); + info(&mut m, 8, 8, 8, 0); + m.memory_mut().write32(DEST, 0xcccc_cccc).unwrap(); + m.memory_mut().write32(DEST + 4, 0xcccc_cccc).unwrap(); + assert_eq!( + fail(&mut m), + MachineError::Cpu(MemoryError::Unmapped(ROM_DATA + length as u32).into()) + ); + assert_eq!( + m.memory().read32(DEST).unwrap(), + if length >= 4 { + 0x1212_1212 + } else { + 0xcccc_cccc + } + ); + assert_eq!(m.memory().read32(DEST + 4).unwrap(), 0xcccc_cccc); + } +} + +#[test] +fn destination_failure_preserves_completed_word() { + let (mut m, _) = prepare( + false, + 0x07ff_fffc, + &[0x12, 0x34, 0x56, 0x78, 1, 2, 3, 4], + 8, + 8, + 0, + ); + assert_eq!( + fail(&mut m), + MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into()) + ); + assert_eq!(m.memory().read32(0x07ff_fffc).unwrap(), 0x7856_3412); +} + +#[test] +fn word_stores_support_palette_oam_and_video_memory() { + for dest in [PALETTE_START + 4, OAM_START + 4, VRAM_START + 4] { + verify(false, dest, &[0xe4, 0x1b], 2, 4, 0); + } +} + +#[test] +fn timers_and_dma_continue_while_irq_delivery_is_masked() { + let (mut m, pc) = prepare(false, DEST, &[0x12; 16], 8, 32, 0); + m.step().unwrap(); // Enter SWI before enabling IRQ sources. + m.memory_mut().write16(IE, 8).unwrap(); + m.memory_mut().write16(IME, 1).unwrap(); + m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap(); + m.memory_mut().write32(SOURCE, 0xa55a_1234).unwrap(); + m.memory_mut().write32(DMA_BASE, SOURCE).unwrap(); + m.memory_mut().write32(DMA_BASE + 4, SOURCE + 4).unwrap(); + m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap(); + let mut dma = 0; + for _ in 0..5000 { + if m.cpu().pc() == pc { + break; + } + let step = m.step().unwrap(); + assert_ne!(step, StepKind::IrqEntry); + if matches!(step, StepKind::Dma { channel: 0 }) { + dma += 1; + } + } + assert_eq!(m.cpu().pc(), pc); + assert_eq!(dma, 1); + assert_eq!(m.memory().read32(SOURCE + 4).unwrap(), 0xa55a_1234); + assert_eq!(m.memory().read16(IF).unwrap() & 8, 8); + assert_eq!(m.memory().read16(IME).unwrap(), 1); + assert_eq!(m.memory().read16(IRQ_FLAGS).unwrap(), 0); + for index in 0..16 { + assert_eq!(m.memory().read32(DEST + index * 4).unwrap(), 0x12); + } +}