2019-04-24

Proportional Fonts in GBA Tile Modes

I wanted to create a system for showing text with proportional fonts on the GBA. Typically on tile-based systems you’ll see fixed-width text with characters that align to tile boundaries. This is easy to do and efficient for the hardware. It’s exactly what I did in Professor Sinister. The new project I’m working on is going to have significantly more text and legibility is crucial, so I wanted to use proportional fonts instead of fixed-width.

First, some information about the GBA’s video system. The GBA’s screen resolution is 240x160px. When in tile mode, the screen is divided into a grid of 8x8px tiles. The hardware supports up to 4 layers of these tile grids overlaid on top of each other. In 4-bit color tile mode, in which each byte holds two pixels, ~19K of VRAM would be required per layer to store a full-screen bitmap in tile form. In addition to the tile bitmaps each layer requires a map to tell the video system how to lay out the tile bitmaps on screen. Each entry in the map is 16 bits wide. That’s another ~1K required per tile layer. Presenting a unique full-screen bitmap in all 4 tile layers simultaneously would require 75K of VRAM for the tile bitmaps and ~4.6K for the maps. The GBA has 64K of VRAM.

The upshot of this is simple: the GBA has nowhere enough VRAM. It can’t possibly show 4 full-screen bitmaps in tile mode. The best it can do is ~3.5 screens of tiles (2048) in 4bpp tile mode or 1.7 screens of tiles (1024) in 8bpp tile mode, but neither scenario leaves room for the tile maps.

In bitmap modes the video system is even more limited: two bitmaps (mode 4; 8bpp; 37.5K each) or one bitmap (mode 3; 16bpp; 75K). These modes eat into sprite memory and sacrifice sprites for bitmap data.

Keeping all of that in mind, how do you use a tile-based layout to show proportional font glyphs? Why, you ignore the benefits and constraints of the tile system and treat it like a bitmap, of course.

The font rendering system I used in Chuckie Egg et al has its roots in the rendering system I created for Woopsi, but with the warts removed. Give it a bitmap object to render into and the renderer will happily use the bitmap’s setPixel() method to draw text. The bitmap itself translates from the screen co-ordinates supplied by the renderer into co-ordinates that match its internal structure. That’s how I got graphics showing up on the 3DS with its weirdly-oriented framebuffer: I created a new bitmap class specifically for the 3DS.

Making tile mode on the GBA behave like a bitmap is a similar problem, but it requires some more setup. The first thing to do is to decide how many tiles to use. Using the full size of a tile layer isn’t advisable because, as we’ve seen, the number of tiles available is severely limited. I wanted the text to pop over the bottom of the screen like the conversation text in Pokemon. I figured I’d need 5 lines of 10px tall text. Add a couple of pixels spacing between each line and that gives me 60px; round it off with a couple of pixels padding above and below the text and I need 64px, which neatly fits into 8 vertical tiles. The screen is 30 tiles wide, so I need to reserve 240 tiles solely for the use of the text renderer. That’s a sizeable chunk of the 2048 tiles available in 4bpp mode, but at least we’re not using the 600 tiles we’d need for a full screen of text.

I’m keeping the first (zeroth) tile bitmap in VRAM empty, so that means I’m reserving tile bitmaps 1 through 241. At this point I can set up the tile map for the tile layer I’m using to display text: rows 12 to 19 need to contain tile bitmaps 1 to 241. Note that the layer is actually 32 tiles wide, not 30, but as two columns of tiles are offscreen and I’ll never scroll the layer I’m leaving those extra columns empty. Also note that the tile bitmaps must be sequential in VRAM or you’ll introduce an extra layer of complexity.

With that done I can feed VRAM+32 (ie the second tile bitmap) into my new bitmap class as its bitmap origin address and teach it how to translate screen co-ordinates to tile bitmap co-ordinates. This is how pixels are laid out in VRAM:

+-----------------+-----------------+  +-----------------+-----------------+
| 000 001 002 003 | 004 005 006 007 |  | 064 065 066 067 | 068 069 070 071 |
+-----------------+-----------------+  +-----------------+-----------------+
| 008 009 010 011 | 012 013 014 015 |  | 072 073 074 075 | 076 077 078 079 |
+-----------------+-----------------+  +-----------------+-----------------+
| 016 017 018 019 | 020 021 022 023 |  | 080 081 082 083 | 084 085 086 087 |
+-----------------+-----------------+  +-----------------+-----------------+
| 024 025 026 027 | 028 029 030 031 |  | 088 089 090 091 | 092 093 094 095 |
+-----------------+-----------------+  +-----------------+-----------------+
| 032 033 034 035 | 036 037 038 039 |  | 096 097 098 099 | 100 101 102 103 |
+-----------------+-----------------+  +-----------------+-----------------+
| 040 041 042 043 | 044 045 046 047 |  | 104 105 106 107 | 108 109 110 111 |
+-----------------+-----------------+  +-----------------+-----------------+
| 048 049 050 051 | 052 053 054 055 |  | 112 113 114 115 | 116 117 118 119 |
+-----------------+-----------------+  +-----------------+-----------------+
| 056 057 058 059 | 060 061 062 063 |  | 120 121 122 123 | 124 125 126 127 |
+-----------------+-----------------+  +-----------------+-----------------+

Each large box represents a tile bitmap. Each cell in the box represents a u16. The numbers represent the index of each 4-byte pixel.

To translate from screen co-ordinates to VRAM index, locate the tile that contains the point:

const int tileSize = 8;
const int pixelsPerTile = 8;
const int tilesPerRow = 30;

int tileX = point.x / tileSize;
int tileY = point.y / tileSize;

Figure out the index of that tile:

int tileOffset = (tileX * tileSize * tileSize / pixelsPerTile) + (tileY * tileSize * tileSize * tilesPerRow / pixelsPerTile);

Find the pixel within the tile:

int x = point.x % 8;
int y = point.y % 8;

Find the offset of the byte containing the pixel:

int offset = tileOffset + x + (y * tileSize);

That has far too many mods and divides. We can take advantage of the fact that almost every calculation is a power of 2 and use some bitwise magic to produce this faster but mostly-illegible one-liner:

#define offsetForPoint(x, y) (((x) & ~0x7) << 3) + ((((y) & ~0x7) << 3) * 30) + ((x) & 0x7) + (((y) & 0x7) << 3)

The define above, coupled with the VRAM address we supplied to the class when we created it and the knowledge that VRAM can only be addressed as u16s, we can create a setPixel() method that the font renderer can write to.

What’s particularly neat about reusing the existing rendering routines is that they can efficiently clip to a rectangle, allowing me to avoid the jerkiness of text rendering seen in something like Pokemon. That game renders text to the screen one character at a time, so the speed at which text appears varies with the width of the characters being rendered. With this rendering system I can smoothly slide the clipping rect across the screen and rely on the renderer to draw fractions of a character - or multiple characters if necessary - at a consistent pace.