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> |
||
|---|---|---|
| .claude | ||
| drivers | ||
| fon | ||
| tools | ||
| ttf | ||
| vbesvga.drv@2da3782d3f | ||
| win31drv | ||
| .gitattributes | ||
| .gitignore | ||
| .gitmodules | ||
| CLAUDE.md | ||
| demo.c | ||
| dosbox-x.conf | ||
| Makefile | ||
| README.md | ||
| run.sh | ||
win31drv
A library and demo for loading and using real Windows 3.x display drivers (.DRV files) from 32-bit DOS programs compiled with DJGPP.
Windows 3.1 shipped with hardware-accelerated display drivers for chipsets like the S3 Trio64 and Tseng ET4000. These drivers are 16-bit NE-format DLLs that implement the Windows DDI (Device Driver Interface). This project loads those drivers into protected mode memory, thunks between 32-bit and 16-bit code, stubs the Windows API functions the drivers import, and exposes a clean C API for drawing.
Tested Drivers
| Driver | Chipset | Resolution | DOSBox-X machine type |
|---|---|---|---|
| S3TRIO.DRV | S3 Trio64 | 800x600 8bpp | svga_s3trio64 |
| VBESVGA.DRV | VESA VBE 2.0 | 800x600 8bpp | svga_s3trio64 |
| VGA.DRV | Standard VGA | 640x480 4-plane | svga_s3trio64 |
| ET4000.DRV | Tseng ET4000 | 640x480 8bpp | svga_et4000 |
Building
Prerequisites
- DJGPP cross-compiler: GCC 12.2.0 targeting
i586-pc-msdosdjgpp. Expected at~/djgpp/djgpp/(override withDJGPP_PREFIX). - CWSDPMI: Included in
tools/cwsdpmi.zip, extracted automatically. - DOSBox-X (for testing):
flatpak run com.dosbox_x.DOSBox-X
Build
make # Builds libwindrv.a and demo.exe
make clean # Removes all build artifacts
The build produces:
win31drv/libwindrv.a-- the librarybin/demo.exe-- the demo programbin/CWSDPMI.EXE-- DPMI host (required to run the demo)
Driver files from drivers/ are copied to bin/ during the build.
Running
Start DOSBox-X with the included configuration:
flatpak run com.dosbox_x.DOSBox-X -conf dosbox-x.conf
Or run manually inside DOSBox-X:
C:\BIN> demo.exe S3TRIO.DRV
C:\BIN> demo.exe -d VBESVGA.DRV (with debug logging)
Output is logged to OUTPUT.LOG in the current directory.
API Overview
The public API is declared in win31drv/windrv.h.
Lifecycle
#include "win31drv/windrv.h"
wdrvInit(); // Initialize library
WdrvHandleT drv = wdrvLoadDriver("S3TRIO.DRV");
wdrvEnable(drv, 0, 0, 0); // Set video mode (0 = driver defaults)
// ... draw ...
wdrvDisable(drv); // Restore text mode
wdrvUnloadDriver(drv); // Free driver resources
wdrvShutdown(); // Shut down library
Drawing
// Solid rectangle fill (BitBlt with PATCOPY)
wdrvFillRect(drv, x, y, w, h, MAKE_RGB(255, 0, 0));
// Set/get individual pixels
wdrvSetPixel(drv, x, y, MAKE_RGB(0, 255, 0));
uint32_t color = wdrvGetPixel(drv, x, y);
// Polyline (Output DDI)
Point16T pts[] = {{0, 0}, {100, 100}};
wdrvPolyline(drv, pts, 2, MAKE_RGB(0, 0, 255));
// Screen-to-screen blit
WdrvBitBltParamsT bp = { .srcX=0, .srcY=0, .dstX=320, .dstY=240,
.width=160, .height=120, .rop3=SRCCOPY };
wdrvBitBlt(drv, &bp);
// Direct framebuffer access
void *fb = wdrvGetFramebuffer(drv);
int32_t pitch = wdrvGetPitch(drv);
Error Handling
All functions return WDRV_OK (0) on success or a negative error code.
Use wdrvGetLastErrorString() for a human-readable description.
Architecture
Component Overview
demo.c User program
|
v
win31drv/windrv.c Public API, DDI call wrappers, interrupt handlers
|
+-- win31drv/neload.c NE executable loader (segments, relocations, exports)
+-- win31drv/thunk.c 32-bit <-> 16-bit thunking via DPMI
+-- win31drv/winstub.c Windows API stubs (KERNEL, GDI, USER, DIBENG)
+-- win31drv/log.c Logging to file
How It Works
-
NE Loading (
neload.c): The driver .DRV file is a 16-bit NE (New Executable) format DLL. The loader reads the MZ+NE headers, allocates 16-bit LDT descriptors for each segment via DPMI, loads segment data from the file, and processes relocation records to fix up inter-segment references and imported function addresses. -
Import Resolution (
winstub.c): Windows 3.x drivers import functions from KERNEL, GDI, USER, and sometimes DIBENG. The stub layer provides minimal implementations of ~80 functions: memory management (GlobalAlloc/Lock/Free, GlobalDOSAlloc), selector management (AllocSelector, PrestoChangoSelector, SetSelectorBase), system queries (GetVersion, GetWinFlags, GetPrivateProfileString), and more. Each stub is registered as a 16-bit callback via the thunking layer. -
Thunking (
thunk.c): The 32-to-16 bit thunking layer bridges DJGPP's 32-bit flat-memory environment with the driver's 16-bit segmented code. A small relay thunk in a 16-bit code segment handles the transition: it receives parameters via a shared data area, sets up a 16-bit stack with DS=SS=DGROUP, far-calls the target function (Pascal calling convention), and captures the DX:AX return value. For callbacks (16-bit driver calling 32-bit stubs), a software interrupt mechanism routes through a registered INT handler that switches to a 32-bit stack and invokes the C callback. -
DDI Wrappers (
windrv.c): The main module wraps each DDI function (Enable, Disable, BitBlt, Output, Pixel, RealizeObject, ColorInfo, SetPalette) with proper parameter marshalling. GDI objects (PDEVICE, physical brush, physical pen, draw mode) are embedded within the driver's DGROUP segment so all far pointers share the same selector, matching how Windows 3.1's GDI heap works. -
Interrupt Handlers (
windrv.c): Several raw assembly interrupt handlers bridge the gap between the driver's expectations and the DOS environment:- INT 10h reflector: Intercepts the driver's video BIOS calls, translates PM selectors to real-mode segments (with transfer buffer bouncing for extended memory), and reflects through DPMI to the real-mode BIOS.
- INT 64h proxy (DPMI 0x300h): Works around a CWSDPMI bug where INT 31h AX=0300h fails from 16-bit code segments. The driver's DoInt10h is patched to use INT 64h instead.
- INT 2Fh handler: Responds to the Windows Enhanced Mode installation check (AX=1600h) and VDS/VMM API calls that the driver issues during initialization.
- Exception handlers (#GP, #PF): Capture fault state with full register dumps before DJGPP's handler (which may crash on faults from 16-bit code).
Binary Patching
The loader applies several patches to the driver's code segments at load time to adapt them for the DOS/DPMI environment:
-
Prolog/epilog patching: Converts Win16
PROLOG_0sequences (mov ax, ds; nop; inc bp) to load the correct DGROUP selector and removes theinc bp/dec bpframe markers that corrupt frame pointers outside of Windows' stack walker. -
DoInt10h INT 31h -> INT 64h: Redirects the driver's DPMI simulate-real-mode-interrupt call to our proxy handler.
-
BIOS data area access: Replaces hardcoded
mov ax, 0040hwith our DPMI selector for the BIOS data area (real-mode segment 0x0040 is invalid in protected mode). -
VFLATD bypass: Forces the DPMI linear framebuffer path instead of the VFLATD VxD path (which requires Windows).
-
VFLATD stack bug: Patches a 20-byte stack imbalance in the VFLATD initialization subroutine.
-
WinFlags repatching: For VGA-class drivers, changes
WF_ENHANCEDtoWF_STANDARDafter Enable reveals the driver type, preventing a hang in the VDD polling loop.
DGROUP Layout
Windows 3.x drivers expect all GDI objects to reside in DGROUP (the automatic data segment), and SS=DS=DGROUP during function execution. The library extends DGROUP to 64K and allocates objects at fixed offsets:
Original driver data: 0x0000 - varies
GDI objects:
PDEVICE: objBase + 0x0000 (4096 bytes)
Physical brush: objBase + 0x1000 (128 bytes)
Logical brush: objBase + 0x1080 (16 bytes)
Draw mode: objBase + 0x1090 (48 bytes)
Physical pen: objBase + 0x10C0 (128 bytes)
Logical pen: objBase + 0x1140 (16 bytes)
Physical color: objBase + 0x1150 (8 bytes)
S3-Specific Handling
The S3 Trio64 driver writes an 8x8 dithered brush pattern to a fixed VRAM location during accelerated pattern fills. The library detects S3 hardware by probing CR30 (chip ID register) and shifts the CRTC display start down 10 scanlines so this scratch area is off-screen. All drawing Y coordinates are offset to compensate. The GP_STAT register (port 0x9AE8) is polled to wait for the graphics engine to become idle between operations.
File Reference
Library (win31drv/)
| File | Lines | Description |
|---|---|---|
windrv.h |
189 | Public API: initialization, driver loading, drawing, errors |
windrv.c |
3223 | DDI wrappers, GDI object management, interrupt handlers, binary patching |
neload.h |
118 | NE loader API |
neload.c |
962 | NE format parser: headers, segments, relocations, exports |
neformat.h |
215 | NE/MZ header structures, segment/relocation/entry table formats, DDI ordinals |
thunk.h |
133 | Thunk layer API |
thunk.c |
1021 | 32/16-bit relay, callback dispatch, DOS memory management |
winstub.h |
257 | Stub declarations, KERNEL/GDI/USER/DIBENG ordinal constants |
winstub.c |
1400 | ~80 Windows API stub implementations |
winddi.h |
261 | DDI structures: GDIINFO, PDEVICE, DRAWMODE, brushes, pens |
wintypes.h |
153 | Win16 basic types, far pointers, colors, ROP codes |
log.h |
37 | Logging API |
log.c |
83 | Log file output |
Demo and Build
| File | Description |
|---|---|
demo.c |
Demo program: loads driver, draws rectangles/pixels/lines/blits |
Makefile |
Top-level build: compiles demo, links with libwindrv.a |
win31drv/Makefile |
Library build: compiles sources into libwindrv.a |
dosbox-x.conf |
DOSBox-X configuration for S3 Trio64 testing |
tools/TEST.BAT |
Quick-test batch file for DOSBox-X |
drivers/*.DRV |
Windows 3.x display driver binaries |
tools/cwsdpmi.zip |
CWSDPMI DPMI host (extracted during build) |
tools/lib/libfl.so.2 |
Shared library needed by DJGPP binutils |
Known Issues
- Mode mismatch: The S3 driver configures 800x600 hardware but GDIINFO reports 640x480. Drawing works within the 640x480 region.
- ET4000 is software-only: DOSBox-X does not emulate the ET4000 accelerator engine; all drawing is CPU-rendered by the driver.
- No text output: ExtTextOut is not yet wired up.
- Single driver instance: Only one driver can be loaded at a time.
License
The library code in this repository is original work. The Windows 3.x
display driver binaries in drivers/ are the property of their respective
copyright holders and are included for interoperability testing purposes.