Skip to content

Instantly share code, notes, and snippets.

@garettbass
Created June 13, 2022 14:58
Show Gist options
  • Save garettbass/df745569ce8b1311300668cf46113836 to your computer and use it in GitHub Desktop.
Save garettbass/df745569ce8b1311300668cf46113836 to your computer and use it in GitHub Desktop.
Assigns a unique monotonically increasing integer to each registered type, and allows retrieval of runtime-accessible TypeInfo using the returned ComponentIndex.
const std = @import("std");
const type_info = @import("type_info.zig");
const component_index = @import("component_index.zig");
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
pub const ComponentRegistryError = error{
ComponentIndexIsOutOfBounds,
ComponentIndexIsUnregistered,
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// Maps concrete types to monotonically increasing `ComponentIndex` values.
pub fn ComponentRegistry(comptime EntityContextType: type) type {
return struct {
pub const EntityContext = EntityContextType;
pub const max_component_count = EntityContext.max_component_count;
pub const max_component_index = EntityContext.max_component_index;
pub const Index = component_index.ComponentIndex(EntityContextType);
pub const TypeInfo = type_info.TypeInfo;
pub const Error = ComponentRegistryError;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const void_type_info = type_info.typeInfo(void);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var mutex = std.Thread.Mutex{};
var count = @as(u32, 0);
var type_info_table: [max_component_count]TypeInfo =
// zig fmt: off
[_]TypeInfo{void_type_info}
** max_component_count;
// zig fmt: on
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// The first call to `register(Foo)` returns a unique `ComponentIndex`
/// and associates the `TypeInfo` for `Foo` is associated with the
/// returned `ComponentIndex`.
///
/// Subsequent calls to `register(Foo)` return the same `ComponentIndex`
/// as the first call and do not change the state of the registry.
///
/// It is safe to call `register()` from different threads.
/// State changes made by `register()` are guarded by a mutex.
pub fn register(comptime Component: type) Error!Index {
const R = Registration(Component);
const info = type_info.typeInfo(Component);
const i = try initOnce(&R.index, info);
return Index.init(i);
}
/// Returns `true` if `index` is less than `max_component_count` and
/// corresponds to a component type that has already been registered
/// by a call to `register()`, otherwise `false`.
pub fn isValid(index: Index) bool {
validate(index) catch return false;
return true;
}
/// If `index` is not valid, returns one of:
/// * `ComponentRegistryError.ComponentIndexIsOutOfBounds`
/// * `ComponentRegistryError.ComponentIndexIsUnregistered`
pub fn validate(index: Index) Error!void {
const i = index.toInt();
if (i > max_component_index)
return Error.ComponentIndexIsOutOfBounds;
if (type_info_table[i] == void_type_info)
return Error.ComponentIndexIsUnregistered;
}
/// Returns `TypeInfo` associated with `index` if valid, otherwise
/// returns one of:
/// * `ComponentRegistryError.ComponentIndexIsOutOfBounds`
/// * `ComponentRegistryError.ComponentIndexIsUnregistered`
pub fn typeInfo(index: Index) Error!TypeInfo {
try validate(index);
return type_info_table[index.toInt()];
}
/// Returns `TypeInfo` associated with `index` if valid, otherwise
/// returns `null`.
pub fn typeInfoIfValid(index: Index) ?TypeInfo {
validate(index) catch return null;
return type_info_table[index.toInt()];
}
/// Returns a `[]TypeInfo` including all of the currently registered
/// component types in the order in which they were registered.
/// Indexing the returned slice with `ComponentIndex.toInt()` is
/// equivalent to calling `ComponentRegistry.typeInfo()`.
pub fn typeInfoTable() []TypeInfo {
return type_info_table[0..count];
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// Returns an iterator that enumerates each registered `ComponentIndex`.
pub fn indexIterator() IndexIterator {
return .{ .count = count };
}
const IndexIterator = struct {
index: u32 = 0,
count: u32 = 0,
pub fn next(self: *@This()) ?Index {
const i = self.index;
if (i < self.count) {
self.index += 1;
return Index.init(i);
}
return null;
}
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const unregistered: u32 = ~@as(u32, 0);
fn Registration(comptime T: type) type {
return struct {
const ComponentType = T;
var index: u32 = unregistered;
};
}
fn initOnce(outdex: *u32, info: TypeInfo) Error!u32 {
var i = @atomicLoad(u32, outdex, .Acquire);
if (i == unregistered) {
i = try initOnceSlow(outdex, info);
}
return i;
}
fn initOnceSlow(outdex: *u32, info: TypeInfo) Error!u32 {
@setCold(true);
mutex.lock();
defer mutex.unlock();
var n = @atomicLoad(u32, &count, .Acquire);
if (n > max_component_index)
return Error.ComponentIndexIsOutOfBounds;
var i = @atomicLoad(u32, outdex, .Acquire);
if (i == unregistered) {
i = @atomicRmw(u32, &count, .Add, 1, .Release);
@atomicStore(u32, outdex, i, .Release);
@atomicStore(TypeInfo, &type_info_table[i], info, .Monotonic);
}
return i;
}
};
}
////////////////////////////////// T E S T S ///////////////////////////////////
test "ComponentRegistry basic operation" {
const TestContext = struct {
const max_component_count = 3;
const max_component_index = 2;
};
const R = ComponentRegistry(TestContext);
const E = R.Error;
const I = R.Index;
const T = R.TypeInfo;
const typeInfo = type_info.typeInfo;
const OutOfBounds = E.ComponentIndexIsOutOfBounds;
const Unregistered = E.ComponentIndexIsUnregistered;
const Position = struct {
v: @Vector(3, f32),
};
const LinearVelocity = struct {
v: @Vector(3, f32),
};
const LinearAcceleration = struct {
v: @Vector(3, f32),
};
const Orientation = struct {
q: @Vector(4, f32),
};
const expectEqual = std.testing.expectEqual;
const expectError = std.testing.expectError;
// typeInfoTable is empty prior to registration
try expectEqual(@as(usize, 0), R.typeInfoTable().len);
{ // component indices are invalid prior to registration
try expectEqual(false, R.isValid(I.init(0)));
try expectEqual(false, R.isValid(I.init(1)));
try expectEqual(false, R.isValid(I.init(2)));
try expectEqual(false, R.isValid(I.init(3)));
try expectError(Unregistered, R.validate(I.init(0)));
try expectError(Unregistered, R.validate(I.init(1)));
try expectError(Unregistered, R.validate(I.init(2)));
try expectError(OutOfBounds, R.validate(I.init(3)));
}
{ // Registry.register() is idempotent
try expectEqual(I.init(0), try R.register(Position));
try expectEqual(I.init(1), try R.register(LinearVelocity));
try expectEqual(I.init(2), try R.register(LinearAcceleration));
try expectError(OutOfBounds, R.register(Orientation));
try expectEqual(I.init(0), try R.register(Position));
try expectEqual(I.init(1), try R.register(LinearVelocity));
try expectEqual(I.init(2), try R.register(LinearAcceleration));
try expectError(OutOfBounds, R.register(Orientation));
}
{ // component indices are valid after registration
try expectEqual(true, R.isValid(I.init(0)));
try expectEqual(true, R.isValid(I.init(1)));
try expectEqual(true, R.isValid(I.init(2)));
try expectEqual(false, R.isValid(I.init(3)));
try R.validate(I.init(0));
try R.validate(I.init(1));
try R.validate(I.init(2));
try expectError(OutOfBounds, R.validate(I.init(3)));
}
{ // component indices map to component type info
try expectEqual(typeInfo(Position), try R.typeInfo(I.init(0)));
try expectEqual(typeInfo(LinearVelocity), try R.typeInfo(I.init(1)));
try expectEqual(typeInfo(LinearAcceleration), try R.typeInfo(I.init(2)));
try expectError(OutOfBounds, R.typeInfo(I.init(3)));
try expectEqual(typeInfo(Position), R.typeInfoIfValid(I.init(0)).?);
try expectEqual(typeInfo(LinearVelocity), R.typeInfoIfValid(I.init(1)).?);
try expectEqual(typeInfo(LinearAcceleration), R.typeInfoIfValid(I.init(2)).?);
try expectEqual(@as(?T, null), R.typeInfoIfValid(I.init(3)));
}
// typeInfoTable is full after registration
try expectEqual(@as(usize, 3), R.typeInfoTable().len);
var itr = R.indexIterator();
try expectEqual(I.init(0), itr.next().?);
try expectEqual(I.init(1), itr.next().?);
try expectEqual(I.init(2), itr.next().?);
try expectEqual(I.nil, itr.next());
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
test "ComponentRegistry with a different Context is isolated." {
const TestContext = struct {
const max_component_count = 3;
const max_component_index = 2;
};
const R = ComponentRegistry(TestContext);
const E = R.Error;
const I = R.Index;
const OutOfBounds = E.ComponentIndexIsOutOfBounds;
const Foo = struct {};
const Bar = struct {};
const Baz = struct {};
const Bun = struct {};
const expectEqual = std.testing.expectEqual;
const expectError = std.testing.expectError;
// typeInfoTable is empty prior to registration
try expectEqual(@as(usize, 0), R.typeInfoTable().len);
try expectEqual(I.init(0), try R.register(Foo));
try expectEqual(I.init(1), try R.register(Bar));
try expectEqual(I.init(2), try R.register(Baz));
try expectError(OutOfBounds, R.register(Bun));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment