247fce8e
Use dirty-row render cache in terminal loop
a73x 2026-04-09 05:52
diff --git a/src/main.zig b/src/main.zig index 08690d5..88b9cbe 100644 --- a/src/main.zig +++ b/src/main.zig @@ -145,12 +145,10 @@ fn runTerminal(alloc: std.mem.Allocator) !void { defer p.deinit(); term.setWritePtyCallback(&p, &writePtyFromTerminal); // === instance buffer === var instances: std.ArrayListUnmanaged(renderer.Instance) = .empty; defer instances.deinit(alloc); try instances.ensureTotalCapacity(alloc, @as(usize, cols) * rows); var row_cache = RowInstanceCache{}; defer row_cache.deinit(alloc); // === render cache === var render_cache = RenderCache.empty; defer render_cache.deinit(alloc); try render_cache.resizeRows(alloc, rows); // === main loop === const wl_fd = conn.display.getFd(); @@ -246,19 +244,42 @@ fn runTerminal(alloc: std.mem.Allocator) !void { if (!shouldRenderFrame(render_pending, false, false)) continue; // === render === const previous_cursor = term.render_state.cursor; try term.snapshot(); instances.clearRetainingCapacity(); const default_bg = term.backgroundColor(); const bg_uv = atlas.cursorUV(); const term_rows = term.render_state.row_data.items(.cells); var row_idx: u32 = 0; const dirty_rows = term.render_state.row_data.items(.dirty); try render_cache.resizeRows(alloc, term_rows.len); const refresh_plan = planRowRefresh( if (term.render_state.dirty == .full) .full else .partial, dirty_rows, .{ .cursor = .{ .old_row = if (previous_cursor.viewport) |cursor| @intCast(cursor.y) else null, .new_row = if (term.render_state.cursor.viewport) |cursor| @intCast(cursor.y) else null, .old_col = if (previous_cursor.viewport) |cursor| @intCast(cursor.x) else null, .new_col = if (term.render_state.cursor.viewport) |cursor| @intCast(cursor.x) else null, .old_visible = previous_cursor.visible, .new_visible = term.render_state.cursor.visible, }, }, ); var rows_rebuilt: usize = 0; var row_idx: usize = 0; while (row_idx < term_rows.len) : (row_idx += 1) { _ = try rebuildRowInstances( if (!refresh_plan.full_rebuild and !refresh_plan.rows_to_rebuild.isSet(row_idx)) continue; const previous_gpu_offset = render_cache.rows[row_idx].gpu_offset_instances; const previous_gpu_len = render_cache.rows[row_idx].gpu_len_instances; const rebuilt = try rebuildRowInstances( alloc, &row_cache, row_idx, &render_cache.rows[row_idx], @intCast(row_idx), term_rows[row_idx], term, &face, @@ -269,13 +290,22 @@ fn runTerminal(alloc: std.mem.Allocator) !void { default_bg, bg_uv, ); try instances.appendSlice(alloc, row_cache.instances.items); if (rebuilt.len_changed) { render_cache.layout_dirty = true; } else { render_cache.rows[row_idx].gpu_offset_instances = previous_gpu_offset; render_cache.rows[row_idx].gpu_len_instances = previous_gpu_len; } rows_rebuilt += 1; } if (term.render_state.cursor.visible) { var cursor_rebuilt = false; if (refresh_plan.cursor_rebuild) { var cursor_instances_buf: [1]renderer.Instance = undefined; var cursor_instances: []const renderer.Instance = &.{}; if (term.render_state.cursor.viewport) |cursor| { const cursor_uv = atlas.cursorUV(); try instances.append(alloc, .{ cursor_instances_buf[0] = .{ .cell_pos = .{ @floatFromInt(cursor.x), @floatFromInt(cursor.y), @@ -293,22 +323,86 @@ fn runTerminal(alloc: std.mem.Allocator) !void { }, .fg = .{ 1.0, 1.0, 1.0, 0.5 }, .bg = .{ 0, 0, 0, 0 }, }); }; cursor_instances = cursor_instances_buf[0..1]; } const previous_total_instance_count = render_cache.total_instance_count; const previous_layout_dirty = render_cache.layout_dirty; const rebuilt = try render_cache.rebuildCursorInstances(alloc, cursor_instances); if (!rebuilt.len_changed) { render_cache.layout_dirty = previous_layout_dirty; render_cache.total_instance_count = previous_total_instance_count; } cursor_rebuilt = true; } // Re-upload atlas if new glyphs were added if (atlas.dirty) { try ctx.uploadAtlas(atlas.pixels); atlas.dirty = false; render_cache.layout_dirty = true; } if (instances.items.len > 0) { try ctx.uploadInstances(instances.items); const upload_plan = applyRenderPlan(.{ .layout_dirty = render_cache.layout_dirty, .rows_rebuilt = rows_rebuilt, .cursor_rebuilt = cursor_rebuilt, }); if (upload_plan.full_upload) { const pack_result = try repackRowCaches( alloc, &render_cache.packed_instances, render_cache.rows, render_cache.cursor_instances.items, ); render_cache.total_instance_count = pack_result.total_instances; render_cache.layout_dirty = false; if (render_cache.packed_instances.items.len > 0) { try ctx.uploadInstances(render_cache.packed_instances.items); } } else if (upload_plan.partial_upload) { var fallback_to_full_upload = false; var upload_row_idx: usize = 0; while (upload_row_idx < term_rows.len) : (upload_row_idx += 1) { if (!refresh_plan.full_rebuild and !refresh_plan.rows_to_rebuild.isSet(upload_row_idx)) continue; const row_cache = &render_cache.rows[upload_row_idx]; if (try ctx.uploadInstanceRange( row_cache.gpu_offset_instances, row_cache.instances.items, )) { fallback_to_full_upload = true; break; } } if (!fallback_to_full_upload and cursor_rebuilt) { if (try ctx.uploadInstanceRange( cursorOffsetInstances(render_cache.rows), render_cache.cursor_instances.items, )) { fallback_to_full_upload = true; } } if (fallback_to_full_upload) { const pack_result = try repackRowCaches( alloc, &render_cache.packed_instances, render_cache.rows, render_cache.cursor_instances.items, ); render_cache.total_instance_count = pack_result.total_instances; render_cache.layout_dirty = false; if (render_cache.packed_instances.items.len > 0) { try ctx.uploadInstances(render_cache.packed_instances.items); } } } ctx.drawCells( @intCast(instances.items.len), render_cache.total_instance_count, .{ @floatFromInt(cell_w), @floatFromInt(cell_h) }, default_bg, ) catch |err| switch (err) { @@ -320,6 +414,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void { }, else => return err, }; clearConsumedDirtyFlags(&term.render_state.dirty, dirty_rows, refresh_plan); render_pending = false; } @@ -391,7 +486,9 @@ fn planRowRefresh( var rows_to_rebuild = std.StaticBitSet(256).initEmpty(); const full_rebuild = state == .full or dirty_rows.len > rows_to_rebuild.capacity(); const cursor_rebuild = full_rebuild or cursorNeedsRebuild(ctx.cursor); const cursor_rebuild = full_rebuild or cursorNeedsRebuild(ctx.cursor) or cursorTouchesDirtyRow(dirty_rows, ctx.cursor); if (!full_rebuild) { var row_idx: usize = 0; @@ -413,6 +510,16 @@ fn cursorNeedsRebuild(cursor: CursorRefreshContext) bool { cursor.old_visible != cursor.new_visible; } fn cursorTouchesDirtyRow(dirty_rows: []const bool, cursor: CursorRefreshContext) bool { if (cursor.old_row) |row| { if (row < dirty_rows.len and dirty_rows[row]) return true; } if (cursor.new_row) |row| { if (row < dirty_rows.len and dirty_rows[row]) return true; } return false; } fn appendCellInstances( alloc: std.mem.Allocator, instances: *std.ArrayListUnmanaged(renderer.Instance), @@ -614,6 +721,23 @@ test "planRowRefresh rebuilds cursor when only column changes on same row" { try std.testing.expectEqual(@as(usize, 0), plan.rows_to_rebuild.count()); } test "planRowRefresh rebuilds cursor when its row is dirty without cursor movement" { const plan = planRowRefresh(.partial, &.{ false, true, false }, .{ .cursor = .{ .old_row = 1, .new_row = 1, .old_col = 4, .new_col = 4, .old_visible = true, .new_visible = true, }, }); try std.testing.expect(!plan.full_rebuild); try std.testing.expect(plan.cursor_rebuild); try std.testing.expectEqual(@as(usize, 1), plan.rows_to_rebuild.count()); } test "repackRowCaches assigns contiguous offsets" { var rows = [_]RowInstanceCache{ .{ @@ -992,6 +1116,37 @@ test "RenderCache resizeRows truncates populated tail rows and preserves prefix try std.testing.expect(cache.layout_dirty); } test "applyRenderPlan requests full upload when layout changes" { const result = applyRenderPlan(.{ .layout_dirty = true, .rows_rebuilt = 1, .cursor_rebuilt = false, }); try std.testing.expect(result.full_upload); try std.testing.expect(!result.partial_upload); } test "clearConsumedDirtyFlags clears only consumed partial rows after successful refresh" { var dirty_rows = [_]bool{ true, false, true, false }; const plan = RowRefreshPlan{ .full_rebuild = false, .cursor_rebuild = false, .rows_to_rebuild = blk: { var rows = std.StaticBitSet(256).initEmpty(); rows.set(0); rows.set(2); break :blk rows; }, }; var render_dirty: vt.RenderDirty = .partial; clearConsumedDirtyFlags(&render_dirty, dirty_rows[0..], plan); try std.testing.expectEqual(@as(@TypeOf(render_dirty), .false), render_dirty); try std.testing.expectEqualSlices(bool, &.{ false, false, false, false }, dirty_rows[0..]); } const RowInstanceCache = struct { instances: std.ArrayListUnmanaged(renderer.Instance) = .empty, gpu_offset_instances: u32 = 0, @@ -1106,6 +1261,31 @@ const CursorRebuildResult = struct { packed_invalidated: bool, }; const RenderUploadPlanInput = struct { layout_dirty: bool, rows_rebuilt: usize, cursor_rebuilt: bool, }; const RenderUploadPlanResult = struct { full_upload: bool, partial_upload: bool, }; fn applyRenderPlan(input: RenderUploadPlanInput) RenderUploadPlanResult { if (input.layout_dirty) { return .{ .full_upload = true, .partial_upload = false, }; } return .{ .full_upload = false, .partial_upload = input.rows_rebuilt > 0 or input.cursor_rebuilt, }; } fn repackRowCaches( alloc: std.mem.Allocator, packed_instances: *std.ArrayListUnmanaged(renderer.Instance), @@ -1191,6 +1371,38 @@ fn markLayoutDirtyOnLenChange(old_len: usize, new_len: usize) bool { return old_len != new_len; } fn clearConsumedDirtyFlags( render_dirty: *vt.RenderDirty, dirty_rows: []bool, plan: RowRefreshPlan, ) void { if (plan.full_rebuild) { @memset(dirty_rows, false); render_dirty.* = .false; return; } var row_idx: usize = 0; while (row_idx < dirty_rows.len) : (row_idx += 1) { if (plan.rows_to_rebuild.isSet(row_idx)) dirty_rows[row_idx] = false; } for (dirty_rows) |dirty| { if (dirty) { render_dirty.* = .partial; return; } } render_dirty.* = .false; } fn cursorOffsetInstances(rows: []const RowInstanceCache) u32 { var offset: u32 = 0; for (rows) |row| offset += row.gpu_len_instances; return offset; } fn makeTestInstances( alloc: std.mem.Allocator, count: usize, diff --git a/src/vt.zig b/src/vt.zig index 3d4912c..eaedd7a 100644 --- a/src/vt.zig +++ b/src/vt.zig @@ -60,6 +60,7 @@ const DeviceAttributesReturn = @typeInfo( pub const InputAction = ghostty_vt.input.KeyAction; pub const InputKey = ghostty_vt.input.Key; pub const InputMods = ghostty_vt.input.KeyMods; pub const RenderDirty = ghostty_vt.RenderState.Dirty; pub const Size = ghostty_vt.size_report.Size; pub const CellColors = struct { fg: [4]f32,