diff --git a/README.md b/README.md index 3b4187d..8901009 100644 --- a/README.md +++ b/README.md @@ -253,7 +253,7 @@ Verified locally on macOS 15.7.3: - Nix system: `aarch64-darwin`. - Rust host: `aarch64-apple-darwin`. - Release executable: native Mach-O `arm64`, without Rosetta. -- All 618 tests pass in debug and release builds. +- All 635 tests pass in debug and release builds. - All ten native window smoke tests pass. The mosaic test submits 128 frames; the other nine submit 60 frames each. - The eight CPU-driven smoke tests check CPU state, every output pixel, and presentation at VBlank entry. - The terminal-only CPU and timer IRQ demos run successfully. @@ -358,7 +358,7 @@ nix develop -c cargo test - Game Pak wait-state control (`WAITCNT`) for all three ROM windows. - Sequential/non-sequential data accesses, branch refill costs, and variable multiply timing. - Optional caller-supplied 16 KiB BIOS mapping for vector code, without CPU BIOS read protection. -- Optional original BIOS replacement: minimal boot, IRQ dispatch, interrupt waits, memory copy/fill, integer arithmetic, bit unpacking, and LZ77/run-length decompression. +- Optional original BIOS replacement: minimal boot, IRQ dispatch, interrupt waits, memory copy/fill, integer arithmetic, bit unpacking, LZ77/run-length decompression, and differential filters. - All 16 ARM data-processing operations, with immediate and shifted-register operands. - Logical operations: `AND`, `EOR`, `TST`, `TEQ`, `ORR`, `MOV`, `BIC`, and `MVN`. - Arithmetic operations: `SUB`, `RSB`, `ADD`, `ADC`, `SBC`, `RSC`, `CMP`, and `CMN`. @@ -516,13 +516,16 @@ Supported software interrupt services: | `0x12` | LZ77UnCompVram | Decompress from r0 to r1 with buffered halfword writes | | `0x14` | RLUnCompWram | Expand run-length data from r0 to r1 with byte writes | | `0x15` | RLUnCompVram | Expand run-length data from r0 to r1 with buffered halfword writes | +| `0x16` | Diff8bitUnFilterWrite8bit | Reconstruct 8-bit samples from r0 to r1 with byte writes | +| `0x17` | Diff8bitUnFilterWrite16bit | Reconstruct 8-bit samples from r0 to r1 with buffered halfword writes | +| `0x18` | Diff16bitUnFilter | Reconstruct 16-bit samples from r0 to r1 with halfword writes | ARM code uses `SWI service_number << 16`; Thumb code uses the service number directly. Only User/System callers are supported. Calls from exception modes return a diagnostic rather than risking nested stack corruption. Services preserve CPSR and every caller register except the documented arithmetic outputs. This is deterministic behavior, not a claim about undocumented firmware outputs. Supervisor stack use is 28 bytes normally and 40 bytes for division. -CpuFastSet, BitUnPack, and both decompression formats use 60 bytes. +CpuFastSet, BitUnPack, both decompression formats, and differential filters use 60 bytes. They do not reproduce the original firmware's internal stack layout or cycle counts. **IRQ callback contract:** install a word-aligned ARM callback address at `0x03007ffc` after boot. @@ -649,13 +652,42 @@ The service preserves caller registers/status and uses original ARM instructions CPU IRQ delivery stays masked until return. IME is unchanged; timers, display clocks, and DMA continue. Exact firmware timing and undocumented register outputs remain unmodeled. +#### Differential filters + +These services reconstruct samples from stored differences. They do not change the payload size. +The source at r0 starts with a word-aligned four-byte header: + +- Low byte `0x81` selects 8-bit samples; `0x82` selects 16-bit samples. +- The upper 24 bits specify output length in bytes, including for the 16-bit service. +- The first payload unit is the first original sample. Each following unit is the difference from the previous sample. + +The accumulator starts at zero. Each input unit adds to the accumulator, producing the next output sample. +Eight-bit samples wrap modulo 256; sixteen-bit samples wrap modulo 65,536. This handles positive and negative differences. +Sixteen-bit input and output units use little-endian byte order. + +`Diff8bitUnFilterWrite8bit` accepts byte-aligned destinations and odd byte counts. It uses ordinary byte stores. +`Diff8bitUnFilterWrite16bit` buffers pairs of reconstructed bytes, then writes halfwords. +`Diff16bitUnFilter` reads and writes halfwords directly. Both halfword-output variants require even destination addresses and byte counts. +Use separate source and output buffers. Overlapping buffers and firmware quirks for odd halfword-output sizes are not supported. + +Invalid header types/unit sizes, source alignment, protected sources, output alignment/size, and wrapping address ranges reach `bios::INVALID_ARGUMENT_TRAP`. +Sources below `0x02000000` are rejected. These strict checks are development diagnostics, not exact firmware behavior on invalid data. +After validation, zero output length reads no payload and writes no output. Decoding stops at the declared length without consuming trailing data. +Unmapped source reads and invalid output writes return normal memory diagnostics. +Earlier completed writes remain committed. The buffered byte variant does not write a lone pending byte after a source failure. +All output uses normal bus rules: byte writes to video memory can duplicate bytes, and byte writes to OAM are ignored. + +These original ARM routines execute through the CPU, not a host-side service hook. +They preserve caller registers/status and mask CPU IRQ delivery until return, without changing IME. +Timers, display clocks, and DMA continue. Exact Nintendo BIOS timing and undocumented side effects remain unmodeled. + #### Firmware diagnostics Unsupported SWIs, unsupported exception vectors, and null/misaligned IRQ callbacks reach an intentional undefined-instruction trap. The CPU reports `CpuError::UnsupportedInstruction` with `bios::UNSUPPORTED_TRAP`, rather than silently treating a service as a no-op. Invalid arithmetic/decompression arguments use the same CPU error type with the distinct `bios::INVALID_ARGUMENT_TRAP` instruction. Prior boot/service steps remain committed on failure. -Trigonometry, Huffman decompression, differential filters, reset services, and STOP remain unimplemented. +Trigonometry, Huffman decompression, reset services, and STOP remain unimplemented. This subset is not sufficient for Pokémon Emerald compatibility. ### Timers, interrupt registers, and the machine clock @@ -1410,7 +1442,8 @@ Important timing limits: | `src/bios/lz77.rs` | Emitted ARM LZ77 decoder with byte and halfword output | | `src/bios/run_length.rs` | Emitted ARM run-length decoder, header validation, and block bounds | | `src/bios/bit_unpack.rs` | Emitted ARM packed-unit expansion, offsets, word stores, and validation | -| `src/bios/decompression.rs` | Shared emitted byte/halfword output routine for both decompression formats | +| `src/bios/decompression.rs` | Shared emitted byte/halfword output routine for decompression and byte differential filters | +| `src/bios/differential.rs` | Emitted ARM byte/halfword differential filters, modular accumulation, and validation | | `src/video.rs` | Presentation buffer, RGB555 conversion, and Mode 0–5 composition | | `src/video/affine.rs` | Programmed registers, internal scanline origins, and tiled/bitmap sampling | | `src/video/windows.rs` | Persistent vertical window edge flags and constant-time clock advancement | @@ -1466,6 +1499,7 @@ Important timing limits: | `tests/bios/lz77.rs` | Token-reference tests, overlaps, output widths, malformed streams, and partial failures | | `tests/bios/run_length.rs` | All block controls, mixed streams, output widths, status, diagnostics, DMA, and IRQ masking | | `tests/bios/bit_unpack.rs` | Width pairs, bit references, offsets, maximum length, memory boundaries, status, DMA, and diagnostics | +| `tests/bios/differential.rs` | Round trips, wraparound, output widths, large lengths, partial failures, status, DMA, and diagnostics | ## Deliberate limits @@ -1533,12 +1567,15 @@ To test the core without building the window dependency: direnv exec . cargo test --locked --no-default-features ``` -This runs 607 core and integration tests; the eleven desktop and command-line tests are excluded. +This runs 624 core and integration tests; the eleven desktop and command-line tests are excluded. On Apple Silicon, `file` must report a Mach-O `arm64` executable. Do not set a Linux cross-compilation target for this validation. -The default suite has 618 tests. +The default suite has 635 tests. +Differential-filter tests round-trip original samples through an independent difference encoder, including all byte and halfword sample values. +They cover seeded inputs, 24-bit byte lengths, full-register accumulation wrap, output widths, source regions, and caller status. +Malformed headers, truncated inputs, partial output, video-bus behavior, DMA progress, and masked IRQ delivery have separate regression tests. BitUnPack tests compare all supported width pairs and byte values with an independent bit-by-bit reference. They cover offsets and the zero-data flag, seeded inputs, maximum source length, source alignment, and ROM descriptors. Other tests check output boundaries, video-memory word writes, malformed/truncated inputs, partial failures, caller status, and DMA/IRQ behavior. @@ -1660,7 +1697,7 @@ These tests do not replace validation against GBA hardware or public hardware te 1. Validate CPU behavior with public ARM7TDMI test programs before claiming instruction compatibility. 2. Refine nominal timing with a fetch pipeline, Game Pak prefetch, per-access device updates, and verified timer/IRQ delays. -3. Expand BIOS services with trigonometry, Huffman decompression, and differential filters; add STOP and remaining DMA device modes. +3. Expand BIOS services with trigonometry and Huffman decompression; add STOP and remaining DMA device modes. 4. Replace nominal sprite work limits with verified individual fetch timing; add background fetch timing and per-pixel composition. 5. Expand graphics, audio, cartridge loading, and saves before testing Emerald compatibility. @@ -1675,7 +1712,7 @@ Hardware references used for the core: - [GBATEK BIOS function calling conventions](https://problemkaputt.de/gbatek-bios-functions.htm). - [GBATEK BIOS memory-copy services](https://problemkaputt.de/gbatek-bios-memory-copy.htm). - [GBATEK BIOS arithmetic services](https://problemkaputt.de/gbatek-bios-arithmetic-functions.htm). -- [GBATEK BIOS decompression services](https://problemkaputt.de/gbatek-bios-decompression-functions.htm), including BitUnPack descriptor fields and width constraints. +- [GBATEK BIOS decompression services](https://problemkaputt.de/gbatek-bios-decompression-functions.htm), including BitUnPack descriptor fields, differential-filter headers, and width constraints. - [mGBA BIOS service implementation](https://github.com/mgba-emu/mgba/blob/master/src/gba/bios.c), reviewed for BitUnPack low-bit-first order and zero-offset selection. - [HALTCNT hardware-access tests](https://github.com/mgba-emu/mgba/issues/2309), for BIOS-only writes and halfword access. - [GBATEK display status and IRQs](https://problemkaputt.de/gbatek-lcd-i-o-interrupts-and-status.htm). diff --git a/src/bios.rs b/src/bios.rs index d014017..d6501da 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -1,6 +1,6 @@ //! Original, optional ARM BIOS replacement. No Nintendo firmware is included. //! Supports interrupt waits, integer arithmetic, memory copy/fill, bit unpacking, -//! and LZ77/run-length decoding. +//! LZ77/run-length decoding, and differential filters. //! This is a functional subset, not a complete boot ROM or a timing-compatible BIOS. use std::collections::BTreeMap; @@ -8,6 +8,7 @@ use std::collections::BTreeMap; mod arithmetic; mod bit_unpack; mod decompression; +mod differential; mod lz77; mod run_length; @@ -25,7 +26,7 @@ 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 separate diagnostic trap for division by zero or invalid unpacking/decompression arguments. +/// A diagnostic trap for division by zero or invalid unpacking, decompression, or filter arguments. pub const INVALID_ARGUMENT_TRAP: u32 = 0xe7f0_00f1; /// Start at reset with our optional firmware. Call Machine::step to execute boot. @@ -126,6 +127,9 @@ pub fn image() -> Vec { (0x12, "lz_vram"), (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); @@ -221,6 +225,7 @@ pub fn image() -> Vec { bit_unpack::emit(&mut a); lz77::emit(&mut a); run_length::emit(&mut a); + differential::emit(&mut a); decompression::emit_writer(&mut a); a.label("return"); diff --git a/src/bios/decompression.rs b/src/bios/decompression.rs index 1346f85..bd64eac 100644 --- a/src/bios/decompression.rs +++ b/src/bios/decompression.rs @@ -1,4 +1,4 @@ -//! Shared output routine for the original ARM decompression services. +//! Shared output routine for original ARM decompression and differential filters. use super::ArmImage; diff --git a/src/bios/differential.rs b/src/bios/differential.rs new file mode 100644 index 0000000..e7b7c2d --- /dev/null +++ b/src/bios/differential.rs @@ -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"); +} diff --git a/tests/bios.rs b/tests/bios.rs index f5abc41..5bed385 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/bit_unpack.rs"] mod bit_unpack; +#[path = "bios/differential.rs"] +mod differential; #[path = "bios/lz77.rs"] mod lz77; #[path = "bios/run_length.rs"] diff --git a/tests/bios/differential.rs b/tests/bios/differential.rs new file mode 100644 index 0000000..a27f14c --- /dev/null +++ b/tests/bios/differential.rs @@ -0,0 +1,411 @@ +use super::*; +use gba_rust::{bios::INVALID_ARGUMENT_TRAP, dma::DMA_BASE, memory::OAM_START}; + +fn filtered(service: u8, samples: &[u16]) -> (Vec, Vec) { + let halfword = service == 0x18; + let mask = if halfword { 65535i64 } else { 255 }; + let length = samples.len() * if halfword { 2 } else { 1 }; + let mut source = ((length as u32) << 8 | if halfword { 0x82 } else { 0x81 }) + .to_le_bytes() + .to_vec(); + let mut expected = Vec::new(); + let mut previous = 0i64; + // Encode differences from original samples, rather than duplicating decoder accumulation. + for &sample in samples { + assert!(i64::from(sample) <= mask); + let difference = ((i64::from(sample) - previous) & mask) as u16; + if halfword { + source.extend(difference.to_le_bytes()); + expected.extend(sample.to_le_bytes()); + } else { + source.push(difference as u8); + expected.push(sample as u8); + } + previous = i64::from(sample); + } + (source, expected) +} + +fn prepare(service: u8, thumb: bool, dest: u32, data: &[u8]) -> (Machine, u32) { + call_status_data(service, thumb, [ROM_DATA, dest, 0x55aa_1234], 0x1f, data) +} + +fn complete(m: &mut Machine, pc: u32) { + m.step().unwrap(); + reach(m, pc, 4_000_000); +} + +fn verify(service: u8, thumb: bool, dest: u32, data: &[u8], expected: &[u8]) { + let (mut m, pc) = prepare(service, thumb, dest, data); + let before = m.cpu().clone(); + let end = dest + expected.len() as u32; + for address in [(dest - 1) & !1, end & !1] { + m.memory_mut().write16(address, 0xa55a).unwrap(); + } + m.memory_mut() + .write32(bios::IRQ_STACK, 0x5a5a_a5a5) + .unwrap(); + complete(&mut m, pc); + for (index, byte) in expected.iter().enumerate() { + assert_eq!( + m.memory().read8(dest + index as u32).unwrap(), + *byte, + "service={service:x}, byte={index}" + ); + } + for address in [dest - 1, end] { + assert_eq!( + m.memory().read8(address).unwrap(), + if address & 1 == 0 { 0x5a } else { 0xa5 } + ); + } + assert_eq!(m.memory().read32(bios::IRQ_STACK).unwrap(), 0x5a5a_a5a5); + assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]); + assert_eq!(m.cpu().cpsr(), before.cpsr()); +} + +fn fail(m: &mut Machine) -> MachineError { + for _ in 0..2000 { + let before = m.cpu().clone(); + let cycles = m.cycles(); + let timing = m.last_timing(); + if let Err(error) = m.step() { + assert_eq!(m.cpu(), &before); + assert_eq!(m.cycles(), cycles); + assert_eq!(m.last_timing(), timing); + assert_eq!(m.step(), Err(error.clone())); + return error; + } + } + panic!("invalid filter did not produce a bounded diagnostic"); +} + +fn assert_invalid(error: MachineError) { + assert!(matches!( + error, + MachineError::Cpu(CpuError::UnsupportedInstruction { + instruction: INVALID_ARGUMENT_TRAP, + .. + }) + )); +} + +#[test] +fn first_unit_is_original_and_following_units_are_deltas() { + for service in [0x16, 0x17, 0x18] { + let (data, expected) = filtered(service, &[10, 11, 12, 13, 14, 15]); + if service != 0x18 { + assert_eq!(&data[4..], &[10, 1, 1, 1, 1, 1]); + } + for thumb in [false, true] { + verify( + service, + thumb, + if service == 0x16 { + DEST + } else { + VRAM_START + 4 + }, + &data, + &expected, + ); + } + } +} + +#[test] +fn byte_filters_cover_every_value_and_wrap_in_both_directions() { + let mut samples: Vec<_> = (0..=255u16).collect(); + samples.extend((0..=255u16).rev()); + samples.extend([255, 0, 255, 128, 0, 127, 0, 0]); + for service in [0x16, 0x17] { + let (data, expected) = filtered(service, &samples); + for thumb in [false, true] { + verify( + service, + thumb, + if service == 0x16 { + DEST + 1 + } else { + VRAM_START + 4 + }, + &data, + &expected, + ); + } + } +} + +#[test] +fn halfword_filter_covers_every_sample_and_uses_byte_length() { + let samples: Vec<_> = (0..=65535u32) + .map(|i| i.wrapping_mul(40503) as u16) + .collect(); + let (data, expected) = filtered(0x18, &samples); + assert_eq!(&data[..4], &[0x82, 0, 0, 2]); // 131072 bytes, not 65536 units. + verify(0x18, true, DEST, &data, &expected); +} + +#[test] +fn modular_accumulation_survives_full_register_wrap_and_large_byte_lengths() { + for (service, length) in [(0x16, 65536usize), (0x17, 65536), (0x18, 65538 * 2)] { + let mut data = ((length as u32) << 8 | if service == 0x18 { 0x82 } else { 0x81 }) + .to_le_bytes() + .to_vec(); + data.extend(std::iter::repeat_n(255, length)); + let expected: Vec = if service == 0x18 { + (1..=length / 2) + .flat_map(|i| (((i as u64 * 65535) % 65536) as u16).to_le_bytes()) + .collect() + } else { + (1..=length) + .map(|i| ((i as u64 * 255) % 256) as u8) + .collect() + }; + verify(service, false, DEST, &data, &expected); + } +} + +#[test] +fn seeded_original_samples_round_trip_through_all_filters() { + let mut seed = 0x415c_a927u32; + for case in 0..64 { + for service in [0x16, 0x17, 0x18] { + let samples: Vec<_> = (0..(case + 1) * 2) + .map(|_| { + seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); + (seed >> 16) as u16 & if service == 0x18 { 65535 } else { 255 } + }) + .collect(); + let (data, expected) = filtered(service, &samples); + verify(service, case % 2 == 0, DEST, &data, &expected); + } + } +} + +#[test] +fn smallest_outputs_and_odd_byte_output_use_exact_boundaries() { + for samples in [vec![0x12], vec![0x12, 0x34, 0x56]] { + let (data, expected) = filtered(0x16, &samples); + verify(0x16, true, DEST + 1, &data, &expected); + } + let (data, expected) = filtered(0x17, &[0x12, 0x34]); + verify(0x17, false, DEST, &data, &expected); + let (data, expected) = filtered(0x18, &[0xabcd]); + verify(0x18, true, VRAM_START + 4, &data, &expected); +} + +#[test] +fn sources_can_be_read_from_either_work_ram_region() { + for service in [0x16, 0x17, 0x18] { + let (data, expected) = filtered(service, &[0, 255, 128, 1]); + for source in [SOURCE, 0x0300_0100] { + let (mut m, pc) = call(service, false, [source, DEST, 0]); + for (index, byte) in data.iter().enumerate() { + m.memory_mut().write8(source + index as u32, *byte).unwrap(); + } + complete(&mut m, pc); + for (index, byte) in expected.iter().enumerate() { + assert_eq!(m.memory().read8(DEST + index as u32).unwrap(), *byte); + } + } + } +} + +#[test] +fn caller_status_masks_and_registers_are_restored() { + for service in [0x16, 0x17, 0x18] { + let (data, _) = filtered(service, &[0x12, 0x34, 0xff, 0]); + for thumb in [false, true] { + for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] { + let (mut m, pc) = + call_status_data(service, thumb, [ROM_DATA, DEST, 0xa55a_1234], status, &data); + let before = m.cpu().clone(); + complete(&mut m, pc); + assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]); + assert_eq!(m.cpu().cpsr(), before.cpsr()); + } + } + } +} + +#[test] +fn zero_length_reads_no_payload_and_does_not_access_destination() { + for service in [0x16, 0x17, 0x18] { + let (data, _) = filtered(service, &[]); + let (mut m, pc) = prepare(service, false, 0x0e00_0000, &data); + complete(&mut m, pc); + } +} + +#[test] +fn all_incorrect_type_or_unit_headers_are_rejected_before_output() { + for service in [0x16, 0x17, 0x18] { + for header in 0u8..=255 { + if header == if service == 0x18 { 0x82 } else { 0x81 } { + continue; + } + let (mut m, _) = prepare(service, false, DEST, &[header, 4, 0, 0]); + assert_invalid(fail(&mut m)); + assert_eq!(m.memory().read32(DEST).unwrap(), 0); + } + } +} + +#[test] +fn protected_sources_alignment_odd_lengths_and_output_wrap_are_diagnostics() { + for service in [0x16, 0x17, 0x18] { + let (data, _) = filtered(service, &[0x12, 0x34]); + for source in [ + 0, + 0x3ffc, + 0x0100_0000, + ROM_DATA + 1, + ROM_DATA + 2, + ROM_DATA + 3, + ] { + let (mut m, _) = call_status_data(service, false, [source, DEST, 0], 0x1f, &data); + assert_invalid(fail(&mut m)); + } + let (mut m, _) = prepare(service, false, 0xffff_fffe, &data); + assert_invalid(fail(&mut m)); + if service == 0x16 { + continue; + } + let (mut m, _) = prepare(service, false, DEST + 1, &data); + assert_invalid(fail(&mut m)); + let mut odd = data.clone(); + odd[1] = 3; + let (mut m, _) = prepare(service, true, DEST, &odd); + assert_invalid(fail(&mut m)); + assert_eq!(m.memory().read32(DEST).unwrap(), 0); + } +} + +#[test] +fn every_truncated_source_prefix_returns_a_memory_diagnostic() { + for service in [0x16, 0x17, 0x18] { + let (data, _) = filtered(service, &[0x12, 0x34, 0x56, 0x78]); + for length in 0..data.len() { + let (mut m, _) = prepare(service, false, DEST, &data[..length]); + assert_eq!( + fail(&mut m), + MachineError::Cpu(MemoryError::Unmapped(ROM_DATA + length as u32).into()), + "service={service:x}, length={length}" + ); + } + } +} + +#[test] +fn failed_source_reads_keep_completed_units_but_not_pending_halfwords() { + for service in [0x16, 0x17, 0x18] { + let (mut data, _) = filtered(service, &[0x12, 0x34, 0x56, 0x78]); + // Three byte samples or one complete halfword plus one byte are available. + data.truncate(7); + let (mut m, _) = prepare(service, false, DEST, &data); + m.memory_mut().write32(DEST, 0xcccc_cccc).unwrap(); + assert!(matches!( + fail(&mut m), + MachineError::Cpu(CpuError::Memory(_)) + )); + assert_eq!( + m.memory().read32(DEST).unwrap(), + match service { + 0x16 => 0xcc56_3412, + 0x17 => 0xcccc_3412, + _ => 0xcccc_0012, + } + ); + } +} + +#[test] +fn destination_failure_preserves_preceding_completed_halfword() { + for service in [0x17, 0x18] { + let (data, _) = filtered(service, &[0x12, 0x34, 0x56, 0x78]); + let (mut m, _) = prepare(service, false, 0x07ff_fffe, &data); + assert_eq!( + fail(&mut m), + MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into()) + ); + assert_eq!( + m.memory().read16(0x07ff_fffe).unwrap(), + if service == 0x17 { 0x3412 } else { 0x12 } + ); + } +} + +#[test] +fn output_uses_normal_byte_and_halfword_video_bus_rules() { + for service in [0x16, 0x17, 0x18] { + let (data, _) = filtered( + service, + if service == 0x18 { + &[0x3412] + } else { + &[0x12, 0x34] + }, + ); + for dest in [VRAM_START, OAM_START] { + let (mut m, pc) = prepare(service, false, dest, &data); + m.memory_mut().write16(dest, 0xcccc).unwrap(); + complete(&mut m, pc); + assert_eq!( + m.memory().read16(dest).unwrap(), + if service != 0x16 { + 0x3412 + } else if dest == OAM_START { + 0xcccc + } else { + 0x3434 + } + ); + } + } +} + +#[test] +fn declared_end_does_not_consume_trailing_payload() { + for service in [0x16, 0x17, 0x18] { + let (mut data, expected) = filtered(service, &[0x12, 0x34]); + data.extend([0xff; 8]); + verify(service, false, DEST, &data, &expected); + } +} + +#[test] +fn dma_and_timers_continue_while_filter_masks_cpu_irq_delivery() { + for service in [0x16, 0x17, 0x18] { + let (data, expected) = filtered(service, &[0x55; 32]); + let (mut m, pc) = prepare(service, false, DEST, &data); + m.step().unwrap(); // Enter SWI before requesting IRQs. + m.memory_mut().write16(IE, 8).unwrap(); + m.memory_mut().write16(IME, 1).unwrap(); + m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap(); + m.memory_mut().write32(SOURCE, 0x8765_4321).unwrap(); + m.memory_mut().write32(DMA_BASE, SOURCE).unwrap(); + m.memory_mut().write32(DMA_BASE + 4, SOURCE + 4).unwrap(); + m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap(); + let mut dma = 0; + for _ in 0..5000 { + if m.cpu().pc() == pc { + break; + } + let step = m.step().unwrap(); + assert_ne!(step, StepKind::IrqEntry); + if matches!(step, StepKind::Dma { channel: 0 }) { + dma += 1; + } + } + assert_eq!(m.cpu().pc(), pc); + assert_eq!(dma, 1); + assert_eq!(m.memory().read32(SOURCE + 4).unwrap(), 0x8765_4321); + assert_eq!(m.memory().read16(IF).unwrap() & 8, 8); + assert_eq!(m.memory().read16(IME).unwrap(), 1); + assert_eq!(m.memory().read16(IRQ_FLAGS).unwrap(), 0); + for (index, byte) in expected.iter().enumerate() { + assert_eq!(m.memory().read8(DEST + index as u32).unwrap(), *byte); + } + } +}