terminal: more strict sizing for page capacities, max cap can fit 64-bit

This commit is contained in:
Mitchell Hashimoto
2026-01-13 13:09:41 -08:00
parent c8295815cb
commit 95a23f756d
5 changed files with 68 additions and 24 deletions

View File

@@ -1318,7 +1318,7 @@ const ReflowCursor = struct {
// Grow our capacity until we can
// definitely fit the extra bytes.
const required = cps.len * @sizeOf(u21);
var new_grapheme_capacity: usize = cap.grapheme_bytes;
var new_grapheme_capacity: size.GraphemeBytesInt = cap.grapheme_bytes;
while (new_grapheme_capacity - cap.grapheme_bytes < required) {
new_grapheme_capacity *= 2;
}
@@ -1362,7 +1362,7 @@ const ReflowCursor = struct {
} else |_| {
// Grow our capacity until we can
// definitely fit the extra bytes.
var new_string_capacity: usize = cap.string_bytes;
var new_string_capacity: size.StringBytesInt = cap.string_bytes;
while (new_string_capacity - cap.string_bytes < additional_required_string_capacity) {
new_string_capacity *= 2;
}
@@ -2647,16 +2647,16 @@ pub const AdjustCapacity = struct {
/// Adjust the number of styles in the page. This may be
/// rounded up if necessary to fit alignment requirements,
/// but it will never be rounded down.
styles: ?usize = null,
styles: ?size.StyleCountInt = null,
/// Adjust the number of available grapheme bytes in the page.
grapheme_bytes: ?usize = null,
grapheme_bytes: ?size.GraphemeBytesInt = null,
/// Adjust the number of available hyperlink bytes in the page.
hyperlink_bytes: ?usize = null,
hyperlink_bytes: ?size.HyperlinkCountInt = null,
/// Adjust the number of available string bytes in the page.
string_bytes: ?usize = null,
string_bytes: ?size.StringBytesInt = null,
};
pub const AdjustCapacityError = Allocator.Error || Page.CloneFromError;
@@ -2692,23 +2692,19 @@ pub fn adjustCapacity(
// All ceilPowerOfTwo is unreachable because we're always same or less
// bit width so maxInt is always possible.
if (adjustment.styles) |v| {
comptime assert(@bitSizeOf(@TypeOf(v)) <= @bitSizeOf(usize));
const aligned = std.math.ceilPowerOfTwo(usize, v) catch unreachable;
const aligned = std.math.ceilPowerOfTwo(size.StyleCountInt, v) catch unreachable;
cap.styles = @max(cap.styles, aligned);
}
if (adjustment.grapheme_bytes) |v| {
comptime assert(@bitSizeOf(@TypeOf(v)) <= @bitSizeOf(usize));
const aligned = std.math.ceilPowerOfTwo(usize, v) catch unreachable;
const aligned = std.math.ceilPowerOfTwo(size.GraphemeBytesInt, v) catch unreachable;
cap.grapheme_bytes = @max(cap.grapheme_bytes, aligned);
}
if (adjustment.hyperlink_bytes) |v| {
comptime assert(@bitSizeOf(@TypeOf(v)) <= @bitSizeOf(usize));
const aligned = std.math.ceilPowerOfTwo(usize, v) catch unreachable;
const aligned = std.math.ceilPowerOfTwo(size.HyperlinkCountInt, v) catch unreachable;
cap.hyperlink_bytes = @max(cap.hyperlink_bytes, aligned);
}
if (adjustment.string_bytes) |v| {
comptime assert(@bitSizeOf(@TypeOf(v)) <= @bitSizeOf(usize));
const aligned = std.math.ceilPowerOfTwo(usize, v) catch unreachable;
const aligned = std.math.ceilPowerOfTwo(size.StringBytesInt, v) catch unreachable;
cap.string_bytes = @max(cap.string_bytes, aligned);
}

View File

@@ -13,8 +13,9 @@ const autoHash = std.hash.autoHash;
const autoHashStrat = std.hash.autoHashStrat;
/// The unique identifier for a hyperlink. This is at most the number of cells
/// that can fit in a single terminal page.
pub const Id = size.CellCountInt;
/// that can fit in a single terminal page, since each cell can only contain
/// at most one hyperlink.
pub const Id = size.HyperlinkCountInt;
// The mapping of cell to hyperlink. We use an offset hash map to save space
// since its very unlikely a cell is a hyperlink, so its a waste to store

View File

@@ -1569,7 +1569,10 @@ pub const Page = struct {
const grapheme_alloc_start = alignForward(usize, styles_end, GraphemeAlloc.base_align.toByteUnits());
const grapheme_alloc_end = grapheme_alloc_start + grapheme_alloc_layout.total_size;
const grapheme_count = @divFloor(cap.grapheme_bytes, grapheme_chunk);
const grapheme_count = std.math.ceilPowerOfTwo(
usize,
@divFloor(cap.grapheme_bytes, grapheme_chunk),
) catch unreachable;
const grapheme_map_layout = GraphemeMap.layout(@intCast(grapheme_count));
const grapheme_map_start = alignForward(usize, grapheme_alloc_end, GraphemeMap.base_align.toByteUnits());
const grapheme_map_end = grapheme_map_start + grapheme_map_layout.total_size;
@@ -1639,25 +1642,33 @@ pub const Size = struct {
};
/// Capacity of this page.
///
/// This capacity can be maxed out (every field max) and still fit
/// within a 64-bit memory space. If you need more than this, you will
/// need to split data across separate pages.
///
/// For 32-bit systems, it is possible to overflow the addressable
/// space and this is something we still need to address in the future
/// likely by limiting the maximum capacity on 32-bit systems further.
pub const Capacity = struct {
/// Number of columns and rows we can know about.
cols: size.CellCountInt,
rows: size.CellCountInt,
/// Number of unique styles that can be used on this page.
styles: usize = 16,
styles: size.StyleCountInt = 16,
/// Number of bytes to allocate for hyperlink data. Note that the
/// amount of data used for hyperlinks in total is more than this because
/// hyperlinks use string data as well as a small amount of lookup metadata.
/// This number is a rough approximation.
hyperlink_bytes: usize = hyperlink_bytes_default,
hyperlink_bytes: size.HyperlinkCountInt = hyperlink_bytes_default,
/// Number of bytes to allocate for grapheme data.
grapheme_bytes: usize = grapheme_bytes_default,
grapheme_bytes: size.GraphemeBytesInt = grapheme_bytes_default,
/// Number of bytes to allocate for strings.
string_bytes: usize = string_bytes_default,
string_bytes: size.StringBytesInt = string_bytes_default,
pub const Adjustment = struct {
cols: ?size.CellCountInt = null,
@@ -2025,6 +2036,19 @@ pub const Cell = packed struct(u64) {
// //const pages = total_size / std.heap.page_size_min;
// }
test "Page.layout can take a maxed capacity" {
// Our intention is for a maxed-out capacity to always fit
// within a page layout without trigering runtime safety on any
// overflow. This simplifies some of our handling downstream of the
// call (relevant to: https://github.com/ghostty-org/ghostty/issues/10258)
var cap: Capacity = undefined;
inline for (@typeInfo(Capacity).@"struct".fields) |field| {
@field(cap, field.name) = std.math.maxInt(field.type);
}
_ = Page.layout(cap);
}
test "Cell is zero by default" {
const cell = Cell.init(0);
const cell_int: u64 = @bitCast(cell);

View File

@@ -11,9 +11,32 @@ pub const max_page_size = std.math.maxInt(u32);
/// derived from the maximum terminal page size.
pub const OffsetInt = std.math.IntFittingRange(0, max_page_size - 1);
/// The int type that can contain the maximum number of cells in a page.
pub const CellCountInt = u16; // TODO: derive
/// Int types for maximum values of things. A lot of these sizes are
/// based on "X is enough for any reasonable use case" principles.
// The goal is that a user can have the maxInt amount of all of these
// present at one time and be able to address them in a single Page.zig.
// Total number of cells that are possible in each dimension (row/col).
// Based on 2^16 being enough for any reasonable terminal size and allowing
// IDs to remain 16-bit.
pub const CellCountInt = u16;
// Total number of styles and hyperlinks that are possible in a page.
// We match CellCountInt here because each cell in a single row can have at
// most one style, making it simple to split a page by splitting rows.
//
// Note due to the way RefCountedSet works, we are short one value, but
// this is a theoretical limit we accept. A page with a single row max
// columns wide would be one short of having every cell have a unique style.
pub const StyleCountInt = CellCountInt;
pub const HyperlinkCountInt = CellCountInt;
// Total number of bytes that can be taken up by grapheme data and string
// data. Both of these technically unlimited with malicious input, but
// we choose a reasonable limit of 2^32 (4GB) per.
pub const GraphemeBytesInt = u32;
pub const StringBytesInt = u32;
/// The offset from the base address of the page to the start of some data.
/// This is typed for ease of use.
///

View File

@@ -11,7 +11,7 @@ const RefCountedSet = @import("ref_counted_set.zig").RefCountedSet;
/// The unique identifier for a style. This is at most the number of cells
/// that can fit into a terminal page.
pub const Id = size.CellCountInt;
pub const Id = size.StyleCountInt;
/// The Id to use for default styling.
pub const default_id: Id = 0;