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;
}
}
