Add RLUnCompWram and RLUnCompVram with block bounds, word-independent buffering, and device-progress tests.
395 lines
14 KiB
Rust
395 lines
14 KiB
Rust
use super::*;
|
|
use gba_rust::{bios::INVALID_ARGUMENT_TRAP, dma::DMA_BASE, memory::OAM_START};
|
|
|
|
#[derive(Clone)]
|
|
enum Block {
|
|
Literal(Vec<u8>),
|
|
Repeat(u8, usize),
|
|
}
|
|
|
|
/// Encode structured blocks and independently expand their expected bytes.
|
|
fn stream(blocks: &[Block]) -> (Vec<u8>, Vec<u8>) {
|
|
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);
|
|
}
|
|
}
|
|
}
|