mirror of
https://github.com/libsdl-org/SDL.git
synced 2026-06-24 12:05:43 +00:00
970 lines
40 KiB
C
970 lines
40 KiB
C
/*
|
|
Simple DirectMedia Layer
|
|
Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
|
|
|
|
This software is provided 'as-is', without any express or implied
|
|
warranty. In no event will the authors be held liable for any damages
|
|
arising from the use of this software.
|
|
|
|
Permission is granted to anyone to use this software for any purpose,
|
|
including commercial applications, and to alter it and redistribute it
|
|
freely, subject to the following restrictions:
|
|
|
|
1. The origin of this software must not be misrepresented; you must not
|
|
claim that you wrote the original software. If you use this software
|
|
in a product, an acknowledgment in the product documentation would be
|
|
appreciated but is not required.
|
|
2. Altered source versions must be plainly marked as such, and must not be
|
|
misrepresented as being the original software.
|
|
3. This notice may not be removed or altered from any source distribution.
|
|
*/
|
|
#include "SDL_internal.h"
|
|
|
|
#ifdef SDL_VIDEO_DRIVER_DOSVESA
|
|
|
|
#include "../../SDL_properties_c.h"
|
|
#include "../../events/SDL_mouse_c.h"
|
|
#include "../SDL_sysvideo.h"
|
|
#include "SDL_dosframebuffer_c.h"
|
|
#include "SDL_dosmodes.h"
|
|
#include "SDL_dosmouse.h"
|
|
#include "SDL_dosvideo.h"
|
|
|
|
#include <pc.h> // for inportb, outportb
|
|
#include <sys/movedata.h> // for dosmemput (banked framebuffer writes)
|
|
|
|
// note that DOS_SURFACE's value is the same string that the dummy driver uses.
|
|
#define DOS_SURFACE "SDL.internal.window.surface"
|
|
|
|
// Consolidated framebuffer state (DOS has only one window)
|
|
typedef struct DOSFramebufferState
|
|
{
|
|
SDL_Surface *surface; // system-RAM surface (app renders here)
|
|
SDL_Surface *lfb_surface; // LFB surface (pixels in VRAM), NULL for direct-FB path
|
|
bool direct_fb; // true when the fast direct-FB hint path is active
|
|
bool use_dosmemput; // true = dosmemput (banked), false = nearptr (LFB)
|
|
int vram_pitch;
|
|
int vram_w;
|
|
int vram_h;
|
|
int bpp; // bytes per pixel
|
|
|
|
// dosmemput path state (use_dosmemput)
|
|
Uint32 vram_phys; // base address for dosmemput (e.g. 0xA0000)
|
|
bool banked_multibank; // true if fb doesn't fit in one bank window
|
|
Uint32 win_gran_bytes; // bank granularity in bytes
|
|
Uint32 win_size_bytes; // bank window size in bytes
|
|
Uint32 win_base; // window base address (segment << 4)
|
|
Uint32 win_func_ptr; // real-mode far pointer to bank-switch function
|
|
|
|
// nearptr path state (use_dosmemput == false)
|
|
Uint8 *vram_ptr; // nearptr into VRAM (LFB)
|
|
|
|
// Cached per-frame values (set at create time, avoid repeated lookups)
|
|
Uint8 *pixels; // == surface->pixels (stable after create)
|
|
int src_pitch; // == surface->pitch
|
|
Uint32 fb_size; // total framebuffer bytes (pitch * h) for full-surface fast path
|
|
bool pitches_match; // src_pitch == vram_pitch (enables single-copy fast path)
|
|
} DOSFramebufferState;
|
|
|
|
static DOSFramebufferState fb_state;
|
|
|
|
// Convert cursor to the current format.
|
|
static SDL_Surface *GetConvertedCursorSurface(SDL_CursorData *curdata, SDL_Surface *dst)
|
|
{
|
|
SDL_Palette *pal = SDL_GetSurfacePalette(dst);
|
|
if (!pal) {
|
|
return NULL;
|
|
}
|
|
|
|
Uint32 pal_version = pal ? pal->version : 0;
|
|
|
|
if (curdata->converted_surface &&
|
|
curdata->converted_surface->format == dst->format &&
|
|
curdata->converted_palette_version == pal_version) {
|
|
return curdata->converted_surface;
|
|
}
|
|
|
|
SDL_DestroySurface(curdata->converted_surface);
|
|
curdata->converted_surface = NULL;
|
|
|
|
SDL_Surface *src = curdata->surface;
|
|
SDL_assert(src->format == SDL_PIXELFORMAT_ARGB8888);
|
|
|
|
int w = src->w;
|
|
int h = src->h;
|
|
SDL_Surface *conv = SDL_CreateSurfaceUninitialized(w, h, dst->format);
|
|
if (!conv) {
|
|
return NULL;
|
|
}
|
|
|
|
// Copy the destination palette.
|
|
SDL_Palette *conv_pal = SDL_CreateSurfacePalette(conv);
|
|
if (conv_pal) {
|
|
SDL_SetPaletteColors(conv_pal, pal->colors, 0, pal->ncolors);
|
|
}
|
|
|
|
// Track which palette indices are used by opaque pixels.
|
|
bool used[256];
|
|
SDL_zeroa(used);
|
|
|
|
// First pass: blit with BLENDMODE_NONE to get raw color-matched indices.
|
|
SDL_SetSurfaceBlendMode(src, SDL_BLENDMODE_NONE);
|
|
SDL_BlitSurface(src, NULL, conv, NULL);
|
|
SDL_SetSurfaceBlendMode(src, SDL_BLENDMODE_BLEND);
|
|
|
|
// Mark which indices are used by non-transparent source pixels.
|
|
for (int y = 0; y < h; y++) {
|
|
const Uint32 *srcrow = (const Uint32 *)((const Uint8 *)src->pixels + y * src->pitch);
|
|
const Uint8 *convrow = (const Uint8 *)conv->pixels + y * conv->pitch;
|
|
for (int x = 0; x < w; x++) {
|
|
Uint8 srcA = (Uint8)(srcrow[x] >> 24);
|
|
if (srcA > 0) {
|
|
used[convrow[x]] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find an unused index for the colorkey.
|
|
Uint32 colorkey = 0;
|
|
for (int i = 0; i < 256; i++) {
|
|
if (!used[i]) {
|
|
colorkey = (Uint32)i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Second pass: set transparent pixels to the colorkey index.
|
|
for (int y = 0; y < h; y++) {
|
|
const Uint32 *srcrow = (const Uint32 *)((const Uint8 *)src->pixels + y * src->pitch);
|
|
Uint8 *convrow = (Uint8 *)conv->pixels + y * conv->pitch;
|
|
for (int x = 0; x < w; x++) {
|
|
Uint8 srcA = (Uint8)(srcrow[x] >> 24);
|
|
if (srcA == 0) {
|
|
convrow[x] = (Uint8)colorkey;
|
|
}
|
|
}
|
|
}
|
|
|
|
SDL_SetSurfaceColorKey(conv, true, colorkey);
|
|
SDL_SetSurfaceBlendMode(conv, SDL_BLENDMODE_NONE);
|
|
|
|
curdata->converted_surface = conv;
|
|
curdata->converted_palette_version = pal_version;
|
|
return conv;
|
|
}
|
|
|
|
// Invalidation (called from SetDisplayMode before freeing DPMI mapping)
|
|
void DOSVESA_InvalidateCachedFramebuffer(void)
|
|
{
|
|
fb_state.direct_fb = false;
|
|
fb_state.vram_ptr = NULL;
|
|
fb_state.vram_phys = 0;
|
|
fb_state.use_dosmemput = false;
|
|
// Clear the LFB surface pointer so UpdateWindowFramebuffer won't try
|
|
// to blit into VRAM after the DPMI mapping has been freed.
|
|
fb_state.lfb_surface = NULL;
|
|
}
|
|
|
|
// Create a system-RAM surface (with a blank palette if INDEX8 and update the VGA DAC).
|
|
static SDL_Surface *CreateSystemSurface(SDL_VideoData *data, int w, int h, SDL_PixelFormat surface_format)
|
|
{
|
|
SDL_Surface *surface = SDL_CreateSurfaceZeroed(w, h, surface_format);
|
|
if (!surface) {
|
|
return NULL;
|
|
}
|
|
|
|
// For 8-bit indexed modes, both surfaces need palettes.
|
|
// Share the same palette object so palette updates propagate to both.
|
|
if (surface_format == SDL_PIXELFORMAT_INDEX8) {
|
|
SDL_Palette *palette = SDL_CreateSurfacePalette(surface);
|
|
if (palette) {
|
|
// Initialize palette to all-black so that transitions start
|
|
// from black instead of flashing uninitialized (white) colors.
|
|
SDL_Color black[256];
|
|
SDL_zeroa(black);
|
|
for (int i = 0; i < 256; i++) {
|
|
black[i].a = SDL_ALPHA_OPAQUE;
|
|
}
|
|
SDL_SetPaletteColors(palette, black, 0, 256);
|
|
}
|
|
data->palette_version = 0; // force DAC update on first present
|
|
|
|
// Also program the VGA DAC to all-black right now, so no flash
|
|
// of stale/white palette colors before the first present.
|
|
outportb(VGA_DAC_WRITE_INDEX, 0);
|
|
for (int i = 0; i < 256; i++) {
|
|
outportb(VGA_DAC_DATA, 0);
|
|
outportb(VGA_DAC_DATA, 0);
|
|
outportb(VGA_DAC_DATA, 0);
|
|
}
|
|
}
|
|
|
|
return surface;
|
|
}
|
|
|
|
// Normal (non-direct-FB) path: system-RAM surface with optional LFB blit,
|
|
// cursor compositing, palette sync, vsync, and page-flipping.
|
|
static bool CreateNormalFramebuffer(SDL_VideoDevice *device, SDL_Window *window,
|
|
SDL_PixelFormat *format, void **pixels, int *pitch)
|
|
{
|
|
SDL_VideoData *data = device->internal;
|
|
const SDL_DisplayMode *mode = &data->current_mode;
|
|
const SDL_DisplayModeData *mdata = mode->internal;
|
|
const SDL_PixelFormat surface_format = mode->format;
|
|
int w, h;
|
|
|
|
SDL_GetWindowSizeInPixels(window, &w, &h);
|
|
|
|
SDL_Surface *surface = CreateSystemSurface(data, w, h, surface_format);
|
|
if (!surface) {
|
|
return false;
|
|
}
|
|
|
|
SDL_Surface *lfb_surface = NULL;
|
|
|
|
if (!data->banked_mode) {
|
|
// LFB path: Make a surface that uses video memory directly, to let SDL do the blitting for us.
|
|
// Point the LFB surface at the back page for tear-free double-buffering.
|
|
int back_page = data->page_flip_available ? (1 - data->current_page) : 0;
|
|
void *lfb_pixels = (Uint8 *)DOS_PhysicalToLinear(data->mapping.address) + data->page_offset[back_page];
|
|
lfb_surface = SDL_CreateSurfaceFrom(mode->w, mode->h, surface_format, lfb_pixels, mdata->pitch);
|
|
if (!lfb_surface) {
|
|
SDL_DestroySurface(surface);
|
|
return false;
|
|
}
|
|
fb_state.lfb_surface = lfb_surface;
|
|
|
|
// Share the palette so updates propagate to both surfaces.
|
|
SDL_Palette *src_palette = SDL_GetSurfacePalette(surface);
|
|
if (src_palette) {
|
|
SDL_SetSurfacePalette(lfb_surface, src_palette);
|
|
}
|
|
}
|
|
|
|
// clear the framebuffer completely, in case another window at a larger size was using this before us.
|
|
if (lfb_surface) {
|
|
SDL_ClearSurface(lfb_surface, 0.0f, 0.0f, 0.0f, 0.0f);
|
|
}
|
|
// (For banked mode, the framebuffer was already zeroed in DOSVESA_SetDisplayMode.)
|
|
|
|
// Save the info and return!
|
|
fb_state.surface = surface;
|
|
SDL_SetSurfaceProperty(SDL_GetWindowProperties(window), DOS_SURFACE, surface);
|
|
|
|
*format = surface_format;
|
|
*pixels = surface->pixels;
|
|
*pitch = surface->pitch;
|
|
return true;
|
|
}
|
|
|
|
bool DOSVESA_CreateWindowFramebuffer(SDL_VideoDevice *device, SDL_Window *window, SDL_PixelFormat *format, void **pixels, int *pitch)
|
|
{
|
|
SDL_zero(fb_state);
|
|
|
|
SDL_VideoData *data = device->internal;
|
|
const SDL_DisplayMode *mode = &data->current_mode;
|
|
const SDL_DisplayModeData *mdata = mode->internal;
|
|
const SDL_PixelFormat surface_format = mode->format;
|
|
int w, h;
|
|
|
|
// writing to video RAM shows up as the screen refreshes, done or not, and it might have a weird pitch, so give the app a buffer of system RAM.
|
|
SDL_GetWindowSizeInPixels(window, &w, &h);
|
|
|
|
// Try to set up fast path where UpdateWindowFramebuffer copies system-RAM directly to VRAM.
|
|
if (SDL_GetHintBoolean(SDL_HINT_DOS_ALLOW_DIRECT_FRAMEBUFFER, false)) {
|
|
int vram_pitch = mdata->pitch;
|
|
|
|
// Check if we have any usable VRAM access path (banked or LFB).
|
|
bool have_vram = false;
|
|
|
|
if (mdata->win_a_segment && mdata->win_size > 0 &&
|
|
(mdata->win_a_attributes & VBE_WINATTR_USABLE) == VBE_WINATTR_USABLE) {
|
|
// Banked window available. Use dosmemput path (preferred).
|
|
have_vram = true;
|
|
} else if (!data->banked_mode && data->mapping.size) {
|
|
// LFB only, use nearptr fallback.
|
|
have_vram = true;
|
|
}
|
|
|
|
if (have_vram) {
|
|
SDL_Surface *surface = CreateSystemSurface(data, w, h, surface_format);
|
|
if (!surface) {
|
|
return false;
|
|
}
|
|
|
|
SDL_SetSurfaceProperty(SDL_GetWindowProperties(window), DOS_SURFACE, surface);
|
|
|
|
fb_state.surface = surface;
|
|
fb_state.direct_fb = true;
|
|
fb_state.vram_pitch = vram_pitch;
|
|
fb_state.vram_w = mdata->w;
|
|
fb_state.vram_h = mdata->h;
|
|
fb_state.bpp = SDL_BYTESPERPIXEL(surface_format);
|
|
fb_state.pixels = (Uint8 *)surface->pixels;
|
|
fb_state.src_pitch = surface->pitch;
|
|
fb_state.fb_size = (Uint32)vram_pitch * mdata->h;
|
|
fb_state.pitches_match = (surface->pitch == vram_pitch);
|
|
|
|
// Prefer dosmemput via the banked VGA window (0xA0000). Real
|
|
// hardware testing shows dosmemput is significantly faster
|
|
// than nearptr writes to DPMI-mapped LFB, even with bank
|
|
// switching overhead at higher resolutions.
|
|
//
|
|
// The banked window always maps to page 0, so page flipping is
|
|
// not possible through this path. We accept tearing: the app
|
|
// opted into the direct-FB hint for maximum throughput, and
|
|
// games like Quake never used page flipping for their software
|
|
// renderers anyway.
|
|
if (mdata->win_a_segment && mdata->win_size > 0 &&
|
|
(mdata->win_a_attributes & VBE_WINATTR_USABLE) == VBE_WINATTR_USABLE) {
|
|
Uint32 win_bytes = (Uint32)mdata->win_size * 1024;
|
|
fb_state.use_dosmemput = true;
|
|
fb_state.vram_ptr = NULL;
|
|
fb_state.vram_phys = (Uint32)mdata->win_a_segment << 4;
|
|
fb_state.win_base = fb_state.vram_phys;
|
|
fb_state.win_gran_bytes = (Uint32)mdata->win_granularity * 1024;
|
|
fb_state.win_size_bytes = win_bytes;
|
|
fb_state.win_func_ptr = mdata->win_func_ptr;
|
|
fb_state.banked_multibank = (fb_state.fb_size > win_bytes);
|
|
} else if (!data->banked_mode && data->mapping.size) {
|
|
// Fallback: nearptr to LFB.
|
|
int back_page = data->page_flip_available ? (1 - data->current_page) : 0;
|
|
fb_state.vram_ptr = (Uint8 *)DOS_PhysicalToLinear(data->mapping.address) + data->page_offset[back_page];
|
|
fb_state.vram_phys = 0;
|
|
fb_state.use_dosmemput = false;
|
|
fb_state.banked_multibank = false;
|
|
} else {
|
|
// No usable VRAM path. Shouldn't happen, but bail out.
|
|
SDL_DestroySurface(surface);
|
|
return CreateNormalFramebuffer(device, window, format, pixels, pitch);
|
|
}
|
|
*format = surface_format;
|
|
*pixels = surface->pixels;
|
|
*pitch = surface->pitch;
|
|
return true;
|
|
}
|
|
// else: couldn't get a direct pointer, fall through to normal path.
|
|
}
|
|
|
|
return CreateNormalFramebuffer(device, window, format, pixels, pitch);
|
|
}
|
|
|
|
bool DOSVESA_SetWindowFramebufferVSync(SDL_VideoDevice *device, SDL_Window *window, int vsync)
|
|
{
|
|
if (vsync < 0) {
|
|
return SDL_SetError("Unsupported vsync type");
|
|
}
|
|
SDL_WindowData *data = window->internal;
|
|
data->framebuffer_vsync = vsync;
|
|
return true;
|
|
}
|
|
|
|
bool DOSVESA_GetWindowFramebufferVSync(SDL_VideoDevice *device, SDL_Window *window, int *vsync)
|
|
{
|
|
if (vsync) {
|
|
SDL_WindowData *data = window->internal;
|
|
*vsync = data->framebuffer_vsync;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Switch the VGA bank window if needed. Returns without doing anything
|
|
// if the requested bank is already active.
|
|
SDL_FORCE_INLINE void SwitchBank(int bank, int *current_bank, Uint32 win_func_ptr)
|
|
{
|
|
if (bank == *current_bank) {
|
|
return;
|
|
}
|
|
__dpmi_regs regs;
|
|
SDL_zero(regs);
|
|
regs.x.bx = 0; // Window A
|
|
regs.x.dx = (Uint16)bank;
|
|
if (win_func_ptr) {
|
|
regs.x.cs = (Uint16)(win_func_ptr >> 16);
|
|
regs.x.ip = (Uint16)(win_func_ptr & 0xFFFF);
|
|
__dpmi_simulate_real_mode_procedure_retf(®s);
|
|
} else {
|
|
regs.x.ax = 0x4F05;
|
|
__dpmi_int(0x10, ®s);
|
|
}
|
|
*current_bank = bank;
|
|
}
|
|
|
|
// Copy a contiguous byte range from system RAM to VRAM through the banked
|
|
// window, switching banks as needed.
|
|
SDL_FORCE_INLINE void BankedDosmemput(const Uint8 *src, Uint32 total_bytes, Uint32 dst_offset,
|
|
Uint32 win_gran_bytes, Uint32 win_size_bytes,
|
|
Uint32 win_base, Uint32 win_func_ptr,
|
|
int *current_bank)
|
|
{
|
|
Uint32 src_off = 0;
|
|
while (total_bytes > 0) {
|
|
int bank = (int)(dst_offset / win_gran_bytes);
|
|
Uint32 off_in_win = dst_offset % win_gran_bytes;
|
|
Uint32 avail = win_size_bytes - off_in_win;
|
|
Uint32 n = total_bytes;
|
|
if (n > avail) {
|
|
n = avail;
|
|
}
|
|
SwitchBank(bank, current_bank, win_func_ptr);
|
|
dosmemput(src + src_off, n, win_base + off_in_win);
|
|
src_off += n;
|
|
dst_offset += n;
|
|
total_bytes -= n;
|
|
}
|
|
}
|
|
|
|
// Bank-switched copy of a rectangular region from a system RAM surface to the
|
|
// VGA banked window. `src_rect` is in source-surface coordinates; `dst_x` and
|
|
// `dst_y` give the top-left of the *surface's* position on screen (the
|
|
// centering offset). The routine handles bank boundaries correctly even when a
|
|
// scanline spans two banks.
|
|
//
|
|
// `current_bank` is an in/out parameter so that consecutive calls (one per dirty
|
|
// rect) can avoid redundant bank switches when rects happen to fall in the same
|
|
// bank. Initialise it to -1 before the first call.
|
|
static void BankedFramebufferCopyRect(const SDL_DisplayModeData *mdata,
|
|
const SDL_Surface *src,
|
|
const SDL_Rect *src_rect,
|
|
int dst_x, int dst_y,
|
|
Uint32 win_gran_bytes,
|
|
Uint32 win_size_bytes,
|
|
Uint32 win_base,
|
|
int *current_bank,
|
|
Uint32 win_func_ptr)
|
|
{
|
|
const Uint16 dst_pitch = mdata->pitch;
|
|
const int bytes_per_pixel = SDL_BYTESPERPIXEL(src->format);
|
|
const int row_bytes = src_rect->w * bytes_per_pixel;
|
|
|
|
// Fast path: if the source row width matches src pitch AND the destination
|
|
// row width matches dst pitch, the data is contiguous in both source and
|
|
// destination — we can copy it as one flat block, minimizing dosmemput calls.
|
|
if (row_bytes == src->pitch && row_bytes == dst_pitch) {
|
|
const Uint8 *src_data = (const Uint8 *)src->pixels + src_rect->y * src->pitch + src_rect->x * bytes_per_pixel;
|
|
Uint32 dst_offset = (Uint32)(dst_y + src_rect->y) * dst_pitch + (Uint32)(dst_x + src_rect->x) * bytes_per_pixel;
|
|
BankedDosmemput(src_data, (Uint32)(row_bytes * src_rect->h), dst_offset,
|
|
win_gran_bytes, win_size_bytes, win_base, win_func_ptr, current_bank);
|
|
return;
|
|
}
|
|
|
|
for (int y = 0; y < src_rect->h; y++) {
|
|
const Uint8 *src_row = (const Uint8 *)src->pixels + (src_rect->y + y) * src->pitch + src_rect->x * bytes_per_pixel;
|
|
Uint32 dst_offset = (Uint32)(dst_y + src_rect->y + y) * dst_pitch + (Uint32)(dst_x + src_rect->x) * bytes_per_pixel;
|
|
BankedDosmemput(src_row, (Uint32)row_bytes, dst_offset,
|
|
win_gran_bytes, win_size_bytes, win_base, win_func_ptr, current_bank);
|
|
}
|
|
}
|
|
|
|
static void WaitForVBlank(void)
|
|
{
|
|
while (inportb(VGA_STATUS_PORT) & VGA_STATUS_VBLANK) {
|
|
SDL_CPUPauseInstruction();
|
|
}
|
|
while (!(inportb(VGA_STATUS_PORT) & VGA_STATUS_VBLANK)) {
|
|
SDL_CPUPauseInstruction();
|
|
}
|
|
}
|
|
|
|
static void ProgramVGADAC(SDL_Palette *palette)
|
|
{
|
|
outportb(VGA_DAC_WRITE_INDEX, 0);
|
|
for (int i = 0; i < palette->ncolors && i < 256; i++) {
|
|
outportb(VGA_DAC_DATA, palette->colors[i].r >> 2);
|
|
outportb(VGA_DAC_DATA, palette->colors[i].g >> 2);
|
|
outportb(VGA_DAC_DATA, palette->colors[i].b >> 2);
|
|
}
|
|
}
|
|
|
|
bool DOSVESA_UpdateWindowFramebuffer(SDL_VideoDevice *device, SDL_Window *window, const SDL_Rect *rects, int numrects)
|
|
{
|
|
SDL_VideoData *vdata = device->internal;
|
|
|
|
// ===================================================================
|
|
// Direct-FB fast path: copy system-RAM to VRAM, program palette,
|
|
// optionally page-flip. No cursor compositing, no SDL_BlitSurface.
|
|
//
|
|
// This path prefers banked dosmemput (through the VGA window at
|
|
// 0xA0000) over nearptr writes to the DPMI-mapped LFB. Real
|
|
// hardware testing on multiple NVIDIA cards showed dosmemput is
|
|
// significantly faster, even with bank-switching overhead at
|
|
// higher resolutions. The nearptr LFB path is only used as a
|
|
// fallback if no usable banked window is available.
|
|
// ===================================================================
|
|
if (fb_state.direct_fb && (fb_state.vram_ptr || fb_state.vram_phys)) {
|
|
|
|
// Palette update.
|
|
if (window->surface) {
|
|
SDL_Palette *pal = window->surface->palette;
|
|
if (pal && pal->version != vdata->palette_version) {
|
|
vdata->palette_version = pal->version;
|
|
ProgramVGADAC(pal);
|
|
}
|
|
}
|
|
|
|
if (fb_state.use_dosmemput) {
|
|
// dosmemput path (banked window, possibly multi-bank)
|
|
const int src_pitch = fb_state.src_pitch;
|
|
const int dst_pitch = fb_state.vram_pitch;
|
|
const Uint8 *pixels = fb_state.pixels;
|
|
|
|
if (!fb_state.banked_multibank) {
|
|
// Single-bank: entire FB fits in one window (e.g. mode 13h).
|
|
// Full-surface fast path when pitches match.
|
|
if (fb_state.pitches_match) {
|
|
dosmemput(pixels, fb_state.fb_size, fb_state.vram_phys);
|
|
} else {
|
|
for (int i = 0; i < numrects; i++) {
|
|
int rx = rects[i].x, ry = rects[i].y;
|
|
int rw = rects[i].w, rh = rects[i].h;
|
|
if (rx < 0) {
|
|
rw += rx;
|
|
rx = 0;
|
|
}
|
|
if (ry < 0) {
|
|
rh += ry;
|
|
ry = 0;
|
|
}
|
|
if (rx + rw > fb_state.vram_w) {
|
|
rw = fb_state.vram_w - rx;
|
|
}
|
|
if (ry + rh > fb_state.vram_h) {
|
|
rh = fb_state.vram_h - ry;
|
|
}
|
|
if (rw <= 0 || rh <= 0) {
|
|
continue;
|
|
}
|
|
const int bx = rx * fb_state.bpp;
|
|
const int bw = rw * fb_state.bpp;
|
|
const Uint8 *sp = pixels + ry * src_pitch + bx;
|
|
Uint32 dst_off = fb_state.vram_phys + ry * dst_pitch + bx;
|
|
for (int row = 0; row < rh; row++) {
|
|
dosmemput(sp, bw, dst_off);
|
|
sp += src_pitch;
|
|
dst_off += dst_pitch;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Multi-bank: bank-switch as needed.
|
|
const Uint32 gran = fb_state.win_gran_bytes;
|
|
const Uint32 wsize = fb_state.win_size_bytes;
|
|
const Uint32 wbase = fb_state.win_base;
|
|
const Uint32 wfunc = fb_state.win_func_ptr;
|
|
int current_bank = -1;
|
|
|
|
if (fb_state.pitches_match) {
|
|
// Contiguous: stream the entire framebuffer through banks.
|
|
BankedDosmemput(pixels, fb_state.fb_size, 0,
|
|
gran, wsize, wbase, wfunc, ¤t_bank);
|
|
} else {
|
|
// Per-rect with bank switching.
|
|
for (int i = 0; i < numrects; i++) {
|
|
int rx = rects[i].x, ry = rects[i].y;
|
|
int rw = rects[i].w, rh = rects[i].h;
|
|
if (rx < 0) {
|
|
rw += rx;
|
|
rx = 0;
|
|
}
|
|
if (ry < 0) {
|
|
rh += ry;
|
|
ry = 0;
|
|
}
|
|
if (rx + rw > fb_state.vram_w) {
|
|
rw = fb_state.vram_w - rx;
|
|
}
|
|
if (ry + rh > fb_state.vram_h) {
|
|
rh = fb_state.vram_h - ry;
|
|
}
|
|
if (rw <= 0 || rh <= 0) {
|
|
continue;
|
|
}
|
|
const int bx = rx * fb_state.bpp;
|
|
const int bw = rw * fb_state.bpp;
|
|
|
|
for (int row = 0; row < rh; row++) {
|
|
const Uint8 *sp = pixels + (ry + row) * src_pitch + bx;
|
|
Uint32 dst_offset = (Uint32)(ry + row) * dst_pitch + bx;
|
|
BankedDosmemput(sp, (Uint32)bw, dst_offset,
|
|
gran, wsize, wbase, wfunc, ¤t_bank);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// nearptr path (LFB fallback)
|
|
if (fb_state.pitches_match) {
|
|
SDL_memcpy(fb_state.vram_ptr, fb_state.pixels, fb_state.fb_size);
|
|
} else {
|
|
const int bpp = fb_state.bpp;
|
|
const int src_pitch = fb_state.src_pitch;
|
|
const int dst_pitch = fb_state.vram_pitch;
|
|
const Uint8 *pixels = fb_state.pixels;
|
|
|
|
for (int i = 0; i < numrects; i++) {
|
|
int rx = rects[i].x, ry = rects[i].y;
|
|
int rw = rects[i].w, rh = rects[i].h;
|
|
if (rx < 0) {
|
|
rw += rx;
|
|
rx = 0;
|
|
}
|
|
if (ry < 0) {
|
|
rh += ry;
|
|
ry = 0;
|
|
}
|
|
if (rx + rw > fb_state.vram_w) {
|
|
rw = fb_state.vram_w - rx;
|
|
}
|
|
if (ry + rh > fb_state.vram_h) {
|
|
rh = fb_state.vram_h - ry;
|
|
}
|
|
if (rw <= 0 || rh <= 0) {
|
|
continue;
|
|
}
|
|
const int bw = rw * bpp;
|
|
const Uint8 *sp = pixels + ry * src_pitch + rx * bpp;
|
|
Uint8 *dp = fb_state.vram_ptr + ry * dst_pitch + rx * bpp;
|
|
for (int row = 0; row < rh; row++) {
|
|
SDL_memcpy(dp, sp, bw);
|
|
sp += src_pitch;
|
|
dp += dst_pitch;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Page flipping is only used with the nearptr LFB path. The banked
|
|
// dosmemput path always writes to page 0 (the visible page) and
|
|
// accepts tearing, avoiding a BIOS call.
|
|
if (!fb_state.use_dosmemput && vdata->page_flip_available) {
|
|
const SDL_DisplayModeData *mdata = vdata->current_mode.internal;
|
|
int back_page = 1 - vdata->current_page;
|
|
Uint16 first_scanline = (Uint16)(vdata->page_offset[back_page] / mdata->pitch);
|
|
|
|
__dpmi_regs regs;
|
|
SDL_zero(regs);
|
|
regs.x.ax = 0x4F07;
|
|
regs.x.bx = 0x0080;
|
|
regs.x.cx = 0;
|
|
regs.x.dx = first_scanline;
|
|
__dpmi_int(0x10, ®s);
|
|
|
|
vdata->current_page = back_page;
|
|
|
|
int new_back = 1 - vdata->current_page;
|
|
fb_state.vram_ptr = (Uint8 *)DOS_PhysicalToLinear(vdata->mapping.address) + vdata->page_offset[new_back];
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// ===================================================================
|
|
// Normal path: system-RAM to LFB surface (or banked copy)
|
|
// with cursor compositing, palette sync, vsync, and page-flipping.
|
|
// ===================================================================
|
|
|
|
SDL_Surface *src = fb_state.surface;
|
|
if (!src) {
|
|
src = (SDL_Surface *)SDL_GetPointerProperty(SDL_GetWindowProperties(window), DOS_SURFACE, NULL);
|
|
}
|
|
if (!src) {
|
|
return SDL_SetError("Couldn't find DOS surface for window");
|
|
}
|
|
|
|
const SDL_DisplayModeData *mdata = vdata->current_mode.internal;
|
|
SDL_WindowData *windata = window->internal;
|
|
|
|
// For 8-bit indexed modes, sync palette data between surfaces so the
|
|
// blit uses the correct color mapping. The actual VGA DAC programming
|
|
// is deferred until vertical blanking to avoid visible palette flicker.
|
|
SDL_Palette *dac_palette = NULL;
|
|
bool dac_needs_update = false;
|
|
|
|
if (src->format == SDL_PIXELFORMAT_INDEX8) {
|
|
SDL_Palette *win_palette = window->surface ? SDL_GetSurfacePalette(window->surface) : NULL;
|
|
SDL_Palette *src_palette = SDL_GetSurfacePalette(src);
|
|
|
|
// Sync: if the app set colors on the window surface palette,
|
|
// copy them to the internal surface so the blit works correctly.
|
|
if (win_palette && src_palette && win_palette != src_palette &&
|
|
win_palette->version != src_palette->version) {
|
|
SDL_SetPaletteColors(src_palette, win_palette->colors, 0,
|
|
SDL_min(win_palette->ncolors, src_palette->ncolors));
|
|
|
|
if (!vdata->banked_mode && fb_state.lfb_surface) {
|
|
// Also update the LFB surface palette for correct blitting
|
|
SDL_Palette *dst_palette = SDL_GetSurfacePalette(fb_state.lfb_surface);
|
|
if (dst_palette && dst_palette != src_palette) {
|
|
SDL_SetPaletteColors(dst_palette, win_palette->colors, 0,
|
|
SDL_min(win_palette->ncolors, dst_palette->ncolors));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determine whether the VGA DAC needs reprogramming.
|
|
dac_palette = win_palette ? win_palette : src_palette;
|
|
if (dac_palette && dac_palette->version != vdata->palette_version) {
|
|
dac_needs_update = true;
|
|
}
|
|
}
|
|
|
|
if (vdata->banked_mode) {
|
|
// --- Banked framebuffer path (dirty-rect aware) ---
|
|
// We composite the cursor onto the source surface (in system RAM),
|
|
// then bank-copy only the dirty rectangles to the VGA window. We
|
|
// need to undo the cursor composite afterwards so the app's surface
|
|
// isn't permanently modified.
|
|
|
|
const int dst_x = ((int)mdata->w - src->w) / 2;
|
|
const int dst_y = ((int)mdata->h - src->h) / 2;
|
|
|
|
SDL_Mouse *mouse = SDL_GetMouse();
|
|
SDL_Surface *cursor = NULL;
|
|
SDL_Rect cursorrect;
|
|
SDL_Rect cursor_clipped; // cursorrect clipped to src bounds
|
|
SDL_Surface *cursor_save = NULL;
|
|
bool have_cursor_rect = false;
|
|
|
|
SDL_Cursor *cur = mouse ? mouse->cur_cursor : NULL;
|
|
if (cur && cur->animation) {
|
|
cur = cur->animation->frames[cur->animation->current_frame];
|
|
}
|
|
if (mouse && mouse->internal && !mouse->relative_mode && mouse->cursor_visible && cur && cur->internal) {
|
|
cursor = cur->internal->surface;
|
|
if (cursor) {
|
|
cursorrect.x = SDL_clamp((int)mouse->x, 0, window->w) - cur->internal->hot_x;
|
|
cursorrect.y = SDL_clamp((int)mouse->y, 0, window->h) - cur->internal->hot_y;
|
|
cursorrect.w = cursor->w;
|
|
cursorrect.h = cursor->h;
|
|
|
|
// Clip cursor rect to src bounds for save/restore.
|
|
cursor_clipped = cursorrect;
|
|
if (cursor_clipped.x < 0) {
|
|
cursor_clipped.w += cursor_clipped.x;
|
|
cursor_clipped.x = 0;
|
|
}
|
|
if (cursor_clipped.y < 0) {
|
|
cursor_clipped.h += cursor_clipped.y;
|
|
cursor_clipped.y = 0;
|
|
}
|
|
if (cursor_clipped.x + cursor_clipped.w > src->w) {
|
|
cursor_clipped.w = src->w - cursor_clipped.x;
|
|
}
|
|
if (cursor_clipped.y + cursor_clipped.h > src->h) {
|
|
cursor_clipped.h = src->h - cursor_clipped.y;
|
|
}
|
|
|
|
if (cursor_clipped.w > 0 && cursor_clipped.h > 0) {
|
|
have_cursor_rect = true;
|
|
|
|
// Save the pixels under the cursor so we can restore them after the copy.
|
|
cursor_save = SDL_CreateSurfaceUninitialized(cursor_clipped.w, cursor_clipped.h, src->format);
|
|
if (cursor_save) {
|
|
if (src->format == SDL_PIXELFORMAT_INDEX8) {
|
|
SDL_Palette *sp = SDL_GetSurfacePalette(src);
|
|
if (sp) {
|
|
SDL_SetSurfacePalette(cursor_save, sp);
|
|
}
|
|
}
|
|
SDL_BlitSurface(src, &cursor_clipped, cursor_save, NULL);
|
|
}
|
|
}
|
|
|
|
// Composite cursor onto the source surface.
|
|
{
|
|
SDL_Surface *blit_cursor = GetConvertedCursorSurface(cur->internal, src);
|
|
SDL_BlitSurface(blit_cursor ? blit_cursor : cursor, NULL, src, &cursorrect);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wait for vsync before the copy to reduce tearing.
|
|
const int vsync_interval = windata->framebuffer_vsync;
|
|
if (vsync_interval > 0 || dac_needs_update) {
|
|
WaitForVBlank();
|
|
}
|
|
|
|
if (dac_needs_update) {
|
|
vdata->palette_version = dac_palette->version;
|
|
ProgramVGADAC(dac_palette);
|
|
}
|
|
|
|
// Bank-switched copy of only the dirty rectangles.
|
|
// Pre-compute constants shared across all rect copies.
|
|
const Uint32 win_gran_bytes = (Uint32)mdata->win_granularity * 1024;
|
|
const Uint32 win_size_bytes = (Uint32)mdata->win_size * 1024;
|
|
const Uint32 win_base = (Uint32)mdata->win_a_segment << 4;
|
|
int current_bank = -1;
|
|
|
|
// Track whether the cursor region was already covered by a dirty rect
|
|
// so we don't copy it twice.
|
|
bool cursor_covered = false;
|
|
|
|
for (int r = 0; r < numrects; r++) {
|
|
// Clip the dirty rect to the source surface bounds.
|
|
SDL_Rect rect = rects[r];
|
|
if (rect.x < 0) {
|
|
rect.w += rect.x;
|
|
rect.x = 0;
|
|
}
|
|
if (rect.y < 0) {
|
|
rect.h += rect.y;
|
|
rect.y = 0;
|
|
}
|
|
if (rect.x + rect.w > src->w) {
|
|
rect.w = src->w - rect.x;
|
|
}
|
|
if (rect.y + rect.h > src->h) {
|
|
rect.h = src->h - rect.y;
|
|
}
|
|
if (rect.w <= 0 || rect.h <= 0) {
|
|
continue;
|
|
}
|
|
|
|
// If the cursor is visible, check whether this rect fully covers it.
|
|
if (have_cursor_rect && !cursor_covered) {
|
|
if (rect.x <= cursor_clipped.x &&
|
|
rect.y <= cursor_clipped.y &&
|
|
rect.x + rect.w >= cursor_clipped.x + cursor_clipped.w &&
|
|
rect.y + rect.h >= cursor_clipped.y + cursor_clipped.h) {
|
|
cursor_covered = true;
|
|
}
|
|
}
|
|
|
|
BankedFramebufferCopyRect(mdata, src, &rect, dst_x, dst_y,
|
|
win_gran_bytes, win_size_bytes, win_base,
|
|
¤t_bank, mdata->win_func_ptr);
|
|
}
|
|
|
|
// If no dirty rect covered the cursor, copy the cursor region separately.
|
|
if (have_cursor_rect && !cursor_covered) {
|
|
BankedFramebufferCopyRect(mdata, src, &cursor_clipped, dst_x, dst_y,
|
|
win_gran_bytes, win_size_bytes, win_base,
|
|
¤t_bank, mdata->win_func_ptr);
|
|
}
|
|
|
|
// Restore the source surface pixels under the cursor.
|
|
if (cursor_save) {
|
|
SDL_Rect restore_rect = cursor_clipped;
|
|
SDL_BlitSurface(cursor_save, NULL, src, &restore_rect);
|
|
SDL_DestroySurface(cursor_save);
|
|
}
|
|
|
|
} else {
|
|
// --- LFB path ---
|
|
SDL_Surface *dst = fb_state.lfb_surface;
|
|
if (!dst) {
|
|
return SDL_SetError("Couldn't find VESA linear framebuffer surface for window");
|
|
}
|
|
|
|
const SDL_Rect dstrect = { (dst->w - src->w) / 2, (dst->h - src->h) / 2, src->w, src->h };
|
|
SDL_Mouse *mouse = SDL_GetMouse();
|
|
SDL_Surface *cursor = NULL;
|
|
SDL_Rect cursorrect;
|
|
|
|
SDL_Cursor *cur = mouse ? mouse->cur_cursor : NULL;
|
|
if (cur && cur->animation) {
|
|
cur = cur->animation->frames[cur->animation->current_frame];
|
|
}
|
|
if (mouse && mouse->internal && !mouse->relative_mode && mouse->cursor_visible && cur && cur->internal) {
|
|
cursor = cur->internal->surface;
|
|
if (cursor) {
|
|
cursorrect.x = dstrect.x + SDL_clamp((int)mouse->x, 0, window->w) - cur->internal->hot_x;
|
|
cursorrect.y = dstrect.y + SDL_clamp((int)mouse->y, 0, window->h) - cur->internal->hot_y;
|
|
}
|
|
}
|
|
|
|
// If both surfaces are INDEX8 and same size, skip the SDL blit
|
|
// machinery (which does per-pixel palette remapping even for
|
|
// identical palettes), copy directly to the LFB surface using
|
|
// SDL_memcpy.
|
|
if (src->format == SDL_PIXELFORMAT_INDEX8 &&
|
|
dst->format == SDL_PIXELFORMAT_INDEX8 &&
|
|
src->w == dstrect.w && src->h == dstrect.h) {
|
|
const Uint8 *sp = (const Uint8 *)src->pixels;
|
|
Uint8 *dp = (Uint8 *)dst->pixels + dstrect.y * dst->pitch + dstrect.x;
|
|
if (src->pitch == dstrect.w && dst->pitch == dstrect.w) {
|
|
SDL_memcpy(dp, sp, (size_t)dstrect.w * dstrect.h);
|
|
} else {
|
|
for (int row = 0; row < dstrect.h; row++) {
|
|
SDL_memcpy(dp, sp, dstrect.w);
|
|
sp += src->pitch;
|
|
dp += dst->pitch;
|
|
}
|
|
}
|
|
} else {
|
|
// Blit to the back page (or the only page, if no page-flipping)
|
|
if (!SDL_BlitSurface(src, NULL, dst, &dstrect)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (cursor) {
|
|
SDL_Surface *blit_cursor = GetConvertedCursorSurface(cur->internal, dst);
|
|
if (!SDL_BlitSurface(blit_cursor ? blit_cursor : cursor, NULL, dst, &cursorrect)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (vdata->page_flip_available) {
|
|
// Page-flip with optional vsync.
|
|
const int vsync_interval = windata->framebuffer_vsync;
|
|
int back_page = 1 - vdata->current_page;
|
|
Uint16 first_scanline = (Uint16)(vdata->page_offset[back_page] / mdata->pitch);
|
|
|
|
if (vsync_interval > 0 || dac_needs_update) {
|
|
// Wait for vblank so the flip and DAC update appear together.
|
|
WaitForVBlank();
|
|
}
|
|
|
|
if (dac_needs_update) {
|
|
vdata->palette_version = dac_palette->version;
|
|
ProgramVGADAC(dac_palette);
|
|
}
|
|
|
|
// Flip: make the back page (which we just drew to) the visible page.
|
|
// Always use subfunction 0x0080 (set display start, don't wait) —
|
|
// vsync is controlled by our manual vblank wait above.
|
|
__dpmi_regs regs;
|
|
SDL_zero(regs);
|
|
regs.x.ax = 0x4F07;
|
|
regs.x.bx = 0x0080;
|
|
regs.x.cx = 0; // first pixel in scan line
|
|
regs.x.dx = first_scanline;
|
|
__dpmi_int(0x10, ®s);
|
|
|
|
vdata->current_page = back_page;
|
|
|
|
// Update LFB surface to point at the new back page (the old front page)
|
|
int new_back = 1 - vdata->current_page;
|
|
dst->pixels = (Uint8 *)DOS_PhysicalToLinear(vdata->mapping.address) + vdata->page_offset[new_back];
|
|
} else {
|
|
// No page-flipping: wait for vsync, then update DAC atomically
|
|
const int vsync_interval = windata->framebuffer_vsync;
|
|
if (vsync_interval > 0 || dac_needs_update) {
|
|
WaitForVBlank();
|
|
}
|
|
|
|
if (dac_needs_update) {
|
|
vdata->palette_version = dac_palette->version;
|
|
ProgramVGADAC(dac_palette);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void DOSVESA_DestroyWindowFramebuffer(SDL_VideoDevice *device, SDL_Window *window)
|
|
{
|
|
// Note: we intentionally do NOT call SDL_ClearSurface on the LFB surface
|
|
// here. SetDisplayMode already blanks the framebuffer (both pages) when
|
|
// setting a new mode, and the LFB surface's pixels pointer may be stale
|
|
// if the DPMI mapping was freed before this function is called.
|
|
(void)device;
|
|
SDL_zero(fb_state);
|
|
SDL_ClearProperty(SDL_GetWindowProperties(window), DOS_SURFACE);
|
|
}
|
|
|
|
#endif // SDL_VIDEO_DRIVER_DOSVESA
|