Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Embedding Images

The <gba/embed> header converts image files into GBA-ready data entirely at compile time. Combined with C23’s #embed directive, this replaces external asset pipelines like grit with a single #include and a constexpr variable.

For procedural sprite generation without source image files, see Shapes. For animated sprite-sheet workflows, see Animated Sprite Sheets. For type-level API details, see Embedded Sprite Type Reference.

This page focuses on still images: framebuffers, tilemaps, and single-frame sprites.

Supported formats

FormatVariantsTransparency
PPM24-bit RGBIndex 0
PNGGrayscale, RGB, indexed, grayscale+alpha, RGBA (8-bit channels)Alpha < 50%
TGAUncompressed, RLE, true-colour (15/16/24/32bpp), colour-mapped, grayscaleAlpha < 50%

Format is auto-detected from the file header.

Conversion functions

FunctionOutputBest for
bitmap15Flat gba::color arrayMode 3 or software blitters
indexed44bpp sprite payload + 16-colour palette + tilemapBackgrounds and 4bpp sprites
indexed88bpp tiles + 256-colour palette + tilemap8bpp backgrounds
indexed4_sheet<FrameW, FrameH>sheet4_resultAnimated OBJ sheets; covered on the next page

All converters take a supplier lambda returning std::array<unsigned char, N>.

Quick start

#include <gba/embed>

static constexpr auto bg = gba::embed::indexed4([] {
    return std::to_array<unsigned char>({
#embed "background.png"
    });
});

static constexpr auto hero = gba::embed::indexed4<gba::embed::dedup::none>([] {
    return std::to_array<unsigned char>({
#embed "hero.png"
    });
});

Use dedup::none for OBJ sprites so tiles stay in 1D sequential order. Use the default dedup::flip for backgrounds to save VRAM when tiles repeat.

Example: scrollable background with sprite

This demo embeds a 512x256 background image and a 16x16 character sprite, both as PNG files. The D-pad scrolls the background, and holding A + D-pad moves the sprite:

#include <gba/bios>
#include <gba/embed>
#include <gba/interrupt>
#include <gba/keyinput>
#include <gba/video>

#include <cstring>

constexpr auto bg = gba::embed::indexed4([] {
    return std::to_array<unsigned char>({
#embed "bg_2x1.png"
    });
});

constexpr auto hero = gba::embed::indexed4<gba::embed::dedup::none>([] {
    return std::to_array<unsigned char>({
#embed "sprite.png"
    });
});

int main() {
    gba::irq_handler = {};
    gba::reg_dispstat = {.enable_irq_vblank = true};
    gba::reg_ie = {.vblank = true};
    gba::reg_ime = true;

    gba::reg_dispcnt = {.video_mode = 0, .linear_obj_tilemap = true, .enable_bg0 = true, .enable_obj = true};
    gba::reg_bgcnt[0] = {.screenblock = 30, .size = 1}; // 512x256

    for (auto&& x : gba::obj_mem) {
        x = {.disable = true};
    }

    // Background palette + tiles
    std::memcpy(gba::memory_map(gba::pal_bg_mem), bg.palette.data(), sizeof(bg.palette));
    std::memcpy(gba::memory_map(gba::mem_tile_4bpp[0]), bg.sprite.data(), bg.sprite.size());

    // Background map: stored in screenblock order, memcpy directly
    std::memcpy(gba::memory_map(gba::mem_se[30]), bg.map.data(), sizeof(bg.map));

    // Sprite palette + tiles (no deduplication - sequential for 1D mapping)
    std::memcpy(gba::memory_map(gba::pal_obj_bank[0]), hero.palette.data(), sizeof(hero.palette));
    std::memcpy(gba::memory_map(gba::mem_vram_obj), hero.sprite.data(), hero.sprite.size());

    int scroll_x = 0, scroll_y = 0;
    int sprite_x = 112, sprite_y = 72;

    gba::object hero_obj = hero.sprite.obj();
    hero_obj.y = static_cast<unsigned short>(sprite_y & 0xFF);
    hero_obj.x = static_cast<unsigned short>(sprite_x & 0x1FF);
    gba::obj_mem[0] = hero_obj;

    gba::keypad keys;
    for (;;) {
        gba::VBlankIntrWait();
        keys = gba::reg_keyinput;

        if (keys.held(gba::key_a)) {
            // A + D-pad moves the sprite
            sprite_x += keys.xaxis();
            sprite_y += keys.i_yaxis();

            hero_obj.y = static_cast<unsigned short>(sprite_y & 0xFF);
            hero_obj.x = static_cast<unsigned short>(sprite_x & 0x1FF);
            gba::obj_mem[0] = hero_obj;
        } else {
            // D-pad scrolls the background
            scroll_x += keys.xaxis();
            scroll_y += keys.i_yaxis();

            gba::reg_bgofs[0][0] = static_cast<short>(scroll_x);
            gba::reg_bgofs[0][1] = static_cast<short>(scroll_y);
        }
    }
}

Scrollable background with sprite

How it works

The background uses a 2x1 screenblock layout (size = 1 in reg_bgcnt), giving 64x32 tiles (512x256 pixels). The indexed4 map is stored in GBA screenblock order, so the entire map can be written to VRAM with one std::memcpy.

The sprite uses dedup::none so its tiles remain sequential - exactly what the GBA expects for 1D OBJ mapping. Without this, deduplication could merge mirrored tiles and break the sprite layout.

Transparent pixels (alpha < 128 in the PNG source) become palette index 0, so the hardware automatically shows the background through the sprite.

Tile deduplication

The indexed4 and indexed8 converters accept a dedup mode as a template parameter:

ModeBehaviourUse case
dedup::flip (default)Matches identity, horizontal flip, vertical flip, and bothBackground tilemaps
dedup::identityMatches exact duplicates onlyTilemaps without flip support
dedup::noneNo deduplication; tiles stay sequentialOBJ sprites
using gba::embed::dedup;

constexpr auto bg = gba::embed::indexed4(supplier);
constexpr auto obj = gba::embed::indexed4<dedup::none>(supplier);

When dedup::flip is active, matching tiles reuse an existing tile index and encode flip flags in the emitted screen_entry. This keeps map VRAM usage low for symmetric art.

Sprite OAM helpers

When image dimensions match a valid GBA sprite size, indexed4 returns a sprite payload with obj() and obj_aff() helpers:

constexpr auto sprite = gba::embed::indexed4<gba::embed::dedup::none>([] {
    return std::to_array<unsigned char>({
#embed "sprite.png"
    });
});

gba::obj_mem[0] = sprite.sprite.obj(0);
gba::obj_aff_mem[0] = sprite.sprite.obj_aff(0);

Valid sprite sizes:

ShapeSizes
Square8x8, 16x16, 32x32, 64x64
Wide16x8, 32x8, 32x16, 64x32
Tall8x16, 8x32, 16x32, 32x64

If the source image does not match one of those shapes, obj() and obj_aff() fail at compile time.

Transparency and palettes

  • PPM: palette index 0 is always reserved as transparent; the first visible colour becomes index 1.
  • PNG: RGBA/GA alpha maps transparent pixels (alpha < 128) to palette index 0.
  • TGA: 32bpp alpha and 16bpp attribute-bit transparency map transparent pixels (alpha < 128) to palette index 0.
  • indexed4: images may spread across multiple palette banks when background tiles use <= 15 opaque colours per tile.
  • indexed8: one 256-entry palette is shared across the whole image.

Constexpr evaluation limits

All image conversion happens at compile time. Large assets can hit GCC’s constexpr operation limit. If you see constexpr evaluation operation count exceeds limit, raise the limit for that target:

target_compile_options(my_target PRIVATE -fconstexpr-ops-limit=335544320)

Small sprites usually fit within default limits. Large backgrounds, especially 512x256 maps, often need a higher ceiling.