├── .gitignore ├── _config.yml ├── LICENSE.txt ├── README.md └── src ├── test.zig └── sparse_set.zig /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | docs/ -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | ------------------------------------------------------------------------------ 3 | This software is available under 2 licenses -- choose whichever you prefer. 4 | ------------------------------------------------------------------------------ 5 | ALTERNATIVE A - MIT License 6 | Copyright (c) 2019 Anders Elfgren 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | this software and associated documentation files (the "Software"), to deal in 9 | the Software without restriction, including without limitation the rights to 10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 11 | of the Software, and to permit persons to whom the Software is furnished to do 12 | so, subject to the following conditions: 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | ------------------------------------------------------------------------------ 23 | ALTERNATIVE B - Public Domain (www.unlicense.org) 24 | This is free and unencumbered software released into the public domain. 25 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this 26 | software, either in source code form or as a compiled binary, for any purpose, 27 | commercial or non-commercial, and by any means. 28 | In jurisdictions that recognize copyright laws, the author or authors of this 29 | software dedicate any and all copyright interest in the software to the public 30 | domain. We make this dedication for the benefit of the public at large and to 31 | the detriment of our heirs and successors. We intend this dedication to be an 32 | overt act of relinquishment in perpetuity of all present and future rights to 33 | this software under copyright law. 34 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 37 | AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 38 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 39 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 40 | ------------------------------------------------------------------------------ 41 | */ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :ferris_wheel: zig-sparse-set :ferris_wheel: 2 | 3 | An implementation of Sparse Sets for Zig. 4 | 5 | ## :confused: What is a Sparse Set? :confused: 6 | 7 | A Sparse Set - well, this implementation technique specifically at least - is a fairly simple data structure with some properties that make it especially useful for some aspects in game development, but you know... it's probably interesting for other areas too. 8 | 9 | Here's a good introduction: https://research.swtch.com/sparse 10 | 11 | Basically, sparse sets solve this problem: You have a bunch of ***sparse*** handles, but you want to loop over the values they represent linearly over memory. 12 | 13 | ## :point_down: Example :point_down: 14 | 15 | Maybe your game has a few hundred **entities** with a certain **component** (specific piece of game data) at any given time. An entity is a 16 bit ID (handle) and the entities containing this component eventually gets spread out randomly over 0..65535 during runtime. 16 | 17 | :anguished: In your frame update function, it would be nice to **not** do this... :anguished: 18 | 19 | ```zig 20 | for (active_entities) |entity| { 21 | var some_big_component = &self.big_components[entity]; 22 | some_big_component.x += 3; 23 | } 24 | ``` 25 | 26 | ... because it would waste memory. Or, in the case that the component isn't huge, but it's expensive to instantiate (i.e. you can't just zero-init it, for example), you need to do that 65k times at startup. Additionally, you'll be skipping over swaths of memory, so every access will likely be a cache miss. 27 | 28 | :worried: Similarly, you might want to avoid this... :worried: 29 | 30 | ```zig 31 | for (self.components) |some_component| { 32 | if (some_component.enabled) { 33 | some_component.x += 3; 34 | } 35 | } 36 | ``` 37 | 38 | ...because you'll be doing a lot of looping over data that isn't of interest. Also, you need to store a flag for every component. So potentially cache misses for the `+=` operation here too. 39 | 40 | (Note that the flag could be implemented as a bitset lookup, for example, which would probably be better, but still have the same underlying problem). 41 | 42 | :heart_eyes: With a sparse set, you can always simply loop over the data linearly: :heart_eyes: 43 | 44 | ```zig 45 | for (self.component_set.toValueSlice()) |*some_component| { 46 | some_component.x += 3; 47 | } 48 | ``` 49 | 50 | ## :sunglasses: But wait, there's more! :sunglasses: 51 | 52 | 1) **O(1)** Lookup from sparse to dense, and vice versa. 53 | 2) **O(1)** **Has,** **Add**, and **Remove**. 54 | 3) **O(1)** **Clear** (remove all elements). 55 | 4) **O(d)** iteration (dense list). 56 | 5) Elements of sparse and dense lists do not need to be (and are not) initialized upon startup - they are undefined. 57 | 6) Supports SOA-style component layout. (See **References** below if you're unfamiliar with what that is) 58 | 7) Supports AOS-style too by optionally storing and managing a value array internally. 59 | 8) Can be inspected "easily" in a debugger. 60 | 9) Optional error-handling. 61 | 10) Even if you don't need to loop over the values, a sparse set is a potential alternative to a hash map. 62 | 11) Optionally growable. 63 | 64 | :star: [1] This is nice and important because you can then do: 65 | 66 | ```zig 67 | for (self.component_set.toValueSlice()) |*some_component, dense_index| { 68 | some_component.x += 3; 69 | var entity = self.component_set.getByDense(dense_index); 70 | self.some_other_system.doStuffWithEntity(entity, some_component.x, 1234567); 71 | } 72 | ``` 73 | 74 | :star: [2] The O(1) remove is important. It is solved by swapping in the last element into the removed spot. So there's two things to consider there: 75 | 76 | 1) Is it cheap to copy the data? Like, if your component is large or needs some kind of allocation logic on copy. 77 | 2) Is it OK that the list of components are **unsorted**? If not, sparse sets are not a good fit. 78 | 79 | :star: [5] Special care has been taken (depending on a couple of coming Zig changes) to ensure that neither Valgrind nor Zig will complain about possibly accessing uninitialized memory. 80 | 81 | :star: [6] With the standard SparseSet implementation, it doesn't actually store any data - you have to do that manually. If you want to, you can store it in an SOA - "Structure of Arrays" manner, like so: 82 | 83 | (**Note:** For both the following SOA example and the AOS example below, you can look at src/test.zig for the whole example.) 84 | 85 | ```zig 86 | const Entity = u32; 87 | const Vec3 = struct { 88 | x: f32 = 0, 89 | y: f32 = 0, 90 | z: f32 = 0, 91 | }; 92 | const MyPositionSystemSOA = struct { 93 | component_set: sparse_set.SparseSet(Entity, u8) = undefined, 94 | xs: [256]f32 = [_]f32{0} ** 256, 95 | ys: [256]f32 = [_]f32{0} ** 256, 96 | zs: [256]f32 = [_]f32{0} ** 256, 97 | ``` 98 | 99 | The trick then is to **make sure** you handle the dense indices: 100 | 101 | ```zig 102 | const MyPositionSystem = struct { 103 | // ... 104 | 105 | pub fn addComp(self: *Self, ent: Entity, pos: Vec3) void { 106 | var dense = self.component_set.add(ent); 107 | self.xs[dense] = pos.x; 108 | self.ys[dense] = pos.y; 109 | self.zs[dense] = pos.z; 110 | } 111 | 112 | pub fn removeComp(self: *Self, ent: Entity) void { 113 | var dense_old: u8 = undefined; 114 | var dense_new: u8 = undefined; 115 | self.component_set.removeWithInfo(ent, &dense_old, &dense_new); 116 | self.xs[dense_new] = self.xs[dense_old]; 117 | self.ys[dense_new] = self.ys[dense_old]; 118 | self.zs[dense_new] = self.zs[dense_old]; 119 | } 120 | 121 | pub fn updateComps(self: *Self) void { 122 | for (self.component_set.toSparseSlice()) |ent, dense| { 123 | self.xs[dense] += 3; 124 | } 125 | } 126 | }; 127 | ``` 128 | 129 | :star: [7] With SparseSetAOS, things are simplified for you, and this will probably be the most common use case. It has the same API but has a few additional functions, and also stores an internal list of all the data. 130 | 131 | ```zig 132 | const MyPositionSystemAOS = struct { 133 | component_set: sparse_set_aos.SparseSetAOS(Entity, u8, Vec3) = undefined, 134 | 135 | // ... 136 | 137 | pub fn addComp(self: *Self, ent: Entity, pos: Vec3) void { 138 | _ = self.component_set.add(ent, pos); 139 | } 140 | 141 | pub fn removeComp(self: *Self, ent: Entity) void { 142 | self.component_set.remove(ent); 143 | } 144 | 145 | pub fn getComp(self: *Self, ent: Entity) Vec3 { 146 | return self.component_set.getValueBySparse(ent).*; 147 | } 148 | 149 | pub fn updateComps(self: Self) void { 150 | for (self.component_set.toValueSlice()) |*value, dense| { 151 | value.x += 3; 152 | } 153 | } 154 | ``` 155 | 156 | :star: [8] Compared to a sparse-handle-as-index lookup, you don't have a long list of "duds" between each valid element. And compared to a hash map, it should be more straightforward to introspect the sparse set's linear list. 157 | 158 | :star: [9] All functions that can assert from bad usage (e.g. adding more handles than the capacity, or indexing out of bounds) also has a corresponding "OrError" function that does Zig-style error handling. 159 | 160 | ## :smiling_imp: Hold up... What's the catch? :smiling_imp: 161 | 162 | 1) Well, there's the unsorted thing. 163 | 2) If you remove things frequently, and/or your objects are expensive to copy, it may outweigh the benefit of having the data continuous in memory. 164 | 3) The lookup requires `@sizeOf(DenseT) * MaxSparseValue` bytes. So for the example above, if you know that you will never have more than 256 of a specific component, then you can store the dense index as a `u8`. This would result in you needing `65536 * 1 byte = 64 kilobytes`. If you need more than 256 components, and say only 16k entities, you'd need 32 kilobytes. 165 | * **Note:** The dense -> sparse lookup is likely significantly smaller: `@sizeof(SparseT) * MaxDenseValue`, so for example `256 * 2 bytes = 512 bytes`. 166 | 4) If you don't need to loop over the elements, and are starved for memory, a hash map might be a better option. 167 | 5) Compared to looking the value up directly using the sparse handle as an array index, there's an extra indirection. 168 | 6) Using uninitialized memory may cause some validators to complain. As mentioned above, Valgrind and Zig should be fine. 169 | 170 | So, all in all, there are of course benefits and drawbacks to sparse sets. You'll have to consider this on a case-by-case basis. 171 | 172 | ## :page_with_curl: License :page_with_curl: 173 | 174 | Pick your license: Public Domain (Unlicense) or MIT. 175 | 176 | ## :statue_of_liberty: Examples :statue_of_liberty: 177 | 178 | See `src/test.zig`. Especially the MyPositionSystem ones. 179 | 180 | Here is the "unit test" that is used for generating documentation, it uses all of the functionality: 181 | 182 | ```zig 183 | test "docs" { 184 | const Entity = u32; 185 | const DenseT = u8; 186 | const DocValueT = i32; 187 | const DocsSparseSet = SparseSet(.{ 188 | .SparseT = Entity, 189 | .DenseT = DenseT, 190 | .ValueT = DocValueT, 191 | .allow_resize = .NoResize, 192 | .value_layout = .InternalArrayOfStructs, 193 | }); 194 | 195 | var ss = DocsSparseSet.init(std.debug.global_allocator, 128, 8) catch unreachable; 196 | defer ss.deinit(); 197 | 198 | var ent1: Entity = 1; 199 | var ent2: Entity = 2; 200 | _ = try ss.addOrError(ent1); 201 | _ = try ss.addValueOrError(ent2, 2); 202 | std.testing.expectEqual(@as(DenseT, 2), ss.len()); 203 | try ss.removeOrError(ent1); 204 | var old: DenseT = undefined; 205 | var new: DenseT = undefined; 206 | try ss.removeWithInfoOrError(ent2, &old, &new); 207 | _ = ss.toSparseSlice(); 208 | _ = ss.toValueSlice(); 209 | std.testing.expectEqual(@as(DenseT, 0), ss.len()); 210 | ss.clear(); 211 | std.testing.expectEqual(@as(DenseT, 8), ss.remainingCapacity()); 212 | 213 | _ = try ss.addValueOrError(ent1, 10); 214 | std.testing.expectEqual(@as(DenseT, 0), try ss.getBySparseOrError(ent1)); 215 | std.testing.expectEqual(@as(DocValueT, 10), (try ss.getValueBySparseOrError(ent1)).*); 216 | std.testing.expectEqual(@as(Entity, ent1), try ss.getByDenseOrError(0)); 217 | std.testing.expectEqual(@as(DocValueT, 10), (try ss.getValueByDenseOrError(0)).*); 218 | } 219 | ``` 220 | 221 | ## :paperclip: References :paperclip: 222 | 223 | * [incrediblejr's](https://gist.github.com/incrediblejr) C implementation [ijss](https://gist.github.com/incrediblejr/931efb7587e1ab328fa65ecc94d1009f). 224 | * The above [article](https://research.swtch.com/sparse) describing this technique. 225 | * The [Zig](https://ziglang.org/) language. 226 | * Jonathan Blow SOA/AOS [video](https://youtu.be/YGTZr6bmNmk) overview. 227 | * My [twitter](https://twitter.com/Srekel). 228 | -------------------------------------------------------------------------------- /src/test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const testing = std.testing; 4 | const SparseSet = @import("sparse_set.zig").SparseSet; 5 | 6 | const Entity = u32; 7 | const DenseT = u8; 8 | 9 | const Vec3 = struct { 10 | x: f32 = 0, 11 | y: f32 = 0, 12 | z: f32 = 0, 13 | }; 14 | 15 | const DefaultTestSparseSet = SparseSet(.{ 16 | .SparseT = Entity, 17 | .DenseT = DenseT, 18 | .allow_resize = .NoResize, 19 | .value_layout = .ExternalStructOfArraysSupport, 20 | }); 21 | 22 | const ResizableDefaultTestSparseSet = SparseSet(.{ 23 | .SparseT = Entity, 24 | .DenseT = DenseT, 25 | .allow_resize = .ResizeAllowed, 26 | .value_layout = .ExternalStructOfArraysSupport, 27 | }); 28 | 29 | const DefaultTestAOSSimpleSparseSet = SparseSet(.{ 30 | .SparseT = Entity, 31 | .DenseT = DenseT, 32 | .ValueT = i32, 33 | .allow_resize = .NoResize, 34 | .value_layout = .InternalArrayOfStructs, 35 | }); 36 | 37 | const DefaultTestAOSSystemSparseSet = SparseSet(.{ 38 | .SparseT = Entity, 39 | .DenseT = DenseT, 40 | .ValueT = Vec3, 41 | .allow_resize = .NoResize, 42 | .value_layout = .InternalArrayOfStructs, 43 | }); 44 | 45 | const DefaultTestAOSVec3ResizableSparseSet = SparseSet(.{ 46 | .SparseT = Entity, 47 | .DenseT = DenseT, 48 | .ValueT = Vec3, 49 | .allow_resize = .ResizeAllowed, 50 | .value_layout = .InternalArrayOfStructs, 51 | .value_init = .ZeroInitialized, 52 | }); 53 | 54 | test "init safe" { 55 | var ss = DefaultTestSparseSet.init(std.testing.allocator, 128, 8) catch unreachable; 56 | try testing.expectEqual(@as(DefaultTestSparseSet.DenseCapacityT, @intCast(0)), ss.len()); 57 | for (ss.sparse_to_dense, 0..) |dense_undefined, sparse| { 58 | _ = dense_undefined; 59 | const usparse = @as(Entity, @intCast(sparse)); 60 | try testing.expect(!(ss.hasSparse(usparse))); 61 | } 62 | try testing.expectEqual(@as(DefaultTestSparseSet.DenseCapacityT, @intCast(0)), ss.len()); 63 | ss.deinit(); 64 | } 65 | 66 | test "add / remove safe 1" { 67 | var ss = DefaultTestSparseSet.init(std.testing.allocator, 128, 8) catch unreachable; 68 | defer ss.deinit(); 69 | 70 | for (ss.dense_to_sparse, 0..) |sparse_undefined, sparse| { 71 | _ = sparse_undefined; 72 | const usparse = @as(Entity, @intCast(sparse)) + 10; 73 | const dense_new = ss.add(usparse); 74 | try testing.expectEqual(@as(DefaultTestSparseSet.DenseCapacityT, @intCast(sparse)), dense_new); 75 | try testing.expect(ss.hasSparse(usparse)); 76 | try testing.expectEqual(dense_new, ss.getBySparse(usparse)); 77 | try testing.expectEqual(usparse, ss.getByDense(dense_new)); 78 | } 79 | try testing.expectError(error.OutOfBounds, ss.addOrError(1)); 80 | try testing.expectEqual(@as(DefaultTestSparseSet.DenseCapacityT, @intCast(0)), ss.remainingCapacity()); 81 | 82 | ss.clear(); 83 | try testing.expect(!(ss.hasSparse(1))); 84 | } 85 | 86 | test "add / remove safe 2" { 87 | var ss = DefaultTestSparseSet.init(std.testing.allocator, 128, 8) catch unreachable; 88 | defer ss.deinit(); 89 | 90 | try testing.expect(!(ss.hasSparse(1))); 91 | try testing.expectEqual(@as(DefaultTestSparseSet.DenseCapacityT, @intCast(0)), ss.add(1)); 92 | try testing.expect(ss.hasSparse(1)); 93 | try testing.expectError(error.AlreadyRegistered, ss.addOrError(1)); 94 | ss.remove(1); 95 | try testing.expect(!(ss.hasSparse(1))); 96 | } 97 | 98 | test "add / remove safe 3" { 99 | var ss = DefaultTestSparseSet.init(std.testing.allocator, 128, 8) catch unreachable; 100 | defer ss.deinit(); 101 | 102 | for (ss.dense_to_sparse, 0..) |sparse_undefined, sparse| { 103 | _ = sparse_undefined; 104 | const usparse = @as(Entity, @intCast(sparse)) + 10; 105 | _ = ss.add(usparse); 106 | } 107 | 108 | try testing.expectEqual(@as(DefaultTestSparseSet.DenseCapacityT, @intCast(0)), ss.remainingCapacity()); 109 | try testing.expect(!(ss.hasSparse(5))); 110 | try testing.expect(ss.hasSparse(15)); 111 | ss.remove(15); 112 | try testing.expect(!(ss.hasSparse(15))); 113 | try testing.expectEqual(@as(DefaultTestSparseSet.DenseCapacityT, @intCast(1)), ss.remainingCapacity()); 114 | _ = ss.add(15); 115 | try testing.expect(ss.hasSparse(15)); 116 | try testing.expectEqual(@as(DefaultTestSparseSet.DenseCapacityT, @intCast(0)), ss.remainingCapacity()); 117 | } 118 | 119 | test "AOS" { 120 | var ss = DefaultTestAOSSimpleSparseSet.init(std.testing.allocator, 128, 8) catch unreachable; 121 | defer ss.deinit(); 122 | 123 | for (ss.dense_to_sparse, 0..) |sparse_undefined, sparse| { 124 | _ = sparse_undefined; 125 | const usparse = @as(Entity, @intCast(sparse)) + 10; 126 | const value = -@as(i32, @intCast(sparse)); 127 | const dense_new = ss.addValue(usparse, value); 128 | try testing.expectEqual(@as(DenseT, @intCast(sparse)), dense_new); 129 | try testing.expect(ss.hasSparse(usparse)); 130 | try testing.expectEqual(dense_new, ss.getBySparse(usparse)); 131 | try testing.expectEqual(usparse, ss.getByDense(dense_new)); 132 | try testing.expectEqual(value, (ss.getValueByDense(dense_new)).*); 133 | try testing.expectEqual(value, ss.getValueBySparse(usparse).*); 134 | } 135 | try testing.expectEqual(@as(DefaultTestSparseSet.DenseCapacityT, @intCast(0)), ss.remainingCapacity()); 136 | 137 | ss.clear(); 138 | try testing.expect(!ss.hasSparse(1)); 139 | } 140 | 141 | test "AOS system" { 142 | var sys = MyPositionSystemAOS.init(); 143 | defer sys.deinit(); 144 | 145 | const ent1: Entity = 10; 146 | const ent2: Entity = 20; 147 | const v1 = Vec3{ .x = 10, .y = 0, .z = 0 }; 148 | const v2 = Vec3{ .x = 20, .y = 0, .z = 0 }; 149 | sys.addComp(ent1, v1); 150 | sys.addComp(ent2, v2); 151 | try testing.expectEqual(v1, sys.getComp(ent1)); 152 | try testing.expectEqual(v2, sys.getComp(ent2)); 153 | try testing.expectEqual(v1, sys.component_set.values[0]); 154 | try testing.expectEqual(v2, sys.component_set.values[1]); 155 | try testing.expectEqual(@as(DenseT, 0), sys.component_set.getBySparse(ent1)); 156 | try testing.expectEqual(@as(DenseT, 1), sys.component_set.getBySparse(ent2)); 157 | 158 | sys.removeComp(ent1); 159 | try testing.expectEqual(v2, sys.getComp(ent2)); 160 | try testing.expectEqual(v2, sys.component_set.values[0]); 161 | try testing.expectEqual(@as(DenseT, 0), sys.component_set.getBySparse(ent2)); 162 | 163 | sys.updateComps(); 164 | try testing.expectEqual(Vec3{ .x = 23, .y = 0, .z = 0 }, sys.getComp(ent2)); 165 | } 166 | 167 | test "SOA system" { 168 | var sys = MyPositionSystemSOA.init(); 169 | defer sys.deinit(); 170 | 171 | const ent1: Entity = 10; 172 | const ent2: Entity = 20; 173 | const v1 = Vec3{ .x = 10, .y = 0, .z = 0 }; 174 | const v2 = Vec3{ .x = 20, .y = 0, .z = 0 }; 175 | sys.addComp(ent1, v1); 176 | sys.addComp(ent2, v2); 177 | try testing.expectEqual(v1, sys.getComp(ent1)); 178 | try testing.expectEqual(v2, sys.getComp(ent2)); 179 | try testing.expectEqual(@as(DenseT, 0), sys.component_set.getBySparse(ent1)); 180 | try testing.expectEqual(@as(DenseT, 1), sys.component_set.getBySparse(ent2)); 181 | 182 | sys.removeComp(ent1); 183 | try testing.expectEqual(v2, sys.getComp(ent2)); 184 | try testing.expectEqual(@as(DenseT, 0), sys.component_set.getBySparse(ent2)); 185 | 186 | sys.updateComps(); 187 | try testing.expectEqual(Vec3{ .x = 23, .y = 0, .z = 0 }, sys.getComp(ent2)); 188 | } 189 | 190 | test "SOA resize true" { 191 | var ss = ResizableDefaultTestSparseSet.init(std.testing.allocator, 128, 8) catch unreachable; 192 | defer ss.deinit(); 193 | 194 | try testing.expectError(error.OutOfBounds, ss.hasSparseOrError(500)); 195 | 196 | for (ss.dense_to_sparse, 0..) |sparse_undefined, sparse| { 197 | _ = sparse_undefined; 198 | const usparse = @as(Entity, @intCast(sparse)) + 10; 199 | _ = ss.add(usparse); 200 | try testing.expect(ss.hasSparse(usparse)); 201 | } 202 | 203 | try testing.expect(!ss.hasSparse(18)); 204 | try testing.expectEqual(@as(DenseT, @intCast(8)), ss.add(18)); 205 | try testing.expect(ss.hasSparse(18)); 206 | try testing.expect(!ss.hasSparse(19)); 207 | try testing.expectEqual(@as(u32, @intCast(16)), @as(u32, @intCast(ss.dense_to_sparse.len))); 208 | try testing.expectEqual(@as(DefaultTestSparseSet.DenseCapacityT, @intCast(7)), ss.remainingCapacity()); 209 | try testing.expectEqual(@as(Entity, @intCast(10)), ss.dense_to_sparse[0]); 210 | try testing.expectEqual(@as(Entity, @intCast(11)), ss.dense_to_sparse[1]); 211 | try testing.expectEqual(@as(Entity, @intCast(12)), ss.dense_to_sparse[2]); 212 | try testing.expectEqual(@as(Entity, @intCast(13)), ss.dense_to_sparse[3]); 213 | try testing.expectEqual(@as(Entity, @intCast(16)), ss.dense_to_sparse[6]); 214 | try testing.expectEqual(@as(Entity, @intCast(17)), ss.dense_to_sparse[7]); 215 | try testing.expectEqual(@as(Entity, @intCast(18)), ss.dense_to_sparse[8]); 216 | 217 | ss.clear(); 218 | try testing.expect(!(ss.hasSparse(1))); 219 | } 220 | 221 | test "AOS resize true" { 222 | var ss = DefaultTestAOSVec3ResizableSparseSet.init(std.testing.allocator, 128, 8) catch unreachable; 223 | defer ss.deinit(); 224 | 225 | try testing.expectError(error.OutOfBounds, ss.hasSparseOrError(500)); 226 | 227 | for (ss.dense_to_sparse, 0..) |sparse_undefined, sparse| { 228 | _ = sparse_undefined; 229 | const usparse = @as(Entity, @intCast(sparse)) + 10; 230 | const value = Vec3{ .x = @as(f32, @floatFromInt(sparse)), .y = 0, .z = 0 }; 231 | _ = ss.addValue(usparse, value); 232 | try testing.expect(ss.hasSparse(usparse)); 233 | try testing.expectEqual(value, ss.getValueBySparse(usparse).*); 234 | } 235 | 236 | try testing.expect(!ss.hasSparse(18)); 237 | try testing.expectEqual(@as(DenseT, @intCast(8)), ss.addValue(18, Vec3{ .x = 8, .y = 0, .z = 0 })); 238 | try testing.expect(ss.hasSparse(18)); 239 | try testing.expect(!ss.hasSparse(19)); 240 | try testing.expectEqual(@as(DefaultTestSparseSet.DenseCapacityT, @intCast(7)), ss.remainingCapacity()); 241 | try testing.expectEqual(Vec3{ .x = 0, .y = 0, .z = 0 }, ss.getValueBySparse(10).*); 242 | try testing.expectEqual(Vec3{ .x = 1, .y = 0, .z = 0 }, ss.getValueBySparse(11).*); 243 | try testing.expectEqual(Vec3{ .x = 2, .y = 0, .z = 0 }, ss.getValueBySparse(12).*); 244 | try testing.expectEqual(Vec3{ .x = 3, .y = 0, .z = 0 }, ss.getValueBySparse(13).*); 245 | try testing.expectEqual(Vec3{ .x = 4, .y = 0, .z = 0 }, ss.getValueBySparse(14).*); 246 | try testing.expectEqual(Vec3{ .x = 5, .y = 0, .z = 0 }, ss.getValueBySparse(15).*); 247 | try testing.expectEqual(Vec3{ .x = 6, .y = 0, .z = 0 }, ss.getValueBySparse(16).*); 248 | try testing.expectEqual(Vec3{ .x = 7, .y = 0, .z = 0 }, ss.getValueBySparse(17).*); 249 | try testing.expectEqual(Vec3{ .x = 8, .y = 0, .z = 0 }, ss.getValueBySparse(18).*); 250 | 251 | ss.clear(); 252 | try testing.expect(!(ss.hasSparse(1))); 253 | } 254 | 255 | const MyPositionSystemAOS = struct { 256 | component_set: DefaultTestAOSSystemSparseSet = undefined, 257 | const Self = @This(); 258 | 259 | pub fn init() MyPositionSystemAOS { 260 | return Self{ 261 | .component_set = DefaultTestAOSSystemSparseSet.init(std.testing.allocator, 128, 8) catch unreachable, 262 | }; 263 | } 264 | 265 | pub fn deinit(self: *MyPositionSystemAOS) void { 266 | self.component_set.deinit(); 267 | } 268 | 269 | pub fn addComp(self: *Self, ent: Entity, pos: Vec3) void { 270 | _ = self.component_set.addValue(ent, pos); 271 | } 272 | 273 | pub fn removeComp(self: *Self, ent: Entity) void { 274 | self.component_set.remove(ent); 275 | } 276 | 277 | pub fn getComp(self: *Self, ent: Entity) Vec3 { 278 | return self.component_set.getValueBySparse(ent).*; 279 | } 280 | 281 | pub fn updateComps(self: Self) void { 282 | for (self.component_set.toValueSlice()) |*value| { 283 | value.x += 3; 284 | } 285 | } 286 | }; 287 | 288 | const MyPositionSystemSOA = struct { 289 | component_set: DefaultTestSparseSet = undefined, 290 | xs: [256]f32 = [_]f32{0} ** 256, 291 | ys: [256]f32 = [_]f32{0} ** 256, 292 | zs: [256]f32 = [_]f32{0} ** 256, 293 | const Self = @This(); 294 | 295 | pub fn init() MyPositionSystemSOA { 296 | return Self{ 297 | .component_set = DefaultTestSparseSet.init(std.testing.allocator, 128, 8) catch unreachable, 298 | }; 299 | } 300 | 301 | pub fn deinit(self: *MyPositionSystemSOA) void { 302 | self.component_set.deinit(); 303 | } 304 | 305 | pub fn addComp(self: *Self, ent: Entity, pos: Vec3) void { 306 | const dense = self.component_set.add(ent); 307 | self.xs[dense] = pos.x; 308 | self.ys[dense] = pos.y; 309 | self.zs[dense] = pos.z; 310 | } 311 | 312 | pub fn removeComp(self: *Self, ent: Entity) void { 313 | var dense_old: DenseT = undefined; 314 | var dense_new: DenseT = undefined; 315 | self.component_set.removeWithInfo(ent, &dense_old, &dense_new); 316 | self.xs[dense_new] = self.xs[dense_old]; 317 | self.ys[dense_new] = self.ys[dense_old]; 318 | self.zs[dense_new] = self.zs[dense_old]; 319 | } 320 | 321 | pub fn getComp(self: *Self, ent: Entity) Vec3 { 322 | const dense = self.component_set.getBySparse(ent); 323 | return Vec3{ .x = self.xs[dense], .y = self.ys[dense], .z = self.zs[dense] }; 324 | } 325 | 326 | pub fn updateComps(self: *Self) void { 327 | for (self.component_set.toSparseSlice(), 0..) |ent, dense| { 328 | _ = ent; 329 | self.xs[dense] += 3; 330 | } 331 | } 332 | }; 333 | -------------------------------------------------------------------------------- /src/sparse_set.zig: -------------------------------------------------------------------------------- 1 | //! Sparse Set 2 | //! 3 | //! Version 1.0.2 4 | //! 5 | //! See https://github.com/Srekel/zig-sparse-set for latest version and documentation. 6 | //! 7 | //! See unit tests for usage examples. 8 | //! 9 | //! Dual license: Unlicense / MIT 10 | const std = @import("std"); 11 | const Allocator = std.mem.Allocator; 12 | const assert = std.debug.assert; 13 | 14 | pub const AllowResize = union(enum) { 15 | /// The fields **dense_to_sparse** and **values** will grow on **add()** and **addValue()**. 16 | ResizeAllowed, 17 | 18 | /// Errors will be generated when adding more elements than **capacity_dense**. 19 | NoResize, 20 | }; 21 | 22 | pub const ValueLayout = union(enum) { 23 | /// AOS style. 24 | InternalArrayOfStructs, 25 | 26 | /// SOA style. 27 | ExternalStructOfArraysSupport, 28 | }; 29 | 30 | pub const ValueInitialization = union(enum) { 31 | /// New values added with add() will contain uninitialized/random memory. 32 | Untouched, 33 | 34 | /// New values added with add() will be memset to zero. 35 | ZeroInitialized, 36 | }; 37 | 38 | pub const SparseSetConfig = struct { 39 | /// The type used for the sparse handle. 40 | SparseT: type, 41 | 42 | /// The type used for dense indices. 43 | DenseT: type, 44 | 45 | /// Optional: The type used for values when using **InternalArrayOfStructs**. 46 | ValueT: type = void, 47 | 48 | /// If you only have a single array of structs - AOS - letting SparseSet handle it 49 | /// with **InternalArrayOfStructs** is convenient. If you want to manage the data 50 | /// yourself or if you're using SOA, use **ExternalStructOfArraysSupport**. 51 | value_layout: ValueLayout, 52 | 53 | /// Set to **ZeroInitialized** to make values created with add() be zero initialized. 54 | /// Only valid with **value_layout == .InternalArrayOfStructs**. 55 | /// Defaults to **Untouched**. 56 | value_init: ValueInitialization = .Untouched, 57 | 58 | /// Whether or not the amount of dense indices (and values) can grow. 59 | allow_resize: AllowResize = .NoResize, 60 | }; 61 | 62 | /// Creates a specific Sparse Set type based on the config. 63 | pub fn SparseSet(comptime config: SparseSetConfig) type { 64 | const SparseT = config.SparseT; 65 | const DenseT = config.DenseT; 66 | const ValueT = config.ValueT; 67 | const allow_resize = config.allow_resize; 68 | const value_layout = config.value_layout; 69 | const value_init = config.value_init; 70 | assert((value_layout == .ExternalStructOfArraysSupport) or (ValueT != @TypeOf(void))); 71 | 72 | return struct { 73 | const Self = @This(); 74 | pub const SparseCapacityT = std.math.IntFittingRange(0, std.math.maxInt(SparseT) + 1); 75 | pub const DenseCapacityT = std.math.IntFittingRange(0, std.math.maxInt(DenseT) + 1); 76 | 77 | /// Allocator used for allocating, growing and freeing **dense_to_sparse**, **sparse_to_dense**, and **values**. 78 | allocator: Allocator, 79 | 80 | /// Mapping from dense indices to sparse handles. 81 | dense_to_sparse: []SparseT, 82 | 83 | /// Mapping from sparse handles to dense indices (and values). 84 | sparse_to_dense: []DenseT, 85 | 86 | /// Optional: A list of **ValueT** that is used with **InternalArrayOfStructs**. 87 | values: if (value_layout == .InternalArrayOfStructs) []ValueT else void, 88 | 89 | /// Current amount of used handles. 90 | dense_count: DenseCapacityT, 91 | 92 | /// Amount of dense indices that can be stored. 93 | capacity_dense: DenseCapacityT, 94 | 95 | /// Amount of sparse handles that can be used for lookups. 96 | capacity_sparse: SparseCapacityT, 97 | 98 | /// You can think of **capacity_sparse** as how many entities you want to support, and 99 | /// **capacity_dense** as how many components. 100 | pub fn init( 101 | allocator: Allocator, 102 | capacity_sparse: SparseCapacityT, 103 | capacity_dense: DenseCapacityT, 104 | ) !Self { 105 | // Could be <= but I'm not sure why'd you use a sparse_set if you don't have more sparse 106 | // indices than dense... 107 | assert(capacity_dense < capacity_sparse); 108 | 109 | const dense_to_sparse = try allocator.alloc(SparseT, capacity_dense); 110 | errdefer allocator.free(dense_to_sparse); 111 | const sparse_to_dense = try allocator.alloc(DenseT, capacity_sparse); 112 | errdefer allocator.free(sparse_to_dense); 113 | 114 | var self: Self = undefined; 115 | if (value_layout == .InternalArrayOfStructs) { 116 | const values = try allocator.alloc(ValueT, capacity_dense); 117 | errdefer allocator.free(values); 118 | 119 | self = Self{ 120 | .allocator = allocator, 121 | .dense_to_sparse = dense_to_sparse, 122 | .sparse_to_dense = sparse_to_dense, 123 | .values = values, 124 | .capacity_dense = capacity_dense, 125 | .capacity_sparse = capacity_sparse, 126 | .dense_count = 0, 127 | }; 128 | } else { 129 | self = Self{ 130 | .allocator = allocator, 131 | .dense_to_sparse = dense_to_sparse, 132 | .sparse_to_dense = sparse_to_dense, 133 | .values = {}, 134 | .capacity_dense = capacity_dense, 135 | .capacity_sparse = capacity_sparse, 136 | .dense_count = 0, 137 | }; 138 | } 139 | 140 | // Ensure Valgrind doesn't complain about hasSparse 141 | _ = std.valgrind.memcheck.makeMemDefined(std.mem.asBytes(&self.sparse_to_dense)); 142 | 143 | return self; 144 | } 145 | 146 | /// Deallocates **dense_to_sparse**, **sparse_to_dense**, and optionally **values**. 147 | pub fn deinit(self: Self) void { 148 | self.allocator.free(self.dense_to_sparse); 149 | self.allocator.free(self.sparse_to_dense); 150 | if (value_layout == .InternalArrayOfStructs) { 151 | self.allocator.free(self.values); 152 | } 153 | } 154 | 155 | /// Resets the set cheaply. 156 | pub fn clear(self: *Self) void { 157 | self.dense_count = 0; 158 | } 159 | 160 | /// Returns the amount of allocated handles. 161 | pub fn len(self: Self) DenseCapacityT { 162 | return self.dense_count; 163 | } 164 | 165 | /// Returns a slice that can be used to loop over the sparse handles. 166 | pub fn toSparseSlice(self: Self) []SparseT { 167 | return self.dense_to_sparse[0..self.dense_count]; 168 | } 169 | 170 | // A bit of a hack to comptime add a function 171 | // TODO: Rewrite after https://github.com/ziglang/zig/issues/1717 172 | pub usingnamespace switch (value_layout) { 173 | .InternalArrayOfStructs => struct { 174 | /// Returns a slice that can be used to loop over the values. 175 | pub fn toValueSlice(self: Self) []ValueT { 176 | return self.values[0..self.dense_count]; 177 | } 178 | }, 179 | else => struct {}, 180 | }; 181 | 182 | /// Returns how many dense indices are still available 183 | pub fn remainingCapacity(self: Self) DenseCapacityT { 184 | return self.capacity_dense - self.dense_count; 185 | } 186 | 187 | /// Registers the sparse value and matches it to a dense index. 188 | /// Grows .dense_to_sparse and .values if needed and resizing is allowed. 189 | /// Note: If resizing is allowed, you must use an allocator that you are sure 190 | /// will never fail for your use cases. 191 | /// If that is not an option, use addOrError. 192 | pub fn add(self: *Self, sparse: SparseT) DenseT { 193 | if (allow_resize == .ResizeAllowed) { 194 | if (self.dense_count == self.capacity_dense) { 195 | self.capacity_dense = self.capacity_dense * 2; 196 | self.dense_to_sparse = self.allocator.realloc(self.dense_to_sparse, self.capacity_dense) catch unreachable; 197 | if (value_layout == .InternalArrayOfStructs) { 198 | self.values = self.allocator.realloc(self.values, self.capacity_dense) catch unreachable; 199 | } 200 | } 201 | } 202 | 203 | assert(sparse < self.capacity_sparse); 204 | assert(self.dense_count < self.capacity_dense); 205 | assert(!self.hasSparse(sparse)); 206 | self.dense_to_sparse[self.dense_count] = sparse; 207 | self.sparse_to_dense[sparse] = @intCast(self.dense_count); 208 | if (value_layout == .InternalArrayOfStructs and value_init == .ZeroInitialized) { 209 | self.values[self.dense_count] = std.mem.zeroes(ValueT); 210 | } 211 | 212 | self.dense_count += 1; 213 | return @intCast(self.dense_count - 1); 214 | } 215 | 216 | /// May return error.OutOfBounds or error.AlreadyRegistered, otherwise calls add. 217 | /// Grows .dense_to_sparse and .values if needed and resizing is allowed. 218 | pub fn addOrError(self: *Self, sparse: SparseT) !DenseT { 219 | if (sparse >= self.capacity_sparse) { 220 | return error.OutOfBounds; 221 | } 222 | 223 | if (try self.hasSparseOrError(sparse)) { 224 | return error.AlreadyRegistered; 225 | } 226 | 227 | if (self.dense_count == self.capacity_dense) { 228 | if (allow_resize == .ResizeAllowed) { 229 | self.capacity_dense = self.capacity_dense * 2; 230 | self.dense_to_sparse = try self.allocator.realloc(self.dense_to_sparse, self.capacity_dense); 231 | if (value_layout == .InternalArrayOfStructs) { 232 | self.values = try self.allocator.realloc(self.values, self.capacity_dense); 233 | } 234 | } else { 235 | return error.OutOfBounds; 236 | } 237 | } 238 | 239 | return self.add(sparse); 240 | } 241 | 242 | // TODO: Rewrite after https://github.com/ziglang/zig/issues/1717 243 | pub usingnamespace switch (value_layout) { 244 | .InternalArrayOfStructs => struct { 245 | /// Registers the sparse value and matches it to a dense index 246 | /// Grows .dense_to_sparse and .values if needed and resizing is allowed. 247 | /// Note: If resizing is allowed, you must use an allocator that you are sure 248 | /// will never fail for your use cases. 249 | /// If that is not an option, use addOrError. 250 | pub fn addValue(self: *Self, sparse: SparseT, value: ValueT) DenseT { 251 | if (allow_resize == .ResizeAllowed) { 252 | if (self.dense_count == self.capacity_dense) { 253 | self.capacity_dense = self.capacity_dense * 2; 254 | self.dense_to_sparse = self.allocator.realloc(self.dense_to_sparse, self.capacity_dense) catch unreachable; 255 | if (value_layout == .InternalArrayOfStructs) { 256 | self.values = self.allocator.realloc(self.values, self.capacity_dense) catch unreachable; 257 | } 258 | } 259 | } 260 | 261 | assert(sparse < self.capacity_sparse); 262 | assert(self.dense_count < self.capacity_dense); 263 | assert(!self.hasSparse(sparse)); 264 | self.dense_to_sparse[self.dense_count] = sparse; 265 | self.sparse_to_dense[sparse] = @intCast(self.dense_count); 266 | self.values[self.dense_count] = value; 267 | self.dense_count += 1; 268 | return @intCast(self.dense_count - 1); 269 | } 270 | 271 | /// May return error.OutOfBounds or error.AlreadyRegistered, otherwise calls add. 272 | /// Grows .dense_to_sparse and .values if needed and resizing is allowed. 273 | pub fn addValueOrError(self: *Self, sparse: SparseT, value: ValueT) !DenseT { 274 | if (sparse >= self.capacity_sparse) { 275 | return error.OutOfBounds; 276 | } 277 | 278 | if (try self.hasSparseOrError(sparse)) { 279 | return error.AlreadyRegistered; 280 | } 281 | 282 | if (self.dense_count == self.capacity_dense) { 283 | if (allow_resize == .ResizeAllowed) { 284 | self.capacity_dense = self.capacity_dense * 2; 285 | self.dense_to_sparse = try self.allocator.realloc(self.dense_to_sparse, self.capacity_dense); 286 | if (value_layout == .InternalArrayOfStructs) { 287 | self.values = try self.allocator.realloc(self.values, self.capacity_dense); 288 | } 289 | } else { 290 | return error.OutOfBounds; 291 | } 292 | } 293 | 294 | return self.addValue(sparse, value); 295 | } 296 | }, 297 | else => struct {}, 298 | }; 299 | 300 | /// Removes the sparse/dense index, and replaces it with the last ones. 301 | /// dense_old and dense_new is 302 | pub fn removeWithInfo(self: *Self, sparse: SparseT, dense_old: *DenseT, dense_new: *DenseT) void { 303 | assert(self.dense_count > 0); 304 | assert(self.hasSparse(sparse)); 305 | const last_dense: DenseT = @intCast(self.dense_count - 1); 306 | const last_sparse = self.dense_to_sparse[last_dense]; 307 | const dense = self.sparse_to_dense[sparse]; 308 | self.dense_to_sparse[dense] = last_sparse; 309 | self.sparse_to_dense[last_sparse] = dense; 310 | if (value_layout == .InternalArrayOfStructs) { 311 | self.values[dense] = self.values[last_dense]; 312 | } 313 | 314 | self.dense_count -= 1; 315 | dense_old.* = last_dense; 316 | dense_new.* = dense; 317 | } 318 | 319 | /// May return error.OutOfBounds, otherwise calls removeWithInfo. 320 | pub fn removeWithInfoOrError(self: *Self, sparse: SparseT, dense_old: *DenseT, dense_new: *DenseT) !void { 321 | if (self.dense_count == 0) { 322 | return error.OutOfBounds; 323 | } 324 | 325 | if (!try self.hasSparseOrError(sparse)) { 326 | return error.NotRegistered; 327 | } 328 | 329 | return self.removeWithInfo(sparse, dense_old, dense_new); 330 | } 331 | 332 | /// Like removeWithInfo info, but slightly faster, in case you don't care about the switch. 333 | pub fn remove(self: *Self, sparse: SparseT) void { 334 | assert(self.dense_count > 0); 335 | assert(self.hasSparse(sparse)); 336 | const last_dense = self.dense_count - 1; 337 | const last_sparse = self.dense_to_sparse[last_dense]; 338 | const dense = self.sparse_to_dense[sparse]; 339 | self.dense_to_sparse[dense] = last_sparse; 340 | self.sparse_to_dense[last_sparse] = dense; 341 | if (value_layout == .InternalArrayOfStructs) { 342 | self.values[dense] = self.values[last_dense]; 343 | } 344 | 345 | self.dense_count -= 1; 346 | } 347 | 348 | /// May return error.OutOfBounds or error.NotRegistered, otherwise calls remove. 349 | pub fn removeOrError(self: *Self, sparse: SparseT) !void { 350 | if (self.dense_count == 0) { 351 | return error.OutOfBounds; 352 | } 353 | 354 | if (!try self.hasSparseOrError(sparse)) { 355 | return error.NotRegistered; 356 | } 357 | 358 | self.remove(sparse); 359 | } 360 | 361 | /// Returns true if the sparse is registered to a dense index. 362 | pub fn hasSparse(self: Self, sparse: SparseT) bool { 363 | // Unsure if this call to disable runtime safety is needed - can add later if so. 364 | // Related: https://github.com/ziglang/zig/issues/978 365 | // @setRuntimeSafety(false); 366 | assert(sparse < self.capacity_sparse); 367 | const dense = self.sparse_to_dense[sparse]; 368 | return dense < self.dense_count and self.dense_to_sparse[dense] == sparse; 369 | } 370 | 371 | /// May return error.OutOfBounds, otherwise calls hasSparse. 372 | pub fn hasSparseOrError(self: Self, sparse: SparseT) !bool { 373 | if (sparse >= self.capacity_sparse) { 374 | return error.OutOfBounds; 375 | } 376 | 377 | return self.hasSparse(sparse); 378 | } 379 | 380 | /// Returns corresponding dense index. 381 | pub fn getBySparse(self: Self, sparse: SparseT) DenseT { 382 | assert(self.hasSparse(sparse)); 383 | return self.sparse_to_dense[sparse]; 384 | } 385 | 386 | /// Tries hasSparseOrError, then returns getBySparse. 387 | pub fn getBySparseOrError(self: Self, sparse: SparseT) !DenseT { 388 | if (!try self.hasSparseOrError(sparse)) { 389 | return error.NotRegistered; 390 | } 391 | 392 | return self.getBySparse(sparse); 393 | } 394 | 395 | /// Returns corresponding sparse index. 396 | pub fn getByDense(self: Self, dense: DenseT) SparseT { 397 | assert(dense < self.dense_count); 398 | return self.dense_to_sparse[dense]; 399 | } 400 | 401 | /// Returns OutOfBounds or getByDense. 402 | pub fn getByDenseOrError(self: Self, dense: DenseT) !SparseT { 403 | if (dense >= self.dense_count) { 404 | return error.OutOfBounds; 405 | } 406 | return self.getByDense(dense); 407 | } 408 | 409 | // TODO: Rewrite after https://github.com/ziglang/zig/issues/1717 410 | pub usingnamespace switch (value_layout) { 411 | .InternalArrayOfStructs => struct { 412 | /// Returns a pointer to the SOA value corresponding to the sparse parameter. 413 | pub fn getValueBySparse(self: Self, sparse: SparseT) *ValueT { 414 | assert(self.hasSparse(sparse)); 415 | const dense = self.sparse_to_dense[sparse]; 416 | return &self.values[dense]; 417 | } 418 | 419 | /// First tries hasSparse, then returns getValueBySparse(). 420 | pub fn getValueBySparseOrError(self: Self, sparse: SparseT) !*ValueT { 421 | if (!try self.hasSparseOrError(sparse)) { 422 | return error.NotRegistered; 423 | } 424 | return self.getValueBySparse(sparse); 425 | } 426 | 427 | /// Returns a pointer to the SOA value corresponding to the sparse parameter. 428 | pub fn getValueByDense(self: Self, dense: DenseT) *ValueT { 429 | assert(dense < self.dense_count); 430 | return &self.values[dense]; 431 | } 432 | 433 | /// Returns error.OutOfBounds or getValueByDense(). 434 | pub fn getValueByDenseOrError(self: Self, dense: DenseT) !*ValueT { 435 | if (dense >= self.dense_count) { 436 | return error.OutOfBounds; 437 | } 438 | return self.getValueByDense(dense); 439 | } 440 | }, 441 | else => struct {}, 442 | }; 443 | }; 444 | } 445 | 446 | test "docs" { 447 | const Entity = u32; 448 | const DenseT = u8; 449 | const DocValueT = i32; 450 | const DocsSparseSet = SparseSet(.{ 451 | .SparseT = Entity, 452 | .DenseT = DenseT, 453 | .ValueT = DocValueT, 454 | .allow_resize = .NoResize, 455 | .value_layout = .InternalArrayOfStructs, 456 | }); 457 | 458 | var ss = DocsSparseSet.init(std.testing.allocator, 128, 8) catch unreachable; 459 | defer ss.deinit(); 460 | 461 | const ent1: Entity = 1; 462 | const ent2: Entity = 2; 463 | _ = try ss.addOrError(ent1); 464 | _ = try ss.addValueOrError(ent2, 2); 465 | try std.testing.expectEqual(@as(DocsSparseSet.DenseCapacityT, 2), ss.len()); 466 | try ss.removeOrError(ent1); 467 | var old: DenseT = undefined; 468 | var new: DenseT = undefined; 469 | try ss.removeWithInfoOrError(ent2, &old, &new); 470 | _ = ss.toSparseSlice(); 471 | _ = ss.toValueSlice(); 472 | try std.testing.expectEqual(@as(DocsSparseSet.DenseCapacityT, 0), ss.len()); 473 | ss.clear(); 474 | try std.testing.expectEqual(@as(DocsSparseSet.DenseCapacityT, 8), ss.remainingCapacity()); 475 | 476 | _ = try ss.addValueOrError(ent1, 10); 477 | try std.testing.expectEqual(@as(DenseT, 0), try ss.getBySparseOrError(ent1)); 478 | try std.testing.expectEqual(@as(DocValueT, 10), (try ss.getValueBySparseOrError(ent1)).*); 479 | try std.testing.expectEqual(@as(Entity, ent1), try ss.getByDenseOrError(0)); 480 | try std.testing.expectEqual(@as(DocValueT, 10), (try ss.getValueByDenseOrError(0)).*); 481 | } 482 | --------------------------------------------------------------------------------