ETHAN CROOKS

GBA Programming - Tilemaps and Objects

2021-03-10

Here is a quick overview of what I've managed to get working on the GBA so far. There was a ton of iteration that I am largely hazy about, so this will just be a brief look without going too in depth.

I have since installed a local history extension so that I can look back on all my mistakes and dead ends so that I can eternalise them forever :)

Hello world

a

Where better to start than a hello world? To get this working, I needed to first work out how to load binary files into the game rom. Thankfully due to my hours of faffing about with the makefile I had already seen how to get this working:

%.o : %.pcx
    @echo $(notdir $<)
    @$(bin2o)

%.bin.o	%_bin.h : %.bin
    @echo $(notdir $<)
    @$(bin2o)

Buried within the DevKitPro makefile chain is the bin2o variable, which runs a tool named bin2o.exe to build an object file to pack into the final rom.

The same tool also generates a header file, allowing me to easily use the binary in my project.

/* Generated by BIN2S - please don't edit directly */
#pragma once
#include <stddef.h>
#include <stdint.h>

extern const uint8_t font_img_bin[];
extern const uint8_t font_img_bin_end[];
#if __cplusplus >= 201103L
static constexpr size_t font_img_bin_size=288;
#else
static const size_t font_img_bin_size=288;
#endif

With that done, I moved onto working with the DISPCNT and BGCNT0 registers, controlling how the main display and the first background layer behaves.

I wanted to use structs to make interacting with the registers as obvious as possible. If I were to use bitwise operations and the like manually, my code can quickly become incomprehensible. Interacting with structs makes my intentions much clearer.

That being said, it took a bit of trial and error to get the structs working how they should. I had to define a couple macros to set the aligned and packed flags, to tell GCC to make sure these structs exactly line up with the registers that they represent:

#define PK_AL(align) __attribute__((aligned(align), packed))
#define PK __attribute__((packed))

With that all worked out, I started to write out the structs.


struct dispcnt {
    /* 0-2 */ unsigned int bg_mode : 3;
    /* 3   */ char __reserved1 : 1;
    /* 4   */ unsigned int display_frame_select : 1;
    /* 5   */ bool hblank_interval_free : 1;
    /* 6   */ bool linear_obj_char_vram_mapping : 1;
    /* 7   */ bool fblank : 1;
    /* 8   */ bool bg0_on : 1;
    /* 9   */ bool bg1_on : 1;
    /* 10  */ bool bg2_on : 1;
    /* 11  */ bool bg3_on : 1;
    /* 12  */ bool obj_on : 1;
    /* 13  */ bool win0_display_flag : 1;
    /* 14  */ bool win1_display_flag : 1;
    /* 15  */ bool winobj_display_flag : 1;
} PK_AL(2);

/*
    PK_AL(2) tells GCC to pack all the struct elements closely together,
    and to make sure to align it to a 2-byte boundary.
*/

const struct dispcnt dispcnt_zero = {};

struct bgcnt {
    /* 0-1 */ unsigned int priority : 2;
    /* 2-3 */ unsigned int char_base_16k_block : 2;
    /* 4-5 */ char __reserved1 : 2;
    /* 6   */ bool mosaic : 1;
    /* 7   */ bool palette_256 : 1;
    /* 8-12*/ unsigned int screen_base_2k_block : 5;
    /* 13  */ char __reserved2 : 1;
    /*14-15*/ unsigned int screen_size : 2;
} PK_AL(2);

const struct bgcnt bgcnt_zero = {};

Hopefully bool test = dispcnt->bg0_on is way clearer than bool test = discnt & 1 << 8, and the compiler should still be doing the same bitwise nonsense under the hood anyway.

I made a similar struct defining a entry in the background map.

struct bgmap {
    /* 0-9 */ unsigned int tile : 10;
    /* 10  */ bool flipH : 1;
    /* 11  */ bool flipV : 1;
    /*12-15*/ unsigned int palette : 4;
} PK_AL(2);

Here is the array describing the indexes of the hello world, using the font I made.

const u8 hello_world_indexes[] = {0,1,2,2,3,8,4,3,5,2,6,7};

From there I can start initialising everything. The example projects included in DevKitPro showed me that I can use the CpuFastSet syscall to quickly load the binary files into VRAM, and to quickly clear areas of memory such as the map data, so long as the size copied is divisible by 32.

struct dispcnt* dispcnt = (struct dispcnt*)0x4000000;
*dispcnt = dispcnt_zero;
dispcnt->bg_mode = 0;
dispcnt->bg0_on = true;

// Copy the palette data
for (u8 i=0; i<font_pal_bin_size; i++) {
    ((u8*)BG_COLORS)[i] = font_pal_bin[i];
}

struct bgcnt* const bgcnt = (struct bgcnt*)BGCTRL;

*bgcnt = bgcnt_zero;
bgcnt->char_base_16k_block = 0;
bgcnt->screen_base_2k_block = 31;

struct bgmap* map = MAP_BASE_ADR(31);

// Clear Map
*(u32 *)MAP_BASE_ADR(31) = 0;
CpuFastSet(MAP_BASE_ADR(31), MAP_BASE_ADR(31), FILL | COPY32 | (0x800/4));

// Fill the map with the hello world tile indexes.
for (u8 i=0; i<sizeof(hello_world_indexes); i++) {
    map[i].tile = hello_world_indexes[i];
} 

// Load the tile data into VRAM.
CpuFastSet(font_img_bin, (u16*)VRAM,(font_img_bin_size/4) | COPY32);

 Heel!olWerd - The letters are all messed up

Hmm. That doesn't seem right.

// I forgot about the space tile, so I was off by one :)
const u8 hello_world_indexes[] = {1,2,3,3,4,9,5,4,6,3,7,8};

There we go.

Hello world! - All fixed now!

Very nice!

My first object

An arrow

I then moved onto working with OBJs, starting with the task of loading the 4 tiles of my arrow info VRAM. This was a bit trickier than loading the background tiles, as the 4 tiles of my arrow need to be lined up in a 2x2 square in memory.

After a lot of iteration, I ended up with the following function, which copies a series of tiles into a destination with a given width and height.


/* Copy a series of 8x8 tiles with a given width and height.
e.g, tiles [1,2,3,4] with size 2x2 gets copies as two rows,
[1,2] and [3,4]. */
void Copy8x8TileArea(void* source, void* dest, u32 width, u32 height) {
    // A tile is 32 bytes, so 8 words.
    const u32 tile_word_size = 0x8;

    // A tile is 32 bytes, and a row is 32 tiles.
    const u32 row_size = 1024;

    while (height-- > 0) {
        // Since a tile is always 32 bytes, CpuFastSet is a nice fit.
    	CpuFastSet(
    		source, 
    		dest,
    		COPY32 | (tile_word_size * width)
    	);
    	source = source + (tile_word_size * width * 4);
    	dest += row_size;
    }
}

Looking at the tiles in VRAM, showing the the arrow loaded in correctly

Here we can see the arrow sitting in VRAM, with the tiles lining up correctly. Now onto working with OAM (Object Attribute Memory) - a table which stores the details about each object.

I started by defining all the data structures I need to work with OAM. First a few enums describing obj mode, size and shape:

enum OBJ_MODE {
    OBJ_MODE_NORMAL = 0,
    OBJ_MODE_SEMI_TRANSPARENT = 1, 
    OBJ_MODE_WINDOW = 2,
};

enum OBJ_SHAPE {
    OBJ_SHAPE_SQUARE = 0,
    OBJ_SHAPE_HORIZONTAL = 1,
    OBJ_SHAPE_VERTICAL = 2
};

enum OBJ_SIZE {
    OBJ_SIZE_8 = 0,
    OBJ_SIZE_16 = 1,
    OBJ_SIZE_32 = 2,
    OBJ_SIZE_64 = 3
};

And then a struct defining an entry into OAM.

struct oam_regular {
    /* 0-7 */ unsigned int y : 8;
    /*FALSE*/ bool affine_enabled : 1;
    /* 9   */ bool disabled : 1;
    /*10-11*/ enum OBJ_MODE mode : 2;
    /* 12  */ bool mosaic_enabled : 1;
    /* 13  */ bool palette_256 : 1;
    /*14-15*/ enum OBJ_SHAPE shape : 2;

    /* 0-8 */ unsigned int x : 9;
    /* 9-11*/ unsigned int __reserved : 3;
    /* 12  */ bool horizontal_flip : 1;
    /* 13  */ bool vertical_flip : 1;
    /*14-15*/ enum OBJ_SIZE size : 2;

    /* 0-9 */ unsigned int tile : 10;
    /*10-11*/ unsigned int priority : 2;
    /*12-15*/ unsigned int pallette : 3;

    unsigned int __affine_param_space : 16;
} PK_AL(4);

const struct oam_regular oam_regular_zero = { .affine_enabled = 0 };

As you can see, there is 16 bits of empty space at the end of each entry. This is filled by the affine parameter table, which I shall get to in a bit. However, I do still need to make sure that writing to OAM leaves these 16 bits untouched. I wrote what is possibly the weirdest code I've written yet to solve this:

void WriteToOAM(const void* data, u8 index) {
/* 
    Only the first 6 bytes of each entry to OAM is the object data.
    The last two bytes are reserved for the rotation/scaling params.

    This weirdness makes sure the last two bytes are untouched.
*/

    // This masks off the oam attribute quarter.
    // It's backwards because of little-endian.
    const u32 lower_mask = 0x0000FFFF;
    u32* source = (u32*)data;
    u32* dest = (u32*)OAM;

    u32 upper = source[0];
    u32 lower = source[1];

    // Write to upper byte, as normal
    dest[index*2] = upper;

    lower &= lower_mask;
    lower |= (dest[index*2+1] & (~lower_mask));

    dest[index*2+1] = lower;
}

This obviously involved a lot of tinkering. Stuff like the little-endian trap I only worked out by experimenting and debugging and staring at the mGBA memory viewer. I learned a bunch doing this, to be fair.

Here's the actual setup of OAM for the arrow sprite.

const struct oam_regular empty_sprite = {
    .disabled = true,
};

// Clear OAM
WriteToOAM(&empty_sprite, 0);
CpuFastSet(OAM, OAM, FILL | COPY32 | (256));

const struct oam_regular sprite_base = {
    .shape = OBJ_SHAPE_SQUARE,
    .size = OBJ_SIZE_16,
    .x = 100,
    .y = 100,
    .tile = 0,
};

WriteToOAM(&sprite_base, 0);

void* obj_tile_addr = (void*)0x6010000;

Copy8x8TileArea((void*)arrow_img_bin, obj_tile_addr, 2, 2);

That's pretty much everything to get an obj working!

Let make the arrow move as well, just for the hell of it.

double counter = 0;

while (1) {
    // Using std's sin() is very slow. I'll get to this later :)
    main_sprite->x = 100 + 20 * sin(counter);

    counter += 0.1;

    UpdateKeyDown();
    VBlankIntrWait();
}

The arrow displays correctly, bobbing from left to right.

Very nice indeed!

In the next blog post, I will be figuring out how to use affine transformations to allow my sprites to rotate and scale.

Source code, at time of post

Source code, up to date