Speeding Up X11 Software Rendering with the MIT-SHM Shared Memory Extension
- https://www.youtube.com/watch?v=YAQJ5QPr7j4
- Original title: I lied about my Game
Tsoding's software-rendered game multithreads pixel computation on the CPU but still leans on SDL2's OpenGL-backed texture upload to get frames onto the screen, which he considers a slight "cheat." The pure X11+PulseAudio platform avoids OpenGL but is roughly 2x slower because XPutImage ships every frame over the X11 client/server socket. This stream is a research session exploring whether the MIT-SHM (MIT shared memory) X11 extension can close that gap by letting client and server share an image buffer instead of copying it over the wire. It works and roughly doubles X11 FPS, but never matches SDL — partly because X11 still does extra server-side display work and partly because Tsoding's own CPU-side frame upscaling, which SDL offloads to the GPU, becomes the new bottleneck.
X11 software rendering with MIT-SHM
The problem and the hypothesis
The game renders every pixel on the CPU across several threads (~120 FPS), splitting the screen into independent regions filled in parallel. Two platforms exist: the default SDL2 platform (which uses an OpenGL-backed SDL texture for presentation) and an "X11+Pulse" platform that talks to X11 directly for rendering and PulseAudio for sound. On the X11 platform FPS drops roughly in half (down to ~60), even though pixel placement is identical. The hypothesis: the cost is in presentation. XPutImage sends the entire frame buffer over the X11 client/server connection (a Unix-domain socket / file descriptor, via a write syscall) to the X server. The MIT-SHM extension lets client and server map a shared memory segment for X images, so the server reads pixels directly from memory instead of receiving them over the wire.
Reading the docs (MIT-SHM by John Corbet, LWN founder)
The extension's canonical reference is essentially the only online doc. Key API differences from a normal X image:
XShmCreateImageinstead ofXCreateImage, andXShmPutImageinstead ofXPutImage.- Headers needed:
sys/ipc.h,sys/shm.h,X11/extensions/XShm.h. These ride on System V (CIS5/SysV) shared memory primitives, which the doc notes must be enabled in the kernel on some systems. - Pixmaps vs X images: the game uses X images, not shared pixmaps, so the pixmap path is ignored.
A digression on the network-transparency caveat (you can run client and server on different machines, where SHM can't work) leads to chat naming RDMA (Remote Direct Memory Access) for cross-machine shared memory — noted as an interesting future stream topic, not used here.
Detecting the extension
Added an XShmQueryVersion call after opening the display. It returns support status plus major/minor version and a "pixmaps supported" boolean. Output confirmed: XShm 1.2 supported, Pixmap support is on. Initial behavior on missing support is a hard crash, with a TODO to fall back to regular X images.
Building the shared X image (the SysV SHM dance)
The sequence, learned partly by reading and partly by stepping through GDB (had to flip an unoptimize build flag because the optimizer was eliding the structs):
XShmCreateImage(display, visual, depth, ZPixmap, NULL, &shminfo, width, height)— note there is no offset / bytes-per-line argument; the server defines those, and you must passNULLfor data so the server tells you the required alignment. Reading this saved work — an earlier offline attempt had manually allocated the segment first, which is harder.shmget(IPC_PRIVATE, image->bytes_per_line * image->height, IPC_CREAT | 0777)to create the SysV segment. Permissions0777matter: the X server will only attach if the segment is readable/writable by others (on local transports it can be locked to the client UID). Error path checks for-1and printserrno.shmat(shmid, NULL, 0)to attach. Gotcha emphasized: on failureshmatreturns(void*)-1, notNULL— same convention as =mmap='sMAP_FAILED. A tangent: NULL-as-invalid is a software/POSIX convention, not a hardware truth (address 0 is a perfectly valid address on bare metal, and on WASM/MMU-less platforms 0 is usable).- Store the attached pointer in both
image->dataandshminfo.shmaddr; setshminfo.readOnly. He set =readOnly = true= (acceptable since he doesn't needXShmGetImage, which is only for reading pixels back / screenshots). XShmAttach(display, &shminfo)to tell the server to attach.
Wiring it into the render loop
Used #ifdef SHARED_MEMORY to flip between shared and non-shared images at compile time (with the stated intent to make it a runtime choice eventually, since fallback is mandatory). Redirected the render target to the shared image's data pointer. Presentation uses XShmPutImage with a send_event flag — passing true makes the server fire a completion event so the client knows when it's safe to mutate the buffer again. He deliberately skipped synchronization for now (and later argued it's largely unneeded because the image is effectively read-only from the server's side and nothing flickers).
Result and the surprise
First run looked worse — because optimizations were still off. With optimizations re-enabled, X11 SHM FPS jumped to ~130, comparable to SDL, with no OpenGL context at all. A genuinely good result for windowed mode.
Resizing — the part that previously crashed
ConfigureNotify (window resize) is where the offline attempt used to crash. Resizing a shared image requires fully tearing down and recreating it. The teardown sequence (recovered from his old Boomer screenshot tool, which used MIT-SHM for real-time capture): XShmDetach → XDestroyImage → shmdt (detach) → shmctl(shmid, IPC_RMID, ...) to remove the segment, then re-run the full create/attach sequence with the new dimensions and reassign the screen buffer pointer. There is notably no XShmDestroyImage — you destroy with the ordinary XDestroyImage. (A confusing detour: Nim's deallocShared refers to Nim's shared heap, unrelated to SysV shared memory.) After this, resize no longer crashes.
Why it still loses to SDL
Even full-screen with SHM, X11 sits around 90–100 and full software (no SHM) drops well below 100; SDL stays far ahead. Two reasons surfaced:
- On the X11 path, Tsoding manually upscales the fixed-resolution render buffer on the CPU to the window size (swizzling pixels in tight loops). Under SDL he just hands SDL a smaller texture and lets
SDL_RenderCopy/ the GPU upscale to the destination rect. So SDL "cheats" more than he realized — not only the texture upload but the entire upscale runs on the GPU. The smaller the window, the faster X11 runs, confirming upscaling is the dominant remaining cost. - Even with a fast transfer, the X server still does more work to actually display the pixels than a GPU would.
Chat floated alternatives (XPresent for vsync, the XRender extension with Porter-Duff compositing) but =XRender='s composition model didn't match the simple "blit pixels" need.
Wrap-up
Made the shared-vs-non-shared choice runtime (XShmQueryVersion → a shm_supported boolean, no exit on failure), so the platform degrades gracefully to plain X images when MIT-SHM is unavailable. The duplicated create/attach code annoyed him — in his Lisp-like language "J" he'd factor it behind a macro, but in C he repeated it. Verdict: treated as a successful research session — the hypothesis held (SHM roughly doubled X11 FPS to ~30+ gain and removed the OpenGL dependency), but the manual CPU upscale is the new bottleneck and X11 still can't match SDL. Further research needed; he's not obligated to ship the X11 SHM path.