Skip to content

Instantly share code, notes, and snippets.

@vejeta
Created July 5, 2026 23:34
Show Gist options
  • Select an option

  • Save vejeta/5945622c01e7c2aac33f47800e0103e9 to your computer and use it in GitHub Desktop.

Select an option

Save vejeta/5945622c01e7c2aac33f47800e0103e9 to your computer and use it in GitHub Desktop.
Reproduction scripts for SDL2-compat/SDL3 stuck-event-queue bug and libx11-compat Cocoa pump-wake fix (sdl2-compat 2.32.70 on SDL3 3.4.12)
/* Validates the proposed long-term fix design: the pump-wake timer writes the
* pipe byte DIRECTLY (no SDL event, no event filter, no SDL queue), rate-limited
* by an atomic 'pendingWakeBytes', and the main-thread pump consumes it. This
* must sustain ~30Hz wakes indefinitely with no freeze and no busy-spin.
*/
#include <SDL.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
static int pipeFds[2] = {-1, -1};
static SDL_atomic_t pendingWakeBytes; /* 0 or 1 */
static SDL_atomic_t timerTicks;
static SDL_atomic_t bytesWritten;
/* Timer thread: mimics xtWakeTimerCallback in the new design. */
static Uint32 timerCb(Uint32 interval, void *param)
{
(void) param;
SDL_AtomicAdd(&timerTicks, 1);
if (pipeFds[1] < 0)
return interval;
if (SDL_AtomicCAS(&pendingWakeBytes, 0, 1)) {
char b = 'e';
if (write(pipeFds[1], &b, 1) == 1)
SDL_AtomicAdd(&bytesWritten, 1);
else
SDL_AtomicSet(&pendingWakeBytes, 0);
}
return interval;
}
/* Main thread: mimics pumpEventsSafe() consuming the wake byte after pumping. */
static void pumpEventsSafe(void)
{
SDL_PumpEvents();
if (SDL_AtomicCAS(&pendingWakeBytes, 1, 0)) {
char buf;
ssize_t r = read(pipeFds[0], &buf, 1);
(void) r;
}
}
int main(void)
{
SDL_version linked;
SDL_GetVersion(&linked);
printf("SDL linked %d.%d.%d\n", linked.major, linked.minor, linked.patch);
SDL_Init(SDL_INIT_VIDEO);
printf("video driver = %s\n", SDL_GetCurrentVideoDriver());
SDL_Window *win = SDL_CreateWindow("fix", 0, 0, 200, 100,
SDL_WINDOW_HIDDEN);
printf("window = %p\n", (void *) win);
if (pipe(pipeFds) != 0) { printf("pipe failed\n"); return 1; }
fcntl(pipeFds[0], F_SETFL, fcntl(pipeFds[0], F_GETFL) | O_NONBLOCK);
fcntl(pipeFds[1], F_SETFL, fcntl(pipeFds[1], F_GETFL) | O_NONBLOCK);
SDL_AtomicSet(&pendingWakeBytes, 0);
SDL_TimerID t = SDL_AddTimer(33, timerCb, NULL);
printf("timer id = %d (0 == FAILED)\n", (int) t);
int pollWakes = 0, immediateReturns = 0;
for (int sec = 0; sec < 5; sec++) {
Uint32 start = SDL_GetTicks();
while (SDL_GetTicks() - start < 1000) {
struct pollfd pfd = {pipeFds[0], POLLIN, 0};
Uint32 t0 = SDL_GetTicks();
int r = poll(&pfd, 1, 1000);
if (r > 0) {
pollWakes++;
if (SDL_GetTicks() - t0 < 2)
immediateReturns++; /* spin detector */
pumpEventsSafe();
/* drain "real" SDL events (none expected) */
SDL_Event evs[64];
SDL_PeepEvents(evs, 64, SDL_GETEVENT, SDL_FIRSTEVENT,
SDL_LASTEVENT);
}
}
printf("[t=%ds] timerTicks=%d bytesWritten=%d pollWakes=%d "
"immediateReturns=%d pendingWakeBytes=%d\n",
sec + 1, SDL_AtomicGet(&timerTicks),
SDL_AtomicGet(&bytesWritten), pollWakes, immediateReturns,
SDL_AtomicGet(&pendingWakeBytes));
}
printf("DONE (healthy: pollWakes keeps climbing ~30/s, immediateReturns "
"stays ~0)\n");
SDL_RemoveTimer(t);
SDL_Quit();
return 0;
}
/* Focused probe: does SDL_PeepEvents(GETEVENT, FIRSTEVENT, LASTEVENT) remove
* user-registered events under sdl2-compat? Push N user events plus one normal
* event and see what a single full-range GETEVENT drain returns.
*/
#include <SDL.h>
#include <stdio.h>
static void report(const char *tag, Uint32 wake)
{
SDL_Event peek[128];
int n = SDL_PeepEvents(peek, 128, SDL_PEEKEVENT, SDL_FIRSTEVENT,
SDL_LASTEVENT);
printf("%s: PEEK count=%d HasEvent(wake)=%d\n", tag, n,
SDL_HasEvent(wake));
for (int i = 0; i < n; i++)
printf(" [%d] type=0x%08x\n", i, peek[i].type);
}
int main(void)
{
SDL_Init(SDL_INIT_VIDEO);
SDL_Window *w = SDL_CreateWindow("p", 0, 0, 100, 100, SDL_WINDOW_HIDDEN);
printf("window=%p\n", (void *) w);
Uint32 wake = SDL_RegisterEvents(1);
printf("wake=0x%x SDL_FIRSTEVENT=0x%x SDL_LASTEVENT=0x%x SDL_USEREVENT=0x%x\n",
wake, SDL_FIRSTEVENT, SDL_LASTEVENT, SDL_USEREVENT);
SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT);
/* Push 3 wake (user) events. */
for (int i = 0; i < 3; i++) {
SDL_Event e;
SDL_zero(e);
e.type = wake;
SDL_PushEvent(&e);
}
/* Pump like libx11-compat does on the main thread. */
SDL_PumpEvents();
report("after push 3 user + PumpEvents", wake);
SDL_Event got[128];
int g = SDL_PeepEvents(got, 128, SDL_GETEVENT, SDL_FIRSTEVENT,
SDL_LASTEVENT);
printf("GETEVENT(FIRST..LAST) returned=%d\n", g);
for (int i = 0; i < g; i++)
printf(" got[%d] type=0x%08x\n", i, got[i].type);
report("after full-range GETEVENT", wake);
/* Now drain remaining by exact user-type range. */
int g2 = SDL_PeepEvents(got, 128, SDL_GETEVENT, wake, wake);
printf("GETEVENT(wake..wake) returned=%d\n", g2);
report("after exact-range GETEVENT", wake);
/* Try the documented SDL_USEREVENT..last range. */
SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT);
for (int i = 0; i < 3; i++) {
SDL_Event e;
SDL_zero(e);
e.type = wake;
SDL_PushEvent(&e);
}
int g3 = SDL_PeepEvents(got, 128, SDL_GETEVENT, SDL_USEREVENT,
SDL_LASTEVENT);
printf("GETEVENT(USEREVENT..LAST) returned=%d\n", g3);
report("after USEREVENT..LAST GETEVENT", wake);
SDL_Quit();
return 0;
}
/* Minimal reproduction of libx11-compat's wake-timer mechanism.
* Mirrors: SDL_Init(VIDEO) only, register user event, AddTimer(33) that
* pushes the event iff !SDL_HasEvent(type), an event filter that counts
* and "writes a pipe byte", and a main thread that blocks in poll() on the
* pipe read end -- exactly like xwpe's e_x_getch loop.
*/
#include <SDL.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <stdatomic.h>
static Uint32 wakeType = (Uint32) -1;
static int pipeFds[2] = {-1, -1};
static atomic_int filterCalls = 0;
static atomic_int timerTicks = 0;
static atomic_int timerPushes = 0;
static atomic_int pipeWrites = 0;
static int SDLCALL onEvent(void *ud, SDL_Event *e)
{
(void) ud;
atomic_fetch_add(&filterCalls, 1);
char b = 'e';
if (write(pipeFds[1], &b, 1) == 1)
atomic_fetch_add(&pipeWrites, 1);
return 1;
}
static Uint32 timerCb(Uint32 interval, void *param)
{
(void) param;
atomic_fetch_add(&timerTicks, 1);
if (wakeType == (Uint32) -1)
return interval;
if (SDL_HasEvent(wakeType))
return interval;
SDL_Event ev;
SDL_zero(ev);
ev.type = wakeType;
SDL_PushEvent(&ev);
atomic_fetch_add(&timerPushes, 1);
return interval;
}
int main(void)
{
printf("SDL compiled %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION,
SDL_PATCHLEVEL);
SDL_version linked;
SDL_GetVersion(&linked);
printf("SDL linked %d.%d.%d\n", linked.major, linked.minor, linked.patch);
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
printf("SDL_Init(VIDEO) failed: %s\n", SDL_GetError());
return 1;
}
printf("video driver = %s\n", SDL_GetCurrentVideoDriver());
SDL_Window *win = SDL_CreateWindow("waketest", 0, 0, 200, 100,
SDL_WINDOW_HIDDEN);
printf("window = %p\n", (void *) win);
if (pipe(pipeFds) != 0) {
printf("pipe failed\n");
return 1;
}
fcntl(pipeFds[0], F_SETFL, fcntl(pipeFds[0], F_GETFL) | O_NONBLOCK);
fcntl(pipeFds[1], F_SETFL, fcntl(pipeFds[1], F_GETFL) | O_NONBLOCK);
wakeType = SDL_RegisterEvents(1);
printf("wakeType = %u\n", wakeType);
SDL_SetEventFilter(onEvent, NULL);
SDL_TimerID t = SDL_AddTimer(33, timerCb, NULL);
printf("timer id = %d (0 == FAILED)\n", (int) t);
/* Mimic xwpe's e_x_getch: block in poll() on the pipe read end. Each time
* poll wakes, "drain" the SDL queue (GETEVENT) like XPending does, which
* removes the wake event so SDL_HasEvent goes false again, and read the
* pipe byte.
*/
int pollWakes = 0, drained = 0;
for (int sec = 0; sec < 3; sec++) {
Uint32 start = SDL_GetTicks();
while (SDL_GetTicks() - start < 1000) {
struct pollfd pfd = {pipeFds[0], POLLIN, 0};
int r = poll(&pfd, 1, 1000);
if (r > 0) {
pollWakes++;
char buf[64];
while (read(pipeFds[0], buf, sizeof(buf)) > 0) {
}
SDL_PumpEvents();
SDL_Event evs[64];
int n = SDL_PeepEvents(evs, 64, SDL_GETEVENT, SDL_FIRSTEVENT,
SDL_LASTEVENT);
if (n > 0)
drained += n;
}
}
SDL_PumpEvents();
SDL_Event peek[64];
int peeked = SDL_PeepEvents(peek, 64, SDL_PEEKEVENT, SDL_FIRSTEVENT,
SDL_LASTEVENT);
int has = SDL_HasEvent(wakeType);
int hasRange = SDL_HasEvents(SDL_FIRSTEVENT, SDL_LASTEVENT);
for (int i = 0; i < peeked; i++)
printf(" stuck event[%d] type=0x%08x (wakeType=0x%x)\n", i,
peek[i].type, wakeType);
if (sec == 1) {
/* Try targeted removals of the stuck wake event. */
SDL_Event g;
int byType = SDL_PeepEvents(&g, 1, SDL_GETEVENT, wakeType, wakeType);
printf(" -> GETEVENT(range wakeType..wakeType) removed=%d\n",
byType);
printf(" -> after: HasEvent(wake)=%d PEEK=%d\n",
SDL_HasEvent(wakeType),
SDL_PeepEvents(peek, 64, SDL_PEEKEVENT, SDL_FIRSTEVENT,
SDL_LASTEVENT));
SDL_FlushEvent(wakeType);
printf(" -> after FlushEvent(wake): HasEvent(wake)=%d PEEK=%d\n",
SDL_HasEvent(wakeType),
SDL_PeepEvents(peek, 64, SDL_PEEKEVENT, SDL_FIRSTEVENT,
SDL_LASTEVENT));
}
printf("[t=%ds] filterCalls=%d timerTicks=%d timerPushes=%d "
"pipeWrites=%d pollWakes=%d drained=%d | PEEK(all)=%d "
"HasEvent(wake)=%d HasEvents(all)=%d\n",
sec + 1, atomic_load(&filterCalls), atomic_load(&timerTicks),
atomic_load(&timerPushes), atomic_load(&pipeWrites), pollWakes,
drained, peeked, has, hasRange);
}
printf("DONE\n");
SDL_RemoveTimer(t);
SDL_Quit();
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment