Format reference · .fnt

Small fonts, packed tight

Read a letter one bit at a time. A four-byte header and a table of offsets are enough to turn a compact font into GUI text.

Pixels
1 bit · MSB first
Metrics
Fixed height · variable width
Character slots
Byte codes up to 255
Offsets
Absolute · little-endian u16

A letter is a stream of bits

FNT stores the bitmap fonts used for menu and in-game GUI text. Each glyph is a rectangle of on/off pixels with a shared height and its own width. Its on bits select pixels; the GUI supplies their color.

The pixels are packed continuously. A row can end halfway through a byte, and the next row starts at the very next bit. This small detail decides whether a letter decodes correctly.

Correct continuous decoding produces a five-column letter A; incorrectly starting each row at a new byte produces a broken letter.
The same five bitmap bytes, interpreted two ways. Left: the format’s continuous bitstream. Right: an intentionally incorrect row-aligned interpretation, with unavailable rows shown blank. Both come from the original example.fnt.

A GUI font gadget resolves a basename such as filename=SMLFONT; to fonts/SMLFONT.FNT. See the GUI research for selection and the owning runtime evidence for text drawing.

Find the glyph, then draw it

4-byte header
  height, unused byte, signed y_offset, first_code
u16 offset table: 256 − first_code entries
  table slot = character_code − first_code
absolute glyph-record offset (0 means absent)
  advance byte + ceil(advance × height / 8) bitmap bytes

All offsets are measured from the start of the file. Codes below first_code are absent; a zero table entry also means absent. A blank space can instead be a present glyph with a positive advance and an all-zero bitmap.

Byte layouts

Four-byte header

Scroll table horizontally to see all fields →

OffsetBytesTypeFieldMeaning
0x001u8heightShared glyph height in pixel rows.
0x011u8Unused byteNot read by traced font consumers; observed zero in surveyed retail fonts.
0x021i8y_offsetFirst glyph row is drawn at penY − y_offset.
0x031u8first_codeStarting code for the shortened offset table.

Character-offset table

Scroll table horizontally to see all fields →

OffsetBytesTypeFieldMeaning
0x042 × (256 − first_code)u16[]Glyph offsetsOne entry for each code from first_code through 255.
0x04 + 2 × (code − first_code)2u16Selected entryAbsolute glyph-record offset; zero means absent.

With first_code=0, the table ends at 0x204. With the example’s first_code=32, it ends at 0x1C4. Retail applies no upper bound to table indexing, so a font must provide all entries through code 255, even when most are zero.

A nonzero glyph start fits in 1–65,535, because its offset is a u16. That does not impose a 64 KiB file-size limit: a bitmap may extend past its start, and trailing bytes may be unreferenced.

Glyph record

Offsets below are relative to the selected glyph record.

Scroll table horizontally to see all fields →

OffsetBytesTypeFieldMeaning
+0x001u8advanceBoth bitmap width and exact horizontal pen advance.
+0x01ceil(advance × height / 8)Packed bitsBitmapTop-to-bottom, left-to-right, MSB first; rows are not byte-aligned.

There is no separate left bearing, kerning pair table, inter-character spacing field, or per-glyph height. Only the header’s signed vertical offset shifts the glyph relative to the caller’s pen position.

Walk through an A

The downloadable 468-byte font has header 07 00 02 20: height 7, unused byte 0, vertical offset +2, and first code 32. It contains a three-pixel space and two five-pixel letters. Every other table entry is zero.

CharacterCodeTable entry offsetStored offset bytesGlyph startsRecord size
Space320x004C4 010x1C44 bytes
A650x046C8 010x1C86 bytes
B660x048CE 010x1CE6 bytes

At 0x1C8, the record is 05 74 63 F8 C6 20. 05 is the advance. The remaining five bytes contain 35 visible bits and five trailing pad bits.

A five-by-seven letter A with cells grouped by packed byte; the bytes are 74, 63, F8, C6, and 20, with the last five bits padding.
Follow border colors across row boundaries. Byte 0 holds all five bits of row 0 and the first three bits of row 1. There is no new byte at each row.

For pixel (x, y), let bit = y × advance + x. Read byte bitmap[bit / 8] using integer division, then test 0x80 >> (bit % 8). For this A, pixel (0, 1) is bit 5: it lives in byte 0, mask 0x04, and is on.

Width, color, and pen position

Original glyphs render A space B A with advances 5, 3, 5, and 5; the first bitmap row sits two pixels above the dashed penY line.
The font determines the bit pattern and advance. The green color and dashed pen guide are presentation choices. With header offset +2, the first bitmap row sits two pixel rows above the supplied penY.
Stored in FNTSupplied by the caller / renderer
On/off glyph pixelsColor
Shared height and signed vertical offsetPen position
Exact glyph advanceAny extra layout or line spacing
Byte-code mappingString interpretation and code-page expectations
Blank space glyph, if authoredCaller-drawn text shadows

Retail font data’s high byte codes match Windows-1252, but the file is simply a mapping of byte values. The engine does not apply a code page. Do not turn that data convention into Unicode metadata that the file does not contain.

Loading and confidence

The retail loader reads the whole font and uses it in place without validation. Missing or empty startup fonts and side fonts are fatal with the path as the message; malformed offsets can send the rasterizer beyond the block [02 R-MALF-01 §9; 03 R-FONT-01 §1].

Corpus counts are observations, not format limits. An earlier layout survey covered 24 fonts; a later traced survey records 25 unique fonts. A per-archive census of totala1.hpi, rev31.gp3, ccdata.ccx, and btdata.ccx reports 39 copies, 25 distinct logical paths, and 21 distinct hashes. Those populations are different and should not be collapsed into one “font count.”

Sources and example files

Adapted from the owning FNT research, pinned to the sidebar revision. The complete source snapshot retains its bibliography, evidence IDs, and corpus observations. Runtime text rendering belongs to [03 §7.1, R-FONT-01], rather than to additional FNT fields.

The original example.fnt and its byte-derived SVG diagrams are reproducible with scripts/fnt-example.py; its --check mode checks the generated artifacts, including the hand-verified A bitstream.