Skip to content

SWI 0x10 — BitUnPack

  • Entry: 0x00000F60 (ARM)
  • Status: verified (hardware-checked 2026-07-08: cycle counts measured and output round-trip-verified on real GBA via the worker ROM)

Summary

Expands packed low-bit-depth source data into wider destination units, packing the results into a stream of 32-bit words. Each source "unit" of 1/2/4/8 bits is read, optionally has a constant data offset added, and is written into the next slot of the chosen destination unit width. Typically used to unpack 1/2/4-bpp graphics into 4/8-bpp tiles, or to build palette-index data.

Parameters

Reg In Meaning
r0 ptr Source address (byte stream; no alignment requirement).
r1 ptr Destination address (word aligned; written in 32-bit words).
r2 ptr Pointer to the BitUnpackInfo control struct (below).

BitUnpackInfo (control struct, 8 bytes)

Offset Type Meaning
0x00 u16 Source length in bytes.
0x02 u8 Source unit width in bits: 1, 2, 4, or 8.
0x03 u8 Destination unit width in bits: 1, 2, 4, 8, 16, or 32.
0x04 u32 Bits 0–30: data offset added to units. Bit 31: zero-data flag.

Returns

None. Expanded data written to r1.

Clobbered registers

r2–r9, sl, fp, lr and 8 bytes of stack used internally (restored). r0/r1 advance.

Hardware audit (2026-07-08, canary r4–r12 + CPSR snapshot): caller-visible clobbers: r0/r1 advanced past source/output; r2 (info pointer) preserved; r3 = 0. r2, r4–r12, r13, and CPSR (flags and mode) came back bit-identical on every tested path. r11/r12 are explained by the SWI dispatcher (it pushes {r11, r12, lr} — see 10_irq_boot_and_iwram.md) and CPSR by the SPSR restore on return; r2 and r4–r10 must be preserved or restored by the routine itself — where a static note above claims r2 is destroyed, the hardware disagrees at the caller level. Raw data: results/clobber_audit_pass2.csv.

Algorithm

  1. Read source length, source width (sw), dest width (dw), the offset (low 31 bits) and the zero-flag (bit 31).
  2. Call the shared guard routine at 0x00000BA4 with the byte length: a zero length aborts, and a source below 0x02000000 is rejected (BIOS-region guard, same as CpuSet/CpuFastSet).
  3. Maintain a 32-bit output accumulator and a destination bit position (0 → 32). For each source byte (looping length times):
  4. Build a source-unit mask (1 << sw) − 1.
  5. Walk the byte in sw-bit steps (8/sw units per byte). For each unit:
    • Extract the unit value v.
    • Offset / zero handling: if v != 0, set v = v + offset. If v == 0, add the offset only when the zero-flag (bit 31) is set; otherwise leave it 0. (So bit 31 controls whether zero source values also receive the offset — used e.g. to keep palette index 0 transparent, or to shift all indices including 0 into a sub-palette.)
    • OR v into the accumulator at the current destination bit position, then advance the position by dw.
    • When the position reaches 32, store the accumulator word to r1 (post- incrementing by 4), reset the accumulator and position to 0.
  6. Finish when all source bytes are consumed.

Edge cases & known bugs

  • Length 0: aborted by the guard, nothing written.
  • Source below 0x02000000: aborted by the guard.
  • Partial final word: the accumulator is flushed to memory only when it fills a complete 32 bits. If the total expanded output is not a multiple of 32 bits, the trailing partial word is not written. Callers must size the source so the output is a whole number of 32-bit words. Confirmed by decompilation (static analysis, Ghidra 12.1.2): the write is guarded by dest_bitpos >= 32; the main loop exits and the function returns with no final store, so a partial word left in the accumulator is silently dropped.
  • Only source widths 1/2/4/8 are meaningful (mask logic assumes sw divides 8).
  • If dw < sw and a unit value plus offset exceeds the destination width, the value overflows into the next slot (no clamping).
  • Source is read with byte loads (unaligned source OK); destination is written with word stores (must be word aligned).

  • Hardware-confirmed (2026-07-08 anomaly tests, results/cycles_phase3_anomaly.csv): source length 0 returns in 92 cycles with nothing written; a source pointer in the BIOS region (0x100) trips the guard in 94 cycles with the destination untouched.

Cycle count

Hardware-measured net CPU cycles (worker-ROM harness, TM0/TM1 cascade at F/1, 13-cycle baseline subtracted; identical across 3 runs — see 02_hardware_verification_checklist.md § Measurement setup).

1 bpp → 8 bpp, 32-byte source → 256-byte output (EWRAM src/dst): 6936 cycles. Output round-trip-verified, confirming LSB-first bit order within each source byte.

Open questions (need hardware verification)

  • Confirm the trailing-partial-word-not-flushed behavior on hardware.
  • Exact behavior for out-of-range width values in the struct.

GBATEK cross-reference

Matches GBATEK's BitUnPack (struct layout, per-unit expansion, bit 31 zero-data handling). Adds: the shared BIOS-region source guard, and the observation that an output not aligned to 32 bits drops its final partial word.