diff --git a/CHANGELOG.md b/CHANGELOG.md index dfa083ea..a25599fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.8.1](#1-8-1) * [1.8.0](#1-8-0) * [1.7.2](#1-7-2) @@ -27,6 +28,58 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added + +* `locked-title=no|yes` to `foot.ini` + (https://codeberg.org/dnkl/foot/issues/386). +* `tweak.overflowing-glyphs` option, which can be enabled to fix rendering + issues with glyphs of any width that appear cut-off + (https://codeberg.org/dnkl/foot/issues/592). + + +### Changed + +* Non-empty lines are now considered to have a hard linebreak, + _unless_ an actual word-wrap is inserted. +* Setting `DECSDM` now _disables_ sixel scrolling, while resetting it + _enables_ scrolling (https://codeberg.org/dnkl/foot/issues/631). + + +### Deprecated +### Removed + +* The `tweak.allow-overflowing-double-width-glyphs` and + `tweak.pua-double-width` options (which have been superseded by + `tweak.overflowing-glyphs`). + + +### Fixed + +* FD exhaustion when repeatedly entering/exiting URL mode with many + URLs. +* Double free of URL while removing duplicated and/or overlapping URLs + in URL mode (https://codeberg.org/dnkl/foot/issues/627). +* Crash when an unclosed OSC-8 URL ran into un-allocated scrollback + rows. +* Some box-drawing characters were rendered incorrectly on big-endian + architectures. +* Crash when resizing the window to the smallest possible size while + scrollback search is active. +* Scrollback indicator being incorrectly rendered when window size is + very small. +* Reduced memory usage in URL mode. +* Crash when the `E3` escape (`\E[3J`) was executed, and there was a + selection, or sixel image, in the scrollback + (https://codeberg.org/dnkl/foot/issues/633). + + +### Security +### Contributors + +* [clktmr](https://codeberg.org/clktmr) + + ## 1.8.1 ### Added @@ -43,7 +96,7 @@ * Grapheme cluster width is now limited to two cells by default. This may cause cursor synchronization issues with many applications. You can set `[tweak].grapheme-width-method=wcswidth` to revert to the - behavior from foot-1.8.0. + behavior in foot-1.8.0. ### Fixed diff --git a/box-drawing.c b/box-drawing.c index 2eb92efd..b06fc322 100644 --- a/box-drawing.c +++ b/box-drawing.c @@ -1244,6 +1244,16 @@ draw_box_drawings_double_vertical_and_horizontal(struct buf *buf) vline(hmid + 2 * thick, buf->height, vmid + 2 * thick, thick); } +static inline void +set_a1_bit(uint8_t *data, size_t ofs, size_t bit_no) +{ +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + data[ofs] |= 1 << bit_no; +#else + data[ofs] |= 1 << (7 - bit_no); +#endif +} + static void draw_box_drawings_light_arc(struct buf *buf, wchar_t wc) { @@ -1354,7 +1364,7 @@ draw_box_drawings_light_arc(struct buf *buf, wchar_t wc) if (fmt == PIXMAN_a1) { size_t idx = c / 8; size_t bit_no = c % 8; - data[r * stride + idx] |= 1 << bit_no; + set_a1_bit(data, r * stride + idx, bit_no); } else data[r * stride + c] = 0xff; } @@ -1376,7 +1386,7 @@ draw_box_drawings_light_arc(struct buf *buf, wchar_t wc) if (fmt == PIXMAN_a1) { size_t ofs = col / 8; size_t bit_no = col % 8; - data[row * stride + ofs] |= 1 << bit_no; + set_a1_bit(data, row * stride + ofs, bit_no); } else data[row * stride + col] = 0xff; } @@ -1392,7 +1402,7 @@ draw_box_drawings_light_arc(struct buf *buf, wchar_t wc) if (fmt == PIXMAN_a1) { size_t ofs = col / 8; size_t bit_no = col % 8; - data[row * stride + ofs] |= 1 << bit_no; + set_a1_bit(data, row * stride + ofs, bit_no); } else data[row * stride + col] = 0xff; } @@ -1767,7 +1777,7 @@ draw_light_shade(struct buf *buf) for (size_t col = 0; col < buf->width; col += 2) { size_t idx = col / 8; size_t bit_no = col % 8; - buf->data[row * buf->stride + idx] |= 1 << bit_no; + set_a1_bit(buf->data, row * buf->stride + idx, bit_no); } } } @@ -1790,7 +1800,7 @@ draw_medium_shade(struct buf *buf) for (size_t col = row % 2; col < buf->width; col += 2) { size_t idx = col / 8; size_t bit_no = col % 8; - buf->data[row * buf->stride + idx] |= 1 << bit_no; + set_a1_bit(buf->data, row * buf->stride + idx, bit_no); } } } @@ -1813,7 +1823,7 @@ draw_dark_shade(struct buf *buf) for (size_t col = 0; col < buf->width; col += 1 + row % 2) { size_t idx = col / 8; size_t bit_no = col % 8; - buf->data[row * buf->stride + idx] |= 1 << bit_no; + set_a1_bit(buf->data, row * buf->stride + idx, bit_no); } } } diff --git a/config.c b/config.c index e02ca3de..a9acb58e 100644 --- a/config.c +++ b/config.c @@ -643,6 +643,9 @@ parse_section_main(const char *key, const char *value, struct config *conf, conf->title = xstrdup(value); } + else if (strcmp(key, "locked-title") == 0) + conf->locked_title = str_to_bool(value); + else if (strcmp(key, "app-id") == 0) { free(conf->app_id); conf->app_id = xstrdup(value); @@ -2210,16 +2213,10 @@ parse_section_tweak( return false; } - else if (strcmp(key, "allow-overflowing-double-width-glyphs") == 0) { - conf->tweak.allow_overflowing_double_width_glyphs = str_to_bool(value); - if (!conf->tweak.allow_overflowing_double_width_glyphs) - LOG_WARN("tweak: disabled overflowing double-width glyphs"); - } - - else if (strcmp(key, "pua-double-width") == 0) { - conf->tweak.pua_double_width = str_to_bool(value); - if (conf->tweak.pua_double_width) - LOG_WARN("tweak: PUA double width glyphs enabled"); + else if (strcmp(key, "overflowing-glyphs") == 0) { + conf->tweak.overflowing_glyphs = str_to_bool(value); + if (!conf->tweak.overflowing_glyphs) + LOG_WARN("tweak: disabled overflowing glyphs"); } else if (strcmp(key, "damage-whole-window") == 0) { @@ -2830,7 +2827,7 @@ config_load(struct config *conf, const char *conf_path, .tweak = { .fcft_filter = FCFT_SCALING_FILTER_LANCZOS3, - .allow_overflowing_double_width_glyphs = true, + .overflowing_glyphs = true, .grapheme_shaping = false, .grapheme_width_method = GRAPHEME_WIDTH_DOUBLE, .delayed_render_lower_ns = 500000, /* 0.5ms */ @@ -2841,7 +2838,6 @@ config_load(struct config *conf, const char *conf_path, .damage_whole_window = false, .box_drawing_base_thickness = 0.04, .box_drawing_solid_shades = true, - .pua_double_width = false, }, .notifications = tll_init(), diff --git a/config.h b/config.h index 2300a0ad..729eba3e 100644 --- a/config.h +++ b/config.h @@ -77,6 +77,7 @@ struct config { wchar_t *word_delimiters; bool login_shell; bool no_wait; + bool locked_title; struct { enum conf_size_type type; @@ -244,7 +245,7 @@ struct config { struct { enum fcft_scaling_filter fcft_filter; - bool allow_overflowing_double_width_glyphs; + bool overflowing_glyphs; bool grapheme_shaping; enum {GRAPHEME_WIDTH_WCSWIDTH, GRAPHEME_WIDTH_DOUBLE} grapheme_width_method; bool render_timer_osd; @@ -255,7 +256,6 @@ struct config { off_t max_shm_pool_size; float box_drawing_base_thickness; bool box_drawing_solid_shades; - bool pua_double_width; } tweak; user_notifications_t notifications; diff --git a/csi.c b/csi.c index fcc2800f..dc5a018a 100644 --- a/csi.c +++ b/csi.c @@ -402,7 +402,7 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) break; case 80: - term->sixel.scrolling = enable; + term->sixel.scrolling = !enable; break; case 1000: @@ -611,7 +611,7 @@ decrqm(const struct terminal *term, unsigned param, bool *enabled) case 12: *enabled = term->cursor_blink.decset; return true; case 25: *enabled = !term->hide_cursor; return true; case 45: *enabled = term->reverse_wrap; return true; - case 80: *enabled = term->sixel.scrolling; return true; + case 80: *enabled = !term->sixel.scrolling; return true; case 1000: *enabled = term->mouse_tracking == MOUSE_CLICK; return true; case 1001: *enabled = false; return true; case 1002: *enabled = term->mouse_tracking == MOUSE_DRAG; return true; @@ -654,7 +654,7 @@ xtsave(struct terminal *term, unsigned param) case 25: term->xtsave.show_cursor = !term->hide_cursor; break; case 45: term->xtsave.reverse_wrap = term->reverse_wrap; break; case 47: term->xtsave.alt_screen = term->grid == &term->alt; break; - case 80: term->xtsave.sixel_scrolling = term->sixel.scrolling; break; + case 80: term->xtsave.sixel_display_mode = !term->sixel.scrolling; break; case 1000: term->xtsave.mouse_click = term->mouse_tracking == MOUSE_CLICK; break; case 1001: break; case 1002: term->xtsave.mouse_drag = term->mouse_tracking == MOUSE_DRAG; break; @@ -696,7 +696,7 @@ xtrestore(struct terminal *term, unsigned param) case 25: enable = term->xtsave.show_cursor; break; case 45: enable = term->xtsave.reverse_wrap; break; case 47: enable = term->xtsave.alt_screen; break; - case 80: enable = term->xtsave.sixel_scrolling; break; + case 80: enable = term->xtsave.sixel_display_mode; break; case 1000: enable = term->xtsave.mouse_click; break; case 1001: return; case 1002: enable = term->xtsave.mouse_drag; break; @@ -917,26 +917,7 @@ csi_dispatch(struct terminal *term, uint8_t final) case 3: { /* Erase scrollback */ - int end = (term->grid->offset + term->rows - 1) % term->grid->num_rows; - for (size_t i = 0; i < term->grid->num_rows; i++) { - if (end >= term->grid->offset) { - /* Not wrapped */ - if (i >= term->grid->offset && i <= end) - continue; - } else { - /* Wrapped */ - if (i >= term->grid->offset || i <= end) - continue; - } - - if (term->render.last_cursor.row == term->grid->rows[i]) - term->render.last_cursor.row = NULL; - - grid_row_free(term->grid->rows[i]); - term->grid->rows[i] = NULL; - } - term->grid->view = term->grid->offset; - term_damage_view(term); + term_erase_scrollback(term); break; } diff --git a/doc/foot.1.scd b/doc/foot.1.scd index ab34a2dd..afa6cf54 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -204,7 +204,7 @@ Note that these are just the defaults; they can be changed in characters are whitespace characters. *ctrl*+*v*, *ctrl*+*y* - Paste from clipboard into the searh buffer. + Paste from clipboard into the search buffer. *shift*+*insert* Paste from primary selection into the search buffer. diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 684497ca..8b2f1dc8 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -188,8 +188,8 @@ in this order: In other words, while you are fiddling with the window size, foot does not send the updated dimensions to the client. Only when you - pause the fiddling for *relay-size-ms* milliseconds is the client - updated. + pause the fiddling for *resize-delay-ms* milliseconds is the + client updated. Emphasis is on _while_ here; as soon as the interactive resize ends (i.e. when you let go of the window border), the final @@ -224,6 +224,10 @@ in this order: *title* Initial window title. Default: _foot_. +*locked-title* + Boolean. If enabled, applications are not allowed to change the + title at run-time. Default: _no_. + *app-id* Value to set the *app-id* property on the Wayland window to. The compositor can use this value to e.g. group multiple windows, or @@ -843,34 +847,24 @@ any of these options. Default: _lanczos3_. -*allow-overflowing-double-width-glyphs* - Boolean. When enabled, double width glyphs with a character width - of 1 are allowed to overflow into the neighbouring cell. +*overflowing-glyphs* + Boolean. When enabled, glyphs wider than their cell(s) are allowed + to render into one additional neighbouring cell. - One use case for this is fonts "icon" characters in the Unicode - private usage area, e.g. Nerd Fonts, or Powerline Fonts. Without - this option, such glyphs will appear "cut off". + One use case for this are fonts with wide italic characters that + "bend" into the next cell. Without this option, such glyphs will + appear "cut off". - Another use case are legacy emoji characters like *WHITE FROWNING - FACE*. + Another use case are fonts with "icon" characters in the Unicode + private usage area, e.g. Nerd Fonts, or Powerline Fonts and legacy + emoji characters like *WHITE FROWNING FACE*. - Note: this feature uses _heuristics_ to determine *which* glyphs - should be allowed to overflow. - - See also: *pua-double-width* + Note: might impact performance depending on the font used. + Especially small font sizes can cause many overflowing glyphs + because of subpixel rendering. Default: _yes_. -*pua-double-width* - Boolean. When enabled, Unicode code points from the private usage - area (PUA) are always considered to be double width, regardless of - the actual glyph width. - - Ignored if *allow-overflowing-double-width-glyphs* has been - disabled. - - Default: _no_. - *render-timer* Enables a frame rendering timer, that prints the time it takes to render each frame, in microseconds, either on-screen, to stderr, diff --git a/foot.ini b/foot.ini index c10ed143..31167cd5 100644 --- a/foot.ini +++ b/foot.ini @@ -4,6 +4,10 @@ # term=foot (or xterm-256color if built with -Dterminfo=disabled) # login-shell=no +# app-id=foot +# title=foot +# locked-title=no + # font=monospace:size=8 # font-bold= # font-italic= @@ -122,7 +126,7 @@ # show-urls-copy=none [search-bindings] -# cancel=Control+g Escape +# cancel=Control+g Control+c Escape # commit=Return # find-prev=Control+r # find-next=Control+s @@ -142,7 +146,7 @@ # primary-paste=Shift+Insert [url-bindings] -# cancel=Control+g Control+d Escape +# cancel=Control+g Control+c Control+d Escape # toggle-url-visible=t [mouse-bindings] diff --git a/render.c b/render.c index 499303f0..7750dd19 100644 --- a/render.c +++ b/render.c @@ -437,6 +437,20 @@ draw_cursor(const struct terminal *term, const struct cell *cell, } } +static inline void +render_cell_prepass(struct terminal *term, struct row *row, int col) +{ + for (; col < term->cols - 1; col++) { + if (row->cells[col].attrs.confined || + (row->cells[col].attrs.clean == row->cells[col + 1].attrs.clean)) { + break; + } + + row->cells[col].attrs.clean = 0; + row->cells[col + 1].attrs.clean = 0; + } +} + static int render_cell(struct terminal *term, pixman_image_t *pix, struct row *row, int col, int row_no, bool has_cursor) @@ -446,6 +460,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, return 0; cell->attrs.clean = 1; + cell->attrs.confined = true; int width = term->cell_width; int height = term->cell_height; @@ -458,7 +473,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, uint32_t _fg = 0; uint32_t _bg = 0; - bool apply_alpha = false; + uint16_t alpha = 0xffff; if (is_selected && term->colors.use_custom_selection) { _fg = term->colors.selection_fg; @@ -472,14 +487,14 @@ render_cell(struct terminal *term, pixman_image_t *pix, uint32_t swap = _fg; _fg = _bg; _bg = swap; - } else - apply_alpha = !cell->attrs.have_bg; + } else if (!cell->attrs.have_bg) + alpha = term->colors.alpha; } if (unlikely(is_selected && _fg == _bg)) { /* Invert bg when selected/highlighted text has same fg/bg */ _bg = ~_bg; - apply_alpha = false; + alpha = 0xffff; } if (cell->attrs.dim) @@ -491,8 +506,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, _fg = color_dim(_fg); pixman_color_t fg = color_hex_to_pixman(_fg); - pixman_color_t bg = color_hex_to_pixman_with_alpha( - _bg, apply_alpha ? term->colors.alpha : 0xffff); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha); if (term->is_searching && !is_selected) { color_dim_for_search(&fg); @@ -598,51 +612,32 @@ render_cell(struct terminal *term, pixman_image_t *pix, cell_cols = max(1, min(cell_cols, cols_left)); /* - * Hack! - * - * Deal with double-width glyphs for which wcwidth() returns - * 1. Typically Unicode private usage area characters, - * e.g. powerline, or nerd hack fonts. - * - * Users can enable a tweak option that lets this glyphs - * overflow/bleed into the neighbouring cell. - * - * We only apply this workaround if: - * - the user has explicitly enabled this feature - * - the *character* width is 1 - * - the *glyph* width is at least 1.5 cells - * - the *glyph* width is less than 3 cells - * - *this* column isn’t the last column - * - *this* cells is followed by an empty cell, or a space + * Determine cells that will bleed into their right neighbor and remember + * them for cleanup in the next frame. */ - if (term->conf->tweak.allow_overflowing_double_width_glyphs && - ((glyph_count > 0 && - cell_cols == 1 && - glyphs[0]->width >= term->cell_width * 15 / 10 && - glyphs[0]->width < 3 * term->cell_width && - col < term->cols - 1) || - (term->conf->tweak.pua_double_width && - ((base >= 0x00e000 && base <= 0x00f8ff) || - (base >= 0x0f0000 && base <= 0x0ffffd) || - (base >= 0x100000 && base <= 0x10fffd)))) && - (row->cells[col + 1].wc == 0 || row->cells[col + 1].wc == L' ')) + int render_width = cell_cols * width; + if (term->conf->tweak.overflowing_glyphs && + glyph_count > 0) { - cell_cols = 2; + int glyph_width = 0, advance = 0; + for (size_t i = 0; i < glyph_count; i++) { + glyph_width = max(glyph_width, + advance + glyphs[i]->x + glyphs[i]->width); + advance += glyphs[i]->advance.x; + } - /* - * Ensure the cell we’re overflowing into gets re-rendered, to - * ensure it is erased if *this* cell is erased. Note that we - * do *not* mark the row as dirty - we don’t need to re-render - * the cell if nothing else on the row has changed. - */ - row->cells[col].attrs.clean = 0; - row->cells[col + 1].attrs.clean = 0; + if (glyph_width > render_width) { + render_width = min(glyph_width, render_width + width); + + for (int i = 0; i < cell_cols; i++) + row->cells[col + i].attrs.confined = false; + } } pixman_region32_t clip; pixman_region32_init_rect( &clip, x, y, - cell_cols * term->cell_width, term->cell_height); + render_width, term->cell_height); pixman_image_set_clip_region32(pix, &clip); /* Background */ @@ -772,6 +767,10 @@ static void render_row(struct terminal *term, pixman_image_t *pix, struct row *row, int row_no, int cursor_col) { + if (term->conf->tweak.overflowing_glyphs) + for (int col = term->cols - 1; col >= 0; col--) + render_cell_prepass(term, row, col); + for (int col = term->cols - 1; col >= 0; col--) render_cell(term, pix, row, col, row_no, cursor_col == col); } @@ -816,13 +815,10 @@ render_margin(struct terminal *term, struct buffer *buf, const int line_count = end_line - start_line; uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg; - if (term->is_searching) - _bg = color_dim(_bg); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, term->colors.alpha); - pixman_color_t bg = color_hex_to_pixman_with_alpha( - _bg, - (_bg == (term->reverse ? term->colors.fg : term->colors.bg) - ? term->colors.alpha : 0xffff)); + if (term->is_searching) + color_dim_for_search(&bg); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &bg, 4, @@ -947,7 +943,7 @@ grid_render_scroll(struct terminal *term, struct buffer *buf, if (try_shm_scroll) { did_shm_scroll = shm_scroll( - term->wl->shm, buf, dmg->lines * term->cell_height, + buf, dmg->lines * term->cell_height, term->margins.top, dmg->region.start * term->cell_height, term->margins.bottom, (term->rows - dmg->region.end) * term->cell_height); } @@ -958,7 +954,7 @@ grid_render_scroll(struct terminal *term, struct buffer *buf, term, buf, dmg->region.end - dmg->lines, term->rows, false); } else { /* Fallback for when we either cannot do SHM scrolling, or it failed */ - uint8_t *raw = buf->mmapped; + uint8_t *raw = buf->data; memmove(raw + dst_y * buf->stride, raw + src_y * buf->stride, height * buf->stride); @@ -1012,7 +1008,7 @@ grid_render_scroll_reverse(struct terminal *term, struct buffer *buf, if (try_shm_scroll) { did_shm_scroll = shm_scroll( - term->wl->shm, buf, -dmg->lines * term->cell_height, + buf, -dmg->lines * term->cell_height, term->margins.top, dmg->region.start * term->cell_height, term->margins.bottom, (term->rows - dmg->region.end) * term->cell_height); } @@ -1023,7 +1019,7 @@ grid_render_scroll_reverse(struct terminal *term, struct buffer *buf, term, buf, dmg->region.start, dmg->region.start + dmg->lines, false); } else { /* Fallback for when we either cannot do SHM scrolling, or it failed */ - uint8_t *raw = buf->mmapped; + uint8_t *raw = buf->data; memmove(raw + dst_y * buf->stride, raw + src_y * buf->stride, height * buf->stride); @@ -1196,8 +1192,10 @@ render_sixel(struct terminal *term, pixman_image_t *pix, (last_col_needs_erase && last_col)) { render_cell(term, pix, row, col, term_row_no, cursor_col == col); - } else + } else { cell->attrs.clean = 1; + cell->attrs.confined = 1; + } } } } @@ -1577,21 +1575,16 @@ render_csd_part(struct terminal *term, } static void -render_csd_title(struct terminal *term) +render_csd_title(struct terminal *term, const struct csd_data *info, + struct buffer *buf) { xassert(term->window->csd_mode == CSD_YES); - struct csd_data info = get_csd_data(term, CSD_SURF_TITLE); struct wl_surface *surf = term->window->csd.surface[CSD_SURF_TITLE].surf; + xassert(info->width > 0 && info->height > 0); - xassert(info.width > 0 && info.height > 0); - - xassert(info.width % term->scale == 0); - xassert(info.height % term->scale == 0); - - unsigned long cookie = shm_cookie_csd(term, CSD_SURF_TITLE); - struct buffer *buf = shm_get_buffer( - term->wl->shm, info.width, info.height, cookie, false, 1); + xassert(info->width % term->scale == 0); + xassert(info->height % term->scale == 0); uint32_t _color = term->conf->colors.fg; uint16_t alpha = 0xffff; @@ -1605,31 +1598,27 @@ render_csd_title(struct terminal *term) _color = color_dim(_color); pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha); - render_csd_part(term, surf, buf, info.width, info.height, &color); + render_csd_part(term, surf, buf, info->width, info->height, &color); csd_commit(term, surf, buf); } static void -render_csd_border(struct terminal *term, enum csd_surface surf_idx) +render_csd_border(struct terminal *term, enum csd_surface surf_idx, + const struct csd_data *info, struct buffer *buf) { xassert(term->window->csd_mode == CSD_YES); xassert(surf_idx >= CSD_SURF_LEFT && surf_idx <= CSD_SURF_BOTTOM); - struct csd_data info = get_csd_data(term, surf_idx); struct wl_surface *surf = term->window->csd.surface[surf_idx].surf; - if (info.width == 0 || info.height == 0) + if (info->width == 0 || info->height == 0) return; - xassert(info.width % term->scale == 0); - xassert(info.height % term->scale == 0); - - unsigned long cookie = shm_cookie_csd(term, surf_idx); - struct buffer *buf = shm_get_buffer( - term->wl->shm, info.width, info.height, cookie, false, 1); + xassert(info->width % term->scale == 0); + xassert(info->height % term->scale == 0); pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0); - render_csd_part(term, surf, buf, info.width, info.height, &color); + render_csd_part(term, surf, buf, info->width, info->height, &color); csd_commit(term, surf, buf); } @@ -1796,23 +1785,19 @@ render_csd_button_close(struct terminal *term, struct buffer *buf) } static void -render_csd_button(struct terminal *term, enum csd_surface surf_idx) +render_csd_button(struct terminal *term, enum csd_surface surf_idx, + const struct csd_data *info, struct buffer *buf) { xassert(term->window->csd_mode == CSD_YES); xassert(surf_idx >= CSD_SURF_MINIMIZE && surf_idx <= CSD_SURF_CLOSE); - struct csd_data info = get_csd_data(term, surf_idx); struct wl_surface *surf = term->window->csd.surface[surf_idx].surf; - if (info.width == 0 || info.height == 0) + if (info->width == 0 || info->height == 0) return; - xassert(info.width % term->scale == 0); - xassert(info.height % term->scale == 0); - - unsigned long cookie = shm_cookie_csd(term, surf_idx); - struct buffer *buf = shm_get_buffer( - term->wl->shm, info.width, info.height, cookie, false, 1); + xassert(info->width % term->scale == 0); + xassert(info->height % term->scale == 0); uint32_t _color; uint16_t alpha = 0xffff; @@ -1861,7 +1846,7 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx) _color = color_dim(_color); pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha); - render_csd_part(term, surf, buf, info.width, info.height, &color); + render_csd_part(term, surf, buf, info->width, info->height, &color); switch (surf_idx) { case CSD_SURF_MINIMIZE: render_csd_button_minimize(term, buf); break; @@ -1885,12 +1870,16 @@ render_csd(struct terminal *term) if (term->window->is_fullscreen) return; + struct csd_data infos[CSD_SURF_COUNT]; + int widths[CSD_SURF_COUNT]; + int heights[CSD_SURF_COUNT]; + for (size_t i = 0; i < CSD_SURF_COUNT; i++) { - struct csd_data info = get_csd_data(term, i); - const int x = info.x; - const int y = info.y; - const int width = info.width; - const int height = info.height; + infos[i] = get_csd_data(term, i); + const int x = infos[i].x; + const int y = infos[i].y; + const int width = infos[i].width; + const int height = infos[i].height; struct wl_surface *surf = term->window->csd.surface[i].surf; struct wl_subsurface *sub = term->window->csd.surface[i].sub; @@ -1899,20 +1888,27 @@ render_csd(struct terminal *term) xassert(sub != NULL); if (width == 0 || height == 0) { + widths[i] = heights[i] = 0; wl_subsurface_set_position(sub, 0, 0); wl_surface_attach(surf, NULL, 0, 0); wl_surface_commit(surf); continue; } + widths[i] = width; + heights[i] = height; + wl_subsurface_set_position(sub, x / term->scale, y / term->scale); } + struct buffer *bufs[CSD_SURF_COUNT]; + shm_get_many(term->render.chains.csd, CSD_SURF_COUNT, widths, heights, bufs); + for (size_t i = CSD_SURF_LEFT; i <= CSD_SURF_BOTTOM; i++) - render_csd_border(term, i); + render_csd_border(term, i, &infos[i], bufs[i]); for (size_t i = CSD_SURF_MINIMIZE; i <= CSD_SURF_CLOSE; i++) - render_csd_button(term, i); - render_csd_title(term); + render_csd_button(term, i, &infos[i], bufs[i]); + render_csd_title(term, &infos[CSD_SURF_TITLE], bufs[CSD_SURF_TITLE]); } static void @@ -2051,10 +2047,6 @@ render_scrollback_position(struct terminal *term) const int height = (2 * margin + term->cell_height + scale - 1) / scale * scale; - unsigned long cookie = shm_cookie_scrollback_indicator(term); - struct buffer *buf = shm_get_buffer( - term->wl->shm, width, height, cookie, false, 1); - /* *Where* to render - parent relative coordinates */ int surf_top = 0; switch (term->conf->scrollback.indicator.position) { @@ -2072,18 +2064,29 @@ render_scrollback_position(struct terminal *term) /* Make sure we don't collide with the scrollback search box */ lines--; } - xassert(lines > 0); - int pixels = lines * term->cell_height - height + 2 * margin; + lines = max(lines, 0); + + int pixels = max(lines * term->cell_height - height + 2 * margin, 0); surf_top = term->cell_height - margin + (int)(percent * pixels); break; } } + const int x = (term->width - margin - width) / scale * scale; + const int y = (term->margins.top + surf_top) / scale * scale; + + if (y + height > term->height) { + wl_surface_attach(win->scrollback_indicator.surf, NULL, 0, 0); + wl_surface_commit(win->scrollback_indicator.surf); + return; + } + + struct buffer_chain *chain = term->render.chains.scrollback_indicator; + struct buffer *buf = shm_get_buffer(chain, width, height); + wl_subsurface_set_position( - win->scrollback_indicator.sub, - (term->width - margin - width) / scale, - (term->margins.top + surf_top) / scale); + win->scrollback_indicator.sub, x / scale, y / scale); render_osd( term, @@ -2092,7 +2095,6 @@ render_scrollback_position(struct terminal *term) buf, text, term->colors.table[0], term->colors.table[8 + 4], width - margin - wcslen(text) * term->cell_width, margin); - } static void @@ -2112,9 +2114,8 @@ render_render_timer(struct terminal *term, struct timeval render_time) const int height = (2 * margin + term->cell_height + scale - 1) / scale * scale; - unsigned long cookie = shm_cookie_render_timer(term); - struct buffer *buf = shm_get_buffer( - term->wl->shm, width, height, cookie, false, 1); + struct buffer_chain *chain = term->render.chains.render_timer; + struct buffer *buf = shm_get_buffer(chain, width, height); wl_subsurface_set_position( win->render_timer.sub, @@ -2157,7 +2158,7 @@ reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old } if (new->age > 1) { - memcpy(new->mmapped, old->mmapped, new->size); + memcpy(new->data, old->data, new->height * new->stride); return; } @@ -2287,9 +2288,8 @@ grid_render(struct terminal *term) xassert(term->width > 0); xassert(term->height > 0); - unsigned long cookie = shm_cookie_grid(term); - struct buffer *buf = shm_get_buffer( - term->wl->shm, term->width, term->height, cookie, true, 1 + term->render.workers.count); + struct buffer_chain *chain = term->render.chains.grid; + struct buffer *buf = shm_get_buffer(chain, term->width, term->height); /* Dirty old and current cursor cell, to ensure they’re repainted */ dirty_old_cursor(term); @@ -2306,7 +2306,7 @@ grid_render(struct terminal *term) } else if (buf->age > 0) { - LOG_DBG("buffer age: %u", buf->age); + LOG_DBG("buffer age: %u (%p)", buf->age, (void *)buf); xassert(term->render.last_buf != NULL); xassert(term->render.last_buf != buf); @@ -2319,17 +2319,18 @@ grid_render(struct terminal *term) } if (term->render.last_buf != NULL) { - free(term->render.last_buf->scroll_damage); - term->render.last_buf->scroll_damage = NULL; + shm_unref(term->render.last_buf); + term->render.last_buf = NULL; } term->render.last_buf = buf; term->render.was_flashing = term->flash.active; term->render.was_searching = term->is_searching; + shm_addref(buf); buf->age = 0; - xassert(buf->scroll_damage == NULL); + free(term->render.last_buf->scroll_damage); buf->scroll_damage_count = tll_length(term->grid->scroll_damage); buf->scroll_damage = xmalloc( buf->scroll_damage_count * sizeof(buf->scroll_damage[0])); @@ -2620,8 +2621,14 @@ render_search_box(struct terminal *term) const size_t visible_cells = (visible_width - 2 * margin) / term->cell_width; 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, false, 1); + struct buffer_chain *chain = term->render.chains.search; + struct buffer *buf = shm_get_buffer(chain, width, height); + + pixman_region32_t clip; + pixman_region32_init_rect(&clip, 0, 0, width, height); + pixman_image_set_clip_region32(buf->pix[0], &clip); + pixman_region32_fini(&clip); + #define WINDOW_X(x) (margin + x) #define WINDOW_Y(y) (term->height - margin - height + y) @@ -2875,6 +2882,10 @@ render_urls(struct terminal *term) struct wl_window *win = term->window; xassert(tll_length(win->urls) > 0); + const int scale = term->scale; + const int x_margin = 2 * scale; + const int y_margin = 1 * scale; + /* Calculate view start, counted from the *current* scrollback start */ const int scrollback_end = (term->grid->offset + term->rows) & (term->grid->num_rows - 1); @@ -2886,6 +2897,45 @@ render_urls(struct terminal *term) const bool show_url = term->urls_show_uri_on_jump_label; + /* + * There can potentially be a lot of URLs. + * + * Since each URL is a separate sub-surface, and requires its own + * SHM buffer, we may be allocating a lot of buffers. + * + * SHM buffers normally have their own, private SHM buffer + * pool. Each pool is mmapped, and thus allocates *at least* + * 4K. Since URL labels are typically small, we end up using an + * excessive amount of both virtual and physical memory. + * + * For this reason, we instead use shm_get_many(), which uses a + * single, shared pool for all buffers. + * + * To be able to use it, we need to have all the *all* the buffer + * dimensions up front. + * + * Thus, the first iteration through the URLs do the heavy + * lifting: builds the label contents and calculates both its + * position and size. But instead of rendering the label + * immediately, we store the calculated data, and then do a second + * pass, where we first get all our buffers, and then render to + * them. + */ + + /* Positioning data + label contents */ + struct { + const struct wl_url *url; + wchar_t *text; + int x; + int y; + } info[tll_length(win->urls)]; + + /* For shm_get_many() */ + int widths[tll_length(win->urls)]; + int heights[tll_length(win->urls)]; + + size_t render_count = 0; + tll_foreach(win->urls, it) { const struct url *url = it->item.url; const wchar_t *key = url->key; @@ -2942,6 +2992,7 @@ render_urls(struct terminal *term) /* Maximum width of label, in pixels */ const int max_width = term->width - term->margins.left - term->margins.right - x; + const int max_cols = max_width / term->cell_width; const size_t key_len = wcslen(key); @@ -2979,10 +3030,6 @@ render_urls(struct terminal *term) * Do it in a way such that we don’t cut the label in the * middle of a double-width character. */ - const int scale = term->scale; - const int x_margin = 2 * scale; - const int y_margin = 1 * scale; - const int max_cols = max_width / term->cell_width; int cols = 0; @@ -3010,23 +3057,48 @@ render_urls(struct terminal *term) const int height = (2 * y_margin + term->cell_height + scale - 1) / scale * scale; - struct buffer *buf = shm_get_buffer( - term->wl->shm, width, height, shm_cookie_url(url), false, 1); + info[render_count].url = &it->item; + info[render_count].text = xwcsdup(label); + info[render_count].x = x; + info[render_count].y = y; + + widths[render_count] = width; + heights[render_count] = height; + + render_count++; + } + + struct buffer_chain *chain = term->render.chains.url; + struct buffer *bufs[render_count]; + shm_get_many(chain, render_count, widths, heights, bufs); + + uint32_t fg = term->conf->colors.use_custom.jump_label + ? term->conf->colors.jump_label.fg + : term->colors.table[0]; + uint32_t bg = term->conf->colors.use_custom.jump_label + ? term->conf->colors.jump_label.bg + : term->colors.table[3]; + + for (size_t i = 0; i < render_count; i++) { + struct wl_surface *surf = info[i].url->surf.surf; + struct wl_subsurface *sub_surf = info[i].url->surf.sub; + + const wchar_t *label = info[i].text; + const int x = info[i].x; + const int y = info[i].y; + + xassert(surf != NULL); + xassert(sub_surf != NULL); wl_subsurface_set_position( sub_surf, (term->margins.left + x) / term->scale, (term->margins.top + y) / term->scale); - uint32_t fg = term->conf->colors.use_custom.jump_label - ? term->conf->colors.jump_label.fg - : term->colors.table[0]; - uint32_t bg = term->conf->colors.use_custom.jump_label - ? term->conf->colors.jump_label.bg - : term->colors.table[3]; - render_osd( - term, surf, sub_surf, buf, label, fg, bg, x_margin, y_margin); + term, surf, sub_surf, bufs[i], label, fg, bg, x_margin, y_margin); + + free(info[i].text); } } @@ -3119,8 +3191,10 @@ fdm_tiocswinsz(struct fdm *fdm, int fd, int events, void *data) if (events & EPOLLIN) tiocswinsz(term); - fdm_del(fdm, fd); - term->window->resize_timeout_fd = -1; + if (term->window->resize_timeout_fd >= 0) { + fdm_del(fdm, term->window->resize_timeout_fd); + term->window->resize_timeout_fd = -1; + } return true; } @@ -3405,6 +3479,7 @@ damage_view: tll_free(term->normal.scroll_damage); tll_free(term->alt.scroll_damage); + shm_unref(term->render.last_buf); term->render.last_buf = NULL; term_damage_view(term); render_refresh_csd(term); diff --git a/selection.c b/selection.c index 57ab8bf6..537614f6 100644 --- a/selection.c +++ b/selection.c @@ -69,10 +69,8 @@ selection_on_rows(const struct terminal *term, int row_start, int row_end) end = tmp; } - if (row_start >= start->row && row_end <= end->row) { - LOG_INFO("ON ROWS"); + if (row_start >= start->row && row_end <= end->row) return true; - } return false; } diff --git a/server.c b/server.c index 9ab70f03..06f477db 100644 --- a/server.c +++ b/server.c @@ -132,13 +132,6 @@ term_shutdown_handler(void *data, int exit_code) { struct terminal_instance *instance = data; - struct wl_shm *shm = instance->server->wayl->shm; - - shm_purge(shm, shm_cookie_grid(instance->terminal)); - shm_purge(shm, shm_cookie_search(instance->terminal)); - for (enum csd_surface surf = 0; surf < CSD_SURF_COUNT; surf++) - shm_purge(shm, shm_cookie_csd(instance->terminal, surf)); - instance->terminal = NULL; instance_destroy(instance, exit_code); } diff --git a/shm.c b/shm.c index ed20af3a..60e45d4d 100644 --- a/shm.c +++ b/shm.c @@ -55,11 +55,43 @@ */ 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; +struct buffer_pool { + int fd; /* memfd */ + struct wl_shm_pool *wl_pool; + + void *real_mmapped; /* Address returned from mmap */ + size_t mmap_size; /* Size of mmap (>= size) */ + + size_t ref_count; +}; + +struct buffer_chain; +struct buffer_private { + struct buffer public; + struct buffer_chain *chain; + + size_t ref_count; + bool busy; /* Owned by compositor */ + + struct buffer_pool *pool; + off_t offset; /* Offset into memfd where data begins */ + size_t size; + + bool scrollable; +}; + +struct buffer_chain { + tll(struct buffer_private *) bufs; + struct wl_shm *shm; + size_t pix_instances; + bool scrollable; +}; + +static tll(struct buffer_private *) deferred; + #undef MEASURE_SHM_ALLOCS #if defined(MEASURE_SHM_ALLOCS) static size_t max_alloced = 0; @@ -85,34 +117,70 @@ buffer_destroy_dont_close(struct buffer *buf) free(buf->pix); buf->pix = NULL; buf->wl_buf = NULL; - buf->mmapped = NULL; + buf->data = NULL; } static void -buffer_destroy(struct buffer *buf) +pool_unref(struct buffer_pool *pool) { - 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); + if (pool == NULL) + return; - buf->real_mmapped = MAP_FAILED; + xassert(pool->ref_count > 0); + pool->ref_count--; + + if (pool->ref_count > 0) + return; + + if (pool->real_mmapped != MAP_FAILED) + munmap(pool->real_mmapped, pool->mmap_size); + if (pool->wl_pool != NULL) + wl_shm_pool_destroy(pool->wl_pool); + if (pool->fd >= 0) + close(pool->fd); + + pool->real_mmapped = MAP_FAILED; + pool->wl_pool = NULL; + pool->fd = -1; + free(pool); +} + +static void +buffer_destroy(struct buffer_private *buf) +{ + buffer_destroy_dont_close(&buf->public); + pool_unref(buf->pool); buf->pool = NULL; - buf->fd = -1; - free(buf->scroll_damage); - pixman_region32_fini(&buf->dirty); + free(buf->public.scroll_damage); + pixman_region32_fini(&buf->public.dirty); + free(buf); +} + +static bool +buffer_unref_no_remove_from_chain(struct buffer_private *buf) +{ + xassert(buf->ref_count > 0); + buf->ref_count--; + + if (buf->ref_count > 0) + return false; + + if (buf->busy) + tll_push_back(deferred, buf); + else + buffer_destroy(buf); + return true; } void shm_fini(void) { - tll_foreach(buffers, it) { - buffer_destroy(&it->item); - tll_remove(buffers, it); + LOG_DBG("deferred buffers: %zu", tll_length(deferred)); + + tll_foreach(deferred, it) { + buffer_destroy(it->item); + tll_remove(deferred, it); } #if defined(MEASURE_SHM_ALLOCS) && MEASURE_SHM_ALLOCS @@ -123,11 +191,28 @@ shm_fini(void) static void buffer_release(void *data, struct wl_buffer *wl_buffer) { - struct buffer *buffer = data; - LOG_DBG("release: cookie=%lx (buf=%p)", buffer->cookie, (void *)buffer); - xassert(buffer->wl_buf == wl_buffer); + struct buffer_private *buffer = data; + + xassert(buffer->public.wl_buf == wl_buffer); xassert(buffer->busy); buffer->busy = false; + + if (buffer->ref_count == 0) { + bool found = false; + tll_foreach(deferred, it) { + if (it->item == buffer) { + found = true; + tll_remove(deferred, it); + break; + } + } + + buffer_destroy(buffer); + + xassert(found); + if (!found) + LOG_WARN("deferred delete: buffer not on the 'deferred' list"); + } } static const struct wl_buffer_listener buffer_listener = { @@ -154,47 +239,53 @@ page_size(void) #endif static bool -instantiate_offset(struct wl_shm *shm, struct buffer *buf, off_t new_offset) +instantiate_offset(struct buffer_private *buf, off_t new_offset) { - xassert(buf->fd >= 0); - xassert(buf->mmapped == NULL); - xassert(buf->wl_buf == NULL); - xassert(buf->pix == NULL); + xassert(buf->public.data == NULL); + xassert(buf->public.pix == NULL); + xassert(buf->public.wl_buf == NULL); + xassert(buf->pool != NULL); + + const struct buffer_pool *pool = buf->pool; void *mmapped = MAP_FAILED; struct wl_buffer *wl_buf = NULL; - pixman_image_t **pix = xcalloc(buf->pix_instances, sizeof(*pix)); + pixman_image_t **pix = xcalloc(buf->public.pix_instances, sizeof(*pix)); - mmapped = (uint8_t *)buf->real_mmapped + new_offset; + mmapped = (uint8_t *)pool->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); + pool->wl_pool, new_offset, + buf->public.width, buf->public.height, buf->public.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?) */ - for (size_t i = 0; i < buf->pix_instances; i++) { + for (size_t i = 0; i < buf->public.pix_instances; i++) { pix[i] = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, buf->width, buf->height, (uint32_t *)mmapped, buf->stride); + PIXMAN_a8r8g8b8, buf->public.width, buf->public.height, + (uint32_t *)mmapped, buf->public.stride); if (pix[i] == NULL) { LOG_ERR("failed to create pixman image"); goto err; } } + buf->public.data = mmapped; + buf->public.wl_buf = wl_buf; + buf->public.pix = pix; 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) { - for (size_t i = 0; i < buf->pix_instances; i++) + for (size_t i = 0; i < buf->public.pix_instances; i++) if (pix[i] != NULL) pixman_image_unref(pix[i]); } @@ -206,76 +297,12 @@ err: return false; } -struct buffer * -shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie, bool scrollable, size_t pix_instances) +static void NOINLINE +get_new_buffers(struct buffer_chain *chain, size_t count, + int widths[static count], int heights[static count], + struct buffer *bufs[static count], bool immediate_purge) { - /* Purge buffers marked for purging */ - tll_foreach(buffers, it) { - if (it->item.cookie != cookie) - continue; - - if (!it->item.purge) - continue; - - xassert(!it->item.busy); - - LOG_DBG("cookie=%lx: purging buffer %p (width=%d, height=%d): %zu KB", - cookie, (void *)&it->item, it->item.width, it->item.height, - it->item.size / 1024); - - buffer_destroy(&it->item); - tll_remove(buffers, it); - } - - struct buffer *cached = NULL; - - tll_foreach(buffers, it) { - if (it->item.width != width) - continue; - if (it->item.height != height) - continue; - if (it->item.cookie != cookie) - continue; - - 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) - continue; - - if (it->item.busy) - continue; - - if (it->item.width == width && it->item.height == height) - continue; - - LOG_DBG("cookie=%lx: marking buffer %p for purging", cookie, (void *)&it->item); - it->item.purge = true; - } - + xassert(count == 1 || !chain->scrollable); /* * No existing buffer available. Create a new one by: * @@ -286,14 +313,21 @@ shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie, * The pixman image and the wayland buffer are now sharing memory. */ + int stride[count]; + int sizes[count]; + + size_t total_size = 0; + for (size_t i = 0; i < count; i++) { + stride[i] = stride_for_format_and_width(PIXMAN_a8r8g8b8, widths[i]); + sizes[i] = stride[i] * heights[i]; + total_size += sizes[i]; + } + int pool_fd = -1; - 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; - - LOG_DBG("cookie=%lx: allocating new buffer: %zu KB", cookie, size / 1024); + struct wl_shm_pool *wl_pool = NULL; + struct buffer_pool *pool = NULL; /* Backing memory for SHM */ #if defined(MEMFD_CREATE) @@ -312,14 +346,20 @@ shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie, } #if __SIZEOF_POINTER__ == 8 - 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; + off_t offset = chain->scrollable && max_pool_size > 0 + ? (max_pool_size / 4) & ~(page_size() - 1) + : 0; + off_t memfd_size = chain->scrollable && max_pool_size > 0 + ? max_pool_size + : total_size; #else - off_t initial_offset = 0; - off_t memfd_size = size; + off_t offset = 0; + off_t memfd_size = total_size; #endif - LOG_DBG("memfd-size: %lu, initial offset: %lu", memfd_size, initial_offset); + xassert(chain->scrollable || (offset == 0 && memfd_size == total_size)); + + LOG_DBG("memfd-size: %lu, initial offset: %lu", memfd_size, offset); if (ftruncate(pool_fd, memfd_size) == -1) { LOG_ERRNO("failed to set size of SHM backing memory file"); @@ -344,10 +384,10 @@ shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie, #endif } - if (scrollable && !can_punch_hole) { - initial_offset = 0; - memfd_size = size; - scrollable = false; + if (chain->scrollable && !can_punch_hole) { + offset = 0; + memfd_size = total_size; + chain->scrollable = false; if (ftruncate(pool_fd, memfd_size) < 0) { LOG_ERRNO("failed to set size of SHM backing memory file"); @@ -375,37 +415,66 @@ shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie, } #endif - pool = wl_shm_create_pool(shm, pool_fd, memfd_size); - if (pool == NULL) { + wl_pool = wl_shm_create_pool(chain->shm, pool_fd, memfd_size); + if (wl_pool == NULL) { LOG_ERR("failed to create SHM pool"); goto err; } - /* Push to list of available buffers, but marked as 'busy' */ - tll_push_back( - buffers, - ((struct buffer){ - .cookie = cookie, - .width = width, - .height = height, - .stride = stride, - .busy = true, - .size = size, - .pix_instances = pix_instances, - .fd = pool_fd, - .pool = pool, - .scrollable = scrollable, - .real_mmapped = real_mmapped, - .mmap_size = memfd_size, - .offset = 0, - .age = 1234, /* Force a full repaint */ - })); - - struct buffer *ret = &tll_back(buffers); - if (!instantiate_offset(shm, ret, initial_offset)) + pool = xmalloc(sizeof(*pool)); + if (pool == NULL) { + LOG_ERRNO("failed to allocate buffer pool"); goto err; + } - pixman_region32_init(&ret->dirty); + *pool = (struct buffer_pool){ + .fd = pool_fd, + .wl_pool = wl_pool, + .real_mmapped = real_mmapped, + .mmap_size = memfd_size, + .ref_count = 0, + }; + + for (size_t i = 0; i < count; i++) { + if (sizes[i] == 0) { + bufs[i] = NULL; + continue; + } + + /* Push to list of available buffers, but marked as 'busy' */ + struct buffer_private *buf = xmalloc(sizeof(*buf)); + *buf = (struct buffer_private){ + .public = { + .width = widths[i], + .height = heights[i], + .stride = stride[i], + .pix_instances = chain->pix_instances, + .age = 1234, /* Force a full repaint */ + }, + .chain = chain, + .ref_count = immediate_purge ? 0 : 1, + .busy = true, + .pool = pool, + .offset = 0, + .size = sizes[i], + .scrollable = chain->scrollable, + }; + + if (!instantiate_offset(buf, offset)) { + free(buf); + goto err; + } + + if (immediate_purge) + tll_push_front(deferred, buf); + else + tll_push_front(chain->bufs, buf); + + pixman_region32_init(&buf->public.dirty); + pool->ref_count++; + offset += buf->size; + bufs[i] = &buf->public; + } #if defined(MEASURE_SHM_ALLOCS) && MEASURE_SHM_ALLOCS { @@ -417,11 +486,19 @@ shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie, } #endif - return ret; + if (!shm_can_scroll(bufs[0])) { + /* We only need to keep the pool FD open if we’re going to SHM + * scroll it */ + close(pool_fd); + pool->fd = -1; + } + + return; err: - if (pool != NULL) - wl_shm_pool_destroy(pool); + pool_unref(pool); + if (wl_pool != NULL) + wl_shm_pool_destroy(wl_pool); if (real_mmapped != MAP_FAILED) munmap(real_mmapped, memfd_size); if (pool_fd != -1) @@ -429,13 +506,80 @@ err: /* We don't handle this */ abort(); - return NULL; +} + +void +shm_get_many(struct buffer_chain *chain, size_t count, + int widths[static count], int heights[static count], + struct buffer *bufs[static count]) +{ + get_new_buffers(chain, count, widths, heights, bufs, true); +} + +struct buffer * +shm_get_buffer(struct buffer_chain *chain, int width, int height) +{ + LOG_DBG( + "chain=%p: looking for a re-usable %dx%d buffer " + "among %zu potential buffers", + (void *)chain, width, height, tll_length(chain->bufs)); + + struct buffer_private *cached = NULL; + tll_foreach(chain->bufs, it) { + struct buffer_private *buf = it->item; + + if (buf->public.width != width || buf->public.height != height) { + LOG_DBG("purging mismatching buffer %p", (void *)buf); + if (buffer_unref_no_remove_from_chain(buf)) + tll_remove(chain->bufs, it); + continue; + } + + if (buf->busy) + buf->public.age++; + else +#if FORCED_DOUBLE_BUFFERING + if (buf->age == 0) + buf->age++; + else +#endif + { + if (cached == NULL) { + LOG_DBG("re-using buffer %p from cache", (void *)buf); + buf->busy = true; + pixman_region32_clear(&buf->public.dirty); + free(buf->public.scroll_damage); + buf->public.scroll_damage = NULL; + xassert(buf->public.pix_instances == chain->pix_instances); + cached = it->item; + } else { + /* We have multiple buffers eligible for + * re-use. Pick the “youngest” one, and mark the + * other one for purging */ + if (buf->public.age < cached->public.age) { + shm_unref(&cached->public); + cached = buf; + } else { + if (buffer_unref_no_remove_from_chain(buf)) + tll_remove(chain->bufs, it); + } + } + } + } + + if (cached != NULL) + return &cached->public; + + struct buffer *ret; + get_new_buffers(chain, 1, &width, &height, &ret, false); + return ret; } bool -shm_can_scroll(const struct buffer *buf) +shm_can_scroll(const struct buffer *_buf) { #if __SIZEOF_POINTER__ == 8 + const struct buffer_private *buf = (const struct buffer_private *)_buf; return can_punch_hole && max_pool_size > 0 && buf->scrollable; #else /* Not enough virtual address space in 32-bit */ @@ -445,14 +589,20 @@ shm_can_scroll(const struct buffer *buf) #if __SIZEOF_POINTER__ == 8 && defined(FALLOC_FL_PUNCH_HOLE) static bool -wrap_buffer(struct wl_shm *shm, struct buffer *buf, off_t new_offset) +wrap_buffer(struct buffer_private *buf, off_t new_offset) { + struct buffer_pool *pool = buf->pool; + xassert(pool->ref_count == 1); + /* We don't allow overlapping offsets */ - off_t UNUSED diff = - new_offset < buf->offset ? buf->offset - new_offset : new_offset - buf->offset; + off_t UNUSED diff = new_offset < buf->offset + ? buf->offset - new_offset + : new_offset - buf->offset; xassert(diff > buf->size); - memcpy((uint8_t *)buf->real_mmapped + new_offset, buf->mmapped, buf->size); + memcpy((uint8_t *)pool->real_mmapped + new_offset, + buf->public.data, + buf->size); off_t trim_ofs, trim_len; if (new_offset > buf->offset) { @@ -462,11 +612,11 @@ wrap_buffer(struct wl_shm *shm, struct buffer *buf, off_t new_offset) } else { /* Trim everything *after* the new buffer location */ trim_ofs = new_offset + buf->size; - trim_len = buf->mmap_size - trim_ofs; + trim_len = pool->mmap_size - trim_ofs; } if (fallocate( - buf->fd, + pool->fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, trim_ofs, trim_len) < 0) { @@ -475,30 +625,34 @@ wrap_buffer(struct wl_shm *shm, struct buffer *buf, off_t new_offset) } /* Re-instantiate pixman+wl_buffer+raw pointersw */ - buffer_destroy_dont_close(buf); - return instantiate_offset(shm, buf, new_offset); + buffer_destroy_dont_close(&buf->public); + return instantiate_offset(buf, new_offset); } static bool -shm_scroll_forward(struct wl_shm *shm, struct buffer *buf, int rows, +shm_scroll_forward(struct buffer_private *buf, int rows, int top_margin, int top_keep_rows, int bottom_margin, int bottom_keep_rows) { + struct buffer_pool *pool = buf->pool; + xassert(can_punch_hole); xassert(buf->busy); - xassert(buf->pix); - xassert(buf->wl_buf); - xassert(buf->fd >= 0); + xassert(buf->public.pix != NULL); + xassert(buf->public.wl_buf != NULL); + xassert(pool != NULL); + xassert(pool->ref_count == 1); + xassert(pool->fd >= 0); - LOG_DBG("scrolling %d rows (%d bytes)", rows, rows * buf->stride); + LOG_DBG("scrolling %d rows (%d bytes)", rows, rows * buf->public.stride); - const off_t diff = rows * buf->stride; + const off_t diff = rows * buf->public.stride; xassert(rows > 0); xassert(diff < buf->size); if (buf->offset + diff + buf->size > max_pool_size) { LOG_DBG("memfd offset wrap around"); - if (!wrap_buffer(shm, buf, 0)) + if (!wrap_buffer(buf, 0)) goto err; } @@ -507,6 +661,7 @@ shm_scroll_forward(struct wl_shm *shm, struct buffer *buf, int rows, xassert(new_offset + buf->size <= max_pool_size); #if TIME_SCROLL + struct timeval tot; struct timeval time1; gettimeofday(&time1, NULL); @@ -515,10 +670,13 @@ shm_scroll_forward(struct wl_shm *shm, struct buffer *buf, int rows, if (top_keep_rows > 0) { /* Copy current 'top' region to its new location */ + const int stride = buf->public.stride; + uint8_t *base = buf->public.data; + memmove( - (uint8_t *)buf->mmapped + (top_margin + rows) * buf->stride, - (uint8_t *)buf->mmapped + (top_margin + 0) * buf->stride, - top_keep_rows * buf->stride); + base + (top_margin + rows) * stride, + base + (top_margin + 0) * stride, + top_keep_rows * stride); #if TIME_SCROLL gettimeofday(&time2, NULL); @@ -528,14 +686,14 @@ shm_scroll_forward(struct wl_shm *shm, struct buffer *buf, int rows, } /* Destroy old objects (they point to the old offset) */ - buffer_destroy_dont_close(buf); + buffer_destroy_dont_close(&buf->public); /* 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, + pool->fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, trim_ofs, trim_len) < 0) { @@ -551,7 +709,7 @@ shm_scroll_forward(struct wl_shm *shm, struct buffer *buf, int rows, #endif /* Re-instantiate pixman+wl_buffer+raw pointersw */ - bool ret = instantiate_offset(shm, buf, new_offset); + bool ret = instantiate_offset(buf, new_offset); #if TIME_SCROLL struct timeval time4; @@ -562,10 +720,14 @@ shm_scroll_forward(struct wl_shm *shm, struct buffer *buf, int rows, if (ret && bottom_keep_rows > 0) { /* Copy 'bottom' region to its new location */ + const size_t size = buf->size; + const int stride = buf->public.stride; + uint8_t *base = buf->public.data; + 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); + base + size - (bottom_margin + bottom_keep_rows) * stride, + base + size - (bottom_margin + rows + bottom_keep_rows) * stride, + bottom_keep_rows * stride); #if TIME_SCROLL struct timeval time5; @@ -584,16 +746,19 @@ err: } static bool -shm_scroll_reverse(struct wl_shm *shm, struct buffer *buf, int rows, +shm_scroll_reverse(struct buffer_private *buf, int rows, int top_margin, int top_keep_rows, int bottom_margin, int bottom_keep_rows) { xassert(rows > 0); - const off_t diff = rows * buf->stride; + struct buffer_pool *pool = buf->pool; + xassert(pool->ref_count == 1); + + const off_t diff = rows * buf->public.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))) + if (!wrap_buffer(buf, (max_pool_size - buf->size) & ~(page_size() - 1))) goto err; } @@ -611,10 +776,14 @@ shm_scroll_reverse(struct wl_shm *shm, struct buffer *buf, int rows, if (bottom_keep_rows > 0) { /* Copy 'bottom' region to its new location */ + const size_t size = buf->size; + const int stride = buf->public.stride; + uint8_t *base = buf->public.data; + 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); + base + size - (bottom_margin + rows + bottom_keep_rows) * stride, + base + size - (bottom_margin + bottom_keep_rows) * stride, + bottom_keep_rows * stride); #if TIME_SCROLL gettimeofday(&time1, NULL); @@ -624,14 +793,14 @@ shm_scroll_reverse(struct wl_shm *shm, struct buffer *buf, int rows, } /* Destroy old objects (they point to the old offset) */ - buffer_destroy_dont_close(buf); + buffer_destroy_dont_close(&buf->public); /* 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; + const off_t trim_len = pool->mmap_size - trim_ofs; if (fallocate( - buf->fd, + pool->fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, trim_ofs, trim_len) < 0) { @@ -646,7 +815,7 @@ shm_scroll_reverse(struct wl_shm *shm, struct buffer *buf, int rows, #endif /* Re-instantiate pixman+wl_buffer+raw pointers */ - bool ret = instantiate_offset(shm, buf, new_offset); + bool ret = instantiate_offset(buf, new_offset); #if TIME_SCROLL struct timeval time3; @@ -657,10 +826,13 @@ shm_scroll_reverse(struct wl_shm *shm, struct buffer *buf, int rows, if (ret && top_keep_rows > 0) { /* Copy current 'top' region to its new location */ + const int stride = buf->public.stride; + uint8_t *base = buf->public.data; + memmove( - (uint8_t *)buf->mmapped + (top_margin + 0) * buf->stride, - (uint8_t *)buf->mmapped + (top_margin + rows) * buf->stride, - top_keep_rows * buf->stride); + base + (top_margin + 0) * stride, + base + (top_margin + rows) * stride, + top_keep_rows * stride); #if TIME_SCROLL struct timeval time4; @@ -679,36 +851,88 @@ err: #endif /* FALLOC_FL_PUNCH_HOLE */ bool -shm_scroll(struct wl_shm *shm, struct buffer *buf, int rows, +shm_scroll(struct buffer *_buf, int rows, int top_margin, int top_keep_rows, int bottom_margin, int bottom_keep_rows) { #if __SIZEOF_POINTER__ == 8 && defined(FALLOC_FL_PUNCH_HOLE) - if (!shm_can_scroll(buf)) + if (!shm_can_scroll(_buf)) return false; + struct buffer_private *buf = (struct buffer_private *)_buf; + xassert(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); + ? shm_scroll_forward(buf, rows, top_margin, top_keep_rows, bottom_margin, bottom_keep_rows) + : shm_scroll_reverse(buf, -rows, top_margin, top_keep_rows, bottom_margin, bottom_keep_rows); #else return false; #endif } void -shm_purge(struct wl_shm *shm, unsigned long cookie) +shm_purge(struct buffer_chain *chain) { - LOG_DBG("cookie=%lx: purging all buffers", cookie); + LOG_DBG("chain: %p: purging all buffers", (void *)chain); /* Purge old buffers associated with this cookie */ - tll_foreach(buffers, it) { - if (it->item.cookie != cookie) - continue; - - xassert(!it->item.busy); - - buffer_destroy(&it->item); - tll_remove(buffers, it); + tll_foreach(chain->bufs, it) { + if (buffer_unref_no_remove_from_chain(it->item)) + tll_remove(chain->bufs, it); } } + +void +shm_addref(struct buffer *_buf) +{ + struct buffer_private *buf = (struct buffer_private *)_buf; + buf->ref_count++; +} + +void +shm_unref(struct buffer *_buf) +{ + if (_buf == NULL) + return; + + struct buffer_private *buf = (struct buffer_private *)_buf; + struct buffer_chain *chain = buf->chain; + + tll_foreach(chain->bufs, it) { + if (it->item != buf) + continue; + + if (buffer_unref_no_remove_from_chain(buf)) + tll_remove(chain->bufs, it); + break; + } +} + +struct buffer_chain * +shm_chain_new(struct wl_shm *shm, bool scrollable, size_t pix_instances) +{ + struct buffer_chain *chain = xmalloc(sizeof(*chain)); + *chain = (struct buffer_chain){ + .bufs = tll_init(), + .shm = shm, + .pix_instances = pix_instances, + .scrollable = scrollable, + }; + return chain; +} + +void +shm_chain_free(struct buffer_chain *chain) +{ + if (chain == NULL) + return; + + shm_purge(chain); + + if (tll_length(chain->bufs) > 0) { + BUG("chain=%p: there are buffers remaining; " + "is there a missing call to shm_unref()?", (void *)chain); + } + + free(chain); +} diff --git a/shm.h b/shm.h index 385a0e30..180c8525 100644 --- a/shm.h +++ b/shm.h @@ -7,58 +7,71 @@ #include #include -#include "terminal.h" +#include + +struct damage; struct buffer { - unsigned long cookie; - int width; int height; int stride; - bool busy; - size_t size; /* Buffer size */ - void *mmapped; /* Raw data (TODO: rename) */ + void *data; struct wl_buffer *wl_buf; pixman_image_t **pix; size_t pix_instances; - /* 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 */ - unsigned age; + struct damage *scroll_damage; size_t scroll_damage_count; pixman_region32_t dirty; }; -struct buffer *shm_get_buffer( - struct wl_shm *shm, int width, int height, unsigned long cookie, bool scrollable, size_t pix_instances); void shm_fini(void); - void shm_set_max_pool_size(off_t max_pool_size); + +struct buffer_chain; +struct buffer_chain *shm_chain_new( + struct wl_shm *shm, bool scrollable, size_t pix_instances); +void shm_chain_free(struct buffer_chain *chain); + +/* + * Returns a single buffer. + * + * May returned a cached buffer. If so, the buffer’s age indicates how + * many shm_get_buffer() calls have been made for the same + * width/height while the buffer was still busy. + * + * A newly allocated buffer has an age of 1234. + */ +struct buffer *shm_get_buffer(struct buffer_chain *chain, int width, int height); +/* + * Returns many buffers, described by ‘info’, all sharing the same SHM + * buffer pool. + * + * Never returns cached buffers. However, the newly created buffers + * are all inserted into the regular buffer cache, and are treated + * just like buffers created by shm_get_buffer(). + * + * This function is useful when allocating many small buffers, with + * (roughly) the same life time. + * + * Buffers are tagged for immediate purging, and will be destroyed as + * soon as the compositor releases them. + */ +void shm_get_many( + struct buffer_chain *chain, size_t count, + int widths[static count], int heights[static count], + struct buffer *bufs[static count]); + bool shm_can_scroll(const struct buffer *buf); -bool shm_scroll(struct wl_shm *shm, struct buffer *buf, int rows, +bool shm_scroll(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); +void shm_addref(struct buffer *buf); +void shm_unref(struct buffer *buf); -struct terminal; -static inline unsigned long shm_cookie_grid(const struct terminal *term) { return (unsigned long)((uintptr_t)term + 0); } -static inline unsigned long shm_cookie_search(const struct terminal *term) { return (unsigned long)((uintptr_t)term + 1); } -static inline unsigned long shm_cookie_scrollback_indicator(const struct terminal *term) { return (unsigned long)(uintptr_t)term + 2; } -static inline unsigned long shm_cookie_render_timer(const struct terminal *term) { return (unsigned long)(uintptr_t)term + 3; } -static inline unsigned long shm_cookie_csd(const struct terminal *term, int n) { return (unsigned long)((uintptr_t)term + 4 + (n)); } - -struct url; -static inline unsigned long shm_cookie_url(const struct url *url) { return (unsigned long)(uintptr_t)url; } +void shm_purge(struct buffer_chain *chain); diff --git a/sixel.c b/sixel.c index 86878978..18a9c146 100644 --- a/sixel.c +++ b/sixel.c @@ -88,9 +88,10 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) void sixel_destroy(struct sixel *sixel) { - pixman_image_unref(sixel->pix); - free(sixel->data); + if (sixel->pix != NULL) + pixman_image_unref(sixel->pix); + free(sixel->data); sixel->pix = NULL; sixel->data = NULL; } diff --git a/terminal.c b/terminal.c index 6c175147..46e7fc01 100644 --- a/terminal.c +++ b/terminal.c @@ -35,6 +35,7 @@ #include "selection.h" #include "sixel.h" #include "slave.h" +#include "shm.h" #include "spawn.h" #include "url-mode.h" #include "util.h" @@ -1143,6 +1144,14 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .tab_stops = tll_init(), .wl = wayl, .render = { + .chains = { + .grid = shm_chain_new(wayl->shm, true, 1 + conf->render_worker_count), + .search = shm_chain_new(wayl->shm, false, 1), + .scrollback_indicator = shm_chain_new(wayl->shm, false, 1), + .render_timer = shm_chain_new(wayl->shm, false, 1), + .url = shm_chain_new(wayl->shm, false, 1), + .csd = shm_chain_new(wayl->shm, false, 1), + }, .scrollback_lines = conf->scrollback.lines, .app_sync_updates.timer_fd = app_sync_updates_fd, .title = { @@ -1458,6 +1467,14 @@ term_destroy(struct terminal *term) xassert(tll_length(term->render.workers.queue) == 0); tll_free(term->render.workers.queue); + shm_unref(term->render.last_buf); + shm_chain_free(term->render.chains.grid); + shm_chain_free(term->render.chains.search); + shm_chain_free(term->render.chains.scrollback_indicator); + shm_chain_free(term->render.chains.render_timer); + shm_chain_free(term->render.chains.url); + shm_chain_free(term->render.chains.csd); + tll_free(term->tab_stops); tll_foreach(term->ptmx_buffers, it) { @@ -1984,6 +2001,208 @@ term_erase(struct terminal *term, const struct coord *start, const struct coord sixel_overwrite_by_row(term, end->row, 0, end->col + 1); } +void +term_erase_scrollback(struct terminal *term) +{ + const int num_rows = term->grid->num_rows; + const int mask = num_rows - 1; + + const int start = (term->grid->offset + term->rows) & mask; + const int end = (term->grid->offset - 1) & mask; + + const int scrollback_start = term->grid->offset + term->rows; + const int rel_start = (start - scrollback_start + num_rows) & mask; + const int rel_end = (end - scrollback_start + num_rows) & mask; + + const int sel_start = term->selection.start.row; + const int sel_end = term->selection.end.row; + + if (sel_end >= 0) { + /* + * Cancel selection if it touches any of the rows in the + * scrollback, since we can’t have the selection reference + * soon-to-be deleted rows. + * + * This is done by range checking the selection range against + * the scrollback range. + * + * To make this comparison simpler, the start/end absolute row + * numbers are “rebased” against the scrollback start, where + * row 0 is the *first* row in the scrollback. A high number + * thus means the row is further *down* in the scrollback, + * closer to the screen bottom. + */ + + const int rel_sel_start = (sel_start - scrollback_start + num_rows) & mask; + const int rel_sel_end = (sel_end - scrollback_start + num_rows) & mask; + + if ((rel_sel_start <= rel_start && rel_sel_end >= rel_start) || + (rel_sel_start <= rel_end && rel_sel_end >= rel_end) || + (rel_sel_start >= rel_start && rel_sel_end <= rel_end)) + { + selection_cancel(term); + } + } + + tll_foreach(term->grid->sixel_images, it) { + struct sixel *six = &it->item; + const int six_start = (six->pos.row - scrollback_start + num_rows) & mask; + const int six_end = (six->pos.row + six->rows - 1 - scrollback_start + num_rows) & mask; + + if ((six_start <= rel_start && six_end >= rel_start) || + (six_start <= rel_end && six_end >= rel_end) || + (six_start >= rel_start && six_end <= rel_end)) + { + sixel_destroy(six); + tll_remove(term->grid->sixel_images, it); + } + } + + for (int i = start;; i = (i + 1) & mask) { + struct row *row = term->grid->rows[i]; + if (row != NULL) { + if (term->render.last_cursor.row == row) + term->render.last_cursor.row = NULL; + + grid_row_free(row); + term->grid->rows[i] = NULL; + } + + if (i == end) + break; + } + + term->grid->view = term->grid->offset; + term_damage_view(term); +} + +UNITTEST +{ + const int scrollback_rows = 16; + const int term_rows = 5; + const int cols = 5; + + struct fdm *fdm = fdm_init(); + xassert(fdm != NULL); + + struct terminal term = { + .fdm = fdm, + .rows = term_rows, + .cols = cols, + .normal = { + .rows = xcalloc(scrollback_rows, sizeof(term.normal.rows[0])), + .num_rows = scrollback_rows, + .num_cols = cols, + }, + .grid = &term.normal, + .selection = { + .start = {-1, -1}, + .end = {-1, -1}, + .kind = SELECTION_NONE, + .auto_scroll = { + .fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK), + }, + }, + }; + + xassert(term.selection.auto_scroll.fd >= 0); + +#define populate_scrollback() do { \ + for (int i = 0; i < scrollback_rows; i++) { \ + if (term.normal.rows[i] == NULL) { \ + struct row *r = xcalloc(1, sizeof(*term.normal.rows[i])); \ + r->cells = xcalloc(cols, sizeof(r->cells[0])); \ + term.normal.rows[i] = r; \ + } \ + } \ + } while (0) + + /* + * Test case 1 - no selection, just verify all rows except those + * on screen have been deleted. + */ + + populate_scrollback(); + term.normal.offset = 11; + term_erase_scrollback(&term); + for (int i = 0; i < scrollback_rows; i++) { + if (i >= term.normal.offset && i < term.normal.offset + term_rows) + xassert(term.normal.rows[i] != NULL); + else + xassert(term.normal.rows[i] == NULL); + } + + /* + * Test case 2 - selection that touches the scrollback. Verify the + * selection is cancelled. + */ + + term.normal.offset = 14; /* Screen covers rows 14,15,0,1,2 */ + + /* Selection covers rows 15,0,1,2,3 */ + term.selection.start = (struct coord){.row = 15}; + term.selection.end = (struct coord){.row = 19}; + term.selection.kind = SELECTION_CHAR_WISE; + + populate_scrollback(); + term_erase_scrollback(&term); + xassert(term.selection.start.row < 0); + xassert(term.selection.end.row < 0); + xassert(term.selection.kind == SELECTION_NONE); + + /* + * Test case 3 - selection that does *not* touch the + * scrollback. Verify the selection is *not* cancelled. + */ + + /* Selection covers rows 15,0 */ + term.selection.start = (struct coord){.row = 15}; + term.selection.end = (struct coord){.row = 16}; + term.selection.kind = SELECTION_CHAR_WISE; + + populate_scrollback(); + term_erase_scrollback(&term); + xassert(term.selection.start.row == 15); + xassert(term.selection.end.row == 16); + xassert(term.selection.kind == SELECTION_CHAR_WISE); + + term.selection.start = (struct coord){-1, -1}; + term.selection.end = (struct coord){-1, -1}; + term.selection.kind = SELECTION_NONE; + + /* + * Test case 4 - sixel that touch the scrollback + */ + + struct sixel six = { + .rows = 5, + .pos = { + .row = 15, + }, + }; + tll_push_back(term.normal.sixel_images, six); + populate_scrollback(); + term_erase_scrollback(&term); + xassert(tll_length(term.normal.sixel_images) == 0); + + /* + * Test case 5 - sixel that does *not* touch the scrollback + */ + six.rows = 3; + tll_push_back(term.normal.sixel_images, six); + populate_scrollback(); + term_erase_scrollback(&term); + xassert(tll_length(term.normal.sixel_images) == 1); + + /* Cleanup */ + tll_free(term.normal.sixel_images); + close(term.selection.auto_scroll.fd); + for (int i = 0; i < scrollback_rows; i++) + grid_row_free(term.normal.rows[i]); + free(term.normal.rows); + fdm_destroy(fdm); +} + int term_row_rel_to_abs(const struct terminal *term, int row) { @@ -2634,9 +2853,13 @@ term_xcursor_update(struct terminal *term) void term_set_window_title(struct terminal *term, const char *title) { + if (term->conf->locked_title && term->window_title_has_been_set) + return; + free(term->window_title); term->window_title = xstrdup(title); render_refresh_title(term); + term->window_title_has_been_set = true; } void @@ -2756,6 +2979,7 @@ print_linewrap(struct terminal *term) return; } + term->grid->cur_row->linebreak = false; term->grid->cursor.lcf = false; const int row = term->grid->cursor.point.row; @@ -2848,7 +3072,7 @@ term_print(struct terminal *term, wchar_t wc, int width) cell->attrs = term->vt.attrs; row->dirty = true; - row->linebreak = false; + row->linebreak = true; /* Advance cursor the 'additional' columns while dirty:ing the cells */ for (int i = 1; i < width && term->grid->cursor.point.col < term->cols - 1; i++) { @@ -2887,7 +3111,7 @@ ascii_printer_fast(struct terminal *term, wchar_t wc) cell->attrs = term->vt.attrs; row->dirty = true; - row->linebreak = false; + row->linebreak = true; /* Advance cursor */ if (unlikely(++term->grid->cursor.point.col >= term->cols)) { diff --git a/terminal.h b/terminal.h index 385b36e0..688e42cc 100644 --- a/terminal.h +++ b/terminal.h @@ -21,6 +21,7 @@ #include "fdm.h" #include "macros.h" #include "reaper.h" +#include "shm.h" #include "wayland.h" /* @@ -42,11 +43,12 @@ struct attributes { uint32_t fg:24; bool clean:1; + bool confined:1; bool have_fg:1; bool have_bg:1; uint32_t selected:2; bool url:1; - uint32_t reserved:2; + uint32_t reserved:1; uint32_t bg:24; }; static_assert(sizeof(struct attributes) == 8, "VT attribute struct too large"); @@ -264,6 +266,7 @@ struct url { enum url_action action; bool url_mode_dont_change_url_attr; /* Entering/exiting URL mode doesn’t touch the cells’ attr.url */ bool osc8; + bool duplicate; }; typedef tll(struct url) url_list_t; @@ -377,11 +380,12 @@ struct terminal { bool ime:1; bool app_sync_updates:1; - bool sixel_scrolling:1; + bool sixel_display_mode:1; bool sixel_private_palette:1; bool sixel_cursor_right_of_graphics:1; } xtsave; + bool window_title_has_been_set; char *window_title; tll(char *) window_title_stack; @@ -472,6 +476,15 @@ struct terminal { enum term_surface active_surface; struct { + struct { + struct buffer_chain *grid; + struct buffer_chain *search; + struct buffer_chain *scrollback_indicator; + struct buffer_chain *render_timer; + struct buffer_chain *url; + struct buffer_chain *csd; + } chains; + /* Scheduled for rendering, as soon-as-possible */ struct { bool grid; @@ -649,6 +662,7 @@ void term_damage_scroll( void term_erase( struct terminal *term, const struct coord *start, const struct coord *end); +void term_erase_scrollback(struct terminal *term); int term_row_rel_to_abs(const struct terminal *term, int row); void term_cursor_home(struct terminal *term); diff --git a/url-mode.c b/url-mode.c index 5f56a2f6..a802cc6c 100644 --- a/url-mode.c +++ b/url-mode.c @@ -469,16 +469,20 @@ remove_overlapping(url_list_t *urls, int cols) */ xassert(in->osc8 || out->osc8); - if (in->osc8) { - url_destroy(&outer->item); - tll_remove(*urls, outer); - } else { - url_destroy(&inner->item); - tll_remove(*urls, inner); - } + if (in->osc8) + outer->item.duplicate = true; + else + inner->item.duplicate = true; } } } + + tll_foreach(*urls, it) { + if (it->item.duplicate) { + url_destroy(&it->item); + tll_remove(*urls, it); + } + } } void @@ -649,6 +653,11 @@ tag_cells_for_url(struct terminal *term, const struct url *url, bool value) c = 0; row = term->grid->rows[r]; + if (row == NULL) { + /* Un-allocated scrollback. This most likely means a + * runaway OSC-8 URL. */ + break; + } row->dirty = true; } } diff --git a/vt.c b/vt.c index b9a4f4b0..b34f8fbe 100644 --- a/vt.c +++ b/vt.c @@ -412,6 +412,29 @@ action_collect(struct terminal *term, uint8_t c) LOG_WARN("only four private/intermediate characters supported"); } +UNITTEST +{ + struct terminal term = {.vt = {.private = 0}}; + uint32_t expected = ' '; + action_collect(&term, ' '); + xassert(term.vt.private == expected); + + expected |= '/' << 8; + action_collect(&term, '/'); + xassert(term.vt.private == expected); + + expected |= '<' << 16; + action_collect(&term, '<'); + xassert(term.vt.private == expected); + + expected |= '?' << 24; + action_collect(&term, '?'); + xassert(term.vt.private == expected); + + action_collect(&term, '?'); + xassert(term.vt.private == expected); +} + static void action_esc_dispatch(struct terminal *term, uint8_t final) { diff --git a/wayland.c b/wayland.c index 80fef88a..0679dee3 100644 --- a/wayland.c +++ b/wayland.c @@ -26,6 +26,7 @@ #include "input.h" #include "render.h" #include "selection.h" +#include "shm.h" #include "util.h" #include "xmalloc.h" @@ -50,8 +51,11 @@ csd_instantiate(struct wl_window *win) static void csd_destroy(struct wl_window *win) { + struct terminal *term = win->term; + for (size_t i = 0; i < ALEN(win->csd.surface); i++) wayl_win_subsurface_destroy(&win->csd.surface[i]); + shm_purge(term->render.chains.csd); } static void @@ -1410,6 +1414,8 @@ wayl_win_destroy(struct wl_window *win) if (win == NULL) return; + struct terminal *term = win->term; + if (win->csd.move_timeout_fd != -1) close(win->csd.move_timeout_fd); @@ -1438,6 +1444,12 @@ wayl_win_destroy(struct wl_window *win) wl_surface_commit(win->search.surf); } + /* URLs */ + tll_foreach(win->urls, it) { + wl_surface_attach(it->item.surf.surf, NULL, 0, 0); + wl_surface_commit(it->item.surf.surf); + } + /* CSD */ for (size_t i = 0; i < ALEN(win->csd.surface); i++) { if (win->csd.surface[i].surf != NULL) { @@ -1465,6 +1477,13 @@ wayl_win_destroy(struct wl_window *win) wayl_win_subsurface_destroy(&win->scrollback_indicator); wayl_win_subsurface_destroy(&win->render_timer); + shm_purge(term->render.chains.search); + shm_purge(term->render.chains.scrollback_indicator); + shm_purge(term->render.chains.render_timer); + shm_purge(term->render.chains.grid); + shm_purge(term->render.chains.url); + shm_purge(term->render.chains.csd); + #if defined(HAVE_XDG_ACTIVATION) if (win->xdg_activation_token != NULL) xdg_activation_token_v1_destroy(win->xdg_activation_token);