diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/terminal/ref_counted_set.zig | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff) | |
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/ref_counted_set.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/ref_counted_set.zig | 209 |
1 files changed, 188 insertions, 21 deletions
diff --git a/vendor/libghostty-vt/src/terminal/ref_counted_set.zig b/vendor/libghostty-vt/src/terminal/ref_counted_set.zig index dd76079f..669c2c57 100644 --- a/vendor/libghostty-vt/src/terminal/ref_counted_set.zig +++ b/vendor/libghostty-vt/src/terminal/ref_counted_set.zig @@ -1,5 +1,6 @@ const std = @import("std"); const assert = @import("../quirks.zig").inlineAssert; +const testing = std.testing; const size = @import("size.zig"); const Offset = size.Offset; @@ -211,6 +212,30 @@ pub fn RefCountedSet( @memset(table.ptr(base)[0..l.table_cap], 0); @memset(items.ptr(base)[0..l.cap], .{}); + return initFromParts(table, items, l, context); + } + + /// Like `init`, but for backing memory that the caller guarantees + /// is already zero-filled (e.g. fresh OS pages). This writes + /// nothing to the backing buffer, so the OS pages behind the table + /// and items stay untouched until the first `add`. + /// + /// Behavior is undefined if the backing memory is not zero. + pub fn initAssumeZeroed(base: OffsetBuf, l: Layout, context: Context) Self { + return initFromParts( + base.member(Id, l.table_start), + base.member(Item, l.items_start), + l, + context, + ); + } + + fn initFromParts( + table: Offset(Id), + items: Offset(Item), + l: Layout, + context: Context, + ) Self { return .{ .table = table, .items = items, @@ -316,6 +341,19 @@ pub fn RefCountedSet( if (id < self.next_id) { if (items[id].meta.ref == 0) { + // Requested ID is dead, but if the value exists not under + // another ID then ref count that and increase the ID. + if (self.lookupContext(base, value, ctx)) |existing_id| { + // Notify the context that the value is "deleted" + // because we're reusing the existing value in the + // set. This allows callers to clean up any + // resources associated with the value. + if (comptime @hasDecl(Context, "deleted")) ctx.deleted(value); + + items[existing_id].meta.ref += 1; + return existing_id; + } + // See comment in `addContext` for details. if (self.psl_stats[self.psl_stats.len - 1] > 0) { @branchHint(.cold); @@ -323,8 +361,7 @@ pub fn RefCountedSet( } self.deleteItem(base, id, ctx); - - const added_id = self.upsert(base, value, id, ctx); + const added_id = self.insert(base, value, id, ctx); items[added_id].meta.ref += 1; @@ -447,6 +484,47 @@ pub fn RefCountedSet( return self.living; } + /// A live entry returned by `Iterator`. + pub const Entry = struct { + id: Id, + value_ptr: *T, + }; + + /// Iterates live entries in ascending ID order. + /// + /// Released entries whose reference count reached zero are skipped. + /// Any mutation of the set invalidates the iterator. + pub const Iterator = struct { + items: [*]Item, + id: Id = 1, + end: Id, + + pub fn next(self: *Iterator) ?Entry { + while (self.id < self.end) { + const id = self.id; + self.id += 1; + + const item = &self.items[id]; + if (item.meta.ref == 0) continue; + + return .{ + .id = id, + .value_ptr = &item.value, + }; + } + + return null; + } + }; + + /// Return an iterator over the live entries in this set. + pub fn iterator(self: *const Self, base: anytype) Iterator { + return .{ + .items = self.items.ptr(base), + .end = self.next_id, + }; + } + /// Delete an item, removing any references from /// the table, and freeing its ID to be reused. fn deleteItem(self: *Self, base: anytype, id: Id, ctx: Context) void { @@ -547,24 +625,6 @@ pub fn RefCountedSet( return null; } - /// Find the provided value in the hash table, or add a new item - /// for it if not present. If a new item is added, `new_id` will - /// be used as the ID. If an existing item is found, the `new_id` - /// is ignored and the existing item's ID is returned. - fn upsert(self: *Self, base: anytype, value: T, new_id: Id, ctx: Context) Id { - // If the item already exists, return it. - if (self.lookupContext(base, value, ctx)) |id| { - // Notify the context that the value is "deleted" because - // we're reusing the existing value in the set. This allows - // callers to clean up any resources associated with the value. - if (comptime @hasDecl(Context, "deleted")) ctx.deleted(value); - - return id; - } - - return self.insert(base, value, new_id, ctx); - } - /// Insert the given value into the hash table with the given ID. /// /// If runtime safety is enabled, asserts that @@ -690,7 +750,9 @@ pub fn RefCountedSet( var psl_stats: [32]Id = @splat(0); - for (items[0..self.layout.cap], 0..) |item, id| { + // Start at item 1 because item 0 is reserved and never + // assigned to. Its metadata doesn't matter. + for (items[1..self.next_id], 1..) |item, id| { if (item.meta.bucket < std.math.maxInt(Id)) { assert(table[item.meta.bucket] == id); psl_stats[item.meta.psl] += 1; @@ -704,6 +766,7 @@ pub fn RefCountedSet( psl_stats = @splat(0); for (table[0..self.layout.table_cap], 0..) |id, bucket| { + if (id == 0) continue; const item = items[id]; if (item.meta.bucket < std.math.maxInt(Id)) { assert(item.meta.bucket == bucket); @@ -721,3 +784,107 @@ pub fn RefCountedSet( } }; } + +test "addWithId dead id resolving to an existing value" { + const alloc = testing.allocator; + const TestSet = RefCountedSet( + u32, + u16, + u16, + struct { + pub fn hash(_: *const @This(), value: u32) u64 { + return std.hash.int(value); + } + + pub fn eql(_: *const @This(), a: u32, b: u32) bool { + return a == b; + } + }, + ); + + const layout: TestSet.Layout = .init(8); + const buf = try alloc.alignedAlloc( + u8, + TestSet.base_align, + layout.total_size, + ); + defer alloc.free(buf); + + var set: TestSet = .init(.init(buf), layout, .{}); + + // Create a dead item between two live ones so that it can't + // be reaped by the trim loop in `add`, then release it. This + // mirrors a page style set after a styled run is erased. + const live = try set.add(buf, 11); + const released = try set.add(buf, 22); + const last = try set.add(buf, 33); + set.release(buf, released); + try testing.expectEqual(@as(usize, 2), set.count()); + + // Request the dead ID for a value that is already live under + // a different ID: we must resolve to the existing item and + // take a reference, without changing the living count. + const resolved = (try set.addWithId(buf, 11, released)).?; + try testing.expectEqual(live, resolved); + try testing.expectEqual(@as(u16, 2), set.refCount(buf, live)); + try testing.expectEqual(@as(usize, 2), set.count()); + + // The living count must agree with the iterator. + var it = set.iterator(buf); + var iterated: usize = 0; + while (it.next()) |_| iterated += 1; + try testing.expectEqual(set.count(), iterated); + + // The dead slot is still reusable for a value that is not + // in the set: the requested ID must be used (null return). + try testing.expectEqual(@as(?u16, null), try set.addWithId(buf, 44, released)); + try testing.expectEqual(@as(u16, 1), set.refCount(buf, released)); + try testing.expectEqual(@as(usize, 3), set.count()); + + _ = last; +} + +test "iterator visits live entries in ID order" { + const alloc = testing.allocator; + const TestSet = RefCountedSet( + u32, + u16, + u16, + struct { + pub fn hash(_: *const @This(), value: u32) u64 { + return std.hash.int(value); + } + + pub fn eql(_: *const @This(), a: u32, b: u32) bool { + return a == b; + } + }, + ); + + const layout: TestSet.Layout = .init(8); + const buf = try alloc.alignedAlloc( + u8, + TestSet.base_align, + layout.total_size, + ); + defer alloc.free(buf); + + var set: TestSet = .init(.init(buf), layout, .{}); + const first = try set.add(buf, 11); + const released = try set.add(buf, 22); + const last = try set.add(buf, 33); + _ = try set.add(buf, 11); + set.release(buf, released); + + var it = set.iterator(buf); + + const first_entry = it.next().?; + try testing.expectEqual(first, first_entry.id); + try testing.expectEqual(@as(u32, 11), first_entry.value_ptr.*); + + const last_entry = it.next().?; + try testing.expectEqual(last, last_entry.id); + try testing.expectEqual(@as(u32, 33), last_entry.value_ptr.*); + + try testing.expect(it.next() == null); +} |
