Init and Main
Everyone who has programmed in C/C++ or even been tangentially exposed to it knows about main
– the special name reserved for the function that will first run when the program begins. This is, of course, perched on a throne of lies; it's not the first to run, it's just the first to run after some boring housekeeping stuff that is essentially identical in every program on a given platform and generally inserted automatically by the compiler. Of course, we are insisting on running bare-metal on a tiny toy for children and so we get to manually include our own pre-main
initialization steps.
As mentioned previously, the ROM header contains the first instruction of the game at 0x08000000
, which we can see is b Init
, where b
(ranch) is equivalent to goto
. The Init
function is located in crt0.s which is simply a conventionalized name for the code that initializes the C runtime. It is written in assembly because it needs to interface directly with the specific hardware at hand in a way that cannot be expressed at the C abstraction level. If you want to look up more about any of the opcodes, keep in mind that the GBA is a 32-bit ARM architecture with THUMB support, not an x86 one.
Note
32-bit ARM processors can run in two distinct modes, dubbed ARM and THUMB, and switch between them on a function-by-function basis. ARM opcodes are four bytes each and THUMB opcodes are two bytes each, and hence can encode less information per operation but in many cases will make the function much smaller overall for the purposes of reducing ROM size. The switching modes is done with a clever trick: since opcodes are always aligned in memory (starting on a multiple of 4 or 2 depending on their size), there's no such thing as an instruction that starts at an odd address. Hence, the smallest bit of an address can be used as a flag to indicate THUMB mode. If the CPU attempts to branch to an odd-numbered address, it switches to THUMB mode and subtracts one from the address before branching. When it branches to an even one, it switches to ARM mode.
crt0.s
Init::
mov r0, #PSR_IRQ_MODE
msr cpsr_cf, r0
ldr sp, sp_irq
mov r0, #PSR_SYS_MODE
msr cpsr_cf, r0
ldr sp, sp_sys
ldr r1, =INTR_VECTOR
adr r0, IntrMain
str r0, [r1]
.if MODERN
mov r0, #255 @ RESET_ALL
svc #1 << 16
.endif @ MODERN
ldr r1, =AgbMain + 1
mov lr, pc
bx r1
b Init
Do not panic if this is completely incomprehensible. The vast majority of the game is written in C.
This code relies heavily on writing magic numbers to magic registers that can only be understood with manufacturer documentation. It first initializes the IRQ (interrupt request) stack at the very end of fast RAM, reserving a small amount of space for the hardware to store information about events the game will want to process. It then initializes the system stack for the arguments of functions, also in fast RAM (IWRAM). Within a specific slot of the IRQ stack we just set up, specifically INTR_VECTOR
== 0x3007FFC
, we store the address of the function IntrMain
so it can be called to process interrupts.
The .if MODERN
brackets our first optional deviation from the original game, although it's just moving the operation from immediately after AgbMain
begins to immediately before it to keep compilers happy. The svc
or supervisor call instruction is an alternate name for the swi
or software interrupt instruction which will call into the utility functions stored in the BIOS. Due to Reasons, the index number of the requested interrupt needs to be different depending on whether the CPU is currently in ARM or THUMB mode. Since we are in ARM mode, we need to take the index we want and shift it by 16. BIOS function #1 is RegisterRamReset
(reminder: documentation) and we are calling it with an argument of RESET_ALL
, meaning all registers and RAM (except for a small carve-out for the IRQ area, which it knows to leave alone) will be set to a clean state of 0. While a game may seem to work fine without doing this, it is a very good idea to always reset memory to a known state. On real hardware, the RAM and registers will power on with random values due to electrical noise. This can lead to random bugs that cannot be reproduced at will. (Emulators usually clean the pretend RAM and registers before emulation begins, but they are not required to do so.)
Note
A real-world example of a bug introduced by not clearing RAM can see seen in Super Mario All-Stars. If a certain byte in RAM happened to randomly settle to 0x80
when the Super Nintendo was powered on, the game would mistakenly believe it was a developer system and enable debug mode. Since this randomization is determined by the physical environment, some Super Nintendos were more prone to initializing in that exact way than others, and hence some people seemingly had a "debug copy" of the game.
Finally, the address of AGBMain
is loaded (consult the note above on THUMB for why we add 1) and called. If AGBMain
ever returns (it shouldn't!) then the last instruction of Init
will loop back to the first. Otherwise, since there is no other layer underneath to return to, execution would simply proceed to whatever happened to be next in memory – which in this case is some constants referenced by Init
which aren't meant to be executed as instructions.
IntrMain
, which was installed by Init
as the interrupt handler, mostly consists of an if-cascade that checks for each interrupt flag one at a time, incrementing an index in r12
as it goes. The bit that actually makes things happen is small:
ldr r1, =gIntrTable
add r1, r1, r12
ldr r0, [r1]
stmfd sp!, {lr}
adr lr, IntrMain_RetAddr
bx r0
This simply indexes into gIntrTable
, which will be set up later, and calls whatever function it finds there. Note that interrupts are not yet enabled as of the moment we are stepping into AgbMain
and hence there's no danger of IntrMain
being called before the table is filled out.
That's it – that's every bit of secret pre-main
initialization starting from the cartridge's first instruction. We may now progress to the master game loop.
main.c
Just as a curiosity, we can see the exact moment that the final build of the English edition of Emerald was compiled:
const char BuildDateTime[] = "2005 02 21 11:10";
Note that's a few months before the game went on sale; physical cartridges have a lot of lead time. Clearly, this time stamp was patched in by the build tools rather than manually typed into the original source code. (Specifically, a header file containing nothing but the current timestamp was generated at each compile time by the Makefile.) It's preserved here exactly as it was so that the ROM can be recompiled back to the same state. Another curiosity is that the leaked source dump has a timestamp of "2005 06 14 15:20"
, so it appears to be a backup made while preparing the European release that launched in October. The final Spanish ROM's timestamp is two weeks later: "2005 07 01 18:30"
. (Almost all such leaks come from third party localizers who needed the ability to edit the source code directly. The disks they were mailed kick around in someone's garage until someone else who never signed a contract with the publisher finds them.)
In IntrMain
we saw an interrupt table which had not yet been initialized. Its default state is encoded here, with many of the entries set to a dummy placeholder as the game does not actually care about them:
const IntrFunc gIntrTableTemplate[] =
{
VCountIntr, // V-count interrupt
SerialIntr, // Serial interrupt
Timer3Intr, // Timer 3 interrupt
HBlankIntr, // H-blank interrupt
VBlankIntr, // V-blank interrupt
IntrDummy, // Timer 0 interrupt
IntrDummy, // Timer 1 interrupt
IntrDummy, // Timer 2 interrupt
IntrDummy, // DMA 0 interrupt
IntrDummy, // DMA 1 interrupt
IntrDummy, // DMA 2 interrupt
IntrDummy, // DMA 3 interrupt
IntrDummy, // Key interrupt
IntrDummy, // Game Pak interrupt
};
Note
HBlank and VBlank are terms dating from the CRT television era. The pixels of the screen are updated left-to-right and top-to-bottom each frame. The horizontal blank interrupt activates each time the end of a row of pixels is reached (hence 160x per frame) and the vertical blank interrupt activates each time the end of rendering is reached and the screen will return to the first pixel in the upper left (hence definitionally once per frame, very slightly faster than 60x per second). This lets the game know when it's time to update the visuals that will be rendered to the screen. It is the responsibility of the programmer to keep the handlers for these interrupts as short as possible; it would be very bad if VBlank activated while only halfway through processing the previous VBlank!
Next are a messy set of miscellaneous globals for tracking game state:
static u16 sUnusedVar; // Never read
u16 gKeyRepeatStartDelay;
bool8 gLinkTransferringData;
struct Main gMain;
u16 gKeyRepeatContinueDelay;
bool8 gSoftResetDisabled;
IntrFunc gIntrTable[INTR_COUNT];
u8 gLinkVSyncDisabled;
u32 IntrMain_Buffer[0x200];
s8 gPcmDmaCounter;
static EWRAM_DATA u16 sTrainerId = 0;
The one marked unused is named mssize
in the original code and is set to the sizeof()
of a structure used for multiplayer on the serial cable. It's presumably vestigial from early scaffolding. The gMain
structure is defined in main.h and bundles together several things that are constantly relevant in every phase of the game:
struct Main
{
/*0x000*/ MainCallback callback1;
/*0x004*/ MainCallback callback2;
/*0x008*/ MainCallback savedCallback;
/*0x00C*/ IntrCallback vblankCallback;
/*0x010*/ IntrCallback hblankCallback;
/*0x014*/ IntrCallback vcountCallback;
/*0x018*/ IntrCallback serialCallback;
/*0x01C*/ vu16 intrCheck;
/*0x020*/ u32 vblankCounter1;
/*0x024*/ u32 vblankCounter2;
/*0x028*/ u16 heldKeysRaw; // held keys without L=A remapping
/*0x02A*/ u16 newKeysRaw; // newly pressed keys without L=A remapping
/*0x02C*/ u16 heldKeys; // held keys with L=A remapping
/*0x02E*/ u16 newKeys; // newly pressed keys with L=A remapping
/*0x030*/ u16 newAndRepeatedKeys; // newly pressed keys plus key repeat
/*0x032*/ u16 keyRepeatCounter; // counts down to 0, triggering key repeat
/*0x034*/ bool16 watchedKeysPressed; // whether one of the watched keys was pressed
/*0x036*/ u16 watchedKeysMask; // bit mask for watched keys
/*0x038*/ struct OamData oamBuffer[128];
/*0x438*/ u8 state;
/*0x439*/ u8 oamLoadDisabled:1;
/*0x439*/ u8 inBattle:1;
/*0x439*/ u8 anyLinkBattlerHasFrontierPass:1;
};
Zoom. Enhance.
u8 state;
This is a load-bearing byte.
gMain.state
is the master state machine tracker value. It's updated in over 500 locations across several dozen source files. Each distinct type of screen (title screen, evolution screen, etc) uses it to keep track of where it is in its sequence of possible events. The meaning of each possible value is specific to each screen. In a hypothetically perfect engine each game mode should track its state independently, but memory is tight and it's not possible for more than one to be running concurrently.
The actual main function is pretty concise, and on a first reading you will almost certainly miss where the actual game happens:
void AgbMain()
{
// Modern compilers are liberal with the stack on entry to this function,
// so RegisterRamReset may crash if it resets IWRAM.
#if !MODERN
RegisterRamReset(RESET_ALL);
#endif //MODERN
*(vu16 *)BG_PLTT = RGB_WHITE; // Set the backdrop to white on startup
InitGpuRegManager();
REG_WAITCNT = WAITCNT_PREFETCH_ENABLE | WAITCNT_WS0_S_1 | WAITCNT_WS0_N_3;
InitKeys();
InitIntrHandlers();
m4aSoundInit();
EnableVCountIntrAtLine150();
InitRFU();
RtcInit();
CheckForFlashMemory();
InitMainCallbacks();
InitMapMusic();
#ifdef BUGFIX
SeedRngWithRtc(); // see comment at SeedRngWithRtc definition below
#endif
ClearDma3Requests();
ResetBgs();
SetDefaultFontsPointer();
InitHeap(gHeap, HEAP_SIZE);
gSoftResetDisabled = FALSE;
if (gFlashMemoryPresent != TRUE)
SetMainCallback2(NULL);
gLinkTransferringData = FALSE;
sUnusedVar = 0xFC0;
#ifndef NDEBUG
#if (LOG_HANDLER == LOG_HANDLER_MGBA_PRINT)
(void) MgbaOpen();
#elif (LOG_HANDLER == LOG_HANDLER_AGB_PRINT)
AGBPrintfInit();
#endif
#endif
for (;;)
{
ReadKeys();
if (gSoftResetDisabled == FALSE
&& JOY_HELD_RAW(A_BUTTON)
&& JOY_HELD_RAW(B_START_SELECT) == B_START_SELECT)
{
rfu_REQ_stopMode();
rfu_waitREQComplete();
DoSoftReset();
}
if (Overworld_SendKeysToLinkIsRunning() == TRUE)
{
gLinkTransferringData = TRUE;
UpdateLinkAndCallCallbacks();
gLinkTransferringData = FALSE;
}
else
{
gLinkTransferringData = FALSE;
UpdateLinkAndCallCallbacks();
if (Overworld_RecvKeysFromLinkIsRunning() == TRUE)
{
gMain.newKeys = 0;
ClearSpriteCopyRequests();
gLinkTransferringData = TRUE;
UpdateLinkAndCallCallbacks();
gLinkTransferringData = FALSE;
}
}
PlayTimeCounter_Update();
MapMusicMain();
WaitForVBlank();
}
}
Did you spot it? The game happens inside UpdateLinkAndCallCallbacks()
. A callback is when a function's address is stored in a variable by one piece of code so that another piece of code will later call it without necessarily knowing or caring exactly what it will do. Various pieces of game logic will stash functions in the callback variables (we saw them in the gMain
struct) for AgbMain
to call on its next loop and keep the game moving. The very first callbacks are initialized in the helper function InitMainCallbacks()
– technically only one is set, CB2_InitCopyrightScreenAfterBootup()
. We will check it out later.
If you browse the various helper functions called in the initialization stage of the main function, you will see something that is traditionally a no-no in application programming – writing directly to a specific hard-coded address:
void StartTimer1(void)
{
REG_TM1CNT_H = 0x80;
}
(Where REG_TM1CNT_H
== 0x04000106
after chasing it down through multiple layers of defines.) When working directly with the real, unprotected memory space of a device, it is typical for specific addresses to be physically wired to hardware components for the purpose of interacting with them. These addresses are unique to every kind of device and must be looked up in documentation. This address is connected to "Timer 1 Control", and by setting it to 0x80
(better understood as 0b10000000
since it is a set of bit flags) the timer is enabled and starts ticking.
There's actually a very serious bug in AgbMain()
, one which has a huge impact on advanced gameplay such as speedrunning and shiny hunting: they forgot to seed the RNG. They straight-up forgot! In Ruby/Sapphire, there is a call to SeedRngWithRtc()
following InitMapMusic()
:
static void SeedRngWithRtc(void)
{
u32 seed = RtcGetMinuteCount();
seed = (seed >> 16) ^ (seed & 0xFFFF);
SeedRng(seed);
}
This means there are thousands of different RNG streams from power-on depending on what value was pulled from the real-time clock, which keeps running when the game is off. This code was removed for Fire Red/Leaf Green as these were ports of older games with no RTC. When they sat down to hack together Emerald, it seems the team used the version of AgbMain
they had made for FRLG rather than RS. They forgot to restore the RNG initialization, and so now every legitimate copy of Emerald is stuck booting with an RNG value of 0x0000
. The PRET decompiliation will include SeedRngWithRtc()
at the correct place if BUGFIX
compile mode is enabled, which allows you to experience an alternate reality where randomness is slightly more actually random. This is a classic Game Freak bug: understandable how it happened, but a solid gameplay testing process should have caught it.
Note
RNG (random number generation) can only be "truly" random if it is reading from a physical random process such as radioactive decay. Algorithms that can run quickly on something like a Game Boy Advance are a very rough approximation, and require a kick-off random number, the "seed", so that they do not simply produce the same sequence each time the game is run. While a linearly increasing clock is not very random at all, the exact value at whatever time you happen to turn on the game might as well be for basic seeding purposes. The seed being stuck at zero after every reboot in Emerald means that the exact same series of player inputs will always* result in the same series of events such as wild encounters, critical hits, capture successes, and what stats are assigned to newly captured mons. Fortunately, normal players are divergent enough in their inputs that the broken RNG is not too wildly obvious. (Shiny hunters are Not Normal.)
* There is a wrinkle to this "always": hardware timing differences may cause more or fewer vblanks to occur on different platforms (particularly when accessing flash storage) and chew up the RNG stream at a different rate. For a given cartridge/console combo, or a given emulator, it should be extremely consistent. If you are trying to do something that relies on resetting the RNG to a known state, do not run the game off a flash cart!
There is another bug just in the functions called directly from AgbMain
: if the option is set to treat the "L" button as equivalent to the "A" button, ReadKeys()
won't correctly do key repeat for the "L" as an "A". Another bug that should have been found in testing, and this one is an original sin of Ruby/Sapphire.
Ignoring complications introduced by whether or not the link cable is active, the structure of the infinite game loop is straightforward:
- Check which buttons are pressed on the controller;
- Check whether it's time to do a soft reset;
- Call the currently registered callbacks, which contain the game logic;
- Update how long the game has been running;
- Do some bookkeeping with the music;
- Wait for the end of the VBlank interrupt which signals the end of the frame.
The last step keeps everything synchronized by ensuring everything happens once per 60th of a second. If too much work is done in a callback to meet the frame deadline, it can cause audio-visual issues.
The last thing we'll go over in main.c
is the VBlank interrupt handler:
static void VBlankIntr(void)
{
if (gWirelessCommType != 0)
RfuVSync();
else if (gLinkVSyncDisabled == FALSE)
LinkVSync();
gMain.vblankCounter1++;
if (gTrainerHillVBlankCounter && *gTrainerHillVBlankCounter < 0xFFFFFFFF)
(*gTrainerHillVBlankCounter)++;
if (gMain.vblankCallback)
gMain.vblankCallback();
gMain.vblankCounter2++;
CopyBufferedValuesToGpuRegs();
ProcessDma3Requests();
gPcmDmaCounter = gSoundInfo.pcmDmaCounter;
m4aSoundMain();
TryReceiveLinkBattleData();
if (!gMain.inBattle || !(gBattleTypeFlags & (BATTLE_TYPE_LINK | BATTLE_TYPE_FRONTIER | BATTLE_TYPE_RECORDED)))
Random();
UpdateWirelessStatusIndicatorSprite();
INTR_CHECK |= INTR_FLAG_VBLANK;
gMain.intrCheck |= INTR_FLAG_VBLANK;
}
The main steps are:
- Call the VBLank callback if one is registered;
- Push pending graphics changes to the video memory;
- Process this frame's audio;
- Advance the RNG;
- Set the flag that
AgbMain()
waits on each loop.
You've likely noticed that special carve-outs for link cable transmissions are scattered around randomly. VBlank also handles displaying an activity indicator for the wireless adapter that I, as someone who was definitely alive at the time, have no recollection about whatsoever but allegedly is a real thing that exists. Finally, they've shoehorned a timer that's only relevant to the Trainer Hill challenge into every single VBlank. This is evidence that it was a bit of a last-minute addition and there wasn't time to rewrite chunks of the engine to accomodate a more elegant solution. (Everything unique to Emerald was a last-minute addition.)
From here we will advance to the title screen sequence which still has a lot of behind-the-scenes bookkeeping to do while finally drawing something meaningful to the display.