A live-reload sandbox for designing generative Game Boy (DMG) cartridges. Edit
C in game/, save, and the patched ROM reloads inside a real SameBoy core in
the browser. Each piece derives an artwork — and a set of named traits — from
a seed + token id supplied by the host.
The reference cartridge is a minimal "Hello Shapes" demo: it draws one
shape with the stock GBDK drawing library (gb/drawing.h) and derives its
traits (palette, shape, size, fill) from the seed. It's fully deterministic —
no interactivity — so the captured frame is the piece. It builds as a plain
32 KiB ROM-only cartridge. Replace its main.c to make your own piece.
macOS and Node ≥ 18. The one-time SameBoy build also needs Emscripten and
RGBDS (brew install emscripten rgbds); GBDK is downloaded automatically.
npm install
npm run setup # downloads GBDK + builds SameBoy (once)
npm run dev # starts the live-reload sandboxOpen the URL Vite prints. Edit game/src/main.c — save triggers a recompile +
reload. Edit game/manifest.json and click Apply in the Bootloader panel
to re-patch the in-memory ROM (no recompile).
seed + tokenId + preview byte
│
▼
host patches ROM (3 fixed addresses)
│
▼
cartridge derives the piece
│
├─► writes traits → host reads them back as labels
│
└─► sets capture flag → host saves the frame as a PNG
The cartridge is deterministic for a given seed/token. The host:
- Patches the seed/token/preview bytes into the compiled ROM.
- Runs the cart inside SameBoy.
- Polls a trigger byte in WRAM. When the cart raises "traits ready", the host reads the trait array and decodes each index against the manifest. When the cart raises "capture", the host saves the current frame as a PNG.
game/manifest.json is the artist-authored contract — a boot:dmg@1.0.0
manifest that ships next to the ROM. The platform reads spec + capture; the
dmg section is what the cartridge bridge (and this sandbox) reads:
WRAM addresses can be anywhere in 0xC000..0xDFFF. The reference demo parks
the communication area at the top of WRAM (0xDFA0..0xDFFF) to stay out of
the way of globals + stack.
npm run packagebuilds the ROM and writes package/<rom>.zip — the artist's upload for
bootloader.art, containing just the compiled .gb ROM and manifest.json. The
DMG bootloader runs on bootloader's generic-web infrastructure, which wraps the
ROM in a SameBoy runtime at publish time.
Each trait is a (name, values[]) pair. The cartridge writes a 1-based
index into the trait byte; the host decodes it to its label. 0 means
"unset" and is hidden.
// In game/src/main.c — indices match the manifest "traits" order
#define TRAIT_PALETTE 0u
#define TRAIT_SHAPE 1u
void publish_traits(void) {
BL_TRAITS[TRAIT_PALETTE] = palette + 1u; // 1=CANON, 2=NEGATIVE, ...
BL_TRAITS[TRAIT_SHAPE] = shape + 1u; // 1=BOX, 2=CIRCLE, 3=CROSS
}
// main() pulses BL_DO_TRIGGER_TRAITS_EXTRACTION() once, to tell the host
// the trait bytes are ready to read.To add a trait: append it to manifest.traits[], add a TRAIT_* index in
main.c matching the manifest order, and write one more byte. Patch / trigger
constants live in game/include/bootloader.h.
When the piece has reached its canonical state, the cartridge pulses the capture trigger:
BL_DO_TRIGGER_CAPTURE(); // host saves the current frame as a PNGThe host capture is a single still PNG, so the cart should hold a finished, identical frame from the moment it pulses the trigger — the capture is the piece. (The reference cart is static after its first draw, so this is free.)
previewMode (the third patch byte) lets the cart skip a title screen,
shorten intros, or render a different "thumbnail" framing when the host is
generating marketplace previews. The Hello-Shapes cart doesn't use it — it's
there for your piece to read via BL_PREVIEW_MODE.
GBDK ships png2asset (in .toolchain/gbdk/bin/) which converts a PNG into
C arrays for tile data + tilemap. The PNG must use 4 colours mapped to DMG
shades 0..3 — Aseprite's indexed mode with a 4-colour palette works directly.
.toolchain/gbdk/bin/png2asset art.png -map -c art.c -spr8x8
#include "art.c" // exposes art_tiles[] + art_map[]
set_bkg_data(0, art_TILE_COUNT, art_tiles);
set_bkg_tiles(0, 0, art_WIDTH, art_HEIGHT, art_map);For sprites use -spr8x8 (or -spr8x16). For seamless tile-based patterns
align your art on the 8×8 grid in Aseprite. See png2asset --help for full
options.
| Key | Action |
|---|---|
| Arrow keys / Z X / Enter Shift | DMG joypad |
| Space | Pause / resume |
| C | Capture frame |
| R | Reload ROM (re-patch with current seed/token) |
| 1 / 2 / 3 | Toggle Graphics / Audio / Bootloader debug panels |
The reference Hello Shapes cart is non-interactive, so the joypad keys have
no visible effect; the rest are host controls. Your own cart can read input
with joypad().
State is encoded into the URL so links capture the configuration:
?seed=ABCD&token=0001&preview=1&palette=dmg
| Param | Default | Notes |
|---|---|---|
seed |
0000 |
Hex; patched into the entropy address. |
token / tokenId |
0001 |
Hex; patched into the token address. |
preview |
1 |
Preview-mode flag byte. |
palette |
dmg |
mono, dmg, pocket, print. |
bootSkip / skipBoot |
0 |
Skip the DMG boot-ROM animation (fast boot). |
audio |
1 |
Enable APU audio output. |
volume |
0.55 |
Audio volume, 0–1. |
graphics / gfx |
1 |
Show the Graphics debug section. |
audioPanel / adbg |
1 |
Show the Audio debug section. |
bootloader / bdbg |
1 |
Show the Bootloader debug section. |
SameBoy (MIT) · GBDK-2020 (zlib) · Pan Docs
MIT. Bundled tools keep their own licenses: SameBoy (MIT), GBDK-2020 (zlib).
{ "spec": "boot:dmg@1.0.0", // Platform capture config (shared with the generic-web bootloader). "capture": { "mode": "trigger", // wait for the cart's capture trigger "viewPortDimension": { "width": 640, "height": 576 } // 160×144, ×4 }, "dmg": { "binary": "bootloader-demo.gb", // ROM filename, sits beside the manifest // Host overwrites these ROM bytes before each load. "romPatch": { "entropy": { "address": "0x7ffb", "size": 2, "endianness": "little" }, "tokenId": { "address": "0x7ffd", "size": 2, "endianness": "little" }, "previewMode": { "address": "0x7fff", "size": 1 } }, // Host reads these WRAM bytes while the cart runs. "communication": { "traitBase": "0xdfa0", // up to 64 bytes of trait indices "traitBytes": 64, "trigger": "0xdfff", "triggerFlags": { "capture": 1, "traits": 2 } }, "traits": [ { "name": "Palette", "values": ["CANON", "NEGATIVE", "DUSK", "SOFT"] }, { "name": "Shape", "values": ["BOX", "CIRCLE", "CROSS"] } // …one entry per trait, in the order the cart writes them ] } }