Add README.md and expand code comments across all source files

Add comprehensive README covering architecture, API usage, build
instructions, tested drivers, binary patching details, and DGROUP
layout. Expand file header comments in all library sources and headers
to document module responsibilities, data flow, and key constraints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Duensing 2026-02-21 18:46:05 -06:00
parent 431f573422
commit 847db7586b
11 changed files with 445 additions and 22 deletions

View file

@ -1,5 +1,17 @@
# ============================================================================ # ============================================================================
# Makefile for windrv demo - Windows 3.x display driver loader for DJGPP/DOS # Makefile for windrv demo - Windows 3.x display driver loader for DJGPP/DOS
#
# Targets:
# all - Build libwindrv.a and demo.exe (default)
# lib - Build only libwindrv.a
# demo - Build only demo.exe
# clean - Remove all build artifacts
#
# Output:
# bin/demo.exe - Demo program
# bin/CWSDPMI.EXE - DPMI host (extracted from tools/cwsdpmi.zip)
# bin/*.DRV - Driver files (copied from drivers/)
# win31drv/libwindrv.a - Static library
# ============================================================================ # ============================================================================
DJGPP_PREFIX ?= $(HOME)/djgpp/djgpp DJGPP_PREFIX ?= $(HOME)/djgpp/djgpp

277
README.md Normal file
View file

@ -0,0 +1,277 @@
# 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
```c
#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
```c
// 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.

View file

@ -1,5 +1,8 @@
# ============================================================================ # ============================================================================
# Makefile for win31drv - Windows 3.x display driver library for DJGPP/DOS # Makefile for win31drv - Windows 3.x display driver library for DJGPP/DOS
#
# Builds libwindrv.a from: log.c neload.c thunk.c winstub.c windrv.c
# windrv.c is compiled with -fno-gcse (see WINDRV_CFLAGS below).
# ============================================================================ # ============================================================================
DJGPP_PREFIX ?= $(HOME)/djgpp/djgpp DJGPP_PREFIX ?= $(HOME)/djgpp/djgpp

View file

@ -1,5 +1,11 @@
// ============================================================================ // ============================================================================
// log.c - Logging to file // log.c - Logging to file
//
// Simple dual-output logger: messages go to a log file (opened by
// logInit) and optionally to stdout (logMsg) or stderr (logErr).
// An optional pre-I/O hook is called before every write, used by
// windrv.c to ensure the S3 engine is idle before file I/O (DOSBox-X
// processes S3 operations during file writes).
// ============================================================================ // ============================================================================
#include <stdio.h> #include <stdio.h>

View file

@ -1,6 +1,27 @@
#ifndef NEFORMAT_H #ifndef NEFORMAT_H
#define NEFORMAT_H #define NEFORMAT_H
// ============================================================================
// neformat.h - NE (New Executable) binary format structures
//
// Defines the on-disk structures for the NE executable format used by
// Windows 3.x 16-bit DLLs and drivers. An NE file has a DOS MZ stub
// header at offset 0, with a pointer at offset 0x3C to the NE header.
//
// The NE header contains:
// - Module metadata (flags, target OS, expected Windows version)
// - Offsets to the segment table, entry table, name tables, etc.
// - Auto-data segment index (DGROUP) and entry point CS:IP
//
// Each segment table entry describes one code or data segment with its
// file offset, size, flags, and minimum allocation size. Relocation
// records follow each segment's data in the file and describe fixups
// for inter-segment references, imported functions, and OS fixups.
//
// Also defines the standard DDI (Device Driver Interface) ordinal
// numbers exported by Windows 3.x display drivers.
// ============================================================================
#include <stdint.h> #include <stdint.h>
// ============================================================================ // ============================================================================

View file

@ -1,8 +1,28 @@
// ============================================================================ // ============================================================================
// neload.c - NE (New Executable) format loader // neload.c - NE (New Executable) format loader
// //
// Loads Windows 3.x 16-bit DLLs/drivers into protected mode memory // Loads Windows 3.x 16-bit DLLs/drivers (.DRV files) into protected
// using DPMI to allocate LDT descriptors for 16-bit code/data segments. // mode memory. Each NE segment gets a DPMI LDT descriptor (16-bit
// code or data), and the segment data is loaded from the file.
//
// Loading steps:
// 1. Read and validate MZ (DOS stub) + NE headers
// 2. Parse the module reference table (imported DLL names)
// 3. Parse the resident name table (module name + named exports)
// 4. Parse the entry table (ordinal -> segment:offset mappings)
// 5. Allocate memory and LDT descriptors for each segment
// 6. Load segment data from the file
// 7. Process relocations (internal refs, imports by ordinal/name)
//
// Import resolution is delegated to a caller-supplied callback
// (ImportResolverT) which returns a 16:16 far pointer for each
// imported function. If the callback returns FARPTR16_NULL, the
// loader patches in a stub address and logs a warning.
//
// Relocations are applied as fixups to the loaded segment data.
// Supported fixup types: LOBYTE, SEGMENT, FAR_ADDR (seg:off),
// OFFSET, and additive fixups. Chained relocations (linked lists
// within the segment) are followed to completion.
// ============================================================================ // ============================================================================
#include <stdio.h> #include <stdio.h>

View file

@ -1,11 +1,30 @@
#ifndef WINDDI_H #ifndef WINDDI_H
#define WINDDI_H #define WINDDI_H
// ============================================================================
// winddi.h - Windows 3.x Display Driver Interface (DDI) structures
//
// These packed structures match the binary layout expected by 16-bit
// Windows display drivers. They are used to communicate between the
// 32-bit host program and the 16-bit driver code via shared memory
// in DGROUP.
//
// Key structures:
// GdiInfo16T - Device capabilities, filled by Enable(style=1)
// PDevice16T - Physical device descriptor, filled by Enable(style=0)
// DrawMode16T - Drawing parameters (ROP2, colors, text spacing)
// LogBrush16T - Logical brush input to RealizeObject
// LogPen16T - Logical pen input to RealizeObject
// DibPDevice16T - DIB engine PDEVICE extension (for software drivers)
//
// All structures use __attribute__((packed)) to match Win16 layout.
// ============================================================================
#include "wintypes.h" #include "wintypes.h"
// ============================================================================ // ============================================================================
// GDIINFO - Device capabilities structure filled by Enable() // GDIINFO - Device capabilities structure filled by Enable(style=1)
// This is the 16-bit Windows 3.1 DDK GDIINFO structure. // This is the 16-bit Windows 3.1 DDK GDIINFO structure (0x6C bytes).
// ============================================================================ // ============================================================================
typedef struct __attribute__((packed)) { typedef struct __attribute__((packed)) {

View file

@ -4,6 +4,25 @@
// Implements the public windrv.h API by coordinating the NE loader, // Implements the public windrv.h API by coordinating the NE loader,
// thunking layer, and Windows API stubs to load and use Windows 3.x // thunking layer, and Windows API stubs to load and use Windows 3.x
// display drivers from DOS programs compiled with DJGPP. // display drivers from DOS programs compiled with DJGPP.
//
// This module handles:
// - Driver loading: NE module loading, DDI entry point resolution,
// binary patching of prologs/epilogs and DPMI workarounds
// - Mode setting: two-phase Enable (InquireInfo then EnableDevice),
// VRAM mapping, S3 hardware detection and setup
// - Drawing: DDI call wrappers for BitBlt, Output, Pixel, ColorInfo,
// RealizeObject (brush/pen), and SetPalette
// - GDI object management: PDEVICE, physical brush/pen, draw mode,
// and physical color buffers are embedded within DGROUP so all far
// pointers share the driver's auto-data selector
// - Interrupt handlers: INT 10h reflector (PM -> real-mode BIOS),
// INT 64h proxy (DPMI 0x300h workaround), INT 2Fh (Enhanced Mode
// check), and exception capture (#GP, #PF diagnostics)
//
// Build note: this file is compiled with -fno-gcse to prevent GCC from
// placing stack temporaries at addresses that overlap with memory
// corrupted during 16-bit driver calls via thunkCall16. See
// WINDRV_CFLAGS in win31drv/Makefile.
// ============================================================================ // ============================================================================
#include <stdio.h> #include <stdio.h>

View file

@ -2,12 +2,35 @@
// winstub.c - Windows API function stubs // winstub.c - Windows API function stubs
// //
// Provides minimal implementations of KERNEL, GDI, USER, and DIBENG // Provides minimal implementations of KERNEL, GDI, USER, and DIBENG
// functions that Windows 3.x display drivers import. Each stub is // functions that Windows 3.x display drivers import. Each stub is
// registered as a 16-bit callback via the thunking layer. // registered as a 16-bit callback via thunkRegisterCallback(), giving
// it a 16:16 far pointer that the NE loader patches into the driver's
// relocation fixups.
// //
// These stubs implement just enough behavior for a display driver to // These stubs implement just enough behavior for a display driver to
// initialize and perform basic drawing operations. Many stubs simply // initialize and perform basic drawing operations:
// return success without doing real work. //
// KERNEL (~50 stubs):
// - Memory: GlobalAlloc/Lock/Unlock/Free/Realloc/Size,
// GlobalDOSAlloc/Free (bump-allocated from a pre-allocated pool),
// LocalAlloc/Lock/Free
// - Selectors: AllocSelector, FreeSelector, AllocCStoDSAlias,
// AllocDSToCSAlias, PrestoChangoSelector, Set/GetSelectorBase,
// Set/GetSelectorLimit, SelectorAccessRights
// - System: GetVersion (3.10), GetWinFlags (Enhanced + 386),
// GetProfileInt, GetPrivateProfileInt/String (returns SYSTEM.INI
// values for display driver configuration)
// - Module: GetModuleHandle, GetProcAddress, LoadLibrary
// - Variable imports: __WINFLAGS, __AHSHIFT, __AHINCR, __A000H,
// __B000H, __B800H, __0040H, etc. (resolved as selector values)
//
// GDI (~8 stubs): GetDeviceCaps, CreateDC, DeleteDC, SelectObject
// USER (2 stubs): GetSystemMetrics, MessageBox
// DIBENG (9 stubs): DIBBitBlt, DIBOutput, DIBPixel, etc. (return 0
// so the driver falls back to its own implementation)
// KEYBOARD (1 stub): ScreenSwitchEnable (no-op)
//
// Stubs that are not implemented return 0 via stubDummy.
// ============================================================================ // ============================================================================
#include <stdio.h> #include <stdio.h>

View file

@ -1,26 +1,36 @@
#ifndef WINSTUB_H #ifndef WINSTUB_H
#define WINSTUB_H #define WINSTUB_H
// ============================================================================
// winstub.h - Windows API stub layer
//
// Declares the stub context, initialization/shutdown, and import
// resolution functions. Also defines the ordinal numbers for all
// stubbed KERNEL, GDI, USER, KEYBOARD, and DIBENG functions.
//
// The stub context (StubContextT) tracks:
// - GlobalAlloc memory blocks and their selectors
// - Extra selectors from AllocSelector/AllocCStoDSAlias
// - A pre-allocated DOS memory pool for GlobalDOSAlloc
// - Well-known memory region selectors (__A000H, __0040H, etc.)
// - A lookup table mapping module+ordinal to 16-bit far pointers
//
// Import resolution flow:
// 1. NE loader encounters an imported reference
// 2. Calls importResolver -> stubResolveImport
// 3. For variable imports (__WINFLAGS, __A000H, etc.): returns the
// value directly (selector or constant) in the far pointer
// 4. For function imports: looks up the stub table for a matching
// module+ordinal entry and returns the 16-bit callback address
// 5. Unknown imports: logs a warning and returns FARPTR16_NULL
// ============================================================================
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include "wintypes.h" #include "wintypes.h"
#include "thunk.h" #include "thunk.h"
#include "neload.h" #include "neload.h"
// ============================================================================
// Windows API stub layer
//
// Provides minimal implementations of KERNEL, GDI, and USER functions
// that Windows 3.x display drivers import. These stubs are registered as
// 16-bit callbacks via the thunking layer so the driver can call them.
//
// Supported modules:
// KERNEL - Memory management (GlobalAlloc/Lock/Free), module queries,
// selector management, system info
// GDI - Minimal DC management, palette, object stubs
// USER - GetSystemMetrics, MessageBox (stub)
// ============================================================================
// ============================================================================ // ============================================================================
// Stub context // Stub context
// ============================================================================ // ============================================================================

View file

@ -1,6 +1,19 @@
#ifndef WINTYPES_H #ifndef WINTYPES_H
#define WINTYPES_H #define WINTYPES_H
// ============================================================================
// wintypes.h - Windows 16-bit type definitions and constants
//
// Provides the fundamental types used by the Win16 API and DDI:
// basic integer types (WORD, DWORD, BOOL16), 16-bit handle types,
// far pointer (segment:offset) helpers, COLORREF, geometry types
// (Point16T, Rect16T), GetWinFlags constants, GlobalAlloc flags,
// and raster operation codes (ROP2 and ROP3).
//
// Uses stdint.h types for portability (DJGPP's uint32_t is unsigned
// long, not unsigned int).
// ============================================================================
#include <stdint.h> #include <stdint.h>
// ============================================================================ // ============================================================================