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:
0xrsydn 2026-10-05 12:55:35 +07:00
commit 17e9351371
5 changed files with 531 additions and 0 deletions

23
src/bios/decompression.rs Normal file
View 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
View 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");
}