Unit: VGAFont
A sprite sheet-based variable-width font renderer for high-quality text display in VGA Mode 13h.
Table of Contents
- Overview
- Comparison to VGAPRINT
- Font Format
- Data Structures
- API Functions
- XML Schema
- Usage Examples
- Font Creation Workflow
- Performance Considerations
- Implementation Details
- Error Handling
Overview
VGAFONT.PAS provides a professional variable-width font rendering system for DOS games. Fonts are defined as sprite sheets (PCX images) with XML metadata describing character positions and widths.
Key Features
- Variable-width characters - Proportional fonts for better readability
- Sprite sheet-based - Use existing PCX image format
- XML metadata - Character positions and widths defined externally
- Efficient rendering - Uses
PutImageRectfor fast character blitting - Flexible character set - Support ASCII 0-127
- Undefined character handling - Skip characters not in font (width=0)
Use Cases
- Game dialogue - Variable-width text looks more professional
- Menu systems - Better visual appearance than monospace
- HUD elements - Score displays, labels, etc.
- Cutscenes - Subtitle text rendering
- Credits screens - Large decorative fonts
Comparison to VGAPRINT
| Feature | VGAPRINT.PAS | VGAFONT.PAS |
|---|---|---|
| Font type | Fixed-width (8×8) | Variable-width |
| Font source | Embedded in code | External PCX + XML |
| Character sizes | All 8×8 | Custom per character |
| Visual quality | Monospace (retro) | Proportional (modern) |
| Setup | None (built-in) | Load font file |
| Memory usage | ~1KB (embedded) | ~Depends on font size |
| Performance | Fast (direct pixel write) | Fast (PutImageRect) |
| Use case | Debug text, FPS counters | Game UI, dialogue |
When to use each:
- VGAPRINT: Quick debug output, FPS counters, simple overlays
- VGAFONT: Professional game UI, dialogue, menus, titles
Font Format
Sprite Sheet Layout
Fonts are stored as PCX images with characters arranged in a sprite sheet:
Example: 32-pixel tall font ┌──────────────────────────────────────┐ │ A B C D E F G H I J K ... │ Row 1 (letters) ├──────────────────────────────────────┤ │ 0 1 2 3 4 5 6 7 8 9 ! ... │ Row 2 (numbers/symbols) ├──────────────────────────────────────┤ │ ... │ Additional rows as needed └──────────────────────────────────────┘ Each character: - Fixed height (e.g., 32 pixels) - Fixed right padding (e.g., 1 pixel) - Variable width (e.g., 'i' = 8px, 'W' = 24px) - Position defined in XML
Dimensions:
- Height: Fixed for all characters (defined in XML)
- Padding: Fixed for all characters (defined in XML)
- Width: Variable per character (defined in XML)
- Sheet size: Depends on character count and sizes
- Typical: 512×64 pixels for full ASCII set
XML Metadata
Character positions and widths are defined in an XML file:
<?xml version="1.0" encoding="UTF-8"?> <font height="32" padding="1" image="FONT.PCX" replace-color="15"> <!-- Uppercase letters --> <char code="65" x="0" y="0" width="18" /> <!-- A --> <char code="66" x="18" y="0" width="16" /> <!-- B --> <char code="67" x="34" y="0" width="17" /> <!-- C --> <!-- Lowercase letters --> <char code="97" x="0" y="32" width="15" /> <!-- a --> <char code="98" x="15" y="32" width="15" /> <!-- b --> <!-- Numbers --> <char code="48" x="200" y="0" width="14" /> <!-- 0 --> <char code="49" x="214" y="0" width="10" /> <!-- 1 --> <!-- Special characters --> <char code="32" x="0" y="0" width="8" /> <!-- Space (no glyph) --> <char code="33" x="300" y="0" width="8" /> <!-- ! --> <!-- Characters not defined have width=0 and are skipped --> </font>
Font Attributes:
height(required): Font height in pixels (applies to all characters)padding(optional): Horizontal spacing between characters (default: 0)image(required): Path to PCX sprite sheet file (relative to XML file)replace-color(optional): Color index to replace for PrintFontTextColored (default: 0 = disabled)
Character Attributes:
code: ASCII character code (0-127)x,y: Position in sprite sheet (pixels)width: Character width in pixels
Data Structures
TFont Type
unit VGAFont;
interface
uses VGA, GenTypes;
const
MaxChars = 128; { ASCII 0-127 }
type
TCharInfo = record
X: Integer; { X position in sprite sheet }
Y: Integer; { Y position in sprite sheet }
Width: Byte; { Character width in pixels }
Defined: Boolean; { True if character exists in font }
end;
TFont = record
Image: TImage; { Font sprite sheet }
Height: Byte; { Height of all characters }
Padding: Byte; { Right padding of all characters }
ReplaceColor: Byte; { Color to replace (0 = no replacement) }
Chars: array[0..MaxChars-1] of TCharInfo; { Character metadata }
Loaded: Boolean; { True if font successfully loaded }
end;
PFont = ^TFont;
Memory layout:
TFont structure: - Image: TImage (Width, Height, Data pointer) - Height: 1 byte - Chars: 128 × TCharInfo (128 × 7 bytes = 896 bytes) - Loaded: 1 byte Total: ~900 bytes + image data
API Functions
LoadFont
function LoadFont(const XMLFile: string; var Font: TFont): Boolean;
Loads a font from PCX image and XML metadata files.
Parameters:
XMLFile- Path to XML metadata (e.g., 'FONTS\MAIN.XML'). The XML file should contain animageattribute pointing to the PCX sprite sheet.Font- TFont structure to populate
Returns:
Trueif font loaded successfullyFalseon error (useGetLoadFontErrorfor details)
Example:
var
GameFont: TFont;
begin
if not LoadFont('FONTS\MAIN.XML', GameFont) then
begin
WriteLn('Error loading font: ', GetLoadFontError);
Halt(1);
end;
{ Font ready to use }
end;
Loading process:
- Load PCX image into
Font.Image - Parse XML file using MINIXML.PAS
- Extract
heightattribute from<font>element - Parse each
<char>element:- Read
code,x,y,widthattributes - Store in
Font.Chars[code] - Set
Defined := True
- Read
- Initialize undefined characters:
- Set
Width := 0 - Set
Defined := False
- Set
- Set
Font.Loaded := True
Error conditions:
- PCX file not found or invalid
- XML file not found or invalid
- XML parsing errors (malformed XML)
- Missing required attributes
- Invalid attribute values (negative, out of range)
GetLoadFontError
function GetLoadFontError: string;
Returns the last error message from LoadFont.
Returns: Error description string
Example:
if not LoadFont('FONT.XML', Font) then
begin
WriteLn('Font loading failed:');
WriteLn(GetLoadFontError);
end;
Typical error messages:
"PCX file not found: FONT.PCX""XML file could not be loaded: FONT.XML""Invalid XML format""Missing height attribute in <font> element""Invalid character code: 200 (must be 0-127)""Missing required attribute: width"
FreeFont
procedure FreeFont(var Font: TFont);
Frees all resources associated with a font.
Parameters:
Font- TFont structure to free
Example:
var
GameFont: TFont;
begin
LoadFont('FONT.XML', GameFont);
{ Use font... }
FreeFont(GameFont); { Free resources before exit }
end;
Cleanup actions:
- Free sprite sheet image data:
FreeImage(Font.Image) - Clear character metadata
- Set
Font.Loaded := False
CRITICAL:
- Always call before program exit
- Prevents memory leaks
- Safe to call on unloaded fonts (no-op)
PrintFontText
procedure PrintFontText( X, Y: Integer; const Text: string; Align: TAlign; var Font: TFont; FrameBuffer: PFrameBuffer );
Renders text using a loaded font with horizontal alignment.
Parameters:
X, Y- Starting position in framebuffer (pixels). X is adjusted based on alignment.Text- String to render (ASCII 0-127 only)Align- Horizontal alignment (from GENTYPES.PAS):Align_Left(0) - X is left edge of textAlign_Center(1) - X is center of textAlign_Right(2) - X is right edge of text
Font- Loaded TFont structureFrameBuffer- Target framebuffer to draw into
Example:
var
GameFont: TFont;
ScreenBuffer: PFrameBuffer;
begin
LoadFont('FONT.XML', GameFont);
ScreenBuffer := GetScreenBuffer;
{ Draw text with different alignments }
PrintFontText(10, 50, 'Left aligned', Align_Left, GameFont, ScreenBuffer);
PrintFontText(160, 90, 'Centered', Align_Center, GameFont, ScreenBuffer);
PrintFontText(310, 130, 'Right aligned', Align_Right, GameFont, ScreenBuffer);
end;
Rendering process:
- Initialize cursor X position
- For each character in text:
- Get ASCII code
- Look up character info in
Font.Chars[code] - If
Defined = FalseorWidth = 0, skip character - Otherwise, call
PutImageRectto draw character:PutImageRect( Font.Image, { Source sprite sheet } CharRect, { Source rectangle } CursorX, Y, { Destination position } True, { Transparent } FrameBuffer { Target buffer } ); - Advance cursor:
CursorX := CursorX + Width
- Return final cursor position (for chaining)
Character handling:
{ Example character rendering logic }
for i := 1 to Length(Text) do
begin
CharCode := Ord(Text[i]);
if CharCode > 127 then Continue; { Skip extended ASCII }
CharInfo := Font.Chars[CharCode];
if (not CharInfo.Defined) or (CharInfo.Width = 0) then Continue;
{ Draw character }
SourceRect.X := CharInfo.X;
SourceRect.Y := CharInfo.Y;
SourceRect.Width := CharInfo.Width;
SourceRect.Height := Font.Height;
PutImageRect(
Font.Image,
SourceRect,
CursorX, Y,
True, { Transparent (color 0 = transparent) }
FrameBuffer
);
CursorX := CursorX + CharInfo.Width + Font.Padding;
end;
Transparency:
- Uses
PutImageRectwithTransparent := True - Color 0 (black) is treated as transparent
- Font sprite sheets should use color 0 for background
PrintFontTextColored
procedure PrintFontTextColored( X, Y: Integer; const Text: string; Color: Byte; Align: TAlign; var Font: TFont; FrameBuffer: PFrameBuffer );
Renders colored text using a loaded font with horizontal alignment and runtime color replacement.
Parameters:
X, Y- Starting position in framebuffer (pixels). X is adjusted based on alignment.Text- String to render (ASCII 0-127 only)Color- Color to use for replacement (0-255)Align- Horizontal alignment (Align_Left, Align_Center, Align_Right)Font- Loaded TFont structure (must have ReplaceColor > 0)FrameBuffer- Target framebuffer to draw into
How it works:
- If
Font.ReplaceColoris 0, callsPrintFontTextinstead (no color replacement) - If
Font.ReplaceColor> 0, renders text pixel-by-pixel:- Color 0 (transparent) is skipped
- Pixels matching
Font.ReplaceColorare replaced withColor - All other pixels are drawn as-is
Example:
var
GameFont: TFont;
ScreenBuffer: PFrameBuffer;
begin
{ Font XML has replace-color="15" attribute }
LoadFont('FONT.XML', GameFont);
ScreenBuffer := GetScreenBuffer;
{ Draw text in different colors }
PrintFontTextColored(160, 50, 'Red', 12, Align_Center, GameFont, ScreenBuffer);
PrintFontTextColored(160, 90, 'Green', 10, Align_Center, GameFont, ScreenBuffer);
PrintFontTextColored(160, 130, 'Blue', 9, Align_Center, GameFont, ScreenBuffer);
end;
Performance note:
- Color replacement uses pixel-by-pixel rendering (slower than PrintFontText)
- Only use when you need dynamic text coloring
- Set
ReplaceColorto 0 in XML if color replacement is not needed
GetTextWidth
function GetTextWidth(const Text: string; var Font: TFont): Integer;
Calculates the pixel width of a text string when rendered with the font.
Parameters:
Text- String to measure (ASCII 0-127 only)Font- Loaded TFont structure
Returns:
- Total width in pixels (including padding between characters)
Example:
var
Width: Integer;
begin
Width := GetTextWidth('Hello World', GameFont);
WriteLn('Text width: ', Width, ' pixels');
end;
Use GetTextWidth with alignment:
{ Manual centering (alternative to Align_Center parameter) }
var
TextWidth, X: Integer;
begin
TextWidth := GetTextWidth('Hello', GameFont);
X := (320 - TextWidth) div 2; { Calculate center X }
PrintFontText(X, 100, 'Hello', Align_Left, GameFont, BackBuffer);
{ Or simply use Align_Center: }
PrintFontText(160, 100, 'Hello', Align_Center, GameFont, BackBuffer);
end;
XML Schema
Font Element
<font height="HEIGHT" padding="PADDING" image="IMAGE.PCX" replace-color="COLOR"> <!-- Character definitions --> </font>
Attributes:
-
height(required): Integer, 1-255- Height of all characters in pixels
- All characters in font have same height
-
padding(optional): Integer, 0-255 (default: 0)- Horizontal spacing between characters in pixels
- Applied after each character (right padding)
- NOT for vertical spacing (use Font.Height for line spacing)
-
image(required): String- Path to PCX sprite sheet file (relative to XML file)
- Example: "FONT.PCX" or "../IMAGES/FONT.PCX"
-
replace-color(optional): Integer, 0-255 (default: 0)- Color index to replace when using PrintFontTextColored
- Set to 0 to disable color replacement
- Example: Use 15 (white) as placeholder color in font image
Child elements:
- One or more
<char>elements
Character Element
<char code="CODE" x="X" y="Y" width="WIDTH" />
Attributes:
-
code(required): Integer, 0-127- ASCII character code
- Must be unique (no duplicate codes)
-
x(required): Integer, 0-65535- X position in sprite sheet (pixels)
- Left edge of character glyph
-
y(required): Integer, 0-65535- Y position in sprite sheet (pixels)
- Top edge of character glyph
-
width(required): Integer, 0-255- Character width in pixels
- Can be 0 for space characters
Example:
<char code="65" x="0" y="0" width="18" /> <!-- 'A' -->
Complete Example
<?xml version="1.0" encoding="UTF-8"?> <font height="32" padding="1" image="FONT.PCX" replace-color="15"> <!-- Uppercase A-Z --> <char code="65" x="0" y="0" width="18" /> <!-- A --> <char code="66" x="18" y="0" width="16" /> <!-- B --> <char code="67" x="34" y="0" width="17" /> <!-- C --> <!-- ... Z --> <!-- Lowercase a-z --> <char code="97" x="0" y="32" width="15" /> <!-- a --> <char code="98" x="15" y="32" width="15" /> <!-- b --> <!-- ... z --> <!-- Numbers 0-9 --> <char code="48" x="200" y="0" width="14" /> <!-- 0 --> <char code="49" x="214" y="0" width="10" /> <!-- 1 --> <char code="50" x="224" y="0" width="14" /> <!-- 2 --> <!-- ... 9 --> <!-- Special characters --> <char code="32" x="0" y="0" width="8" /> <!-- Space --> <char code="33" x="300" y="0" width="8" /> <!-- ! --> <char code="46" x="308" y="0" width="6" /> <!-- . --> <char code="58" x="314" y="0" width="6" /> <!-- : --> <!-- Extended special characters --> <char code="44" x="320" y="0" width="6" /> <!-- , --> <char code="63" x="326" y="0" width="14" /> <!-- ? --> </font>
Usage Examples
Basic Text Rendering
program FontTest;
uses VGA, VGAFont, PCX;
var
GameFont: TFont;
ScreenBuffer: PFrameBuffer;
Running: Boolean;
begin
InitVGA;
ScreenBuffer := GetScreenBuffer;
{ Load font }
if not LoadFont('FONTS\MAIN.XML', GameFont) then
begin
DoneVGA;
WriteLn('Error: ', GetLoadFontError);
Halt(1);
end;
{ Clear screen }
ClearFrameBuffer(BackBuffer);
{ Draw text with alignment }
PrintFontText(10, 10, 'Hello, World!', Align_Left, GameFont, ScreenBuffer);
PrintFontText(160, 50, 'Centered!', Align_Center, GameFont, ScreenBuffer);
PrintFontText(310, 90, 'Right', Align_Right, GameFont, ScreenBuffer);
ReadLn;
{ Cleanup }
FreeFont(GameFont);
DoneVGA;
end.
Multiple Fonts
var
TitleFont: TFont; { Large decorative font }
NormalFont: TFont; { Regular game text }
SmallFont: TFont; { Small UI text }
begin
{ Load different fonts for different purposes }
LoadFont('FONTS\TITLE.XML', TitleFont);
LoadFont('FONTS\NORMAL.XML', NormalFont);
LoadFont('FONTS\SMALL.XML', SmallFont);
{ Use appropriate font for each element }
PrintFontText(160, 50, 'XICLONE', Align_Center, TitleFont, BackBuffer);
PrintFontText(160, 100, 'Press ENTER to start', Align_Center, NormalFont, BackBuffer);
PrintFontText(10, 190, 'v1.0', Align_Left, SmallFont, BackBuffer);
{ Cleanup all fonts }
FreeFont(TitleFont);
FreeFont(NormalFont);
FreeFont(SmallFont);
end;
Text Wrapping (Advanced)
procedure PrintWrappedText(
X, Y, MaxWidth: Integer;
const Text: string;
var Font: TFont;
FrameBuffer: PFrameBuffer
);
var
Words: array[0..99] of string;
WordCount, i: Integer;
Line: string;
LineWidth: Integer;
CursorY: Integer;
begin
{ Split text into words }
WordCount := SplitWords(Text, Words);
CursorY := Y;
Line := '';
for i := 0 to WordCount - 1 do
begin
{ Try adding word to current line }
if Line = '' then
TestLine := Words[i]
else
TestLine := Line + ' ' + Words[i];
LineWidth := GetTextWidth(TestLine, Font);
if LineWidth > MaxWidth then
begin
{ Line too long - print current line and start new one }
if Line <> '' then
begin
PrintFontText(X, CursorY, Line, Align_Left, Font, FrameBuffer);
CursorY := CursorY + Font.Height + 4; { Line spacing }
end;
Line := Words[i];
end
else
Line := TestLine;
end;
{ Print remaining line }
if Line <> '' then
PrintFontText(X, CursorY, Line, Align_Left, Font, FrameBuffer);
end;
Font Creation Workflow
Step 1: Design Font in Pix8
Use Pix8, our own 256-color pixel art editor (browser + Windows desktop app) — images are 8-bit indexed by default. GrafX2 works just as well if you prefer it; the steps below are the same in both.
-
Create new image (File → New...):
- Width: 512 pixels (or as needed)
- Height: 64-128 pixels (depends on character count)
- Colors: 256 (indexed palette)
-
Draw characters:
- Fixed height per character (e.g., 32 pixels)
- Variable width (proportional spacing)
- Use color 0 (black) for transparent background
- Leave padding between characters for clarity
-
Layout example:
Row 1: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z Row 2: a b c d e f g h i j k l m n o p q r s t u v w x y z Row 3: 0 1 2 3 4 5 6 7 8 9 ! ? . , : ; ' "
-
Save as PCX:
- File → Export as... (Ctrl+Shift+E) → PCX
- Save to
DATA\FONTS\MYFONT.PCX - Pix8's rulers and guides make it easy to read off the per-character
x/widthvalues you need for the XML metadata in Step 2
Step 2: Generate XML Metadata
Create MYFONT.XML:
<?xml version="1.0" encoding="US-ASCII"?> <font height="32" padding="1" image="MYFONT.PCX" replace-color="15"> <char code="65" x="0" y="0" width="18" /> <!-- A --> <!-- Add all characters... --> </font>
Step 3: Test Font
program TestFont;
uses VGA, VGAFont;
var
Font: TFont;
Buffer: PFrameBuffer;
begin
InitVGA;
Buffer := CreateFrameBuffer;
if not LoadFont('FONTS\MYFONT.XML', Font) then
begin
DoneVGA;
WriteLn('Error: ', GetLoadFontError);
Halt(1);
end;
ClearFrameBuffer(Buffer);
{ Test all printable ASCII with different alignments }
PrintFontText(10, 10, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', Align_Left, Font, Buffer);
PrintFontText(160, 50, 'abcdefghijklmnopqrstuvwxyz', Align_Center, Font, Buffer);
PrintFontText(310, 90, '0123456789 !?.,:;', Align_Right, Font, Buffer);
RenderFrameBuffer(Buffer);
ReadLn;
FreeFont(Font);
FreeFrameBuffer(Buffer);
DoneVGA;
end.
Performance Considerations
Rendering Speed
Character rendering cost:
Single character (16×32 pixels): - PutImageRect call: ~0.5ms on 286 - Memory copied: 512 bytes Text line "SCORE: 12345" (12 characters): - Total time: ~6ms on 286 - 166 FPS if only drawing text Typical HUD (5 text lines): - Total time: ~30ms on 286 - Still achieves 30+ FPS
Optimization tips:
- Cache static text - Don't re-render every frame
- Use dirty rectangles - Only redraw changed text
- Pre-render common strings - "SCORE:", "LEVEL:", etc.
- Avoid text in inner loops - Render UI once per frame max
Memory Usage
Per font:
TFont structure: ~900 bytes Sprite sheet (512×64, 256 colors): 32KB Total per font: ~33KB Multiple fonts (Title + Normal + Small): ~100KB Still plenty of room in 640KB conventional memory
Comparison:
- VGAPRINT (embedded): 1KB
- VGAFONT (typical): 33KB per font
- Trade-off: Quality vs. memory
XML Parsing Performance
Loading time:
Parse XML (128 characters): ~50ms on 286 Load PCX image: ~100ms on 286 Total load time: ~150ms (done once at startup) Acceptable for: - Game initialization - Level loading - Not acceptable for: Every frame rendering
Best practice:
- Load fonts at startup
- Keep loaded for entire game session
- Don't reload fonts during gameplay
Error Handling
Error Categories
File errors:
- PCX file not found
- XML file not found
- File read errors
XML errors:
- Invalid XML format (malformed)
- Missing root element
- Missing required attributes
- Invalid attribute values
Data errors:
- Character code out of range (not 0-127)
- Negative dimensions
- Duplicate character codes
Example Error Messages
"PCX file not found: FONTS\MAIN.PCX" "XML file could not be loaded: FONTS\MAIN.XML" "Invalid XML format" "Missing root <font> element" "Missing height attribute in <font> element" "Invalid height: -5 (must be 1-255)" "Missing required attribute: width" "Invalid character code: 200 (must be 0-127)" "Invalid width: -10 (must be 0-255)" "Duplicate character code: 65"
Conclusion
VGAFONT.PAS provides a professional variable-width font system that:
✅ Advantages:
- Better visual appearance than monospace fonts
- Flexible character sizes (proportional spacing)
- External font definitions (easy to modify)
- Efficient rendering (PutImageRect)
- Multiple fonts support
⚠️ Considerations:
- Requires external PCX + XML files
- Higher memory usage than VGAPRINT
- Slower loading (XML parsing)
- More complex setup
Best for:
- Game UI, menus, dialogues
- Professional-looking text
- Games with high production values
Use VGAPRINT for:
- Debug output, FPS counters
- Quick prototypes
- Minimal memory footprint