mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 00:36:07 +03:00
perf: introduce RefCountedSet structure, use it for Style.Set
This commit is contained in:
@ -1158,17 +1158,17 @@ fn reflowPage(
|
|||||||
|
|
||||||
// If the source cell has a style, we need to copy it.
|
// If the source cell has a style, we need to copy it.
|
||||||
if (src_cursor.page_cell.style_id != stylepkg.default_id) {
|
if (src_cursor.page_cell.style_id != stylepkg.default_id) {
|
||||||
const src_style = src_cursor.page.styles.lookupId(
|
const src_style = src_cursor.page.styles.get(
|
||||||
src_cursor.page.memory,
|
src_cursor.page.memory,
|
||||||
src_cursor.page_cell.style_id,
|
src_cursor.page_cell.style_id,
|
||||||
).?.*;
|
).*;
|
||||||
|
if (try dst_cursor.page.styles.addWithId(
|
||||||
const dst_md = try dst_cursor.page.styles.upsert(
|
|
||||||
dst_cursor.page.memory,
|
dst_cursor.page.memory,
|
||||||
src_style,
|
src_style,
|
||||||
);
|
src_cursor.page_cell.style_id,
|
||||||
dst_md.ref += 1;
|
)) |id| {
|
||||||
dst_cursor.page_cell.style_id = dst_md.id;
|
dst_cursor.page_cell.style_id = id;
|
||||||
|
}
|
||||||
dst_cursor.page_row.styled = true;
|
dst_cursor.page_row.styled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1905,18 +1905,6 @@ pub fn adjustCapacity(
|
|||||||
return new_page;
|
return new_page;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compact a page, reallocating to minimize the amount of memory
|
|
||||||
/// required for the page. This is useful when we've overflowed ID
|
|
||||||
/// spaces, are archiving a page, etc.
|
|
||||||
///
|
|
||||||
/// Note today: this doesn't minimize the memory usage, but it does
|
|
||||||
/// fix style ID overflow. A future update can shrink the memory
|
|
||||||
/// allocation.
|
|
||||||
pub fn compact(self: *PageList, page: *List.Node) !*List.Node {
|
|
||||||
// Adjusting capacity with no adjustments forces a reallocation.
|
|
||||||
return try self.adjustCapacity(page, .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new page node. This does not add it to the list and this
|
/// Create a new page node. This does not add it to the list and this
|
||||||
/// does not do any memory size accounting with max_size/page_size.
|
/// does not do any memory size accounting with max_size/page_size.
|
||||||
fn createPage(
|
fn createPage(
|
||||||
@ -3039,10 +3027,10 @@ pub const Pin = struct {
|
|||||||
/// Returns the style for the given cell in this pin.
|
/// Returns the style for the given cell in this pin.
|
||||||
pub fn style(self: Pin, cell: *pagepkg.Cell) stylepkg.Style {
|
pub fn style(self: Pin, cell: *pagepkg.Cell) stylepkg.Style {
|
||||||
if (cell.style_id == stylepkg.default_id) return .{};
|
if (cell.style_id == stylepkg.default_id) return .{};
|
||||||
return self.page.data.styles.lookupId(
|
return self.page.data.styles.get(
|
||||||
self.page.data.memory,
|
self.page.data.memory,
|
||||||
cell.style_id,
|
cell.style_id,
|
||||||
).?.*;
|
).*;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if this pin is dirty.
|
/// Check if this pin is dirty.
|
||||||
@ -3302,10 +3290,10 @@ const Cell = struct {
|
|||||||
/// Not meant for non-test usage since this is inefficient.
|
/// Not meant for non-test usage since this is inefficient.
|
||||||
pub fn style(self: Cell) stylepkg.Style {
|
pub fn style(self: Cell) stylepkg.Style {
|
||||||
if (self.cell.style_id == stylepkg.default_id) return .{};
|
if (self.cell.style_id == stylepkg.default_id) return .{};
|
||||||
return self.page.data.styles.lookupId(
|
return self.page.data.styles.get(
|
||||||
self.page.data.memory,
|
self.page.data.memory,
|
||||||
self.cell.style_id,
|
self.cell.style_id,
|
||||||
).?.*;
|
).*;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the screen point for the given cell.
|
/// Gets the screen point for the given cell.
|
||||||
@ -7363,18 +7351,20 @@ test "PageList resize reflow less cols copy style" {
|
|||||||
|
|
||||||
// Create a style
|
// Create a style
|
||||||
const style: stylepkg.Style = .{ .flags = .{ .bold = true } };
|
const style: stylepkg.Style = .{ .flags = .{ .bold = true } };
|
||||||
const style_md = try page.styles.upsert(page.memory, style);
|
const style_id = try page.styles.add(page.memory, style);
|
||||||
|
|
||||||
for (0..s.cols - 1) |x| {
|
for (0..s.cols - 1) |x| {
|
||||||
const rac = page.getRowAndCell(x, 0);
|
const rac = page.getRowAndCell(x, 0);
|
||||||
rac.cell.* = .{
|
rac.cell.* = .{
|
||||||
.content_tag = .codepoint,
|
.content_tag = .codepoint,
|
||||||
.content = .{ .codepoint = @intCast(x) },
|
.content = .{ .codepoint = @intCast(x) },
|
||||||
.style_id = style_md.id,
|
.style_id = style_id,
|
||||||
};
|
};
|
||||||
|
page.styles.use(page.memory, style_id);
|
||||||
style_md.ref += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We're over-counted by 1 because `add` implies `use`.
|
||||||
|
page.styles.release(page.memory, style_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize
|
// Resize
|
||||||
@ -7391,10 +7381,10 @@ test "PageList resize reflow less cols copy style" {
|
|||||||
const style_id = rac.cell.style_id;
|
const style_id = rac.cell.style_id;
|
||||||
try testing.expect(style_id != 0);
|
try testing.expect(style_id != 0);
|
||||||
|
|
||||||
const style = offset.page.data.styles.lookupId(
|
const style = offset.page.data.styles.get(
|
||||||
offset.page.data.memory,
|
offset.page.data.memory,
|
||||||
style_id,
|
style_id,
|
||||||
).?;
|
);
|
||||||
try testing.expect(style.flags.bold);
|
try testing.expect(style.flags.bold);
|
||||||
|
|
||||||
const row = rac.row;
|
const row = rac.row;
|
||||||
|
@ -100,7 +100,6 @@ pub const Cursor = struct {
|
|||||||
/// we change pages we need to ensure that we update that page with
|
/// we change pages we need to ensure that we update that page with
|
||||||
/// our style when used.
|
/// our style when used.
|
||||||
style_id: style.Id = style.default_id,
|
style_id: style.Id = style.default_id,
|
||||||
style_ref: ?*size.CellCountInt = null,
|
|
||||||
|
|
||||||
/// The pointers into the page list where the cursor is currently
|
/// The pointers into the page list where the cursor is currently
|
||||||
/// located. This makes it faster to move the cursor.
|
/// located. This makes it faster to move the cursor.
|
||||||
@ -202,31 +201,6 @@ pub fn assertIntegrity(self: *const Screen) void {
|
|||||||
) orelse unreachable;
|
) orelse unreachable;
|
||||||
assert(self.cursor.x == pt.active.x);
|
assert(self.cursor.x == pt.active.x);
|
||||||
assert(self.cursor.y == pt.active.y);
|
assert(self.cursor.y == pt.active.y);
|
||||||
|
|
||||||
if (self.cursor.style_id == style.default_id) {
|
|
||||||
// If our style is default, we should have no refs.
|
|
||||||
assert(self.cursor.style.default());
|
|
||||||
assert(self.cursor.style_ref == null);
|
|
||||||
} else {
|
|
||||||
// If our style is not default, we should have a ref.
|
|
||||||
assert(!self.cursor.style.default());
|
|
||||||
assert(self.cursor.style_ref != null);
|
|
||||||
|
|
||||||
// Further, the ref should be valid within the current page.
|
|
||||||
const page = &self.cursor.page_pin.page.data;
|
|
||||||
const page_style = page.styles.lookupId(
|
|
||||||
page.memory,
|
|
||||||
self.cursor.style_id,
|
|
||||||
).?.*;
|
|
||||||
assert(self.cursor.style.eql(page_style));
|
|
||||||
|
|
||||||
// The metadata pointer should be equal to the ref.
|
|
||||||
const md = page.styles.upsert(
|
|
||||||
page.memory,
|
|
||||||
page_style,
|
|
||||||
) catch unreachable;
|
|
||||||
assert(&md.ref == self.cursor.style_ref.?);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -524,14 +498,6 @@ pub fn cursorReload(self: *Screen) void {
|
|||||||
// If we have a style, we need to ensure it is in the page because this
|
// If we have a style, we need to ensure it is in the page because this
|
||||||
// method may also be called after a page change.
|
// method may also be called after a page change.
|
||||||
if (self.cursor.style_id != style.default_id) {
|
if (self.cursor.style_id != style.default_id) {
|
||||||
// We set our ref to null because manualStyleUpdate will refresh it.
|
|
||||||
// If we had a valid ref and it was zero before, then manualStyleUpdate
|
|
||||||
// will reload the same ref.
|
|
||||||
//
|
|
||||||
// We want to avoid the scenario this was non-null but the pointer
|
|
||||||
// is now invalid because it pointed to a page that no longer exists.
|
|
||||||
self.cursor.style_ref = null;
|
|
||||||
|
|
||||||
self.manualStyleUpdate() catch |err| {
|
self.manualStyleUpdate() catch |err| {
|
||||||
// This failure should not happen because manualStyleUpdate
|
// This failure should not happen because manualStyleUpdate
|
||||||
// handles page splitting, overflow, and more. This should only
|
// handles page splitting, overflow, and more. This should only
|
||||||
@ -540,7 +506,6 @@ pub fn cursorReload(self: *Screen) void {
|
|||||||
log.err("failed to update style on cursor reload err={}", .{err});
|
log.err("failed to update style on cursor reload err={}", .{err});
|
||||||
self.cursor.style = .{};
|
self.cursor.style = .{};
|
||||||
self.cursor.style_id = 0;
|
self.cursor.style_id = 0;
|
||||||
self.cursor.style_ref = null;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -595,7 +560,6 @@ pub fn cursorDownScroll(self: *Screen) !void {
|
|||||||
log.err("failed to update style on cursor scroll err={}", .{err});
|
log.err("failed to update style on cursor scroll err={}", .{err});
|
||||||
self.cursor.style = .{};
|
self.cursor.style = .{};
|
||||||
self.cursor.style_id = 0;
|
self.cursor.style_id = 0;
|
||||||
self.cursor.style_ref = null;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -683,7 +647,6 @@ pub fn cursorCopy(self: *Screen, other: Cursor) !void {
|
|||||||
// invalid.
|
// invalid.
|
||||||
self.cursor.style = .{};
|
self.cursor.style = .{};
|
||||||
self.cursor.style_id = 0;
|
self.cursor.style_id = 0;
|
||||||
self.cursor.style_ref = null;
|
|
||||||
|
|
||||||
// We need to keep our old x/y because that is our cursorAbsolute
|
// We need to keep our old x/y because that is our cursorAbsolute
|
||||||
// will fix up our pointers.
|
// will fix up our pointers.
|
||||||
@ -698,7 +661,6 @@ pub fn cursorCopy(self: *Screen, other: Cursor) !void {
|
|||||||
// We keep the old style ref so manualStyleUpdate can clean our old style up.
|
// We keep the old style ref so manualStyleUpdate can clean our old style up.
|
||||||
self.cursor.style = other.style;
|
self.cursor.style = other.style;
|
||||||
self.cursor.style_id = old.style_id;
|
self.cursor.style_id = old.style_id;
|
||||||
self.cursor.style_ref = old.style_ref;
|
|
||||||
try self.manualStyleUpdate();
|
try self.manualStyleUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -744,7 +706,6 @@ fn cursorChangePin(self: *Screen, new: Pin) void {
|
|||||||
log.err("failed to update style on cursor change err={}", .{err});
|
log.err("failed to update style on cursor change err={}", .{err});
|
||||||
self.cursor.style = .{};
|
self.cursor.style = .{};
|
||||||
self.cursor.style_id = 0;
|
self.cursor.style_id = 0;
|
||||||
self.cursor.style_ref = null;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -889,33 +850,7 @@ pub fn clearCells(
|
|||||||
if (row.styled) {
|
if (row.styled) {
|
||||||
for (cells) |*cell| {
|
for (cells) |*cell| {
|
||||||
if (cell.style_id == style.default_id) continue;
|
if (cell.style_id == style.default_id) continue;
|
||||||
|
page.styles.release(page.memory, cell.style_id);
|
||||||
// Fast-path, the style ID matches, in this case we just update
|
|
||||||
// our own ref and continue. We never delete because our style
|
|
||||||
// is still active.
|
|
||||||
if (page == &self.cursor.page_pin.page.data and
|
|
||||||
cell.style_id == self.cursor.style_id)
|
|
||||||
{
|
|
||||||
self.cursor.style_ref.?.* -= 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slow path: we need to lookup this style so we can decrement
|
|
||||||
// the ref count. Since we've already loaded everything, we also
|
|
||||||
// just go ahead and GC it if it reaches zero, too.
|
|
||||||
if (page.styles.lookupId(
|
|
||||||
page.memory,
|
|
||||||
cell.style_id,
|
|
||||||
)) |prev_style| {
|
|
||||||
// Below upsert can't fail because it should already be present
|
|
||||||
const md = page.styles.upsert(
|
|
||||||
page.memory,
|
|
||||||
prev_style.*,
|
|
||||||
) catch unreachable;
|
|
||||||
assert(md.ref > 0);
|
|
||||||
md.ref -= 1;
|
|
||||||
if (md.ref == 0) page.styles.remove(page.memory, cell.style_id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have no left/right scroll region we can be sure that
|
// If we have no left/right scroll region we can be sure that
|
||||||
@ -1044,17 +979,37 @@ pub fn resizeWithoutReflow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Resize the screen.
|
/// Resize the screen.
|
||||||
// TODO: replace resize and resizeWithoutReflow with this.
|
|
||||||
fn resizeInternal(
|
fn resizeInternal(
|
||||||
self: *Screen,
|
self: *Screen,
|
||||||
cols: size.CellCountInt,
|
cols: size.CellCountInt,
|
||||||
rows: size.CellCountInt,
|
rows: size.CellCountInt,
|
||||||
reflow: bool,
|
reflow: bool,
|
||||||
) !void {
|
) !void {
|
||||||
|
defer self.assertIntegrity();
|
||||||
|
|
||||||
// No matter what we mark our image state as dirty
|
// No matter what we mark our image state as dirty
|
||||||
self.kitty_images.dirty = true;
|
self.kitty_images.dirty = true;
|
||||||
|
|
||||||
// Perform the resize operation. This will update cursor by reference.
|
// Release the cursor style while resizing just
|
||||||
|
// in case the cursor ends up on a different page.
|
||||||
|
const cursor_style = self.cursor.style;
|
||||||
|
self.cursor.style = .{};
|
||||||
|
self.manualStyleUpdate() catch unreachable;
|
||||||
|
defer {
|
||||||
|
// Restore the cursor style.
|
||||||
|
self.cursor.style = cursor_style;
|
||||||
|
self.manualStyleUpdate() catch |err| {
|
||||||
|
// This failure should not happen because manualStyleUpdate
|
||||||
|
// handles page splitting, overflow, and more. This should only
|
||||||
|
// happen if we're out of RAM. In this case, we'll just degrade
|
||||||
|
// gracefully back to the default style.
|
||||||
|
log.err("failed to update style on cursor reload err={}", .{err});
|
||||||
|
self.cursor.style = .{};
|
||||||
|
self.cursor.style_id = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the resize operation.
|
||||||
try self.pages.resize(.{
|
try self.pages.resize(.{
|
||||||
.rows = rows,
|
.rows = rows,
|
||||||
.cols = cols,
|
.cols = cols,
|
||||||
@ -1072,7 +1027,6 @@ fn resizeInternal(
|
|||||||
// If our cursor was updated, we do a full reload so all our cursor
|
// If our cursor was updated, we do a full reload so all our cursor
|
||||||
// state is correct.
|
// state is correct.
|
||||||
self.cursorReload();
|
self.cursorReload();
|
||||||
self.assertIntegrity();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a style attribute for the current cursor.
|
/// Set a style attribute for the current cursor.
|
||||||
@ -1223,21 +1177,14 @@ pub fn manualStyleUpdate(self: *Screen) !void {
|
|||||||
|
|
||||||
// std.log.warn("active styles={}", .{page.styles.count(page.memory)});
|
// std.log.warn("active styles={}", .{page.styles.count(page.memory)});
|
||||||
|
|
||||||
// Remove our previous style if is unused.
|
// Release our previous style if it was not default.
|
||||||
if (self.cursor.style_ref) |ref| {
|
if (self.cursor.style_id != style.default_id) {
|
||||||
if (ref.* == 0) {
|
page.styles.release(page.memory, self.cursor.style_id);
|
||||||
page.styles.remove(page.memory, self.cursor.style_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset our ID and ref to null since the ref is now invalid.
|
|
||||||
self.cursor.style_id = 0;
|
|
||||||
self.cursor.style_ref = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If our new style is the default, just reset to that
|
// If our new style is the default, just reset to that
|
||||||
if (self.cursor.style.default()) {
|
if (self.cursor.style.default()) {
|
||||||
self.cursor.style_id = 0;
|
self.cursor.style_id = 0;
|
||||||
self.cursor.style_ref = null;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1251,42 +1198,29 @@ pub fn manualStyleUpdate(self: *Screen) !void {
|
|||||||
// if that makes a meaningful difference. Our priority is to keep print
|
// if that makes a meaningful difference. Our priority is to keep print
|
||||||
// fast because setting a ton of styles that do nothing is uncommon
|
// fast because setting a ton of styles that do nothing is uncommon
|
||||||
// and weird.
|
// and weird.
|
||||||
const md = page.styles.upsert(
|
const id = page.styles.add(
|
||||||
page.memory,
|
page.memory,
|
||||||
self.cursor.style,
|
self.cursor.style,
|
||||||
) catch |err| md: {
|
) catch id: {
|
||||||
switch (err) {
|
// Our style map is full. Let's allocate a new
|
||||||
// Our style map is full. Let's allocate a new page by doubling
|
// page by doubling the size and then try again.
|
||||||
// the size and then try again.
|
const node = try self.pages.adjustCapacity(
|
||||||
error.OutOfMemory => {
|
self.cursor.page_pin.page,
|
||||||
const node = try self.pages.adjustCapacity(
|
.{ .styles = page.capacity.styles * 2 },
|
||||||
self.cursor.page_pin.page,
|
);
|
||||||
.{ .styles = page.capacity.styles * 2 },
|
|
||||||
);
|
|
||||||
|
|
||||||
page = &node.data;
|
page = &node.data;
|
||||||
},
|
|
||||||
|
|
||||||
// We've run out of style IDs. This is fixed by doing a page
|
|
||||||
// compaction.
|
|
||||||
error.Overflow => {
|
|
||||||
const node = try self.pages.compact(
|
|
||||||
self.cursor.page_pin.page,
|
|
||||||
);
|
|
||||||
page = &node.data;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since this modifies our cursor page, we need to reload
|
// Since this modifies our cursor page, we need to reload
|
||||||
cursor_reload = true;
|
cursor_reload = true;
|
||||||
|
|
||||||
break :md try page.styles.upsert(
|
break :id try page.styles.add(
|
||||||
page.memory,
|
page.memory,
|
||||||
self.cursor.style,
|
self.cursor.style,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
self.cursor.style_id = md.id;
|
self.cursor.style_id = id;
|
||||||
self.cursor.style_ref = &md.ref;
|
|
||||||
if (cursor_reload) self.cursorReload();
|
if (cursor_reload) self.cursorReload();
|
||||||
self.assertIntegrity();
|
self.assertIntegrity();
|
||||||
}
|
}
|
||||||
@ -2271,8 +2205,9 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// If we have a ref-counted style, increase.
|
// If we have a ref-counted style, increase.
|
||||||
if (self.cursor.style_ref) |ref| {
|
if (self.cursor.style_id != style.default_id) {
|
||||||
ref.* += 1;
|
const page = self.cursor.page_pin.page.data;
|
||||||
|
page.styles.use(page.memory, self.cursor.style_id);
|
||||||
self.cursor.page_row.styled = true;
|
self.cursor.page_row.styled = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2310,6 +2245,14 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
|
|||||||
.wide = .spacer_tail,
|
.wide = .spacer_tail,
|
||||||
.protected = self.cursor.protected,
|
.protected = self.cursor.protected,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If we have a ref-counted style, increase twice.
|
||||||
|
if (self.cursor.style_id != style.default_id) {
|
||||||
|
const page = self.cursor.page_pin.page.data;
|
||||||
|
page.styles.use(page.memory, self.cursor.style_id);
|
||||||
|
page.styles.use(page.memory, self.cursor.style_id);
|
||||||
|
self.cursor.page_row.styled = true;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
@ -2792,10 +2735,10 @@ test "Screen: cursorDown across pages preserves style" {
|
|||||||
try s.setAttribute(.{ .bold = {} });
|
try s.setAttribute(.{ .bold = {} });
|
||||||
{
|
{
|
||||||
const page = &s.cursor.page_pin.page.data;
|
const page = &s.cursor.page_pin.page.data;
|
||||||
const styleval = page.styles.lookupId(
|
const styleval = page.styles.get(
|
||||||
page.memory,
|
page.memory,
|
||||||
s.cursor.style_id,
|
s.cursor.style_id,
|
||||||
).?;
|
);
|
||||||
try testing.expect(styleval.flags.bold);
|
try testing.expect(styleval.flags.bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2803,10 +2746,10 @@ test "Screen: cursorDown across pages preserves style" {
|
|||||||
s.cursorDown(1);
|
s.cursorDown(1);
|
||||||
{
|
{
|
||||||
const page = &s.cursor.page_pin.page.data;
|
const page = &s.cursor.page_pin.page.data;
|
||||||
const styleval = page.styles.lookupId(
|
const styleval = page.styles.get(
|
||||||
page.memory,
|
page.memory,
|
||||||
s.cursor.style_id,
|
s.cursor.style_id,
|
||||||
).?;
|
);
|
||||||
try testing.expect(styleval.flags.bold);
|
try testing.expect(styleval.flags.bold);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2835,10 +2778,10 @@ test "Screen: cursorUp across pages preserves style" {
|
|||||||
try s.setAttribute(.{ .bold = {} });
|
try s.setAttribute(.{ .bold = {} });
|
||||||
{
|
{
|
||||||
const page = &s.cursor.page_pin.page.data;
|
const page = &s.cursor.page_pin.page.data;
|
||||||
const styleval = page.styles.lookupId(
|
const styleval = page.styles.get(
|
||||||
page.memory,
|
page.memory,
|
||||||
s.cursor.style_id,
|
s.cursor.style_id,
|
||||||
).?;
|
);
|
||||||
try testing.expect(styleval.flags.bold);
|
try testing.expect(styleval.flags.bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2848,10 +2791,10 @@ test "Screen: cursorUp across pages preserves style" {
|
|||||||
const page = &s.cursor.page_pin.page.data;
|
const page = &s.cursor.page_pin.page.data;
|
||||||
try testing.expect(start_page == page);
|
try testing.expect(start_page == page);
|
||||||
|
|
||||||
const styleval = page.styles.lookupId(
|
const styleval = page.styles.get(
|
||||||
page.memory,
|
page.memory,
|
||||||
s.cursor.style_id,
|
s.cursor.style_id,
|
||||||
).?;
|
);
|
||||||
try testing.expect(styleval.flags.bold);
|
try testing.expect(styleval.flags.bold);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2880,10 +2823,10 @@ test "Screen: cursorAbsolute across pages preserves style" {
|
|||||||
try s.setAttribute(.{ .bold = {} });
|
try s.setAttribute(.{ .bold = {} });
|
||||||
{
|
{
|
||||||
const page = &s.cursor.page_pin.page.data;
|
const page = &s.cursor.page_pin.page.data;
|
||||||
const styleval = page.styles.lookupId(
|
const styleval = page.styles.get(
|
||||||
page.memory,
|
page.memory,
|
||||||
s.cursor.style_id,
|
s.cursor.style_id,
|
||||||
).?;
|
);
|
||||||
try testing.expect(styleval.flags.bold);
|
try testing.expect(styleval.flags.bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2893,10 +2836,10 @@ test "Screen: cursorAbsolute across pages preserves style" {
|
|||||||
const page = &s.cursor.page_pin.page.data;
|
const page = &s.cursor.page_pin.page.data;
|
||||||
try testing.expect(start_page == page);
|
try testing.expect(start_page == page);
|
||||||
|
|
||||||
const styleval = page.styles.lookupId(
|
const styleval = page.styles.get(
|
||||||
page.memory,
|
page.memory,
|
||||||
s.cursor.style_id,
|
s.cursor.style_id,
|
||||||
).?;
|
);
|
||||||
try testing.expect(styleval.flags.bold);
|
try testing.expect(styleval.flags.bold);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3013,10 +2956,10 @@ test "Screen: scrolling across pages preserves style" {
|
|||||||
const page = &s.pages.pages.last.?.data;
|
const page = &s.pages.pages.last.?.data;
|
||||||
try testing.expect(start_page != page);
|
try testing.expect(start_page != page);
|
||||||
|
|
||||||
const styleval = page.styles.lookupId(
|
const styleval = page.styles.get(
|
||||||
page.memory,
|
page.memory,
|
||||||
s.cursor.style_id,
|
s.cursor.style_id,
|
||||||
).?;
|
);
|
||||||
try testing.expect(styleval.flags.bold);
|
try testing.expect(styleval.flags.bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -600,8 +600,19 @@ fn printCell(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep track of the previous style so we can decrement the ref count
|
// We don't need to update the style refs unless the
|
||||||
const prev_style_id = cell.style_id;
|
// cell's new style will be different after writing.
|
||||||
|
const style_changed = cell.style_id != self.screen.cursor.style_id;
|
||||||
|
|
||||||
|
if (style_changed) {
|
||||||
|
var page = &self.screen.cursor.page_pin.page.data;
|
||||||
|
|
||||||
|
// Release the old style.
|
||||||
|
if (cell.style_id != style.default_id) {
|
||||||
|
assert(self.screen.cursor.page_row.styled);
|
||||||
|
page.styles.release(page.memory, cell.style_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Write
|
// Write
|
||||||
cell.* = .{
|
cell.* = .{
|
||||||
@ -612,50 +623,12 @@ fn printCell(
|
|||||||
.protected = self.screen.cursor.protected,
|
.protected = self.screen.cursor.protected,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (comptime std.debug.runtime_safety) {
|
if (style_changed) {
|
||||||
// We've had bugs around this, so let's add an assertion: every
|
var page = &self.screen.cursor.page_pin.page.data;
|
||||||
// style we use should be present in the style table.
|
|
||||||
if (self.screen.cursor.style_id != style.default_id) {
|
|
||||||
const page = &self.screen.cursor.page_pin.page.data;
|
|
||||||
if (page.styles.lookupId(
|
|
||||||
page.memory,
|
|
||||||
self.screen.cursor.style_id,
|
|
||||||
) == null) {
|
|
||||||
log.err("can't find style page={X} id={}", .{
|
|
||||||
@intFromPtr(&self.screen.cursor.page_pin.page.data),
|
|
||||||
self.screen.cursor.style_id,
|
|
||||||
});
|
|
||||||
@panic("style not found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the style ref count handling
|
// Use the new style.
|
||||||
style_ref: {
|
if (cell.style_id != style.default_id) {
|
||||||
if (prev_style_id != style.default_id) {
|
page.styles.use(page.memory, cell.style_id);
|
||||||
const row = self.screen.cursor.page_row;
|
|
||||||
assert(row.styled);
|
|
||||||
|
|
||||||
// If our previous cell had the same style ID as us currently,
|
|
||||||
// then we don't bother with any ref counts because we're the same.
|
|
||||||
if (prev_style_id == self.screen.cursor.style_id) break :style_ref;
|
|
||||||
|
|
||||||
// Slow path: we need to lookup this style so we can decrement
|
|
||||||
// the ref count. Since we've already loaded everything, we also
|
|
||||||
// just go ahead and GC it if it reaches zero, too.
|
|
||||||
var page = &self.screen.cursor.page_pin.page.data;
|
|
||||||
if (page.styles.lookupId(page.memory, prev_style_id)) |prev_style| {
|
|
||||||
// Below upsert can't fail because it should already be present
|
|
||||||
const md = page.styles.upsert(page.memory, prev_style.*) catch unreachable;
|
|
||||||
assert(md.ref > 0);
|
|
||||||
md.ref -= 1;
|
|
||||||
if (md.ref == 0) page.styles.remove(page.memory, prev_style_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a ref-counted style, increase.
|
|
||||||
if (self.screen.cursor.style_ref) |ref| {
|
|
||||||
ref.* += 1;
|
|
||||||
self.screen.cursor.page_row.styled = true;
|
self.screen.cursor.page_row.styled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2190,8 +2163,12 @@ pub fn decaln(self: *Terminal) !void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// If we have a ref-counted style, increase
|
// If we have a ref-counted style, increase
|
||||||
if (self.screen.cursor.style_ref) |ref| {
|
if (self.screen.cursor.style_id != style.default_id) {
|
||||||
ref.* += @intCast(cells.len);
|
page.styles.useMultiple(
|
||||||
|
page.memory,
|
||||||
|
self.screen.cursor.style_id,
|
||||||
|
@intCast(cells.len),
|
||||||
|
);
|
||||||
row.styled = true;
|
row.styled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7066,7 +7043,8 @@ test "Terminal: bold style" {
|
|||||||
const cell = list_cell.cell;
|
const cell = list_cell.cell;
|
||||||
try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint);
|
try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint);
|
||||||
try testing.expect(cell.style_id != 0);
|
try testing.expect(cell.style_id != 0);
|
||||||
try testing.expect(t.screen.cursor.style_ref.?.* > 0);
|
const page = t.screen.cursor.page_pin.page.data;
|
||||||
|
try testing.expect(page.styles.refCount(page.memory, t.screen.cursor.style_id) > 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,6 +197,7 @@ pub const Page = struct {
|
|||||||
.styles = style.Set.init(
|
.styles = style.Set.init(
|
||||||
buf.add(l.styles_start),
|
buf.add(l.styles_start),
|
||||||
l.styles_layout,
|
l.styles_layout,
|
||||||
|
style.StyleSetContext{},
|
||||||
),
|
),
|
||||||
.grapheme_alloc = GraphemeAlloc.init(
|
.grapheme_alloc = GraphemeAlloc.init(
|
||||||
buf.add(l.grapheme_alloc_start),
|
buf.add(l.grapheme_alloc_start),
|
||||||
@ -324,17 +325,11 @@ pub const Page = struct {
|
|||||||
|
|
||||||
if (cell.style_id != style.default_id) {
|
if (cell.style_id != style.default_id) {
|
||||||
// If a cell has a style, it must be present in the styles
|
// If a cell has a style, it must be present in the styles
|
||||||
// set.
|
// set. Accessing it with `get` asserts that.
|
||||||
_ = self.styles.lookupId(
|
_ = self.styles.get(
|
||||||
self.memory,
|
self.memory,
|
||||||
cell.style_id,
|
cell.style_id,
|
||||||
) orelse {
|
);
|
||||||
log.warn(
|
|
||||||
"page integrity violation y={} x={} style missing id={}",
|
|
||||||
.{ y, x, cell.style_id },
|
|
||||||
);
|
|
||||||
return IntegrityError.MissingStyle;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!row.styled) {
|
if (!row.styled) {
|
||||||
log.warn(
|
log.warn(
|
||||||
@ -424,12 +419,11 @@ pub const Page = struct {
|
|||||||
{
|
{
|
||||||
var it = styles_seen.iterator();
|
var it = styles_seen.iterator();
|
||||||
while (it.next()) |entry| {
|
while (it.next()) |entry| {
|
||||||
const style_val = self.styles.lookupId(self.memory, entry.key_ptr.*).?.*;
|
const ref_count = self.styles.refCount(self.memory, entry.key_ptr.*);
|
||||||
const md = self.styles.upsert(self.memory, style_val) catch unreachable;
|
if (ref_count < entry.value_ptr.*) {
|
||||||
if (md.ref < entry.value_ptr.*) {
|
|
||||||
log.warn(
|
log.warn(
|
||||||
"page integrity violation style ref count mismatch id={} expected={} actual={}",
|
"page integrity violation style ref count mismatch id={} expected={} actual={}",
|
||||||
.{ entry.key_ptr.*, entry.value_ptr.*, md.ref },
|
.{ entry.key_ptr.*, entry.value_ptr.*, ref_count },
|
||||||
);
|
);
|
||||||
return IntegrityError.MismatchedStyleRef;
|
return IntegrityError.MismatchedStyleRef;
|
||||||
}
|
}
|
||||||
@ -474,7 +468,7 @@ pub const Page = struct {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const CloneFromError = Allocator.Error || style.Set.UpsertError;
|
pub const CloneFromError = Allocator.Error || error{OutOfMemory};
|
||||||
|
|
||||||
/// Clone the contents of another page into this page. The capacities
|
/// Clone the contents of another page into this page. The capacities
|
||||||
/// can be different, but the size of the other page must fit into
|
/// can be different, but the size of the other page must fit into
|
||||||
@ -586,11 +580,22 @@ pub const Page = struct {
|
|||||||
for (cps) |cp| try self.appendGrapheme(dst_row, dst_cell, cp);
|
for (cps) |cp| try self.appendGrapheme(dst_row, dst_cell, cp);
|
||||||
}
|
}
|
||||||
if (src_cell.style_id != style.default_id) {
|
if (src_cell.style_id != style.default_id) {
|
||||||
const other_style = other.styles.lookupId(other.memory, src_cell.style_id).?.*;
|
|
||||||
const md = try self.styles.upsert(self.memory, other_style);
|
|
||||||
md.ref += 1;
|
|
||||||
dst_cell.style_id = md.id;
|
|
||||||
dst_row.styled = true;
|
dst_row.styled = true;
|
||||||
|
|
||||||
|
if (other == self) {
|
||||||
|
// If it's the same page we don't have to worry about
|
||||||
|
// copying the style, we can use the style ID directly.
|
||||||
|
dst_cell.style_id = src_cell.style_id;
|
||||||
|
self.styles.use(self.memory, dst_cell.style_id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slow path: Get the style from the other
|
||||||
|
// page and add it to this page's style set.
|
||||||
|
const other_style = other.styles.get(other.memory, src_cell.style_id).*;
|
||||||
|
if (try self.styles.addWithId(self.memory, other_style, src_cell.style_id)) |id| {
|
||||||
|
dst_cell.style_id = id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -767,13 +772,7 @@ pub const Page = struct {
|
|||||||
for (cells) |*cell| {
|
for (cells) |*cell| {
|
||||||
if (cell.style_id == style.default_id) continue;
|
if (cell.style_id == style.default_id) continue;
|
||||||
|
|
||||||
if (self.styles.lookupId(self.memory, cell.style_id)) |prev_style| {
|
self.styles.release(self.memory, cell.style_id);
|
||||||
// Below upsert can't fail because it should already be present
|
|
||||||
const md = self.styles.upsert(self.memory, prev_style.*) catch unreachable;
|
|
||||||
assert(md.ref > 0);
|
|
||||||
md.ref -= 1;
|
|
||||||
if (md.ref == 0) self.styles.remove(self.memory, cell.style_id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cells.len == self.size.cols) row.styled = false;
|
if (cells.len == self.size.cols) row.styled = false;
|
||||||
@ -2112,7 +2111,7 @@ test "Page verifyIntegrity styles good" {
|
|||||||
defer page.deinit();
|
defer page.deinit();
|
||||||
|
|
||||||
// Upsert a style we'll use
|
// Upsert a style we'll use
|
||||||
const md = try page.styles.upsert(page.memory, .{ .flags = .{
|
const id = try page.styles.add(page.memory, .{ .flags = .{
|
||||||
.bold = true,
|
.bold = true,
|
||||||
} });
|
} });
|
||||||
|
|
||||||
@ -2123,11 +2122,15 @@ test "Page verifyIntegrity styles good" {
|
|||||||
rac.cell.* = .{
|
rac.cell.* = .{
|
||||||
.content_tag = .codepoint,
|
.content_tag = .codepoint,
|
||||||
.content = .{ .codepoint = @intCast(x + 1) },
|
.content = .{ .codepoint = @intCast(x + 1) },
|
||||||
.style_id = md.id,
|
.style_id = id,
|
||||||
};
|
};
|
||||||
md.ref += 1;
|
page.styles.use(page.memory, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The original style add would have incremented the
|
||||||
|
// ref count too, so release it to balance that out.
|
||||||
|
page.styles.release(page.memory, id);
|
||||||
|
|
||||||
try page.verifyIntegrity(testing.allocator);
|
try page.verifyIntegrity(testing.allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2140,7 +2143,7 @@ test "Page verifyIntegrity styles ref count mismatch" {
|
|||||||
defer page.deinit();
|
defer page.deinit();
|
||||||
|
|
||||||
// Upsert a style we'll use
|
// Upsert a style we'll use
|
||||||
const md = try page.styles.upsert(page.memory, .{ .flags = .{
|
const id = try page.styles.add(page.memory, .{ .flags = .{
|
||||||
.bold = true,
|
.bold = true,
|
||||||
} });
|
} });
|
||||||
|
|
||||||
@ -2151,13 +2154,17 @@ test "Page verifyIntegrity styles ref count mismatch" {
|
|||||||
rac.cell.* = .{
|
rac.cell.* = .{
|
||||||
.content_tag = .codepoint,
|
.content_tag = .codepoint,
|
||||||
.content = .{ .codepoint = @intCast(x + 1) },
|
.content = .{ .codepoint = @intCast(x + 1) },
|
||||||
.style_id = md.id,
|
.style_id = id,
|
||||||
};
|
};
|
||||||
md.ref += 1;
|
page.styles.use(page.memory, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The original style add would have incremented the
|
||||||
|
// ref count too, so release it to balance that out.
|
||||||
|
page.styles.release(page.memory, id);
|
||||||
|
|
||||||
// Miss a ref
|
// Miss a ref
|
||||||
md.ref -= 1;
|
page.styles.release(page.memory, id);
|
||||||
|
|
||||||
try testing.expectError(
|
try testing.expectError(
|
||||||
Page.IntegrityError.MismatchedStyleRef,
|
Page.IntegrityError.MismatchedStyleRef,
|
||||||
|
573
src/terminal/ref_counted_set.zig
Normal file
573
src/terminal/ref_counted_set.zig
Normal file
@ -0,0 +1,573 @@
|
|||||||
|
const size = @import("size.zig");
|
||||||
|
const Offset = size.Offset;
|
||||||
|
const OffsetBuf = size.OffsetBuf;
|
||||||
|
|
||||||
|
const fastmem = @import("../fastmem.zig");
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
/// A reference counted set.
|
||||||
|
///
|
||||||
|
/// This set is created with some capacity in mind. You can determine
|
||||||
|
/// the exact memory requirement of a given capacity by calling `layout`
|
||||||
|
/// and checking the total size.
|
||||||
|
///
|
||||||
|
/// When the set exceeds capacity, `error.OutOfMemory` is returned from
|
||||||
|
/// any memory-using methods. The caller is responsible for determining
|
||||||
|
/// a path forward.
|
||||||
|
///
|
||||||
|
/// This set is reference counted. Each item in the set has an associated
|
||||||
|
/// reference count. The caller is responsible for calling release for an
|
||||||
|
/// item when it is no longer being used. Items with 0 references will be
|
||||||
|
/// kept until another item is written to their bucket. This allows items
|
||||||
|
/// to be ressurected if they are re-added before they get overwritten.
|
||||||
|
///
|
||||||
|
/// The backing data structure of this set is an open addressed hash table
|
||||||
|
/// with linear probing and Robin Hood hashing, and a flat array of items.
|
||||||
|
///
|
||||||
|
/// The table maps values to item IDs, which are indices in the item array
|
||||||
|
/// which contain the item's value and its reference count. Item IDs can be
|
||||||
|
/// used to efficiently access an item and update its reference count after
|
||||||
|
/// it has been added to the table, to avoid having to use the hash map to
|
||||||
|
/// look the value back up.
|
||||||
|
///
|
||||||
|
/// ID 0 is reserved and will never be assigned.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// `Context`
|
||||||
|
/// A type containing methods to define behaviors.
|
||||||
|
/// - `fn hash(*Context, T) u64` - Return a hash for an item.
|
||||||
|
/// - `fn eql(*Context, T, T) bool` - Check two items for equality.
|
||||||
|
///
|
||||||
|
/// - `fn deleted(*Context, T) void` - [OPTIONAL] Deletion callback.
|
||||||
|
/// If present, called whenever an item is finally deleted.
|
||||||
|
/// Useful if the item has memory that needs to be freed.
|
||||||
|
///
|
||||||
|
pub fn RefCountedSet(
|
||||||
|
comptime T: type,
|
||||||
|
comptime Id: type,
|
||||||
|
comptime RefCountInt: type,
|
||||||
|
comptime Context: type,
|
||||||
|
) type {
|
||||||
|
return struct {
|
||||||
|
const Self = RefCountedSet(T, Id, RefCountInt, Context);
|
||||||
|
|
||||||
|
pub const base_align = @max(
|
||||||
|
@alignOf(Context),
|
||||||
|
@alignOf(Layout),
|
||||||
|
@alignOf(Item),
|
||||||
|
@alignOf(Id),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Set item
|
||||||
|
pub const Item = struct {
|
||||||
|
/// The value this item represents.
|
||||||
|
value: T = undefined,
|
||||||
|
/// Metadata for this item.
|
||||||
|
meta: Metadata = .{},
|
||||||
|
|
||||||
|
pub const Metadata = struct {
|
||||||
|
/// The bucket in the hash table where this item
|
||||||
|
/// is referenced.
|
||||||
|
bucket: Id = std.math.maxInt(Id),
|
||||||
|
|
||||||
|
/// The length of the probe sequence between this
|
||||||
|
/// item's starting bucket and the bucket it's in,
|
||||||
|
/// used for Robin Hood hashing.
|
||||||
|
psl: Id = 0,
|
||||||
|
|
||||||
|
/// The reference count for this item.
|
||||||
|
ref: RefCountInt = 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A hash table of item indices
|
||||||
|
table: Offset(Id),
|
||||||
|
|
||||||
|
/// By keeping track of the max probe sequence length
|
||||||
|
/// we can bail out early when looking up values that
|
||||||
|
/// aren't present.
|
||||||
|
max_psl: Id = 0,
|
||||||
|
|
||||||
|
/// We keep track of how many items have a PSL of any
|
||||||
|
/// given length, so that we can shrink max_psl when
|
||||||
|
/// we delete items.
|
||||||
|
///
|
||||||
|
/// A probe sequence of length 32 or more is astronomically
|
||||||
|
/// unlikely. Roughly a (1/table_cap)^32 -- with any normal
|
||||||
|
/// table capacity that is so unlikely that it's not worth
|
||||||
|
/// handling.
|
||||||
|
psl_stats: [32]Id = [_]Id{0} ** 32,
|
||||||
|
|
||||||
|
/// The backing store of items
|
||||||
|
items: Offset(Item),
|
||||||
|
|
||||||
|
/// The next index to store an item at.
|
||||||
|
/// Id 0 is reserved for unused items.
|
||||||
|
next_id: Id = 1,
|
||||||
|
|
||||||
|
layout: Layout,
|
||||||
|
|
||||||
|
/// An instance of the context structure.
|
||||||
|
context: Context,
|
||||||
|
|
||||||
|
/// Returns the memory layout for the given base offset and
|
||||||
|
/// desired capacity. The layout can be used by the caller to
|
||||||
|
/// determine how much memory to allocate, and the layout must
|
||||||
|
/// be used to initialize the set so that the set knows all
|
||||||
|
/// the offsets for the various buffers.
|
||||||
|
///
|
||||||
|
/// The capacity passed for cap will be used for the hash table,
|
||||||
|
/// which has a load factor of `0.8125` (13/16), so the number of
|
||||||
|
/// items which can actually be stored in the set will be smaller.
|
||||||
|
///
|
||||||
|
/// The laid out capacity will be at least `cap`, but may be higher,
|
||||||
|
/// since it is rounded up to the next power of 2 for efficiency.
|
||||||
|
///
|
||||||
|
/// The returned layout `cap` property will be 1 more than the number
|
||||||
|
/// of items that the set can actually store, since ID 0 is reserved.
|
||||||
|
pub fn layout(cap: Id) Layout {
|
||||||
|
// Experimentally, this load factor works quite well.
|
||||||
|
const load_factor = 0.8125;
|
||||||
|
|
||||||
|
const table_cap: Id = std.math.ceilPowerOfTwoAssert(Id, cap);
|
||||||
|
const table_mask: Id = (@as(Id, 1) << std.math.log2_int(Id, table_cap)) - 1;
|
||||||
|
const items_cap: Id = @intFromFloat(load_factor * @as(f64, @floatFromInt(table_cap)));
|
||||||
|
|
||||||
|
const table_start = 0;
|
||||||
|
const table_end = table_start + table_cap * @sizeOf(Id);
|
||||||
|
|
||||||
|
const items_start = std.mem.alignForward(usize, table_end, @alignOf(Item));
|
||||||
|
const items_end = items_start + items_cap * @sizeOf(Item);
|
||||||
|
|
||||||
|
const total_size = items_end;
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.cap = items_cap,
|
||||||
|
.table_cap = table_cap,
|
||||||
|
.table_mask = table_mask,
|
||||||
|
.table_start = table_start,
|
||||||
|
.items_start = items_start,
|
||||||
|
.total_size = total_size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Layout = struct {
|
||||||
|
cap: Id,
|
||||||
|
table_cap: Id,
|
||||||
|
table_mask: Id,
|
||||||
|
table_start: usize,
|
||||||
|
items_start: usize,
|
||||||
|
total_size: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(base: OffsetBuf, l: Layout, context: Context) Self {
|
||||||
|
const table = base.member(Id, l.table_start);
|
||||||
|
const items = base.member(Item, l.items_start);
|
||||||
|
|
||||||
|
@memset(table.ptr(base)[0..l.table_cap], 0);
|
||||||
|
@memset(items.ptr(base)[0..l.cap], .{});
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.table = table,
|
||||||
|
.items = items,
|
||||||
|
.layout = l,
|
||||||
|
.context = context,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an item to the set if not present
|
||||||
|
/// and increment its reference count.
|
||||||
|
///
|
||||||
|
/// Returns the item's ID.
|
||||||
|
///
|
||||||
|
/// If the set has no more room, then an
|
||||||
|
/// OutOfMemory error is returned instead.
|
||||||
|
pub fn add(self: *Self, base: anytype, value: T) error{OutOfMemory}!Id {
|
||||||
|
const items = self.items.ptr(base);
|
||||||
|
|
||||||
|
// Trim dead items from the end of the list.
|
||||||
|
while (self.next_id > 1 and items[self.next_id - 1].meta.ref == 0) {
|
||||||
|
self.next_id -= 1;
|
||||||
|
|
||||||
|
self.deleteItem(base, self.next_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we still don't have an available ID, we're out of memory.
|
||||||
|
if (self.next_id >= self.layout.cap) {
|
||||||
|
return error.OutOfMemory;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = self.upsert(base, value, self.next_id);
|
||||||
|
items[id].meta.ref += 1;
|
||||||
|
|
||||||
|
if (id == self.next_id) {
|
||||||
|
self.next_id += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an item to the set if not present
|
||||||
|
/// and increment its reference count.
|
||||||
|
/// If possible, use the provided ID.
|
||||||
|
///
|
||||||
|
/// Returns the item's ID, or null
|
||||||
|
/// if the provided ID was used.
|
||||||
|
///
|
||||||
|
/// If the set has no more room, then an
|
||||||
|
/// OutOfMemory error is returned instead.
|
||||||
|
pub fn addWithId(self: *Self, base: anytype, value: T, id: Id) error{OutOfMemory}!?Id {
|
||||||
|
const items = self.items.ptr(base);
|
||||||
|
|
||||||
|
if (id < self.next_id) {
|
||||||
|
if (items[id].meta.ref == 0) {
|
||||||
|
self.deleteItem(base, id);
|
||||||
|
|
||||||
|
const added_id = self.upsert(base, value, id);
|
||||||
|
|
||||||
|
items[added_id].meta.ref += 1;
|
||||||
|
|
||||||
|
return if (added_id == id) null else added_id;
|
||||||
|
} else if (self.context.eql(value, items[id].value)) {
|
||||||
|
items[id].meta.ref += 1;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return try self.add(base, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increment an item's reference count by 1.
|
||||||
|
///
|
||||||
|
/// Asserts that the item's reference count is greater than 0.
|
||||||
|
pub fn use(self: *const Self, base: anytype, id: Id) void {
|
||||||
|
assert(id > 0);
|
||||||
|
assert(id < self.layout.cap);
|
||||||
|
|
||||||
|
const items = self.items.ptr(base);
|
||||||
|
const item = &items[id];
|
||||||
|
|
||||||
|
// If `use` is being called on an item with 0 references, then
|
||||||
|
// either someone forgot to call it before, released too early
|
||||||
|
// or lied about releasing. In any case something is wrong and
|
||||||
|
// shouldn't be allowed.
|
||||||
|
assert(item.meta.ref > 0);
|
||||||
|
|
||||||
|
item.meta.ref += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increment an item's reference count by a specified number.
|
||||||
|
///
|
||||||
|
/// Asserts that the item's reference count is greater than 0.
|
||||||
|
pub fn useMultiple(self: *const Self, base: anytype, id: Id, n: RefCountInt) void {
|
||||||
|
assert(id > 0);
|
||||||
|
assert(id < self.layout.cap);
|
||||||
|
|
||||||
|
const items = self.items.ptr(base);
|
||||||
|
const item = &items[id];
|
||||||
|
|
||||||
|
// If `use` is being called on an item with 0 references, then
|
||||||
|
// either someone forgot to call it before, released too early
|
||||||
|
// or lied about releasing. In any case something is wrong and
|
||||||
|
// shouldn't be allowed.
|
||||||
|
assert(item.meta.ref > 0);
|
||||||
|
|
||||||
|
item.meta.ref += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an item by its ID without incrementing its reference count.
|
||||||
|
///
|
||||||
|
/// Asserts that the item's reference count is greater than 0.
|
||||||
|
pub fn get(self: *const Self, base: anytype, id: Id) *T {
|
||||||
|
assert(id > 0);
|
||||||
|
assert(id < self.layout.cap);
|
||||||
|
|
||||||
|
const items = self.items.ptr(base);
|
||||||
|
const item = &items[id];
|
||||||
|
|
||||||
|
assert(item.meta.ref > 0);
|
||||||
|
|
||||||
|
return @ptrCast(&item.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Releases a reference to an item by its ID.
|
||||||
|
///
|
||||||
|
/// Asserts that the item's reference count is greater than 0.
|
||||||
|
pub fn release(self: *Self, base: anytype, id: Id) void {
|
||||||
|
assert(id > 0);
|
||||||
|
assert(id < self.layout.cap);
|
||||||
|
|
||||||
|
const items = self.items.ptr(base);
|
||||||
|
const item = &items[id];
|
||||||
|
|
||||||
|
assert(item.meta.ref > 0);
|
||||||
|
item.meta.ref -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release a specified number of references to an item by its ID.
|
||||||
|
///
|
||||||
|
/// Asserts that the item's reference count is at least `n`.
|
||||||
|
pub fn releaseMultiple(self: *Self, base: anytype, id: Id, n: Id) void {
|
||||||
|
assert(id > 0);
|
||||||
|
assert(id < self.layout.cap);
|
||||||
|
|
||||||
|
const items = self.items.ptr(base);
|
||||||
|
const item = &items[id];
|
||||||
|
|
||||||
|
assert(item.meta.ref >= n);
|
||||||
|
item.meta.ref -= n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the ref count for an item by its ID.
|
||||||
|
pub fn refCount(self: *const Self, base: anytype, id: Id) RefCountInt {
|
||||||
|
assert(id > 0);
|
||||||
|
assert(id < self.layout.cap);
|
||||||
|
|
||||||
|
const items = self.items.ptr(base);
|
||||||
|
const item = &items[id];
|
||||||
|
return item.meta.ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current number of non-dead items in the set.
|
||||||
|
///
|
||||||
|
/// NOT DESIGNED TO BE USED OUTSIDE OF TESTING, this is a very slow
|
||||||
|
/// operation, since it traverses the entire structure to count.
|
||||||
|
///
|
||||||
|
/// Additionally, because this is a testing method, it does extra
|
||||||
|
/// work to verify the integrity of the structure when called.
|
||||||
|
pub fn count(self: *const Self, base: anytype) usize {
|
||||||
|
const table = self.table.ptr(base);
|
||||||
|
const items = self.items.ptr(base);
|
||||||
|
|
||||||
|
// The number of items accessible through the table.
|
||||||
|
var tb_ct: usize = 0;
|
||||||
|
|
||||||
|
for (table[0..self.layout.table_cap]) |id| {
|
||||||
|
if (id != 0) {
|
||||||
|
const item = items[id];
|
||||||
|
if (item.meta.ref > 0) {
|
||||||
|
tb_ct += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The number of items accessible through the backing store.
|
||||||
|
// The two counts should always match- it shouldn't be possible
|
||||||
|
// to have untracked items in the backing store.
|
||||||
|
var it_ct: usize = 0;
|
||||||
|
|
||||||
|
for (items[0..self.layout.cap]) |it| {
|
||||||
|
if (it.meta.ref > 0) {
|
||||||
|
it_ct += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(tb_ct == it_ct);
|
||||||
|
|
||||||
|
return tb_ct;
|
||||||
|
}
|
||||||
|
|
||||||
|
//================================================//
|
||||||
|
// The functions below are all internal functions //
|
||||||
|
// for performing operations on the hash table. //
|
||||||
|
//================================================//
|
||||||
|
|
||||||
|
/// Delete an item, removing any references from
|
||||||
|
/// the table, and freeing its ID to be re-used.
|
||||||
|
fn deleteItem(self: *Self, base: anytype, id: Id) void {
|
||||||
|
const table = self.table.ptr(base);
|
||||||
|
const items = self.items.ptr(base);
|
||||||
|
|
||||||
|
const item = items[id];
|
||||||
|
|
||||||
|
if (item.meta.bucket > self.layout.table_cap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (table[item.meta.bucket] != id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comptime @hasDecl(Context, "deleted")) {
|
||||||
|
// Inform the context struct that we're
|
||||||
|
// deleting the dead item's value for good.
|
||||||
|
self.context.deleted(item.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.psl_stats[item.meta.psl] -= 1;
|
||||||
|
table[item.meta.bucket] = 0;
|
||||||
|
items[id] = .{};
|
||||||
|
|
||||||
|
var p: Id = item.meta.bucket;
|
||||||
|
var n: Id = (p + 1) & self.layout.table_mask;
|
||||||
|
|
||||||
|
while (table[n] != 0 and items[table[n]].meta.psl > 0) {
|
||||||
|
items[table[n]].meta.bucket = p;
|
||||||
|
self.psl_stats[items[table[n]].meta.psl] -= 1;
|
||||||
|
items[table[n]].meta.psl -= 1;
|
||||||
|
self.psl_stats[items[table[n]].meta.psl] += 1;
|
||||||
|
table[p] = table[n];
|
||||||
|
p = n;
|
||||||
|
n = (n + 1) & self.layout.table_mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (self.max_psl > 0 and self.psl_stats[self.max_psl] == 0) {
|
||||||
|
self.max_psl -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[p] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find an item in the table and return its ID.
|
||||||
|
/// If the item does not exist in the table, null is returned.
|
||||||
|
fn lookup(self: *Self, base: anytype, value: T) ?Id {
|
||||||
|
const table = self.table.ptr(base);
|
||||||
|
const items = self.items.ptr(base);
|
||||||
|
|
||||||
|
const hash: u64 = self.context.hash(value);
|
||||||
|
|
||||||
|
for (0..self.max_psl + 1) |i| {
|
||||||
|
const p = (hash + i) & self.layout.table_mask;
|
||||||
|
|
||||||
|
const id = table[p];
|
||||||
|
|
||||||
|
// Empty bucket, our item cannot have probed to
|
||||||
|
// any point after this, meaning it's not present.
|
||||||
|
if (id == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = items[id];
|
||||||
|
|
||||||
|
// An item with a shorter probe sequence length would never
|
||||||
|
// end up in the middle of another sequence, since it would
|
||||||
|
// be swapped out if inserted before the new sequence, and
|
||||||
|
// would not be swapped in if inserted afterwards.
|
||||||
|
//
|
||||||
|
// As such, our item cannot be present.
|
||||||
|
if (item.meta.psl < i) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't bother checking dead items.
|
||||||
|
if (item.meta.ref == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the item is a part of the same probe sequence,
|
||||||
|
// we check if it matches the value we're looking for.
|
||||||
|
if (item.meta.psl == i and
|
||||||
|
self.context.eql(value, item.value))
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
fn upsert(self: *Self, base: anytype, value: T, new_id: Id) Id {
|
||||||
|
// If the item already exists, return it.
|
||||||
|
if (self.lookup(base, value)) |id| {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = self.table.ptr(base);
|
||||||
|
const items = self.items.ptr(base);
|
||||||
|
|
||||||
|
// The new item that we'll put in to the table.
|
||||||
|
var new_item: Item = .{
|
||||||
|
.value = value,
|
||||||
|
.meta = .{
|
||||||
|
.psl = 0,
|
||||||
|
.ref = 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const hash: u64 = self.context.hash(value);
|
||||||
|
|
||||||
|
var held_id: Id = new_id;
|
||||||
|
var held_item: *Item = &new_item;
|
||||||
|
|
||||||
|
var chosen_p: ?Id = null;
|
||||||
|
var chosen_id: Id = new_id;
|
||||||
|
|
||||||
|
for (0..self.layout.table_cap - 1) |i| {
|
||||||
|
const p: Id = @intCast((hash + i) & self.layout.table_mask);
|
||||||
|
const id = table[p];
|
||||||
|
|
||||||
|
// Empty bucket, put our held item in to it and break.
|
||||||
|
if (id == 0) {
|
||||||
|
table[p] = held_id;
|
||||||
|
held_item.meta.bucket = p;
|
||||||
|
self.psl_stats[held_item.meta.psl] += 1;
|
||||||
|
self.max_psl = @max(self.max_psl, held_item.meta.psl);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = &items[id];
|
||||||
|
|
||||||
|
// If there's a dead item then we resurrect it
|
||||||
|
// for our value so that we can re-use its ID.
|
||||||
|
if (item.meta.ref == 0) {
|
||||||
|
if (comptime @hasDecl(Context, "deleted")) {
|
||||||
|
// Inform the context struct that we're
|
||||||
|
// deleting the dead item's value for good.
|
||||||
|
self.context.deleted(item.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
chosen_id = id;
|
||||||
|
|
||||||
|
held_item.meta.bucket = p;
|
||||||
|
self.psl_stats[held_item.meta.psl] += 1;
|
||||||
|
self.max_psl = @max(self.max_psl, held_item.meta.psl);
|
||||||
|
|
||||||
|
// If we're not still holding our new item then we
|
||||||
|
// need to make sure that we put the re-used ID in
|
||||||
|
// the right place, where we previously put new_id.
|
||||||
|
if (chosen_p) |c| {
|
||||||
|
table[c] = id;
|
||||||
|
table[p] = held_id;
|
||||||
|
} else {
|
||||||
|
// If we're still holding our new item then we
|
||||||
|
// don't actually have to do anything, because
|
||||||
|
// the table already has the correct ID here.
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This item has a lower PSL, swap it out with our held item.
|
||||||
|
if (item.meta.psl < held_item.meta.psl) {
|
||||||
|
if (held_id == new_id) {
|
||||||
|
chosen_p = p;
|
||||||
|
new_item.meta.bucket = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[p] = held_id;
|
||||||
|
items[held_id].meta.bucket = p;
|
||||||
|
self.psl_stats[held_item.meta.psl] += 1;
|
||||||
|
self.max_psl = @max(self.max_psl, held_item.meta.psl);
|
||||||
|
|
||||||
|
held_id = id;
|
||||||
|
held_item = item;
|
||||||
|
self.psl_stats[item.meta.psl] -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance to the next probe position for our held item.
|
||||||
|
held_item.meta.psl += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
items[chosen_id] = new_item;
|
||||||
|
return chosen_id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -6,8 +6,11 @@ const page = @import("page.zig");
|
|||||||
const size = @import("size.zig");
|
const size = @import("size.zig");
|
||||||
const Offset = size.Offset;
|
const Offset = size.Offset;
|
||||||
const OffsetBuf = size.OffsetBuf;
|
const OffsetBuf = size.OffsetBuf;
|
||||||
const hash_map = @import("hash_map.zig");
|
|
||||||
const AutoOffsetHashMap = hash_map.AutoOffsetHashMap;
|
const Wyhash = std.hash.Wyhash;
|
||||||
|
const autoHash = std.hash.autoHash;
|
||||||
|
|
||||||
|
const RefCountedSet = @import("ref_counted_set.zig").RefCountedSet;
|
||||||
|
|
||||||
/// The unique identifier for a style. This is at most the number of cells
|
/// The unique identifier for a style. This is at most the number of cells
|
||||||
/// that can fit into a terminal page.
|
/// that can fit into a terminal page.
|
||||||
@ -43,11 +46,34 @@ pub const Style = struct {
|
|||||||
none: void,
|
none: void,
|
||||||
palette: u8,
|
palette: u8,
|
||||||
rgb: color.RGB,
|
rgb: color.RGB,
|
||||||
|
|
||||||
|
/// Formatting to make debug logs easier to read
|
||||||
|
/// by only including non-default attributes.
|
||||||
|
pub fn format(
|
||||||
|
self: Color,
|
||||||
|
comptime fmt: []const u8,
|
||||||
|
options: std.fmt.FormatOptions,
|
||||||
|
writer: anytype,
|
||||||
|
) !void {
|
||||||
|
_ = fmt;
|
||||||
|
_ = options;
|
||||||
|
switch (self) {
|
||||||
|
.none => {
|
||||||
|
_ = try writer.write("Color.none");
|
||||||
|
},
|
||||||
|
.palette => |p| {
|
||||||
|
_ = try writer.print("Color.palette{{ {} }}", .{ p });
|
||||||
|
},
|
||||||
|
.rgb => |rgb| {
|
||||||
|
_ = try writer.print("Color.rgb{{ {}, {}, {} }}", .{ rgb.r, rgb.g, rgb.b });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// True if the style is the default style.
|
/// True if the style is the default style.
|
||||||
pub fn default(self: Style) bool {
|
pub fn default(self: Style) bool {
|
||||||
return std.meta.eql(self, .{});
|
return self.eql(.{});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True if the style is equal to another style.
|
/// True if the style is equal to another style.
|
||||||
@ -133,6 +159,83 @@ pub const Style = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Formatting to make debug logs easier to read
|
||||||
|
/// by only including non-default attributes.
|
||||||
|
pub fn format(
|
||||||
|
self: Style,
|
||||||
|
comptime fmt: []const u8,
|
||||||
|
options: std.fmt.FormatOptions,
|
||||||
|
writer: anytype,
|
||||||
|
) !void {
|
||||||
|
_ = fmt;
|
||||||
|
_ = options;
|
||||||
|
|
||||||
|
const dflt: Style = .{};
|
||||||
|
|
||||||
|
_ = try writer.write("Style{ ");
|
||||||
|
|
||||||
|
var started = false;
|
||||||
|
|
||||||
|
inline for (std.meta.fields(Style)) |f| {
|
||||||
|
if (std.mem.eql(u8, f.name, "flags")) {
|
||||||
|
if (started) {
|
||||||
|
_ = try writer.write(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = try writer.write("flags={ ");
|
||||||
|
|
||||||
|
started = false;
|
||||||
|
|
||||||
|
inline for (std.meta.fields(@TypeOf(self.flags))) |ff| {
|
||||||
|
const v = @as(ff.type, @field(self.flags, ff.name));
|
||||||
|
const d = @as(ff.type, @field(dflt.flags, ff.name));
|
||||||
|
if (ff.type == bool) {
|
||||||
|
if (v) {
|
||||||
|
if (started) {
|
||||||
|
_ = try writer.write(", ");
|
||||||
|
}
|
||||||
|
_ = try writer.print("{s}", .{ff.name});
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
} else if (!std.meta.eql(v, d)) {
|
||||||
|
if (started) {
|
||||||
|
_ = try writer.write(", ");
|
||||||
|
}
|
||||||
|
_ = try writer.print(
|
||||||
|
"{s}={any}",
|
||||||
|
.{ ff.name, v },
|
||||||
|
);
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = try writer.write(" }");
|
||||||
|
|
||||||
|
started = true;
|
||||||
|
comptime continue;
|
||||||
|
}
|
||||||
|
const value = @as(f.type, @field(self, f.name));
|
||||||
|
const d_val = @as(f.type, @field(dflt, f.name));
|
||||||
|
if (!std.meta.eql(value, d_val)) {
|
||||||
|
if (started) {
|
||||||
|
_ = try writer.write(", ");
|
||||||
|
}
|
||||||
|
_ = try writer.print(
|
||||||
|
"{s}={any}",
|
||||||
|
.{ f.name, value },
|
||||||
|
);
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = try writer.write(" }");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash(self: *const Style) u64 {
|
||||||
|
var hasher = Wyhash.init(0);
|
||||||
|
autoHash(&hasher, self.*);
|
||||||
|
return hasher.final();
|
||||||
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
// The size of the struct so we can be aware of changes.
|
// The size of the struct so we can be aware of changes.
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
@ -140,170 +243,24 @@ pub const Style = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A set of styles.
|
pub const StyleSetContext = struct {
|
||||||
///
|
pub fn hash(self: *StyleSetContext, style: Style) u64 {
|
||||||
/// This set is created with some capacity in mind. You can determine
|
_ = self;
|
||||||
/// the exact memory requirement for a capacity by calling `layout`
|
return style.hash();
|
||||||
/// and checking the total size.
|
|
||||||
///
|
|
||||||
/// When the set exceeds capacity, `error.OutOfMemory` is returned
|
|
||||||
/// from memory-using methods. The caller is responsible for determining
|
|
||||||
/// a path forward.
|
|
||||||
///
|
|
||||||
/// The general idea behind this structure is that it is optimized for
|
|
||||||
/// the scenario common in terminals where there aren't many unique
|
|
||||||
/// styles, and many cells are usually drawn with a single style before
|
|
||||||
/// changing styles.
|
|
||||||
///
|
|
||||||
/// Callers should call `upsert` when a new style is set. This will
|
|
||||||
/// return a stable pointer to metadata. You should use this metadata
|
|
||||||
/// to keep a ref count of the style usage. When it falls to zero you
|
|
||||||
/// can remove it.
|
|
||||||
pub const Set = struct {
|
|
||||||
pub const base_align = @max(MetadataMap.base_align, IdMap.base_align);
|
|
||||||
|
|
||||||
/// The mapping of a style to associated metadata. This is
|
|
||||||
/// the map that contains the actual style definitions
|
|
||||||
/// (in the form of the key).
|
|
||||||
styles: MetadataMap,
|
|
||||||
|
|
||||||
/// The mapping from ID to style.
|
|
||||||
id_map: IdMap,
|
|
||||||
|
|
||||||
/// The next ID to use for a style that isn't in the set.
|
|
||||||
/// When this overflows we'll begin returning an IdOverflow
|
|
||||||
/// error and the caller must manually compact the style
|
|
||||||
/// set.
|
|
||||||
///
|
|
||||||
/// Id zero is reserved and always is the default style. The
|
|
||||||
/// default style isn't present in the map, its dependent on
|
|
||||||
/// the terminal configuration.
|
|
||||||
next_id: Id = 1,
|
|
||||||
|
|
||||||
/// Maps a style definition to metadata about that style.
|
|
||||||
const MetadataMap = AutoOffsetHashMap(Style, Metadata);
|
|
||||||
|
|
||||||
/// Maps the unique style ID to the concrete style definition.
|
|
||||||
const IdMap = AutoOffsetHashMap(Id, Offset(Style));
|
|
||||||
|
|
||||||
/// Returns the memory layout for the given base offset and
|
|
||||||
/// desired capacity. The layout can be used by the caller to
|
|
||||||
/// determine how much memory to allocate, and the layout must
|
|
||||||
/// be used to initialize the set so that the set knows all
|
|
||||||
/// the offsets for the various buffers.
|
|
||||||
pub fn layout(cap: usize) Layout {
|
|
||||||
const md_layout = MetadataMap.layout(@intCast(cap));
|
|
||||||
const md_start = 0;
|
|
||||||
const md_end = md_start + md_layout.total_size;
|
|
||||||
|
|
||||||
const id_layout = IdMap.layout(@intCast(cap));
|
|
||||||
const id_start = std.mem.alignForward(usize, md_end, IdMap.base_align);
|
|
||||||
const id_end = id_start + id_layout.total_size;
|
|
||||||
|
|
||||||
const total_size = id_end;
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.md_start = md_start,
|
|
||||||
.md_layout = md_layout,
|
|
||||||
.id_start = id_start,
|
|
||||||
.id_layout = id_layout,
|
|
||||||
.total_size = total_size,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Layout = struct {
|
pub fn eql(self: *StyleSetContext, a: Style, b: Style) bool {
|
||||||
md_start: usize,
|
_ = self;
|
||||||
md_layout: MetadataMap.Layout,
|
return a.eql(b);
|
||||||
id_start: usize,
|
|
||||||
id_layout: IdMap.Layout,
|
|
||||||
total_size: usize,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(base: OffsetBuf, l: Layout) Set {
|
|
||||||
const styles_buf = base.add(l.md_start);
|
|
||||||
const id_buf = base.add(l.id_start);
|
|
||||||
return .{
|
|
||||||
.styles = MetadataMap.init(styles_buf, l.md_layout),
|
|
||||||
.id_map = IdMap.init(id_buf, l.id_layout),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Possible errors for upsert.
|
|
||||||
pub const UpsertError = error{
|
|
||||||
/// No more space in the backing buffer. Remove styles or
|
|
||||||
/// grow and reinitialize.
|
|
||||||
OutOfMemory,
|
|
||||||
|
|
||||||
/// No more available IDs. Perform a garbage collection
|
|
||||||
/// operation to compact ID space.
|
|
||||||
Overflow,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Upsert a style into the set and return a pointer to the metadata
|
|
||||||
/// for that style. The pointer is valid for the lifetime of the set
|
|
||||||
/// so long as the style is not removed.
|
|
||||||
///
|
|
||||||
/// The ref count for new styles is initialized to zero and
|
|
||||||
/// for existing styles remains unmodified.
|
|
||||||
pub fn upsert(self: *Set, base: anytype, style: Style) UpsertError!*Metadata {
|
|
||||||
// If we already have the style in the map, this is fast.
|
|
||||||
var map = self.styles.map(base);
|
|
||||||
const gop = try map.getOrPut(style);
|
|
||||||
if (gop.found_existing) return gop.value_ptr;
|
|
||||||
|
|
||||||
// New style, we need to setup all the metadata. First thing,
|
|
||||||
// let's get the ID we'll assign, because if we're out of space
|
|
||||||
// we need to fail early.
|
|
||||||
errdefer map.removeByPtr(gop.key_ptr);
|
|
||||||
const id = self.next_id;
|
|
||||||
self.next_id = try std.math.add(Id, self.next_id, 1);
|
|
||||||
errdefer self.next_id -= 1;
|
|
||||||
gop.value_ptr.* = .{ .id = id };
|
|
||||||
|
|
||||||
// Setup our ID mapping
|
|
||||||
var id_map = self.id_map.map(base);
|
|
||||||
const id_gop = try id_map.getOrPut(id);
|
|
||||||
errdefer id_map.removeByPtr(id_gop.key_ptr);
|
|
||||||
assert(!id_gop.found_existing);
|
|
||||||
id_gop.value_ptr.* = size.getOffset(Style, base, gop.key_ptr);
|
|
||||||
return gop.value_ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lookup a style by its unique identifier.
|
|
||||||
pub fn lookupId(self: *const Set, base: anytype, id: Id) ?*Style {
|
|
||||||
const id_map = self.id_map.map(base);
|
|
||||||
const offset = id_map.get(id) orelse return null;
|
|
||||||
return @ptrCast(offset.ptr(base));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a style by its id.
|
|
||||||
pub fn remove(self: *Set, base: anytype, id: Id) void {
|
|
||||||
// Lookup by ID, if it doesn't exist then we return. We use
|
|
||||||
// getEntry so that we can make removal faster later by using
|
|
||||||
// the entry's key pointer.
|
|
||||||
var id_map = self.id_map.map(base);
|
|
||||||
const id_entry = id_map.getEntry(id) orelse return;
|
|
||||||
|
|
||||||
var style_map = self.styles.map(base);
|
|
||||||
const style_ptr: *Style = @ptrCast(id_entry.value_ptr.ptr(base));
|
|
||||||
|
|
||||||
id_map.removeByPtr(id_entry.key_ptr);
|
|
||||||
style_map.removeByPtr(style_ptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the number of styles currently in the set.
|
|
||||||
pub fn count(self: *const Set, base: anytype) usize {
|
|
||||||
return self.id_map.map(base).count();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Metadata about a style. This is used to track the reference count
|
pub const Set = RefCountedSet(
|
||||||
/// and the unique identifier for a style. The unique identifier is used
|
Style,
|
||||||
/// to track the style in the full style map.
|
Id,
|
||||||
pub const Metadata = struct {
|
size.CellCountInt,
|
||||||
ref: size.CellCountInt = 0,
|
StyleSetContext,
|
||||||
id: Id = 0,
|
);
|
||||||
};
|
|
||||||
|
|
||||||
test "Set basic usage" {
|
test "Set basic usage" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
@ -313,29 +270,49 @@ test "Set basic usage" {
|
|||||||
defer alloc.free(buf);
|
defer alloc.free(buf);
|
||||||
|
|
||||||
const style: Style = .{ .flags = .{ .bold = true } };
|
const style: Style = .{ .flags = .{ .bold = true } };
|
||||||
|
const style2: Style = .{ .flags = .{ .italic = true } };
|
||||||
|
|
||||||
var set = Set.init(OffsetBuf.init(buf), layout);
|
var set = Set.init(OffsetBuf.init(buf), layout, StyleSetContext{});
|
||||||
|
|
||||||
// Upsert
|
// Add style
|
||||||
const meta = try set.upsert(buf, style);
|
const id = try set.add(buf, style);
|
||||||
try testing.expect(meta.id > 0);
|
try testing.expect(id > 0);
|
||||||
|
|
||||||
// Second upsert should return the same metadata.
|
// Second add should return the same metadata.
|
||||||
{
|
{
|
||||||
const meta2 = try set.upsert(buf, style);
|
const id2 = try set.add(buf, style);
|
||||||
try testing.expectEqual(meta.id, meta2.id);
|
try testing.expectEqual(id, id2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look it up
|
// Look it up
|
||||||
{
|
{
|
||||||
const v = set.lookupId(buf, meta.id).?;
|
const v = set.get(buf, id);
|
||||||
try testing.expect(v.flags.bold);
|
try testing.expect(v.flags.bold);
|
||||||
|
|
||||||
const v2 = set.lookupId(buf, meta.id).?;
|
const v2 = set.get(buf, id);
|
||||||
try testing.expectEqual(v, v2);
|
try testing.expectEqual(v, v2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removal
|
// Add a second style
|
||||||
set.remove(buf, meta.id);
|
const id2 = try set.add(buf, style2);
|
||||||
try testing.expect(set.lookupId(buf, meta.id) == null);
|
|
||||||
|
// Look it up
|
||||||
|
{
|
||||||
|
const v = set.get(buf, id2);
|
||||||
|
try testing.expect(v.flags.italic);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ref count
|
||||||
|
try testing.expect(set.refCount(buf, id) == 2);
|
||||||
|
try testing.expect(set.refCount(buf, id2) == 1);
|
||||||
|
|
||||||
|
// Release
|
||||||
|
set.release(buf, id);
|
||||||
|
try testing.expect(set.refCount(buf, id) == 1);
|
||||||
|
set.release(buf, id2);
|
||||||
|
try testing.expect(set.refCount(buf, id2) == 0);
|
||||||
|
|
||||||
|
// We added the first one twice, so
|
||||||
|
set.release(buf, id);
|
||||||
|
try testing.expect(set.refCount(buf, id) == 0);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user