31 Jul 2026
While working on the PICO-8 fantasy console, I wanted to create a custom small font. The process was interesting, so I decided to document it. Read more about PICO-8 here.
To create a font, all that is needed is to poke some holes in the right virtual memory locations of the fantasy console. The following line of Lua, the console's programming language, will suffice:
poke(0x5600,unpack(split"4,0,4,0,0,1,0,0,0,0,0,0,0,0,0,0,102,0,0,96,119,0,7,6,112,7,119,119,112,118,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,112,112,0,7,0,0,7,96,7,7,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,1,0,0,0,0,5,5,0,0,0,0,0,0,5,7,7,5,0,0,0,0,2,7,7,2,0,0,0,0,1,6,3,4,0,0,0,0,6,3,5,7,0,0,0,0,1,1,0,0,0,0,0,0,2,1,1,2,0,0,0,0,1,2,2,1,0,0,0,0,5,2,5,0,0,0,0,0,2,7,2,0,0,0,0,0,0,0,2,1,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,1,0,0,0,0,4,2,2,1,0,0,0,0,7,5,5,7,0,0,0,0,2,3,2,2,0,0,0,0,1,2,1,3,0,0,0,0,3,6,4,3,0,0,0,0,1,1,3,2,0,0,0,0,3,1,2,3,0,0,0,0,1,1,3,3,0,0,0,0,3,2,1,1,0,0,0,0,7,5,7,7,0,0,0,0,3,3,2,2,0,0,0,0,1,0,1,0,0,0,0,0,2,0,2,1,0,0,0,0,2,1,2,0,0,0,0,0,7,0,7,0,0,0,0,0,1,2,1,0,0,0,0,0,7,6,0,2,0,0,0,0,6,7,5,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,1,1,3,0,0,0,0,1,2,2,4,0,0,0,0,3,2,2,3,0,0,0,0,2,5,0,0,0,0,0,0,0,0,0,7,0,0,0,0,1,2,0,0,0,0,0,0,3,6,5,6,0,0,0,0,1,3,5,3,0,0,0,0,6,1,1,6,0,0,0,0,4,6,5,6,0,0,0,0,2,7,1,6,0,0,0,0,2,1,3,1,0,0,0,0,6,1,5,6,0,0,0,0,1,3,5,5,0,0,0,0,1,0,1,1,0,0,0,0,2,0,2,3,0,0,0,0,1,5,3,5,0,0,0,0,1,1,1,2,0,0,0,0,7,7,5,5,0,0,0,0,3,5,5,5,0,0,0,0,2,5,5,2,0,0,0,0,3,5,3,1,0,0,0,0,6,5,6,4,0,0,0,0,2,5,3,5,0,0,0,0,6,1,4,3,0,0,0,0,2,7,2,4,0,0,0,0,5,5,5,7,0,0,0,0,5,5,5,2,0,0,0,0,5,5,7,7,0,0,0,0,5,2,2,5,0,0,0,0,5,2,4,3,0,0,0,0,3,4,1,6,0,0,0,0,6,3,3,6,0,0,0,0,1,1,1,1,0,0,0,0,3,6,6,3,0,0,0,0,0,6,3,0,0,0,0,0"))
Understandably, this line looks a bit cramped with so much information. The poke function places the string of binary data at the memory location starting at address 0x5600. The organization of the memory layout of the custom font section is especially interesting.
The manual provides the following information about the memory layout:
A custom font can be defined at 0x5600, consisting of 8 bytes per
character * 256 characters = 2048 bytes. Each character is an 8x8
bitfield (1 bit/pixel), where starting from the top, each row is a
single byte starting with 0x1 on the left.
It further states that the first 128 bytes describe the font's attributes and that characters 0 to 15 are never drawn. To start visualizing this, let's convert part of the top string into a hex dump.
5600: 0000 0100 0000 0000 0000 0100 0000 0000
5604: 0000 0000 0000 0001 0000 0000 0000 0000
; -------- ^ settings of the font ^ ---------
5608: 0000 0000 0000 0000 0000 0000 0000 0000
560c: 0000 0000 0000 0000 0000 0000 0000 0000
5610: 0110 0110 0000 0000 0000 0000 0110 0000
; ...
; ------- ^ attributes of the font ^ --------
5680: 0000 0000 0000 0000 0000 0000 0000 0000
5788: 0000 0001 0000 0001 0000 0000 0000 0001
; -------------- ^ font data ^ --------------
Examining the font's attributes reveals that the top 128 bytes can be used to adjust the font settings. The first eight bytes of this group are used to specify the font's general width, height, offsets, and special flags. In this case, the font size was chosen to be 4x4 bytes. The flag (the next highlighted part) indicates that special width adjustments can be applied to each character in the font set, effectively making this a non-monospaced font.
Another highlighted value is located at memory position 0x5610. This reduces the width of an individual font entry to save space. The next highlighted value is the font entry itself. It is stored in four bytes and resembles an exclamation point. It is easier to show than explain how this is done, so next, a map is shown of the 8x8 (in this case, 4x4) font grid is shown.
0 1 2 3 4 5 6 7
0 X O O O O O O O
1 X O O O O O O O
2 O O O O O O O O
3 X O O O O O O O
4 O O O O O O O O
5 O O O O O O O O
6 O O O O O O O O
7 O O O O O O O O
By default, the full grid size is 8x8 bit. Each set bit creates a pixel of the font. The general settings at 0x5600 set the grid's width and height to 4x4 bits. The specific font setting at 0x5610 reduces the drawn width of the character by two. To understand how this works, another visualization is needed.
___33___|___32___
5610: 0 1 1 0 | 0 1 1 0
___35___|___34___
5611: 1 0 1 1 | 0 1 0 0
; - ^ attributes of the font ^ -
; (applied as little endian)
nibble | modification
=====================
0000 | width-0
0001 | width+1
0010 | width+2
0011 | width+3
0100 | width-4
0101 | width-3
0110 | width-2
0111 | width-1
; - ^ width modification table ^ -
As it is shown here, the settings inside the nibbles are applied in little endian order. Therefore the character in our font with ID 33 comes first (our exclamation mark). Three more examples are given for clarity. Character ID 32 resembles a space and also has a width of two. Characters 34 and 35 are the double quotation mark (") and the number sign (#). Since the first (or in little endian eigth) bit is set, the character will be drawn one pixel higher, which could be useful for Latin accents. It also has a width of four (general) plus three pixels, while the other has a width of zero, rendering it useless.
The unofficial wiki entry of the character set of PICO-8 is a nice additions to the kinda sparse official manual. To help generating fonts, the P8SCII custom font utility from AkiPot can be used. The font used in my first code block is inspired from TCartmill's Pixel Font - 3x4.
The finished font can then be used with the P8SCII string command, \014, in print statements, for example.