merge: Optymalizacja wdyajności rysowania
This commit is contained in:
@@ -45,6 +45,16 @@ void grid_clear(grid_t *g) {
|
||||
memset(g->cells, 0, (size_t)g->cols * (size_t)g->rows);
|
||||
}
|
||||
|
||||
void grid_copy(grid_t *dst, const grid_t *src) {
|
||||
if (!dst || !src || !src->cells) return;
|
||||
if (dst->cols != src->cols || dst->rows != src->rows) {
|
||||
grid_resize(dst, src->cols, src->rows);
|
||||
}
|
||||
if (dst->cols == src->cols && dst->rows == src->rows && dst->cells) {
|
||||
memcpy(dst->cells, src->cells, (size_t)src->cols * (size_t)src->rows);
|
||||
}
|
||||
}
|
||||
|
||||
int grid_get(const grid_t *g, int c, int r) {
|
||||
if (!g || !g->cells) return 0;
|
||||
if (c < 0 || r < 0 || c >= g->cols || r >= g->rows) return 0;
|
||||
|
||||
@@ -12,6 +12,7 @@ void grid_init(grid_t *g, int cols, int rows);
|
||||
void grid_free(grid_t *g);
|
||||
void grid_resize(grid_t *g, int new_cols, int new_rows);
|
||||
void grid_clear(grid_t *g);
|
||||
void grid_copy(grid_t *dst, const grid_t *src);
|
||||
int grid_get(const grid_t *g, int c, int r);
|
||||
void grid_set(grid_t *g, int c, int r, int val);
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-11-08
|
||||
@@ -0,0 +1,74 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
`redraw()` in `wlgol.c` currently does everything: accumulates time and runs simulation ticks, handles editing, clears the whole framebuffer, draws grid lines, scans all cells, and commits. It is invoked from `wl_callack_done` whenever the (single) buffer is released, i.e. at display refresh rate. The simulation ticks at ~20 tps and starts paused, so most frames are identical repaints. The app renders into one persistent SHM buffer and waits for its release before reattaching.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Near-zero CPU when paused and idle; render cost proportional to changed cells while running.
|
||||
- Keep the visible output identical to the current renderer.
|
||||
- Keep the blocking `wl_display_dispatch` event-loop model.
|
||||
|
||||
**Non-Goals:**
|
||||
- Optimizing `fill_rect` itself (SIMD-style row fills); a full repaint is now rare.
|
||||
- Double buffering, EGL/Vulkan, subsurfaces.
|
||||
- Changing simulation semantics, editing rules, or key bindings.
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Simulation timing moves out of the renderer into the event loop
|
||||
Tick accumulation (`sim_accumulator`, `simulation_tick()`) runs once per loop iteration after dispatch, not inside `redraw()`. Rationale: if rendering is skipped, ticks must still occur or the simulation freezes. The event loop stays blocking on `wl_display_dispatch`; each frame callback (or input event) wakes it.
|
||||
|
||||
### 2. Idle = empty commit
|
||||
When nothing is dirty, the loop attaches nothing and commits only a freshly requested `wl_surface_frame` callback. This keeps frame callbacks flowing (the loop stays event-driven) without giving the compositor new content. Alternative considered: a timer fd / sleep-based loop — rejected, it changes the event-loop model for no benefit since frame callbacks already provide the heartbeat.
|
||||
|
||||
### 3. Dirty sources, one flag each
|
||||
A single `screen_dirty` flag plus a `full_redraw` flag, set by:
|
||||
- simulation tick applied → `screen_dirty`
|
||||
- grid modification (paint drag, click placement) → `screen_dirty`
|
||||
- pointer motion/crossing while `active_tool >= 0` → `screen_dirty`
|
||||
- tool select / rotate / grid-line toggle → `screen_dirty` + `full_redraw`
|
||||
- `need_resize` → `screen_dirty` + `full_redraw`
|
||||
|
||||
Rationale: content has exactly five sources of truth (ticks, edits, preview, toggle, resize); covering all of them is what makes "skip the frame" safe. Forgetting the preview path is the classic bug here — without it the cursor preview would smear.
|
||||
|
||||
### 4. Diff against a rendered-state snapshot, not dirty-tracking in `grid_set`
|
||||
Add `grid_rendered` (a third grid buffer alongside `grid`/`grid_back`). After each render, copy the current grid into it. At render time, diff row-by-row with `memcmp` (9216 bytes total — negligible) and repaint only differing cells. Alternatives:
|
||||
- Marking dirty cells inside `grid_set` — rejected: every call site becomes a correctness dependency, and `simulation_tick` swaps whole buffers anyway, so per-set tracking would need extra bookkeeping after the swap.
|
||||
- Diffing `grid` vs `grid_back` (previous generation) — rejected: `grid_back` is clobbered by the next tick's computation and misses edits made between ticks.
|
||||
|
||||
The snapshot approach is independent of *why* the grid changed, so ticks, painting, and pattern placement all work through one mechanism.
|
||||
|
||||
### 5. Per-cell repaint includes shared edges
|
||||
Repainting cell (c, r) draws: background fill for the cell area, the cell body if alive, and the grid-line fragments on all four of its edges (x = c·cell_px and (c+1)·cell_px columns, y = r·cell_px and (r+1)·cell_px rows, clipped to the cell). When a neighbor didn't change, its edge gets redrawn identically — harmless. When it did change, it repaints itself. This avoids the "line between two cells vanishes" bug that cell-local-only edges would cause.
|
||||
|
||||
### 6. Preview = extra dirty cells, drawn last
|
||||
The preview is overlay state, not grid state, so `grid_rendered` cannot track it. At render time, the dirty cell set is: `diff(grid, grid_rendered)` ∪ `preview_cells(last frame)` ∪ `preview_cells(this frame)`. Store the last preview's cells (and whether it was shown) between frames. Preview cells are repainted (so the overlay disappears correctly) and the new overlay is drawn after all cell work, preserving the current draw order.
|
||||
|
||||
### 7. Keep the single buffer; gate real frames on release
|
||||
Incremental rendering relies on the framebuffer surviving between frames — the existing single SHM buffer already provides that. When dirty, render only if the buffer is `released`; otherwise leave `screen_dirty` set and render on the next frame callback after release (same gating as today, just conditional). Alternative: double buffering + copying the front buffer — rejected as a bigger change for no additional benefit at this scale.
|
||||
|
||||
### 8. Damage = bounding box of repainted cells
|
||||
`wl_surface_damage` accepts one rectangle per commit; use the bounding box of all cells repainted this frame (whole canvas for full redraws, empty for idle commits). Per-cell damage rects would need `wl_surface_damage_buffer` per rect — not worth the complexity for a SHM client where the compositor-side gain is small.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Empty-commit frame callbacks may throttle or behave inconsistently across compositors] → The heartbeat is only used for timing ticks and deferred renders; if a compositor stops calling back, the next input event still wakes the loop. Behavior on the reference compositor (existing setup) is verified manually.
|
||||
- [Preview drag while painting causes near-continuous rendering] → Accepted: that is genuinely changing content every frame, and per-frame cost is now proportional to the moved cells, not the canvas.
|
||||
- [FPS counter now counts rendered frames, not display refreshes] → Expected side effect of skipping idle frames; the log line semantics shift with this change.
|
||||
- [Diff covers whole grid on dense animations] → `full_redraw` style fallback is not needed; even a full-diff repaint via per-cell work stays correct, just slower. Revisit only if it shows in practice.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Two independently verifiable stages, both internal to `wlgol.c` (plus a `grid_rendered` snapshot helper):
|
||||
|
||||
1. Event-driven rendering: move tick timing out of `redraw()`, add dirty flags and the empty-commit idle path. Verify: paused idle CPU ≈ 0%, running still animates, editing/preview still update.
|
||||
2. Incremental repaint: add `grid_rendered`, diff, per-cell repaint, bounded damage. Verify: no visual regression vs stage 1 (grid lines intact at shared edges, preview clears correctly).
|
||||
|
||||
Rollback is a plain `git revert` — no data or protocol changes.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
The render loop repaints the full canvas on every frame callback (~60 fps) even though the simulation ticks at only 20 tps and starts paused, so most frames draw an identical image. This keeps CPU usage at ~85% during idle, dominated by the full-canvas clear and the full grid scan.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Split simulation timing out of `redraw()`; ticks are driven by the event loop, not by the render call.
|
||||
- Render a frame only when displayed content can have changed: a simulation tick, cell edit or pattern placement, pattern preview movement (pointer motion, tool selection, rotation), grid-line toggle, or canvas resize.
|
||||
- When nothing changed, commit an empty frame (frame callback without attaching a new buffer), so the idle loop costs nearly nothing.
|
||||
- Draw incrementally: keep a snapshot of the grid as last rendered, diff it per row against the current grid, and repaint only changed cells (background, grid-line fragments, cell) instead of clearing the whole canvas.
|
||||
- Report damage for the changed region instead of the whole surface.
|
||||
- Full-canvas repaint remains for resize, grid-line toggle, and when the diff covers most of the grid.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `on-demand-rendering`: Scheduling of window repaints — when a frame is produced (only on content change) and how changed cells are drawn incrementally.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
## Impact
|
||||
|
||||
- `wlgol.c`: render loop (`redraw`, `wl_callack_done`, `main` dispatch loop), simulation tick timing, buffer commit/damage logic.
|
||||
- `include/grid.h` / `grid.c`: possible helper for snapshotting the grid (no behavioral change to existing grid functions).
|
||||
- No protocol, dependency, or configuration changes. Rendering output is visually identical to the current renderer; only the frequency and extent of repainting change.
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
Schedules window repaints for the Game of Life canvas: frames are produced only when displayed content changes, and only the changed cells are drawn, so idle CPU usage is near zero while the visible output stays identical to a full repaint.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Frame is rendered only on content change
|
||||
The system SHALL produce a new rendered frame only when displayed content has changed since the last rendered frame, and SHALL NOT redraw when it has not.
|
||||
|
||||
#### Scenario: Idle while paused
|
||||
- **WHEN** the simulation is paused, no tool is active, and no input arrives
|
||||
- **THEN** no new frame is rendered over consecutive display refreshes
|
||||
|
||||
#### Scenario: Tick changes grid
|
||||
- **WHEN** a simulation tick changes the grid while the simulation is running
|
||||
- **THEN** a new frame is rendered that reflects the new generation
|
||||
|
||||
#### Scenario: Edit marks content dirty
|
||||
- **WHEN** the user paints cells or places a pattern
|
||||
- **THEN** a new frame is rendered with the modified cells
|
||||
|
||||
#### Scenario: Preview follows pointer
|
||||
- **WHEN** a tool is selected and the pointer moves to a different cell
|
||||
- **THEN** a new frame is rendered with the pattern preview at the new position
|
||||
|
||||
#### Scenario: Preview-affecting action
|
||||
- **WHEN** the user selects a tool, rotates the active tool, or toggles grid lines
|
||||
- **THEN** a new frame is rendered reflecting the change
|
||||
|
||||
#### Scenario: Resize forces full redraw
|
||||
- **WHEN** the canvas size or scale changes
|
||||
- **THEN** a new frame is rendered over the whole canvas
|
||||
|
||||
### Requirement: Simulation advances independently of rendering
|
||||
The system SHALL accumulate simulation time and apply ticks based on elapsed time, regardless of whether frames are rendered.
|
||||
|
||||
#### Scenario: Ticks accumulate while idle
|
||||
- **WHEN** multiple tick intervals elapse without a rendered frame
|
||||
- **THEN** every elapsed interval results in exactly one applied tick
|
||||
|
||||
### Requirement: Only changed cells are repainted
|
||||
The system SHALL redraw only the cells whose state differs from the last rendered frame, drawing each changed cell's background, grid-line fragments, and cell state, and SHALL leave unchanged cells' pixels untouched.
|
||||
|
||||
#### Scenario: Single cell changes
|
||||
- **WHEN** exactly one cell changes state between rendered frames
|
||||
- **THEN** only that cell's pixel area is rewritten in the framebuffer
|
||||
|
||||
#### Scenario: Unchanged pixels are preserved
|
||||
- **WHEN** a frame is rendered after a change
|
||||
- **THEN** pixels of unchanged cells are identical to the previous frame
|
||||
|
||||
### Requirement: Damage is limited to the changed region
|
||||
The system SHALL report surface damage covering only the area repainted in the current frame.
|
||||
|
||||
#### Scenario: Small change damages small region
|
||||
- **WHEN** only a few cells changed
|
||||
- **THEN** the reported damage is limited to the union of those cells' areas rather than the whole surface
|
||||
@@ -0,0 +1,20 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Event-driven rendering (stage 1)
|
||||
|
||||
- [x] 1.1 Move simulation tick timing (dt accumulation, `simulation_tick()` calls) out of `redraw()` into the main dispatch loop; verify with `ctest --test-dir build` and by running `./build/wlgol` that animation still advances when unpaused
|
||||
- [x] 1.2 Add `screen_dirty` / `full_redraw` flags set from all content sources (tick, paint drag, click placement, pointer motion with active tool, tool select/rotate, grid-line toggle, `need_resize`); verify each source produces a repaint by exercising it in the app
|
||||
- [x] 1.3 Rework `wl_callack_done`/loop so a clean frame commits only a new frame callback (no attach, no damage) and a dirty frame renders only when the buffer is `released`, otherwise defers to the next callback; verify with `top` that paused idle CPU is ~0% and that unpausing at 20 tps renders without visual glitches
|
||||
- [x] 1.4 Update the FPS log line to report rendered frames per second and verify the printed values match expected render counts (≈20 while running, 0 while idle)
|
||||
|
||||
## 2. Incremental repaint (stage 2)
|
||||
|
||||
- [x] 2.1 Add a `grid_rendered` snapshot buffer sized/moved together with `grid` in `canvas_resize` (helper in `grid.c`/`grid.h` is acceptable), and verify it survives a window resize without out-of-bounds access
|
||||
- [x] 2.2 Implement the per-row diff between `grid` and `grid_rendered` and a `repaint_cell()` that draws background, grid-line fragments on all four edges, and the cell body for each changed cell; verify by stepping (`ACTION_STEP`) a single-cell change and inspecting that only that cell's pixels change and shared edges stay intact
|
||||
- [x] 2.3 Track last-frame preview cells (and whether the preview was shown) and add them to the repaint set alongside the diff, drawing the new preview last; verify the preview leaves no smearing when moving the cursor and rotates/selects cleanly
|
||||
- [x] 2.4 Copy `grid` into `grid_rendered` at the end of each rendered frame and report damage as the bounding box of repainted cells (whole canvas on `full_redraw`); verify with `WAYLAND_DEBUG=1 ./build/wlgol` that damage rects match changed cells
|
||||
- [x] 2.5 Extend grid tests in `tests/test_grid.c` for the snapshot helper (resize copy bounds, snapshot/diff round-trip) and verify with `ctest --test-dir build`
|
||||
|
||||
## 3. Integration verification
|
||||
|
||||
- [x] 3.1 Full visual pass vs the pre-change renderer: paused idle, painting while paused, pattern placement and rotation, grid-line toggle, resize with fractional scale, unpause at min/max speed — confirm no visual differences; measure CPU with `top` in each state and record before/after numbers
|
||||
@@ -0,0 +1,59 @@
|
||||
# on-demand-rendering Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Schedules window repaints for the Game of Life canvas: frames are produced only when displayed content changes, and only the changed cells are drawn, so idle CPU usage is near zero while the visible output stays identical to a full repaint.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Frame is rendered only on content change
|
||||
The system SHALL produce a new rendered frame only when displayed content has changed since the last rendered frame, and SHALL NOT redraw when it has not.
|
||||
|
||||
#### Scenario: Idle while paused
|
||||
- **WHEN** the simulation is paused, no tool is active, and no input arrives
|
||||
- **THEN** no new frame is rendered over consecutive display refreshes
|
||||
|
||||
#### Scenario: Tick changes grid
|
||||
- **WHEN** a simulation tick changes the grid while the simulation is running
|
||||
- **THEN** a new frame is rendered that reflects the new generation
|
||||
|
||||
#### Scenario: Edit marks content dirty
|
||||
- **WHEN** the user paints cells or places a pattern
|
||||
- **THEN** a new frame is rendered with the modified cells
|
||||
|
||||
#### Scenario: Preview follows pointer
|
||||
- **WHEN** a tool is selected and the pointer moves to a different cell
|
||||
- **THEN** a new frame is rendered with the pattern preview at the new position
|
||||
|
||||
#### Scenario: Preview-affecting action
|
||||
- **WHEN** the user selects a tool, rotates the active tool, or toggles grid lines
|
||||
- **THEN** a new frame is rendered reflecting the change
|
||||
|
||||
#### Scenario: Resize forces full redraw
|
||||
- **WHEN** the canvas size or scale changes
|
||||
- **THEN** a new frame is rendered over the whole canvas
|
||||
|
||||
### Requirement: Simulation advances independently of rendering
|
||||
The system SHALL accumulate simulation time and apply ticks based on elapsed time, regardless of whether frames are rendered.
|
||||
|
||||
#### Scenario: Ticks accumulate while idle
|
||||
- **WHEN** multiple tick intervals elapse without a rendered frame
|
||||
- **THEN** every elapsed interval results in exactly one applied tick
|
||||
|
||||
### Requirement: Only changed cells are repainted
|
||||
The system SHALL redraw only the cells whose state differs from the last rendered frame, drawing each changed cell's background, grid-line fragments, and cell state, and SHALL leave unchanged cells' pixels untouched.
|
||||
|
||||
#### Scenario: Single cell changes
|
||||
- **WHEN** exactly one cell changes state between rendered frames
|
||||
- **THEN** only that cell's pixel area is rewritten in the framebuffer
|
||||
|
||||
#### Scenario: Unchanged pixels are preserved
|
||||
- **WHEN** a frame is rendered after a change
|
||||
- **THEN** pixels of unchanged cells are identical to the previous frame
|
||||
|
||||
### Requirement: Damage is limited to the changed region
|
||||
The system SHALL report surface damage covering only the area repainted in the current frame.
|
||||
|
||||
#### Scenario: Small change damages small region
|
||||
- **WHEN** only a few cells changed
|
||||
- **THEN** the reported damage is limited to the union of those cells' areas rather than the whole surface
|
||||
@@ -19,6 +19,32 @@ int main(void) {
|
||||
grid_clear(&g);
|
||||
assert(grid_get(&g, 0, 0) == 0);
|
||||
|
||||
/* grid_copy: same-size round trip */
|
||||
grid_t snap;
|
||||
grid_init(&snap, 1, 1);
|
||||
grid_set(&g, 2, 1, 1);
|
||||
grid_copy(&snap, &g);
|
||||
assert(snap.cols == 4 && snap.rows == 4);
|
||||
assert(grid_get(&snap, 2, 1) == 1);
|
||||
assert(grid_get(&snap, 0, 0) == 0);
|
||||
|
||||
/* grid_copy: grow preserves cells, new area is dead */
|
||||
grid_set(&g, 3, 3, 1);
|
||||
grid_resize(&g, 6, 6);
|
||||
grid_copy(&snap, &g);
|
||||
assert(snap.cols == 6 && snap.rows == 6);
|
||||
assert(grid_get(&snap, 2, 1) == 1);
|
||||
assert(grid_get(&snap, 3, 3) == 1);
|
||||
assert(grid_get(&snap, 5, 5) == 0);
|
||||
|
||||
/* grid_copy: shrink truncates out-of-bounds cells */
|
||||
grid_resize(&g, 2, 2);
|
||||
grid_copy(&snap, &g);
|
||||
assert(snap.cols == 2 && snap.rows == 2);
|
||||
assert(grid_get(&snap, 2, 1) == 0);
|
||||
assert(grid_get(&snap, 1, 1) == 0);
|
||||
|
||||
grid_free(&snap);
|
||||
grid_free(&g);
|
||||
printf("grid tests passed\n");
|
||||
return 0;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user