const std = @import("std"); /// Returns a struct consisting of an array `index` and a semi-unique `cycle`, /// which exists to distinguish handles with the same array `index`. /// /// The `cycle` value is only unique within the incremental period of an /// unsigned integer with `cycle_bits`, so a larger number of `cycle_bits` /// provides a larger scope of identifiable conflicts between handles for the /// same `index`. /// /// `Handle` is generic because while the `{ index, cycle }` pattern is widely /// applicable, a good distribution of bits between `index` and `cycle` and the /// overall size of a handle are highly dependent on the lifecycle of the /// resource being identified by a handle and the systems consuming handles. /// /// Reasonable values for `index_bits` depend on the maximum number of /// uniquely identifiable resources your API will to identify with handles. /// Generally this is directly tied to the length of the array(s) in which /// you will store data to be referenced by a handle's `index`. /// /// Reasonable values for `cycle_bits` depend on the frequency with which your /// API expects to be issuing handles, and how many cycles of your application /// are likely to elapse before an expired handle will likely no longer be /// retained by the API caller's data structures. /// /// For example, a `Handle(16, 16)` may be sufficient for a GPU resource like /// a texture or buffer, where 64k instances of that resource is a reasonable /// upper bound. /// /// A `Handle(22, 10)` may be more appropriate to identify an entity in a /// system where we can safely assume that 4 million entities, is a lot, and /// that API callers can discover and discard expired entity handles within /// 1024 frames of an entity being destroyed and its handle's `index` being /// reissued for use by a distinct entity. /// /// `TResource` identifies type of resource referenced by a handle, and /// provides a type-safe distinction between two otherwise equivalently /// configured `Handle` types, such as: /// * `const BufferHandle = Handle(16, 16, Buffer);` /// * `const TextureHandle = Handle(16, 16, Texture);` /// /// The total size of a handle will always be the size of an addressable /// unsigned integer of type `u8`, `u16`, `u32`, `u64`, `u128`, or `u256`. pub fn Handle( comptime index_bits: u8, comptime cycle_bits: u8, comptime TResource: type, ) type { if (index_bits == 0) @compileError("index_bits must be greater than 0"); if (cycle_bits == 0) @compileError("cycle_bits must be greater than 0"); const id_bits: u16 = @as(u16, index_bits) + @as(u16, cycle_bits); const Id = switch (id_bits) { 8 => u8, 16 => u16, 32 => u32, 64 => u64, 128 => u128, 256 => u256, else => @compileError("index_bits + cycle_bits must sum to exactly " ++ "8, 16, 32, 64, 128, or 256 bits"), }; const field_bits = @max(index_bits, cycle_bits); const utils = @import("utils.zig"); const UInt = utils.UInt; const AddressableUInt = utils.AddressableUInt; return extern struct { const Self = @This(); const HandleType = Self; const IndexType = UInt(index_bits); const CycleType = UInt(cycle_bits); const HandleUnion = extern union { id: Id, bits: packed struct { cycle: CycleType, // least significant bits index: IndexType, // most significant bits }, }; pub const Resource = TResource; pub const AddressableCycle = AddressableUInt(field_bits); pub const AddressableIndex = AddressableUInt(field_bits); pub const max_cycle = ~@as(CycleType, 0); pub const max_index = ~@as(IndexType, 0); pub const max_count = @as(Id, max_index - 1) + 2; id: Id = 0, pub const nil = Self{ .id = 0 }; pub fn init(i: IndexType, c: CycleType) Self { const u = HandleUnion{ .bits = .{ .cycle = c, .index = i, } }; return .{ .id = u.id }; } pub fn cycle(self: Self) CycleType { const u = HandleUnion{ .id = self.id }; return u.bits.cycle; } pub fn index(self: Self) IndexType { const u = HandleUnion{ .id = self.id }; return u.bits.index; } /// Unpacks the `index` and `cycle` bit fields that comprise /// `Handle.id` into an `AddressableHandle`, which stores /// the `index` and `cycle` values in pointer-addressable fields. pub fn addressable(self: Self) AddressableHandle { const u = HandleUnion{ .id = self.id }; return .{ .cycle = u.bits.cycle, .index = u.bits.index, }; } /// When you want to directly access the `index` and `cycle` of a /// handle, first convert it to an `AddressableHandle` by calling /// `Handle.addressable()`. /// An `AddressableHandle` can be converted back into a "compact" /// `Handle` by calling `AddressableHandle.compact()`. pub const AddressableHandle = struct { cycle: AddressableCycle = 0, index: AddressableIndex = 0, /// Returns the corresponding `Handle` pub fn handle(self: AddressableHandle) HandleType { const u = HandleUnion{ .bits = .{ .cycle = @as(CycleType, @intCast(self.cycle)), .index = @as(IndexType, @intCast(self.index)), } }; return .{ .id = u.id }; } }; pub fn format( self: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype, ) !void { _ = fmt; _ = options; const n = @typeName(Resource); const a = self.addressable(); return writer.print("{s}[{}#{}]", .{ n, a.index, a.cycle }); } }; } //////////////////////////////////////////////////////////////////////////////// test "Handle sizes and alignments" { const expectEqual = std.testing.expectEqual; { const H = Handle(4, 4, void); try expectEqual(@sizeOf(u8), @sizeOf(H)); try expectEqual(@alignOf(u8), @alignOf(H)); try expectEqual(4, @bitSizeOf(H.IndexType)); try expectEqual(4, @bitSizeOf(H.CycleType)); try expectEqual(8, @bitSizeOf(H.AddressableIndex)); try expectEqual(8, @bitSizeOf(H.AddressableCycle)); const A = H.AddressableHandle; try expectEqual(@sizeOf(u16), @sizeOf(A)); try expectEqual(@alignOf(u8), @alignOf(A)); } { const H = Handle(6, 2, void); try expectEqual(@sizeOf(u8), @sizeOf(H)); try expectEqual(@alignOf(u8), @alignOf(H)); try expectEqual(6, @bitSizeOf(H.IndexType)); try expectEqual(2, @bitSizeOf(H.CycleType)); try expectEqual(8, @bitSizeOf(H.AddressableIndex)); try expectEqual(8, @bitSizeOf(H.AddressableCycle)); const A = H.AddressableHandle; try expectEqual(@sizeOf(u16), @sizeOf(A)); try expectEqual(@alignOf(u8), @alignOf(A)); } { const H = Handle(8, 8, void); try expectEqual(@sizeOf(u16), @sizeOf(H)); try expectEqual(@alignOf(u16), @alignOf(H)); try expectEqual(8, @bitSizeOf(H.IndexType)); try expectEqual(8, @bitSizeOf(H.CycleType)); try expectEqual(8, @bitSizeOf(H.AddressableIndex)); try expectEqual(8, @bitSizeOf(H.AddressableCycle)); const A = H.AddressableHandle; try expectEqual(@sizeOf(u16), @sizeOf(A)); try expectEqual(@alignOf(u8), @alignOf(A)); } { const H = Handle(12, 4, void); try expectEqual(@sizeOf(u16), @sizeOf(H)); try expectEqual(@alignOf(u16), @alignOf(H)); try expectEqual(12, @bitSizeOf(H.IndexType)); try expectEqual(4, @bitSizeOf(H.CycleType)); try expectEqual(16, @bitSizeOf(H.AddressableIndex)); try expectEqual(16, @bitSizeOf(H.AddressableCycle)); const A = H.AddressableHandle; try expectEqual(@sizeOf(u32), @sizeOf(A)); try expectEqual(@alignOf(u16), @alignOf(A)); } { const H = Handle(16, 16, void); try expectEqual(@sizeOf(u32), @sizeOf(H)); try expectEqual(@alignOf(u32), @alignOf(H)); try expectEqual(16, @bitSizeOf(H.IndexType)); try expectEqual(16, @bitSizeOf(H.CycleType)); try expectEqual(16, @bitSizeOf(H.AddressableIndex)); try expectEqual(16, @bitSizeOf(H.AddressableCycle)); const A = H.AddressableHandle; try expectEqual(@sizeOf(u32), @sizeOf(A)); try expectEqual(@alignOf(u16), @alignOf(A)); } { const H = Handle(22, 10, void); try expectEqual(@sizeOf(u32), @sizeOf(H)); try expectEqual(@alignOf(u32), @alignOf(H)); try expectEqual(22, @bitSizeOf(H.IndexType)); try expectEqual(10, @bitSizeOf(H.CycleType)); try expectEqual(32, @bitSizeOf(H.AddressableIndex)); try expectEqual(32, @bitSizeOf(H.AddressableCycle)); const A = H.AddressableHandle; try expectEqual(@sizeOf(u64), @sizeOf(A)); try expectEqual(@alignOf(u32), @alignOf(A)); } } //////////////////////////////////////////////////////////////////////////////// test "Handle sort order" { const expect = std.testing.expect; const handle = Handle(4, 4, void).init; const a = handle(0, 3); const b = handle(1, 1); // id order is consistent with index order, even when cycle order is not try expect(a.id < b.id); try expect(a.index() < b.index()); try expect(a.cycle() > b.cycle()); } //////////////////////////////////////////////////////////////////////////////// test "Handle.format()" { const bufPrint = std.fmt.bufPrint; const expectEqualStrings = std.testing.expectEqualStrings; const Foo = struct {}; const H = Handle(12, 4, Foo); const h = H.init(0, 1); var buffer = [_]u8{0} ** 128; const s = try bufPrint(buffer[0..], "{}", .{h}); try expectEqualStrings("handle.test.Handle.format().Foo[0#1]", s); }