summaryrefslogtreecommitdiff
path: root/src/snapshot.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/snapshot.c')
-rw-r--r--src/snapshot.c283
1 files changed, 155 insertions, 128 deletions
diff --git a/src/snapshot.c b/src/snapshot.c
index 8dbb498..82a2805 100644
--- a/src/snapshot.c
+++ b/src/snapshot.c
@@ -46,6 +46,8 @@ void term_snapshot_free(struct term_snapshot *s)
free(s);
}
+/* Make room for cols x rows. A failed call keeps the old contents, so the
+ * snapshot stays readable. */
static bool reserve(struct term_snapshot *s, int cols, int rows)
{
size_t cells = (size_t)cols * (size_t)rows;
@@ -73,38 +75,13 @@ static bool reserve(struct term_snapshot *s, int cols, int rows)
return true;
}
-/* The public contract makes every failed update an empty, retryable snapshot. */
-static void invalidate(struct term_snapshot *s)
-{
- s->view = (struct term_view){ .palette = s->palette };
- s->source = 0;
- s->arena_len = 0;
- s->arena_dead = 0;
- if (s->row_cap > 0) {
- memset(s->keys, 0, (size_t)s->row_cap * sizeof(*s->keys));
- memset(s->row_clusters, 0, (size_t)s->row_cap * sizeof(*s->row_clusters));
- memset(s->selection, 0, (size_t)s->row_cap * sizeof(*s->selection));
- }
-}
-
-static uint32_t arena_add(struct term_snapshot *s, const struct gstore *g, uint32_t handle,
- bool *ok)
+/* Append a cluster record. The caller reserved room for it. */
+static uint32_t arena_add(struct term_snapshot *s, const struct gstore *g, uint32_t handle)
{
uint32_t cps[GRAPHEME_MAX_LEN];
int n = term_gstore_expand(g, handle, cps, GRAPHEME_MAX_LEN);
if (n <= 0)
return ' ';
- if (s->arena_len + (uint32_t)n + 1 > s->arena_cap) {
- uint32_t cap = term_grow_cap32(s->arena_cap ? s->arena_cap * 2 : 256,
- s->arena_len + (uint32_t)n + 1);
- uint32_t *a = realloc(s->arena, cap * sizeof(*a));
- if (!a) {
- *ok = false;
- return 0;
- }
- s->arena = a;
- s->arena_cap = cap;
- }
uint32_t off = s->arena_len;
s->arena[off] = (uint32_t)n;
memcpy(&s->arena[off + 1], cps, (size_t)n * sizeof(*cps));
@@ -112,10 +89,26 @@ static uint32_t arena_add(struct term_snapshot *s, const struct gstore *g, uint3
return TERM_TEXT_CLUSTER | off;
}
+/* Arena words that converting src adds. Counts what arena_add() writes. */
+static uint64_t row_words(const struct gstore *g, const struct cell *src, int cols)
+{
+ uint32_t cps[GRAPHEME_MAX_LEN];
+ uint64_t words = 0;
+ for (int x = 0; x < cols; x++) {
+ uint32_t text = src[x].c;
+ if (!grapheme_is_handle(text))
+ continue;
+ int n = term_gstore_expand(g, text, cps, GRAPHEME_MAX_LEN);
+ if (n > 0)
+ words += (uint64_t)n + 1;
+ }
+ return words;
+}
+
/* Convert one stored row; returns the number of arena words it added. */
static uint32_t convert_row(struct term_snapshot *s, const struct gstore *g,
struct term_cell *dst,
- const struct cell *src, int cols, bool *ok)
+ const struct cell *src, int cols)
{
uint32_t before = s->arena_len;
for (int x = 0; x < cols; x++) {
@@ -124,11 +117,8 @@ static uint32_t convert_row(struct term_snapshot *s, const struct gstore *g,
uint32_t text = c->c;
if (TERM_EXPECT(text < 0x20, 0))
text = text ? ' ' : 0;
- else if (TERM_EXPECT(grapheme_is_handle(text), 0)) {
- text = arena_add(s, g, text, ok);
- if (!*ok)
- return 0;
- }
+ else if (TERM_EXPECT(grapheme_is_handle(text), 0))
+ text = arena_add(s, g, text);
dst[x] = (struct term_cell){
.text = text,
.fg = c->fg,
@@ -140,43 +130,129 @@ static uint32_t convert_row(struct term_snapshot *s, const struct gstore *g,
return s->arena_len - before;
}
+/* What one update does. Phase 1 fills it in and changes no snapshot field. */
+struct plan {
+ bool all; /* rebuild every row and reset the arena */
+ int scrolled; /* rows the content moved up */
+ const uint64_t *keys; /* key of each new row */
+ const int *c0, *c1; /* selected columns of each row */
+};
+
+/* Phase 2: write the plan into the snapshot. Nothing here can fail. */
+static void apply(struct term_snapshot *s, const struct term *t, const struct plan *p)
+{
+ int cols = t->cols, rows = t->rows;
+ int k = p->scrolled;
+ if (p->all) {
+ s->arena_len = 0;
+ s->arena_dead = 0;
+ }
+
+ bool changed = p->all;
+ if (k > 0) {
+ for (int y = 0; y < k; y++)
+ s->arena_dead += s->row_clusters[y];
+ memmove(s->cells, s->cells + (size_t)k * cols,
+ (size_t)(rows - k) * cols * sizeof(struct term_cell));
+ memmove(s->row_clusters, s->row_clusters + k,
+ (size_t)(rows - k) * sizeof(uint32_t));
+ memmove(s->keys, s->keys + k,
+ (size_t)(rows - k) * sizeof(uint64_t));
+ memset(s->keys + (rows - k), 0, (size_t)k * sizeof(uint64_t));
+ memset(s->row_clusters + (rows - k), 0, (size_t)k * sizeof(uint32_t));
+ }
+
+ for (int y = 0; y < rows; y++) {
+ uint64_t key = p->keys[y];
+ if (!p->all && s->keys[y] == key)
+ continue;
+ if (!p->all)
+ s->arena_dead += s->row_clusters[y];
+ const struct cell *src = term_screen_view_row(t, y);
+ s->row_clusters[y] = convert_row(s, t->gstore, s->cells + (size_t)y * cols,
+ src, cols);
+ s->keys[y] = key;
+ changed = true;
+ }
+
+ for (int y = 0; y < rows; y++) {
+ struct term_span span = p->c1[y] >= p->c0[y] ? (struct term_span){ p->c0[y], p->c1[y] + 1 }
+ : (struct term_span){ 0 };
+ if (p->all || span.begin != s->selection[y].begin || span.end != s->selection[y].end)
+ changed = true;
+ s->selection[y] = span;
+ }
+
+ struct term_cursor cursor = term_cursor(t);
+ int view_offset = term_view_offset(t);
+ cursor.pos.y += view_offset;
+ if (cursor.pos.y >= rows)
+ cursor.visible = false;
+ const struct term_cursor *old = &s->view.cursor;
+ if (cursor.pos.x != old->pos.x || cursor.pos.y != old->pos.y ||
+ cursor.style != old->style || cursor.visible != old->visible ||
+ cursor.blink != old->blink ||
+ s->view.scroll_offset != view_offset ||
+ s->view.reverse_video != t->reverse_video)
+ changed = true;
+ if (p->all || s->view.palette_version != t->palette.version) {
+ memcpy(s->palette, t->palette.colors, sizeof(s->palette));
+ s->view.palette_version = t->palette.version;
+ changed = true;
+ }
+
+ s->source = t->serial;
+ s->view.size = (struct term_vector2i){ .x = cols, .y = rows };
+ s->view.cursor = cursor;
+ s->view.scroll_offset = view_offset;
+ s->view.scrolled = k;
+ s->view.reverse_video = t->reverse_video;
+ if (changed || k)
+ s->view.version++;
+}
+
+/* Phase 1 for one update. Allocates and checks everything that can fail,
+ * then hands the plan to apply(). A false return leaves the content as it was. */
static bool term_snapshot_update_inner(struct term_snapshot *s, struct term *t)
{
if (!s || !t)
return false;
int cols = t->cols, rows = t->rows;
- if (!reserve(s, cols, rows)) {
- invalidate(s);
+ bool ok = false;
+ bool resized = s->view.size.x != cols || s->view.size.y != rows;
+ bool all = resized || s->source != t->serial;
+ int scrolled = 0;
+ uint64_t need = 0;
+
+ if (!reserve(s, cols, rows))
return false;
+ /* Scratch for rows above 256: new keys, then the selection spans. */
+ uint64_t stack_keys[256];
+ int stack[2][256];
+ uint64_t *new_keys = stack_keys;
+ int *c0 = stack[0], *c1 = stack[1];
+ void *heap = NULL;
+ if (rows > 256) {
+ heap = malloc((size_t)rows * (sizeof(*new_keys) + 2 * sizeof(*c0)));
+ if (!heap)
+ return false;
+ new_keys = heap;
+ c0 = (int *)(new_keys + rows);
+ c1 = c0 + rows;
}
+ struct plan p = { .keys = new_keys, .c0 = c0, .c1 = c1 };
- bool resized = s->view.size.x != cols || s->view.size.y != rows;
- bool all = resized || s->source != t->serial;
/* Reclaim cluster records once most of the arena is garbage. */
if (s->arena_dead > 4096 && s->arena_dead * 2 > s->arena_len)
all = true;
- if (all) {
- s->arena_len = 0;
- s->arena_dead = 0;
- }
- bool changed = all;
term_hist_prepare_rows(&t->hist, cols);
- if (!term_hist_view_sync(&t->view, &t->hist, cols)) {
- invalidate(s);
- return false;
- }
+ if (!term_hist_view_sync(&t->view, &t->hist, cols))
+ goto done;
term_hist_view_refine(&t->view, &t->hist, cols, rows);
- uint64_t stack_keys[256];
- uint64_t *new_keys = rows <= 256 ? stack_keys : malloc((size_t)rows * sizeof(uint64_t));
- if (!new_keys) {
- invalidate(s);
- return false;
- }
for (int y = 0; y < rows; y++)
new_keys[y] = term_screen_view_row_key(t, y);
- int scrolled = 0;
if (!all && rows > 1) {
int cand_k = 0;
/* Find candidate scroll offset k by checking where top rows moved. */
@@ -219,87 +295,38 @@ static bool term_snapshot_update_inner(struct term_snapshot *s, struct term *t)
}
}
- if (scrolled > 0) {
- int k = scrolled;
- for (int y = 0; y < k; y++)
- s->arena_dead += s->row_clusters[y];
- memmove(s->cells, s->cells + (size_t)k * cols,
- (size_t)(rows - k) * cols * sizeof(struct term_cell));
- memmove(s->row_clusters, s->row_clusters + k,
- (size_t)(rows - k) * sizeof(uint32_t));
- memmove(s->keys, s->keys + k,
- (size_t)(rows - k) * sizeof(uint64_t));
- memset(s->keys + (rows - k), 0, (size_t)k * sizeof(uint64_t));
- memset(s->row_clusters + (rows - k), 0, (size_t)k * sizeof(uint32_t));
- }
-
+ /* Arena words for the rows that apply() converts. Count them before
+ * anything changes, then grow the arena once so apply() cannot fail. */
+ need = all ? 0 : s->arena_len;
for (int y = 0; y < rows; y++) {
- uint64_t key = new_keys[y];
- if (!all && s->keys[y] == key)
+ uint64_t kept = s->keys[y];
+ if (scrolled)
+ kept = y + scrolled < rows ? s->keys[y + scrolled] : 0;
+ if (!all && kept == new_keys[y])
continue;
- if (!all)
- s->arena_dead += s->row_clusters[y];
- const struct cell *src = term_screen_view_row(t, y);
- bool converted = true;
- s->row_clusters[y] = convert_row(s, t->gstore, s->cells + (size_t)y * cols,
- src, cols, &converted);
- if (!converted) {
- if (new_keys != stack_keys)
- free(new_keys);
- invalidate(s);
- return false;
- }
- s->keys[y] = key;
- changed = true;
- }
- if (new_keys != stack_keys)
- free(new_keys);
-
- int stack[2][256];
- int *c0 = rows <= 256 ? stack[0] : malloc((size_t)rows * 2 * sizeof(int));
- if (!c0) {
- invalidate(s);
- return false;
+ need += row_words(t->gstore, term_screen_view_row(t, y), cols);
}
- int *c1 = rows <= 256 ? stack[1] : c0 + rows;
- term_selection_view_spans(t, c0, c1);
- for (int y = 0; y < rows; y++) {
- struct term_span span = c1[y] >= c0[y] ? (struct term_span){ c0[y], c1[y] + 1 }
- : (struct term_span){ 0 };
- if (all || span.begin != s->selection[y].begin || span.end != s->selection[y].end)
- changed = true;
- s->selection[y] = span;
+ /* Offsets must fit below TERM_TEXT_CLUSTER. */
+ if (need > TERM_TEXT_CLUSTER)
+ goto done;
+ if (need > s->arena_cap) {
+ uint32_t cap = term_grow_cap32(s->arena_cap ? s->arena_cap * 2 : 256, (uint32_t)need);
+ uint32_t *a = realloc(s->arena, cap * sizeof(*a));
+ if (!a)
+ goto done;
+ s->arena = a;
+ s->arena_cap = cap;
}
- if (c0 != stack[0])
- free(c0);
- struct term_cursor cursor = term_cursor(t);
- int view_offset = term_view_offset(t);
- cursor.pos.y += view_offset;
- if (cursor.pos.y >= rows)
- cursor.visible = false;
- const struct term_cursor *old = &s->view.cursor;
- if (cursor.pos.x != old->pos.x || cursor.pos.y != old->pos.y ||
- cursor.style != old->style || cursor.visible != old->visible ||
- cursor.blink != old->blink ||
- s->view.scroll_offset != view_offset ||
- s->view.reverse_video != t->reverse_video)
- changed = true;
- if (all || s->view.palette_version != t->palette.version) {
- memcpy(s->palette, t->palette.colors, sizeof(s->palette));
- s->view.palette_version = t->palette.version;
- changed = true;
- }
+ term_selection_view_spans(t, c0, c1);
+ p.all = all;
+ p.scrolled = scrolled;
+ apply(s, t, &p);
+ ok = true;
- s->source = t->serial;
- s->view.size = (struct term_vector2i){ .x = cols, .y = rows };
- s->view.cursor = cursor;
- s->view.scroll_offset = view_offset;
- s->view.scrolled = scrolled;
- s->view.reverse_video = t->reverse_video;
- if (changed || scrolled)
- s->view.version++;
- return true;
+done:
+ free(heap);
+ return ok;
}
bool term_snapshot_update(struct term_snapshot *s, struct term *t)