a73x

f4135ed2

Fix dirty-row refresh planning helpers

a73x   2026-04-08 18:28

Commit message
Fix dirty-row refresh planning helpers

src/main.zig
Old New
@@ -372,10 +372,15 @@ const RowRefreshState = enum {
372 partial, 372 partial,
373 }; 373 };
374 374
375 const CursorRefreshContext = struct {
376 old_row: ?usize,
377 new_row: ?usize,
378 old_visible: bool,
379 new_visible: bool,
380 };
381
375 const RowRefreshContext = struct { 382 const RowRefreshContext = struct {
376 cursor_changed: bool, 383 cursor: CursorRefreshContext,
377 old_cursor_row: ?usize,
378 new_cursor_row: ?usize,
379 }; 384 };
380 385
381 const RowRefreshPlan = struct { 386 const RowRefreshPlan = struct {
@@ -391,22 +396,27 @@ fn planRowRefresh(
391 ) RowRefreshPlan { 396 ) RowRefreshPlan {
392 var rows_to_rebuild = std.StaticBitSet(256).initEmpty(); 397 var rows_to_rebuild = std.StaticBitSet(256).initEmpty();
393 398
394 const full_rebuild = state == .full; 399 const full_rebuild = state == .full or dirty_rows.len > rows_to_rebuild.capacity();
400 const cursor_rebuild = full_rebuild or cursorNeedsRebuild(ctx.cursor);
401
395 if (!full_rebuild) { 402 if (!full_rebuild) {
396 const limit = @min(dirty_rows.len, rows_to_rebuild.capacity());
397 var row_idx: usize = 0; 403 var row_idx: usize = 0;
398 while (row_idx < limit) : (row_idx += 1) { 404 while (row_idx < dirty_rows.len) : (row_idx += 1) {
399 if (dirty_rows[row_idx]) rows_to_rebuild.set(row_idx); 405 if (dirty_rows[row_idx]) rows_to_rebuild.set(row_idx);
400 } 406 }
401 } 407 }
402 408
403 return .{ 409 return .{
404 .full_rebuild = full_rebuild, 410 .full_rebuild = full_rebuild,
405 .cursor_rebuild = ctx.cursor_changed, 411 .cursor_rebuild = cursor_rebuild,
406 .rows_to_rebuild = rows_to_rebuild, 412 .rows_to_rebuild = rows_to_rebuild,
407 }; 413 };
408 } 414 }
409 415
416 fn cursorNeedsRebuild(cursor: CursorRefreshContext) bool {
417 return cursor.old_row != cursor.new_row or cursor.old_visible != cursor.new_visible;
418 }
419
410 fn appendCellInstances( 420 fn appendCellInstances(
411 alloc: std.mem.Allocator, 421 alloc: std.mem.Allocator,
412 instances: *std.ArrayListUnmanaged(renderer.Instance), 422 instances: *std.ArrayListUnmanaged(renderer.Instance),
@@ -518,33 +528,46 @@ test "event loop redraws only when terminal or window state changed" {
518 528
519 test "planRowRefresh requests full rebuild for full dirty state" { 529 test "planRowRefresh requests full rebuild for full dirty state" {
520 const plan = planRowRefresh(.full, &.{ false, true, false }, .{ 530 const plan = planRowRefresh(.full, &.{ false, true, false }, .{
521 .cursor_changed = false, 531 .cursor = .{
522 .old_cursor_row = null, 532 .old_row = null,
523 .new_cursor_row = null, 533 .new_row = null,
534 .old_visible = false,
535 .new_visible = false,
536 },
524 }); 537 });
525 538
526 try std.testing.expect(plan.full_rebuild); 539 try std.testing.expect(plan.full_rebuild);
540 try std.testing.expect(plan.cursor_rebuild);
527 try std.testing.expectEqual(@as(usize, 0), plan.rows_to_rebuild.count()); 541 try std.testing.expectEqual(@as(usize, 0), plan.rows_to_rebuild.count());
528 } 542 }
529 543
530 test "planRowRefresh selects only dirty rows for partial state" { 544 test "planRowRefresh selects only dirty rows for partial state" {
531 const plan = planRowRefresh(.partial, &.{ false, true, false, true }, .{ 545 const plan = planRowRefresh(.partial, &.{ false, true, false, true }, .{
532 .cursor_changed = false, 546 .cursor = .{
533 .old_cursor_row = null, 547 .old_row = null,
534 .new_cursor_row = null, 548 .new_row = null,
549 .old_visible = false,
550 .new_visible = false,
551 },
535 }); 552 });
536 553
537 try std.testing.expect(!plan.full_rebuild); 554 try std.testing.expect(!plan.full_rebuild);
555 try std.testing.expect(!plan.cursor_rebuild);
556 try std.testing.expectEqual(@as(usize, 2), plan.rows_to_rebuild.count());
538 try std.testing.expect(plan.rows_to_rebuild.isSet(1)); 557 try std.testing.expect(plan.rows_to_rebuild.isSet(1));
539 try std.testing.expect(plan.rows_to_rebuild.isSet(3)); 558 try std.testing.expect(plan.rows_to_rebuild.isSet(3));
540 try std.testing.expect(!plan.rows_to_rebuild.isSet(0)); 559 try std.testing.expect(!plan.rows_to_rebuild.isSet(0));
560 try std.testing.expect(!plan.rows_to_rebuild.isSet(2));
541 } 561 }
542 562
543 test "planRowRefresh handles cursor-only updates without unrelated rows" { 563 test "planRowRefresh handles cursor-only updates without unrelated rows" {
544 const plan = planRowRefresh(.partial, &.{ false, false, false }, .{ 564 const plan = planRowRefresh(.partial, &.{ false, false, false }, .{
545 .cursor_changed = true, 565 .cursor = .{
546 .old_cursor_row = 1, 566 .old_row = 1,
547 .new_cursor_row = 2, 567 .new_row = 2,
568 .old_visible = true,
569 .new_visible = true,
570 },
548 }); 571 });
549 572
550 try std.testing.expect(!plan.full_rebuild); 573 try std.testing.expect(!plan.full_rebuild);
@@ -552,6 +575,24 @@ test "planRowRefresh handles cursor-only updates without unrelated rows" {
552 try std.testing.expectEqual(@as(usize, 0), plan.rows_to_rebuild.count()); 575 try std.testing.expectEqual(@as(usize, 0), plan.rows_to_rebuild.count());
553 } 576 }
554 577
578 test "planRowRefresh forces full rebuild when dirty rows exceed fixed capacity" {
579 var dirty_rows: [257]bool = .{false} ** 257;
580 dirty_rows[256] = true;
581
582 const plan = planRowRefresh(.partial, dirty_rows[0..], .{
583 .cursor = .{
584 .old_row = null,
585 .new_row = null,
586 .old_visible = true,
587 .new_visible = true,
588 },
589 });
590
591 try std.testing.expect(plan.full_rebuild);
592 try std.testing.expect(plan.cursor_rebuild);
593 try std.testing.expectEqual(@as(usize, 0), plan.rows_to_rebuild.count());
594 }
595
555 fn runDrawSmokeTest(alloc: std.mem.Allocator) !void { 596 fn runDrawSmokeTest(alloc: std.mem.Allocator) !void {
556 var conn = try wayland_client.Connection.init(); 597 var conn = try wayland_client.Connection.init();
557 defer conn.deinit(); 598 defer conn.deinit();