a73x

5e9ae29b

Fix render cache resize invariants

a73x   2026-04-08 18:58

Commit message
Fix render cache resize invariants

src/main.zig
Old New
@@ -769,6 +769,57 @@ test "RenderCache deinit resets fields after releasing row storage" {
769 try std.testing.expect(cache.layout_dirty); 769 try std.testing.expect(cache.layout_dirty);
770 } 770 }
771 771
772 test "RenderCache resizeRows to zero clears derived state after live row data" {
773 var cache = RenderCache.empty;
774
775 try cache.resizeRows(std.testing.allocator, 2);
776 cache.rows[0].instances = try makeTestInstances(std.testing.allocator, 1);
777 cache.rows[1].instances = try makeTestInstances(std.testing.allocator, 2);
778
779 var cursor_instances = try makeTestInstances(std.testing.allocator, 1);
780 defer cursor_instances.deinit(std.testing.allocator);
781 try cache.cursor_instances.append(std.testing.allocator, cursor_instances.items[0]);
782
783 var packed_instances = try makeTestInstances(std.testing.allocator, 1);
784 defer packed_instances.deinit(std.testing.allocator);
785 try cache.packed_instances.append(std.testing.allocator, packed_instances.items[0]);
786
787 cache.total_instance_count = 3;
788 cache.layout_dirty = false;
789
790 try cache.resizeRows(std.testing.allocator, 0);
791
792 try std.testing.expectEqual(@as(usize, 0), cache.rows.len);
793 try std.testing.expectEqual(@as(usize, 0), cache.cursor_instances.items.len);
794 try std.testing.expectEqual(@as(usize, 0), cache.packed_instances.items.len);
795 try std.testing.expectEqual(@as(u32, 0), cache.total_instance_count);
796 try std.testing.expect(cache.layout_dirty);
797
798 cache.deinit(std.testing.allocator);
799 }
800
801 test "RenderCache resizeRows truncates populated tail rows and preserves prefix only" {
802 var cache = RenderCache.empty;
803 defer cache.deinit(std.testing.allocator);
804
805 try cache.resizeRows(std.testing.allocator, 3);
806 cache.rows[0].instances = try makeTestInstances(std.testing.allocator, 1);
807 cache.rows[1].instances = try makeTestInstances(std.testing.allocator, 2);
808 cache.rows[2].instances = try makeTestInstances(std.testing.allocator, 3);
809
810 cache.rows[0].gpu_offset_instances = 10;
811 cache.rows[1].gpu_offset_instances = 20;
812 cache.rows[2].gpu_offset_instances = 30;
813
814 try cache.resizeRows(std.testing.allocator, 1);
815
816 try std.testing.expectEqual(@as(usize, 1), cache.rows.len);
817 try std.testing.expectEqual(@as(usize, 1), cache.rows[0].instances.items.len);
818 try std.testing.expectEqual(@as(u32, 10), cache.rows[0].gpu_offset_instances);
819 try std.testing.expectEqual(@as(u32, 0), cache.total_instance_count);
820 try std.testing.expect(cache.layout_dirty);
821 }
822
772 const RowInstanceCache = struct { 823 const RowInstanceCache = struct {
773 instances: std.ArrayListUnmanaged(renderer.Instance) = .empty, 824 instances: std.ArrayListUnmanaged(renderer.Instance) = .empty,
774 gpu_offset_instances: u32 = 0, 825 gpu_offset_instances: u32 = 0,
@@ -791,8 +842,8 @@ const RenderCache = struct {
791 842
792 fn resizeRows(self: *RenderCache, alloc: std.mem.Allocator, row_count: usize) !void { 843 fn resizeRows(self: *RenderCache, alloc: std.mem.Allocator, row_count: usize) !void {
793 if (self.rows.len == row_count) return; 844 if (self.rows.len == row_count) return;
845 const old_rows = self.rows;
794 if (row_count == 0) { 846 if (row_count == 0) {
795 const old_rows = self.rows;
796 if (old_rows.len > 0) { 847 if (old_rows.len > 0) {
797 var row_idx: usize = 0; 848 var row_idx: usize = 0;
798 while (row_idx < old_rows.len) : (row_idx += 1) { 849 while (row_idx < old_rows.len) : (row_idx += 1) {
@@ -801,23 +852,22 @@ const RenderCache = struct {
801 alloc.free(old_rows); 852 alloc.free(old_rows);
802 } 853 }
803 self.rows = &.{}; 854 self.rows = &.{};
855 self.invalidateAfterResize();
804 return; 856 return;
805 } 857 }
806 858
807 const old_rows = self.rows;
808 var new_rows = try alloc.alloc(RowInstanceCache, row_count); 859 var new_rows = try alloc.alloc(RowInstanceCache, row_count);
809 for (new_rows) |*row| row.* = .{}; 860 for (new_rows) |*row| row.* = .{};
810 861
811 const copy_len = @min(old_rows.len, row_count); 862 const copy_len = @min(old_rows.len, row_count);
812 if (copy_len > 0) { 863 var row_idx: usize = 0;
813 @memcpy(new_rows[0..copy_len], old_rows[0..copy_len]); 864 while (row_idx < copy_len) : (row_idx += 1) {
865 // Preserve only the surviving prefix by moving ownership row-by-row.
866 new_rows[row_idx] = old_rows[row_idx];
867 old_rows[row_idx] = .{};
814 } 868 }
815 869 while (row_idx < old_rows.len) : (row_idx += 1) {
816 if (row_count < old_rows.len) { 870 old_rows[row_idx].deinit(alloc);
817 var row_idx = row_count;
818 while (row_idx < old_rows.len) : (row_idx += 1) {
819 old_rows[row_idx].deinit(alloc);
820 }
821 } 871 }
822 872
823 if (old_rows.len > 0) { 873 if (old_rows.len > 0) {
@@ -825,6 +875,7 @@ const RenderCache = struct {
825 } 875 }
826 876
827 self.rows = new_rows; 877 self.rows = new_rows;
878 self.invalidateAfterResize();
828 } 879 }
829 880
830 fn deinit(self: *RenderCache, alloc: std.mem.Allocator) void { 881 fn deinit(self: *RenderCache, alloc: std.mem.Allocator) void {
@@ -836,6 +887,13 @@ const RenderCache = struct {
836 self.packed_instances.deinit(alloc); 887 self.packed_instances.deinit(alloc);
837 self.* = .{}; 888 self.* = .{};
838 } 889 }
890
891 fn invalidateAfterResize(self: *RenderCache) void {
892 self.cursor_instances.clearRetainingCapacity();
893 self.packed_instances.clearRetainingCapacity();
894 self.total_instance_count = 0;
895 self.layout_dirty = true;
896 }
839 }; 897 };
840 898
841 const RowPackResult = struct { 899 const RowPackResult = struct {