diff --git a/CHANGELOG.md b/CHANGELOG.md index d75ee569..ebc55631 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,9 @@ (https://codeberg.org/dnkl/foot/issues/466). * Background alpha no longer applied to palette or RGB colors that matches the background color. +* Improved performance on compositors that does not release shm + buffers immediately, e.g. KWin + (https://codeberg.org/dnkl/foot/issues/478). ### Deprecated diff --git a/render.c b/render.c index 9bcd11ba..1ba3516a 100644 --- a/render.c +++ b/render.c @@ -759,6 +759,18 @@ render_margin(struct terminal *term, struct buffer *buf, if (term->render.urgency) render_urgency(term, buf); + /* Ensure the updated regions are copied to the next frame's + * buffer when we're double buffering */ + pixman_region32_union_rect( + &buf->dirty, &buf->dirty, 0, 0, term->width, term->margins.top); + pixman_region32_union_rect( + &buf->dirty, &buf->dirty, 0, bmargin, term->width, term->margins.bottom); + pixman_region32_union_rect( + &buf->dirty, &buf->dirty, 0, 0, term->margins.left, term->height); + pixman_region32_union_rect( + &buf->dirty, &buf->dirty, + rmargin, 0, term->margins.right, term->height); + if (apply_damage) { /* Top */ wl_surface_damage_buffer( @@ -2025,13 +2037,150 @@ static const struct wl_callback_listener frame_listener = { .done = &frame_callback, }; +static void +force_full_repaint(struct terminal *term, struct buffer *buf) +{ + tll_free(term->grid->scroll_damage); + render_margin(term, buf, 0, term->rows, true); + term_damage_view(term); +} + +static void +reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old) +{ + static int counter = 0; + static bool have_warned = false; + if (!have_warned && ++counter > 5) { + LOG_WARN("compositor is not releasing buffers immediately; " + "expect lower rendering performance"); + have_warned = true; + } + + if (new->age > 1) { + memcpy(new->mmapped, old->mmapped, new->size); + return; + } + + /* + * TODO: remove this frame’s damage from the region we copy from + * the old frame. + * + * - this frame’s dirty region is only valid *after* we’ve applied + * its scroll damage. + * - last frame’s dirty region is only valid *before* we’ve + * applied this frame’s scroll damage. + * + * Can we transform one of the regions? It’s not trivial, since + * scroll damage isn’t just about counting lines; there may be + * multiple damage records, each with different scrolling regions. + */ + pixman_region32_t dirty; + pixman_region32_init(&dirty); + + bool full_repaint_needed = true; + + for (int r = 0; r < term->rows; r++) { + const struct row *row = grid_row_in_view(term->grid, r); + + bool row_all_dirty = true; + for (int c = 0; c < term->cols; c++) { + if (row->cells[c].attrs.clean) { + row_all_dirty = false; + full_repaint_needed = false; + break; + } + } + + if (row_all_dirty) { + pixman_region32_union_rect( + &dirty, &dirty, + term->margins.left, + term->margins.top + r * term->cell_height, + term->width - term->margins.left - term->margins.right, + term->cell_height); + } + } + + if (full_repaint_needed) { + force_full_repaint(term, new); + return; + } + + for (size_t i = 0; i < old->scroll_damage_count; i++) { + const struct damage *dmg = &old->scroll_damage[i]; + + switch (dmg->type) { + case DAMAGE_SCROLL: + if (term->grid->view == term->grid->offset) + grid_render_scroll(term, new, dmg); + break; + + case DAMAGE_SCROLL_REVERSE: + if (term->grid->view == term->grid->offset) + grid_render_scroll_reverse(term, new, dmg); + break; + + case DAMAGE_SCROLL_IN_VIEW: + grid_render_scroll(term, new, dmg); + break; + + case DAMAGE_SCROLL_REVERSE_IN_VIEW: + grid_render_scroll_reverse(term, new, dmg); + break; + } + } + + if (tll_length(term->grid->scroll_damage) == 0) { + pixman_region32_subtract(&dirty, &old->dirty, &dirty); + pixman_image_set_clip_region32(new->pix[0], &dirty); + } else + pixman_image_set_clip_region32(new->pix[0], &old->dirty); + + pixman_image_composite32( + PIXMAN_OP_SRC, old->pix[0], NULL, new->pix[0], + 0, 0, 0, 0, 0, 0, term->width, term->height); + + pixman_image_set_clip_region32(new->pix[0], NULL); + pixman_region32_fini(&dirty); +} + +static void +dirty_old_cursor(struct terminal *term) +{ + if (term->render.last_cursor.row != NULL && !term->render.last_cursor.hidden) { + struct row *row = term->render.last_cursor.row; + struct cell *cell = &row->cells[term->render.last_cursor.col]; + cell->attrs.clean = 0; + row->dirty = true; + } + + /* Remember current cursor position, for the next frame */ + term->render.last_cursor.row = grid_row(term->grid, term->grid->cursor.point.row); + term->render.last_cursor.col = term->grid->cursor.point.col; + term->render.last_cursor.hidden = term->hide_cursor; +} + +static void +dirty_cursor(struct terminal *term) +{ + if (term->hide_cursor) + return; + + const struct coord *cursor = &term->grid->cursor.point; + + struct row *row = grid_row(term->grid, cursor->row); + struct cell *cell = &row->cells[cursor->col]; + cell->attrs.clean = 0; + row->dirty = true; +} + static void grid_render(struct terminal *term) { if (term->is_shutting_down) return; - struct timeval start_time; + struct timeval start_time, start_double_buffering = {0}, stop_double_buffering = {0}; if (term->conf->tweak.render_timer_osd || term->conf->tweak.render_timer_log) gettimeofday(&start_time, NULL); @@ -2042,66 +2191,76 @@ grid_render(struct terminal *term) struct buffer *buf = shm_get_buffer( term->wl->shm, term->width, term->height, cookie, true, 1 + term->render.workers.count); - /* If we resized the window, or is flashing, or just stopped flashing */ - if (term->render.last_buf != buf || + /* Dirty old and current cursor cell, to ensure they’re repainted */ + dirty_old_cursor(term); + dirty_cursor(term); + + if (term->render.last_buf == NULL || + term->render.last_buf->width != buf->width || + term->render.last_buf->height != buf->height || term->flash.active || term->render.was_flashing || term->is_searching != term->render.was_searching || term->render.margins) { - if (term->render.last_buf != NULL && - term->render.last_buf->width == buf->width && - term->render.last_buf->height == buf->height && - !term->flash.active && - !term->render.was_flashing && - term->is_searching == term->render.was_searching && - !term->render.margins) - { - static bool has_warned = false; - if (!has_warned) { - LOG_WARN( - "it appears your Wayland compositor does not support " - "buffer re-use for SHM clients; expect lower " - "performance."); - has_warned = true; - } - - xassert(term->render.last_buf->size == buf->size); - memcpy(buf->mmapped, term->render.last_buf->mmapped, buf->size); - } - - else { - tll_free(term->grid->scroll_damage); - render_margin(term, buf, 0, term->rows, true); - term_damage_view(term); - } - - term->render.last_buf = buf; - term->render.was_flashing = term->flash.active; - term->render.was_searching = term->is_searching; + force_full_repaint(term, buf); } - tll_foreach(term->grid->scroll_damage, it) { - switch (it->item.type) { - case DAMAGE_SCROLL: - if (term->grid->view == term->grid->offset) + else if (buf->age > 0) { + LOG_DBG("buffer age: %u", buf->age); + + xassert(term->render.last_buf != NULL); + xassert(term->render.last_buf != buf); + xassert(term->render.last_buf->width == buf->width); + xassert(term->render.last_buf->height == buf->height); + + gettimeofday(&start_double_buffering, NULL); + reapply_old_damage(term, buf, term->render.last_buf); + gettimeofday(&stop_double_buffering, NULL); + } + + if (term->render.last_buf != NULL) { + free(term->render.last_buf->scroll_damage); + term->render.last_buf->scroll_damage = NULL; + } + + term->render.last_buf = buf; + term->render.was_flashing = term->flash.active; + term->render.was_searching = term->is_searching; + + buf->age = 0; + + xassert(buf->scroll_damage == NULL); + buf->scroll_damage_count = tll_length(term->grid->scroll_damage); + buf->scroll_damage = xmalloc( + buf->scroll_damage_count * sizeof(buf->scroll_damage[0])); + + { + size_t i = 0; + tll_foreach(term->grid->scroll_damage, it) { + buf->scroll_damage[i++] = it->item; + + switch (it->item.type) { + case DAMAGE_SCROLL: + if (term->grid->view == term->grid->offset) + grid_render_scroll(term, buf, &it->item); + break; + + case DAMAGE_SCROLL_REVERSE: + if (term->grid->view == term->grid->offset) + grid_render_scroll_reverse(term, buf, &it->item); + break; + + case DAMAGE_SCROLL_IN_VIEW: grid_render_scroll(term, buf, &it->item); - break; + break; - case DAMAGE_SCROLL_REVERSE: - if (term->grid->view == term->grid->offset) + case DAMAGE_SCROLL_REVERSE_IN_VIEW: grid_render_scroll_reverse(term, buf, &it->item); - break; + break; + } - case DAMAGE_SCROLL_IN_VIEW: - grid_render_scroll(term, buf, &it->item); - break; - - case DAMAGE_SCROLL_REVERSE_IN_VIEW: - grid_render_scroll_reverse(term, buf, &it->item); - break; + tll_remove(term->grid->scroll_damage, it); } - - tll_remove(term->grid->scroll_damage, it); } /* @@ -2123,29 +2282,6 @@ grid_render(struct terminal *term) */ selection_dirty_cells(term); - /* Mark old cursor cell as dirty, to force it to be re-rendered */ - if (term->render.last_cursor.row != NULL && !term->render.last_cursor.hidden) { - struct row *row = term->render.last_cursor.row; - struct cell *cell = &row->cells[term->render.last_cursor.col]; - cell->attrs.clean = 0; - row->dirty = true; - } - - /* Remember current cursor position, for the next frame */ - term->render.last_cursor.row = grid_row(term->grid, term->grid->cursor.point.row); - term->render.last_cursor.col = term->grid->cursor.point.col; - term->render.last_cursor.hidden = term->hide_cursor; - - /* Mark current cursor cell as dirty, to ensure it is rendered */ - if (!term->hide_cursor) { - const struct coord *cursor = &term->grid->cursor.point; - - struct row *row = grid_row(term->grid, cursor->row); - struct cell *cell = &row->cells[cursor->col]; - cell->attrs.clean = 0; - row->dirty = true; - } - /* Translate offset-relative row to view-relative, unless cursor * is hidden, then we just set it to -1 */ struct coord cursor = {-1, -1}; @@ -2173,12 +2309,15 @@ grid_render(struct terminal *term) if (!row->dirty) { if (first_dirty_row >= 0) { + int x = term->margins.left; + int y = term->margins.top + first_dirty_row * term->cell_height; + int width = term->width - term->margins.left - term->margins.right; + int height = (r - first_dirty_row) * term->cell_height; + wl_surface_damage_buffer( - term->window->surface, - term->margins.left, - term->margins.top + first_dirty_row * term->cell_height, - term->width - term->margins.left - term->margins.right, - (r - first_dirty_row) * term->cell_height); + term->window->surface, x, y, width, height); + pixman_region32_union_rect( + &buf->dirty, &buf->dirty, 0, y, buf->width, height); } first_dirty_row = -1; continue; @@ -2199,12 +2338,13 @@ grid_render(struct terminal *term) } if (first_dirty_row >= 0) { - wl_surface_damage_buffer( - term->window->surface, - term->margins.left, - term->margins.top + first_dirty_row * term->cell_height, - term->width - term->margins.left - term->margins.right, - (term->rows - first_dirty_row) * term->cell_height); + int x = term->margins.left; + int y = term->margins.top + first_dirty_row * term->cell_height; + int width = term->width - term->margins.left - term->margins.right; + int height = (term->rows - first_dirty_row) * term->cell_height; + + wl_surface_damage_buffer(term->window->surface, x, y, width, height); + pixman_region32_union_rect(&buf->dirty, &buf->dirty, 0, y, buf->width, height); } /* Signal workers the frame is done */ @@ -2242,10 +2382,16 @@ grid_render(struct terminal *term) struct timeval render_time; timersub(&end_time, &start_time, &render_time); + struct timeval double_buffering_time; + timersub(&stop_double_buffering, &start_double_buffering, &double_buffering_time); + if (term->conf->tweak.render_timer_log) { - LOG_INFO("frame rendered in %llds %lld µs", + LOG_INFO("frame rendered in %llds %lld µs " + "(%llds %lld µs double buffering)", (long long)render_time.tv_sec, - (long long)render_time.tv_usec); + (long long)render_time.tv_usec, + (long long)double_buffering_time.tv_sec, + (long long)double_buffering_time.tv_usec); } if (term->conf->tweak.render_timer_osd) diff --git a/shm.c b/shm.c index 2ec581c4..ed20af3a 100644 --- a/shm.c +++ b/shm.c @@ -30,6 +30,8 @@ #define TIME_SCROLL 0 +#define FORCED_DOUBLE_BUFFERING 0 + /* * Maximum memfd size allowed. * @@ -100,6 +102,9 @@ buffer_destroy(struct buffer *buf) buf->real_mmapped = MAP_FAILED; buf->pool = NULL; buf->fd = -1; + + free(buf->scroll_damage); + pixman_region32_fini(&buf->dirty); } void @@ -222,6 +227,8 @@ shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie, tll_remove(buffers, it); } + struct buffer *cached = NULL; + tll_foreach(buffers, it) { if (it->item.width != width) continue; @@ -230,16 +237,30 @@ shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie, if (it->item.cookie != cookie) continue; - if (!it->item.busy) { - LOG_DBG("cookie=%lx: re-using buffer from cache (buf=%p)", - cookie, (void *)&it->item); - it->item.busy = true; - it->item.purge = false; - xassert(it->item.pix_instances == pix_instances); - return &it->item; - } + if (it->item.busy) + it->item.age++; + else +#if FORCED_DOUBLE_BUFFERING + if (it->item.age == 0) + it->item.age++; + else +#endif + { + LOG_DBG("cookie=%lx: re-using buffer from cache (buf=%p)", + cookie, (void *)&it->item); + it->item.busy = true; + it->item.purge = false; + pixman_region32_clear(&it->item.dirty); + free(it->item.scroll_damage); + it->item.scroll_damage = NULL; + xassert(it->item.pix_instances == pix_instances); + cached = &it->item; + } } + if (cached != NULL) + return cached; + /* Purge old buffers associated with this cookie */ tll_foreach(buffers, it) { if (it->item.cookie != cookie) @@ -376,14 +397,16 @@ shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie, .scrollable = scrollable, .real_mmapped = real_mmapped, .mmap_size = memfd_size, - .offset = 0} - ) - ); + .offset = 0, + .age = 1234, /* Force a full repaint */ + })); struct buffer *ret = &tll_back(buffers); if (!instantiate_offset(shm, ret, initial_offset)) goto err; + pixman_region32_init(&ret->dirty); + #if defined(MEASURE_SHM_ALLOCS) && MEASURE_SHM_ALLOCS { size_t currently_alloced = 0; diff --git a/shm.h b/shm.h index 2e819369..385a0e30 100644 --- a/shm.h +++ b/shm.h @@ -7,6 +7,8 @@ #include #include +#include "terminal.h" + struct buffer { unsigned long cookie; @@ -32,6 +34,11 @@ struct buffer { bool scrollable; bool purge; /* True if this buffer should be destroyed */ + + unsigned age; + struct damage *scroll_damage; + size_t scroll_damage_count; + pixman_region32_t dirty; }; struct buffer *shm_get_buffer(