codeslinger.co.uk

Gameboy - Graphics Emulation.

Graphics Emulation:

The gameboy uses the tile and sprite method of storing and drawing graphics to the screen. The tiles are what form the background and are not interactive. Each tile is 8x8 pixels. The sprites are the interactive graphics on the display. An example is the game Mario. The character mario is a sprite. The graphic can move and it can collide with the other sprites. The bad guys are also sprites as they can fly around and attack mario. The tiles are the background which defines the level and its terrain.

As stated previously the resolution of the gameboy is 160x144 however this is just what can be displayed on the screen. The real resolution is 256x256 (32x32 tiles). The visual display can show any 160x144 pixels of the 256x256 background, this allows for scrolling the viewing area over the background.

Aswell as having a 256x256 background and a 160x144 viewing the display the gameboy has a window which appears above the background but behind the sprites (unless the attributes of the sprite specify otherwise, discussed later). The purpose of the window is to put a fixed panel over the background that does not scroll. For example some games have a panel on the screen which displays the characters health and collected items, (notably links awakening) and this panel does not scroll with the background when the character moves. This is the window.

This part of the site shows how to emulate everything I have just discussed. The examples I give below were designed to give the easiest understanding of how the tile and sprite system works but it is far from efficient. Luckily graphic emulation is the area where most speed optimization can be achieved with the least amount of effort (by using dirty rectangles and the likes) but this would not give a good demonstration.

The LCD Control Register:

In the LCD chapter I briefly touched upon the LCD Control register. This register contains a lot of data that we need to understand before we can emulate graphics. This is the breakdown of the 8-bit special register:

Taken from the pandocs:

Bit 7 - LCD Display Enable (0=Off, 1=On)
Bit 6 - Window Tile Map Display Select (0=9800-9BFF, 1=9C00-9FFF)
Bit 5 - Window Display Enable (0=Off, 1=On)
Bit 4 - BG & Window Tile Data Select (0=8800-97FF, 1=8000-8FFF)
Bit 3 - BG Tile Map Display Select (0=9800-9BFF, 1=9C00-9FFF)
Bit 2 - OBJ (Sprite) Size (0=8x8, 1=8x16)
Bit 1 - OBJ (Sprite) Display Enable (0=Off, 1=On)
Bit 0 - BG Display (for CGB see below) (0=Off, 1=On)

Bit 7: I have already discussed Bit7. Basically it says if the lcd is enabled, if not we dont draw anything. This is already handled in the UpdateGraphics function.

Bit 6: This is where to read to read the tile identity number to draw onto the window

Bit 5: If this is set to 0 then the window is not enabled so we dont draw it

Bit 4: You use the identity number for both the window and the background tiles that need to be draw to the screen here to get the data of the tile that needs to be displayed. The important thing to remember about this bit is that if it is set to 0 (i.e. read from address 0x8800) then the tile identity number we looked up is actually a signed byte not unsigned (explained fully later)

Bit 3: This is the same as Bit6 but for the background not the window

Bit 2: This is the size of the sprites that need to draw. Unlike tiles that are always 8x8 sprites can be 8x16

Bit 1: Same as Bit5 but for sprites

Bit 0: Same as Bit5 and 1 but for the background

You may remember that the UpdateGraphics function called the DrawScanLine function which I've yet to implement. However with the above lcd control register info I now can:

void Emulator::DrawScanLine( )
{
   BYTE control = ReadMemory(0xFF40) ;
   if (TestBit(control,0))
     RenderTiles( ) ;

   if (TestBit(control,1))
     RenderSprites( ) ;
}

Rendering the Tiles Part 1:

As previously stated the background is made up of 256x256 pixels (32x32 tiles) however as we only display 160x144 pixels there is no need to draw the rest. Before we can start drawing the background we need to know where to draw the background, i.e. which of 160x144 of the 256x256 background is going to be displayed?

ScrollY (0xFF42): The Y Position of the BACKGROUND where to start drawing the viewing area from
ScrollX (0xFF43): The X Position of the BACKGROUND to start drawing the viewing area from
WindowY (0xFF4A): The Y Position of the VIEWING AREA to start drawing the window from
WindowX (0xFF4B): The X Positions -7 of the VIEWING AREA to start drawing the window from

So now we know where to draw the viewing area in relation to the background and where to draw the window in relation to the viewing area. The minus 7 of the windowX pos is necessary. So if you wanted to start drawing the window in the upper left hand corner (coordinates 0,0) of the viewing area you'd set WindowY to 0 and WindowX to 7. Well when I say "when you want to draw the window" its actually the game decides not the emulator. On a side note if the window was drawn in the top left corner then the window would completely cover the background. If the window wanted to cover only the bottom half of the background the WindowY would bet set to 72 (144/2) and WindowX 7.

So now we know where to draw the background and the window we need to determine what we need to draw. The gameboy has two regions of memoory for the background layout which is shared by the window. The memory regions are 0x9800-0x9BFF and 0x9C00-9FFF. We need to check bit 3 of the lcd contol register to see which region we are using for the background and bit 6 for the window. Each byte in the memory region is a tile identification number of what needs to be drawn. This identification number is used to lookup the tile data in video ram so we know how to draw it.

Looking at bit 4 of the LCD control register we can see that there are two places where the tile date is located 0x8000-0x8FFF and 0x88000x97FF. Each of these memory regions gives us 4096 (0x1000) bytes of data to store all the tiles. Each tile is stored in memory as 16 bytes. Remember that a tile is 8x8 pixels and that in memory each line of the tile requires two bytes to represent, hence the 16 bytes per tile. So now we know where the tile data is stored and where the background layout is stored. The background layout gives the tile identification number to look up the tile in the tile data area. However this is the tricky part, if the tile data memory area we are using is 0x8000-0x8FFF then the tile identifier read from the background layout regions is an UNSIGNED BYTE meaning the tile identifier will range from 0 - 255. However if we are using tile data area 0x8800-0x97FF then the tile identifier read from the background layout is a SIGNED BYTE meaning the tile identifier will range from -127 to 127. The is the algroithm to locate the tile in memory region 0x8000-0x8FFF

const WORD memoryRegion = 0x8000 ;
const int sizeOfTileInMemory = 16 ;

WORD tileDataAddress = memoryRegion + (tileIdentifier*sizeOfTileInMemory) ;

So if the background layout gave us the unsigned tile identifier as 0 then the tile data would be between 0x8000-0x800F. However if we needed to lookup tile identifier 37 then it would be in memory region 0x8250-0x825F. This is the algorithm for calculating where the tile data is in memory region 0x8800-0x97FF

const WORD memoryRegion = 0x8800 ;
const int sizeOfTileInMemory = 16 ;
const int offset = 128 ;

WORD tileDataAddress = memoryRegion + ((tileIdentifier+offset)*sizeOfTileInMemory) ;

So if the tile identier was 0 then the tile would be in memory region 0x9000-0x900F.

How to draw a tile/sprite from memory:

This area follows on from the above tile data rendering but it is also the same for sprites as you will see later on.

So what do we now know about the tile data? First is that the background layout region identifies each tile in the current background that needs to be drawn. The tile identity number obtained from the background layout is used to lookup the tile data in the tile data region. We know that each tile is 8x8 pixels and that each horizontal line in the 8x8 takes up two bytes of memory meaning that each tile in memory needs 16 bytes of data.

So if two bytes of data form one line of the tile then we need to combine these two bytes to form a break down of each pixel in the 8 pixel line. So if byte 1 looked like so: 00110101 and data 2 looked like this 10101110 then we can combine the two together to get the following colour information:

pixel# = 1 2 3 4 5 6 7 8
data 2 = 1 0 1 0 1 1 1 0
data 1 = 0 0 1 1 0 1 0 1

Pixel 1 colour id: 10
Pixel 2 colour id: 00
Pixel 3 colour id: 11
Pixel 4 colour id: 01
Pixel 5 colour id: 10
Pixel 6 colour id: 11
Pixel 7 colour id: 10
Pixel 8 colour id: 01

As you can see there are only 4 possible colour id's (00,01,10,11) which map to the 4 gameboy colours (white, light grey, dark grey, black). However we still need to determine how to map the pixel colours id's to the correct colours. This is what palettes are used for. Palettes are not fixed, the programmer can change the mapping. This means you can change the colour of tiles and sprites without changing the tile data because in one state colour id 00 might represent dark grey but it might change to represent white. This is how they do cool special effects like when Mario becomes invincible they invert his colours every half a second to give him that flashing effect.

The background tiles only have the one monochrome colour palette stored in memory address 0xFF47. However sprites can have two palettes (discussed later). They work exactly the same as the background palettes except that colour white is actually transparent. The sprite palettes are located 0xFF48 and 0xFF49.

Every two bits in the palette data byte represent a colour. Bits 7-6 maps to colour id 11, bits 5-4 map to colour id 10, bits 3-2 map to colour id 01 and bits 1-0 map to colour id 00. Each two bits will give the colour to use like so:

00: White
01: Light Grey
10: Dark Grey
11: Black

So now we have our pixel colour ids and a way to map these id's to colours. If we take the above example of the pixel colour id's and assume that the following is the palette data byte: 11001001 we can now work out each colour of the pixels.

Pixel 1 colour id: 10: Means look at bits of palette data 5-4 gives colour white
Pixel 2 colour id: 00: Means look at bits of palette data 1-0 gives colour light grey
Pixel 3 colour id: 11: Means look at bits of palette data 7-6 gives colour black
Pixel 4 colour id: 01: Means look at bits of palette data 3-2 gives colour dark grey
Pixel 5 colour id: 10: Means look at bits of palette data 5-4 gives colour white
Pixel 6 colour id: 11: Means look at bits of palette data 7-6 gives colour black
Pixel 7 colour id: 10: Means look at bits of palette data 5-4 gives colour white
Pixel 8 colour id: 01: Means look at bits of palette data 3-2 gives colour dark grey

So now we know what the colour ares for the tile data line. However if the palette changed from 11001001 to a different value then the colours of the tile would all completely change without the tile data itself changing.

Rendering the Tiles Part 2:

So now we have all the information needed to render the background and the window. So taking all this information we can implement it:

void Emulator::RenderTiles( )
{
   WORD tileData = 0 ;
   WORD backgroundMemory =0 ;
   bool unsig = true ;

   // where to draw the visual area and the window
   BYTE scrollY = ReadMemory(0xFF42) ;
   BYTE scrollX = ReadMemory(0xFF43) ;
   BYTE windowY = ReadMemory(0xFF4A) ;
   BYTE windowX = ReadMemory(0xFF4B) - 7;

   bool usingWindow = false ;

   // is the window enabled?
   if (TestBit(lcdControl,5))
   {
     // is the current scanline we're drawing
     // within the windows Y pos?,
     if (windowY <= ReadMemory(0xFF44))
       usingWindow = true ;
   }

   // which tile data are we using?
   if (TestBit(lcdControl,4))
   {
     tileData = 0x8000 ;
   }
   else
   {
     // IMPORTANT: This memory region uses signed
     // bytes as tile identifiers
     tileData = 0x8800 ;
     unsig= false ;
   }

   // which background mem?
   if (false == usingWindow)
   {
     if (TestBit(lcdControl,3))
       backgroundMemory = 0x9C00 ;
     else
       backgroundMemory = 0x9800 ;
   }
   else
   {
     // which window memory?
     if (TestBit(lcdControl,6))
       backgroundMemory = 0x9C00 ;
     else
       backgroundMemory = 0x9800 ;
   }

   BYTE yPos = 0 ;

   // yPos is used to calculate which of 32 vertical tiles the
   // current scanline is drawing
   if (!usingWindow)
     yPos = scrollY + ReadMemory(0xFF44) ;
   else
     yPos = ReadMemory(0xFF44) - windowY;

   // which of the 8 vertical pixels of the current
   // tile is the scanline on?
   WORD tileRow = (((BYTE)(yPos/8))*32) ;

   // time to start drawing the 160 horizontal pixels
   // for this scanline
   for (int pixel = 0 ; pixel < 160; pixel++)
   {
     BYTE xPos = pixel+scrollX ;

     // translate the current x pos to window space if necessary
     if (usingWindow)
     {
       if (pixel >= windowX)
         {
           xPos = pixel - windowX ;
         }
     }

     // which of the 32 horizontal tiles does this xPos fall within?
     WORD tileCol = (xPos/8) ;
     SIGNED_WORD tileNum ;

     // get the tile identity number. Remember it can be signed
     // or unsigned
     WORD tileAddrss = backgroundMemory+tileRow+tileCol;
     if(unsig)
       tileNum =(BYTE)ReadMemory(tileAddrss);
     else
       tileNum =(SIGNED_BYTE)ReadMemory(tileAddrss );

     // deduce where this tile identifier is in memory. Remember i
     // shown this algorithm earlier
     WORD tileLocation = tileData ;

     if (unsig)
       tileLocation += (tileNum * 16) ;
     else
       tileLocation += ((tileNum+128) *16) ;

     // find the correct vertical line we're on of the
     // tile to get the tile data
     //from in memory
     BYTE line = yPos % 8 ;
     line *= 2; // each vertical line takes up two bytes of memory
     BYTE data1 = ReadMemory(tileLocation + line) ;
     BYTE data2 = ReadMemory(tileLocation + line + 1) ;

     // pixel 0 in the tile is it 7 of data 1 and data2.
     // Pixel 1 is bit 6 etc..
     int colourBit = xPos % 8 ;
     colourBit -= 7 ;
     colourBit *= -1 ;

     // combine data 2 and data 1 to get the colour id for this pixel
     // in the tile
     int colourNum = BitGetVal(data2,colourBit) ;
     colourNum <<= 1;
     colourNum |= BitGetVal(data1,colourBit) ;

     // now we have the colour id get the actual
     // colour from palette 0xFF47
     COLOUR col = GetColour(colourNum, 0xFF47) ;
     int red = 0;
     int green = 0;
     int blue = 0;

     // setup the RGB values
     switch(col)
     {
       case WHITE: red = 255; green = 255 ; blue = 255; break ;
       case LIGHT_GRAY:red = 0xCC; green = 0xCC ; blue = 0xCC; break ;
       case DARK_GRAY: red = 0x77; green = 0x77 ; blue = 0x77; break ;
     }

     int finaly = ReadMemory(0xFF44) ;

     // safety check to make sure what im about
     // to set is int the 160x144 bounds
     if ((finaly<0)||(finaly>143)||(pixel<0)||(pixel>159))
     {
       continue ;
     }

     m_ScreenData[pixel][finaly][0] = red ;
     m_ScreenData[pixel][finaly][1] = green ;
     m_ScreenData[pixel][finaly][2] = blue ;
   }
}

I know at first that can seem daunting but as long as you understand the logic to drawing the tiles just keep looking at the above code and it'll make sense. I've yet to implement the GetColour function which is used in the above code. This function takes a colour ID then uses the monochrome palette to deduce what colour that colour ID relates to. Remember that this is a dynamic palette so the colour id's will map to different colours during different parts of the game.

void COLOUR Emulator::GetColour(BYTE colourNum, WORD address) const
{
   COLOUR res = WHITE ;
   BYTE palette = ReadMemory(address) ;
   int hi = 0 ;
   int lo = 0 ;

   // which bits of the colour palette does the colour id map to?
   switch (colourNum)
   {
     case 0: hi = 1 ; lo = 0 ;break ;
     case 1: hi = 3 ; lo = 2 ;break ;
     case 2: hi = 5 ; lo = 4 ;break ;
     case 3: hi = 7 ; lo = 6 ;break ;
   }

   // use the palette to get the colour
   int colour = 0;
   colour = BitGetVal(palette, hi) << 1;
   colour |= BitGetVal(palette, lo) ;

   // convert the game colour to emulator colour
   switch (colour)
   {
     case 0: res = WHITE ;break ;
     case 1: res = LIGHT_GRAY ;break ;
     case 2: res = DARK_GRAY ;break ;
     case 3: res = BLACK ;break ;
   }

   return res ;
}

Now you should have everything you need to render the tiles

Rendering the Sprites:

Rendering the sprites is a bit more difficult then the tiles but luckily the sprite data is located in memory address 0x8000-0x8FFF which means the sprite identifiers are all unsigned values which makes finding them easier. There are 40 tiles located in memory region 0x8000-0x8FFF and we need to scan through them all and check their attributes to find where they need to be rendered. The sprite attributes are found in the sprite attribute table (DUH!) located in memory region 0xFE00-0xFE9F. In this memory region each sprite has 4 bytes of attributes associtated to it, these are:

0: Sprite Y Position: Position of the sprite on the Y axis of the viewing display minus 16
1: Sprite X Position: Position of the sprite on the X axis of the viewing display minus 8
2: Pattern number: This is the sprite identifier used for looking up the sprite data in memory region 0x8000-0x8FFF
3: Attributes: These are the attributes of the sprite, discussed later.

A sprite can either be 8x8 pixels or 8x16 pixels, this can be determined by the sprites attributes. This is a break down of the sprites attributes:

Bit7: Sprite to Background Priority
Bit6: Y flip
Bit5: X flip
Bit4: Palette number
Bit3: Not used in standard gameboy
Bit2-0: Not used in standard gameboy

Sprite to Background Priority: If this flag is set to 0 then sprite is always rendered above the background and the window. However if it is set to 1 then the sprite is hidden behind the background and window unless the colour of the background or window is white, then it is still rendered on top.
Y flip: If this bit is set then the sprite is mirrored vertically. This will be used in the game to turn sprites upside down.
X flip: If this bit is set then the sprite is mirrored horizontally. This will be used in the game to change the direction of the characters etc
Palette Number: Sprites can either get their monochrome palettes from 0xFF48 or 0xFF49. If this bit is 0 then it gets it palette from 0xFF48 otherwise 0xFF49

I find the best way to handle the X and Y flipping is to read the sprite data in backwards as this will give the flip effect.

We now have enough information to render the sprites. This is done almost identically to the rendering of the tiles, except that instead of looping through a layout region in memory to get the next identifier of the tile to draw, we have to loop through all 40 sprites and detect which ones are visible and are intercepting with the current scanline.

void Emulator::RenderSprites( )
{
   bool use8x16 = false ;
   if (TestBit(lcdControl,2))
     use8x16 = true ;

   for (int sprite = 0 ; sprite < 40; sprite++)
   {
     // sprite occupies 4 bytes in the sprite attributes table
     BYTE index = sprite*4 ;
     BYTE yPos = ReadMemory(0xFE00+index) - 16;
     BYTE xPos = ReadMemory(0xFE00+index+1)-8;
     BYTE tileLocation = ReadMemory(0xFE00+index+2) ;
     BYTE attributes = ReadMemory(0xFE00+index+3) ;

     bool yFlip = TestBit(attributes,6) ;
     bool xFlip = TestBit(attributes,5) ;

     int scanline = ReadMemory(0xFF44);

     int ysize = 8;
     if (use8x16)
       ysize = 16;

     // does this sprite intercept with the scanline?
     if ((scanline >= yPos) && (scanline < (yPos+ysize)))
     {
       int line = scanline - yPos ;

       // read the sprite in backwards in the y axis
       if (yFlip)
       {
         line -= ysize ;
         line *= -1 ;
       }

       line *= 2; // same as for tiles
       WORD dataAddress = (0x8000 + (tileLocation * 16)) + line ;
       BYTE data1 = ReadMemory( dataAddress ) ;
       BYTE data2 = ReadMemory( dataAddress +1 ) ;

       // its easier to read in from right to left as pixel 0 is
       // bit 7 in the colour data, pixel 1 is bit 6 etc...
       for (int tilePixel = 7; tilePixel >= 0; tilePixel--)
       {
         int colourbit = tilePixel ;
         // read the sprite in backwards for the x axis
         if (xFlip)
         {
           colourbit -= 7 ;
           colourbit *= -1 ;
         }

         // the rest is the same as for tiles
         int colourNum = BitGetVal(data2,colourbit) ;
         colourNum <<= 1;
         colourNum |= BitGetVal(data1,colourbit) ;

         WORD colourAddress = TestBit(attributes,4)?0xFF49:0xFF48 ;
         COLOUR col=GetColour(colourNum, colourAddress ) ;

         // white is transparent for sprites.
         if (col == WHITE)
           continue ;

         int red = 0;
         int green = 0;
         int blue = 0;

         switch(col)
         {
           case WHITE: red =255;green=255;blue=255;break ;
           case LIGHT_GRAY:red =0xCC;green=0xCC ;blue=0xCC;break ;
           case DARK_GRAY:red=0x77;green=0x77;blue=0x77;break ;
         }

         int xPix = 0 - tilePixel ;
         xPix += 7 ;

         int pixel = xPos+xPix ;

         // sanity check
         if ((scanline<0)||(scanline>143)||(pixel<0)||(pixel>159))
         {
           continue ;
         }

         m_ScreenData[pixel][scanline][0] = red ;
         m_ScreenData[pixel][scanline][1] = green ;
         m_ScreenData[pixel][scanline][2] = blue ;
       }
     }
   }
}

Thats everything you need to get the graphics emulating correctly. Now all you have to do is bit-blit m_ScreenData to the display. I personally use glDrawPixels but you can use what you want. Head over here to the next chapter on Joypad