Tiles & Maps
Tile modes (0-2) are the backbone of GBA graphics. The display hardware composites 8x8 pixel tiles from VRAM, using a tilemap to arrange them into backgrounds. This is extremely memory-efficient and the scrolling is handled entirely by hardware.
How it works
- Tile data (the pixel art) is stored in VRAM “character base blocks”
- Tilemap (which tile goes where) is stored in VRAM “screen base blocks”
- Palette maps pixel indices to colours
- The hardware reads the map, looks up each tile, applies the palette, and draws the scanline
Loading tile data
Tile graphics are usually pre-converted at build time and copied into VRAM. Each 8x8 tile in 4bpp mode is 32 bytes (4 bits per pixel, 64 pixels):
#include <gba/peripherals>
#include <gba/dma>
#include <gba/video>
// Assuming tile_data is a const array in ROM
extern const unsigned short tile_data[];
extern const unsigned int tile_data_size;
// Copy tile data to character base block 0 (0x06000000)
gba::reg_dma[3] = gba::dma::copy(
tile_data,
gba::memory_map(gba::mem_vram_bg),
tile_data_size / 4
);
Setting up a background
// Configure BG0: 256x256, 4bpp tiles
// Character base = 0 (tile data at 0x06000000)
// Screen base = 31 (map at 0x0600F800)
gba::reg_bgcnt[0] = {
.charblock = 0,
.screenblock = 31,
.size = 0, // 256x256 (32x32 tiles)
};
// Scroll BG0
gba::reg_bgofs[0][0] = 0;
gba::reg_bgofs[0][1] = 0;
Background sizes
| Size value | Dimensions (pixels) | Dimensions (tiles) |
|---|---|---|
| 0 | 256x256 | 32x32 |
| 1 | 512x256 | 64x32 |
| 2 | 256x512 | 32x64 |
| 3 | 512x512 | 64x64 |
Scrolling
Scrolling is a single register write per axis:
gba::reg_bgofs[0][0] = scroll_x; // BG0 horizontal offset
gba::reg_bgofs[0][1] = scroll_y; // BG0 vertical offset
The hardware wraps seamlessly at the background boundaries. A 256x256 background scrolled past x=255 wraps back to x=0 - perfect for side-scrolling games.
Here is a scrollable checkerboard built from two solid tiles:
#include <gba/interrupt>
#include <gba/video>
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, .enable_bg0 = true};
gba::reg_bgcnt[0] = {.screenblock = 31};
// Palette
gba::pal_bg_mem[0] = {.red = 2, .green = 2, .blue = 6};
gba::pal_bg_bank[0][1] = {.red = 10, .green = 14, .blue = 20};
gba::pal_bg_bank[0][2] = {.red = 4, .green = 6, .blue = 12};
// Tile 1: solid light (palette index 1)
gba::mem_tile_4bpp[0][1] = {
0x11111111, 0x11111111, 0x11111111, 0x11111111, 0x11111111, 0x11111111, 0x11111111, 0x11111111,
};
// Tile 2: solid dark (palette index 2)
gba::mem_tile_4bpp[0][2] = {
0x22222222, 0x22222222, 0x22222222, 0x22222222, 0x22222222, 0x22222222, 0x22222222, 0x22222222,
};
// Fill the 32x32 tilemap with a checkerboard
for (int ty = 0; ty < 32; ++ty)
for (int tx = 0; tx < 32; ++tx)
gba::mem_se[31][tx + ty * 32] = {
.tile_index = static_cast<unsigned short>(((tx ^ ty) & 1) ? 2 : 1),
};
int scroll_x = 0, scroll_y = 0;
while (true) {
gba::VBlankIntrWait();
++scroll_x;
++scroll_y;
gba::reg_bgofs[0][0] = static_cast<short>(scroll_x);
gba::reg_bgofs[0][1] = static_cast<short>(scroll_y);
}
}
