diff --git a/overlay/rootfs/usr/bin/snake b/overlay/rootfs/usr/bin/snake index 9f8bcdf..8bdf63a 100755 Binary files a/overlay/rootfs/usr/bin/snake and b/overlay/rootfs/usr/bin/snake differ diff --git a/overlay/rootfs/usr/bin/tetris b/overlay/rootfs/usr/bin/tetris index 84af923..ebc99c4 100755 Binary files a/overlay/rootfs/usr/bin/tetris and b/overlay/rootfs/usr/bin/tetris differ diff --git a/prebuilt/games/usr/bin/snake b/prebuilt/games/usr/bin/snake index 9f8bcdf..8bdf63a 100755 Binary files a/prebuilt/games/usr/bin/snake and b/prebuilt/games/usr/bin/snake differ diff --git a/prebuilt/games/usr/bin/tetris b/prebuilt/games/usr/bin/tetris index 84af923..ebc99c4 100755 Binary files a/prebuilt/games/usr/bin/tetris and b/prebuilt/games/usr/bin/tetris differ diff --git a/src/games/snake.cpp b/src/games/snake.cpp index c657105..e268cea 100644 --- a/src/games/snake.cpp +++ b/src/games/snake.cpp @@ -21,6 +21,10 @@ #include #include #include +#include +#include +#include +#include static const int TOPBAR = 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 bool running = true; static unsigned long now_ms() { - struct timeval tv; - gettimeofday(&tv, nullptr); - return (unsigned long)(tv.tv_sec * 1000 + tv.tv_usec / 1000); + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (unsigned long)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000); } // Derive the per-step delay (ms) from the target pixels/second. The snake @@ -270,14 +274,20 @@ static void render() { // head XSetForeground(dpy, gc, C_HEAD); 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]; 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) { 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) { @@ -402,55 +412,117 @@ int main() { render(); int xfd = ConnectionNumber(dpy); - const unsigned long FRAME_MS = 1000 / 30; // 30 fps rendering - unsigned long lastFrame = now_ms(); - unsigned long accumulator = 0; + + // Precise, drift-free stepping via a kernel high-resolution timer (timerfd). + // 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) { - // Process all pending input immediately (non-blocking) before each frame - while (XPending(dpy)) { - XEvent e; - XNextEvent(dpy, &e); - handle_event(e); + fd_set fds; + FD_ZERO(&fds); + FD_SET(xfd, &fds); + int nfds = xfd + 1; + 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; - unsigned long now = now_ms(); - unsigned long elapsed = now - lastFrame; - lastFrame = now; - - // Cap elapsed time to prevent spiral of death - if (elapsed > 200) elapsed = 200; - - // 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++; + // 2) (Re)start play cleanly: resync the clock on resume / restart / launch, + // and stop the timer whenever we leave the PLAYING state. + if (state != prevState) { + if (state == PLAYING) { nextStep = now_ms() + speed; if (tfd >= 0) arm_timer(nextStep); } + else if (tfd >= 0) disarm_timer(); + prevState = state; } - // 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(); - - // 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); XFreePixmap(dpy, back); XDestroyWindow(dpy, win); diff --git a/src/games/tetris.cpp b/src/games/tetris.cpp index def1d2a..14aa29b 100644 --- a/src/games/tetris.cpp +++ b/src/games/tetris.cpp @@ -22,6 +22,10 @@ #include #include #include +#include +#include +#include +#include static const int TOPBAR = 24; 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() { - struct timeval tv; - gettimeofday(&tv, nullptr); - return (unsigned long)(tv.tv_sec * 1000 + tv.tv_usec / 1000); + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (unsigned long)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000); } // Derive the gravity delay (ms) from the target pixels/second. A piece drops @@ -516,55 +520,117 @@ int main() { render(); int xfd = ConnectionNumber(dpy); - const unsigned long FRAME_MS = 1000 / 30; // 30 fps rendering - unsigned long lastFrame = now_ms(); - unsigned long accumulator = 0; + + // Precise, drift-free stepping via a kernel high-resolution timer (timerfd). + // 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) { - // Process all pending input immediately (non-blocking) before each frame - while (XPending(dpy)) { - XEvent e; - XNextEvent(dpy, &e); - handle_event(e); + fd_set fds; + FD_ZERO(&fds); + FD_SET(xfd, &fds); + int nfds = xfd + 1; + 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; - unsigned long now = now_ms(); - unsigned long elapsed = now - lastFrame; - lastFrame = now; - - // Cap elapsed time to prevent spiral of death - if (elapsed > 200) elapsed = 200; - - // 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++; + // 2) (Re)start play cleanly: resync the clock on resume / restart / launch, + // and stop the timer whenever we leave the PLAYING state. + if (state != prevState) { + if (state == PLAYING) { nextStep = now_ms() + speed; if (tfd >= 0) arm_timer(nextStep); } + else if (tfd >= 0) disarm_timer(); + prevState = state; } - // 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(); - - // 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); XFreePixmap(dpy, back); XDestroyWindow(dpy, win);