Mastodon
Table of Contents
Banking is a slightly advanced topic. It gives the developer the ability to create larger and more complex games.
Table of Contents

How to use ROM Memory Banks

Banking is a slightly advanced [but often necessary] topic when it comes to Game Boy game development. Before learning about banking, one should be familiar with the GBDK-2020 in general. For additional help, check out the official GBDK Documentation page on banking

What is “Banking”?

Banking gives the developer the ability to create larger and more complex games. It does this by providing an additional set of memory for the developer to use. This extra memory is stored in the cartridge, in different “memory banks”. Usage of that memory is facilitated by a Memory Bank Controller, or MBC for short. MBCs, will not be explained in detail. For more information about MBCs, refer to the Gameboy Pan Docs.

There are two types of Banking: ROM Banking, and RAM Banking. This tutorial will focus on ROM banking since it’s most common.

How do i know if i need banking in my game?

You’ll need banking if your game needs to be larger than 32kb in size.

During development, the easiest way to know this, is when you run into a compile error along the lines of:

“Warning: Multiple write of XXXX bytes at 0x4000”.

This means that data is leaking from one bank, into another.

You also might get this error:

error: ROM is too large for number of banks specified

Explaining the Memory Banks

⚠️ NOTE: Some game boy cartridges do not support banking. If you’re looking to publish your game physically, make sure you know the specs of the cartridge being used.

Gameboy cartridges that support banking, will have a (fixed) Bank 0, and a variable number of additional (non-fixed) banks. Bank 0 starts at memory address: 0x0000 and ends at 0x3FFF. Here’s where things get complicated.

The address space from 0x4000h to 0x7FFF will be shared between (non-fixed) memory banks, but not all at the same time. The developer will be able to change which bank uses the space

The fixed Bank 0 is always active and accessible. However, Only one of the non-fixed memory banks can be active at a given time. In addition, functions & constants inside a non-active memory bank cannot be used.

It’s important to put your universal or “bank-agnostic” functions/data into bank 0. However, Because it has a fixed size, it’s not good to overcrowd it with things that would be fine in ba switchable bank.

Enabling Memory Banking

You MUST enable memory banking during compilation. There are 3 arguments you can pass to lcc during compilation of your ROM file.

  • Wl-yo<N> where <N> is the number of ROM banks. 2, 4, 8, 16, 32, 64, 128, 256, 512
    • Wl-yoA may be used for automatic bank size.
  • Wl-ya<N> where <N> is the number of RAM banks. 2, 4, 8, 16, 32
  • Wl-yt<N> where <N> is the type of MBC cartridge (see cartridge types below).

The MBC settings below are available when using the makebin MBC switch. Additional details available at Pandocs.

⚠️ To enable memory banking, choose a cartridge type that has MBC1, MBC2, MBC3, or MBC5 in it.

For most GBDK projects, MBC5 is recommended. The SWITCH_ROM() / ref SWITCH_RAM() macros work with MBC5 (up to ROM bank 255, SWITCH_ROM_MBC5_8M may be used if a larger size is needed). MBC1 is not recommended. Some banks in it’s range are unavailable. See pandocs for more details. https://gbdev.io/pandocs/MBC1

# 0147: Cartridge type:
# 0-ROM ONLY            12-ROM+MBC3+RAM
# 1-ROM+MBC1            13-ROM+MBC3+RAM+BATT
# 2-ROM+MBC1+RAM        19-ROM+MBC5
# 3-ROM+MBC1+RAM+BATT   1A-ROM+MBC5+RAM
# 5-ROM+MBC2            1B-ROM+MBC5+RAM+BATT
# 6-ROM+MBC2+BATTERY    1C-ROM+MBC5+RUMBLE
# 8-ROM+RAM             1D-ROM+MBC5+RUMBLE+SRAM
# 9-ROM+RAM+BATTERY     1E-ROM+MBC5+RUMBLE+SRAM+BATT
# B-ROM+MMM01           1F-Pocket Camera
# C-ROM+MMM01+SRAM      FD-Bandai TAMA5
# D-ROM+MMM01+SRAM+BATT FE - Hudson HuC-3
# F-ROM+MBC3+TIMER+BATT FF - Hudson HuC-1
# 10-ROM+MBC3+TIMER+RAM+BATT
# 11-ROM+MBC3

Example:

/path/to/gbdk/bin/lcc -Wl-yt0x1A -Wl-yo4 -Wl-ya4 -o MYRomFile.gb file1.c file2.c file3.c

Putting source code into memory banks

There are two ways to put source code into memory banks.

The first way is by using the “-Wf-bo<N>” argument when compiling that .c file into a .o file. Where <N> is the memory bank you want to place the contents of the file in.

Here’s an example for putting the contents of “file1.c” (made up) into ROM bank 1:

/path/to/gbdk/bin/lcc -Wf-bo1 -Wf-ba1 -c -o file1.c file1.o

The second way is by placing “#pragma bank <N>” at the start of the .c file’s source code. Where <N> is the memory bank you want to place the contents of the file in.

#pragma BANK 3

NOTE: GBDK-2020 has an “autobanking” feature you can use instead of manually specifying a bank. More on that later.

BANKED and NONBANKED functions

When you declare and define functions, you can optionally add one of two keywords that’ll tell GBDK some more information about it. These 2 keywords are BANKED and/or NONBANKED.

If you are inside a file that will be placed into a memory bank: By default, functions are “bank local”. Meaning they are banked, and cannot be accessed outside of the given bank. If you use the “BANKED” keyword when defining/declaring the function, it will be accessible outside of it’s file.

#pragma BANK 2

void MyBankLocalFunction(){
     // Do something cool in bank 2
}

void MyExportedBankedFunction() BANKED{
     // Do something cool in bank 2
}

You can also force a single function to be in Bank 0, by using the “NONBANKED” keyword when defining/declaring the function

#pragma BANK 3

void MyFunctionInBank0() NONBANKED{
     // Do something cool in bank 0
}

Be careful, as Bank 0 only has 16kb. You should save it for critical things.

Pitfall – BANKED/NONBANKED header file mismatch

If a function is “bank local”, BANKED, or NONBANKED in it’s .c implementation, it should also be declared such in a matching header file declaration. Mismatching types can cause unforeseen problems.

Accessing functions & constants in different bank

To use functions and/or constants in a different bank you must switch to that bank. This is called “bank switching”.

To Switch ROM memory banks, you can simply use the builtin GBDK-2020 macro “SWITCH_ROM

uint8_t GetSomeValue() NONBANKED{

     uint8_t someValue=0;

     // We don't need to change banks manually here
     someValue += FunctionInDifferentBank();

     // CURRENT_BANK is a gbdk-2020 macro letting us know
     // which bank is currently active
     uint8_t _previous_bank = CURRENT_BANK;

     // Switch the upper 16kb to point towards Bank 2
     SWITCH_ROM(2);

     someValue += uint8ConstantInBankTwo;

     // Switch BACK
     SWITCH_ROM(_previous_bank);

     return someValue;

}

Fortunately, the GBDK-2020 devs have already implemented automatic bank switching when calling functions in another bank (as long as their not “bank local”).

The difficulty arises when accessing constants inside another bank.

⚠️ Reminder: You don’t need to manually switch banks when calling a banked function. GBDK-2020 already does that for you.

Pitfall – Manually Switching Banks in a Banked function.

The SWITCH_ROM macro can only be used in a nonbanked function. Otherwise, if you use it in a banked function, you will interrupt execution of the current function.

You won’t get any compiler issues if you try this, your game will just completely break when it happens.

⚠️ Rule of Thumb: If your game suddenly breaks and VRAM looks glitched/empty there are two likely causes. A) you either tried to switch banks in a banked function, or B) you tried to access constants in a inactive memory bank.

void MyBankedFunction() BANKED{
	
	// Switch the upper 16kb to point towards Bank 2
	// If bank 2 isn't the currently active bank, this next line WILL COMPLETELY BREAK YOUR GAME!
	SWITCH_ROM(2);
	
	// If bank 2 isn't the currently active bank, the gameboy/emulator will not get this far.
	... // I want to do something with bank 2's data
	
}

Pitfall – Not Restoring the previous active memory bank.

Often in a NONBANKED function, you’ll have to temporarily switch the currently active memory bank so you can access different data. Weird things can occur (including your game-breaking), if you do not switch back to the previous bank when done.

After you’ve used the data in the newly active bank, switch back to the previous bank.

Very often, you will not be aware of which bank is currently active, so the simple solution is to record the current bank before you switch. You can use the built-in gbdk-2020 macro “_current_bank” for this.

Autobanking

To simplify the banking process for GBDK-2020 devs introduced “autobanking”. To enable auto banking, use the “-autobank” argument when compiling your ROM file.

rom compilation example:

/path/to/gbdk/bin/lcc -autobank -Wl-yt0x1A -Wl-yo4 -Wl-ya4 -o MYRomFile.gb file1.c file2.c file3.c

With autobanking enabled, you can let the compiler automatically place data into different banks by adding 2 things to your code:

  • Adding “#pragma bank 255” to the start of the file tells lcc to use autobanking.
  • Adding a unique BANKREF identifier will create a constant you can later use to get the actual bank number.
#pragma bank 255
BANKREF(level_1_map)
...
const uint8_t level_1_map[] = {... some map data here ...};

In a separate file, use the matching “BANKREF_EXTERN” macro to declare the externally banked data.

With that set, you can pass the original unique id with the “BANK” macro to get the actual bank value.

BANKREF_EXTERN(level_1_map)

void MyNonBankedFunction() NONBANKED{

	// "_current_bank" is a built-in GBDK-2020 macro for the currently active bank
	uint8_t previous_bank = _current_bank;
	
  SWITCH_ROM( BANK(level_1_map) );
  // Do something with level_1_map[]
	
	// Switch to whatever bank was active before
	SWITCH_ROM(previous_bank);

}

Autobanking also has the benefit of “no cost” and automatic rearranging banks when needed.

Did an asset grow too large to fit in bank 2? You don’t have to go back and reorganize possibly multiple source files after something like that, it just gets organized and updated automatically.

Far Pointers

By default, pointers to data outside of an active bank (including bank 0) will not work.

If you need a way to point towards a constant and/or function outside of the current bank, you can use far pointers. Far pointers are essentially pointers to objects in another bank.

A far pointer consists of two parts:

  • The pointer (OFS)
  • The bank that pointer’s in (SEG)

For the best example of using far pointers, check out the cross-platform “bank_farptr” example in your gbdk/examples folder.

Here’s a small portion of it:

#include <stdint.h>
#include <stdio.h>
#include <gbdk/platform.h>
#include <gbdk/far_ptr.h>

// functions from bank2code.c
BANKREF_EXTERN(some_bank2_proc0)
extern void some_bank2_proc0(void) __banked;

// far pointers
FAR_PTR farptr_var2;

void run(void) {
    // compose far pointer at runtime
    farptr_var2 = to_far_ptr(some_bank2_proc0, BANK(some_bank2_proc0));

    // try calling far function by far pointer without params
    FAR_CALL(farptr_var2, void (*)(void));

}

Far pointers are useful if your game is getting large, and you need to dynamically point to data in different banks.

PNG2Asset Usage

PNG2Asset is a utility that comes with the gbdk-2020. This utility converts a PNG file into a GBDK acceptable format. Many different arguments are accepted to suit your needs.

The PNG2Asset utility accepts an argument “-b” for banking. You can pass into it 255 for autobanking, or a specific bank. As a result, generated .c and .h files will have a “bank” pragma directive added to the top

In addition, “BANKREF”s will be generated for the converted asset.

You can find more information about PNG2Asset on my other tutorial for it here.

If you want to test out your game on an actual Game Boy, you can run your game on a SD-Based Flash Cart like the EZ Flash JR
Who else is making Game Boy Games?

There are many individuals who are developing games for retro consoles. Here’s one you might like:

GB Wordyl

Created By: bbbbr

A portable word game that tests your skill! Use strategy and knowledge to guess the hidden word within 6 tries!

Other Tutorials:

Sign-Up for the "Junkyard Newsletter" for more Game Boy news!

If you like Game Boy Game Development, subscribe to the Junkyard Newsletter. Stay up-to-date with all the latest Game Boy news. It’s free, and you can unsubscribe any time!