Rewriting a GBA game in Rust — gssa
I’m Nicola, I’ll discuss how I converted a C game written as a student assignment into Rust, and tell you about all the friends (later referred as “little UBs”) I made along the way.
The year is 2018, as assignment in professor Leone’s systems programming class, I write a game for the Game Boy Advance (GBA), a console released in 2001, without any operating system, a blazing fast CPU of 16.8 MHz, a mind-bending 256Kb of RAM (it ought to be enough for everyone) and a fantastical memory-mapped (mmio) interface to program graphics, audio, basically everything the GBA can do.
In short, the perfect device to teach bare-metal programming.
In the year 2018, I write my first significant C program. A game for the GBA, I called it “Generic Space Shooter Advance” (GSSA). A stinky pile of undefined behavior (UB). UB is what you get when you write C code the C compiler can assume could never possibly exist (about one out of every three lines of C ever written). It is such a stinky pile, in fact, that for a while, it only worked on inaccurate GBA emulators, until I spent about 2 weeks to suss out where I was accessing invalid memory addresses (hint: in way too many places).
After those two weeks, I could finally play GSSA on mgba
(an accurate GBA emulator).
Yet, when I opened the live memory view of mgba
,
I could see random bits in memory being swapped for no apparent reasons,
but since it didn’t touch the bits of memory containing my game data or the mmio
registers, my game worked (kinda).
The development of the game was over.
A shame we never learned about UB at uni (sorry Mr Leone).
The year is 2020, one year in my first job as scala developer, trying to fight the existential dread of spending most of my waking hours helping make the world a worse place, working on a depressingly slow virtualized dev machine, where every key presses is followed by a 500ms coffee break before even altering anything on screen. I now know elm, Haskell, scala and rust. I’ve read “Expert C programming” twice, I now can see some of the previously invisible UBs that plagued my game, all the little UBs, here in front of me, sneering, provoking, spitting me, telling me “You’ll never catch us!”
I hate it, I despise them.
I learn about the gba crate. Finally, an escape hatch. A way to live in the impossible world I always wanted to live, the world where I make the best selling GBA game, without constantly fighting the little UBs of the C tribe.
A way to escape the morose every day life of an investment bank developer, I can enter again a world without allocations, without an unknowable Garbage Collector that somehow makes all my program eat about 500MB of RAM at minimum (not a problem, just spin up a new prod server, say my colleagues).
I feel like playing TIS-100 again, but this time I can write it on my resume.
Let’s port GSSA to Rust, call it gssa-rs, free it from the tyranny of the little UBs, and brag about it on the internet.
Setup
Look at that, look at those setup instructions, so terse, so simple.
rustup install nightly
rustup +nightly component add rust-src
sudo apt-get install binutils-arm-none-eabi
# copy an example from the github.com/rust-console/gba repository
# Add gba as a dependency to the Cargo.toml
cargo run
It works! Just like that, I even get full code completion and a working
package manager, cargo
my love, my savior.
The sbt exorcism
Begone sbt
, my calvary, my daily ordeal for the sins of scala.
Begone the 10 seconds startup time, the excruciating compilation times.
Begone sbt
’s cryptic configuration, each dependency an additional shibboleth,
each compilation target an incantation, each error message a gnomic curse.
Begone sbt
, the oxymoronically named build tool.
Oh what is this, a compilation error in my rust dependency?
Hmm, a change to the rust asm!
macro.
This, the swi
instruction, I did learn in Mr Leone’s lecture.
A requirement to summon the GBA’s BIOS functions.
Despite my unreasonably zealous belief, rust still manages to amaze me: Now, even writing inline assembly is safe(ish). A quick fix, a quick merge.
- asm!(/* ASM */ "swi 0x06"
- :/* OUT */ "={r0}"(div_out), "={r1}"(rem_out)
- :/* INP */ "{r0}"(numerator), "{r1}"(denominator)
- :/* CLO */ "r3"
- :/* OPT */
+ asm!(
+ "swi 0x06",
+ inout("r0") numerator => div_out,
+ inout("r1") denominator => rem_out,
+ out("r3") _,
+ options(nostack, nomem),
+ );
Making the game
Now is the time, little UBs. Watch out. Your time is up, time to demolish your house of card, and each little nook of my C code you hide yourselves in.
Time to rebuild GSSA on sounder foundations.
Sprites and color palettes
I have sprites, the GBA video processor needs those sprites in video memory (VRAM). I need the sprites to be in the game ROM.
In C, using DevkitPro, it’s easy.
Add the binary to a resource
directory,
then add resource
to Devkit’s Makefile
’s DATA
list.
You then can #include "menuset_til_bin.h"
in the C’s header file
(this is fine 🥲).
How to do this in Rust? Looks like include_bytes!
it is.
So let’s replace those _bin
s with include_bytes!
.
…
error[E0080]: it is undefined behavior to use this value
--> src/assets.rs:138:5
|
138 | pub(crate) const menuset_til: &[Color; 12288] = unsafe {
139 | transmute(include_bytes!("../resources/menuset_til.bin"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ constructing invalid value at .0.data.data:
| encountered an unaligned reference (required 2 byte alignment but found 1)
|
= note: The rules on what exactly is undefined behavior aren't clear, so this check might be
overzealous. Please open an issue on the rustc repository if you believe it should not be
considered undefined behavior.
= note: the raw bytes of the constant (size: 64, align: 4) {
0x00 │ ╾─alloc26─╼ 06 00 00 00 ╾alloc381─╼ 00 00 00 00 │ ╾──╼....╾──╼....
0x10 │ ╾─alloc27─╼ 06 00 00 00 ╾alloc390─╼ 00 00 00 00 │ ╾──╼....╾──╼....
0x20 │ ╾─alloc28─╼ 06 00 00 00 ╾alloc399─╼ 00 00 00 00 │ ╾──╼....╾──╼....
0x30 │ ╾─alloc29─╼ 06 00 00 00 ╾alloc408─╼ 00 00 00 00 │ ╾──╼....╾──╼....
}
ROGNTUDJU
What even is this? Why is rust showing me circuit diagrams with random numbers now?
Ok, let’s just store the sprites in an array of bytes (&[u8]
)
instead of an array of Color
s, aka “shorts,” or u16
(&[Color]
).
Honestly
To get the previous screenshot, I reverted to the version of the game and compiler used before fixing this bug.
But I never managed to reproduce the bug. The screenshot is actually an artist interpretation, edited from the “working” version.
Now my menu is broken! I’ve seen this in the C version. It was easy to solve, just move around some unrelated variables and it works again!
No such luck with rust, let’s take closer attention to rust’s error message:
error[E0080]: it is undefined behavior to use this value
…
constructing invalid value at .0.data.data: encountered an unaligned reference (required 2 byte alignment but found 1)
It’s an UB! Rustc ruthlessly (rustlessly huehue) seizes the little UB jerk, and slay it without giving it the opportunity to make your life a misery (that is, until you work around it without paying attention).
unaligned reference. Yes, this we learned about in Mr Leone’s lectures.
Alignment, this is important for the GBA processor, ARM7TDMI. To write data to VRAM, you must only use 2 byte length store instructions. 1 byte length stores will still write 2 bytes, storing one and an undefined value in the other (normally, it should be the same value repeated twice, but for some reasons I ended up with one out of two values zeroed out).
The workaround is a bit involved, but here it is, UB-free title screen!
To be perfectly fair, I did solve it more correctly in the C version, using the GBA DMA for writing to VRAM.
No nonsense tl;dr
Even knowing about alignment didn’t stop me from ignoring it when storing to memory in C.
Compiler errors help catch, and understand bugs before they even exist. Rust’s high quality error messages help understand what caused the error and actually put in practice your knowledge to write correct code.
History repeats itself
The year is now 2022.
I vanquished my dread, I now left my salaried job and I spend the day contributing to an open source game engine. I’ve become a cyber-hippie. Kumbaya!
Two weeks away from keyboard. I pick up my unworn laptop.
I bore myself quickly from the lacking literature I picked up departing.
I find an old abandoned project, gssa-rs.
Very rustic, it compiles, I can run it on mgba
,
but it is nothing more than the title screen.
Ah! Time to hone my skill on embedded rust and finally complete this dream of mine.
I’m free now, I can do it.
I go all in, writing a full hardware abstraction layer (HAL),
pick apart GBA documentations, and in the last 12 hours,
get around to implement menus beyond the title screen.
Back from the trip, I discover on my desktop PC a major uncommitted set of changes on the local copy of the gssa-rs repository. The five days of intensive programming I did in vacation were the programmer’s first sin: “Duplicated effort.” Perusing the code, I find eerily familiar 2 years old solutions to problems I had just two days ago.
I don’t remember any of this. Oh, what is this entry in my notebook? A 300 lines document describing my experience converting GSSA to rust, dated September 2020. In fact, it is this very article (before editing).
Amazing! Sure, a sin, but an opportunity as well. I get a peek at how I would solve the exact same problem at a 2 year interval. Did my experience since then changed the way write code? Do I write better code, worse code?
What I think of my 2020 code, I’ll leave for a later post.
(you may be interested in this blog’s rss feed)
Teaser
Dread fills me. What will I see? I’ve gazed in the abyss of legacy entreprise™ 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 monstruosity?