mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge pull request #1872 from qwerasd205/various-performance
Various Performance Changes
This commit is contained in:
@ -19,6 +19,17 @@ pub const String = opaque {
|
|||||||
)))) orelse Allocator.Error.OutOfMemory;
|
)))) orelse Allocator.Error.OutOfMemory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn createWithCharactersNoCopy(
|
||||||
|
unichars: []const u16,
|
||||||
|
) *String {
|
||||||
|
return @as(*String, @ptrFromInt(@intFromPtr(c.CFStringCreateWithCharactersNoCopy(
|
||||||
|
null,
|
||||||
|
@ptrCast(unichars.ptr),
|
||||||
|
@intCast(unichars.len),
|
||||||
|
foundation.c.kCFAllocatorNull,
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn release(self: *String) void {
|
pub fn release(self: *String) void {
|
||||||
c.CFRelease(self);
|
c.CFRelease(self);
|
||||||
}
|
}
|
||||||
|
181
src/cache_table.zig
Normal file
181
src/cache_table.zig
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
const fastmem = @import("./fastmem.zig");
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
/// An associative data structure used for efficiently storing and
|
||||||
|
/// retrieving values which are able to be recomputed if necessary.
|
||||||
|
///
|
||||||
|
/// This structure is effectively a hash table with fixed-sized buckets.
|
||||||
|
///
|
||||||
|
/// When inserting an item in to a full bucket, the least recently used
|
||||||
|
/// item is replaced.
|
||||||
|
///
|
||||||
|
/// To achieve this, when an item is accessed, it's moved to the end of
|
||||||
|
/// the bucket, and the rest of the items are moved over to fill the gap.
|
||||||
|
///
|
||||||
|
/// This should provide very good query performance and keep frequently
|
||||||
|
/// accessed items cached indefinitely.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// `Context`
|
||||||
|
/// A type containing methods to define CacheTable behaviors.
|
||||||
|
/// - `fn hash(*Context, K) u64` - Return a hash for a key.
|
||||||
|
/// - `fn eql(*Context, K, K) bool` - Check two keys for equality.
|
||||||
|
///
|
||||||
|
/// - `fn evicted(*Context, K, V) void` - [OPTIONAL] Eviction callback.
|
||||||
|
/// If present, called whenever an item is evicted from the cache.
|
||||||
|
///
|
||||||
|
/// `bucket_count`
|
||||||
|
/// Should ideally be close to the median number of important items that
|
||||||
|
/// you expect to be cached at any given point. This is required to be a
|
||||||
|
/// power of 2 since performance suffers if it's not and there's no good
|
||||||
|
/// reason to allow it to be anything else.
|
||||||
|
///
|
||||||
|
/// `bucket_size`
|
||||||
|
/// should be larger if you expect a large number of unimportant items to
|
||||||
|
/// enter the cache at a time. Having larger buckets will avoid important
|
||||||
|
/// items being dropped from the cache prematurely.
|
||||||
|
///
|
||||||
|
pub fn CacheTable(
|
||||||
|
comptime K: type,
|
||||||
|
comptime V: type,
|
||||||
|
comptime Context: type,
|
||||||
|
comptime bucket_count: usize,
|
||||||
|
comptime bucket_size: u8,
|
||||||
|
) type {
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
const KV = struct {
|
||||||
|
key: K,
|
||||||
|
value: V,
|
||||||
|
};
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
assert(std.math.isPowerOfTwo(bucket_count));
|
||||||
|
assert(bucket_count <= std.math.maxInt(usize));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `bucket_count` buckets containing `bucket_size` KV pairs each.
|
||||||
|
///
|
||||||
|
/// We don't need to initialize this memory because we don't use it
|
||||||
|
/// unless it's within a bucket's stored length, which will guarantee
|
||||||
|
/// that we put actual items there.
|
||||||
|
buckets: [bucket_count][bucket_size]KV = undefined,
|
||||||
|
|
||||||
|
/// We use this array to keep track of how many slots in each bucket
|
||||||
|
/// have actual items in them. Once all the buckets fill up this will
|
||||||
|
/// become a pointless check, but hopefully branch prediction picks
|
||||||
|
/// up on it at that point. The memory cost isn't too bad since it's
|
||||||
|
/// just bytes, so should be a fraction the size of the main table.
|
||||||
|
lengths: [bucket_count]u8 = [_]u8{0} ** bucket_count,
|
||||||
|
|
||||||
|
/// An instance of the context structure.
|
||||||
|
/// Must be initialized before calling any operations.
|
||||||
|
context: Context,
|
||||||
|
|
||||||
|
/// Adds an item to the cache table. If an old value was removed to
|
||||||
|
/// make room then it is returned in a struct with its key and value.
|
||||||
|
pub fn put(self: *Self, key: K, value: V) ?KV {
|
||||||
|
const kv: KV = .{ .key = key, .value = value };
|
||||||
|
const idx: usize = @intCast(self.context.hash(key) % bucket_count);
|
||||||
|
|
||||||
|
// If we have space available in the bucket then we just append
|
||||||
|
if (self.lengths[idx] < bucket_size) {
|
||||||
|
self.buckets[idx][self.lengths[idx]] = kv;
|
||||||
|
self.lengths[idx] += 1;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
assert(self.lengths[idx] == bucket_size);
|
||||||
|
|
||||||
|
// Append our new item and return the oldest
|
||||||
|
const evicted = fastmem.rotateIn(KV, &self.buckets[idx], kv);
|
||||||
|
|
||||||
|
// The Context is allowed to register an eviction hook.
|
||||||
|
if (comptime @hasDecl(Context, "evicted")) self.context.evicted(
|
||||||
|
evicted.key,
|
||||||
|
evicted.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
return evicted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves an item from the cache table.
|
||||||
|
///
|
||||||
|
/// Returns null if no item is found with the provided key.
|
||||||
|
pub fn get(self: *Self, key: K) ?V {
|
||||||
|
const idx: usize = @intCast(self.context.hash(key) % bucket_count);
|
||||||
|
const len = self.lengths[idx];
|
||||||
|
var i: usize = len;
|
||||||
|
while (i > 0) {
|
||||||
|
i -= 1;
|
||||||
|
if (self.context.eql(key, self.buckets[idx][i].key)) {
|
||||||
|
defer fastmem.rotateOnce(KV, self.buckets[idx][i..len]);
|
||||||
|
return self.buckets[idx][i].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes all items from the cache table.
|
||||||
|
///
|
||||||
|
/// If your `Context` has an `evicted` method,
|
||||||
|
/// it will be called with all removed items.
|
||||||
|
pub fn clear(self: *Self) void {
|
||||||
|
if (comptime @hasDecl(Context, "evicted")) {
|
||||||
|
for (self.buckets, self.lengths) |b, l| {
|
||||||
|
for (b[0..l]) |kv| {
|
||||||
|
self.context.evicted(kv.key, kv.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@memset(&self.lengths, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a Context automatically for the given key type. This uses the
|
||||||
|
/// same logic as std.hash_map.AutoContext today since the API matches.
|
||||||
|
fn AutoContext(comptime K: type) type {
|
||||||
|
return std.hash_map.AutoContext(K);
|
||||||
|
}
|
||||||
|
|
||||||
|
test CacheTable {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
// Construct a table that purposely has a predictable hash so we can
|
||||||
|
// test all edge cases.
|
||||||
|
const T = CacheTable(u32, u32, struct {
|
||||||
|
pub fn hash(self: *const @This(), key: u32) u64 {
|
||||||
|
_ = self;
|
||||||
|
return @intCast(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eql(self: *const @This(), a: u32, b: u32) bool {
|
||||||
|
_ = self;
|
||||||
|
return a == b;
|
||||||
|
}
|
||||||
|
}, 2, 2);
|
||||||
|
var t: T = .{ .context = .{} };
|
||||||
|
|
||||||
|
// Fill the table
|
||||||
|
try testing.expect(t.put(0, 0) == null);
|
||||||
|
try testing.expect(t.put(1, 0) == null);
|
||||||
|
try testing.expect(t.put(2, 0) == null);
|
||||||
|
try testing.expect(t.put(3, 0) == null);
|
||||||
|
|
||||||
|
// It should now be full, so any insert should evict the oldest item.
|
||||||
|
// NOTE: For the sake of this test, we're assuming that the first item
|
||||||
|
// is evicted but we don't need to promise this.
|
||||||
|
try testing.expectEqual(T.KV{
|
||||||
|
.key = 0,
|
||||||
|
.value = 0,
|
||||||
|
}, t.put(4, 0).?);
|
||||||
|
|
||||||
|
// The first item should now be gone
|
||||||
|
try testing.expect(t.get(0) == null);
|
||||||
|
}
|
@ -22,13 +22,58 @@ pub inline fn copy(comptime T: type, dest: []T, source: []const T) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Moves the first item to the end.
|
||||||
|
/// For the reverse of this, use `fastmem.rotateOnceR`.
|
||||||
|
///
|
||||||
/// Same as std.mem.rotate(T, items, 1) but more efficient by using memmove
|
/// Same as std.mem.rotate(T, items, 1) but more efficient by using memmove
|
||||||
/// and a tmp var for the single rotated item instead of 3 calls to reverse.
|
/// and a tmp var for the single rotated item instead of 3 calls to reverse.
|
||||||
|
///
|
||||||
|
/// e.g. `0 1 2 3` -> `1 2 3 0`.
|
||||||
pub inline fn rotateOnce(comptime T: type, items: []T) void {
|
pub inline fn rotateOnce(comptime T: type, items: []T) void {
|
||||||
const tmp = items[0];
|
const tmp = items[0];
|
||||||
move(T, items[0 .. items.len - 1], items[1..items.len]);
|
move(T, items[0 .. items.len - 1], items[1..items.len]);
|
||||||
items[items.len - 1] = tmp;
|
items[items.len - 1] = tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Moves the last item to the start.
|
||||||
|
/// Reverse operation of `fastmem.rotateOnce`.
|
||||||
|
///
|
||||||
|
/// Same as std.mem.rotate(T, items, items.len - 1) but more efficient by
|
||||||
|
/// using memmove and a tmp var for the single rotated item instead of 3
|
||||||
|
/// calls to reverse.
|
||||||
|
///
|
||||||
|
/// e.g. `0 1 2 3` -> `3 0 1 2`.
|
||||||
|
pub inline fn rotateOnceR(comptime T: type, items: []T) void {
|
||||||
|
const tmp = items[items.len - 1];
|
||||||
|
move(T, items[1..items.len], items[0 .. items.len - 1]);
|
||||||
|
items[0] = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rotates a new item in to the end of a slice.
|
||||||
|
/// The first item from the slice is removed and returned.
|
||||||
|
///
|
||||||
|
/// e.g. rotating `4` in to `0 1 2 3` makes it `1 2 3 4` and returns `0`.
|
||||||
|
///
|
||||||
|
/// For the reverse of this, use `fastmem.rotateInR`.
|
||||||
|
pub inline fn rotateIn(comptime T: type, items: []T, item: T) T {
|
||||||
|
const removed = items[0];
|
||||||
|
move(T, items[0 .. items.len - 1], items[1..items.len]);
|
||||||
|
items[items.len - 1] = item;
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rotates a new item in to the start of a slice.
|
||||||
|
/// The last item from the slice is removed and returned.
|
||||||
|
///
|
||||||
|
/// e.g. rotating `4` in to `0 1 2 3` makes it `4 0 1 2` and returns `3`.
|
||||||
|
///
|
||||||
|
/// Reverse operation of `fastmem.rotateIn`.
|
||||||
|
pub inline fn rotateInR(comptime T: type, items: []T, item: T) T {
|
||||||
|
const removed = items[items.len - 1];
|
||||||
|
move(T, items[1..items.len], items[0 .. items.len - 1]);
|
||||||
|
items[0] = item;
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
extern "c" fn memcpy(*anyopaque, *const anyopaque, usize) *anyopaque;
|
extern "c" fn memcpy(*anyopaque, *const anyopaque, usize) *anyopaque;
|
||||||
extern "c" fn memmove(*anyopaque, *const anyopaque, usize) *anyopaque;
|
extern "c" fn memmove(*anyopaque, *const anyopaque, usize) *anyopaque;
|
||||||
|
@ -14,55 +14,57 @@ const std = @import("std");
|
|||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const font = @import("../main.zig");
|
const font = @import("../main.zig");
|
||||||
const lru = @import("../../lru.zig");
|
const CacheTable = @import("../../cache_table.zig").CacheTable;
|
||||||
|
|
||||||
const log = std.log.scoped(.font_shaper_cache);
|
const log = std.log.scoped(.font_shaper_cache);
|
||||||
|
|
||||||
/// Our LRU is the run hash to the shaped cells.
|
/// Context for cache table.
|
||||||
const LRU = lru.AutoHashMap(u64, []font.shape.Cell);
|
const CellCacheTableContext = struct {
|
||||||
|
pub fn hash(self: *const CellCacheTableContext, key: u64) u64 {
|
||||||
|
_ = self;
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
pub fn eql(self: *const CellCacheTableContext, a: u64, b: u64) bool {
|
||||||
|
_ = self;
|
||||||
|
return a == b;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/// This is the threshold of evictions at which point we reset
|
/// Cache table for run hash -> shaped cells.
|
||||||
/// the LRU completely. This is a workaround for the issue that
|
const CellCacheTable = CacheTable(
|
||||||
/// Zig stdlib hashmap gets slower over time
|
u64,
|
||||||
/// (https://github.com/ziglang/zig/issues/17851).
|
[]font.shape.Cell,
|
||||||
///
|
CellCacheTableContext,
|
||||||
/// The value is based on naive measuring on my local machine.
|
|
||||||
/// If someone has a better idea of what this value should be,
|
|
||||||
/// please let me know.
|
|
||||||
const evictions_threshold = 8192;
|
|
||||||
|
|
||||||
/// The cache of shaped cells.
|
// Capacity is slightly arbitrary. These numbers are guesses.
|
||||||
map: LRU,
|
//
|
||||||
|
// I'd expect then an average of 256 frequently cached runs is a
|
||||||
|
// safe guess most terminal screens.
|
||||||
|
256,
|
||||||
|
// 8 items per bucket to give decent resilliency to important runs.
|
||||||
|
8,
|
||||||
|
);
|
||||||
|
|
||||||
/// Keep track of the number of evictions. We use this to workaround
|
/// The cache table of shaped cells.
|
||||||
/// the issue that Zig stdlib hashmap gets slower over time
|
map: CellCacheTable,
|
||||||
/// (https://github.com/ziglang/zig/issues/17851). When evictions
|
|
||||||
/// reaches a certain threshold, we reset the LRU.
|
|
||||||
evictions: std.math.IntFittingRange(0, evictions_threshold) = 0,
|
|
||||||
|
|
||||||
pub fn init() Cache {
|
pub fn init() Cache {
|
||||||
// Note: this is very arbitrary. Increasing this number will increase
|
return .{ .map = .{ .context = .{} } };
|
||||||
// the cache hit rate, but also increase the memory usage. We should do
|
|
||||||
// some more empirical testing to see what the best value is.
|
|
||||||
const capacity = 1024;
|
|
||||||
|
|
||||||
return .{ .map = LRU.init(capacity) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Cache, alloc: Allocator) void {
|
pub fn deinit(self: *Cache, alloc: Allocator) void {
|
||||||
var it = self.map.map.iterator();
|
self.clear(alloc);
|
||||||
while (it.next()) |entry| alloc.free(entry.value_ptr.*.data.value);
|
|
||||||
self.map.deinit(alloc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the shaped cells for the given text run or null if they are not
|
/// Get the shaped cells for the given text run,
|
||||||
/// in the cache.
|
/// or null if they are not in the cache.
|
||||||
pub fn get(self: *const Cache, run: font.shape.TextRun) ?[]const font.shape.Cell {
|
pub fn get(self: *Cache, run: font.shape.TextRun) ?[]const font.shape.Cell {
|
||||||
return self.map.get(run.hash);
|
return self.map.get(run.hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert the shaped cells for the given text run into the cache. The
|
/// Insert the shaped cells for the given text run into the cache.
|
||||||
/// cells will be duplicated.
|
///
|
||||||
|
/// The cells will be duplicated.
|
||||||
pub fn put(
|
pub fn put(
|
||||||
self: *Cache,
|
self: *Cache,
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
@ -70,33 +72,19 @@ pub fn put(
|
|||||||
cells: []const font.shape.Cell,
|
cells: []const font.shape.Cell,
|
||||||
) Allocator.Error!void {
|
) Allocator.Error!void {
|
||||||
const copy = try alloc.dupe(font.shape.Cell, cells);
|
const copy = try alloc.dupe(font.shape.Cell, cells);
|
||||||
const gop = try self.map.getOrPut(alloc, run.hash);
|
const evicted = self.map.put(run.hash, copy);
|
||||||
if (gop.evicted) |evicted| {
|
if (evicted) |kv| {
|
||||||
alloc.free(evicted.value);
|
alloc.free(kv.value);
|
||||||
|
|
||||||
// See the doc comment on evictions_threshold for why we do this.
|
|
||||||
self.evictions += 1;
|
|
||||||
if (self.evictions >= evictions_threshold) {
|
|
||||||
log.debug("resetting cache due to too many evictions", .{});
|
|
||||||
// We need to put our value here so deinit can free
|
|
||||||
gop.value_ptr.* = copy;
|
|
||||||
self.clear(alloc);
|
|
||||||
|
|
||||||
// We need to call put again because self is now a
|
|
||||||
// different pointer value so our gop pointers are invalid.
|
|
||||||
return try self.put(alloc, run, cells);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
gop.value_ptr.* = copy;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn count(self: *const Cache) usize {
|
|
||||||
return self.map.map.count();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear(self: *Cache, alloc: Allocator) void {
|
fn clear(self: *Cache, alloc: Allocator) void {
|
||||||
self.deinit(alloc);
|
for (self.map.buckets, self.map.lengths) |b, l| {
|
||||||
self.* = init();
|
for (b[0..l]) |kv| {
|
||||||
|
alloc.free(kv.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.map.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
test Cache {
|
test Cache {
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const macos = @import("macos");
|
const macos = @import("macos");
|
||||||
const trace = @import("tracy").trace;
|
const trace = @import("tracy").trace;
|
||||||
const font = @import("../main.zig");
|
const font = @import("../main.zig");
|
||||||
|
const os = @import("../../os/main.zig");
|
||||||
|
const terminal = @import("../../terminal/main.zig");
|
||||||
const Face = font.Face;
|
const Face = font.Face;
|
||||||
const Collection = font.Collection;
|
const Collection = font.Collection;
|
||||||
const DeferredFace = font.DeferredFace;
|
const DeferredFace = font.DeferredFace;
|
||||||
@ -13,7 +16,7 @@ const Library = font.Library;
|
|||||||
const SharedGrid = font.SharedGrid;
|
const SharedGrid = font.SharedGrid;
|
||||||
const Style = font.Style;
|
const Style = font.Style;
|
||||||
const Presentation = font.Presentation;
|
const Presentation = font.Presentation;
|
||||||
const terminal = @import("../../terminal/main.zig");
|
const CFReleaseThread = os.CFReleaseThread;
|
||||||
|
|
||||||
const log = std.log.scoped(.font_shaper);
|
const log = std.log.scoped(.font_shaper);
|
||||||
|
|
||||||
@ -31,7 +34,7 @@ const log = std.log.scoped(.font_shaper);
|
|||||||
/// See: https://github.com/mitchellh/ghostty/issues/1643
|
/// See: https://github.com/mitchellh/ghostty/issues/1643
|
||||||
///
|
///
|
||||||
pub const Shaper = struct {
|
pub const Shaper = struct {
|
||||||
/// The allocated used for the feature list and cell buf.
|
/// The allocated used for the feature list, font cache, and cell buf.
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
|
|
||||||
/// The string used for shaping the current run.
|
/// The string used for shaping the current run.
|
||||||
@ -49,6 +52,24 @@ pub const Shaper = struct {
|
|||||||
/// and releasing many objects when shaping.
|
/// and releasing many objects when shaping.
|
||||||
writing_direction: *macos.foundation.Array,
|
writing_direction: *macos.foundation.Array,
|
||||||
|
|
||||||
|
/// List where we cache fonts, so we don't have to remake them for
|
||||||
|
/// every single shaping operation.
|
||||||
|
///
|
||||||
|
/// Fonts are cached as attribute dictionaries to be applied directly to
|
||||||
|
/// attributed strings.
|
||||||
|
cached_fonts: std.ArrayListUnmanaged(?*macos.foundation.Dictionary),
|
||||||
|
|
||||||
|
/// The list of CoreFoundation objects to release on the dedicated
|
||||||
|
/// release thread. This is built up over the course of shaping and
|
||||||
|
/// sent to the release thread when endFrame is called.
|
||||||
|
cf_release_pool: std.ArrayListUnmanaged(*anyopaque),
|
||||||
|
|
||||||
|
/// Dedicated thread for releasing CoreFoundation objects. Some objects,
|
||||||
|
/// such as those produced by CoreText, have excessively slow release
|
||||||
|
/// callback logic.
|
||||||
|
cf_release_thread: *CFReleaseThread,
|
||||||
|
cf_release_thr: std.Thread,
|
||||||
|
|
||||||
const CellBuf = std.ArrayListUnmanaged(font.shape.Cell);
|
const CellBuf = std.ArrayListUnmanaged(font.shape.Cell);
|
||||||
const CodepointList = std.ArrayListUnmanaged(Codepoint);
|
const CodepointList = std.ArrayListUnmanaged(Codepoint);
|
||||||
const Codepoint = struct {
|
const Codepoint = struct {
|
||||||
@ -57,24 +78,21 @@ pub const Shaper = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RunState = struct {
|
const RunState = struct {
|
||||||
str: *macos.foundation.MutableString,
|
|
||||||
codepoints: CodepointList,
|
codepoints: CodepointList,
|
||||||
|
unichars: std.ArrayListUnmanaged(u16),
|
||||||
|
|
||||||
fn init() !RunState {
|
fn init() RunState {
|
||||||
var str = try macos.foundation.MutableString.create(0);
|
return .{ .codepoints = .{}, .unichars = .{} };
|
||||||
errdefer str.release();
|
|
||||||
return .{ .str = str, .codepoints = .{} };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deinit(self: *RunState, alloc: Allocator) void {
|
fn deinit(self: *RunState, alloc: Allocator) void {
|
||||||
self.codepoints.deinit(alloc);
|
self.codepoints.deinit(alloc);
|
||||||
self.str.release();
|
self.unichars.deinit(alloc);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset(self: *RunState) !void {
|
fn reset(self: *RunState) !void {
|
||||||
self.codepoints.clearRetainingCapacity();
|
self.codepoints.clearRetainingCapacity();
|
||||||
self.str.release();
|
self.unichars.clearRetainingCapacity();
|
||||||
self.str = try macos.foundation.MutableString.create(0);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -177,7 +195,7 @@ pub const Shaper = struct {
|
|||||||
for (hardcoded_features) |name| try feats.append(name);
|
for (hardcoded_features) |name| try feats.append(name);
|
||||||
for (opts.features) |name| try feats.append(name);
|
for (opts.features) |name| try feats.append(name);
|
||||||
|
|
||||||
var run_state = try RunState.init();
|
var run_state = RunState.init();
|
||||||
errdefer run_state.deinit(alloc);
|
errdefer run_state.deinit(alloc);
|
||||||
|
|
||||||
// For now we only support LTR text. If we shape RTL text then
|
// For now we only support LTR text. If we shape RTL text then
|
||||||
@ -202,12 +220,30 @@ pub const Shaper = struct {
|
|||||||
};
|
};
|
||||||
errdefer writing_direction.release();
|
errdefer writing_direction.release();
|
||||||
|
|
||||||
return Shaper{
|
// Create the CF release thread.
|
||||||
|
var cf_release_thread = try alloc.create(CFReleaseThread);
|
||||||
|
errdefer alloc.destroy(cf_release_thread);
|
||||||
|
cf_release_thread.* = try CFReleaseThread.init(alloc);
|
||||||
|
errdefer cf_release_thread.deinit();
|
||||||
|
|
||||||
|
// Start the CF release thread.
|
||||||
|
var cf_release_thr = try std.Thread.spawn(
|
||||||
|
.{},
|
||||||
|
CFReleaseThread.threadMain,
|
||||||
|
.{cf_release_thread},
|
||||||
|
);
|
||||||
|
cf_release_thr.setName("cf_release") catch {};
|
||||||
|
|
||||||
|
return .{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
.cell_buf = .{},
|
.cell_buf = .{},
|
||||||
.run_state = run_state,
|
.run_state = run_state,
|
||||||
.features = feats,
|
.features = feats,
|
||||||
.writing_direction = writing_direction,
|
.writing_direction = writing_direction,
|
||||||
|
.cached_fonts = .{},
|
||||||
|
.cf_release_pool = .{},
|
||||||
|
.cf_release_thread = cf_release_thread,
|
||||||
|
.cf_release_thr = cf_release_thr,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,6 +252,67 @@ pub const Shaper = struct {
|
|||||||
self.run_state.deinit(self.alloc);
|
self.run_state.deinit(self.alloc);
|
||||||
self.features.deinit();
|
self.features.deinit();
|
||||||
self.writing_direction.release();
|
self.writing_direction.release();
|
||||||
|
|
||||||
|
{
|
||||||
|
for (self.cached_fonts.items) |ft| {
|
||||||
|
if (ft) |f| f.release();
|
||||||
|
}
|
||||||
|
self.cached_fonts.deinit(self.alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.cf_release_pool.items.len > 0) {
|
||||||
|
for (self.cf_release_pool.items) |ref| macos.foundation.CFRelease(ref);
|
||||||
|
|
||||||
|
// For tests this logic is normal because we don't want to
|
||||||
|
// wait for a release thread. But in production this is a bug
|
||||||
|
// and we should warn.
|
||||||
|
if (comptime !builtin.is_test) log.warn(
|
||||||
|
"BUG: CFRelease pool was not empty, releasing remaining objects",
|
||||||
|
.{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.cf_release_pool.deinit(self.alloc);
|
||||||
|
|
||||||
|
// Stop the CF release thread
|
||||||
|
{
|
||||||
|
self.cf_release_thread.stop.notify() catch |err|
|
||||||
|
log.err("error notifying cf release thread to stop, may stall err={}", .{err});
|
||||||
|
self.cf_release_thr.join();
|
||||||
|
}
|
||||||
|
self.cf_release_thread.deinit();
|
||||||
|
self.alloc.destroy(self.cf_release_thread);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn endFrame(self: *Shaper) void {
|
||||||
|
if (self.cf_release_pool.items.len == 0) return;
|
||||||
|
|
||||||
|
// Get all the items in the pool as an owned slice so we can
|
||||||
|
// send it to the dedicated release thread.
|
||||||
|
const items = self.cf_release_pool.toOwnedSlice(self.alloc) catch |err| {
|
||||||
|
log.warn("error converting release pool to owned slice, slow release err={}", .{err});
|
||||||
|
for (self.cf_release_pool.items) |ref| macos.foundation.CFRelease(ref);
|
||||||
|
self.cf_release_pool.clearRetainingCapacity();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send the items. If the send succeeds then we wake up the
|
||||||
|
// thread to process the items. If the send fails then do a manual
|
||||||
|
// cleanup.
|
||||||
|
if (self.cf_release_thread.mailbox.push(.{ .release = .{
|
||||||
|
.refs = items,
|
||||||
|
.alloc = self.alloc,
|
||||||
|
} }, .{ .forever = {} }) != 0) {
|
||||||
|
self.cf_release_thread.wakeup.notify() catch |err| {
|
||||||
|
log.warn(
|
||||||
|
"error notifying cf release thread to wake up, may stall err={}",
|
||||||
|
.{err},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (items) |ref| macos.foundation.CFRelease(ref);
|
||||||
|
self.alloc.free(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runIterator(
|
pub fn runIterator(
|
||||||
@ -236,7 +333,13 @@ pub const Shaper = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shape(self: *Shaper, run: font.shape.TextRun) ![]const font.shape.Cell {
|
/// Note that this will accumulate garbage in the release pool. The
|
||||||
|
/// caller must ensure you're properly calling endFrame to release
|
||||||
|
/// all the objects.
|
||||||
|
pub fn shape(
|
||||||
|
self: *Shaper,
|
||||||
|
run: font.shape.TextRun,
|
||||||
|
) ![]const font.shape.Cell {
|
||||||
const state = &self.run_state;
|
const state = &self.run_state;
|
||||||
|
|
||||||
// {
|
// {
|
||||||
@ -267,66 +370,27 @@ pub const Shaper = struct {
|
|||||||
defer arena.deinit();
|
defer arena.deinit();
|
||||||
const alloc = arena.allocator();
|
const alloc = arena.allocator();
|
||||||
|
|
||||||
// Get our font. We have to apply the font features we want for
|
const attr_dict: *macos.foundation.Dictionary = try self.getFont(
|
||||||
// the font here.
|
run.grid,
|
||||||
const run_font: *macos.text.Font = font: {
|
run.font_index,
|
||||||
// The CoreText shaper relies on CoreText and CoreText claims
|
);
|
||||||
// that CTFonts are threadsafe. See:
|
|
||||||
// https://developer.apple.com/documentation/coretext/
|
|
||||||
//
|
|
||||||
// Quote:
|
|
||||||
// All individual functions in Core Text are thread-safe. Font
|
|
||||||
// objects (CTFont, CTFontDescriptor, and associated objects) can
|
|
||||||
// be used simultaneously by multiple operations, work queues, or
|
|
||||||
// threads. However, the layout objects (CTTypesetter,
|
|
||||||
// CTFramesetter, CTRun, CTLine, CTFrame, and associated objects)
|
|
||||||
// should be used in a single operation, work queue, or thread.
|
|
||||||
//
|
|
||||||
// Because of this, we only acquire the read lock to grab the
|
|
||||||
// face and set it up, then release it.
|
|
||||||
run.grid.lock.lockShared();
|
|
||||||
defer run.grid.lock.unlockShared();
|
|
||||||
|
|
||||||
const face = try run.grid.resolver.collection.getFace(run.font_index);
|
// Make room for the attributed string and the CTLine.
|
||||||
const original = face.font;
|
try self.cf_release_pool.ensureUnusedCapacity(self.alloc, 3);
|
||||||
|
|
||||||
const attrs = try self.features.attrsDict(face.quirks_disable_default_font_features);
|
const str = macos.foundation.String.createWithCharactersNoCopy(state.unichars.items);
|
||||||
defer attrs.release();
|
self.cf_release_pool.appendAssumeCapacity(str);
|
||||||
|
|
||||||
const desc = try macos.text.FontDescriptor.createWithAttributes(attrs);
|
|
||||||
defer desc.release();
|
|
||||||
|
|
||||||
const copied = try original.copyWithAttributes(0, null, desc);
|
|
||||||
errdefer copied.release();
|
|
||||||
break :font copied;
|
|
||||||
};
|
|
||||||
defer run_font.release();
|
|
||||||
|
|
||||||
// Get our font and use that get the attributes to set for the
|
|
||||||
// attributed string so the whole string uses the same font.
|
|
||||||
const attr_dict = dict: {
|
|
||||||
var keys = [_]?*const anyopaque{
|
|
||||||
macos.text.StringAttribute.font.key(),
|
|
||||||
macos.text.StringAttribute.writing_direction.key(),
|
|
||||||
};
|
|
||||||
var values = [_]?*const anyopaque{
|
|
||||||
run_font,
|
|
||||||
self.writing_direction,
|
|
||||||
};
|
|
||||||
break :dict try macos.foundation.Dictionary.create(&keys, &values);
|
|
||||||
};
|
|
||||||
defer attr_dict.release();
|
|
||||||
|
|
||||||
// Create an attributed string from our string
|
// Create an attributed string from our string
|
||||||
const attr_str = try macos.foundation.AttributedString.create(
|
const attr_str = try macos.foundation.AttributedString.create(
|
||||||
state.str.string(),
|
str,
|
||||||
attr_dict,
|
attr_dict,
|
||||||
);
|
);
|
||||||
defer attr_str.release();
|
self.cf_release_pool.appendAssumeCapacity(attr_str);
|
||||||
|
|
||||||
// We should always have one run because we do our own run splitting.
|
// We should always have one run because we do our own run splitting.
|
||||||
const line = try macos.text.Line.createWithAttributedString(attr_str);
|
const line = try macos.text.Line.createWithAttributedString(attr_str);
|
||||||
defer line.release();
|
self.cf_release_pool.appendAssumeCapacity(line);
|
||||||
|
|
||||||
// This keeps track of the current offsets within a single cell.
|
// This keeps track of the current offsets within a single cell.
|
||||||
var cell_offset: struct {
|
var cell_offset: struct {
|
||||||
@ -416,6 +480,83 @@ pub const Shaper = struct {
|
|||||||
return self.cell_buf.items;
|
return self.cell_buf.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get an attr dict for a font from a specific index.
|
||||||
|
/// These items are cached, do not retain or release them.
|
||||||
|
fn getFont(
|
||||||
|
self: *Shaper,
|
||||||
|
grid: *font.SharedGrid,
|
||||||
|
index: font.Collection.Index,
|
||||||
|
) !*macos.foundation.Dictionary {
|
||||||
|
const index_int = index.int();
|
||||||
|
|
||||||
|
// The cached fonts are indexed directly by the font index, since
|
||||||
|
// this number is usually low. Therefore, we set any index we haven't
|
||||||
|
// seen to null.
|
||||||
|
if (self.cached_fonts.items.len <= index_int) {
|
||||||
|
try self.cached_fonts.ensureTotalCapacity(self.alloc, index_int + 1);
|
||||||
|
while (self.cached_fonts.items.len <= index_int) {
|
||||||
|
self.cached_fonts.appendAssumeCapacity(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have it, return the cached attr dict.
|
||||||
|
if (self.cached_fonts.items[index_int]) |cached| return cached;
|
||||||
|
|
||||||
|
// Features dictionary, font descriptor, font
|
||||||
|
try self.cf_release_pool.ensureUnusedCapacity(self.alloc, 3);
|
||||||
|
|
||||||
|
const run_font = font: {
|
||||||
|
// The CoreText shaper relies on CoreText and CoreText claims
|
||||||
|
// that CTFonts are threadsafe. See:
|
||||||
|
// https://developer.apple.com/documentation/coretext/
|
||||||
|
//
|
||||||
|
// Quote:
|
||||||
|
// All individual functions in Core Text are thread-safe. Font
|
||||||
|
// objects (CTFont, CTFontDescriptor, and associated objects) can
|
||||||
|
// be used simultaneously by multiple operations, work queues, or
|
||||||
|
// threads. However, the layout objects (CTTypesetter,
|
||||||
|
// CTFramesetter, CTRun, CTLine, CTFrame, and associated objects)
|
||||||
|
// should be used in a single operation, work queue, or thread.
|
||||||
|
//
|
||||||
|
// Because of this, we only acquire the read lock to grab the
|
||||||
|
// face and set it up, then release it.
|
||||||
|
grid.lock.lockShared();
|
||||||
|
defer grid.lock.unlockShared();
|
||||||
|
|
||||||
|
const face = try grid.resolver.collection.getFace(index);
|
||||||
|
const original = face.font;
|
||||||
|
|
||||||
|
const attrs = try self.features.attrsDict(face.quirks_disable_default_font_features);
|
||||||
|
self.cf_release_pool.appendAssumeCapacity(attrs);
|
||||||
|
|
||||||
|
const desc = try macos.text.FontDescriptor.createWithAttributes(attrs);
|
||||||
|
self.cf_release_pool.appendAssumeCapacity(desc);
|
||||||
|
|
||||||
|
const copied = try original.copyWithAttributes(0, null, desc);
|
||||||
|
errdefer copied.release();
|
||||||
|
|
||||||
|
break :font copied;
|
||||||
|
};
|
||||||
|
self.cf_release_pool.appendAssumeCapacity(run_font);
|
||||||
|
|
||||||
|
// Get our font and use that get the attributes to set for the
|
||||||
|
// attributed string so the whole string uses the same font.
|
||||||
|
const attr_dict = dict: {
|
||||||
|
var keys = [_]?*const anyopaque{
|
||||||
|
macos.text.StringAttribute.font.key(),
|
||||||
|
macos.text.StringAttribute.writing_direction.key(),
|
||||||
|
};
|
||||||
|
var values = [_]?*const anyopaque{
|
||||||
|
run_font,
|
||||||
|
self.writing_direction,
|
||||||
|
};
|
||||||
|
break :dict try macos.foundation.Dictionary.create(&keys, &values);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.cached_fonts.items[index_int] = attr_dict;
|
||||||
|
return attr_dict;
|
||||||
|
}
|
||||||
|
|
||||||
/// The hooks for RunIterator.
|
/// The hooks for RunIterator.
|
||||||
pub const RunIteratorHook = struct {
|
pub const RunIteratorHook = struct {
|
||||||
shaper: *Shaper,
|
shaper: *Shaper,
|
||||||
@ -426,15 +567,20 @@ pub const Shaper = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn addCodepoint(self: RunIteratorHook, cp: u32, cluster: u32) !void {
|
pub fn addCodepoint(self: RunIteratorHook, cp: u32, cluster: u32) !void {
|
||||||
|
const state = &self.shaper.run_state;
|
||||||
|
|
||||||
// Build our UTF-16 string for CoreText
|
// Build our UTF-16 string for CoreText
|
||||||
var unichars: [2]u16 = undefined;
|
try state.unichars.ensureUnusedCapacity(self.shaper.alloc, 2);
|
||||||
|
|
||||||
|
state.unichars.appendNTimesAssumeCapacity(0, 2);
|
||||||
|
|
||||||
const pair = macos.foundation.stringGetSurrogatePairForLongCharacter(
|
const pair = macos.foundation.stringGetSurrogatePairForLongCharacter(
|
||||||
cp,
|
cp,
|
||||||
&unichars,
|
state.unichars.items[state.unichars.items.len - 2 ..][0..2],
|
||||||
);
|
);
|
||||||
const len: usize = if (pair) 2 else 1;
|
if (!pair) {
|
||||||
const state = &self.shaper.run_state;
|
state.unichars.items.len -= 1;
|
||||||
state.str.appendCharacters(unichars[0..len]);
|
}
|
||||||
|
|
||||||
// Build our reverse lookup table for codepoints to clusters
|
// Build our reverse lookup table for codepoints to clusters
|
||||||
try state.codepoints.append(self.shaper.alloc, .{
|
try state.codepoints.append(self.shaper.alloc, .{
|
||||||
|
@ -74,6 +74,10 @@ pub const Shaper = struct {
|
|||||||
self.hb_feats.deinit(self.alloc);
|
self.hb_feats.deinit(self.alloc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn endFrame(self: *const Shaper) void {
|
||||||
|
_ = self;
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns an iterator that returns one text run at a time for the
|
/// Returns an iterator that returns one text run at a time for the
|
||||||
/// given terminal row. Note that text runs are are only valid one at a time
|
/// given terminal row. Note that text runs are are only valid one at a time
|
||||||
/// for a Shaper struct since they share state.
|
/// for a Shaper struct since they share state.
|
||||||
|
@ -64,6 +64,10 @@ pub const Shaper = struct {
|
|||||||
self.run_state.deinit(self.alloc);
|
self.run_state.deinit(self.alloc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn endFrame(self: *const Shaper) void {
|
||||||
|
_ = self;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn runIterator(
|
pub fn runIterator(
|
||||||
self: *Shaper,
|
self: *Shaper,
|
||||||
grid: *SharedGrid,
|
grid: *SharedGrid,
|
||||||
|
@ -52,6 +52,10 @@ pub const Shaper = struct {
|
|||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn endFrame(self: *const Shaper) void {
|
||||||
|
_ = self;
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns an iterator that returns one text run at a time for the
|
/// Returns an iterator that returns one text run at a time for the
|
||||||
/// given terminal row. Note that text runs are are only valid one at a time
|
/// given terminal row. Note that text runs are are only valid one at a time
|
||||||
/// for a Shaper struct since they share state.
|
/// for a Shaper struct since they share state.
|
||||||
|
@ -319,6 +319,7 @@ test {
|
|||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
_ = @import("blocking_queue.zig");
|
_ = @import("blocking_queue.zig");
|
||||||
|
_ = @import("cache_table.zig");
|
||||||
_ = @import("config.zig");
|
_ = @import("config.zig");
|
||||||
_ = @import("lru.zig");
|
_ = @import("lru.zig");
|
||||||
}
|
}
|
||||||
|
183
src/os/cf_release_thread.zig
Normal file
183
src/os/cf_release_thread.zig
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
//! Represents the CFRelease thread. Pools of CFTypeRefs are sent to
|
||||||
|
//! this thread to be released, so that their release callback logic
|
||||||
|
//! doesn't block the execution of a high throughput thread like the
|
||||||
|
//! renderer thread.
|
||||||
|
pub const Thread = @This();
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const xev = @import("xev");
|
||||||
|
const macos = @import("macos");
|
||||||
|
|
||||||
|
const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const log = std.log.scoped(.cf_release_thread);
|
||||||
|
|
||||||
|
pub const Message = union(enum) {
|
||||||
|
/// Release a slice of CFTypeRefs. Uses alloc to free the slice after
|
||||||
|
/// releasing all the refs.
|
||||||
|
release: struct {
|
||||||
|
refs: []*anyopaque,
|
||||||
|
alloc: Allocator,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The type used for sending messages to the thread. For now this is
|
||||||
|
/// hardcoded with a capacity. We can make this a comptime parameter in
|
||||||
|
/// the future if we want it configurable.
|
||||||
|
pub const Mailbox = BlockingQueue(Message, 64);
|
||||||
|
|
||||||
|
/// Allocator used for some state
|
||||||
|
alloc: std.mem.Allocator,
|
||||||
|
|
||||||
|
/// The main event loop for the thread. The user data of this loop
|
||||||
|
/// is always the allocator used to create the loop. This is a convenience
|
||||||
|
/// so that users of the loop always have an allocator.
|
||||||
|
loop: xev.Loop,
|
||||||
|
|
||||||
|
/// This can be used to wake up the thread.
|
||||||
|
wakeup: xev.Async,
|
||||||
|
wakeup_c: xev.Completion = .{},
|
||||||
|
|
||||||
|
/// This can be used to stop the thread on the next loop iteration.
|
||||||
|
stop: xev.Async,
|
||||||
|
stop_c: xev.Completion = .{},
|
||||||
|
|
||||||
|
/// The mailbox that can be used to send this thread messages. Note
|
||||||
|
/// this is a blocking queue so if it is full you will get errors (or block).
|
||||||
|
mailbox: *Mailbox,
|
||||||
|
|
||||||
|
flags: packed struct {
|
||||||
|
/// This is set to true only when an abnormal exit is detected. It
|
||||||
|
/// tells our mailbox system to drain and ignore all messages.
|
||||||
|
drain: bool = false,
|
||||||
|
} = .{},
|
||||||
|
|
||||||
|
/// Initialize the thread. This does not START the thread. This only sets
|
||||||
|
/// up all the internal state necessary prior to starting the thread. It
|
||||||
|
/// is up to the caller to start the thread with the threadMain entrypoint.
|
||||||
|
pub fn init(
|
||||||
|
alloc: Allocator,
|
||||||
|
) !Thread {
|
||||||
|
// Create our event loop.
|
||||||
|
var loop = try xev.Loop.init(.{});
|
||||||
|
errdefer loop.deinit();
|
||||||
|
|
||||||
|
// This async handle is used to "wake up" the thread to collect objects.
|
||||||
|
var wakeup_h = try xev.Async.init();
|
||||||
|
errdefer wakeup_h.deinit();
|
||||||
|
|
||||||
|
// This async handle is used to stop the loop and force the thread to end.
|
||||||
|
var stop_h = try xev.Async.init();
|
||||||
|
errdefer stop_h.deinit();
|
||||||
|
|
||||||
|
// The mailbox for messaging this thread
|
||||||
|
var mailbox = try Mailbox.create(alloc);
|
||||||
|
errdefer mailbox.destroy(alloc);
|
||||||
|
|
||||||
|
return Thread{
|
||||||
|
.alloc = alloc,
|
||||||
|
.loop = loop,
|
||||||
|
.wakeup = wakeup_h,
|
||||||
|
.stop = stop_h,
|
||||||
|
.mailbox = mailbox,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clean up the thread. This is only safe to call once the thread
|
||||||
|
/// completes executing; the caller must join prior to this.
|
||||||
|
pub fn deinit(self: *Thread) void {
|
||||||
|
self.stop.deinit();
|
||||||
|
self.wakeup.deinit();
|
||||||
|
self.loop.deinit();
|
||||||
|
|
||||||
|
// Nothing can possibly access the mailbox anymore, destroy it.
|
||||||
|
self.mailbox.destroy(self.alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The main entrypoint for the thread.
|
||||||
|
pub fn threadMain(self: *Thread) void {
|
||||||
|
// Call child function so we can use errors...
|
||||||
|
self.threadMain_() catch |err| {
|
||||||
|
log.warn("error in cf release thread err={}", .{err});
|
||||||
|
};
|
||||||
|
|
||||||
|
// If our loop is not stopped, then we need to keep running so that
|
||||||
|
// messages are drained and we can wait for the surface to send a stop
|
||||||
|
// message.
|
||||||
|
if (!self.loop.flags.stopped) {
|
||||||
|
log.warn("abrupt cf release thread exit detected, starting xev to drain mailbox", .{});
|
||||||
|
defer log.debug("cf release thread fully exiting after abnormal failure", .{});
|
||||||
|
self.flags.drain = true;
|
||||||
|
self.loop.run(.until_done) catch |err| {
|
||||||
|
log.err("failed to start xev loop for draining err={}", .{err});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn threadMain_(self: *Thread) !void {
|
||||||
|
defer log.debug("cf release thread exited", .{});
|
||||||
|
|
||||||
|
// Start the async handlers. We start these first so that they're
|
||||||
|
// registered even if anything below fails so we can drain the mailbox.
|
||||||
|
self.wakeup.wait(&self.loop, &self.wakeup_c, Thread, self, wakeupCallback);
|
||||||
|
self.stop.wait(&self.loop, &self.stop_c, Thread, self, stopCallback);
|
||||||
|
|
||||||
|
// Run
|
||||||
|
log.debug("starting cf release thread", .{});
|
||||||
|
defer log.debug("starting cf release thread shutdown", .{});
|
||||||
|
try self.loop.run(.until_done);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drain the mailbox, handling all the messages in our terminal implementation.
|
||||||
|
fn drainMailbox(self: *Thread) !void {
|
||||||
|
// If we're draining, we just drain the mailbox and return.
|
||||||
|
if (self.flags.drain) {
|
||||||
|
while (self.mailbox.pop()) |_| {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (self.mailbox.pop()) |message| {
|
||||||
|
// log.debug("mailbox message={}", .{message});
|
||||||
|
switch (message) {
|
||||||
|
.release => |msg| {
|
||||||
|
for (msg.refs) |ref| macos.foundation.CFRelease(ref);
|
||||||
|
// log.debug("Released {} CFTypeRefs.", .{ msg.refs.len });
|
||||||
|
msg.alloc.free(msg.refs);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wakeupCallback(
|
||||||
|
self_: ?*Thread,
|
||||||
|
_: *xev.Loop,
|
||||||
|
_: *xev.Completion,
|
||||||
|
r: xev.Async.WaitError!void,
|
||||||
|
) xev.CallbackAction {
|
||||||
|
_ = r catch |err| {
|
||||||
|
log.err("error in wakeup err={}", .{err});
|
||||||
|
return .rearm;
|
||||||
|
};
|
||||||
|
|
||||||
|
const t = self_.?;
|
||||||
|
|
||||||
|
// When we wake up, we check the mailbox. Mailbox producers should
|
||||||
|
// wake up our thread after publishing.
|
||||||
|
t.drainMailbox() catch |err|
|
||||||
|
log.err("error draining mailbox err={}", .{err});
|
||||||
|
|
||||||
|
return .rearm;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stopCallback(
|
||||||
|
self_: ?*Thread,
|
||||||
|
_: *xev.Loop,
|
||||||
|
_: *xev.Completion,
|
||||||
|
r: xev.Async.WaitError!void,
|
||||||
|
) xev.CallbackAction {
|
||||||
|
_ = r catch unreachable;
|
||||||
|
self_.?.loop.stop();
|
||||||
|
return .disarm;
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
//! The "os" package contains utilities for interfacing with the operating
|
//! The "os" package contains utilities for interfacing with the operating
|
||||||
//! system.
|
//! system. These aren't restricted to syscalls or low-level operations, but
|
||||||
|
//! also OS-specific features and conventions.
|
||||||
|
|
||||||
pub usingnamespace @import("env.zig");
|
pub usingnamespace @import("env.zig");
|
||||||
pub usingnamespace @import("desktop.zig");
|
pub usingnamespace @import("desktop.zig");
|
||||||
@ -12,6 +13,7 @@ pub usingnamespace @import("mouse.zig");
|
|||||||
pub usingnamespace @import("open.zig");
|
pub usingnamespace @import("open.zig");
|
||||||
pub usingnamespace @import("pipe.zig");
|
pub usingnamespace @import("pipe.zig");
|
||||||
pub usingnamespace @import("resourcesdir.zig");
|
pub usingnamespace @import("resourcesdir.zig");
|
||||||
|
pub const CFReleaseThread = @import("cf_release_thread.zig");
|
||||||
pub const TempDir = @import("TempDir.zig");
|
pub const TempDir = @import("TempDir.zig");
|
||||||
pub const cgroup = @import("cgroup.zig");
|
pub const cgroup = @import("cgroup.zig");
|
||||||
pub const passwd = @import("passwd.zig");
|
pub const passwd = @import("passwd.zig");
|
||||||
|
@ -15,6 +15,7 @@ const xev = @import("xev");
|
|||||||
const apprt = @import("../apprt.zig");
|
const apprt = @import("../apprt.zig");
|
||||||
const configpkg = @import("../config.zig");
|
const configpkg = @import("../config.zig");
|
||||||
const font = @import("../font/main.zig");
|
const font = @import("../font/main.zig");
|
||||||
|
const os = @import("../os/main.zig");
|
||||||
const terminal = @import("../terminal/main.zig");
|
const terminal = @import("../terminal/main.zig");
|
||||||
const renderer = @import("../renderer.zig");
|
const renderer = @import("../renderer.zig");
|
||||||
const math = @import("../math.zig");
|
const math = @import("../math.zig");
|
||||||
@ -25,6 +26,7 @@ const shadertoy = @import("shadertoy.zig");
|
|||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
const CFReleaseThread = os.CFReleaseThread;
|
||||||
const Terminal = terminal.Terminal;
|
const Terminal = terminal.Terminal;
|
||||||
const Health = renderer.Health;
|
const Health = renderer.Health;
|
||||||
|
|
||||||
@ -690,7 +692,7 @@ pub fn loopEnter(self: *Metal, thr: *renderer.Thread) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Called by renderer.Thread when it exits the main loop.
|
/// Called by renderer.Thread when it exits the main loop.
|
||||||
pub fn loopExit(self: *const Metal) void {
|
pub fn loopExit(self: *Metal) void {
|
||||||
// If we don't support a display link we have no work to do.
|
// If we don't support a display link we have no work to do.
|
||||||
if (comptime DisplayLink == void) return;
|
if (comptime DisplayLink == void) return;
|
||||||
|
|
||||||
@ -996,6 +998,10 @@ pub fn updateFrame(
|
|||||||
&critical.color_palette,
|
&critical.color_palette,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Notify our shaper we're done for the frame. For some shapers like
|
||||||
|
// CoreText this triggers off-thread cleanup logic.
|
||||||
|
self.font_shaper.endFrame();
|
||||||
|
|
||||||
// Update our viewport pin
|
// Update our viewport pin
|
||||||
self.cells_viewport = critical.viewport_pin;
|
self.cells_viewport = critical.viewport_pin;
|
||||||
|
|
||||||
@ -1877,9 +1883,11 @@ fn rebuildCells(
|
|||||||
color_palette: *const terminal.color.Palette,
|
color_palette: *const terminal.color.Palette,
|
||||||
) !void {
|
) !void {
|
||||||
// const start = try std.time.Instant.now();
|
// const start = try std.time.Instant.now();
|
||||||
|
// const start_micro = std.time.microTimestamp();
|
||||||
// defer {
|
// defer {
|
||||||
// const end = std.time.Instant.now() catch unreachable;
|
// const end = std.time.Instant.now() catch unreachable;
|
||||||
// std.log.warn("rebuildCells time={}us", .{end.since(start) / std.time.ns_per_us});
|
// // "[rebuildCells time] <START us>\t<TIME_TAKEN us>"
|
||||||
|
// std.log.warn("[rebuildCells time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us});
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// Create an arena for all our temporary allocations while rebuilding
|
// Create an arena for all our temporary allocations while rebuilding
|
||||||
|
@ -732,6 +732,10 @@ pub fn updateFrame(
|
|||||||
critical.cursor_style,
|
critical.cursor_style,
|
||||||
&critical.color_palette,
|
&critical.color_palette,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Notify our shaper we're done for the frame. For some shapers like
|
||||||
|
// CoreText this triggers off-thread cleanup logic.
|
||||||
|
self.font_shaper.endFrame();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
.{},
|
||||||
),
|
),
|
||||||
.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,
|
||||||
|
552
src/terminal/ref_counted_set.zig
Normal file
552
src/terminal/ref_counted_set.zig
Normal file
@ -0,0 +1,552 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
const size = @import("size.zig");
|
||||||
|
const Offset = size.Offset;
|
||||||
|
const OffsetBuf = size.OffsetBuf;
|
||||||
|
|
||||||
|
const fastmem = @import("../fastmem.zig");
|
||||||
|
|
||||||
|
/// 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 IdT: type,
|
||||||
|
comptime RefCountInt: type,
|
||||||
|
comptime ContextT: type,
|
||||||
|
) type {
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-export these types so they can be referenced by the caller.
|
||||||
|
pub const Id = IdT;
|
||||||
|
pub const Context = ContextT;
|
||||||
|
|
||||||
|
/// 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 ref count.
|
||||||
|
///
|
||||||
|
/// Returns the item's ID.
|
||||||
|
///
|
||||||
|
/// If the set has no more room, then an OutOfMemory error is returned.
|
||||||
|
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
|
||||||
|
/// ref 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.
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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: usize = @intCast((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. 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) 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,22 @@ pub const Style = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A set of styles.
|
pub const Set = RefCountedSet(
|
||||||
///
|
Style,
|
||||||
/// This set is created with some capacity in mind. You can determine
|
Id,
|
||||||
/// the exact memory requirement for a capacity by calling `layout`
|
size.CellCountInt,
|
||||||
/// and checking the total size.
|
struct {
|
||||||
///
|
pub fn hash(self: *const @This(), style: Style) u64 {
|
||||||
/// When the set exceeds capacity, `error.OutOfMemory` is returned
|
_ = self;
|
||||||
/// from memory-using methods. The caller is responsible for determining
|
return style.hash();
|
||||||
/// 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
|
pub fn eql(self: *const @This(), a: Style, b: Style) bool {
|
||||||
/// the map that contains the actual style definitions
|
_ = self;
|
||||||
/// (in the form of the key).
|
return a.eql(b);
|
||||||
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 {
|
|
||||||
md_start: usize,
|
|
||||||
md_layout: MetadataMap.Layout,
|
|
||||||
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
|
|
||||||
/// and the unique identifier for a style. The unique identifier is used
|
|
||||||
/// to track the style in the full style map.
|
|
||||||
pub const Metadata = struct {
|
|
||||||
ref: size.CellCountInt = 0,
|
|
||||||
id: Id = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
test "Set basic usage" {
|
test "Set basic usage" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
@ -313,29 +268,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, .{});
|
||||||
|
|
||||||
// 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