Nintendo 64 console with EverDrive cartridge

Nintendo 64 Part 9: Fonts and Drawing Text

, Nintendo 64, Programming

Text is not just an important part of most games, but it’s also useful for debugging, since we can print debugging messages to the screen. In this post I’m going to convert a TTF font to a bitmap, pack the resulting glyphs into a texture usable by the Nintendo 64, and write code to draw text on-screen as a collection of sprites.

I’ve done this for games before, so I dug through source code from various old projects, copied it into this project, and modified it to meet my needs for Nintendo 64 development.

Rasterizing Outline Fonts

We could rasterize fonts on the Nintendo 64. It’s certainly powerful enough, and not only is the FreeType library very portable, but FreeType can be configured to build a very minimal version that only contains the features you need. We’re not going to do that, we’re going to convert the fonts to raster images during the build process. The first step is to just convert normal font files (like TrueType or OpenType files) into bitmaps.

Most of my tooling is in Go, but font rasterization itself is mostly just FreeType library calls, so I wrote a small font rasterizer program in C. The program basically just parses command-line options, reads a font file with FreeType, rasterizes the glyphs, and prints the image data to standard output.

Here’s what happens when you run the font rasterizer program:

$ bazel run :raster -- rasterize \
    -font=/Library/Fonts/Arial.ttf -size=12
char 32 3
char 33 4
char 34 5
char 65532 863
glyph 7 8 1 8 9 .notdef 273030303030273000000000...
glyph 0 0 0 0 0 .null -
glyph 0 0 0 0 3 nonmarkingreturn -
glyph 0 0 0 0 3 space -
glyph 2 9 1 9 3 exclam F858F858F050E040CF30BE20A...

It’s very ugly output, but it’s designed to be consumed by another program.

Packing Glyphs Into Textures

To cut down on the number of different textures we need to use, I’ll take these glyph bitmaps and pack them into a smaller number of textures. Nintendo 64 texture memory is very limited (only 4 KiB), so if you want to use a format with 4 bits per pixel (the smallest pixel format the system supports), you just get a single 128×64 pixel texture, or a texture with a similar size like 96×85. If I want to add a drop shadow, I’ll probably have to use an 8-bit pixel format, which cuts the available space in half.

I’ll make this work by doing three things:

  1. Use a rectangle packing algorithm to find an efficient way to pack many glyphs into a single texture.
  2. Omit glyphs from the font that I don’t need in my game.
  3. Use multiple textures as necessary.

The rectangle packing algorithm is something that I’ve done before in other projects, so I just ported my rectangle packing code to Go, write some simple tests, and modify it so the rectangles can be split across multiple bins.

The reference I used to write the algorithm is A thousand ways to pack the bin—a practical approach to two-dimensional rectangle bin packing by Jukka Jylänki in 2010 [CiteSeerX]. The algorithm I used is the MAXRECTS algorithm mentioned in the paper. It’s slower than the other algorithms, but since we’re not packing many rectangles, it doesn’t matter.

I like the Grenze Gotisch font, so I’m using it for this test. This command will create two images: one with the glyphs laid out on a grid, and one with the glyphs packed efficiently into a 96×85 texture:

$ bazel run :font -- \
    -font path/to/GrenzeGotisch-Medium.ttf \
    -size 20 \
    -out-grid $PWD/grid.png \
    -texture-size 96:85 \
    -charset 20,41-5a,61-7a,21,2c,2e,3a,2018,2019,201c,201d \
    -out-texture $PWD/texture.png \

Here are the results:

Glyphs from Grenze Gotisch font laid out in grid
Grid layout
Glyphs from Grenze Gotisch font packed into a small texture
Packed texture, 96×85

Creating a Font Format

My tool spits out a fairly simple custom font format with three sections in it:

// Header at the beginning of a font file.
struct font_header {
  uint16_t charmap_size;  // Number of font_char.
  uint16_t glyph_count;   // Number of font_glyph.
  uint16_t texture_count; // Number of font_texture.

// Character map entry.
struct font_char {
  uint16_t codepoint; // Unicode code point.
  uint16_t glyph;     // Glyph index.

// Information about a glyph.
struct font_glyph {
  uint8_t size[2];  // Size in pixels.
  int8_t offset[2]; // Sprite offset from drawing position.
  uint8_t pos[2];   // Sprite location in texture.
  uint8_t texindex; // Which texture.
  uint8_t advance;  // How far to move after drawing.

// Data for a single texture.
struct font_texture {
  uint16_t width;
  uint16_t height;
  uint8_t pixels[];

The font file with 20 pixel height ends up being under 5 KB in size, so we could easily include much larger fonts or more fonts.

Drawing Text

Drawing a single line of text like this is fairly easy, so there’s not much code in the engine itself. Basically, the code keeps track of the current (x,y) position, and each time it draws a character it moves a little bit forward.

In the future, I’d like to at least support hard and soft line breaks.

Prototype game screen with “Hello World! Release every zig!” written in a gothic font.
Words for the ages

Community Comments

Some comments from the Discord:

A: Looking through your articles I have one question

A: Why do you do this to yourself

A: Really cool stuff there, but if you ever need counseling, I know some people

Me: Isn’t everyone here doing this, on some level, because it’s difficult?

A: Touché 😛

It’s not like I love the Nintendo 64 so much that I’ll go through all these difficulties just to make a game. My nostalgia for the Nintendo 64 is pretty limited since I never owned a Nintendo 64 system, so I only got to play them at places like Blockbuster Video or watch my college roommates play Mario Kart 64.

No, the reason I’m doing this is because I like the challenge and I like the Nintendo 64 homebrew community.

B: Read your blog entries, [Dietrich]. Inspiring, but also reaffirms to me that I don't know anywhere near enough to do this without all the prior work others in the community have done.

I’m definitely not able to do this without a lot of help, too. We’re able to accomplish so much because there are so many people supporting us.

Here are some of the people that have helped me (definitely not a comprehensive list):