mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 16:11:43 +00:00
terminal/snapshot: vectorize grid cell decoding
Decoding one- and two-byte cells widened them to their 8-byte words one
scalar store at a time. The bulk codec now widens sixteen transported
bytes per step with byte shuffles against a zero vector, degrading
width-two surrogate lanes to U+FFFD with a vector select, exactly
matching the scalar path. Short row tails reprocess the final full
window with overlapping stores that rewrite identical bytes.
This is a native win: the shuffles lower to NEON and take ascii decode
from 38.9 ms to 34.0 ms (measured by toggling this path at the tip of
this series). On wasm, V8 runs the scalar fallback at the same speed as
the shuffle version — the loop is store-bound either way — so the wasm
deltas below are flat.
Row decoding also drops per-field packed-struct read-modify-writes in
favor of one load and one store per row header, and rows that are fully
default (zero header byte, zero encoded cells) skip all work: decoded
pages start zeroed, which is exactly the default row and cell state.
Benchmarks ("prev" is the parent commit):
| wasm | encode prev | encode | decode prev | decode |
|-----------|------------:|---------:|------------:|---------:|
| ascii | 2.07 ms | 2.18 ms | 2.69 ms | 2.68 ms |
| styled | 2.29 ms | 2.28 ms | 7.06 ms | 6.93 ms |
| truecolor | 4.91 ms | 4.93 ms | 11.99 ms | 11.91 ms |
| cjk | 6.14 ms | 6.11 ms | 11.83 ms | 11.72 ms |
| grapheme | 8.26 ms | 8.28 ms | 11.18 ms | 11.38 ms |
| native | mode | prev | this |
|--------|--------|--------:|--------:|
| ascii | encode | 24.3 ms | 24.3 ms |
| ascii | decode | 50.9 ms | 46.7 ms |
| utf8 | encode | 41.2 ms | 41.0 ms |
| utf8 | decode | 59.5 ms | 59.5 ms |
This commit is contained in:
@@ -865,13 +865,21 @@ pub fn decode(
|
||||
break :header .{ row_header, count };
|
||||
};
|
||||
|
||||
// A fully default row needs no work at all: decoded pages start
|
||||
// zeroed, which is exactly the default row and cell state.
|
||||
if (@as(u8, @bitCast(row_header)) == 0 and count == 0) continue;
|
||||
|
||||
// Update the row fields through one load and store instead of a
|
||||
// read-modify-write per packed field.
|
||||
const row = page.getRow(y);
|
||||
row.wrap = row_header.wrap;
|
||||
row.wrap_continuation = row_header.wrap_continuation;
|
||||
row.semantic_prompt = std.enums.fromInt(
|
||||
var row_value = row.*;
|
||||
row_value.wrap = row_header.wrap;
|
||||
row_value.wrap_continuation = row_header.wrap_continuation;
|
||||
row_value.semantic_prompt = std.enums.fromInt(
|
||||
TerminalRow.SemanticPrompt,
|
||||
row_header.semantic_prompt,
|
||||
) orelse .none;
|
||||
row.* = row_value;
|
||||
|
||||
const cells = page.getCells(row);
|
||||
if (count > cells.len) return error.InvalidRowCellCount;
|
||||
@@ -983,8 +991,34 @@ fn widenCells(
|
||||
) void {
|
||||
const size = comptime width.size();
|
||||
|
||||
// With the bulk codec layout this is a pure widening store: sixteen
|
||||
// transported bytes per step, shuffling each pair of encoded values
|
||||
// into u64 lane position against a zero vector and shifting them into
|
||||
// the content field. Zig 0.16 disables loop auto-vectorization, so the
|
||||
// scalar loop would issue one widening store per cell.
|
||||
if (comptime bulk_codec) {
|
||||
const words: [*]u64 = @ptrCast(cells.ptr);
|
||||
const step = 16 / size;
|
||||
var i: usize = 0;
|
||||
while (i + step <= cells.len) : (i += step) {
|
||||
widenStep(width, bytes, words, i);
|
||||
}
|
||||
if (i < cells.len) {
|
||||
if (cells.len >= step) {
|
||||
// Reprocess the final full window with overlapping stores,
|
||||
// which rewrite the same widened values.
|
||||
widenStep(width, bytes, words, cells.len - step);
|
||||
} else {
|
||||
while (i < cells.len) : (i += 1) {
|
||||
words[i] = width.extend(widenValue(width, bytes[i * size ..]));
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// When the native cell matches the wire word, this is a pure widening
|
||||
// loop over integers that the compiler can vectorize.
|
||||
// loop over integers.
|
||||
if (comptime native_matches_wire) {
|
||||
const words: [*]u64 = @ptrCast(cells.ptr);
|
||||
for (0..cells.len) |i| {
|
||||
@@ -998,6 +1032,58 @@ fn widenCells(
|
||||
}
|
||||
}
|
||||
|
||||
/// Widen sixteen transported bytes into their cell words at cell index `i`:
|
||||
/// shuffle each pair of encoded values into u64 lane position against a
|
||||
/// zero vector, then shift the value into the content field. Width two
|
||||
/// first degrades surrogate lanes to U+FFFD, matching `widenValue`.
|
||||
inline fn widenStep(
|
||||
comptime width: Cell.EncodedWidth,
|
||||
bytes: []const u8,
|
||||
words: [*]u64,
|
||||
i: usize,
|
||||
) void {
|
||||
const size = comptime width.size();
|
||||
const step = 16 / size;
|
||||
|
||||
var in: @Vector(16, u8) = @as(
|
||||
*align(1) const @Vector(16, u8),
|
||||
@ptrCast(bytes[i * size ..].ptr),
|
||||
).*;
|
||||
|
||||
// Width two admits surrogates, which degrade to U+FFFD exactly
|
||||
// like `widenValue`. Width one cannot encode an invalid scalar.
|
||||
if (comptime width == .two) {
|
||||
const values: @Vector(8, u16) = @bitCast(in);
|
||||
const invalid = (values & @as(@Vector(8, u16), @splat(0xF800))) ==
|
||||
@as(@Vector(8, u16), @splat(0xD800));
|
||||
in = @bitCast(@select(
|
||||
u16,
|
||||
invalid,
|
||||
@as(@Vector(8, u16), @splat(0xFFFD)),
|
||||
values,
|
||||
));
|
||||
}
|
||||
|
||||
const zero: @Vector(16, u8) = @splat(0);
|
||||
inline for (0..step / 2) |pair| {
|
||||
const mask: @Vector(16, i32) = comptime mask: {
|
||||
var mask: [16]i32 = @splat(~@as(i32, 0));
|
||||
for (0..size) |byte| {
|
||||
mask[byte] = @intCast((2 * pair) * size + byte);
|
||||
mask[8 + byte] = @intCast((2 * pair + 1) * size + byte);
|
||||
}
|
||||
break :mask mask;
|
||||
};
|
||||
const lanes: @Vector(2, u64) = @bitCast(
|
||||
@shuffle(u8, in, zero, mask),
|
||||
);
|
||||
@as(
|
||||
*align(@alignOf(u64)) @Vector(2, u64),
|
||||
@ptrCast(words + i + 2 * pair),
|
||||
).* = lanes << @splat(@bitOffsetOf(Cell, "content"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Read and validate one narrow transported value.
|
||||
inline fn widenValue(
|
||||
comptime width: Cell.EncodedWidth,
|
||||
|
||||
Reference in New Issue
Block a user