a73x

917999fd

Fix dirty-row packing contract

a73x   2026-04-08 18:41


diff --git a/src/main.zig b/src/main.zig
index 11fadd0..39c40e4 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -640,11 +640,38 @@ test "repackRowCaches assigns contiguous offsets" {
    var packed_instances: std.ArrayListUnmanaged(renderer.Instance) = .empty;
    defer packed_instances.deinit(std.testing.allocator);

    const total = try repackRowCaches(std.testing.allocator, &packed_instances, &rows, &.{});
    var cursor_instances = try makeTestInstances(std.testing.allocator, 1);
    defer cursor_instances.deinit(std.testing.allocator);

    rows[0].instances.items[0].cell_pos[1] = 10.0;
    rows[0].instances.items[1].cell_pos[1] = 10.0;
    rows[1].instances.items[0].cell_pos[1] = 20.0;
    rows[1].instances.items[1].cell_pos[1] = 20.0;
    rows[1].instances.items[2].cell_pos[1] = 20.0;
    cursor_instances.items[0].cell_pos[0] = 99.0;
    cursor_instances.items[0].cell_pos[1] = 99.0;

    const packed_result = try repackRowCaches(
        std.testing.allocator,
        &packed_instances,
        &rows,
        cursor_instances.items,
    );

    try std.testing.expectEqual(@as(u32, 0), rows[0].gpu_offset_instances);
    try std.testing.expectEqual(@as(u32, 2), rows[0].gpu_len_instances);
    try std.testing.expectEqual(@as(u32, 2), rows[1].gpu_offset_instances);
    try std.testing.expectEqual(@as(u32, 5), total);
    try std.testing.expectEqual(@as(u32, 3), rows[1].gpu_len_instances);
    try std.testing.expectEqual(@as(u32, 6), packed_result.total_instances);
    try std.testing.expectEqual(@as(u32, 5), packed_result.cursor_offset_instances);
    try std.testing.expectEqual(@as(u32, 1), packed_result.cursor_len_instances);
    try std.testing.expectEqual(@as(usize, 6), packed_instances.items.len);
    try std.testing.expectEqualDeep([2]f32{ 0.0, 10.0 }, packed_instances.items[0].cell_pos);
    try std.testing.expectEqualDeep([2]f32{ 1.0, 10.0 }, packed_instances.items[1].cell_pos);
    try std.testing.expectEqualDeep([2]f32{ 0.0, 20.0 }, packed_instances.items[2].cell_pos);
    try std.testing.expectEqualDeep([2]f32{ 1.0, 20.0 }, packed_instances.items[3].cell_pos);
    try std.testing.expectEqualDeep([2]f32{ 2.0, 20.0 }, packed_instances.items[4].cell_pos);
    try std.testing.expectEqualDeep([2]f32{ 99.0, 99.0 }, packed_instances.items[5].cell_pos);
}

test "markLayoutDirtyOnLenChange returns true when row length changes" {
@@ -652,6 +679,46 @@ test "markLayoutDirtyOnLenChange returns true when row length changes" {
    try std.testing.expect(!markLayoutDirtyOnLenChange(3, 3));
}

test "repackRowCaches keeps cursor span explicit for empty and non-empty cursor instances" {
    var rows = [_]RowInstanceCache{
        .{
            .instances = try makeTestInstances(std.testing.allocator, 1),
        },
    };
    defer for (&rows) |*row| row.instances.deinit(std.testing.allocator);

    var packed_instances: std.ArrayListUnmanaged(renderer.Instance) = .empty;
    defer packed_instances.deinit(std.testing.allocator);

    const empty_cursor_result = try repackRowCaches(std.testing.allocator, &packed_instances, &rows, &.{});
    try std.testing.expectEqual(@as(u32, 1), empty_cursor_result.total_instances);
    try std.testing.expectEqual(@as(u32, 1), empty_cursor_result.cursor_offset_instances);
    try std.testing.expectEqual(@as(u32, 0), empty_cursor_result.cursor_len_instances);

    var cursor_instances = try makeTestInstances(std.testing.allocator, 2);
    defer cursor_instances.deinit(std.testing.allocator);

    cursor_instances.items[0].cell_pos[0] = 7.0;
    cursor_instances.items[0].cell_pos[1] = 8.0;
    cursor_instances.items[1].cell_pos[0] = 9.0;
    cursor_instances.items[1].cell_pos[1] = 10.0;

    const non_empty_cursor_result = try repackRowCaches(
        std.testing.allocator,
        &packed_instances,
        &rows,
        cursor_instances.items,
    );

    try std.testing.expectEqual(@as(u32, 3), non_empty_cursor_result.total_instances);
    try std.testing.expectEqual(@as(u32, 1), non_empty_cursor_result.cursor_offset_instances);
    try std.testing.expectEqual(@as(u32, 2), non_empty_cursor_result.cursor_len_instances);
    try std.testing.expectEqual(@as(usize, 3), packed_instances.items.len);
    try std.testing.expectEqualDeep([2]f32{ 0.0, 0.0 }, packed_instances.items[0].cell_pos);
    try std.testing.expectEqualDeep([2]f32{ 7.0, 8.0 }, packed_instances.items[1].cell_pos);
    try std.testing.expectEqualDeep([2]f32{ 9.0, 10.0 }, packed_instances.items[2].cell_pos);
}

const RowInstanceCache = struct {
    instances: std.ArrayListUnmanaged(renderer.Instance) = .empty,
    gpu_offset_instances: u32 = 0,
@@ -662,24 +729,42 @@ const RowInstanceCache = struct {
    }
};

const RowPackResult = struct {
    total_instances: u32,
    cursor_offset_instances: u32,
    cursor_len_instances: u32,
};

fn repackRowCaches(
    alloc: std.mem.Allocator,
    packed_instances: *std.ArrayListUnmanaged(renderer.Instance),
    rows: []RowInstanceCache,
    cursor_instances: []const renderer.Instance,
) !u32 {
) !RowPackResult {
    packed_instances.clearRetainingCapacity();

    var total_instances: u32 = 0;
    for (rows) |*row| {
        try packed_instances.appendSlice(alloc, row.instances.items);
        total_instances += @intCast(row.instances.items.len);
    }

    const cursor_offset_instances = total_instances;
    try packed_instances.appendSlice(alloc, cursor_instances);
    total_instances += @intCast(cursor_instances.len);

    var offset: u32 = 0;
    for (rows) |*row| {
        row.gpu_offset_instances = offset;
        row.gpu_len_instances = @intCast(row.instances.items.len);
        try packed_instances.appendSlice(alloc, row.instances.items);
        offset += @intCast(row.instances.items.len);
    }

    try packed_instances.appendSlice(alloc, cursor_instances);
    return offset + @as(u32, @intCast(cursor_instances.len));
    return .{
        .total_instances = total_instances,
        .cursor_offset_instances = cursor_offset_instances,
        .cursor_len_instances = @intCast(cursor_instances.len),
    };
}

fn markLayoutDirtyOnLenChange(old_len: usize, new_len: usize) bool {