perf(render): redraw on demand and only changed cells

Rendering used to clear the whole canvas and redraw every cell on
every frame callback (~60 fps) even though the simulation ticks at
20 tps and starts paused, keeping CPU usage at ~85% while idle.

Split simulation timing out of redraw() into the event loop and
render only when displayed content can have changed (tick, painting,
pattern placement, preview movement, grid-line toggle, resize);
idle frames commit only a new frame callback. Repaint incrementally
by diffing the grid against a rendered-state snapshot per row and
redrawing just the changed cells, with damage limited to the
repainted bounding box.

Idle CPU drops to ~1% of one core; running frames render at tick
rate (~20 fps) at microsecond cost. Adds grid_copy() snapshot
helper with tests and archives the optimize-render-on-demand change
with the new on-demand-rendering spec.
This commit is contained in:
2026-11-08 20:11:55 +01:00
parent 80fa575317
commit 81a1b26842
10 changed files with 510 additions and 99 deletions
+231 -99
View File
@@ -50,7 +50,10 @@ int need_resize = 1;
grid_t grid;
grid_t grid_back;
grid_t grid_rendered;
int sim_paused = 1;
int screen_dirty = 1;
int full_redraw = 1;
double sim_interval = 0.05; /* 20 tps */
double sim_accumulator = 0.0;
struct timespec sim_last_time;
@@ -58,6 +61,7 @@ int sim_time_initialized = 0;
int mouse_left_down = 0;
int mouse_right_down = 0;
double cursor_x = 0, cursor_y = 0;
double preview_cursor_x = 0, preview_cursor_y = 0;
int click_pending = 0;
double click_x = 0, click_y = 0;
int show_grid_lines = 1;
@@ -242,7 +246,105 @@ const struct wl_buffer_listener wl_buffer_listener = {
void redraw(); // forward declaration
static void simulation_tick(void);
static int simulation_tick(void); /* returns 1 if the grid changed */
static void canvas_resize(int log_w, int log_h, double scale);
extern const struct wl_callback_listener wl_surface_frame_listener;
/* Per-wakeup update: advance simulation time, apply input-driven grid
* changes, then either render (if dirty and the buffer is free) or
* request the next frame callback without new content. */
static void loop_update(void)
{
if (need_resize) {
canvas_resize(logical_width, logical_height, current_scale);
screen_dirty = 1;
full_redraw = 1;
}
/* update cursor from pointer */
cursor_x = pointer_data.x;
cursor_y = pointer_data.y;
/* pattern preview follows the cursor */
if (active_tool >= 0 && (cursor_x != preview_cursor_x || cursor_y != preview_cursor_y)) {
screen_dirty = 1;
preview_cursor_x = cursor_x;
preview_cursor_y = cursor_y;
}
/* tick timing */
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if (!sim_time_initialized) {
sim_last_time = now;
sim_time_initialized = 1;
}
double dt = (now.tv_sec - sim_last_time.tv_sec) + (now.tv_nsec - sim_last_time.tv_nsec) / 1e9;
sim_last_time = now;
if (!sim_paused) {
sim_accumulator += dt;
while (sim_accumulator >= sim_interval) {
if (simulation_tick()) {
screen_dirty = 1;
}
sim_accumulator -= sim_interval;
}
}
/* paused-only editing */
if (sim_paused && active_tool < 0) {
if (mouse_left_down || mouse_right_down) {
int cx = (int)(cursor_x / 8.0);
int cy = (int)(cursor_y / 8.0);
if (cx >= 0 && cy >= 0 && cx < grid.cols && cy < grid.rows) {
if (mouse_left_down) {
if (!grid_get(&grid, cx, cy)) screen_dirty = 1;
grid_set(&grid, cx, cy, 1);
}
if (mouse_right_down) {
if (grid_get(&grid, cx, cy)) screen_dirty = 1;
grid_set(&grid, cx, cy, 0);
}
}
}
}
/* handle pending click placement */
if (click_pending) {
click_pending = 0;
if (active_tool >= 0) {
int cx = (int)(click_x / 8.0);
int cy = (int)(click_y / 8.0);
int dx[16], dy[16];
int n = pattern_rotated_offsets(active_tool, active_rotation, dx, dy);
int ok = 1;
for (int i = 0; i < n; i++) {
int c = cx + dx[i];
int r = cy + dy[i];
if (c < 0 || r < 0 || c >= grid.cols || r >= grid.rows) { ok = 0; break; }
}
if (ok) {
for (int i = 0; i < n; i++) {
grid_set(&grid, cx + dx[i], cy + dy[i], 1);
}
screen_dirty = 1;
}
}
}
if (screen_dirty && released) {
redraw();
released = 0;
screen_dirty = 0;
full_redraw = 0;
} else {
/* nothing new (or buffer still busy): keep the frame callback
* alive without presenting new content */
canvas.frame = wl_surface_frame(canvas.surface);
wl_callback_add_listener(canvas.frame, &wl_surface_frame_listener, NULL);
wl_surface_commit(canvas.surface);
}
}
static void handle_key(uint32_t key, uint32_t state, xkb_keysym_t sym, uint32_t mods, void *user_data)
{
@@ -260,34 +362,36 @@ static void handle_key(uint32_t key, uint32_t state, xkb_keysym_t sym, uint32_t
break;
case ACTION_CANCEL:
active_tool = -1;
screen_dirty = 1;
printf("tool cancelled\n");
break;
case ACTION_ROTATE:
if (active_tool >= 0) {
active_rotation = (active_rotation + 1) % 4;
screen_dirty = 1;
printf("rotation=%d\n", active_rotation);
}
break;
case ACTION_SELECT_GLIDER:
active_tool = 0; active_rotation = 0; printf("selected glider\n"); break;
active_tool = 0; active_rotation = 0; screen_dirty = 1; printf("selected glider\n"); break;
case ACTION_SELECT_BLINKER:
active_tool = 1; active_rotation = 0; printf("selected blinker\n"); break;
active_tool = 1; active_rotation = 0; screen_dirty = 1; printf("selected blinker\n"); break;
case ACTION_SELECT_BLOCK:
active_tool = 2; active_rotation = 0; printf("selected block\n"); break;
active_tool = 2; active_rotation = 0; screen_dirty = 1; printf("selected block\n"); break;
case ACTION_SELECT_TOAD:
active_tool = 3; active_rotation = 0; printf("selected toad\n"); break;
active_tool = 3; active_rotation = 0; screen_dirty = 1; printf("selected toad\n"); break;
case ACTION_SELECT_BEACON:
active_tool = 4; active_rotation = 0; printf("selected beacon\n"); break;
active_tool = 4; active_rotation = 0; screen_dirty = 1; printf("selected beacon\n"); break;
case ACTION_SELECT_BOAT:
active_tool = 5; active_rotation = 0; printf("selected boat\n"); break;
active_tool = 5; active_rotation = 0; screen_dirty = 1; printf("selected boat\n"); break;
case ACTION_SELECT_TUB:
active_tool = 6; active_rotation = 0; printf("selected tub\n"); break;
active_tool = 6; active_rotation = 0; screen_dirty = 1; printf("selected tub\n"); break;
case ACTION_SELECT_BEEHIVE:
active_tool = 7; active_rotation = 0; printf("selected beehive\n"); break;
active_tool = 7; active_rotation = 0; screen_dirty = 1; printf("selected beehive\n"); break;
case ACTION_SELECT_RPENTOMINO:
active_tool = 8; active_rotation = 0; printf("selected rpentomino\n"); break;
active_tool = 8; active_rotation = 0; screen_dirty = 1; printf("selected rpentomino\n"); break;
case ACTION_SELECT_ACORN:
active_tool = 9; active_rotation = 0; printf("selected acorn\n"); break;
active_tool = 9; active_rotation = 0; screen_dirty = 1; printf("selected acorn\n"); break;
case ACTION_CLEAR:
printf("clear requested\n");
break;
@@ -298,7 +402,7 @@ static void handle_key(uint32_t key, uint32_t state, xkb_keysym_t sym, uint32_t
break;
case ACTION_STEP:
if (sim_paused) {
simulation_tick();
if (simulation_tick()) screen_dirty = 1;
printf("stepped\n");
}
break;
@@ -314,6 +418,8 @@ static void handle_key(uint32_t key, uint32_t state, xkb_keysym_t sym, uint32_t
break;
case ACTION_TOGGLE_GRID:
show_grid_lines = !show_grid_lines;
screen_dirty = 1;
full_redraw = 1;
break;
default:
break;
@@ -350,11 +456,6 @@ void wl_callack_done(void *data, struct wl_callback *callback, uint32_t time)
wl_callback_destroy(canvas.frame);
canvas.frame = NULL;
}
if (released)
{
redraw();
released = 0;
}
}
const struct wl_callback_listener wl_surface_frame_listener = {
@@ -395,6 +496,7 @@ static void canvas_resize(int log_w, int log_h, double scale) {
grid_resize(&grid, log_w / 8, log_h / 8);
grid_resize(&grid_back, grid.cols, grid.rows);
grid_resize(&grid_rendered, grid.cols, grid.rows);
need_resize = 0;
printf("canvas resized logical=%dx%d phys=%dx%d scale=%g grid=%dx%d\n", log_w, log_h, phys_w, phys_h, scale, grid.cols, grid.rows);
}
@@ -471,11 +573,12 @@ int main(void)
else snprintf(config_path, sizeof(config_path), ".config/wlgol/keys.conf");
g_config = config_load(config_path);
redraw();
released = 1; /* no buffer presented yet */
loop_update();
while (wl_display_dispatch(display) != -1 && running)
{
// nothing inside
loop_update();
}
xdg_toplevel_destroy(xdg_toplevel);
@@ -502,6 +605,10 @@ int main(void)
int framecounter = 0;
/* cells covered by the pattern preview at the last rendered frame */
static int last_preview[16][2];
static int last_preview_count = 0;
static void fill_rect(int x0, int y0, int w, int h, unsigned char r, unsigned char g, unsigned char b, unsigned char a) {
if (w <= 0 || h <= 0) return;
for (int y = y0; y < y0 + h && y < canvas.height; y++) {
@@ -533,12 +640,13 @@ static int count_neighbors(int c, int r) {
return n;
}
static void simulation_tick(void) {
static int simulation_tick(void) {
if (grid_back.cols != grid.cols || grid_back.rows != grid.rows) {
grid_free(&grid_back);
grid_init(&grid_back, grid.cols, grid.rows);
}
grid_clear(&grid_back);
int changed = 0;
for (int r = 0; r < grid.rows; r++) {
for (int c = 0; c < grid.cols; c++) {
int alive = grid_get(&grid, c, r);
@@ -546,6 +654,7 @@ static void simulation_tick(void) {
int next = 0;
if (alive && (nb == 2 || nb == 3)) next = 1;
if (!alive && nb == 3) next = 1;
if (next != alive) changed = 1;
grid_set(&grid_back, c, r, next);
}
}
@@ -553,95 +662,100 @@ static void simulation_tick(void) {
unsigned char *tmp = grid.cells;
grid.cells = grid_back.cells;
grid_back.cells = tmp;
return changed;
}
/* Repaint one cell: background, the grid-line fragments it contains
* (its left and top edges, plus the trailing edge after the last
* column/row) and the cell body if alive. */
static void repaint_cell(int c, int r, int cell_px)
{
int x = c * cell_px;
int y = r * cell_px;
int w = cell_px;
int h = cell_px;
if (c == grid.cols - 1) w = canvas.width - x;
if (r == grid.rows - 1) h = canvas.height - y;
fill_rect(x, y, w, h, 20, 20, 24, 255);
if (show_grid_lines && grid.show_lines) {
fill_rect(x, y, 1, h, 40, 40, 48, 255);
fill_rect(x, y, w, 1, 40, 40, 48, 255);
if (c == grid.cols - 1 && (c + 1) * cell_px < canvas.width)
fill_rect((c + 1) * cell_px, y, 1, h, 40, 40, 48, 255);
if (r == grid.rows - 1 && (r + 1) * cell_px < canvas.height)
fill_rect(x, (r + 1) * cell_px, w, 1, 40, 40, 48, 255);
}
if (grid_get(&grid, c, r)) {
fill_rect(x + 1, y + 1, cell_px - 2, cell_px - 2, 80, 200, 120, 255);
}
}
void redraw()
{
if (need_resize) {
canvas_resize(logical_width, logical_height, current_scale);
}
clock_t start = clock();
/* update cursor from pointer */
cursor_x = pointer_data.x;
cursor_y = pointer_data.y;
/* tick timing */
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if (!sim_time_initialized) {
sim_last_time = now;
sim_time_initialized = 1;
}
double dt = (now.tv_sec - sim_last_time.tv_sec) + (now.tv_nsec - sim_last_time.tv_nsec) / 1e9;
sim_last_time = now;
if (!sim_paused) {
sim_accumulator += dt;
while (sim_accumulator >= sim_interval) {
simulation_tick();
sim_accumulator -= sim_interval;
}
}
/* paused-only editing */
if (sim_paused && active_tool < 0) {
if (mouse_left_down || mouse_right_down) {
int cx = (int)(cursor_x / 8.0);
int cy = (int)(cursor_y / 8.0);
if (cx >= 0 && cy >= 0 && cx < grid.cols && cy < grid.rows) {
if (mouse_left_down) grid_set(&grid, cx, cy, 1);
if (mouse_right_down) grid_set(&grid, cx, cy, 0);
}
}
}
/* handle pending click placement */
if (click_pending) {
click_pending = 0;
if (active_tool >= 0) {
int cx = (int)(click_x / 8.0);
int cy = (int)(click_y / 8.0);
int dx[16], dy[16];
int n = pattern_rotated_offsets(active_tool, active_rotation, dx, dy);
int ok = 1;
for (int i = 0; i < n; i++) {
int c = cx + dx[i];
int r = cy + dy[i];
if (c < 0 || r < 0 || c >= grid.cols || r >= grid.rows) { ok = 0; break; }
}
if (ok) {
for (int i = 0; i < n; i++) {
grid_set(&grid, cx + dx[i], cy + dy[i], 1);
}
}
}
}
int cell_px = (grid.cols > 0) ? (canvas.width / grid.cols) : 8;
if (cell_px < 1) cell_px = 1;
/* clear background */
fill_rect(0, 0, canvas.width, canvas.height, 20, 20, 24, 255);
/* damage bounding box, in cells (logical damage = cells * 8 px) */
int dmg_c0 = 0, dmg_r0 = 0, dmg_c1 = grid.cols - 1, dmg_r1 = grid.rows - 1;
/* grid lines */
if (show_grid_lines && grid.show_lines) {
for (int c = 0; c <= grid.cols; c++) {
int x = c * cell_px;
fill_rect(x, 0, 1, canvas.height, 40, 40, 48, 255);
}
for (int r = 0; r <= grid.rows; r++) {
int y = r * cell_px;
fill_rect(0, y, canvas.width, 1, 40, 40, 48, 255);
}
}
if (full_redraw) {
/* clear background */
fill_rect(0, 0, canvas.width, canvas.height, 20, 20, 24, 255);
/* alive cells */
for (int r = 0; r < grid.rows; r++) {
for (int c = 0; c < grid.cols; c++) {
if (grid_get(&grid, c, r)) {
fill_rect(c * cell_px + 1, r * cell_px + 1, cell_px - 2, cell_px - 2, 80, 200, 120, 255);
/* grid lines */
if (show_grid_lines && grid.show_lines) {
for (int c = 0; c <= grid.cols; c++) {
int x = c * cell_px;
fill_rect(x, 0, 1, canvas.height, 40, 40, 48, 255);
}
for (int r = 0; r <= grid.rows; r++) {
int y = r * cell_px;
fill_rect(0, y, canvas.width, 1, 40, 40, 48, 255);
}
}
/* alive cells */
for (int r = 0; r < grid.rows; r++) {
for (int c = 0; c < grid.cols; c++) {
if (grid_get(&grid, c, r)) {
fill_rect(c * cell_px + 1, r * cell_px + 1, cell_px - 2, cell_px - 2, 80, 200, 120, 255);
}
}
}
} else {
dmg_c0 = grid.cols; dmg_r0 = grid.rows; dmg_c1 = -1; dmg_r1 = -1;
/* repaint cells whose state differs from the last rendered frame */
for (int r = 0; r < grid.rows; r++) {
if (memcmp(grid.cells + (size_t)r * grid.cols,
grid_rendered.cells + (size_t)r * grid_rendered.cols,
(size_t)grid.cols) == 0) {
continue;
}
for (int c = 0; c < grid.cols; c++) {
if (grid_get(&grid, c, r) != grid_get(&grid_rendered, c, r)) {
repaint_cell(c, r, cell_px);
if (c < dmg_c0) dmg_c0 = c;
if (c > dmg_c1) dmg_c1 = c;
if (r < dmg_r0) dmg_r0 = r;
if (r > dmg_r1) dmg_r1 = r;
}
}
}
/* cells under the previous preview were overlaid - repaint them */
for (int i = 0; i < last_preview_count; i++) {
int c = last_preview[i][0], r = last_preview[i][1];
repaint_cell(c, r, cell_px);
if (c < dmg_c0) dmg_c0 = c;
if (c > dmg_c1) dmg_c1 = c;
if (r < dmg_r0) dmg_r0 = r;
if (r > dmg_r1) dmg_r1 = r;
}
}
@@ -664,14 +778,32 @@ void redraw()
int c = cx + dx[i];
int r = cy + dy[i];
fill_rect(c * cell_px + 2, r * cell_px + 2, cell_px - 4, cell_px - 4, pr, pg, pb, 160);
last_preview[i][0] = c;
last_preview[i][1] = r;
if (c < dmg_c0) dmg_c0 = c;
if (c > dmg_c1) dmg_c1 = c;
if (r < dmg_r0) dmg_r0 = r;
if (r > dmg_r1) dmg_r1 = r;
}
last_preview_count = n;
} else {
last_preview_count = 0;
}
/* remember the rendered state for the next diff */
grid_copy(&grid_rendered, &grid);
// callback
canvas.frame = wl_surface_frame(canvas.surface);
wl_callback_add_listener(canvas.frame, &wl_surface_frame_listener, NULL);
wl_surface_attach(canvas.surface, canvas.buffer, 0, 0);
wl_surface_damage(canvas.surface, 0, 0, canvas.width, canvas.height);
if (full_redraw) {
wl_surface_damage(canvas.surface, 0, 0, logical_width, logical_height);
} else if (dmg_c1 >= dmg_c0 && dmg_r1 >= dmg_r0) {
/* surface-local (logical) space: one cell is 8x8 logical px */
wl_surface_damage(canvas.surface, dmg_c0 * 8, dmg_r0 * 8,
(dmg_c1 - dmg_c0 + 1) * 8, (dmg_r1 - dmg_r0 + 1) * 8);
}
wl_surface_commit(canvas.surface);
framecounter++;
@@ -684,7 +816,7 @@ void redraw()
{
clock_t end = clock();
double dur = ((double)end - start) / CLOCKS_PER_SEC;
printf("FPS: %d in %g sec\n", framecounter, dur);
printf("Rendered frames: %d in %g sec CPU\n", framecounter, dur);
mytime = ntime;
framecounter = 0;
}