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
swiinstruction;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] ArcTan — done (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) − 1almost everywhere. Cycle cost constant 100. - [x] ArcTan2 — done (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 → 0x80000000without hanging. Remainder sign follows the numerator in all four sign combinations of100/7. The predicted hang for|n| >= 2 / 0is deliberately not exercised (would wedge the harness); static analysis only. - [x] Sqrt — done (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)=kwith zero deviations — floor rounding confirmed;Sqrt(0xFFFFFFFF)=0xFFFF. - [x] GetBiosChecksum —
0xBAAE187Fconfirmed 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 × coordproduct is anintshifted>> 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 length → confirmed (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] RegisterRamReset — done (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=0— behavior 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 onmode & 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).
- DMA1/2 FIFO running, which can bit-slip the SIO link (see
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 → poly0xC37B, CRC init0xC387, XOR key0x43202F2F; multiplay → poly0xA517, CRC init0xFFF8, XOR key0x6465646F. 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–….)