This page looks best with JavaScript enabled

GameBoy Dev04: Window

 ·  🎃 kr0m

It is recommended to read the previous articles to better understand the current one:


In this article, we will learn how to use the GameBoy Window, the problems that may arise, and how to solve them efficiently. This is a fairly extensive article with advanced technical content, which is why it is divided into three parts:


Introduction

As we already mentioned in the previous article, the GameBoy screen is composed of 3 layers:

  • Background: The game’s level background.
  • Sprites: Moving elements such as characters or enemies.
  • Window: Part where information such as score, lives, and other informative elements are displayed.

In this article, we will focus on the Window layer, which we will use as a HUD. It is quite limited, to begin with, it does not support transparency, its shape is fixed and rectangular, and we can only control the position of the upper-left pixel of it.

We could simulate the window using sprites, but it would decrease the number of sprites available for the rest of the screen since we are using some for the HUD.


First approach

The first step will be to load the fonts that GBDK brings, in this way, we will be able to write text in the Window. GBDK provides us with 5 different fonts , we will have to choose one of them.

#include <gbdk/font.h>            --> Include the source file
font_init();                      --> Initialize the fonts
font_t min_font;                  --> Create a type font_t variable
min_font = font_load(font_min);   --> Load the font
font_set(min_font);               --> Set the font
set_win_tiles(1,0,4,1,WindowMap); --> Set the window in coordenates (1,0), the length text is 4x1(kr0m) and the tiles map must be readed from the WindowMap array
move_win(40,40);                  --> Position in pixels where the upper left corner of the Window must be located.

First, we load the fonts without using them for anything, but we note the positions of the tiles corresponding to the letters we are going to use:

vi 04a.c

#include <gb/gb.h>
#include <gbdk/font.h>

void main(){
    font_init();
    font_t min_font;
    min_font = font_load(font_min);
    font_set(min_font);

    while(1){
        wait_vbl_done();
    }
}

We compile the code and load it into bgb, we can see the content of the fonts in the VRAM viewer:

~/GBDEV/gbdk/bin/lcc 04a.c -o 04a.gb

If we position ourselves on any letter, we can see the position in VRAM where it is located, for example:

k: 0x15
r: 0x1C
0: 0x01
m: 0x17

Knowing the positions, we will manually generate a WindowMap indicating said tiles:

vi WindowMap.c

/*

 WINDOW.C
 Hand crafted window map, tile position looked in bgb VRAM viewer

*/

unsigned char WindowMap[] =
{
  0x15,0x1C,0x01,0x17
};

/* End of WINDOW.C */

Now we can use the fonts:

vi 04b.c

#include <gb/gb.h>
#include <gbdk/font.h>
#include "WindowMap.c"

void main(){
    font_init();
    font_t min_font;
    min_font = font_load(font_min);
    font_set(min_font);
    
    set_win_tiles(1,0,4,1,WindowMap);
    move_win(40,40);

    SHOW_BKG;
    SHOW_WIN;
    DISPLAY_ON;

    while(1){
        wait_vbl_done();
    }
}

We compile the code and load it into bgb:

~/GBDEV/gbdk/bin/lcc 04a.c -o 04a.gb

If we decide to add a background as we did in the previous article, we will see that something strange happens:

vi 04c.c

#include <gb/gb.h>
#include <gbdk/font.h>
#include "BackgroundTiles.c"
#include "BackgroundMap.c"
#include "WindowMap.c"

void main(){
    font_init();
    font_t min_font;
    min_font = font_load(font_min);
    font_set(min_font);

    set_bkg_data(0,3,BackgroundTiles);
    set_bkg_tiles(0,0,32,18,BackgroundMap);
    
    set_win_tiles(0,0,5,1,WindowMap);
    move_win(40,40);

    SHOW_BKG;
    SHOW_WIN;
    DISPLAY_ON;

    while(1){
        scroll_bkg(1,0);
        wait_vbl_done();
    }
}

What’s happening? Why is it behaving like this? You may be wondering. First, the background does not appear because the Window is overlapping the Background, and secondly, the correct characters no longer appear in the Window because set_bkg_data loads BackgroundTiles in the first positions of the VRAM, overwriting what font_load had previously loaded.

First, we will address the problem of tile overlap since it is easier to solve of the two. To solve it, we will load the background tiles starting from position 0x25 -> 37, which is where the font tiles end:

And we will have to regenerate the BackgroundMap taking into account that “tile offset”.

vi 04d.c

#include <gb/gb.h>
#include <gbdk/font.h>
#include "BackgroundTiles.c"
#include "BackgroundMap.c"
#include "WindowMap.c"

void main(){
    font_init();
    font_t min_font;
    min_font = font_load(font_min);
    font_set(min_font);

    set_bkg_data(37,3,BackgroundTiles); // Hemos retocado el offset: 37
    set_bkg_tiles(0,0,32,18,BackgroundMap);
    
    set_win_tiles(0,0,5,1,WindowMap);
    move_win(40,40);

    SHOW_BKG;
    SHOW_WIN;
    DISPLAY_ON;

    while(1){
        scroll_bkg(1,0);
        wait_vbl_done();
    }
}

We regenerate the BackgroundMap, we just have to export it as usual but indicating an offset:

We compile the code and load it into bgb:

~/GBDEV/gbdk/bin/lcc 04a.c -o 04a.gb

We can see that tiles no longer overlap even though the Window still interferes with the Background:

NOTE: Fonts cannot be loaded in other positions of the VRAM, so we must always adapt the rest of the elements.


VBLANK/LCD STATUS Interrupts

To solve the problem of interference between the Window and Background layers, we will have to delve a little deeper into the internal workings of the GameBoy, specifically the interrupt system and the PPU (Pixel Processor Unit).

Some important points are:

  • VBLANK is the time it takes for the PPU to go from the last pixel of the LCD screen to the first to start displaying the next image. Every time it goes from the last pixel to the first, an interrupt is generated in the GameBoy. By controlling this interrupt, we can know if we are drawing the first line of the screen.
  • In the GameBoy, there is a register called LY that indicates the line of the screen that is being drawn at that moment.
  • The GameBoy allows configuring when an LCD-STAT interrupt will occur through a register called STAT.

The idea is to enable the Window while we draw the first 8 rows of the LCD, which correspond to the first row of tiles on the screen and make up our Window. Then, detect that the Window has been drawn and disable it, draw the rest of the screen, and when it reaches the end, detect it to enable the Window again and repeat the process. This way, we can display the Window and the Background without interfering with each other.

NOTE: Disabling the Window is not a problem because once the PPU has painted certain pixels, they remain in that state until the next pass.

To detect that we have painted the first 8 lines of pixels, we will use the LY register, which contains the value of the line that is being painted at that moment, and the LCD-STAT interrupt, whose behavior is configured through the STAT register:

Bit 6 - LYC=LY STAT Interrupt source         (1=Enable) (Read/Write)
Bit 5 - Mode 2 OAM STAT Interrupt source     (1=Enable) (Read/Write)
Bit 4 - Mode 1 VBlank STAT Interrupt source  (1=Enable) (Read/Write)
Bit 3 - Mode 0 HBlank STAT Interrupt source  (1=Enable) (Read/Write)
Bit 2 - LYC=LY Flag                          (0=Different, 1=Equal) (Read Only)
Bit 1-0 - Mode Flag                          (Mode 0-3, see below) (Read Only)
          0: HBlank
          1: VBlank
          2: Searching OAM
          3: Transferring Data to LCD Controller

We will set the register to 01000000 -> 0x40 since we only want to enable bit number 6. This way, when the LYC register equals LY, the LCD-STAT interruption will occur.

STAT_REG = 0x40;

NOTE: Actually, the register only allows us to configure bits 4, 5, and 6 since the rest are read-only.

In our case, we want the interruption to occur just when the eighth line of the screen has finished painting, which is when the Window’s tile row has finished painting. We assign the value of 0x08 to the LYC register.

LYC_REG = 0x08;

So far, we have two interruptions: one that occurs when we reach the eighth line of the screen and another when we reach the last line and return to the beginning. We must add our own code to the existing service routines. GBDK allows us to do this by:

add_LCD(FUNCTION_NAME); -> We add our function to the service routine associated with LCD-STAT.
add_VBL(FUNCTION_NAME); -> We add our function to the service routine associated with VBLANK.

Some considerations to keep in mind when working with interruptions are:

  • To add code to the ISRs, we must first disable the interruptions.
  • Our own ISRs should be as small as possible so that their execution is fast and the CPU has clock cycles to attend to the main code.

I think the explanation is quite clear, but I leave some recommended reading links below:
https://gbdk-2020.github.io/gbdk-2020/docs/api/docs_using_gbdk.html
https://gbdev.io/pandocs/Interrupt_Sources.html
https://gbdev.io/pandocs/STAT.html#ff41—stat-lcd-status-rw
https://en.wikipedia.org/wiki/VBlank

The final code would be as follows.

vi 04e.c

#include <gb/gb.h>
#include <gbdk/font.h>
#include "BackgroundTiles.c"
#include "BackgroundMap.c"
#include "WindowMap.c"

// Interrupt triggered when PPU reaches line 8 -> hide window
void interruptLCD()
{
    // When window is hidden it doesnt interfere with the other PPU operations
    HIDE_WIN;
}

// VBlank interrupt generated when PPU reaches last pixel, so enable window again
void interruptVBL() {
    SHOW_WIN;
}

void main(){
    font_init();
    font_t min_font;
    min_font = font_load(font_min);
    font_set(min_font);

    set_bkg_data(37, 3, BackgroundTiles);
    set_bkg_tiles(0, 0, 32, 18, BackgroundMap);

    // coordinates x,y
    // Tiles dimension 4 col X 1 row : kr0m
    // Tile mapping: WindowMap
    set_win_tiles(1,0,4,1,WindowMap);
    // 0,0 position is conflictive, we move 1 pixel to right
    move_win(1,0);

    // Enable LCD interrupt when LC = LYC
    // LC: PPU line counter
    // LYC: CPU register that we control to triger LCD-STAT interrupt
    
    // 01000000 -Hex->0x40 we have enabled bit 6 of LCD STATUS REGISTER, so LCD interrupt will be trigerred when LYC == LY
    STAT_REG = 0x40;
    // LCD interrupt will be triggered when PPU reaches 8 LCD line
    LYC_REG = 0x07;

    // Needed to be able to manipulate ISR routines
    disable_interrupts();

    // Add extra function to LCD ISR, in that way when LC == LYC, LCD interrupt will be triggered and interruptLCD code executed
    add_LCD(interruptLCD);
    // Add extra function to VBL ISR, in that way when PPU reaches last LCD pixel, interrupt will be triggered and interruptVBL code executed
    add_VBL(interruptVBL);

    // Reenable interrupts
    enable_interrupts();

    // Define which interrupts has to be looking for: VBL and LCD
    set_interrupts(VBL_IFLAG | LCD_IFLAG);  

    // Enable all screen layers
    SHOW_BKG;
    SHOW_WIN;
    DISPLAY_ON;

    while(1){
        scroll_bkg(1,0);
        wait_vbl_done();
    }
}

We compile the code and load it into bgb:

~/GBDEV/gbdk/bin/lcc 04e.c -o 04e.gb

If you liked the article, you can treat me to a RedBull here