libghostty: much faster wide-character reflow on resize

Resizing a terminal whose buffer is heavy with wide characters (CJK,
emoji) is now 3-5x faster on the reflow path. 

This was relatively simple work. We already have a bulk fast path for
same-style cells. We previously omitted ANY wide characters from this.
We relaxed this by making it work with complete wide pairs (wide followed
by spacer tail).

### Benchmarks

Resize dance (13 column resizes, 80 -> 40 -> 132 and back and forth again) 
over a ~2,000-row scrollback buffer.

| Workload | Before | After | Speedup | 
|---|---|---|---|
| cjk | 0.938 | 0.296 | 3.2x | 
| emoji | 0.875 | 0.191 | 4.6x |
| mixed build-log | 0.306 | 0.155 | 2.0x |
| grapheme | 2.449 | 1.831 | 1.3x | 
| ascii-short | 0.115 | 0.117 | 1.0x |
| ascii-long | 0.109 | 0.112 | 1.0x |
| latin | 0.093 | 0.095 | 1.0x | 
| sgr-truecolor | 0.939 | 0.965 | 1.0x |

**AI usage:** Profiled, implemented, benchmarked, and written by Fable.
Plan validated by me before doing it, I wrote all the
comments/commits/blah.
This commit is contained in:
Mitchell Hashimoto
2026-08-14 13:24:20 -07:00
parent 6b22215c5d
commit 88ed6bebf4

View File

@@ -1711,18 +1711,20 @@ const ReflowCursor = struct {
// Fast path: bulk-copy a run of simple cells directly
// into the destination row. The vast majority of cells
// are narrow, have no managed memory (graphemes,
// hyperlinks), and share a single style in long runs, so
// this avoids the per-cell state machine below for most
// of the work. Rows with tracked pins take the slow path
// so pin remapping behaves identically.
// are narrow cells or complete wide pairs, have no
// managed memory (graphemes, hyperlinks), and share a
// single style in long runs, so this avoids the
// per-cell state machine below for most of the work.
// Rows with tracked pins take the slow path so pin
// remapping behaves identically.
if (!row_has_pins) {
const max_run = @min(
cols_len - x,
@as(usize, self.page.size.cols) - self.x,
);
const run = bulkRunLength(cells[x..][0..max_run]);
if (run > 0 and self.copyRun(cells[x..][0..run], src_page)) {
const window = cells[x..][0..max_run];
const run = bulkRunLength(window);
if (run > 0 and self.copyRun(window[0..run], src_page)) {
x += run;
continue;
}
@@ -1898,9 +1900,21 @@ const ReflowCursor = struct {
fn bulkRunLength(cells: []const pagepkg.Cell) usize {
if (cells.len == 0) return 0;
const first = cells[0];
if (!bulkCopyable(first)) return 0;
const run_pattern = BulkRunMask.pattern(first);
// A run may start on a bulk-copyable narrow cell or on a wide pair.
const pair_start = first.content_tag == .codepoint and
first.wide == .wide and
!first.hyperlink;
if (!pair_start and !bulkCopyable(first)) return 0;
// Patterns for each cell shape admitted to the run.
var proto = first;
proto.wide = .narrow;
const narrow_pattern = BulkRunMask.pattern(proto);
proto.wide = .wide;
const wide_pattern = BulkRunMask.pattern(proto);
proto.wide = .spacer_tail;
const tail_pattern = BulkRunMask.pattern(proto);
// Only text cells can contain a placeholder; for bg color
// tags the content bits are a color, so we skip the check for
@@ -1919,28 +1933,50 @@ const ReflowCursor = struct {
));
};
var len: usize = 1;
var len: usize = 0;
outer: while (true) {
// Vectorized scan: check whole groups of narrow cells at
// once. If a group fully matches, the run extends by the
// whole group; otherwise fall through to the scalar loop
// below, which finds the exact end of the run within it
// or continues through a wide pair.
while (cells.len - len >= bulk_group_len) {
if (!BulkRunMask.eql(cells, len, narrow_pattern)) break;
if (check_placeholder and PlaceholderMask.eqlAny(
cells,
len,
placeholder_pattern,
)) break;
len += bulk_group_len;
}
// Vectorized scan: check whole groups of cells at once. If a
// group fully matches, the run extends by the whole group;
// otherwise fall through to the scalar loop below, which
// finds the exact end of the run within it.
while (cells.len - len >= bulk_group_len) {
if (!BulkRunMask.eql(cells, len, run_pattern)) break;
if (check_placeholder and PlaceholderMask.eqlAny(
cells,
len,
placeholder_pattern,
)) break;
len += bulk_group_len;
}
while (len < cells.len) {
const cell = cells[len];
if (BulkRunMask.eqlScalar(cell, narrow_pattern)) {
if (check_placeholder and PlaceholderMask.eqlScalar(
cell,
placeholder_pattern,
)) break :outer;
len += 1;
continue;
}
while (len < cells.len) : (len += 1) {
if (!BulkRunMask.eqlScalar(cells[len], run_pattern)) break;
if (check_placeholder and PlaceholderMask.eqlScalar(
cells[len],
placeholder_pattern,
)) break;
// Wide pairs are consumed atomically so the run
// (or window) end can never split a pair.
if (len + 1 < cells.len and
BulkRunMask.eqlScalar(cell, wide_pattern) and
BulkRunMask.eqlScalar(cells[len + 1], tail_pattern))
{
len += 2;
// Re-enter the vectorized loop: narrow groups
// commonly follow a stretch of pairs.
continue :outer;
}
break :outer;
}
break;
}
return len;
@@ -18207,6 +18243,370 @@ test "PageList resize reflow less cols to wrap a wide char" {
}
}
test "PageList resize reflow less cols wide char bulk run" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, .{ .cols = 8, .rows = 1, .max_size = 0 });
defer s.deinit();
{
try testing.expect(s.pages.first == s.pages.last);
const page = s.pages.first.?.page();
// A full row of wide character pairs so the reflow takes
// the bulk run path.
for (0..4) |i| {
{
const rac = page.getRowAndCell(i * 2, 0);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = .{ .data = @intCast(0x4E00 + i) } },
.wide = .wide,
};
}
{
const rac = page.getRowAndCell(i * 2 + 1, 0);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = .{ .data = 0 } },
.wide = .spacer_tail,
};
}
}
}
// Resize to exactly two pairs per row: runs end on the row
// boundary with no spacer heads needed.
try s.resize(.{ .cols = 4, .reflow = true });
try testing.expectEqual(@as(usize, 4), s.cols);
try testing.expectEqual(@as(usize, 2), s.totalRows());
{
try testing.expect(s.pages.first == s.pages.last);
const page = s.pages.first.?.page();
for (0..4) |i| {
const y = i / 2;
const x = (i % 2) * 2;
{
const rac = page.getRowAndCell(x, y);
try testing.expectEqual(
@as(u21, @intCast(0x4E00 + i)),
rac.cell.content.codepoint.data,
);
try testing.expectEqual(pagepkg.Cell.Wide.wide, rac.cell.wide);
}
{
const rac = page.getRowAndCell(x + 1, y);
try testing.expectEqual(@as(u21, 0), rac.cell.content.codepoint.data);
try testing.expectEqual(pagepkg.Cell.Wide.spacer_tail, rac.cell.wide);
}
}
{
const rac = page.getRowAndCell(0, 0);
try testing.expect(rac.row.wrap);
try testing.expect(!rac.row.wrap_continuation);
}
{
const rac = page.getRowAndCell(0, 1);
try testing.expect(!rac.row.wrap);
try testing.expect(rac.row.wrap_continuation);
}
}
}
test "PageList resize reflow less cols wide char bulk run odd cols spacer head" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, .{ .cols = 8, .rows = 1, .max_size = 0 });
defer s.deinit();
{
try testing.expect(s.pages.first == s.pages.last);
const page = s.pages.first.?.page();
for (0..4) |i| {
{
const rac = page.getRowAndCell(i * 2, 0);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = .{ .data = @intCast(0x4E00 + i) } },
.wide = .wide,
};
}
{
const rac = page.getRowAndCell(i * 2 + 1, 0);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = .{ .data = 0 } },
.wide = .spacer_tail,
};
}
}
}
// Resize to an odd number of columns: the bulk run must stop a
// pair short of the row boundary and the slow path inserts a
// spacer head in the final column.
try s.resize(.{ .cols = 5, .reflow = true });
try testing.expectEqual(@as(usize, 5), s.cols);
try testing.expectEqual(@as(usize, 2), s.totalRows());
{
try testing.expect(s.pages.first == s.pages.last);
const page = s.pages.first.?.page();
for (0..4) |i| {
const y = i / 2;
const x = (i % 2) * 2;
{
const rac = page.getRowAndCell(x, y);
try testing.expectEqual(
@as(u21, @intCast(0x4E00 + i)),
rac.cell.content.codepoint.data,
);
try testing.expectEqual(pagepkg.Cell.Wide.wide, rac.cell.wide);
}
{
const rac = page.getRowAndCell(x + 1, y);
try testing.expectEqual(@as(u21, 0), rac.cell.content.codepoint.data);
try testing.expectEqual(pagepkg.Cell.Wide.spacer_tail, rac.cell.wide);
}
}
{
const rac = page.getRowAndCell(4, 0);
try testing.expectEqual(@as(u21, 0), rac.cell.content.codepoint.data);
try testing.expectEqual(pagepkg.Cell.Wide.spacer_head, rac.cell.wide);
try testing.expect(rac.row.wrap);
}
{
const rac = page.getRowAndCell(4, 1);
try testing.expectEqual(pagepkg.Cell.Wide.narrow, rac.cell.wide);
try testing.expect(rac.row.wrap_continuation);
}
}
}
test "PageList resize reflow less cols wide char bulk run mixed narrow round trip" {
const testing = std.testing;
const alloc = testing.allocator;
// The emoji-in-prose shape: single wide pairs separated by
// narrow cells, which must all share a single bulk run.
const Shape = struct { cp: u21, wide: pagepkg.Cell.Wide };
const shape: []const Shape = &.{
.{ .cp = 0x4E00, .wide = .wide },
.{ .cp = 0, .wide = .spacer_tail },
.{ .cp = 'x', .wide = .narrow },
.{ .cp = 0x4E01, .wide = .wide },
.{ .cp = 0, .wide = .spacer_tail },
.{ .cp = 'y', .wide = .narrow },
.{ .cp = 0x4E02, .wide = .wide },
.{ .cp = 0, .wide = .spacer_tail },
.{ .cp = 'z', .wide = .narrow },
};
var s = try init(alloc, .{ .cols = 9, .rows = 1, .max_size = 0 });
defer s.deinit();
{
try testing.expect(s.pages.first == s.pages.last);
const page = s.pages.first.?.page();
for (shape, 0..) |c, x| {
const rac = page.getRowAndCell(x, 0);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = .{ .data = c.cp } },
.wide = c.wide,
};
}
}
// Shrink: the run ends exactly on the row boundary after the
// second narrow cell.
try s.resize(.{ .cols = 6, .reflow = true });
try testing.expectEqual(@as(usize, 6), s.cols);
try testing.expectEqual(@as(usize, 2), s.totalRows());
{
try testing.expect(s.pages.first == s.pages.last);
const page = s.pages.first.?.page();
for (shape[0..6], 0..) |c, x| {
const rac = page.getRowAndCell(x, 0);
try testing.expectEqual(c.cp, rac.cell.content.codepoint.data);
try testing.expectEqual(c.wide, rac.cell.wide);
}
for (shape[6..], 0..) |c, x| {
const rac = page.getRowAndCell(x, 1);
try testing.expectEqual(c.cp, rac.cell.content.codepoint.data);
try testing.expectEqual(c.wide, rac.cell.wide);
}
try testing.expect(page.getRowAndCell(0, 0).row.wrap);
try testing.expect(page.getRowAndCell(0, 1).row.wrap_continuation);
}
// Grow back: the wrapped rows must rejoin into the original
// single-row layout.
try s.resize(.{ .cols = 9, .reflow = true });
try testing.expectEqual(@as(usize, 9), s.cols);
try testing.expectEqual(@as(usize, 1), s.totalRows());
{
try testing.expect(s.pages.first == s.pages.last);
const page = s.pages.first.?.page();
for (shape, 0..) |c, x| {
const rac = page.getRowAndCell(x, 0);
try testing.expectEqual(c.cp, rac.cell.content.codepoint.data);
try testing.expectEqual(c.wide, rac.cell.wide);
}
try testing.expect(!page.getRowAndCell(0, 0).row.wrap);
}
}
test "PageList resize reflow less cols wide char bulk run styled" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, .{ .cols = 8, .rows = 1, .max_size = 0 });
defer s.deinit();
{
try testing.expect(s.pages.first == s.pages.last);
const page = s.pages.first.?.page();
// Create a style
const style: stylepkg.Style = .{ .flags = .{ .bold = true } };
const style_id = try page.styles.add(page.memory, style);
// Styled pairs: the tail shares the wide cell's style, as
// the print path writes them.
for (0..4) |i| {
{
const rac = page.getRowAndCell(i * 2, 0);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = .{ .data = @intCast(0x4E00 + i) } },
.wide = .wide,
.style_id = style_id,
};
page.styles.use(page.memory, style_id);
}
{
const rac = page.getRowAndCell(i * 2 + 1, 0);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = .{ .data = 0 } },
.wide = .spacer_tail,
.style_id = style_id,
};
page.styles.use(page.memory, style_id);
}
}
// We're over-counted by 1 because `add` implies `use`.
page.styles.release(page.memory, style_id);
}
// Resize
try s.resize(.{ .cols = 4, .reflow = true });
try testing.expectEqual(@as(usize, 4), s.cols);
try testing.expectEqual(@as(usize, 2), s.totalRows());
{
try testing.expect(s.pages.first == s.pages.last);
const page = s.pages.first.?.page();
for (0..2) |y| {
for (0..4) |x| {
const rac = page.getRowAndCell(x, y);
const style_id = rac.cell.style_id;
try testing.expect(style_id != 0);
const style = page.styles.get(page.memory, style_id);
try testing.expect(style.flags.bold);
try testing.expect(rac.row.styled);
try testing.expectEqual(
if (x % 2 == 0) pagepkg.Cell.Wide.wide else pagepkg.Cell.Wide.spacer_tail,
rac.cell.wide,
);
}
}
}
}
test "PageList resize reflow less cols wide char bulk run degenerate spacer tail style" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, .{ .cols = 4, .rows = 1, .max_size = 0 });
defer s.deinit();
{
try testing.expect(s.pages.first == s.pages.last);
const page = s.pages.first.?.page();
// A styled wide cell whose tail does NOT share its style.
// This can't come from the print path, but the run scan must
// reject the pair (slow path) rather than rewrite the tail
// to the wide cell's style.
const style: stylepkg.Style = .{ .flags = .{ .bold = true } };
const style_id = try page.styles.add(page.memory, style);
{
const rac = page.getRowAndCell(0, 0);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = .{ .data = 0x4E00 } },
.wide = .wide,
.style_id = style_id,
};
}
{
const rac = page.getRowAndCell(1, 0);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = .{ .data = 0 } },
.wide = .spacer_tail,
};
}
{
const rac = page.getRowAndCell(2, 0);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = .{ .data = 'x' } },
};
}
}
// Resize
try s.resize(.{ .cols = 3, .reflow = true });
try testing.expectEqual(@as(usize, 3), s.cols);
try testing.expectEqual(@as(usize, 1), s.totalRows());
{
try testing.expect(s.pages.first == s.pages.last);
const page = s.pages.first.?.page();
{
const rac = page.getRowAndCell(0, 0);
try testing.expectEqual(@as(u21, 0x4E00), rac.cell.content.codepoint.data);
try testing.expectEqual(pagepkg.Cell.Wide.wide, rac.cell.wide);
try testing.expect(rac.cell.style_id != 0);
const style = page.styles.get(page.memory, rac.cell.style_id);
try testing.expect(style.flags.bold);
}
{
const rac = page.getRowAndCell(1, 0);
try testing.expectEqual(pagepkg.Cell.Wide.spacer_tail, rac.cell.wide);
try testing.expectEqual(stylepkg.default_id, rac.cell.style_id);
}
{
const rac = page.getRowAndCell(2, 0);
try testing.expectEqual(@as(u21, 'x'), rac.cell.content.codepoint.data);
try testing.expectEqual(pagepkg.Cell.Wide.narrow, rac.cell.wide);
}
}
}
test "PageList resize reflow less cols to wrap a multi-codepoint grapheme with a spacer head" {
const testing = std.testing;
const alloc = testing.allocator;