diff --git a/Makefile b/Makefile index 945d02b..17b09e8 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,17 @@ # ============================================================================ # 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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..c10e0cb --- /dev/null +++ b/README.md @@ -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. diff --git a/win31drv/Makefile b/win31drv/Makefile index 7d790d0..92e6f78 100644 --- a/win31drv/Makefile +++ b/win31drv/Makefile @@ -1,5 +1,8 @@ # ============================================================================ # 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 diff --git a/win31drv/log.c b/win31drv/log.c index 3dee254..bb7edf4 100644 --- a/win31drv/log.c +++ b/win31drv/log.c @@ -1,5 +1,11 @@ // ============================================================================ // 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 diff --git a/win31drv/neformat.h b/win31drv/neformat.h index a4796f6..eb441cd 100644 --- a/win31drv/neformat.h +++ b/win31drv/neformat.h @@ -1,6 +1,27 @@ #ifndef 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 // ============================================================================ diff --git a/win31drv/neload.c b/win31drv/neload.c index e86ff92..b038d87 100644 --- a/win31drv/neload.c +++ b/win31drv/neload.c @@ -1,8 +1,28 @@ // ============================================================================ // neload.c - NE (New Executable) format loader // -// Loads Windows 3.x 16-bit DLLs/drivers into protected mode memory -// using DPMI to allocate LDT descriptors for 16-bit code/data segments. +// Loads Windows 3.x 16-bit DLLs/drivers (.DRV files) into protected +// 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 diff --git a/win31drv/winddi.h b/win31drv/winddi.h index 3ab2fea..11a4390 100644 --- a/win31drv/winddi.h +++ b/win31drv/winddi.h @@ -1,11 +1,30 @@ #ifndef 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" // ============================================================================ -// GDIINFO - Device capabilities structure filled by Enable() -// This is the 16-bit Windows 3.1 DDK GDIINFO structure. +// GDIINFO - Device capabilities structure filled by Enable(style=1) +// This is the 16-bit Windows 3.1 DDK GDIINFO structure (0x6C bytes). // ============================================================================ typedef struct __attribute__((packed)) { diff --git a/win31drv/windrv.c b/win31drv/windrv.c index 2aeb09a..812c28d 100644 --- a/win31drv/windrv.c +++ b/win31drv/windrv.c @@ -4,6 +4,25 @@ // Implements the public windrv.h API by coordinating the NE loader, // thunking layer, and Windows API stubs to load and use Windows 3.x // 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 diff --git a/win31drv/winstub.c b/win31drv/winstub.c index 5b7853c..da91a6c 100644 --- a/win31drv/winstub.c +++ b/win31drv/winstub.c @@ -2,12 +2,35 @@ // winstub.c - Windows API function stubs // // Provides minimal implementations of KERNEL, GDI, USER, and DIBENG -// functions that Windows 3.x display drivers import. Each stub is -// registered as a 16-bit callback via the thunking layer. +// functions that Windows 3.x display drivers import. Each stub is +// 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 -// initialize and perform basic drawing operations. Many stubs simply -// return success without doing real work. +// initialize and perform basic drawing operations: +// +// 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 diff --git a/win31drv/winstub.h b/win31drv/winstub.h index 6ebd3c0..1e90dc7 100644 --- a/win31drv/winstub.h +++ b/win31drv/winstub.h @@ -1,26 +1,36 @@ #ifndef 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 #include #include "wintypes.h" #include "thunk.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 // ============================================================================ diff --git a/win31drv/wintypes.h b/win31drv/wintypes.h index 362edea..ca179aa 100644 --- a/win31drv/wintypes.h +++ b/win31drv/wintypes.h @@ -1,6 +1,19 @@ #ifndef 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 // ============================================================================