a73x

5e9ae29b

Fix render cache resize invariants

a73x   2026-04-08 18:58


diff --git a/src/main.zig b/src/main.zig
index 5455e3c..c98a9d1 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -769,6 +769,57 @@ test "RenderCache deinit resets fields after releasing row storage" {
    try std.testing.expect(cache.layout_dirty);
}

test "RenderCache resizeRows to zero clears derived state after live row data" {
    var cache = RenderCache.empty;

    try cache.resizeRows(std.testing.allocator, 2);
    cache.rows[0].instances = try makeTestInstances(std.testing.allocator, 1);
    cache.rows[1].instances = try makeTestInstances(std.testing.allocator, 2);

    var cursor_instances = try makeTestInstances(std.testing.allocator, 1);
    defer cursor_instances.deinit(std.testing.allocator);
    try cache.cursor_instances.append(std.testing.allocator, cursor_instances.items[0]);

    var packed_instances = try makeTestInstances(std.testing.allocator, 1);
    defer packed_instances.deinit(std.testing.allocator);
    try cache.packed_instances.append(std.testing.allocator, packed_instances.items[0]);

    cache.total_instance_count = 3;
    cache.layout_dirty = false;

    try cache.resizeRows(std.testing.allocator, 0);

    try std.testing.expectEqual(@as(usize, 0), cache.rows.len);
    try std.testing.expectEqual(@as(usize, 0), cache.cursor_instances.items.len);
    try std.testing.expectEqual(@as(usize, 0), cache.packed_instances.items.len);
    try std.testing.expectEqual(@as(u32, 0), cache.total_instance_count);
    try std.testing.expect(cache.layout_dirty);

    cache.deinit(std.testing.allocator);
}

test "RenderCache resizeRows truncates populated tail rows and preserves prefix only" {
    var cache = RenderCache.empty;
    defer cache.deinit(std.testing.allocator);

    try cache.resizeRows(std.testing.allocator, 3);
    cache.rows[0].instances = try makeTestInstances(std.testing.allocator, 1);
    cache.rows[1].instances = try makeTestInstances(std.testing.allocator, 2);
    cache.rows[2].instances = try makeTestInstances(std.testing.allocator, 3);

    cache.rows[0].gpu_offset_instances = 10;
    cache.rows[1].gpu_offset_instances = 20;
    cache.rows[2].gpu_offset_instances = 30;

    try cache.resizeRows(std.testing.allocator, 1);

    try std.testing.expectEqual(@as(usize, 1), cache.rows.len);
    try std.testing.expectEqual(@as(usize, 1), cache.rows[0].instances.items.len);
    try std.testing.expectEqual(@as(u32, 10), cache.rows[0].gpu_offset_instances);
    try std.testing.expectEqual(@as(u32, 0), cache.total_instance_count);
    try std.testing.expect(cache.layout_dirty);
}

const RowInstanceCache = struct {
    instances: std.ArrayListUnmanaged(renderer.Instance) = .empty,
    gpu_offset_instances: u32 = 0,
@@ -791,8 +842,8 @@ const RenderCache = struct {

    fn resizeRows(self: *RenderCache, alloc: std.mem.Allocator, row_count: usize) !void {
        if (self.rows.len == row_count) return;
        const old_rows = self.rows;
        if (row_count == 0) {
            const old_rows = self.rows;
            if (old_rows.len > 0) {
                var row_idx: usize = 0;
                while (row_idx < old_rows.len) : (row_idx += 1) {
@@ -801,23 +852,22 @@ const RenderCache = struct {
                alloc.free(old_rows);
            }
            self.rows = &.{};
            self.invalidateAfterResize();
            return;
        }

        const old_rows = self.rows;
        var new_rows = try alloc.alloc(RowInstanceCache, row_count);
        for (new_rows) |*row| row.* = .{};

        const copy_len = @min(old_rows.len, row_count);
        if (copy_len > 0) {
            @memcpy(new_rows[0..copy_len], old_rows[0..copy_len]);
        var row_idx: usize = 0;
        while (row_idx < copy_len) : (row_idx += 1) {
            // Preserve only the surviving prefix by moving ownership row-by-row.
            new_rows[row_idx] = old_rows[row_idx];
            old_rows[row_idx] = .{};
        }

        if (row_count < old_rows.len) {
            var row_idx = row_count;
            while (row_idx < old_rows.len) : (row_idx += 1) {
                old_rows[row_idx].deinit(alloc);
            }
        while (row_idx < old_rows.len) : (row_idx += 1) {
            old_rows[row_idx].deinit(alloc);
        }

        if (old_rows.len > 0) {
@@ -825,6 +875,7 @@ const RenderCache = struct {
        }

        self.rows = new_rows;
        self.invalidateAfterResize();
    }

    fn deinit(self: *RenderCache, alloc: std.mem.Allocator) void {
@@ -836,6 +887,13 @@ const RenderCache = struct {
        self.packed_instances.deinit(alloc);
        self.* = .{};
    }

    fn invalidateAfterResize(self: *RenderCache) void {
        self.cursor_instances.clearRetainingCapacity();
        self.packed_instances.clearRetainingCapacity();
        self.total_instance_count = 0;
        self.layout_dirty = true;
    }
};

const RowPackResult = struct {