From 7728620ea8b86266a00e69909c33a4cd1265237f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 20 Nov 2025 20:59:12 -0800 Subject: [PATCH] terminal: render state dirty state --- src/renderer/generic.zig | 6 ++-- src/terminal/render.zig | 63 ++++++++++++++++++++++++++-------------- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 591b0643b..bc7dc0321 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -937,7 +937,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// Mark the full screen as dirty so that we redraw everything. pub inline fn markDirty(self: *Self) void { - self.terminal_state.redraw = true; + self.terminal_state.dirty = .full; } /// Called when we get an updated display ID for our display link. @@ -2265,7 +2265,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { links: *const terminal.RenderState.CellSet, ) !void { const state: *terminal.RenderState = &self.terminal_state; - defer state.redraw = false; + defer state.dirty = .false; self.draw_mutex.lock(); defer self.draw_mutex.unlock(); @@ -2317,7 +2317,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self.uniforms.grid_size = .{ new_size.columns, new_size.rows }; } - const rebuild = state.redraw or grid_size_diff; + const rebuild = state.dirty == .full or grid_size_diff; if (rebuild) { // If we are doing a full rebuild, then we clear the entire cell buffer. self.cells.reset(); diff --git a/src/terminal/render.zig b/src/terminal/render.zig index 8bfeff501..8dcf67dcb 100644 --- a/src/terminal/render.zig +++ b/src/terminal/render.zig @@ -60,11 +60,10 @@ pub const RenderState = struct { /// use cases. row_data: std.MultiArrayList(Row), - /// This is set to true if the terminal state has changed in a way - /// that the renderer should do a full redraw of the grid. The renderer - /// should se this to false when it has done so. `update` will only - /// ever tick this to true. - redraw: bool, + /// The dirty state of the render state. This is set by the update method. + /// The renderer/caller should set this to false when it has handled + /// the dirty state. + dirty: Dirty, /// The screen type that this state represents. This is used primarily /// to detect changes. @@ -93,7 +92,7 @@ pub const RenderState = struct { .style = undefined, }, .row_data = .empty, - .redraw = false, + .dirty = .false, .screen = .primary, }; @@ -179,6 +178,21 @@ pub const RenderState = struct { style: Style, }; + // Dirty state + pub const Dirty = enum { + /// Not dirty at all. Can skip rendering if prior state was + /// already rendered. + false, + + /// Partially dirty. Some rows changed but not all. None of the + /// global state changed such as colors. + partial, + + /// Fully dirty. Global state changed or dimensions changed. All rows + /// should be redrawn. + full, + }; + pub fn deinit(self: *RenderState, alloc: Allocator) void { for ( self.row_data.items(.arena), @@ -238,15 +252,6 @@ pub const RenderState = struct { break :redraw false; }; - // Full redraw resets our state completely. - if (redraw) { - self.screen = t.screens.active_key; - self.redraw = true; - - // Note: we don't clear any row_data here because our rebuild - // below is going to do that for us. - } - // Always set our cheap fields, its more expensive to compare self.rows = s.pages.rows; self.cols = s.pages.cols; @@ -339,6 +344,7 @@ pub const RenderState = struct { null, ); var y: size.CellCountInt = 0; + var any_dirty: bool = false; while (row_it.next()) |row_pin| : (y = y + 1) { // Find our cursor if we haven't found it yet. We do this even // if the row is not dirty because the cursor is unrelated. @@ -390,6 +396,9 @@ pub const RenderState = struct { continue; } + // Set that at least one row was dirty. + any_dirty = true; + // Clear our row dirty, we'll clear our page dirty later. // We can't clear it now because we have more rows to go through. page_rac.row.dirty = false; @@ -540,6 +549,18 @@ pub const RenderState = struct { } } + // Handle dirty state. + if (redraw) { + // Fully redraw resets some other state. + self.screen = t.screens.active_key; + self.dirty = .full; + + // Note: we don't clear any row_data here because our rebuild + // above did this. + } else if (any_dirty and self.dirty == .false) { + self.dirty = .partial; + } + // Finalize our final dirty page if (last_dirty_page) |last_p| last_p.dirty = false; @@ -931,19 +952,19 @@ test "dirty state" { // First update should trigger redraw due to resize try state.update(alloc, &t); - try testing.expect(state.redraw); + try testing.expectEqual(.full, state.dirty); - // Reset redraw flag and dirty rows - state.redraw = false; + // Reset dirty flag and dirty rows + state.dirty = .false; { const row_data = state.row_data.slice(); const dirty = row_data.items(.dirty); @memset(dirty, false); } - // Second update with no changes - no redraw, no dirty rows + // Second update with no changes - no dirty rows try state.update(alloc, &t); - try testing.expect(!state.redraw); + try testing.expectEqual(.false, state.dirty); { const row_data = state.row_data.slice(); const dirty = row_data.items(.dirty); @@ -953,7 +974,7 @@ test "dirty state" { // Write to first line try s.nextSlice("A"); try state.update(alloc, &t); - try testing.expect(!state.redraw); // Should not trigger full redraw + try testing.expectEqual(.partial, state.dirty); { const row_data = state.row_data.slice(); const dirty = row_data.items(.dirty);