feat(bios): decode LZ77 streams through ARM instructions
Add byte and buffered-halfword output, overlapping references, argument diagnostics, and a reusable output routine.
This commit is contained in:
parent
096bcfe314
commit
17e9351371
5 changed files with 531 additions and 0 deletions
|
|
@ -1,11 +1,14 @@
|
|||
//! Original, optional ARM BIOS replacement. No Nintendo firmware is included.
|
||||
//! Supports interrupt waits and memory copy/fill.
|
||||
//! Includes integer arithmetic services.
|
||||
//! Includes LZ77 decoding.
|
||||
//! This is a functional subset, not a complete boot ROM or a timing-compatible BIOS.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
mod arithmetic;
|
||||
mod decompression;
|
||||
mod lz77;
|
||||
|
||||
use crate::{
|
||||
cpu::Cpu,
|
||||
|
|
@ -117,6 +120,8 @@ pub fn image() -> Vec<u8> {
|
|||
(8, "sqrt"),
|
||||
(0x0b, "cpu_set"),
|
||||
(0x0c, "cpu_fast_set"),
|
||||
(0x11, "lz_wram"),
|
||||
(0x12, "lz_vram"),
|
||||
] {
|
||||
a.emit(0xe35c_0000 | number); // CMP r12,#service
|
||||
a.branch(0, target);
|
||||
|
|
@ -209,6 +214,8 @@ pub fn image() -> Vec<u8> {
|
|||
);
|
||||
|
||||
arithmetic::emit(&mut a);
|
||||
lz77::emit(&mut a);
|
||||
decompression::emit_writer(&mut a);
|
||||
|
||||
a.label("return");
|
||||
a.emit(0xe10f_3000); // MRS r3,CPSR
|
||||
|
|
|
|||
23
src/bios/decompression.rs
Normal file
23
src/bios/decompression.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
//! Shared output routine for the original ARM decompression services.
|
||||
|
||||
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
|
||||
}
|
||||
82
src/bios/lz77.rs
Normal file
82
src/bios/lz77.rs
Normal 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");
|
||||
}
|
||||
|
|
@ -14,6 +14,8 @@ const ROM_DATA: u32 = ROM_START + 13 * 4;
|
|||
|
||||
#[path = "bios/arithmetic.rs"]
|
||||
mod arithmetic;
|
||||
#[path = "bios/lz77.rs"]
|
||||
mod lz77;
|
||||
|
||||
fn words(code: &[u32]) -> Vec<u8> {
|
||||
code.iter().flat_map(|word| word.to_le_bytes()).collect()
|
||||
|
|
|
|||
417
tests/bios/lz77.rs
Normal file
417
tests/bios/lz77.rs
Normal file
|
|
@ -0,0 +1,417 @@
|
|||
use super::*;
|
||||
use gba_rust::bios::INVALID_ARGUMENT_TRAP;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Token {
|
||||
Literal(u8),
|
||||
Reference { distance: usize, length: usize },
|
||||
}
|
||||
|
||||
/// Test-only token encoder and byte-vector reference. Neither decodes the emitted ARM code.
|
||||
fn stream(tokens: &[Token]) -> (Vec<u8>, Vec<u8>) {
|
||||
let mut compressed = vec![0x10, 0, 0, 0];
|
||||
let mut expected = Vec::new();
|
||||
for group in tokens.chunks(8) {
|
||||
let flags_index = compressed.len();
|
||||
compressed.push(0);
|
||||
for (index, token) in group.iter().enumerate() {
|
||||
match *token {
|
||||
Token::Literal(value) => {
|
||||
compressed.push(value);
|
||||
expected.push(value);
|
||||
}
|
||||
Token::Reference { distance, length } => {
|
||||
assert!((1..=4096).contains(&distance));
|
||||
assert!((3..=18).contains(&length));
|
||||
assert!(distance <= expected.len());
|
||||
compressed[flags_index] |= 0x80 >> index;
|
||||
compressed.push((((length - 3) << 4) | ((distance - 1) >> 8)) as u8);
|
||||
compressed.push((distance - 1) as u8);
|
||||
for _ in 0..length {
|
||||
expected.push(expected[expected.len() - distance]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let length = expected.len() as u32;
|
||||
compressed[..4].copy_from_slice(&(length << 8 | 0x10).to_le_bytes());
|
||||
(compressed, expected)
|
||||
}
|
||||
|
||||
fn prepare(service: u8, thumb: bool, destination: u32, compressed: &[u8]) -> (Machine, u32) {
|
||||
call_status_data(
|
||||
service,
|
||||
thumb,
|
||||
[ROM_DATA, destination, 0x55aa_1234],
|
||||
0x1f,
|
||||
compressed,
|
||||
)
|
||||
}
|
||||
|
||||
fn complete(machine: &mut Machine, return_pc: u32) {
|
||||
machine.step().unwrap();
|
||||
reach(machine, return_pc, 500_000);
|
||||
}
|
||||
|
||||
fn verify(service: u8, thumb: bool, destination: u32, compressed: &[u8], expected: &[u8]) {
|
||||
let (mut machine, return_pc) = prepare(service, thumb, destination, compressed);
|
||||
let before = machine.cpu().clone();
|
||||
// Halfword sentinels also work in VRAM without byte-write duplication.
|
||||
machine
|
||||
.memory_mut()
|
||||
.write16((destination - 2) & !1, 0xa55a)
|
||||
.unwrap();
|
||||
let after = (destination + expected.len() as u32 + 1) & !1;
|
||||
machine.memory_mut().write16(after, 0xa55a).unwrap();
|
||||
for address in [destination - 1, destination + expected.len() as u32] {
|
||||
machine.memory_mut().write16(address & !1, 0xa55a).unwrap();
|
||||
}
|
||||
machine
|
||||
.memory_mut()
|
||||
.write32(bios::IRQ_STACK, 0x5a5a_a5a5)
|
||||
.unwrap();
|
||||
complete(&mut machine, return_pc);
|
||||
for (index, value) in expected.iter().enumerate() {
|
||||
assert_eq!(
|
||||
machine.memory().read8(destination + index as u32).unwrap(),
|
||||
*value,
|
||||
"output byte {index}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
machine.memory().read16((destination - 2) & !1).unwrap(),
|
||||
0xa55a
|
||||
);
|
||||
assert_eq!(machine.memory().read16(after).unwrap(), 0xa55a);
|
||||
for address in [destination - 1, destination + expected.len() as u32] {
|
||||
assert_eq!(
|
||||
machine.memory().read8(address).unwrap(),
|
||||
if address & 1 == 0 { 0x5a } else { 0xa5 }
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
machine.memory().read32(bios::IRQ_STACK).unwrap(),
|
||||
0x5a5a_a5a5
|
||||
);
|
||||
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
|
||||
assert_eq!(machine.cpu().cpsr(), before.cpsr());
|
||||
}
|
||||
|
||||
fn fail(machine: &mut Machine) -> MachineError {
|
||||
for _ in 0..1000 {
|
||||
let before = machine.cpu().clone();
|
||||
let cycles = machine.cycles();
|
||||
let timing = machine.last_timing();
|
||||
if let Err(error) = machine.step() {
|
||||
assert_eq!(machine.cpu(), &before);
|
||||
assert_eq!(machine.cycles(), cycles);
|
||||
assert_eq!(machine.last_timing(), timing);
|
||||
assert_eq!(machine.step(), Err(error.clone()));
|
||||
return error;
|
||||
}
|
||||
}
|
||||
panic!("invalid stream did not produce a bounded diagnostic");
|
||||
}
|
||||
|
||||
fn assert_invalid(error: MachineError) {
|
||||
assert!(matches!(
|
||||
error,
|
||||
MachineError::Cpu(CpuError::UnsupportedInstruction {
|
||||
instruction: INVALID_ARGUMENT_TRAP,
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literals_cross_flag_groups_in_arm_and_thumb_for_both_write_widths() {
|
||||
let tokens: Vec<_> = (0..34).map(|value| Token::Literal(value * 7)).collect();
|
||||
let (compressed, expected) = stream(&tokens);
|
||||
for thumb in [false, true] {
|
||||
verify(0x11, thumb, DEST, &compressed, &expected);
|
||||
verify(0x12, thumb, VRAM_START + 4, &compressed, &expected);
|
||||
verify(0x12, thumb, DEST, &compressed, &expected); // Halfword variant also works in RAM.
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_and_reference_flags_are_read_most_significant_bit_first() {
|
||||
let tokens = [
|
||||
Token::Literal(b'A'),
|
||||
Token::Literal(b'B'),
|
||||
Token::Reference {
|
||||
distance: 2,
|
||||
length: 3,
|
||||
},
|
||||
Token::Literal(b'C'),
|
||||
Token::Reference {
|
||||
distance: 4,
|
||||
length: 4,
|
||||
},
|
||||
Token::Literal(b'D'),
|
||||
Token::Literal(b'E'),
|
||||
Token::Reference {
|
||||
distance: 3,
|
||||
length: 6,
|
||||
},
|
||||
Token::Reference {
|
||||
distance: 8,
|
||||
length: 4,
|
||||
},
|
||||
Token::Literal(b'F'),
|
||||
Token::Literal(b'G'),
|
||||
];
|
||||
let (compressed, expected) = stream(&tokens);
|
||||
assert_eq!(compressed[4], 0x29); // References in token positions 2,4,7.
|
||||
assert_eq!(expected.len() % 2, 0);
|
||||
verify(0x11, false, DEST, &compressed, &expected);
|
||||
verify(0x12, true, VRAM_START + 4, &compressed, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlapping_references_support_every_run_length() {
|
||||
for length in 3..=18 {
|
||||
let mut tokens = vec![
|
||||
Token::Literal(0x12),
|
||||
Token::Literal(0xab),
|
||||
Token::Reference {
|
||||
distance: 2,
|
||||
length,
|
||||
},
|
||||
];
|
||||
if length % 2 != 0 {
|
||||
tokens.push(Token::Literal(0x99));
|
||||
}
|
||||
let (compressed, expected) = stream(&tokens);
|
||||
verify(0x11, false, DEST + 1, &compressed, &expected); // Byte destination need not be aligned.
|
||||
verify(0x12, true, VRAM_START + 4, &compressed, &expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wram_variant_supports_distance_one_runs() {
|
||||
let (compressed, expected) = stream(&[
|
||||
Token::Literal(0x5a),
|
||||
Token::Reference {
|
||||
distance: 1,
|
||||
length: 18,
|
||||
},
|
||||
]);
|
||||
verify(0x11, false, DEST, &compressed, &expected);
|
||||
verify(0x11, true, DEST + 1, &compressed, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_twelve_bit_displacements_reach_back_4096_bytes() {
|
||||
for distance in [4095, 4096] {
|
||||
let mut tokens: Vec<_> = (0..4096)
|
||||
.map(|index| Token::Literal((index * 37 % 251) as u8))
|
||||
.collect();
|
||||
tokens.push(Token::Reference {
|
||||
distance,
|
||||
length: 18,
|
||||
});
|
||||
let (compressed, expected) = stream(&tokens);
|
||||
verify(0x11, false, DEST, &compressed, &expected);
|
||||
verify(0x12, true, VRAM_START + 4, &compressed, &expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seeded_valid_streams_match_independent_token_expansion() {
|
||||
let mut state = 0x1234_abcd_u32;
|
||||
for seed in 0..16 {
|
||||
let mut tokens = vec![Token::Literal(0x13), Token::Literal(0x57)];
|
||||
let mut length = 2;
|
||||
for _ in 0..40 {
|
||||
state = state.wrapping_mul(1664525).wrapping_add(1013904223);
|
||||
if state & 3 == 0 {
|
||||
tokens.push(Token::Literal((state >> 16) as u8));
|
||||
length += 1;
|
||||
} else {
|
||||
let run = 3 + (state >> 8) as usize % 16;
|
||||
let distance = 2 + (state >> 16) as usize % (length - 1).min(4095);
|
||||
tokens.push(Token::Reference {
|
||||
distance,
|
||||
length: run,
|
||||
});
|
||||
length += run;
|
||||
}
|
||||
}
|
||||
if length % 2 != 0 {
|
||||
tokens.push(Token::Literal(0));
|
||||
}
|
||||
let (compressed, expected) = stream(&tokens);
|
||||
verify(0x11, seed & 1 == 0, DEST, &compressed, &expected);
|
||||
verify(0x12, seed & 1 != 0, VRAM_START + 4, &compressed, &expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compressed_sources_can_be_read_from_either_work_ram_region() {
|
||||
let (compressed, expected) = stream(&[
|
||||
Token::Literal(0x55),
|
||||
Token::Literal(0xaa),
|
||||
Token::Reference {
|
||||
distance: 2,
|
||||
length: 4,
|
||||
},
|
||||
]);
|
||||
for source in [SOURCE, 0x0300_0100] {
|
||||
for service in [0x11, 0x12] {
|
||||
let (mut machine, return_pc) = call(service, false, [source, DEST, 0]);
|
||||
for (offset, byte) in compressed.iter().enumerate() {
|
||||
machine
|
||||
.memory_mut()
|
||||
.write8(source + offset as u32, *byte)
|
||||
.unwrap();
|
||||
}
|
||||
complete(&mut machine, return_pc);
|
||||
for (offset, byte) in expected.iter().enumerate() {
|
||||
assert_eq!(machine.memory().read8(DEST + offset as u32).unwrap(), *byte);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decompression_restores_user_mode_masks_and_all_caller_registers() {
|
||||
let (compressed, _) = stream(&[
|
||||
Token::Literal(0x55),
|
||||
Token::Literal(0xaa),
|
||||
Token::Reference {
|
||||
distance: 2,
|
||||
length: 4,
|
||||
},
|
||||
]);
|
||||
for service in [0x11, 0x12] {
|
||||
for thumb in [false, true] {
|
||||
for status in [0x10, 0x90, 0x9f, 0xdf] {
|
||||
let (mut machine, return_pc) = call_status_data(
|
||||
service,
|
||||
thumb,
|
||||
[ROM_DATA, DEST, 0x12345678],
|
||||
status,
|
||||
&compressed,
|
||||
);
|
||||
let before = machine.cpu().clone();
|
||||
complete(&mut machine, return_pc);
|
||||
assert_eq!(machine.cpu().cpsr(), before.cpsr());
|
||||
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_output_length_does_not_read_flags_or_touch_destination() {
|
||||
for service in [0x11, 0x12] {
|
||||
let (mut machine, return_pc) = prepare(service, false, 0x0e00_0000, &[0x10, 0, 0, 0]);
|
||||
complete(&mut machine, return_pc); // Destination and next source byte are unmapped.
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_headers_alignment_and_wrapping_destinations_are_rejected() {
|
||||
for header in [0_u32, 0x0200, 0x0211, 0x0220, 0x02ff] {
|
||||
let (mut machine, _) = prepare(0x11, false, DEST, &header.to_le_bytes());
|
||||
assert_invalid(fail(&mut machine));
|
||||
assert_eq!(machine.memory().read32(DEST).unwrap(), 0);
|
||||
}
|
||||
for source in [0, 0x3ffc, ROM_DATA + 1, ROM_DATA + 2, ROM_DATA + 3] {
|
||||
let (mut machine, _) =
|
||||
call_status_data(0x11, false, [source, DEST, 0], 0x1f, &[0x10, 4, 0, 0]);
|
||||
assert_invalid(fail(&mut machine));
|
||||
}
|
||||
let (mut machine, _) = prepare(0x12, false, 0xffff_fffe, &[0x10, 4, 0, 0]);
|
||||
assert_invalid(fail(&mut machine));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_before_output_or_beyond_declared_length_are_rejected() {
|
||||
for compressed in [
|
||||
vec![0x10, 3, 0, 0, 0x80, 0, 0], // No preceding output.
|
||||
vec![0x10, 4, 0, 0, 0x40, b'A', 0, 1], // Only one preceding byte, distance two.
|
||||
vec![0x10, 3, 0, 0, 0x40, b'A', 0, 0], // Length three exceeds two remaining bytes.
|
||||
] {
|
||||
let (mut machine, _) = prepare(0x11, false, DEST, &compressed);
|
||||
machine.memory_mut().write32(DEST, 0xcccc_cccc).unwrap();
|
||||
assert_invalid(fail(&mut machine));
|
||||
assert_eq!(
|
||||
machine.memory().read8(DEST).unwrap(),
|
||||
if compressed[4] == 0x80 { 0xcc } else { b'A' }
|
||||
);
|
||||
assert_eq!(machine.memory().read8(DEST + 1).unwrap(), 0xcc);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vram_variant_rejects_odd_sizes_odd_destinations_and_distance_one() {
|
||||
for (destination, compressed) in [
|
||||
(VRAM_START + 4, vec![0x10, 1, 0, 0, 0, 0x77]),
|
||||
(VRAM_START + 5, vec![0x10, 2, 0, 0, 0, 0x77, 0x88]),
|
||||
(VRAM_START + 4, vec![0x10, 4, 0, 0, 0x40, 0x77, 0, 0]),
|
||||
] {
|
||||
let (mut machine, _) = prepare(0x12, true, destination, &compressed);
|
||||
machine
|
||||
.memory_mut()
|
||||
.write32(VRAM_START + 4, 0xcccc_cccc)
|
||||
.unwrap();
|
||||
assert_invalid(fail(&mut machine));
|
||||
assert_eq!(
|
||||
machine.memory().read32(VRAM_START + 4).unwrap(),
|
||||
0xcccc_cccc
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_rom_inputs_produce_memory_diagnostics() {
|
||||
for compressed in [
|
||||
vec![],
|
||||
vec![0x10, 2],
|
||||
vec![0x10, 2, 0, 0],
|
||||
vec![0x10, 2, 0, 0, 0],
|
||||
vec![0x10, 3, 0, 0, 0x80, 0],
|
||||
] {
|
||||
let (mut machine, _) = prepare(0x11, false, DEST, &compressed);
|
||||
assert_eq!(
|
||||
fail(&mut machine),
|
||||
MachineError::Cpu(MemoryError::Unmapped(ROM_DATA + compressed.len() as u32).into())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn halfword_output_never_commits_a_lone_pending_byte_on_failure() {
|
||||
for service in [0x11, 0x12] {
|
||||
let compressed = [0x10, 2, 0, 0, 0, b'A']; // Second literal is missing.
|
||||
let (mut machine, _) = prepare(service, false, DEST, &compressed);
|
||||
machine.memory_mut().write16(DEST, 0xcccc).unwrap();
|
||||
assert!(matches!(
|
||||
fail(&mut machine),
|
||||
MachineError::Cpu(CpuError::Memory(_))
|
||||
));
|
||||
assert_eq!(
|
||||
machine.memory().read16(DEST).unwrap(),
|
||||
if service == 0x11 { 0xcc41 } else { 0xcccc }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn destination_failure_preserves_preceding_completed_halfword() {
|
||||
let (compressed, _) = stream(&[
|
||||
Token::Literal(0x12),
|
||||
Token::Literal(0x34),
|
||||
Token::Literal(0x56),
|
||||
Token::Literal(0x78),
|
||||
]);
|
||||
let (mut machine, _) = prepare(0x12, false, 0x07ff_fffe, &compressed);
|
||||
assert_eq!(
|
||||
fail(&mut machine),
|
||||
MachineError::Cpu(MemoryError::ReadOnly(0x0800_0000).into())
|
||||
);
|
||||
assert_eq!(machine.memory().read16(0x07ff_fffe).unwrap(), 0x3412);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue