Implement ExtTextOut DDI wrapper with built-in VGA ROM font

Add text rendering support via the Windows 3.x ExtTextOut DDI function.
Builds a .FNT v3 font structure from the VGA BIOS 8x16 ROM font
(INT 10h AH=11h), with v3 char table (6-byte entries, absolute offsets)
required by VBESVGA.DRV's BigFontFlags in protected mode. Provides a
full-screen clip rect since STRBLT.ASM unconditionally dereferences
lpClipRect. Tested with both VBESVGA.DRV and S3TRIO.DRV.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Duensing 2026-02-21 21:21:23 -06:00
parent 847db7586b
commit 946719052f
6 changed files with 366 additions and 1 deletions

View file

@ -75,6 +75,20 @@ windriver/
pattern-only ROPs. S3TRIO.DRV tolerates it but correct behavior is NULL.
- Source dependency check: `ropNeedsSrc = (((rop8 >> 2) ^ rop8) & 0x33) != 0`
## ExtTextOut DDI Notes
- **Font format**: VBESVGA.DRV requires .FNT v3 (fsVersion=0x0300) when BigFontFlags is set
(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}.
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).
VGA BIOS 8x16 font (INT 10h AH=11h AL=30h BH=06) is already in this format — no transpose needed.
- **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
covering the full screen (0, 0, 0x7FFF, 0x7FFF).
- **lpTextXForm**: declared but never read by VBESVGA — pass NULL.
- **lp_font offset**: passed as fontSel:0x42 (points to fsType within the .FNT block).
- **Return value**: DX bit 15 = error. AX=0 is NOT necessarily failure.
## INT 10h ES Translation
- Different INT 10h function families use different ES:offset registers:
VBE 4Fxx → ES:DI, AH=10h (palette) → ES:DX, AH=11h (fonts) → ES:BP, AH=1Bh → ES:DI
@ -102,6 +116,7 @@ windriver/
- Demo 2: Pixel patterns (Pixel) — works
- Demo 3: Lines/starburst (Output/Polyline) — works
- Demo 4: Screen-to-screen blit (BitBlt SRCCOPY) — works
- Demo 5: ExtTextOut text rendering — works (VBESVGA.DRV)
- 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
- Drivers stored in `drivers/` directory, copied to `bin/` during build

33
demo.c
View file

@ -308,6 +308,39 @@ static void demoDrawing(WdrvHandleT drv)
wdrvBitBlt(drv, &bp);
logMsg(" Screen blit done\n");
}
// Demo 5: Text output using ExtTextOut
if (info.hasExtTextOut) {
logMsg("Demo 5: ExtTextOut text rendering\n");
int32_t ret;
// Opaque text: white on blue
const char *msg1 = "Hello from ExtTextOut!";
ret = wdrvExtTextOut(drv, 10, 10, msg1, (int16_t)strlen(msg1),
MAKE_RGB(255, 255, 255), MAKE_RGB(0, 0, 170), true);
logMsg(" msg1 ret=%" PRId32 "\n", ret);
// Opaque text: yellow on dark red
const char *msg2 = "Win3.x DDI Text Output";
ret = wdrvExtTextOut(drv, 10, 30, msg2, (int16_t)strlen(msg2),
MAKE_RGB(255, 255, 0), MAKE_RGB(170, 0, 0), true);
logMsg(" msg2 ret=%" PRId32 "\n", ret);
// Transparent text: green on existing background
const char *msg3 = "Transparent mode";
ret = wdrvExtTextOut(drv, 10, 50, msg3, (int16_t)strlen(msg3),
MAKE_RGB(0, 255, 0), MAKE_RGB(0, 0, 0), false);
logMsg(" msg3 ret=%" PRId32 "\n", ret);
// Show resolution info
char buf[64];
int len = sprintf(buf, "Mode: %dx%d", screenW, screenH);
ret = wdrvExtTextOut(drv, 10, 70, buf, (int16_t)len,
MAKE_RGB(255, 255, 255), MAKE_RGB(0, 0, 0), true);
logMsg(" msg4 ret=%" PRId32 "\n", ret);
logMsg(" Text output done\n");
}
}

View file

@ -35,6 +35,6 @@ ems = true
mount c /home/scott/claude/windriver
c:
cd bin
DEMO.EXE -d VGA.DRV
DEMO.EXE S3TRIO.DRV
rem exit

View file

@ -276,4 +276,59 @@ typedef struct __attribute__((packed)) {
// ... additional fields follow
} DibPDevice16T;
// ============================================================================
// .FNT v2 file header (117 bytes)
//
// Display drivers expect the full .FNT format with fsVersion at offset 0.
// lp_font passed to ExtTextOut points to fsType at offset 0x42.
// ============================================================================
typedef struct __attribute__((packed)) {
uint16_t fsVersion; // 0x00: 0x0200
uint32_t fsSize; // 0x02: total structure size
char fsCopyright[60]; // 0x06: copyright string
uint16_t fsType; // 0x42: 0 = raster
uint16_t fsPoints; // 0x44: point size
uint16_t fsVertRes; // 0x46: vertical resolution
uint16_t fsHorizRes; // 0x48: horizontal resolution
uint16_t fsAscent; // 0x4A: baseline from top
uint16_t fsInternalLeading; // 0x4C
uint16_t fsExternalLeading; // 0x4E
uint8_t fsItalic; // 0x50
uint8_t fsUnderline; // 0x51
uint8_t fsStrikeOut; // 0x52
uint16_t fsWeight; // 0x53: 400 = normal
uint8_t fsCharSet; // 0x55: 255 = OEM
uint16_t fsPixWidth; // 0x56: 0=proportional, else fixed
uint16_t fsPixHeight; // 0x58: character cell height
uint8_t fsPitchAndFamily; // 0x5A
uint16_t fsAvgWidth; // 0x5B
uint16_t fsMaxWidth; // 0x5D
uint8_t fsFirstChar; // 0x5F
uint8_t fsLastChar; // 0x60
uint8_t fsDefaultChar; // 0x61: relative to fsFirstChar
uint8_t fsBreakChar; // 0x62: relative to fsFirstChar
uint16_t fsWidthBytes; // 0x63: bytes per bitmap row
uint32_t fsDevice; // 0x65: offset to device name
uint32_t fsFace; // 0x69: offset to face name
uint32_t fsBitsPointer; // 0x6D: far ptr to bitmap data
uint32_t fsBitsOffset; // 0x71: offset to bitmap data
} FntHeader16T; // 0x75 = 117 bytes
// v2 character table entry (at offset 0x76 after header + pad byte)
typedef struct __attribute__((packed)) {
uint16_t width; // character width in pixels
uint16_t offset; // byte offset from bitmap start
} FntCharEntry16T; // 4 bytes
// v3 character table entry (at offset 0x94 after v3 extension fields)
typedef struct __attribute__((packed)) {
uint16_t width; // character width in pixels
uint32_t offset; // absolute byte offset from segment base
} FntCharEntry30T; // 6 bytes
// ExtTextOut option flags
#define ETO_OPAQUE 0x0002
#define ETO_CLIPPED 0x0004
#endif // WINDDI_H

View file

@ -107,6 +107,12 @@ struct WdrvDriverS {
uint16_t drawModeOff;
uint32_t drawModeLinear;
// Font (.FNT v3 format, separate 16-bit block)
uint16_t fontSel;
uint32_t fontLinear;
uint32_t fontSize;
bool fontInitialized;
// Current state
bool enabled;
uint32_t currentColor;
@ -152,6 +158,7 @@ static uint32_t colorToPhys(struct WdrvDriverS *drv, uint32_t colorRef);
static void setDisplayStart(struct WdrvDriverS *drv, uint32_t byteOffset);
static void freeDrawObjects(struct WdrvDriverS *drv);
static bool initFont(struct WdrvDriverS *drv);
static uint16_t alloc16BitBlock(uint32_t size, uint32_t *linearOut);
static void free16BitBlock(uint16_t sel, uint32_t linear);
static void setError(int32_t err);
@ -2092,6 +2099,132 @@ int32_t wdrvSetPalette(WdrvHandleT handle, int32_t startIndex, int32_t count, co
}
// ============================================================================
// Text output
// ============================================================================
int32_t wdrvExtTextOut(WdrvHandleT handle, int16_t x, int16_t y,
const char *text, int16_t length,
uint32_t fgColor, uint32_t bgColor,
bool opaque)
{
if (!handle || !handle->enabled) {
return WDRV_ERR_NOT_ENABLED;
}
if (!handle->ddiEntry[DDI_ORD_EXTTEXTOUT].present) {
return WDRV_ERR_UNSUPPORTED;
}
// Lazy-init the VGA BIOS font
if (!handle->fontInitialized) {
if (!initFont(handle)) {
logErr("windrv: ExtTextOut: font init failed\n");
return WDRV_ERR_NO_MEMORY;
}
}
// Convert colors to physical
uint32_t physFg = colorToPhys(handle, fgColor);
uint32_t physBg = colorToPhys(handle, bgColor);
// Set up DrawMode for text
DrawMode16T *dm = (DrawMode16T *)handle->drawModeLinear;
dm->rop2 = R2_COPYPEN;
dm->bkMode = opaque ? BM_OPAQUE : BM_TRANSPARENT;
dm->bkColor = physBg;
dm->textColor = physFg;
dm->lbkColor = bgColor;
dm->ltextColor = fgColor;
dm->tBreakExtra = 0;
dm->breakExtra = 0;
dm->breakErr = 0;
dm->breakRem = 0;
dm->breakCount = 0;
dm->charExtra = 0;
// Copy the string and clip rect into a 16-bit accessible block.
// The driver always dereferences lpClipRect (no NULL check), so we
// must provide a valid rectangle covering the full screen.
uint16_t clipOff = (uint16_t)((length + 1) & ~1); // word-align
uint32_t blockSize = (uint32_t)clipOff + 8; // + RECT (4 WORDs)
uint32_t strLinear;
uint16_t strSel = alloc16BitBlock(blockSize, &strLinear);
if (strSel == 0) {
return WDRV_ERR_NO_MEMORY;
}
memcpy((void *)strLinear, text, length);
// Write clip rect after the string (left, top, right, bottom)
int16_t *clipRect = (int16_t *)(strLinear + clipOff);
clipRect[0] = 0;
clipRect[1] = 0;
clipRect[2] = 0x7FFF;
clipRect[3] = 0x7FFF;
// DWORD ExtTextOut(
// LPPDEVICE lpDestDev, 2w
// WORD x, 1w
// WORD y, 1w
// LPRECT lpClipRect, 2w
// LPSTR lpString, 2w
// int count, 1w
// LPFONTINFO lpFont, 2w fontSel:0x42
// LPDRAWMODE lpDrawMode, 2w
// LPTEXTXFORM lpTextXForm,2w NULL
// LPSHORT lpCharWidths, 2w NULL
// LPRECT lpOpaqueRect, 2w NULL
// WORD wOptions 1w
// )
// Total: 20 words (Pascal order, left to right)
uint16_t dgSel = handle->neMod.autoDataSel;
uint16_t params[20];
int i = 0;
params[i++] = dgSel; // lpDestDev seg
params[i++] = handle->pdevOff; // lpDestDev off
params[i++] = (uint16_t)x; // x
params[i++] = (uint16_t)(y + handle->dispYOffset); // y
params[i++] = strSel; // lpClipRect seg
params[i++] = clipOff; // lpClipRect off
params[i++] = strSel; // lpString seg
params[i++] = 0; // lpString off
params[i++] = (uint16_t)length; // count
params[i++] = handle->fontSel; // lpFont seg
params[i++] = 0x0042; // lpFont off (fsType)
params[i++] = dgSel; // lpDrawMode seg
params[i++] = handle->drawModeOff; // lpDrawMode off
params[i++] = 0; // lpTextXForm seg (NULL)
params[i++] = 0; // lpTextXForm off
params[i++] = 0; // lpCharWidths seg (NULL)
params[i++] = 0; // lpCharWidths off
params[i++] = 0; // lpOpaqueRect seg (NULL)
params[i++] = 0; // lpOpaqueRect off
params[i++] = 0; // wOptions
dbg("windrv: ExtTextOut(%d,%d) len=%d font=%04X:0042 dm=%04X:%04X fg=0x%08lX bg=0x%08lX %s\n",
x, y, length, handle->fontSel, dgSel, handle->drawModeOff,
(unsigned long)physFg, (unsigned long)physBg,
opaque ? "OPAQUE" : "TRANSPARENT");
waitForEngine();
uint32_t result = thunkCall16(&gThunkCtx,
handle->ddiEntry[DDI_ORD_EXTTEXTOUT].sel,
handle->ddiEntry[DDI_ORD_EXTTEXTOUT].off,
params, i);
waitForEngine();
free16BitBlock(strSel, strLinear);
dbg("windrv: ExtTextOut result=0x%08lX\n", (unsigned long)result);
// DX bit 15 set = DDI error. AX=0 is not necessarily failure.
return (result & 0x80000000) ? WDRV_ERR_UNSUPPORTED : WDRV_OK;
}
// ============================================================================
// Framebuffer access
// ============================================================================
@ -2586,6 +2719,128 @@ static void freeDrawObjects(struct WdrvDriverS *drv)
// Objects are embedded in DGROUP - freed when module is unloaded
drv->brushRealized = false;
drv->penRealized = false;
// Font is a separate 16-bit block
if (drv->fontInitialized) {
free16BitBlock(drv->fontSel, drv->fontLinear);
drv->fontSel = 0;
drv->fontLinear = 0;
drv->fontInitialized = false;
}
}
// Build a .FNT v3 font structure from the VGA BIOS 8x16 ROM font.
//
// v3 layout offsets:
// 0x0000: FntHeader16T (117 bytes)
// 0x0075: pad byte
// 0x0076: v3 extension (fsFlags..fsReserved, 30 bytes)
// 0x0094: v3 character table (257 entries x 6 bytes = 1542)
// 0x069A: bitmap data (256 chars x 16 rows = 4096)
// 0x169A: device name "\0"
// 0x169B: face name "System\0"
#define FONT_V3EXT_OFF 0x0076
#define FONT_CHAR_TABLE_OFF 0x0094
#define FONT_BITMAP_OFF 0x069A
#define FONT_DEVNAME_OFF 0x169A
#define FONT_FACENAME_OFF 0x169B
#define FONT_TOTAL_SIZE 0x16A2
static bool initFont(struct WdrvDriverS *drv)
{
// Get VGA BIOS 8x16 font pointer via INT 10h AH=11h AL=30h BH=06
__dpmi_regs regs;
memset(&regs, 0, sizeof(regs));
regs.x.ax = 0x1130;
regs.x.bx = 0x0600;
__dpmi_int(0x10, &regs);
uint32_t fontRmAddr = ((uint32_t)regs.x.es << 4) + regs.x.bp;
// Copy 4096 bytes of glyph data (256 chars x 16 bytes each)
uint8_t vgaFont[4096];
dosmemget(fontRmAddr, 4096, vgaFont);
dbg("windrv: initFont: VGA 8x16 font at real %04X:%04X (linear 0x%08lX)\n",
regs.x.es, regs.x.bp, (unsigned long)fontRmAddr);
// Allocate a 16-bit accessible block for the .FNT v3 structure
uint32_t linearOut;
uint16_t sel = alloc16BitBlock(FONT_TOTAL_SIZE, &linearOut);
if (sel == 0) {
logErr("windrv: initFont: failed to allocate font block\n");
return false;
}
uint8_t *base = (uint8_t *)linearOut;
memset(base, 0, FONT_TOTAL_SIZE);
// Fill the .FNT v3 header (same base structure as v2)
FntHeader16T *hdr = (FntHeader16T *)base;
hdr->fsVersion = 0x0300;
hdr->fsSize = FONT_TOTAL_SIZE;
hdr->fsType = 0; // raster
hdr->fsPoints = 16;
hdr->fsVertRes = 96;
hdr->fsHorizRes = 96;
hdr->fsAscent = 14;
hdr->fsInternalLeading = 2;
hdr->fsExternalLeading = 0;
hdr->fsItalic = 0;
hdr->fsUnderline = 0;
hdr->fsStrikeOut = 0;
hdr->fsWeight = 400; // normal
hdr->fsCharSet = 255; // OEM
hdr->fsPixWidth = 8; // fixed width
hdr->fsPixHeight = 16;
hdr->fsPitchAndFamily = 0x30; // FF_MODERN | FIXED_PITCH
hdr->fsAvgWidth = 8;
hdr->fsMaxWidth = 8;
hdr->fsFirstChar = 0;
hdr->fsLastChar = 255;
hdr->fsDefaultChar = 0; // relative to fsFirstChar
hdr->fsBreakChar = 32; // relative to fsFirstChar
hdr->fsWidthBytes = 1; // row stride: 1 byte per row per char
hdr->fsDevice = FONT_DEVNAME_OFF;
hdr->fsFace = FONT_FACENAME_OFF;
hdr->fsBitsPointer = ((uint32_t)sel << 16) | FONT_BITMAP_OFF;
hdr->fsBitsOffset = FONT_BITMAP_OFF;
// v3 extension fields at 0x76 are already zeroed (fsFlags, fsAspace,
// fsBspace, fsCspace, fsColorPointer, fsReserved)
// Build v3 character table (257 entries x 6 bytes: 256 chars + sentinel)
// v3 entries have a DWORD absolute offset from segment base.
// Glyph data is per-character contiguous: char C's 16 rows at
// FONT_BITMAP_OFF + C*16.
FntCharEntry30T *charTable = (FntCharEntry30T *)(base + FONT_CHAR_TABLE_OFF);
for (int c = 0; c <= 256; c++) {
charTable[c].width = 8;
charTable[c].offset = FONT_BITMAP_OFF + (uint32_t)(c < 256 ? c : 0) * 16;
}
// Copy VGA ROM glyphs directly — already in per-character contiguous
// format (16 consecutive bytes per character, stride=1 between rows)
memcpy(base + FONT_BITMAP_OFF, vgaFont, 4096);
// Device name (empty string)
base[FONT_DEVNAME_OFF] = '\0';
// Face name
memcpy(base + FONT_FACENAME_OFF, "System", 7);
drv->fontSel = sel;
drv->fontLinear = linearOut;
drv->fontSize = FONT_TOTAL_SIZE;
drv->fontInitialized = true;
dbg("windrv: initFont: v3 font block sel=%04X linear=0x%08lX size=%u\n",
sel, (unsigned long)linearOut, FONT_TOTAL_SIZE);
dbg("windrv: initFont: fsBitsPointer=%08lX charTable@0x%04X bitmap@0x%04X\n",
(unsigned long)hdr->fsBitsPointer, FONT_CHAR_TABLE_OFF, FONT_BITMAP_OFF);
return true;
}

View file

@ -148,6 +148,13 @@ int32_t wdrvPolyline(WdrvHandleT handle, Point16T *points, int16_t count, uint32
// Draw a rectangle outline.
int32_t wdrvRectangle(WdrvHandleT handle, int16_t x, int16_t y, int16_t w, int16_t h, uint32_t color);
// Draw text using the ExtTextOut DDI function.
// Uses a built-in 8x16 VGA bitmap font.
int32_t wdrvExtTextOut(WdrvHandleT handle, int16_t x, int16_t y,
const char *text, int16_t length,
uint32_t fgColor, uint32_t bgColor,
bool opaque);
// ============================================================================
// Palette operations (for 8bpp modes)
// ============================================================================