Skip to content

Hardware verification checklist (Phase 2)

Static analysis produces hypotheses. Each item below must be confirmed on real hardware by the worker ROM before its doc is marked verified. Priority ordered: compatibility-critical first.

Measurement setup

All cycle counts in swi_*.md were measured 2026-07-08 on a real GBA (AGB) via the worker ROM + Pico host:

  • Worker ROM multibooted into EWRAM; the timed call stub runs from IWRAM.
  • TM0/TM1 cascaded at F/1 bracket the swi instruction; IME = 0.
  • Net cycles = raw − 13-cycle harness baseline (cal, a no-op stub call).
  • Every value was stable across 3 consecutive runs unless noted.
  • Pointer arguments target the worker's EWRAM scratch buffer (default waitstates) unless a row says IWRAM. BIOS code itself always executes from the zero-wait BIOS ROM, so region choice affects only data accesses.
  • Raw logs: results/cycles_phase1_math.csv, results/cycles_phase1_sound.csv, results/cycles_phase2_decomp.csv.

Math SWIs (full input dumps)

  • [x] ArcTandone (2026-07-08): all 65536 inputs dumped (results/arctan_full_dump.csv), bit-for-bit match with the extracted algorithm (incl. 32-bit MUL wraparound). Error < 1° up to x=1.374; 12 sign flips in (1,2); x=1.75 → −93.27°; peak error 242.77°; f(−x) = −f(x) − 1 almost everywhere. Cycle cost constant 100.
  • [x] ArcTan2done (2026-07-08): boundary sweep over all magnitudes (diagonals, axes, diagonal±1; 393,216 points, results/arctan2_boundary_dump.csv) matches the reference model bit-for-bit; octant table corrected (two formula rows were swapped in the static-analysis draft). (0,0) → 0x0000; every diagonal/axis ray is exactly constant.
  • [~] Div — degenerate zero-division cases verified: 1/0 → (1,1,1), -1/0 → (-1,-1,1), 0/0 → (1,0,1) (69 cycles each, no hang); INT_MIN/-1 → 0x80000000 without hanging. Remainder sign follows the numerator in all four sign combinations of 100/7. The predicted hang for |n| >= 2 / 0 is deliberately not exercised (would wedge the harness); static analysis only.
  • [x] Sqrtdone (2026-07-08): exhaustive sweep of every perfect square ±1 (k=0..0xFFFF, 196,608 points, results/sqrt_squares_dump.csv): Sqrt(k²)=k, Sqrt(k²−1)=k−1, Sqrt(k²+1)=k with zero deviations — floor rounding confirmed; Sqrt(0xFFFFFFFF)=0xFFFF.
  • [x] GetBiosChecksum0xBAAE187F confirmed on hardware (41010 cycles). CPSR before/after identical in the clobber audit: the temporary System-mode window does not leak to the caller.

Memory / affine

  • [x] CpuFastSet non-multiple-of-8 count → verified: count=12 wrote exactly 16 words (rounds up to the next 8-word block; sentinels beyond untouched).
  • [x] BitUnPack trailing partial-word → NOT flushed (Ghidra decompile, 2026-07-08): a store happens only when the destination bit accumulator reaches ≥32; the loop ends and returns with no final flush, so a non-word-multiple output loses its trailing partial word. (Matches the aligned hardware round trip, which produced only whole words.)
  • [x] CpuSet/CpuFastSet/BitUnPack source in BIOS region → all three guards verified: CpuSet 84 / CpuFastSet 86 / BitUnPack 94 cycles, destination untouched (sentinel-checked).
  • [x] BgAffineSet/ObjAffineSet rounding → ASR truncation, no saturation (Ghidra, 2026-07-08): every sin/cos × coord product is an int shifted >> 14 (arithmetic, sign-preserving, floors toward −∞ for negatives); results are stored via a plain (short) cast that wraps — there is no min/max clamp anywhere.

Decompression

All five decompressors + BitUnPack round-trip verified on hardware with 256-byte vectors via the load/swix scratch mechanism (this also validates the Huffman tree/bitstream layout and BitUnPack's LSB-first bit order). - [x] VRAM variants with odd output lengthconfirmed (2026-07-08): LZ77Vram, RLUnCompVram and Diff8bitVram all leave the final odd byte unwritten (sentinel intact at byte 254 for size 255); the Wram variants write exactly the declared byte count. Diff16bitUnFilter rounds up instead: size 255 writes the full 256th byte (whole final halfword). - [x] LZ77Vram back-reference into an unflushed half-word → captured: with pre-zeroed destination, a disp=1 len=7 reference after one literal produced 10 00 00 00 00 00 00 00 — referenced bytes read old memory (0x00), which then propagates. disp≥2 streams decode correctly. - [x] Wrong type nibble in header → confirmed ignored (2026-07-08): LZ77 with nibble 0x0/0xF, RLE with 0x1, Diff8 with 0x0 all decode byte-identically with identical cycle counts. - [x] Huffman output size not a multiple of 4 → measured (2026-07-08): the final 32-bit word is always written in full; the 1–3 extra bytes are the next correctly decoded symbols (decoding continues to the word boundary), and the cycle count equals the word-aligned run. - [x] Decompressed size 0 → verified for LZ77Wram (84 cyc), RLEWram (94 cyc), Diff8bitWram (87 cyc), Huffman (122 cyc), BitUnPack src-length 0 (92 cyc): immediate return, nothing written.

Reset / IRQ

  • [x] RegisterRamResetdone (2026-07-08) for bits 2/3/4/5/6/7 + flags=0 via seeded-register I/O diffs (results/blacklist_rrr.csv): bit6 = sound block only (SOUNDCNT_H write consistent with 0x880E), bit7 = display/DMA/timer/KEYCNT/IE/ WAITCNT excluding sound, bit5 = SIO reset kills the link irrecoverably from remote (physical reset needed). The unconditional 0x114/0x120 writes are not readback-observable. Bits 0/1 (EWRAM/IWRAM) deliberately untested (would erase the worker).
  • [x] IntrWait r0=0behavior confirmed: with the flag already pending it returns in 95 cycles (no halt) and consumes the flag; the internal HALTCNT-to-ROM-dud mechanism is consistent with this but observable only in the net effect. IntrWait leaves IME=1.
  • [x] Halt/Stop wake vs. IME — done: Halt wakes on IE&IF with IME=0 (1720 ms slept of a 4 s timer; 72-cycle immediate return when pending; IF not acked). Stop wakes on keypad IRQ with IME=0, and timers freeze during Stop (57 k cycles counted across a multi-second stop). SoftReset clears exactly 0x03007E00-0x03007FFF and honors the 0x03007FFA return flag; HardReset returns to the cold-boot BIOS path (re-multiboot works without power cycle).

Sound

  • [x] PCM channel struct (0x40 bytes) → skeleton extracted from SoundDriverMain (Ghidra, 2026-07-08): status/flags @+0x00, envelope rates @+0x05/+0x07, envelope level @+0x0C and phase @+0x0D, computed R/L output volumes @+0x0A/+0x0B (from raw R/L @+0x02/+0x03), wave-data pointer @+0x24, current sample pointer @+0x18, fractional position @+0x1C, play-position pointer @+0x28. Full bit-level field semantics still partial.
  • [x] MusicPlayerInfo / Track / Song header (Ghidra, 2026-07-08, from MPlayOpen/MPlayStart): MusicPlayerInfo: song ptr @+0x00, status @+0x04, track count @+0x08, byte @+0x09, tempo/pitch words @+0x1C/+0x20/+0x22, track-array ptr @+0x2C, voicegroup @+0x30, identity 0x68736D53 @+0x34, linked-list @+0x38/+0x3C. Track = 0x50 bytes, status byte @+0x00 (0xC0=active), track-data ptr @+0x40. Song header: track count @+0x00, byte @+0x02, flags byte @+0x03 (bit7 → SoundDriverMode), voicegroup ptr @+0x04, per-track pointers from @+0x08.
  • [x] SOUNDBIAS bit14-15 ← SoundDriverMode bits20-21 → confirmed (Ghidra, 2026-07-08): SoundDriverMode does SOUNDBIAS[0x04000089] = (mode & 0x300000) >> 14 | (old & 0x3F), mapping mode bits 20-21 straight onto SOUNDBIAS bits 14-15. The update is gated on mode & 0xB00000 (bits 20,21,23), so setting bit 23 alone re-writes the field to 0.
  • Hazard (found the hard way): SoundDriverInit leaves the PCM sample timer
    • DMA1/2 FIFO running, which can bit-slip the SIO link (see host/README.md § Bit-slip hazard). Follow every DMA-starting sound SWI with SoundDriverVsyncOff (0x28).

Multiboot (needed to trust the Pico host)

  • [x] CRC poly↔mode labelling → confirmed not swapped (Ghidra, 2026-07-08). At the once-per-session setup the mode flag selects, in one if: normal → poly 0xC37B, CRC init 0xC387, XOR key 0x43202F2F; multiplay → poly 0xA517, CRC init 0xFFF8, XOR key 0x6465646F. All six constants are literal slices of "// Coded by Kawasedo" at 0x2D2C–0x2D47. Per-word cipher key steps by LCG × 0x6F646573 + 1 (matches the working host).
  • [x] Seed derivation → confirmed (Ghidra, 2026-07-08): the rolling cipher key is seeded as key = word@(param+0x18) with its **low byte overwritten by palette_data @(param+0x1C); theclient_databytes at param+0x05/+0x06/+0x07 come from the slave's reply halfwords (forced to 0xFF in multiplay mode). Matches the host'sm = 0xFFFF0000 | (cc<<8) | pp`.
  • [~] Final ASCII-token handshake → FSM extracted (Ghidra, 2026-07-08): end tokens are 'e'=0x65 / 'f'=0x66 (CRC-exchange markers) and 't'=0x74 / 'u'=0x75 (fail/OK after the 16-bit CRC compare). The doc's earlier 's'=0x73 is a header-parse byte, not an end token. Exact master/slave emission order still wants a real link capture.

Cross-cutting

  • [x] Cycle counts for every SWI via TM0 (cascade TM1), calibrated against an empty SWI stub — done for all 34 non-blacklisted SWIs (see Measurement setup above and the per-SWI docs). Blacklisted SWIs: fixture-measured 2026-07-08 (immediate-return costs + slept-time behavior); only MultiBoot (0x25) remains unexercised (needs a second GBA).
  • [x] Clobbered-register audit per SWI — done (2026-07-08) via the worker's canary harness (swir, r4–r12 canaries + CPSR before/after snapshot, results/clobber_audit_pass2.csv). Headline: every non-blacklisted SWI preserves r2, r4–r12, sp, and CPSR (flags + mode) — r11/r12 via the BIOS dispatcher, CPSR via SPSR restore. Caller-visible clobbers are confined to r0/r1/r3 and are listed per SWI in each doc's "Clobbered registers". (Recurring observation: many THUMB-implemented SWIs return r3 = 0x170, an address inside the SWI dispatcher's code region at 0x140–….)