Skip to main content

Hacker-first, scriptable GBA emulator with HTTP API

Project description

gbax

PyPI Python CI License: MPL-2.0 Platform

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.5.0. Works on Linux x86_64 — the wheel bundles the libretro core, so pip install gbax is a one-step setup. macOS / Windows / ARM 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. Pass --fullscreen (or -f) to skip straight to borderless-desktop fullscreen at launch.

  • 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
  • Ctrl+R — toggle macro recording (see Macros)
  • F10 — toggle upscale filter (linear ↔ nearest)
  • F11 — toggle borderless-desktop fullscreen
  • F12 — screenshot to ~/.gbax/screenshots/
  • Tab (hold) — fast-forward at 8×

The window is resizable; SDL preserves the 3:2 aspect ratio with auto letterboxing, and the upscale runs through your GPU's bilinear sampler by default (looks smooth at any size). Hit F10 for crisp nearest- neighbor pixels if you prefer the chunky look.

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.

Macros

Record a button sequence once, replay it whenever you want — without losing your current state. Useful for grinding routines, in-battle combos, menu navigation patterns.

[in-game]
Ctrl+R                          # start recording
… play your sequence …
Ctrl+R                          # stop; alt-tab to the terminal
bind to which key? [F1-F9]: F3
name (optional): heal-pokemon-center
bound F3 → heal-pokemon-center

[mid-battle, later]
F3                              # replays the recorded sequence

Macros are scoped per-ROM and persisted to ~/.gbax/macros/<rom-sha1>/<slot>.json. List and remove from the CLI:

$ gbax macros emerald
  F3  →  heal-pokemon-center  (123 frames, recorded 2026-06-10 23:14)
  F5  →  (unnamed)              (47 frames, recorded 2026-06-10 23:21)

$ gbax macro delete emerald F3
deleted F3 (heal-pokemon-center).

When a slot has both a cheat pin and a macro, the macro wins. Player input during replay is merged set-union with the macro's held buttons — useful if you want to nudge a direction mid-routine, harmless if you don't.

Note: the record-stop prompt is plain input() on the terminal where you launched gbax. Alt-tab to the terminal to type the slot + name; the game pauses momentarily.

Automation: Controller, Scenarios, Tournaments

In-process Python automation. No HTTP needed.

import gbax

with gbax.Controller("pokemon emerald") as g:
    g.press(["start"], frames=2)
    g.wait(60)
    g.press(["a"], frames=2)
    print(g.read_u32(0x02024284))
    g.screenshot("/tmp/run.png")

For repeatable runs and tournaments, define a scenario (a small Python file with setup / observe / score / done) and have one or more players race through it:

$ gbax scenario create "pokemon emerald" --name catch-snorlax
$ gbax train --rom emerald --scenario catch-snorlax --player ./my_bot.py
$ gbax tournament --rom "mortal kombat" \
    --scenario mk-arcade-easy \
    --player "python -m gbax.data.bots.press_a" \
    --player "python -m gbax.data.bots.random"

Scenarios choose their own observation shape (raw bytes, structured dict, framebuffer) and scoring criteria. Two reference scenarios ship in the wheel: mk-arcade-easy (Mortal Kombat Advance ladder) and smb3-world-1-1 (Super Mario Advance 4, World 1-1). Full design at docs/automation.md.

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

One command on Linux x86_64 and you're done. The wheel ships a prebuilt mgba_libretro.so inside it, so there's no cmake, no apt-get, no $GBAX_CORE_PATH to set — gbax play emerald works on a fresh box.

The bundled core is built from mgba-emu/mgba at the tag pinned in .mgba-version — currently 0.10.5, MPL-2.0, upstream license shipped alongside at gbax/cores/LICENSE.mGBA. Want to swap in a debug build, a different mGBA version, or another libretro core? Point $GBAX_CORE_PATH at your own .so; the env var always wins.

Outside Linux x86_64 (macOS, Windows, ARM, ancient glibc) pip falls through to the sdist, which carries no binary. gbax play will exit at startup with instructions; bring your own core and set $GBAX_CORE_PATH. PRs adding more platform wheels are welcome.

Full coverage in docs/installing.md: lookup order, supported distros, version bumps, compliance.

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
Macros — record + replay input sequences via Ctrl+R, F1-F9 (see Macros)
Per-game plugins — Python plugins expose /state and /actions for Pokémon, etc.
Bundled libretro core — pip install gbax ships a working emulator on Linux x86_64
Fullscreen + GPU-accelerated linear upscale (F11), runtime filter toggle (F10)
CRT / scanline / hqx shaders via wgpu
macOS / Windows / aarch64 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.5.0.tar.gz (991.6 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.5.0-py3-none-manylinux_2_28_x86_64.whl (875.3 kB view details)

Uploaded Python 3manylinux: glibc 2.28+ x86-64

File details

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

File metadata

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

File hashes

Hashes for gbax-0.5.0.tar.gz
Algorithm Hash digest
SHA256 48f63a3dc36046a3afd7a3c11cfb057455284b35e5363a762380576a361e77bb
MD5 f44a41a166ecceb59f66bd6e974e6bb5
BLAKE2b-256 5fa83c0fb97a37a828c91c6581228833f4ee51b9d5e3ebb9b508adc827e3b0e6

See more details on using hashes here.

Provenance

The following attestation bundles were made for gbax-0.5.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.5.0-py3-none-manylinux_2_28_x86_64.whl.

File metadata

  • Download URL: gbax-0.5.0-py3-none-manylinux_2_28_x86_64.whl
  • Upload date:
  • Size: 875.3 kB
  • Tags: Python 3, manylinux: glibc 2.28+ x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for gbax-0.5.0-py3-none-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 686d24e64ed76fba3e6b72c40cfd55ddb858c14b07d4ad80939774b22e63da9c
MD5 a339168ea4fb66c0ab9c9071fb368eef
BLAKE2b-256 ba25fb8d5d3ab1ee4368e07c33e84e2f8009e20d9d262e36b0b4fd35440ea631

See more details on using hashes here.

Provenance

The following attestation bundles were made for gbax-0.5.0-py3-none-manylinux_2_28_x86_64.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