// dvxPlatformDos.c — DOS/DJGPP platform implementation for DVX 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 #include #include #include #include #include #include #include #include #include // ============================================================ // 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); void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void *userData), void *userData); 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; } // ============================================================ // platformVideoEnumModes // ============================================================ void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void *userData), void *userData) { __dpmi_regs r; memset(&r, 0, sizeof(r)); r.x.ax = 0x4F00; r.x.es = __tb >> 4; r.x.di = __tb & 0x0F; // Write "VBE2" signature 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'); __dpmi_int(0x10, &r); if (r.x.ax != 0x004F) { return; } uint16_t modeListOff = _farpeekw(_dos_ds, __tb + 14); uint16_t modeListSeg = _farpeekw(_dos_ds, __tb + 16); uint32_t modeListAddr = ((uint32_t)modeListSeg << 4) + modeListOff; for (int32_t i = 0; i < 256; i++) { uint16_t mode = _farpeekw(_dos_ds, modeListAddr + i * 2); if (mode == 0xFFFF) { break; } 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) { continue; } uint16_t attr = _farpeekw(_dos_ds, __tb + 0); // Only report LFB-capable graphics modes if (!(attr & 0x0080) || !(attr & 0x0010)) { continue; } int32_t w = _farpeekw(_dos_ds, __tb + 18); int32_t h = _farpeekw(_dos_ds, __tb + 20); int32_t bpp = _farpeekb(_dos_ds, __tb + 25); cb(w, h, bpp, userData); } } // ============================================================ // 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; }