a73x

8975cd18

Add dirty-row refresh planning helpers

a73x   2026-04-08 18:25


diff --git a/src/main.zig b/src/main.zig
index d0abf95..e0b4313 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -367,6 +367,46 @@ fn shouldRenderFrame(terminal_dirty: bool, window_dirty: bool, forced: bool) boo
    return terminal_dirty or window_dirty or forced;
}

const RowRefreshState = enum {
    full,
    partial,
};

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

const RowRefreshPlan = struct {
    full_rebuild: bool,
    cursor_rebuild: bool,
    rows_to_rebuild: std.StaticBitSet(256),
};

fn planRowRefresh(
    state: RowRefreshState,
    dirty_rows: []const bool,
    ctx: RowRefreshContext,
) RowRefreshPlan {
    var rows_to_rebuild = std.StaticBitSet(256).initEmpty();

    const full_rebuild = state == .full;
    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) {
            if (dirty_rows[row_idx]) rows_to_rebuild.set(row_idx);
        }
    }

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

fn appendCellInstances(
    alloc: std.mem.Allocator,
    instances: *std.ArrayListUnmanaged(renderer.Instance),
@@ -476,6 +516,42 @@ test "event loop redraws only when terminal or window state changed" {
    try std.testing.expect(!shouldRenderFrame(false, false, false));
}

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,
    });

    try std.testing.expect(plan.full_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,
    });

    try std.testing.expect(!plan.full_rebuild);
    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));
}

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,
    });

    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();