5e9ae29b
Fix render cache resize invariants
a73x 2026-04-08 18:58
Commit message
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 { |