Home

👋 Welcome to gb-asm-tutorial! This tutorial will teach you how to make games for the Game Boy and Game Boy Color.

⚠️

While the Game Boy and Game Boy Color are almost the same console, the Game Boy Advance is entirely different. However, the GBA is able to run GB and GBC games! If you are looking to program GBC games and run them on a GBA, you’re at the right place; however, if you want to make games specifically for the GBA, please check out Tonc instead.

Controls

There are some handy icons near the top of your screen!

  • The “burger” toggles the navigation side panel;
  • The brush allows selecting a different color theme;
  • The magnifying glass pops up a search bar;
  • The printer gives a single-page version of the entire tutorial, which you can print if you want;
  • The GitHub icon links to the tutorial’s source repository;
  • The edit button allows you to suggest changes to the tutorial, provided that you have a GitHub account.

Additionally, there are arrows to the left and to the right of the page (they are at the bottom instead on mobile) to more easily navigate to the next page.

With that said, you can get started by simply navigating to the following page :)

Licensing

In short:

  • Code within the tutorial is essentially public domain, meaning that you are allowed to copy it freely without restrictions.
  • You are free to copy the tutorial’s contents (prose, diagrams, etc.), modify them, and share that, but you must give credit and license any copies permissively.
  • This site’s source code can be freely copied, but you must give a license and copyright notice.

Full details, please follow these links for more information on the respective licenses:

  • All the code contained within the tutorial itself is licensed under CC0. To the extent possible under law, all copyright and related or neighboring rights to code presented within GB ASM Tutorial have been waived. This work is published from France.
  • The contents (prose, images, etc.) of this tutorial are licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
  • Code used to display and format the site is licensed under the MIT License unless otherwise specified.

Roadmap

The tutorial is split into three sections. I strongly advise you go through the tutorial in order!

In Part Ⅰ, we run our first “Hello World!” program, which we then dissect to learn what makes the Game Boy tick.

In Part Ⅱ, we program our first game, a clone of Arkanoid; we learn how to prod the hardware into having something we can call a “game”. Along the way, we will make plenty of mistakes, so we can learn how to debug our code.

And finally, Part Ⅲ is about “advanced” use of the hardware, where we learn how to make even better-looking games, and we program a Shoot ‘Em Up!


Unfortunately, this tutorial is a work in progress. Currently, Part I is being written, and Parts II and III should follow soon after.

Help and feedback

If you are stuck in a certain part of the tutorial, want some advice, or just wish to chat with us, the GBDev community chat is the place to go! I actively participate there, and don’t be afraid to ask questions! (The “ASM” channel should be the most appropriate to discuss the tutorial, by the way.)

If you are interested in contributing to or translating the tutorial, thank you! Follow the link above, I’d be happy to work with you.

Noticed a problem with the tutorial? Please check out our issue tracker; if there is no open issue about your problem, please create a new one, or reach out to us via the link above.

If you prefer email, my address is tutorial@<domain>, where you replace <domain> with this website’s domain name (it ends with .fr). Anti-spam measure, I hope you understand.

Setup

First, we should set up our dev environment. We will need:

  1. A POSIX environment
  2. RGBDS v0.5.1 (though v0.5.0 should be compatible)
  3. GNU Make (preferably a recent version)
  4. A code editor
  5. A debugging emulator

❓😕

The following install instructions are provided on a “best-effort” basis, but may be outdated, or not work for you for some reason. Don’t worry, we’re here to help: ask away in GBDev, and we’ll help you with installing everything!

Tools

Linux & macOS

Good news: you’re already fulfilling step 1! You just need to install RGBDS, and maybe update GNU Make.

macOS

At the time of writing this, macOS (up to 11.0, the current latest release) ships a very outdated GNU Make. You can check it by opening a terminal, and running make --version, which should indicate “GNU Make” and a date, among other things.

If your Make is too old, you can update it using Homebrew‘s formula make. At the time of writing, this should print a warning that the updated Make has been installed as gmake; you can either follow the suggestion to use it as your “default” make, or use gmake instead of make in this tutorial.

Linux

Once RGBDS is installed, open a terminal and run make --version to check your Make version (which is likely GNU Make).

If make cannot be found, you may need to install your distribution’s build-essentials.

Windows

The sad truth is that Windows is a terrible OS for development; however, you can install environments that solve most issues.

On Windows 10, your best bet is WSL, which sort of allows running a Linux distribution within Windows. Install WSL 1 or WSL 2, then a distribution of your choice, and then follow these steps again, but for the Linux distribution you installed.

If WSL is not an option, you can use MSYS2 or Cygwin instead; then check out the bottom of RGBDS’ Windows install instructions. As far as I’m aware, both of these provide a sufficiently up-to-date version of GNU Make.

If you have programmed for other consoles, such as the GBA, check if MSYS2 isn’t already installed on your machine. This is because devkitPro, a popular homebrew development bundle, includes MSYS2.

Code editor

Any code editor is fine; I personally use Sublime Text with its RGBDS syntax package; however, you can use any text editor, including Notepad, if you’re crazy enough. Awesome GBDev has a section on syntax highlighting packages, see there if your favorite editor supports RGBDS.

Emulator

Using an emulator to play games is one thing; using it to program games is another. The two aspects an emulator must fulfill to allow an enjoyable programming experience are:

  • Debugging tools: When your code goes haywire on an actual console, it’s very difficult to figure out why or how. There is no console output, no way to gdb the program, nothing. However, an emulator can provide debugging tools, allowing you to control execution, inspect memory, etc. These are vital if you want GB dev to be fun, trust me!
  • Good accuracy: Accuracy means “how faithful to the original console something is”. Using a bad emulator for playing games can work (to some extent, and even then...), but using it for developing a game makes it likely to accidentally render your game incompatible with the actual console. For more info, read this article on Ars Technica (especially the An emulator for every game section at the top of page 2). You can compare GB emulator accuracy on Daid’s GB-emulator-shootout.

The emulator I will be using for this tutorial is BGB (1.5.9 when I’m writing this). It’s Windows-only, but macOS and Linux users can install Wine to be able to run it, and macOS users will additionally have to use the 64-bit version. Other debugging emulators are possible (such as SameBoy on macOS), but I will be giving directives for and including screenshots of BGB.

Hello World!

In this lesson, we will begin by assembling our first program. The rest of this chapter will be dedicated to explaining how and why it works.

Note that we will need to type a lot of commands, so open a terminal now. It’s a good idea to create a new directory (mkdir gb_hello_world, for example, then cd gb_hello_world to enter the new directory).

Grab the following files (right-click each link, “Save Link As...”), and place them all in this new directory:

Then, still from a terminal within that directory, run the following three commands.

⚠️

To clarify where each individual command begins, I’ve added a $ before each command, but don’t type them!

rgbasm -L -o hello-world.o hello-world.asm
rgblink -o hello-world.gb hello-world.o
rgbfix -v -p 0xFF hello-world.gb

‼️

Be careful with arguments! Some options, such as -o here, use the argument after them as a parameter:

  1. rgbasm -L -o hello-world.asm hello-world.o won’t work (and may corrupt hello-world.asm!)
  2. rgbasm -L hello-world.asm -o hello-world.o will work
  3. rgbasm hello-world.asm -o hello-world.o -L will also work

If you need whitespace within an argument, you must quote it:

  1. rgbasm -L -o hello world.o hello world.asm won’t work
  2. rgbasm -L -o "hello world.o" "hello world.asm" will work

It should look like this:

(If you encounter an error you can’t figure out by yourself, don’t be afraid to ask us! We’ll sort it out.)

Congrats! You just assembled your first Game Boy ROM! Now, we just need to run it; open BGB, right-click the window that opened, and load hello-world.gb.

🤕

If you are using Wine and BGB is failing to start (or gives a blank/black screen), try these troubleshooting tips.

And in any case, don’t hesitate to ask for help!

You could also take a flash cart (I use the EverDrive GB X5, but there are plenty of alternatives), load up your ROM onto it, and run it on an actual console!

Picture of the Hello World running on a physical DMG

Well, now that we have something working, it’s time to peel back the curtains...

The toolchain

So, in the previous lesson, we built a nice little “Hello World!” ROM. Now, let’s find out exactly what we did.

Let’s begin by explaining what rgbasm and rgblink do.

RGBASM is an assembler. It is responsible for reading the source code (in our case, hello-world.asm and hardware.inc), and generating blocks of code with some “holes”. RGBASM does not always have enough information to produce a full ROM, so it does most of the work, and stores its intermediary results in what’s known as object files (hence the .o extension).

RGBLINK is a linker. Its job is taking object files (or, like in our case, just one), and “linking” them into a ROM. RGBLINK’s purpose may not be obvious with programs as simple as this Hello World, but it will become much clearer in Part Ⅱ.

So: Source code → rgbasm → Object files → rgblink → ROM, right? Well, not exactly.

RGBFIX

RGBLINK does produces a ROM, but it’s not quite usable yet. See, actual ROMs have what’s called a header. It’s a special area of the ROM that contains metadata about the ROM; for example, the game’s name, Game Boy Color compatibility, and more. For simplicity, we defaulted a lot of these values to 0 for the time being; we’ll come back to them in Part Ⅱ.

However, the header contains three crucial fields:

When the console first starts up, it runs a little program known as the boot ROM, which reads and draws the logo from the cartridge, and displays the little boot animation. When the animation is finished, the console checks if the logo matches a copy that it stores internally; if there is a mismatch, it locks up! And, since it locks up, our game never gets to run... 😦 This was meant as an anti-piracy measure; however, that measure has since then been ruled as invalid, so don’t worry, we are clear! 😄

Similarly, the boot ROM also computes a checksum of the header, supposedly to ensure that it isn’t corrupted. The header also contains a copy of this checksum; if it doesn’t match what the boot ROM computed, then the boot ROM also locks up!

The header also contains a checksum over the whole ROM, but nothing ever uses it. It doesn’t hurt to get it right, though.

Finally, the header also contains the ROM’s size, which is required by emulators and flash carts.

RGBFIX’s role is to fill in the header, especially these 3 fields, which are required for our ROM to be guaranteed to run fine. The -v option instructs RGBFIX to make the header valid, by injecting the Nintendo logo and computing the two checksums. The -p 0xFF option instructs it to pad the ROM to a valid size, and set the corresponding value in the “ROM size” header field.

Alright! So the full story is: Source code → rgbasm → Object files → rgblink → “Raw” ROM → rgbfix → “Fixed” ROM. Good.

You might be wondering why RGBFIX’s functionality hasn’t been included directly in RGBLINK. There are some historical reasons, but RGBLINK can also be used to produce things other than ROMs (especially via the -x option), and RGBFIX is sometimes used without RGBLINK anywhere in sight.

File names

Note that RGBDS does not care at all about the files’ extensions. Some people call their source code .s, for example, or their object files .obj. The file names don’t matter, either; it’s just practical to keep the same name.

Binary and hexadecimal

Before we talk about the code, a bit of background knowledge is in order. When programming at a low level, understanding of binary and hexadecimal is mandatory. Since you may already know about both of these, a summary of the RGBDS-specific information is available at the end of this lesson.

So, what’s binary? It’s a different way to represent numbers, in what’s called base 2. We’re used to counting in base 10, so we have 10 digits: 0, 1, 2, 3, 4, 5, 6, 7, 8, and 9. Here’s how digits work:

  42 =                       4 × 10   + 2
     =                       4 × 10^1 + 2 × 10^0
                                  ↑          ↑
    These tens come from us counting in base 10!

1024 = 1 × 1000 + 0 × 100  + 2 × 10   + 4
     = 1 × 10^3 + 0 × 10^2 + 2 × 10^1 + 4 × 10^0
       ↑          ↑          ↑          ↑
And here we can see the digits that make up the number!

ℹ️

^ here means “to the power of”, where X^N is equal to multiplying X with itself N times, and X ^ 0 = 1.

Decimal digits form a unique decomposition of numbers in powers of 10 (decimal is base 10, remember?). But why stop at powers of 10? We could use other bases instead, such as base 2. (Why base 2 specifically will be explained later.)

Binary is base 2, so there are only two digits, called bits: 0 and 1. Thus, we can generalize the principle outlined above, and write these two numbers in a similar way:

  42 =                                                    1 × 32  + 0 × 16  + 1 × 8   + 0 × 4   + 1 × 2   + 0
     =                                                    1 × 2^5 + 0 × 2^4 + 1 × 2^3 + 0 × 2^2 + 1 × 2^1 + 0 × 2^0
                                                              ↑         ↑         ↑         ↑         ↑         ↑
                                          And since now we're counting in base 2, we're seeing twos instead of tens!

1024 = 1 × 1024 + 0 × 512 + 0 × 256 + 0 × 128 + 0 × 64  + 0 × 32  + 0 × 16  + 0 × 8   + 0 × 4   + 0 × 2   + 0
     = 1 × 2^10 + 0 × 2^9 + 0 × 2^8 + 0 × 2^7 + 0 × 2^6 + 0 × 2^5 + 0 × 2^4 + 0 × 2^3 + 0 × 2^2 + 0 × 2^1 + 0 × 2^0
       ↑          ↑         ↑         ↑         ↑         ↑         ↑         ↑         ↑         ↑         ↑

So, by applying the same principle, we can say that in base 2, 42 is written as 101010, and 2048 as 1000000000. Since you can’t tell ten (decimal 10) and two (binary 10) apart, RGBDS assembly has binary numbers prefixed by a percent sign: 10 is ten, and %10 is two.

Okay, but why base 2 specifically? Rather conveniently, a bit can only be 0 or 1, which are easy to represent as “ON” or “OFF”, empty or full, etc! If you want, at home, to create a one-bit memory, just take a box. If it’s empty, it stores a 0; if it contains something, it stores a 1. Computers thus primarily manipulate binary numbers, and this has a slew of implications, as we will see throughout this entire tutorial.

Hexadecimal

To recap, decimal isn’t practical for a computer to work with, instead relying on binary (base 2) numbers. Okay, but binary is really impractical to work with. Take %10000000000, aka 2048; when in decimal only 4 digits are required, binary instead needs 10! And, did you notice that I actually wrote one zero too many? Fortunately, hexadecimal is here to save the day! 🦸

Base 16 works just the same as every other base, but with 16 digits, called nibbles: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, and F.

  42 =            2 × 16   + 10
     =            2 × 16^1 + A × 16^0

1024 = 4 × 256  + 0 × 16   + 0
     = 4 × 16^2 + 2 × 16^1 + 4 × 16^0

Like binary, we will use a prefix to denote hexadecimal, namely $. So, 42 = $2A, and 1024 = $400. This is much more compact than binary, and slightly more than decimal, too; but what makes hexadecimal very interesting is that one nibble corresponds exactly to 4 bits!

NibbleBits
$0%0000
$1%0001
$2%0010
$3%0011
$4%0100
$5%0101
$6%0110
$7%0111
$8%1000
$9%1001
$A%1010
$B%1010
$C%1100
$D%1101
$E%1110
$F%1111

This makes it very easy to convert between binary and hexadecimal, while retaining a compact enough notation. Thus, hexadecimal is used a lot more than binary. And, don’t worry, decimal can still be used 😜

(Side note: one could point that octal, i.e. base 8, would also work for this; however, we will primarily deal with units of 8 bits, for which hexadecimal works much better than octal. RGBDS supports octal via the & prefix, but I have yet to see it used.)

💡

If you’re having trouble converting between decimal and binary/hexadecimal, check if your favorite calculator program doesn’t have a “programmer” mode, or a way to convert between bases.

Summary

  • In RGBDS assembly, the hexadecimal prefix is $, and the binary prefix is %.
  • Hexadecimal can be used as a “compact binary” notation.
  • Using binary or hexadecimal is useful when individual bits matter; otherwise, decimal works just as well.
  • For when numbers get a bit too long, RGBASM allows underscores between digits (123_465, %10_1010, $DE_AD_BE_EF, etc.)

Registers

Alright! Now that we know what bits are, let’s talk about how they’re used. Don’t worry, this is mostly prep work for the next section, where we will—finally!—look at the code 👀

First, if you opened BGB, you have been greeted with just the Game Boy screen. So, it’s time we pop the debugger open! Right-click the screen, select “Other”, and click “Debugger”. Oh, and while we’re at this, we might as well increase the screen size a little.

The debugger may look intimidating at first, but don’t worry, soon we’ll be very familiar with it! For now, let’s focus on this small box near the top-right, the register viewer.

Picture of the register viewer’s location

⚠️

The register viewer shows both CPU registers and some hardware registers. This lesson will only deal with CPU registers, so that’s why we will be ignoring some of these entries here.

What are CPU registers? Well, imagine you’re preparing a cake. You will be following a recipe, whose instructions may be “melt 125g of chocolate and 125g of butter, blend with 2 eggs” and so on. You will fetch some ingredients from the fridge as needed, but you don’t cook inside the fridge; for that, you have a small workspace.

Registers are pretty much the CPU’s workspace. They are small, tiny chunks of memory embedded directly in the CPU (only 10 bytes for the Game Boy’s CPU, and even modern CPUs have less than a kilobyte if you don’t count SIMD registers). Operations are not performed directly on data stored in memory, which would be equivalent to breaking eggs directly inside our fridge, but they are performed on registers.

ℹ️

There are exceptions to this rule, like many other “rules” I will give in this tutorial; I will paper over them to keep the mental complexity reasonable, but don’t treat my word as gospel either.

General-purpose registers

CPU registers can be placed into two categories: general-purpose and special-purpose. A “general-purpose” register (GPR for short) can be used for storing arbitrary data. Some GPRs are special nonetheless, as we will see later; but the distinction is “can I store arbitrary data in it?”.

I won’t introduce special-purpose registers quite yet, as their purpose wouldn’t make sense yet. Rather, they will be discussed as the relevant concepts are introduced.

The Game Boy CPU has seven 8-bit GPRs: a, b, c, d, e, h, and l. “8-bit” means that, well, they store 8 bits. Thus, they can store integers from 0 to 255 (%1111_1111 aka $FF).

a is the accumulator, and we will see later that it can be used in special ways.

A special feature is that these registers, besides a, are paired up, and the pairs can be treated as the 16-bit registers bc, de, and hl. The pairs are not separate from the individual registers; for example, if d contains 192 ($C0) and e contains 222 ($DE), then de contains 49374 ($C0DE) = 192 × 256 + 222. The other pairs work similarly.

Modifying de actually modifies both d and e at the same time, and modifying either individually also affects the pair. How do we modify registers? Let’s see how, with our first assembly instructions!

Assembly basics

Alright, now that we know what the tools do, let’s see what language RGBASM speaks. I will take a short slice of the beginning of hello-world.asm, so that we agree on the line numbers, and you can get some syntax highlighting even if your editor doesn’t support it.

INCLUDE "hardware.inc"

SECTION "Header", ROM0[$100]

	jp EntryPoint

	ds $150 - @, 0 ; Make room for the header

EntryPoint:
	; Shut down audio circuitry
	ld a, 0
	ld [rNR52], a

Let’s analyze it. Note that I will be ignoring a lot of RGBASM’s functionality; if you’re curious to know more, you should wait until parts II and III, or read the docs.

Comments

We’ll start with line 10, which should appear gray above. Semicolons ; denote comments. Everything from a semicolon to the end of the line is ignored by RGBASM. As you can see on line 7, comments need not be on an otherwise empty line.

Comments are a staple of every good programming language; they are useful to give context as to what code is doing. They’re the difference between “Pre-heat the oven at 180 °C” and “Pre-heat the oven at 180 °C, any higher and the cake would burn”, basically. In any language, good comments are very useful; in assembly, they play an even more important role, as many common semantic facilities are not available.

Instructions

Assembly is a very line-based language. Each line can contain a directive, which instructs RGBASM to do something, or an instruction1, which is copied directly into the ROM. We will talk about directives later, for now let’s focus on instructions: for example, in the snippet above, we will ignore lines 1 (INCLUDE), 7 (ds), and 3 (SECTION).

To continue the cake-baking analogy even further, instructions are like steps in a recipe. The console’s processor (CPU) executes instructions one at a time, and that... eventually does something! Like baking a cake, drawing a “Hello World” image, or displaying a Game Boy programming tutorial! *wink* *wink*

Instructions have a mnemonic, which is a name they are given, and operands, which indicate what they should act upon. For example, in “melt the chocolate and butter in a saucepan”, the whole sentence would be the instruction, the verb “melt” would be the mnemonic, and “chocolate”, “butter”, and “saucepan” the operands, i.e. some kind of parameters to the operation.

Let’s discuss the most fundamental instruction, ld. ld stands for “LoaD”, and its purpose is simply to copy data from its right operand (RHS) into its left operand (LHS). For example, take line 11’s ld a, 0: it copies (”loads”) the value 0 into the 8-bit register a2. If you look further in the file, line 33 has ld a, b, which causes the value in register b to be copied into register a.

InstructionMnemonicEffect
LoadldCopies values around

ℹ️

Due to CPU limitations, not all operand combinations are valid for ld and many other instructions; we will talk about this when writing our own code later.

🤔

RGBDS has an instruction reference worth bookmarking, and you can also consult it locally with man 7 gbz80 if RGBDS is installed on your machine (except Windows...). The descriptions there are more succinct, since they’re intended as reminders, not as tutorials.


1

Technically, instructions in RGBASM are implemented as directives, basically writing their encoded form to the ROM; but the distinction between the instructions in the source code and those in the final ROM is not worth bringing up right now.

2

The curious reader may ask where the value is copied from. The answer is simply that the “immediate“ byte ($00 in this example) is stored in ROM just after the instruction’s opcode byte, and it’s what gets copied to a. We will come back to this when we talk about how instructions are encoded later on.

Memory

🎉

Congrats, you have just finished the hardest lessons of the tutorial! Since you have the basics, from now on, we’ll be looking at more and more concrete code.

If we look at line 29, we see ld a, [de]. Given what we just learned, this copies a value into register a... but where from? What do these brackets mean? To answer that, we need to talk about memory.

What’s a memory?

The purpose of memory is to store information. On a piece of paper or a whiteboard, you can write letters to store the grocery list, for example. But what can you store in a computer memory? The answer to that question is current1. Computer memory is made of little cells that can store current. But, as we saw in the lesson about binary, the presence or absence of current can be used to encode binary numbers!

tl;dr: memory stores numbers. In fact, memory is a long array of numbers, stored in cells. To uniquely identify each cell, it’s given a number (what else!) called its address. Like street numbers! The first cell has address 0, then address 1, 2, and so on. On the Game Boy, each cell contains 8 bits, i.e. a byte.

How many cells are there? Well, this is actually a trick question...

The many types of memory

There are several memory chips in the Game Boy, but we can put them into two categories: ROM and RAM2. ROM simply designates memory that cannot be written to3, and RAM memory that can be written to.

Due to how they work, the CPU, as well as the memory chips, can only use a single number for addresses. Let’s go back to the “street numbers” analogy: each memory chip is a street, with its own set of numbers, but the CPU has no idea what a street is, it only deals with street numbers. To allow the CPU to talk to multiple chips, a sort of “postal service”, the chip selector, is tasked with translating the CPU’s street numbers into a street & street number.

For example, let’s say a convention is established where addresses 0 through 1999 go to chip A’s addresses 0–1999, 2000–2999 to chip B’s 0–999, and 3000–3999 to chip C’s 0–999. Then, if the CPU asks for the byte at address 2791, the chip selector will ask chip B for the byte at its own address 791, and forward the reply to the CPU.

Since addresses dealt with by the CPU do not directly correspond to the chips’ addresses, we talk about logical addresses (here, the CPU’s) versus physical addresses (here, the chips’), and the correspondence is called a memory map. Since we are programming the CPU, we will only be dealing with logical addresses, but it’s crucial to keep in mind that different addresses may be backed by different memory chips, since each chip has unique characteristics.

This may sound complicated, so here is a summary:

  • Memory stores numbers, each 8-bit on the Game Boy.
  • Memory is accessed byte by byte, and the cell being accessed is determined by an address, which is just a number.
  • The CPU deals with all memory uniformly, but there are several memory chips each with their own characteristics.

Game Boy memory map

Let’s answer the question that introduced this section: how many memory cells are there on the Game Boy? Well, now, we can reframe this question as “how many logical addresses are there?” or “how many physical addresses are there in total?”.

Logical addresses, which again are just numbers, are 16-bit on the Game Boy. Therefore, there are 2^16 = 65536 logical addresses, from $0000 to $FFFF. How many physical addresses, though? Well, here is a memory map courtesy of Pan Docs (though I will simplify it a bit):

StartEndNameDescription
$0000$7FFFROMThe game ROM, supplied by the cartridge.
$8000$9FFFVRAMVideo RAM, where graphics are stored and arranged.
$A000$BFFFSRAMSave RAM, optionally supplied by the cartridge to save data to.
$C000$DFFFWRAMWork RAM, general-purpose RAM for the game to store things in.
$FE00$FE9FOAMObject Attribute Memory, where “objects” are stored.
$FF00$FF7FI/ONeither ROM nor RAM, but this is where you control the console.
$FF80$FFFEHRAMHigh RAM, a tiny bit of general-purpose RAM which can be accessed faster.
$FFFF$FFFFIEA lone I/O byte that’s separated from the rest for some reason.

$8000 + $2000 + $2000 + $2000 + $A0 + $80 + $7F + 1 adds up to $E1A0, or 57760 bytes of memory that can be actually accessed. The curious reader will naturally ask, “What about the remaining 7776 bytes? What happens when accessing them?”; the answer is: “It depends, it’s complicated; avoid accessing them”.

Labels

Okay, memory addresses are nice, but you can’t possibly expect me to keep track of all these addresses manually, right?? Well, fear not, for we have labels!

Labels are symbols which basically allow attaching a name to a byte of memory. A label is declared like at line 9 (EntryPoint:): at the beginning of the line, write the label’s name, followed by a colon, and it will refer to the byte right after itself. So, for example, EntryPoint refers to the ld a, 0 right below it (more accurately, the first byte of that instruction, but we will get there when we get there).

If you peek inside hardware.inc, you will see that for example rNR52 is not defined as a label. That’s because they are constants, which we will touch on later; since they can be used mostly like labels, we will conflate the two for now.

Writing out a label’s name is equivalent to writing the address of the byte it’s referencing (with a few exceptions we will see in Part Ⅱ). For example, consider the ld de, Tiles at line 25. Tiles (line 64) is referring to the first byte of the tile data; if we assume that the tile data ends up being stored starting at $0193, then ld de, Tiles is equivalent to ld de, $0193!

What’s with the brackets?

Right, we came into this because we wanted to know what the brackets in ld a, [de] mean. Well, they can basically be read as “at address...”. For example, ld a, b can be read as “copy into a the value stored in b“; ld a, [$5414] would be read as “copy into a the value stored at address $5414”, and ld a, [de] would be read as “copy into a the value stored at address de“. Wait, what does that mean? Well, if de contains the value $5414, then ld a, [de] will do the same thing as ld a, [$5414].

If you’re familiar with C, these brackets are basically how the dereference operator is implemented.

hli

An astute reader will have noticed the ld [hli], a just below the ld a, [de] we have just studied. [de] makes sense because it’s one of the register pairs we saw a couple lessons ago, but [hli]? It’s actually a special notation, which can also be written as [hl+]. It functions as [hl], but hl is incremented just after memory is accessed. [hld]/[hl-] is the mirror of this one, decrementing hl instead of incrementing it.

An example

So, if we look at the first two instructions of .copyTiles:

	ld a, [de]
	ld [hli], a

...we can see that we’re copying the byte in memory pointed to by de (that is, whose address is contained in de) into the byte pointed to by hl. Here, a serves as temporary storage, since the CPU is unable to perform ld [hl], [de] directly.

While we’re at this, let’s examine the rest of .copyTiles in the following lessons!


1

Actually, this depends a lot on the type of memory. A lot of memory nowadays uses magnetic storage, but to keep the explanation simple, and to parallel the explanation of binary given earlier, let’s assume that current is being used.

2

There are other types of memory, such as flash memory or EEPROM, but only Flash has been used on the Game Boy, and for only a handful of games; so we can mostly forget about them.

3

No, really! Mask ROM is created by literally punching holes into a layer of silicon using acid, and e.g. the console’s boot ROM is made of hard-wired transitors within the CPU die. Good luck writing to that!
“ROM” is sometimes (mis)used to refer to “persistent memory” chips, such as flash memory, whose write functionality was disabled. Most bootleg / “repro” Game Boy cartridges you can find nowadays actually contain flash; this is why you can reflash them using specialized hardware, but not original cartridges.

Operations & flags

Alright, we know how to pass values around, but just copying numbers is no fun; we want to modify them!

The GB CPU does not provide every operation under the sun (for example, there is no multiplication instruction), but we can just program those ourselves with what we have. Let’s talk about some of the operations that it does have; I will be omitting some not used in the Hello World for now.

Arithmetic

The simplest arithmetic instructions the CPU supports are inc and dec, which INCrement and DECrement their operand, respectively. (If you aren’t sure, “to increment” means “to add 1”, and “to decrement” means “to subtract 1”.) So for example, the dec bc at line 32 of hello-world.asm simply subtracts 1 from bc.

Okay, cool! Can we go a bit faster, though? Sure we can, with add and sub! These respectively ADD and SUBtract arbitrary values (either a constant, or a register). Neither is used in the tutorial, but a sibling of sub‘s is: have you noticed little cp over at line 17? cp allows ComParing values. It works the same as sub, but it discards the result instead of writing it back. “Wait, so it does nothing?” you may ask; well, it does update the flags.

Flags

The time has come to talk about the special-purpose register (remember those?) f, for, well, flags. The f register contains 4 bits, called “flags”, which are updated depending on an operation’s results. These 4 flags are:

NameDescription
ZZero flag
NAddition/subtraction
HHalf-carry
CCarry

Yes, there is a flag called “C” and a register called “c”, and they are different, unrelated things. This makes the syntax a bit confusing at the beginning, but they are always used in different contexts, so it’s fine.

We will forget about N and H for now; let’s focus on Z and C. Z is the simplest flag: it gets set when an operation’s result is 0, and gets reset otherwise. C is set when an operation overflows or underflows.

What’s an overflow? Let’s take the simple instruction add a, 42. This simply adds 42 to the contents of register a, and writes the result back into a.

    ld a, 200
    add a, 42

At the end of this snippet, a equals 200 + 42 = 242, great! But what if I write this instead?

    ld a, 220
    add a, 42

Well, one could think that a would be equal to 220 + 42 = 262, but that would be incorrect. Remember, a is an 8-bit register, it can only store eight bits of information! And if we were to write 262 in binary, we would get %100000110, which requires at least 9 bits... So what happens? Simply, that ninth bit is lost, and the value that we end up with is %00000110 = 6. This is called an overflow: after adding, we get a value smaller than what we started with.

We can also do the opposite with sub, and—for example—subtract 42 from 6; as we know, for all X and Y, X + Y - Y = X, and we just saw that 220 + 42 = 6 (this is called modulo 256 arithmetic, by the way); so, 6 - 42 = (220 + 42) - 42 = 220. This is called an underflow: after subtracting, we get a value greater than what we started with.

When an operation is performed, it sets the carry flag if an overflow or underflow occurred, and clears it otherwise. (We will see later that not all operations update the carry flag, though.)

Summary

  • We can add and subtract numbers.
  • The Z flag lets us know if the result was 0.
  • However, registers can only store a limited range of integers.
  • Going outside this range is called an overflow or underflow, for addition and subtraction respectively.
  • The C flag lets us know if either occurred.

Comparison

Now, let’s talk more about how cp is used to compare numbers. Here is a refresher: cp subtracts its operand from a and updates flags accordingly, but doesn’t write the result back. We can use flags to check properties about the values being compared, and we will see in the next lesson how to use the flags.

The simplest interaction is with the Z flag. If it’s set, we know that the subtraction yielded 0, i.e. a - operand == 0; therefore, a == operand! If it’s not set, well, then we know that a != operand.

Okay, checking for equality is nice, but we may also want to perform comparisons. Fret not, for the carry flag is here to do just that! See, when performing a subtraction, the carry flag gets set when the result goes below 0—but that’s just a fancy way of saying “becomes negative”!

So, when the carry flag gets set, we know that a - operand < 0, therefore that a < operand..! And, conversely, we know that if it’s not set, a >= operand. Great!

Instruction summary

InstructionMnemonicEffect
AddaddAdds values to a
SubtractsubSubtracts values from a
ComparecpCompares values with what’s contained in a

Jumps

Once this lesson is done, we will be able to understand all of CopyTiles!

So far, all the code we have seen was linear: it executes top to bottom. But this doesn’t scale: sometimes, we need to perform certain actions depending on the result of others (”if the crêpes start sticking, grease the pan again”), and sometimes, we need to perform actions repeatedly (”If there is some batter left, repeat from step 5”).

Both of these imply reading the recipe non-lineraly. In assembly, this is achieved using jumps.

The CPU has a special-purpose register called “PC”, for Program Counter. It contains the address of the instruction currently being executed1, like how you’d keep in mind the number of the recipe step you’re currently doing. PC increases automatically as the CPU reads instructions, so “by default” they are read sequentially; however, jump instructions allow writing a different value to PC, effectively jumping to another piece of the program. Hence the name.

Okay, so, let’s talk about those jump instructions, shall we? There are four of them:

InstructionMnemonicEffect
JumpjpJump execution to a location
Jump RelativejrJump to a location close by
CallcallCall a subroutine
ReturnretReturn from a subroutine

We will focus on jp for now. jp, such as the one line 5, simply sets PC to its argument, jumping execution there. In other words, after executing jp EntryPoint (line 5), the next instruction executed is the one below EntryPoint (line 16).

🤔

You may be wondering what is the point of that specific jp. Don’t worry, we will see later why it’s required.

Conditional jumps

Now to the really interesting part. Let’s examine the loop responsible for copying tiles:

	; Copy the tile data
	ld de, Tiles
	ld hl, $9000
	ld bc, TilesEnd - Tiles
CopyTiles:
	ld a, [de]
	ld [hli], a
	inc de
	dec bc
	ld a, b
	or a, c
	jp nz, CopyTiles

Don’t worry if you don’t quite get all the following, as we’ll see it live in action in the next lesson. If you’re having trouble, try going to the next lesson, watch the code execute step by step; then, coming back here, it should make more sense.

First, we copy Tiles, the address of the first byte of tile data, into de. Then, we set hl to $9000, which is the address where we will start copying the tile data to. ld bc, TilesEnd - Tiles sets bc to the length of the tile data: TilesEnd is the address of the first byte after the tile data, so subtracting Tiles to that yields the length.

So, basically:

  • de contains the address where data will be copied from;
  • hl contains the address where data will be copied to;
  • bc contains how many bytes we have to copy.

Then we arrive at the main loop. We read one byte from the source (line 6), and write it to the destination (line 7). We increment the destination (via the implicit inc hl done by ld [hli], a) and source pointers (line 8), so the following loop iteration processes the next byte.

Here’s the interesting part: since we’ve just copied one byte, that means we have one less to go, so we dec bc. (We have seen dec two lessons ago; as a refresher, it simply decreases the value stored in bc by one.) Since bc contains the amount of bytes that still need to be copied, it’s trivial to see that we should simply repeat the operation if bc != 0.

😓

dec usually updates flags, but unfortunately dec bc doesn’t, so we must check if bc reached 0 manually.

ld a, b and or c “bitwise OR” b and c together; it’s enough to know for now that it leaves 0 in a if and only if bc == 0. And or updates the Z flag! So, after line 11, the Z flag is set if and only if bc == 0, that is, if we should exit the loop.

And this is where conditional jumps come into the picture! See, it’s possible to conditionally “take” a jump depending on the state of the flags.

There are four “conditions”:

NameMnemonicDescription
ZerozZ is set (last operation had a result of 0)
Non-zeronzZ is not set (last operation had a non-zero result)
CarrycC is set (last operation overflowed)
No carryncC is not set (last operation did not overflow)

Thus, jp nz, CopyTiles can be read as “if the Z flag is not set, then jump to CopyTiles“. Since we’re jumping backwards, we will repeat the instructions again: we have just created a loop!

Okay, we’ve been talking about the code a lot, and we have seen it run, but we haven’t really seen how it runs. Let’s watch the magic unfold in slow-motion in the next lesson!


1

Not exactly; instructions may be several bytes long, and PC increments after reading each byte. Notably, this means that when an instruction finishes executing, PC is pointing to the following instruction. Still, it’s pretty much “where the CPU is currently reading from”, but it’s better to keep it simple and avoid mentioning instruction encoding for now.

Tracing

Ever dreamed of being a wizard? Well, this won’t give you magical powers, but let’s see how emulators can be used to control time!

First, make sure to focus the debugger window. Let’s first explain the debugger’s layout: Top-left is the code viewer, bottom-left is the data viewer, top-right are some registers (as we saw in the registers lesson), and bottom-right is the stack viewer. What’s the stack? We will answer that question a bit later... in Part Ⅱ 😅

Setup

For now, let’s focus on the code viewer. If you focus the debugger while the emulator is running, it will pause, as indicated by the screen window’s title changing to bgb (debugging).

Okay, but what’s with this weird syntax? Well, there are several syntaxes for Game Boy assembly, and BGB doesn’t use RGBDS’ by default. We can fix that in the options, which we can access by either:

  • Right-clicking the screen and selecting “Options”
  • In the debugger, open the “Window” menu, and select “Options”
  • Press F11 while focusing either the screen or debugger

BGB has a ton of options, but don’t worry, it’s got good defaults, so we don’t need to look at most of them for now. Select the “Debug” tab, and set “Disasm syntax” to “rgbds”. Oh, and check the “Local symbols” box, too. Now, click “OK” to apply the options.

Screenshot of the options window

You can also customize a lot of key bindings in the options, but I will stick to the default ones for this tutorial for the sake of simplicity.

Alright, great! But where are the labels? Well, as we have seen a couple of lessons ago, labels are merely a convenience provided by RGBASM, but they are not part of the ROM itself. It is very much inconvenient to debug without them, and so sym files (for “symbols”) have been developed. Let’s run RGBLINK to generate a sym file for our ROM:

rgblink -n hello-world.sym hello-world.o

‼️

The file names matter! When looking for a ROM’s sym file, BGB takes the ROM’s file name, strips the extension (here, .gb), replaces it with .sym, and looks for a file in the same directory with that name.

Then, in the debugger, we can go in the “File” menu and select “reload SYM file”.

Screenshot of the debugger showing the labels in the code viewer

Much better!

🔍

If a sym file is loaded, pressing Tab allows toggling whether labels are displayed or not.

Stepping

When pausing execution, the debugger will automatically focus on the instruction the CPU is about to execute, as indicated by the line highlighted in blue. Screenshot of the debugger showing that the highlighted line corresponds to PC

ℹ️

The instruction highlighted in blue is always what the CPU is about to execute, not what it just executed. Keep this in mind.

If we want to watch execution from the beginning, we need to reset the emulator. Go into the debugger’s “Run” menu, and select “Reset”, or tap your numpad’s * key.

The blue line should automatically move to address $01001, and now we’re ready to trace! All the commands for that are in the “Run” menu.

  • “Run” simply unpauses the emulator. Clicking on the screen also does the same.
  • “Trace” (more commonly known as “Step Into”) and “Step Over” advance the emulator by one instruction. They only really differ on the call instruction and interrupts, neither of which we are using here, so we will use “Trace”.
  • The other options are not relevant for now.

We will have to “Trace” a bunch of times, so it’s a good idea to use the key shortcut. If we press F7 once, the jp EntryPoint is executed. And if we press it a few more times, can see the instructions being executed, one by one!

Now, you may notice the WaitVBlank loop runs a lot of times, but what we are interested in is the CopyTiles loop. We can easily skip over it in several ways; this time, we will use a breakpoint. We will place the breakpoint on the ld de, Tiles at 00:0162; either double-click on that line, or select it and press F2. The line will turn red:

Debugger screenshot showcasing the breakpoint

Then you can resume execution either by clicking the screen or pressing F9, and BGB will automatically pause. Whenever BGB is running, and the (emulated) CPU is about to execute an instruction a breakpoint was placed on, it automatically pauses.

Debugger screenshot showcasing execution paused on the breakpoint

You can see where execution is being paused both from the green arrow and the value of PC.

If we trace the next three instructions, we can see the three arguments to the CopyTiles loop getting loaded into registers.

The state of some registers at the beginning of the CopyTiles loop

For fun, let’s watch the tiles as they’re being copied. For that, obviously, we will use the data viewer, and position it at the destination. As we can see from the image above, that would be $9000!

Select the data viewer (either click somewhere in it, or use Ctrl+Tab to switch focus, as indicated by the grey bar on the left), and press Ctrl+G (for “Goto”). In the popup, type the address you wish to go to, in our case 9000 (sans dollar sign!!).

Awesome, right?

What next?

Congrats, you have just learned how to use a debugger! We have only scratched the surface, though; we will use more of BGB’s tools to illustrate the next parts. Don’t worry, from here on, lessons will go with a lot more images—you’ve made it through the hardest part!


1

Why does execution start at $0100? That’s because it’s where the boot ROM hands off control to our game once it’s done.

Tiles

💭

“Tiles” were called differently in documentation of yore. They were usually called “patterns” or “characters”, the latter giving birth to the “CHR” abbreviation which is sometimes used to refer to tiles.

For example, on the NES, tile data is usually provided by the cartridge in either CHR ROM or CHR RAM. The term “CHR” is typically not used on the Game Boy, though exchanges between communities cause terms to “leak”, so some refer to the area of VRAM where tiles are stored as “CHR RAM” or “CHR VRAM”, for example.

As with all such jargon whose meaning may depend on who you are talking to, I will stick to “tiles” across this entire tutorial for consistency, being what is the most standard in the GB dev community now.

Well, copying this data blindly is fine and dandy, but how exactly are those graphics?

Screenshot of some tile definitions in the code
Ah, yes, pixels.

Let’s see about that!

Helpful hand

Now, figuring out the format with an explanation alone is going to be very confusing; but fortunately, BGB got us covered thanks to its VRAM viewer. You can open it either by selecting “Window” then “VRAM viewer”, or pressing F5 when the debugger is in focus. It is also in “Other” in the screen’s right-click menu.

Screenshot of the VRAM viewer’s “Tiles” tab

By default, it should open on the “Tiles” tab like in the image, but if not, please select that. We will come to the other tabs in due time. This one shows the tiles present in the Game Boy’s video memory (or “VRAM“).

🤔

I encourage you to experiment with the VRAM viewer, hover over things, tick and untick checkboxes, see by yourself what’s what. Any questions you might have will be answered in due time, don’t worry! And if what you’re seeing later on doesn’t match my screenshots, ensure that the checkboxes match mine.

Don’t mind the “Nintendo” in the top-left; we did not put it there ourselves, and we will see why it’s there later. We will also be ignoring the right half of the tile grid, as it is exclusive to the Game Boy Color: we will get there when we get there.

Short primer

You may have heard of tiles before, especially as they were really popular in 8-bit and 16-bit systems. That’s no coincidence: tiles are very useful. Instead of storing every on-screen pixel (144 × 160 pixels × 2 bits/pixel = 46080 bits = 5760 bytes, compared to the console’s 8192 bytes of VRAM), pixels are grouped into tiles, and then tiles are assembled in various ways to produce the final image.

In particular, tiles can be reused very easily and at basically no cost, saving a lot of memory! In addition, manipulating whole tiles at once is much cheaper than manipulating the individual pixels, so this spares processing time as well.

The concept of a “tile” is very general, but on the Game Boy, tiles are always 8 by 8 pixels. Often, hardware tiles are grouped to manipulate them as larger tiles (often 16×16); to avoid the confusion, those are referred to as meta-tiles.

“bpp”?

You may be wondering where that “2 bits/pixel” figure earlier came from... This is something called “bit depth”.

See, colors are not stored in the tiles themselves! Instead, it works like a coloring book: the tile itself contains 8 by 8 indices, not colors; you give the hardware a tile and a set of colors—a palette—and it colorizes them! (This is also why color swaps were very common back then: you could create enemy variations by storing tiny palettes instead of large different graphics.)

Anyway, as it is, Game Boy palettes are 4 colors large.1 This means that the indices into those palettes, stored in the tiles, can be represented in only two bits! This is called “2 bits per pixel”, noted “2bpp”.

With that in mind, we are ready to explain how these bytes turn into pixels!

Encoding

As I explained, each pixel takes up 2 bits. Since there are 8 bits in a byte, you might expect each byte to contain 4 pixels... and you would be neither entirely right, nor entirely wrong. See, each row of 8 pixels is stored in 2 bytes, but neither of these bytes contains the info for 4 pixels. (Think of it like a 10 € banknote torn in half: neither half is worth anything, but the full bill is worth, well, 10 €.)

For each pixel, the least significant bit of its index is stored in the first byte, and the most significant bit is stored in the second byte. Since each byte is a collection of one of the bits for each pixel, it’s called a bitplane.

The leftmost pixel is stored in the leftmost bit of both bytes, the pixel to its right in the second leftmost bit, and so on. The first pair of bytes stores the topmost row, the second byte the row below that, and so on.

Here is a more visual demonstration; note that it’s for the SNES, but its 2BPP format is the same as the Game Boy’s.

This encoding may seem a little weird at first, and it can be; it’s made to be more convenient for the hardware to decode, keeping the circuitry simple and low-power. It even makes a few cool tricks possible, as we will see (much) later!

You can read up more about the encoding in the Pan Docs and ShantyTown’s site.

In the next lesson, we shall see how colors are applied!


1

Other consoles can have varying bit depths; for example, the SNES has 2bpp, 4bpp, and 8bpp depending on the graphics mode and a few other parameters.

Palettes

In the previous lesson, I briefly mentioned that colors are applied to tiles via palettes, but we haven’t talked much about those yet.

The black & white Game Boy has three palettes, one for the background called BGP (”BackGround Palette”), and two for the objects called OBP0 and OBP1 (”OBject Palette 0/1”). If you are wondering what “objects” are, you will have to wait until Part Ⅱ to find out; for now, let’s focus on the background.

🌈

The Game Boy Color introduced, obviously, colors, and this was mainly done by reworking the way palettes are handled. We will not talk about Game Boy Color features in Part Ⅰ for the sake of simplicity, but we will do so in later parts.

Please select the “Palettes” tab of BGB’s VRAM viewer.

Screenshot of the VRAM viewer’s Palette tab

🥴

The VRAM viewer’s layout may seem weird. As mentioned in the previous tip box, the Game Boy Color changed the way palettes work, and the Super Game Boy throws another wrench into the mix.

The VRAM viewer uses the same layout for monochrome and Color, leading to this admittedly strange layout.

We will be taking a look at the “BGP” line. As I explained before, tiles store “color indices” for each pixel, which are used to index into the palette. Color number 01 is the leftmost in that line, and number 3 is the rightmost.

So, in our case, color number 0 is “white”, color number 1 is “light gray”, number 2 is “dark gray”, and number 3 “black”. I put air quotes because “black” isn’t true black, and “white” isn’t true white. Further, note that the original Game Boy had shades of green, but the later Game Boy Pocket’s screen produced shades of gray instead. And, even better, the Game Boy Color will automatically colorize games that lack Game Boy Color support!

Screenshot of our Hello World, automatically colorized by the Game Boy Color

All this to say, one shouldn’t expect specific colors out of a Game Boy game2, just four more or less bright colors.

Getting our hands dirty

Well, so far in this tutorial, besides running the Hello World, we have been pretty passive, watching it unfold. What do you say we start prodding the ROM a bit?

In BGB’s debugger, select the “Window” menu, and open the “IO map” (or just press F10 within the debugger).

Screenshot of the IO map

While the VRAM viewer offers a visual representation of the palette, the IO map shows the nitty-gritty: how it’s encoded. The IO map also lets us modify BGP easily; but to do so, we need to understand how values we write are turned into colors.

Encoding

Fortunately, the encoding is very simple. I will explain it, and at the same time, give an example with the palette we have at hand, $E4.

Take the byte, break its 8 bits into 4 groups of 2.

[BGP] = $E4
$E4 = %11100100 (refresh your memory in the "Binary and hexadecimal" lesson if needed!)
That gets broken down into %11, %10, %01, %00

Color number 0 is the rightmost “group”, color number 3 is the leftmost one. Simple! And this matches what the VRAM viewer is showing us: color number 0, the rightmost, is the brightest (%00), up to color number 3, the leftmost and the darkest (%11).

Lights out

For fun, let’s make the screen completely black. We can easily do this by setting all colors in the palette to black (%11). This would be %11 %11 %11 %11 = $FF.

In the IO map, click the text box left of “BGP”, erase the “E4”, type “FF”, and hit Enter. BGP immediately updates, turning the screen black!

Screenshot of the palette viewer with the BGP line entirely black
Observe how the BGP line is entirely black now. Also, I could have shown a screenshot of the black screen, but that would have been silly.

What if we wanted to take the original palette, but invert it? %11 would become %00, %01 would become %10, %10 would become %01, and %11 would become %00. We would get thus:

%11_10_01_00
 ↓  ↓  ↓  ↓
%00_01_10_11

(I’m not giving the value in hexadecimal, use this as an opportunity to exercise your bin-to-hex conversions!)

Screenshot of the color-inverted Hello World
If you got it right, it should look like this!

If you go to the Tile Viewer and uncheck the “show paletted” checkbox, you will notice that the tile data stays the same regardless of how the palette is modified! This is an advantage of using palettes: fading the screen in and out is very cheap, just modifying a single byte, instead of having to update every single on-screen pixel.

Got all that? Then let’s take a look at the last missing puzzle piece in the Hello World’s rendering process, the tilemap!


1

Numbering often starts at 0 when working with computers. We will understand why later, but for now, please bear with it!

2

Well, it is possible to detect these different models and account for them, but this would require taking plenty of corner cases into consideration, so it’s probably not worth the effort.

Tilemap

🧐

Some spell them “tile map”, some “tilemap”. I will be using the latter by preference, but I also stay consistent with it in the code (Tilemap and not TileMap), as well as later when we will talk about attribute maps (”attrmap” and Attrmap instead of AttrMap).

We are almost there. We have seen how graphics on the Game Boy are composed of 8×8 “tiles”, and we have seen how color is added into the mix.

But we have not seen yet how those tiles are arranged into a final picture!

Tiles are basically a grid of pixels; well, the tilemaps are basically a grid of tiles! To allow for cheap reuse, tiles aren’t stored in the tilemap directly; instead, tiles are referred to by an ID, which you can see in BGB’s VRAM viewer.

Screenshot highlighting where a tile's ID can be seen
The ID is displayed in hexadecimal without a prefix, so this is tile number $10, aka 16. As you may have noticed, the tiles are displayed in rows of 16, so it's easier to locate them by hexadecimal ID. Nifty!

Now, of course, tile IDs are numbers, like everything that computers deal with. IDs are stored in bytes, so there are 256 possible tile IDs. However, the astute reader will have noticed that there are 384 tiles in total1! By virtue of the pigeonhole principle, this means that some IDs refer to several tiles at the same time.

Indeed, BGB reports that the first 128 tiles have the same IDs as the last 128. There exists a mechanism to select whether IDs 0–127 reference the first or last 128 tiles, but for simplicity’s sake, we will overlook this for now, so please ignore the first (topmost) 128 tiles for the time being.

Now, please turn your attention to the “BG map” tab of BGB’s VRAM viewer, pictured below.

Screenshot of the tilemap viewer

You may notice that the image shown is larger than what is displayed on-screen. Only part of the tilemap, outlined by a thicker border in the VRAM viewer, is displayed on-screen at a given time. We will explain this in more detail in Part Ⅱ.

Here we will be able to see the power of tile reuse in full force. As a convenience and a refresher, here are the tiles our Hello World loads into VRAM:

Enlarged view of the tiles loaded in VRAM

You can see that we only loaded a single “blank” tile ($00, the first aka. top-left one), but it can be repeated to cover the whole background at no extra cost!

Repetition can be more subtle: for example, tile $01 is used for the top-left corner of the H, E, L, L, and W (red lines below)! The R, L, and D also both share their top-left tile ($2D, blue lines below); and so on. You can confirm this by hovering over tiles in the BG map tab, which shows the ID of the tile at that position.

Diagram of some tile mappings
Here are some examples of tile reuse. Not everything is drawn, as it would become a mess.

All in all, we can surmise that displaying graphics on the Game Boy consists of loading “patterns” (the tiles), and then telling the console which tile to display for each given location.


1

The even more astute (astuter?) reader will have noticed that 384 = 3 × 128. Thus, tiles are often conceptually grouped into three “blocks” of 128 tiles each, which BGB shows as separated by thicker horizontal lines.

Wrapping up

Congrats! You have made it through the first part of this tutorial. By this point, you have a basic enough understanding of the console that you know how to display a picture. And hey, that doesn’t sound like much, but consider everything you have seen so far—there is a lot that goes into it!

🥳

Honestly, congrats on coming this far—many people have given up earlier than this. So you can give yourself a pat on the back, you honestly deserve it! Now may also be a good time to take a break if you are reading all this in a single trait. I encourage you to give it a little time to sink in, and maybe go back to the lessons you struggled on the most. Maybe a second read can help.


And yes, you could simply have let a library handle all that. However, the details always leak through eventually, so knowing about them is helpful, if only for debugging.

Plus, understanding what’s really going on under the hood makes you a better programmer, even if you don’t end up using ASM in the long run. Amusingly, even modern systems work similarly to older ones in unexpected places, so some things you just learned will carry over! Trust me, everything you have learned and will learn is worth it! ✊

That said, right now, you may have a lot of questions.

  • Why do we turn off the LCD?
  • We know how to make a static picture, but how to we add motion into the mix?
  • Also, how do I get input from the player?
  • The code mentions shutting down audio, but how do I play some of those famed beeps and bloops?
  • Writing graphics in that way sound tedious, is there no other way?
  • Actually, wait, how do we make a game out of all this??

... All of that answered, and more, in Part Ⅱ! 👀

Work in progress

🚧 🚧 🚧 🚧 🚧 🚧 🚧

As explained in the initial tutorial presentation, Part Ⅱ consists of us building an Arkanoid game. However, this requires a lot of planning: the entire roadmap needs to be designed before I can start writing the first lesson.

If I was just making a game, I could afford to make bad design decisions and correct them later; however, such mistakes would only make the tutorial more difficult and confusing to follow. Some mistakes will however be made intentionally, so that we can learn how to overcome them, should you encounter them on your own. These need to be planned as well.

For these reasons, Part Ⅱ is being worked on behind the scenes, and this may take a significant amount of time. Please hold tight while I (and others! ^^) are working on this, maybe follow me on Twitter for (infrequent) updates, and go to the next page to find out what you can do in the meantime!

Thank you for your patience 😊 and see you around on GBDev!

Where to go next

Oh. Well, you’ve reached the end of the tutorial... And yes, as you can see, it’s not finished yet. I’m actively working on fixing that, though, please be a little patient :)

In the meantime, the best course of action is to peruse the resources in the next section, and experiment by yourself. Well, given that, it may be a good idea to ask around for advice. A lot of the problems and questions you will be encountering have already been solved, so others can—and will!—help you getting started faster.

Resources

A.k.a. “where to go from here”.

FIXME: This is not properly written yet...

Special Thanks

Big thank you to Twoflower/Triad for making the Hello World graphic.

I can’t thank enough Chloé and many others for their continued support.

Thanks to the GBDev community for being so nice throughout the years.

You are all great. Thank you so very much.


Thank you to the Rust language team for making mdBook, which powers this book (this honestly slick design is the stock one!!)

Greets to AYCE, Phantasy, TPPDevs/RainbowDevs, Plutiedev, lft/kryo :)

Shoutouts to GreenAndEievui, Rangi, cVui, ax6, Baŝto, bbbbbr, and bitnenfer!