Skip to content

Instantly share code, notes, and snippets.

@zeichensystem
Created May 22, 2021 06:35
Show Gist options
  • Save zeichensystem/0729edcddf8f24db14e5b1b4ef4c0c3f to your computer and use it in GitHub Desktop.
Save zeichensystem/0729edcddf8f24db14e5b1b4ef4c0c3f to your computer and use it in GitHub Desktop.
Game Boy Advance flicker-free mode switching
#include <tonc.h>
/*
Self-contained example demonstrating flicker-free switching between the page-flipping display modes
of the GBA (i.e. the bitmap modes 4 and 5).
The problem: If done carelessly, switching between the page-flipping display modes can lead to essentially
random persisting flickering/screen tearing.
The solution: If we intend to switch modes by writing to REG_DISPCNT and the front page (vid_mem_front) is
the write-page (which is always pointed to by vid_page), then it is necessary to indicate the back page
should be the displayed one (and *not* the front page) by setting the DCNT_PAGE bits.
(DCNT_PAGE is not named ideally in libtonc in my opinion, a better name would be DCNT_SHOW_BACK, for example).
If we just overwrite REG_DISPCNT when switching modes without considering this, we would implicitly assume
the front page to be the currently displayed page even when it is the write page, which is what leads
to the unpleasant artefacts if we are unlucky and this is not actually the case coincidentally.
cf. https://www.coranac.com/tonc/text/bitmaps.htm (last retrieved 2021-05-22)
http://ianfinlayson.net/class/cpsc305/notes/09-graphics (last retrieved 2021-05-22)
*/
static int frameCount;
static int textX, textY;
enum MODE_ID {M4_ID=0, M5_ID};
static int sceneNum = M4_ID;
void m4Init(void)
{
if (vid_page == vid_mem_front) { // If the front page (vid_mem_front) is the current write-page, we have to indicate that the back page is the displayed page by setting DCNT_PAGE.
REG_DISPCNT = DCNT_MODE4 | DCNT_BG2 | DCNT_PAGE;
} else { // The current write page is the back page, so we display the front page (which happens by default if we don't set DCNT_PAGE).
REG_DISPCNT = DCNT_MODE4 | DCNT_BG2;
}
pal_bg_mem[0] = CLR_TEAL;
pal_bg_mem[1] = CLR_WHITE;
pal_bg_mem[2] = CLR_FUCHSIA;
}
void m5Init(void)
{
if (vid_page == vid_mem_front) { // See above.
REG_DISPCNT = DCNT_MODE5 | DCNT_BG2 | DCNT_PAGE;
} else {
REG_DISPCNT = DCNT_MODE5 | DCNT_BG2;
}
}
void m4Scene(void)
{
m4_fill(0);
const int TAU = 0xffff;
textX = M4_WIDTH / 2 + (30 * lu_cos(frameCount * TAU / 1024) >> 12) - 30;
textY = M4_HEIGHT / 2 + (30 * lu_sin(frameCount * TAU / 1024) >> 12);
m4_puts(textX, textY, "mode 4", 1);
m4_frame(32, 32, 100, 80, 2);
}
void m5Scene(void)
{
m5_fill(CLR_CREAM);
const int TAU = 0xffff;
textX = M5_WIDTH / 2 + (30 * lu_cos(frameCount * TAU / 1024) >> 12) - 15;
textY = M5_HEIGHT / 2 + (30 * lu_sin(frameCount * TAU / 1024) >> 12);
m5_puts(textX, textY, "mode 5", CLR_BLUE);
m5_frame(32, 32, 100, 80, CLR_TEAL);
}
int main(void)
{
txt_init_std();
irq_init(NULL);
irq_add(II_VBLANK, NULL);
if (sceneNum == M4_ID) {
m4Init();
} else {
m5Init();
}
for (;;) {
key_poll();
if (key_hit(KEY_START)) { // Handle mode switching.
sceneNum = (sceneNum + 1) % 2;
switch (sceneNum) {
case M4_ID:
// We're switching from mode 5, clear the screen accordingly:
m5_fill(CLR_BLACK);
VBlankIntrWait();
vid_flip();
m5_fill(CLR_BLACK);
m4Init(); // Finally, switch modes.
break;
case M5_ID:
// We're switching from mode 4, clear the screen accordingly:
pal_bg_mem[0] = CLR_BLACK;
m4_fill(0);
VBlankIntrWait();
vid_flip();
m4_fill(0);
m5Init(); // Finally, switch modes.
break;
}
}
switch (sceneNum) { // Draw the selected scene.
case M4_ID:
m4Scene();
break;
case M5_ID:
m5Scene();
break;
}
// In case you want to vsync, simply put a VBlankIntrWait() before vid_flip().
vid_flip();
++frameCount;
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment