mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
terminal: add dirty bits to the page structure
This commit is contained in:
@ -1917,6 +1917,7 @@ fn createPage(
|
|||||||
self: *PageList,
|
self: *PageList,
|
||||||
cap: Capacity,
|
cap: Capacity,
|
||||||
) !*List.Node {
|
) !*List.Node {
|
||||||
|
log.debug("create page cap={}", .{cap});
|
||||||
return try createPageExt(&self.pool, cap, &self.page_size);
|
return try createPageExt(&self.pool, cap, &self.page_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +91,44 @@ pub const Page = struct {
|
|||||||
/// The available set of styles in use on this page.
|
/// The available set of styles in use on this page.
|
||||||
styles: style.Set,
|
styles: style.Set,
|
||||||
|
|
||||||
|
/// The offset to the first mask of dirty bits in the page.
|
||||||
|
///
|
||||||
|
/// The dirty bits is a contiguous array of usize where each bit represents
|
||||||
|
/// a row in the page, in order. If the bit is set, then the row is dirty
|
||||||
|
/// and requires a redraw. Dirty status is only ever meant to convey that
|
||||||
|
/// a cell has changed visually. A cell which changes in a way that doesn't
|
||||||
|
/// affect the visual representation may not be marked as dirty.
|
||||||
|
///
|
||||||
|
/// Dirty tracking may have false positives but should never have false
|
||||||
|
/// negatives. A false negative would result in a visual artifact on the
|
||||||
|
/// screen.
|
||||||
|
///
|
||||||
|
/// Dirty bits are only ever unset by consumers of a page. The page
|
||||||
|
/// structure itself does not unset dirty bits since the page does not
|
||||||
|
/// know when a cell has been redrawn.
|
||||||
|
///
|
||||||
|
/// As implementation background: it may seem that dirty bits should be
|
||||||
|
/// stored elsewhere and not on the page itself, because the only data
|
||||||
|
/// that could possibly change is in the active area of a terminal
|
||||||
|
/// historically and that area is small compared to the typical scrollback.
|
||||||
|
/// My original thinking was to put the dirty bits on Screen instead and
|
||||||
|
/// have them only track the active area. However, I decided to put them
|
||||||
|
/// into the page directly for a few reasons:
|
||||||
|
///
|
||||||
|
/// 1. It's simpler. The page is a self-contained unit and it's nice
|
||||||
|
/// to have all the data for a page in one place.
|
||||||
|
///
|
||||||
|
/// 2. It's cheap. Even a very large page might have 1000 rows and
|
||||||
|
/// that's only ~128 bytes of 64-bit integers to track all the dirty
|
||||||
|
/// bits. Compared to the hundreds of kilobytes a typical page
|
||||||
|
/// consumes, this is nothing.
|
||||||
|
///
|
||||||
|
/// 3. It's more flexible. If we ever want to implement new terminal
|
||||||
|
/// features that allow non-active area to be dirty, we can do that
|
||||||
|
/// with minimal dirty-tracking work.
|
||||||
|
///
|
||||||
|
dirty: Offset(usize),
|
||||||
|
|
||||||
/// The current dimensions of the page. The capacity may be larger
|
/// The current dimensions of the page. The capacity may be larger
|
||||||
/// than this. This allows us to allocate a larger page than necessary
|
/// than this. This allows us to allocate a larger page than necessary
|
||||||
/// and also to resize a page smaller witout reallocating.
|
/// and also to resize a page smaller witout reallocating.
|
||||||
@ -155,6 +193,7 @@ pub const Page = struct {
|
|||||||
.memory = @alignCast(buf.start()[0..l.total_size]),
|
.memory = @alignCast(buf.start()[0..l.total_size]),
|
||||||
.rows = rows,
|
.rows = rows,
|
||||||
.cells = cells,
|
.cells = cells,
|
||||||
|
.dirty = buf.member(usize, l.dirty_start),
|
||||||
.styles = style.Set.init(
|
.styles = style.Set.init(
|
||||||
buf.add(l.styles_start),
|
buf.add(l.styles_start),
|
||||||
l.styles_layout,
|
l.styles_layout,
|
||||||
@ -866,12 +905,26 @@ pub const Page = struct {
|
|||||||
return self.grapheme_map.map(self.memory).count();
|
return self.grapheme_map.map(self.memory).count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the bitset for the dirty bits on this page.
|
||||||
|
///
|
||||||
|
/// The returned value is a DynamicBitSetUnmanaged but it is NOT
|
||||||
|
/// actually dynamic; do NOT call resize on this. It is safe to
|
||||||
|
/// read and write but do not resize it.
|
||||||
|
pub fn dirtyBitSet(self: *const Page) std.DynamicBitSetUnmanaged {
|
||||||
|
return .{
|
||||||
|
.bit_length = self.capacity.rows,
|
||||||
|
.masks = self.dirty.ptr(self.memory),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub const Layout = struct {
|
pub const Layout = struct {
|
||||||
total_size: usize,
|
total_size: usize,
|
||||||
rows_start: usize,
|
rows_start: usize,
|
||||||
rows_size: usize,
|
rows_size: usize,
|
||||||
cells_start: usize,
|
cells_start: usize,
|
||||||
cells_size: usize,
|
cells_size: usize,
|
||||||
|
dirty_start: usize,
|
||||||
|
dirty_size: usize,
|
||||||
styles_start: usize,
|
styles_start: usize,
|
||||||
styles_layout: style.Set.Layout,
|
styles_layout: style.Set.Layout,
|
||||||
grapheme_alloc_start: usize,
|
grapheme_alloc_start: usize,
|
||||||
@ -892,8 +945,19 @@ pub const Page = struct {
|
|||||||
const cells_start = alignForward(usize, rows_end, @alignOf(Cell));
|
const cells_start = alignForward(usize, rows_end, @alignOf(Cell));
|
||||||
const cells_end = cells_start + (cells_count * @sizeOf(Cell));
|
const cells_end = cells_start + (cells_count * @sizeOf(Cell));
|
||||||
|
|
||||||
|
// The division below cannot fail because our row count cannot
|
||||||
|
// exceed the maximum value of usize.
|
||||||
|
const dirty_bit_length: usize = rows_count;
|
||||||
|
const dirty_usize_length: usize = std.math.divCeil(
|
||||||
|
usize,
|
||||||
|
dirty_bit_length,
|
||||||
|
@bitSizeOf(usize),
|
||||||
|
) catch unreachable;
|
||||||
|
const dirty_start = alignForward(usize, cells_end, @alignOf(usize));
|
||||||
|
const dirty_end: usize = dirty_start + (dirty_usize_length * @sizeOf(usize));
|
||||||
|
|
||||||
const styles_layout = style.Set.layout(cap.styles);
|
const styles_layout = style.Set.layout(cap.styles);
|
||||||
const styles_start = alignForward(usize, cells_end, style.Set.base_align);
|
const styles_start = alignForward(usize, dirty_end, style.Set.base_align);
|
||||||
const styles_end = styles_start + styles_layout.total_size;
|
const styles_end = styles_start + styles_layout.total_size;
|
||||||
|
|
||||||
const grapheme_alloc_layout = GraphemeAlloc.layout(cap.grapheme_bytes);
|
const grapheme_alloc_layout = GraphemeAlloc.layout(cap.grapheme_bytes);
|
||||||
@ -913,6 +977,8 @@ pub const Page = struct {
|
|||||||
.rows_size = rows_end - rows_start,
|
.rows_size = rows_end - rows_start,
|
||||||
.cells_start = cells_start,
|
.cells_start = cells_start,
|
||||||
.cells_size = cells_end - cells_start,
|
.cells_size = cells_end - cells_start,
|
||||||
|
.dirty_start = dirty_start,
|
||||||
|
.dirty_size = dirty_end - dirty_start,
|
||||||
.styles_start = styles_start,
|
.styles_start = styles_start,
|
||||||
.styles_layout = styles_layout,
|
.styles_layout = styles_layout,
|
||||||
.grapheme_alloc_start = grapheme_alloc_start,
|
.grapheme_alloc_start = grapheme_alloc_start,
|
||||||
@ -981,9 +1047,18 @@ pub const Capacity = struct {
|
|||||||
const grapheme_alloc_start = alignBackward(usize, grapheme_map_start - layout.grapheme_alloc_layout.total_size, GraphemeAlloc.base_align);
|
const grapheme_alloc_start = alignBackward(usize, grapheme_map_start - layout.grapheme_alloc_layout.total_size, GraphemeAlloc.base_align);
|
||||||
const styles_start = alignBackward(usize, grapheme_alloc_start - layout.styles_layout.total_size, style.Set.base_align);
|
const styles_start = alignBackward(usize, grapheme_alloc_start - layout.styles_layout.total_size, style.Set.base_align);
|
||||||
|
|
||||||
const available_size = styles_start;
|
// The size per row is:
|
||||||
const size_per_row = @sizeOf(Row) + (@sizeOf(Cell) * @as(usize, @intCast(cols)));
|
// - The row metadata itself
|
||||||
const new_rows = @divFloor(available_size, size_per_row);
|
// - The cells per row (n=cols)
|
||||||
|
// - 1 bit for dirty tracking
|
||||||
|
const bits_per_row: usize = size: {
|
||||||
|
var bits: usize = @bitSizeOf(Row); // Row metadata
|
||||||
|
bits += @bitSizeOf(Cell) * @as(usize, @intCast(cols)); // Cells (n=cols)
|
||||||
|
bits += 1; // The dirty bit
|
||||||
|
break :size bits;
|
||||||
|
};
|
||||||
|
const available_bits: usize = styles_start * 8;
|
||||||
|
const new_rows: usize = @divFloor(available_bits, bits_per_row);
|
||||||
|
|
||||||
// If our rows go to zero then we can't fit any row metadata
|
// If our rows go to zero then we can't fit any row metadata
|
||||||
// for the desired number of columns.
|
// for the desired number of columns.
|
||||||
@ -1315,6 +1390,10 @@ test "Page init" {
|
|||||||
.styles = 32,
|
.styles = 32,
|
||||||
});
|
});
|
||||||
defer page.deinit();
|
defer page.deinit();
|
||||||
|
|
||||||
|
// Dirty set should be empty
|
||||||
|
const dirty = page.dirtyBitSet();
|
||||||
|
try std.testing.expectEqual(@as(usize, 0), dirty.count());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Page read and write cells" {
|
test "Page read and write cells" {
|
||||||
|
Reference in New Issue
Block a user