mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-14 03:25:50 +00:00
terminal: PageList can initialize with memory requirements > std
This commit is contained in:
@@ -54,9 +54,6 @@ const NodePool = std.heap.MemoryPool(List.Node);
|
||||
/// usage to support using our pool.
|
||||
const std_capacity = pagepkg.std_capacity;
|
||||
|
||||
/// The maximum columns we can support with the standard capacity.
|
||||
const std_max_cols = std_capacity.maxCols().?;
|
||||
|
||||
/// The byte size required for a standard page.
|
||||
const std_size = Page.layout(std_capacity).total_size;
|
||||
|
||||
@@ -231,19 +228,30 @@ pub const Viewport = union(enum) {
|
||||
/// But this gives us a nice fast heuristic for determining min/max size.
|
||||
/// Therefore, if the page size is violated you should always also verify
|
||||
/// that we have enough space for the active area.
|
||||
fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) !usize {
|
||||
fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) usize {
|
||||
// Invariant required to ensure our divCeil below cannot overflow.
|
||||
comptime {
|
||||
const max_rows = std.math.maxInt(size.CellCountInt);
|
||||
_ = std.math.divCeil(usize, max_rows, 1) catch unreachable;
|
||||
}
|
||||
|
||||
// Get our capacity to fit our rows. If the cols are too big, it may
|
||||
// force less rows than we want meaning we need more than one page to
|
||||
// represent a viewport.
|
||||
const cap = try std_capacity.adjust(.{ .cols = cols });
|
||||
const cap = initialCapacity(cols);
|
||||
|
||||
// Calculate the number of standard sized pages we need to represent
|
||||
// an active area.
|
||||
const pages_exact = if (cap.rows >= rows) 1 else try std.math.divCeil(
|
||||
const pages_exact = if (cap.rows >= rows) 1 else std.math.divCeil(
|
||||
usize,
|
||||
rows,
|
||||
cap.rows,
|
||||
);
|
||||
) catch {
|
||||
// Not possible:
|
||||
// - initialCapacity guarantees at least 1 row
|
||||
// - numerator/denominator can't overflow because of comptime check above
|
||||
unreachable;
|
||||
};
|
||||
|
||||
// We always need at least one page extra so that we
|
||||
// can fit partial pages to spread our active area across two pages.
|
||||
@@ -263,6 +271,49 @@ fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) !usize {
|
||||
return PagePool.item_size * pages;
|
||||
}
|
||||
|
||||
/// Calculates the initial capacity for a new page for a given column
|
||||
/// count. This will attempt to fit within std_size at all times so we
|
||||
/// can use our memory pool, but if cols is too big, this will return a
|
||||
/// larger capacity.
|
||||
///
|
||||
/// The returned capacity is always guaranteed to layout properly (not
|
||||
/// overflow). We are able to support capacities up to the maximum int
|
||||
/// value of cols, so this will never overflow.
|
||||
fn initialCapacity(cols: size.CellCountInt) Capacity {
|
||||
// This is an important invariant that ensures that this function
|
||||
// can never return an error. We verify here that our standard capacity
|
||||
// when increased to maximum possible columns can always support at
|
||||
// least one row in memory.
|
||||
//
|
||||
// IF THIS EVER FAILS: We probably need to modify our logic below
|
||||
// to reduce other elements of the capacity (styles, graphemes, etc.).
|
||||
// But, instead, I recommend taking a step back and re-evaluating
|
||||
// life choices.
|
||||
comptime {
|
||||
var cap = std_capacity;
|
||||
cap.cols = std.math.maxInt(size.CellCountInt);
|
||||
_ = Page.layout(cap);
|
||||
}
|
||||
|
||||
if (std_capacity.adjust(
|
||||
.{ .cols = cols },
|
||||
)) |cap| {
|
||||
// If we can adjust our standard capacity, we fit within the
|
||||
// standard size and we're good!
|
||||
return cap;
|
||||
} else |err| {
|
||||
// Ensure our error set doesn't change.
|
||||
comptime assert(@TypeOf(err) == error{OutOfMemory});
|
||||
}
|
||||
|
||||
// This code path means that our standard capacity can't even
|
||||
// accommodate our column count! The only solution is to increase
|
||||
// our capacity and go non-standard.
|
||||
var cap: Capacity = std_capacity;
|
||||
cap.cols = cols;
|
||||
return cap;
|
||||
}
|
||||
|
||||
/// This is the page allocator we'll use for all our underlying
|
||||
/// VM page allocations.
|
||||
inline fn pageAllocator() Allocator {
|
||||
@@ -318,7 +369,7 @@ pub fn init(
|
||||
);
|
||||
|
||||
// Get our minimum max size, see doc comments for more details.
|
||||
const min_max_size = try minMaxSize(cols, rows);
|
||||
const min_max_size = minMaxSize(cols, rows);
|
||||
|
||||
// We always track our viewport pin to ensure this is never an allocation
|
||||
const viewport_pin = try pool.pins.create();
|
||||
@@ -352,17 +403,31 @@ fn initPages(
|
||||
serial: *u64,
|
||||
cols: size.CellCountInt,
|
||||
rows: size.CellCountInt,
|
||||
) !struct { List, usize } {
|
||||
) Allocator.Error!struct { List, usize } {
|
||||
var page_list: List = .{};
|
||||
var page_size: usize = 0;
|
||||
|
||||
// Add pages as needed to create our initial viewport.
|
||||
const cap = try std_capacity.adjust(.{ .cols = cols });
|
||||
const cap = initialCapacity(cols);
|
||||
const layout = Page.layout(cap);
|
||||
const pooled = layout.total_size <= std_size;
|
||||
const page_alloc = pool.pages.arena.child_allocator;
|
||||
|
||||
var rem = rows;
|
||||
while (rem > 0) {
|
||||
const node = try pool.nodes.create();
|
||||
const page_buf = try pool.pages.create();
|
||||
// no errdefer because the pool deinit will clean these up
|
||||
const page_buf = if (pooled)
|
||||
try pool.pages.create()
|
||||
else
|
||||
try page_alloc.alignedAlloc(
|
||||
u8,
|
||||
.fromByteUnits(std.heap.page_size_min),
|
||||
layout.total_size,
|
||||
);
|
||||
errdefer if (pooled)
|
||||
pool.pages.destroy(page_buf)
|
||||
else
|
||||
page_alloc.free(page_buf);
|
||||
|
||||
// In runtime safety modes we have to memset because the Zig allocator
|
||||
// interface will always memset to 0xAA for undefined. In non-safe modes
|
||||
@@ -372,10 +437,7 @@ fn initPages(
|
||||
// Initialize the first set of pages to contain our viewport so that
|
||||
// the top of the first page is always the active area.
|
||||
node.* = .{
|
||||
.data = .initBuf(
|
||||
.init(page_buf),
|
||||
Page.layout(cap),
|
||||
),
|
||||
.data = .initBuf(.init(page_buf), layout),
|
||||
.serial = serial.*,
|
||||
};
|
||||
node.data.size.rows = @min(rem, node.data.capacity.rows);
|
||||
@@ -541,10 +603,8 @@ pub fn reset(self: *PageList) void {
|
||||
// We need enough pages/nodes to keep our active area. This should
|
||||
// never fail since we by definition have allocated a page already
|
||||
// that fits our size but I'm not confident to make that assertion.
|
||||
const cap = std_capacity.adjust(
|
||||
.{ .cols = self.cols },
|
||||
) catch @panic("reset: std_capacity.adjust failed");
|
||||
assert(cap.rows > 0); // adjust should never return 0 rows
|
||||
const cap = initialCapacity(self.cols);
|
||||
assert(cap.rows > 0);
|
||||
|
||||
// The number of pages we need is the number of rows in the active
|
||||
// area divided by the row capacity of a page.
|
||||
@@ -836,7 +896,7 @@ pub fn resize(self: *PageList, opts: Resize) !void {
|
||||
// when increasing beyond our initial minimum max size or explicit max
|
||||
// size to fit the active area.
|
||||
const old_min_max_size = self.min_max_size;
|
||||
self.min_max_size = try minMaxSize(
|
||||
self.min_max_size = minMaxSize(
|
||||
opts.cols orelse self.cols,
|
||||
opts.rows orelse self.rows,
|
||||
);
|
||||
@@ -1600,7 +1660,7 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void {
|
||||
// We only set the new min_max_size if we're not reflowing. If we are
|
||||
// reflowing, then resize handles this for us.
|
||||
const old_min_max_size = self.min_max_size;
|
||||
self.min_max_size = if (!opts.reflow) try minMaxSize(
|
||||
self.min_max_size = if (!opts.reflow) minMaxSize(
|
||||
opts.cols orelse self.cols,
|
||||
opts.rows orelse self.rows,
|
||||
) else old_min_max_size;
|
||||
@@ -4559,6 +4619,38 @@ test "PageList init rows across two pages" {
|
||||
}, s.scrollbar());
|
||||
}
|
||||
|
||||
test "PageList init more than max cols" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// Initialize with more columns than we can fit in our standard
|
||||
// capacity. This is going to force us to go to a non-standard page
|
||||
// immediately.
|
||||
var s = try init(
|
||||
alloc,
|
||||
std_capacity.maxCols().? + 1,
|
||||
80,
|
||||
null,
|
||||
);
|
||||
defer s.deinit();
|
||||
try testing.expect(s.viewport == .active);
|
||||
try testing.expectEqual(@as(usize, s.rows), s.totalRows());
|
||||
|
||||
// We expect a single, non-standard page
|
||||
try testing.expect(s.pages.first != null);
|
||||
try testing.expect(s.pages.first.?.data.memory.len > std_size);
|
||||
|
||||
// Initial total rows should be our row count
|
||||
try testing.expectEqual(s.rows, s.total_rows);
|
||||
|
||||
// Scrollbar should be where we expect it
|
||||
try testing.expectEqual(Scrollbar{
|
||||
.total = s.rows,
|
||||
.offset = 0,
|
||||
.len = s.rows,
|
||||
}, s.scrollbar());
|
||||
}
|
||||
|
||||
test "PageList pointFromPin active no history" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@@ -196,7 +196,8 @@ pub const Page = struct {
|
||||
// We need to go through and initialize all the rows so that
|
||||
// they point to a valid offset into the cells, since the rows
|
||||
// zero-initialized aren't valid.
|
||||
const cells_ptr = cells.ptr(buf)[0 .. cap.cols * cap.rows];
|
||||
const cells_len = @as(usize, cap.cols) * @as(usize, cap.rows);
|
||||
const cells_ptr = cells.ptr(buf)[0..cells_len];
|
||||
for (rows.ptr(buf)[0..cap.rows], 0..) |*row, y| {
|
||||
const start = y * cap.cols;
|
||||
row.* = .{
|
||||
@@ -1556,7 +1557,7 @@ pub const Page = struct {
|
||||
const rows_start = 0;
|
||||
const rows_end: usize = rows_start + (rows_count * @sizeOf(Row));
|
||||
|
||||
const cells_count: usize = @intCast(cap.cols * cap.rows);
|
||||
const cells_count: usize = @as(usize, cap.cols) * @as(usize, cap.rows);
|
||||
const cells_start = alignForward(usize, rows_end, @alignOf(Cell));
|
||||
const cells_end = cells_start + (cells_count * @sizeOf(Cell));
|
||||
|
||||
@@ -1676,7 +1677,7 @@ pub const Capacity = struct {
|
||||
if (available_bits <= @bitSizeOf(Row)) return null;
|
||||
|
||||
// We do the math of how many columns we can fit in the remaining
|
||||
// bits ignoring the metadat of a row.
|
||||
// bits ignoring the metadata of a row.
|
||||
const remaining_bits = available_bits - @bitSizeOf(Row);
|
||||
const max_cols = remaining_bits / @bitSizeOf(Cell);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user