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

Backup Storage

gba::backup is a codec-driven save API built on top of the raw SRAM/Flash/EEPROM access described in Low-Level Save Access. Instead of hand-rolling offsets and byte layouts, you declare one codec per value you want to persist; stdgba builds a small on-cartridge directory that maps each codec to its own record and auto-places every field, fixed- or variable-size, in the backend.

Creating the backup slot

Define each value’s codec as a static constexpr member, then list every codec as a template argument to a backup_* factory. gba::backup.emplace(...) builds the directory (reading it back if the backend already holds one), replaces whatever table was previously held, and returns a reference to the newly built table:

#include <gba/backup>
#include <gba/codec>

struct player_save {
    std::uint8_t x = 0;
    std::uint8_t y = 0;

    static constexpr auto codec = gba::codec::make_tuple_codec(gba::codec::member<&player_save::x>(gba::codec::uint8_codec),
                                                                gba::codec::member<&player_save::y>(gba::codec::uint8_codec))
                                      .apply<player_save>();
};

struct settings_save {
    bool invert = false;

    static constexpr auto codec = gba::codec::make_tuple_codec(gba::codec::member<&settings_save::invert>(gba::codec::bool_codec))
                                      .apply<settings_save>();
};

auto& saves = gba::backup.emplace(gba::backup_sram<player_save::codec, settings_save::codec>);

gba::backup represents the cartridge’s one backup chip, much like gba::link represents the GBA’s one serial port. Like gba::link, it erases its held type until assigned; unlike gba::link, emplace(...) here also builds the table for you, from a backup_* factory, so the common case never needs the held type spelled out. Reach the same table again from elsewhere, without the reference emplace(...) returned, by naming its type explicitly:

using save_table = decltype(gba::backup_sram<player_save::codec, settings_save::codec>)::table_type;

gba::backup.get<save_table>().emplace<player_save>(player);

Available factories: gba::backup_sram (byte-addressable, no setup), gba::backup_flash_64k / backup_flash_128k (4KB sectors, Macronix/Panasonic/Sanyo/SST chips, auto-detected on first access), gba::backup_flash_atmel (128-byte pages), and gba::backup_eeprom_512b / backup_eeprom_8k (8-byte blocks). Every field in a table shares one backend.

Loading and storing values

get<T>() returns std::optional<T>, std::nullopt if the field was never stored (or has been erased). emplace<T>(args...) constructs a T from args, or default-constructs it with none, and stores it, returning false only if the value did not fit:

player_save player = saves.get<player_save>().value_or(player_save{});

if (!saves.get<settings_save>().has_value()) {
    saves.emplace<settings_save>(); // first boot: default-construct and store
}
settings_save settings = *saves.get<settings_save>();

// ...later, in response to input:
saves.emplace<player_save>(player);

T is looked up by decoded value type, so it must be unique across every codec passed to the backup_* factory (a field can also be reached by its declaration index, e.g. saves.get<0>(), which does allow repeated types). erase<T>() erases a field; a subsequent get<T>() reports no value again.

Fixed- vs. variable-size codecs

A codec built entirely from fixed-width components (uint8_codec, bool_codec, make_tuple_codec of such, etc.) is detected at compile time and reserves its bytes once, at the front of the backend, alongside the directory itself; this space can never fragment. A codec that can vary in size (e.g. one built with gba::codec::vector_of()) is placed in a shared dynamic arena that follows the fixed region, and is only ever as large as the value currently stored.

Prefer fixed-size codecs for values with a known upper bound (positions, settings, small structs). Reserve variable-size codecs for genuinely variable-length data; see Redundancy & Fragmentation for how the dynamic arena is kept usable over the cartridge’s lifetime.

Demo: Position and settings persistence

The demo saves the ball’s position on A and toggles (and saves) an inverted-colours setting on SELECT. Both fields live in one SRAM-backed table; the settings field is default-constructed on first boot, when no prior save exists.

/// @file demo_backup_save.cpp
/// @brief Persist a sprite's position and a background colour setting across resets.
///
/// Two independently placed `gba::backup` fields share one SRAM chip: `player_save` (the
/// ball's position, saved with A) and `settings_save` (an inverted-colours toggle, saved
/// with SELECT). Both are loaded once at boot, falling back to defaults when absent.

#include <gba/backup>
#include <gba/bios>
#include <gba/codec>
#include <gba/color>
#include <gba/interrupt>
#include <gba/keyinput>
#include <gba/shapes>
#include <gba/video>

#include <algorithm>
#include <cstring>

using namespace gba::shapes;
using gba::operator""_clr;

namespace {

    constexpr int screen_width = 240;
    constexpr int screen_height = 160;
    constexpr int sprite_size = 16;

    constexpr auto spr_ball = sprite_16x16(circle(8.0, 8.0, 7.0));

    struct player {
        std::uint8_t x = (screen_width - sprite_size) / 2;
        std::uint8_t y = (screen_height - sprite_size) / 2;

        static constexpr auto codec =
            gba::codec::make_tuple_codec(gba::codec::member<&player::x>(gba::codec::uint8_codec),
                                         gba::codec::member<&player::y>(gba::codec::uint8_codec))
                .apply<player>();
    };

    struct settings {
        bool invert = false;

        static constexpr auto codec =
            gba::codec::make_tuple_codec(gba::codec::member<&settings::invert>(gba::codec::bool_codec))
                .apply<settings>();
    };

    int clamp(const int value, const int lo, const int hi) {
        if (value > hi) {
            return value < lo ? lo : hi;
        }
        return value < lo ? lo : value;
    }

    void apply_theme(const bool invert) {
        gba::pal_bg_mem[0] = invert ? "white"_clr : "#102040"_clr;
        gba::pal_obj_bank[0][1] = invert ? "#102040"_clr : "white"_clr;
    }

} // namespace

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

    gba::reg_dispcnt = {
        .linear_obj_tilemap = true,
        .enable_obj = true,
    };

    // One SRAM-backed table for both fields; each is auto-placed on its own aligned offset.
    auto& backup = gba::backup.emplace(gba::backup_sram<player::codec, settings::codec>);

    // Load the ball's last saved position, or fall back to player_save's default centre.
    player player = backup.get<::player>().value_or(::player{});

    // Load the settings, default-constructing and storing them if this is the first boot.
    if (!backup.get<settings>().has_value()) {
        backup.emplace<settings>();
    }
    settings settings = *backup.get<::settings>();

    apply_theme(settings.invert);

    auto* objDst = gba::memory_map(gba::mem_vram_obj);
    std::memcpy(objDst, spr_ball.data(), spr_ball.size());

    auto obj = spr_ball.obj(gba::tile_index(objDst));
    obj.x = player.x;
    obj.y = player.y;
    gba::obj_mem[0] = obj;

    std::fill(std::begin(gba::obj_mem) + 1, std::end(gba::obj_mem), gba::object{.disable = true});

    gba::keypad keys;

    while (true) {
        gba::VBlankIntrWait();
        keys = gba::reg_keyinput;

        const int x = clamp(player.x + keys.xaxis(), 0, screen_width - sprite_size);
        const int y = clamp(player.y + keys.i_yaxis(), 0, screen_height - sprite_size);
        player.x = static_cast<std::uint8_t>(x);
        player.y = static_cast<std::uint8_t>(y);

        // Save on the rising edge of A, so a held button doesn't wear out Flash/EEPROM
        // with repeated writes (SRAM has no such limit, but the pattern still applies).
        if (keys.pressed(gba::key_a)) {
            backup.emplace<::player>(player);
        }

        if (keys.pressed(gba::key_select)) {
            settings.invert = !settings.invert;
            apply_theme(settings.invert);
            backup.emplace<::settings>(settings);
        }

        obj.x = player.x;
        obj.y = player.y;
        gba::obj_mem[0] = obj;
    }
}

Backup save demo

See also