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.
The snake speed instability was caused by cumulative timing drift in the
select()-based loop (nextTick reset to now+speed each step). Both games now
use a fixed-timestep accumulator: elapsed time is accumulated each frame and
the game logic steps when enough has built up, so movement is perfectly
periodic regardless of frame timing or rendering cost. Frame pacing caps
elapsed at 200ms to prevent a spiral of death.
Both files were fully rewritten to eliminate structural corruption (duplicate
function definitions / missing braces) introduced by many incremental edits:
- Snake: turn queue (max 2), batched body rendering, 90 px/sec base, 30 fps.
- Tetris: batched beveled-block rendering (XFillRectangles), Gameboy palette,
XK_KP_Divide pause, 30 fps accumulator loop.
Both compile cleanly (no -Wall/-Wextra warnings) as valid armv7 ELF.
Root cause of unstable speed: nextTick was reset to 'now + speed' each step,
accumulating timing drift. Now uses 'nextTick += speed' with a floor (if
nextTick < now, reset to now) to prevent drift and catch-up spirals.
- Both games: 30 fps render cap (FRAME_MS = 33 ms), nextTick updated
cumulatively to eliminate drift.
- Snake: base speed 90 px/sec (~100 ms per step at cell=9), turn queue
unchanged, edge wrap unchanged.
- Tetris: gravity step uses cumulative nextTick, controls unchanged.
Switched both games back to 60 fps (FRAME_MS = 16 ms) for smoother rendering
and reduced Snake base speed from 150 to 90 px/sec (~60 ms -> ~100 ms per
step at cell=9) so the snake is controllable and doesn't feel too fast.
Progressive difficulty (+1 px/sec per 5 food) and turn queue are unchanged.
Identified and fixed rendering/input optimization issues:
- Snake: added turn queue (max 2 pending turns) so rapid key presses are
never lost; filters reversals and duplicates against the last queued
direction. Batched snake body rendering into a single XFillRectangles call
instead of one XFillRectangle per segment.
- Tetris: batched settled-block rendering into 3 XFillRectangles calls
(body, highlight, shadow) instead of 5 XFillRectangle calls per cell.
This reduces ~1000 X11 calls per frame to 3, fixing frame drops and
input lag.
- Both: non-blocking input polling before every frame (already in place)
ensures no keypress is missed.
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.
Switched both games to a 30 fps render cap (FRAME_MS = 33 ms) for smoother,
more predictable frame pacing. Snake base speed raised to 150 px/sec (~60 ms
per step at cell=9) so turn commands take effect at the next step without
perceived lag. The buffered-direction model (latest keypress wins) is
unchanged — turns apply at the next step, which is immediate in snake terms.
Progressive difficulty (+1 px/sec per 5 food in Snake, +1 px/sec per 10 lines
in Tetris), edge wrap, Gameboy-style beveled blocks, and all controls are
unchanged.
Both games now render at 60 fps independent of the game-logic step rate:
the main loop caps select() at one frame (16 ms) so the screen updates
smoothly even when gravity/snake steps are slow, and renders every frame
or whenever state changed.
Snake: replaced the turn queue with a single buffered direction (latest
keypress wins) so rapid key presses don't queue up and misfire. Base speed
set to 100 px/sec (~90 ms per step) for controllable, responsive play.
Progressive difficulty (+1 px/sec per 5 food) and edge wrap are unchanged.
Snake: add a turn queue (up to 4 pending direction changes) so rapid key
presses are never lost, and raise base speed from 70->130 px/sec
(~128ms -> ~69ms per step) for snappier, more responsive play. Progressive
difficulty (+1 px/sec per 5 food) is unchanged.
Tetris: replace solid-green blocks with Gameboy-style 4-shade beveled cells
(pale top/left highlight, medium body, dark bottom/right shadow). Settled
blocks use C_GB_MED, the active piece uses C_GB_LIGHT, and the next-piece
preview is beveled too.
Font: add missing bitmap glyphs (/ # * + =) to both Snake and Tetris so
control labels like '4/6 MOVE' and '# PAUSE' render correctly. Also hoist
XSetForeground out of the per-pixel loop in draw_text (both games).
Snake: remove the wall-collision death; the head now wraps through the
opposite edge ((hx+COLS)%COLS, (hy+ROWS)%ROWS). Base speed raised from
70->100 px/sec (~128ms -> ~90ms per step) so it feels less laggy; the
+1 px/sec per 5 food progressive difficulty is unchanged.
Snake: speed derived from pixels/second; +1 px/sec per 5 food eaten
(pps = 70 + food/5, step = cell*1000/pps, floored at 40ms).
Tetris: speed derived from pixels/second; +1 px/sec per 10 lines cleared
(pps = 23 + lines/10, step = CELL*1000/pps, floored at 40ms), replacing
the old level-driven formula. Level still drives the score multiplier.
Tetris: list all controls concisely in the free space of the right panel
(KEYS, 4/6 MOVE, 2 ROTATE, 8 SOFT, 0 DROP, # PAUSE, 5 OK).
Draw a bright vertical divider in the gap between the playfield and the side
panel so the two areas are clearly separated, and render the NEXT piece larger
(pcell 6 -> 11) centered inside a framed preview box.
dwm adds a borderpx (1) border around every client, so a window sized to
fill the usable area (240x296) overflowed the 240x320 screen by 2px on the
right and bottom, clipping the native window border. Offset the requested
position by the border width and shrink by 3*borderpx so the framed window
exactly meets the screen edges with the native border fully visible.
- Tetris: replace pivot-based rotation with explicit 4-state SHAPES
table (classic SRS-style) so rotation is correct and the O piece no
longer drifts on rotate; keep simple wall-kick offsets.
- Both: render immediately after processing input (not only on gravity
steps) so rotation/movement is no longer laggy.
- Both: draw the window frame inset by 2-3px (double line) so the right
and bottom edges are actually visible instead of clipped at the edge.
- Font: add missing glyphs (U, W and complete A-Z) to both Snake and
Tetris 5x7 bitmap fonts so PAUSE/RESUME/NEW GAME render correctly.
- Tetris: the gravity loop never advanced nextTick after a step, so the
piece dropped almost instantly every frame. Advance nextTick by speed
after each step (matching Snake) so gravity honours the configured rate.
- Both: draw a 1px frame around the whole 240x296 window (right/bottom
edges were previously absent) and clear the playfield to black behind
PAUSE/GAME OVER text for contrast.
- Both: fix centering of overlay labels (GAME OVER, 5 RESUME, 5 NEW GAME).
Snake: the snake was initialized with its head at the left end while
moving right, so the first gravity step walked the head into its own
body and the game started at Game Over. Initialize the head at the
right end (cx) so the initial direction is valid.
Tetris: enlarge the playfield cell (10 -> 12px) so pieces are not too
small, and slow gravity (800ms base, gentler per-level ramp, 250ms
floor) so pieces do not fall too fast.
Both: brighten the secondary/dim text colour (0x0a3a0a -> 0x2f9f2f)
so score/label/menu text is actually legible on the black background.
Also wire the Games section into the real main menu: the system menu
is hardcoded in the dwm-plugin dmenu (src/sxmo-dmenu/dmenu.c), not in
sxmo_appmenu.sh, so add a 'games' builtin menu that loads
sxmo_hook_games.sh and a Games entry after Apps in the sys menu, plus
scripts/build-dmenu.sh to cross-compile dmenu.so.
Add two standalone X11 games (not DWM plugins) for the Nokia 8110 keypad:
- src/games/snake.cpp: Nokia Snake II-style game, 240x296, built-in 5x7 font
- src/games/tetris.cpp: Tetris with next-piece preview, levels, line clears
Both float as dialog windows in dwm, use a self-contained bitmap font (no
font-file dependency), and are controlled via the numeric keypad (2/4/6/8
directions, 5 action) with arrow-key fallbacks.
- scripts/build-games.sh: Docker armv7 cross-compile (mirrors build-sxmo-x11.sh)
- overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_games.sh: Games menu hook
- overlay/rootfs/usr/bin/sxmo_appmenu.sh: add 'Games' entry after 'Apps' and a
games submenu case
Binaries committed to prebuilt/games and overlay/rootfs/usr/bin per repo
convention. Deployed and verified on the live device (window maps, dwm focuses
it, keypad input reaches the game).
The previous commit disabled sxmo_idle.sh and the idle_locker in the
unlock/lock hooks, leaving the display backlight pinned at maximum
brightness forever (Xorg runs with -s 0 -dpms and /sys/power/state is
empty, so the panel never blanks). On a phone with no suspend this is by
far the dominant battery drain.
Restore sxmo_idle.sh and start idle_locker from the unlock and lock hooks
so the screen blanks after SXMO_UNLOCK_IDLE_TIME (default 30s) / once
locked after SXMO_LOCK_IDLE_TIME (default 8s). Locking itself stays
explicit-only, matching the documented design; only the display blanks.
Verified live on the device: after the idle timeout the state transitions
to screenoff and /sys/class/backlight/backlight/brightness drops to 0.
SXMO_OS is always bananarchy (an Alpine derivative) on this device, so
the SXMO_OS case, the change_systemd branch, and the nixos branch were
dead code. Drop them and always run the Alpine flow
(doas setup-timezone -z), which is what bananarchy uses.