Link's Awakening Disassembly Progress Report – part 13

After a solid two-year hiatus, here’s a new progress report for the Zelda: Link’s Awakening disassembly! Here we’ll cover the changes that happened in the past two years.

✨ New contributors

First let’s congratulate the following new contributors, who made their first commit to the project during the past two years:

📰 New blog

This series of articles moved to a new blog! Instead of being hosted on kemenaran’s personal blog, interleaved with other content, they are now published on this dedicated website. Of course, the former URLs now redirect to these new pages.

This move makes subscribing to new articles easier, since only relevant Link’s Awakening content will be published. I hope it will also encourage a more collaborative process for getting these articles out.

Also, the source code of this website is public! If you notice a typo or something missing, feel free to submit a pull request. Contributing right from Github’s UI usually works well, without the need to fork and run the website locally.

🎨 Palette documentation

The biggest addition of Link’s Awakening DX, compared to the original monochrome version, is of course color.

Link's Awakening overworld on a black-and-white Game Boy Link's Awakening overworld on a Game Boy Color
Comparing the original and colorized overworld.

Unlike modern games, these colors are applied not by coloring each individual pixel — but by using a fixed set of color palettes.

Each palette has 4 colors. And at a given time, the game can use 8 palettes for the background, and 8 palettes for the sprites.

A screenshot of all 16 palettes used in the overworld screen
The palettes used for the overworld screenshot above.

But this wasn’t well reflected in the disassembly until now. Color palettes were represented in a binary format, matching the underlying hardware, but difficult to read and edit by humans.

    ds  $FF, $47, $00, $00, $A2, $22, $FF, $46

The OBJ0 palette, as it was appearing in the source code.

Kelsey Higham wanted colors that were easier to read. After a bit of collective macro writing on the Discord server, the final format she ended up with reads like standard RGB hexadecimal colors.

    rgb   #F8F888, #000000, #10A840, #F8B888

With this new format, the same OBJ0 palette is much easier to edit.

There’s a fair amount of hairy macro code at compile-time to convert these #RGB colors to a two-byte GBC color. But the result is very pleasant to read: hexdecimal RGB colors are used everywhere, especially on the web, and many color editors can import and export from this format.

Then Kelsey Higham started the daunting task of converting all color palettes of the game to this format. Quite a task — but the end result is worth is: as far as we know, all color palettes in the source code are now decoded.

And the #RGB format has another advantage: as it is so widely used, many text editors can display the described color right in the editor itself.

Look at the result:

A screenshot of the source code opened in a text editor, with the rgb colors being appropriately colored
Visual representation of the game palettes in VS Code.

Now that’s a really easy way to see the content of a palette, right from the source code.

🔧 Fixes to the tilemap encoder

A primer on tilemaps

To display large pictures or sceneries, Link’s Awakening DX uses tilemaps (like almost all Game Boy games do). Tilemaps store the indices of tiles in a large array, and can be easily displayed by the hardware.

Except that Link’s Awakening DX doesn’t use raw tilemaps, but somehow compresses them. Instead of a linear sequence of tile indices, the game stores what we call Draw Commands. These little chunk of data instruct the decoder to “paint” the tilemap with a specific tile.

This hand-written compression helps to reduce the size of the tilemaps. For instance, if a tilemap is mostly black, but with a tiny patch of repeated details in the middle, the game can simply instruct to paint a row or a column of detailed tiles, and ignore the rest of the tilemap (which will use the background color).

To read more on encoded tilemaps, see the relevant entry in a previous progress report.

The issue with some tilemaps

User @Javs on Discord reported an issue occurring when editing the tilemap of the File creation menu. When decoding, editing and then re-encoding this specific tilemap, the tile indicating the save slot number would disappear.

file creation screenshots
On the left, the original version.
On the right, the edited version, lacking the save slot number 2️⃣ on top.

Now why did that happen? Turns out it was a combination of different issues.

Investigating a weird bug

The first thing we tried was to disassemble the relevant code.

When displaying this specific screen, there are two loading stages before the screen becomes interactive:

  1. The game requests the BG map to be filled with black tiles during the next vblank,
  2. Then the game simultaneously:

    1. requests the file creation tilemap (and attrmap) to be loaded during the next vblank,

    2. and requests a specific tile to be written to the BG map during the next vblank: the save slot index.

So far, so good. Now why doesn’t this work anymore when the tilemap has been edited?

A possible cause of problems is that Link’s Awakening tilemaps use a custom compression format, where repeated tiles can be “painted” over the screen. And most of the time, these compressed tilemaps were handwritten. So when we decode and re-encode a tilemap, there’s always a difference in how the compression is expressed (because the automatic encoding program doesn’t make the same choices as the original artists). In the end, the re-encoded tilemap is supposed to be functionally equivalent.

But could the different encoding trigger some underlying issues, like a race condition? What if the original encoding wrote to the top of the screen first, but the new re-encoding wrote to the top of the screen last, overwriting the changes made manually to the BG map?

Turns out the issue was simpler than that.

To save space, the original tilemaps often don’t encode the bytes for the background color. Instead they first fill the whole BG map with black (or white) tiles, then “paint” the tilemap over this background color. This is precisely what the File creation BG tilemap does: it only paints the bricks and letters, not the black areas.

File creation screen + overlay
The original tilemap only draws the tiles different from the background color.

When decoding the tilemap, we want the result to be editable using an external tool. So the decoder does the same steps: filling the background with a default color, and then painting over. Which means the background color gets included into the decoded tilemap.

But when re-encoding the tilemap, the background color was also imported into the file. Which resulted in the tilemap containing draw commands for all the black background areas.

File creation screen edited + overlay
But an edited tilemap would draw on the whole screen, including over the background tiles.

This is not only wasteful, it also means that the game paints the tilemap bytes twice: once when filling the Background with the default color, and once again when reading the tilemap.

And that was our issue: the game filled the background with black, then wrote the tile for the save slot number and painted the tilemap. As the save slot number is written over a black tile, it wasn’t overwritten by the original tilemap. But it was by the re-encoded version.

The fix

In theory, fixing the issue was easy: we just had to ignore the background color when re-encoding the BG map.

That said, the actual fix took some evenings. The encoder didn’t had any proper compression scheme implemented (all bytes were always written sequentially), but to allow some bytes to be skipped, a proper implementation of writing only to certain regions was needed. This also uncovered several bugs in the decoder part, which had to be solved.

All done

In the end:

And here’s our fixed version in-game:

The edited File creation tilemap, with the save slot number correctly displayed.

A remaining caveat is that, for now, the background color has to be specified manually, both when decoding and encoding the tilemap. For instance:

# Decoding
tools/ decode src/data/backgrounds/menu_file_creation.tilemap.encoded --filler 0x7E --outfile src/data/backgrounds/menu_file_creation.tilemap
# Editing using an external tool
# …
# Re-encoding
tools/ encode src/data/backgrounds/menu_file_creation.tilemap --filler 0x7E --outfile src/data/backgrounds/menu_file_creation.tilemap.encoded

But hopefully this is something that can be defined by the file name at some point.

🔀 RAM shiftability

The disassembled code has been shiftable for quite a while now. That means it is possible to add or remove some code, build the game, and have things still working: all pointer addresses that used to be hardcoded now resolve to the new locations automatically.

But at the beginning of 2022, there were still issues with the RAM shiftability: adding, removing or moving some variables in memory would break various things in the game.

Now, after a good number of fixes, the RAM is now properly shiftable.

For instance, you can add a block of 5 bytes at the beginning of the RAM definitions (thus shifting all RAM addresses by 5), and the game will still work properly. Or, to free up some space, a developer may choose to move a big block of RAM data out of RAM bank 0 to another RAM bank: this is expected to work without too much work.

All of this makes extensive ROM hacks possible: for instance, theoretically, it opens the gates to increase the maximum number of entities, or the number of letters reserved for the player’s name.

✂️ Split entities

Entities are the various NPCs, enemies, and actors that form the dynamic elements of the game. The game has more than 200 of these entities, and they make up a good part of the entire game code.

A grid with all the entities of Link's Awakening
Yes, there are that many entities in the game.
Each of them can require thousands of lines of code.

In the original source code, we have good reasons to believe that the entities code was grouped in a handful of source files.

├── entities3.asm
├── entities4.asm
├── entities5.asm
├── entities6.asm
├── entities7.asm
├── entities15.asm
└── entities18.asm
└── entities19.asm
└── entities36.asm

How the original code was probably structured.

But to make the code easier to browse and to understand, the disassembly attempts to split the code of each entity into its own source file.

├── 03__helpers.asm
├── 03_arrow.asm
├── 03_bomb.asm
├── 03_droppable_fairy.asm
├── 03_hookshot_hit.asm
├── 03_liftable_rock.asm
├── 03_moblin.asm
├── 03_octorok.asm
└── …

How the disassembly attempts to split the entities each into their own file.

These splits are not straightforward: the entities’ code is not cleanly isolated, but instead references a kind-of-standard set of helper functions, duplicated into each original file. Sometime an entity will even use some code from another entity in the same file!

So this is still very much a work in progress: at least one file needs to be split, and the file structure is not final yet. But it progresses steadily.

📖 Sprite-slots documentation

Daid took some time to research and document the ways entities sprites are defined and loaded on each room transition.

As the Game Boy video memory is quite limited, management of graphical resources is quite important. As for the NPC sprites, the game had a few challenges:

After a lot of research, this ended up in a large PR documenting the sprite-slots mechanism, and a higher-level wiki article on this topic.

The four spritesheets for room 07 on the Overworld.

To summarize the key points of the sprites resource management:

That’s the gist of it – but of course there’s more.

For a more detailed read on this topic, and details about how the following NPCs interact with this system, head to the sprite-sheets article on the wiki!

🕵️ Peephole replacement

Often, in the code, we need to turn a numerical value into a constant.

For instance, there may be a lot of patterns like this:

ld   a, $08              ; load the constant "08" into register a
ldh  [hMusicTrack], a    ; write the content of a to the variable hMusicTrack

There may be dozens of similar uses of hMusicTrack in the code.

At some point, someone may identify the meaning of all these numerical values:

MUSIC_NONE                              equ $00 
MUSIC_TITLE_SCREEN                      equ $01 
MUSIC_MINIGAME                          equ $02 
MUSIC_GAME_OVER                         equ $03
MUSIC_MABE_VILLAGE                      equ $04
MUSIC_OVERWORLD                         equ $05
MUSIC_TAL_TAL_RANGE                     equ $06
MUSIC_SHOP                              equ $07
MUSIC_RAFT_RIDE_RAPIDS                  equ $08
MUSIC_MYSTERIOUS_FOREST                 equ $09
; …

Good! But it now means that we need to look up all usages of hMusicTrack, and manually replace the numerical value by the proper constant. Tedious.

Luckily, @daid wrote a generic tool to make this task easier: the peephole replacer.

This tool can read a list of constants, a code pattern to look for — and then scan the whole code for this specific pattern.

In our case, we can use the peephole replacer with the following declaration:

    ld   a, $@@
    ldh  [hMusicTrack], a
""", read_enum("constants/sfx.asm", "MUSIC_"))

Now invoking ./tools/ will detect all uses of hMusicTrack in the code, and automatically replace the numerical value with the proper constant.

ld   a, MUSIC_RAFT_RIDE_RAPIDS              
ldh  [hMusicTrack], a    

Of course this has been used with many other constants as well (sound effects, entity flags, etc.). The peephole replacer can even perform more complex operations, like expanding the values of bitflags:

; Before running the peephole replacer, with a raw numerical constant
ld   hl, wEntitiesOptions1Table               
add  hl, bc                                  
ld   [hl], $D0
; After, the bitflag is properly decoded
ld   hl, wEntitiesOptions1Table               
add  hl, bc                                  

👥 Dialog lines attribution

A disassembled game is a great tool for fan-translations. Compared to ROM hacking, the script is easier to edit, and doesn’t require to relocate text pointers manually. Plus any language-specific features can be hacked in relatively easily.

So it’s no surprise that a handful of fan-translations started popping up (as seen in the next section).

Each translation has to go through all the dialog files. However, in these files, the dialogs are unordered, and out of context: there is no indication about where a specific dialog line or text is used. And looking up the dialog reference in the code doesn’t always work (because of dialog identifiers generated dynamically).

Fortunately, Kelsey Higham decided to improve this situation – starting with the speakers’ names. Now, beside almost every dialog line, a comment indicates which character or entity uses the line in the game.

Dialog19B:: ; Schule Donavitch
    db "Ya, I am Schule "
    db "Donavitch!      "

Some lines are easy to attribute to a specific character.

Dialog27A:: ; Marin
    db "Whew!  What a   "
    db "surprise!@"

Without context, that one would be less clear.

Now even the most obscure lines can be traced back. And it greatly helps to imagine the line in context, and translate it properly.

⛓ rgbds 0.6

The toolchain used to compile the Game Boy code, rgbds, is surprisingly active. Every year or so, its assembler, linker and tools get new features – and sometimes new deprecations. rgbds 0.6, released in October 2022, introduced a handful of breaking changes.

Modders are usually keen to work with the latest version of the toolchain. So @tobiasvl took on the task to fix the code for the latest assembler version.

But before that, a handful of issues needed to be resolved:

And finally, @tobiasvl messed with the Makefile, which can now pass the correct compilation flags to both older and newer versions of rgbds.

🧰 Windfish interactive disassembler

Most disassembly projects are presented as a bunch of text files, with barely any of the interactive tooling. A bare-bone syntax highlighting when lucky — but no navigation, code structure or type inference one can expect when working on modern languages.

This is where disassembly IDEs fill a gap. Instead of text files, they present an interactive and navigable view of the code. They also usually feature an integrated disassembler, pictures rendering, ties with an emulator for live code inspection, and so on. A notable example is DiztinGUIsh, a disassembler for Super NES games.

Until recently, no such IDE existed for Game Boy disassembly projects. That is, until jverkoey started working on his project.

Enter the Windfish interactive Game Boy disassembler.

Screenshot of the Windfish IDE
Syntax highlighting, navigation, memory regions, emulator, debugger: this GUI has it all.

Windfish can disassemble a Game Boy ROM, but that’s just the beginning. It is an interactive tool to explore the code, understand how it works, and document the various routines and memory locations.

One of its main features is that it integrates a tracing disassembler: it doesn’t just print the disassembled code, but attempts to simulate the execution, so that it can follow the code. And in the end, everything that has not been traced to executable code is probably data.

Windfish can also associate a memory region to a picture type, so that it is known that this memory represents tiles. Or it can recognize some code patterns, and generate RGBDS macros.

There are many more things to say about Windfish: how it has some neat coding tricks, documented in the Architecture description; how it integrates with the SameBoy emulator for a live exploration of the code; and so on.

The project is written in the Swift language, and runs on macOS. The core libraries (invoked from the command line) should theoretically work on Linux and Windows though. It still has some rough edges, but all the hard computer-science foundations are definitely present. That makes it one of the most promising tools of the scene.

✅ Powering ROM hacks

The disassembly, even in its unfinished state, made several romhacks possible (or at least way easier). Here are a few of them!

Among these projects, Daid’s LADX Randomizer holds a special place. While this randomizer is not directly based on the disassembly (internally the ROM is edited using binary patching), the disassembly is instrumental for its development. Daid also contributes its findings to the disassembly documentation.

What’s next?

A few months ago, the high-level engine documentation was featured on Hacker News, and widely appreciated. Since then it didn’t get much more content though. Some missing sections could clearly be extended.

Code-wise, the main missing areas are still the physics engine and the entities code, which are fully disassembled but not documented yet. A good point of focus for the next months!

Link's Awakening Disassembly Progress Report part 12

✨ New contributors

First let’s congratulate the following new contributors, who made their first commit to the project during the past months:

🔀 Building revisions

For a long time, this project only disassembled the source code for a single version of the game: the US v1.0 release.

A few months ago, Marijn van der Werf started to add support for the German version of the game.

Not an easy task. Not only the dialogs differ from the US version, of course – but there are quite some more differences: a few tilemaps (like the translated Game Over screen), some tiles (e.g. extra alphabet letters), a handful of regional differences…

But in the end, after carefully finding all the differences in the game resources and code, and storing the differences with the baseline English version into German-specific files, Marijn managed to add German support to the disassembly.

Zelda: Link's Awakening File Selection menu in German
The File Selection menu in all its German glory.

But Marijn didn’t stop there.

While he was at it, he casually added support for every version of the game ever released.

In all languages.

Japanese v1.0? Got it. French v1.2? Here you go. English v1.1? There it is.

Zelda: Link's Awakening File Selection menu in Japanese Zelda: Link's Awakening File Selection menu in French
It is now easy to study the Japanese or French games: they will be compiled along the other versions.

These versions all have many small differences between them. Some places were improved, some bugs were patched. Supporting all these versions meant identifying each of those small changes.

Zelda: Link's Awakening Title screen in Japanese Zelda: Link's Awakening Title screen menu in English
Some changes are obvious, like the Title screen between languages.
But other patches are much more subtle.

Moreover, the version are not linear: some patches applied to the Japanese 1.1 and 1.2 versions are not present in other languages’ 1.1 and 1.2 releases.

Fortunately, Xkeeper took some time to research and document these patches: when they were written, what they do. The resulting matrix accurately the complexity of the actual revisions:

|       -       | JP 1.0 | JP 1.1 | JP 1.2 | US 1.0 | US 1.1 | US 1.2 | FR 1.0 | FR 1.1 | DE 1.0 | DE 1.1 |
| `__PATCH_0__` |        |  Yes   |  Yes   |        |  Yes   |  Yes   |  Yes   |  Yes   |  Yes   |  Yes   |
| `__PATCH_1__` |        |        |        |        |        |        |  Yes   |  Yes   |  Yes   |  Yes   |
| `__PATCH_2__` |        |  Yes   |  Yes   |        |        |  Yes   |  Yes   |  Yes   |  Yes   |  Yes   |
| `__PATCH_3__` |        |  Yes   |  Yes   |        |  Yes   |  Yes   |        |        |        |        |
| `__PATCH_4__` |        |        |  Yes   |        |        |  Yes   |        |  Yes   |        |  Yes   |
| `__PATCH_5__` |        |        |        |        |        |        |        |        |  Yes   |  Yes   |
| `__PATCH_6__` |  Yes   |  Yes   |  Yes   |        |        |        |        |        |        |        |
| `__PATCH_7__` |        |        |        |        |        |        |  Yes   |  Yes   |        |        |
| `__PATCH_8__` |        |  Yes   |  Yes   |        |        |        |        |        |        |        |
| `__PATCH_9__` |  Yes   |  Yes   |  Yes   |        |        |        |        |        |  Yes   |  Yes   |
| `__PATCH_A__` |    1   |    1   |    1   |        |        |        |        |        |    2   |    2   |
| `__PATCH_B__` |    1   |    1   |    1   |        |        |        |    2   |    2   |    1   |    1   |
| `__PATCH_C__` |        |        |        |  Yes   |  Yes   |  Yes   |        |        |        |        |

Read the full patches notes to get a idea of what each patch does.

Also, people at The Cutting Room Floor have been documenting the version differences for many years now. So now it’s all a matter of matching the differences in the code to the observable behavior changes.

With this massive work, the ZLADX disassembly can now build ten different revisions of the game, with exact byte-for-byte compatibility.

🧩 Fixing the spritesheets

Sprites modding has been a feature of the ZLADX modding community for a long time. Popular randomizers like Z4R or LADXR allow you to customize the characters visuals by replacing the spritesheets.

However, until now, the spritesheets in the disassembly were not easy to edit. Actually, they were in a sorry state.

Well, that’s not all bad: spritesheets were stored as PNG files, which makes them easy to view, and automatically converted to the Game Boy 2bpp format at compile-time. But many things were confusing in those PNG files. Here’s for instance how the first Link’s sprites appeared in the disassembly:

A sample spritesheet, with anything hardly recognizable
This raw dump of the sprites to a PNG file is not very clear.

It hard to see anything. That’s because this is a raw conversion of the in-ROM 2bpp tiles format to a PNG file. No other conversion is made, which causes the picture to be hard to read.

There are two things missing here.

First, the colors are wrong. Or, more precisely, the grayscale is wrong. When running on a Game Boy Color, the colors are applied at runtime, by matching each tile with a separately-defined color palette. So even on the Game Boy Color, graphics remain stored as grayscale, with 4 possible gray values.

But on the picture above, what should be rendered as the blackest gray value is instead rendered as white. And other grays are wrong to.

Contributor @AriaHiro64 found a fix for this: by tweaking the PNG file to reorder the indexed colors table, they were able to fix the grayscale values – while still retaining compatibility with the tool that transform these PNG files into 2bpp files at compile-time.

The same spritesheet with inverted colors, which makes things slightly easier to see
Proper color indexation already makes it more legible.
Still looks like a puzzle though.

Now it’s easier to see the other missing element: the tiles are not ordered in the most natural way.

This is because on the Game Boy, sprites can be either a single tile (8×8 px) or two tiles (8×16 px). And on Link’s Awakening, most characters made of sprites are at least 16×16 px – that is, each character is composed of two 8×16 sprites stitched together vertically.

So tiles for sprites often differ from tiles used to store background maps. Tiles for background maps are usually stored horizontally, from left to right, as:


So the conversion of background map tiles from 2bpp tilesheets to PNG is straightforward.

But tiles for sprites are usually stored vertically, from top to bottom, as:


So naïvely converting a spritesheet to a PNG file yields tiles ordered as 1️⃣ 3️⃣ 2️⃣ 4️⃣, which will look wrong, exactly like on the picture above.

To solve this, we have to go through a process named interleaving: when extracting the original tiles to a PNG file, we fix the tiles ordering by de-interleaving them. The resulting PNG file then has the tiles in the proper order.

And at compile-time, when transforming the PNG files to to the native 2bpp format, the same Python script interleaves the tiles, back to the original representation.

The same spritesheet inverted *and* interleaved, which makes all sprites appear clearly
When properly de-interleaved, Link’s sprites appear in their full glory.

To make this process easier to automate, a simple Make rule specifies that all PNG files prefixed with oam_ are automatically inverted and interleaved at compile-time.

So thanks to these conversion steps, now all spritesheets of the game can be easily browsed and edited. Have a look!

🏞 Decoding the tilemaps

A few months ago, in the disassembly source code, background tilemaps were all stored sequentially in a single ASM file.

This was suboptimal for many reasons:

  1. The tilemaps were not named, which made identifying a tilemap difficult;
  2. The ASM file format could not be imported into a tilemap editor;
  3. The tilemaps are stored encoded, and it was difficult to write a tool to decode a single tilemap to a format readable by a tilemap editor.

But since June, the situation greatly improved. First, Daid wrote a tool to parse the data format used by the tilemaps, and decode the data as readable instructions.

Then another PR identified and named the tilemaps. And last, all tilemaps are now exported them as individual binary files. So you can simply browse the tilemaps, and peach.tilemap.encoded will contain the data you expect.

Having separate files also makes easier to compare the differences between the successive revisions of the game. Before, all tilemaps were stored for each revision. But now only the tilemaps that actually differ from revision to revision are stored (usually mostly the file menus, because they include text that had to be localized).

A screenshot of Tilemap Studio editing a decoded Link's Awakening tilemap
Tilemaps can now be edited graphically using standard tools, like Tilemap Studio.

Why storing the tilemaps encoded?

Ideally, the tilemaps would be stored in a decoded, easily manipulable format (that is, as a raw sequence of tile identifiers). And at compile-time, they would be re-encoded into the format expected by the game engine.

But unfortunately, when developing the original game, the encoded tilemaps were not machine-generated from decoded files. Instead they were hand-written by the original developers. So if we used an automated tool to encode the tilemaps, we wouldn’t get exactly the same result than the hand-written encoding: it would be functionally similar, and produce the same tilemaps, but the exact bytes wouldn’t be the same. Which means we would no longer have a byte-for-byte identical ROM.

Instead, the files are stored in the original encoded format. And to made them easier to edit, the disassembly now includes a command-line tool to decode the tilemaps to the raw binary format suitable for import into tilemap editors.

Initializing the fishing minigame

On a smaller note, Daid documented the initialization values used by the fishing minigame.

Did you ever want to build a custom version of the game with only the bigger fishes? Now is your chance!

Link's Awakening modded fishing game, with only the bigger fishes
Wow, fishes in this pond sure must be well-fed…

What’s next?

Of course many more improvements were done in the past months, much more than what is presented there.

And as for next steps, although the tilemap values are now decoded, the tilemap attributes are not. As the attributes associate a tile to a color palette, that means editing the tilemap colors is still harder than it should be. Fortunately, the tilemap attributes are stored in the same format than the tilemap values, so writing a tool to decode them should be straightforward.

Also the high-level engine documentation is still evolving. It started as an incomplete description of the various systems of Link’s Awakening game engine, but is becoming more and more fleshed out. Many topics are still waiting to be explained though.

Achieving partial translucency on the Game Boy Color

Neither the original Game Boy or the upgraded Game Boy Color had hardware support for partial translucency. This was always achieved with various hacks and workaround.

Let’s have a look at two of the most elaborated examples of these techniques, as showcased on the Title screen and End credits of Zelda Link’s Awakening.

Progressive fade-in on the title screen

On the title screen, after the main title appears, the “DX” logo appears with a nice, smooth fade.

Link's Awakening Title screen

But wait, the GBC doesn’t have variable opacity values. A sprite pixel can be fully transparent, or fully opaque – but there’s no “fade opacity from 0 to 1”.

So how is this gradual fading effect done?

The trick is that, instead of updating the opacity, the game updates the palette of the “DX” logo. Each frame, the “DX” palette is changed to go gradually from the sky shade to the logo shade. Nice.

The palettes of the “DX” sprite during the fade-in

But wait! That shouldn’t work. The “DX” logo doesn’t sit over a flat-colored surface: it overlaps both the sky and the clouds. So how could changing the palette affect both the “sky → logo” and “clouds → logo” color progression?

Indeed, that’s an issue. And to solve it, the “DX” logo is split into two parts: one set of sprites for the part overlapping the sky, and one set for the part overlapping the clouds. Each part has its own palette, with its own gradual progression.

First set of sprites

Second set of sprites

Overall, a lot of effort for a small effect, that looks really easy to perform on modern hardware nowadays. But on the GBC, it actually required a good lot of tricks.

Secret ending: fading Marin in and out

When beating the game without dying once, a portrait of Marin is displayed after the end credits.


We can see the same kind of transparency effects than on the Title screen:

  1. Marin’s face fades in, and displays many colors.
  2. Marin’s face remains half-translucent for a while
  3. A seagull fades in
  4. Marin’s face fades out

That’s a lot to unpack here. Each of these effects is not easily doable using the GBC hardware.

1. Marin’s fades in

The fade-in uses the same palette-update trick than the “DX” logo on the title screen: the sprite palettes are updated every few frames, and move from blue to the actual portrait colors.


However, notice that the portrait is quite colorful. Some 8x8 areas even display up to 6 different colors at the same time, like Marin’s medallion:

There are more colors on this single tile than the Game Boy Color usually allows.

A standard sprite can only display 4 colors on the GBC (that is, 3 actual colors + a transparent one). So how is it done?

The answer is that the portrait is split into two layers of sprites, each with their own palette.

First layer of Marin's sprites
Layer 1

Second layer of Marin's sprites
Layer 2

It is then composited to get the full portrait.

Composited Marin's portrait
The two layers one on top of each other

This solves the “Many different colors” issue: different layers of 3-colors sprites are overlaid on top of each other.

(However, unlike the DX title logo, the difference of fading between the sky and the clouds is not accounted for. The overlapped cloud areas become blue at the beginning of the animation.)

2. Marin’s face remain half-translucent for a while

Neither the GB or the GBC have half-transparent rendering – but they have the latency of the LCD screen. So the good old 50% transparency trick is used: displaying the portrait only every other frame.

Marin's portrait slowed down
The blinking effect, slowed down.

On full speed, the latency of the original screen then creates the half-transparency effect.

3. A seagull fades in

The seagull fades in gradually, on the middle of Marin’s face.

Same trick: this is done by gradually updating the palettes of the seagull sprite.

4. Marin’s face fades out

Again, Marin’s portrait palettes are gradually shifted to becoming blue again.

And that’s it. A neat composition of several tricks, for a good and moving ending.

So there it is: some of the tricks used to unlock partial translucency on the Game Boy Color hardware. Of course these techniques are quite time-consuming, and thus are only used during a handful of key moments.

A few years later, the hardware translucency support of the Game Boy Advance will make these kind of effects much easier to achieve.