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");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue