Architecture of a GBA game — gssa

This is part 2 of a series of blog posts on developing GSSA. The first part introduced why I ended up rewriting a C game in Rust for a 20 years old console.

Acronyms and characters

This is where I narrate how I actually implement the menu in my first rewrite of 2020 in rust.

Architecture

A neophyte wrote the GSSA C code. Myself, writing a large program, not only for the first time in C, but for the first time in any language.

In 2020, I’m different. I wrote a whole frontend app in elm, and the corresponding backend in rust. 2020 me is also an enterprise™ developer. I am an acolyte of data driven programming, I’ve been the midwife of many software, seeing them from birth to their maturity.

The C version of GSSA was a struggle. How to structure code? I had never seen game or C code before. I wrote most of the code in public transport, on a shitty 1st gen Microsoft surface, with the shitty “surface cover” finger-numbing keyboard. I didn’t know git. I wrote regressions I took days to fix. Remember, it took weeks to fix the code to run mgba, the accurate GBA emulator.

Dread fills me. What will I see? I’ve gazed in the abyss of legacy enterprise™ code. I’ve seen a massive visual basic app, I’ve seen a compiler written seemingly as an exercise of how to write the most confusing chain of JavaScript callbacks, unspeakable blasphemy, chtonic incantations that somehow a computer can parse, let alone a human being. Someone with less fortitude would have not survived, not with their sanity intact. Will I gaze into my own code, and witness my own spawn as a monstrosity?

Curiously no.

There are gotos, the code may have more magic numbers and mutable globals than local variables, I abuse criminally the C pre-processor, and the code is indeed a barely working pile of little UBs riddled with bugs.

But I can read it. It is clean, I understand the purpose and intent of everything, architectural design decisions are documented, the code is structured coherently, using common patterns.

In fact, it’s almost better than the code I write today 🥲

tldr

bug ridden does not imply bad code.

Furthermore bad code does not imply bug ridden.

experience does not imply better code.

The design principle is clear: ownership of control flow. I structured the GSSA code like a bunch of happy little games that wait their turn and take the helm and drive exclusively the entirety of the console.

The main menu code looks like this:

// main.c
for (;;) {
    bootMenu(gameState);
    playGame(gameState);
}
// mainmenu.c
void bootMenu(GameState* globalState) {
    // ...
    for (;;) {
        frameCount = (frameCount+1) % (BLANK_SEC<<1);
        if (frameCount == BLANK_SEC){
            drawText("     ", 10, 16, BOOT_SBB);
        } else if (frameCount == 0) {
            drawText("Press Start", 10, 16, BOOT_SBB);
        }
        if (CHECK_INPUT(KEY_START)) {
            srand(frameCount);
            goto gameMenu;
        }
        WAIT_BLANK;
    }
    // ...
gameMenu: {
    // ...
    if (TST_FG(curKeys,KEY_A)) { //Changes submenu.
        drawText(" ", 5, currentRow, MAIN_SBB);
        switch (currentRow) {
            //Exits the game menu loop
            case 0:
                break;

            case BEGIN_ROW:
                return; //goes to game

            case SHIP_ROW:
                goto shipMenu;

            case CHEAT_ROW:
                goto cheatMenu;
        }
    }
    // ...
}
shipMenu: {
    // ...
}
cheatMenu: {
    // ...
}
}

The bootMenu() handles the initial screens, including ship selection, difficulty selection, cheat code menu, and the title card. Each one of those represent a different screen. Each screen has a corresponding loop, when the user changes screen, a goto goes to the selected screen’s loop.

Without goto, we bloat the stack, we enter a new context whenever we enter or exit a menu, potentially leading to a stack overflow on the poor GBA’s tinny memory. This implies giving up the concept of functions, but it works.

The C GSSA uses liberally mutable global variables. I pick arbitrary location in memory and split them between all the happy little games:

#define OAMCLONE_MEM 0x03004000
#define oamClone ((spriteTag*) OAMCLONE_MEM)

// ... in another c header

// a single value of size sizeof(PlayerShip)
#define PSHIP_MEM (OAMCLONE_MEM + sizeof(spriteTag) * 128)

// An array of size MAX_BULLET * sizeof(ActiveBullet)
#define ACTIVBULLET_MEM (PSHIP_MEM + sizeof(char) * MAX_BULLET + 8)

// An array of size MAX_ENNEMY * sizeof(ActiveEnnemy)
#define ACTIVENNEMY_MEM (ACTIVBULLET_MEM + sizeof(ActiveBullet) * MAX_BULLET)

// An array of size MAX_PICKUP * sizeof(ActivePickup)
#define ACTIVPICKUP_MEM (ACTIVENNEMY_MEM + sizeof(ActiveEnnemy) * MAX_ENNEMY)

// ...
// In main.c

#define GAMESTATE_MEM 0x03003F00

int main()
{
    GameState* gameState = (GameState*) GAMESTATE_MEM;

I’m sure a few readers ground their teeth to dust seeing this code. But on the GBA it is fine. Remember? No OS, just GSSA running on the hardware. Only risk is for the stack to grow so much they overwrite the hardcoded memory. (Though apparently I was an “ennemy” of spelling when I wrote this)

This is what I learned from Mr Leone’s lecture: some memory regions are faster to read/write than others, so make sure to use the fast ones for critical data structures.

So how will I write the rust version?

No such code in my rewrite. I’ll leave it to the compiler to do the hard work of picking where to place my beloved data structures. (In any case, I will specify the stack memory position in the linker.ld file, this tells the linker which addresses to use for what purpose)

The acolyte of data-driven programming (DDP) than I am now has lost the ability to reason in terms of control flow.

DDP is all about data structures. I first define the game state, and design code as transformations on the state.

GameState is now an enum, each variant relevant to a different point in the game. Each variant its own little state, all describing what to draw on screen and what the next state is based on button input.

There is a single loop, that looks at the state and call its corresponding draw and update methods.

I need to split draw and update on the GBA, changing VRAM memory at a narrow specific interval, otherwise, I’ll see ugly screen tearing.

This is the elm architecture. The discarded cargo, filled with bobbles of another kind of civilisation, that I picked up at university and to this day still praise as gospel of clean code. Regardless, I have other better reasons to idolize cargo (my savior).

In fact, there is a bit more to that. I can split the GameState in two:

Constants will replace all the magic values. This is more code, but I don’t have to uncover what 5 or 10 means.

GSSA's main menu
const MAIN_MENU: Menu<3> = Menu {
    entries: ["BEGIN GAME !!!", "Ship select", "Code zone"],
    pos: (7, 4),
    dir: MenuDir::Verti(3),
};

A Menu tells me how to draw something based on the global GameState. Each struct has a draw method that takes as argument everything that is mutable and it needs to know. In the case of Menu the signature of draw is as follow:

fn draw(&self, selected: u8, tile_map: Sbb) {

(where tile_map is going to be a point to the location in memory we need to write to to display things, it has nothing to do with the Schweizerische Bundesbahnen)

This architecture greatly reduces the size of what is going to be passed around in drawing and updating routines (the state), which is necessary for performance reasons. One positive side effect of this is that the menus are entirely declaratively defined.

The corresponding C code fails to decouple drawing from updating, but on the flip side, is dead easy to understand:

drawText("BEGIN GAME !!!", 7,  BEGIN_ROW, MAIN_SBB);
drawText("Ship Select", 7,     SHIP_ROW,  MAIN_SBB);
drawText("Code zone", 7,       CHEAT_ROW, MAIN_SBB);

Yes, you’ll find magic numbers all around the code, this is quite ugly, but not a difficulty to code comprehension.

There is more to it though. The blinking cursor. The damn blinking cursor. You think it’s easy? My draw method only has access to the current state, but I need to erase the cursor at the previous position.

The cursor is not in the struct definition of Menu, it can’t since the cursor position changes and Menu is immutable. The cursor must be part of the GameState.

With the C model, the menu has an internal state updated every frames, and draws based on that state in the same loop:

frameCount = (frameCount+1) % BLANK_SEC;
if (frameCount == 0 || frameCount == 4800) {
    drawText(">", curPos, 5, MAIN_SBB);
} else if (frameCount == 2400 || frameCount == 7200) {
    drawText(" ", curPos, 5, MAIN_SBB);
}

This snippet is repeated in every submenu loops.

With the rust model, the cursor is represented as a text box:

fn cursor(pos: (u8, u8)) -> TextBox {
    TextBox {text: ">",pos,blink_rate: Some(30)}
}

And the update:

fn update(&self, cycle: u16, selected: Diff<u8>, tile_map: Sbb) {
    if selected.changed() {
        cursor(self.cursor_pos(selected.prev)).clear(tile_map);
    }
    cursor(self.cursor_pos(selected.current)).update(cycle, tile_map);
}

More precisely, the blinking happens in the TextBox::update method:

pub fn update(&self, cycle: u16, tile_map: Sbb) {
    if let Some(blink_rate) = self.blink_rate {
        let tick = cycle % blink_rate;
        if tick == 0 {
            self.clear(tile_map);
        } else if tick == blink_rate / 2 {
            self.draw(tile_map);
        }
    }
}

It’s a bit more verbose.

Note the Diff for the selected entry. I need to know the previous state to clear the cursor if it changed position. I can do that, because update is not updating self (notice the &self, non-mutable!) it is creating a new self, this way, the old self is kept around, and can be used later to compare new and old value, and execute code when relevant.

This doubles memory requirement, but for simple menus, this works great and the memory footprint is tinny enough to not be a worry.

The C code has the following snippet wherever it needs to change the cursor position:

if (TST_FG(curKeys, KEY_UP)) {
    drawText(" ", 5, currentRow, MAIN_SBB);
    currentRow = (currentRow == FIRST_ROW)? LAST_ROW :    (currentRow - 3);
    drawText(">", 5, currentRow, MAIN_SBB);
} else if (TST_FG(curKeys, KEY_DOWN)) {
    drawText(" ", 5, currentRow, MAIN_SBB);
    currentRow = (currentRow == LAST_ROW)? FIRST_ROW :    (currentRow + 3);
    drawText(">", 5, currentRow, MAIN_SBB);
}

This demonstrates also the unabashed mixing between state change and drawing in the C version.

Let’s try this again

Forget everything you just read. Or rather, let me forget that I ever wrote that. The year is now 2022, no hoverboards, just even dimmer hopes, and me, a bored dude, alone with his computer. And I do what most lone bored dude with computers do: rewrite a C GBA game in Rust.

This time I take a different approach. My first action is to split hardware access and game code, this is called a HAL (hardware abstraction layer). I call it haldvance, because I am a victim of puns.

The second decision I make, is to use a MVC approach. I first design the data, and then, I… I mean I just spent a thousand words telling you about that, but for me this was a new and innovative solution.

Architecture bis

Oh, but in the mean time, I forgot about all this fancy “diffing” I nicely described earlier. My update methods take a &mut self, and the previous state is not kept track of.

The split between draw and update has become my nemesis. Why? When I change from one state to another, this typically requires “exceptional” draw operations. For example, when starting the game, I change the video mode, generate a star map and load the game’s sprites into video memory. But here is the question: How do I know that I “just entered” the game state? I only have the current state, outside of any context of changes.

Oh I see several solutions to this, but I made the mistake of trying to solve it before thinking about it. Here are the possibilities:

I initially went with callbacks. My game currently uses this design. It probably kills perfs by preventing potential inlining. But more than that, it only partially solves the issue, because now I have to surface callbacks from inner functions, handle conflicts of callbacks… Fun in perspective.

Disaster, my prophetic faith in DDP is shook. Maybe I should take another approach? This is a question for another time.

HAL (elujah)

Now let’s talk about the HAL.

Generally, designing and writing a completely generic, fully documented, opinionated and flexible library API takes much more time than going at the hardware with a raw hammer and accepting your hardware access bugs will surface all around your game logic.

For reference, currently the HAL is 2K lines of code with over 800 lines of documentation, while the game itself is about 1K lines of code (suspicious, he doesn’t say how much doc the game has).

And development is sluggish: every time I add a functionality, I have to first design an API for it, and wrap the hardware, thinking about the cheapest way of doing it, thinking of edge cases, explore rust features allowing them, and all of that is before I even get started writing game code, which itself is a slog, because this 2nd rust rewrite started with a crippling architectural decision.

Ah! But at least the separation between gameplay and hardware code did help me understand in isolation why certain things do not work. It’s also so much easier to read, and as I write the HAL, I’m writing an exhaustive manual on how to write GBA games in Rust, with directions in the more hairy parts of the API.

The HAL is great, but I worry it’s a massive waste of time, we are talking about an abstraction layer for a 20 years old console. And why? Just because. A weird obsession.

And more

Now my second rewrite reached feature parity with the first one. Ah, accidentally. It is time to start making strides toward feature parity with The C Version. Fun in perspective, to the next post!