Transform Game Boy Advance ROMs into standalone Python files.
NOT an emulator. GBAtoPy transpiles GBA ROMs into human-readable Python code that, when executed, reproduces the game's behavior. The goal is a
.pyfile you can open, read, and modify.
GBAtoPy converts ARM/Thumb assembly → Python code using a Rust pipeline:
ROM bytes → Disassembly → Python Code Gen → Executable Python
- Disassembler (
crates/gbatopy-disasm/) - Decodes ARM/Thumb instructions - Code Generator (
crates/gbatopy-cli/src/codegen/) - ARM/Thumb → Python translation (600+ opcodes) - Memory Model - GBA memory map (0x08000000 ROM, 0x06000000 VRAM, 0x04000000 MMIO)
- Game Loop - pygame-based display and input
- Python Runtime - Core emulation modules (CPU, PPU, Memory, DMA, Timers, APU) embedded in generated Python (see
crates/gbatopy-cli/assets/gba_runtime/). Derived from PyBoyAdvance (MIT-licensed).
# ROM data embedded
ROM_DATA = bytearray([...])
def func_08000000():
global r0, r1, ..., r15
r0 = r1 + r2 # Example: ADD instruction
memory.write_32(0x08000100, value) # Example: STR instruction
def main_entry():
# ROM execution loop with pygame display
while True:
call_func(r15)| Component | Status |
|---|---|
| ARM/Thumb codegen | ✅ Complete (600+ opcodes, zero stubs) |
| PPU rendering | ✅ Mode 3/4 (100% golden match on stripes.gba), Mode 0 (4BPP) partial |
| Memory subsystem | ✅ VRAM, Palette, OAM, MMIO with mirrors |
| IRQ/DMA/Timers | ✅ 4 DMA channels, Timers 0-3, VBlank/HBlank/VCount interrupts |
| BIOS SWI | ✅ 54 handlers (Sqrt, Div, Halt, CpuSet, etc.) |
| Keypad input | ✅ KEYINPUT/KEYCNT |
| Sprite rendering | ✅ OAM + tile fetch + palette lookup |
| APU audio | ❌ PSG + FIFO infrastructure exists, no sound output |
| Affine backgrounds | ❌ Mode 1/2 code exists, MMIO wiring broken |
| Window/Blend/Mosaic | ❌ Register stubs only |
- Rust toolchain (1.70+):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - Python 3.10+
- Pygame:
pip install pygame - SDL2 (optional, for display):
sudo apt install libsdl2-dev
cargo build --releasecargo run --release -p gbatopy-cli -- pipeline --rom test_roms/roms/arm.gba --output /tmp/test.pyHeadless mode (for testing):
python3 /tmp/test.py --headless --frame=60 --screenshot /tmp/test.pngInteractive mode (with display):
python3 /tmp/test.py --scale=2--headless: Run without display (for testing/screenshot)--frame=N: Run exactly N frames then exit--screenshot=FILE: Save screenshot at end--scale=N: Scale display by N (e.g., 3 = 720×480 pixels)
| Metric | Value |
|---|---|
| Test ROMs | 66 |
| Transpile success | ✅ 100% (66/66) |
| Golden match | ✅ stripes.gba (100%) |
| Zero stubs | ✅ All 66 ROMs |
| ARM/Thumb codegen | ✅ 600+ opcodes |
| 54 BIOS handlers | Implemented |
| VRAM writes | Working (Mode 3/4) |
| DMA channels | ✅ 4 channels |
| IRQ handlers | ✅ VBlank/HBlank/VCount |
Known Gaps:
- ❌ APU audio synthesis (no sound output)
- ❌ Affine backgrounds (Mode 1/2)
- ❌ Window layers, blend modes, mosaic effects
- ❌ CPSR flags (conditional branches unreliable)
Test ROMs are downloaded automatically via scripts/setup/download_roms.sh (66 ROMs):
# First time setup
bash scripts/setup/download_roms.sh# Debug
cargo build
# Release (faster)
cargo build --release
# All crates
cargo build --workspace# Rust unit tests
cargo test --workspace
# Python tests (inside gba_runtime module)
python3 -m pytest crates/gbatopy-cli/assets/gba_runtime/tests/ -v
# Transpile smoke test
bash scripts/setup/download_roms.sh # First time only
for rom in test_roms/roms/*.gba; do
cargo run --release -p gbatopy-cli -- pipeline --rom "$rom" --output /tmp/test.py && \
python3 -m py_compile /tmp/test.py && \
echo "✓ $(basename "$rom")"
done# Generate ROM
cargo run --release -p gbatopy-cli -- pipeline --rom test_roms/roms/arm.gba --output /tmp/test.py
# Check syntax
python3 -m py_compile /tmp/test.py
# Verify stripes.gba golden match
python3 scripts/screenshot/compare_screenshots.py test_roms/roms/stripes.gba
# Expected: 100% pixel matchFor golden screenshots and debugging, GBAtoPy uses mGBA with custom patches for Lua scripting via --script flag.
mGBA source is NOT included in this repository (see .gitignore). To build:
# Clone mGBA
git clone https://github.com/mgba-emu/mgba.git
cd mgba
# Apply custom patches (adds --script flag + mScriptContext integration)
patch -p1 < ../mgba-custom-patches.diff
# Build with Lua scripting enabled
cmake -B build -DENABLE_SCRIPTING=ON -DBUILD_PYTHON=OFF -DUSE_QT=OFF
cmake --build build -j$(nproc)# Single ROM
./build/sdl/mgba --script scripts/screenshot/screenshot.lua test_roms/roms/stripes.gba
# Full comparison (golden + transpile + compare)
python3 scripts/screenshot/compare_screenshots.py test_roms/roms/stripes.gbaThe Lua API exposed by mGBA (after patching):
emu:currentFrame()- Get current frame numberemu:runFrame()- Advance one frameemu:screenshot(filename)- Save screenshot to PNGcallbacks:add("frame", fn)- Register per-frame callback
See mgba-custom-patches.diff for the full diff against upstream mGBA. See PR #3752 for the upstream Lua scripting extension.