diff --git a/CHANGELOG.md b/CHANGELOG.md index dc893176..be5bb8f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ * Spaces no longer removed from zsh font name completions. * Default key binding for _spawn-terminal_ to ctrl+shift+n. +* Renderer is now much faster with interactive scrolling + (https://codeberg.org/dnkl/foot/issues/4) ### Deprecated diff --git a/config.c b/config.c index da723e97..b33f5818 100644 --- a/config.c +++ b/config.c @@ -22,6 +22,7 @@ #include "wayland.h" #define ALEN(v) (sizeof(v) / sizeof(v[0])) +#define min(x, y) ((x) < (y) ? (x) : (y)) static const uint32_t default_foreground = 0xdcdccc; static const uint32_t default_background = 0x111111; @@ -636,6 +637,18 @@ parse_section_tweak( LOG_WARN("tweak: delayed-render-upper=%lu", ns); } + else if (strcmp(key, "max-shm-pool-size-mb") == 0) { + unsigned long mb; + if (!str_to_ulong(value, 10, &mb)) { + LOG_ERR("%s:%d: expected an integer: %s", path, lineno, value); + return false; + } + + conf->tweak.max_shm_pool_size = min(mb * 1024 * 1024, INT32_MAX); + LOG_WARN("tweak: max-shm-pool-size=%lu bytes", + conf->tweak.max_shm_pool_size); + } + else { LOG_ERR("%s:%u: invalid key: %s", path, lineno, key); return false; @@ -906,6 +919,7 @@ config_load(struct config *conf, const char *conf_path) .tweak = { .delayed_render_lower_ns = 500000, /* 0.5ms */ .delayed_render_upper_ns = 16666666 / 2, /* half a frame period (60Hz) */ + .max_shm_pool_size = 512 * 1024 * 1024, }, }; diff --git a/config.h b/config.h index 635abb20..32e381eb 100644 --- a/config.h +++ b/config.h @@ -78,6 +78,7 @@ struct config { struct { uint64_t delayed_render_lower_ns; uint64_t delayed_render_upper_ns; + off_t max_shm_pool_size; } tweak; }; diff --git a/main.c b/main.c index 0d8dc139..06497cbe 100644 --- a/main.c +++ b/main.c @@ -350,6 +350,8 @@ main(int argc, char *const *argv) } while (errno == ERANGE); } + shm_set_max_pool_size(conf.tweak.max_shm_pool_size); + if ((fdm = fdm_init()) == NULL) goto out; diff --git a/quirks.c b/quirks.c index 05d2dd5f..6685d614 100644 --- a/quirks.c +++ b/quirks.c @@ -2,6 +2,7 @@ #include #include +#include #define LOG_MODULE "quirks" #define LOG_ENABLE_DBG 0 @@ -12,19 +13,6 @@ static bool is_weston(void) { - /* - * On weston (8.0), synchronized subsurfaces aren't updated - * correctly. - - * They appear to render once, but after that, updates are - * sporadic. Sometimes they update, most of the time they - * don't. - * - * Adding explicit parent surface commits right after the - * subsurface commit doesn't help (and would be useless anyway, - * since it would defeat the purpose of having the subsurface - * synchronized in the first place). - */ static bool is_weston = false; static bool initialized = false; @@ -79,3 +67,26 @@ quirk_weston_csd_off(struct terminal *term) for (int i = 0; i < ALEN(term->window->csd.surface); i++) quirk_weston_subsurface_desync_off(term->window->csd.sub_surface[i]); } + +void +quirk_kde_damage_before_attach(struct wl_surface *surface) +{ + static bool is_kde = false; + static bool initialized = false; + + if (!initialized) { + initialized = true; + + const char *cur_desktop = getenv("XDG_CURRENT_DESKTOP"); + if (cur_desktop != NULL) + is_kde = strcasestr(cur_desktop, "kde") != NULL; + + if (is_kde) + LOG_WARN("applying wl_surface_damage_buffer() workaround for KDE"); + } + + if (!is_kde) + return; + + wl_surface_damage_buffer(surface, 0, 0, INT32_MAX, INT32_MAX); +} diff --git a/quirks.h b/quirks.h index 7b6ab4f8..583c0c9f 100644 --- a/quirks.h +++ b/quirks.h @@ -4,9 +4,28 @@ #include "terminal.h" +/* + * On weston (8.0), synchronized subsurfaces aren't updated correctly. + + * They appear to render once, but after that, updates are + * sporadic. Sometimes they update, most of the time they don't. + * + * Adding explicit parent surface commits right after the subsurface + * commit doesn't help (and would be useless anyway, since it would + * defeat the purpose of having the subsurface synchronized in the + * first place). + */ void quirk_weston_subsurface_desync_on(struct wl_subsurface *sub); void quirk_weston_subsurface_desync_off(struct wl_subsurface *sub); /* Shortcuts to call desync_{on,off} on all CSD subsurfaces */ void quirk_weston_csd_on(struct terminal *term); void quirk_weston_csd_off(struct terminal *term); + +/* + * KDE discards all previous damage when a buffer is attached to a + * surface. Thus, if you have recorded damage before you call + * wl_surface_attach(), call this function to record a full buffer + * damage. + */ +void quirk_kde_damage_before_attach(struct wl_surface *surface); diff --git a/render.c b/render.c index 46e849a4..f77b7b3d 100644 --- a/render.c +++ b/render.c @@ -22,6 +22,9 @@ #include "selection.h" #include "shm.h" +#define TIME_FRAME_RENDERING 0 +#define TIME_SCROLL_DAMAGE 0 + #define min(x, y) ((x) < (y) ? (x) : (y)) #define max(x, y) ((x) > (y) ? (x) : (y)) @@ -475,56 +478,228 @@ draw_cursor: return cell_cols; } +static void +render_margin(struct terminal *term, struct buffer *buf, int start_line, int end_line, + bool top, bool bottom) +{ + /* Fill area outside the cell grid with the default background color */ + const int rmargin = term->width - term->margins.right; + const int bmargin = term->height - term->margins.bottom; + const int line_count = end_line - start_line; + + uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg; + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, term->colors.alpha); + if (term->is_searching) + pixman_color_dim(&bg); + + if (top) { + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix, &bg, 1, + &(pixman_rectangle16_t){0, 0, term->width, term->margins.top}); + wl_surface_damage_buffer( + term->window->surface, 0, 0, term->width, term->margins.top); + } + + if (bottom) { + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix, &bg, 1, + &(pixman_rectangle16_t){0, bmargin, term->width, term->margins.bottom}); + wl_surface_damage_buffer( + term->window->surface, 0, bmargin, term->width, term->margins.bottom); + } + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix, &bg, 2, + (pixman_rectangle16_t[]){ + /* Left */ + {0, term->margins.top + start_line * term->cell_height, + term->margins.left, line_count * term->cell_height}, + + /* Right */ + {rmargin, term->margins.top + start_line * term->cell_height, + term->margins.right, line_count * term->cell_height}, + }); + + /* Left */ + wl_surface_damage_buffer( + term->window->surface, + 0, term->margins.top + start_line * term->cell_height, + term->margins.left, line_count * term->cell_height); + + /* Right */ + wl_surface_damage_buffer( + term->window->surface, + rmargin, term->margins.top + start_line * term->cell_height, + term->margins.right, line_count * term->cell_height); +} + static void grid_render_scroll(struct terminal *term, struct buffer *buf, const struct damage *dmg) { - int dst_y = term->margins.top + (dmg->scroll.region.start + 0) * term->cell_height; - int src_y = term->margins.top + (dmg->scroll.region.start + dmg->scroll.lines) * term->cell_height; int height = (dmg->scroll.region.end - dmg->scroll.region.start - dmg->scroll.lines) * term->cell_height; - LOG_DBG("damage: SCROLL: %d-%d by %d lines (dst-y: %d, src-y: %d, " - "height: %d, stride: %d, mmap-size: %zu)", - dmg->scroll.region.start, dmg->scroll.region.end, - dmg->scroll.lines, - dst_y, src_y, height, buf->stride, - buf->size); + LOG_DBG( + "damage: SCROLL: %d-%d by %d lines", + dmg->scroll.region.start, dmg->scroll.region.end, dmg->scroll.lines); - if (height > 0) { + if (height <= 0) + return; + +#if TIME_SCROLL_DAMAGE + struct timeval start_time; + gettimeofday(&start_time, NULL); +#endif + + int dst_y = term->margins.top + (dmg->scroll.region.start + 0) * term->cell_height; + int src_y = term->margins.top + (dmg->scroll.region.start + dmg->scroll.lines) * term->cell_height; + + /* + * SHM scrolling can be *much* faster, but it depends on how many + * lines we're scrolling, and how much repairing we need to do. + * + * In short, scrolling a *large* number of rows is faster with a + * memmove, while scrolling a *small* number of lines is faster + * with SHM scrolling. + * + * However, since we need to restore the scrolling regions when + * SHM scrolling, we also need to take this into account. + * + * Finally, we also have to restore the window margins, and this + * is a *huge* performance hit when scrolling a large number of + * lines (in addition to the sloweness of SHM scrolling as + * method). + * + * So, we need to figure out when to SHM scroll, and when to + * memmove. + * + * For now, assume that the both methods perform rougly the same, + * given an equal number of bytes to move/allocate, and use the + * method that results in the least amount of bytes to touch. + * + * Since number of lines directly translates to bytes, we can + * simply count lines. + * + * SHM scrolling needs to first "move" (punch hole + allocate) + * dmg->scroll.lines number of lines, and then we need to restore + * the bottom scroll region. + * + * If the total number of lines is less than half the screen - use + * SHM. Otherwise use memmove. + */ + bool try_shm_scroll = + shm_can_scroll(buf) && ( + dmg->scroll.lines + + dmg->scroll.region.start + + (term->rows - dmg->scroll.region.end)) < term->rows / 2; + + bool did_shm_scroll = false; + + //try_shm_scroll = false; + //try_shm_scroll = true; + + if (try_shm_scroll) { + did_shm_scroll = shm_scroll( + term->wl->shm, buf, dmg->scroll.lines * term->cell_height, + term->margins.top, dmg->scroll.region.start * term->cell_height, + term->margins.bottom, (term->rows - dmg->scroll.region.end) * term->cell_height); + } + + if (did_shm_scroll) { + + /* Restore margins */ + render_margin( + term, buf, dmg->scroll.region.end - dmg->scroll.lines, term->rows, + true, true); + } else { + /* Fallback for when we either cannot do SHM scrolling, or it failed */ uint8_t *raw = buf->mmapped; memmove(raw + dst_y * buf->stride, raw + src_y * buf->stride, height * buf->stride); - - wl_surface_damage_buffer( - term->window->surface, term->margins.left, dst_y, term->width - term->margins.left - term->margins.right, height); } + +#if TIME_SCROLL_DAMAGE + struct timeval end_time; + gettimeofday(&end_time, NULL); + + struct timeval memmove_time; + timersub(&end_time, &start_time, &memmove_time); + LOG_INFO("scrolled %dKB (%d lines) using %s in %lds %ldus", + height * buf->stride / 1024, dmg->scroll.lines, + did_shm_scroll ? "SHM" : try_shm_scroll ? "memmove (SHM failed)" : "memmove", + memmove_time.tv_sec, memmove_time.tv_usec); +#endif + + wl_surface_damage_buffer( + term->window->surface, term->margins.left, dst_y, + term->width - term->margins.left - term->margins.right, height); } static void grid_render_scroll_reverse(struct terminal *term, struct buffer *buf, const struct damage *dmg) { - int src_y = term->margins.top + (dmg->scroll.region.start + 0) * term->cell_height; - int dst_y = term->margins.top + (dmg->scroll.region.start + dmg->scroll.lines) * term->cell_height; int height = (dmg->scroll.region.end - dmg->scroll.region.start - dmg->scroll.lines) * term->cell_height; - LOG_DBG("damage: SCROLL REVERSE: %d-%d by %d lines (dst-y: %d, src-y: %d, " - "height: %d, stride: %d, mmap-size: %zu)", - dmg->scroll.region.start, dmg->scroll.region.end, - dmg->scroll.lines, - dst_y, src_y, height, buf->stride, - buf->size); + LOG_DBG( + "damage: SCROLL REVERSE: %d-%d by %d lines"m + dmg->scroll.region.start, dmg->scroll.region.end, dmg->scroll.lines); - if (height > 0) { + if (height <= 0) + return; + +#if TIME_SCROLL_DAMAGE + struct timeval start_time; + gettimeofday(&start_time, NULL); +#endif + + int src_y = term->margins.top + (dmg->scroll.region.start + 0) * term->cell_height; + int dst_y = term->margins.top + (dmg->scroll.region.start + dmg->scroll.lines) * term->cell_height; + + bool try_shm_scroll = + shm_can_scroll(buf) && ( + dmg->scroll.lines + + dmg->scroll.region.start + + (term->rows - dmg->scroll.region.end)) < term->rows / 2; + + bool did_shm_scroll = false; + + if (try_shm_scroll) { + did_shm_scroll = shm_scroll( + term->wl->shm, buf, -dmg->scroll.lines * term->cell_height, + term->margins.top, dmg->scroll.region.start * term->cell_height, + term->margins.bottom, (term->rows - dmg->scroll.region.end) * term->cell_height); + } + + if (did_shm_scroll) { + /* Restore margins */ + render_margin( + term, buf, dmg->scroll.region.start, dmg->scroll.region.start + dmg->scroll.lines, + true, true); + } else { + /* Fallback for when we either cannot do SHM scrolling, or it failed */ uint8_t *raw = buf->mmapped; memmove(raw + dst_y * buf->stride, raw + src_y * buf->stride, height * buf->stride); - - wl_surface_damage_buffer( - term->window->surface, term->margins.left, dst_y, term->width - term->margins.left - term->margins.right, height); } + +#if TIME_SCROLL_DAMAGE + struct timeval end_time; + gettimeofday(&end_time, NULL); + + struct timeval memmove_time; + timersub(&end_time, &start_time, &memmove_time); + LOG_INFO("scrolled REVERSE %dKB (%d lines) using %s in %lds %ldus", + height * buf->stride / 1024, dmg->scroll.lines, + did_shm_scroll ? "SHM" : try_shm_scroll ? "memmove (SHM failed)" : "memmove", + memmove_time.tv_sec, memmove_time.tv_usec); +#endif + + wl_surface_damage_buffer( + term->window->surface, term->margins.left, dst_y, + term->width - term->margins.left - term->margins.right, height); } static void @@ -739,7 +914,7 @@ render_csd_title(struct terminal *term) unsigned long cookie = shm_cookie_csd(term, CSD_SURF_TITLE); struct buffer *buf = shm_get_buffer( - term->wl->shm, info.width, info.height, cookie); + term->wl->shm, info.width, info.height, cookie, false); uint32_t _color = term->colors.default_fg; uint16_t alpha = 0xffff; @@ -770,7 +945,7 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx) unsigned long cookie = shm_cookie_csd(term, surf_idx); struct buffer *buf = shm_get_buffer( - term->wl->shm, info.width, info.height, cookie); + term->wl->shm, info.width, info.height, cookie, false); pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0); render_csd_part(term, surf, buf, info.width, info.height, &color); @@ -939,7 +1114,7 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx) unsigned long cookie = shm_cookie_csd(term, surf_idx); struct buffer *buf = shm_get_buffer( - term->wl->shm, info.width, info.height, cookie); + term->wl->shm, info.width, info.height, cookie, false); uint32_t _color; uint16_t alpha = 0xffff; @@ -1056,8 +1231,6 @@ grid_render(struct terminal *term) if (term->is_shutting_down) return; -#define TIME_FRAME_RENDERING 0 - #if TIME_FRAME_RENDERING struct timeval start_time; gettimeofday(&start_time, NULL); @@ -1068,14 +1241,9 @@ grid_render(struct terminal *term) unsigned long cookie = shm_cookie_grid(term); struct buffer *buf = shm_get_buffer( - term->wl->shm, term->width, term->height, cookie); + term->wl->shm, term->width, term->height, cookie, true); - wl_surface_attach(term->window->surface, buf->wl_buf, 0, 0); - - pixman_image_t *pix = buf->pix; - pixman_region16_t clip; - pixman_region_init_rect(&clip, term->margins.left, term->margins.top, term->cols * term->cell_width, term->rows * term->cell_height); - pixman_image_set_clip_region(pix, &clip); + pixman_image_set_clip_region(buf->pix, NULL); /* If we resized the window, or is flashing, or just stopped flashing */ if (term->render.last_buf != buf || @@ -1091,7 +1259,10 @@ grid_render(struct terminal *term) { 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."); + LOG_WARN( + "it appears your Wayland compositor does not support " + "buffer re-use for SHM clients; expect lower " + "performance."); has_warned = true; } @@ -1100,35 +1271,8 @@ grid_render(struct terminal *term) } else { - /* Fill area outside the cell grid with the default background color */ - int rmargin = term->width - term->margins.right; - int bmargin = term->height - term->margins.bottom; - - uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg; - pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, term->colors.alpha); - if (term->is_searching) - pixman_color_dim(&bg); - - pixman_image_set_clip_region(pix, NULL); - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, pix, &bg, 4, - (pixman_rectangle16_t[]){ - {0, 0, term->width, term->margins.top}, /* Top */ - {0, 0, term->margins.left, term->height}, /* Left */ - {rmargin, 0, term->margins.right, term->height}, /* Right */ - {0, bmargin, term->width, term->margins.bottom}}); /* Bottom */ - pixman_image_set_clip_region(pix, &clip); - - wl_surface_damage_buffer( - term->window->surface, 0, 0, term->width, term->margins.top); - wl_surface_damage_buffer( - term->window->surface, 0, 0, term->margins.left, term->height); - wl_surface_damage_buffer( - term->window->surface, rmargin, 0, term->margins.right, term->height); - wl_surface_damage_buffer( - term->window->surface, 0, bmargin, term->width, term->margins.bottom); - - /* Force a full grid refresh */ + tll_free(term->grid->scroll_damage); + render_margin(term, buf, 0, term->rows, true, true); term_damage_view(term); } @@ -1137,6 +1281,14 @@ grid_render(struct terminal *term) term->render.was_searching = term->is_searching; } + /* Set clip region to prevent cells from overflowing into the margins */ + pixman_region16_t clip; + pixman_region_init_rect( + &clip, + term->margins.left, term->margins.top, + term->cols * term->cell_width, term->rows * term->cell_height); + pixman_image_set_clip_region(buf->pix, &clip); + /* Erase old cursor (if we rendered a cursor last time) */ if (term->render.last_cursor.cell != NULL) { @@ -1147,7 +1299,7 @@ grid_render(struct terminal *term) /* If cell is already dirty, it will be rendered anyway */ if (cell->attrs.clean) { cell->attrs.clean = 0; - int cols = render_cell(term, pix, cell, at.col, at.row, false); + int cols = render_cell(term, buf->pix, cell, at.col, at.row, false); wl_surface_damage_buffer( term->window->surface, @@ -1181,6 +1333,9 @@ grid_render(struct terminal *term) tll_remove(term->grid->scroll_damage, it); } + /* Reset clip region since scrolling may have instantiated a new pixman image */ + pixman_image_set_clip_region(buf->pix, &clip); + if (term->render.workers.count > 0) { term->render.workers.buf = buf; @@ -1220,7 +1375,7 @@ grid_render(struct terminal *term) if (!row->dirty) continue; - render_row(term, pix, row, r); + render_row(term, buf->pix, row, r); row->dirty = false; wl_surface_damage_buffer( @@ -1274,7 +1429,7 @@ grid_render(struct terminal *term) cell->attrs.clean = 0; term->render.last_cursor.cell = cell; int cols_updated = render_cell( - term, pix, cell, term->cursor.point.col, view_aligned_row, true); + term, buf->pix, cell, term->cursor.point.col, view_aligned_row, true); wl_surface_damage_buffer( term->window->surface, @@ -1283,13 +1438,14 @@ grid_render(struct terminal *term) cols_updated * term->cell_width, term->cell_height); } - render_sixel_images(term, pix); + render_sixel_images(term, buf->pix); if (term->flash.active) { /* Note: alpha is pre-computed in each color component */ /* TODO: dim while searching */ + pixman_image_set_clip_region(buf->pix, NULL); pixman_image_fill_rectangles( - PIXMAN_OP_OVER, pix, + PIXMAN_OP_OVER, buf->pix, &(pixman_color_t){.red=0x7fff, .green=0x7fff, .blue=0, .alpha=0x7fff}, 1, &(pixman_rectangle16_t){0, 0, term->width, term->height}); @@ -1333,6 +1489,8 @@ grid_render(struct terminal *term) } } + wl_surface_attach(term->window->surface, buf->wl_buf, 0, 0); + quirk_kde_damage_before_attach(term->window->surface); wl_surface_commit(term->window->surface); #if TIME_FRAME_RENDERING @@ -1370,7 +1528,7 @@ render_search_box(struct terminal *term) size_t glyph_offset = term->render.search_glyph_offset; unsigned long cookie = shm_cookie_search(term); - struct buffer *buf = shm_get_buffer(term->wl->shm, width, height, cookie); + struct buffer *buf = shm_get_buffer(term->wl->shm, width, height, cookie, false); /* Background - yellow on empty/match, red on mismatch */ pixman_color_t color = color_hex_to_pixman( @@ -1446,6 +1604,24 @@ render_search_box(struct terminal *term) quirk_weston_subsurface_desync_off(term->window->search_sub_surface); } +static void +render_update_title(struct terminal *term) +{ + /* TODO: figure out what the limit actually is */ + static const size_t max_len = 100; + + const char *title = term->window_title != NULL ? term->window_title : "foot"; + char *copy = NULL; + + if (strlen(title) > max_len) { + copy = strndup(title, max_len); + title = copy; + } + + xdg_toplevel_set_title(term->window->xdg_toplevel, title); + free(copy); +} + static void frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data) { @@ -1458,10 +1634,12 @@ frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_da bool grid = term->render.pending.grid; bool csd = term->render.pending.csd; bool search = term->render.pending.search; + bool title = term->render.pending.title; term->render.pending.grid = false; term->render.pending.csd = false; term->render.pending.search = false; + term->render.pending.title = false; if (csd && term->window->use_csd == CSD_YES) { quirk_weston_csd_on(term); @@ -1469,6 +1647,9 @@ frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_da quirk_weston_csd_off(term); } + if (title) + render_update_title(term); + if (search && term->is_searching) render_search_box(term); @@ -1801,10 +1982,12 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) bool grid = term->render.refresh.grid; bool csd = term->render.refresh.csd; bool search = term->render.refresh.search; + bool title = term->render.refresh.title; term->render.refresh.grid = false; term->render.refresh.csd = false; term->render.refresh.search = false; + term->render.refresh.title = false; if (term->window->frame_callback == NULL) { if (csd && term->window->use_csd == CSD_YES) { @@ -1812,6 +1995,8 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) render_csd(term); quirk_weston_csd_off(term); } + if (title) + render_update_title(term); if (search) render_search_box(term); if (grid) @@ -1821,6 +2006,7 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) term->render.pending.grid |= grid; term->render.pending.csd |= csd; term->render.pending.search |= search; + term->render.pending.title |= title; } } @@ -1835,21 +2021,9 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) } void -render_set_title(struct terminal *term, const char *_title) +render_refresh_title(struct terminal *term) { - /* TODO: figure out what the limit actually is */ - static const size_t max_len = 100; - - const char *title = _title; - char *copy = NULL; - - if (strlen(title) > max_len) { - copy = strndup(_title, max_len); - title = copy; - } - - xdg_toplevel_set_title(term->window->xdg_toplevel, title); - free(copy); + term->render.refresh.title = true; } void diff --git a/render.h b/render.h index 99246f17..0c03f125 100644 --- a/render.h +++ b/render.h @@ -12,10 +12,10 @@ void render_destroy(struct renderer *renderer); bool render_resize(struct terminal *term, int width, int height); bool render_resize_force(struct terminal *term, int width, int height); -void render_set_title(struct terminal *term, const char *title); void render_refresh(struct terminal *term); void render_refresh_csd(struct terminal *term); void render_refresh_search(struct terminal *term); +void render_refresh_title(struct terminal *term); bool render_xcursor_set(struct terminal *term); struct render_worker_context { diff --git a/shm.c b/shm.c index a4fc8797..5ae5653d 100644 --- a/shm.c +++ b/shm.c @@ -1,11 +1,15 @@ #include "shm.h" -#include +#include +#include #include #include +#include +#include #include #include +#include #include #include #include @@ -19,21 +23,85 @@ #define LOG_ENABLE_DBG 0 #include "log.h" +#define TIME_SCROLL 0 + +/* + * Maximum memfd size allowed. + * + * On 64-bit, we could in theory use up to 2GB (wk_shm_create_pool() + * is limited to int32_t), since we never mmap() the entire region. + * + * The compositor is different matter - it needs to mmap() the entire + * range, and *keep* the mapping for as long as is has buffers + * referencing it (thus - always). And if we open multiple terminals, + * then the required address space multiples... + * + * That said, 128TB (the total amount of available user address space + * on 64-bit) is *a lot*; we can fit 67108864 2GB memfds into + * that. But, let's be conservative for now. + * + * On 32-bit the available address space is too small and SHM + * scrolling is disabled. + * + * Note: this is the _default_ size. It can be overridden by calling + * shm_set_max_pool_size(); + */ +static off_t max_pool_size = 512 * 1024 * 1024; + static tll(struct buffer) buffers; +static bool can_punch_hole = false; +static bool can_punch_hole_initialized = false; + +void +shm_set_max_pool_size(off_t _max_pool_size) +{ + max_pool_size = _max_pool_size; +} + +static void +buffer_destroy_dont_close(struct buffer *buf) +{ + if (buf->pix != NULL) + pixman_image_unref(buf->pix); + if (buf->wl_buf != NULL) + wl_buffer_destroy(buf->wl_buf); + + buf->pix = NULL; + buf->wl_buf = NULL; + buf->mmapped = NULL; +} + static void buffer_destroy(struct buffer *buf) { - if (buf->pix != NULL) - pixman_image_unref(buf->pix); - wl_buffer_destroy(buf->wl_buf); - munmap(buf->mmapped, buf->size); + buffer_destroy_dont_close(buf); + if (buf->real_mmapped != MAP_FAILED) + munmap(buf->real_mmapped, buf->mmap_size); + if (buf->pool != NULL) + wl_shm_pool_destroy(buf->pool); + if (buf->fd >= 0) + close(buf->fd); + + buf->real_mmapped = MAP_FAILED; + buf->pool = NULL; + buf->fd = -1; +} + +void +shm_fini(void) +{ + tll_foreach(buffers, it) { + buffer_destroy(&it->item); + tll_remove(buffers, it); + } } static void buffer_release(void *data, struct wl_buffer *wl_buffer) { struct buffer *buffer = data; + LOG_DBG("release: cookie=%lx (buf=%p)", buffer->cookie, buffer); assert(buffer->wl_buf == wl_buffer); assert(buffer->busy); buffer->busy = false; @@ -43,8 +111,70 @@ static const struct wl_buffer_listener buffer_listener = { .release = &buffer_release, }; +static size_t +page_size(void) +{ + static size_t size = 0; + if (size == 0) { + size = sysconf(_SC_PAGE_SIZE); + if (size < 0) { + LOG_ERRNO("failed to get page size"); + size = 4096; + } + } + assert(size > 0); + return size; +} + +static bool +instantiate_offset(struct wl_shm *shm, struct buffer *buf, off_t new_offset) +{ + assert(buf->fd >= 0); + assert(buf->mmapped == NULL); + assert(buf->wl_buf == NULL); + assert(buf->pix == NULL); + + void *mmapped = MAP_FAILED; + struct wl_buffer *wl_buf = NULL; + pixman_image_t *pix = NULL; + + mmapped = (uint8_t *)buf->real_mmapped + new_offset; + + wl_buf = wl_shm_pool_create_buffer( + buf->pool, new_offset, buf->width, buf->height, buf->stride, WL_SHM_FORMAT_ARGB8888); + if (wl_buf == NULL) { + LOG_ERR("failed to create SHM buffer"); + goto err; + } + + /* One pixman image for each worker thread (do we really need multiple?) */ + pix = pixman_image_create_bits_no_clear( + PIXMAN_a8r8g8b8, buf->width, buf->height, (uint32_t *)mmapped, buf->stride); + if (pix == NULL) { + LOG_ERR("failed to create pixman image"); + goto err; + } + + buf->offset = new_offset; + buf->mmapped = mmapped; + buf->wl_buf = wl_buf; + buf->pix = pix; + + wl_buffer_add_listener(wl_buf, &buffer_listener, buf); + return true; + +err: + if (pix != NULL) + pixman_image_unref(pix); + if (wl_buf != NULL) + wl_buffer_destroy(wl_buf); + + abort(); + return false; +} + struct buffer * -shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie) +shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie, bool scrollable) { /* Purge buffers marked for purging */ tll_foreach(buffers, it) { @@ -73,7 +203,8 @@ shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie) continue; if (!it->item.busy) { - LOG_DBG("cookie=%lx: re-using buffer from cache", cookie); + LOG_DBG("cookie=%lx: re-using buffer from cache (buf=%p)", + cookie, &it->item); it->item.busy = true; it->item.purge = false; return &it->item; @@ -106,71 +237,83 @@ shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie) */ int pool_fd = -1; - void *mmapped = MAP_FAILED; - size_t size = 0; + const int stride = stride_for_format_and_width(PIXMAN_a8r8g8b8, width); + const size_t size = stride * height; + void *real_mmapped = MAP_FAILED; struct wl_shm_pool *pool = NULL; - struct wl_buffer *buf = NULL; - pixman_image_t *pix = NULL; + LOG_DBG("cookie=%lx: allocating new buffer: %zu KB", cookie, size / 1024); /* Backing memory for SHM */ - pool_fd = memfd_create("foot-wayland-shm-buffer-pool", MFD_CLOEXEC); + pool_fd = memfd_create("foot-wayland-shm-buffer-pool", MFD_CLOEXEC | MFD_ALLOW_SEALING); if (pool_fd == -1) { LOG_ERRNO("failed to create SHM backing memory file"); goto err; } - const int stride = stride_for_format_and_width(PIXMAN_a8r8g8b8, width); +#if defined(__i386__) + off_t initial_offset = 0; + off_t memfd_size = size; +#else + off_t initial_offset = scrollable && max_pool_size > 0 ? (max_pool_size / 4) & ~(page_size() - 1) : 0; + off_t memfd_size = scrollable && max_pool_size > 0 ? max_pool_size : size; +#endif - /* Total size */ - size = stride * height; - LOG_DBG("cookie=%lx: allocating new buffer: %zu KB", cookie, size / 1024); + LOG_DBG("memfd-size: %lu, initial offset: %lu", memfd_size, initial_offset); - int err = EINTR; - while (err == EINTR) - err = posix_fallocate(pool_fd, 0, size); - if (err != 0) { - static bool failure_logged = false; + if (ftruncate(pool_fd, memfd_size) == -1) { + LOG_ERRNO("failed to set size of SHM backing memory file"); + goto err; + } - if (!failure_logged) { - failure_logged = true; - LOG_ERRNO_P("failed to fallocate %zu bytes", err, size); + if (!can_punch_hole_initialized) { + can_punch_hole_initialized = true; + can_punch_hole = fallocate( + pool_fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 0, 1) == 0; + + if (!can_punch_hole) { + LOG_WARN( + "fallocate(FALLOC_FL_PUNCH_HOLE) not " + "supported (%s): expect lower performance", strerror(errno)); } + } - if (ftruncate(pool_fd, size) == -1) { - LOG_ERRNO("failed to truncate SHM pool"); + if (scrollable && !can_punch_hole) { + initial_offset = 0; + memfd_size = size; + scrollable = false; + + if (ftruncate(pool_fd, memfd_size) < 0) { + LOG_ERRNO("failed to set size of SHM backing memory file"); goto err; } } - mmapped = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_UNINITIALIZED, pool_fd, 0); - if (mmapped == MAP_FAILED) { + real_mmapped = mmap( + NULL, memfd_size, PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_UNINITIALIZED, pool_fd, 0); + + if (real_mmapped == MAP_FAILED) { LOG_ERRNO("failed to mmap SHM backing memory file"); goto err; } - pool = wl_shm_create_pool(shm, pool_fd, size); + /* Seal file - we no longer allow any kind of resizing */ + /* TODO: wayland mmaps(PROT_WRITE), for some unknown reason, hence we cannot use F_SEAL_FUTURE_WRITE */ + if (fcntl(pool_fd, F_ADD_SEALS, + F_SEAL_GROW | F_SEAL_SHRINK | /*F_SEAL_FUTURE_WRITE |*/ F_SEAL_SEAL) < 0) + { + LOG_ERRNO("failed to seal SHM backing memory file"); + goto err; + } + + pool = wl_shm_create_pool(shm, pool_fd, memfd_size); if (pool == NULL) { LOG_ERR("failed to create SHM pool"); goto err; } - buf = wl_shm_pool_create_buffer( - pool, 0, width, height, stride, WL_SHM_FORMAT_ARGB8888); - if (buf == NULL) { - LOG_ERR("failed to create SHM buffer"); - goto err; - } - - /* We use the entire pool for our single buffer */ - wl_shm_pool_destroy(pool); pool = NULL; - close(pool_fd); pool_fd = -1; - - /* One pixman image for each worker thread (do we really need multiple?) */ - pix = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, width, height, (uint32_t *)mmapped, stride); - /* Push to list of available buffers, but marked as 'busy' */ tll_push_back( buffers, @@ -181,43 +324,292 @@ shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie) .stride = stride, .busy = true, .size = size, - .mmapped = mmapped, - .wl_buf = buf, - .pix = pix} + .fd = pool_fd, + .pool = pool, + .scrollable = scrollable, + .real_mmapped = real_mmapped, + .mmap_size = memfd_size, + .offset = 0} ) ); struct buffer *ret = &tll_back(buffers); - wl_buffer_add_listener(ret->wl_buf, &buffer_listener, ret); + if (!instantiate_offset(shm, ret, initial_offset)) + goto err; return ret; err: - if (pix != NULL) - pixman_image_unref(pix); - if (buf != NULL) - wl_buffer_destroy(buf); if (pool != NULL) wl_shm_pool_destroy(pool); + if (real_mmapped != MAP_FAILED) + munmap(real_mmapped, memfd_size); if (pool_fd != -1) close(pool_fd); - if (mmapped != MAP_FAILED) - munmap(mmapped, size); /* We don't handle this */ abort(); return NULL; } -void -shm_fini(void) +bool +shm_can_scroll(const struct buffer *buf) { - tll_foreach(buffers, it) { - buffer_destroy(&it->item); - tll_remove(buffers, it); - } +#if defined(__i386__) + /* Not enough virtual address space in 32-bit */ + return false; +#else + return can_punch_hole && max_pool_size > 0 && buf->scrollable; +#endif } -void +static bool +wrap_buffer(struct wl_shm *shm, struct buffer *buf, off_t new_offset) +{ + /* We don't allow overlapping offsets */ + off_t diff __attribute__((unused)) = + new_offset < buf->offset ? buf->offset - new_offset : new_offset - buf->offset; + assert(diff > buf->size); + + memcpy((uint8_t *)buf->real_mmapped + new_offset, buf->mmapped, buf->size); + + off_t trim_ofs, trim_len; + if (new_offset > buf->offset) { + /* Trim everything *before* the new offset */ + trim_ofs = 0; + trim_len = new_offset; + } else { + /* Trim everything *after* the new buffer location */ + trim_ofs = new_offset + buf->size; + trim_len = buf->mmap_size - trim_ofs; + } + + if (fallocate( + buf->fd, + FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, + trim_ofs, trim_len) < 0) + { + LOG_ERRNO("failed to trim SHM backing memory file"); + return false; + } + + /* Re-instantiate pixman+wl_buffer+raw pointersw */ + buffer_destroy_dont_close(buf); + return instantiate_offset(shm, buf, new_offset); +} + +static bool +shm_scroll_forward(struct wl_shm *shm, struct buffer *buf, int rows, + int top_margin, int top_keep_rows, + int bottom_margin, int bottom_keep_rows) +{ + assert(can_punch_hole); + assert(buf->busy); + assert(buf->pix); + assert(buf->wl_buf); + assert(buf->fd >= 0); + + LOG_DBG("scrolling %d rows (%d bytes)", rows, rows * buf->stride); + + const off_t diff = rows * buf->stride; + assert(rows > 0); + assert(diff < buf->size); + + if (buf->offset + diff + buf->size > max_pool_size) { + LOG_DBG("memfd offset wrap around"); + if (!wrap_buffer(shm, buf, 0)) + goto err; + } + + off_t new_offset = buf->offset + diff; + assert(new_offset > buf->offset); + assert(new_offset + buf->size <= max_pool_size); + +#if TIME_SCROLL + struct timeval time1; + gettimeofday(&time1, NULL); + + struct timeval time2 = time1; +#endif + + if (top_keep_rows > 0) { + /* Copy current 'top' region to its new location */ + memmove( + (uint8_t *)buf->mmapped + (top_margin + rows) * buf->stride, + (uint8_t *)buf->mmapped + (top_margin + 0) * buf->stride, + top_keep_rows * buf->stride); + +#if TIME_SCROLL + gettimeofday(&time2, NULL); + timersub(&time2, &time1, &tot); + LOG_INFO("memmove (top region): %lds %ldus", tot.tv_sec, tot.tv_usec); +#endif + } + + /* Destroy old objects (they point to the old offset) */ + buffer_destroy_dont_close(buf); + + /* Free unused memory - everything up until the new offset */ + const off_t trim_ofs = 0; + const off_t trim_len = new_offset; + + if (fallocate( + buf->fd, + FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, + trim_ofs, trim_len) < 0) + { + LOG_ERRNO("failed to trim SHM backing memory file"); + goto err; + } + +#if TIME_SCROLL + struct timeval time3; + gettimeofday(&time3, NULL); + timersub(&time3, &time2, &tot); + LOG_INFO("PUNCH HOLE: %lds %ldus", tot.tv_sec, tot.tv_usec); +#endif + + /* Re-instantiate pixman+wl_buffer+raw pointersw */ + bool ret = instantiate_offset(shm, buf, new_offset); + +#if TIME_SCROLL + struct timeval time4; + gettimeofday(&time4, NULL); + timersub(&time4, &time3, &tot); + LOG_INFO("instantiate offset: %lds %ldus", tot.tv_sec, tot.tv_usec); +#endif + + if (ret && bottom_keep_rows > 0) { + /* Copy 'bottom' region to its new location */ + memmove( + (uint8_t *)buf->mmapped + buf->size - (bottom_margin + bottom_keep_rows) * buf->stride, + (uint8_t *)buf->mmapped + buf->size - (bottom_margin + rows + bottom_keep_rows) * buf->stride, + bottom_keep_rows * buf->stride); + +#if TIME_SCROLL + struct timeval time5; + gettimeofday(&time5, NULL); + + timersub(&time5, &time4, &tot); + LOG_INFO("memmove (bottom region): %lds %ldus", tot.tv_sec, tot.tv_usec); +#endif + } + + return ret; + +err: + abort(); + return false; +} + +static bool +shm_scroll_reverse(struct wl_shm *shm, struct buffer *buf, int rows, + int top_margin, int top_keep_rows, + int bottom_margin, int bottom_keep_rows) +{ + assert(rows > 0); + + const off_t diff = rows * buf->stride; + if (diff > buf->offset) { + LOG_DBG("memfd offset reverse wrap-around"); + if (!wrap_buffer(shm, buf, (max_pool_size - buf->size) & ~(page_size() - 1))) + goto err; + } + + off_t new_offset = buf->offset - diff; + assert(new_offset < buf->offset); + assert(new_offset <= max_pool_size); + +#if TIME_SCROLL + struct timeval time0; + gettimeofday(&time0, NULL); + + struct timeval tot; + struct timeval time1 = time0; +#endif + + if (bottom_keep_rows > 0) { + /* Copy 'bottom' region to its new location */ + memmove( + (uint8_t *)buf->mmapped + buf->size - (bottom_margin + rows + bottom_keep_rows) * buf->stride, + (uint8_t *)buf->mmapped + buf->size - (bottom_margin + bottom_keep_rows) * buf->stride, + bottom_keep_rows * buf->stride); + +#if TIME_SCROLL + gettimeofday(&time1, NULL); + timersub(&time1, &time0, &tot); + LOG_INFO("memmove (bottom region): %lds %ldus", tot.tv_sec, tot.tv_usec); +#endif + } + + /* Destroy old objects (they point to the old offset) */ + buffer_destroy_dont_close(buf); + + /* Free unused memory - everything after the relocated buffer */ + const off_t trim_ofs = new_offset + buf->size; + const off_t trim_len = buf->mmap_size - trim_ofs; + + if (fallocate( + buf->fd, + FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, + trim_ofs, trim_len) < 0) + { + LOG_ERRNO("failed to trim SHM backing memory"); + goto err; + } +#if TIME_SCROLL + struct timeval time2; + gettimeofday(&time2, NULL); + timersub(&time2, &time1, &tot); + LOG_INFO("fallocate: %lds %ldus", tot.tv_sec, tot.tv_usec); +#endif + + /* Re-instantiate pixman+wl_buffer+raw pointers */ + bool ret = instantiate_offset(shm, buf, new_offset); + +#if TIME_SCROLL + struct timeval time3; + gettimeofday(&time3, NULL); + timersub(&time3, &time2, &tot); + LOG_INFO("instantiate offset: %lds %ldus", tot.tv_sec, tot.tv_usec); +#endif + + if (ret && top_keep_rows > 0) { + /* Copy current 'top' region to its new location */ + memmove( + (uint8_t *)buf->mmapped + (top_margin + 0) * buf->stride, + (uint8_t *)buf->mmapped + (top_margin + rows) * buf->stride, + top_keep_rows * buf->stride); + +#if TIME_SCROLL + struct timeval time4; + gettimeofday(&time4, NULL); + timersub(&time4, &time2, &tot); + LOG_INFO("memmove (top region): %lds %ldus", tot.tv_sec, tot.tv_usec); +#endif + } + + return ret; + +err: + abort(); + return false; +} + +bool +shm_scroll(struct wl_shm *shm, struct buffer *buf, int rows, + int top_margin, int top_keep_rows, + int bottom_margin, int bottom_keep_rows) +{ + if (!shm_can_scroll(buf)) + return false; + + assert(rows != 0); + return rows > 0 + ? shm_scroll_forward(shm, buf, rows, top_margin, top_keep_rows, bottom_margin, bottom_keep_rows) + : shm_scroll_reverse(shm, buf, -rows, top_margin, top_keep_rows, bottom_margin, bottom_keep_rows); +} + + void shm_purge(struct wl_shm *shm, unsigned long cookie) { LOG_DBG("cookie=%lx: purging all buffers", cookie); diff --git a/shm.h b/shm.h index eb9987ed..aff53d76 100644 --- a/shm.h +++ b/shm.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -13,20 +14,35 @@ struct buffer { int height; int stride; - bool purge; - bool busy; - size_t size; - void *mmapped; + size_t size; /* Buffer size */ + void *mmapped; /* Raw data (TODO: rename) */ struct wl_buffer *wl_buf; pixman_image_t *pix; + + /* Internal */ + int fd; /* memfd */ + struct wl_shm_pool *pool; + + void *real_mmapped; /* Address returned from mmap */ + size_t mmap_size; /* Size of mmap (>= size) */ + off_t offset; /* Offset into memfd where data begins */ + + bool scrollable; + bool purge; /* True if this buffer should be destroyed */ }; struct buffer *shm_get_buffer( - struct wl_shm *shm, int width, int height, unsigned long cookie); + struct wl_shm *shm, int width, int height, unsigned long cookie, bool scrollable); void shm_fini(void); +void shm_set_max_pool_size(off_t max_pool_size); +bool shm_can_scroll(const struct buffer *buf); +bool shm_scroll(struct wl_shm *shm, struct buffer *buf, int rows, + int top_margin, int top_keep_rows, + int bottom_margin, int bottom_keep_rows); + void shm_purge(struct wl_shm *shm, unsigned long cookie); struct terminal; diff --git a/terminal.c b/terminal.c index 73c5d943..14f1666a 100644 --- a/terminal.c +++ b/terminal.c @@ -1960,7 +1960,7 @@ term_set_window_title(struct terminal *term, const char *title) { free(term->window_title); term->window_title = strdup(title); - render_set_title(term, term->window_title); + render_refresh_title(term); } void @@ -2069,6 +2069,7 @@ term_enable_app_sync_updates(struct terminal *term) timerfd_settime( term->delayed_render_timer.upper_fd, 0, &(struct itimerspec){{0}}, NULL); + term->delayed_render_timer.is_armed = false; } void diff --git a/terminal.h b/terminal.h index d7d4d705..40088b04 100644 --- a/terminal.h +++ b/terminal.h @@ -336,6 +336,7 @@ struct terminal { bool grid; bool csd; bool search; + bool title; } refresh; /* Scheduled for rendering, in the next frame callback */ @@ -343,6 +344,7 @@ struct terminal { bool grid; bool csd; bool search; + bool title; } pending; int scrollback_lines; /* Number of scrollback lines, from conf (TODO: move out from render struct?) */