Introduction

This is the book for learning how to write GBA games in Rust.

I'm Lokathor, the main author of the book. There's also Ketsuban who provides the technical advisement, reviews the PRs, and keeps my crazy in check.

The book is a work in progress, as you can see if you actually try to open many of the pages listed in the Table Of Contents.

Feedback

It's also often hard to tell when you've explained something properly to someone who doesn't understand the concept yet. Please, if things don't make sense then file an issue about it so I know where things need to improve.

Reader Requirements

This book naturally assumes that you've already read Rust's core book:

Now, I know it sounds silly to say "if you wanna program Rust on this old video game system you should already know how to program Rust", but the more people I meet and chat with the more they tell me that they jumped into Rust without reading any or all of the book. You know who you are.

Please, read the whole book!

In addition to the core book, there's also an expansion book that I will declare to be required reading for this:

The Rustonomicon is all about trying to demystify unsafe. We'll end up using a fair bit of unsafe code as a natural consequence of doing direct hardware manipulations. Using unsafe is like swinging a sword, you should start slowly, practice carefully, and always pay attention no matter how experienced you think you've become.

That said, it's sometimes a necessary tool to get the job done, so you have to break out of the borderline pathological fear of using it that most rust programmers tend to have.

Book Goals and Style

So, what's this book actually gonna teach you?

I'm not gonna tell you how to use a crate that already exists.

Don't get me wrong, there is a gba crate, and it's on crates.io and all that jazz.

However, unlike most crates that come with a tutorial book, I don't want to just teach you how to use the crate. What I want is to teach you what you need to know so that you could build the crate yourself, from scratch, if it didn't already exist for you. Let's call it the Handmade Hero school of design. Much more than you might find in other Rust crate books, I'll be attempting to show a lot of the why in addition to just the how. Once you know how to do it all on your own, you can decide for yourself if the gba crate does it well, or if you think you can come up with something that suits your needs better.

Overall the book is sorted for easy review once you're trying to program something, and the GBA has a few interconnected concepts, so some parts of the book end up having to refer you to portions that you haven't read yet. The chapters and sections are sorted so that minimal future references are required, but it's unavoidable.

The actual "tutorial order" of the book is the Examples chapter. Each section of that chapter breaks down one of the provided examples in the examples directory of the repository. We go over what sections of the book you'll need to have read for the example code to make sense, and also how we apply the general concepts described in the book to the specific example cases.

Development Setup

Before you can build a GBA game you'll have to follow some special steps to setup the development environment.

Once again, extra special thanks to Ketsuban, who first dove into how to make this all work with rust and then shared it with the world.

Per System Setup

Obviously you need your computer to have a working rust installation. However, you'll also need to ensure that you're using a nightly toolchain (we will need it for inline assembly, among other potential useful features). You can run rustup default nightly to set nightly as the system wide default toolchain, or you can use a toolchain file to use nightly just on a specific project, but either way we'll be assuming the use of nightly from now on. You'll also need the rust-src component so that cargo-xbuild will be able to compile the core crate for us in a bit, so run rustup component add rust-src.

Next, you need devkitpro. They've got a graphical installer for Windows that runs nicely, and I guess pacman support on Linux (I'm on Windows so I haven't tried the Linux install myself). We'll be using a few of their general binutils for the arm-none-eabi target, and we'll also be using some of their tools that are specific to GBA development, so even if you already have the right binutils for whatever reason, you'll still want devkitpro for the gbafix utility.

  • On Windows you'll want something like C:\devkitpro\devkitARM\bin and C:\devkitpro\tools\bin to be added to your PATH, depending on where you installed it to and such.
  • On Linux you can use pacman to get it, and the default install puts the stuff in /opt/devkitpro/devkitARM/bin and /opt/devkitpro/tools/bin. If you need help you can look in our repository's .travis.yml file to see exactly what our CI does.

Finally, you'll need cargo-xbuild. Just run cargo install cargo-xbuild and cargo will figure it all out for you.

Per Project Setup

Once the system wide tools are ready, you'll need some particular files each time you want to start a new project. You can find them in the root of the rust-console/gba repo.

  • thumbv4-none-agb.json describes the overall GBA to cargo-xbuild (and LLVM) so it knows what to do. Technically the GBA is thumbv4-none-eabi, but we change the eabi to agb so that we can distinguish it from other eabi devices when using cfg flags.
  • crt0.s describes some ASM startup stuff. If you have more ASM to place here later on this is where you can put it. You also need to build it into a crt0.o file before it can actually be used, but we'll cover that below.
  • linker.ld tells the linker all the critical info about the layout expectations that the GBA has about our program, and that it should also include the crt0.o file with our compiled rust code.

Compiling

Once all the tools are in place, there's particular steps that you need to compile the project. For these to work you'll need some source code to compile. Unlike with other things, an empty main file and/or an empty lib file will cause a total build failure, because we'll need a no_std build, and rust defaults to builds that use the standard library. The next section has a minimal example file you can use (along with explanation), but we'll describe the build steps here.

  • arm-none-eabi-as crt0.s -o target/crt0.o

    • This builds your text format crt0.s file into object format crt0.o that's placed in the target/ directory. Note that if the target/ directory doesn't exist yet it will fail, so you have to make the directory if it's not there. You don't need to rebuild crt0.s every single time, only when it changes, but you might as well throw a line to do it every time into your build script so that you never forget because it's a practically instant operation anyway.
  • cargo xbuild --target thumbv4-none-agb.json

    • This builds your Rust source. It accepts most of the normal options, such as --release, and options, such as --bin foo or --examples, that you'd expect cargo to accept.
    • You can not build and run tests this way, because they require std, which the GBA doesn't have. If you want you can still run some of your project's tests with cargo test --lib or similar, but that builds for your local machine, so anything specific to the GBA (such as reading and writing registers) won't be testable that way. If you want to isolate and try out some piece code running on the GBA you'll unfortunately have to make a demo for it in your examples/ directory and then run the demo in an emulator and see if it does what you expect.
    • The file extension is important! It will work if you forget it, but cargo xbuild takes the inclusion of the extension as a flag to also compile dependencies with the same sysroot, so you can include other crates in your build. Well, crates that work in the GBA's limited environment, but you get the idea.

At this point you have an ELF binary that some emulators can execute directly (more on that later). However, if you want a "real" ROM that works in all emulators and that you could transfer to a flash cart to play on real hardware there's a little more to do.

  • arm-none-eabi-objcopy -O binary target/thumbv4-none-agb/MODE/BIN_NAME target/ROM_NAME.gba

    • This will perform an objcopy on our program. Here I've named the program arm-none-eabi-objcopy, which is what devkitpro calls their version of objcopy that's specific to the GBA in the Windows install. If the program isn't found under that name, have a look in your installation directory to see if it's under a slightly different name or something.
    • As you can see from reading the man page, the -O binary option takes our lovely ELF file with symbols and all that and strips it down to basically a bare memory dump of the program.
    • The next argument is the input file. You might not be familiar with how cargo arranges stuff in the target/ directory, and between RLS and cargo doc and stuff it gets kinda crowded, so it goes like this:
      • Since our program was built for a non-local target, first we've got a directory named for that target, thumbv4-none-agb/
      • Next, the "MODE" is either debug/ or release/, depending on if we had the --release flag included. You'll probably only be packing release mode programs all the way into GBA roms, but it works with either mode.
      • Finally, the name of the program. If your program is something out of the project's src/bin/ then it'll be that file's name, or whatever name you configured for the bin in the Cargo.toml file. If your program is something out of the project's examples/ directory there will be a similar examples/ sub-directory first, and then the example's name.
    • The final argument is the output of the objcopy, which I suggest putting at just the top level of the target/ directory. Really it could go anywhere, but if you're using git then it's likely that your .gitignore file is already setup to exclude everything in target/, so this makes sure that your intermediate game builds don't get checked into your git.
  • gbafix target/ROM_NAME.gba

    • The gbafix tool also comes from devkitpro. The GBA is very picky about a ROM's format, and gbafix patches the ROM's header and such so that it'll work right. Unlike objcopy, this tool is custom built for GBA development, so it works just perfectly without any arguments beyond the file name. The ROM is patched in place, so we don't even need to specify a new destination.

And you're finally done!

Of course, you probably want to make a script for all that, but it's up to you. On our own project we have it mostly set up within a Makefile.toml which runs using the cargo-make plugin.

Hello, Magic

So we know all the steps to build our source, we just need some source.

We're beginners, so we'll start small. With normal programming there's usually a console available, so the minimal program prints "Hello, world" to the terminal. On a GBA we don't have a terminal and standard out and all that, so the minimal program draws a red, blue, and green dot to the screen.

At the lowest level of device programming, it's all Magic Numbers. You write special values to special places and then the hardware does something. A clear API makes every magic number and magic location easy to understand. A clear and good API also prevents you from using the wrong magic number in the wrong place and causing problems for yourself.

This is the minimal example to just test that our build system is all set, so just this once we'll go full magic number crazy town, for fun. Ready? Here goes:

hello_magic.rs:

#![feature(start)]
#![no_std]

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
  loop {}
}

#[start]
fn main(_argc: isize, _argv: *const *const u8) -> isize {
  unsafe {
    (0x400_0000 as *mut u16).write_volatile(0x0403);
    (0x600_0000 as *mut u16).offset(120 + 80 * 240).write_volatile(0x001F);
    (0x600_0000 as *mut u16).offset(136 + 80 * 240).write_volatile(0x03E0);
    (0x600_0000 as *mut u16).offset(120 + 96 * 240).write_volatile(0x7C00);
    loop {}
  }
}

Throw that into your project skeleton, build the program, and give it a run. You should see a red, green, and blue dot close-ish to the middle of the screen. If you don't, something already went wrong. Double check things, phone a friend, write your senators, try asking Lokathor or Ketsuban on the Rust Community Discord, until you're eventually able to get your three dots going.

Of course, I'm sure you want to know why those numbers are the numbers to use. Well that's what the whole rest of the book is about!

Newtype

There's one thing I want to get out of the way near the start of the book and it didn't really have a good fit anywhere else in the book so it goes right here.

We're talking about the "Newtype Pattern"!

Now, I told you to read the Rust Book before you read this book, and I'm sure you're all good students who wouldn't sneak into this book without doing the required reading, so I'm sure you all remember exactly what I'm talking about, because they touch on the newtype concept in the book twice, in two very long named sections:

...Yeah... The Rust Book doesn't know how to make a short sub-section name to save its life. Shame.

Newtype Basics

So, we have all these pieces of data, and we want to keep them separated, and we don't wanna pay the cost for it at runtime. Well, we're in luck, we can pay the cost at compile time.


# #![allow(unused_variables)]
#fn main() {
pub struct PixelColor(u16);
#}

Ah, except that, as I'm sure you remember from The Rustonomicon (and from the RFC too, of course), if we have a single field struct that's sometimes different from having just the bare value, so we should be using #[repr(transparent)] with our newtypes.


# #![allow(unused_variables)]
#fn main() {
#[repr(transparent)]
pub struct PixelColor(u16);
#}

Ah, and of course we'll need to make it so you can unwrap the value:


# #![allow(unused_variables)]
#fn main() {
#[repr(transparent)]
pub struct PixelColor(u16);

impl From<PixelColor> for u16 {
  fn from(color: PixelColor) -> u16 {
    color.0
  }
}
#}

And then we'll need to do that same thing for every other newtype we want.

Except there's only two tiny parts that actually differ between newtype declarations: the new name and the base type. All the rest is just the same rote code over and over. Generating piles and piles of boilerplate code? Sounds like a job for a macro to me!

Making It A Macro

The most basic version of the macro we want goes like this:


# #![allow(unused_variables)]
#fn main() {
#[macro_export]
macro_rules! newtype {
  ($new_name:ident, $old_name:ident) => {
    #[repr(transparent)]
    pub struct $new_name($old_name);
  };
}
#}

Except we also want to be able to add attributes (which includes doc comments), so we upgrade our macro a bit:


# #![allow(unused_variables)]
#fn main() {
#[macro_export]
macro_rules! newtype {
  ($(#[$attr:meta])* $new_name:ident, $old_name:ident) => {
    $(#[$attr])*
    #[repr(transparent)]
    pub struct $new_name($old_name);
  };
}
#}

And we want to automatically add the ability to turn the wrapper type back into the wrapped type.


# #![allow(unused_variables)]
#fn main() {
#[macro_export]
macro_rules! newtype {
  ($(#[$attr:meta])* $new_name:ident, $old_name:ident) => {
    $(#[$attr])*
    #[repr(transparent)]
    pub struct $new_name($old_name);
    
    impl From<$new_name> for $old_name {
      fn from(x: $new_name) -> $old_name {
        x.0
      }
    }
  };
}
#}

That seems like enough for all of our examples, so we'll stop there. We could add more things, such as making the From impl optional (because what if you shouldn't unwrap it for some weird reason?), allowing for more precise visibility controls (on both the newtype overall and the inner field), and maybe even other things I can't think of right now. We won't really need those in our example code for this book, so it's probably nicer to just keep the macro simpler and quit while we're ahead.

As a reminder: remember that macros have to appear before they're invoked in your source, so the newtype macro will always have to be at the very top of your file, or in a module that's declared before other modules and code.

Help and Resources

Help

So you're stuck on a problem and the book doesn't say what to do. Where can you find out more?

The first place I would suggest is the Rust Community Discord. If it's a general Rust question then you can ask anyone in any channel you feel is appropriate. If it's GBA specific then you can try asking me (Lokathor) or Ketsuban in the #gamedev channel.

Emulators

You certainly might want to eventually write a game that you can put on a flash cart and play on real hardware, but for most of your development you'll probably want to be using an emulator for testing, because you don't have to fiddle with cables and all that.

In terms of emulators, you want to be using mGBA, and you want to be using the 0.7 Beta 1 or later. This update lets you run raw ELF files, which means that you can have full debug symbols available while you're debugging problems.

Information Resources

Ketsuban and I didn't magically learn this all from nowhere, we read various technical manuals and guides ourselves and then distilled the knowledge (usually oriented towards C and C++) into this book for Rust.

We have personally used some or all of the following:

  • GBATEK: This is the resource. It covers not only the GBA, but also the DS and DSi, and also a run down of ARM assembly (32-bit and 16-bit opcodes). The link there is to the 2.9b version on problemkaputt.de (the official home of the document), but if you just google for gbatek the top result is for the 2.5 version on akkit.org, so make sure you're looking at the newest version. Sometimes problemkaputt.de is a little sluggish so I've also mirrored the 2.9b version on my own site as well. GBATEK is rather large, over 2mb of text, so if you're on a phone or similar you might want to save an offline copy to go easy on your data usage.
  • TONC: While GBATEK is basically just a huge tech specification, TONC is an actual guide on how to make sense of the GBA's abilities and organize it into a game. It's written for C of course, but as a Rust programmer you should always be practicing your ability to read C code anyway. It's the programming equivalent of learning Latin because all the old academic books are written in Latin.
  • CowBite: This is more like GBATEK, and it's less complete, but it mixes in a little more friendly explanation of things in between the hardware spec parts.

And I haven't had time to look at it myself, The Audio Advance seems to be very good. It explains in depth how you can get audio working on the GBA. Note that the table of contents for each page goes along the top instead of down the side.

Non-Rust GBA Community

There's also the GBADev.org site, which has a forum and everything. They're coding in C and C++, but you can probably overcome that difference with a little work on your part.

I also found a place called GBATemp, which seems to have a more active forum but less of a focus on actual coding.

GBA Limitations

No Floats

Core Only

Volatile Destination

Broad Concepts

CPU

BIOS

Work RAM

IO Registers

Palette RAM

Video RAM

Object Attribute Memory

Game Pak ROM / Flash ROM

Save RAM

Video

RBG15 Color

TODO

Non-Video

Buttons

Timers

Direct Memory Access

Sound

Interrupts

Network

Game Pak

Examples

hello_magic

hello_world

light_cycle

bg_demo