games: responsive game loop with non-blocking input polling

Replaced the blocking select()-based main loop with a proper game loop:
- Process ALL pending X11 input (XPending) before every frame — no more
  missed keypresses from select() blocking for up to a full frame
- Render every frame at 30 fps (FRAME_MS = 33 ms) consistently
- Frame pacing uses select() with a timeout that wakes immediately on
  input, so keypresses are processed within milliseconds
- Game logic (snake steps, tetris gravity) still runs at its own rate via
  nextTick, decoupled from render rate

This eliminates input lag and missed keypresses in both Snake and Tetris.
This commit is contained in:
Luxferre
2026-10-24 09:03:38 +03:00
parent 8ccfdb8d44
commit eee12181c7
6 changed files with 56 additions and 62 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+28 -31
View File
@@ -376,45 +376,42 @@ int main() {
int xfd = ConnectionNumber(dpy);
const unsigned long FRAME_MS = 1000 / 30; // 30 fps rendering
unsigned long lastFrame = now_ms();
unsigned long frameStart = now_ms();
while (running) {
unsigned long now = now_ms();
long until = (long)nextTick - (long)now;
if (until < 0) until = 0;
// Never block select longer than one frame so the screen stays smooth
long wait = (until < (long)FRAME_MS) ? until : (long)FRAME_MS;
struct timeval tv;
tv.tv_sec = wait / 1000;
tv.tv_usec = (wait % 1000) * 1000;
fd_set fds;
FD_ZERO(&fds);
FD_SET(xfd, &fds);
int r = select(xfd + 1, &fds, nullptr, nullptr, &tv);
bool dirty = false;
if (r > 0) {
// Process all pending input immediately (non-blocking) before each frame
while (XPending(dpy)) {
XEvent e;
while (XPending(dpy)) {
XNextEvent(dpy, &e);
handle_event(e);
if (!running) break;
dirty = true;
}
XNextEvent(dpy, &e);
handle_event(e);
if (!running) break;
}
now = now_ms();
if (running && now >= nextTick) {
if (!running) break;
unsigned long now = now_ms();
// Game logic step at its own rate
if (now >= nextTick) {
if (state == PLAYING) step();
if (state == PLAYING) nextTick = now + speed;
else nextTick = now + 80; // keep loop responsive while paused/over
dirty = true;
nextTick = now + (state == PLAYING ? speed : 80);
}
// Render at 60 fps or whenever state changed
if (dirty || now - lastFrame >= FRAME_MS) {
render();
lastFrame = now;
// Render every frame
render();
// Frame pacing: sleep until next frame, but wake up immediately on input
now = now_ms();
long sleepMs = (long)FRAME_MS - (long)(now - frameStart);
if (sleepMs > 0) {
struct timeval tv;
tv.tv_sec = sleepMs / 1000;
tv.tv_usec = (sleepMs % 1000) * 1000;
fd_set fds;
FD_ZERO(&fds);
FD_SET(xfd, &fds);
select(xfd + 1, &fds, nullptr, nullptr, &tv);
}
frameStart = now_ms();
}
XFreeGC(dpy, gc);
+28 -31
View File
@@ -500,45 +500,42 @@ int main() {
int xfd = ConnectionNumber(dpy);
const unsigned long FRAME_MS = 1000 / 30; // 30 fps rendering
unsigned long lastFrame = now_ms();
unsigned long frameStart = now_ms();
while (running) {
unsigned long now = now_ms();
long until = (long)nextTick - (long)now;
if (until < 0) until = 0;
// Never block select longer than one frame so the screen stays smooth
long wait = (until < (long)FRAME_MS) ? until : (long)FRAME_MS;
struct timeval tv;
tv.tv_sec = wait / 1000;
tv.tv_usec = (wait % 1000) * 1000;
fd_set fds;
FD_ZERO(&fds);
FD_SET(xfd, &fds);
int r = select(xfd + 1, &fds, nullptr, nullptr, &tv);
bool dirty = false;
if (r > 0) {
// Process all pending input immediately (non-blocking) before each frame
while (XPending(dpy)) {
XEvent e;
while (XPending(dpy)) {
XNextEvent(dpy, &e);
handle_event(e);
if (!running) break;
dirty = true;
}
XNextEvent(dpy, &e);
handle_event(e);
if (!running) break;
}
now = now_ms();
if (running && now >= nextTick) {
if (!running) break;
unsigned long now = now_ms();
// Game logic step at its own rate
if (now >= nextTick) {
if (state == PLAYING) step();
if (state == PLAYING) nextTick = now + speed;
else nextTick = now + 80;
dirty = true;
nextTick = now + (state == PLAYING ? speed : 80);
}
// Render at 60 fps or whenever state changed
if (dirty || now - lastFrame >= FRAME_MS) {
render();
lastFrame = now;
// Render every frame
render();
// Frame pacing: sleep until next frame, but wake up immediately on input
now = now_ms();
long sleepMs = (long)FRAME_MS - (long)(now - frameStart);
if (sleepMs > 0) {
struct timeval tv;
tv.tv_sec = sleepMs / 1000;
tv.tv_usec = (sleepMs % 1000) * 1000;
fd_set fds;
FD_ZERO(&fds);
FD_SET(xfd, &fds);
select(xfd + 1, &fds, nullptr, nullptr, &tv);
}
frameStart = now_ms();
}
XFreeGC(dpy, gc);