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 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 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) ;
const WORD memoryRegion = 0x8800 ;
const int sizeOfTileInMemory = 16 ;
const int offset = 128 ;
WORD tileDataAddress = memoryRegion + ((tileIdentifier+offset)*sizeOfTileInMemory) ;
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
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
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
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 ;
}
}
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 ;
}
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.
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
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 ;
}
}
}
}