Basic controller impl., some file reordering

This commit is contained in:
duck
2025-04-29 14:26:44 +05:00
parent bd03a573a4
commit d36f7de1a5
4 changed files with 321 additions and 158 deletions

View File

@@ -1,60 +1,27 @@
const std = @import("std");
const utils = @import("graph/utils.zig");
const Resource = @import("graph/resource.zig");
const System = @import("graph/system.zig");
// TODO:
// - Use arena allocator?
// - Resolve missing resource problem
// - Split up this file
pub const Controller = Resource.Controller;
const MAX_SYSTEM_REQUESTS = 8;
const DEFAULT_SYSTEM_CAPACITY = 16;
const DEFAULT_CONTROLLERS = 2;
const HashType = u32;
pub const HashAlgorithm = std.crypto.hash.blake2.Blake2s(32);
const Resource = struct {
/// Aligned pointer
pointer: *anyopaque,
/// Storage
mem: []u8,
};
const ResourceMap = std.AutoArrayHashMapUnmanaged(u32, Resource);
const System = struct {
const Request = union(enum) {
resource: HashType,
// TODO:
// - Params
// - Controller
};
const RequestList = []const Request;
function_runner: *const fn ([]const *anyopaque) void,
requested_types: RequestList,
fn from_function(comptime function: anytype, alloc: std.mem.Allocator) !System {
validate_system(function);
var requests: [@typeInfo(@TypeOf(function)).Fn.params.len]Request = undefined;
inline for (0.., @typeInfo(@TypeOf(function)).Fn.params) |i, param| {
switch (@typeInfo(param.type.?).Pointer.child) {
else => |resource_type| requests[i] = .{ .resource = hash_type(resource_type) },
}
}
return System{
.requested_types = try alloc.dupe(Request, &requests),
.function_runner = generate_runner(function),
};
}
fn deinit(self: System, alloc: std.mem.Allocator) void {
alloc.free(self.requested_types);
}
};
const ResourceMap = std.AutoArrayHashMapUnmanaged(utils.Hash, Resource);
const SystemQueue = std.ArrayListUnmanaged(System);
const Controllers = std.ArrayListUnmanaged(Controller);
/// Assumed to be thread-safe
alloc: std.mem.Allocator,
resources: ResourceMap,
system_queue: SystemQueue,
controllers: Controllers,
const Self = @This();
pub fn init(alloc: std.mem.Allocator) !Self {
@@ -64,17 +31,30 @@ pub fn init(alloc: std.mem.Allocator) !Self {
var system_queue = try SystemQueue.initCapacity(alloc, DEFAULT_SYSTEM_CAPACITY);
errdefer system_queue.deinit(alloc);
var controllers = try Controllers.initCapacity(alloc, DEFAULT_CONTROLLERS);
errdefer controllers.deinit(alloc);
errdefer for (controllers.items) |*controller| {
controller.deinit();
};
for (0..DEFAULT_CONTROLLERS) |_| {
const controller = try Controller.create(alloc);
controllers.appendAssumeCapacity(controller);
}
return .{
.alloc = alloc,
.resources = resources,
.system_queue = system_queue,
.controllers = controllers,
};
}
pub fn deinit(self: *Self) void {
var resource_iter = self.resources.iterator();
while (resource_iter.next()) |entry| {
self.alloc.free(entry.value_ptr.mem);
entry.value_ptr.deinit(self.alloc);
}
self.resources.clearAndFree(self.alloc);
self.resources.deinit(self.alloc);
@@ -83,159 +63,105 @@ pub fn deinit(self: *Self) void {
self.alloc.free(system.requested_types);
}
self.system_queue.deinit(self.alloc);
for (self.controllers.items) |*controller| {
controller.deinit();
}
self.controllers.deinit(self.alloc);
}
fn enqueue_system(self: *Self, comptime function: anytype) !void {
validate_system(function);
const system = try System.from_function(function, self.alloc);
fn enqueue_system(self: *Self, system: System) !void {
errdefer system.deinit(self.alloc);
try self.system_queue.append(self.alloc, system);
}
fn run_all_systems(self: *Self) GraphError!void {
while (self.system_queue.items.len > 0) {
const next_system = self.system_queue.getLast();
const next_system = self.system_queue.pop();
defer next_system.deinit(self.alloc);
defer _ = self.system_queue.pop();
try self.run_system(next_system);
}
}
/// Does not consume the system
/// Does not deallocate the system
fn run_system(self: *Self, system: System) GraphError!void {
var buffer: [MAX_SYSTEM_REQUESTS]*anyopaque = undefined;
var controller: ?Controller = null;
errdefer if (controller) |*c| c.deinit();
var buffer_len: usize = 0;
for (system.requested_types) |request| {
switch (request) {
.resource => |resource| {
buffer[buffer_len] = self.get_anyopaque_resource(resource) orelse return GraphError.MissingResource;
},
.controller => {
controller = try self.get_controller();
buffer[buffer_len] = @ptrCast(&controller.?);
},
}
buffer_len += 1;
}
system.function_runner(buffer[0..buffer_len]);
if (controller) |c| {
defer controller = null;
try self.free_controller(c);
}
}
fn apply_commands(self: *Self, commands: []const Controller.Command) !void {
for (commands) |command| {
switch (command) {
.add_resource => |r| try self.add_resource(r),
.queue_system => |s| try self.enqueue_system(s),
}
}
}
fn get_controller(self: *Self) !Controller {
if (self.controllers.popOrNull()) |c| {
return c;
}
return Controller.create(self.alloc);
}
/// Evaluates and clears the controller (even if errors out)
fn free_controller(self: *Self, controller: Controller) !void {
var c = controller;
try self.apply_commands(c.commands());
c.clear();
try self.controllers.append(self.alloc, c);
// TODO: Handle controller error state
}
pub inline fn get_resource(self: *Self, comptime resource: type) ?*resource {
validate_resource(resource);
if (get_anyopaque_resource(self, hash_type(resource))) |ptr| {
utils.validate_resource(resource);
if (get_anyopaque_resource(self, utils.hash_type(resource))) |ptr| {
return @alignCast(@ptrCast(ptr));
}
return null;
}
fn get_anyopaque_resource(self: *Self, resource_hash: HashType) ?*anyopaque {
fn get_anyopaque_resource(self: *Self, resource_hash: utils.Hash) ?*anyopaque {
if (self.resources.get(resource_hash)) |resource| {
return resource.pointer;
}
return null;
}
/// Copies resource into storage, returning previous value if any
pub inline fn add_resource(self: *Self, resource: anytype) !?@TypeOf(resource) {
validate_resource(@TypeOf(resource));
var previous: @TypeOf(resource) = undefined;
if (try self.add_anyopaque_resource(
@ptrCast(&resource),
hash_type(@TypeOf(resource)),
@sizeOf(@TypeOf(resource)),
@alignOf(@TypeOf(resource)),
@ptrCast(&previous),
)) {
return previous;
/// Discards any previous resource data, resource is assumed to be allocated with `self.alloc`
pub inline fn add_resource(self: *Self, resource: Resource) !void {
var previous = try self.resources.fetchPut(self.alloc, resource.hash, resource);
if (previous) |*p| {
p.value.deinit(self.alloc);
}
return null;
}
/// `previous_output` is expected to be aligned accordingly
fn add_anyopaque_resource(
self: *Self,
resource: *const anyopaque,
hash: HashType,
size: usize,
align_to: usize,
previous_output: *anyopaque,
) !bool {
// TODO: Review this shady function
const resource_buffer = try self.alloc.alloc(u8, size + align_to);
errdefer self.alloc.free(resource_buffer);
const align_offset = std.mem.alignPointerOffset(
@as([*]u8, @ptrCast(resource_buffer)),
align_to,
) orelse unreachable;
@memcpy(
resource_buffer[align_offset..size],
@as([*]const u8, @ptrCast(resource))[0..size],
);
const previous = try self.resources.fetchPut(self.alloc, hash, .{
.pointer = @ptrCast(resource_buffer[align_offset..]),
.mem = resource_buffer,
});
if (previous) |previous_value| {
@memcpy(
@as([*]u8, @ptrCast(previous_output)),
@as([*]const u8, @ptrCast(&previous_value.value.pointer))[0..size],
);
self.alloc.free(previous_value.value.mem);
return true;
}
return false;
}
inline fn hash_type(comptime h_type: type) HashType {
return hash_string(@typeName(h_type));
}
fn hash_string(comptime name: []const u8) HashType {
@setEvalBranchQuota(100000);
var output: [@divExact(@bitSizeOf(HashType), 8)]u8 = undefined;
HashAlgorithm.hash(name, &output, .{});
return std.mem.readInt(
HashType,
output[0..],
@import("builtin").cpu.arch.endian(),
);
}
fn validate_resource(comptime resource_type: type) void {
switch (@typeInfo(resource_type)) {
.Struct, .Enum, .Union => return,
else => @compileError("Invalid resource type \"" ++ @typeName(resource_type) ++ "\""),
}
}
fn validate_system(comptime system: anytype) void {
const info = @typeInfo(@TypeOf(system));
if (info != .Fn) @compileError("System can only be a function, got " ++ @typeName(system));
if (info.Fn.return_type != void) @compileError("Systems are not allowed to return any value (" ++ @typeName(info.Fn.return_type.?) ++ " returned)");
if (info.Fn.is_var_args) @compileError("System cannot be variadic");
if (info.Fn.is_generic) @compileError("System cannot be generic");
inline for (info.Fn.params) |param| {
if (@typeInfo(param.type.?) != .Pointer) @compileError("Systems can only have pointer parameters");
validate_resource(@typeInfo(param.type.?).Pointer.child);
}
}
fn generate_runner(comptime system: anytype) fn ([]const *anyopaque) void {
const RunnerImpl = struct {
fn runner(resources: []const *anyopaque) void {
var args: std.meta.ArgsTuple(@TypeOf(system)) = undefined;
inline for (0..@typeInfo(@TypeOf(system)).Fn.params.len) |index| {
args[index] = @alignCast(@ptrCast(resources[index]));
}
@call(.always_inline, system, args);
}
};
return RunnerImpl.runner;
}
const GraphError = error{
MissingResource,
OutOfMemory,
};
test {
@@ -249,19 +175,26 @@ test {
fn add_ten(rsc: *@This()) void {
rsc.number += 10;
}
fn add_eleven(cmd: *Controller) void {
cmd.queue_system(add_ten);
cmd.queue_system(add_one);
}
};
var graph = try Graph.init(std.testing.allocator);
defer graph.deinit();
try std.testing.expectEqual(graph.add_resource(TestResource{ .number = 100 }), null);
var controller = try graph.get_controller();
controller.add_resource(TestResource{ .number = 100 });
try graph.enqueue_system(TestResource.add_one);
try graph.enqueue_system(TestResource.add_one);
try graph.enqueue_system(TestResource.add_one);
controller.queue_system(TestResource.add_one);
controller.queue_system(TestResource.add_one);
try graph.enqueue_system(TestResource.add_ten);
try graph.enqueue_system(TestResource.add_ten);
controller.queue_system(TestResource.add_ten);
controller.queue_system(TestResource.add_eleven);
try graph.free_controller(controller);
try graph.run_all_systems();