Enable usage of Windows 3.1 accelerated video drivers in DJGPP DOS applications.
Find a file
Scott Duensing fbb1cce5c3 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>
2026-02-22 00:38:16 -06:00
.claude Initial commit. 2026-02-21 18:01:54 -06:00
drivers Initial commit. 2026-02-21 18:01:54 -06:00
fon Add loadable font support: .FON/.FNT loading with v2→v3 conversion 2026-02-21 23:04:30 -06:00
tools Initial commit. 2026-02-21 18:01:54 -06:00
ttf Add TrueType font support via stb_truetype.h 2026-02-22 00:38:16 -06:00
vbesvga.drv@2da3782d3f Initial commit. 2026-02-21 18:01:54 -06:00
win31drv Add TrueType font support via stb_truetype.h 2026-02-22 00:38:16 -06:00
.gitattributes Add TrueType font support via stb_truetype.h 2026-02-22 00:38:16 -06:00
.gitignore Initial commit. 2026-02-21 18:01:54 -06:00
.gitmodules Initial commit. 2026-02-21 18:01:54 -06:00
CLAUDE.md Add TrueType font support via stb_truetype.h 2026-02-22 00:38:16 -06:00
demo.c Add TrueType font support via stb_truetype.h 2026-02-22 00:38:16 -06:00
dosbox-x.conf Implement ExtTextOut DDI wrapper with built-in VGA ROM font 2026-02-21 21:21:23 -06:00
Makefile Add TrueType font support via stb_truetype.h 2026-02-22 00:38:16 -06:00
README.md Add README.md and expand code comments across all source files 2026-02-21 18:46:05 -06:00
run.sh Add loadable font support: .FON/.FNT loading with v2→v3 conversion 2026-02-21 23:04:30 -06:00

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 with DJGPP_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 library
  • bin/demo.exe -- the demo program
  • bin/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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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_0 sequences (mov ax, ds; nop; inc bp) to load the correct DGROUP selector and removes the inc bp/dec bp frame 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, 0040h with 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_ENHANCED to WF_STANDARD after 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.