The Game Boy (1989) has a monochrome LCD screen which can display four shades of green. The basic "unit" of Game Boy graphics is the eight pixel by eight pixel tile. With four levels of colour, we need to store two bits per pixel, or 16 bytes per tile.
Tile graphics are stored in Video RAM as two interleaved bit planes ( Figure 1). The first two bytes of a tile combine to specify the top row of eight pixels, the next two bytes combine to specify the second row of eight pixels, and so on.
Figure 1: Tile graphics are stored as two interleaved bit planes. a) The colour of a pixel is determined by reading one bit from each bit plane. b) 16 bytes define a tile. c) Each bit plane can be visualised as its own one-bit picture.
Toggle the bits in section b) to change the resulting tile.
A useful consequence of this format is that we can easily create palette-shifted copies of tiles: tiles that have the same pattern, but with some or all of the colours swapped around.
It's as simple as this: we read two bytes (one row of eight pixels) from the source tile data, then we write two bytes (one row of eight pixels) to the destination tile data. The two bytes that we write are bitwise combinations of the two bytes that we read. Different bitwise operations result in different mappings between the source and destination palettes.
As a concrete example, the function in Listing 1 creates a copy of the source data where light grey and dark grey are exchanged. As the tiles are copied from source_ptr to destination_ptr, the order of each pair of bytes is reversed, which has the effect of swapping the bit planes. Black (11) pixels and white (00) pixels are unchanged, but light grey (10) pixels become dark grey (01), and dark grey pixels (01) become light grey (10).
#include<stdint.h>
void swap_greys(const uint8_t *source_ptr,
uint8_t *destination_ptr,
uint8_t tiles_count)
{
uint16_t row_count = 8 * tiles_count; // 8 rows per tile
while (row_count-- > 0) {
uint8_t byte0 = *source_ptr++; // read plane 0
uint8_t byte1 = *source_ptr++; // read plane 1
// write planes in reverse order:
*destination_ptr++ = byte1;
*destination_ptr++ = byte0;
}
}
Listing 1: A function to copy Game Boy tile data, while swapping light grey and dark grey pixels. source_ptr points to graphics data for one or more tiles, destination_ptr points to where the tiles should be copied, and tiles_count is the number of eight pixel by eight pixel tiles to copy.
All possible mappings between source and destination palettes can be achieved using just bitwise operations. These mappings are demonstrated in Figure 2.
| x0 | x1 | y0 |
|---|---|---|
| 0 | 0 | |
| 1 | 0 | |
| 0 | 1 | |
| 1 | 1 |
| x0 | x1 | y1 |
|---|---|---|
| 0 | 0 | |
| 1 | 0 | |
| 0 | 1 | |
| 1 | 1 |
Figure 2: Palette-shifted tiles are generated by taking bitwise combinations of the input bit planes. a) Each input colour can be mapped to any output colour. b) Truth tables showing how the two output bit planes (y0 and y1) are related to the two input bit planes (x0 and x1) by bitwise logical operations. c) C code to apply the transformation while copying tiles.
Click the boxes in section a) to change the mapping.
Monochrome Game Boys have a background palette register, which allows us to globally remap the colours of the background. In other words, we can choose that all 00 "white" pixels actually appear as black or dark grey or light grey on screen. This is very useful, but completely unrelated to editing tile data.
What the recipes in Figure 2 achieve is to create new tile data with different bit patterns. A 00 pixel permanently becomes a 01 or 10 or 11 pixel. The two techniques shouldn't be confused.