mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
vt: row dirty tracking
This commit is contained in:
@@ -39,6 +39,25 @@ extern "C" {
|
||||
* 2. Update it from a terminal instance whenever you need.
|
||||
* 3. Read from the render state to get the data needed to draw your frame.
|
||||
*
|
||||
* ## Dirty Tracking
|
||||
*
|
||||
* Dirty tracking is a key feature of the render state that allows renderers
|
||||
* to efficiently determine what parts of the screen have changed and only
|
||||
* redraw changed regions.
|
||||
*
|
||||
* The render state API keeps track of dirty state at two independent layers:
|
||||
* a global dirty state that indicates whether the entire frame is clean,
|
||||
* partially dirty, or fully dirty, and a per-row dirty state that allows
|
||||
* tracking which rows in a partially dirty frame have changed.
|
||||
*
|
||||
* The user of the render state API is expected to unset both of these.
|
||||
* The `update` call does not unset dirty state, it only updates it.
|
||||
*
|
||||
* An extremely important detail: setting one dirty state doesn't unset
|
||||
* the other. For example, setting the global dirty state to false does not
|
||||
* reset the row-level dirty flags. So, the caller of the render state API must
|
||||
* be careful to manage both layers of dirty state correctly.
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* @snippet c-vt-render/src/main.c render-state-update
|
||||
@@ -128,6 +147,18 @@ typedef struct {
|
||||
GhosttyResult ghostty_render_state_new(const GhosttyAllocator* allocator,
|
||||
GhosttyRenderState* state);
|
||||
|
||||
/**
|
||||
* Free a render state instance.
|
||||
*
|
||||
* Releases all resources associated with the render state. After this call,
|
||||
* the render state handle becomes invalid.
|
||||
*
|
||||
* @param state The render state handle to free (may be NULL)
|
||||
*
|
||||
* @ingroup render
|
||||
*/
|
||||
void ghostty_render_state_free(GhosttyRenderState state);
|
||||
|
||||
/**
|
||||
* Update a render state instance from a terminal.
|
||||
*
|
||||
@@ -227,6 +258,15 @@ GhosttyResult ghostty_render_state_row_iterator_new(
|
||||
GhosttyRenderState state,
|
||||
GhosttyRenderStateRowIterator* out_iterator);
|
||||
|
||||
/**
|
||||
* Free a render-state row iterator.
|
||||
*
|
||||
* @param iterator The iterator handle to free (may be NULL)
|
||||
*
|
||||
* @ingroup render
|
||||
*/
|
||||
void ghostty_render_state_row_iterator_free(GhosttyRenderStateRowIterator iterator);
|
||||
|
||||
/**
|
||||
* Move a render-state row iterator to the next row.
|
||||
*
|
||||
@@ -242,25 +282,37 @@ GhosttyResult ghostty_render_state_row_iterator_new(
|
||||
bool ghostty_render_state_row_iterator_next(GhosttyRenderStateRowIterator iterator);
|
||||
|
||||
/**
|
||||
* Free a render-state row iterator.
|
||||
* Get the dirty state of the current row in a render-state row iterator.
|
||||
*
|
||||
* @param iterator The iterator handle to free (may be NULL)
|
||||
* This reads the dirty flag at the iterator's current row position.
|
||||
* Call ghostty_render_state_row_iterator_next() at least once before
|
||||
* calling this function.
|
||||
*
|
||||
* @param iterator The iterator handle to query (may be NULL)
|
||||
* @return true if the current row is dirty, false if the row is clean,
|
||||
* `iterator` is NULL, or the iterator is not positioned on a row
|
||||
*
|
||||
* @ingroup render
|
||||
*/
|
||||
void ghostty_render_state_row_iterator_free(GhosttyRenderStateRowIterator iterator);
|
||||
bool ghostty_render_state_row_dirty_get(GhosttyRenderStateRowIterator iterator);
|
||||
|
||||
/**
|
||||
* Free a render state instance.
|
||||
* Set the dirty state of the current row in a render-state row iterator.
|
||||
*
|
||||
* Releases all resources associated with the render state. After this call,
|
||||
* the render state handle becomes invalid.
|
||||
* This writes the dirty flag at the iterator's current row position.
|
||||
* Call ghostty_render_state_row_iterator_next() at least once before
|
||||
* calling this function.
|
||||
*
|
||||
* @param state The render state handle to free (may be NULL)
|
||||
* @param iterator The iterator handle to update (may be NULL)
|
||||
* @param dirty The dirty state to set for the current row
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if
|
||||
* `iterator` is NULL or the iterator is not positioned on a row
|
||||
*
|
||||
* @ingroup render
|
||||
*/
|
||||
void ghostty_render_state_free(GhosttyRenderState state);
|
||||
GhosttyResult ghostty_render_state_row_dirty_set(
|
||||
GhosttyRenderStateRowIterator iterator,
|
||||
bool dirty);
|
||||
|
||||
/** @} */
|
||||
|
||||
|
||||
@@ -195,6 +195,8 @@ comptime {
|
||||
@export(&c.render_state_dirty_set, .{ .name = "ghostty_render_state_dirty_set" });
|
||||
@export(&c.render_state_row_iterator_new, .{ .name = "ghostty_render_state_row_iterator_new" });
|
||||
@export(&c.render_state_row_iterator_next, .{ .name = "ghostty_render_state_row_iterator_next" });
|
||||
@export(&c.render_state_row_dirty_get, .{ .name = "ghostty_render_state_row_dirty_get" });
|
||||
@export(&c.render_state_row_dirty_set, .{ .name = "ghostty_render_state_row_dirty_set" });
|
||||
@export(&c.render_state_row_iterator_free, .{ .name = "ghostty_render_state_row_iterator_free" });
|
||||
@export(&c.render_state_free, .{ .name = "ghostty_render_state_free" });
|
||||
@export(&c.terminal_new, .{ .name = "ghostty_terminal_new" });
|
||||
|
||||
@@ -45,6 +45,8 @@ pub const render_state_dirty_get = render.dirty_get;
|
||||
pub const render_state_dirty_set = render.dirty_set;
|
||||
pub const render_state_row_iterator_new = render.row_iterator_new;
|
||||
pub const render_state_row_iterator_next = render.row_iterator_next;
|
||||
pub const render_state_row_dirty_get = render.row_dirty_get;
|
||||
pub const render_state_row_dirty_set = render.row_dirty_set;
|
||||
pub const render_state_row_iterator_free = render.row_iterator_free;
|
||||
|
||||
pub const sgr_new = sgr.new;
|
||||
|
||||
@@ -7,6 +7,7 @@ const CAllocator = lib_alloc.Allocator;
|
||||
const colorpkg = @import("../color.zig");
|
||||
const page = @import("../page.zig");
|
||||
const size = @import("../size.zig");
|
||||
const Style = @import("../style.zig").Style;
|
||||
const terminal_c = @import("terminal.zig");
|
||||
const renderpkg = @import("../render.zig");
|
||||
const Result = @import("result.zig").Result;
|
||||
@@ -24,8 +25,8 @@ const RowIteratorWrapper = struct {
|
||||
|
||||
/// These are the raw pointers into the render state data.
|
||||
raws: []const page.Row,
|
||||
cells: []const std.MultiArrayList(renderpkg.RenderState.Cell),
|
||||
dirty: []const bool,
|
||||
cells: []const std.MultiArrayList(renderpkg.RenderState.Cell).Slice,
|
||||
dirty: []bool,
|
||||
};
|
||||
|
||||
/// C: GhosttyRenderState
|
||||
@@ -217,7 +218,7 @@ fn row_iterator_new_(
|
||||
.alloc = alloc,
|
||||
.y = null,
|
||||
.raws = row_data.items(.raw),
|
||||
.cells = row_data.items(.cells),
|
||||
.cells = row_data.items(.cells).slice(),
|
||||
.dirty = row_data.items(.dirty),
|
||||
};
|
||||
|
||||
@@ -238,6 +239,22 @@ pub fn row_iterator_next(iterator_: RowIterator) callconv(.c) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn row_dirty_get(iterator_: RowIterator) callconv(.c) bool {
|
||||
const it = iterator_ orelse return false;
|
||||
const y = it.y orelse return false;
|
||||
return it.dirty[y];
|
||||
}
|
||||
|
||||
pub fn row_dirty_set(
|
||||
iterator_: RowIterator,
|
||||
dirty: bool,
|
||||
) callconv(.c) Result {
|
||||
const it = iterator_ orelse return .invalid_value;
|
||||
const y = it.y orelse return .invalid_value;
|
||||
it.dirty[y] = dirty;
|
||||
return .success;
|
||||
}
|
||||
|
||||
test "render: new/free" {
|
||||
var state: RenderState = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
@@ -432,6 +449,132 @@ test "render: row iterator next null" {
|
||||
try testing.expect(!row_iterator_next(null));
|
||||
}
|
||||
|
||||
test "render: row iterator dirty get null" {
|
||||
try testing.expect(!row_dirty_get(null));
|
||||
}
|
||||
|
||||
test "render: row iterator dirty set invalid value" {
|
||||
try testing.expectEqual(Result.invalid_value, row_dirty_set(null, false));
|
||||
|
||||
var terminal: terminal_c.Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(
|
||||
&lib_alloc.test_allocator,
|
||||
&terminal,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 10_000,
|
||||
},
|
||||
));
|
||||
defer terminal_c.free(terminal);
|
||||
|
||||
var state: RenderState = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&state,
|
||||
));
|
||||
defer free(state);
|
||||
|
||||
try testing.expectEqual(Result.success, update(state, terminal));
|
||||
|
||||
var iterator: RowIterator = null;
|
||||
try testing.expectEqual(Result.success, row_iterator_new(
|
||||
&lib_alloc.test_allocator,
|
||||
state,
|
||||
&iterator,
|
||||
));
|
||||
defer row_iterator_free(iterator);
|
||||
|
||||
try testing.expectEqual(Result.invalid_value, row_dirty_set(iterator, false));
|
||||
}
|
||||
|
||||
test "render: row iterator dirty get before iteration" {
|
||||
var terminal: terminal_c.Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(
|
||||
&lib_alloc.test_allocator,
|
||||
&terminal,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 10_000,
|
||||
},
|
||||
));
|
||||
defer terminal_c.free(terminal);
|
||||
|
||||
var state: RenderState = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&state,
|
||||
));
|
||||
defer free(state);
|
||||
|
||||
try testing.expectEqual(Result.success, update(state, terminal));
|
||||
|
||||
var iterator: RowIterator = null;
|
||||
try testing.expectEqual(Result.success, row_iterator_new(
|
||||
&lib_alloc.test_allocator,
|
||||
state,
|
||||
&iterator,
|
||||
));
|
||||
defer row_iterator_free(iterator);
|
||||
|
||||
try testing.expect(!row_dirty_get(iterator));
|
||||
}
|
||||
|
||||
test "render: row iterator dirty get" {
|
||||
var terminal: terminal_c.Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(
|
||||
&lib_alloc.test_allocator,
|
||||
&terminal,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 10_000,
|
||||
},
|
||||
));
|
||||
defer terminal_c.free(terminal);
|
||||
|
||||
var state: RenderState = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&state,
|
||||
));
|
||||
defer free(state);
|
||||
|
||||
try testing.expectEqual(Result.success, update(state, terminal));
|
||||
|
||||
// Dirty the first row so the iterator has at least one dirty row to observe.
|
||||
terminal_c.vt_write(terminal, "hello", 5);
|
||||
try testing.expectEqual(Result.success, update(state, terminal));
|
||||
|
||||
// Create an iterator and verify it is dirty.
|
||||
var it: RowIterator = null;
|
||||
try testing.expectEqual(Result.success, row_iterator_new(
|
||||
&lib_alloc.test_allocator,
|
||||
state,
|
||||
&it,
|
||||
));
|
||||
defer row_iterator_free(it);
|
||||
|
||||
try testing.expect(row_iterator_next(it));
|
||||
try testing.expect(row_dirty_get(it));
|
||||
|
||||
// Clear dirty on this row.
|
||||
try testing.expectEqual(Result.success, row_dirty_set(it, false));
|
||||
|
||||
// It should not be dirty anymore.
|
||||
var it2: RowIterator = null;
|
||||
try testing.expectEqual(Result.success, row_iterator_new(
|
||||
&lib_alloc.test_allocator,
|
||||
state,
|
||||
&it2,
|
||||
));
|
||||
defer row_iterator_free(it2);
|
||||
|
||||
try testing.expect(row_iterator_next(it2));
|
||||
try testing.expect(!row_dirty_get(it2));
|
||||
}
|
||||
|
||||
test "render: row iterator next" {
|
||||
var terminal: terminal_c.Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(
|
||||
|
||||
Reference in New Issue
Block a user