a73x

247fce8e

Use dirty-row render cache in terminal loop

a73x   2026-04-09 05:52

Commit message
Use dirty-row render cache in terminal loop

src/main.zig
Old New
@@ -145,12 +145,10 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
145 defer p.deinit(); 145 defer p.deinit();
146 term.setWritePtyCallback(&p, &writePtyFromTerminal); 146 term.setWritePtyCallback(&p, &writePtyFromTerminal);
147 147
148 // === instance buffer === 148 // === render cache ===
149 var instances: std.ArrayListUnmanaged(renderer.Instance) = .empty; 149 var render_cache = RenderCache.empty;
150 defer instances.deinit(alloc); 150 defer render_cache.deinit(alloc);
151 try instances.ensureTotalCapacity(alloc, @as(usize, cols) * rows); 151 try render_cache.resizeRows(alloc, rows);
152 var row_cache = RowInstanceCache{};
153 defer row_cache.deinit(alloc);
154 152
155 // === main loop === 153 // === main loop ===
156 const wl_fd = conn.display.getFd(); 154 const wl_fd = conn.display.getFd();
@@ -246,19 +244,42 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
246 if (!shouldRenderFrame(render_pending, false, false)) continue; 244 if (!shouldRenderFrame(render_pending, false, false)) continue;
247 245
248 // === render === 246 // === render ===
247 const previous_cursor = term.render_state.cursor;
249 try term.snapshot(); 248 try term.snapshot();
250 249
251 instances.clearRetainingCapacity();
252 const default_bg = term.backgroundColor(); 250 const default_bg = term.backgroundColor();
253 const bg_uv = atlas.cursorUV(); 251 const bg_uv = atlas.cursorUV();
254 252
255 const term_rows = term.render_state.row_data.items(.cells); 253 const term_rows = term.render_state.row_data.items(.cells);
256 var row_idx: u32 = 0; 254 const dirty_rows = term.render_state.row_data.items(.dirty);
255 try render_cache.resizeRows(alloc, term_rows.len);
256
257 const refresh_plan = planRowRefresh(
258 if (term.render_state.dirty == .full) .full else .partial,
259 dirty_rows,
260 .{
261 .cursor = .{
262 .old_row = if (previous_cursor.viewport) |cursor| @intCast(cursor.y) else null,
263 .new_row = if (term.render_state.cursor.viewport) |cursor| @intCast(cursor.y) else null,
264 .old_col = if (previous_cursor.viewport) |cursor| @intCast(cursor.x) else null,
265 .new_col = if (term.render_state.cursor.viewport) |cursor| @intCast(cursor.x) else null,
266 .old_visible = previous_cursor.visible,
267 .new_visible = term.render_state.cursor.visible,
268 },
269 },
270 );
271
272 var rows_rebuilt: usize = 0;
273 var row_idx: usize = 0;
257 while (row_idx < term_rows.len) : (row_idx += 1) { 274 while (row_idx < term_rows.len) : (row_idx += 1) {
258 _ = try rebuildRowInstances( 275 if (!refresh_plan.full_rebuild and !refresh_plan.rows_to_rebuild.isSet(row_idx)) continue;
276
277 const previous_gpu_offset = render_cache.rows[row_idx].gpu_offset_instances;
278 const previous_gpu_len = render_cache.rows[row_idx].gpu_len_instances;
279 const rebuilt = try rebuildRowInstances(
259 alloc, 280 alloc,
260 &row_cache, 281 &render_cache.rows[row_idx],
261 row_idx, 282 @intCast(row_idx),
262 term_rows[row_idx], 283 term_rows[row_idx],
263 term, 284 term,
264 &face, 285 &face,
@@ -269,13 +290,22 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
269 default_bg, 290 default_bg,
270 bg_uv, 291 bg_uv,
271 ); 292 );
272 try instances.appendSlice(alloc, row_cache.instances.items); 293 if (rebuilt.len_changed) {
294 render_cache.layout_dirty = true;
295 } else {
296 render_cache.rows[row_idx].gpu_offset_instances = previous_gpu_offset;
297 render_cache.rows[row_idx].gpu_len_instances = previous_gpu_len;
298 }
299 rows_rebuilt += 1;
273 } 300 }
274 301
275 if (term.render_state.cursor.visible) { 302 var cursor_rebuilt = false;
303 if (refresh_plan.cursor_rebuild) {
304 var cursor_instances_buf: [1]renderer.Instance = undefined;
305 var cursor_instances: []const renderer.Instance = &.{};
276 if (term.render_state.cursor.viewport) |cursor| { 306 if (term.render_state.cursor.viewport) |cursor| {
277 const cursor_uv = atlas.cursorUV(); 307 const cursor_uv = atlas.cursorUV();
278 try instances.append(alloc, .{ 308 cursor_instances_buf[0] = .{
279 .cell_pos = .{ 309 .cell_pos = .{
280 @floatFromInt(cursor.x), 310 @floatFromInt(cursor.x),
281 @floatFromInt(cursor.y), 311 @floatFromInt(cursor.y),
@@ -293,22 +323,86 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
293 }, 323 },
294 .fg = .{ 1.0, 1.0, 1.0, 0.5 }, 324 .fg = .{ 1.0, 1.0, 1.0, 0.5 },
295 .bg = .{ 0, 0, 0, 0 }, 325 .bg = .{ 0, 0, 0, 0 },
296 }); 326 };
327 cursor_instances = cursor_instances_buf[0..1];
297 } 328 }
329 const previous_total_instance_count = render_cache.total_instance_count;
330 const previous_layout_dirty = render_cache.layout_dirty;
331 const rebuilt = try render_cache.rebuildCursorInstances(alloc, cursor_instances);
332 if (!rebuilt.len_changed) {
333 render_cache.layout_dirty = previous_layout_dirty;
334 render_cache.total_instance_count = previous_total_instance_count;
335 }
336 cursor_rebuilt = true;
298 } 337 }
299 338
300 // Re-upload atlas if new glyphs were added 339 // Re-upload atlas if new glyphs were added
301 if (atlas.dirty) { 340 if (atlas.dirty) {
302 try ctx.uploadAtlas(atlas.pixels); 341 try ctx.uploadAtlas(atlas.pixels);
303 atlas.dirty = false; 342 atlas.dirty = false;
343 render_cache.layout_dirty = true;
304 } 344 }
305 345
306 if (instances.items.len > 0) { 346 const upload_plan = applyRenderPlan(.{
307 try ctx.uploadInstances(instances.items); 347 .layout_dirty = render_cache.layout_dirty,
348 .rows_rebuilt = rows_rebuilt,
349 .cursor_rebuilt = cursor_rebuilt,
350 });
351
352 if (upload_plan.full_upload) {
353 const pack_result = try repackRowCaches(
354 alloc,
355 &render_cache.packed_instances,
356 render_cache.rows,
357 render_cache.cursor_instances.items,
358 );
359 render_cache.total_instance_count = pack_result.total_instances;
360 render_cache.layout_dirty = false;
361 if (render_cache.packed_instances.items.len > 0) {
362 try ctx.uploadInstances(render_cache.packed_instances.items);
363 }
364 } else if (upload_plan.partial_upload) {
365 var fallback_to_full_upload = false;
366
367 var upload_row_idx: usize = 0;
368 while (upload_row_idx < term_rows.len) : (upload_row_idx += 1) {
369 if (!refresh_plan.full_rebuild and !refresh_plan.rows_to_rebuild.isSet(upload_row_idx)) continue;
370 const row_cache = &render_cache.rows[upload_row_idx];
371 if (try ctx.uploadInstanceRange(
372 row_cache.gpu_offset_instances,
373 row_cache.instances.items,
374 )) {
375 fallback_to_full_upload = true;
376 break;
377 }
378 }
379
380 if (!fallback_to_full_upload and cursor_rebuilt) {
381 if (try ctx.uploadInstanceRange(
382 cursorOffsetInstances(render_cache.rows),
383 render_cache.cursor_instances.items,
384 )) {
385 fallback_to_full_upload = true;
386 }
387 }
388
389 if (fallback_to_full_upload) {
390 const pack_result = try repackRowCaches(
391 alloc,
392 &render_cache.packed_instances,
393 render_cache.rows,
394 render_cache.cursor_instances.items,
395 );
396 render_cache.total_instance_count = pack_result.total_instances;
397 render_cache.layout_dirty = false;
398 if (render_cache.packed_instances.items.len > 0) {
399 try ctx.uploadInstances(render_cache.packed_instances.items);
400 }
401 }
308 } 402 }
309 403
310 ctx.drawCells( 404 ctx.drawCells(
311 @intCast(instances.items.len), 405 render_cache.total_instance_count,
312 .{ @floatFromInt(cell_w), @floatFromInt(cell_h) }, 406 .{ @floatFromInt(cell_w), @floatFromInt(cell_h) },
313 default_bg, 407 default_bg,
314 ) catch |err| switch (err) { 408 ) catch |err| switch (err) {
@@ -320,6 +414,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
320 }, 414 },
321 else => return err, 415 else => return err,
322 }; 416 };
417 clearConsumedDirtyFlags(&term.render_state.dirty, dirty_rows, refresh_plan);
323 render_pending = false; 418 render_pending = false;
324 } 419 }
325 420
@@ -391,7 +486,9 @@ fn planRowRefresh(
391 var rows_to_rebuild = std.StaticBitSet(256).initEmpty(); 486 var rows_to_rebuild = std.StaticBitSet(256).initEmpty();
392 487
393 const full_rebuild = state == .full or dirty_rows.len > rows_to_rebuild.capacity(); 488 const full_rebuild = state == .full or dirty_rows.len > rows_to_rebuild.capacity();
394 const cursor_rebuild = full_rebuild or cursorNeedsRebuild(ctx.cursor); 489 const cursor_rebuild = full_rebuild or
490 cursorNeedsRebuild(ctx.cursor) or
491 cursorTouchesDirtyRow(dirty_rows, ctx.cursor);
395 492
396 if (!full_rebuild) { 493 if (!full_rebuild) {
397 var row_idx: usize = 0; 494 var row_idx: usize = 0;
@@ -413,6 +510,16 @@ fn cursorNeedsRebuild(cursor: CursorRefreshContext) bool {
413 cursor.old_visible != cursor.new_visible; 510 cursor.old_visible != cursor.new_visible;
414 } 511 }
415 512
513 fn cursorTouchesDirtyRow(dirty_rows: []const bool, cursor: CursorRefreshContext) bool {
514 if (cursor.old_row) |row| {
515 if (row < dirty_rows.len and dirty_rows[row]) return true;
516 }
517 if (cursor.new_row) |row| {
518 if (row < dirty_rows.len and dirty_rows[row]) return true;
519 }
520 return false;
521 }
522
416 fn appendCellInstances( 523 fn appendCellInstances(
417 alloc: std.mem.Allocator, 524 alloc: std.mem.Allocator,
418 instances: *std.ArrayListUnmanaged(renderer.Instance), 525 instances: *std.ArrayListUnmanaged(renderer.Instance),
@@ -614,6 +721,23 @@ test "planRowRefresh rebuilds cursor when only column changes on same row" {
614 try std.testing.expectEqual(@as(usize, 0), plan.rows_to_rebuild.count()); 721 try std.testing.expectEqual(@as(usize, 0), plan.rows_to_rebuild.count());
615 } 722 }
616 723
724 test "planRowRefresh rebuilds cursor when its row is dirty without cursor movement" {
725 const plan = planRowRefresh(.partial, &.{ false, true, false }, .{
726 .cursor = .{
727 .old_row = 1,
728 .new_row = 1,
729 .old_col = 4,
730 .new_col = 4,
731 .old_visible = true,
732 .new_visible = true,
733 },
734 });
735
736 try std.testing.expect(!plan.full_rebuild);
737 try std.testing.expect(plan.cursor_rebuild);
738 try std.testing.expectEqual(@as(usize, 1), plan.rows_to_rebuild.count());
739 }
740
617 test "repackRowCaches assigns contiguous offsets" { 741 test "repackRowCaches assigns contiguous offsets" {
618 var rows = [_]RowInstanceCache{ 742 var rows = [_]RowInstanceCache{
619 .{ 743 .{
@@ -992,6 +1116,37 @@ test "RenderCache resizeRows truncates populated tail rows and preserves prefix
992 try std.testing.expect(cache.layout_dirty); 1116 try std.testing.expect(cache.layout_dirty);
993 } 1117 }
994 1118
1119 test "applyRenderPlan requests full upload when layout changes" {
1120 const result = applyRenderPlan(.{
1121 .layout_dirty = true,
1122 .rows_rebuilt = 1,
1123 .cursor_rebuilt = false,
1124 });
1125
1126 try std.testing.expect(result.full_upload);
1127 try std.testing.expect(!result.partial_upload);
1128 }
1129
1130 test "clearConsumedDirtyFlags clears only consumed partial rows after successful refresh" {
1131 var dirty_rows = [_]bool{ true, false, true, false };
1132 const plan = RowRefreshPlan{
1133 .full_rebuild = false,
1134 .cursor_rebuild = false,
1135 .rows_to_rebuild = blk: {
1136 var rows = std.StaticBitSet(256).initEmpty();
1137 rows.set(0);
1138 rows.set(2);
1139 break :blk rows;
1140 },
1141 };
1142
1143 var render_dirty: vt.RenderDirty = .partial;
1144 clearConsumedDirtyFlags(&render_dirty, dirty_rows[0..], plan);
1145
1146 try std.testing.expectEqual(@as(@TypeOf(render_dirty), .false), render_dirty);
1147 try std.testing.expectEqualSlices(bool, &.{ false, false, false, false }, dirty_rows[0..]);
1148 }
1149
995 const RowInstanceCache = struct { 1150 const RowInstanceCache = struct {
996 instances: std.ArrayListUnmanaged(renderer.Instance) = .empty, 1151 instances: std.ArrayListUnmanaged(renderer.Instance) = .empty,
997 gpu_offset_instances: u32 = 0, 1152 gpu_offset_instances: u32 = 0,
@@ -1106,6 +1261,31 @@ const CursorRebuildResult = struct {
1106 packed_invalidated: bool, 1261 packed_invalidated: bool,
1107 }; 1262 };
1108 1263
1264 const RenderUploadPlanInput = struct {
1265 layout_dirty: bool,
1266 rows_rebuilt: usize,
1267 cursor_rebuilt: bool,
1268 };
1269
1270 const RenderUploadPlanResult = struct {
1271 full_upload: bool,
1272 partial_upload: bool,
1273 };
1274
1275 fn applyRenderPlan(input: RenderUploadPlanInput) RenderUploadPlanResult {
1276 if (input.layout_dirty) {
1277 return .{
1278 .full_upload = true,
1279 .partial_upload = false,
1280 };
1281 }
1282
1283 return .{
1284 .full_upload = false,
1285 .partial_upload = input.rows_rebuilt > 0 or input.cursor_rebuilt,
1286 };
1287 }
1288
1109 fn repackRowCaches( 1289 fn repackRowCaches(
1110 alloc: std.mem.Allocator, 1290 alloc: std.mem.Allocator,
1111 packed_instances: *std.ArrayListUnmanaged(renderer.Instance), 1291 packed_instances: *std.ArrayListUnmanaged(renderer.Instance),
@@ -1191,6 +1371,38 @@ fn markLayoutDirtyOnLenChange(old_len: usize, new_len: usize) bool {
1191 return old_len != new_len; 1371 return old_len != new_len;
1192 } 1372 }
1193 1373
1374 fn clearConsumedDirtyFlags(
1375 render_dirty: *vt.RenderDirty,
1376 dirty_rows: []bool,
1377 plan: RowRefreshPlan,
1378 ) void {
1379 if (plan.full_rebuild) {
1380 @memset(dirty_rows, false);
1381 render_dirty.* = .false;
1382 return;
1383 }
1384
1385 var row_idx: usize = 0;
1386 while (row_idx < dirty_rows.len) : (row_idx += 1) {
1387 if (plan.rows_to_rebuild.isSet(row_idx)) dirty_rows[row_idx] = false;
1388 }
1389
1390 for (dirty_rows) |dirty| {
1391 if (dirty) {
1392 render_dirty.* = .partial;
1393 return;
1394 }
1395 }
1396
1397 render_dirty.* = .false;
1398 }
1399
1400 fn cursorOffsetInstances(rows: []const RowInstanceCache) u32 {
1401 var offset: u32 = 0;
1402 for (rows) |row| offset += row.gpu_len_instances;
1403 return offset;
1404 }
1405
1194 fn makeTestInstances( 1406 fn makeTestInstances(
1195 alloc: std.mem.Allocator, 1407 alloc: std.mem.Allocator,
1196 count: usize, 1408 count: usize,
src/vt.zig
Old New
@@ -60,6 +60,7 @@ const DeviceAttributesReturn = @typeInfo(
60 pub const InputAction = ghostty_vt.input.KeyAction; 60 pub const InputAction = ghostty_vt.input.KeyAction;
61 pub const InputKey = ghostty_vt.input.Key; 61 pub const InputKey = ghostty_vt.input.Key;
62 pub const InputMods = ghostty_vt.input.KeyMods; 62 pub const InputMods = ghostty_vt.input.KeyMods;
63 pub const RenderDirty = ghostty_vt.RenderState.Dirty;
63 pub const Size = ghostty_vt.size_report.Size; 64 pub const Size = ghostty_vt.size_report.Size;
64 pub const CellColors = struct { 65 pub const CellColors = struct {
65 fg: [4]f32, 66 fg: [4]f32,