Blitting pixels in X11

March 6, 2024

The following are source code snippets for showing how to blit pixels directly using X11. They are a little hardcode-y, since I wrote them as quick programs to figure out roughly how to use the X11 APIs (a living note, essentially). You can get a lot more information on how to use the X11 APIs by reading the documentation, possibly using the following as a guide for procedures to start looking at.

I’ve built and tested them on OpenBSD, but they should work on Linux, or any other X11 based system with the appropriate compiler switches.

Consider the following to be free for use (public domain). Any credit for this having been useful would of course be appreciated!

Blitting pixels with the default X11 APIs

Built on OpenBSD with

cc -std=c99 -I/usr/X11R6/include -L/usr/X11R6/lib -lX11 -o x11-blit x11-blit.c

assuming the below is saved in x11-blit.c. This uses vanilla X11 to create an XImage and write pixels to its data buffer. We set the data buffer to be a statically allocated global buffer, just to show how to do that.

/*
 * Example of blitting pixels directly in x11.
 */
#include <X11/Xlib.h>
#include <X11/Xutil.h> /* XDestroyImage */

#include <stdio.h>
#include <stdint.h>

#define LOG(...)	fprintf(stderr, __VA_ARGS__)
#define ERROR(...)	LOG("[ERROR] " __VA_ARGS__)

#define WINDOW_POS_X	100
#define WINDOW_POS_Y	100
#define WINDOW_WIDTH	800
#define WINDOW_HEIGHT	600
#define WINDOW_BORDER	1

#define RGBA(r, g, b, a)	((a) << 24 | (r) << 16 | (g) << 8 | (b))

static uint32_t canvas[WINDOW_WIDTH * WINDOW_HEIGHT] = { 0 };

enum
{
	RC_OK		= 0,
	RC_ERROR	= 1,
};

int
main()
{
	int rc = RC_ERROR;

	Display *display = NULL;
	XImage *image = NULL;

	display = XOpenDisplay(NULL);

	if (display == NULL)
	{
		ERROR("failed to open display\n");
		goto error;
	}

	int screen = DefaultScreen(display);
	Window window = XCreateSimpleWindow(display, RootWindow(display, screen),
			                    WINDOW_POS_X, WINDOW_POS_Y,
			                    WINDOW_WIDTH, WINDOW_HEIGHT,
			                    WINDOW_BORDER,
			                    BlackPixel(display, screen),
			                    WhitePixel(display, screen));

	XSelectInput(display, window, ExposureMask|KeyPressMask);
	XMapWindow(display, window);

	image = XCreateImage(display, DefaultVisual(display, screen), DefaultDepth(display, screen), ZPixmap, 0,
		             (char *)canvas, WINDOW_WIDTH, WINDOW_HEIGHT, 32, 0);


	if (image == NULL)
	{
		ERROR("failed to create image\n");
		goto error;
	}

	int quit = 0;
	while (!quit)
	{
		XEvent e;
		XNextEvent(display, &e);

		switch (e.type)
		{
			case Expose:
			{
				for (int x = 0; x < WINDOW_WIDTH; ++x)
				{
					for (int y = 0; y < WINDOW_HEIGHT; ++y)
					{
						canvas[y * WINDOW_WIDTH + x] = RGBA(x & 0xFF, (y & 0xFF) + (x & 0xFF), 0, 0); /* alpha is unused? */
					}
				}

				XPutImage(display, window, DefaultGC(display, screen), image, 0, 0, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
			}
			break;

			case KeyPress:
			{
				LOG("key press received, exiting\n");
				quit = 1;
			}
			break;
		}
	}

	rc = RC_OK;

error:
	if (image)
	{
		/* XDestroyImage tries to free the image data, even though create doesn't
		 * allocate it. Set the data to NULL to avoid this (our data is defined
		 * as a global variable). */
		image->data = NULL;
		XDestroyImage(image);
	}

	if (display)
	{
		XDestroyWindow(display, window);
		XCloseDisplay(display);
	}

	return rc;
}

Blitting pixels using shared memory extensions

X11 uses a client server network model, meaning native objects in X11 need sending over its wire protocol. This isn’t fantastically efficient for bulk data transfer, of which image transfer is a special case, so X11 has extensions for using shared memory between client and server for setting image data.

In practice this seems to mostly reduce the time spent in the client sending image data to the server (which makes sense), but a fair bit of time is still spent by the server pushing these pixels to screen. I’ve not given much effort to figuring out how hard this can be pushed, or where its limits are. Note that you can listen for events (as part of the normal X11 event infrastructure) to determine when the X server has finished putting the image to screen, so as to not change that memory while its in use. This allows you to cycle between two or three different buffers as needed.

The following was compiled (again on OpenBSD) with

cc -std=c99 -g -I/usr/X11R6/include -L/usr/X11R6/lib -lX11 -lXext -o x11shm-blit x11shm-blit.c

assuming the below is saved into x11shm-blit.c.

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <X11/extensions/XShm.h>

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>

#include <unistd.h>

#define LOG(...)	fprintf(stderr, __VA_ARGS__)
#define ERROR(...)	LOG("[ERROR] " __VA_ARGS__)

#define WINDOW_POS_X	100
#define WINDOW_POS_Y	100
#define WINDOW_WIDTH	800
#define WINDOW_HEIGHT	600
#define WINDOW_BORDER	1

enum
{
	RC_OK,
	RC_ERROR,
};

static XShmSegmentInfo shminfo = { 0 };

int
main()
{
	int rc = RC_ERROR;

	Display *display = NULL;
	XImage *image = NULL;

	display = XOpenDisplay(NULL);

	if (display == NULL)
	{
		ERROR("could not open display\n");
		goto error;
	}

	if (!XShmQueryExtension(display))
	{
		ERROR("display does not support shared memory extension\n");
		goto error;
	}

	int screen = DefaultScreen(display);

	Window window = XCreateSimpleWindow(display, RootWindow(display, screen),
			                    WINDOW_POS_X, WINDOW_POS_Y,
			                    WINDOW_WIDTH, WINDOW_HEIGHT,
			                    WINDOW_BORDER, BlackPixel(display, screen),
			                    WhitePixel(display, screen));

	XSelectInput(display, window, ExposureMask|KeyPressMask);
	XMapWindow(display, window);

	image = XShmCreateImage(display, DefaultVisual(display, screen),
		                DefaultDepth(display, screen), ZPixmap,
				NULL, &shminfo, 800, 600);

	if (image == NULL)
	{
		ERROR("failed to create image\n");
		goto error;
	}

	shminfo.shmid = shmget(IPC_PRIVATE, image->bytes_per_line * image->height,
			       IPC_CREAT|0777);

	if (shminfo.shmid < 0)
	{
		ERROR("failed to create shared memory: %s\n", strerror(errno));
		goto error;
	}

	image->data = shmat(shminfo.shmid, 0, 0);
	shminfo.shmaddr = image->data;
	shminfo.readOnly = True;

	if (!XShmAttach(display, &shminfo))
	{
		ERROR("failed to attach shared memory\n");
		goto error;
	}
	
	rc = RC_OK;

	uint32_t *pixel = (uint32_t *)image->data;
	for (int x = 0; x < image->width; ++x)
	{
		for (int y = 0; y < image->height; ++y)
		{
			pixel[y * image->width + x] = ((x & 0xff) << 8) | 0xff;
		}
	}

	int quit = 0;
	while (!quit)
	{
		XEvent e;
		XNextEvent(display, &e);

		switch (e.type)
		{
			case Expose:
			{
				XShmPutImage(display, window, DefaultGC(display, screen),
				             image, 0, 0, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,
					     True);
			}
			break;

			case KeyPress:
			{
				quit = 1;
			}
			break;

			default: break;
		}
	}

	if (!XShmDetach(display, &shminfo))
	{
		ERROR("failed to detach shared memory - continuing shutdown\n");
	}

error:
	if (shminfo.shmid >= 0)
	{
		if (shmctl(shminfo.shmid, IPC_RMID, NULL))
		{
			ERROR("could not remove shared memory with IPC_RMID: %s\n",
			      strerror(errno));
		}
	}

	if (image)
	{
		XDestroyImage(image);
	}

	if (display != NULL)
	{
		XDestroyWindow(display, window);
		XCloseDisplay(display);
	}

	return rc;
}