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:
@@ -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