From 5984551784523e9f16f40b05a02d4896e5f25b7a Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Mon, 5 Oct 2026 12:55:35 +0700 Subject: [PATCH] feat(bios): decode run-length streams through ARM instructions Add RLUnCompWram and RLUnCompVram with block bounds, word-independent buffering, and device-progress tests. --- src/bios.rs | 5 + src/bios/run_length.rs | 64 +++++++ tests/bios.rs | 2 + tests/bios/run_length.rs | 395 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 466 insertions(+) create mode 100644 src/bios/run_length.rs create mode 100644 tests/bios/run_length.rs diff --git a/src/bios.rs b/src/bios.rs index 34e0e99..0fe2768 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -2,6 +2,7 @@ //! Supports interrupt waits and memory copy/fill. //! Includes integer arithmetic services. //! Includes LZ77 decoding. +//! Includes run-length decoding. //! This is a functional subset, not a complete boot ROM or a timing-compatible BIOS. use std::collections::BTreeMap; @@ -9,6 +10,7 @@ use std::collections::BTreeMap; mod arithmetic; mod decompression; mod lz77; +mod run_length; use crate::{ cpu::Cpu, @@ -122,6 +124,8 @@ pub fn image() -> Vec { (0x0c, "cpu_fast_set"), (0x11, "lz_wram"), (0x12, "lz_vram"), + (0x14, "rl_wram"), + (0x15, "rl_vram"), ] { a.emit(0xe35c_0000 | number); // CMP r12,#service a.branch(0, target); @@ -215,6 +219,7 @@ pub fn image() -> Vec { arithmetic::emit(&mut a); lz77::emit(&mut a); + run_length::emit(&mut a); decompression::emit_writer(&mut a); a.label("return"); diff --git a/src/bios/run_length.rs b/src/bios/run_length.rs new file mode 100644 index 0000000..ae7d5d3 --- /dev/null +++ b/src/bios/run_length.rs @@ -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"); +} diff --git a/tests/bios.rs b/tests/bios.rs index ced6d45..0f6dbbd 100644 --- a/tests/bios.rs +++ b/tests/bios.rs @@ -16,6 +16,8 @@ const ROM_DATA: u32 = ROM_START + 13 * 4; mod arithmetic; #[path = "bios/lz77.rs"] mod lz77; +#[path = "bios/run_length.rs"] +mod run_length; fn words(code: &[u32]) -> Vec { code.iter().flat_map(|word| word.to_le_bytes()).collect() diff --git a/tests/bios/run_length.rs b/tests/bios/run_length.rs new file mode 100644 index 0000000..0bc90de --- /dev/null +++ b/tests/bios/run_length.rs @@ -0,0 +1,395 @@ +use super::*; +use gba_rust::{bios::INVALID_ARGUMENT_TRAP, dma::DMA_BASE, memory::OAM_START}; + +#[derive(Clone)] +enum Block { + Literal(Vec), + Repeat(u8, usize), +} + +/// Encode structured blocks and independently expand their expected bytes. +fn stream(blocks: &[Block]) -> (Vec, Vec) { + let mut encoded = vec![0x30, 0, 0, 0]; + let mut expected = Vec::new(); + for block in blocks { + match block { + Block::Literal(bytes) => { + assert!((1..=128).contains(&bytes.len())); + encoded.push((bytes.len() - 1) as u8); + encoded.extend(bytes); + expected.extend(bytes); + } + Block::Repeat(byte, count) => { + assert!((3..=130).contains(count)); + encoded.extend([0x80 | (*count - 3) as u8, *byte]); + expected.extend(std::iter::repeat_n(*byte, *count)); + } + } + } + encoded[..4].copy_from_slice(&((expected.len() as u32) << 8 | 0x30).to_le_bytes()); + (encoded, expected) +} + +fn prepare(service: u8, thumb: bool, destination: u32, data: &[u8]) -> (Machine, u32) { + call_status_data( + service, + thumb, + [ROM_DATA, destination, 0xa55a_1234], + 0x1f, + data, + ) +} + +fn complete(m: &mut Machine, return_pc: u32) { + m.step().unwrap(); + reach(m, return_pc, 2_000_000); +} + +fn verify(service: u8, thumb: bool, destination: u32, data: &[u8], expected: &[u8]) { + let (mut m, return_pc) = prepare(service, thumb, destination, data); + let before = m.cpu().clone(); + let end = destination + expected.len() as u32; + // Halfword sentinels support both RAM and video memory. + for address in [(destination - 1) & !1, end & !1] { + m.memory_mut().write16(address, 0xa55a).unwrap(); + } + // The 60-byte SVC frame must not touch the IRQ stack boundary at SVC_SP-64. + m.memory_mut() + .write32(bios::IRQ_STACK, 0x5a5a_a5a5) + .unwrap(); + complete(&mut m, return_pc); + for (index, byte) in expected.iter().enumerate() { + assert_eq!( + m.memory().read8(destination + index as u32).unwrap(), + *byte, + "byte={index}" + ); + } + for address in [destination - 1, end] { + assert_eq!( + m.memory().read8(address).unwrap(), + if address & 1 == 0 { 0x5a } else { 0xa5 } + ); + } + assert_eq!(m.memory().read32(bios::IRQ_STACK).unwrap(), 0x5a5a_a5a5); + assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]); + assert_eq!(m.cpu().cpsr(), before.cpsr()); +} + +fn fail(m: &mut Machine) -> MachineError { + for _ in 0..2000 { + let before = m.cpu().clone(); + let cycles = m.cycles(); + let timing = m.last_timing(); + if let Err(error) = m.step() { + assert_eq!(m.cpu(), &before); + assert_eq!(m.cycles(), cycles); + assert_eq!(m.last_timing(), timing); + assert_eq!(m.step(), Err(error.clone())); + return error; + } + } + panic!("invalid stream did not reach a bounded diagnostic"); +} + +fn assert_invalid(error: MachineError) { + assert!(matches!( + error, + MachineError::Cpu(CpuError::UnsupportedInstruction { + instruction: INVALID_ARGUMENT_TRAP, + .. + }) + )); +} + +#[test] +fn every_block_control_value_matches_reference_in_arm_and_thumb() { + for flag in 0u8..=255 { + let count = usize::from(flag & 127) + if flag & 128 == 0 { 1 } else { 3 }; + let block = if flag & 128 == 0 { + Block::Literal((0..count).map(|i| (i * 37) as u8).collect()) + } else { + Block::Repeat(flag, count) + }; + let mut blocks = vec![block]; + // The final literal can complete a halfword started by the prior block. + if count % 2 != 0 { + blocks.push(Block::Literal(vec![0x42])); + } + let (data, expected) = stream(&blocks); + assert_eq!(data[4], flag); + for thumb in [false, true] { + verify(0x14, thumb, DEST + 1, &data, &expected); + verify(0x15, thumb, VRAM_START + 4, &data, &expected); + } + } +} + +#[test] +fn repeated_values_cover_all_bytes_and_cross_halfword_boundaries() { + let blocks: Vec<_> = (0..=255).map(|byte| Block::Repeat(byte, 3)).collect(); + let (data, expected) = stream(&blocks); + verify(0x14, false, DEST, &data, &expected); + verify(0x15, true, DEST, &data, &expected); + verify(0x15, false, VRAM_START + 4, &data, &expected); +} + +#[test] +fn seeded_mixed_streams_match_independent_block_expansion() { + let mut seed = 0x83ca_206bu32; + for case in 0..64 { + let mut blocks = Vec::new(); + let mut length = 0; + for _ in 0..32 { + seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); + let repeat = seed & 128 != 0; + let count = ((seed >> 8) & 127) as usize + if repeat { 3 } else { 1 }; + blocks.push(if repeat { + Block::Repeat((seed >> 24) as u8, count) + } else { + Block::Literal( + (0..count) + .map(|i| (seed.wrapping_add(i as u32 * 71) >> 3) as u8) + .collect(), + ) + }); + length += count; + } + if length % 2 != 0 { + blocks.push(Block::Literal(vec![0xab])); + } + let (data, expected) = stream(&blocks); + verify(0x14, case % 2 == 0, DEST, &data, &expected); + verify(0x15, case % 2 != 0, VRAM_START + 4, &data, &expected); + } +} + +#[test] +fn output_length_uses_all_twenty_four_header_bits() { + let mut blocks = vec![Block::Repeat(0x5a, 130); 504]; + blocks.push(Block::Repeat(0xa5, 16)); // 65,536 bytes, crossing the 16-bit count boundary. + let (data, expected) = stream(&blocks); + assert_eq!(expected.len(), 65536); + assert_eq!(&data[..4], &[0x30, 0, 0, 1]); + verify(0x14, false, DEST, &data, &expected); + verify(0x15, true, DEST, &data, &expected); +} + +#[test] +fn source_can_be_in_either_work_ram_region() { + let (data, expected) = stream(&[Block::Literal(vec![1, 2, 3]), Block::Repeat(0xaa, 3)]); + for source in [SOURCE, 0x0300_0100] { + for service in [0x14, 0x15] { + let (mut m, return_pc) = call(service, false, [source, DEST, 0]); + for (index, byte) in data.iter().enumerate() { + m.memory_mut().write8(source + index as u32, *byte).unwrap(); + } + complete(&mut m, return_pc); + for (index, byte) in expected.iter().enumerate() { + assert_eq!(m.memory().read8(DEST + index as u32).unwrap(), *byte); + } + } + } +} + +#[test] +fn user_system_status_and_all_caller_registers_are_restored() { + let (data, _) = stream(&[Block::Repeat(0x88, 3), Block::Literal(vec![0x12])]); + for service in [0x14, 0x15] { + for thumb in [false, true] { + for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] { + let (mut m, return_pc) = + call_status_data(service, thumb, [ROM_DATA, DEST, 0x12345678], status, &data); + let before = m.cpu().clone(); + complete(&mut m, return_pc); + assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]); + assert_eq!(m.cpu().cpsr(), before.cpsr()); + } + } + } +} + +#[test] +fn zero_length_reads_no_blocks_and_does_not_access_destination() { + for service in [0x14, 0x15] { + let (mut m, return_pc) = prepare(service, false, 0x0e00_0000, &[0x30, 0, 0, 0]); + complete(&mut m, return_pc); + } +} + +#[test] +fn bad_type_source_alignment_protection_and_output_wrap_are_diagnostics() { + for service in [0x14, 0x15] { + for header in [0u32, 0x0410, 0x0420, 0x0431, 0x04ff] { + let (mut m, _) = prepare(service, false, DEST, &header.to_le_bytes()); + assert_invalid(fail(&mut m)); + assert_eq!(m.memory().read32(DEST).unwrap(), 0); + } + for source in [ + 0, + 0x3ffc, + 0x0100_0000, + ROM_DATA + 1, + ROM_DATA + 2, + ROM_DATA + 3, + ] { + let (mut m, _) = + call_status_data(service, false, [source, DEST, 0], 0x1f, &[0x30, 4, 0, 0]); + assert_invalid(fail(&mut m)); + } + let (mut m, _) = prepare(service, false, 0xffff_fffe, &[0x30, 4, 0, 0]); + assert_invalid(fail(&mut m)); + } +} + +#[test] +fn halfword_variant_rejects_odd_size_or_address_before_writing() { + for (destination, data) in [ + (VRAM_START + 4, vec![0x30, 3, 0, 0, 0x80, 0x77]), + (VRAM_START + 5, vec![0x30, 4, 0, 0, 0x81, 0x77]), + (VRAM_START + 5, vec![0x30, 0, 0, 0]), + ] { + let (mut m, _) = prepare(0x15, true, destination, &data); + m.memory_mut().write32(VRAM_START + 4, 0xcccc_cccc).unwrap(); + assert_invalid(fail(&mut m)); + assert_eq!(m.memory().read32(VRAM_START + 4).unwrap(), 0xcccc_cccc); + } + let (data, expected) = stream(&[Block::Repeat(0x77, 3)]); + verify(0x14, true, DEST + 1, &data, &expected); +} + +#[test] +fn oversized_blocks_fail_before_payload_reads_or_writes() { + for service in [0x14, 0x15] { + for flag in [1, 0x7f, 0x80, 0xff] { + // One literal succeeds, then the next block exceeds one remaining byte. + // No payload follows the invalid block: its bounds check must fail first. + let data = [0x30, 2, 0, 0, 0, 0x12, flag]; + let (mut m, _) = prepare(service, false, DEST, &data); + m.memory_mut().write16(DEST, 0xcccc).unwrap(); + assert_invalid(fail(&mut m)); + assert_eq!( + m.memory().read16(DEST).unwrap(), + if service == 0x14 { 0xcc12 } else { 0xcccc } + ); + } + } +} + +#[test] +fn every_truncated_input_prefix_returns_a_memory_diagnostic() { + let (data, _) = stream(&[ + Block::Literal(vec![0x12, 0x34, 0x56]), + Block::Repeat(0x78, 3), + ]); + for service in [0x14, 0x15] { + for length in 0..data.len() { + let (mut m, _) = prepare(service, false, DEST, &data[..length]); + assert_eq!( + fail(&mut m), + MachineError::Cpu(MemoryError::Unmapped(ROM_DATA + length as u32).into()), + "service={service:x}, length={length}" + ); + } + } +} + +#[test] +fn incomplete_halfwords_are_not_committed_after_source_failure() { + for service in [0x14, 0x15] { + // Three of four literal bytes are available. One halfword can be committed. + let data = [0x30, 4, 0, 0, 3, 0x12, 0x34, 0x56]; + let (mut m, _) = prepare(service, false, DEST, &data); + m.memory_mut().write32(DEST, 0xcccc_cccc).unwrap(); + assert!(matches!( + fail(&mut m), + MachineError::Cpu(CpuError::Memory(_)) + )); + assert_eq!( + m.memory().read32(DEST).unwrap(), + if service == 0x14 { + 0xcc56_3412 + } else { + 0xcccc_3412 + } + ); + } +} + +#[test] +fn destination_failure_preserves_earlier_completed_writes() { + let (data, _) = stream(&[Block::Literal(vec![0x12, 0x34, 0x56, 0x78])]); + let (mut m, _) = prepare(0x15, false, 0x07ff_fffe, &data); + assert_eq!( + fail(&mut m), + MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into()) + ); + assert_eq!(m.memory().read16(0x07ff_fffe).unwrap(), 0x3412); +} + +#[test] +fn declared_end_ignores_padding_and_trailing_invalid_blocks() { + let (mut data, expected) = stream(&[Block::Repeat(0xab, 4)]); + data.extend([0x7f, 0xff, 0x80]); // Invalid if read as another block. + for service in [0x14, 0x15] { + verify(service, true, DEST, &data, &expected); + } +} + +#[test] +fn byte_and_halfword_variants_use_normal_video_bus_write_rules() { + let (data, _) = stream(&[Block::Literal(vec![0x12, 0x34])]); + for service in [0x14, 0x15] { + for destination in [VRAM_START, OAM_START] { + let (mut m, return_pc) = prepare(service, false, destination, &data); + m.memory_mut().write16(destination, 0xcccc).unwrap(); + complete(&mut m, return_pc); + let expected = if service == 0x15 { + 0x3412 + } else if destination == OAM_START { + 0xcccc + } else { + 0x3434 + }; + assert_eq!(m.memory().read16(destination).unwrap(), expected); + } + } +} + +#[test] +fn device_clocks_and_dma_continue_while_service_masks_irq_delivery() { + let (data, expected) = stream(&[Block::Repeat(0x55, 130)]); + for service in [0x14, 0x15] { + let (mut m, return_pc) = prepare(service, false, DEST, &data); + m.step().unwrap(); // Enter Supervisor mode before requesting any IRQ. + let start = m.cycles(); + m.memory_mut().write16(IE, 8).unwrap(); + m.memory_mut().write16(IME, 1).unwrap(); + m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap(); + m.memory_mut().write32(SOURCE, 0x8765_4321).unwrap(); + m.memory_mut().write32(DMA_BASE, SOURCE).unwrap(); + m.memory_mut().write32(DMA_BASE + 4, SOURCE + 4).unwrap(); + m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap(); + let mut dma = 0; + for _ in 0..5000 { + if m.cpu().pc() == return_pc { + break; + } + let step = m.step().unwrap(); + assert_ne!(step, StepKind::IrqEntry); + if matches!(step, StepKind::Dma { channel: 0 }) { + dma += 1; + } + } + assert_eq!(m.cpu().pc(), return_pc); + assert_eq!(dma, 1); + assert!(m.cycles() - start > 16); + assert_eq!(m.memory().read32(SOURCE + 4).unwrap(), 0x8765_4321); + assert_eq!(m.memory().read16(IF).unwrap() & 8, 8); + assert_eq!(m.memory().read16(IME).unwrap(), 1); + assert_eq!(m.memory().read16(IRQ_FLAGS).unwrap(), 0); + for (index, byte) in expected.iter().enumerate() { + assert_eq!(m.memory().read8(DEST + index as u32).unwrap(), *byte); + } + } +}