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¶
- Read source length, source width (
sw), dest width (dw), the offset (low 31 bits) and the zero-flag (bit 31). - Call the shared guard routine at
0x00000BA4with the byte length: a zero length aborts, and a source below0x02000000is rejected (BIOS-region guard, same as CpuSet/CpuFastSet). - Maintain a 32-bit output accumulator and a destination bit position
(0 → 32). For each source byte (looping
lengthtimes): - Build a source-unit mask
(1 << sw) − 1. - Walk the byte in
sw-bit steps (8/swunits per byte). For each unit:- Extract the unit value
v. - Offset / zero handling: if
v != 0, setv = v + offset. Ifv == 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
vinto the accumulator at the current destination bit position, then advance the position bydw. - When the position reaches 32, store the accumulator word to
r1(post- incrementing by 4), reset the accumulator and position to 0.
- Extract the unit value
- 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
swdivides 8). - If
dw < swand 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.