selection: rework how we update a selection

Before this patch, each selection update would result in grid covered
by the selection being walked *three* times. First to “premark” the
area that *will* be selected after the update, then again to unmark
the previous selection (excluding the cells that were premarked - but
the cells are still iterated), and then one more time to finalize the
selection state in the grid.

Furthermore, each time a frame is rendered, the entire selection were
iterated again, to ensure all the cells have their ‘selected’ bit
set.

This quickly gets *very* slow.

This patch takes a completely different approach. Instead of looking
at the selection as a range of cells to iterate, we view it as an
area, or region. Thus, on each update, we have to regions: the region
representing the previous selection, and the region representing the
to-be selection.

By diffing these two regions, we get two new regions: one that
represents the cells that were selected, but aren’t any more, and one
that represents the cells that previously were not selected, but now
will be.

We implement the regions using pixman regions. By subtracting the
current selection from the previous selection, we get the region
representing the cells that are no longer selected, and that should be
unmarked.

By subtracting the previous selection from the current, we get the
region representing the cells that was added to the selection in this
update, and that should be marked.

selection_dirty_cells() is rewritten in a similar manner. We create
pixman regions for the selection, and the current scrollback view. The
intersection represents the (selected) cells that are visible. These
need to iterated and marked as being selected.

Closes #1114
This commit is contained in:
Daniel Eklöf 2022-07-28 18:45:25 +02:00
parent b8506bbea0
commit fa2d9f8699
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
2 changed files with 196 additions and 110 deletions

View file

@ -98,12 +98,14 @@
highlighted correctly. highlighted correctly.
* Selection not being cancelled correctly when scrolled out. * Selection not being cancelled correctly when scrolled out.
* Extending a multi-page selection behaving inconsistently. * Extending a multi-page selection behaving inconsistently.
* Poor performance when making very large selections ([#1114][1114]).
[1055]: https://codeberg.org/dnkl/foot/issues/1055 [1055]: https://codeberg.org/dnkl/foot/issues/1055
[1092]: https://codeberg.org/dnkl/foot/issues/1092 [1092]: https://codeberg.org/dnkl/foot/issues/1092
[1097]: https://codeberg.org/dnkl/foot/issues/1097 [1097]: https://codeberg.org/dnkl/foot/issues/1097
[1111]: https://codeberg.org/dnkl/foot/issues/1111 [1111]: https://codeberg.org/dnkl/foot/issues/1111
[1120]: https://codeberg.org/dnkl/foot/issues/1120 [1120]: https://codeberg.org/dnkl/foot/issues/1120
[1114]: https://codeberg.org/dnkl/foot/issues/1114
### Security ### Security

View file

@ -9,6 +9,8 @@
#include <sys/epoll.h> #include <sys/epoll.h>
#include <sys/timerfd.h> #include <sys/timerfd.h>
#include <pixman.h>
#define LOG_MODULE "selection" #define LOG_MODULE "selection"
#define LOG_ENABLE_DBG 0 #define LOG_ENABLE_DBG 0
#include "log.h" #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 static pixman_region32_t
* exclude empty cells */ pixman_region_for_coords_normal(const struct terminal *term,
struct mark_context { const struct coord *start,
const struct row *last_row; const struct coord *end)
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)
{ {
if (!cell->attrs.selected) pixman_region32_t region;
return true; pixman_region32_init(&region);
struct mark_context *ctx = data; const int rel_start_row =
const uint8_t *keep_selection = grid_row_abs_to_sb(term->grid, term->rows, start->row);
ctx->keep_selection != NULL ? ctx->keep_selection[row_no] : NULL; const int rel_end_row =
grid_row_abs_to_sb(term->grid, term->rows, end->row);
if (keep_selection != NULL) { if (rel_start_row < rel_end_row) {
unsigned idx = (unsigned)col / 8; /* First partial row (start ->)*/
unsigned ofs = (unsigned)col % 8; pixman_region32_union_rect(
&region, &region,
start->col, rel_start_row,
term->cols - start->col, 1);
if (keep_selection[idx] & (1 << ofs)) { /* Full rows between start and end */
/* Were updating the selection, and this cell is still if (rel_start_row + 1 < rel_end_row) {
* going to be selected */ pixman_region32_union_rect(
return true; &region, &region,
0, rel_start_row + 1,
term->cols, rel_end_row - rel_start_row - 1);
} }
/* Last partial row (-> end) */
pixman_region32_union_rect(
&region, &region,
0, rel_end_row,
end->col + 1, 1);
} else if (rel_start_row > rel_end_row) {
/* First partial row (end ->) */
pixman_region32_union_rect(
&region, &region,
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(
&region, &region,
0, rel_end_row + 1,
term->cols, rel_start_row - rel_end_row - 1);
}
/* Last partial row (-> start) */
pixman_region32_union_rect(
&region, &region,
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(
&region, &region,
start_col, rel_start_row,
end_col + 1 - start_col, 1);
} }
row->dirty = true; return region;
cell->attrs.selected = false;
cell->attrs.clean = false;
return true;
} }
static bool static pixman_region32_t
premark_selected(struct terminal *term, struct row *row, struct cell *cell, pixman_region_for_coords_block(const struct terminal *term,
int row_no, int col, void *data) const struct coord *start, const struct coord *end)
{ {
struct mark_context *ctx = data; pixman_region32_t region;
xassert(ctx != NULL); pixman_region32_init(&region);
if (ctx->last_row != row) { const int rel_start_row =
ctx->last_row = row; grid_row_abs_to_sb(term->grid, term->rows, start->row);
ctx->empty_count = 0; 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) { pixman_region32_union_rect(
ctx->empty_count++; &region, &region,
return true; 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]; return region;
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;
} }
static bool /* Returns a pixman region representing the selection between start
mark_selected(struct terminal *term, struct row *row, struct cell *cell, * and end (given the current selection kind), in *scrollback
int row_no, int col, void *data) * 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; switch (term->selection.kind) {
xassert(ctx != NULL); default: return pixman_region_for_coords_normal(term, start, end);
case SELECTION_BLOCK: return pixman_region_for_coords_block(term, start, end);
if (ctx->last_row != row) {
ctx->last_row = row;
ctx->empty_count = 0;
} }
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 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; for (size_t i = 0; i < count; i++) {
ctx->empty_count = 0; 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 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(start.row != -1 && start.col != -1);
xassert(end.row != -1 && end.col != -1); xassert(end.row != -1 && end.col != -1);
uint8_t **keep_selection = pixman_region32_t previous_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);
if (term->selection.coords.end.row >= 0) { if (term->selection.coords.end.row >= 0) {
/* Unmark previous selection, ignoring cells that are part of previous_selection = pixman_region_for_coords(
* the new selection */ term,
foreach_selected(term, term->selection.coords.start, term->selection.coords.end, &term->selection.coords.start,
&unmark_selected, &ctx); &term->selection.coords.end);
reset_modify_context(&ctx); } 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, &current_selection);
pixman_region32_t newly_selected;
pixman_region32_init(&newly_selected);
pixman_region32_subtract(
&newly_selected, &current_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(&current_selection);
pixman_region32_fini(&previous_selection);
term->selection.coords.start = start; term->selection.coords.start = start;
term->selection.coords.end = end; term->selection.coords.end = end;
/* Mark new selection */
foreach_selected(term, start, end, &mark_selected, &ctx);
render_refresh(term); render_refresh(term);
for (size_t i = 0; i < term->grid->num_rows; i++)
free(keep_selection[i]);
free(keep_selection);
} }
static void 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) if (term->selection.coords.start.row < 0 || term->selection.coords.end.row < 0)
return; return;
foreach_selected( pixman_region32_t selection = pixman_region_for_coords(
term, term->selection.coords.start, term->selection.coords.end, &mark_selected, term, &term->selection.coords.start, &term->selection.coords.end);
&(struct mark_context){0});
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 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 void
selection_cancel(struct terminal *term) 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) { if (term->selection.coords.start.row >= 0 && term->selection.coords.end.row >= 0) {
foreach_selected( foreach_selected(
term, term->selection.coords.start, term->selection.coords.end, term, term->selection.coords.start, term->selection.coords.end,
&unmark_selected, &(struct mark_context){0}); &unmark_selected, NULL);
render_refresh(term); render_refresh(term);
} }