Add TrueType font support via stb_truetype.h

Integrate stb_truetype.h to rasterize TTF glyphs into 1-bit .FNT v3
format consumed by Win3.x display drivers. Key implementation detail:
.FNT bitmaps use column-major byte order (all rows of byte-column 0
first, then byte-column 1, etc.), not row-major.

New API: wdrvLoadFontTtf(path, pointSize) loads any TTF at any size.
Demo 7 renders Liberation Sans/Serif/Mono at 16/20/24pt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Duensing 2026-02-22 00:38:16 -06:00
parent e8a7812233
commit fbb1cce5c3
11 changed files with 5449 additions and 6 deletions

4
.gitattributes vendored
View file

@ -8,3 +8,7 @@
*.BMP filter=lfs diff=lfs merge=lfs -text *.BMP filter=lfs diff=lfs merge=lfs -text
*.ICO filter=lfs diff=lfs merge=lfs -text *.ICO filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text
*.FON filter=lfs diff=lfs merge=lfs -text
*.fon filter=lfs diff=lfs merge=lfs -text
*.TTF filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text

View file

@ -80,8 +80,10 @@ windriver/
(386 protected mode with WF_PMODE + WF_CPU386). v2 (0x0200) is rejected at runtime. (386 protected mode with WF_PMODE + WF_CPU386). v2 (0x0200) is rejected at runtime.
- **v3 char table**: at file offset 0x94 (fs30CharOffset), 6-byte entries {WORD width, DWORD offset}. - **v3 char table**: at file offset 0x94 (fs30CharOffset), 6-byte entries {WORD width, DWORD offset}.
The DWORD offset is absolute from the font segment base. Use FntCharEntry30T. The DWORD offset is absolute from the font segment base. Use FntCharEntry30T.
- **Bitmap layout**: per-character contiguous (16 bytes per char for 8x16 font, stride=1 between rows). - **Bitmap layout**: per-character contiguous, **column-major** byte order. For each character,
VGA BIOS 8x16 font (INT 10h AH=11h AL=30h BH=06) is already in this format — no transpose needed. all pixHeight rows of byte-column 0 come first, then all rows of byte-column 1, etc.
Address formula: `(byteCol * pixHeight) + row`. For 8px-wide chars (1 byte column), this
is identical to row-major. VGA BIOS 8x16 font is already in this format — no transpose needed.
- **lpClipRect must NOT be NULL**: VBESVGA's get_clip unconditionally dereferences lpClipRect - **lpClipRect must NOT be NULL**: VBESVGA's get_clip unconditionally dereferences lpClipRect
(STRBLT.ASM:1008 "We assume that we will never get passed a null rectangle"). Pass a RECT (STRBLT.ASM:1008 "We assume that we will never get passed a null rectangle"). Pass a RECT
covering the full screen (0, 0, 0x7FFF, 0x7FFF). covering the full screen (0, 0, 0x7FFF, 0x7FFF).
@ -118,8 +120,10 @@ windriver/
Correct v3 offset = v2offset + shift (where shift = newBitmapOff - origBitsOff) Correct v3 offset = v2offset + shift (where shift = newBitmapOff - origBitsOff)
- `wdrvLoadFontFon(path, index)` loads from .FON; `wdrvLoadFontFnt(path)` loads raw .FNT - `wdrvLoadFontFon(path, index)` loads from .FON; `wdrvLoadFontFnt(path)` loads raw .FNT
- `wdrvLoadFontBuiltin()` returns the VGA ROM 8x16 singleton; must NOT be passed to wdrvUnloadFont - `wdrvLoadFontBuiltin()` returns the VGA ROM 8x16 singleton; must NOT be passed to wdrvUnloadFont
- `wdrvLoadFontTtf(path, pointSize)` loads TrueType via stb_truetype, rasterizes to 1-bit v3 FNT
- `wdrvExtTextOut` takes a `WdrvFontT font` parameter (NULL = built-in) - `wdrvExtTextOut` takes a `WdrvFontT font` parameter (NULL = built-in)
- Available test fonts in `fon/`: COURE.FON (8x13, 9x16, 12x20), SSERIFE.FON, SERIFE.FON, VGASYS.FON, etc. - Available test fonts in `fon/`: COURE.FON (8x13, 9x16, 12x20), SSERIFE.FON, SERIFE.FON, VGASYS.FON, etc.
- Available TTF fonts in `ttf/`: LIBMONO.TTF, LIBSANS.TTF, LIBSERIF.TTF (Liberation family)
## Current Demo Status ## Current Demo Status
- S3TRIO.DRV, VBESVGA.DRV, VGA.DRV, ET4000.DRV all work: Load → Enable → Draw → Disable → Unload - S3TRIO.DRV, VBESVGA.DRV, VGA.DRV, ET4000.DRV all work: Load → Enable → Draw → Disable → Unload
@ -128,6 +132,7 @@ windriver/
- Demo 3: Lines/starburst (Output/Polyline) — works - Demo 3: Lines/starburst (Output/Polyline) — works
- Demo 4: Screen-to-screen blit (BitBlt SRCCOPY) — works - Demo 4: Screen-to-screen blit (BitBlt SRCCOPY) — works
- Demo 5: ExtTextOut text rendering — works (VBESVGA.DRV) - Demo 5: ExtTextOut text rendering — works (VBESVGA.DRV)
- Demo 7: TrueType font rendering at multiple sizes — works
- VGA.DRV: 640x480 4-plane 16-color mode; limited color palette but functional - VGA.DRV: 640x480 4-plane 16-color mode; limited color palette but functional
- ET4000.DRV: 640x480 8bpp on svga_et4000; software-only, no hw acceleration - ET4000.DRV: 640x480 8bpp on svga_et4000; software-only, no hw acceleration
- Drivers stored in `drivers/` directory, copied to `bin/` during build - Drivers stored in `drivers/` directory, copied to `bin/` during build

View file

@ -18,7 +18,7 @@ DJGPP_PREFIX ?= $(HOME)/djgpp/djgpp
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
CFLAGS = -Wall -Wextra -O2 -std=gnu99 -Iwin31drv CFLAGS = -Wall -Wextra -O2 -std=gnu99 -Iwin31drv
LDFLAGS = LDFLAGS = -lm
# DJGPP binutils need libfl.so.2 which may not be installed system-wide # DJGPP binutils need libfl.so.2 which may not be installed system-wide
export LD_LIBRARY_PATH := $(realpath tools/lib):$(LD_LIBRARY_PATH) export LD_LIBRARY_PATH := $(realpath tools/lib):$(LD_LIBRARY_PATH)
@ -48,6 +48,7 @@ $(DEMO_EXE): $(DEMO_OBJ) lib | $(BINDIR)
cp tools/TEST.BAT $(BINDIR)/ cp tools/TEST.BAT $(BINDIR)/
-cp -n drivers/*.DRV $(BINDIR)/ 2>/dev/null; true -cp -n drivers/*.DRV $(BINDIR)/ 2>/dev/null; true
-cp -n fon/*.FON $(BINDIR)/ 2>/dev/null; true -cp -n fon/*.FON $(BINDIR)/ 2>/dev/null; true
-cp -n ttf/*.TTF $(BINDIR)/ 2>/dev/null; true
$(OBJDIR)/%.o: %.c | $(OBJDIR) $(OBJDIR)/%.o: %.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $< $(CC) $(CFLAGS) -c -o $@ $<

46
demo.c
View file

@ -395,6 +395,52 @@ static void demoDrawing(WdrvHandleT drv)
wdrvUnloadFont(sysFont); wdrvUnloadFont(sysFont);
logMsg(" Font demo done\n"); logMsg(" Font demo done\n");
} }
// Demo 7: TrueType font rendering
if (info.hasExtTextOut) {
logMsg("Demo 7: TrueType fonts\n");
int32_t ret;
int16_t textY = 180;
WdrvFontT ttfSmall = wdrvLoadFontTtf("LIBSANS.TTF", 16);
WdrvFontT ttfMedium = wdrvLoadFontTtf("LIBSERIF.TTF", 20);
WdrvFontT ttfLarge = wdrvLoadFontTtf("LIBMONO.TTF", 24);
if (ttfSmall) {
const char *msg = "TrueType Sans 16pt: The quick brown fox jumps over the lazy dog";
ret = wdrvExtTextOut(drv, 10, textY, msg, (int16_t)strlen(msg),
MAKE_RGB(255, 255, 255), MAKE_RGB(0, 0, 128), true, ttfSmall);
logMsg(" TTF small ret=%" PRId32 "\n", ret);
textY += 22;
} else {
logMsg(" LIBSANS.TTF 16pt not loaded\n");
}
if (ttfMedium) {
const char *msg = "TrueType Serif 20pt: ABCDEFGHIJKLMNOPQRSTUVWXYZ";
ret = wdrvExtTextOut(drv, 10, textY, msg, (int16_t)strlen(msg),
MAKE_RGB(255, 255, 0), MAKE_RGB(128, 0, 0), true, ttfMedium);
logMsg(" TTF medium ret=%" PRId32 "\n", ret);
textY += 26;
} else {
logMsg(" LIBSERIF.TTF 20pt not loaded\n");
}
if (ttfLarge) {
const char *msg = "TrueType Mono 24pt: 0123456789";
ret = wdrvExtTextOut(drv, 10, textY, msg, (int16_t)strlen(msg),
MAKE_RGB(0, 255, 255), MAKE_RGB(0, 0, 0), true, ttfLarge);
logMsg(" TTF large ret=%" PRId32 "\n", ret);
textY += 30;
} else {
logMsg(" LIBMONO.TTF 24pt not loaded\n");
}
wdrvUnloadFont(ttfSmall);
wdrvUnloadFont(ttfMedium);
wdrvUnloadFont(ttfLarge);
logMsg(" TTF demo done\n");
}
} }

BIN
ttf/LIBMONO.TTF (Stored with Git LFS) Normal file

Binary file not shown.

BIN
ttf/LIBSANS.TTF (Stored with Git LFS) Normal file

Binary file not shown.

BIN
ttf/LIBSERIF.TTF (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -46,7 +46,7 @@ $(OBJDIR)/log.o: log.c log.h
$(OBJDIR)/neload.o: neload.c neload.h neformat.h wintypes.h log.h $(OBJDIR)/neload.o: neload.c neload.h neformat.h wintypes.h log.h
$(OBJDIR)/thunk.o: thunk.c thunk.h wintypes.h log.h $(OBJDIR)/thunk.o: thunk.c thunk.h wintypes.h log.h
$(OBJDIR)/winstub.o: winstub.c winstub.h thunk.h wintypes.h log.h $(OBJDIR)/winstub.o: winstub.c winstub.h thunk.h wintypes.h log.h
$(OBJDIR)/windrv.o: windrv.c windrv.h wintypes.h winddi.h neformat.h neload.h thunk.h winstub.h log.h $(OBJDIR)/windrv.o: windrv.c windrv.h wintypes.h winddi.h neformat.h neload.h thunk.h winstub.h log.h stb_truetype.h
$(CC) $(WINDRV_CFLAGS) -c -o $@ $< $(CC) $(WINDRV_CFLAGS) -c -o $@ $<
clean: clean:

5079
win31drv/stb_truetype.h Normal file

File diff suppressed because it is too large Load diff

View file

@ -46,6 +46,10 @@
#include "winstub.h" #include "winstub.h"
#include "log.h" #include "log.h"
#include <math.h>
#define STB_TRUETYPE_IMPLEMENTATION
#include "stb_truetype.h"
// ============================================================================ // ============================================================================
// Driver instance structure (opaque handle) // Driver instance structure (opaque handle)
// ============================================================================ // ============================================================================
@ -3166,6 +3170,296 @@ WdrvFontT wdrvLoadFontFon(const char *fonPath, int32_t fontIndex)
} }
WdrvFontT wdrvLoadFontTtf(const char *ttfPath, int32_t pointSize)
{
// Read TTF file into memory
FILE *f = fopen(ttfPath, "rb");
if (!f) {
logErr("windrv: wdrvLoadFontTtf: cannot open '%s'\n", ttfPath);
setError(WDRV_ERR_FILE_NOT_FOUND);
return NULL;
}
fseek(f, 0, SEEK_END);
long fileSize = ftell(f);
fseek(f, 0, SEEK_SET);
if (fileSize < 12 || fileSize > 0x1000000) {
logErr("windrv: wdrvLoadFontTtf: bad file size %ld\n", fileSize);
fclose(f);
setError(WDRV_ERR_BAD_FONT);
return NULL;
}
uint8_t *ttfBuf = (uint8_t *)malloc((uint32_t)fileSize);
if (!ttfBuf) {
fclose(f);
setError(WDRV_ERR_NO_MEMORY);
return NULL;
}
if (fread(ttfBuf, 1, (uint32_t)fileSize, f) != (size_t)fileSize) {
logErr("windrv: wdrvLoadFontTtf: read error\n");
free(ttfBuf);
fclose(f);
setError(WDRV_ERR_BAD_FONT);
return NULL;
}
fclose(f);
// Initialize stb_truetype
stbtt_fontinfo stbFont;
if (!stbtt_InitFont(&stbFont, ttfBuf, 0)) {
logErr("windrv: wdrvLoadFontTtf: stbtt_InitFont failed\n");
free(ttfBuf);
setError(WDRV_ERR_BAD_FONT);
return NULL;
}
// Compute scale for target pixel height
float scale = stbtt_ScaleForPixelHeight(&stbFont, (float)pointSize);
// Get font vertical metrics (unscaled)
int stbAscent;
int stbDescent;
int stbLineGap;
stbtt_GetFontVMetrics(&stbFont, &stbAscent, &stbDescent, &stbLineGap);
int32_t scaledAscent = (int32_t)(stbAscent * scale + 0.5f);
int32_t scaledDescent = (int32_t)(-stbDescent * scale + 0.5f);
int32_t pixHeight = scaledAscent + scaledDescent;
if (pixHeight < 1) {
pixHeight = pointSize;
}
// Measure all glyphs (chars 32..255)
uint16_t nChars = 224;
uint8_t firstChar = 32;
uint8_t lastChar = 255;
uint16_t advanceWidths[224];
int32_t maxAdvance = 0;
int32_t totalAdvance = 0;
int32_t maxBitmapW = 0;
bool allSameWidth = true;
for (int32_t i = 0; i < nChars; i++) {
int ch = firstChar + i;
int advance;
int lsb;
stbtt_GetCodepointHMetrics(&stbFont, ch, &advance, &lsb);
int32_t advW = (int32_t)(advance * scale + 0.5f);
if (advW < 1) {
advW = 1;
}
advanceWidths[i] = (uint16_t)advW;
totalAdvance += advW;
if (advW > maxAdvance) {
maxAdvance = advW;
}
if (i > 0 && advanceWidths[i] != advanceWidths[0]) {
allSameWidth = false;
}
// Check bitmap bounding box too
int x0;
int y0;
int x1;
int y1;
stbtt_GetCodepointBitmapBox(&stbFont, ch, scale, scale, &x0, &y0, &x1, &y1);
int32_t bw = x1 - x0;
if (bw > maxBitmapW) {
maxBitmapW = bw;
}
}
// fsWidthBytes = sum of all per-character byte strides (matches .FON convention)
uint32_t widthBytesSum = 0;
for (int32_t i = 0; i < nChars; i++) {
uint16_t s = (advanceWidths[i] + 7) / 8;
if (s < 1) {
s = 1;
}
widthBytesSum += s;
}
uint16_t fsWidthBytes = (uint16_t)widthBytesSum;
// Compute v3 layout with per-character bitmap sizes.
// Each character's bitmap has its own row stride = ceil(charWidth/8).
uint32_t charTableOff = 0x0094;
uint32_t bitmapOff = charTableOff + (uint32_t)(nChars + 1) * 6;
uint32_t charOffsets[225];
uint32_t curOff = bitmapOff;
for (int32_t i = 0; i < nChars; i++) {
charOffsets[i] = curOff;
uint16_t byteStride = (advanceWidths[i] + 7) / 8;
if (byteStride < 1) {
byteStride = 1;
}
curOff += (uint32_t)byteStride * (uint32_t)pixHeight;
}
charOffsets[nChars] = charOffsets[0]; // sentinel reuses char 0
uint32_t devNameOff = curOff;
uint32_t faceNameOff = devNameOff + 1;
// Extract face name from filename stem
const char *baseName = ttfPath;
for (const char *p = ttfPath; *p; p++) {
if (*p == '/' || *p == '\\') {
baseName = p + 1;
}
}
char faceName[64];
strncpy(faceName, baseName, sizeof(faceName) - 1);
faceName[sizeof(faceName) - 1] = '\0';
char *dot = strrchr(faceName, '.');
if (dot) {
*dot = '\0';
}
uint32_t faceNameLen = (uint32_t)strlen(faceName);
uint32_t totalSize = faceNameOff + faceNameLen + 1;
// Allocate 16-bit accessible block
uint32_t linearOut;
uint16_t sel = alloc16BitBlock(totalSize, &linearOut);
if (sel == 0) {
free(ttfBuf);
setError(WDRV_ERR_NO_MEMORY);
return NULL;
}
uint8_t *base = (uint8_t *)linearOut;
// Fill .FNT v3 header
FntHeader16T *hdr = (FntHeader16T *)base;
hdr->fsVersion = 0x0300;
hdr->fsSize = totalSize;
hdr->fsType = 0; // raster
hdr->fsPoints = (uint16_t)pointSize;
hdr->fsVertRes = 96;
hdr->fsHorizRes = 96;
hdr->fsAscent = (uint16_t)scaledAscent;
hdr->fsInternalLeading = 0;
hdr->fsExternalLeading = 0;
hdr->fsItalic = 0;
hdr->fsUnderline = 0;
hdr->fsStrikeOut = 0;
hdr->fsWeight = 400;
hdr->fsCharSet = 0; // ANSI
hdr->fsPixWidth = allSameWidth ? advanceWidths[0] : 0;
hdr->fsPixHeight = (uint16_t)pixHeight;
hdr->fsPitchAndFamily = allSameWidth ? 0x30 : 0x01;
hdr->fsAvgWidth = (uint16_t)(totalAdvance / nChars);
hdr->fsMaxWidth = (uint16_t)maxAdvance;
hdr->fsFirstChar = firstChar;
hdr->fsLastChar = lastChar;
hdr->fsDefaultChar = (uint8_t)('.' - firstChar);
hdr->fsBreakChar = (uint8_t)(' ' - firstChar);
hdr->fsWidthBytes = fsWidthBytes;
hdr->fsDevice = devNameOff;
hdr->fsFace = faceNameOff;
hdr->fsBitsPointer = ((uint32_t)sel << 16) | bitmapOff;
hdr->fsBitsOffset = bitmapOff;
// v3 extension at 0x76 already zeroed by calloc
// Fill v3 char table at 0x94
FntCharEntry30T *charTable = (FntCharEntry30T *)(base + charTableOff);
for (int32_t i = 0; i <= nChars; i++) {
int32_t idx = (i < nChars) ? i : 0; // sentinel reuses char 0
charTable[i].width = advanceWidths[idx];
charTable[i].offset = charOffsets[i];
}
// Rasterize glyphs and threshold to 1-bit
for (int32_t i = 0; i < nChars; i++) {
int ch = firstChar + i;
int bw;
int bh;
int xoff;
int yoff;
unsigned char *bitmap = stbtt_GetCodepointBitmap(
&stbFont, scale, scale, ch, &bw, &bh, &xoff, &yoff);
if (!bitmap || bw <= 0 || bh <= 0) {
if (bitmap) {
stbtt_FreeBitmap(bitmap, NULL);
}
continue;
}
// Position glyph within the fixed-height cell.
// xoff/yoff are relative to the pen position; use directly and
// let the bounds checks clip anything outside the cell.
int32_t cellY = scaledAscent + yoff;
uint16_t charStride = (advanceWidths[i] + 7) / 8;
if (charStride < 1) {
charStride = 1;
}
uint8_t *dst = base + charOffsets[i];
// .FNT bitmap format is column-major: all pixHeight rows of
// byte-column 0 first, then all rows of byte-column 1, etc.
// Layout: dst[(byteCol * pixHeight) + row] bit = MSB-first
for (int row = 0; row < bh; row++) {
int32_t dstRow = cellY + row;
if (dstRow < 0 || dstRow >= pixHeight) {
continue;
}
for (int col = 0; col < bw; col++) {
int32_t dstCol = xoff + col;
if (dstCol < 0 || dstCol >= (int32_t)(charStride * 8)) {
continue;
}
uint8_t alpha = bitmap[row * bw + col];
if (alpha >= 128) {
uint32_t byteCol = (uint32_t)dstCol / 8;
dst[byteCol * pixHeight + dstRow] |= (0x80 >> (dstCol & 7));
}
}
}
stbtt_FreeBitmap(bitmap, NULL);
}
// Write device name (empty) and face name
base[devNameOff] = '\0';
memcpy(base + faceNameOff, faceName, faceNameLen + 1);
// Done with TTF data
free(ttfBuf);
// Allocate font handle
struct WdrvFontS *font = (struct WdrvFontS *)calloc(1, sizeof(struct WdrvFontS));
if (!font) {
free16BitBlock(sel, linearOut);
setError(WDRV_ERR_NO_MEMORY);
return NULL;
}
font->fontSel = sel;
font->fontLinear = linearOut;
font->fontSize = totalSize;
font->pixHeight = (uint16_t)pixHeight;
font->pixWidth = allSameWidth ? advanceWidths[0] : 0;
font->fsVersion = 0x0300;
strncpy(font->faceName, faceName, sizeof(font->faceName) - 1);
font->faceName[sizeof(font->faceName) - 1] = '\0';
dbg("windrv: wdrvLoadFontTtf: '%s' %dpt -> %dx%d (max=%d avg=%d) sel=%04X size=%" PRIu32 "\n",
faceName, (int)pointSize,
allSameWidth ? (int)advanceWidths[0] : 0, (int)pixHeight,
(int)maxAdvance, (int)(totalAdvance / nChars),
sel, totalSize);
return font;
}
void wdrvUnloadFont(WdrvFontT font) void wdrvUnloadFont(WdrvFontT font)
{ {
if (!font || font == &gBuiltinFont) { if (!font || font == &gBuiltinFont) {

View file

@ -174,8 +174,13 @@ WdrvFontT wdrvLoadFontFnt(const char *fntPath);
// The returned handle must NOT be passed to wdrvUnloadFont. // The returned handle must NOT be passed to wdrvUnloadFont.
WdrvFontT wdrvLoadFontBuiltin(void); WdrvFontT wdrvLoadFontBuiltin(void);
// Unload a font previously loaded with wdrvLoadFontFon or wdrvLoadFontFnt. // Load a font from a TrueType (.TTF) file at the given point size.
// Silently ignores NULL and the built-in font. // Rasterizes glyphs to 1-bit bitmaps and packs into .FNT v3 format.
// Returns NULL on failure (call wdrvGetLastError for details).
WdrvFontT wdrvLoadFontTtf(const char *ttfPath, int32_t pointSize);
// Unload a font previously loaded with wdrvLoadFontFon, wdrvLoadFontFnt,
// or wdrvLoadFontTtf. Silently ignores NULL and the built-in font.
void wdrvUnloadFont(WdrvFontT font); void wdrvUnloadFont(WdrvFontT font);
// ============================================================================ // ============================================================================