games: jitter-free stepping via timerfd + clearer Snake playfield
Replace the select() timeout sleep with a Linux timerfd high-resolution timer. It fires on an absolute CLOCK_MONOTONIC deadline, so wakeups are no longer quantized to the kernel tick (a short final wait was rounded up to a whole tick, causing +-10-22ms jitter and lurches). The loop now select()s on both the X connection fd and the timer fd, so input still wakes it immediately and no busy-spin is needed -- the controls stay responsive. A select()-timeout fallback is kept for non-timerfd setups. Snake: draw a bright frame around the actual playfield so its boundary is unmistakable, and move the SCORE/BEST label down into the margin between the window frame and the playfield frame.
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+117
-45
@@ -21,6 +21,10 @@
|
|||||||
#include <X11/Xutil.h>
|
#include <X11/Xutil.h>
|
||||||
#include <X11/keysym.h>
|
#include <X11/keysym.h>
|
||||||
#include <X11/Xatom.h>
|
#include <X11/Xatom.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <sys/timerfd.h>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cerrno>
|
||||||
|
|
||||||
static const int TOPBAR = 24;
|
static const int TOPBAR = 24;
|
||||||
static const int COLS = 24;
|
static const int COLS = 24;
|
||||||
@@ -111,9 +115,9 @@ static int speed = 100; // ms per step (derived from pps below)
|
|||||||
static int pps = 90; // base speed px/sec; +1 px/sec per 5 food eaten
|
static int pps = 90; // base speed px/sec; +1 px/sec per 5 food eaten
|
||||||
static bool running = true;
|
static bool running = true;
|
||||||
static unsigned long now_ms() {
|
static unsigned long now_ms() {
|
||||||
struct timeval tv;
|
struct timespec ts;
|
||||||
gettimeofday(&tv, nullptr);
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
return (unsigned long)(tv.tv_sec * 1000 + tv.tv_usec / 1000);
|
return (unsigned long)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derive the per-step delay (ms) from the target pixels/second. The snake
|
// Derive the per-step delay (ms) from the target pixels/second. The snake
|
||||||
@@ -270,14 +274,20 @@ static void render() {
|
|||||||
// head
|
// head
|
||||||
XSetForeground(dpy, gc, C_HEAD);
|
XSetForeground(dpy, gc, C_HEAD);
|
||||||
XFillRectangle(dpy, back, gc, gx0 + snake[snakelen - 1][0] * cell, gy0 + snake[snakelen - 1][1] * cell, cell, cell);
|
XFillRectangle(dpy, back, gc, gx0 + snake[snakelen - 1][0] * cell, gy0 + snake[snakelen - 1][1] * cell, cell, cell);
|
||||||
|
// bright frame around the playfield so its boundaries are unmistakable
|
||||||
|
XSetForeground(dpy, gc, C_FG);
|
||||||
|
XDrawRectangle(dpy, back, gc, gx0, gy0, gw, gh);
|
||||||
|
|
||||||
// score (top status strip, clear of the playfield border)
|
// score / best sit in the margin between the window frame (outer) and the
|
||||||
|
// playfield frame (inner), vertically centred there so the playfield boundary
|
||||||
|
// stays clear
|
||||||
|
int scoreY = (2 + gy0) / 2 - 3;
|
||||||
char buf[32];
|
char buf[32];
|
||||||
snprintf(buf, sizeof(buf), "SCORE %d", score);
|
snprintf(buf, sizeof(buf), "SCORE %d", score);
|
||||||
draw_text(gx0, 4, 1, C_DIM, buf);
|
draw_text(gx0, scoreY, 1, C_DIM, buf);
|
||||||
if (best > 0) {
|
if (best > 0) {
|
||||||
snprintf(buf, sizeof(buf), "BEST %d", best);
|
snprintf(buf, sizeof(buf), "BEST %d", best);
|
||||||
draw_text(gx0 + gw - 11 * 6, 4, 1, C_DIM, buf);
|
draw_text(gx0 + gw - 11 * 6, scoreY, 1, C_DIM, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state == PAUSED || state == GAMEOVER) {
|
if (state == PAUSED || state == GAMEOVER) {
|
||||||
@@ -402,55 +412,117 @@ int main() {
|
|||||||
render();
|
render();
|
||||||
|
|
||||||
int xfd = ConnectionNumber(dpy);
|
int xfd = ConnectionNumber(dpy);
|
||||||
const unsigned long FRAME_MS = 1000 / 30; // 30 fps rendering
|
|
||||||
unsigned long lastFrame = now_ms();
|
// Precise, drift-free stepping via a kernel high-resolution timer (timerfd).
|
||||||
unsigned long accumulator = 0;
|
// The timer fires on an absolute CLOCK_MONOTONIC deadline, so wakeups are not
|
||||||
|
// quantized to the kernel tick the way a select() timeout is. A select()
|
||||||
|
// timeout shorter than one tick gets rounded UP to a whole tick, which made
|
||||||
|
// the final few milliseconds of each wait overshoot by up to ~20ms and showed
|
||||||
|
// up as speed jitter. Input still arrives on the X connection fd, so we
|
||||||
|
// select() on BOTH fds and react to either immediately -- there is no
|
||||||
|
// busy-spin, so the X server stays free and the controls stay responsive.
|
||||||
|
int tfd = -1;
|
||||||
|
#if defined(__linux__)
|
||||||
|
tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
unsigned long nextStep = now_ms() + speed;
|
||||||
|
State prevState = PLAYING;
|
||||||
|
|
||||||
|
auto arm_timer = [&](unsigned long deadline_ms) {
|
||||||
|
struct itimerspec its;
|
||||||
|
its.it_interval.tv_sec = 0; its.it_interval.tv_nsec = 0; // one-shot
|
||||||
|
its.it_value.tv_sec = (time_t)(deadline_ms / 1000);
|
||||||
|
its.it_value.tv_nsec = (long)((deadline_ms % 1000) * 1000000UL);
|
||||||
|
if (its.it_value.tv_nsec >= 1000000000L) {
|
||||||
|
its.it_value.tv_sec += 1; its.it_value.tv_nsec -= 1000000000L;
|
||||||
|
}
|
||||||
|
timerfd_settime(tfd, TFD_TIMER_ABSTIME, &its, nullptr);
|
||||||
|
};
|
||||||
|
auto disarm_timer = [&]() {
|
||||||
|
struct itimerspec its;
|
||||||
|
memset(&its, 0, sizeof(its));
|
||||||
|
timerfd_settime(tfd, 0, &its, nullptr);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tfd >= 0) arm_timer(nextStep);
|
||||||
|
|
||||||
while (running) {
|
while (running) {
|
||||||
// Process all pending input immediately (non-blocking) before each frame
|
fd_set fds;
|
||||||
while (XPending(dpy)) {
|
FD_ZERO(&fds);
|
||||||
XEvent e;
|
FD_SET(xfd, &fds);
|
||||||
XNextEvent(dpy, &e);
|
int nfds = xfd + 1;
|
||||||
handle_event(e);
|
struct timeval tvbuf, *tvp = nullptr;
|
||||||
|
|
||||||
|
if (tfd >= 0) {
|
||||||
|
// While playing, also wait on the timer fd. While paused / game over the
|
||||||
|
// timer is disarmed, so we block on input only (resume stays instant).
|
||||||
|
if (state == PLAYING) {
|
||||||
|
FD_SET(tfd, &fds);
|
||||||
|
if (tfd + 1 > nfds) nfds = tfd + 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback (timerfd unavailable): bounded select so input stays live.
|
||||||
|
long sleepMs = (state == PLAYING) ? (long)nextStep - (long)now_ms() : 30;
|
||||||
|
if (sleepMs < 0) sleepMs = 0;
|
||||||
|
tvbuf.tv_sec = sleepMs / 1000;
|
||||||
|
tvbuf.tv_usec = (sleepMs % 1000) * 1000;
|
||||||
|
tvp = &tvbuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
int r = select(nfds, &fds, nullptr, nullptr, tvp);
|
||||||
|
if (r < 0) { if (errno == EINTR) continue; break; }
|
||||||
|
|
||||||
|
// 1) Drain pending input first so key presses are never delayed.
|
||||||
|
if (FD_ISSET(xfd, &fds) || XPending(dpy)) {
|
||||||
|
while (XPending(dpy)) {
|
||||||
|
XEvent e;
|
||||||
|
XNextEvent(dpy, &e);
|
||||||
|
handle_event(e);
|
||||||
|
if (!running) break;
|
||||||
|
}
|
||||||
if (!running) break;
|
if (!running) break;
|
||||||
}
|
}
|
||||||
if (!running) break;
|
|
||||||
|
|
||||||
unsigned long now = now_ms();
|
// 2) (Re)start play cleanly: resync the clock on resume / restart / launch,
|
||||||
unsigned long elapsed = now - lastFrame;
|
// and stop the timer whenever we leave the PLAYING state.
|
||||||
lastFrame = now;
|
if (state != prevState) {
|
||||||
|
if (state == PLAYING) { nextStep = now_ms() + speed; if (tfd >= 0) arm_timer(nextStep); }
|
||||||
// Cap elapsed time to prevent spiral of death
|
else if (tfd >= 0) disarm_timer();
|
||||||
if (elapsed > 200) elapsed = 200;
|
prevState = state;
|
||||||
|
|
||||||
// Accumulate time
|
|
||||||
accumulator += elapsed;
|
|
||||||
|
|
||||||
// Step game logic at fixed rate (max 2 steps per frame to prevent spiral)
|
|
||||||
int steps = 0;
|
|
||||||
while (accumulator >= (unsigned long)speed && steps < 2) {
|
|
||||||
if (state == PLAYING) step();
|
|
||||||
accumulator -= speed;
|
|
||||||
steps++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render every frame
|
// 3) Advance the simulation exactly once per timer expiration (or, in the
|
||||||
|
// fallback path, whenever the deadline is reached). If we ever fell more
|
||||||
|
// than one interval behind (e.g. the process was frozen for a screenshot)
|
||||||
|
// we drop the backlog instead of racing to catch up.
|
||||||
|
if (state == PLAYING) {
|
||||||
|
bool due = false;
|
||||||
|
if (tfd >= 0) {
|
||||||
|
if (FD_ISSET(tfd, &fds)) {
|
||||||
|
uint64_t expirations = 0;
|
||||||
|
ssize_t n = read(tfd, &expirations, sizeof(expirations));
|
||||||
|
(void)n; (void)expirations;
|
||||||
|
due = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ((long)(now_ms() - nextStep) >= 0) due = true;
|
||||||
|
}
|
||||||
|
if (due) {
|
||||||
|
step();
|
||||||
|
nextStep += speed; // advance by exactly one interval (no drift)
|
||||||
|
unsigned long now = now_ms();
|
||||||
|
if ((long)(now - nextStep) > (long)speed) nextStep = now + speed;
|
||||||
|
if (tfd >= 0 && state == PLAYING) arm_timer(nextStep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Repaint on every loop turn (input or step) so the display never
|
||||||
|
// appears frozen and key feedback is immediate.
|
||||||
render();
|
render();
|
||||||
|
|
||||||
// Frame pacing: sleep until next frame, but wake up immediately on input
|
|
||||||
now = now_ms();
|
|
||||||
long sleepMs = (long)FRAME_MS - (long)(now - (lastFrame - elapsed));
|
|
||||||
if (sleepMs > 0 && sleepMs <= (long)FRAME_MS) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tfd >= 0) close(tfd);
|
||||||
XFreeGC(dpy, gc);
|
XFreeGC(dpy, gc);
|
||||||
XFreePixmap(dpy, back);
|
XFreePixmap(dpy, back);
|
||||||
XDestroyWindow(dpy, win);
|
XDestroyWindow(dpy, win);
|
||||||
|
|||||||
+108
-42
@@ -22,6 +22,10 @@
|
|||||||
#include <X11/Xutil.h>
|
#include <X11/Xutil.h>
|
||||||
#include <X11/keysym.h>
|
#include <X11/keysym.h>
|
||||||
#include <X11/Xatom.h>
|
#include <X11/Xatom.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <sys/timerfd.h>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cerrno>
|
||||||
|
|
||||||
static const int TOPBAR = 24;
|
static const int TOPBAR = 24;
|
||||||
static const int PCOLS = 10;
|
static const int PCOLS = 10;
|
||||||
@@ -136,9 +140,9 @@ static bool collide(int px, int py, int cells[4][2]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static unsigned long now_ms() {
|
static unsigned long now_ms() {
|
||||||
struct timeval tv;
|
struct timespec ts;
|
||||||
gettimeofday(&tv, nullptr);
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
return (unsigned long)(tv.tv_sec * 1000 + tv.tv_usec / 1000);
|
return (unsigned long)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derive the gravity delay (ms) from the target pixels/second. A piece drops
|
// Derive the gravity delay (ms) from the target pixels/second. A piece drops
|
||||||
@@ -516,55 +520,117 @@ int main() {
|
|||||||
render();
|
render();
|
||||||
|
|
||||||
int xfd = ConnectionNumber(dpy);
|
int xfd = ConnectionNumber(dpy);
|
||||||
const unsigned long FRAME_MS = 1000 / 30; // 30 fps rendering
|
|
||||||
unsigned long lastFrame = now_ms();
|
// Precise, drift-free stepping via a kernel high-resolution timer (timerfd).
|
||||||
unsigned long accumulator = 0;
|
// The timer fires on an absolute CLOCK_MONOTONIC deadline, so wakeups are not
|
||||||
|
// quantized to the kernel tick the way a select() timeout is. A select()
|
||||||
|
// timeout shorter than one tick gets rounded UP to a whole tick, which made
|
||||||
|
// the final few milliseconds of each wait overshoot by up to ~20ms and showed
|
||||||
|
// up as speed jitter. Input still arrives on the X connection fd, so we
|
||||||
|
// select() on BOTH fds and react to either immediately -- there is no
|
||||||
|
// busy-spin, so the X server stays free and the controls stay responsive.
|
||||||
|
int tfd = -1;
|
||||||
|
#if defined(__linux__)
|
||||||
|
tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
unsigned long nextStep = now_ms() + speed;
|
||||||
|
State prevState = PLAYING;
|
||||||
|
|
||||||
|
auto arm_timer = [&](unsigned long deadline_ms) {
|
||||||
|
struct itimerspec its;
|
||||||
|
its.it_interval.tv_sec = 0; its.it_interval.tv_nsec = 0; // one-shot
|
||||||
|
its.it_value.tv_sec = (time_t)(deadline_ms / 1000);
|
||||||
|
its.it_value.tv_nsec = (long)((deadline_ms % 1000) * 1000000UL);
|
||||||
|
if (its.it_value.tv_nsec >= 1000000000L) {
|
||||||
|
its.it_value.tv_sec += 1; its.it_value.tv_nsec -= 1000000000L;
|
||||||
|
}
|
||||||
|
timerfd_settime(tfd, TFD_TIMER_ABSTIME, &its, nullptr);
|
||||||
|
};
|
||||||
|
auto disarm_timer = [&]() {
|
||||||
|
struct itimerspec its;
|
||||||
|
memset(&its, 0, sizeof(its));
|
||||||
|
timerfd_settime(tfd, 0, &its, nullptr);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tfd >= 0) arm_timer(nextStep);
|
||||||
|
|
||||||
while (running) {
|
while (running) {
|
||||||
// Process all pending input immediately (non-blocking) before each frame
|
fd_set fds;
|
||||||
while (XPending(dpy)) {
|
FD_ZERO(&fds);
|
||||||
XEvent e;
|
FD_SET(xfd, &fds);
|
||||||
XNextEvent(dpy, &e);
|
int nfds = xfd + 1;
|
||||||
handle_event(e);
|
struct timeval tvbuf, *tvp = nullptr;
|
||||||
|
|
||||||
|
if (tfd >= 0) {
|
||||||
|
// While playing, also wait on the timer fd. While paused / game over the
|
||||||
|
// timer is disarmed, so we block on input only (resume stays instant).
|
||||||
|
if (state == PLAYING) {
|
||||||
|
FD_SET(tfd, &fds);
|
||||||
|
if (tfd + 1 > nfds) nfds = tfd + 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback (timerfd unavailable): bounded select so input stays live.
|
||||||
|
long sleepMs = (state == PLAYING) ? (long)nextStep - (long)now_ms() : 30;
|
||||||
|
if (sleepMs < 0) sleepMs = 0;
|
||||||
|
tvbuf.tv_sec = sleepMs / 1000;
|
||||||
|
tvbuf.tv_usec = (sleepMs % 1000) * 1000;
|
||||||
|
tvp = &tvbuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
int r = select(nfds, &fds, nullptr, nullptr, tvp);
|
||||||
|
if (r < 0) { if (errno == EINTR) continue; break; }
|
||||||
|
|
||||||
|
// 1) Drain pending input first so key presses are never delayed.
|
||||||
|
if (FD_ISSET(xfd, &fds) || XPending(dpy)) {
|
||||||
|
while (XPending(dpy)) {
|
||||||
|
XEvent e;
|
||||||
|
XNextEvent(dpy, &e);
|
||||||
|
handle_event(e);
|
||||||
|
if (!running) break;
|
||||||
|
}
|
||||||
if (!running) break;
|
if (!running) break;
|
||||||
}
|
}
|
||||||
if (!running) break;
|
|
||||||
|
|
||||||
unsigned long now = now_ms();
|
// 2) (Re)start play cleanly: resync the clock on resume / restart / launch,
|
||||||
unsigned long elapsed = now - lastFrame;
|
// and stop the timer whenever we leave the PLAYING state.
|
||||||
lastFrame = now;
|
if (state != prevState) {
|
||||||
|
if (state == PLAYING) { nextStep = now_ms() + speed; if (tfd >= 0) arm_timer(nextStep); }
|
||||||
// Cap elapsed time to prevent spiral of death
|
else if (tfd >= 0) disarm_timer();
|
||||||
if (elapsed > 200) elapsed = 200;
|
prevState = state;
|
||||||
|
|
||||||
// Accumulate time
|
|
||||||
accumulator += elapsed;
|
|
||||||
|
|
||||||
// Step game logic at fixed rate (max 2 steps per frame to prevent spiral)
|
|
||||||
int steps = 0;
|
|
||||||
while (accumulator >= (unsigned long)speed && steps < 2) {
|
|
||||||
if (state == PLAYING) step();
|
|
||||||
accumulator -= speed;
|
|
||||||
steps++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render every frame
|
// 3) Advance the simulation exactly once per timer expiration (or, in the
|
||||||
|
// fallback path, whenever the deadline is reached). If we ever fell more
|
||||||
|
// than one interval behind (e.g. the process was frozen for a screenshot)
|
||||||
|
// we drop the backlog instead of racing to catch up.
|
||||||
|
if (state == PLAYING) {
|
||||||
|
bool due = false;
|
||||||
|
if (tfd >= 0) {
|
||||||
|
if (FD_ISSET(tfd, &fds)) {
|
||||||
|
uint64_t expirations = 0;
|
||||||
|
ssize_t n = read(tfd, &expirations, sizeof(expirations));
|
||||||
|
(void)n; (void)expirations;
|
||||||
|
due = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ((long)(now_ms() - nextStep) >= 0) due = true;
|
||||||
|
}
|
||||||
|
if (due) {
|
||||||
|
step();
|
||||||
|
nextStep += speed; // advance by exactly one interval (no drift)
|
||||||
|
unsigned long now = now_ms();
|
||||||
|
if ((long)(now - nextStep) > (long)speed) nextStep = now + speed;
|
||||||
|
if (tfd >= 0 && state == PLAYING) arm_timer(nextStep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Repaint on every loop turn (input or step) so the display never
|
||||||
|
// appears frozen and key feedback is immediate.
|
||||||
render();
|
render();
|
||||||
|
|
||||||
// Frame pacing: sleep until next frame, but wake up immediately on input
|
|
||||||
now = now_ms();
|
|
||||||
long sleepMs = (long)FRAME_MS - (long)(now - (lastFrame - elapsed));
|
|
||||||
if (sleepMs > 0 && sleepMs <= (long)FRAME_MS) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tfd >= 0) close(tfd);
|
||||||
XFreeGC(dpy, gc);
|
XFreeGC(dpy, gc);
|
||||||
XFreePixmap(dpy, back);
|
XFreePixmap(dpy, back);
|
||||||
XDestroyWindow(dpy, win);
|
XDestroyWindow(dpy, win);
|
||||||
|
|||||||
Reference in New Issue
Block a user