a73x

f4135ed2

Fix dirty-row refresh planning helpers

a73x   2026-04-08 18:28


diff --git a/src/main.zig b/src/main.zig
index e0b4313..a0b0ab3 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -372,10 +372,15 @@ const RowRefreshState = enum {
    partial,
};

const CursorRefreshContext = struct {
    old_row: ?usize,
    new_row: ?usize,
    old_visible: bool,
    new_visible: bool,
};

const RowRefreshContext = struct {
    cursor_changed: bool,
    old_cursor_row: ?usize,
    new_cursor_row: ?usize,
    cursor: CursorRefreshContext,
};

const RowRefreshPlan = struct {
@@ -391,22 +396,27 @@ fn planRowRefresh(
) RowRefreshPlan {
    var rows_to_rebuild = std.StaticBitSet(256).initEmpty();

    const full_rebuild = state == .full;
    const full_rebuild = state == .full or dirty_rows.len > rows_to_rebuild.capacity();
    const cursor_rebuild = full_rebuild or cursorNeedsRebuild(ctx.cursor);

    if (!full_rebuild) {
        const limit = @min(dirty_rows.len, rows_to_rebuild.capacity());
        var row_idx: usize = 0;
        while (row_idx < limit) : (row_idx += 1) {
        while (row_idx < dirty_rows.len) : (row_idx += 1) {
            if (dirty_rows[row_idx]) rows_to_rebuild.set(row_idx);
        }
    }

    return .{
        .full_rebuild = full_rebuild,
        .cursor_rebuild = ctx.cursor_changed,
        .cursor_rebuild = cursor_rebuild,
        .rows_to_rebuild = rows_to_rebuild,
    };
}

fn cursorNeedsRebuild(cursor: CursorRefreshContext) bool {
    return cursor.old_row != cursor.new_row or cursor.old_visible != cursor.new_visible;
}

fn appendCellInstances(
    alloc: std.mem.Allocator,
    instances: *std.ArrayListUnmanaged(renderer.Instance),
@@ -518,33 +528,46 @@ test "event loop redraws only when terminal or window state changed" {

test "planRowRefresh requests full rebuild for full dirty state" {
    const plan = planRowRefresh(.full, &.{ false, true, false }, .{
        .cursor_changed = false,
        .old_cursor_row = null,
        .new_cursor_row = null,
        .cursor = .{
            .old_row = null,
            .new_row = null,
            .old_visible = false,
            .new_visible = false,
        },
    });

    try std.testing.expect(plan.full_rebuild);
    try std.testing.expect(plan.cursor_rebuild);
    try std.testing.expectEqual(@as(usize, 0), plan.rows_to_rebuild.count());
}

test "planRowRefresh selects only dirty rows for partial state" {
    const plan = planRowRefresh(.partial, &.{ false, true, false, true }, .{
        .cursor_changed = false,
        .old_cursor_row = null,
        .new_cursor_row = null,
        .cursor = .{
            .old_row = null,
            .new_row = null,
            .old_visible = false,
            .new_visible = false,
        },
    });

    try std.testing.expect(!plan.full_rebuild);
    try std.testing.expect(!plan.cursor_rebuild);
    try std.testing.expectEqual(@as(usize, 2), plan.rows_to_rebuild.count());
    try std.testing.expect(plan.rows_to_rebuild.isSet(1));
    try std.testing.expect(plan.rows_to_rebuild.isSet(3));
    try std.testing.expect(!plan.rows_to_rebuild.isSet(0));
    try std.testing.expect(!plan.rows_to_rebuild.isSet(2));
}

test "planRowRefresh handles cursor-only updates without unrelated rows" {
    const plan = planRowRefresh(.partial, &.{ false, false, false }, .{
        .cursor_changed = true,
        .old_cursor_row = 1,
        .new_cursor_row = 2,
        .cursor = .{
            .old_row = 1,
            .new_row = 2,
            .old_visible = true,
            .new_visible = true,
        },
    });

    try std.testing.expect(!plan.full_rebuild);
@@ -552,6 +575,24 @@ test "planRowRefresh handles cursor-only updates without unrelated rows" {
    try std.testing.expectEqual(@as(usize, 0), plan.rows_to_rebuild.count());
}

test "planRowRefresh forces full rebuild when dirty rows exceed fixed capacity" {
    var dirty_rows: [257]bool = .{false} ** 257;
    dirty_rows[256] = true;

    const plan = planRowRefresh(.partial, dirty_rows[0..], .{
        .cursor = .{
            .old_row = null,
            .new_row = null,
            .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, 0), plan.rows_to_rebuild.count());
}

fn runDrawSmokeTest(alloc: std.mem.Allocator) !void {
    var conn = try wayland_client.Connection.init();
    defer conn.deinit();