diff --git a/CHANGELOG.md b/CHANGELOG.md index fc84a398..6e0b3052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,12 +98,14 @@ highlighted correctly. * Selection not being cancelled correctly when scrolled out. * Extending a multi-page selection behaving inconsistently. +* Poor performance when making very large selections ([#1114][1114]). [1055]: https://codeberg.org/dnkl/foot/issues/1055 [1092]: https://codeberg.org/dnkl/foot/issues/1092 [1097]: https://codeberg.org/dnkl/foot/issues/1097 [1111]: https://codeberg.org/dnkl/foot/issues/1111 [1120]: https://codeberg.org/dnkl/foot/issues/1120 +[1114]: https://codeberg.org/dnkl/foot/issues/1114 ### Security diff --git a/selection.c b/selection.c index 54faf49b..8c3a8357 100644 --- a/selection.c +++ b/selection.c @@ -9,6 +9,8 @@ #include #include +#include + #define LOG_MODULE "selection" #define LOG_ENABLE_DBG 0 #include "log.h" @@ -609,111 +611,150 @@ selection_start(struct terminal *term, int col, int row, } -/* Context used while (un)marking selected cells, to be able to - * exclude empty cells */ -struct mark_context { - const struct row *last_row; - int empty_count; - uint8_t **keep_selection; -}; - -static bool -unmark_selected(struct terminal *term, struct row *row, struct cell *cell, - int row_no, int col, void *data) +static pixman_region32_t +pixman_region_for_coords_normal(const struct terminal *term, + const struct coord *start, + const struct coord *end) { - if (!cell->attrs.selected) - return true; + pixman_region32_t region; + pixman_region32_init(®ion); - struct mark_context *ctx = data; - const uint8_t *keep_selection = - ctx->keep_selection != NULL ? ctx->keep_selection[row_no] : NULL; + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); - if (keep_selection != NULL) { - unsigned idx = (unsigned)col / 8; - unsigned ofs = (unsigned)col % 8; + if (rel_start_row < rel_end_row) { + /* First partial row (start ->)*/ + pixman_region32_union_rect( + ®ion, ®ion, + start->col, rel_start_row, + term->cols - start->col, 1); - if (keep_selection[idx] & (1 << ofs)) { - /* We’re updating the selection, and this cell is still - * going to be selected */ - return true; + /* Full rows between start and end */ + if (rel_start_row + 1 < rel_end_row) { + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_start_row + 1, + term->cols, rel_end_row - rel_start_row - 1); } + + /* Last partial row (-> end) */ + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_end_row, + end->col + 1, 1); + + } else if (rel_start_row > rel_end_row) { + /* First partial row (end ->) */ + pixman_region32_union_rect( + ®ion, ®ion, + end->col, rel_end_row, + term->cols - end->col, 1); + + /* Full rows between end and start */ + if (rel_end_row + 1 < rel_start_row) { + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_end_row + 1, + term->cols, rel_start_row - rel_end_row - 1); + } + + /* Last partial row (-> start) */ + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_start_row, + start->col + 1, 1); + } else { + const int start_col = min(start->col, end->col); + const int end_col = max(start->col, end->col); + + pixman_region32_union_rect( + ®ion, ®ion, + start_col, rel_start_row, + end_col + 1 - start_col, 1); } - row->dirty = true; - cell->attrs.selected = false; - cell->attrs.clean = false; - return true; + return region; } -static bool -premark_selected(struct terminal *term, struct row *row, struct cell *cell, - int row_no, int col, void *data) +static pixman_region32_t +pixman_region_for_coords_block(const struct terminal *term, + const struct coord *start, const struct coord *end) { - struct mark_context *ctx = data; - xassert(ctx != NULL); + pixman_region32_t region; + pixman_region32_init(®ion); - if (ctx->last_row != row) { - ctx->last_row = row; - ctx->empty_count = 0; - } + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); - if (cell->wc == 0 && term->selection.kind != SELECTION_BLOCK) { - ctx->empty_count++; - return true; - } + pixman_region32_union_rect( + ®ion, ®ion, + min(start->col, end->col), min(rel_start_row, rel_end_row), + abs(start->col - end->col) + 1, abs(rel_start_row - rel_end_row) + 1); - uint8_t *keep_selection = ctx->keep_selection[row_no]; - if (keep_selection == NULL) { - keep_selection = xcalloc((term->grid->num_cols + 7) / 8, sizeof(keep_selection[0])); - ctx->keep_selection[row_no] = keep_selection; - } - - /* Tell unmark to leave this be */ - for (int i = 0; i < ctx->empty_count + 1; i++) { - unsigned idx = (unsigned)(col - i) / 8; - unsigned ofs = (unsigned)(col - i) % 8; - keep_selection[idx] |= 1 << ofs; - } - - ctx->empty_count = 0; - return true; + return region; } -static bool -mark_selected(struct terminal *term, struct row *row, struct cell *cell, - int row_no, int col, void *data) +/* Returns a pixman region representing the selection between ‘start’ + * and ‘end’ (given the current selection kind), in *scrollback + * relative coordinates* */ +static pixman_region32_t +pixman_region_for_coords(const struct terminal *term, + const struct coord *start, const struct coord *end) { - struct mark_context *ctx = data; - xassert(ctx != NULL); - - if (ctx->last_row != row) { - ctx->last_row = row; - ctx->empty_count = 0; + switch (term->selection.kind) { + default: return pixman_region_for_coords_normal(term, start, end); + case SELECTION_BLOCK: return pixman_region_for_coords_block(term, start, end); } - - if (cell->wc == 0 && term->selection.kind != SELECTION_BLOCK) { - ctx->empty_count++; - return true; - } - - for (int i = 0; i < ctx->empty_count + 1; i++) { - struct cell *c = &row->cells[col - i]; - if (!c->attrs.selected) { - row->dirty = true; - c->attrs.selected = true; - c->attrs.clean = false; - } - } - - ctx->empty_count = 0; - return true; } static void -reset_modify_context(struct mark_context *ctx) +mark_selected_region(struct terminal *term, pixman_box32_t *boxes, + size_t count, bool selected, bool dirty_cells) { - ctx->last_row = NULL; - ctx->empty_count = 0; + for (size_t i = 0; i < count; i++) { + const pixman_box32_t *box = &boxes[i]; + + LOG_DBG("%s selection in region: %dx%d - %dx%d", + selected ? "marking" : "unmarking", + box->x1, box->y1, + box->x2, box->y2); + + int abs_row_start = grid_row_sb_to_abs( + term->grid, term->rows, box->y1); + + for (int r = abs_row_start, rel_r = box->y1; + rel_r < box->y2; + r = (r + 1) & (term->grid->num_rows - 1), rel_r++) + { + struct row *row = term->grid->rows[r]; + xassert(row != NULL); + + if (dirty_cells) + row->dirty = true; + + for (int c = box->x1, empty_count = 0; c < box->x2; c++) { + if (selected && row->cells[c].wc == 0) { + empty_count++; + continue; + } + + for (int j = 0; j < empty_count + 1; j++) { + xassert(c - j >= 0); + struct cell *cell = &row->cells[c - j]; + + if (dirty_cells) + cell->attrs.clean = false; + cell->attrs.selected = selected; + } + + empty_count = 0; + } + } + } } static void @@ -723,33 +764,46 @@ selection_modify(struct terminal *term, struct coord start, struct coord end) xassert(start.row != -1 && start.col != -1); xassert(end.row != -1 && end.col != -1); - uint8_t **keep_selection = - xcalloc(term->grid->num_rows, sizeof(keep_selection[0])); - - struct mark_context ctx = {.keep_selection = keep_selection}; - - /* Premark all cells that *will* be selected */ - foreach_selected(term, start, end, &premark_selected, &ctx); - reset_modify_context(&ctx); - + pixman_region32_t previous_selection; if (term->selection.coords.end.row >= 0) { - /* Unmark previous selection, ignoring cells that are part of - * the new selection */ - foreach_selected(term, term->selection.coords.start, term->selection.coords.end, - &unmark_selected, &ctx); - reset_modify_context(&ctx); - } + previous_selection = pixman_region_for_coords( + term, + &term->selection.coords.start, + &term->selection.coords.end); + } else + pixman_region32_init(&previous_selection); + + pixman_region32_t current_selection = pixman_region_for_coords( + term, &start, &end); + + pixman_region32_t no_longer_selected; + pixman_region32_init(&no_longer_selected); + pixman_region32_subtract( + &no_longer_selected, &previous_selection, ¤t_selection); + + pixman_region32_t newly_selected; + pixman_region32_init(&newly_selected); + pixman_region32_subtract( + &newly_selected, ¤t_selection, &previous_selection); + + /* Clear selection in cells no longer selected */ + int n_rects = -1; + pixman_box32_t *boxes = NULL; + + boxes = pixman_region32_rectangles(&no_longer_selected, &n_rects); + mark_selected_region(term, boxes, n_rects, false, true); + + boxes = pixman_region32_rectangles(&newly_selected, &n_rects); + mark_selected_region(term, boxes, n_rects, true, true); + + pixman_region32_fini(&newly_selected); + pixman_region32_fini(&no_longer_selected); + pixman_region32_fini(¤t_selection); + pixman_region32_fini(&previous_selection); term->selection.coords.start = start; term->selection.coords.end = end; - - /* Mark new selection */ - foreach_selected(term, start, end, &mark_selected, &ctx); render_refresh(term); - - for (size_t i = 0; i < term->grid->num_rows; i++) - free(keep_selection[i]); - free(keep_selection); } static void @@ -990,9 +1044,26 @@ selection_dirty_cells(struct terminal *term) if (term->selection.coords.start.row < 0 || term->selection.coords.end.row < 0) return; - foreach_selected( - term, term->selection.coords.start, term->selection.coords.end, &mark_selected, - &(struct mark_context){0}); + pixman_region32_t selection = pixman_region_for_coords( + term, &term->selection.coords.start, &term->selection.coords.end); + + pixman_region32_t view = pixman_region_for_coords( + term, + &(struct coord){0, term->grid->view}, + &(struct coord){term->cols - 1, term->grid->view + term->rows - 1}); + + pixman_region32_t visible_and_selected; + pixman_region32_init(&visible_and_selected); + pixman_region32_intersect(&visible_and_selected, &selection, &view); + + int n_rects = -1; + pixman_box32_t *boxes = + pixman_region32_rectangles(&visible_and_selected, &n_rects); + mark_selected_region(term, boxes, n_rects, true, false); + + pixman_region32_fini(&visible_and_selected); + pixman_region32_fini(&view); + pixman_region32_fini(&selection); } static void @@ -1270,6 +1341,19 @@ selection_finalize(struct seat *seat, struct terminal *term, uint32_t serial) } } +static bool +unmark_selected(struct terminal *term, struct row *row, struct cell *cell, + int row_no, int col, void *data) +{ + if (!cell->attrs.selected) + return true; + + row->dirty = true; + cell->attrs.selected = false; + cell->attrs.clean = false; + return true; +} + void selection_cancel(struct terminal *term) { @@ -1282,7 +1366,7 @@ selection_cancel(struct terminal *term) if (term->selection.coords.start.row >= 0 && term->selection.coords.end.row >= 0) { foreach_selected( term, term->selection.coords.start, term->selection.coords.end, - &unmark_selected, &(struct mark_context){0}); + &unmark_selected, NULL); render_refresh(term); }