Skip to main content

Hacker-first, scriptable GBA emulator with HTTP API

Project description

gbax

A hacker-first GBA emulator. Play with your keyboard, drive it from HTTP.

gbax is a pip-installable Game Boy Advance emulator built for people who like typing commands. It plays games in a window with a keyboard, like any emulator — and it also exposes its entire state over a local HTTP API, so any script, shell pipeline, or LLM in any language can read pixels, peek memory, and press buttons.

$ pip install gbax
$ gbax download "pokemon emerald"
$ gbax play emerald

Pokémon Emerald, in a window, with sound, in three commands.

$ gbax serve emerald
gbax serving Pokemon - Emerald Version (USA, Europe).gba on http://127.0.0.1:8420
  mode=step  rom_sha1=f3ae088181bf583e55daf962a92bb46f4f1d07b7
  endpoints: /mode /step /speed /frame /buttons /memory /frame_count

$ curl -X POST localhost:8420/buttons -d '{"buttons":["a","right"]}' -H 'content-type: application/json'
$ curl -X POST 'localhost:8420/step?frames=4'
$ curl localhost:8420/frame -o frame.png

That's the headline: it's an emulator you can pipe.

🤫 Pst, that whole "let's do science on GBA games" is the public reason you're here, but we all know you just want to play Pokémon on Linux. Go ahead, that's allowed too.

Status

  • Alpha. v0.0.1. Works on Linux. macOS / Windows are PR-welcome.
  • MPL-2.0. Same license as the underlying mGBA core.
  • No ROMs bundled. gbax download pulls from the public No-Intro mirror at archive.org. Use it for games you own; respect your local laws.

What's here

Play

gbax play <rom> opens an SDL window, wires the keyboard, plays audio.

  • D-pad: arrow keys · A: X · B: Z · L: A · R: S · Start: Enter · Select: Right-Shift
  • Ctrl+1Ctrl+9 — save state to slot N (auto-persisted to ~/.gbax/saves/<rom-sha1>/)
  • Shift+1Shift+9 — load slot N
  • F12 — screenshot to ~/.gbax/screenshots/
  • Tab (hold) — fast-forward at 8×

Slots survive restarts. Open a game, save in slot 3, close the window, open the game again, Shift+3 — you're back.

Cheats

$ gbax cheats emerald | head -3
  1-hit-kill                           1-Hit Kill
  max-money                            Max Money
  walk-through-walls-l-r               Walk Through Walls [Press L+R]
…

$ gbax play emerald --cheats max-money,walk-through-walls-l-r
cheat ON: Max Money
cheat ON: Walk Through Walls [Press L+R]

Pin cheats to keys so the same hotkey toggles the same cheat across sessions:

$ gbax pin emerald F1 max-money
$ gbax pin emerald F2 walk-through-walls-l-r
$ gbax pin emerald F7 complete-pokedex
$ gbax pins emerald
  F1  →  max-money
  F2  →  walk-through-walls-l-r
  F7  →  complete-pokedex

Pins persist to ~/.gbax/pins/<rom-sha1>.json. In play, pressing F1 toggles the pinned cheat directly — autoloading it from the catalog if needed. Unpinned F-keys fall back to "toggle the Nth currently-active cheat."

Over the API: POST /cheats/<slug>/enable, POST /cheats/<slug>/disable, POST /cheats/custom for ad-hoc codes, DELETE /cheats to clear.

The libretro-database snapshot (~6700 GameShark / Action Replay / Code Breaker codes covering 512 GBA games) ships in the wheel — no network at runtime.

Library

$ gbax search "metroid"
    1. Metroid - Zero Mission (USA).zip  (4.0 MB)
    2. Metroid Fusion (USA, Australia).zip  (5.5 MB)
    …

$ gbax download "metroid fusion"
match: Metroid Fusion (USA, Australia).zip
  size: 5.5 MB
  downloading… 100%  (5.5/5.5 MB)
saved: /home/<you>/.gbax/roms/Metroid Fusion (USA, Australia).gba

$ gbax list-roms
  Pokemon - Emerald Version (USA, Europe).gba  (16.0 MB)  sha1:f3ae088181
  Metroid Fusion (USA, Australia).gba          ( 8.0 MB)  sha1:fbe10b78b6

Search is instantaneous (~13 ms) — the full 3555-entry No-Intro GBA index ships in the wheel. gbax download is the only thing that touches the network.

Serve

gbax serve <rom> boots the emulator in step mode and exposes a FastAPI control surface on 127.0.0.1:8420. In step mode the emulator is paused by default; a controller posts /step?frames=N to advance. That's what makes slow controllers (an LLM that takes 2 seconds to think, an RL agent that runs in Python) actually viable — the game waits.

GET  /mode                                  → "step" | "free"
POST /mode                {mode}              switch
POST /step?frames=N                          advance N frames
POST /speed               {multiplier}        free-run wall-clock speed
GET  /frame_count

GET  /frame                                  PNG of current frame
GET  /frame?fmt=raw                          240×160×3 RGB888 bytes

GET  /buttons                                → ["a","right",…]
POST /buttons             {buttons}           replace held set

GET  /memory?addr=…&len=… → {data: "deadbeef…"}
POST /memory              {addr, data, width} write hex

The address space gbax exposes is the full GBA bus — IWRAM at 0x03000000, EWRAM at 0x02000000, VRAM at 0x06000000, OAM, I/O, ROM, BIOS. So a Pokémon-aware controller can read 0x02024362 and know your party's HP.

Free-run mode (POST /mode {"mode":"free"}) advances at wall-clock 60 fps (or faster with /speed), and /buttons writes still take effect. Use this when you want a human at the keyboard and a script reading state.

Architecture

flowchart TB
    subgraph clients[" "]
        direction LR
        kbd([Keyboard])
        http([HTTP client<br/>script · LLM · shell])
    end

    subgraph cli["gbax CLI (Typer)"]
        play["gbax play"]
        serve["gbax serve"]
        other["search · download · list-roms · …"]
    end

    sdl["SDL renderer<br/>window + audio + input"]
    api["FastAPI server<br/>/mode /step /frame /buttons /memory /speed"]
    rt["EmulatorRuntime<br/>load · step · framebuffer · memory · save slots · ticker"]
    lr["LibretroCore<br/>~300 LOC cffi shim over the libretro ABI"]
    so["mgba_libretro.so"]

    kbd --> sdl
    http --> api
    play --> sdl
    serve --> api
    sdl --> rt
    api --> rt
    rt --> lr
    lr --> so

    classDef ext fill:#eef,stroke:#33a,stroke-width:1px;
    classDef core fill:#fef9c3,stroke:#a16207,stroke-width:1px;
    class kbd,http ext;
    class so core;
  • LibretroCore is a ~300-line cffi wrapper around the libretro ABI. It dlopens mgba_libretro.so, captures the framebuffer + audio + memory-map callbacks, drives input. Swapping in another libretro core (vba-next, gpsp) is mostly a one-line config change.
  • EmulatorRuntime is the thread-safe gbax-shaped API on top: load, step, framebuffer, memory, save states, free-run ticker.
  • The SDL window and the FastAPI server are independent clients of the runtime. They don't know about each other.

Step-mode controller loop

When you gbax serve, the emulator is paused. A controller drives it:

sequenceDiagram
    autonumber
    participant Ctl as Controller<br/>(script / LLM / RL)
    participant API as FastAPI
    participant RT as EmulatorRuntime
    participant Core as mgba_libretro

    loop every decision
        Ctl->>API: GET /frame
        API->>RT: framebuffer()
        RT-->>API: 240×160×3 RGB
        API-->>Ctl: PNG

        Ctl->>Ctl: think (any wall-clock time)

        Ctl->>API: POST /buttons {a, right}
        API->>RT: set_buttons(...)
        RT->>Core: retro_set_input_state
        Ctl->>API: POST /step?frames=4
        API->>RT: step(4)
        RT->>Core: retro_run() × 4
    end

The game waits for the controller. That's what makes a 2-second-per-decision LLM viable, and what makes RL training reproducible.

Why libretro and not mGBA's Python bindings directly? Because the upstream bindings are brittle on modern toolchains and require building libmgba with a specific feature set. The libretro ABI is stable, well-documented, and the core is a single self-contained .so. See know-how/building-libretro-core.md.

Install

pip install gbax

You also need the libretro mGBA core (mgba_libretro.so). The wheel doesn't bundle it yet — for now, build it from source:

git clone --depth=1 https://github.com/mgba-emu/mgba.git /tmp/mgba
cd /tmp/mgba && mkdir build && cd build
cmake .. \
  -DBUILD_QT=OFF -DBUILD_SDL=OFF -DBUILD_LIBRETRO=ON \
  -DBUILD_SHARED=OFF -DBUILD_STATIC=OFF \
  -DUSE_LUA=OFF -DUSE_FREETYPE=OFF -DUSE_DISCORD_RPC=OFF \
  -DUSE_LIBZIP=OFF -DBUILD_LTO=OFF -DCMAKE_BUILD_TYPE=Release
make mgba_libretro -j$(nproc)

mkdir -p ~/.gbax/cores
cp mgba_libretro.so ~/.gbax/cores/
export GBAX_CORE_PATH=~/.gbax/cores/mgba_libretro.so

System packages needed: cmake build-essential libsdl2-dev libpng-dev libsqlite3-dev. Then re-run pip install gbax.

The full procedure (including why each flag matters) is in know-how/building-libretro-core.md.

Examples

Pipe the framebuffer into ImageMagick

$ gbax serve emerald &
$ for i in $(seq 1 60); do
    curl -s 'localhost:8420/step?frames=1' > /dev/null
    curl -s localhost:8420/frame > frame-$i.png
  done
$ convert -delay 5 frame-*.png loop.gif

Have an LLM play Pokémon

import base64, requests
from openai import OpenAI

g = "http://localhost:8420"

while True:
    requests.post(f"{g}/step?frames=4")
    frame = requests.get(f"{g}/frame").content
    response = OpenAI().chat.completions.create(
        model="gpt-5",
        messages=[{
            "role": "user",
            "content": [
                {"type": "text", "text": "Press one button to make progress."},
                {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64.b64encode(frame).decode()}"}},
            ],
        }],
    )
    button = response.choices[0].message.content.strip().lower()
    requests.post(f"{g}/buttons", json={"buttons": [button]})

(The LLM rendered above is illustrative — gbax makes no assumption about your controller.)

Read your Pokémon party from a shell

$ # EWRAM byte 0x2024284 is the start of the party block in Pokemon Emerald
$ curl -s 'localhost:8420/memory?addr=33718916&len=4' | jq -r .data
01000000

Roadmap

Status Slice
gbax play — keyboard + audio + save state slots that survive restarts
gbax serve — HTTP API for memory / framebuffer / buttons / step / speed
ROM library — search, download, list-roms against archive.org
Cheat codes — vendored libretro DB (~6700 codes), F1–F9 toggle, /cheats API
YAML user scripts — Ctrl+H runs a sequence of presses + memory pokes
Recording / replay — deterministic input log + divergence detection
Per-game plugins — Python plugins expose /state and /actions for Pokémon, etc.
macOS / Windows wheels

Full design at vault/Atlas/Architecture/2026-06-09-gbax-design.md (in the companion vault, not this repo).

Credits

  • mGBA by endrift — the emulator core doing the actual heavy lifting. MPL-2.0.
  • No-Intro — the canonical ROM-naming and SHA-1 reference.
  • archive.org — hosts the No-Intro snapshot we point at by default.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gbax-0.1.0.tar.gz (907.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gbax-0.1.0-py3-none-any.whl (295.4 kB view details)

Uploaded Python 3

File details

Details for the file gbax-0.1.0.tar.gz.

File metadata

  • Download URL: gbax-0.1.0.tar.gz
  • Upload date:
  • Size: 907.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for gbax-0.1.0.tar.gz
Algorithm Hash digest
SHA256 4bb99baf26c0e33401553ddc78733461ecf17353e2f92dc62635b457d801a7c9
MD5 fbc028d74e0fbdda9ad45f4c34a01828
BLAKE2b-256 67af230bc335cb0a2e70070180bb8e8bac149bcd20ced83c8e4d84a085089226

See more details on using hashes here.

Provenance

The following attestation bundles were made for gbax-0.1.0.tar.gz:

Publisher: release.yml on apiad/gbax

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file gbax-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: gbax-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 295.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for gbax-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 eb5f7e4e76e35db4800198bb6c0589d89db5fd2d47c2b68846159aed3026512d
MD5 d8898abb3e722a6b1879a0455a5e1c03
BLAKE2b-256 07c9a36cb53e65e7dae48f016f1ffa2e1311b198eed7f79107665ca98001a467

See more details on using hashes here.

Provenance

The following attestation bundles were made for gbax-0.1.0-py3-none-any.whl:

Publisher: release.yml on apiad/gbax

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page