Skip to content

Improving the WaitForVBlank function

Deokishisu edited this page Dec 22, 2023 · 7 revisions

The credits for the original tutorial belong to DizzyEggg.

In both Pokémon FireRed/LeafGreen and Pokémon Emerald, Game Freak modified the WaitForVBlank function present in Pokémon Ruby/Sapphire.

WaitForVBlank is used for what is called vsyncing. In short, VBlank is a short period of time in between each refresh of the screen, and it is used to keep the game processes in sync with the GBA's screen.

In FireRed/LeafGreen and Emerald, Gamefreak used a while loop that kept on checking for VBlank over and over again. In the past, this was thought to have been a mistake or perhaps an anti-pirating measure designed to slow down the emulators of the time. Recently, it has been discovered that this was done to prevent the Wireless Adapter from desyncing and disconnecting while linking, and that all games that feature Wireless Adapter functionality employ this slower version of the standard WaitForVBlank function in some way.

The correct way to vsync for games without Wireless Adapter functionality is to use a software interrupt/BIOS call. In short, we put the CPU in low power mode while we wait for the next VBlank, and on the next VBlank the CPU comes back to life. Unfortunately, by waiting for the next VBlank, we only respond to the VBlank interrupt, so updates from the Wireless Adapter can be missed. This means that, since each system will have been turned on at different times, VBlanks will not happen at the same time and desyncs will eventually occur which will disconnect in-progress link sessions.

There are two paths that can be taken to deal with this while still getting a more efficient WaitForVBlank function:

If you're okay with removing the Wireless Adapter (or linking altogether):

The easiest way to fix the WaitForVBlank function is by copying and pasting the function directly from Ruby and Sapphire, which means modifying the function in src/main.c like this:

static void WaitForVBlank(void)
{
    gMain.intrCheck &= ~INTR_FLAG_VBLANK;
-   while (!(gMain.intrCheck & INTR_FLAG_VBLANK))
-       ;
+   VBlankIntrWait();
}

One thing to note is that on agbcc's -O2, we still pay the price of a function call, as VBlankIntrWait fails to be inlined. In general, agbcc is very conservative on optimizations. Most other decent optimizing compilers (or if you make modern) will inline the function.

A solution is to inline it ourselves, for a negligible performance benefit:

static void WaitForVBlank(void)
{
    gMain.intrCheck &= ~INTR_FLAG_VBLANK;
-   while (!(gMain.intrCheck & INTR_FLAG_VBLANK))
-       ;
+   asm("swi 0x5");
}

Save, build a ROM, and so the game will no longer waste CPU power while it is waiting for the next VBlank. The easiest way to notice this is while running the game on a GBA emulator with a FastForward feature implemented, where implementing this fix will give you a 100-150% increase in speed.

If you want to preserve the Wireless Adapter functionality:

At the cost of one extra comparison in main.c, we can have the best of both worlds. Replace your WaitForVBlank function with this:

static void WaitForVBlank(void)
{
    gMain.intrCheck &= ~INTR_FLAG_VBLANK;
    if(!gWirelessCommType)
    {
        asm("swi 0x5");
        return;
    }

    while (!(gMain.intrCheck & INTR_FLAG_VBLANK))
        ;
}

The less-efficient WaitForVBlank will now only run while the Wireless Adapter is communicating. This has been written so that, during normal gameplay, performance losses over the above two versions are as minimized as possible. You won't lose performance due to an extra jump as the comparison follows right into setting the swi and then the function exits immediately afterwards. The only loss over the above two versions are the cycles needed to perform the comparison. The code jumps only while the Wireless Adapter is communicating, but the busy loop tanks performance in that scenario anyway so the cost is negligible.

Clone this wiki locally