Created
July 5, 2026 23:34
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* 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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* 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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* 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