Platform specific code isolated to make porting easier.

This commit is contained in:
Scott Duensing 2026-03-16 20:35:16 -05:00
parent d0e308d7cf
commit 5c00bc8ae2
8 changed files with 1012 additions and 796 deletions

View file

@ -9,10 +9,13 @@ CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586
OBJDIR = ../obj/dvx OBJDIR = ../obj/dvx
WOBJDIR = ../obj/dvx/widgets WOBJDIR = ../obj/dvx/widgets
POBJDIR = ../obj/dvx/platform
LIBDIR = ../lib LIBDIR = ../lib
SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxImageWrite.c dvxApp.c dvxDialog.c SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxImageWrite.c dvxApp.c dvxDialog.c
PSRCS = platform/dvxPlatformDos.c
WSRCS = widgets/widgetAnsiTerm.c \ WSRCS = widgets/widgetAnsiTerm.c \
widgets/widgetClass.c \ widgets/widgetClass.c \
widgets/widgetCore.c \ widgets/widgetCore.c \
@ -46,6 +49,7 @@ WSRCS = widgets/widgetAnsiTerm.c \
widgets/widgetTreeView.c widgets/widgetTreeView.c
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
POBJS = $(patsubst platform/%.c,$(POBJDIR)/%.o,$(PSRCS))
WOBJS = $(patsubst widgets/%.c,$(WOBJDIR)/%.o,$(WSRCS)) WOBJS = $(patsubst widgets/%.c,$(WOBJDIR)/%.o,$(WSRCS))
TARGET = $(LIBDIR)/libdvx.a TARGET = $(LIBDIR)/libdvx.a
@ -53,19 +57,25 @@ TARGET = $(LIBDIR)/libdvx.a
all: $(TARGET) all: $(TARGET)
$(TARGET): $(OBJS) $(WOBJS) | $(LIBDIR) $(TARGET): $(OBJS) $(POBJS) $(WOBJS) | $(LIBDIR)
$(AR) rcs $@ $(OBJS) $(WOBJS) $(AR) rcs $@ $(OBJS) $(POBJS) $(WOBJS)
$(RANLIB) $@ $(RANLIB) $@
$(OBJDIR)/%.o: %.c | $(OBJDIR) $(OBJDIR)/%.o: %.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $< $(CC) $(CFLAGS) -c -o $@ $<
$(POBJDIR)/%.o: platform/%.c | $(POBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(WOBJDIR)/%.o: widgets/%.c | $(WOBJDIR) $(WOBJDIR)/%.o: widgets/%.c | $(WOBJDIR)
$(CC) $(CFLAGS) -c -o $@ $< $(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR): $(OBJDIR):
mkdir -p $(OBJDIR) mkdir -p $(OBJDIR)
$(POBJDIR):
mkdir -p $(POBJDIR)
$(WOBJDIR): $(WOBJDIR):
mkdir -p $(WOBJDIR) mkdir -p $(WOBJDIR)
@ -73,14 +83,17 @@ $(LIBDIR):
mkdir -p $(LIBDIR) mkdir -p $(LIBDIR)
# Dependencies # Dependencies
$(OBJDIR)/dvxVideo.o: dvxVideo.c dvxVideo.h dvxTypes.h dvxPalette.h $(OBJDIR)/dvxVideo.o: dvxVideo.c dvxVideo.h platform/dvxPlatform.h dvxTypes.h dvxPalette.h
$(OBJDIR)/dvxDraw.o: dvxDraw.c dvxDraw.h dvxTypes.h $(OBJDIR)/dvxDraw.o: dvxDraw.c dvxDraw.h platform/dvxPlatform.h dvxTypes.h
$(OBJDIR)/dvxComp.o: dvxComp.c dvxComp.h dvxTypes.h $(OBJDIR)/dvxComp.o: dvxComp.c dvxComp.h platform/dvxPlatform.h dvxTypes.h
$(OBJDIR)/dvxWm.o: dvxWm.c dvxWm.h dvxTypes.h dvxDraw.h dvxComp.h dvxVideo.h thirdparty/stb_image.h $(OBJDIR)/dvxWm.o: dvxWm.c dvxWm.h dvxTypes.h dvxDraw.h dvxComp.h dvxVideo.h thirdparty/stb_image.h
$(OBJDIR)/dvxIcon.o: dvxIcon.c thirdparty/stb_image.h $(OBJDIR)/dvxIcon.o: dvxIcon.c thirdparty/stb_image.h
$(OBJDIR)/dvxImageWrite.o: dvxImageWrite.c thirdparty/stb_image_write.h $(OBJDIR)/dvxImageWrite.o: dvxImageWrite.c thirdparty/stb_image_write.h
$(OBJDIR)/dvxApp.o: dvxApp.c dvxApp.h dvxTypes.h dvxVideo.h dvxDraw.h dvxComp.h dvxWm.h dvxFont.h dvxCursor.h $(OBJDIR)/dvxApp.o: dvxApp.c dvxApp.h platform/dvxPlatform.h dvxTypes.h dvxVideo.h dvxDraw.h dvxComp.h dvxWm.h dvxFont.h dvxCursor.h
$(OBJDIR)/dvxDialog.o: dvxDialog.c dvxDialog.h dvxApp.h dvxWidget.h widgets/widgetInternal.h dvxTypes.h dvxDraw.h $(OBJDIR)/dvxDialog.o: dvxDialog.c dvxDialog.h platform/dvxPlatform.h dvxApp.h dvxWidget.h widgets/widgetInternal.h dvxTypes.h dvxDraw.h
# Platform file dependencies
$(POBJDIR)/dvxPlatformDos.o: platform/dvxPlatformDos.c platform/dvxPlatform.h dvxTypes.h dvxPalette.h
# Widget file dependencies # Widget file dependencies
WIDGET_DEPS = widgets/widgetInternal.h dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h WIDGET_DEPS = widgets/widgetInternal.h dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h

View file

@ -6,11 +6,11 @@
#include "dvxFont.h" #include "dvxFont.h"
#include "dvxCursor.h" #include "dvxCursor.h"
#include "platform/dvxPlatform.h"
#include <string.h> #include <string.h>
#include <ctype.h> #include <ctype.h>
#include <time.h> #include <time.h>
#include <dpmi.h>
#include <signal.h>
#include "thirdparty/stb_image_write.h" #include "thirdparty/stb_image_write.h"
@ -43,7 +43,6 @@ static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd);
static WindowT *findWindowById(AppContextT *ctx, int32_t id); static WindowT *findWindowById(AppContextT *ctx, int32_t id);
static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons); static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons);
static void initColorScheme(AppContextT *ctx); static void initColorScheme(AppContextT *ctx);
static void initMouse(AppContextT *ctx);
static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY); static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY);
static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx); static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx);
static void openSubMenu(AppContextT *ctx); static void openSubMenu(AppContextT *ctx);
@ -60,22 +59,6 @@ static void updateTooltip(AppContextT *ctx);
// Button pressed via keyboard — shared with widgetEvent.c for Space/Enter // Button pressed via keyboard — shared with widgetEvent.c for Space/Enter
WidgetT *sKeyPressedBtn = NULL; WidgetT *sKeyPressedBtn = NULL;
// Alt+key scan code to ASCII lookup table (indexed by scan code)
// BIOS INT 16h returns these scan codes with ascii=0 for Alt+key combos
static const char sAltScanToAscii[256] = {
// Alt+letters
[0x10] = 'q', [0x11] = 'w', [0x12] = 'e', [0x13] = 'r',
[0x14] = 't', [0x15] = 'y', [0x16] = 'u', [0x17] = 'i',
[0x18] = 'o', [0x19] = 'p', [0x1E] = 'a', [0x1F] = 's',
[0x20] = 'd', [0x21] = 'f', [0x22] = 'g', [0x23] = 'h',
[0x24] = 'j', [0x25] = 'k', [0x26] = 'l', [0x2C] = 'z',
[0x2D] = 'x', [0x2E] = 'c', [0x2F] = 'v', [0x30] = 'b',
[0x31] = 'n', [0x32] = 'm',
// Alt+digits
[0x78] = '1', [0x79] = '2', [0x7A] = '3', [0x7B] = '4',
[0x7C] = '5', [0x7D] = '6', [0x7E] = '7', [0x7F] = '8',
[0x80] = '9', [0x81] = '0',
};
// ============================================================ // ============================================================
@ -1260,8 +1243,8 @@ const BitmapFontT *dvxGetFont(const AppContextT *ctx) {
int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) { int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
memset(ctx, 0, sizeof(*ctx)); memset(ctx, 0, sizeof(*ctx));
// Disable Ctrl+C/Break termination // Platform-specific initialization (signal handling, etc.)
signal(SIGINT, SIG_IGN); platformInit();
// Initialize video // Initialize video
if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) { if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) {
@ -1292,7 +1275,11 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_
ctx->cursorBg = packColor(&ctx->display, 0, 0, 0); ctx->cursorBg = packColor(&ctx->display, 0, 0, 0);
// Initialize mouse // Initialize mouse
initMouse(ctx); platformMouseInit(ctx->display.width, ctx->display.height);
ctx->mouseX = ctx->display.width / 2;
ctx->mouseY = ctx->display.height / 2;
ctx->prevMouseX = ctx->mouseX;
ctx->prevMouseY = ctx->mouseY;
ctx->running = true; ctx->running = true;
ctx->lastIconClickId = -1; ctx->lastIconClickId = -1;
@ -1395,7 +1382,7 @@ bool dvxUpdate(AppContextT *ctx) {
} else if (ctx->idleCallback) { } else if (ctx->idleCallback) {
ctx->idleCallback(ctx->idleCtx); ctx->idleCallback(ctx->idleCtx);
} else { } else {
__dpmi_yield(); platformYield();
} }
// After compositing, release key-pressed button (one frame of animation) // After compositing, release key-pressed button (one frame of animation)
@ -1919,45 +1906,6 @@ static void initColorScheme(AppContextT *ctx) {
} }
// ============================================================
// initMouse
// ============================================================
static void initMouse(AppContextT *ctx) {
__dpmi_regs r;
// Reset mouse driver
memset(&r, 0, sizeof(r));
r.x.ax = 0x0000;
__dpmi_int(0x33, &r);
// Set horizontal range to match screen width
memset(&r, 0, sizeof(r));
r.x.ax = 0x0007;
r.x.cx = 0;
r.x.dx = ctx->display.width - 1;
__dpmi_int(0x33, &r);
// Set vertical range to match screen height
memset(&r, 0, sizeof(r));
r.x.ax = 0x0008;
r.x.cx = 0;
r.x.dx = ctx->display.height - 1;
__dpmi_int(0x33, &r);
// Position cursor at center of screen
ctx->mouseX = ctx->display.width / 2;
ctx->mouseY = ctx->display.height / 2;
ctx->prevMouseX = ctx->mouseX;
ctx->prevMouseY = ctx->mouseY;
// Set mouse position
memset(&r, 0, sizeof(r));
r.x.ax = 0x0004;
r.x.cx = ctx->mouseX;
r.x.dx = ctx->mouseY;
__dpmi_int(0x33, &r);
}
// ============================================================ // ============================================================
@ -2228,40 +2176,17 @@ static void pollAnsiTermWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win)
// ============================================================ // ============================================================
static void pollKeyboard(AppContextT *ctx) { static void pollKeyboard(AppContextT *ctx) {
__dpmi_regs r; // Read modifier state once per poll
int32_t shiftFlags = platformKeyboardGetModifiers();
// Read shift state once per poll (INT 16h, AH=12h)
memset(&r, 0, sizeof(r));
r.x.ax = 0x1200;
__dpmi_int(0x16, &r);
int32_t shiftFlags = r.x.ax & 0xFF;
ctx->keyModifiers = shiftFlags; ctx->keyModifiers = shiftFlags;
bool shiftHeld = (shiftFlags & 0x03) != 0; // left or right shift bool shiftHeld = (shiftFlags & 0x03) != 0; // left or right shift
// Process buffered keys // Process buffered keys
while (1) { PlatformKeyEventT evt;
// Check if key is available (INT 16h, enhanced function 11h)
r.x.ax = 0x1100;
__dpmi_int(0x16, &r);
// Zero flag set = no key available while (platformKeyboardRead(&evt)) {
if (r.x.flags & 0x40) { int32_t scancode = evt.scancode;
break; int32_t ascii = evt.ascii;
}
// Read the key (INT 16h, enhanced function 10h)
r.x.ax = 0x1000;
__dpmi_int(0x16, &r);
int32_t scancode = (r.x.ax >> 8) & 0xFF;
int32_t ascii = r.x.ax & 0xFF;
// Enhanced INT 16h returns ascii=0xE0 for grey/extended keys
// (arrows, Home, End, Insert, Delete, etc. on 101-key keyboards).
// Normalize to 0 so all extended key checks work uniformly.
if (ascii == 0xE0) {
ascii = 0;
}
// Alt+Tab / Shift+Alt+Tab — cycle windows // Alt+Tab / Shift+Alt+Tab — cycle windows
// Alt+Tab: scancode=0xA5, ascii=0x00 // Alt+Tab: scancode=0xA5, ascii=0x00
@ -2453,7 +2378,7 @@ static void pollKeyboard(AppContextT *ctx) {
// System menu keyboard navigation // System menu keyboard navigation
if (ctx->sysMenu.active) { if (ctx->sysMenu.active) {
// Alt+key — close system menu and let it fall through to accel dispatch // Alt+key — close system menu and let it fall through to accel dispatch
if (ascii == 0 && scancode < 256 && sAltScanToAscii[scancode]) { if (ascii == 0 && platformAltScanToChar(scancode)) {
closeSysMenu(ctx); closeSysMenu(ctx);
// Fall through to dispatchAccelKey below // Fall through to dispatchAccelKey below
} else if (ascii == 0x1B) { } else if (ascii == 0x1B) {
@ -2541,8 +2466,8 @@ static void pollKeyboard(AppContextT *ctx) {
} }
// Check for Alt+key (BIOS returns ascii=0 with specific scancodes) // Check for Alt+key (BIOS returns ascii=0 with specific scancodes)
if (ascii == 0 && scancode < 256 && sAltScanToAscii[scancode]) { if (ascii == 0 && platformAltScanToChar(scancode)) {
char accelKey = sAltScanToAscii[scancode]; char accelKey = platformAltScanToChar(scancode);
if (dispatchAccelKey(ctx, accelKey)) { if (dispatchAccelKey(ctx, accelKey)) {
continue; continue;
@ -2896,15 +2821,15 @@ nextKey:;
// ============================================================ // ============================================================
static void pollMouse(AppContextT *ctx) { static void pollMouse(AppContextT *ctx) {
__dpmi_regs r; int32_t mx;
int32_t my;
int32_t buttons;
memset(&r, 0, sizeof(r)); platformMousePoll(&mx, &my, &buttons);
r.x.ax = 0x0003;
__dpmi_int(0x33, &r);
ctx->mouseX = r.x.cx; ctx->mouseX = mx;
ctx->mouseY = r.x.dx; ctx->mouseY = my;
ctx->mouseButtons = r.x.bx; ctx->mouseButtons = buttons;
} }

View file

@ -1,6 +1,7 @@
// dvx_comp.c — Layer 3: Dirty rectangle compositor for DV/X GUI (optimized) // dvx_comp.c — Layer 3: Dirty rectangle compositor for DV/X GUI (optimized)
#include "dvxComp.h" #include "dvxComp.h"
#include "platform/dvxPlatform.h"
#include <string.h> #include <string.h>
@ -109,62 +110,7 @@ void dirtyListMerge(DirtyListT *dl) {
// ============================================================ // ============================================================
void flushRect(DisplayT *d, const RectT *r) { void flushRect(DisplayT *d, const RectT *r) {
int32_t bpp = d->format.bytesPerPixel; platformFlushRect(d, r);
// Caller (compositeAndFlush) already clips to screen bounds
int32_t x = r->x;
int32_t y = r->y;
int32_t w = r->w;
int32_t h = r->h;
if (__builtin_expect(w <= 0 || h <= 0, 0)) { return; }
int32_t rowBytes = w * bpp;
int32_t pitch = d->pitch;
uint8_t *src = d->backBuf + y * pitch + x * bpp;
uint8_t *dst = d->lfb + y * pitch + x * bpp;
// Full-width flush: single large copy (write-combined memory loves sequential writes)
if (rowBytes == pitch) {
// Entire scanlines — copy as one contiguous block
int32_t totalBytes = pitch * h;
int32_t dwords = totalBytes >> 2;
int32_t remainder = totalBytes & 3;
__asm__ __volatile__ (
"rep movsl"
: "+D"(dst), "+S"(src), "+c"(dwords)
:
: "memory"
);
// Handle any trailing bytes (unlikely for aligned pitch)
while (remainder-- > 0) {
*dst++ = *src++;
}
} else {
// Partial scanlines — copy row by row with rep movsd
int32_t dwords = rowBytes >> 2;
int32_t remainder = rowBytes & 3;
for (int32_t i = 0; i < h; i++) {
int32_t dc = dwords;
uint8_t *s = src;
uint8_t *dd = dst;
__asm__ __volatile__ (
"rep movsl"
: "+D"(dd), "+S"(s), "+c"(dc)
:
: "memory"
);
// Trailing bytes (dd and s already advanced by rep movsl)
if (__builtin_expect(remainder > 0, 0)) {
int32_t rem = remainder;
while (rem-- > 0) {
*dd++ = *s++;
}
}
src += pitch;
dst += pitch;
}
}
} }

View file

@ -1,6 +1,7 @@
// dvxDialog.c — Modal dialogs for DV/X GUI // dvxDialog.c — Modal dialogs for DV/X GUI
#include "dvxDialog.h" #include "dvxDialog.h"
#include "platform/dvxPlatform.h"
#include "dvxWidget.h" #include "dvxWidget.h"
#include "widgets/widgetInternal.h" #include "widgets/widgetInternal.h"
@ -753,81 +754,13 @@ static void fdNavigate(const char *path) {
// ============================================================ // ============================================================
static bool fdValidateFilename(const char *name) { static bool fdValidateFilename(const char *name) {
// DOS 8.3 validation const char *error = platformValidateFilename(name);
static const char *reserved[] = {
"CON", "PRN", "AUX", "NUL",
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
NULL
};
if (!name || name[0] == '\0') { if (error) {
dvxMessageBox(sFd.ctx, "Invalid Filename", error, MB_OK | MB_ICONERROR);
return false; return false;
} }
// Split into base and extension
const char *dot = strrchr(name, '.');
int32_t baseLen;
int32_t extLen;
if (dot) {
baseLen = (int32_t)(dot - name);
extLen = (int32_t)strlen(dot + 1);
} else {
baseLen = (int32_t)strlen(name);
extLen = 0;
}
if (baseLen < 1 || baseLen > 8) {
dvxMessageBox(sFd.ctx, "Invalid Filename", "Filename must be 1-8 characters before the extension.", MB_OK | MB_ICONERROR);
return false;
}
if (extLen > 3) {
dvxMessageBox(sFd.ctx, "Invalid Filename", "Extension must be 3 characters or fewer.", MB_OK | MB_ICONERROR);
return false;
}
// Check for invalid characters
for (const char *p = name; *p; p++) {
if (*p == '.') {
continue;
}
if (*p < '!' || *p > '~') {
dvxMessageBox(sFd.ctx, "Invalid Filename", "Filename contains invalid characters.", MB_OK | MB_ICONERROR);
return false;
}
if (strchr(" \"*+,/:;<=>?[\\]|", *p)) {
dvxMessageBox(sFd.ctx, "Invalid Filename", "Filename contains invalid characters.", MB_OK | MB_ICONERROR);
return false;
}
}
// Check for multiple dots
if (dot && strchr(name, '.') != dot) {
dvxMessageBox(sFd.ctx, "Invalid Filename", "Filename may contain only one dot.", MB_OK | MB_ICONERROR);
return false;
}
// Check reserved device names (compare base only, case-insensitive)
char base[9];
int32_t copyLen = baseLen < 8 ? baseLen : 8;
for (int32_t i = 0; i < copyLen; i++) {
base[i] = toupper((unsigned char)name[i]);
}
base[copyLen] = '\0';
for (const char **r = reserved; *r; r++) {
if (strcmp(base, *r) == 0) {
dvxMessageBox(sFd.ctx, "Invalid Filename", "That name is a reserved device name.", MB_OK | MB_ICONERROR);
return false;
}
}
return true; return true;
} }

View file

@ -1,6 +1,7 @@
// dvx_draw.c — Layer 2: Drawing primitives for DV/X GUI (optimized) // dvx_draw.c — Layer 2: Drawing primitives for DV/X GUI (optimized)
#include "dvxDraw.h" #include "dvxDraw.h"
#include "platform/dvxPlatform.h"
#include <string.h> #include <string.h>
@ -11,12 +12,6 @@
char accelParse(const char *text); char accelParse(const char *text);
static inline void clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t *w, int32_t *h); static inline void clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t *w, int32_t *h);
static inline void putPixel(uint8_t *dst, uint32_t color, int32_t bpp); static inline void putPixel(uint8_t *dst, uint32_t color, int32_t bpp);
static void spanCopy8(uint8_t *dst, const uint8_t *src, int32_t count);
static void spanCopy16(uint8_t *dst, const uint8_t *src, int32_t count);
static void spanCopy32(uint8_t *dst, const uint8_t *src, int32_t count);
static void spanFill8(uint8_t *dst, uint32_t color, int32_t count);
static void spanFill16(uint8_t *dst, uint32_t color, int32_t count);
static void spanFill32(uint8_t *dst, uint32_t color, int32_t count);
// Bit lookup tables — avoids per-pixel shift on 486 (40+ cycle savings per shift) // Bit lookup tables — avoids per-pixel shift on 486 (40+ cycle savings per shift)
static const uint8_t sGlyphBit[8] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01}; static const uint8_t sGlyphBit[8] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
@ -571,20 +566,20 @@ void drawInit(BlitOpsT *ops, const DisplayT *d) {
switch (d->format.bytesPerPixel) { switch (d->format.bytesPerPixel) {
case 1: case 1:
ops->spanFill = spanFill8; ops->spanFill = platformSpanFill8;
ops->spanCopy = spanCopy8; ops->spanCopy = platformSpanCopy8;
break; break;
case 2: case 2:
ops->spanFill = spanFill16; ops->spanFill = platformSpanFill16;
ops->spanCopy = spanCopy16; ops->spanCopy = platformSpanCopy16;
break; break;
case 4: case 4:
ops->spanFill = spanFill32; ops->spanFill = platformSpanFill32;
ops->spanCopy = spanCopy32; ops->spanCopy = platformSpanCopy32;
break; break;
default: default:
ops->spanFill = spanFill8; ops->spanFill = platformSpanFill8;
ops->spanCopy = spanCopy8; ops->spanCopy = platformSpanCopy8;
break; break;
} }
} }
@ -990,190 +985,16 @@ void rectFill(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w,
return; return;
} }
int32_t bpp = d->format.bytesPerPixel; uint8_t *row = d->backBuf + y * d->pitch + x * d->format.bytesPerPixel;
uint8_t *row = d->backBuf + y * d->pitch + x * bpp;
int32_t pitch = d->pitch; int32_t pitch = d->pitch;
// Inline rep stosl for 32bpp — avoids function pointer call overhead for (int32_t i = 0; i < h; i++) {
// per scanline which is significant for the many 1px fills (Item 10) ops->spanFill(row, color, w);
if (__builtin_expect(bpp == 4, 1)) { row += pitch;
for (int32_t i = 0; i < h; i++) {
int32_t count = w;
uint8_t *dst = row;
__asm__ __volatile__ (
"rep stosl"
: "+D"(dst), "+c"(count)
: "a"(color)
: "memory"
);
row += pitch;
}
} else {
for (int32_t i = 0; i < h; i++) {
ops->spanFill(row, color, w);
row += pitch;
}
} }
} }
// ============================================================
// spanCopy8
// ============================================================
static void spanCopy8(uint8_t *dst, const uint8_t *src, int32_t count) {
// Align to 4 bytes
while (((uintptr_t)dst & 3) && count > 0) {
*dst++ = *src++;
count--;
}
if (count >= 4) {
int32_t dwordCount = count >> 2;
__asm__ __volatile__ (
"rep movsl"
: "+D"(dst), "+S"(src), "+c"(dwordCount)
:
: "memory"
);
dst += dwordCount * 4;
src += dwordCount * 4;
}
int32_t rem = count & 3;
while (rem-- > 0) {
*dst++ = *src++;
}
}
// ============================================================
// spanCopy16
// ============================================================
static void spanCopy16(uint8_t *dst, const uint8_t *src, int32_t count) {
// Handle odd leading pixel for dword alignment
if (((uintptr_t)dst & 2) && count > 0) {
*(uint16_t *)dst = *(const uint16_t *)src;
dst += 2;
src += 2;
count--;
}
if (count >= 2) {
int32_t dwordCount = count >> 1;
__asm__ __volatile__ (
"rep movsl"
: "+D"(dst), "+S"(src), "+c"(dwordCount)
:
: "memory"
);
dst += dwordCount * 4;
src += dwordCount * 4;
}
if (count & 1) {
*(uint16_t *)dst = *(const uint16_t *)src;
}
}
// ============================================================
// spanCopy32
// ============================================================
static void spanCopy32(uint8_t *dst, const uint8_t *src, int32_t count) {
__asm__ __volatile__ (
"rep movsl"
: "+D"(dst), "+S"(src), "+c"(count)
:
: "memory"
);
}
// ============================================================
// spanFill8
// ============================================================
static void spanFill8(uint8_t *dst, uint32_t color, int32_t count) {
uint8_t c = (uint8_t)color;
uint32_t dword = (uint32_t)c | ((uint32_t)c << 8) | ((uint32_t)c << 16) | ((uint32_t)c << 24);
// Align to 4 bytes — skip if already aligned (Item 11)
if (__builtin_expect((uintptr_t)dst & 3, 0)) {
while (((uintptr_t)dst & 3) && count > 0) {
*dst++ = c;
count--;
}
}
if (count >= 4) {
int32_t dwordCount = count >> 2;
__asm__ __volatile__ (
"rep stosl"
: "+D"(dst), "+c"(dwordCount)
: "a"(dword)
: "memory"
);
dst += dwordCount * 4;
}
int32_t rem = count & 3;
while (rem-- > 0) {
*dst++ = c;
}
}
// ============================================================
// spanFill16
// ============================================================
static void spanFill16(uint8_t *dst, uint32_t color, int32_t count) {
uint16_t c = (uint16_t)color;
// Handle odd leading pixel for dword alignment
if (((uintptr_t)dst & 2) && count > 0) {
*(uint16_t *)dst = c;
dst += 2;
count--;
}
// Fill pairs of pixels as 32-bit dwords
if (count >= 2) {
uint32_t dword = ((uint32_t)c << 16) | c;
int32_t dwordCount = count >> 1;
__asm__ __volatile__ (
"rep stosl"
: "+D"(dst), "+c"(dwordCount)
: "a"(dword)
: "memory"
);
dst += dwordCount * 4;
}
// Handle trailing odd pixel
if (count & 1) {
*(uint16_t *)dst = c;
}
}
// ============================================================
// spanFill32
// ============================================================
static void spanFill32(uint8_t *dst, uint32_t color, int32_t count) {
__asm__ __volatile__ (
"rep stosl"
: "+D"(dst), "+c"(count)
: "a"(color)
: "memory"
);
}
// ============================================================ // ============================================================
// textWidth // textWidth
// ============================================================ // ============================================================

View file

@ -1,262 +1,15 @@
// dvx_video.c — Layer 1: VESA VBE video backend for DV/X GUI // dvx_video.c — Layer 1: Video backend for DV/X GUI
//
// Platform-independent video utilities. The actual VESA/VBE code
// now lives in dvxPlatformDos.c (or the platform file for whatever
// OS we're targeting).
#include "dvxVideo.h" #include "dvxVideo.h"
#include "platform/dvxPlatform.h"
#include "dvxPalette.h" #include "dvxPalette.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <dpmi.h>
#include <go32.h>
#include <pc.h>
#include <sys/nearptr.h>
#include <sys/farptr.h>
// ============================================================
// Prototypes
// ============================================================
static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t preferredBpp, uint16_t *outMode, DisplayT *d);
static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requestedW, int32_t requestedH, int32_t preferredBpp);
static int32_t mapLfb(DisplayT *d, uint32_t physAddr);
static int32_t setVesaMode(uint16_t mode);
static void videoSetPalette(const uint8_t *pal, int32_t firstEntry, int32_t count);
// ============================================================
// findBestMode
// ============================================================
static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t preferredBpp, uint16_t *outMode, DisplayT *d) {
__dpmi_regs r;
uint16_t bestMode = 0;
int32_t bestScore = -1;
DisplayT bestDisplay;
memset(&bestDisplay, 0, sizeof(bestDisplay));
// Get VBE controller info
// Put "VBE2" signature at ES:DI in conventional memory
uint32_t infoSeg = __tb >> 4;
uint32_t infoOff = __tb & 0x0F;
// Write "VBE2" at transfer buffer to request VBE 2.0 info
_farpokeb(_dos_ds, __tb + 0, 'V');
_farpokeb(_dos_ds, __tb + 1, 'B');
_farpokeb(_dos_ds, __tb + 2, 'E');
_farpokeb(_dos_ds, __tb + 3, '2');
memset(&r, 0, sizeof(r));
r.x.ax = 0x4F00;
r.x.es = infoSeg;
r.x.di = infoOff;
__dpmi_int(0x10, &r);
if (r.x.ax != 0x004F) {
fprintf(stderr, "VBE: Function 0x4F00 failed (AX=0x%04X)\n", r.x.ax);
return -1;
}
// Verify VBE signature
char sig[5];
for (int32_t i = 0; i < 4; i++) {
sig[i] = _farpeekb(_dos_ds, __tb + i);
}
sig[4] = '\0';
if (strcmp(sig, "VESA") != 0) {
fprintf(stderr, "VBE: Bad signature '%s'\n", sig);
return -1;
}
// Check VBE version (need 2.0+)
uint16_t vbeVersion = _farpeekw(_dos_ds, __tb + 4);
if (vbeVersion < 0x0200) {
fprintf(stderr, "VBE: Version %d.%d too old (need 2.0+)\n",
vbeVersion >> 8, vbeVersion & 0xFF);
return -1;
}
// Get mode list pointer (far pointer at offset 14)
uint16_t modeListOff = _farpeekw(_dos_ds, __tb + 14);
uint16_t modeListSeg = _farpeekw(_dos_ds, __tb + 16);
uint32_t modeListAddr = ((uint32_t)modeListSeg << 4) + modeListOff;
// Walk mode list
for (int32_t i = 0; i < 256; i++) {
uint16_t mode = _farpeekw(_dos_ds, modeListAddr + i * 2);
if (mode == 0xFFFF) {
break;
}
DisplayT candidate;
int32_t score = 0;
memset(&candidate, 0, sizeof(candidate));
getModeInfo(mode, &candidate, &score, requestedW, requestedH, preferredBpp);
if (score > bestScore) {
bestScore = score;
bestMode = mode;
bestDisplay = candidate;
}
}
if (bestScore < 0) {
fprintf(stderr, "VBE: No suitable mode found for %ldx%ld\n", (long)requestedW, (long)requestedH);
return -1;
}
*outMode = bestMode;
*d = bestDisplay;
return 0;
}
// ============================================================
// getModeInfo
// ============================================================
static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
__dpmi_regs r;
*score = -1;
memset(&r, 0, sizeof(r));
r.x.ax = 0x4F01;
r.x.cx = mode;
r.x.es = __tb >> 4;
r.x.di = __tb & 0x0F;
__dpmi_int(0x10, &r);
if (r.x.ax != 0x004F) {
return;
}
// Read mode attributes
uint16_t attr = _farpeekw(_dos_ds, __tb + 0);
// Must have LFB support (bit 7)
if (!(attr & 0x0080)) {
return;
}
// Must be a graphics mode (bit 4)
if (!(attr & 0x0010)) {
return;
}
int32_t w = _farpeekw(_dos_ds, __tb + 18);
int32_t h = _farpeekw(_dos_ds, __tb + 20);
int32_t bpp = _farpeekb(_dos_ds, __tb + 25);
int32_t pitch = _farpeekw(_dos_ds, __tb + 16);
uint32_t physAddr = _farpeekl(_dos_ds, __tb + 40);
// Must match or exceed requested resolution
if (w < requestedW || h < requestedH) {
return;
}
// Must be a supported bpp
if (bpp != 8 && bpp != 15 && bpp != 16 && bpp != 32) {
return;
}
// Score this mode
int32_t s = 0;
if (bpp == 16) {
s = 100;
} else if (bpp == 15) {
s = 90;
} else if (bpp == 32) {
s = 85;
} else if (bpp == 8) {
s = 70;
}
// Prefer the user's preferred bpp
if (bpp == preferredBpp) {
s += 20;
}
// Exact resolution match is preferred
if (w == requestedW && h == requestedH) {
s += 10;
} else {
s -= 10;
}
*score = s;
// Fill in display info
d->width = w;
d->height = h;
d->pitch = pitch;
d->format.bitsPerPixel = bpp;
d->format.bytesPerPixel = (bpp + 7) / 8;
// Read color masks from mode info
if (bpp >= 15) {
int32_t redSize = _farpeekb(_dos_ds, __tb + 31);
int32_t redPos = _farpeekb(_dos_ds, __tb + 32);
int32_t greenSize = _farpeekb(_dos_ds, __tb + 33);
int32_t greenPos = _farpeekb(_dos_ds, __tb + 34);
int32_t blueSize = _farpeekb(_dos_ds, __tb + 35);
int32_t bluePos = _farpeekb(_dos_ds, __tb + 36);
d->format.redBits = redSize;
d->format.redShift = redPos;
d->format.redMask = ((1U << redSize) - 1) << redPos;
d->format.greenBits = greenSize;
d->format.greenShift = greenPos;
d->format.greenMask = ((1U << greenSize) - 1) << greenPos;
d->format.blueBits = blueSize;
d->format.blueShift = bluePos;
d->format.blueMask = ((1U << blueSize) - 1) << bluePos;
}
// Store physical address in lfb field temporarily (will be remapped)
d->lfb = (uint8_t *)(uintptr_t)physAddr;
}
// ============================================================
// mapLfb
// ============================================================
static int32_t mapLfb(DisplayT *d, uint32_t physAddr) {
__dpmi_meminfo info;
uint32_t fbSize = (uint32_t)d->pitch * (uint32_t)d->height;
info.address = physAddr;
info.size = fbSize;
if (__dpmi_physical_address_mapping(&info) != 0) {
fprintf(stderr, "VBE: Failed to map LFB at 0x%08lX\n", (unsigned long)physAddr);
return -1;
}
// Lock the region to prevent paging
__dpmi_meminfo lockInfo;
lockInfo.address = info.address;
lockInfo.size = fbSize;
__dpmi_lock_linear_region(&lockInfo);
// Enable near pointers for direct access
if (__djgpp_nearptr_enable() == 0) {
fprintf(stderr, "VBE: Failed to enable near pointers\n");
return -1;
}
d->lfb = (uint8_t *)(info.address + __djgpp_conventional_base);
return 0;
}
// ============================================================ // ============================================================
// packColor // packColor
@ -307,108 +60,12 @@ void setClipRect(DisplayT *d, int32_t x, int32_t y, int32_t w, int32_t h) {
} }
// ============================================================
// setVesaMode
// ============================================================
static int32_t setVesaMode(uint16_t mode) {
__dpmi_regs r;
memset(&r, 0, sizeof(r));
r.x.ax = 0x4F02;
r.x.bx = mode | 0x4000; // bit 14 = use LFB
__dpmi_int(0x10, &r);
if (r.x.ax != 0x004F) {
fprintf(stderr, "VBE: Failed to set mode 0x%04X (AX=0x%04X)\n", mode, r.x.ax);
return -1;
}
return 0;
}
// ============================================================ // ============================================================
// videoInit // videoInit
// ============================================================ // ============================================================
int32_t videoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) { int32_t videoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
uint16_t bestMode; return platformVideoInit(d, requestedW, requestedH, preferredBpp);
uint32_t physAddr;
memset(d, 0, sizeof(*d));
// Find the best VESA mode
if (findBestMode(requestedW, requestedH, preferredBpp, &bestMode, d) != 0) {
return -1;
}
// Save the physical address before we overwrite it
physAddr = (uint32_t)(uintptr_t)d->lfb;
// Set the mode
if (setVesaMode(bestMode) != 0) {
return -1;
}
// Map the LFB
if (mapLfb(d, physAddr) != 0) {
return -1;
}
// Allocate backbuffer
uint32_t fbSize = (uint32_t)d->pitch * (uint32_t)d->height;
d->backBuf = (uint8_t *)malloc(fbSize);
if (!d->backBuf) {
fprintf(stderr, "VBE: Failed to allocate %lu byte backbuffer\n", (unsigned long)fbSize);
__djgpp_nearptr_disable();
return -1;
}
memset(d->backBuf, 0, fbSize);
// Set up palette for 8-bit mode
if (d->format.bitsPerPixel == 8) {
d->palette = (uint8_t *)malloc(768);
if (!d->palette) {
fprintf(stderr, "VBE: Failed to allocate palette\n");
free(d->backBuf);
d->backBuf = NULL;
__djgpp_nearptr_disable();
return -1;
}
dvxGeneratePalette(d->palette);
videoSetPalette(d->palette, 0, 256);
}
// Initialize clip rect to full display
resetClipRect(d);
fprintf(stderr, "VBE: Mode 0x%04X set: %ldx%ldx%ld, pitch=%ld\n",
bestMode, (long)d->width, (long)d->height, (long)d->format.bitsPerPixel, (long)d->pitch);
return 0;
}
// ============================================================
// videoSetPalette
// ============================================================
static void videoSetPalette(const uint8_t *pal, int32_t firstEntry, int32_t count) {
// Set VGA DAC registers directly via port I/O
// Port 0x3C8 = write index, Port 0x3C9 = data (R, G, B in 6-bit values)
outportb(0x3C8, (uint8_t)firstEntry);
for (int32_t i = 0; i < count; i++) {
int32_t idx = (firstEntry + i) * 3;
outportb(0x3C9, pal[idx + 0] >> 2); // VGA DAC uses 6-bit values
outportb(0x3C9, pal[idx + 1] >> 2);
outportb(0x3C9, pal[idx + 2] >> 2);
}
} }
@ -417,23 +74,5 @@ static void videoSetPalette(const uint8_t *pal, int32_t firstEntry, int32_t coun
// ============================================================ // ============================================================
void videoShutdown(DisplayT *d) { void videoShutdown(DisplayT *d) {
// Restore text mode (mode 3) platformVideoShutdown(d);
__dpmi_regs r;
memset(&r, 0, sizeof(r));
r.x.ax = 0x0003;
__dpmi_int(0x10, &r);
if (d->backBuf) {
free(d->backBuf);
d->backBuf = NULL;
}
if (d->palette) {
free(d->palette);
d->palette = NULL;
}
d->lfb = NULL;
__djgpp_nearptr_disable();
} }

View file

@ -0,0 +1,98 @@
// dvxPlatform.h — Platform abstraction layer for DV/X GUI
//
// All OS-specific and CPU-specific code is isolated behind this
// interface. To port DV/X to a new platform, implement a new
// dvxPlatformXxx.c against this header.
#ifndef DVX_PLATFORM_H
#define DVX_PLATFORM_H
#include "../dvxTypes.h"
// ============================================================
// Keyboard event
// ============================================================
typedef struct {
int32_t ascii; // ASCII value, 0 for extended/function keys
int32_t scancode; // PC scan code (0x48=Up, 0x50=Down, etc.)
} PlatformKeyEventT;
// ============================================================
// System lifecycle
// ============================================================
// One-time platform initialisation (signal handling, etc.)
void platformInit(void);
// Cooperative multitasking yield (give up CPU when idle)
void platformYield(void);
// ============================================================
// Video
// ============================================================
// Initialise video mode, map framebuffer, allocate backbuffer.
// Fills in all DisplayT fields on success. Returns 0/-1.
int32_t platformVideoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp);
// Shut down video — restore text mode, unmap framebuffer, free backbuffer.
void platformVideoShutdown(DisplayT *d);
// Set palette entries (8-bit mode). pal is R,G,B triplets.
void platformVideoSetPalette(const uint8_t *pal, int32_t firstEntry, int32_t count);
// ============================================================
// Framebuffer flush
// ============================================================
// Copy a rectangle from d->backBuf to the display surface (d->lfb).
void platformFlushRect(const DisplayT *d, const RectT *r);
// ============================================================
// Optimised memory operations (span fill / copy)
// ============================================================
void platformSpanFill8(uint8_t *dst, uint32_t color, int32_t count);
void platformSpanFill16(uint8_t *dst, uint32_t color, int32_t count);
void platformSpanFill32(uint8_t *dst, uint32_t color, int32_t count);
void platformSpanCopy8(uint8_t *dst, const uint8_t *src, int32_t count);
void platformSpanCopy16(uint8_t *dst, const uint8_t *src, int32_t count);
void platformSpanCopy32(uint8_t *dst, const uint8_t *src, int32_t count);
// ============================================================
// Input — Mouse
// ============================================================
// Initialise mouse driver, set movement range, centre cursor.
void platformMouseInit(int32_t screenW, int32_t screenH);
// Read current mouse position and button state.
void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons);
// ============================================================
// Input — Keyboard
// ============================================================
// Return current modifier flags (BIOS shift-state format:
// bits 0-1 = shift, bit 2 = ctrl, bit 3 = alt).
int32_t platformKeyboardGetModifiers(void);
// Read the next key from the keyboard buffer.
// Returns true if a key was available, false if the buffer was empty.
// Normalises extended-key markers (e.g. 0xE0 → 0).
bool platformKeyboardRead(PlatformKeyEventT *evt);
// Map a scan code to its Alt+letter ASCII character.
// Returns the lowercase letter if the scan code corresponds to an
// Alt+letter/digit combo, or 0 if it does not.
char platformAltScanToChar(int32_t scancode);
// ============================================================
// File system
// ============================================================
// Validate a filename for the current platform.
// Returns NULL if valid, or a human-readable error string if invalid.
const char *platformValidateFilename(const char *name);
#endif // DVX_PLATFORM_H

View file

@ -0,0 +1,841 @@
// dvxPlatformDos.c — DOS/DJGPP platform implementation for DV/X GUI
//
// All BIOS calls, DPMI functions, port I/O, inline assembly, and
// DOS-specific file handling are isolated in this single file.
#include "dvxPlatform.h"
#include "../dvxPalette.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <signal.h>
#include <dpmi.h>
#include <go32.h>
#include <pc.h>
#include <sys/nearptr.h>
#include <sys/farptr.h>
// ============================================================
// Prototypes
// ============================================================
static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t preferredBpp, uint16_t *outMode, DisplayT *d);
static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requestedW, int32_t requestedH, int32_t preferredBpp);
static int32_t mapLfb(DisplayT *d, uint32_t physAddr);
static int32_t setVesaMode(uint16_t mode);
// Alt+key scan code to ASCII lookup table (indexed by BIOS scan code).
// INT 16h returns these scan codes with ascii=0 for Alt+key combos.
static const char sAltScanToAscii[256] = {
// Alt+letters
[0x10] = 'q', [0x11] = 'w', [0x12] = 'e', [0x13] = 'r',
[0x14] = 't', [0x15] = 'y', [0x16] = 'u', [0x17] = 'i',
[0x18] = 'o', [0x19] = 'p', [0x1E] = 'a', [0x1F] = 's',
[0x20] = 'd', [0x21] = 'f', [0x22] = 'g', [0x23] = 'h',
[0x24] = 'j', [0x25] = 'k', [0x26] = 'l', [0x2C] = 'z',
[0x2D] = 'x', [0x2E] = 'c', [0x2F] = 'v', [0x30] = 'b',
[0x31] = 'n', [0x32] = 'm',
// Alt+digits
[0x78] = '1', [0x79] = '2', [0x7A] = '3', [0x7B] = '4',
[0x7C] = '5', [0x7D] = '6', [0x7E] = '7', [0x7F] = '8',
[0x80] = '9', [0x81] = '0',
};
// ============================================================
// findBestMode
// ============================================================
static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t preferredBpp, uint16_t *outMode, DisplayT *d) {
__dpmi_regs r;
uint16_t bestMode = 0;
int32_t bestScore = -1;
DisplayT bestDisplay;
memset(&bestDisplay, 0, sizeof(bestDisplay));
// Get VBE controller info
uint32_t infoSeg = __tb >> 4;
uint32_t infoOff = __tb & 0x0F;
// Write "VBE2" at transfer buffer to request VBE 2.0 info
_farpokeb(_dos_ds, __tb + 0, 'V');
_farpokeb(_dos_ds, __tb + 1, 'B');
_farpokeb(_dos_ds, __tb + 2, 'E');
_farpokeb(_dos_ds, __tb + 3, '2');
memset(&r, 0, sizeof(r));
r.x.ax = 0x4F00;
r.x.es = infoSeg;
r.x.di = infoOff;
__dpmi_int(0x10, &r);
if (r.x.ax != 0x004F) {
fprintf(stderr, "VBE: Function 0x4F00 failed (AX=0x%04X)\n", r.x.ax);
return -1;
}
// Verify VBE signature
char sig[5];
for (int32_t i = 0; i < 4; i++) {
sig[i] = _farpeekb(_dos_ds, __tb + i);
}
sig[4] = '\0';
if (strcmp(sig, "VESA") != 0) {
fprintf(stderr, "VBE: Bad signature '%s'\n", sig);
return -1;
}
// Check VBE version (need 2.0+)
uint16_t vbeVersion = _farpeekw(_dos_ds, __tb + 4);
if (vbeVersion < 0x0200) {
fprintf(stderr, "VBE: Version %d.%d too old (need 2.0+)\n",
vbeVersion >> 8, vbeVersion & 0xFF);
return -1;
}
// Get mode list pointer (far pointer at offset 14)
uint16_t modeListOff = _farpeekw(_dos_ds, __tb + 14);
uint16_t modeListSeg = _farpeekw(_dos_ds, __tb + 16);
uint32_t modeListAddr = ((uint32_t)modeListSeg << 4) + modeListOff;
// Walk mode list
for (int32_t i = 0; i < 256; i++) {
uint16_t mode = _farpeekw(_dos_ds, modeListAddr + i * 2);
if (mode == 0xFFFF) {
break;
}
DisplayT candidate;
int32_t score = 0;
memset(&candidate, 0, sizeof(candidate));
getModeInfo(mode, &candidate, &score, requestedW, requestedH, preferredBpp);
if (score > bestScore) {
bestScore = score;
bestMode = mode;
bestDisplay = candidate;
}
}
if (bestScore < 0) {
fprintf(stderr, "VBE: No suitable mode found for %ldx%ld\n", (long)requestedW, (long)requestedH);
return -1;
}
*outMode = bestMode;
*d = bestDisplay;
return 0;
}
// ============================================================
// getModeInfo
// ============================================================
static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
__dpmi_regs r;
*score = -1;
memset(&r, 0, sizeof(r));
r.x.ax = 0x4F01;
r.x.cx = mode;
r.x.es = __tb >> 4;
r.x.di = __tb & 0x0F;
__dpmi_int(0x10, &r);
if (r.x.ax != 0x004F) {
return;
}
// Read mode attributes
uint16_t attr = _farpeekw(_dos_ds, __tb + 0);
// Must have LFB support (bit 7)
if (!(attr & 0x0080)) {
return;
}
// Must be a graphics mode (bit 4)
if (!(attr & 0x0010)) {
return;
}
int32_t w = _farpeekw(_dos_ds, __tb + 18);
int32_t h = _farpeekw(_dos_ds, __tb + 20);
int32_t bpp = _farpeekb(_dos_ds, __tb + 25);
int32_t pitch = _farpeekw(_dos_ds, __tb + 16);
uint32_t physAddr = _farpeekl(_dos_ds, __tb + 40);
// Must match or exceed requested resolution
if (w < requestedW || h < requestedH) {
return;
}
// Must be a supported bpp
if (bpp != 8 && bpp != 15 && bpp != 16 && bpp != 32) {
return;
}
// Score this mode
int32_t s = 0;
if (bpp == 16) {
s = 100;
} else if (bpp == 15) {
s = 90;
} else if (bpp == 32) {
s = 85;
} else if (bpp == 8) {
s = 70;
}
// Prefer the user's preferred bpp
if (bpp == preferredBpp) {
s += 20;
}
// Exact resolution match is preferred
if (w == requestedW && h == requestedH) {
s += 10;
} else {
s -= 10;
}
*score = s;
// Fill in display info
d->width = w;
d->height = h;
d->pitch = pitch;
d->format.bitsPerPixel = bpp;
d->format.bytesPerPixel = (bpp + 7) / 8;
// Read color masks from mode info
if (bpp >= 15) {
int32_t redSize = _farpeekb(_dos_ds, __tb + 31);
int32_t redPos = _farpeekb(_dos_ds, __tb + 32);
int32_t greenSize = _farpeekb(_dos_ds, __tb + 33);
int32_t greenPos = _farpeekb(_dos_ds, __tb + 34);
int32_t blueSize = _farpeekb(_dos_ds, __tb + 35);
int32_t bluePos = _farpeekb(_dos_ds, __tb + 36);
d->format.redBits = redSize;
d->format.redShift = redPos;
d->format.redMask = ((1U << redSize) - 1) << redPos;
d->format.greenBits = greenSize;
d->format.greenShift = greenPos;
d->format.greenMask = ((1U << greenSize) - 1) << greenPos;
d->format.blueBits = blueSize;
d->format.blueShift = bluePos;
d->format.blueMask = ((1U << blueSize) - 1) << bluePos;
}
// Store physical address in lfb field temporarily (will be remapped)
d->lfb = (uint8_t *)(uintptr_t)physAddr;
}
// ============================================================
// mapLfb
// ============================================================
static int32_t mapLfb(DisplayT *d, uint32_t physAddr) {
__dpmi_meminfo info;
uint32_t fbSize = (uint32_t)d->pitch * (uint32_t)d->height;
info.address = physAddr;
info.size = fbSize;
if (__dpmi_physical_address_mapping(&info) != 0) {
fprintf(stderr, "VBE: Failed to map LFB at 0x%08lX\n", (unsigned long)physAddr);
return -1;
}
// Lock the region to prevent paging
__dpmi_meminfo lockInfo;
lockInfo.address = info.address;
lockInfo.size = fbSize;
__dpmi_lock_linear_region(&lockInfo);
// Enable near pointers for direct access
if (__djgpp_nearptr_enable() == 0) {
fprintf(stderr, "VBE: Failed to enable near pointers\n");
return -1;
}
d->lfb = (uint8_t *)(info.address + __djgpp_conventional_base);
return 0;
}
// ============================================================
// platformAltScanToChar
// ============================================================
char platformAltScanToChar(int32_t scancode) {
if (scancode < 0 || scancode > 255) {
return 0;
}
return sAltScanToAscii[scancode];
}
// ============================================================
// platformFlushRect
// ============================================================
void platformFlushRect(const DisplayT *d, const RectT *r) {
int32_t bpp = d->format.bytesPerPixel;
int32_t x = r->x;
int32_t y = r->y;
int32_t w = r->w;
int32_t h = r->h;
if (__builtin_expect(w <= 0 || h <= 0, 0)) {
return;
}
int32_t rowBytes = w * bpp;
int32_t pitch = d->pitch;
uint8_t *src = d->backBuf + y * pitch + x * bpp;
uint8_t *dst = d->lfb + y * pitch + x * bpp;
// Full-width flush: single large copy
if (rowBytes == pitch) {
int32_t totalBytes = pitch * h;
int32_t dwords = totalBytes >> 2;
int32_t remainder = totalBytes & 3;
__asm__ __volatile__ (
"rep movsl"
: "+D"(dst), "+S"(src), "+c"(dwords)
:
: "memory"
);
while (remainder-- > 0) {
*dst++ = *src++;
}
} else {
// Partial scanlines — copy row by row with rep movsd
int32_t dwords = rowBytes >> 2;
int32_t remainder = rowBytes & 3;
for (int32_t i = 0; i < h; i++) {
int32_t dc = dwords;
uint8_t *s = src;
uint8_t *dd = dst;
__asm__ __volatile__ (
"rep movsl"
: "+D"(dd), "+S"(s), "+c"(dc)
:
: "memory"
);
if (__builtin_expect(remainder > 0, 0)) {
int32_t rem = remainder;
while (rem-- > 0) {
*dd++ = *s++;
}
}
src += pitch;
dst += pitch;
}
}
}
// ============================================================
// platformInit
// ============================================================
void platformInit(void) {
// Disable Ctrl+C/Break termination
signal(SIGINT, SIG_IGN);
}
// ============================================================
// platformKeyboardGetModifiers
// ============================================================
int32_t platformKeyboardGetModifiers(void) {
__dpmi_regs r;
memset(&r, 0, sizeof(r));
r.x.ax = 0x1200;
__dpmi_int(0x16, &r);
return r.x.ax & 0xFF;
}
// ============================================================
// platformKeyboardRead
// ============================================================
bool platformKeyboardRead(PlatformKeyEventT *evt) {
__dpmi_regs r;
// Check if key is available (INT 16h, enhanced function 11h)
r.x.ax = 0x1100;
__dpmi_int(0x16, &r);
// Zero flag set = no key available
if (r.x.flags & 0x40) {
return false;
}
// Read the key (INT 16h, enhanced function 10h)
r.x.ax = 0x1000;
__dpmi_int(0x16, &r);
evt->scancode = (r.x.ax >> 8) & 0xFF;
evt->ascii = r.x.ax & 0xFF;
// Enhanced INT 16h returns ascii=0xE0 for grey/extended keys
// (arrows, Home, End, Insert, Delete, etc. on 101-key keyboards).
// Normalize to 0 so all extended key checks work uniformly.
if (evt->ascii == 0xE0) {
evt->ascii = 0;
}
return true;
}
// ============================================================
// platformMouseInit
// ============================================================
void platformMouseInit(int32_t screenW, int32_t screenH) {
__dpmi_regs r;
// Reset mouse driver
memset(&r, 0, sizeof(r));
r.x.ax = 0x0000;
__dpmi_int(0x33, &r);
// Set horizontal range
memset(&r, 0, sizeof(r));
r.x.ax = 0x0007;
r.x.cx = 0;
r.x.dx = screenW - 1;
__dpmi_int(0x33, &r);
// Set vertical range
memset(&r, 0, sizeof(r));
r.x.ax = 0x0008;
r.x.cx = 0;
r.x.dx = screenH - 1;
__dpmi_int(0x33, &r);
// Position cursor at center
memset(&r, 0, sizeof(r));
r.x.ax = 0x0004;
r.x.cx = screenW / 2;
r.x.dx = screenH / 2;
__dpmi_int(0x33, &r);
}
// ============================================================
// platformMousePoll
// ============================================================
void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) {
__dpmi_regs r;
memset(&r, 0, sizeof(r));
r.x.ax = 0x0003;
__dpmi_int(0x33, &r);
*mx = r.x.cx;
*my = r.x.dx;
*buttons = r.x.bx;
}
// ============================================================
// platformSpanCopy8
// ============================================================
void platformSpanCopy8(uint8_t *dst, const uint8_t *src, int32_t count) {
// Align to 4 bytes
while (((uintptr_t)dst & 3) && count > 0) {
*dst++ = *src++;
count--;
}
if (count >= 4) {
int32_t dwordCount = count >> 2;
__asm__ __volatile__ (
"rep movsl"
: "+D"(dst), "+S"(src), "+c"(dwordCount)
:
: "memory"
);
dst += dwordCount * 4;
src += dwordCount * 4;
}
int32_t rem = count & 3;
while (rem-- > 0) {
*dst++ = *src++;
}
}
// ============================================================
// platformSpanCopy16
// ============================================================
void platformSpanCopy16(uint8_t *dst, const uint8_t *src, int32_t count) {
// Handle odd leading pixel for dword alignment
if (((uintptr_t)dst & 2) && count > 0) {
*(uint16_t *)dst = *(const uint16_t *)src;
dst += 2;
src += 2;
count--;
}
if (count >= 2) {
int32_t dwordCount = count >> 1;
__asm__ __volatile__ (
"rep movsl"
: "+D"(dst), "+S"(src), "+c"(dwordCount)
:
: "memory"
);
dst += dwordCount * 4;
src += dwordCount * 4;
}
if (count & 1) {
*(uint16_t *)dst = *(const uint16_t *)src;
}
}
// ============================================================
// platformSpanCopy32
// ============================================================
void platformSpanCopy32(uint8_t *dst, const uint8_t *src, int32_t count) {
__asm__ __volatile__ (
"rep movsl"
: "+D"(dst), "+S"(src), "+c"(count)
:
: "memory"
);
}
// ============================================================
// platformSpanFill8
// ============================================================
void platformSpanFill8(uint8_t *dst, uint32_t color, int32_t count) {
uint8_t c = (uint8_t)color;
uint32_t dword = (uint32_t)c | ((uint32_t)c << 8) | ((uint32_t)c << 16) | ((uint32_t)c << 24);
// Align to 4 bytes — skip if already aligned
if (__builtin_expect((uintptr_t)dst & 3, 0)) {
while (((uintptr_t)dst & 3) && count > 0) {
*dst++ = c;
count--;
}
}
if (count >= 4) {
int32_t dwordCount = count >> 2;
__asm__ __volatile__ (
"rep stosl"
: "+D"(dst), "+c"(dwordCount)
: "a"(dword)
: "memory"
);
dst += dwordCount * 4;
}
int32_t rem = count & 3;
while (rem-- > 0) {
*dst++ = c;
}
}
// ============================================================
// platformSpanFill16
// ============================================================
void platformSpanFill16(uint8_t *dst, uint32_t color, int32_t count) {
uint16_t c = (uint16_t)color;
// Handle odd leading pixel for dword alignment
if (((uintptr_t)dst & 2) && count > 0) {
*(uint16_t *)dst = c;
dst += 2;
count--;
}
// Fill pairs of pixels as 32-bit dwords
if (count >= 2) {
uint32_t dword = ((uint32_t)c << 16) | c;
int32_t dwordCount = count >> 1;
__asm__ __volatile__ (
"rep stosl"
: "+D"(dst), "+c"(dwordCount)
: "a"(dword)
: "memory"
);
dst += dwordCount * 4;
}
// Handle trailing odd pixel
if (count & 1) {
*(uint16_t *)dst = c;
}
}
// ============================================================
// platformSpanFill32
// ============================================================
void platformSpanFill32(uint8_t *dst, uint32_t color, int32_t count) {
__asm__ __volatile__ (
"rep stosl"
: "+D"(dst), "+c"(count)
: "a"(color)
: "memory"
);
}
// ============================================================
// platformValidateFilename — DOS 8.3 filename validation
// ============================================================
const char *platformValidateFilename(const char *name) {
static const char *reserved[] = {
"CON", "PRN", "AUX", "NUL",
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
NULL
};
if (!name || name[0] == '\0') {
return "Filename must not be empty.";
}
// Split into base and extension
const char *dot = strrchr(name, '.');
int32_t baseLen;
int32_t extLen;
if (dot) {
baseLen = (int32_t)(dot - name);
extLen = (int32_t)strlen(dot + 1);
} else {
baseLen = (int32_t)strlen(name);
extLen = 0;
}
if (baseLen < 1 || baseLen > 8) {
return "Filename must be 1-8 characters before the extension.";
}
if (extLen > 3) {
return "Extension must be 3 characters or fewer.";
}
// Check for invalid characters
for (const char *p = name; *p; p++) {
if (*p == '.') {
continue;
}
if (*p < '!' || *p > '~') {
return "Filename contains invalid characters.";
}
if (strchr(" \"*+,/:;<=>?[\\]|", *p)) {
return "Filename contains invalid characters.";
}
}
// Check for multiple dots
if (dot && strchr(name, '.') != dot) {
return "Filename may contain only one dot.";
}
// Check reserved device names (compare base only, case-insensitive)
char base[9];
int32_t copyLen = baseLen < 8 ? baseLen : 8;
for (int32_t i = 0; i < copyLen; i++) {
base[i] = toupper((unsigned char)name[i]);
}
base[copyLen] = '\0';
for (const char **r = reserved; *r; r++) {
if (strcmp(base, *r) == 0) {
return "That name is a reserved device name.";
}
}
return NULL;
}
// ============================================================
// platformVideoInit
// ============================================================
int32_t platformVideoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
uint16_t bestMode;
uint32_t physAddr;
memset(d, 0, sizeof(*d));
// Find the best VESA mode
if (findBestMode(requestedW, requestedH, preferredBpp, &bestMode, d) != 0) {
return -1;
}
// Save the physical address before we overwrite it
physAddr = (uint32_t)(uintptr_t)d->lfb;
// Set the mode
if (setVesaMode(bestMode) != 0) {
return -1;
}
// Map the LFB
if (mapLfb(d, physAddr) != 0) {
return -1;
}
// Allocate backbuffer
uint32_t fbSize = (uint32_t)d->pitch * (uint32_t)d->height;
d->backBuf = (uint8_t *)malloc(fbSize);
if (!d->backBuf) {
fprintf(stderr, "VBE: Failed to allocate %lu byte backbuffer\n", (unsigned long)fbSize);
__djgpp_nearptr_disable();
return -1;
}
memset(d->backBuf, 0, fbSize);
// Set up palette for 8-bit mode
if (d->format.bitsPerPixel == 8) {
d->palette = (uint8_t *)malloc(768);
if (!d->palette) {
fprintf(stderr, "VBE: Failed to allocate palette\n");
free(d->backBuf);
d->backBuf = NULL;
__djgpp_nearptr_disable();
return -1;
}
dvxGeneratePalette(d->palette);
platformVideoSetPalette(d->palette, 0, 256);
}
// Initialize clip rect to full display
d->clipX = 0;
d->clipY = 0;
d->clipW = d->width;
d->clipH = d->height;
fprintf(stderr, "VBE: Mode 0x%04X set: %ldx%ldx%ld, pitch=%ld\n",
bestMode, (long)d->width, (long)d->height, (long)d->format.bitsPerPixel, (long)d->pitch);
return 0;
}
// ============================================================
// platformVideoSetPalette
// ============================================================
void platformVideoSetPalette(const uint8_t *pal, int32_t firstEntry, int32_t count) {
// Set VGA DAC registers directly via port I/O
outportb(0x3C8, (uint8_t)firstEntry);
for (int32_t i = 0; i < count; i++) {
int32_t idx = (firstEntry + i) * 3;
outportb(0x3C9, pal[idx + 0] >> 2);
outportb(0x3C9, pal[idx + 1] >> 2);
outportb(0x3C9, pal[idx + 2] >> 2);
}
}
// ============================================================
// platformVideoShutdown
// ============================================================
void platformVideoShutdown(DisplayT *d) {
// Restore text mode (mode 3)
__dpmi_regs r;
memset(&r, 0, sizeof(r));
r.x.ax = 0x0003;
__dpmi_int(0x10, &r);
if (d->backBuf) {
free(d->backBuf);
d->backBuf = NULL;
}
if (d->palette) {
free(d->palette);
d->palette = NULL;
}
d->lfb = NULL;
__djgpp_nearptr_disable();
}
// ============================================================
// platformYield
// ============================================================
void platformYield(void) {
__dpmi_yield();
}
// ============================================================
// setVesaMode
// ============================================================
static int32_t setVesaMode(uint16_t mode) {
__dpmi_regs r;
memset(&r, 0, sizeof(r));
r.x.ax = 0x4F02;
r.x.bx = mode | 0x4000; // bit 14 = use LFB
__dpmi_int(0x10, &r);
if (r.x.ax != 0x004F) {
fprintf(stderr, "VBE: Failed to set mode 0x%04X (AX=0x%04X)\n", mode, r.x.ax);
return -1;
}
return 0;
}