f4135ed2
Fix dirty-row refresh planning helpers
a73x 2026-04-08 18:28
Commit message
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(); |