The infrastructure of agb
agb
is an open source project I run for creating games for the GBA in Rust.
We have a website and a repository on GitHub.
In this article I will discuss how we run the agb
project and the technologies we use.
The monorepo
agb
uses a monorepo.
All the crates, the public games, the book, the website, the test runner, and the source of truth for the template are in the agbrs/agb
repository.
While the agb
project isn’t large by any means, having all this together I think has proven to be valuable with how we can make atomic changes.
We use just
to provide helpful commands to perform useful actions such as running the entire test suite and building the website which requires a fair number of dependencies.
There’s no real reason we use just
over a Makefile
.
Continuous integration
Our test suite covers both code that is run on the host and code that is run on the GBA. On the host we run tests for the hashmap and fixed point number implementations. The real benefit to this is for hashmaps as it contains unsafe code that we can run with miri to gain confidence in its soundness. Fixed point numbers have no real benefit of being run on the host platform, but as it is platform agnostic it currently is run on the host architecture.
We run agb
tests 4 times.
We compile it for thumb, and run tests in release mode and debug mode.
We also compile it for arm and run tests in both again.
While we expect people to use thumb (usually faster with higher code density), we still use arm.
Why?
There are two main reasons:
- Some people want to use arm
- It should help catch unsafe code that is actually UB
The saying we have is that anything not checked by CI will eventually be broken. We’ve had this with arm and multiboot1 in the past. Therefore we check that both still compile.
In the past we’ve had code that runs in debug or release mode be broken due to UB in unsafe code. Thankfully running in both debug and release has caught this by one giving broken results. Running the tests 4 times helps to try get at least one of them to use different behaviour. It would be nice to run the tests with miri, but that certainly feels improbable.
Running tests on the GBA
In Rust, our test harness runs each test and wraps them with log messages and we accurately define the start and end times using a write to a random unused hardware register. Our test runner is built on top of mGBA. We intercept the log messages to decide when tests start and finish and whether they have failed, the log message of writing to an unused hardware register is used to measure timings. Currently a test cannot expect to fail as it will kill the whole test run, and if a single test fails then the remaining tests won’t be run. This could be an improvement that we make.
While you can do all the usual assertions, the test runner has the additional power of being able to assert the content of the screen. This is done by using the test using a log message that conveys to the test runner that it should inspect the screen and compare it to the image at the provided path. If the image doesn’t exist it is written to it (and fails the test run). We’ve relied on these tests during optimisation and API changes to make sure the outputs are the same.
Furthermore, we also run our doctests which gives us confidence in them being up to date and correct. Due to limitations in Rust, the tests used to be only compiled and not run, so it was surprising how many of them worked when we ran them.
Daily nightly runs
We keep up to date with Rust nightly by running our CI process every day. This regularly breaks the build with new Rust lints or a variety of other changes. We like this! By having CI break, we keep up to date with the newest Rust features and maintain our lint situation. If we were to suddenly update, the number of lints would be insurmountable.
Sometimes nightly acts like nightly and core features break. This is increasingly rare, but we have a workaround. For the daily runs we always use the latest nightly, but for runs from merging to master or PR runs we use a known good version. This allows us to keep moving in terms of development (and also makes the contribution experience not awful) while letting us know if our issue has been resolved. By running every day, we’ve also helped out bisecting the Rust compiler to find where the issue was introduced as we know the exact version that caused the breakage.
PRs
We use standard open source practices, each developer using a fork and we raise PRs into agb
.
When a PR is merged, we use a GitHub merge queue which could slow us down, but as both of us use either GitButler or Jujutsu it’s not that big a problem to use stacked branches.
I decided to use a merge queue not because we face problems from merging lots of PRs, but because of the principle.
Each PR also reminds us to consider updating the changelog, if this wasn’t there the changelog almost certainly would never be updated.
Releasing agb
Every now and then, we decide to release agb
.
Nowadays this is quite a smooth process, we have a just
command that uses the release tool to update all the version numbers in all the crates and runs the tests to make sure it’s not catastrophically broken.
It generates a commit for these changes and tags it.
We then push the commit and the tag directly - this is the only part that could be considered uncomfortable.
Then CI picks it up and performs the actual publishes to crates.io.
The publish tool uses a graph of the crates dependencies and in parallel starting from the leaves publishes the crates.
This used to take a long time but improvements to crates.io have made it relatively quick.
Once all the publishes have completed, we kick off the process of building and releasing the build server. There is a small amount of downtime as the new website with the new examples are available but only the old build server that could be incompatible with the new code. We are not concerned by this.
The website
The website has the following pages:
- Home page, featuring an emulator with built in games made during various game jams.
- Examples, small usually interactive examples that show how to use various features of
agb
. The example code is editable and compilable straight in the browser. - Book, comprehensive documentation for how to use the various parts of
agb
including tutorials and other articles. - Crash page, when
agb
crashes it includes a QR code that takes you to the crash page. On this page you can see the backtrace and when given the elf file can give you line numbers. - Showcase, where we include games made with
agb
by various people and link out to where you can play them. Includes a small description and a couple screenshots of each.
The website itself is made using Next that we statically compile and serve with GitHub pages. I’ll discuss a couple technical aspects for each of these.
Home page
The emulator on the home page is a modified mGBA compiled to wasm. The bulk of the modifications are part of Nicholas VanCise’s fork, but we layered on a couple more fixes for frame rates. We build mGBA and the game collection during our CI process.
Examples
The examples come from the latest released agb
version.
We do this by checking out the latest tag and using the examples from there.
The examples page contains images from each of the examples, these are generated during CI.
Each example is built and run using our own screenshot generator which is another mGBA wrapper that runs the game for a few frames and saving the screen as a screenshot.
The source code and screenshots are then bundled into an archive to be included in the website.
On an example page, you have the example running in an emulator on the right and the editable source code on the left.
The editor is codemirror which I think provides a very nice editor experience.
The Build and Run
button takes the code and submits it to our server to compile it and send the generated .gba
file back.
The build server
This server runs on a small Digitalocean droplet with all the dependencies pre compiled. Each compilation is run in a fresh docker container with some resource limits. We again build the server and compiler container images in CI and use GitHub packages to make them available to our droplet. There is a deploy script that spawns a new droplet that will pull in the server and compiler images before waiting for the droplet to become live and destroying the old one. This keeps our cloud costs very low. We plan to use Digitalocean’s funding to make the hosting costs zero.
Book
The book is made using mdbook which is quite standard within the Rust community for writing these sorts of resources. For the website build, CI again builds the book for the latest release. Of course we also build the current book to make sure it compiles.
Crash page
When agb
crashes, it displays the panic message along with a QR code.
Scanning this QR code takes you to the agb
website where it is decoded to give you the memory addresses.
The encoding scheme for the code is known as “Gwilym code”.

The screen that is shown when a game made with agb
panics with the backtrace feature enabled
We have a tool called agb-debug
which takes a backtrace generated by agb and gives the underlying memory addresses.
It can also take an elf file or specially prepared gba file and give the filenames and line numbers.
The website includes backtrace
, a project built to wasm that uses agb-debug
as a crate to perform this on the website.
Showcase
A couple of developers have made games using agb
and we want to treat them equally.
For that reason the order of the games are randomised each build.
But each game could be developed by multiple people!
So we also randomise the order of each person in each game each build.
Rust
Obviously we have a Rust project, we write games for the GBA using Rust. However, we use Rust for a lot more.
- Internal tools for maintaining the project are written in Rust.
- We wrap an emulator written in C in Rust.
- We have a server written in Rust.
- We use Rust on the frontend to share code with our
agb-debug
tool.
We absolutely view Rust to be the default choice for not just the main project, but for all the related tooling. It’s a comfortable choice where it’s easy to write reliable code and has an ecosystem that fulfils that goal.
-
A mode of the GBA that allows for games to be sent via link cable into RAM and have code execution start from there. ↩︎