Add wdrvScreenshot() to capture the screen to PNG via stb_image_write.h, reading the framebuffer (or DDI bitmap fallback) and VGA DAC palette. Convert demo.c to non-interactive mode with automatic screenshots after each demo (DEMO01-15.PNG) and no keypress waits, plus per-driver DOSBox-X configs for automated testing. Set a standard Windows 3.1 256-color palette (8R x 8G x 4B color cube with 20 static system colors) to ensure consistent output across drivers. Fix wdrvSetPalette to also program the VGA DAC directly, since VBESVGA's SetPalette DDI updates its internal color table but not the hardware. Detect DAC width via VBE 4F08 (S3TRIO=6-bit, VBESVGA=8-bit) and use correct shift in both DAC writes and reads — fixes dark display on VBESVGA where 6-bit values in 8-bit DAC produced 1/4 brightness. Fix S3 dispYOffset: extend PDEVICE deHeight by the offset so the driver's internal clipping allows the full 600-row logical screen, rather than incorrectly reducing dpVertRes to 590. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5608 lines
190 KiB
C
5608 lines
190 KiB
C
// ============================================================================
|
|
// windrv.c - Main driver interface
|
|
//
|
|
// 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 <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdarg.h>
|
|
#include <inttypes.h>
|
|
#include <pc.h>
|
|
#include <dpmi.h>
|
|
#include <go32.h>
|
|
#include <sys/nearptr.h>
|
|
#include <sys/movedata.h>
|
|
#include <sys/farptr.h>
|
|
|
|
#include "windrv.h"
|
|
#include "wintypes.h"
|
|
#include "winddi.h"
|
|
#include "neformat.h"
|
|
#include "neload.h"
|
|
#include "thunk.h"
|
|
#include "winstub.h"
|
|
#include "log.h"
|
|
|
|
#include <math.h>
|
|
#define STB_TRUETYPE_IMPLEMENTATION
|
|
#include "stb_truetype.h"
|
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
|
#include "stb_image_write.h"
|
|
|
|
// ============================================================================
|
|
// Driver instance structure (opaque handle)
|
|
// ============================================================================
|
|
|
|
struct WdrvDriverS {
|
|
NeModuleT neMod;
|
|
char filePath[256];
|
|
|
|
// DDI entry point addresses (16-bit selector:offset)
|
|
struct {
|
|
uint16_t sel;
|
|
uint16_t off;
|
|
bool present;
|
|
} ddiEntry[DDI_MAX_ORDINAL];
|
|
|
|
// Device info from Enable (style=0) call
|
|
GdiInfo16T gdiInfo;
|
|
bool gdiInfoValid;
|
|
|
|
// GDI objects embedded within DGROUP.
|
|
// Windows 3.x drivers expect all GDI objects (PDEVICE, brush,
|
|
// drawMode) to share the same segment, because in Win3.1 they
|
|
// are all in the global GDI heap. When the driver does e.g.
|
|
// "lds si, lpBrush" it expects DS to still cover DGROUP.
|
|
// We achieve this by allocating objects at offsets within the
|
|
// DGROUP segment, so every far pointer uses autoDataSel.
|
|
uint32_t dgroupObjBase; // Start offset of object area in DGROUP
|
|
|
|
// Physical device structure (within DGROUP)
|
|
uint16_t pdevOff; // Offset within DGROUP
|
|
uint32_t pdevLinear; // Linear address for C access
|
|
uint32_t pdevSize; // Allocated size
|
|
|
|
// Logical brush (within DGROUP, input to RealizeObject)
|
|
uint16_t logBrushOff;
|
|
uint32_t logBrushLinear;
|
|
|
|
// Physical brush (within DGROUP, output of RealizeObject)
|
|
uint16_t brushOff;
|
|
uint32_t brushLinear;
|
|
uint32_t brushRealizedColor; // Color of last realized brush
|
|
bool brushRealized;
|
|
|
|
// Logical pen (within DGROUP, input to RealizeObject)
|
|
uint16_t logPenOff;
|
|
uint32_t logPenLinear;
|
|
|
|
// Physical pen (within DGROUP, output of RealizeObject)
|
|
uint16_t penOff;
|
|
uint32_t penLinear;
|
|
uint32_t penRealizedColor;
|
|
int16_t penRealizedStyle;
|
|
bool penRealized;
|
|
|
|
// Physical color (within DGROUP, output of ColorInfo)
|
|
uint16_t physColorOff;
|
|
uint32_t physColorLinear;
|
|
|
|
// Draw mode (within DGROUP)
|
|
uint16_t drawModeOff;
|
|
uint32_t drawModeLinear;
|
|
|
|
// Current state
|
|
bool enabled;
|
|
uint32_t currentColor;
|
|
|
|
// Video RAM mapping
|
|
void *vramPtr;
|
|
uint32_t vramPhysAddr;
|
|
uint32_t vramSize;
|
|
uint32_t vramLinear;
|
|
int32_t pitch;
|
|
|
|
// Display Y offset: the S3 driver writes an 8x8 color brush pattern
|
|
// to a fixed VRAM location (~(144,1)-(151,8)) during dithered fills.
|
|
// We shift the CRTC display start down by this many scanlines so the
|
|
// scratch area is off-screen, and add the offset to all Y coordinates.
|
|
int16_t dispYOffset;
|
|
bool isS3;
|
|
uint8_t dacWidth; // DAC bits per channel: 6 (standard VGA) or 8
|
|
|
|
// Hardware cursor (persistent allocation — some drivers keep a pointer)
|
|
uint16_t cursorSel;
|
|
uint32_t cursorLinear;
|
|
uint32_t cursorAllocSize;
|
|
};
|
|
|
|
// ============================================================================
|
|
// Font instance structure
|
|
// ============================================================================
|
|
|
|
struct WdrvFontS {
|
|
uint16_t fontSel;
|
|
uint32_t fontLinear;
|
|
uint32_t fontSize;
|
|
char faceName[64];
|
|
uint16_t pixHeight;
|
|
uint16_t pixWidth; // 0 = proportional
|
|
uint16_t fsVersion; // original version before conversion
|
|
};
|
|
|
|
// ============================================================================
|
|
// Global state
|
|
// ============================================================================
|
|
|
|
static ThunkContextT gThunkCtx;
|
|
static StubContextT gStubCtx;
|
|
static bool gInitialized = false;
|
|
static int32_t gLastError = WDRV_OK;
|
|
static bool gDebug = false;
|
|
static bool gIsS3 = false;
|
|
static struct WdrvFontS gBuiltinFont;
|
|
static bool gBuiltinFontInit = false;
|
|
|
|
|
|
// Forward declarations
|
|
static FarPtr16T importResolver(const char *moduleName, uint16_t ordinal, const char *funcName);
|
|
static bool resolveDriverEntries(struct WdrvDriverS *drv);
|
|
static bool extendDgroupForObjects(struct WdrvDriverS *drv);
|
|
static bool allocPDevice(struct WdrvDriverS *drv);
|
|
static bool allocDrawMode(struct WdrvDriverS *drv);
|
|
static bool allocBrushBuffers(struct WdrvDriverS *drv);
|
|
static bool allocPenBuffers(struct WdrvDriverS *drv);
|
|
static bool realizeBrush(struct WdrvDriverS *drv, uint32_t color);
|
|
static bool realizeStyledPen(struct WdrvDriverS *drv, uint32_t color, int16_t style);
|
|
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 initBuiltinFont(void);
|
|
static WdrvFontT buildFontFromFnt(const uint8_t *fntData, uint32_t fntSize);
|
|
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);
|
|
static void waitForEngine(void);
|
|
|
|
static void dbg(const char *fmt, ...);
|
|
static void patchPrologs(NeModuleT *mod);
|
|
static void patchVflatdStackBug(NeModuleT *mod);
|
|
static void patchVflatdBypassCall(NeModuleT *mod);
|
|
static bool installInt10hReflector(void);
|
|
static void removeInt10hReflector(void);
|
|
static bool installDpmi300Proxy(void);
|
|
static void removeDpmi300Proxy(void);
|
|
static bool patchDoInt10h(struct WdrvDriverS *drv);
|
|
static bool patchBiosDataAccess(struct WdrvDriverS *drv);
|
|
static void patchWinFlags(struct WdrvDriverS *drv, uint16_t oldFlags, uint16_t newFlags);
|
|
static bool installInt2FhHandler(void);
|
|
static void removeInt2FhHandler(void);
|
|
static bool installExceptionCapture(void);
|
|
static void removeExceptionCapture(void);
|
|
|
|
static void drawStyledLine(struct WdrvDriverS *drv, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint32_t color, const int16_t *pattern, int16_t patLen, int16_t *patIdx, int16_t *patRemain);
|
|
static int32_t drawStyledPolyline(struct WdrvDriverS *drv, Point16T *points, int16_t count, uint32_t color, int16_t penStyle);
|
|
static int32_t floodFillSoftware(struct WdrvDriverS *drv, int16_t x, int16_t y, uint32_t fillColor);
|
|
static int32_t blitPixelsSoftware(struct WdrvDriverS *drv, int16_t x, int16_t y, int16_t w, int16_t h, const uint8_t *pixels, int32_t srcPitch);
|
|
static void readDacPalette(uint8_t rgb[768], uint8_t dacWidth);
|
|
|
|
// Pen dash patterns: even indices = draw, odd indices = skip (pixel counts)
|
|
static const int16_t gPatDash[] = {18, 6};
|
|
static const int16_t gPatDot[] = {3, 3};
|
|
static const int16_t gPatDashDot[] = {9, 6, 3, 6};
|
|
static const int16_t gPatDashDotDot[] = {9, 3, 3, 3, 3, 3};
|
|
|
|
// ============================================================================
|
|
// INT 10h (Video BIOS) reflector
|
|
//
|
|
// Win 3.x display drivers call INT 10h for video mode setting and BIOS
|
|
// queries. In protected mode, these calls won't reach the real-mode BIOS
|
|
// unless we intercept them and use DPMI to simulate a real-mode interrupt.
|
|
// ============================================================================
|
|
|
|
static __dpmi_paddr gOldInt10hVec;
|
|
static bool gInt10hInstalled = false;
|
|
|
|
// Globals for the raw INT 10h handler assembly stub.
|
|
// Non-static so the asm symbols (prefixed with _) are accessible.
|
|
uint16_t gInt10hDsSel; // DJGPP DS selector
|
|
uint32_t gInt10hSavedSS; // Interrupted SS
|
|
uint32_t gInt10hSavedESP; // Interrupted ESP
|
|
uint32_t gInt10hSavedFS; // Interrupted FS
|
|
uint8_t gInt10hStack[4096] __attribute__((aligned(16))); // Handler stack
|
|
uint32_t gInt10hStackTop; // Top of handler stack
|
|
|
|
// ============================================================================
|
|
// Exception capture - captures primary fault CS:EIP before DJGPP's handler
|
|
// (which may itself crash handling exceptions from 16-bit code).
|
|
//
|
|
// DPMI 0.9 exception frame on the locked exception stack:
|
|
// ESP+0x00: Return EIP (to DPMI host, for RETF)
|
|
// ESP+0x04: Return CS
|
|
// ESP+0x08: Error code
|
|
// ESP+0x0C: Faulting EIP
|
|
// ESP+0x10: Faulting CS
|
|
// ESP+0x14: Faulting EFLAGS
|
|
// ESP+0x18: Faulting ESP
|
|
// ESP+0x1C: Faulting SS
|
|
// ============================================================================
|
|
|
|
volatile uint32_t gFaultCaptured = 0;
|
|
volatile uint32_t gFaultNum = 0;
|
|
volatile uint32_t gFaultErr = 0;
|
|
volatile uint32_t gFaultEIP = 0;
|
|
volatile uint32_t gFaultCS = 0;
|
|
volatile uint32_t gFaultESP = 0;
|
|
volatile uint32_t gFaultSS = 0;
|
|
volatile uint32_t gFaultEAX = 0;
|
|
volatile uint32_t gFaultEBX = 0;
|
|
volatile uint32_t gFaultECX = 0;
|
|
volatile uint32_t gFaultEDX = 0;
|
|
volatile uint32_t gFaultESI = 0;
|
|
volatile uint32_t gFaultEDI = 0;
|
|
volatile uint32_t gFaultEBP = 0;
|
|
volatile uint32_t gFaultDS = 0;
|
|
volatile uint32_t gFaultES = 0;
|
|
uint8_t gFaultStack[4096] __attribute__((aligned(16)));
|
|
uint32_t gFaultStackTop;
|
|
|
|
// Packed 48-bit far pointers for chaining to old exception handlers.
|
|
// ljmp indirect reads offset32 + selector16 (6 bytes), so no padding allowed.
|
|
typedef struct __attribute__((packed)) {
|
|
uint32_t offset;
|
|
uint16_t selector;
|
|
} FarPtr48T;
|
|
|
|
FarPtr48T gOldExc0dFar;
|
|
FarPtr48T gOldExc0eFar;
|
|
|
|
static __dpmi_paddr gOldExc0D;
|
|
static __dpmi_paddr gOldExc0E;
|
|
static bool gExcCaptureInstalled = false;
|
|
|
|
static __dpmi_paddr gOldInt2FhVec;
|
|
FarPtr48T gOldInt2FhFar;
|
|
static bool gInt2FhInstalled = false;
|
|
|
|
// Saved register state for the raw INT 10h handler.
|
|
// Layout matches the save/restore sequence in the assembly stub.
|
|
typedef struct __attribute__((packed)) {
|
|
uint32_t edi; // +0
|
|
uint32_t esi; // +4
|
|
uint32_t ebp; // +8
|
|
uint32_t _reserved; // +12 (alignment padding)
|
|
uint32_t ebx; // +16
|
|
uint32_t edx; // +20
|
|
uint32_t ecx; // +24
|
|
uint32_t eax; // +28
|
|
uint32_t es; // +32 (zero-extended from 16-bit)
|
|
uint32_t ds; // +36 (zero-extended from 16-bit)
|
|
uint32_t eip; // +40 (from IRET frame)
|
|
uint32_t cs; // +44 (from IRET frame)
|
|
uint32_t eflags; // +48 (from IRET frame)
|
|
} Int10FrameT;
|
|
|
|
// Non-static so the asm symbol _gInt10Frame is accessible.
|
|
Int10FrameT gInt10Frame;
|
|
|
|
// Worker function called from the assembly stub.
|
|
// Non-static so the asm symbol _int10hWorker is accessible.
|
|
void int10hWorker(Int10FrameT *frame)
|
|
{
|
|
__dpmi_regs rRegs;
|
|
memset(&rRegs, 0, sizeof(rRegs));
|
|
|
|
uint16_t func = (uint16_t)frame->eax;
|
|
|
|
// NOTE: No dbg()/logErr() here — file I/O from the INT 10h handler corrupts
|
|
// callback state (observed: GlobalDOSAlloc params garbled after 4F15h stub).
|
|
|
|
rRegs.x.ax = (uint16_t)frame->eax;
|
|
rRegs.x.bx = (uint16_t)frame->ebx;
|
|
rRegs.x.cx = (uint16_t)frame->ecx;
|
|
rRegs.x.dx = (uint16_t)frame->edx;
|
|
rRegs.x.si = (uint16_t)frame->esi;
|
|
rRegs.x.di = (uint16_t)frame->edi;
|
|
rRegs.x.bp = (uint16_t)frame->ebp;
|
|
|
|
// VBE Set Mode: translate S3 OEM modes to VESA standard modes.
|
|
if (func == 0x4F02) {
|
|
uint16_t origBX = rRegs.x.bx;
|
|
uint16_t modeNum = origBX & 0x3FFF;
|
|
uint16_t flags = origBX & 0xC000;
|
|
|
|
uint16_t vesaMode = modeNum;
|
|
switch (modeNum) {
|
|
case 0x0201: vesaMode = 0x0101; break; // 640x480x256
|
|
case 0x0202: vesaMode = 0x0103; break; // 800x600x256
|
|
case 0x0203: vesaMode = 0x0103; break; // 800x600x256
|
|
case 0x0204: vesaMode = 0x0105; break; // 1024x768x256
|
|
case 0x0205: vesaMode = 0x0105; break; // 1024x768x256
|
|
}
|
|
|
|
if (vesaMode != modeNum) {
|
|
rRegs.x.bx = flags | vesaMode;
|
|
logErr("INT10: VBE mode 0x%04X -> 0x%04X (S3 OEM -> VESA)\n",
|
|
origBX, rRegs.x.bx);
|
|
}
|
|
}
|
|
|
|
// Stub out VBE functions we cannot support.
|
|
if (func == 0x4F0A || func == 0x4F15) {
|
|
frame->eax = (frame->eax & 0xFFFF0000) | 0x0100;
|
|
return;
|
|
}
|
|
|
|
// ================================================================
|
|
// Translate ES for real-mode reflection.
|
|
//
|
|
// The driver's ES is a PM selector. Real-mode INT 10h expects a
|
|
// real-mode paragraph segment. If ES points to conventional memory
|
|
// (<1MB), compute the real-mode segment directly. If ES points to
|
|
// extended memory (>=1MB), bounce through the DOS transfer buffer.
|
|
//
|
|
// Only specific sub-functions use ES as a buffer pointer. Each
|
|
// function family uses a different offset register:
|
|
// VBE 4Fxx: ES:DI
|
|
// AH=10h: ES:DX (palette)
|
|
// AH=11h: ES:BP (font data)
|
|
// AH=1Bh: ES:DI (state info)
|
|
// ================================================================
|
|
uint16_t pmES = (uint16_t)frame->es;
|
|
bool useTB = false;
|
|
uint32_t tb = 0;
|
|
uint32_t copySize = 0;
|
|
bool copyIn = false; // PM -> transfer buffer before INT
|
|
bool copyOut = false; // transfer buffer -> PM after INT
|
|
|
|
// Identify which offset register this function uses, and determine
|
|
// the exact copy size and direction. pmOff holds the PM-side
|
|
// offset from the appropriate register; offReg identifies which
|
|
// real-mode register to update after translation.
|
|
// 0 = DI, 1 = DX, 2 = BP
|
|
uint16_t pmOff = 0;
|
|
int offReg = 0;
|
|
bool needsES = false;
|
|
|
|
uint8_t ah = (uint8_t)(func >> 8);
|
|
uint8_t al = (uint8_t)(func & 0xFF);
|
|
|
|
if ((func & 0xFF00) == 0x4F00) {
|
|
// VBE functions — ES:DI
|
|
offReg = 0;
|
|
pmOff = rRegs.x.di;
|
|
if (al == 0x00) {
|
|
needsES = true; copyIn = true; copyOut = true; copySize = 512;
|
|
} else if (al == 0x01) {
|
|
needsES = true; copyOut = true; copySize = 256;
|
|
} else if (al == 0x04) {
|
|
needsES = true; copyIn = true; copyOut = true; copySize = 1024;
|
|
} else if (al == 0x09) {
|
|
needsES = true; copyIn = true;
|
|
copySize = rRegs.x.cx * 4;
|
|
if (copySize > 4096) {
|
|
copySize = 4096;
|
|
}
|
|
}
|
|
} else if (ah == 0x10) {
|
|
// Palette functions — ES:DX
|
|
offReg = 1;
|
|
pmOff = rRegs.x.dx;
|
|
if (al == 0x02) {
|
|
// Set All Palette Registers: 17 bytes (16 regs + overscan)
|
|
needsES = true; copyIn = true; copySize = 17;
|
|
} else if (al == 0x09) {
|
|
// Read All Palette Registers: 17 bytes
|
|
needsES = true; copyOut = true; copySize = 17;
|
|
} else if (al == 0x12) {
|
|
// Set Block of DAC Color Registers: CX * 3 bytes
|
|
needsES = true; copyIn = true;
|
|
copySize = rRegs.x.cx * 3;
|
|
if (copySize > 4096) {
|
|
copySize = 4096;
|
|
}
|
|
} else if (al == 0x17) {
|
|
// Read Block of DAC Color Registers: CX * 3 bytes
|
|
needsES = true; copyOut = true;
|
|
copySize = rRegs.x.cx * 3;
|
|
if (copySize > 4096) {
|
|
copySize = 4096;
|
|
}
|
|
}
|
|
} else if (ah == 0x11) {
|
|
// Character generator — ES:BP
|
|
offReg = 2;
|
|
pmOff = rRegs.x.bp;
|
|
if (al == 0x00 || al == 0x10) {
|
|
// Load User Font: CX chars * BH bytes/char
|
|
needsES = true; copyIn = true;
|
|
copySize = rRegs.x.cx * (rRegs.x.bx >> 8);
|
|
if (copySize > 8192) {
|
|
copySize = 8192;
|
|
}
|
|
}
|
|
// AL=20/21 set interrupt vectors to ES:BP — the address must
|
|
// point at resident data, not a temporary buffer, so skip.
|
|
} else if (ah == 0x1B) {
|
|
// Functionality/State Info — ES:DI, 64-byte buffer
|
|
offReg = 0;
|
|
pmOff = rRegs.x.di;
|
|
needsES = true; copyOut = true; copySize = 64;
|
|
}
|
|
|
|
if (pmES != 0 && needsES && copySize > 0) {
|
|
unsigned long esBase;
|
|
__dpmi_get_segment_base_address(pmES, &esBase);
|
|
|
|
if (esBase < 0x100000) {
|
|
// Conventional memory: compute real-mode ES + offset directly.
|
|
uint32_t linear = esBase + pmOff;
|
|
rRegs.x.es = (uint16_t)(linear >> 4);
|
|
uint16_t rmOff = (uint16_t)(linear & 0x0F);
|
|
if (offReg == 0) {
|
|
rRegs.x.di = rmOff;
|
|
} else if (offReg == 1) {
|
|
rRegs.x.dx = rmOff;
|
|
} else {
|
|
rRegs.x.bp = rmOff;
|
|
}
|
|
} else {
|
|
// Extended memory: bounce through the DOS transfer buffer.
|
|
tb = _go32_info_block.linear_address_of_transfer_buffer;
|
|
|
|
if (copyIn) {
|
|
movedata(pmES, pmOff, _dos_ds, tb, copySize);
|
|
}
|
|
|
|
rRegs.x.es = (uint16_t)(tb >> 4);
|
|
uint16_t rmOff = (uint16_t)(tb & 0x0F);
|
|
if (offReg == 0) {
|
|
rRegs.x.di = rmOff;
|
|
} else if (offReg == 1) {
|
|
rRegs.x.dx = rmOff;
|
|
} else {
|
|
rRegs.x.bp = rmOff;
|
|
}
|
|
useTB = true;
|
|
}
|
|
}
|
|
|
|
__dpmi_simulate_real_mode_interrupt(0x10, &rRegs);
|
|
|
|
if (useTB && copyOut) {
|
|
movedata(_dos_ds, tb, pmES, pmOff, copySize);
|
|
}
|
|
|
|
// Update return registers.
|
|
frame->eax = (frame->eax & 0xFFFF0000) | rRegs.x.ax;
|
|
frame->ebx = (frame->ebx & 0xFFFF0000) | rRegs.x.bx;
|
|
frame->ecx = (frame->ecx & 0xFFFF0000) | rRegs.x.cx;
|
|
frame->edx = (frame->edx & 0xFFFF0000) | rRegs.x.dx;
|
|
frame->esi = (frame->esi & 0xFFFF0000) | rRegs.x.si;
|
|
frame->ebp = (frame->ebp & 0xFFFF0000) | rRegs.x.bp;
|
|
frame->eflags = (frame->eflags & 0xFFFF0000) | rRegs.x.flags;
|
|
|
|
if (!needsES) {
|
|
// No ES translation was done — pass through real-mode DI
|
|
frame->edi = (frame->edi & 0xFFFF0000) | rRegs.x.di;
|
|
}
|
|
|
|
// Log VBE failures
|
|
if ((func & 0xFF00) == 0x4F00) {
|
|
uint16_t retAX = (uint16_t)frame->eax;
|
|
if (retAX != 0x004F) {
|
|
logErr("INT10: VBE func %04X returned AX=%04X (FAILED)\n",
|
|
func, retAX);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Raw INT 10h handler stub in assembly.
|
|
//
|
|
// The _go32_dpmi_allocate_iret_wrapper mechanism fails when an interrupt
|
|
// fires during 16-bit code execution — software interrupts are dispatched
|
|
// on the CURRENT stack (DPMI spec), so the wrapper tries to build its
|
|
// _go32_dpmi_registers structure on the 16-bit stack with a different
|
|
// SS base, producing an invalid pointer (observed: regs=0x7a2, page fault).
|
|
//
|
|
// This handler avoids the problem by:
|
|
// 1. Saving ALL registers to a global structure using CS-relative
|
|
// addressing (CS base == DS base in DJGPP)
|
|
// 2. Switching SS:ESP to a dedicated 32-bit handler stack in DJGPP's
|
|
// data segment (so SS base == DS base, safe for C library calls)
|
|
// 3. Calling the C worker function
|
|
// 4. Restoring SS:ESP and all registers from the global structure
|
|
// 5. Returning via IRET
|
|
//
|
|
// NOT re-entrant — uses global state. Acceptable because the handler
|
|
// doesn't enable interrupts, and INT 10h is a software interrupt that
|
|
// cannot nest (our worker uses DPMI INT 31h, not INT 10h).
|
|
//
|
|
// Uses FS for writes (code segments are read-only in protected mode).
|
|
// CS-relative reads are fine (readable code segment).
|
|
__asm__(
|
|
" .text\n"
|
|
" .p2align 4\n"
|
|
" .globl _int10hRawHandler\n"
|
|
"_int10hRawHandler:\n"
|
|
|
|
// ---- Save original FS, then load FS with our writable DS selector ----
|
|
" pushl %eax\n"
|
|
" pushl %ecx\n"
|
|
" xorl %eax, %eax\n"
|
|
" movw %fs, %ax\n"
|
|
" movw %cs:_gInt10hDsSel, %cx\n"
|
|
" movw %cx, %fs\n"
|
|
" movl %eax, %fs:_gInt10hSavedFS\n"
|
|
|
|
" popl %ecx\n"
|
|
" popl %eax\n"
|
|
|
|
// ---- Save all GP registers to global frame via FS (writable) ----
|
|
" movl %eax, %fs:_gInt10Frame+28\n"
|
|
" movl %ecx, %fs:_gInt10Frame+24\n"
|
|
" movl %edx, %fs:_gInt10Frame+20\n"
|
|
" movl %ebx, %fs:_gInt10Frame+16\n"
|
|
" movl %ebp, %fs:_gInt10Frame+8\n"
|
|
" movl %esi, %fs:_gInt10Frame+4\n"
|
|
" movl %edi, %fs:_gInt10Frame+0\n"
|
|
|
|
// ---- Save segment registers (zero-extended to 32 bits) ----
|
|
" xorl %eax, %eax\n"
|
|
" movw %es, %ax\n"
|
|
" movl %eax, %fs:_gInt10Frame+32\n"
|
|
" movw %ds, %ax\n"
|
|
" movl %eax, %fs:_gInt10Frame+36\n"
|
|
|
|
// ---- Save IRET frame from interrupted stack (SS:ESP) ----
|
|
" movl (%esp), %eax\n"
|
|
" movl %eax, %fs:_gInt10Frame+40\n"
|
|
" movl 4(%esp), %eax\n"
|
|
" movl %eax, %fs:_gInt10Frame+44\n"
|
|
" movl 8(%esp), %eax\n"
|
|
" movl %eax, %fs:_gInt10Frame+48\n"
|
|
|
|
// ---- Save SS:ESP and switch to DJGPP handler stack ----
|
|
" movl %esp, %fs:_gInt10hSavedESP\n"
|
|
" xorl %eax, %eax\n"
|
|
" movw %ss, %ax\n"
|
|
" movl %eax, %fs:_gInt10hSavedSS\n"
|
|
|
|
" movw %cs:_gInt10hDsSel, %ax\n"
|
|
" movw %ax, %ds\n"
|
|
" movw %ax, %es\n"
|
|
" movw %ax, %ss\n"
|
|
" movl _gInt10hStackTop, %esp\n"
|
|
|
|
// ---- Call C worker: int10hWorker(&gInt10Frame) ----
|
|
" leal _gInt10Frame, %eax\n"
|
|
" pushl %eax\n"
|
|
" call _int10hWorker\n"
|
|
" addl $4, %esp\n"
|
|
|
|
// ---- Restore SS:ESP (back to interrupted code's stack) ----
|
|
" movl %cs:_gInt10hSavedESP, %eax\n"
|
|
" movl %cs:_gInt10hSavedSS, %ecx\n"
|
|
" movw %cx, %ss\n"
|
|
" movl %eax, %esp\n"
|
|
|
|
// ---- Write modified EFLAGS back to IRET frame on stack ----
|
|
// The C worker updates frame->eflags with real-mode return flags
|
|
// (e.g. CF for VBE success/failure). Write it back so IRET uses it.
|
|
" movl %cs:_gInt10Frame+48, %eax\n"
|
|
" movl %eax, 8(%esp)\n"
|
|
|
|
// ---- Restore GP registers from global frame (CS reads OK) ----
|
|
" movl %cs:_gInt10Frame+0, %edi\n"
|
|
" movl %cs:_gInt10Frame+4, %esi\n"
|
|
" movl %cs:_gInt10Frame+8, %ebp\n"
|
|
" movl %cs:_gInt10Frame+16, %ebx\n"
|
|
" movl %cs:_gInt10Frame+20, %edx\n"
|
|
" movl %cs:_gInt10Frame+24, %ecx\n"
|
|
|
|
// ---- Restore segment registers (FS/GS always set to DGROUP) ----
|
|
" movl %cs:_gCbDgroupSel, %eax\n"
|
|
" movw %ax, %fs\n"
|
|
" movw %ax, %gs\n"
|
|
" movl %cs:_gInt10Frame+32, %eax\n"
|
|
" movw %ax, %es\n"
|
|
" movl %cs:_gInt10Frame+36, %eax\n"
|
|
" movw %ax, %ds\n"
|
|
|
|
// ---- Restore EAX last (was used as scratch) ----
|
|
" movl %cs:_gInt10Frame+28, %eax\n"
|
|
|
|
" iret\n"
|
|
);
|
|
|
|
|
|
// ============================================================================
|
|
// DPMI 0x300h (Simulate Real Mode Interrupt) proxy
|
|
//
|
|
// The VBESVGA driver's DoInt10h calls DPMI INT 31h AX=0300h from 16-bit
|
|
// code to perform real-mode INT 10h for VBE BIOS calls. CWSDPMI does
|
|
// not correctly service this DPMI function when the INT 31h originates
|
|
// from a 16-bit code segment inside a 32-bit DPMI client.
|
|
//
|
|
// Fix: after the driver's entry point has been called (which patches
|
|
// DoInt10h for 386 via SetupInt10h), we change the single "CD 31"
|
|
// (INT 31h) instruction in DoInt10h to "CD 64" (INT 64h). Our INT 64h
|
|
// handler reads the Real Mode Call Structure (RMCS) that DoInt10h built
|
|
// on the 16-bit stack, calls __dpmi_simulate_real_mode_interrupt from
|
|
// 32-bit code (which CWSDPMI handles correctly), and writes the results
|
|
// back to the RMCS so DoInt10h can unpack them normally.
|
|
// ============================================================================
|
|
|
|
#define DPMI300_INT_NUM 0x64
|
|
|
|
static __dpmi_paddr gOldDpmi300Vec;
|
|
static bool gDpmi300Installed = false;
|
|
|
|
// Globals for the raw handler assembly stub
|
|
uint16_t gDpmi300DsSel;
|
|
uint32_t gDpmi300SavedSS;
|
|
uint32_t gDpmi300SavedESP;
|
|
uint32_t gDpmi300SavedFS;
|
|
uint32_t gDpmi300SavedDS;
|
|
uint32_t gDpmi300SavedES;
|
|
uint32_t gDpmi300SavedGS;
|
|
uint32_t gDpmi300RmcsSel; // ES at time of interrupt (RMCS segment)
|
|
uint32_t gDpmi300RmcsEdi; // EDI at time of interrupt (RMCS offset)
|
|
uint32_t gDpmi300IntNum; // EBX at time of interrupt (BL=int number)
|
|
uint8_t gDpmi300Stack[4096] __attribute__((aligned(16)));
|
|
uint32_t gDpmi300StackTop;
|
|
|
|
// Worker: reads RMCS, performs real-mode interrupt, writes results back.
|
|
// The DPMI RMCS layout is byte-compatible with DJGPP's __dpmi_regs (50 bytes).
|
|
void dpmi300Worker(void)
|
|
{
|
|
uint16_t rmcsSel = (uint16_t)gDpmi300RmcsSel;
|
|
uint32_t rmcsOff = gDpmi300RmcsEdi;
|
|
uint8_t intNum = (uint8_t)gDpmi300IntNum;
|
|
|
|
__dpmi_regs regs;
|
|
memset(®s, 0, sizeof(regs));
|
|
movedata(rmcsSel, rmcsOff, _my_ds(), (unsigned)®s, 50);
|
|
|
|
dbg("DPMI300: INT %02Xh AX=%04X BX=%04X ES=%04X DI=%04X SS:SP=%04X:%04X\n",
|
|
intNum, regs.x.ax, regs.x.bx, regs.x.es, regs.x.di,
|
|
regs.x.ss, regs.x.sp);
|
|
|
|
__dpmi_simulate_real_mode_interrupt(intNum, ®s);
|
|
|
|
dbg("DPMI300: result AX=%04X\n", regs.x.ax);
|
|
|
|
// Dump VBE info buffer contents for VBE 4F00h
|
|
if (intNum == 0x10 && regs.x.ax == 0x004F) {
|
|
uint32_t bufLin = (uint32_t)regs.x.es * 16 + regs.x.di;
|
|
uint8_t hdr[32];
|
|
dosmemget(bufLin, 32, hdr);
|
|
dbg("DPMI300: VBE buf[0..3]=%c%c%c%c ver=%02X%02X modes=%02X%02X:%02X%02X\n",
|
|
hdr[0], hdr[1], hdr[2], hdr[3],
|
|
hdr[5], hdr[4],
|
|
hdr[0x0F], hdr[0x0E], hdr[0x11], hdr[0x10]);
|
|
// Mode list pointer at offset 0x0E: offset(word) + segment(word)
|
|
uint16_t modesOff = hdr[0x0E] | ((uint16_t)hdr[0x0F] << 8);
|
|
uint16_t modesSeg = hdr[0x10] | ((uint16_t)hdr[0x11] << 8);
|
|
dbg("DPMI300: VBE modes ptr %04X:%04X (buf at %04X:%04X)\n",
|
|
modesSeg, modesOff, regs.x.es, regs.x.di);
|
|
// Read first 16 mode numbers
|
|
uint32_t modesLin = (uint32_t)modesSeg * 16 + modesOff;
|
|
uint16_t modes[16];
|
|
dosmemget(modesLin, 32, modes);
|
|
dbg("DPMI300: VBE modes:");
|
|
for (int i = 0; i < 16 && modes[i] != 0xFFFF; i++) {
|
|
dbg(" %03X", modes[i]);
|
|
}
|
|
dbg("\n");
|
|
}
|
|
|
|
movedata(_my_ds(), (unsigned)®s, rmcsSel, rmcsOff, 50);
|
|
}
|
|
|
|
extern void dpmi300RawHandler(void);
|
|
|
|
// Raw INT 64h handler. Same save/restore pattern as the INT 10h reflector
|
|
// but simpler: we only need the RMCS pointer (ES:EDI) and interrupt number
|
|
// (BL) from the interrupted context. All GP and segment registers are
|
|
// preserved across the call — the only visible side effect is that the
|
|
// RMCS on the driver's stack is updated and the carry flag is cleared.
|
|
__asm__(
|
|
" .text\n"
|
|
" .p2align 4\n"
|
|
" .globl _dpmi300RawHandler\n"
|
|
"_dpmi300RawHandler:\n"
|
|
|
|
// ---- Save FS, load FS with our DS selector ----
|
|
" pushl %eax\n"
|
|
" pushl %ecx\n"
|
|
" xorl %eax, %eax\n"
|
|
" movw %fs, %ax\n"
|
|
" movw %cs:_gDpmi300DsSel, %cx\n"
|
|
" movw %cx, %fs\n"
|
|
" movl %eax, %fs:_gDpmi300SavedFS\n"
|
|
|
|
// ---- Save communication values: ES (RMCS sel), EDI, EBX ----
|
|
" xorl %eax, %eax\n"
|
|
" movw %es, %ax\n"
|
|
" movl %eax, %fs:_gDpmi300SavedES\n"
|
|
" movl %eax, %fs:_gDpmi300RmcsSel\n"
|
|
" movl %edi, %fs:_gDpmi300RmcsEdi\n"
|
|
" movl %ebx, %fs:_gDpmi300IntNum\n"
|
|
|
|
// ---- Save remaining segment registers ----
|
|
" xorl %eax, %eax\n"
|
|
" movw %ds, %ax\n"
|
|
" movl %eax, %fs:_gDpmi300SavedDS\n"
|
|
" movw %gs, %ax\n"
|
|
" movl %eax, %fs:_gDpmi300SavedGS\n"
|
|
|
|
// ---- Restore scratch, then PUSHAL to save all GP regs ----
|
|
" popl %ecx\n"
|
|
" popl %eax\n"
|
|
" pushal\n"
|
|
|
|
// ---- Save interrupted SS:ESP and switch to handler stack ----
|
|
" movw %cs:_gDpmi300DsSel, %ax\n"
|
|
" movw %ax, %fs\n"
|
|
" movl %esp, %fs:_gDpmi300SavedESP\n"
|
|
" xorl %eax, %eax\n"
|
|
" movw %ss, %ax\n"
|
|
" movl %eax, %fs:_gDpmi300SavedSS\n"
|
|
|
|
" movw %fs:_gDpmi300DsSel, %ax\n"
|
|
" movw %ax, %ds\n"
|
|
" movw %ax, %es\n"
|
|
" movw %ax, %ss\n"
|
|
" movl _gDpmi300StackTop, %esp\n"
|
|
|
|
// ---- Call C worker ----
|
|
" call _dpmi300Worker\n"
|
|
|
|
// ---- Restore interrupted SS:ESP ----
|
|
" movl %cs:_gDpmi300SavedSS, %ecx\n"
|
|
" movl %cs:_gDpmi300SavedESP, %eax\n"
|
|
" movw %cx, %ss\n"
|
|
" movl %eax, %esp\n"
|
|
|
|
// ---- POPAL to restore all GP registers ----
|
|
" popal\n"
|
|
|
|
// ---- Restore segment registers ----
|
|
" pushl %eax\n"
|
|
" movl %cs:_gDpmi300SavedFS, %eax\n"
|
|
" movw %ax, %fs\n"
|
|
" movl %cs:_gDpmi300SavedGS, %eax\n"
|
|
" movw %ax, %gs\n"
|
|
" movl %cs:_gDpmi300SavedES, %eax\n"
|
|
" movw %ax, %es\n"
|
|
" movl %cs:_gDpmi300SavedDS, %eax\n"
|
|
" movw %ax, %ds\n"
|
|
" popl %eax\n"
|
|
|
|
// ---- Clear carry flag in IRET frame EFLAGS (success) ----
|
|
" andl $0xFFFFFFFE, 8(%esp)\n"
|
|
|
|
" iret\n"
|
|
);
|
|
|
|
|
|
// Worker function for exception handler — logs full diagnostics and exits.
|
|
// Non-static so the asm symbol _faultWorker is accessible.
|
|
void faultWorker(void)
|
|
{
|
|
logErr("\n=== EXCEPTION #%" PRIu32 " ===\n", gFaultNum);
|
|
logErr(" CS:EIP = %04" PRIX32 ":%08" PRIX32 " error=%04" PRIX32 "\n",
|
|
gFaultCS, gFaultEIP, gFaultErr);
|
|
logErr(" SS:ESP = %04" PRIX32 ":%08" PRIX32 "\n", gFaultSS, gFaultESP);
|
|
logErr(" eax=%08" PRIX32 " ebx=%08" PRIX32 " ecx=%08" PRIX32 " edx=%08" PRIX32 "\n",
|
|
gFaultEAX, gFaultEBX, gFaultECX, gFaultEDX);
|
|
logErr(" esi=%08" PRIX32 " edi=%08" PRIX32 " ebp=%08" PRIX32 "\n",
|
|
gFaultESI, gFaultEDI, gFaultEBP);
|
|
logErr(" ds=%04" PRIX32 " es=%04" PRIX32 "\n", gFaultDS, gFaultES);
|
|
|
|
// Dump instruction bytes at CS:EIP using _farpeekb
|
|
// (movedata fails on 16-bit code segments in fault context)
|
|
uint16_t faultSel = (uint16_t)gFaultCS;
|
|
uint32_t faultOff = gFaultEIP;
|
|
unsigned long csBase;
|
|
if (__dpmi_get_segment_base_address(faultSel, &csBase) == 0) {
|
|
unsigned csLimit = __dpmi_get_segment_limit(faultSel);
|
|
logErr(" cs: base=%08lX limit=%04X\n", csBase, csLimit);
|
|
logErr(" code:");
|
|
for (int i = 0; i < 16 && (faultOff + i) <= csLimit; i++) {
|
|
logErr(" %02X", _farpeekb(faultSel, faultOff + i));
|
|
}
|
|
logErr("\n");
|
|
}
|
|
|
|
// Dump segment info for DS and ES
|
|
unsigned long dsBase;
|
|
unsigned long esBase;
|
|
if ((uint16_t)gFaultDS != 0 && __dpmi_get_segment_base_address((uint16_t)gFaultDS, &dsBase) == 0) {
|
|
logErr(" ds: base=%08lX\n", dsBase);
|
|
}
|
|
if ((uint16_t)gFaultES != 0 && __dpmi_get_segment_base_address((uint16_t)gFaultES, &esBase) == 0) {
|
|
logErr(" es: base=%08lX\n", esBase);
|
|
}
|
|
|
|
// Dump 32 words from the faulting stack using _farpeekw
|
|
if ((uint16_t)gFaultSS != 0) {
|
|
unsigned ssLimit = __dpmi_get_segment_limit((uint16_t)gFaultSS);
|
|
logErr(" ss: limit=%04X\n", ssLimit);
|
|
logErr(" stack:");
|
|
for (int i = 0; i < 32 && (gFaultESP + i * 2 + 1) <= ssLimit; i++) {
|
|
if (i == 16) {
|
|
logErr("\n ");
|
|
}
|
|
logErr(" %04X", _farpeekw((uint16_t)gFaultSS, gFaultESP + i * 2));
|
|
}
|
|
logErr("\n");
|
|
}
|
|
|
|
// Exit cleanly via DOS
|
|
__asm__ volatile ("movl $0x4CFF, %%eax; int $0x21" ::: "eax");
|
|
__builtin_unreachable();
|
|
}
|
|
|
|
// Raw exception handlers for GPF (#13) and PF (#14).
|
|
//
|
|
// These capture fault state (GP registers, segment registers, instruction
|
|
// bytes) then switch to a private stack and call faultWorker() to log
|
|
// full diagnostics and exit cleanly (avoiding secondary crashes from
|
|
// DJGPP's handler trying to process faults from 16-bit code).
|
|
//
|
|
// DPMI exception frame on stack:
|
|
// ESP+0x00: Return EIP (to DPMI host, for RETF)
|
|
// ESP+0x04: Return CS
|
|
// ESP+0x08: Error code
|
|
// ESP+0x0C: Faulting EIP
|
|
// ESP+0x10: Faulting CS
|
|
// ESP+0x14: Faulting EFLAGS
|
|
// ESP+0x18: Faulting ESP
|
|
// ESP+0x1C: Faulting SS
|
|
//
|
|
// After pushing EAX, offsets shift by +4.
|
|
|
|
__asm__(
|
|
" .text\n"
|
|
" .p2align 4\n"
|
|
" .globl _exc0dRawHandler\n"
|
|
"_exc0dRawHandler:\n"
|
|
" pushl %eax\n"
|
|
" movw %cs:_gInt10hDsSel, %ax\n"
|
|
" movw %ax, %fs\n"
|
|
" cmpl $0, %fs:_gFaultCaptured\n"
|
|
" jne 1f\n"
|
|
// First fault — capture everything
|
|
" movl $1, %fs:_gFaultCaptured\n"
|
|
" movl $13, %fs:_gFaultNum\n"
|
|
// Save GP registers via FS
|
|
" popl %eax\n"
|
|
" movl %eax, %fs:_gFaultEAX\n"
|
|
" movl %ebx, %fs:_gFaultEBX\n"
|
|
" movl %ecx, %fs:_gFaultECX\n"
|
|
" movl %edx, %fs:_gFaultEDX\n"
|
|
" movl %esi, %fs:_gFaultESI\n"
|
|
" movl %edi, %fs:_gFaultEDI\n"
|
|
" movl %ebp, %fs:_gFaultEBP\n"
|
|
" xorl %eax, %eax\n"
|
|
" movw %ds, %ax\n"
|
|
" movl %eax, %fs:_gFaultDS\n"
|
|
" movw %es, %ax\n"
|
|
" movl %eax, %fs:_gFaultES\n"
|
|
// Save exception frame fields (no pushed EAX shift now)
|
|
" movl 0x08(%esp), %eax\n"
|
|
" movl %eax, %fs:_gFaultErr\n"
|
|
" movl 0x0C(%esp), %eax\n"
|
|
" movl %eax, %fs:_gFaultEIP\n"
|
|
" movl 0x10(%esp), %eax\n"
|
|
" movl %eax, %fs:_gFaultCS\n"
|
|
" movl 0x18(%esp), %eax\n"
|
|
" movl %eax, %fs:_gFaultESP\n"
|
|
" movl 0x1C(%esp), %eax\n"
|
|
" movl %eax, %fs:_gFaultSS\n"
|
|
// Switch to our private stack and call faultWorker
|
|
" movw %fs:_gInt10hDsSel, %ax\n"
|
|
" movw %ax, %ds\n"
|
|
" movw %ax, %es\n"
|
|
" movw %ax, %ss\n"
|
|
" movl _gFaultStackTop, %esp\n"
|
|
" call _faultWorker\n"
|
|
// faultWorker doesn't return, but just in case:
|
|
" hlt\n"
|
|
"1:\n"
|
|
// Secondary fault — chain to old handler
|
|
" popl %eax\n"
|
|
" ljmp *%cs:_gOldExc0dFar\n"
|
|
);
|
|
|
|
__asm__(
|
|
" .text\n"
|
|
" .p2align 4\n"
|
|
" .globl _exc0eRawHandler\n"
|
|
"_exc0eRawHandler:\n"
|
|
" pushl %eax\n"
|
|
" movw %cs:_gInt10hDsSel, %ax\n"
|
|
" movw %ax, %fs\n"
|
|
" cmpl $0, %fs:_gFaultCaptured\n"
|
|
" jne 1f\n"
|
|
// First fault — capture everything
|
|
" movl $1, %fs:_gFaultCaptured\n"
|
|
" movl $14, %fs:_gFaultNum\n"
|
|
" popl %eax\n"
|
|
" movl %eax, %fs:_gFaultEAX\n"
|
|
" movl %ebx, %fs:_gFaultEBX\n"
|
|
" movl %ecx, %fs:_gFaultECX\n"
|
|
" movl %edx, %fs:_gFaultEDX\n"
|
|
" movl %esi, %fs:_gFaultESI\n"
|
|
" movl %edi, %fs:_gFaultEDI\n"
|
|
" movl %ebp, %fs:_gFaultEBP\n"
|
|
" xorl %eax, %eax\n"
|
|
" movw %ds, %ax\n"
|
|
" movl %eax, %fs:_gFaultDS\n"
|
|
" movw %es, %ax\n"
|
|
" movl %eax, %fs:_gFaultES\n"
|
|
" movl 0x08(%esp), %eax\n"
|
|
" movl %eax, %fs:_gFaultErr\n"
|
|
" movl 0x0C(%esp), %eax\n"
|
|
" movl %eax, %fs:_gFaultEIP\n"
|
|
" movl 0x10(%esp), %eax\n"
|
|
" movl %eax, %fs:_gFaultCS\n"
|
|
" movl 0x18(%esp), %eax\n"
|
|
" movl %eax, %fs:_gFaultESP\n"
|
|
" movl 0x1C(%esp), %eax\n"
|
|
" movl %eax, %fs:_gFaultSS\n"
|
|
" movw %fs:_gInt10hDsSel, %ax\n"
|
|
" movw %ax, %ds\n"
|
|
" movw %ax, %es\n"
|
|
" movw %ax, %ss\n"
|
|
" movl _gFaultStackTop, %esp\n"
|
|
" call _faultWorker\n"
|
|
" hlt\n"
|
|
"1:\n"
|
|
" popl %eax\n"
|
|
" ljmp *%cs:_gOldExc0eFar\n"
|
|
);
|
|
|
|
|
|
// Raw INT 2Fh handler for Windows API emulation.
|
|
//
|
|
// Windows 3.x display drivers call INT 2Fh to check for the Windows
|
|
// Enhanced Mode environment. Without this handler, the calls are
|
|
// reflected to real mode where DOS returns "not installed", causing
|
|
// the driver's initialization to fail.
|
|
//
|
|
// Handled functions:
|
|
// AX=1600h: Windows Enhanced Mode installation check
|
|
// Returns AL=03h, AH=0Ah (Windows 3.10 Enhanced Mode)
|
|
// AX=4000h-400Ah: Virtual DMA Services (VDS)
|
|
// Returns carry clear (success, no-op)
|
|
// AX=4010h+: Windows/386 VMM API calls
|
|
// Returns AX=0 (not present, proceed normally)
|
|
//
|
|
// All other INT 2Fh calls are chained to the previous handler.
|
|
// This handler modifies only AX and returns via IRET, so no stack
|
|
// switching is needed (unlike the INT 10h handler).
|
|
extern void int2FhRawHandler(void);
|
|
|
|
__asm__(
|
|
" .text\n"
|
|
" .p2align 4\n"
|
|
" .globl _int2FhRawHandler\n"
|
|
"_int2FhRawHandler:\n"
|
|
" cmpw $0x1600, %ax\n"
|
|
" je 1f\n"
|
|
" cmpb $0x40, %ah\n"
|
|
" je 3f\n"
|
|
" ljmp *%cs:_gOldInt2FhFar\n"
|
|
"1:\n"
|
|
// Windows 3.10 Enhanced Mode is "running"
|
|
" movw $0x0A03, %ax\n"
|
|
" iret\n"
|
|
"3:\n"
|
|
// AH=40h: VDS and Windows/386 API calls
|
|
// VDS calls (AL=00h-0Ah): return carry clear (success, no-op)
|
|
// VMM calls (AL=10h+): return AX=0 (not present)
|
|
" cmpb $0x0A, %al\n"
|
|
" jbe 4f\n"
|
|
// VMM/Win386 API: not present
|
|
" xorw %ax, %ax\n"
|
|
" iret\n"
|
|
"4:\n"
|
|
// VDS: success (carry clear)
|
|
" clc\n"
|
|
" iret\n"
|
|
);
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
// Library initialization
|
|
// ============================================================================
|
|
|
|
int32_t wdrvInit(void)
|
|
{
|
|
if (gInitialized) {
|
|
return WDRV_OK;
|
|
}
|
|
|
|
// Initialize the thunking layer
|
|
if (!thunkInit(&gThunkCtx)) {
|
|
setError(WDRV_ERR_THUNK_FAILED);
|
|
return gLastError;
|
|
}
|
|
|
|
// Initialize the Windows API stubs
|
|
if (!stubInit(&gStubCtx, &gThunkCtx)) {
|
|
thunkShutdown(&gThunkCtx);
|
|
setError(WDRV_ERR_INIT);
|
|
return gLastError;
|
|
}
|
|
|
|
// Install PM interrupt reflector for INT 10h.
|
|
// CWSDPMI's default reflection doesn't work correctly when the
|
|
// interrupt fires from 16-bit code segments (stack frame mismatch).
|
|
if (!installInt10hReflector()) {
|
|
logErr("windrv: warning: could not install INT 10h reflector\n");
|
|
}
|
|
|
|
// Install DPMI 0x300h proxy on INT 64h.
|
|
// CWSDPMI doesn't correctly handle INT 31h AX=0300h (simulate real-mode
|
|
// interrupt) when called from 16-bit code segments within a 32-bit DPMI
|
|
// client. DoInt10h in the VBESVGA driver calls INT 31h from 16-bit code
|
|
// to perform VBE calls. We redirect those to our proxy which performs the
|
|
// same operation from 32-bit code via __dpmi_simulate_real_mode_interrupt.
|
|
if (!installDpmi300Proxy()) {
|
|
logErr("windrv: warning: could not install DPMI 300h proxy\n");
|
|
}
|
|
|
|
// Install PM handler for INT 2Fh (Windows API emulation).
|
|
// The driver calls INT 2Fh AX=1600h to check for Windows Enhanced
|
|
// Mode. Without this, the check fails and Enable() returns 0.
|
|
// This raw handler only intercepts specific AX values and chains
|
|
// to the old handler for everything else, so it's safe for
|
|
// DJGPP/CWSDPMI internal INT 2Fh usage.
|
|
if (!installInt2FhHandler()) {
|
|
logErr("windrv: warning: could not install INT 2Fh handler\n");
|
|
}
|
|
|
|
// Install exception capture to diagnose primary fault CS:EIP
|
|
// (must be after installInt10hReflector which sets gInt10hDsSel)
|
|
if (!installExceptionCapture()) {
|
|
logErr("windrv: warning: could not install exception capture\n");
|
|
}
|
|
|
|
// Enable near pointer access for direct memory operations
|
|
if (__djgpp_nearptr_enable() == 0) {
|
|
logErr("windrv: warning: near pointer access not available\n");
|
|
}
|
|
|
|
gInitialized = true;
|
|
setError(WDRV_OK);
|
|
return WDRV_OK;
|
|
}
|
|
|
|
|
|
void wdrvShutdown(void)
|
|
{
|
|
if (!gInitialized) {
|
|
return;
|
|
}
|
|
|
|
// Free the built-in font singleton
|
|
if (gBuiltinFontInit) {
|
|
free16BitBlock(gBuiltinFont.fontSel, gBuiltinFont.fontLinear);
|
|
memset(&gBuiltinFont, 0, sizeof(gBuiltinFont));
|
|
gBuiltinFontInit = false;
|
|
}
|
|
|
|
removeExceptionCapture();
|
|
removeInt2FhHandler();
|
|
removeDpmi300Proxy();
|
|
removeInt10hReflector();
|
|
stubShutdown(&gStubCtx);
|
|
thunkShutdown(&gThunkCtx);
|
|
__djgpp_nearptr_disable();
|
|
|
|
gInitialized = false;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Driver loading
|
|
// ============================================================================
|
|
|
|
WdrvHandleT wdrvLoadDriver(const char *driverPath)
|
|
{
|
|
if (!gInitialized) {
|
|
setError(WDRV_ERR_INIT);
|
|
return NULL;
|
|
}
|
|
|
|
struct WdrvDriverS *drv = (struct WdrvDriverS *)calloc(1, sizeof(struct WdrvDriverS));
|
|
if (!drv) {
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return NULL;
|
|
}
|
|
|
|
strncpy(drv->filePath, driverPath, sizeof(drv->filePath) - 1);
|
|
|
|
// Load the NE module
|
|
if (gDebug) {
|
|
neSetDebug(true);
|
|
}
|
|
|
|
if (!neLoadModule(&drv->neMod, driverPath, importResolver)) {
|
|
setError(WDRV_ERR_LOAD_FAILED);
|
|
free(drv);
|
|
return NULL;
|
|
}
|
|
|
|
stubSetModule(&gStubCtx, &drv->neMod);
|
|
|
|
if (gDebug) {
|
|
neDumpModule(&drv->neMod);
|
|
}
|
|
|
|
// Extend DGROUP to include space for GDI objects (PDEVICE, brush, etc.)
|
|
if (!extendDgroupForObjects(drv)) {
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
neUnloadModule(&drv->neMod);
|
|
free(drv);
|
|
return NULL;
|
|
}
|
|
|
|
// Set the driver's DGROUP selector so the thunk loads DS correctly
|
|
gThunkCtx.dgroupSel = drv->neMod.autoDataSel;
|
|
dbg("windrv: DGROUP selector = 0x%04X\n", gThunkCtx.dgroupSel);
|
|
|
|
// Patch Windows PROLOG_0 sequences in all code segments.
|
|
// In real Windows, the module loader converts the 3-byte prolog
|
|
// "mov ax, ds; nop" (8C D8 90) to "mov ax, <DGROUP_sel>" (B8 xx xx)
|
|
// so that AX always gets the correct DGROUP selector regardless of
|
|
// the current DS value at function entry. Without this, internal
|
|
// near/far calls within the driver (where AX has been clobbered)
|
|
// will fault when the prolog tries to load DS from AX.
|
|
patchPrologs(&drv->neMod);
|
|
|
|
// Patch the VFLATD initialization routine's stack imbalance bug.
|
|
// The function at seg5:0x2368 pushes 20 bytes of intermediate values
|
|
// during API calls but never cleans them before ret. In Windows 3.x
|
|
// the caller restores SP from BP so this is harmless, but our thunk
|
|
// relies on a clean ret.
|
|
patchVflatdStackBug(&drv->neMod);
|
|
|
|
// Bypass the VFLATD API call for framebuffer mapping.
|
|
// The driver checks [8889] to choose between VFLATD (VxD call through
|
|
// a far pointer at [0D76]) and DPMI (INT 31h to map physical memory).
|
|
// Since VFLATD isn't available, force the DPMI path which uses standard
|
|
// DPMI functions (0800h, 0007h, 0008h) that CWSDPMI supports.
|
|
patchVflatdBypassCall(&drv->neMod);
|
|
|
|
// Resolve DDI entry points
|
|
if (!resolveDriverEntries(drv)) {
|
|
setError(WDRV_ERR_NO_ENTRY);
|
|
neUnloadModule(&drv->neMod);
|
|
free(drv);
|
|
return NULL;
|
|
}
|
|
|
|
// Verify that at least Enable and Disable are present
|
|
if (!drv->ddiEntry[DDI_ORD_ENABLE].present ||
|
|
!drv->ddiEntry[DDI_ORD_DISABLE].present) {
|
|
logErr("windrv: driver missing Enable (%d) or Disable (%d)\n",
|
|
drv->ddiEntry[DDI_ORD_ENABLE].present,
|
|
drv->ddiEntry[DDI_ORD_DISABLE].present);
|
|
setError(WDRV_ERR_NO_ENTRY);
|
|
neUnloadModule(&drv->neMod);
|
|
free(drv);
|
|
return NULL;
|
|
}
|
|
|
|
// Patch DoInt10h's INT 31h -> INT 64h BEFORE calling the entry point.
|
|
// The entry point calls SetupInt10h which self-modifies the Code segment
|
|
// (patches PUSHAD/POPAD on 386). We patch first so that when the entry
|
|
// point later calls DoInt10h for VBE queries, it uses our proxy.
|
|
patchDoInt10h(drv);
|
|
patchBiosDataAccess(drv);
|
|
|
|
// Call the NE module entry point (driver_initialization).
|
|
// This runs the driver's one-time init code:
|
|
// - SetupInt10h: allocates a real-mode stack for VBE INT 10h calls
|
|
// - dev_initialization: sets ScreenSelector, checks CPU type, VDD query
|
|
// Without this, DoInt10h uses an uninitialized stack and all VBE calls
|
|
// fail, causing the driver's Enable to hit its fatal error path.
|
|
if (drv->neMod.neHeader.entryPointCS != 0) {
|
|
uint16_t epSegIdx = drv->neMod.neHeader.entryPointCS - 1;
|
|
if (epSegIdx < drv->neMod.segmentCount) {
|
|
uint16_t epSel = drv->neMod.segments[epSegIdx].selector;
|
|
uint16_t epOff = drv->neMod.neHeader.entryPointIP;
|
|
dbg("windrv: calling entry point at %04X:%04X\n", epSel, epOff);
|
|
uint32_t epResult = thunkCall16(&gThunkCtx, epSel, epOff, NULL, 0);
|
|
dbg("windrv: entry point returned %u\n", (uint16_t)epResult);
|
|
}
|
|
}
|
|
|
|
setError(WDRV_OK);
|
|
return drv;
|
|
}
|
|
|
|
|
|
void wdrvUnloadDriver(WdrvHandleT handle)
|
|
{
|
|
if (!handle) {
|
|
return;
|
|
}
|
|
|
|
// Free cursor allocation if present
|
|
if (handle->cursorSel) {
|
|
free16BitBlock(handle->cursorSel, handle->cursorLinear);
|
|
handle->cursorSel = 0;
|
|
handle->cursorLinear = 0;
|
|
handle->cursorAllocSize = 0;
|
|
}
|
|
|
|
freeDrawObjects(handle);
|
|
// PDEVICE and other objects are in DGROUP - freed by neUnloadModule
|
|
neUnloadModule(&handle->neMod);
|
|
free(handle);
|
|
}
|
|
|
|
|
|
int32_t wdrvGetInfo(WdrvHandleT handle, WdrvInfoT *info)
|
|
{
|
|
if (!handle) {
|
|
return WDRV_ERR_NOT_LOADED;
|
|
}
|
|
|
|
memset(info, 0, sizeof(WdrvInfoT));
|
|
memcpy(info->driverName, handle->neMod.moduleName, sizeof(info->driverName) - 1);
|
|
info->driverName[sizeof(info->driverName) - 1] = '\0';
|
|
|
|
// If we've queried GDIINFO, fill in from that
|
|
if (handle->gdiInfoValid) {
|
|
info->driverVersion = handle->gdiInfo.dpVersion;
|
|
info->maxWidth = handle->gdiInfo.dpHorzRes;
|
|
info->maxHeight = handle->gdiInfo.dpVertRes;
|
|
info->maxBpp = handle->gdiInfo.dpBitsPixel * handle->gdiInfo.dpPlanes;
|
|
info->numColors = handle->gdiInfo.dpNumColors;
|
|
info->rasterCaps = handle->gdiInfo.dpRaster;
|
|
}
|
|
|
|
info->hasBitBlt = handle->ddiEntry[DDI_ORD_BITBLT].present;
|
|
info->hasOutput = handle->ddiEntry[DDI_ORD_OUTPUT].present;
|
|
info->hasPixel = handle->ddiEntry[DDI_ORD_PIXEL].present;
|
|
info->hasStretchBlt = handle->ddiEntry[DDI_ORD_STRETCHBLT].present;
|
|
info->hasExtTextOut = handle->ddiEntry[DDI_ORD_EXTTEXTOUT].present;
|
|
info->hasSetPalette = handle->ddiEntry[DDI_ORD_SETPALETTE].present;
|
|
info->hasSetCursor = handle->ddiEntry[DDI_ORD_SETCURSOR].present;
|
|
info->hasMoveCursor = handle->ddiEntry[DDI_ORD_MOVECURSOR].present;
|
|
info->hasScanLR = handle->ddiEntry[DDI_ORD_SCANLR].present;
|
|
info->hasGetCharWidth = handle->ddiEntry[DDI_ORD_GETCHARWIDTH].present;
|
|
info->hasCreateBitmap = handle->ddiEntry[DDI_ORD_CREATEBITMAP].present;
|
|
|
|
return WDRV_OK;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Mode setting
|
|
// ============================================================================
|
|
|
|
int32_t wdrvEnable(WdrvHandleT handle, int32_t width, int32_t height, int32_t bpp)
|
|
{
|
|
if (!handle) {
|
|
return WDRV_ERR_NOT_LOADED;
|
|
}
|
|
|
|
(void)width;
|
|
(void)height;
|
|
(void)bpp;
|
|
|
|
// Allocate the PDEVICE structure
|
|
if (!allocPDevice(handle)) {
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return gLastError;
|
|
}
|
|
|
|
// Allocate draw mode and physical objects
|
|
if (!allocDrawMode(handle)) {
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return gLastError;
|
|
}
|
|
|
|
if (!allocBrushBuffers(handle)) {
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return gLastError;
|
|
}
|
|
|
|
if (!allocPenBuffers(handle)) {
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return gLastError;
|
|
}
|
|
|
|
// ================================================================
|
|
// Enable the display driver (DDK standard order).
|
|
//
|
|
// WORD PASCAL Enable(LPDEVICE lpDevice, WORD style,
|
|
// LPSTR lpDeviceType, LPSTR lpOutputFile,
|
|
// LPGDIINFO lpData)
|
|
//
|
|
// Per the DDK and VBESVGA source, the correct call order is:
|
|
//
|
|
// Step 1: Enable(gdiInfoBuf, style=1/InquireInfo) — returns GDIINFO
|
|
// lpDevice is a GDIINFO-sized buffer (NOT the PDEVICE).
|
|
// The driver reads SYSTEM.INI settings and returns mode info.
|
|
//
|
|
// Step 2: Enable(pdevBuf, style=0/EnableDevice) — initializes device
|
|
// lpDevice is the PDEVICE buffer. The driver copies its
|
|
// physical device template there and sets the video mode.
|
|
// ================================================================
|
|
|
|
// Allocate a 16-bit "DISPLAY" string for lpDeviceType
|
|
uint32_t devTypeLin;
|
|
uint16_t devTypeSel = alloc16BitBlock(16, &devTypeLin);
|
|
if (devTypeSel) {
|
|
memcpy((void *)devTypeLin, "DISPLAY", 8);
|
|
}
|
|
|
|
uint16_t dgSel = handle->neMod.autoDataSel;
|
|
uint16_t params[9];
|
|
|
|
// ================================================================
|
|
// Step 1: Enable(style=1/InquireInfo) — get GDIINFO
|
|
//
|
|
// lpDevice = separate GDIINFO buffer (driver writes GDIINFO here).
|
|
// [0x8894] starts at 0x00, so S3 driver runs full mode selection
|
|
// (reads SCREEN-SIZE, COLOR-FORMAT, etc. from SYSTEM.INI).
|
|
// ================================================================
|
|
// Allocate 256 bytes — some drivers (e.g. S3) write extended
|
|
// GDIINFO fields beyond the standard 108-byte structure.
|
|
uint32_t gdiInfoLinear;
|
|
uint16_t gdiInfoSel = alloc16BitBlock(256, &gdiInfoLinear);
|
|
if (gdiInfoSel == 0) {
|
|
if (devTypeSel) {
|
|
free16BitBlock(devTypeSel, devTypeLin);
|
|
}
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return gLastError;
|
|
}
|
|
|
|
params[0] = gdiInfoSel; // lpDevice = GDIINFO buffer (NOT PDEVICE!)
|
|
params[1] = 0;
|
|
params[2] = ENABLE_ENABLE; // style = 1 (InquireInfo)
|
|
params[3] = devTypeSel; // lpDeviceType = "DISPLAY"
|
|
params[4] = 0;
|
|
params[5] = 0; // lpOutputFile = NULL
|
|
params[6] = 0;
|
|
params[7] = 0; // lpData = NULL
|
|
params[8] = 0;
|
|
|
|
dbg("windrv: calling Enable(style=1, InquireInfo)\n");
|
|
|
|
uint32_t result = thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_ENABLE].sel,
|
|
handle->ddiEntry[DDI_ORD_ENABLE].off,
|
|
params, 9);
|
|
|
|
logErr("windrv: Enable(style=1) returned %u\n", (uint16_t)result);
|
|
|
|
// Read GDIINFO from the buffer
|
|
memcpy(&handle->gdiInfo, (void *)gdiInfoLinear, sizeof(GdiInfo16T));
|
|
handle->gdiInfoValid = true;
|
|
free16BitBlock(gdiInfoSel, gdiInfoLinear);
|
|
|
|
logErr("windrv: GDIINFO: %dx%d %dbpp %dplanes, PDEVICE size=%d\n",
|
|
handle->gdiInfo.dpHorzRes, handle->gdiInfo.dpVertRes,
|
|
handle->gdiInfo.dpBitsPixel, handle->gdiInfo.dpPlanes,
|
|
handle->gdiInfo.dpDEVICEsize);
|
|
|
|
// ================================================================
|
|
// For VGA-class drivers (1bpp, 4 planes), repatch __WINFLAGS from
|
|
// WF_ENHANCED to WF_STANDARD. VGA.DRV's physical_enable hangs in
|
|
// Enhanced mode because it tries to communicate with the VDD.
|
|
// ================================================================
|
|
if (handle->gdiInfoValid &&
|
|
handle->gdiInfo.dpBitsPixel == 1 && handle->gdiInfo.dpPlanes == 4) {
|
|
uint16_t enhFlags = WF_PMODE | WF_CPU386 | WF_ENHANCED;
|
|
uint16_t stdFlags = WF_PMODE | WF_CPU386 | WF_STANDARD;
|
|
patchWinFlags(handle, enhFlags, stdFlags);
|
|
}
|
|
|
|
// ================================================================
|
|
// Step 2: Enable(style=0/EnableDevice) — initialize PDEVICE + mode
|
|
//
|
|
// lpDevice = the PDEVICE buffer. The driver copies its physical
|
|
// device template there and calls physical_enable (sets INT 10h
|
|
// video mode, initializes hardware).
|
|
// ================================================================
|
|
params[0] = dgSel;
|
|
params[1] = handle->pdevOff;
|
|
params[2] = ENABLE_INQUIRE; // style = 0 (EnableDevice)
|
|
params[3] = devTypeSel; // lpDeviceType = "DISPLAY"
|
|
params[4] = 0;
|
|
params[5] = 0; // lpOutputFile = NULL
|
|
params[6] = 0;
|
|
params[7] = 0; // lpData = NULL
|
|
params[8] = 0;
|
|
|
|
dbg("windrv: calling Enable(style=0, EnableDevice)\n");
|
|
|
|
result = thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_ENABLE].sel,
|
|
handle->ddiEntry[DDI_ORD_ENABLE].off,
|
|
params, 9);
|
|
|
|
logErr("windrv: Enable(style=0) returned %u\n", (uint16_t)result);
|
|
|
|
if (devTypeSel) {
|
|
free16BitBlock(devTypeSel, devTypeLin);
|
|
}
|
|
|
|
if ((uint16_t)result == 0) {
|
|
setError(WDRV_ERR_ENABLE_FAILED);
|
|
return gLastError;
|
|
}
|
|
|
|
// Log PDEVICE after EnableDevice
|
|
{
|
|
uint16_t crtcBase = (inportb(0x3CC) & 0x01) ? 0x3D4 : 0x3B4;
|
|
outportb(crtcBase, 0x13);
|
|
uint8_t cr13 = inportb(crtcBase + 1);
|
|
logErr("windrv: CR13 after Enable(style=0): 0x%02X (pitch=%u)\n",
|
|
cr13, (uint16_t)cr13 * 8);
|
|
}
|
|
|
|
{
|
|
DibPDevice16T *pd = (DibPDevice16T *)handle->pdevLinear;
|
|
logErr("windrv: PDEVICE: deType=0x%04X deWidth=%u deHeight=%u "
|
|
"deWidthBytes=%u dePlanes=%u deBitsPixel=%u\n",
|
|
pd->deType, pd->deWidth, pd->deHeight,
|
|
pd->deWidthBytes, pd->dePlanes, pd->deBitsPixel);
|
|
|
|
// Dump all PDEVICE bytes
|
|
uint8_t *pdb = (uint8_t *)handle->pdevLinear;
|
|
uint32_t pdSize = handle->pdevSize < 64 ? handle->pdevSize : 64;
|
|
logErr("windrv: PDEVICE hex (%lu bytes):", (unsigned long)pdSize);
|
|
for (uint32_t bi = 0; bi < pdSize; bi++) {
|
|
logErr(" %02X", pdb[bi]);
|
|
}
|
|
logErr("\n");
|
|
|
|
// If EnableDevice left deWidth/deHeight/deBitsPixel as zero,
|
|
// fill them from GDIINFO
|
|
if (pd->deWidth == 0 && handle->gdiInfoValid) {
|
|
pd->deWidth = (uint16_t)handle->gdiInfo.dpHorzRes;
|
|
}
|
|
if (pd->deHeight == 0 && handle->gdiInfoValid) {
|
|
pd->deHeight = (uint16_t)handle->gdiInfo.dpVertRes;
|
|
}
|
|
if (pd->deBitsPixel == 0 && handle->gdiInfoValid) {
|
|
pd->deBitsPixel = (uint8_t)handle->gdiInfo.dpBitsPixel;
|
|
}
|
|
}
|
|
|
|
// Query DAC width via VBE 4F08 subfunc 01 (Get DAC Palette Width).
|
|
// Standard VGA = 6 bits, some SVGA cards support 8 bits.
|
|
handle->dacWidth = 6; // default
|
|
{
|
|
__dpmi_regs vr;
|
|
memset(&vr, 0, sizeof(vr));
|
|
vr.x.ax = 0x4F08;
|
|
vr.h.bl = 0x01; // Get DAC palette width
|
|
__dpmi_int(0x10, &vr);
|
|
if ((vr.x.ax & 0xFF) == 0x4F) {
|
|
handle->dacWidth = vr.h.bh;
|
|
}
|
|
logErr("windrv: DAC width: %u bits\n", handle->dacWidth);
|
|
}
|
|
|
|
// Query current VBE mode for diagnostics
|
|
{
|
|
__dpmi_regs vr;
|
|
memset(&vr, 0, sizeof(vr));
|
|
vr.x.ax = 0x4F03; // VBE Return Current VBE Mode
|
|
__dpmi_int(0x10, &vr);
|
|
logErr("windrv: VBE current mode: AX=%04X BX=%04X (mode=0x%03X)\n",
|
|
vr.x.ax, vr.x.bx, vr.x.bx & 0x3FFF);
|
|
|
|
uint16_t crtcBase = (inportb(0x3CC) & 0x01) ? 0x3D4 : 0x3B4;
|
|
outportb(crtcBase, 0x13);
|
|
uint8_t cr13 = inportb(crtcBase + 1);
|
|
logErr("windrv: CR13 after Enable complete: 0x%02X (pitch=%u)\n",
|
|
cr13, (uint16_t)cr13 * 8);
|
|
|
|
// Read display start address (CR0C:CR0D + S3 extensions CR31, CR51, CR69)
|
|
outportb(crtcBase, 0x0C);
|
|
uint8_t cr0c = inportb(crtcBase + 1);
|
|
outportb(crtcBase, 0x0D);
|
|
uint8_t cr0d = inportb(crtcBase + 1);
|
|
outportb(crtcBase, 0x31);
|
|
uint8_t cr31 = inportb(crtcBase + 1);
|
|
outportb(crtcBase, 0x51);
|
|
uint8_t cr51 = inportb(crtcBase + 1);
|
|
uint32_t dispStart = ((uint32_t)cr0c << 8) | cr0d;
|
|
dispStart |= ((uint32_t)(cr31 & 0x30)) << 12; // bits 17:16
|
|
dispStart |= ((uint32_t)(cr51 & 0x03)) << 18; // bits 19:18
|
|
logErr("windrv: display start: CR0C=0x%02X CR0D=0x%02X CR31=0x%02X CR51=0x%02X -> offset 0x%lX (byte %lu)\n",
|
|
cr0c, cr0d, cr31, cr51, (unsigned long)dispStart, (unsigned long)(dispStart * 4));
|
|
}
|
|
|
|
// Check that our pre-allocated PDEVICE is large enough
|
|
if (handle->gdiInfo.dpDEVICEsize > 0 &&
|
|
(uint32_t)handle->gdiInfo.dpDEVICEsize > handle->pdevSize) {
|
|
logErr("windrv: PDEVICE too small (%u < %d), max is %d\n",
|
|
(unsigned)handle->pdevSize, handle->gdiInfo.dpDEVICEsize,
|
|
PDEVICE_MAX_SIZE);
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return gLastError;
|
|
}
|
|
|
|
// Try to set up a default draw mode
|
|
DrawMode16T *dm = (DrawMode16T *)handle->drawModeLinear;
|
|
dm->rop2 = R2_COPYPEN;
|
|
dm->bkMode = BM_OPAQUE;
|
|
dm->bkColor = 0x00FFFFFF;
|
|
dm->textColor = 0x00000000;
|
|
|
|
// Map video RAM for direct access.
|
|
// Query VBE to get the linear framebuffer physical address and total
|
|
// VRAM size, then map the FULL VRAM via DPMI 0800h. The driver's own
|
|
// Enable only maps the visible framebuffer, but other DDI functions
|
|
// (e.g. SetPalette) access off-screen VRAM areas that need to be mapped.
|
|
handle->vramPhysAddr = 0xA0000;
|
|
handle->vramSize = 0x10000;
|
|
{
|
|
// Get current VBE mode number
|
|
__dpmi_regs vr;
|
|
memset(&vr, 0, sizeof(vr));
|
|
vr.x.ax = 0x4F03;
|
|
__dpmi_int(0x10, &vr);
|
|
uint16_t curMode = vr.x.bx & 0x3FFF;
|
|
|
|
if (vr.x.ax == 0x004F && curMode >= 0x100) {
|
|
// Query VBE controller info for total VRAM
|
|
unsigned long tbuf = __tb & 0xFFFFF;
|
|
uint16_t tbSeg = (uint16_t)(tbuf >> 4);
|
|
uint16_t tbOff = (uint16_t)(tbuf & 0x0F);
|
|
|
|
memset(&vr, 0, sizeof(vr));
|
|
vr.x.ax = 0x4F00;
|
|
vr.x.es = tbSeg;
|
|
vr.x.di = tbOff;
|
|
// Write "VBE2" signature to get VBE 2.0+ info
|
|
dosmemput("VBE2", 4, tbuf);
|
|
__dpmi_int(0x10, &vr);
|
|
|
|
uint32_t totalVram = 0;
|
|
if (vr.x.ax == 0x004F) {
|
|
uint16_t mem64k;
|
|
dosmemget(tbuf + 0x12, 2, &mem64k);
|
|
totalVram = (uint32_t)mem64k * 65536UL;
|
|
dbg("windrv: VBE total VRAM: %" PRIu32 " bytes (%" PRIu32 " KB)\n",
|
|
totalVram, totalVram / 1024);
|
|
}
|
|
|
|
// Query mode info for LFB physical base
|
|
memset(&vr, 0, sizeof(vr));
|
|
vr.x.ax = 0x4F01;
|
|
vr.x.cx = curMode;
|
|
vr.x.es = tbSeg;
|
|
vr.x.di = tbOff;
|
|
__dpmi_int(0x10, &vr);
|
|
|
|
if (vr.x.ax == 0x004F) {
|
|
uint32_t physBase;
|
|
dosmemget(tbuf + 0x28, 4, &physBase);
|
|
dbg("windrv: VBE LFB physical base: 0x%08lX\n", (unsigned long)physBase);
|
|
|
|
if (physBase != 0) {
|
|
handle->vramPhysAddr = physBase;
|
|
// Map at least 4MB even if VBE reports less — drivers
|
|
// access off-screen VRAM (cursor masks, palette tables,
|
|
// pattern caches) beyond the visible framebuffer.
|
|
if (totalVram < 4UL * 1024 * 1024) {
|
|
totalVram = 4UL * 1024 * 1024;
|
|
}
|
|
handle->vramSize = totalVram;
|
|
dbg("windrv: VRAM size after fixup: 0x%lX\n",
|
|
(unsigned long)handle->vramSize);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Map physical VRAM for direct access
|
|
__dpmi_meminfo mi;
|
|
mi.address = handle->vramPhysAddr;
|
|
mi.size = handle->vramSize;
|
|
if (__dpmi_physical_address_mapping(&mi) == 0) {
|
|
handle->vramLinear = mi.address;
|
|
handle->vramPtr = (void *)(mi.address + __djgpp_conventional_base);
|
|
dbg("windrv: mapped VRAM: phys=0x%08lX size=0x%lX linear=0x%08lX\n",
|
|
(unsigned long)handle->vramPhysAddr,
|
|
(unsigned long)handle->vramSize,
|
|
(unsigned long)handle->vramLinear);
|
|
}
|
|
|
|
handle->pitch = handle->gdiInfo.dpHorzRes *
|
|
((handle->gdiInfo.dpBitsPixel + 7) / 8);
|
|
|
|
// Realize a default white brush
|
|
if (handle->ddiEntry[DDI_ORD_REALIZEOBJECT].present) {
|
|
if (!realizeBrush(handle, 0x00FFFFFF)) {
|
|
dbg("windrv: warning: initial RealizeObject(brush) failed\n");
|
|
}
|
|
}
|
|
|
|
// Check if this is a hardware (S3-style) or software (DIB) driver.
|
|
// deType == 0xFFFF indicates a DIB engine / software renderer.
|
|
DibPDevice16T *pd = (DibPDevice16T *)handle->pdevLinear;
|
|
bool isHardwareDriver = (pd->deType >= 0);
|
|
|
|
// Detect S3 hardware by probing the chip ID register (CR30).
|
|
// Only S3 chips need cursor disable and display start offset.
|
|
outportb(0x3D4, 0x38);
|
|
outportb(0x3D5, 0x48); // unlock S3 registers
|
|
outportb(0x3D4, 0x30);
|
|
uint8_t cr30 = inportb(0x3D5);
|
|
bool isS3 = (cr30 >= 0x81 && cr30 <= 0xE1);
|
|
handle->isS3 = isS3;
|
|
gIsS3 = isS3;
|
|
dbg("windrv: S3 chip ID probe: CR30=0x%02X isS3=%d\n", cr30, isS3);
|
|
|
|
// VGA-class drivers (1bpp, 4 planes) run as basic VGA even on S3
|
|
// hardware — they don't use the S3 accelerator or scratch area.
|
|
bool isVgaClass = handle->gdiInfoValid &&
|
|
handle->gdiInfo.dpBitsPixel == 1 &&
|
|
handle->gdiInfo.dpPlanes == 4;
|
|
|
|
if (isHardwareDriver && isS3 && !isVgaClass) {
|
|
// Disable the hardware cursor. S3 Trio64 (and compatible) drivers
|
|
// may enable a default cursor during Enable that we don't manage.
|
|
// CR45 bit 0 = hardware cursor enable on S3.
|
|
outportb(0x3D4, 0x45);
|
|
outportb(0x3D5, inportb(0x3D5) & ~0x01);
|
|
|
|
// Shift the visible display down by 10 scanlines so the S3 driver's
|
|
// pattern scratch area at VRAM (144,1)-(151,8) is off-screen.
|
|
// All drawing Y coordinates are offset by dispYOffset to compensate.
|
|
// This just uses slightly more VRAM — the full dpVertRes is usable.
|
|
handle->dispYOffset = 10;
|
|
setDisplayStart(handle, (uint32_t)handle->dispYOffset * handle->pitch);
|
|
|
|
// Extend the PDEVICE height by dispYOffset so the driver's internal
|
|
// clipping allows the full logical screen. Logical y=0..599 maps
|
|
// to VRAM y=10..609, so the driver needs to accept up to y=609.
|
|
DibPDevice16T *pd = (DibPDevice16T *)handle->pdevLinear;
|
|
pd->deHeight += handle->dispYOffset;
|
|
} else {
|
|
handle->dispYOffset = 0;
|
|
}
|
|
|
|
handle->enabled = true;
|
|
|
|
setError(WDRV_OK);
|
|
return WDRV_OK;
|
|
}
|
|
|
|
|
|
int32_t wdrvDisable(WdrvHandleT handle)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
|
|
// Call Disable(lpDevice)
|
|
// VOID PASCAL Disable(LPDEVICE lpDevice)
|
|
// 1 far pointer = 2 words
|
|
uint16_t params[2];
|
|
params[0] = handle->neMod.autoDataSel; // lpDevice seg (DGROUP)
|
|
params[1] = handle->pdevOff; // lpDevice off
|
|
|
|
dbg("windrv: calling Disable()\n");
|
|
|
|
// Reset display start to 0 before Disable restores text mode
|
|
if (handle->dispYOffset != 0) {
|
|
setDisplayStart(handle, 0);
|
|
handle->dispYOffset = 0;
|
|
}
|
|
|
|
waitForEngine();
|
|
|
|
thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_DISABLE].sel,
|
|
handle->ddiEntry[DDI_ORD_DISABLE].off,
|
|
params, 2);
|
|
|
|
dbg("windrv: Disable() returned\n");
|
|
|
|
handle->enabled = false;
|
|
|
|
setError(WDRV_OK);
|
|
return WDRV_OK;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Drawing operations
|
|
// ============================================================================
|
|
|
|
int32_t wdrvBitBlt(WdrvHandleT handle, WdrvBitBltParamsT *p)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
logErr("windrv: BitBlt: not enabled (handle=%p enabled=%d)\n",
|
|
(void *)handle, handle ? handle->enabled : -1);
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
if (!handle->ddiEntry[DDI_ORD_BITBLT].present) {
|
|
logErr("windrv: BitBlt: not present\n");
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
// BOOL PASCAL BitBlt(LPDEVICE lpDstDev, WORD DstX, WORD DstY,
|
|
// LPDEVICE lpSrcDev, WORD SrcX, WORD SrcY,
|
|
// WORD xExt, WORD yExt, DWORD Rop3,
|
|
// LPBRUSH lpBrush, LPDRAWMODE lpDrawMode)
|
|
//
|
|
// Pascal push order (left to right):
|
|
// lpDstDev(2w), DstX(1w), DstY(1w),
|
|
// lpSrcDev(2w), SrcX(1w), SrcY(1w),
|
|
// xExt(1w), yExt(1w), Rop3(2w),
|
|
// lpBrush(2w), lpDrawMode(2w)
|
|
// Total: 16 words
|
|
|
|
uint16_t dgSel = handle->neMod.autoDataSel;
|
|
uint16_t params[16];
|
|
int i = 0;
|
|
|
|
// Determine if the ROP uses the source. The 8-bit ROP is in bits 23-16.
|
|
// If flipping the source bit doesn't change any output bit, source is
|
|
// not used and lpSrcDev must be NULL per the DDI spec.
|
|
uint8_t rop8 = (uint8_t)(p->rop3 >> 16);
|
|
bool ropNeedsSrc = (((rop8 >> 2) ^ rop8) & 0x33) != 0;
|
|
|
|
// lpDstDev
|
|
params[i++] = dgSel;
|
|
params[i++] = handle->pdevOff;
|
|
// DstX, DstY (offset Y into hidden-scanline region)
|
|
params[i++] = (uint16_t)p->dstX;
|
|
params[i++] = (uint16_t)(p->dstY + handle->dispYOffset);
|
|
// lpSrcDev (NULL for pattern-only ROPs, screen PDEVICE otherwise)
|
|
if (ropNeedsSrc) {
|
|
params[i++] = dgSel;
|
|
params[i++] = handle->pdevOff;
|
|
} else {
|
|
params[i++] = 0;
|
|
params[i++] = 0;
|
|
}
|
|
// SrcX, SrcY (offset Y for screen-to-screen blits)
|
|
params[i++] = (uint16_t)p->srcX;
|
|
params[i++] = (uint16_t)(p->srcY + handle->dispYOffset);
|
|
// xExt, yExt
|
|
params[i++] = (uint16_t)p->width;
|
|
params[i++] = (uint16_t)p->height;
|
|
// Rop3 (DWORD: high word first in Pascal push order)
|
|
params[i++] = (uint16_t)(p->rop3 >> 16);
|
|
params[i++] = (uint16_t)(p->rop3 & 0xFFFF);
|
|
// lpBrush
|
|
params[i++] = dgSel;
|
|
params[i++] = handle->brushOff;
|
|
// lpDrawMode
|
|
params[i++] = dgSel;
|
|
params[i++] = handle->drawModeOff;
|
|
|
|
dbg("windrv: BitBlt dst=%04X:%04X (%d,%d) src=%04X:%04X (%d,%d) %dx%d rop=0x%08lX brush=%04X:%04X dm=%04X:%04X\n",
|
|
dgSel, handle->pdevOff, p->dstX, p->dstY,
|
|
ropNeedsSrc ? dgSel : 0, ropNeedsSrc ? handle->pdevOff : 0,
|
|
p->srcX, p->srcY,
|
|
p->width, p->height, (unsigned long)p->rop3,
|
|
dgSel, handle->brushOff, dgSel, handle->drawModeOff);
|
|
|
|
waitForEngine();
|
|
|
|
uint32_t result = thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_BITBLT].sel,
|
|
handle->ddiEntry[DDI_ORD_BITBLT].off,
|
|
params, i);
|
|
|
|
waitForEngine();
|
|
|
|
dbg("windrv: BitBlt returned %lu\n", (unsigned long)(result & 0xFFFF));
|
|
|
|
return ((int16_t)(result & 0xFFFF)) ? WDRV_OK : WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
|
|
int32_t wdrvFillRect(WdrvHandleT handle, int16_t x, int16_t y, int16_t w, int16_t h, uint32_t color)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
|
|
// Realize brush with the requested color
|
|
if (!handle->brushRealized || handle->brushRealizedColor != color) {
|
|
if (handle->ddiEntry[DDI_ORD_REALIZEOBJECT].present) {
|
|
realizeBrush(handle, color);
|
|
}
|
|
}
|
|
|
|
// If driver supports BitBlt, use PATCOPY
|
|
if (handle->ddiEntry[DDI_ORD_BITBLT].present) {
|
|
WdrvBitBltParamsT bp;
|
|
memset(&bp, 0, sizeof(bp));
|
|
bp.dstX = x;
|
|
bp.dstY = y;
|
|
bp.srcX = 0;
|
|
bp.srcY = 0;
|
|
bp.width = w;
|
|
bp.height = h;
|
|
bp.rop3 = PATCOPY;
|
|
return wdrvBitBlt(handle, &bp);
|
|
}
|
|
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
|
|
int32_t wdrvSetPixel(WdrvHandleT handle, int16_t x, int16_t y, uint32_t color)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
if (!handle->ddiEntry[DDI_ORD_PIXEL].present) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
// DWORD PASCAL Pixel(LPDEVICE lpDevice, WORD x, WORD y,
|
|
// DWORD color, LPDRAWMODE lpDrawMode)
|
|
// Pascal push order:
|
|
// lpDevice(2w), x(1w), y(1w), color(2w), lpDrawMode(2w)
|
|
// Total: 8 words
|
|
|
|
// Convert COLORREF to physical color via ColorInfo DDI
|
|
uint32_t physColor = colorToPhys(handle, color);
|
|
|
|
// Set draw mode to COPYPEN for setting pixels
|
|
DrawMode16T *dm = (DrawMode16T *)handle->drawModeLinear;
|
|
dm->rop2 = R2_COPYPEN;
|
|
|
|
uint16_t dgSel = handle->neMod.autoDataSel;
|
|
uint16_t params[8];
|
|
int i = 0;
|
|
params[i++] = dgSel; // lpDevice seg
|
|
params[i++] = handle->pdevOff; // lpDevice off
|
|
params[i++] = (uint16_t)x; // x
|
|
params[i++] = (uint16_t)(y + handle->dispYOffset); // y (offset)
|
|
params[i++] = (uint16_t)(physColor >> 16); // color high
|
|
params[i++] = (uint16_t)(physColor); // color low
|
|
params[i++] = dgSel; // lpDrawMode seg
|
|
params[i++] = handle->drawModeOff; // lpDrawMode off
|
|
|
|
waitForEngine();
|
|
|
|
thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_PIXEL].sel,
|
|
handle->ddiEntry[DDI_ORD_PIXEL].off,
|
|
params, i);
|
|
|
|
waitForEngine();
|
|
|
|
return WDRV_OK;
|
|
}
|
|
|
|
|
|
uint32_t wdrvGetPixel(WdrvHandleT handle, int16_t x, int16_t y)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return 0;
|
|
}
|
|
if (!handle->ddiEntry[DDI_ORD_PIXEL].present) {
|
|
return 0;
|
|
}
|
|
|
|
// Pixel with color = -1 (0xFFFFFFFF) reads instead of writes
|
|
DrawMode16T *dm = (DrawMode16T *)handle->drawModeLinear;
|
|
dm->rop2 = R2_COPYPEN;
|
|
|
|
uint16_t dgSel = handle->neMod.autoDataSel;
|
|
uint16_t params[8];
|
|
int i = 0;
|
|
params[i++] = dgSel;
|
|
params[i++] = handle->pdevOff;
|
|
params[i++] = (uint16_t)x;
|
|
params[i++] = (uint16_t)(y + handle->dispYOffset);
|
|
params[i++] = 0xFFFF; // color = -1 means "get pixel"
|
|
params[i++] = 0xFFFF;
|
|
params[i++] = dgSel;
|
|
params[i++] = handle->drawModeOff;
|
|
|
|
return thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_PIXEL].sel,
|
|
handle->ddiEntry[DDI_ORD_PIXEL].off,
|
|
params, i);
|
|
}
|
|
|
|
|
|
static void drawStyledLine(struct WdrvDriverS *drv, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint32_t color, const int16_t *pattern, int16_t patLen, int16_t *patIdx, int16_t *patRemain)
|
|
{
|
|
int16_t dx = (x1 > x0) ? (x1 - x0) : (x0 - x1);
|
|
int16_t dy = (y1 > y0) ? (y1 - y0) : (y0 - y1);
|
|
int16_t sx = (x0 < x1) ? 1 : -1;
|
|
int16_t sy = (y0 < y1) ? 1 : -1;
|
|
int16_t err = dx - dy;
|
|
|
|
int16_t cx = x0;
|
|
int16_t cy = y0;
|
|
|
|
for (;;) {
|
|
// Even index = draw, odd index = skip
|
|
if ((*patIdx & 1) == 0) {
|
|
wdrvSetPixel((WdrvHandleT)drv, cx, cy, color);
|
|
}
|
|
|
|
// Advance pattern state
|
|
(*patRemain)--;
|
|
if (*patRemain <= 0) {
|
|
*patIdx = (*patIdx + 1) % patLen;
|
|
*patRemain = pattern[*patIdx];
|
|
}
|
|
|
|
if (cx == x1 && cy == y1) {
|
|
break;
|
|
}
|
|
|
|
int16_t e2 = 2 * err;
|
|
if (e2 > -dy) {
|
|
err -= dy;
|
|
cx += sx;
|
|
}
|
|
if (e2 < dx) {
|
|
err += dx;
|
|
cy += sy;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static int32_t drawStyledPolyline(struct WdrvDriverS *drv, Point16T *points, int16_t count, uint32_t color, int16_t penStyle)
|
|
{
|
|
const int16_t *pattern;
|
|
int16_t patLen;
|
|
|
|
switch (penStyle) {
|
|
case PS_DASH:
|
|
pattern = gPatDash;
|
|
patLen = 2;
|
|
break;
|
|
case PS_DOT:
|
|
pattern = gPatDot;
|
|
patLen = 2;
|
|
break;
|
|
case PS_DASHDOT:
|
|
pattern = gPatDashDot;
|
|
patLen = 4;
|
|
break;
|
|
case PS_DASHDOTDOT:
|
|
pattern = gPatDashDotDot;
|
|
patLen = 6;
|
|
break;
|
|
default:
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
int16_t patIdx = 0;
|
|
int16_t patRemain = pattern[0];
|
|
|
|
for (int16_t i = 0; i < count - 1; i++) {
|
|
drawStyledLine(drv, points[i].x, points[i].y, points[i + 1].x, points[i + 1].y, color, pattern, patLen, &patIdx, &patRemain);
|
|
}
|
|
|
|
return WDRV_OK;
|
|
}
|
|
|
|
|
|
int32_t wdrvPolyline(WdrvHandleT handle, Point16T *points, int16_t count, uint32_t color)
|
|
{
|
|
return wdrvPolylineEx(handle, points, count, color, PS_SOLID);
|
|
}
|
|
|
|
|
|
int32_t wdrvPolylineEx(WdrvHandleT handle, Point16T *points, int16_t count, uint32_t color, int16_t penStyle)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
// Non-SOLID pen styles always use software rendering.
|
|
// S3TRIO silently accepts styled pens but doesn't render them;
|
|
// software Bresenham gives identical output on all drivers.
|
|
if (penStyle != PS_SOLID) {
|
|
if (!handle->ddiEntry[DDI_ORD_PIXEL].present) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
return drawStyledPolyline(handle, points, count, color, penStyle);
|
|
}
|
|
|
|
if (!handle->ddiEntry[DDI_ORD_OUTPUT].present) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
// Realize a physical pen (driver expects RealizeObject output, not a logical pen)
|
|
if (!handle->penRealized || handle->penRealizedColor != color || handle->penRealizedStyle != penStyle) {
|
|
if (!realizeStyledPen(handle, color, penStyle)) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
}
|
|
|
|
// Allocate 16-bit memory for points + clip rect, offsetting Y coordinates.
|
|
// DIB engine drivers (VBESVGA) dereference lpClipRect unconditionally.
|
|
uint32_t ptsSize = count * sizeof(Point16T);
|
|
uint16_t clipOff = (uint16_t)((ptsSize + 1) & ~1); // word-align
|
|
uint32_t blockSize = (uint32_t)clipOff + 8; // + RECT (4 WORDs)
|
|
uint32_t ptsLinear;
|
|
uint16_t ptsSel = alloc16BitBlock(blockSize, &ptsLinear);
|
|
if (ptsSel == 0) {
|
|
return WDRV_ERR_NO_MEMORY;
|
|
}
|
|
memcpy((void *)ptsLinear, points, ptsSize);
|
|
{
|
|
Point16T *dst = (Point16T *)ptsLinear;
|
|
for (int16_t pi = 0; pi < count; pi++) {
|
|
dst[pi].y += handle->dispYOffset;
|
|
}
|
|
}
|
|
int16_t *clipRect = (int16_t *)(ptsLinear + clipOff);
|
|
clipRect[0] = 0;
|
|
clipRect[1] = 0;
|
|
clipRect[2] = 0x7FFF;
|
|
clipRect[3] = 0x7FFF;
|
|
|
|
// Output(lpDstDev, style, count, lpPoints, lpPen, lpBrush, lpDrawMode, lpClipRect)
|
|
uint16_t dgSel = handle->neMod.autoDataSel;
|
|
uint16_t params[14];
|
|
int i = 0;
|
|
params[i++] = dgSel;
|
|
params[i++] = handle->pdevOff;
|
|
params[i++] = OS_POLYLINE;
|
|
params[i++] = count;
|
|
params[i++] = ptsSel;
|
|
params[i++] = 0;
|
|
params[i++] = dgSel; // lpPen in DGROUP (physical pen)
|
|
params[i++] = handle->penOff;
|
|
params[i++] = 0; // lpBrush = NULL
|
|
params[i++] = 0;
|
|
params[i++] = dgSel;
|
|
params[i++] = handle->drawModeOff;
|
|
params[i++] = ptsSel; // lpClipRect seg
|
|
params[i++] = clipOff; // lpClipRect off
|
|
|
|
waitForEngine();
|
|
|
|
uint32_t result = thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_OUTPUT].sel,
|
|
handle->ddiEntry[DDI_ORD_OUTPUT].off,
|
|
params, i);
|
|
|
|
waitForEngine();
|
|
|
|
free16BitBlock(ptsSel, ptsLinear);
|
|
|
|
return ((int16_t)(result & 0xFFFF)) ? WDRV_OK : WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
|
|
int32_t wdrvRectangle(WdrvHandleT handle, int16_t x, int16_t y, int16_t w, int16_t h, uint32_t color)
|
|
{
|
|
return wdrvRectangleEx(handle, x, y, w, h, color, PS_SOLID);
|
|
}
|
|
|
|
|
|
int32_t wdrvRectangleEx(WdrvHandleT handle, int16_t x, int16_t y, int16_t w, int16_t h, uint32_t color, int16_t penStyle)
|
|
{
|
|
// Draw a rectangle outline using two polylines (left+top, right+bottom).
|
|
// OS_RECTANGLE crashes on DIB engine drivers (VBESVGA, ET4000) because
|
|
// they expect GDI-level curve decomposition callbacks. Polylines are
|
|
// universally supported.
|
|
Point16T side1[3];
|
|
side1[0].x = x;
|
|
side1[0].y = y + h;
|
|
side1[1].x = x;
|
|
side1[1].y = y;
|
|
side1[2].x = x + w;
|
|
side1[2].y = y;
|
|
|
|
int32_t ret = wdrvPolylineEx(handle, side1, 3, color, penStyle);
|
|
if (ret != WDRV_OK) {
|
|
return ret;
|
|
}
|
|
|
|
Point16T side2[3];
|
|
side2[0].x = x + w;
|
|
side2[0].y = y;
|
|
side2[1].x = x + w;
|
|
side2[1].y = y + h;
|
|
side2[2].x = x;
|
|
side2[2].y = y + h;
|
|
|
|
return wdrvPolylineEx(handle, side2, 3, color, penStyle);
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Palette operations
|
|
// ============================================================================
|
|
|
|
int32_t wdrvSetPalette(WdrvHandleT handle, int32_t startIndex, int32_t count, const uint8_t *colors)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
if (!handle->ddiEntry[DDI_ORD_SETPALETTE].present) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
// SetPalette(nStartIndex:WORD, nNumEntries:WORD, lpPalette:DWORD)
|
|
// Pascal order: nStartIndex(1w), nNumEntries(1w), lpPalette(2w)
|
|
// Total: 4 words
|
|
|
|
// Allocate 16-bit memory for the palette data
|
|
uint32_t palSize = count * 4; // RGBQUAD per entry
|
|
uint32_t palLinear;
|
|
uint16_t palSel = alloc16BitBlock(palSize, &palLinear);
|
|
if (palSel == 0) {
|
|
return WDRV_ERR_NO_MEMORY;
|
|
}
|
|
memcpy((void *)palLinear, colors, palSize);
|
|
|
|
uint16_t params[4];
|
|
params[0] = (uint16_t)startIndex;
|
|
params[1] = (uint16_t)count;
|
|
params[2] = palSel;
|
|
params[3] = 0;
|
|
|
|
thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_SETPALETTE].sel,
|
|
handle->ddiEntry[DDI_ORD_SETPALETTE].off,
|
|
params, 4);
|
|
|
|
free16BitBlock(palSel, palLinear);
|
|
|
|
// Also program the VGA DAC directly. Some drivers (e.g. VBESVGA)
|
|
// update their internal color table via SetPalette but do not
|
|
// program the DAC hardware — in real Windows 3.1, GDI does that
|
|
// separately. Writing the DAC is idempotent, so this is harmless
|
|
// on drivers (like S3TRIO) that already programmed it.
|
|
uint8_t dacShift = 8 - handle->dacWidth; // 2 for 6-bit, 0 for 8-bit
|
|
outportb(0x3C8, (uint8_t)startIndex);
|
|
for (int32_t i = 0; i < count; i++) {
|
|
outportb(0x3C9, colors[i * 4 + 0] >> dacShift);
|
|
outportb(0x3C9, colors[i * 4 + 1] >> dacShift);
|
|
outportb(0x3C9, colors[i * 4 + 2] >> dacShift);
|
|
}
|
|
|
|
return WDRV_OK;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// 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, WdrvFontT font)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
if (!handle->ddiEntry[DDI_ORD_EXTTEXTOUT].present) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
// Use built-in font if none specified
|
|
if (!font) {
|
|
font = wdrvLoadFontBuiltin();
|
|
if (!font) {
|
|
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++] = font->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, font->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;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// ScanLR and Flood Fill
|
|
// ============================================================================
|
|
|
|
int16_t wdrvScanLR(WdrvHandleT handle, int16_t x, int16_t y, uint32_t color, int16_t style)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return -1;
|
|
}
|
|
if (!handle->ddiEntry[DDI_ORD_SCANLR].present) {
|
|
return -1;
|
|
}
|
|
|
|
uint32_t physColor = colorToPhys(handle, color);
|
|
|
|
// WORD ScanLR(LPDEVICE, WORD x, WORD y, DWORD color, WORD style)
|
|
// Pascal push order: lpDevice(2w), x(1w), y(1w), color(2w), style(1w)
|
|
// Total: 7 words
|
|
uint16_t dgSel = handle->neMod.autoDataSel;
|
|
uint16_t params[7];
|
|
int i = 0;
|
|
params[i++] = dgSel;
|
|
params[i++] = handle->pdevOff;
|
|
params[i++] = (uint16_t)x;
|
|
params[i++] = (uint16_t)(y + handle->dispYOffset);
|
|
params[i++] = (uint16_t)(physColor >> 16);
|
|
params[i++] = (uint16_t)(physColor);
|
|
params[i++] = (uint16_t)style;
|
|
|
|
waitForEngine();
|
|
|
|
uint32_t result = thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_SCANLR].sel,
|
|
handle->ddiEntry[DDI_ORD_SCANLR].off,
|
|
params, i);
|
|
|
|
waitForEngine();
|
|
|
|
return (int16_t)(result & 0xFFFF);
|
|
}
|
|
|
|
|
|
// ScanLR with a pre-converted physical color (no colorToPhys round-trip).
|
|
// Used by floodFillSoftware where the seed color comes from GetPixel DX:AX,
|
|
// which is already in the physical format that ScanLR expects.
|
|
|
|
static int32_t floodFillSoftware(struct WdrvDriverS *drv, int16_t x, int16_t y, uint32_t fillColor)
|
|
{
|
|
WdrvHandleT handle = (WdrvHandleT)drv;
|
|
int16_t screenW = drv->gdiInfo.dpHorzRes;
|
|
int16_t screenH = drv->gdiInfo.dpVertRes;
|
|
|
|
if (x < 0 || x >= screenW || y < 0 || y >= screenH) {
|
|
return WDRV_OK;
|
|
}
|
|
|
|
// Determine seed color for ScanLR by probing palette indices.
|
|
// We CANNOT use GetPixel — the ET4000 DIB engine's Pixel DDI with color=-1
|
|
// has a side effect that corrupts the pixel's VRAM representation, causing
|
|
// subsequent ScanLR calls to not match that pixel.
|
|
// Instead, probe with ScanLR(SCAN_RIGHT) at (x,y) for each palette index
|
|
// until we find one that matches (returns > x).
|
|
if (!drv->ddiEntry[DDI_ORD_SCANLR].present) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
uint16_t dgSel = drv->neMod.autoDataSel;
|
|
int32_t seedIdx = -1;
|
|
|
|
for (int32_t idx = 0; idx < 256; idx++) {
|
|
uint32_t color = PALETTEINDEX(idx);
|
|
uint32_t phys = colorToPhys(handle, color);
|
|
uint16_t params[7];
|
|
int pi = 0;
|
|
params[pi++] = dgSel;
|
|
params[pi++] = drv->pdevOff;
|
|
params[pi++] = (uint16_t)x;
|
|
params[pi++] = (uint16_t)(y + drv->dispYOffset);
|
|
params[pi++] = (uint16_t)(phys >> 16);
|
|
params[pi++] = (uint16_t)(phys);
|
|
params[pi++] = (uint16_t)WDRV_SCAN_RIGHT;
|
|
int16_t r = (int16_t)(thunkCall16(&gThunkCtx, drv->ddiEntry[DDI_ORD_SCANLR].sel, drv->ddiEntry[DDI_ORD_SCANLR].off, params, pi) & 0xFFFF);
|
|
if (r > x) {
|
|
seedIdx = idx;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (seedIdx < 0) {
|
|
return WDRV_OK; // Cannot determine seed color
|
|
}
|
|
|
|
// Convert seed and fill to physical colors for ScanLR
|
|
uint32_t seedPhys = colorToPhys(handle, PALETTEINDEX(seedIdx));
|
|
uint32_t fillPhys = colorToPhys(handle, fillColor);
|
|
|
|
// If seed and fill are the same physical color, nothing to do
|
|
if (seedPhys == fillPhys) {
|
|
return WDRV_OK;
|
|
}
|
|
|
|
// Scanline stack flood fill using ScanLR + FillRect only
|
|
typedef struct {
|
|
int16_t x;
|
|
int16_t y;
|
|
} SeedT;
|
|
|
|
#define FLOOD_STACK_SIZE 4096
|
|
static SeedT stack[FLOOD_STACK_SIZE];
|
|
int32_t sp = 0;
|
|
|
|
stack[sp].x = x;
|
|
stack[sp].y = y;
|
|
sp++;
|
|
|
|
while (sp > 0) {
|
|
sp--;
|
|
int16_t cx = stack[sp].x;
|
|
int16_t cy = stack[sp].y;
|
|
|
|
if (cx < 0 || cx >= screenW || cy < 0 || cy >= screenH) {
|
|
continue;
|
|
}
|
|
|
|
// Check if pixel at (cx,cy) still matches seed color
|
|
{
|
|
uint16_t params[7];
|
|
int pi = 0;
|
|
params[pi++] = dgSel;
|
|
params[pi++] = drv->pdevOff;
|
|
params[pi++] = (uint16_t)cx;
|
|
params[pi++] = (uint16_t)(cy + drv->dispYOffset);
|
|
params[pi++] = (uint16_t)(seedPhys >> 16);
|
|
params[pi++] = (uint16_t)(seedPhys);
|
|
params[pi++] = (uint16_t)WDRV_SCAN_RIGHT;
|
|
int16_t r = (int16_t)(thunkCall16(&gThunkCtx, drv->ddiEntry[DDI_ORD_SCANLR].sel, drv->ddiEntry[DDI_ORD_SCANLR].off, params, pi) & 0xFFFF);
|
|
if (r <= cx) {
|
|
continue; // Pixel no longer matches seed
|
|
}
|
|
}
|
|
|
|
// Find left boundary: scan left from cx
|
|
int16_t left;
|
|
{
|
|
uint16_t params[7];
|
|
int pi = 0;
|
|
params[pi++] = dgSel;
|
|
params[pi++] = drv->pdevOff;
|
|
params[pi++] = (uint16_t)cx;
|
|
params[pi++] = (uint16_t)(cy + drv->dispYOffset);
|
|
params[pi++] = (uint16_t)(seedPhys >> 16);
|
|
params[pi++] = (uint16_t)(seedPhys);
|
|
params[pi++] = (uint16_t)WDRV_SCAN_LEFT; // scan left, find not-matching
|
|
left = (int16_t)(thunkCall16(&gThunkCtx, drv->ddiEntry[DDI_ORD_SCANLR].sel, drv->ddiEntry[DDI_ORD_SCANLR].off, params, pi) & 0xFFFF);
|
|
left++; // ScanLR returns the non-matching pixel; span starts one pixel right
|
|
}
|
|
|
|
// Find right boundary: scan right from cx
|
|
int16_t right;
|
|
{
|
|
uint16_t params[7];
|
|
int pi = 0;
|
|
params[pi++] = dgSel;
|
|
params[pi++] = drv->pdevOff;
|
|
params[pi++] = (uint16_t)cx;
|
|
params[pi++] = (uint16_t)(cy + drv->dispYOffset);
|
|
params[pi++] = (uint16_t)(seedPhys >> 16);
|
|
params[pi++] = (uint16_t)(seedPhys);
|
|
params[pi++] = (uint16_t)WDRV_SCAN_RIGHT; // scan right, find not-matching
|
|
right = (int16_t)(thunkCall16(&gThunkCtx, drv->ddiEntry[DDI_ORD_SCANLR].sel, drv->ddiEntry[DDI_ORD_SCANLR].off, params, pi) & 0xFFFF);
|
|
right--; // ScanLR returns the non-matching pixel; span ends one pixel left
|
|
}
|
|
|
|
if (left > right) {
|
|
continue;
|
|
}
|
|
|
|
// Fill the span
|
|
wdrvFillRect(handle, left, cy, right - left + 1, 1, fillColor);
|
|
|
|
// Seed adjacent rows using ScanLR to find spans of seed color
|
|
for (int16_t dir = -1; dir <= 1; dir += 2) {
|
|
int16_t ny = cy + dir;
|
|
if (ny < 0 || ny >= screenH) {
|
|
continue;
|
|
}
|
|
|
|
int16_t sx = left;
|
|
while (sx <= right) {
|
|
// Find next seed-colored pixel in adjacent row
|
|
uint16_t params[7];
|
|
int pi = 0;
|
|
params[pi++] = dgSel;
|
|
params[pi++] = drv->pdevOff;
|
|
params[pi++] = (uint16_t)sx;
|
|
params[pi++] = (uint16_t)(ny + drv->dispYOffset);
|
|
params[pi++] = (uint16_t)(seedPhys >> 16);
|
|
params[pi++] = (uint16_t)(seedPhys);
|
|
params[pi++] = (uint16_t)WDRV_SCAN_RIGHT_MATCH;
|
|
int16_t matchStart = (int16_t)(thunkCall16(&gThunkCtx, drv->ddiEntry[DDI_ORD_SCANLR].sel, drv->ddiEntry[DDI_ORD_SCANLR].off, params, pi) & 0xFFFF);
|
|
|
|
if (matchStart > right || matchStart < sx) {
|
|
break; // No more seed-colored pixels in this row within span
|
|
}
|
|
|
|
// Push one seed point for this sub-span
|
|
if (sp < FLOOD_STACK_SIZE) {
|
|
stack[sp].x = matchStart;
|
|
stack[sp].y = ny;
|
|
sp++;
|
|
}
|
|
|
|
// Skip past this seed-colored run to find the next gap
|
|
params[2] = (uint16_t)matchStart;
|
|
params[6] = (uint16_t)WDRV_SCAN_RIGHT;
|
|
int16_t matchEnd = (int16_t)(thunkCall16(&gThunkCtx, drv->ddiEntry[DDI_ORD_SCANLR].sel, drv->ddiEntry[DDI_ORD_SCANLR].off, params, pi) & 0xFFFF);
|
|
|
|
sx = matchEnd; // Continue from where the non-match was found
|
|
if (matchEnd <= matchStart) {
|
|
break; // Safety: avoid infinite loop
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#undef FLOOD_STACK_SIZE
|
|
return WDRV_OK;
|
|
}
|
|
|
|
|
|
int32_t wdrvFloodFill(WdrvHandleT handle, int16_t x, int16_t y, uint32_t fillColor)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
|
|
// Check if we can use the fast framebuffer path
|
|
bool useFb = (handle->vramPtr != NULL);
|
|
if (handle->gdiInfoValid && handle->gdiInfo.dpPlanes > 1) {
|
|
useFb = false;
|
|
}
|
|
|
|
if (!useFb) {
|
|
// Fall back to software using ScanLR + FillRect (no GetPixel — it
|
|
// corrupts pixel VRAM on ET4000 DIB engine, breaking ScanLR matching)
|
|
if (!handle->ddiEntry[DDI_ORD_SCANLR].present) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
return floodFillSoftware(handle, x, y, fillColor);
|
|
}
|
|
|
|
// Fast path: direct framebuffer access
|
|
uint8_t *fb = (uint8_t *)handle->vramPtr;
|
|
|
|
int16_t screenW = handle->gdiInfo.dpHorzRes;
|
|
int16_t screenH = handle->gdiInfo.dpVertRes;
|
|
int32_t pitch = handle->pitch;
|
|
|
|
if (x < 0 || x >= screenW || y < 0 || y >= screenH) {
|
|
return WDRV_OK;
|
|
}
|
|
|
|
// Read seed pixel from framebuffer
|
|
waitForEngine();
|
|
uint8_t seedColor = fb[(y + handle->dispYOffset) * pitch + x];
|
|
|
|
// If seed is same as fill, nothing to do
|
|
uint32_t fillPhys = colorToPhys(handle, fillColor);
|
|
uint8_t fillIdx = (uint8_t)(fillPhys & 0xFF);
|
|
if (seedColor == fillIdx) {
|
|
return WDRV_OK;
|
|
}
|
|
|
|
// Scanline stack flood fill
|
|
typedef struct {
|
|
int16_t x;
|
|
int16_t y;
|
|
} SeedT;
|
|
|
|
#define FLOOD_STACK_SIZE 4096
|
|
static SeedT stack[FLOOD_STACK_SIZE];
|
|
int32_t sp = 0;
|
|
|
|
stack[sp].x = x;
|
|
stack[sp].y = y;
|
|
sp++;
|
|
|
|
while (sp > 0) {
|
|
sp--;
|
|
int16_t cx = stack[sp].x;
|
|
int16_t cy = stack[sp].y;
|
|
|
|
if (cx < 0 || cx >= screenW || cy < 0 || cy >= screenH) {
|
|
continue;
|
|
}
|
|
|
|
waitForEngine();
|
|
uint8_t *row = fb + (cy + handle->dispYOffset) * pitch;
|
|
if (row[cx] != seedColor) {
|
|
continue;
|
|
}
|
|
|
|
// Scan left
|
|
int16_t left = cx;
|
|
while (left > 0 && row[left - 1] == seedColor) {
|
|
left--;
|
|
}
|
|
|
|
// Scan right
|
|
int16_t right = cx;
|
|
while (right < screenW - 1 && row[right + 1] == seedColor) {
|
|
right++;
|
|
}
|
|
|
|
// Fill the span
|
|
wdrvFillRect(handle, left, cy, right - left + 1, 1, fillColor);
|
|
|
|
// Resync framebuffer after fill
|
|
waitForEngine();
|
|
|
|
// Push seeds for rows above and below
|
|
for (int16_t dir = -1; dir <= 1; dir += 2) {
|
|
int16_t ny = cy + dir;
|
|
if (ny < 0 || ny >= screenH) {
|
|
continue;
|
|
}
|
|
uint8_t *nrow = fb + (ny + handle->dispYOffset) * pitch;
|
|
bool inSpan = false;
|
|
for (int16_t sx = left; sx <= right; sx++) {
|
|
if (nrow[sx] == seedColor) {
|
|
if (!inSpan) {
|
|
if (sp < FLOOD_STACK_SIZE) {
|
|
stack[sp].x = sx;
|
|
stack[sp].y = ny;
|
|
sp++;
|
|
}
|
|
inSpan = true;
|
|
}
|
|
} else {
|
|
inSpan = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#undef FLOOD_STACK_SIZE
|
|
return WDRV_OK;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Text measurement
|
|
// ============================================================================
|
|
|
|
int32_t wdrvGetCharWidths(WdrvHandleT handle, WdrvFontT font, uint8_t firstChar, uint8_t lastChar, int16_t *widths)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
if (!handle->ddiEntry[DDI_ORD_GETCHARWIDTH].present) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
// Use built-in font if none specified
|
|
if (!font) {
|
|
font = wdrvLoadFontBuiltin();
|
|
if (!font) {
|
|
return WDRV_ERR_NO_MEMORY;
|
|
}
|
|
}
|
|
|
|
uint16_t count = (uint16_t)(lastChar - firstChar + 1);
|
|
|
|
// Allocate 16-bit buffer for results
|
|
uint32_t bufSize = count * 2; // array of WORDs
|
|
uint32_t bufLinear;
|
|
uint16_t bufSel = alloc16BitBlock(bufSize, &bufLinear);
|
|
if (bufSel == 0) {
|
|
return WDRV_ERR_NO_MEMORY;
|
|
}
|
|
memset((void *)bufLinear, 0, bufSize);
|
|
|
|
// Set up DrawMode for GetCharWidth
|
|
DrawMode16T *dm = (DrawMode16T *)handle->drawModeLinear;
|
|
dm->rop2 = R2_COPYPEN;
|
|
dm->bkMode = BM_TRANSPARENT;
|
|
|
|
// BOOL GetCharWidth(LPDEVICE, LPINT lpBuffer, WORD firstChar, WORD lastChar,
|
|
// LPFONTINFO, LPDRAWMODE, LPTEXTXFORM)
|
|
// Pascal push order: left to right
|
|
// Total: 12 words
|
|
uint16_t dgSel = handle->neMod.autoDataSel;
|
|
uint16_t params[12];
|
|
int i = 0;
|
|
params[i++] = dgSel; // lpDevice seg
|
|
params[i++] = handle->pdevOff; // lpDevice off
|
|
params[i++] = bufSel; // lpBuffer seg
|
|
params[i++] = 0; // lpBuffer off
|
|
params[i++] = (uint16_t)firstChar; // firstChar
|
|
params[i++] = (uint16_t)lastChar; // lastChar
|
|
params[i++] = font->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
|
|
|
|
waitForEngine();
|
|
|
|
uint32_t result = thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_GETCHARWIDTH].sel,
|
|
handle->ddiEntry[DDI_ORD_GETCHARWIDTH].off,
|
|
params, i);
|
|
|
|
waitForEngine();
|
|
|
|
dbg("windrv: GetCharWidth(%d..%d) returned %d\n",
|
|
(int)firstChar, (int)lastChar, (int16_t)(result & 0xFFFF));
|
|
|
|
// Copy results back
|
|
int16_t *src = (int16_t *)bufLinear;
|
|
for (uint16_t ci = 0; ci < count; ci++) {
|
|
widths[ci] = src[ci];
|
|
}
|
|
|
|
free16BitBlock(bufSel, bufLinear);
|
|
|
|
return ((int16_t)(result & 0xFFFF)) ? WDRV_OK : WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
|
|
int32_t wdrvMeasureText(WdrvHandleT handle, WdrvFontT font, const char *text, int16_t length)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return 0;
|
|
}
|
|
|
|
// Use built-in font if none specified
|
|
if (!font) {
|
|
font = wdrvLoadFontBuiltin();
|
|
if (!font) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Get widths for full printable range
|
|
int16_t widths[256];
|
|
memset(widths, 0, sizeof(widths));
|
|
|
|
int32_t ret = wdrvGetCharWidths(handle, font, 0, 255, widths);
|
|
if (ret != WDRV_OK) {
|
|
// Fallback: use font's average width
|
|
return (int32_t)font->pixWidth * length;
|
|
}
|
|
|
|
// Sum character widths
|
|
int32_t totalWidth = 0;
|
|
for (int16_t ci = 0; ci < length; ci++) {
|
|
uint8_t ch = (uint8_t)text[ci];
|
|
totalWidth += widths[ch];
|
|
}
|
|
|
|
return totalWidth;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Pixel buffer blitting
|
|
// ============================================================================
|
|
|
|
static int32_t blitPixelsSoftware(struct WdrvDriverS *drv, int16_t x, int16_t y, int16_t w, int16_t h, const uint8_t *pixels, int32_t srcPitch)
|
|
{
|
|
int16_t screenW = drv->gdiInfo.dpHorzRes;
|
|
int16_t screenH = drv->gdiInfo.dpVertRes;
|
|
|
|
// Clip to screen
|
|
int16_t sx = 0;
|
|
int16_t sy = 0;
|
|
int16_t dw = w;
|
|
int16_t dh = h;
|
|
|
|
if (x < 0) {
|
|
sx = -x;
|
|
dw += x;
|
|
x = 0;
|
|
}
|
|
if (y < 0) {
|
|
sy = -y;
|
|
dh += y;
|
|
y = 0;
|
|
}
|
|
if (x + dw > screenW) {
|
|
dw = screenW - x;
|
|
}
|
|
if (y + dh > screenH) {
|
|
dh = screenH - y;
|
|
}
|
|
if (dw <= 0 || dh <= 0) {
|
|
return WDRV_OK;
|
|
}
|
|
|
|
for (int16_t row = 0; row < dh; row++) {
|
|
const uint8_t *src = pixels + (sy + row) * srcPitch + sx;
|
|
for (int16_t col = 0; col < dw; col++) {
|
|
wdrvSetPixel((WdrvHandleT)drv, x + col, y + row, PALETTEINDEX(src[col]));
|
|
}
|
|
}
|
|
|
|
return WDRV_OK;
|
|
}
|
|
|
|
|
|
int32_t wdrvBlitPixels(WdrvHandleT handle, int16_t x, int16_t y, int16_t w, int16_t h, const uint8_t *pixels, int32_t srcPitch)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
|
|
// Check if we can use the fast framebuffer path
|
|
bool useFb = (handle->vramPtr != NULL);
|
|
if (handle->gdiInfoValid && handle->gdiInfo.dpPlanes > 1) {
|
|
useFb = false;
|
|
}
|
|
|
|
if (!useFb) {
|
|
// Fall back to software using Pixel DDI
|
|
if (!handle->ddiEntry[DDI_ORD_PIXEL].present) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
return blitPixelsSoftware(handle, x, y, w, h, pixels, srcPitch);
|
|
}
|
|
|
|
// Fast path: direct framebuffer access
|
|
uint8_t *fb = (uint8_t *)handle->vramPtr;
|
|
|
|
int16_t screenW = handle->gdiInfo.dpHorzRes;
|
|
int16_t screenH = handle->gdiInfo.dpVertRes;
|
|
int32_t fbPitch = handle->pitch;
|
|
|
|
// Clip to screen
|
|
int16_t sx = 0;
|
|
int16_t sy = 0;
|
|
int16_t dw = w;
|
|
int16_t dh = h;
|
|
|
|
if (x < 0) {
|
|
sx = -x;
|
|
dw += x;
|
|
x = 0;
|
|
}
|
|
if (y < 0) {
|
|
sy = -y;
|
|
dh += y;
|
|
y = 0;
|
|
}
|
|
if (x + dw > screenW) {
|
|
dw = screenW - x;
|
|
}
|
|
if (y + dh > screenH) {
|
|
dh = screenH - y;
|
|
}
|
|
if (dw <= 0 || dh <= 0) {
|
|
return WDRV_OK;
|
|
}
|
|
|
|
waitForEngine();
|
|
|
|
for (int16_t row = 0; row < dh; row++) {
|
|
uint8_t *dst = fb + (y + row + handle->dispYOffset) * fbPitch + x;
|
|
const uint8_t *src = pixels + (sy + row) * srcPitch + sx;
|
|
memcpy(dst, src, dw);
|
|
}
|
|
|
|
return WDRV_OK;
|
|
}
|
|
|
|
|
|
int32_t wdrvBlitBmp(WdrvHandleT handle, int16_t x, int16_t y, const char *bmpPath, bool setPaletteFlag)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
|
|
FILE *f = fopen(bmpPath, "rb");
|
|
if (!f) {
|
|
return WDRV_ERR_FILE_NOT_FOUND;
|
|
}
|
|
|
|
// Read BMP file header (14 bytes)
|
|
uint8_t fileHdr[14];
|
|
if (fread(fileHdr, 1, 14, f) != 14) {
|
|
fclose(f);
|
|
return WDRV_ERR_BAD_FORMAT;
|
|
}
|
|
if (fileHdr[0] != 'B' || fileHdr[1] != 'M') {
|
|
fclose(f);
|
|
return WDRV_ERR_BAD_FORMAT;
|
|
}
|
|
|
|
uint32_t pixelOffset = *(uint32_t *)(fileHdr + 10);
|
|
|
|
// Read BITMAPINFOHEADER (40 bytes)
|
|
uint8_t infoHdr[40];
|
|
if (fread(infoHdr, 1, 40, f) != 40) {
|
|
fclose(f);
|
|
return WDRV_ERR_BAD_FORMAT;
|
|
}
|
|
|
|
int32_t bmpW = *(int32_t *)(infoHdr + 4);
|
|
int32_t bmpH = *(int32_t *)(infoHdr + 8);
|
|
uint16_t bmpBpp = *(uint16_t *)(infoHdr + 14);
|
|
uint32_t bmpCompr = *(uint32_t *)(infoHdr + 16);
|
|
|
|
if (bmpBpp != 8 || bmpCompr != 0) {
|
|
logErr("windrv: BlitBmp: only 8bpp uncompressed supported (got %dbpp compr=%lu)\n",
|
|
(int)bmpBpp, (unsigned long)bmpCompr);
|
|
fclose(f);
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
bool bottomUp = (bmpH > 0);
|
|
if (bmpH < 0) {
|
|
bmpH = -bmpH;
|
|
}
|
|
|
|
// Read 256-entry palette
|
|
uint8_t palette[1024]; // 256 * 4 (BGRA)
|
|
if (fread(palette, 1, 1024, f) != 1024) {
|
|
fclose(f);
|
|
return WDRV_ERR_BAD_FORMAT;
|
|
}
|
|
|
|
if (setPaletteFlag) {
|
|
// Convert BGRA to RGBX for wdrvSetPalette
|
|
uint8_t rgbPal[1024];
|
|
for (int pi = 0; pi < 256; pi++) {
|
|
rgbPal[pi * 4 + 0] = palette[pi * 4 + 2]; // R
|
|
rgbPal[pi * 4 + 1] = palette[pi * 4 + 1]; // G
|
|
rgbPal[pi * 4 + 2] = palette[pi * 4 + 0]; // B
|
|
rgbPal[pi * 4 + 3] = 0;
|
|
}
|
|
wdrvSetPalette(handle, 0, 256, rgbPal);
|
|
}
|
|
|
|
// BMP rows are padded to 4-byte alignment
|
|
int32_t bmpPitch = (bmpW + 3) & ~3;
|
|
|
|
// Allocate pixel buffer
|
|
uint8_t *pixels = (uint8_t *)malloc(bmpPitch * bmpH);
|
|
if (!pixels) {
|
|
fclose(f);
|
|
return WDRV_ERR_NO_MEMORY;
|
|
}
|
|
|
|
// Seek to pixel data and read
|
|
fseek(f, pixelOffset, SEEK_SET);
|
|
|
|
if (bottomUp) {
|
|
// BMP is bottom-up: read rows in reverse
|
|
for (int32_t row = bmpH - 1; row >= 0; row--) {
|
|
fread(pixels + row * bmpPitch, 1, bmpPitch, f);
|
|
}
|
|
} else {
|
|
fread(pixels, 1, bmpPitch * bmpH, f);
|
|
}
|
|
|
|
fclose(f);
|
|
|
|
int32_t ret = wdrvBlitPixels(handle, x, y, (int16_t)bmpW, (int16_t)bmpH, pixels, bmpPitch);
|
|
free(pixels);
|
|
return ret;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Hardware cursor
|
|
// ============================================================================
|
|
|
|
// Built-in 32x32 cursor AND masks (128 bytes each) and XOR masks (128 bytes each).
|
|
// AND mask: 1=transparent, 0=use XOR value. XOR mask: 1=white/invert, 0=black.
|
|
|
|
// Arrow cursor (top-left pointing)
|
|
static const uint8_t gCursorArrowAnd[128] = {
|
|
0x3F,0xFF,0xFF,0xFF, 0x1F,0xFF,0xFF,0xFF, 0x0F,0xFF,0xFF,0xFF, 0x07,0xFF,0xFF,0xFF,
|
|
0x03,0xFF,0xFF,0xFF, 0x01,0xFF,0xFF,0xFF, 0x00,0xFF,0xFF,0xFF, 0x00,0x7F,0xFF,0xFF,
|
|
0x00,0x3F,0xFF,0xFF, 0x00,0x1F,0xFF,0xFF, 0x00,0x0F,0xFF,0xFF, 0x00,0xFF,0xFF,0xFF,
|
|
0x00,0xFF,0xFF,0xFF, 0x04,0x7F,0xFF,0xFF, 0x06,0x7F,0xFF,0xFF, 0x06,0x7F,0xFF,0xFF,
|
|
0x0F,0x3F,0xFF,0xFF, 0x0F,0x3F,0xFF,0xFF, 0x1F,0xBF,0xFF,0xFF, 0x1F,0xFF,0xFF,0xFF,
|
|
0x3F,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,
|
|
0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,
|
|
0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,
|
|
};
|
|
static const uint8_t gCursorArrowXor[128] = {
|
|
0x00,0x00,0x00,0x00, 0x40,0x00,0x00,0x00, 0x60,0x00,0x00,0x00, 0x70,0x00,0x00,0x00,
|
|
0x78,0x00,0x00,0x00, 0x7C,0x00,0x00,0x00, 0x7E,0x00,0x00,0x00, 0x7F,0x00,0x00,0x00,
|
|
0x7F,0x80,0x00,0x00, 0x7F,0xC0,0x00,0x00, 0x7F,0xE0,0x00,0x00, 0x7E,0x00,0x00,0x00,
|
|
0x7E,0x00,0x00,0x00, 0x73,0x00,0x00,0x00, 0x61,0x00,0x00,0x00, 0x61,0x00,0x00,0x00,
|
|
0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
};
|
|
|
|
// Crosshair cursor
|
|
static const uint8_t gCursorCrosshairAnd[128] = {
|
|
0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,
|
|
0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,
|
|
0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0x01,0xFF,0xFF,
|
|
0xFF,0x01,0xFF,0xFF, 0xFF,0x01,0xFF,0xFF, 0xFF,0x01,0xFF,0xFF, 0x80,0x00,0x01,0xFF,
|
|
0xFF,0x01,0xFF,0xFF, 0xFF,0x01,0xFF,0xFF, 0xFF,0x01,0xFF,0xFF, 0xFF,0x01,0xFF,0xFF,
|
|
0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,
|
|
0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,
|
|
0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,
|
|
};
|
|
static const uint8_t gCursorCrosshairXor[128] = {
|
|
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x80,0x00,0x00,
|
|
0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00, 0x3F,0xFF,0xFC,0x00,
|
|
0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00,
|
|
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
};
|
|
|
|
// I-beam cursor
|
|
static const uint8_t gCursorIbeamAnd[128] = {
|
|
0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,
|
|
0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xF8,0x38,0xFF,0xFF, 0xFE,0x7E,0xFF,0xFF,
|
|
0xFF,0x7E,0xFF,0xFF, 0xFF,0x7E,0xFF,0xFF, 0xFF,0x7E,0xFF,0xFF, 0xFF,0x7E,0xFF,0xFF,
|
|
0xFF,0x7E,0xFF,0xFF, 0xFF,0x7E,0xFF,0xFF, 0xFF,0x7E,0xFF,0xFF, 0xFF,0x7E,0xFF,0xFF,
|
|
0xFF,0x7E,0xFF,0xFF, 0xFF,0x7E,0xFF,0xFF, 0xFF,0x7E,0xFF,0xFF, 0xFF,0x7E,0xFF,0xFF,
|
|
0xFF,0x7E,0xFF,0xFF, 0xFF,0x7E,0xFF,0xFF, 0xFF,0x7E,0xFF,0xFF, 0xFE,0x7E,0xFF,0xFF,
|
|
0xF8,0x38,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,
|
|
0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,
|
|
};
|
|
static const uint8_t gCursorIbeamXor[128] = {
|
|
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x03,0x80,0x00,0x00, 0x00,0x80,0x00,0x00,
|
|
0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00,
|
|
0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00,
|
|
0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00,
|
|
0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00, 0x00,0x80,0x00,0x00,
|
|
0x03,0x80,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
};
|
|
|
|
// Hand cursor (pointing up)
|
|
static const uint8_t gCursorHandAnd[128] = {
|
|
0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,
|
|
0xF3,0xFF,0xFF,0xFF, 0xE1,0xFF,0xFF,0xFF, 0xE1,0xFF,0xFF,0xFF, 0xE1,0xFF,0xFF,0xFF,
|
|
0xE1,0xFF,0xFF,0xFF, 0xE0,0x7F,0xFF,0xFF, 0xE0,0x07,0xFF,0xFF, 0xE0,0x03,0xFF,0xFF,
|
|
0xA0,0x03,0xFF,0xFF, 0x80,0x03,0xFF,0xFF, 0x80,0x03,0xFF,0xFF, 0x80,0x07,0xFF,0xFF,
|
|
0xC0,0x07,0xFF,0xFF, 0xC0,0x0F,0xFF,0xFF, 0xC0,0x0F,0xFF,0xFF, 0xE0,0x0F,0xFF,0xFF,
|
|
0xE0,0x1F,0xFF,0xFF, 0xF0,0x1F,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,
|
|
0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,
|
|
0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,
|
|
};
|
|
static const uint8_t gCursorHandXor[128] = {
|
|
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
0x0C,0x00,0x00,0x00, 0x0C,0x00,0x00,0x00, 0x0C,0x00,0x00,0x00, 0x0C,0x00,0x00,0x00,
|
|
0x0C,0x00,0x00,0x00, 0x0D,0x80,0x00,0x00, 0x0D,0xB0,0x00,0x00, 0x0D,0xB8,0x00,0x00,
|
|
0x4D,0xB8,0x00,0x00, 0x6D,0xB8,0x00,0x00, 0x6F,0xB8,0x00,0x00, 0x7F,0xF0,0x00,0x00,
|
|
0x3F,0xF0,0x00,0x00, 0x1F,0xE0,0x00,0x00, 0x1F,0xE0,0x00,0x00, 0x0F,0xE0,0x00,0x00,
|
|
0x0F,0xC0,0x00,0x00, 0x07,0xC0,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
};
|
|
|
|
|
|
int32_t wdrvSetCursor(WdrvHandleT handle, WdrvCursorShapeE shape)
|
|
{
|
|
if (shape == WDRV_CURSOR_NONE) {
|
|
// Hide cursor by passing NULL
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
if (!handle->ddiEntry[DDI_ORD_SETCURSOR].present) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
uint16_t params[2];
|
|
params[0] = 0; // NULL seg
|
|
params[1] = 0; // NULL off
|
|
|
|
waitForEngine();
|
|
thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_SETCURSOR].sel,
|
|
handle->ddiEntry[DDI_ORD_SETCURSOR].off,
|
|
params, 2);
|
|
waitForEngine();
|
|
return WDRV_OK;
|
|
}
|
|
|
|
const uint8_t *andMask = NULL;
|
|
const uint8_t *xorMask = NULL;
|
|
int16_t hotX = 0;
|
|
int16_t hotY = 0;
|
|
|
|
switch (shape) {
|
|
case WDRV_CURSOR_ARROW:
|
|
andMask = gCursorArrowAnd;
|
|
xorMask = gCursorArrowXor;
|
|
hotX = 1;
|
|
hotY = 1;
|
|
break;
|
|
case WDRV_CURSOR_CROSSHAIR:
|
|
andMask = gCursorCrosshairAnd;
|
|
xorMask = gCursorCrosshairXor;
|
|
hotX = 15;
|
|
hotY = 15;
|
|
break;
|
|
case WDRV_CURSOR_IBEAM:
|
|
andMask = gCursorIbeamAnd;
|
|
xorMask = gCursorIbeamXor;
|
|
hotX = 8;
|
|
hotY = 15;
|
|
break;
|
|
case WDRV_CURSOR_HAND:
|
|
andMask = gCursorHandAnd;
|
|
xorMask = gCursorHandXor;
|
|
hotX = 5;
|
|
hotY = 0;
|
|
break;
|
|
default:
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
return wdrvSetCursorCustom(handle, hotX, hotY, andMask, xorMask);
|
|
}
|
|
|
|
|
|
int32_t wdrvSetCursorCustom(WdrvHandleT handle, int16_t hotX, int16_t hotY, const uint8_t *andMask, const uint8_t *xorMask)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
if (!handle->ddiEntry[DDI_ORD_SETCURSOR].present) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
// CursorInfo16T (12 bytes) + AND mask (128 bytes) + XOR mask (128 bytes) = 268 bytes
|
|
uint32_t allocSize = sizeof(CursorInfo16T) + 128 + 128;
|
|
|
|
// Free previous cursor allocation
|
|
if (handle->cursorSel) {
|
|
free16BitBlock(handle->cursorSel, handle->cursorLinear);
|
|
handle->cursorSel = 0;
|
|
handle->cursorLinear = 0;
|
|
handle->cursorAllocSize = 0;
|
|
}
|
|
|
|
// Allocate new cursor block
|
|
uint32_t curLinear;
|
|
uint16_t curSel = alloc16BitBlock(allocSize, &curLinear);
|
|
if (curSel == 0) {
|
|
return WDRV_ERR_NO_MEMORY;
|
|
}
|
|
|
|
handle->cursorSel = curSel;
|
|
handle->cursorLinear = curLinear;
|
|
handle->cursorAllocSize = allocSize;
|
|
|
|
// Build the cursor shape structure
|
|
CursorInfo16T *ci = (CursorInfo16T *)curLinear;
|
|
ci->csHotX = hotX;
|
|
ci->csHotY = hotY;
|
|
ci->csWidth = 32;
|
|
ci->csHeight = 32;
|
|
ci->csWidthB = 4; // 32 / 8
|
|
ci->csColor = 1; // mono: planes*bpp = 1
|
|
|
|
// Copy masks after the header
|
|
uint8_t *maskDst = (uint8_t *)curLinear + sizeof(CursorInfo16T);
|
|
memcpy(maskDst, andMask, 128);
|
|
memcpy(maskDst + 128, xorMask, 128);
|
|
|
|
// SetCursor(LPCURSORSHAPE) — 2 words (far ptr)
|
|
uint16_t params[2];
|
|
params[0] = curSel;
|
|
params[1] = 0;
|
|
|
|
waitForEngine();
|
|
|
|
thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_SETCURSOR].sel,
|
|
handle->ddiEntry[DDI_ORD_SETCURSOR].off,
|
|
params, 2);
|
|
|
|
waitForEngine();
|
|
|
|
dbg("windrv: SetCursor(%dx%d hot=%d,%d) done\n", 32, 32, hotX, hotY);
|
|
return WDRV_OK;
|
|
}
|
|
|
|
|
|
int32_t wdrvMoveCursor(WdrvHandleT handle, int16_t x, int16_t y)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
if (!handle->ddiEntry[DDI_ORD_MOVECURSOR].present) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
// MoveCursor(WORD absX, WORD absY) — 2 words
|
|
uint16_t params[2];
|
|
params[0] = (uint16_t)x;
|
|
params[1] = (uint16_t)(y + handle->dispYOffset);
|
|
|
|
thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_MOVECURSOR].sel,
|
|
handle->ddiEntry[DDI_ORD_MOVECURSOR].off,
|
|
params, 2);
|
|
|
|
return WDRV_OK;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Off-screen bitmaps
|
|
// ============================================================================
|
|
|
|
struct WdrvBitmapS {
|
|
uint16_t pdevSel;
|
|
uint32_t pdevLinear;
|
|
uint32_t pdevSize;
|
|
int16_t width;
|
|
int16_t height;
|
|
uint8_t bpp;
|
|
};
|
|
|
|
|
|
WdrvBitmapT wdrvCreateBitmap(WdrvHandleT handle, int16_t width, int16_t height)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return NULL;
|
|
}
|
|
if (!handle->ddiEntry[DDI_ORD_CREATEBITMAP].present) {
|
|
return NULL;
|
|
}
|
|
|
|
uint8_t bpp = (uint8_t)(handle->gdiInfo.dpBitsPixel * handle->gdiInfo.dpPlanes);
|
|
|
|
// Allocate PDEVICE-sized block for the bitmap descriptor
|
|
uint32_t pdevLinear;
|
|
uint16_t pdevSel = alloc16BitBlock(PDEVICE_MAX_SIZE, &pdevLinear);
|
|
if (pdevSel == 0) {
|
|
return NULL;
|
|
}
|
|
memset((void *)pdevLinear, 0, PDEVICE_MAX_SIZE);
|
|
|
|
// Init the Bitmap16T header
|
|
Bitmap16T *bm = (Bitmap16T *)pdevLinear;
|
|
bm->bmType = 0; // memory bitmap
|
|
bm->bmWidth = width;
|
|
bm->bmHeight = height;
|
|
bm->bmWidthBytes = (int16_t)(((width * bpp + 15) / 16) * 2);
|
|
bm->bmPlanes = (uint8_t)handle->gdiInfo.dpPlanes;
|
|
bm->bmBitsPixel = (uint8_t)handle->gdiInfo.dpBitsPixel;
|
|
bm->bmBits = 0; // driver will manage bits
|
|
|
|
// DDK: short FAR PASCAL CreateBitmap(LPPDEVICE lpDevice, short style,
|
|
// LPBITMAP lpBitmap, LPSTR lpBits)
|
|
// style > 0 = create, style < 0 = delete
|
|
// Total: lpDevice(2w) + style(1w) + lpBitmap(2w) + lpBits(2w) = 7 words
|
|
uint16_t dgSel = handle->neMod.autoDataSel;
|
|
uint16_t params[7];
|
|
int i = 0;
|
|
params[i++] = dgSel; // lpDevice seg
|
|
params[i++] = handle->pdevOff; // lpDevice off
|
|
params[i++] = 1; // style = create
|
|
params[i++] = pdevSel; // lpBitmap seg
|
|
params[i++] = 0; // lpBitmap off
|
|
params[i++] = 0; // lpBits seg (NULL = don't init)
|
|
params[i++] = 0; // lpBits off
|
|
|
|
waitForEngine();
|
|
|
|
uint32_t result = thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_CREATEBITMAP].sel,
|
|
handle->ddiEntry[DDI_ORD_CREATEBITMAP].off,
|
|
params, i);
|
|
|
|
waitForEngine();
|
|
|
|
dbg("windrv: CreateBitmap(%dx%d %dbpp) returned %d\n",
|
|
width, height, (int)bpp, (int16_t)(result & 0xFFFF));
|
|
|
|
if ((int16_t)(result & 0xFFFF) <= 0) {
|
|
free16BitBlock(pdevSel, pdevLinear);
|
|
return NULL;
|
|
}
|
|
|
|
struct WdrvBitmapS *bitmap = (struct WdrvBitmapS *)calloc(1, sizeof(struct WdrvBitmapS));
|
|
if (!bitmap) {
|
|
free16BitBlock(pdevSel, pdevLinear);
|
|
return NULL;
|
|
}
|
|
|
|
bitmap->pdevSel = pdevSel;
|
|
bitmap->pdevLinear = pdevLinear;
|
|
bitmap->pdevSize = PDEVICE_MAX_SIZE;
|
|
bitmap->width = width;
|
|
bitmap->height = height;
|
|
bitmap->bpp = bpp;
|
|
|
|
return bitmap;
|
|
}
|
|
|
|
|
|
void wdrvDeleteBitmap(WdrvHandleT handle, WdrvBitmapT bitmap)
|
|
{
|
|
if (!bitmap) {
|
|
return;
|
|
}
|
|
|
|
if (handle && handle->enabled && handle->ddiEntry[DDI_ORD_DELETEBITMAP].present) {
|
|
// DeleteBitmap(LPDEVICE lpDevice, LPBITMAP lpBitmap)
|
|
// Pascal push order: lpDevice(2w) + lpBitmap(2w) = 4 words... but ordinal says 2 words
|
|
// Actually DeleteBitmap is just: void DeleteBitmap(LPBITMAP) = 2 words
|
|
uint16_t params[2];
|
|
params[0] = bitmap->pdevSel;
|
|
params[1] = 0;
|
|
|
|
waitForEngine();
|
|
thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_DELETEBITMAP].sel,
|
|
handle->ddiEntry[DDI_ORD_DELETEBITMAP].off,
|
|
params, 2);
|
|
waitForEngine();
|
|
}
|
|
|
|
free16BitBlock(bitmap->pdevSel, bitmap->pdevLinear);
|
|
free(bitmap);
|
|
}
|
|
|
|
|
|
int32_t wdrvBitmapSetPixels(WdrvHandleT handle, WdrvBitmapT bitmap, const uint8_t *data, uint32_t dataSize)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
if (!bitmap) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
if (!handle->ddiEntry[DDI_ORD_BITMAPBITS].present) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
// Allocate 16-bit buffer for the pixel data
|
|
uint32_t bufLinear;
|
|
uint16_t bufSel = alloc16BitBlock(dataSize, &bufLinear);
|
|
if (bufSel == 0) {
|
|
return WDRV_ERR_NO_MEMORY;
|
|
}
|
|
memcpy((void *)bufLinear, data, dataSize);
|
|
|
|
// BitmapBits(LPDEVICE lpDevice, DWORD fFlags, DWORD dwCount, LPSTR lpBits)
|
|
// Pascal push order: lpDevice(2w), fFlags(2w), dwCount(2w), lpBits(2w) = 8 words
|
|
// fFlags=0 means SET bits, fFlags!=0 means GET bits
|
|
// Actually: long BitmapBits(LPPDEVICE, fFlags, dwCount, lpBits)
|
|
uint16_t params[8];
|
|
int i = 0;
|
|
params[i++] = bitmap->pdevSel; // lpDevice seg
|
|
params[i++] = 0; // lpDevice off
|
|
params[i++] = 0; // fFlags high (0 = set)
|
|
params[i++] = 0; // fFlags low
|
|
params[i++] = (uint16_t)(dataSize >> 16); // dwCount high
|
|
params[i++] = (uint16_t)(dataSize); // dwCount low
|
|
params[i++] = bufSel; // lpBits seg
|
|
params[i++] = 0; // lpBits off
|
|
|
|
waitForEngine();
|
|
|
|
uint32_t result = thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_BITMAPBITS].sel,
|
|
handle->ddiEntry[DDI_ORD_BITMAPBITS].off,
|
|
params, i);
|
|
|
|
waitForEngine();
|
|
|
|
free16BitBlock(bufSel, bufLinear);
|
|
|
|
dbg("windrv: BitmapBits(SET, %lu bytes) returned %ld\n",
|
|
(unsigned long)dataSize, (long)(int32_t)result);
|
|
|
|
return WDRV_OK;
|
|
}
|
|
|
|
|
|
int32_t wdrvBitmapGetPixels(WdrvHandleT handle, WdrvBitmapT bitmap, uint8_t *data, uint32_t dataSize)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
if (!bitmap) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
if (!handle->ddiEntry[DDI_ORD_BITMAPBITS].present) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
// Allocate 16-bit buffer for receiving the pixel data
|
|
uint32_t bufLinear;
|
|
uint16_t bufSel = alloc16BitBlock(dataSize, &bufLinear);
|
|
if (bufSel == 0) {
|
|
return WDRV_ERR_NO_MEMORY;
|
|
}
|
|
memset((void *)bufLinear, 0, dataSize);
|
|
|
|
// fFlags=1 means GET bits
|
|
uint16_t params[8];
|
|
int i = 0;
|
|
params[i++] = bitmap->pdevSel;
|
|
params[i++] = 0;
|
|
params[i++] = 0; // fFlags high
|
|
params[i++] = 1; // fFlags low (1 = get)
|
|
params[i++] = (uint16_t)(dataSize >> 16);
|
|
params[i++] = (uint16_t)(dataSize);
|
|
params[i++] = bufSel;
|
|
params[i++] = 0;
|
|
|
|
waitForEngine();
|
|
|
|
uint32_t result = thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_BITMAPBITS].sel,
|
|
handle->ddiEntry[DDI_ORD_BITMAPBITS].off,
|
|
params, i);
|
|
|
|
waitForEngine();
|
|
|
|
memcpy(data, (void *)bufLinear, dataSize);
|
|
free16BitBlock(bufSel, bufLinear);
|
|
|
|
dbg("windrv: BitmapBits(GET, %lu bytes) returned %ld\n",
|
|
(unsigned long)dataSize, (long)(int32_t)result);
|
|
|
|
return WDRV_OK;
|
|
}
|
|
|
|
|
|
int32_t wdrvBitBltFromBitmap(WdrvHandleT handle, WdrvBitmapT srcBitmap, int16_t srcX, int16_t srcY, int16_t dstX, int16_t dstY, int16_t w, int16_t h, uint32_t rop3)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
if (!srcBitmap) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
if (!handle->ddiEntry[DDI_ORD_BITBLT].present) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
uint16_t dgSel = handle->neMod.autoDataSel;
|
|
uint16_t params[16];
|
|
int i = 0;
|
|
|
|
// lpDstDev = screen PDEVICE
|
|
params[i++] = dgSel;
|
|
params[i++] = handle->pdevOff;
|
|
// DstX, DstY
|
|
params[i++] = (uint16_t)dstX;
|
|
params[i++] = (uint16_t)(dstY + handle->dispYOffset);
|
|
// lpSrcDev = bitmap PDEVICE
|
|
params[i++] = srcBitmap->pdevSel;
|
|
params[i++] = 0;
|
|
// SrcX, SrcY
|
|
params[i++] = (uint16_t)srcX;
|
|
params[i++] = (uint16_t)srcY;
|
|
// xExt, yExt
|
|
params[i++] = (uint16_t)w;
|
|
params[i++] = (uint16_t)h;
|
|
// Rop3
|
|
params[i++] = (uint16_t)(rop3 >> 16);
|
|
params[i++] = (uint16_t)(rop3 & 0xFFFF);
|
|
// lpBrush
|
|
params[i++] = dgSel;
|
|
params[i++] = handle->brushOff;
|
|
// lpDrawMode
|
|
params[i++] = dgSel;
|
|
params[i++] = handle->drawModeOff;
|
|
|
|
waitForEngine();
|
|
|
|
uint32_t result = thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_BITBLT].sel,
|
|
handle->ddiEntry[DDI_ORD_BITBLT].off,
|
|
params, i);
|
|
|
|
waitForEngine();
|
|
|
|
dbg("windrv: BitBltFromBitmap returned %lu\n", (unsigned long)(result & 0xFFFF));
|
|
|
|
return ((int16_t)(result & 0xFFFF)) ? WDRV_OK : WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
|
|
int32_t wdrvBitBltToBitmap(WdrvHandleT handle, WdrvBitmapT dstBitmap, int16_t srcX, int16_t srcY, int16_t dstX, int16_t dstY, int16_t w, int16_t h, uint32_t rop3)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
if (!dstBitmap) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
if (!handle->ddiEntry[DDI_ORD_BITBLT].present) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
uint16_t dgSel = handle->neMod.autoDataSel;
|
|
uint16_t params[16];
|
|
int i = 0;
|
|
|
|
// lpDstDev = bitmap PDEVICE
|
|
params[i++] = dstBitmap->pdevSel;
|
|
params[i++] = 0;
|
|
// DstX, DstY
|
|
params[i++] = (uint16_t)dstX;
|
|
params[i++] = (uint16_t)dstY;
|
|
// lpSrcDev = screen PDEVICE
|
|
params[i++] = dgSel;
|
|
params[i++] = handle->pdevOff;
|
|
// SrcX, SrcY
|
|
params[i++] = (uint16_t)srcX;
|
|
params[i++] = (uint16_t)(srcY + handle->dispYOffset);
|
|
// xExt, yExt
|
|
params[i++] = (uint16_t)w;
|
|
params[i++] = (uint16_t)h;
|
|
// Rop3
|
|
params[i++] = (uint16_t)(rop3 >> 16);
|
|
params[i++] = (uint16_t)(rop3 & 0xFFFF);
|
|
// lpBrush
|
|
params[i++] = dgSel;
|
|
params[i++] = handle->brushOff;
|
|
// lpDrawMode
|
|
params[i++] = dgSel;
|
|
params[i++] = handle->drawModeOff;
|
|
|
|
waitForEngine();
|
|
|
|
uint32_t result = thunkCall16(&gThunkCtx,
|
|
handle->ddiEntry[DDI_ORD_BITBLT].sel,
|
|
handle->ddiEntry[DDI_ORD_BITBLT].off,
|
|
params, i);
|
|
|
|
waitForEngine();
|
|
|
|
dbg("windrv: BitBltToBitmap returned %lu\n", (unsigned long)(result & 0xFFFF));
|
|
|
|
return ((int16_t)(result & 0xFFFF)) ? WDRV_OK : WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Framebuffer access
|
|
// ============================================================================
|
|
|
|
void *wdrvGetFramebuffer(WdrvHandleT handle)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return NULL;
|
|
}
|
|
return handle->vramPtr;
|
|
}
|
|
|
|
|
|
int32_t wdrvGetPitch(WdrvHandleT handle)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return 0;
|
|
}
|
|
return handle->pitch;
|
|
}
|
|
|
|
|
|
static void readDacPalette(uint8_t rgb[768], uint8_t dacWidth)
|
|
{
|
|
uint8_t dacShift = 8 - dacWidth; // 2 for 6-bit, 0 for 8-bit
|
|
outportb(0x3C7, 0);
|
|
for (int32_t i = 0; i < 768; i++) {
|
|
rgb[i] = (uint8_t)(inportb(0x3C9) << dacShift);
|
|
}
|
|
}
|
|
|
|
|
|
int32_t wdrvScreenshot(WdrvHandleT handle, const char *filename)
|
|
{
|
|
if (!handle || !handle->enabled) {
|
|
return WDRV_ERR_NOT_ENABLED;
|
|
}
|
|
if (!filename) {
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
int16_t screenW = handle->gdiInfo.dpHorzRes;
|
|
int16_t screenH = handle->gdiInfo.dpVertRes;
|
|
|
|
// Read the VGA DAC palette (256 entries x 3 bytes = 768 bytes)
|
|
uint8_t palette[768];
|
|
readDacPalette(palette, handle->dacWidth);
|
|
|
|
// Allocate RGB output buffer
|
|
uint8_t *rgbBuf = (uint8_t *)malloc((uint32_t)screenW * screenH * 3);
|
|
if (!rgbBuf) {
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return WDRV_ERR_NO_MEMORY;
|
|
}
|
|
|
|
// Determine which read path to use:
|
|
// 1. Linear FB: vramPtr != NULL, single plane, and VRAM large enough
|
|
// for the full screen (excludes banked 64KB VGA aperture).
|
|
// 2. DDI bitmap: BitBltToBitmap + BitmapGetPixels in strips.
|
|
// 3. GetPixel fallback: per-pixel Pixel DDI (slow but universal).
|
|
uint32_t screenBytes = (uint32_t)screenW * screenH;
|
|
bool useLinearFb = (handle->vramPtr != NULL) &&
|
|
(handle->gdiInfo.dpPlanes == 1) &&
|
|
(handle->vramSize >= screenBytes);
|
|
|
|
if (useLinearFb) {
|
|
// Fast path: linear framebuffer read
|
|
uint8_t *fb = (uint8_t *)handle->vramPtr;
|
|
int32_t pitch = handle->pitch;
|
|
|
|
waitForEngine();
|
|
|
|
for (int16_t y = 0; y < screenH; y++) {
|
|
uint8_t *src = fb + (y + handle->dispYOffset) * pitch;
|
|
uint8_t *dst = rgbBuf + y * screenW * 3;
|
|
for (int16_t x = 0; x < screenW; x++) {
|
|
uint8_t idx = src[x];
|
|
dst[x * 3 + 0] = palette[idx * 3 + 0];
|
|
dst[x * 3 + 1] = palette[idx * 3 + 1];
|
|
dst[x * 3 + 2] = palette[idx * 3 + 2];
|
|
}
|
|
}
|
|
dbg("windrv: screenshot via linear FB\n");
|
|
} else {
|
|
// Try DDI bitmap path first
|
|
#define SCREENSHOT_STRIP_HEIGHT 32
|
|
WdrvBitmapT bmp = wdrvCreateBitmap(handle, screenW, SCREENSHOT_STRIP_HEIGHT);
|
|
|
|
if (bmp) {
|
|
// DDI bitmap path: read screen in strips
|
|
uint32_t stripBytes = (uint32_t)screenW * SCREENSHOT_STRIP_HEIGHT;
|
|
uint8_t *stripBuf = (uint8_t *)malloc(stripBytes);
|
|
if (!stripBuf) {
|
|
wdrvDeleteBitmap(handle, bmp);
|
|
free(rgbBuf);
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return WDRV_ERR_NO_MEMORY;
|
|
}
|
|
|
|
bool ok = true;
|
|
for (int16_t y = 0; y < screenH; y += SCREENSHOT_STRIP_HEIGHT) {
|
|
int16_t stripH = SCREENSHOT_STRIP_HEIGHT;
|
|
if (y + stripH > screenH) {
|
|
stripH = screenH - y;
|
|
}
|
|
|
|
int32_t ret = wdrvBitBltToBitmap(handle, bmp, 0, y, 0, 0, screenW, stripH, 0x00CC0020);
|
|
if (ret != WDRV_OK) {
|
|
ok = false;
|
|
break;
|
|
}
|
|
|
|
ret = wdrvBitmapGetPixels(handle, bmp, stripBuf, stripBytes);
|
|
if (ret != WDRV_OK) {
|
|
ok = false;
|
|
break;
|
|
}
|
|
|
|
for (int16_t row = 0; row < stripH; row++) {
|
|
uint8_t *src = stripBuf + row * screenW;
|
|
uint8_t *dst = rgbBuf + (y + row) * screenW * 3;
|
|
for (int16_t x = 0; x < screenW; x++) {
|
|
uint8_t idx = src[x];
|
|
dst[x * 3 + 0] = palette[idx * 3 + 0];
|
|
dst[x * 3 + 1] = palette[idx * 3 + 1];
|
|
dst[x * 3 + 2] = palette[idx * 3 + 2];
|
|
}
|
|
}
|
|
}
|
|
|
|
free(stripBuf);
|
|
wdrvDeleteBitmap(handle, bmp);
|
|
|
|
if (!ok) {
|
|
free(rgbBuf);
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
dbg("windrv: screenshot via DDI bitmap\n");
|
|
} else {
|
|
// No viable read path (no linear FB, no CreateBitmap)
|
|
dbg("windrv: screenshot unsupported (no read path)\n");
|
|
free(rgbBuf);
|
|
setError(WDRV_ERR_UNSUPPORTED);
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
#undef SCREENSHOT_STRIP_HEIGHT
|
|
}
|
|
|
|
// Write PNG
|
|
int32_t ok = stbi_write_png(filename, screenW, screenH, 3, rgbBuf, screenW * 3);
|
|
free(rgbBuf);
|
|
|
|
if (!ok) {
|
|
setError(WDRV_ERR_UNSUPPORTED);
|
|
return WDRV_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
dbg("windrv: screenshot saved to %s (%dx%d)\n", filename, screenW, screenH);
|
|
return WDRV_OK;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Error handling
|
|
// ============================================================================
|
|
|
|
int32_t wdrvGetLastError(void)
|
|
{
|
|
return gLastError;
|
|
}
|
|
|
|
|
|
const char *wdrvGetLastErrorString(void)
|
|
{
|
|
switch (gLastError) {
|
|
case WDRV_OK: return "no error";
|
|
case WDRV_ERR_INIT: return "initialization failed";
|
|
case WDRV_ERR_NO_DPMI: return "DPMI not available";
|
|
case WDRV_ERR_FILE_NOT_FOUND: return "file not found";
|
|
case WDRV_ERR_BAD_FORMAT: return "not a valid NE executable";
|
|
case WDRV_ERR_LOAD_FAILED: return "failed to load driver";
|
|
case WDRV_ERR_NO_MEMORY: return "out of memory";
|
|
case WDRV_ERR_RELOC_FAILED: return "relocation failed";
|
|
case WDRV_ERR_NO_ENTRY: return "required DDI entry not found";
|
|
case WDRV_ERR_ENABLE_FAILED: return "driver Enable() failed";
|
|
case WDRV_ERR_THUNK_FAILED: return "thunk setup failed";
|
|
case WDRV_ERR_NOT_LOADED: return "no driver loaded";
|
|
case WDRV_ERR_NOT_ENABLED: return "driver not enabled";
|
|
case WDRV_ERR_UNSUPPORTED: return "operation not supported";
|
|
default: return "unknown error";
|
|
}
|
|
}
|
|
|
|
|
|
void wdrvSetDebug(bool enable)
|
|
{
|
|
gDebug = enable;
|
|
neSetDebug(enable);
|
|
thunkSetDebug(enable);
|
|
stubSetDebug(enable);
|
|
}
|
|
|
|
|
|
void wdrvDumpSegmentBases(WdrvHandleT handle)
|
|
{
|
|
if (!handle) {
|
|
return;
|
|
}
|
|
|
|
logErr("=== NE Module Segment Bases ===\n");
|
|
for (int i = 0; i < handle->neMod.segmentCount; i++) {
|
|
LoadedSegT *seg = &handle->neMod.segments[i];
|
|
unsigned long base = 0;
|
|
__dpmi_get_segment_base_address(seg->selector, &base);
|
|
unsigned long limit = __dpmi_get_segment_limit(seg->selector);
|
|
logErr(" seg[%d] sel=%04X base=0x%08lX limit=0x%08lX size=%" PRIu32 " %s\n",
|
|
i + 1, seg->selector, base, limit, seg->size,
|
|
seg->isCode ? "CODE" : "DATA");
|
|
}
|
|
|
|
unsigned long dgBase = 0;
|
|
__dpmi_get_segment_base_address(handle->neMod.autoDataSel, &dgBase);
|
|
logErr(" DGROUP sel=%04X base=0x%08lX\n", handle->neMod.autoDataSel, dgBase);
|
|
logErr(" pdevOff=%04X brushOff=%04X drawModeOff=%04X\n",
|
|
handle->pdevOff, handle->brushOff, handle->drawModeOff);
|
|
logErr(" dgroupObjBase=0x%" PRIX32 " pdevLinear=0x%" PRIX32 "\n",
|
|
handle->dgroupObjBase, handle->pdevLinear);
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Internal implementation
|
|
// ============================================================================
|
|
|
|
static FarPtr16T importResolver(const char *moduleName, uint16_t ordinal, const char *funcName)
|
|
{
|
|
return stubResolveImport(&gStubCtx, moduleName, ordinal, funcName);
|
|
}
|
|
|
|
|
|
static bool resolveDriverEntries(struct WdrvDriverS *drv)
|
|
{
|
|
// Resolve all known DDI ordinals
|
|
static const uint16_t ddiOrdinals[] = {
|
|
DDI_ORD_BITBLT, DDI_ORD_COLORINFO, DDI_ORD_CONTROL,
|
|
DDI_ORD_DISABLE, DDI_ORD_ENABLE, DDI_ORD_ENUMDFFONTS,
|
|
DDI_ORD_ENUMOBJ, DDI_ORD_OUTPUT, DDI_ORD_PIXEL,
|
|
DDI_ORD_REALIZEOBJECT, DDI_ORD_STRBLT, DDI_ORD_SCANLR,
|
|
DDI_ORD_DEVICEMODE, DDI_ORD_EXTTEXTOUT, DDI_ORD_GETCHARWIDTH,
|
|
DDI_ORD_DEVICEBITMAP, DDI_ORD_FASTBORDER, DDI_ORD_SETATTRIBUTE,
|
|
DDI_ORD_DIBTODEVICE, DDI_ORD_CREATEBITMAP, DDI_ORD_DELETEBITMAP,
|
|
DDI_ORD_SELECTBITMAP, DDI_ORD_BITMAPBITS, DDI_ORD_RECLIP,
|
|
DDI_ORD_GETPALETTE, DDI_ORD_SETPALETTE, DDI_ORD_SETPALETTETRANS,
|
|
DDI_ORD_UPDATECOLORS, DDI_ORD_STRETCHBLT, DDI_ORD_STRETCHDIBITS,
|
|
DDI_ORD_SELECTPALETTE,
|
|
DDI_ORD_INQUIRE, DDI_ORD_SETCURSOR, DDI_ORD_MOVECURSOR,
|
|
DDI_ORD_CHECKCRSR,
|
|
0 // Sentinel
|
|
};
|
|
|
|
int found = 0;
|
|
for (int i = 0; ddiOrdinals[i] != 0; i++) {
|
|
uint16_t ord = ddiOrdinals[i];
|
|
uint16_t seg;
|
|
uint16_t off;
|
|
uint16_t sel;
|
|
|
|
if (neLookupExport(&drv->neMod, ord, &seg, &off, &sel)) {
|
|
drv->ddiEntry[ord].sel = sel;
|
|
drv->ddiEntry[ord].off = off;
|
|
drv->ddiEntry[ord].present = true;
|
|
found++;
|
|
|
|
dbg("windrv: DDI ord %u -> %04X:%04X\n", ord, sel, off);
|
|
}
|
|
}
|
|
|
|
dbg("windrv: resolved %d DDI entry points\n", found);
|
|
return found > 0;
|
|
}
|
|
|
|
|
|
// Extend DGROUP to include space for GDI objects.
|
|
// Layout within the extension area (16-byte aligned):
|
|
// +0x0000: PDEVICE (4096 bytes)
|
|
// +0x1000: PhysBrush (128 bytes)
|
|
// +0x1080: LogBrush (16 bytes)
|
|
// +0x1090: DrawMode (48 bytes)
|
|
// +0x10C0: PhysPen (128 bytes)
|
|
// +0x1140: LogPen (16 bytes)
|
|
// Total: 0x1150 bytes
|
|
#define DGROUP_OBJ_PDEV_OFF 0x0000
|
|
#define DGROUP_OBJ_BRUSH_OFF 0x1000
|
|
#define DGROUP_OBJ_LOGBRUSH_OFF 0x1080
|
|
#define DGROUP_OBJ_DRAWMODE_OFF 0x1090
|
|
#define DGROUP_OBJ_PEN_OFF 0x10C0
|
|
#define DGROUP_OBJ_LOGPEN_OFF 0x1140
|
|
#define DGROUP_OBJ_PHYSCOLOR_OFF 0x1150
|
|
#define DGROUP_OBJ_TOTAL_SIZE 0x1158
|
|
|
|
static bool extendDgroupForObjects(struct WdrvDriverS *drv)
|
|
{
|
|
int dgIdx = drv->neMod.neHeader.autoDataSegIndex - 1;
|
|
if (dgIdx < 0 || dgIdx >= drv->neMod.segmentCount) {
|
|
logErr("windrv: no DGROUP segment\n");
|
|
return false;
|
|
}
|
|
|
|
uint32_t oldSize = drv->neMod.segments[dgIdx].size;
|
|
|
|
// Align object area start to 16 bytes
|
|
uint32_t objBase = (oldSize + 15) & ~15;
|
|
|
|
// The S3 driver uses DGROUP offsets well beyond the initial data for
|
|
// graphics engine working buffers (e.g., 0xA6E8, 0xBEE8). In Windows
|
|
// 3.x, DGROUP is typically the full 64K segment. Extend to 64K to
|
|
// ensure the driver has all the working space it expects.
|
|
uint32_t targetSize = 0x10000;
|
|
if (objBase + DGROUP_OBJ_TOTAL_SIZE > targetSize) {
|
|
logErr("windrv: DGROUP objects don't fit in 64K\n");
|
|
return false;
|
|
}
|
|
uint32_t extraBytes = targetSize - oldSize;
|
|
|
|
uint32_t oldSizeOut;
|
|
if (!neExtendSegment(&drv->neMod, dgIdx, extraBytes, &oldSizeOut)) {
|
|
return false;
|
|
}
|
|
|
|
uint32_t dgLinear = drv->neMod.segments[dgIdx].linearAddr;
|
|
|
|
// Initialize DGROUP stack management fields if needed. In real Windows,
|
|
// KERNEL sets these during module loading. VGA.DRV ships with
|
|
// [0x0A]=0xFFFF which its stack check function interprets as "no stack
|
|
// space available", causing all deep functions (BitBlt, etc.) to fail.
|
|
// Only patch if the original data has the 0xFFFF sentinel.
|
|
{
|
|
uint16_t *dgWords = (uint16_t *)dgLinear;
|
|
if (dgWords[5] == 0xFFFF) {
|
|
dgWords[5] = (uint16_t)objBase; // [0x0A] pStackBot
|
|
dbg("windrv: patched DGROUP stack bottom [0x0A] from FFFF to %04X\n",
|
|
(uint16_t)objBase);
|
|
}
|
|
if (dgWords[4] == 0xFFFF) {
|
|
dgWords[4] = 0xFFFE; // [0x08] pStackMin
|
|
}
|
|
}
|
|
|
|
drv->dgroupObjBase = objBase;
|
|
|
|
drv->pdevOff = (uint16_t)(objBase + DGROUP_OBJ_PDEV_OFF);
|
|
drv->pdevLinear = dgLinear + objBase + DGROUP_OBJ_PDEV_OFF;
|
|
drv->pdevSize = PDEVICE_MAX_SIZE;
|
|
|
|
drv->brushOff = (uint16_t)(objBase + DGROUP_OBJ_BRUSH_OFF);
|
|
drv->brushLinear = dgLinear + objBase + DGROUP_OBJ_BRUSH_OFF;
|
|
|
|
drv->logBrushOff = (uint16_t)(objBase + DGROUP_OBJ_LOGBRUSH_OFF);
|
|
drv->logBrushLinear = dgLinear + objBase + DGROUP_OBJ_LOGBRUSH_OFF;
|
|
|
|
drv->drawModeOff = (uint16_t)(objBase + DGROUP_OBJ_DRAWMODE_OFF);
|
|
drv->drawModeLinear = dgLinear + objBase + DGROUP_OBJ_DRAWMODE_OFF;
|
|
|
|
drv->penOff = (uint16_t)(objBase + DGROUP_OBJ_PEN_OFF);
|
|
drv->penLinear = dgLinear + objBase + DGROUP_OBJ_PEN_OFF;
|
|
|
|
drv->logPenOff = (uint16_t)(objBase + DGROUP_OBJ_LOGPEN_OFF);
|
|
drv->logPenLinear = dgLinear + objBase + DGROUP_OBJ_LOGPEN_OFF;
|
|
|
|
drv->physColorOff = (uint16_t)(objBase + DGROUP_OBJ_PHYSCOLOR_OFF);
|
|
drv->physColorLinear = dgLinear + objBase + DGROUP_OBJ_PHYSCOLOR_OFF;
|
|
|
|
dbg("windrv: DGROUP extended by %" PRIu32 " bytes (old=%" PRIu32 " new=%" PRIu32 ")\n",
|
|
extraBytes, oldSize, drv->neMod.segments[dgIdx].size);
|
|
dbg("windrv: DGROUP objects: pdev=%04X brush=%04X logBrush=%04X drawMode=%04X pen=%04X logPen=%04X\n",
|
|
drv->pdevOff, drv->brushOff, drv->logBrushOff, drv->drawModeOff, drv->penOff, drv->logPenOff);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
static bool allocPDevice(struct WdrvDriverS *drv)
|
|
{
|
|
// PDEVICE is pre-allocated within DGROUP by extendDgroupForObjects
|
|
memset((void *)drv->pdevLinear, 0, drv->pdevSize);
|
|
return true;
|
|
}
|
|
|
|
|
|
static bool allocDrawMode(struct WdrvDriverS *drv)
|
|
{
|
|
// DrawMode is pre-allocated within DGROUP
|
|
DrawMode16T *dm = (DrawMode16T *)drv->drawModeLinear;
|
|
memset(dm, 0, sizeof(DrawMode16T));
|
|
dm->rop2 = R2_COPYPEN;
|
|
dm->bkMode = BM_OPAQUE;
|
|
return true;
|
|
}
|
|
|
|
|
|
static bool allocBrushBuffers(struct WdrvDriverS *drv)
|
|
{
|
|
// Both brushes are pre-allocated within DGROUP
|
|
LogBrush16T *lb = (LogBrush16T *)drv->logBrushLinear;
|
|
memset(lb, 0, sizeof(LogBrush16T));
|
|
lb->lbStyle = BS_SOLID;
|
|
lb->lbColor = 0x00FFFFFF;
|
|
|
|
memset((void *)drv->brushLinear, 0, PHYS_OBJ_MAX_SIZE);
|
|
drv->brushRealized = false;
|
|
return true;
|
|
}
|
|
|
|
|
|
static bool allocPenBuffers(struct WdrvDriverS *drv)
|
|
{
|
|
// Both pens are pre-allocated within DGROUP
|
|
LogPen16T *lp = (LogPen16T *)drv->logPenLinear;
|
|
memset(lp, 0, sizeof(LogPen16T));
|
|
lp->lopnStyle = PS_SOLID;
|
|
lp->lopnWidth.x = 1;
|
|
lp->lopnWidth.y = 0;
|
|
lp->lopnColor = 0x00000000;
|
|
|
|
memset((void *)drv->penLinear, 0, PHYS_OBJ_MAX_SIZE);
|
|
drv->penRealized = false;
|
|
return true;
|
|
}
|
|
|
|
|
|
static uint32_t colorToPhys(struct WdrvDriverS *drv, uint32_t colorRef)
|
|
{
|
|
if (!drv->ddiEntry[DDI_ORD_COLORINFO].present) {
|
|
return colorRef;
|
|
}
|
|
|
|
// DWORD PASCAL ColorInfo(LPDEVICE lpDevice, DWORD dwColorIn,
|
|
// LPDWORD lpPhysColor)
|
|
// Pascal push order: lpDevice(2w), dwColorIn(2w), lpPhysColor(2w)
|
|
uint16_t dgSel = drv->neMod.autoDataSel;
|
|
uint16_t params[6];
|
|
params[0] = dgSel; // lpDevice seg
|
|
params[1] = drv->pdevOff; // lpDevice off
|
|
params[2] = (uint16_t)(colorRef >> 16); // dwColorIn high
|
|
params[3] = (uint16_t)(colorRef); // dwColorIn low
|
|
params[4] = dgSel; // lpPhysColor seg
|
|
params[5] = drv->physColorOff; // lpPhysColor off
|
|
|
|
// Clear the output buffer
|
|
*(uint32_t *)drv->physColorLinear = 0;
|
|
|
|
waitForEngine();
|
|
|
|
thunkCall16(&gThunkCtx,
|
|
drv->ddiEntry[DDI_ORD_COLORINFO].sel,
|
|
drv->ddiEntry[DDI_ORD_COLORINFO].off,
|
|
params, 6);
|
|
|
|
waitForEngine();
|
|
|
|
uint32_t physColor = *(uint32_t *)drv->physColorLinear;
|
|
dbg("windrv: ColorInfo(0x%06lX) -> phys 0x%08lX\n",
|
|
(unsigned long)colorRef, (unsigned long)physColor);
|
|
|
|
return physColor;
|
|
}
|
|
|
|
|
|
static void setDisplayStart(struct WdrvDriverS *drv, uint32_t byteOffset)
|
|
{
|
|
(void)drv;
|
|
|
|
// S3 display start address is in units of 4 bytes (DWORDs).
|
|
// CR0C:CR0D = bits 15:0, CR31[5:4] = bits 17:16, CR51[1:0] = bits 19:18
|
|
uint32_t startAddr = byteOffset / 4;
|
|
uint16_t crtcBase = (inportb(0x3CC) & 0x01) ? 0x3D4 : 0x3B4;
|
|
|
|
// Unlock S3 registers
|
|
outportb(crtcBase, 0x38);
|
|
outportb(crtcBase + 1, 0x48);
|
|
outportb(crtcBase, 0x39);
|
|
outportb(crtcBase + 1, 0xA5);
|
|
|
|
// Write display start address bits 15:0
|
|
outportb(crtcBase, 0x0D);
|
|
outportb(crtcBase + 1, (uint8_t)(startAddr & 0xFF));
|
|
outportb(crtcBase, 0x0C);
|
|
outportb(crtcBase + 1, (uint8_t)((startAddr >> 8) & 0xFF));
|
|
|
|
// Write bits 17:16 to CR31
|
|
outportb(crtcBase, 0x31);
|
|
uint8_t cr31 = inportb(crtcBase + 1);
|
|
cr31 = (cr31 & ~0x30) | (uint8_t)(((startAddr >> 16) & 0x03) << 4);
|
|
outportb(crtcBase + 1, cr31);
|
|
|
|
// Write bits 19:18 to CR51
|
|
outportb(crtcBase, 0x51);
|
|
uint8_t cr51 = inportb(crtcBase + 1);
|
|
cr51 = (cr51 & ~0x03) | (uint8_t)((startAddr >> 18) & 0x03);
|
|
outportb(crtcBase + 1, cr51);
|
|
|
|
dbg("windrv: display start set to byte offset %lu (reg=0x%lX)\n",
|
|
(unsigned long)byteOffset, (unsigned long)startAddr);
|
|
}
|
|
|
|
|
|
static bool realizeBrush(struct WdrvDriverS *drv, uint32_t color)
|
|
{
|
|
if (!drv->ddiEntry[DDI_ORD_REALIZEOBJECT].present) {
|
|
return false;
|
|
}
|
|
|
|
uint16_t dgSel = drv->neMod.autoDataSel;
|
|
|
|
// Set up the logical brush
|
|
LogBrush16T *lb = (LogBrush16T *)drv->logBrushLinear;
|
|
lb->lbStyle = BS_SOLID;
|
|
lb->lbColor = color;
|
|
lb->lbHatch = 0;
|
|
|
|
// Clear the physical brush buffer
|
|
memset((void *)drv->brushLinear, 0, PHYS_OBJ_MAX_SIZE);
|
|
|
|
// RealizeObject(lpDevice, nStyle, lpInObj, lpOutObj, lpTextXForm)
|
|
// Pascal push order: left-to-right
|
|
uint16_t params[9];
|
|
params[0] = dgSel; // lpDevice seg
|
|
params[1] = drv->pdevOff; // lpDevice off
|
|
params[2] = OBJ_BRUSH; // nStyle
|
|
params[3] = dgSel; // lpInObj seg
|
|
params[4] = drv->logBrushOff; // lpInObj off
|
|
params[5] = dgSel; // lpOutObj seg
|
|
params[6] = drv->brushOff; // lpOutObj off
|
|
params[7] = 0; // lpTextXForm seg (NULL)
|
|
params[8] = 0; // lpTextXForm off (NULL)
|
|
|
|
waitForEngine();
|
|
|
|
uint32_t result = thunkCall16(&gThunkCtx,
|
|
drv->ddiEntry[DDI_ORD_REALIZEOBJECT].sel,
|
|
drv->ddiEntry[DDI_ORD_REALIZEOBJECT].off,
|
|
params, 9);
|
|
|
|
waitForEngine();
|
|
|
|
dbg("windrv: RealizeObject(brush, color=0x%06lX) returned %d\n",
|
|
(unsigned long)color, (int16_t)(result & 0xFFFF));
|
|
|
|
if ((int16_t)(result & 0xFFFF) > 0) {
|
|
drv->brushRealized = true;
|
|
drv->brushRealizedColor = color;
|
|
|
|
// Dump the first 16 bytes of the realized brush
|
|
uint8_t *bdata = (uint8_t *)drv->brushLinear;
|
|
dbg("windrv: brush[0..15]:");
|
|
for (int k = 0; k < 16; k++) {
|
|
dbg(" %02X", bdata[k]);
|
|
}
|
|
dbg("\n");
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
static bool realizeStyledPen(struct WdrvDriverS *drv, uint32_t color, int16_t style)
|
|
{
|
|
if (!drv->ddiEntry[DDI_ORD_REALIZEOBJECT].present) {
|
|
return false;
|
|
}
|
|
|
|
uint16_t dgSel = drv->neMod.autoDataSel;
|
|
|
|
// Set up the logical pen
|
|
LogPen16T *lp = (LogPen16T *)drv->logPenLinear;
|
|
lp->lopnStyle = (uint16_t)style;
|
|
lp->lopnWidth.x = 1;
|
|
lp->lopnWidth.y = 0;
|
|
lp->lopnColor = color;
|
|
|
|
// Clear the physical pen buffer
|
|
memset((void *)drv->penLinear, 0, PHYS_OBJ_MAX_SIZE);
|
|
|
|
// RealizeObject(lpDevice, nStyle, lpInObj, lpOutObj, lpTextXForm)
|
|
// Pascal push order: left-to-right
|
|
uint16_t params[9];
|
|
params[0] = dgSel; // lpDevice seg
|
|
params[1] = drv->pdevOff; // lpDevice off
|
|
params[2] = OBJ_PEN; // nStyle
|
|
params[3] = dgSel; // lpInObj seg
|
|
params[4] = drv->logPenOff; // lpInObj off
|
|
params[5] = dgSel; // lpOutObj seg
|
|
params[6] = drv->penOff; // lpOutObj off
|
|
params[7] = 0; // lpTextXForm seg (NULL)
|
|
params[8] = 0; // lpTextXForm off (NULL)
|
|
|
|
waitForEngine();
|
|
|
|
uint32_t result = thunkCall16(&gThunkCtx,
|
|
drv->ddiEntry[DDI_ORD_REALIZEOBJECT].sel,
|
|
drv->ddiEntry[DDI_ORD_REALIZEOBJECT].off,
|
|
params, 9);
|
|
|
|
waitForEngine();
|
|
|
|
dbg("windrv: RealizeObject(pen, color=0x%06lX, style=%d) returned %d\n",
|
|
(unsigned long)color, (int)style, (int16_t)(result & 0xFFFF));
|
|
|
|
if ((int16_t)(result & 0xFFFF) > 0) {
|
|
drv->penRealized = true;
|
|
drv->penRealizedColor = color;
|
|
drv->penRealizedStyle = style;
|
|
|
|
// Dump the first 16 bytes of the realized pen
|
|
uint8_t *pdata = (uint8_t *)drv->penLinear;
|
|
dbg("windrv: pen[0..15]:");
|
|
for (int k = 0; k < 16; k++) {
|
|
dbg(" %02X", pdata[k]);
|
|
}
|
|
dbg("\n");
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
static void freeDrawObjects(struct WdrvDriverS *drv)
|
|
{
|
|
// Objects are embedded in DGROUP - freed when module is unloaded
|
|
drv->brushRealized = false;
|
|
drv->penRealized = false;
|
|
drv->penRealizedStyle = 0;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Font loading
|
|
//
|
|
// buildFontFromFnt() converts raw .FNT data (v2 or v3) into a 16-bit
|
|
// accessible font block in v3 format. v2->v3 conversion expands the
|
|
// 4-byte char table entries to 6-byte entries and adjusts all offsets.
|
|
//
|
|
// initBuiltinFont() builds a v3 font from the VGA BIOS 8x16 ROM font.
|
|
// ============================================================================
|
|
|
|
// Built-in font layout offsets (fixed for the synthesized VGA ROM font)
|
|
#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 WdrvFontT buildFontFromFnt(const uint8_t *fntData, uint32_t fntSize)
|
|
{
|
|
if (fntSize < sizeof(FntHeader16T)) {
|
|
logErr("windrv: buildFontFromFnt: data too small (%" PRIu32 " bytes)\n", fntSize);
|
|
setError(WDRV_ERR_BAD_FONT);
|
|
return NULL;
|
|
}
|
|
|
|
const FntHeader16T *srcHdr = (const FntHeader16T *)fntData;
|
|
|
|
if (srcHdr->fsVersion != 0x0200 && srcHdr->fsVersion != 0x0300) {
|
|
logErr("windrv: buildFontFromFnt: unsupported version 0x%04X\n", srcHdr->fsVersion);
|
|
setError(WDRV_ERR_BAD_FONT);
|
|
return NULL;
|
|
}
|
|
if (srcHdr->fsType != 0) {
|
|
logErr("windrv: buildFontFromFnt: not a raster font (type=%u)\n", srcHdr->fsType);
|
|
setError(WDRV_ERR_BAD_FONT);
|
|
return NULL;
|
|
}
|
|
|
|
uint16_t origVersion = srcHdr->fsVersion;
|
|
uint16_t nChars = (uint16_t)(srcHdr->fsLastChar - srcHdr->fsFirstChar + 1);
|
|
|
|
uint32_t newSize;
|
|
uint32_t linearOut;
|
|
uint16_t sel;
|
|
uint8_t *base;
|
|
|
|
if (origVersion == 0x0300) {
|
|
// Already v3 — allocate, copy, patch fsBitsPointer
|
|
newSize = fntSize;
|
|
sel = alloc16BitBlock(newSize, &linearOut);
|
|
if (sel == 0) {
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return NULL;
|
|
}
|
|
base = (uint8_t *)linearOut;
|
|
memcpy(base, fntData, fntSize);
|
|
|
|
FntHeader16T *hdr = (FntHeader16T *)base;
|
|
hdr->fsBitsPointer = ((uint32_t)sel << 16) | hdr->fsBitsOffset;
|
|
} else {
|
|
// v2 -> v3 conversion
|
|
//
|
|
// v2 layout:
|
|
// 0x0000: Header (117 bytes)
|
|
// 0x0075: Pad byte
|
|
// 0x0076: v2 char table ((nChars+1) * 4 bytes)
|
|
// fsBitsOffset: Bitmap data + strings
|
|
//
|
|
// v3 layout:
|
|
// 0x0000: Header (117 bytes, fsVersion -> 0x0300)
|
|
// 0x0075: Pad byte
|
|
// 0x0076: v3 extension (30 bytes, zeroed)
|
|
// 0x0094: v3 char table ((nChars+1) * 6 bytes)
|
|
// newBitmapOff: Bitmap data + strings (copied)
|
|
|
|
uint32_t v3CharTableOff = 0x0094;
|
|
uint32_t newBitmapOff = v3CharTableOff + (uint32_t)(nChars + 1) * 6;
|
|
uint32_t origBitsOff = srcHdr->fsBitsOffset;
|
|
uint32_t bitmapLen = fntSize - origBitsOff;
|
|
newSize = newBitmapOff + bitmapLen;
|
|
|
|
sel = alloc16BitBlock(newSize, &linearOut);
|
|
if (sel == 0) {
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return NULL;
|
|
}
|
|
base = (uint8_t *)linearOut;
|
|
memset(base, 0, newSize);
|
|
|
|
// Copy the 117-byte header
|
|
memcpy(base, fntData, sizeof(FntHeader16T));
|
|
|
|
// Patch header for v3
|
|
FntHeader16T *hdr = (FntHeader16T *)base;
|
|
hdr->fsVersion = 0x0300;
|
|
hdr->fsSize = newSize;
|
|
hdr->fsBitsOffset = newBitmapOff;
|
|
hdr->fsBitsPointer = ((uint32_t)sel << 16) | newBitmapOff;
|
|
|
|
// Adjust string offsets that point into the bitmap area
|
|
int32_t shift = (int32_t)newBitmapOff - (int32_t)origBitsOff;
|
|
if (hdr->fsDevice >= origBitsOff) {
|
|
hdr->fsDevice += shift;
|
|
}
|
|
if (hdr->fsFace >= origBitsOff) {
|
|
hdr->fsFace += shift;
|
|
}
|
|
|
|
// v3 extension at 0x76 is already zeroed (30 bytes)
|
|
|
|
// Convert v2 char table to v3 char table
|
|
const FntCharEntry16T *v2Table = (const FntCharEntry16T *)(fntData + 0x0076);
|
|
FntCharEntry30T *v3Table = (FntCharEntry30T *)(base + v3CharTableOff);
|
|
for (uint16_t c = 0; c <= nChars; c++) {
|
|
v3Table[c].width = v2Table[c].width;
|
|
v3Table[c].offset = (uint32_t)((int32_t)v2Table[c].offset + shift);
|
|
}
|
|
|
|
// Copy bitmap data + string data
|
|
memcpy(base + newBitmapOff, fntData + origBitsOff, bitmapLen);
|
|
}
|
|
|
|
// Allocate the font handle
|
|
struct WdrvFontS *font = (struct WdrvFontS *)calloc(1, sizeof(struct WdrvFontS));
|
|
if (!font) {
|
|
free16BitBlock(sel, linearOut);
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return NULL;
|
|
}
|
|
|
|
FntHeader16T *hdr = (FntHeader16T *)base;
|
|
font->fontSel = sel;
|
|
font->fontLinear = linearOut;
|
|
font->fontSize = newSize;
|
|
font->pixHeight = hdr->fsPixHeight;
|
|
font->pixWidth = hdr->fsPixWidth;
|
|
font->fsVersion = origVersion;
|
|
|
|
// Read face name string from the font block
|
|
if (hdr->fsFace < newSize) {
|
|
strncpy(font->faceName, (const char *)(base + hdr->fsFace), sizeof(font->faceName) - 1);
|
|
font->faceName[sizeof(font->faceName) - 1] = '\0';
|
|
} else {
|
|
strcpy(font->faceName, "Unknown");
|
|
}
|
|
|
|
dbg("windrv: buildFontFromFnt: \"%s\" %ux%u v%c->v3 sel=%04X size=%" PRIu32 "\n",
|
|
font->faceName, font->pixWidth, font->pixHeight,
|
|
origVersion == 0x0200 ? '2' : '3', sel, newSize);
|
|
|
|
return font;
|
|
}
|
|
|
|
|
|
static bool initBuiltinFont(void)
|
|
{
|
|
// Get VGA BIOS 8x16 font pointer via INT 10h AH=11h AL=30h BH=06
|
|
__dpmi_regs regs;
|
|
memset(®s, 0, sizeof(regs));
|
|
regs.x.ax = 0x1130;
|
|
regs.x.bx = 0x0600;
|
|
__dpmi_int(0x10, ®s);
|
|
|
|
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: initBuiltinFont: 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: initBuiltinFont: 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
|
|
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
|
|
|
|
// Build v3 character table (257 entries: 256 chars + sentinel)
|
|
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
|
|
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);
|
|
|
|
gBuiltinFont.fontSel = sel;
|
|
gBuiltinFont.fontLinear = linearOut;
|
|
gBuiltinFont.fontSize = FONT_TOTAL_SIZE;
|
|
gBuiltinFont.pixHeight = 16;
|
|
gBuiltinFont.pixWidth = 8;
|
|
gBuiltinFont.fsVersion = 0x0300;
|
|
strcpy(gBuiltinFont.faceName, "System");
|
|
gBuiltinFontInit = true;
|
|
|
|
dbg("windrv: initBuiltinFont: v3 font block sel=%04X linear=0x%08lX size=%u\n",
|
|
sel, (unsigned long)linearOut, FONT_TOTAL_SIZE);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
WdrvFontT wdrvLoadFontBuiltin(void)
|
|
{
|
|
if (!gBuiltinFontInit) {
|
|
if (!initBuiltinFont()) {
|
|
return NULL;
|
|
}
|
|
}
|
|
return &gBuiltinFont;
|
|
}
|
|
|
|
|
|
WdrvFontT wdrvLoadFontFnt(const char *fntPath)
|
|
{
|
|
FILE *f = fopen(fntPath, "rb");
|
|
if (!f) {
|
|
logErr("windrv: wdrvLoadFontFnt: cannot open '%s'\n", fntPath);
|
|
setError(WDRV_ERR_FILE_NOT_FOUND);
|
|
return NULL;
|
|
}
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
long fileSize = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
if (fileSize < (long)sizeof(FntHeader16T) || fileSize > 0x100000) {
|
|
logErr("windrv: wdrvLoadFontFnt: bad file size %ld\n", fileSize);
|
|
fclose(f);
|
|
setError(WDRV_ERR_BAD_FONT);
|
|
return NULL;
|
|
}
|
|
|
|
uint8_t *buf = (uint8_t *)malloc((uint32_t)fileSize);
|
|
if (!buf) {
|
|
fclose(f);
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return NULL;
|
|
}
|
|
|
|
if (fread(buf, 1, (uint32_t)fileSize, f) != (size_t)fileSize) {
|
|
logErr("windrv: wdrvLoadFontFnt: read error\n");
|
|
free(buf);
|
|
fclose(f);
|
|
setError(WDRV_ERR_BAD_FONT);
|
|
return NULL;
|
|
}
|
|
fclose(f);
|
|
|
|
WdrvFontT font = buildFontFromFnt(buf, (uint32_t)fileSize);
|
|
free(buf);
|
|
return font;
|
|
}
|
|
|
|
|
|
WdrvFontT wdrvLoadFontFon(const char *fonPath, int32_t fontIndex)
|
|
{
|
|
FILE *f = fopen(fonPath, "rb");
|
|
if (!f) {
|
|
logErr("windrv: wdrvLoadFontFon: cannot open '%s'\n", fonPath);
|
|
setError(WDRV_ERR_FILE_NOT_FOUND);
|
|
return NULL;
|
|
}
|
|
|
|
// Read MZ header
|
|
MzHeaderT mz;
|
|
if (fread(&mz, sizeof(mz), 1, f) != 1 || mz.signature != MZ_SIGNATURE) {
|
|
logErr("windrv: wdrvLoadFontFon: not a valid MZ executable\n");
|
|
fclose(f);
|
|
setError(WDRV_ERR_BAD_FORMAT);
|
|
return NULL;
|
|
}
|
|
|
|
// Read NE header
|
|
uint32_t neOff = mz.neHeaderOffset;
|
|
fseek(f, (long)neOff, SEEK_SET);
|
|
NeHeaderT ne;
|
|
if (fread(&ne, sizeof(ne), 1, f) != 1 || ne.signature != NE_SIGNATURE) {
|
|
logErr("windrv: wdrvLoadFontFon: not a valid NE executable\n");
|
|
fclose(f);
|
|
setError(WDRV_ERR_BAD_FORMAT);
|
|
return NULL;
|
|
}
|
|
|
|
// Seek to resource table
|
|
uint32_t resTableOff = neOff + ne.resourceTableOffset;
|
|
fseek(f, (long)resTableOff, SEEK_SET);
|
|
|
|
// Read resource alignment shift
|
|
uint16_t rscAlignShift;
|
|
if (fread(&rscAlignShift, 2, 1, f) != 1) {
|
|
logErr("windrv: wdrvLoadFontFon: cannot read resource table\n");
|
|
fclose(f);
|
|
setError(WDRV_ERR_BAD_FORMAT);
|
|
return NULL;
|
|
}
|
|
|
|
// Walk type groups looking for RT_FONT
|
|
WdrvFontT result = NULL;
|
|
for (;;) {
|
|
NeResourceTypeT typeHdr;
|
|
if (fread(&typeHdr, sizeof(typeHdr), 1, f) != 1) {
|
|
break;
|
|
}
|
|
if (typeHdr.typeId == 0) {
|
|
break; // end of resource table
|
|
}
|
|
|
|
uint16_t typeNum = typeHdr.typeId & 0x7FFF;
|
|
if (typeNum == RT_FONT) {
|
|
// Found font resources — read all entries
|
|
if (fontIndex < 0 || fontIndex >= (int32_t)typeHdr.count) {
|
|
logErr("windrv: wdrvLoadFontFon: fontIndex %" PRId32 " out of range (0..%u)\n",
|
|
fontIndex, typeHdr.count - 1);
|
|
fclose(f);
|
|
setError(WDRV_ERR_BAD_FONT);
|
|
return NULL;
|
|
}
|
|
|
|
// Read all entries to get to the requested one
|
|
NeResourceEntryT *entries = (NeResourceEntryT *)malloc(
|
|
(uint32_t)typeHdr.count * sizeof(NeResourceEntryT));
|
|
if (!entries) {
|
|
fclose(f);
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return NULL;
|
|
}
|
|
|
|
if (fread(entries, sizeof(NeResourceEntryT), typeHdr.count, f) != typeHdr.count) {
|
|
logErr("windrv: wdrvLoadFontFon: cannot read resource entries\n");
|
|
free(entries);
|
|
fclose(f);
|
|
setError(WDRV_ERR_BAD_FORMAT);
|
|
return NULL;
|
|
}
|
|
|
|
uint32_t fntOffset = (uint32_t)entries[fontIndex].fileOffset << rscAlignShift;
|
|
uint32_t fntLength = (uint32_t)entries[fontIndex].length << rscAlignShift;
|
|
free(entries);
|
|
|
|
dbg("windrv: wdrvLoadFontFon: font[%" PRId32 "] offset=0x%08" PRIX32 " length=%" PRIu32 "\n",
|
|
fontIndex, fntOffset, fntLength);
|
|
|
|
// Read the .FNT data
|
|
uint8_t *fntBuf = (uint8_t *)malloc(fntLength);
|
|
if (!fntBuf) {
|
|
fclose(f);
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return NULL;
|
|
}
|
|
|
|
fseek(f, (long)fntOffset, SEEK_SET);
|
|
if (fread(fntBuf, 1, fntLength, f) != fntLength) {
|
|
logErr("windrv: wdrvLoadFontFon: cannot read font data\n");
|
|
free(fntBuf);
|
|
fclose(f);
|
|
setError(WDRV_ERR_BAD_FONT);
|
|
return NULL;
|
|
}
|
|
fclose(f);
|
|
|
|
result = buildFontFromFnt(fntBuf, fntLength);
|
|
free(fntBuf);
|
|
return result;
|
|
}
|
|
|
|
// Skip past this type's resource entries
|
|
fseek(f, (long)typeHdr.count * sizeof(NeResourceEntryT), SEEK_CUR);
|
|
}
|
|
|
|
logErr("windrv: wdrvLoadFontFon: no FONT resources found in '%s'\n", fonPath);
|
|
fclose(f);
|
|
setError(WDRV_ERR_BAD_FONT);
|
|
return NULL;
|
|
}
|
|
|
|
|
|
WdrvFontT wdrvLoadFontTtf(const char *ttfPath, int32_t pointSize)
|
|
{
|
|
// Read TTF file into memory
|
|
FILE *f = fopen(ttfPath, "rb");
|
|
if (!f) {
|
|
logErr("windrv: wdrvLoadFontTtf: cannot open '%s'\n", ttfPath);
|
|
setError(WDRV_ERR_FILE_NOT_FOUND);
|
|
return NULL;
|
|
}
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
long fileSize = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
if (fileSize < 12 || fileSize > 0x1000000) {
|
|
logErr("windrv: wdrvLoadFontTtf: bad file size %ld\n", fileSize);
|
|
fclose(f);
|
|
setError(WDRV_ERR_BAD_FONT);
|
|
return NULL;
|
|
}
|
|
|
|
uint8_t *ttfBuf = (uint8_t *)malloc((uint32_t)fileSize);
|
|
if (!ttfBuf) {
|
|
fclose(f);
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return NULL;
|
|
}
|
|
|
|
if (fread(ttfBuf, 1, (uint32_t)fileSize, f) != (size_t)fileSize) {
|
|
logErr("windrv: wdrvLoadFontTtf: read error\n");
|
|
free(ttfBuf);
|
|
fclose(f);
|
|
setError(WDRV_ERR_BAD_FONT);
|
|
return NULL;
|
|
}
|
|
fclose(f);
|
|
|
|
// Initialize stb_truetype
|
|
stbtt_fontinfo stbFont;
|
|
if (!stbtt_InitFont(&stbFont, ttfBuf, 0)) {
|
|
logErr("windrv: wdrvLoadFontTtf: stbtt_InitFont failed\n");
|
|
free(ttfBuf);
|
|
setError(WDRV_ERR_BAD_FONT);
|
|
return NULL;
|
|
}
|
|
|
|
// Compute scale for target pixel height
|
|
float scale = stbtt_ScaleForPixelHeight(&stbFont, (float)pointSize);
|
|
|
|
// Get font vertical metrics (unscaled)
|
|
int stbAscent;
|
|
int stbDescent;
|
|
int stbLineGap;
|
|
stbtt_GetFontVMetrics(&stbFont, &stbAscent, &stbDescent, &stbLineGap);
|
|
|
|
int32_t scaledAscent = (int32_t)(stbAscent * scale + 0.5f);
|
|
int32_t scaledDescent = (int32_t)(-stbDescent * scale + 0.5f);
|
|
int32_t pixHeight = scaledAscent + scaledDescent;
|
|
if (pixHeight < 1) {
|
|
pixHeight = pointSize;
|
|
}
|
|
|
|
// Measure all glyphs (chars 32..255)
|
|
uint16_t nChars = 224;
|
|
uint8_t firstChar = 32;
|
|
uint8_t lastChar = 255;
|
|
|
|
uint16_t advanceWidths[224];
|
|
int32_t maxAdvance = 0;
|
|
int32_t totalAdvance = 0;
|
|
int32_t maxBitmapW = 0;
|
|
bool allSameWidth = true;
|
|
|
|
for (int32_t i = 0; i < nChars; i++) {
|
|
int ch = firstChar + i;
|
|
int advance;
|
|
int lsb;
|
|
stbtt_GetCodepointHMetrics(&stbFont, ch, &advance, &lsb);
|
|
|
|
int32_t advW = (int32_t)(advance * scale + 0.5f);
|
|
if (advW < 1) {
|
|
advW = 1;
|
|
}
|
|
advanceWidths[i] = (uint16_t)advW;
|
|
totalAdvance += advW;
|
|
if (advW > maxAdvance) {
|
|
maxAdvance = advW;
|
|
}
|
|
if (i > 0 && advanceWidths[i] != advanceWidths[0]) {
|
|
allSameWidth = false;
|
|
}
|
|
|
|
// Check bitmap bounding box too
|
|
int x0;
|
|
int y0;
|
|
int x1;
|
|
int y1;
|
|
stbtt_GetCodepointBitmapBox(&stbFont, ch, scale, scale, &x0, &y0, &x1, &y1);
|
|
int32_t bw = x1 - x0;
|
|
if (bw > maxBitmapW) {
|
|
maxBitmapW = bw;
|
|
}
|
|
}
|
|
|
|
// fsWidthBytes = sum of all per-character byte strides (matches .FON convention)
|
|
uint32_t widthBytesSum = 0;
|
|
for (int32_t i = 0; i < nChars; i++) {
|
|
uint16_t s = (advanceWidths[i] + 7) / 8;
|
|
if (s < 1) {
|
|
s = 1;
|
|
}
|
|
widthBytesSum += s;
|
|
}
|
|
uint16_t fsWidthBytes = (uint16_t)widthBytesSum;
|
|
|
|
// Compute v3 layout with per-character bitmap sizes.
|
|
// Each character's bitmap has its own row stride = ceil(charWidth/8).
|
|
uint32_t charTableOff = 0x0094;
|
|
uint32_t bitmapOff = charTableOff + (uint32_t)(nChars + 1) * 6;
|
|
|
|
uint32_t charOffsets[225];
|
|
uint32_t curOff = bitmapOff;
|
|
for (int32_t i = 0; i < nChars; i++) {
|
|
charOffsets[i] = curOff;
|
|
uint16_t byteStride = (advanceWidths[i] + 7) / 8;
|
|
if (byteStride < 1) {
|
|
byteStride = 1;
|
|
}
|
|
curOff += (uint32_t)byteStride * (uint32_t)pixHeight;
|
|
}
|
|
charOffsets[nChars] = charOffsets[0]; // sentinel reuses char 0
|
|
|
|
uint32_t devNameOff = curOff;
|
|
uint32_t faceNameOff = devNameOff + 1;
|
|
|
|
// Extract face name from filename stem
|
|
const char *baseName = ttfPath;
|
|
for (const char *p = ttfPath; *p; p++) {
|
|
if (*p == '/' || *p == '\\') {
|
|
baseName = p + 1;
|
|
}
|
|
}
|
|
char faceName[64];
|
|
strncpy(faceName, baseName, sizeof(faceName) - 1);
|
|
faceName[sizeof(faceName) - 1] = '\0';
|
|
char *dot = strrchr(faceName, '.');
|
|
if (dot) {
|
|
*dot = '\0';
|
|
}
|
|
uint32_t faceNameLen = (uint32_t)strlen(faceName);
|
|
uint32_t totalSize = faceNameOff + faceNameLen + 1;
|
|
|
|
// Allocate 16-bit accessible block
|
|
uint32_t linearOut;
|
|
uint16_t sel = alloc16BitBlock(totalSize, &linearOut);
|
|
if (sel == 0) {
|
|
free(ttfBuf);
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return NULL;
|
|
}
|
|
|
|
uint8_t *base = (uint8_t *)linearOut;
|
|
|
|
// Fill .FNT v3 header
|
|
FntHeader16T *hdr = (FntHeader16T *)base;
|
|
hdr->fsVersion = 0x0300;
|
|
hdr->fsSize = totalSize;
|
|
hdr->fsType = 0; // raster
|
|
hdr->fsPoints = (uint16_t)pointSize;
|
|
hdr->fsVertRes = 96;
|
|
hdr->fsHorizRes = 96;
|
|
hdr->fsAscent = (uint16_t)scaledAscent;
|
|
hdr->fsInternalLeading = 0;
|
|
hdr->fsExternalLeading = 0;
|
|
hdr->fsItalic = 0;
|
|
hdr->fsUnderline = 0;
|
|
hdr->fsStrikeOut = 0;
|
|
hdr->fsWeight = 400;
|
|
hdr->fsCharSet = 0; // ANSI
|
|
hdr->fsPixWidth = allSameWidth ? advanceWidths[0] : 0;
|
|
hdr->fsPixHeight = (uint16_t)pixHeight;
|
|
hdr->fsPitchAndFamily = allSameWidth ? 0x30 : 0x01;
|
|
hdr->fsAvgWidth = (uint16_t)(totalAdvance / nChars);
|
|
hdr->fsMaxWidth = (uint16_t)maxAdvance;
|
|
hdr->fsFirstChar = firstChar;
|
|
hdr->fsLastChar = lastChar;
|
|
hdr->fsDefaultChar = (uint8_t)('.' - firstChar);
|
|
hdr->fsBreakChar = (uint8_t)(' ' - firstChar);
|
|
hdr->fsWidthBytes = fsWidthBytes;
|
|
hdr->fsDevice = devNameOff;
|
|
hdr->fsFace = faceNameOff;
|
|
hdr->fsBitsPointer = ((uint32_t)sel << 16) | bitmapOff;
|
|
hdr->fsBitsOffset = bitmapOff;
|
|
|
|
// v3 extension at 0x76 already zeroed by calloc
|
|
|
|
// Fill v3 char table at 0x94
|
|
FntCharEntry30T *charTable = (FntCharEntry30T *)(base + charTableOff);
|
|
for (int32_t i = 0; i <= nChars; i++) {
|
|
int32_t idx = (i < nChars) ? i : 0; // sentinel reuses char 0
|
|
charTable[i].width = advanceWidths[idx];
|
|
charTable[i].offset = charOffsets[i];
|
|
}
|
|
|
|
// Rasterize glyphs and threshold to 1-bit
|
|
for (int32_t i = 0; i < nChars; i++) {
|
|
int ch = firstChar + i;
|
|
int bw;
|
|
int bh;
|
|
int xoff;
|
|
int yoff;
|
|
unsigned char *bitmap = stbtt_GetCodepointBitmap(
|
|
&stbFont, scale, scale, ch, &bw, &bh, &xoff, &yoff);
|
|
|
|
if (!bitmap || bw <= 0 || bh <= 0) {
|
|
if (bitmap) {
|
|
stbtt_FreeBitmap(bitmap, NULL);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Position glyph within the fixed-height cell.
|
|
// xoff/yoff are relative to the pen position; use directly and
|
|
// let the bounds checks clip anything outside the cell.
|
|
int32_t cellY = scaledAscent + yoff;
|
|
|
|
uint16_t charStride = (advanceWidths[i] + 7) / 8;
|
|
if (charStride < 1) {
|
|
charStride = 1;
|
|
}
|
|
uint8_t *dst = base + charOffsets[i];
|
|
|
|
// .FNT bitmap format is column-major: all pixHeight rows of
|
|
// byte-column 0 first, then all rows of byte-column 1, etc.
|
|
// Layout: dst[(byteCol * pixHeight) + row] bit = MSB-first
|
|
for (int row = 0; row < bh; row++) {
|
|
int32_t dstRow = cellY + row;
|
|
if (dstRow < 0 || dstRow >= pixHeight) {
|
|
continue;
|
|
}
|
|
for (int col = 0; col < bw; col++) {
|
|
int32_t dstCol = xoff + col;
|
|
if (dstCol < 0 || dstCol >= (int32_t)(charStride * 8)) {
|
|
continue;
|
|
}
|
|
uint8_t alpha = bitmap[row * bw + col];
|
|
if (alpha >= 128) {
|
|
uint32_t byteCol = (uint32_t)dstCol / 8;
|
|
dst[byteCol * pixHeight + dstRow] |= (0x80 >> (dstCol & 7));
|
|
}
|
|
}
|
|
}
|
|
|
|
stbtt_FreeBitmap(bitmap, NULL);
|
|
}
|
|
|
|
// Write device name (empty) and face name
|
|
base[devNameOff] = '\0';
|
|
memcpy(base + faceNameOff, faceName, faceNameLen + 1);
|
|
|
|
// Done with TTF data
|
|
free(ttfBuf);
|
|
|
|
// Allocate font handle
|
|
struct WdrvFontS *font = (struct WdrvFontS *)calloc(1, sizeof(struct WdrvFontS));
|
|
if (!font) {
|
|
free16BitBlock(sel, linearOut);
|
|
setError(WDRV_ERR_NO_MEMORY);
|
|
return NULL;
|
|
}
|
|
|
|
font->fontSel = sel;
|
|
font->fontLinear = linearOut;
|
|
font->fontSize = totalSize;
|
|
font->pixHeight = (uint16_t)pixHeight;
|
|
font->pixWidth = allSameWidth ? advanceWidths[0] : 0;
|
|
font->fsVersion = 0x0300;
|
|
strncpy(font->faceName, faceName, sizeof(font->faceName) - 1);
|
|
font->faceName[sizeof(font->faceName) - 1] = '\0';
|
|
|
|
dbg("windrv: wdrvLoadFontTtf: '%s' %dpt -> %dx%d (max=%d avg=%d) sel=%04X size=%" PRIu32 "\n",
|
|
faceName, (int)pointSize,
|
|
allSameWidth ? (int)advanceWidths[0] : 0, (int)pixHeight,
|
|
(int)maxAdvance, (int)(totalAdvance / nChars),
|
|
sel, totalSize);
|
|
|
|
return font;
|
|
}
|
|
|
|
|
|
void wdrvUnloadFont(WdrvFontT font)
|
|
{
|
|
if (!font || font == &gBuiltinFont) {
|
|
return;
|
|
}
|
|
free16BitBlock(font->fontSel, font->fontLinear);
|
|
free(font);
|
|
}
|
|
|
|
|
|
static uint16_t alloc16BitBlock(uint32_t size, uint32_t *linearOut)
|
|
{
|
|
uint8_t *mem = (uint8_t *)calloc(1, size);
|
|
if (!mem) {
|
|
return 0;
|
|
}
|
|
|
|
uint32_t ptrVal = (uint32_t)mem;
|
|
|
|
int sel = __dpmi_allocate_ldt_descriptors(1);
|
|
if (sel < 0) {
|
|
free(mem);
|
|
return 0;
|
|
}
|
|
|
|
// True linear address = DJGPP pointer + DS base
|
|
__dpmi_set_segment_base_address(sel, ptrVal + __djgpp_base_address);
|
|
__dpmi_set_segment_limit(sel, size - 1);
|
|
__dpmi_set_descriptor_access_rights(sel, 0x00F2); // 16-bit data RW
|
|
|
|
*linearOut = ptrVal;
|
|
return (uint16_t)sel;
|
|
}
|
|
|
|
|
|
static void free16BitBlock(uint16_t sel, uint32_t linear)
|
|
{
|
|
if (sel) {
|
|
__dpmi_free_ldt_descriptor(sel);
|
|
}
|
|
if (linear) {
|
|
free((void *)linear);
|
|
}
|
|
}
|
|
|
|
|
|
static void setError(int32_t err)
|
|
{
|
|
gLastError = err;
|
|
}
|
|
|
|
|
|
static void waitForEngine(void)
|
|
{
|
|
if (!gIsS3) {
|
|
return;
|
|
}
|
|
|
|
// Wait for the S3 graphics engine to become idle by polling GP_STAT.
|
|
// Bit 9 (0x0200) = hardware busy.
|
|
for (int i = 0; i < 100000; i++) {
|
|
uint16_t stat = inportw(0x9AE8);
|
|
if (!(stat & 0x0200)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Declared in file-scope asm above
|
|
extern void int10hRawHandler(void);
|
|
|
|
static bool installInt10hReflector(void)
|
|
{
|
|
// Save DJGPP's DS selector for the assembly stub.
|
|
// The stub uses CS-relative addressing to load this value since
|
|
// DS is undefined on PM interrupt handler entry.
|
|
gInt10hDsSel = _my_ds();
|
|
gInt10hStackTop = (uint32_t)gInt10hStack + sizeof(gInt10hStack);
|
|
|
|
__dpmi_get_protected_mode_interrupt_vector(0x10, &gOldInt10hVec);
|
|
|
|
__dpmi_paddr newVec;
|
|
newVec.offset32 = (unsigned long)int10hRawHandler;
|
|
newVec.selector = _my_cs();
|
|
|
|
if (__dpmi_set_protected_mode_interrupt_vector(0x10, &newVec) != 0) {
|
|
return false;
|
|
}
|
|
|
|
gInt10hInstalled = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
static void removeInt10hReflector(void)
|
|
{
|
|
if (gInt10hInstalled) {
|
|
__dpmi_set_protected_mode_interrupt_vector(0x10, &gOldInt10hVec);
|
|
gInt10hInstalled = false;
|
|
}
|
|
}
|
|
|
|
|
|
static bool installDpmi300Proxy(void)
|
|
{
|
|
gDpmi300DsSel = _my_ds();
|
|
gDpmi300StackTop = (uint32_t)gDpmi300Stack + sizeof(gDpmi300Stack);
|
|
|
|
__dpmi_get_protected_mode_interrupt_vector(DPMI300_INT_NUM, &gOldDpmi300Vec);
|
|
|
|
__dpmi_paddr newVec;
|
|
newVec.offset32 = (unsigned long)dpmi300RawHandler;
|
|
newVec.selector = _my_cs();
|
|
|
|
if (__dpmi_set_protected_mode_interrupt_vector(DPMI300_INT_NUM, &newVec) != 0) {
|
|
return false;
|
|
}
|
|
|
|
gDpmi300Installed = true;
|
|
dbg("windrv: DPMI 300h proxy installed on INT %02Xh\n", DPMI300_INT_NUM);
|
|
return true;
|
|
}
|
|
|
|
|
|
// Search a loaded driver's code segments for the DoInt10h INT 31h instruction
|
|
// and patch it to use our proxy interrupt instead. DoInt10h builds a RMCS on
|
|
// the stack and then does:
|
|
// mov ax, 0300h ; B8 00 03
|
|
// ...
|
|
// int 31h ; CD 31
|
|
// We find "CD 31" within a small window after "B8 00 03" and change the 0x31
|
|
// to DPMI300_INT_NUM (0x64).
|
|
static bool patchDoInt10h(struct WdrvDriverS *drv)
|
|
{
|
|
bool patched = false;
|
|
|
|
for (int s = 0; s < drv->neMod.segmentCount; s++) {
|
|
if (!drv->neMod.segments[s].isCode) {
|
|
continue;
|
|
}
|
|
|
|
uint16_t sel = drv->neMod.segments[s].selector;
|
|
uint32_t lin = drv->neMod.segments[s].linearAddr;
|
|
uint32_t size = drv->neMod.segments[s].size;
|
|
|
|
// Scan for "B8 00 03" (mov ax, 0300h)
|
|
for (uint32_t i = 0; i + 2 < size; i++) {
|
|
uint8_t b0 = *(uint8_t *)(lin + i);
|
|
uint8_t b1 = *(uint8_t *)(lin + i + 1);
|
|
uint8_t b2 = *(uint8_t *)(lin + i + 2);
|
|
|
|
if (b0 != 0xB8 || b1 != 0x00 || b2 != 0x03) {
|
|
continue;
|
|
}
|
|
|
|
// Found "mov ax, 0300h" at offset i. Search ahead for "CD 31".
|
|
uint32_t searchEnd = i + 24;
|
|
if (searchEnd > size - 1) {
|
|
searchEnd = size - 1;
|
|
}
|
|
|
|
for (uint32_t j = i + 3; j + 1 <= searchEnd; j++) {
|
|
uint8_t c0 = *(uint8_t *)(lin + j);
|
|
uint8_t c1 = *(uint8_t *)(lin + j + 1);
|
|
|
|
if (c0 == 0xCD && c1 == 0x31) {
|
|
// Create a data alias for the code segment so we can write
|
|
uint16_t dataSel = __dpmi_create_alias_descriptor(sel);
|
|
if (dataSel == 0) {
|
|
logErr("windrv: patchDoInt10h: cannot create alias for seg %d\n", s);
|
|
break;
|
|
}
|
|
|
|
// Patch 0x31 -> DPMI300_INT_NUM
|
|
_farpokeb(dataSel, j + 1, DPMI300_INT_NUM);
|
|
|
|
// Verify
|
|
uint8_t verify = _farpeekb(sel, j + 1);
|
|
dbg("windrv: patched INT 31h -> INT %02Xh at seg%d:%04" PRIX32
|
|
" (verify: %02X)\n", DPMI300_INT_NUM, s + 1, j, verify);
|
|
|
|
__dpmi_free_ldt_descriptor(dataSel);
|
|
patched = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!patched) {
|
|
dbg("windrv: patchDoInt10h: no INT 31h found after MOV AX,0300h\n");
|
|
}
|
|
return patched;
|
|
}
|
|
|
|
|
|
// Patch hardcoded "mov ax, 0040h; mov es, ax" in driver code segments.
|
|
//
|
|
// physical_enable in VGA.ASM loads ES with the literal value 0x0040 to
|
|
// access the BIOS data area. In real Windows 3.1, selector 0x0040 either
|
|
// maps to 0040:0000 or is trapped by the VDD. Under CWSDPMI, 0x0040 is
|
|
// an invalid ring-0 GDT selector that causes a GPF.
|
|
//
|
|
// We scan for the byte pattern B8 40 00 8E C0 (mov ax,0040h; mov es,ax)
|
|
// and patch the immediate to our biosDataSel from the stub context.
|
|
static bool patchBiosDataAccess(struct WdrvDriverS *drv)
|
|
{
|
|
uint16_t biosSel = gStubCtx.biosDataSel;
|
|
if (biosSel == 0) {
|
|
logErr("windrv: patchBiosDataAccess: no biosDataSel\n");
|
|
return false;
|
|
}
|
|
|
|
bool patched = false;
|
|
|
|
for (int s = 0; s < drv->neMod.segmentCount; s++) {
|
|
if (!drv->neMod.segments[s].isCode) {
|
|
continue;
|
|
}
|
|
|
|
uint16_t sel = drv->neMod.segments[s].selector;
|
|
uint32_t lin = drv->neMod.segments[s].linearAddr;
|
|
uint32_t size = drv->neMod.segments[s].size;
|
|
|
|
for (uint32_t i = 0; i + 4 < size; i++) {
|
|
uint8_t *p = (uint8_t *)(lin + i);
|
|
// B8 40 00 8E C0 = mov ax, 0040h; mov es, ax
|
|
if (p[0] == 0xB8 && p[1] == 0x40 && p[2] == 0x00 &&
|
|
p[3] == 0x8E && p[4] == 0xC0) {
|
|
uint16_t dataSel = __dpmi_create_alias_descriptor(sel);
|
|
if (dataSel == 0) {
|
|
logErr("windrv: patchBiosDataAccess: cannot create alias for seg %d\n", s);
|
|
break;
|
|
}
|
|
|
|
_farpokeb(dataSel, i + 1, (uint8_t)(biosSel & 0xFF));
|
|
_farpokeb(dataSel, i + 2, (uint8_t)(biosSel >> 8));
|
|
|
|
uint8_t v0 = _farpeekb(sel, i + 1);
|
|
uint8_t v1 = _farpeekb(sel, i + 2);
|
|
logErr("windrv: patched mov ax,0040h -> mov ax,%04Xh at seg%d:%04" PRIX32
|
|
" (verify: %02X %02X)\n", biosSel, s + 1, i, v0, v1);
|
|
|
|
__dpmi_free_ldt_descriptor(dataSel);
|
|
patched = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!patched) {
|
|
dbg("windrv: patchBiosDataAccess: pattern not found (OK for some drivers)\n");
|
|
}
|
|
return patched;
|
|
}
|
|
|
|
|
|
// Repatch __WINFLAGS in all driver segments.
|
|
//
|
|
// The NE loader patches __WINFLAGS (KERNEL.178) into the driver's code/data
|
|
// segments at relocation time. After Enable(style=1) reveals the driver type,
|
|
// we may need to change WF_ENHANCED to WF_STANDARD for VGA-class drivers
|
|
// whose Enable(style=0) hangs waiting for a VDD that doesn't exist.
|
|
//
|
|
// We scan all segments for the 16-bit word pattern and replace it.
|
|
static void patchWinFlags(struct WdrvDriverS *drv, uint16_t oldFlags, uint16_t newFlags)
|
|
{
|
|
if (oldFlags == newFlags) {
|
|
return;
|
|
}
|
|
|
|
uint8_t oldLo = (uint8_t)(oldFlags & 0xFF);
|
|
uint8_t oldHi = (uint8_t)(oldFlags >> 8);
|
|
uint8_t newLo = (uint8_t)(newFlags & 0xFF);
|
|
uint8_t newHi = (uint8_t)(newFlags >> 8);
|
|
int count = 0;
|
|
|
|
for (int s = 0; s < drv->neMod.segmentCount; s++) {
|
|
uint16_t sel = drv->neMod.segments[s].selector;
|
|
uint32_t lin = drv->neMod.segments[s].linearAddr;
|
|
uint32_t size = drv->neMod.segments[s].size;
|
|
bool isCode = drv->neMod.segments[s].isCode;
|
|
|
|
if (size < 2) {
|
|
continue;
|
|
}
|
|
|
|
// Need a writable alias for code segments
|
|
uint16_t dataSel = 0;
|
|
if (isCode) {
|
|
dataSel = __dpmi_create_alias_descriptor(sel);
|
|
if (dataSel == 0) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
for (uint32_t i = 0; i + 1 < size; i++) {
|
|
uint8_t *p = (uint8_t *)(lin + i);
|
|
if (p[0] == oldLo && p[1] == oldHi) {
|
|
if (isCode) {
|
|
_farpokeb(dataSel, i, newLo);
|
|
_farpokeb(dataSel, i + 1, newHi);
|
|
} else {
|
|
p[0] = newLo;
|
|
p[1] = newHi;
|
|
}
|
|
count++;
|
|
}
|
|
}
|
|
|
|
if (dataSel != 0) {
|
|
__dpmi_free_ldt_descriptor(dataSel);
|
|
}
|
|
}
|
|
|
|
if (count > 0) {
|
|
dbg("windrv: patched %d __WINFLAGS locations: 0x%04X -> 0x%04X\n",
|
|
count, oldFlags, newFlags);
|
|
}
|
|
}
|
|
|
|
|
|
static void removeDpmi300Proxy(void)
|
|
{
|
|
if (gDpmi300Installed) {
|
|
__dpmi_set_protected_mode_interrupt_vector(DPMI300_INT_NUM, &gOldDpmi300Vec);
|
|
gDpmi300Installed = false;
|
|
}
|
|
}
|
|
|
|
|
|
// Declared in file-scope asm above
|
|
extern void exc0dRawHandler(void);
|
|
extern void exc0eRawHandler(void);
|
|
|
|
static bool installExceptionCapture(void)
|
|
{
|
|
// Initialize fault handler stack
|
|
gFaultStackTop = (uint32_t)gFaultStack + sizeof(gFaultStack);
|
|
|
|
// Get old exception handlers
|
|
__dpmi_get_processor_exception_handler_vector(0x0D, &gOldExc0D);
|
|
__dpmi_get_processor_exception_handler_vector(0x0E, &gOldExc0E);
|
|
|
|
// Copy to packed far pointers for asm indirect far jumps
|
|
gOldExc0dFar.offset = (uint32_t)gOldExc0D.offset32;
|
|
gOldExc0dFar.selector = (uint16_t)gOldExc0D.selector;
|
|
gOldExc0eFar.offset = (uint32_t)gOldExc0E.offset32;
|
|
gOldExc0eFar.selector = (uint16_t)gOldExc0E.selector;
|
|
|
|
// Install our handlers
|
|
__dpmi_paddr newVec;
|
|
newVec.selector = _my_cs();
|
|
|
|
newVec.offset32 = (unsigned long)exc0dRawHandler;
|
|
if (__dpmi_set_processor_exception_handler_vector(0x0D, &newVec) != 0) {
|
|
return false;
|
|
}
|
|
|
|
newVec.offset32 = (unsigned long)exc0eRawHandler;
|
|
if (__dpmi_set_processor_exception_handler_vector(0x0E, &newVec) != 0) {
|
|
__dpmi_set_processor_exception_handler_vector(0x0D, &gOldExc0D);
|
|
return false;
|
|
}
|
|
|
|
gExcCaptureInstalled = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
static void removeExceptionCapture(void)
|
|
{
|
|
if (gExcCaptureInstalled) {
|
|
__dpmi_set_processor_exception_handler_vector(0x0D, &gOldExc0D);
|
|
__dpmi_set_processor_exception_handler_vector(0x0E, &gOldExc0E);
|
|
gExcCaptureInstalled = false;
|
|
}
|
|
}
|
|
|
|
|
|
static bool installInt2FhHandler(void)
|
|
{
|
|
__dpmi_paddr oldVec;
|
|
__dpmi_get_protected_mode_interrupt_vector(0x2F, &oldVec);
|
|
gOldInt2FhVec = oldVec;
|
|
gOldInt2FhFar.offset = oldVec.offset32;
|
|
gOldInt2FhFar.selector = oldVec.selector;
|
|
|
|
__dpmi_paddr newVec;
|
|
newVec.offset32 = (unsigned long)int2FhRawHandler;
|
|
newVec.selector = _my_cs();
|
|
if (__dpmi_set_protected_mode_interrupt_vector(0x2F, &newVec) != 0) {
|
|
return false;
|
|
}
|
|
|
|
gInt2FhInstalled = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
static void removeInt2FhHandler(void)
|
|
{
|
|
if (gInt2FhInstalled) {
|
|
__dpmi_set_protected_mode_interrupt_vector(0x2F, &gOldInt2FhVec);
|
|
gInt2FhInstalled = false;
|
|
}
|
|
}
|
|
|
|
|
|
static void dbg(const char *fmt, ...)
|
|
{
|
|
if (!gDebug) {
|
|
return;
|
|
}
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
logErrV(fmt, ap);
|
|
va_end(ap);
|
|
}
|
|
|
|
|
|
// Patch Windows PROLOG_0 sequences in all code segments.
|
|
//
|
|
// The Windows 3.x module loader converts the 3-byte function prolog
|
|
// 8C D8 90 (mov ax, ds ; nop)
|
|
// to
|
|
// B8 xx xx (mov ax, <DGROUP selector>)
|
|
//
|
|
// This ensures AX holds the correct DGROUP selector when the function
|
|
// body executes "push ds ; mov ds, ax" for FAR entry.
|
|
//
|
|
// However, NEAR calls enter at offset+3 (skipping the mov ax), so AX
|
|
// may be clobbered. Since DS is always DGROUP at both entry paths
|
|
// (the relay sets it for far calls, the caller preserves it for near
|
|
// calls), the "mov ds, ax" is redundant. We NOP it out so the
|
|
// function simply does "push ds" (saving DGROUP for the epilog) and
|
|
// continues with DS already correct.
|
|
//
|
|
// Full original 10-byte prolog:
|
|
// 8C D8 90 mov ax, ds ; nop offset+0 (far entry)
|
|
// 45 inc bp offset+3 (near entry)
|
|
// 55 push bp offset+4
|
|
// 8B EC mov bp, sp offset+5
|
|
// 1E push ds offset+7
|
|
// 8E D8 mov ds, ax offset+8
|
|
//
|
|
// Patched:
|
|
// B8 xx xx mov ax, DGROUP offset+0 (for far entry AX)
|
|
// 45 inc bp offset+3
|
|
// 55 push bp offset+4
|
|
// 8B EC mov bp, sp offset+5
|
|
// 1E push ds offset+7
|
|
// 90 90 nop ; nop offset+8 (DS already correct)
|
|
// Patch Win16 PROLOG_0/PROLOG_1 function prologs and their matching epilogs.
|
|
//
|
|
// Win16 PROLOG_0 functions use `inc bp` to mark far frames for stack walking
|
|
// and `dec bp` in the epilog to undo it. The Windows kernel needs these odd
|
|
// BP markers for stack traversal and memory management, but our DOS environment
|
|
// has no such requirement. Leaving them in causes frame pointer corruption
|
|
// when the odd BP propagates through the call chain.
|
|
//
|
|
// Prolog pattern (two variants):
|
|
// 8C D8 90 45 55 8B EC [1E 8E D8] mov ax,ds; nop; inc bp; push bp; mov bp,sp; [push ds; mov ds,ax]
|
|
// B8 XX XX 45 55 8B EC [1E 8E D8] mov ax,IMMED; inc bp; push bp; mov bp,sp; [push ds; mov ds,ax]
|
|
//
|
|
// Epilog pattern:
|
|
// 5D 4D CB pop bp; dec bp; retf
|
|
// 5D 4D C3 pop bp; dec bp; ret
|
|
//
|
|
// Patches applied:
|
|
// - 8C D8 90 → B8 DGROUP_LO DGROUP_HI (load correct DGROUP selector)
|
|
// - 45 → 90 (NOP out inc bp)
|
|
// - 8E D8 → 90 90 (NOP out mov ds,ax — DS already set by thunk)
|
|
// - 4D → 90 in epilog (NOP out dec bp)
|
|
static void patchPrologs(NeModuleT *mod)
|
|
{
|
|
uint16_t dgroupSel = mod->autoDataSel;
|
|
int prologCount = 0;
|
|
int epilogCount = 0;
|
|
|
|
for (int s = 0; s < mod->segmentCount; s++) {
|
|
if (!mod->segments[s].isCode) {
|
|
continue;
|
|
}
|
|
|
|
uint8_t *base = (uint8_t *)mod->segments[s].linearAddr;
|
|
uint32_t size = mod->segments[s].size;
|
|
|
|
// Pass 1: Patch prologs — find "45 55 8B EC" (inc bp; push bp; mov bp,sp)
|
|
for (uint32_t i = 0; i + 3 < size; i++) {
|
|
if (base[i] != 0x45 ||
|
|
base[i + 1] != 0x55 ||
|
|
base[i + 2] != 0x8B ||
|
|
base[i + 3] != 0xEC) {
|
|
continue;
|
|
}
|
|
|
|
// NOP out inc bp
|
|
base[i] = 0x90;
|
|
prologCount++;
|
|
|
|
// If preceded by "8C D8 90" (mov ax,ds; nop), patch to mov ax,DGROUP
|
|
if (i >= 3 &&
|
|
base[i - 3] == 0x8C &&
|
|
base[i - 2] == 0xD8 &&
|
|
base[i - 1] == 0x90) {
|
|
base[i - 3] = 0xB8;
|
|
base[i - 2] = (uint8_t)(dgroupSel & 0xFF);
|
|
base[i - 1] = (uint8_t)(dgroupSel >> 8);
|
|
}
|
|
|
|
// "1E 8E D8" (push ds; mov ds,ax) must be kept intact!
|
|
// The driver expects DS = DGROUP for all DS-relative data access.
|
|
// Do NOT NOP these out.
|
|
}
|
|
|
|
// Pass 2: Patch epilogs — find "5D 4D" followed by any return:
|
|
// CB = retf, C3 = ret, CA xx xx = retf N, C2 xx xx = ret N
|
|
// Pascal calling convention uses retf N (CA) to clean parameters,
|
|
// so most epilogs are "5D 4D CA xx xx", not "5D 4D CB".
|
|
for (uint32_t i = 0; i + 2 < size; i++) {
|
|
if (base[i] == 0x5D &&
|
|
base[i + 1] == 0x4D &&
|
|
(base[i + 2] == 0xCB || base[i + 2] == 0xC3 ||
|
|
base[i + 2] == 0xCA || base[i + 2] == 0xC2)) {
|
|
base[i + 1] = 0x90;
|
|
epilogCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
dbg("windrv: patched %d prologs, %d epilogs (DGROUP=0x%04X)\n",
|
|
prologCount, epilogCount, dgroupSel);
|
|
}
|
|
|
|
|
|
// Patch VFLATD initialization code to avoid a 20-byte stack imbalance.
|
|
//
|
|
// The VFLATD init code at seg5:0x2368 is a subroutine (no prolog, near ret
|
|
// at 0x252B) called from the mode setup function. It allocates DOS memory
|
|
// via GlobalDOSAlloc/GlobalAlloc/GlobalLock/GetCurrentPDB, pushing 20 bytes
|
|
// of intermediate values onto the stack. All exit paths converge at 0x2519
|
|
// (GlobalFree) -> 0x2522 (SetSwapAreaSize) -> 0x252B (ret) WITHOUT cleaning
|
|
// these 20 bytes.
|
|
//
|
|
// In real Windows 3.x the caller at 0x3613 restores SP from BP, so the
|
|
// imbalance is harmless. But our thunk returns via a clean `ret`, which
|
|
// pops 0x2362 (junk) instead of the real return address 0x3613, landing
|
|
// in the middle of a `lea sp,[bp-2]` instruction -> SIGILL.
|
|
//
|
|
// There are TWO entry points to this init code:
|
|
// 0x22C5: wrapper function that checks [0EE9] and proceeds with init
|
|
// 0x2368: direct entry from mode setup (after VBE mode set)
|
|
//
|
|
// Fix: patch BOTH to C3 (near ret) so neither path executes VFLATD init.
|
|
// With LFB mode forced via DPMI (patchVflatdBypassCall), VFLATD setup
|
|
// is unnecessary.
|
|
static void patchVflatdStackBug(NeModuleT *mod)
|
|
{
|
|
int segIdx = -1;
|
|
for (int s = 0; s < mod->segmentCount; s++) {
|
|
if (mod->segments[s].isCode && mod->segments[s].size > 0x2369) {
|
|
uint8_t *base = (uint8_t *)mod->segments[s].linearAddr;
|
|
// Verify the call at 0x02A4 targets 0x22C5: E8 1E 20
|
|
if (base[0x02A4] == 0xE8 && base[0x02A5] == 0x1E &&
|
|
base[0x02A6] == 0x20) {
|
|
segIdx = s;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (segIdx < 0) {
|
|
dbg("windrv: VFLATD init patch: pattern not found, skipping\n");
|
|
return;
|
|
}
|
|
|
|
uint8_t *base = (uint8_t *)mod->segments[segIdx].linearAddr;
|
|
|
|
// Patch wrapper function at 0x22C5 to immediate return
|
|
base[0x22C5] = 0xC3;
|
|
dbg("windrv: patched VFLATD init wrapper at seg %d offset 0x22C5 (ret)\n", segIdx);
|
|
|
|
// Patch direct entry at 0x2368 (C6 06 A4 49 00 = mov byte [49A4],0)
|
|
if (base[0x2368] == 0xC6 && base[0x2369] == 0x06 &&
|
|
base[0x236A] == 0xA4 && base[0x236B] == 0x49) {
|
|
base[0x2368] = 0xC3;
|
|
dbg("windrv: patched VFLATD init direct entry at seg %d offset 0x2368 (ret)\n", segIdx);
|
|
} else {
|
|
dbg("windrv: VFLATD init direct entry at 0x2368: unexpected bytes, skipping\n");
|
|
}
|
|
}
|
|
|
|
|
|
// Bypass the VFLATD API call at seg5:0x3FD4.
|
|
//
|
|
// The driver checks [DS:8889] to decide between two framebuffer paths:
|
|
// [8889] == 0xFF: DPMI path (allocate descriptor, map physical via INT 31h)
|
|
// [8889] != 0xFF: VFLATD path (call far through [DS:0D76])
|
|
//
|
|
// Since VFLATD is not available, the far pointer at [0D76] is null, causing
|
|
// a GPF. Force the DPMI path by patching the conditional jump to unconditional.
|
|
//
|
|
// Original at 0x3FA9: 80 3E 89 88 FF 74 32 (cmp byte [8889],0xFF; jz +0x32)
|
|
// Patched: EB 37 90 90 90 90 90 (jmp +0x37; nop*5)
|
|
//
|
|
// Both reach 0x3FE2 which uses DPMI INT 31h functions 0800h/0007h/0008h
|
|
// to map the physical framebuffer — fully supported by CWSDPMI.
|
|
static void patchVflatdBypassCall(NeModuleT *mod)
|
|
{
|
|
int segIdx = -1;
|
|
for (int s = 0; s < mod->segmentCount; s++) {
|
|
if (mod->segments[s].isCode && mod->segments[s].size > 0x3FB0) {
|
|
uint8_t *base = (uint8_t *)mod->segments[s].linearAddr;
|
|
if (base[0x3FA9] == 0x80 && base[0x3FAA] == 0x3E &&
|
|
base[0x3FAB] == 0x89 && base[0x3FAC] == 0x88 &&
|
|
base[0x3FAD] == 0xFF && base[0x3FAE] == 0x74 &&
|
|
base[0x3FAF] == 0x32) {
|
|
segIdx = s;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (segIdx < 0) {
|
|
dbg("windrv: VFLATD bypass patch: pattern not found, skipping\n");
|
|
return;
|
|
}
|
|
|
|
uint8_t *base = (uint8_t *)mod->segments[segIdx].linearAddr;
|
|
|
|
// 0x3FA9: EB 37 jmp 0x3FE2 (unconditional -> DPMI path)
|
|
// 0x3FAB: 90*5 nop padding
|
|
base[0x3FA9] = 0xEB;
|
|
base[0x3FAA] = 0x37;
|
|
base[0x3FAB] = 0x90;
|
|
base[0x3FAC] = 0x90;
|
|
base[0x3FAD] = 0x90;
|
|
base[0x3FAE] = 0x90;
|
|
base[0x3FAF] = 0x90;
|
|
|
|
dbg("windrv: patched VFLATD bypass at seg %d offset 0x3FA9\n", segIdx);
|
|
|
|
// NOP all "call far [DS:0D76]" (FF 1E 76 0D) in the code segment.
|
|
// These call through the VFLATD entry point which is null since VFLATD
|
|
// isn't present. With LFB mode via DPMI, bank switching is unnecessary.
|
|
uint32_t segSize = mod->segments[segIdx].size;
|
|
int nopCount = 0;
|
|
for (uint32_t i = 0; i + 3 < segSize; i++) {
|
|
if (base[i] == 0xFF && base[i + 1] == 0x1E &&
|
|
base[i + 2] == 0x76 && base[i + 3] == 0x0D) {
|
|
base[i] = 0x90;
|
|
base[i + 1] = 0x90;
|
|
base[i + 2] = 0x90;
|
|
base[i + 3] = 0x90;
|
|
dbg("windrv: NOPed VFLATD call at seg %d offset 0x%04" PRIX32 "\n", segIdx, i);
|
|
nopCount++;
|
|
}
|
|
}
|
|
dbg("windrv: NOPed %d VFLATD call(s) total\n", nopCount);
|
|
}
|