Skip to content

objkt-com/bootloader-dmg-sandbox

Repository files navigation

bootloader: DMG dev sandbox

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.

Quick start

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 sandbox

Open 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).

How a piece runs

       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:

  1. Patches the seed/token/preview bytes into the compiled ROM.
  2. Runs the cart inside SameBoy.
  3. 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.

The manifest

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:

{
  "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
    ]
  }
}

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.

Packaging for upload

npm run package

builds 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.

Configuring traits

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.

Capturing the canonical frame

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 PNG

The 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.

Importing artwork from Aseprite / PNG

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.

Keyboard

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().

URL parameters

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, 01.
graphics / gfx 1 Show the Graphics debug section.
audioPanel / adbg 1 Show the Audio debug section.
bootloader / bdbg 1 Show the Bootloader debug section.

Credits

SameBoy (MIT) · GBDK-2020 (zlib) · Pan Docs

License

MIT. Bundled tools keep their own licenses: SameBoy (MIT), GBDK-2020 (zlib).

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors