const std = @import("std"); const ArrayList = std.ArrayList; const ArrayListUnmanaged = std.ArrayListUnmanaged; // can be index by an 18 bit value, and 262144 of anything ought to be enough... right? pub const DefaultSparseSize = 262144 / 2; pub fn SparseSet(comptime T: type) type { return SparseSetAdvanced(T, DefaultSparseSize); } pub const IndexType = u24; pub const GenerationType = u7; pub const SetHandle = packed struct { index: IndexType = 0, generation: GenerationType = 0, alive: bool = false, pub fn hash(self: @This()) u32 { return @as(u32, @bitCast(self)); } pub fn eql(self: @This(), other: @This()) bool { return self.hash() == other.hash(); } }; pub fn SparseMultiSet(comptime T: type) type { return SparseMultiSetAdvanced(T, DefaultSparseSize); } pub const ContainerListener = struct { ptr: *anyopaque, onHandleRemoved: *const fn (*anyopaque, u32, SetHandle) void, onHandleAdded: *const fn (*anyopaque, u32, SetHandle) void, }; // works by converting a datastructure into an AOS type. pub fn SparseMultiSetAdvanced(comptime T: type, comptime SparseSize: u32) type { return struct { pub const SetType = std.MultiArrayList(T); pub const InnerType = T; allocator: std.mem.Allocator, denseIndices: ArrayListUnmanaged(SetHandle), dense: SetType, sparse: []SetHandle, containerID: u32 = 0, containerListener: ?ContainerListener = null, opCount: u32 = 0, pub const Field = SetType.Field; pub const Slice = SetType.Slice; pub const IsMultiset = true; pub const StableReferences = false; pub fn init(allocator: std.mem.Allocator) @This() { var self = @This(){ .allocator = allocator, .denseIndices = .{}, .dense = .{}, .sparse = allocator.alloc(SetHandle, SparseSize) catch unreachable, }; for (self.sparse, 0..) |_, i| { self.sparse[i] = .{ .generation = 0, .index = 0x0, .alive = false }; } return self; } pub fn create(allocator: std.mem.Allocator) !*@This() { const self = try allocator.create(@This()); self.* = init(allocator); return self; } pub fn destroy(self: *@This()) void { const allocator = self.allocator; self.deinit(); allocator.destroy(self); } pub fn denseItems(self: *@This(), comptime field: Field) []FieldType(field) { return self.dense.items(field); } pub fn deinit(self: *@This()) void { self.dense.deinit(self.allocator); self.denseIndices.deinit(self.allocator); self.allocator.free(self.sparse); } pub fn readDense(self: @This(), offset: usize, comptime field: Field) *const FieldType(field) { return &self.dense.items(field)[offset]; } pub fn getDense(self: *@This(), offset: usize, comptime field: Field) *FieldType(field) { return &self.dense.items(field)[offset]; } // ----- sparse set features ----- pub fn handleFromSparseIndex(self: @This(), sparseIndex: IndexType) SetHandle { var handle: SetHandle = self.sparse[@as(usize, @intCast(sparseIndex))]; handle.index = sparseIndex; return handle; } pub fn sparseToDense(self: @This(), handle: SetHandle) ?usize { const denseHandle = self.sparse[@as(usize, @intCast(handle.index))]; // todo: need to update generation if (denseHandle.generation != handle.generation) // tombstone value { // Generation mismatch, this handle is totally dead. return null; } if (denseHandle.alive == false) { return null; } const denseIndex = @as(usize, @intCast(denseHandle.index)); if (denseIndex >= self.denseIndices.items.len) { return null; } return denseIndex; } pub fn FieldType(comptime field: Field) type { return std.meta.fieldInfo(T, field).type; } pub fn get(self: *@This(), handle: SetHandle, comptime field: Field) ?*FieldType(field) { const denseIndex = self.sparseToDense(handle) orelse return null; return &self.dense.items(field)[denseIndex]; } // the idea behind a sparse array is that the sethandle is // highly stable. // if this container is registered as part of an ECS, this is unsafe to call directly pub fn createObject(self: *@This(), initValue: T) !SetHandle { var newSparseIndex = newRandomIndex(); var denseHandle = self.sparse[@as(usize, @intCast(newSparseIndex))]; while (denseHandle.alive == true) { newSparseIndex = newRandomIndex(); denseHandle = self.sparse[@as(usize, @intCast(newSparseIndex))]; } const generation = (denseHandle.generation + 1) % (std.math.maxInt(GenerationType)); return self.createObjectInternal(initValue, newSparseIndex, generation); } pub fn createObjectInternal(self: *@This(), initValue: T, newSparseIndex: IndexType, generation: GenerationType) !SetHandle { const newDenseIndex = self.denseIndices.items.len; self.sparse[@as(usize, @intCast(newSparseIndex))] = SetHandle{ .alive = true, .generation = @as(GenerationType, @intCast(generation)), .index = @as(IndexType, @intCast(newDenseIndex)), }; const setHandle = SetHandle{ .alive = true, .generation = generation, .index = newSparseIndex, }; try self.denseIndices.append(self.allocator, setHandle); try self.dense.append(self.allocator, initValue); std.debug.assert(self.sparse[newSparseIndex].index < self.dense.len); self.opCount +%= 1; if (self.containerListener) |l| { l.onHandleAdded(l.ptr, @intCast(self.containerID), setHandle); } return setHandle; } pub fn denseToSparse(self: @This(), dense: u32) SetHandle { return self.denseIndices.items[dense]; } pub fn createWithHandle(self: *@This(), handle: SetHandle, initValue: T) !SetHandle { const currentDenseHandle = self.sparse[handle.index]; if (currentDenseHandle.alive) { return error.ObjectAlreadyExists; } return self.createObjectInternal(initValue, handle.index, handle.generation); } pub fn destroyObject(self: *@This(), handle: SetHandle) bool { // to destroy an object // get handle and get the dense position, swap and remove. // Then insert the tombstone value into the sparse handle // if this fails it means the object is already destroyed const denseIndex = self.sparseToDense(handle) orelse return false; // get the indec of the last object in the dense set const tailDenseIndex = self.dense.len - 1; // get the sparse index of the last object const sparseIndexToSwap = self.denseIndices.items[tailDenseIndex]; // redirect the sparse index to the new position of the swapped object. self.sparse[@as(usize, @intCast(sparseIndexToSwap.index))].index = @as(IndexType, @intCast(denseIndex)); // perform the swap and remove, mark the tombstone as well. _ = self.dense.swapRemove(denseIndex); _ = self.denseIndices.swapRemove(denseIndex); self.sparse[@as(usize, @intCast(handle.index))].alive = false; self.opCount +%= 1; if (self.containerListener) |l| { l.onHandleRemoved(l.ptr, self.containerID, handle); } return true; } var prng = std.Random.DefaultPrng.init(0x1234); var rand = prng.random(); pub fn newRandomIndex() IndexType { if (SparseSize == std.math.maxInt(IndexType)) { return rand.int(IndexType); } return rand.int(IndexType) % @as(IndexType, @intCast(SparseSize)); } // ecs interface pub const EcsContainerInterfaceVTable = EcsContainerInterface.Implement(@This()); pub fn handleExists(self: @This(), handle: SetHandle) bool { return self.sparseToDense(handle) != null; } pub fn getContainerID(self: @This()) u32 { return self.containerID; } pub fn onRegister(self: *@This(), id: u32, listener: ContainerListener) void { self.containerID = id; self.containerListener = listener; } pub fn evictFromRegistry(self: *@This()) void { self.containerListener = null; } pub fn getStateCount(self: @This()) u32 { return self.opCount; } // replaces get. // really nasty trick here. I'm relying on absolutely fucked aliasing // for sparse-multiset // which is not a type which can be represented in lua directly. // rather the resulting component type should be a THIN component // which only contains the handle. // // Zig MAY break this in the future. in which case I'll have to // treat the sparse sets in this handle as C ABI with a fixed bit layout pub fn getHandleRef(self: @This(), handle: SetHandle) *SetHandle { if (self.handleExists(handle)) { // std.debug.print("get: {p}\n", .{@as(*anyopaque, @ptrCast(&self.sparse[handle.index]))}); return &self.sparse[handle.index]; } @panic("uh oh "); } pub fn createWithHandleECS(self: *@This(), handle: SetHandle) *SetHandle { _ = self.createWithHandle(handle, .{}) catch @panic("unable to create with handle"); return self.getHandleRef(handle); } pub const ContainerTypeName = "SparseMultiSet"; }; } // A quick little sparse set implementation, this feeds the core of the // ECS. A sparse set provides Constant time random access to a range of objects through stable handles // While providing dense memory locality for iterating. // // should probably never use this one outside of the base-set for checking entity existence. pub fn SparseSetAdvanced(comptime T: type, comptime SparseSize: u32) type { return struct { allocator: std.mem.Allocator, dense: ArrayListUnmanaged(struct { value: T, sparseIndex: SetHandle, }), sparse: []SetHandle, containerID: u32 = 0, containerListener: ?ContainerListener = null, opCount: u32 = 0, pub const StableReferences = false; pub const InnerType = T; pub fn getStateCount(self: @This()) u32 { return self.opCount; } pub fn handleFromSparseIndex(self: @This(), sparseIndex: IndexType) SetHandle { var handle: SetHandle = self.sparse[@as(usize, @intCast(sparseIndex))]; handle.index = sparseIndex; return handle; } pub fn create(allocator: std.mem.Allocator) !*@This() { const self = try allocator.create(@This()); self.* = init(allocator); return self; } pub fn destroy(self: *@This()) void { self.deinit(); self.allocator.destroy(self); } pub fn init(allocator: std.mem.Allocator) @This() { var self = @This(){ .allocator = allocator, .dense = .{}, .sparse = allocator.alloc(SetHandle, SparseSize) catch unreachable, }; for (self.sparse, 0..) |_, i| { self.sparse[i] = .{ .generation = 0, .index = 0x0, .alive = false }; } return self; } pub fn readDense(self: @This(), offset: usize) *const T { return &self.dense.items[offset].value; } pub fn getDense(self: *@This(), offset: usize) *T { return &self.dense.items[offset].value; } pub fn count(self: @This()) usize { return self.dense.items.len; } pub fn sparseToDense(self: @This(), handle: SetHandle) ?usize { const denseHandle = self.sparse[@as(usize, @intCast(handle.index))]; // todo: need to update generation if (denseHandle.generation != handle.generation) // tombstone value { // Generation mismatch, this handle is totally dead. return null; } if (denseHandle.alive == false) { return null; } const denseIndex = @as(usize, @intCast(denseHandle.index)); if (denseIndex >= self.dense.items.len) { return null; } return denseIndex; } pub fn get(self: @This(), handle: SetHandle) ?*T { const denseIndex = self.sparseToDense(handle) orelse return null; return &self.dense.items[denseIndex].value; } pub fn destroyObject(self: *@This(), handle: SetHandle) void { // to destroy an object // get handle and get the dense position, swap and remove. // Then insert the tombstone value into the sparse handle if (@hasDecl(T, "deinitECS")) { self.get(handle).?.deinitECS(handle); } const denseIndex = self.sparseToDense(handle) orelse return; const tailDenseIndex = self.dense.items.len - 1; const sparseIndexToSwap = self.dense.items[tailDenseIndex].sparseIndex; self.opCount +%= 1; self.sparse[@as(usize, @intCast(sparseIndexToSwap.index))].index = @as(IndexType, @intCast(denseIndex)); _ = self.dense.swapRemove(denseIndex); self.sparse[@as(usize, @intCast(handle.index))].alive = false; if (self.containerListener) |l| { l.onHandleRemoved(l.ptr, self.containerID, handle); } } var prng = std.Random.DefaultPrng.init(0x1234); var rand = prng.random(); fn newRandomIndex() IndexType { if (SparseSize == std.math.maxInt(IndexType)) { return rand.int(IndexType); } return rand.int(IndexType) % @as(IndexType, @intCast(SparseSize)); } // the idea behind a sparse array is that the sethandle is // highly stable. pub fn createObject(self: *@This(), initValue: T) !SetHandle { var newSparseIndex = newRandomIndex(); self.opCount +%= 1; var denseHandle = self.sparse[@as(usize, @intCast(newSparseIndex))]; while (denseHandle.alive == true) { newSparseIndex = newRandomIndex(); denseHandle = self.sparse[@as(usize, @intCast(newSparseIndex))]; } const generation = (denseHandle.generation + 1) % (std.math.maxInt(GenerationType)); const newDenseIndex = self.dense.items.len; const sparseToDenseHandle = SetHandle{ .alive = true, .generation = @as(GenerationType, @intCast(generation)), .index = @as(IndexType, @intCast(newDenseIndex)), }; const denseToSparseHandle = SetHandle{ .alive = true, .generation = @as(GenerationType, @intCast(generation)), .index = @as(IndexType, @intCast(newSparseIndex)), }; try self.dense.append(self.allocator, .{ .value = initValue, .sparseIndex = denseToSparseHandle, }); self.sparse[@as(usize, @intCast(newSparseIndex))] = sparseToDenseHandle; const setHandle = SetHandle{ .alive = true, .generation = generation, .index = newSparseIndex, }; return setHandle; } pub const ConstructResult = struct { ptr: *T, handle: SetHandle, }; // Will fail if the handle already exists. pub fn createWithHandle(self: *@This(), handle: SetHandle, initValue: T) !ConstructResult { if (handle.index >= DefaultSparseSize) { std.debug.print("this should never happen handle index is huge: {x} {d}\n", .{ handle.index, handle.index }); @panic("impossible handle"); } var currentDenseHandle = self.sparse[handle.index]; if (currentDenseHandle.alive) { return error.ObjectAlreadyExists; } currentDenseHandle.generation = handle.generation; currentDenseHandle.alive = true; return try self.createAndGetInternal(currentDenseHandle, handle.index, initValue, false); } pub fn createWithHandleECS(self: *@This(), handle: SetHandle) *T { const rv = (self.createWithHandle(handle, .{}) catch @panic("unable to create")).ptr; if (@hasDecl(T, "initECS")) { rv.initECS(handle); } return rv; } fn createAndGetInternal(self: *@This(), denseHandle: SetHandle, sparseIndex: IndexType, initValue: T, comptime bumpGeneration: bool) !ConstructResult { const newDenseIndex = self.dense.items.len; try self.dense.append(self.allocator, .{ .value = initValue, .sparseIndex = .{ .index = sparseIndex, .generation = denseHandle.generation, .alive = true, }, }); var generation = denseHandle.generation; if (bumpGeneration) { generation = (generation + 1) % (std.math.maxInt(GenerationType)); } self.sparse[@as(usize, @intCast(sparseIndex))] = SetHandle{ .alive = true, .generation = @as(GenerationType, @intCast(generation)), .index = @as(IndexType, @intCast(newDenseIndex)), }; const setHandle = SetHandle{ .alive = true, .generation = generation, .index = sparseIndex, }; const rv = ConstructResult{ .ptr = &self.dense.items[@as(usize, @intCast(newDenseIndex))].value, .handle = setHandle, }; if (self.containerListener) |l| { l.onHandleAdded(l.ptr, self.containerID, setHandle); } return rv; } pub fn createAndGet(self: *@This(), initValue: T) !ConstructResult { var newSparseIndex = newRandomIndex(); var denseHandle = self.sparse[@as(usize, @intCast(newSparseIndex))]; while (denseHandle.alive == true) { newSparseIndex = newRandomIndex(); denseHandle = self.sparse[@as(usize, @intCast(newSparseIndex))]; } return try self.createAndGetInternal(denseHandle, newSparseIndex, initValue, true); } pub fn deinit(self: *@This()) void { self.allocator.free(self.sparse); self.dense.deinit(self.allocator); } pub const EcsContainerInterfaceVTable = EcsContainerInterface.Implement(@This()); pub fn handleExists(self: @This(), handle: SetHandle) bool { return self.sparseToDense(handle) != null; } pub fn getContainerID(self: @This()) u32 { return self.containerID; } pub fn onRegister(self: *@This(), id: u32, listener: ContainerListener) void { self.containerID = id; self.containerListener = listener; } // might never need to call this one... pub fn evictFromRegistry(self: *@This()) void { self.containerListener = null; } pub const ContainerTypeName = "SparseSet"; }; } // slowish look-up, fast-ish iteration time, // very little memory overhead, stable pointers // good all-around choice if you have a small // number of these objects around, and the object itself is quite big. pub fn SparseMap(comptime T: type) type { return struct { backingAllocator: std.mem.Allocator, arena: std.heap.ArenaAllocator, map: std.AutoHashMapUnmanaged(SetHandle, *T) = .{}, listEntriesByHandle: std.AutoHashMapUnmanaged(SetHandle, u32) = .{}, list: std.ArrayListUnmanaged(*T) = .{}, handles: std.ArrayListUnmanaged(SetHandle) = .{}, containerID: u32 = 0, containerListener: ?ContainerListener = null, opCount: u32 = 0, pub const InnerType = T; pub const StableReferences = true; pub fn create(backingAllocator: std.mem.Allocator) !*@This() { const self = try backingAllocator.create(@This()); self.* = .{ .arena = std.heap.ArenaAllocator.init(backingAllocator), .backingAllocator = backingAllocator, }; return self; } pub fn indexToHandle(self: @This(), index: usize) SetHandle { return self.handles.items[index]; } pub fn destroyObject(self: *@This(), handle: SetHandle) void { std.debug.assert(self.map.contains(handle)); if (@hasDecl(T, "deinitECS")) { self.map.get(handle).?.deinitECS(handle); } const alloc = self.allocator(); const index = self.listEntriesByHandle.get(handle).?; alloc.destroy(self.map.get(handle).?); _ = self.map.remove(handle); _ = self.listEntriesByHandle.remove(handle); _ = self.list.swapRemove(index); // oops. const swappedHandle = self.handles.getLastOrNull(); _ = self.handles.swapRemove(index); if (swappedHandle) |swapped| self.listEntriesByHandle.put(alloc, swapped, index) catch unreachable; self.opCount +%= 1; if (self.containerListener) |l| { l.onHandleRemoved(l.ptr, self.containerID, handle); } } pub fn createWithHandle(self: *@This(), handle: SetHandle, initValue: T) !*T { std.debug.assert(!self.map.contains(handle)); const alloc = self.allocator(); const new = try alloc.create(T); new.* = initValue; try self.map.put(alloc, handle, new); try self.list.append(alloc, new); try self.handles.append(alloc, handle); try self.listEntriesByHandle.put(alloc, handle, @intCast(self.list.items.len - 1)); if (self.containerListener) |l| { l.onHandleAdded(l.ptr, self.containerID, handle); } self.opCount +%= 1; return new; } pub fn createWithHandleECS(self: *@This(), handle: SetHandle) *T { const rv = self.createWithHandle(handle, .{}) catch @panic("Unable to create"); if (@hasDecl(T, "initECS")) { rv.initECS(handle); } return rv; } pub fn allocator(self: *@This()) std.mem.Allocator { return self.arena.allocator(); } pub fn destroy(self: *@This()) void { self.arena.deinit(); self.backingAllocator.destroy(self); } // == interface below == pub const EcsContainerInterfaceVTable = EcsContainerInterface.Implement(@This()); pub fn get(self: @This(), handle: SetHandle) ?*anyopaque { return self.map.get(handle); } pub fn handleExists(self: @This(), handle: SetHandle) bool { return self.map.contains(handle); } pub fn getContainerID(self: @This()) u32 { return self.containerID; } pub fn onRegister(self: *@This(), id: u32, listener: ContainerListener) void { self.containerID = id; self.containerListener = listener; } // might never need to call this one... pub fn evictFromRegistry(self: *@This()) void { self.containerListener = null; } pub fn getStateCount(self: @This()) u32 { return self.opCount; } pub const ContainerTypeName = "SparseMap"; }; } // this is quickly becoming the ecs containers file // todo.. move EcsContainerInterface into somewhere else. const interface = @import("interface.zig"); pub const EcsContainerInterface = interface.MakeInterface("EcsContainerInterfaceVTable", struct { containerTypeName: []const u8, componentName: []const u8, handleExists: *const fn (*const anyopaque, SetHandle) bool, get: *const fn (*const anyopaque, SetHandle) ?*anyopaque, createWithHandle: *const fn (*anyopaque, SetHandle) *anyopaque, destroyObject: *const fn (*anyopaque, SetHandle) void, getContainerID: *const fn (*const anyopaque) u32, onRegister: *const fn (*anyopaque, u32, ContainerListener) void, evictFromRegistry: *const fn (*anyopaque) void, getStateCount: *const fn (*anyopaque) u32, // returns an internal number representing the state of the container pub const Reference = struct { vtable: *const @This(), ptr: *anyopaque, }; pub fn Implement(comptime TargetType: type) @This() { const Wrap = struct { pub fn handleExists(p: *const anyopaque, handle: SetHandle) bool { var ptr = @as(*const TargetType, @ptrCast(@alignCast(p))); return ptr.handleExists(handle); } // gets a function. assuming it exists pub fn get(p: *const anyopaque, handle: SetHandle) ?*anyopaque { // std.debug.print("get {p}\n", .{p}); var ptr = @as(*const TargetType, @ptrCast(@alignCast(p))); if (@hasDecl(TargetType, "getHandleRef")) { return ptr.getHandleRef(handle); } return @ptrCast(ptr.get(handle)); } pub fn destroyObject(p: *anyopaque, handle: SetHandle) void { // std.debug.print("creat with handle {p}\n", .{p}); var ptr = @as(*TargetType, @ptrCast(@alignCast(p))); ptr.destroyObject(handle); } pub fn createWithHandle(p: *anyopaque, handle: SetHandle) *anyopaque { // std.debug.print("creat with handle {p}\n", .{p}); var ptr = @as(*TargetType, @ptrCast(@alignCast(p))); return ptr.createWithHandleECS(handle); } pub fn getStateCount(p: *const anyopaque) u32 { // std.debug.print("getstatecount {p}\n", .{p}); var ptr = @as(*const TargetType, @ptrCast(@alignCast(p))); return ptr.getStateCount(); } pub fn getContainerID(p: *const anyopaque) u32 { var ptr = @as(*const TargetType, @ptrCast(@alignCast(p))); return ptr.getContainerID(); } pub fn onRegister(p: *anyopaque, id: u32, listener: ContainerListener) void { var ptr = @as(*TargetType, @ptrCast(@alignCast(p))); return ptr.onRegister(id, listener); } pub fn evictFromRegistry(p: *anyopaque) void { var ptr = @as(*TargetType, @ptrCast(@alignCast(p))); return ptr.evictFromRegistry(); } }; return .{ .containerTypeName = TargetType.ContainerTypeName, .componentName = @typeName(TargetType.InnerType), .handleExists = Wrap.handleExists, .get = Wrap.get, .createWithHandle = Wrap.createWithHandle, .destroyObject = Wrap.destroyObject, .getContainerID = Wrap.getContainerID, .onRegister = Wrap.onRegister, .evictFromRegistry = Wrap.evictFromRegistry, .getStateCount = Wrap.getStateCount, }; } }); test "sparse-multiset-recycle-handles" { const testHandle: SetHandle = .{ .index = 420, .alive = true, .generation = 12, }; const testHandle2: SetHandle = .{ .index = 421, .alive = true, .generation = 12, }; const TestStruct = struct { wutang: u32 = 0, lmao: u32 = 0, }; var testSet = SparseMultiSet(TestStruct).init(std.testing.allocator); defer testSet.deinit(); _ = testSet.createWithHandleECS(testHandle); _ = testSet.createWithHandleECS(testHandle2); std.debug.assert(testSet.get(testHandle, .wutang) != null); std.debug.assert(testSet.get(testHandle2, .wutang) != null); }