Add double-buffering to avoid screen flashes with XShm

When using the X shared memory extension, the X server may start
reading the buffer at a time when the client is writing to it. This
can result in the next unfinished frame flashing onto the screen. The
canonical way to solve this is to wait for an event to come in
signaling that it's safe to continue, but that would add considerable
latency to rendering. Instead, we bet on the fact that it won't take
all that long, and so we double-buffer. We have two buffers shared
with the X server and swap them at the end of each frame. We only draw
to the one not being displayed. That way the X server should always
have time to read out a full finished frame.
This commit is contained in:
John Lorentzson 2025-04-26 14:44:58 +02:00
parent 719937c06f
commit 82e7864903

View file

@ -13,15 +13,22 @@
#include <X11/extensions/XShm.h> #include <X11/extensions/XShm.h>
static Window window; static Window window;
// Framebuffer is the image that the game gets to write to
static XImage* framebufferImage; static XImage* framebufferImage;
static unsigned char* framebufferPixels; static char *framebufferPixels;
static XShmSegmentInfo framebufferSegInfo;
// Display is the one X gets to read from, we swap them every frame
static XImage* displayImage;
static char *displayPixels;
static XShmSegmentInfo displaySegInfo;
static int framebufferPitch; static int framebufferPitch;
static char* xfriendlyFB; static char* xfriendlyFB;
static GC gc; static GC gc;
static int screenWidth; static int screenWidth;
static int screenHeight; static int screenHeight;
static bool useShmP = false; static bool useShmP = false;
static XShmSegmentInfo seginfo;
extern Display* xDisplay; extern Display* xDisplay;
extern Visual* xVisual; extern Visual* xVisual;
@ -45,22 +52,34 @@ void createFramebuffer() {
int depth = DefaultDepth(xDisplay, DefaultScreen(xDisplay)); int depth = DefaultDepth(xDisplay, DefaultScreen(xDisplay));
// Setting up the framebuffer // Setting up the framebuffer
xfriendlyFB = NULL; displayPixels = NULL;
if(useShmP) { if(useShmP) {
displayImage = XShmCreateImage(xDisplay, xVisual,
depth, ZPixmap, NULL,
&displaySegInfo, screenWidth, screenHeight);
displaySegInfo.shmid = shmget(IPC_PRIVATE,
displayImage->bytes_per_line * displayImage->height,
IPC_CREAT | 0777);
displaySegInfo.shmaddr = displayPixels = displayImage->data =
(char*)shmat(displaySegInfo.shmid, NULL, 0);
XShmAttach(xDisplay, &displaySegInfo);
framebufferImage = XShmCreateImage(xDisplay, xVisual, framebufferImage = XShmCreateImage(xDisplay, xVisual,
depth, ZPixmap, NULL, depth, ZPixmap, NULL,
&seginfo, screenWidth, screenHeight); &framebufferSegInfo, screenWidth, screenHeight);
seginfo.shmid = shmget(IPC_PRIVATE, framebufferSegInfo.shmid = shmget(IPC_PRIVATE,
framebufferImage->bytes_per_line * framebufferImage->height, framebufferImage->bytes_per_line * displayImage->height,
IPC_CREAT | 0777); IPC_CREAT | 0777);
seginfo.shmaddr = xfriendlyFB = framebufferImage->data = (char*)shmat(seginfo.shmid, NULL, 0); framebufferSegInfo.shmaddr = framebufferPixels = framebufferImage->data =
XShmAttach(xDisplay, &seginfo); (char*)shmat(framebufferSegInfo.shmid, NULL, 0);
XShmAttach(xDisplay, &framebufferSegInfo);
} else { } else {
xfriendlyFB = (char*)malloc(screenWidth * screenHeight * 4); displayPixels = (char*)malloc(screenWidth * screenHeight * 4);
framebufferImage = XCreateImage(xDisplay, xVisual, framebufferPixels = displayPixels;
displayImage = XCreateImage(xDisplay, xVisual,
depth, ZPixmap, 0, depth, ZPixmap, 0,
(char*)xfriendlyFB, screenWidth, screenHeight, (char*)displayPixels, screenWidth, screenHeight,
32, 0); 32, 0);
} }
} }
@ -91,12 +110,12 @@ bool WindowBackend_Software_CreateWindow(const char *window_title, size_t screen
XSetWMProtocols(xDisplay, window, &WM_DELETE_WINDOW, 1); XSetWMProtocols(xDisplay, window, &WM_DELETE_WINDOW, 1);
createFramebuffer(); createFramebuffer();
if(checkImageOK(framebufferImage) == false) { if(checkImageOK(displayImage) == false) {
Backend_PrintError("Something's off with the framebuffer.\n"); Backend_PrintError("Something's off with the framebuffer.\n");
return false; return false;
} }
if(framebufferImage == 0) { if(displayImage == 0) {
return false; return false;
} }
@ -108,16 +127,24 @@ unsigned char* WindowBackend_Software_GetFramebuffer(size_t *pitch)
{ {
*pitch = framebufferPitch; *pitch = framebufferPitch;
return (unsigned char*)xfriendlyFB; return (unsigned char*)framebufferPixels;
} }
void WindowBackend_Software_DestroyWindow(void) { void WindowBackend_Software_DestroyWindow(void) {
if(useShmP) { if(useShmP) {
XShmDetach(xDisplay, &seginfo); XShmDetach(xDisplay, &displaySegInfo);
XShmDetach(xDisplay, &framebufferSegInfo);
XDestroyImage(displayImage);
XDestroyImage(framebufferImage); XDestroyImage(framebufferImage);
shmdt(seginfo.shmaddr);
shmctl(seginfo.shmid, IPC_RMID, 0); shmdt(displaySegInfo.shmaddr);
shmctl(displaySegInfo.shmid, IPC_RMID, 0);
shmdt(framebufferSegInfo.shmaddr);
shmctl(framebufferSegInfo.shmid, IPC_RMID, 0);
} }
XDestroyWindow(xDisplay, window); XDestroyWindow(xDisplay, window);
} }
@ -129,22 +156,19 @@ void WindowBackend_Software_HandleWindowResize(size_t width, size_t height) {
} }
void WindowBackend_Software_Display(void) { void WindowBackend_Software_Display(void) {
// Convert Cave Story's framebuffer format to the Sun's
// This is the exact reverse of how it is on modern
// computers. Yup, it's endianess.
// int srci = 0;
// for(int i = 0; i < screenWidth * screenHeight * 4; i += 4) {
// int src;
// xfriendlyFB[i + 3] = 0; // UNUSED
// xfriendlyFB[i + 2] = framebufferPixels[srci + 0]; // BLUE
// xfriendlyFB[i + 1] = framebufferPixels[srci + 1]; // GREEN
// xfriendlyFB[i + 0] = framebufferPixels[srci + 2]; // RED
// srci += 4;
// }
//memcpy(xfriendlyFB, framebufferPixels, screenWidth * screenHeight * 4);
if(useShmP) { if(useShmP) {
XShmPutImage(xDisplay, window, gc, framebufferImage, 0, 0, 0, 0, screenWidth, screenHeight, 0); XShmPutImage(xDisplay, window, gc, displayImage, 0, 0, 0, 0, screenWidth, screenHeight, 0);
// Swap images
XImage* tempI = displayImage;
displayImage = framebufferImage;
framebufferImage = tempI;
// Swap pixel pointers
char* tempP = displayPixels;
displayPixels = framebufferPixels;
framebufferPixels = tempP;
} else { } else {
XPutImage(xDisplay, window, gc, framebufferImage, 0, 0, 0, 0, screenWidth, screenHeight); XPutImage(xDisplay, window, gc, displayImage, 0, 0, 0, 0, screenWidth, screenHeight);
} }
} }