render: delay reflow for ‘resize-delay-ms’ milliseconds

Reflowing a large scrollback is *slow*. During an interactive resize,
it can easily take long enough that the compositor fills the Wayland
socket with configure events. Eventually, the socket becomes full and
the compositor terminates the connection, causing foot to exit.

This patch is work-in-progress, and the first step towards alleviating
this.

It delays the reflow by:

* Snapshotting (copying) the original grid when an interactive resize
  is started.
* While resizing, we apply a simple truncation resize of the
  grid (like we handle the alt screen).
* When the resize is done, or paused for ‘resize-delay-ms’, the grid
  is reflowed.

TODO: we *must* not allow any changes to the temporary (truncated)
grid during the resize. Any changes to the grid would be lost when the
final reflow is applied. That is, we must completely pause the ptmx
pipe while a resize is in progress.

Future improvements:

The initial copy can be slow. We should be able to avoid it by
rewriting the reflow algorithm to not free anything. This is
complicated by the fact that some resources (e.g. sixel images) are
currently *moved* to the new grid. They’d instead have to be copied.
This commit is contained in:
Daniel Eklöf 2022-10-05 17:05:44 +02:00
parent a9fc7ce180
commit 8179d73daa
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
4 changed files with 123 additions and 20 deletions

View file

@ -213,15 +213,19 @@ commented out will usually be installed to */etc/xdg/foot/foot.ini*.
Default: _0x0_.
*resize-delay-ms*
Time, in milliseconds, of "idle time" before foot sends the new
window dimensions to the client application while doing an
interactive resize of a foot window. Idle time in this context is
a period of time where the window size is not changing.
Time, in milliseconds, of "idle time" before foot performs text
reflow, and sends the new window dimensions to the client
application while doing an interactive resize of a foot
window. Idle time in this context is a period of time where the
window size is not changing.
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 *resize-delay-ms* milliseconds is the
client updated.
does not send the updated dimensions to the client. It also does a
fast "truncating" resize of the grid, instead of actually
reflowing the contents. Only when you pause the fiddling for
*resize-delay-ms* milliseconds is the client updated, and the
contents properly reflowed.
Emphasis is on _while_ here; as soon as the interactive resize
ends (i.e. when you let go of the window border), the final

2
grid.c
View file

@ -210,6 +210,8 @@ grid_snapshot(const struct grid *grid)
clone->offset = grid->offset;
clone->view = grid->view;
clone->cursor = grid->cursor;
clone->saved_cursor = grid->saved_cursor;
clone->kitty_kbd = grid->kitty_kbd;
clone->rows = xcalloc(grid->num_rows, sizeof(clone->rows[0]));
memset(&clone->scroll_damage, 0, sizeof(clone->scroll_damage));
memset(&clone->sixel_images, 0, sizeof(clone->sixel_images));

118
render.c
View file

@ -3663,13 +3663,54 @@ tiocswinsz(struct terminal *term)
}
}
static void
delayed_reflow_of_normal_grid(struct terminal *term)
{
if (term->render.resizing.grid == NULL)
return;
struct coord *const tracking_points[] = {
&term->selection.coords.start,
&term->selection.coords.end,
};
/* Reflow the original (since before the resize was started) grid,
* to the *current* dimensions */
grid_resize_and_reflow(
term->render.resizing.grid,
term->grid->num_rows, term->grid->num_cols,
term->render.resizing.screen_rows, term->rows,
term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0,
tracking_points);
/* Replace the current, truncated, “normal” grid with the
* correctly reflowed one */
grid_free(&term->normal);
term->normal = *term->render.resizing.grid;
free(term->render.resizing.grid);
/* Reset */
term->render.resizing.grid = NULL;
term->render.resizing.screen_rows = 0;
/* Invalidate render pointers */
shm_unref(term->render.last_buf);
term->render.last_buf = NULL;
term->render.last_cursor.row = NULL;
if (term->grid == &term->normal)
term_damage_view(term);
}
static bool
fdm_tiocswinsz(struct fdm *fdm, int fd, int events, void *data)
{
struct terminal *term = data;
if (events & EPOLLIN)
if (events & EPOLLIN) {
tiocswinsz(term);
delayed_reflow_of_normal_grid(term);
}
if (term->window->resize_timeout_fd >= 0) {
fdm_del(fdm, term->window->resize_timeout_fd);
@ -3686,6 +3727,7 @@ send_dimensions_to_client(struct terminal *term)
if (!win->is_resizing || term->conf->resize_delay_ms == 0) {
/* Send new dimensions to client immediately */
tiocswinsz(term);
delayed_reflow_of_normal_grid(term);
/* And make sure to reset and deallocate a lingering timer */
if (win->resize_timeout_fd >= 0) {
@ -3846,9 +3888,30 @@ maybe_resize(struct terminal *term, int width, int height, bool force)
const uint32_t scrollback_lines = term->render.scrollback_lines;
/*
* Snapshot the normal grid.
*
* Since text reflow is slow, dont do it *while* resizing. Only
* do it when done, or after pausing the resize for sufficiently
* long. We re-use the TIOCSWINSZ timer to handle this. See
* send_dimensions_to_client() and fdm_tiocswinsz().
*
* To be able to do the final reflow correctly, we need a copy of
* the original grid, before the resize started.
*/
if (term->window->is_resizing && term->render.resizing.grid == NULL) {
/*
* TODO: snapshotting a large grid is slow. To improve, move
* normal -> resizing.grid, and instantiate a small (screen
* sized) new normal
*/
term->render.resizing.grid = grid_snapshot(&term->normal);
term->render.resizing.screen_rows = term->rows;
}
/* Screen rows/cols before resize */
const int old_cols = term->cols;
const int old_rows = term->rows;
int old_cols = term->cols;
int old_rows = term->rows;
/* Screen rows/cols after resize */
const int new_cols = (term->width - 2 * pad_x) / term->cell_width;
@ -3882,7 +3945,9 @@ maybe_resize(struct terminal *term, int width, int height, bool force)
xassert(term->margins.top >= pad_y);
xassert(term->margins.bottom >= pad_y);
if (new_cols == old_cols && new_rows == old_rows) {
if (new_cols == old_cols && new_rows == old_rows &&
(term->render.resizing.grid == NULL || term->window->is_resizing))
{
LOG_DBG("grid layout unaffected; skipping reflow");
goto damage_view;
}
@ -3906,16 +3971,43 @@ maybe_resize(struct terminal *term, int width, int height, bool force)
* selections pivot point coordinates *must* be added to the
* tracking points list.
*/
struct coord *const tracking_points[] = {
&term->selection.coords.start,
&term->selection.coords.end,
};
/* Resize grids */
grid_resize_and_reflow(
&term->normal, new_normal_grid_rows, new_cols, old_rows, new_rows,
term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0,
tracking_points);
if (term->window->is_resizing) {
/* Simple truncating resize, *while* an interactive resize is
* ongoing. */
xassert(term->render.resizing.grid != NULL);
grid_resize_without_reflow(
&term->normal,
new_normal_grid_rows, new_cols,
old_rows,
new_rows);
} else {
/* Full text reflow */
if (term->render.resizing.grid != NULL) {
/* Throw away the current, truncated, “normal” grid, and
* use the original grid instead (from before the resize
* started) */
grid_free(&term->normal);
term->normal = *term->render.resizing.grid;
free(term->render.resizing.grid);
old_rows = term->render.resizing.screen_rows;
term->render.resizing.grid = NULL;
term->render.resizing.screen_rows = 0;
}
struct coord *const tracking_points[] = {
&term->selection.coords.start,
&term->selection.coords.end,
};
grid_resize_and_reflow(
&term->normal, new_normal_grid_rows, new_cols, old_rows, new_rows,
term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0,
tracking_points);
}
grid_resize_without_reflow(
&term->alt, new_alt_grid_rows, new_cols, old_rows, new_rows);

View file

@ -595,6 +595,11 @@ struct terminal {
size_t search_glyph_offset;
struct {
struct grid *grid;
int screen_rows;
} resizing;
struct timespec input_time;
} render;