diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 23:28:05 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 23:29:28 +0300 |
| commit | 4a3302d19a2baa3a33e910e0088c24e780905b90 (patch) | |
| tree | dcaf76fae1c82102d933c9225b17ca7ac5d3a847 /vendor/libghostty-vt/src/terminal/page.zig | |
| parent | 550e9f8cc085f428429f5cf2da0bb2dba897e8b8 (diff) | |
chore: update vendored libghostty-vt
refs #276
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/page.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/page.zig | 336 |
1 files changed, 326 insertions, 10 deletions
diff --git a/vendor/libghostty-vt/src/terminal/page.zig b/vendor/libghostty-vt/src/terminal/page.zig index 0942e260..ca6d9854 100644 --- a/vendor/libghostty-vt/src/terminal/page.zig +++ b/vendor/libghostty-vt/src/terminal/page.zig @@ -92,7 +92,11 @@ const grapheme_chunk = grapheme_chunk_len * @sizeOf(u21); const GraphemeAlloc = BitmapAllocator(grapheme_chunk); const grapheme_count_default = GraphemeAlloc.bitmap_bit_size; pub const grapheme_bytes_default = grapheme_count_default * grapheme_chunk; -const GraphemeMap = AutoOffsetHashMap(Offset(Cell), Offset(u21).Slice); +const GraphemeMap = AutoOffsetHashMap( + Offset(Cell), + Offset(u21).Slice, + hash_map.default_max_load_percentage, +); /// The allocator used for shared utf8-encoded strings within a page. /// Note the chunk size below is the minimum size of a single allocation @@ -145,6 +149,15 @@ pub const Page = struct { @alignOf(Cell), StyleSet.base_align.toByteUnits(), ) == 0); + + // The PageList memory pool requires that initBuf overwrites at + // least the first pointer-size bytes of the backing buffer: + // std.heap.MemoryPool stores its free list node there when a + // page buffer is returned to it, and pool reuse skips zeroing + // in release builds. This holds because the rows array is at + // offset 0 (see layout), a page always has at least one row, + // and initBuf fully rewrites every row. + assert(@sizeOf(Row) >= @sizeOf(usize)); } /// The backing memory for the page. A page is always made up of a @@ -238,6 +251,13 @@ pub const Page = struct { /// It is up to the caller to not call deinit on these pages. pub inline fn initBuf(buf: OffsetBuf, l: Layout) Page { const cap = l.capacity; + + // A page must always have at least one row. Aside from being + // useless otherwise, the row initialization below must always + // overwrite the start of the buffer for pool reuse. See the + // comptime assert at the top of Page. + assert(cap.rows > 0); + const rows = buf.member(Row, l.rows_start); const cells = buf.member(Cell, l.cells_start); @@ -754,11 +774,11 @@ pub const Page = struct { } } - // The hyperlink_map capacity in layout() is computed as: - // hyperlink_count * hyperlink_cell_multiplier (rounded to power of 2) - // We need enough hyperlink_bytes so that when layout() computes - // the map capacity, it can accommodate all hyperlink cells. This - // is unit tested. + // layout() requests `hyperlink_count * hyperlink_cell_multiplier` + // usable map entries. The map layout adds load-factor headroom and + // rounds the raw slot count to a power of two. We need enough + // hyperlink_bytes for that requested entry count to accommodate all + // hyperlink cells. This is unit tested. const hyperlink_cap = cap: { const hyperlink_count = id_set.count(); const hyperlink_set_cap = hyperlink.Set.capacityForCount(hyperlink_count); @@ -1461,7 +1481,7 @@ pub const Page = struct { const entry = map.getEntry(src_offset).?; const value = entry.value_ptr.*; map.removeByPtr(entry.key_ptr); - map.putAssumeCapacity(dst_offset, value); + map.putAssumeCapacityNoClobber(dst_offset, value); // NOTE: We must not set src/dst.hyperlink here because this // function is used in various cases where we swap cell contents @@ -1478,7 +1498,7 @@ pub const Page = struct { /// Returns the hyperlink capacity for the page. This isn't the byte /// size but the number of unique cells that can have hyperlink data. pub inline fn hyperlinkCapacity(self: *const Page) usize { - return self.hyperlink_map.map(self.memory).capacity(); + return self.hyperlink_map.map(self.memory).maxLoad(); } /// Set the graphemes for the given cell. This asserts that the cell @@ -1609,7 +1629,7 @@ pub const Page = struct { const entry = map.getEntry(src_offset).?; const value = entry.value_ptr.*; map.removeByPtr(entry.key_ptr); - map.putAssumeCapacity(dst_offset, value); + map.putAssumeCapacityNoClobber(dst_offset, value); } /// Clear the graphemes for a given cell. @@ -1703,6 +1723,11 @@ pub const Page = struct { /// and rows size. pub inline fn layout(cap: Capacity) Layout { const rows_count: usize = @intCast(cap.rows); + + // The rows array must stay at offset 0: the PageList memory + // pool relies on initBuf overwriting the first bytes of a + // reused page buffer, which hold the pool's free list node. + // See the comptime assert at the top of Page. const rows_start = 0; const rows_end: usize = rows_start + (rows_count * @sizeOf(Row)); @@ -1744,7 +1769,7 @@ pub const Page = struct { u32, hyperlink_count * hyperlink_cell_multiplier, ) orelse break :count std.math.maxInt(u32); - break :count std.math.ceilPowerOfTwoAssert(u32, mult); + break :count mult; }; const hyperlink_map_layout = hyperlink.Map.layout(hyperlink_map_count); const hyperlink_map_start = alignForward(usize, hyperlink_set_end, hyperlink.Map.base_align.toByteUnits()); @@ -1988,6 +2013,11 @@ pub const Row = packed struct(u64) { prompt_continuation = 2, }; + /// The backing integer of this packed struct. Prefer this over + /// hardcoding the integer type so that code is resilient to the + /// size changing. + pub const Backing = @typeInfo(Row).@"struct".backing_integer.?; + /// C ABI type. pub const C = u64; @@ -2100,6 +2130,11 @@ pub const Cell = packed struct(u64) { prompt = 2, }; + /// The backing integer of this packed struct. Prefer this over + /// hardcoding the integer type so that code is resilient to the + /// size changing. + pub const Backing = @typeInfo(Cell).@"struct".backing_integer.?; + /// C ABI type. pub const C = u64; @@ -2194,6 +2229,270 @@ pub const Cell = packed struct(u64) { } }; +/// Returns a mask with all bits set for the given fields of the packed +/// struct T, used for masked compares of raw backing-integer values +/// (e.g. `Row.Backing`, `Cell.Backing`). This is an implementation +/// detail of `Mask`, which is the public API built on top of this. +fn fieldMask( + comptime T: type, + comptime fields: []const []const u8, +) @typeInfo(T).@"struct".backing_integer.? { + // Backing int of the packed struct + const Int = @typeInfo(T).@"struct".backing_integer.?; + + var mask: Int = 0; + inline for (fields) |field| { + // The type that fits all the bits we need to set. + const Ones = std.meta.Int( + .unsigned, + @bitSizeOf(@FieldType(T, field)), + ); + + // Mask out the ones + mask |= @as(Int, std.math.maxInt(Ones)) << @bitOffsetOf(T, field); + } + + return mask; +} + +/// A comptime-generated helper for classifying and comparing packed +/// struct values (e.g. Row, Cell) in bulk, using masked compares of +/// their raw backing integers. +/// +/// Masked compares are the key to making bulk row/cell processing fast. +/// Rows and cells are small packed structs specifically so that a single +/// integer load observes every field at once. A masked compare can then +/// answer a multi-field question with one AND and one compare, instead +/// of extracting and branching on each field individually (each packed +/// field access compiles to its own shift/mask). Just as importantly, +/// the integer form vectorizes trivially: `@splat` the mask and expected +/// value, and whole groups of rows or cells can be classified with a +/// few SIMD instructions. +/// +/// Some real examples of this in use: +/// +/// - Terminal print fast path: a cell can be overwritten by the +/// simple/fast path only if its content tag is a plain codepoint, +/// it has no style, isn't wide, and isn't a hyperlink. Masking +/// with those fields and comparing against a template answers all +/// four questions in one compare per cell. +/// +/// - Render state updates: a cell whose masked +/// `{content_tag, style_id}` bits are zero is a plain cell that +/// needs no managed-memory handling, so a vector OR-reduce can +/// skip entire groups of plain cells at once. Similarly, cells +/// whose masked bits equal the first cell's form a run sharing one +/// style, letting the update record one style lookup per run +/// rather than per cell. +/// +/// - Dirty scans: OR-reducing groups of rows against the `dirty` +/// field mask finds whether any row in the group needs a rebuild +/// without touching each row's flag individually. +/// +/// T is the packed struct type, fields are the fields covered by the +/// mask, and group_len is the number of values processed at once by +/// the group (vectorized) operations. Callers typically scan a slice +/// with the group operations and fall back to the scalar variants for +/// the remainder and for pinpointing values within a matched group. +pub fn Mask( + comptime T: type, + comptime fields: []const []const u8, + comptime group_len_param: comptime_int, +) type { + return struct { + const Backing = @typeInfo(T).@"struct".backing_integer.?; + const mask: Backing = fieldMask(T, fields); + + /// The number of values processed at once by group operations. + pub const group_len = group_len_param; + + /// A group of raw values for the vectorized operations. + const Group = @Vector(group_len, Backing); + + /// Load a group of values from the slice starting at index i. + /// Asserts that at least group_len values are available. + inline fn load(values: []const T, i: usize) Group { + return @bitCast(values[i..][0..group_len].*); + } + + /// Returns the raw backing bits of a single value. + pub inline fn bits(v: T) Backing { + return @bitCast(v); + } + + /// Returns the masked bits of a single value: the bits of the + /// masked fields with all other fields zeroed. Use this to + /// build the expected value for the eql functions. + pub inline fn pattern(v: T) Backing { + return bits(v) & mask; + } + + /// Returns the backing bits of a single value with the masked + /// fields zeroed: the complement of `pattern`. Use this to + /// compare values while ignoring the masked fields. + pub inline fn strip(v: T) Backing { + return bits(v) & ~mask; + } + + /// Returns true if every value in the group of group_len + /// values starting at index i matches, where a value matches + /// when none of the masked fields have any bits set: false + /// for bools, zero for ints, the zero tag for enums, and so + /// on. Asserts that at least group_len values are available. + pub inline fn match(values: []const T, i: usize) bool { + return @reduce(.Or, load(values, i)) & mask == 0; + } + + /// Scalar variant of `match` for a single value. + pub inline fn matchScalar(v: T) bool { + return bits(v) & mask == 0; + } + + /// Returns true if the masked fields of every value in the + /// group of group_len values starting at index i equal the + /// expected pattern (see `pattern`). + /// + /// This is a masked compare: fields outside the mask may vary + /// freely. Use this to detect runs of values that share the + /// masked field contents while other fields differ, e.g. a run + /// of cells with the same style ID but different codepoints. + /// If the result you derive from a run depends on fields + /// outside the mask, use `eqlExact` instead. + pub inline fn eql( + values: []const T, + i: usize, + expected: Backing, + ) bool { + const masked = load(values, i) & @as(Group, @splat(mask)); + return @reduce(.And, masked == @as(Group, @splat(expected))); + } + + /// Scalar variant of `eql` for a single value. + pub inline fn eqlScalar(v: T, expected: Backing) bool { + return pattern(v) == expected; + } + + /// Like `eql` but returns the number of leading values whose + /// masked fields equal the expected pattern, i.e. group_len if + /// the entire group matches. This is useful for early-exit run + /// scans that need to pinpoint exactly where a run ends rather + /// than only whether the whole group matches. + pub inline fn eqlPrefix( + values: []const T, + i: usize, + expected: Backing, + ) usize { + const masked = load(values, i) & @as(Group, @splat(mask)); + const ok = masked == @as(Group, @splat(expected)); + + // Test the whole group before extracting the prefix + // count: turning a vector compare into a scalar bitmask + // is expensive on some targets (e.g. NEON has no movemask + // instruction) and run scans overwhelmingly see fully + // matching groups, so we only pay for the extraction on + // the final group of a run. + if (@reduce(.And, ok)) { + @branchHint(.likely); + return group_len; + } + + const ok_bits: std.meta.Int( + .unsigned, + group_len, + ) = @bitCast(ok); + return @ctz(~ok_bits); + } + + /// Returns true if every value in the group of group_len + /// values starting at index i is bit-identical to the expected + /// value. Note: this compares entire values; it is NOT + /// affected by the field mask. + /// + /// This exists alongside `eql` for run detection where the + /// derived result depends on fields outside the mask, so a + /// masked compare would incorrectly extend the run. For + /// example, the background color of a bg-color cell lives in + /// the content field: two such cells only share a background + /// if their content bits are identical, not merely their + /// content tag. Values in such runs are typically produced by + /// bulk fills (e.g. erase with a pending background) and are + /// bit-identical in practice, so exact equality is both + /// correct and cheap. + pub inline fn eqlExact( + values: []const T, + i: usize, + expected: Backing, + ) bool { + const group = load(values, i); + return @reduce(.And, group == @as(Group, @splat(expected))); + } + }; +} + +test "Mask" { + const M = Mask(Cell, &.{ "content_tag", "style_id" }, 4); + + const plain: Cell = .init('A'); + var styled: Cell = .init('B'); + styled.style_id = 5; + var styled2: Cell = .init('C'); + styled2.style_id = 5; + var other: Cell = .init('D'); + other.style_id = 6; + + // match: plain cells only + { + var cells: [4]Cell = .{ plain, plain, plain, plain }; + try testing.expect(M.match(&cells, 0)); + try testing.expect(M.matchScalar(plain)); + + cells[2] = styled; + try testing.expect(!M.match(&cells, 0)); + try testing.expect(!M.matchScalar(styled)); + } + + // eql: runs of matching masked fields, other fields may vary + { + const expected = M.pattern(styled); + var cells: [4]Cell = .{ styled, styled2, styled, styled2 }; + try testing.expect(M.eql(&cells, 0, expected)); + try testing.expect(M.eqlScalar(styled2, expected)); + + cells[1] = other; + try testing.expect(!M.eql(&cells, 0, expected)); + try testing.expect(!M.eqlScalar(other, expected)); + } + + // eqlPrefix: count of leading values matching the pattern + { + const expected = M.pattern(styled); + var cells: [4]Cell = .{ styled, styled2, other, styled }; + try testing.expectEqual(2, M.eqlPrefix(&cells, 0, expected)); + + cells[2] = styled; + try testing.expectEqual(4, M.eqlPrefix(&cells, 0, expected)); + } + + // eqlExact: bit-identical values only + { + const expected = M.bits(styled); + var cells: [4]Cell = .{ styled, styled, styled, styled }; + try testing.expect(M.eqlExact(&cells, 0, expected)); + + // Same masked fields but different codepoint is not exact. + cells[3] = styled2; + try testing.expect(!M.eqlExact(&cells, 0, expected)); + } + + // strip: compare values while ignoring the masked fields + { + var styled_other: Cell = .init('B'); + styled_other.style_id = 6; + try testing.expectEqual(M.strip(styled), M.strip(styled_other)); + try testing.expect(M.strip(styled) != M.strip(styled2)); + } +} + // Uncomment this when you want to do some math. // test "Page size calculator" { // const total_size = alignForward( @@ -3916,3 +4215,20 @@ test "Page exactRowCapacity hyperlink map capacity for many cells" { try testing.expect(cloned_cell.hyperlink); } } + +test "Page layout avoids double rounding hyperlink map capacity" { + const hyperlink_count = 3; + const layout = Page.layout(.{ + .cols = 1, + .rows = 1, + .hyperlink_bytes = hyperlink_count * @sizeOf(hyperlink.Set.Item), + }); + + // Three set entries request 48 usable map entries. Scaling that for the + // 80% load factor needs 60 raw slots, which rounds once to 64. Rounding + // the request before applying the load factor would allocate 128 slots. + try std.testing.expectEqual( + @as(u32, 64), + layout.hyperlink_map_layout.capacity, + ); +} |
