From d36f7de1a5ef40df0e0ee3edce444394b45fd4d0 Mon Sep 17 00:00:00 2001 From: duck Date: Tue, 29 Apr 2025 14:26:44 +0500 Subject: [PATCH] Basic controller impl., some file reordering --- src/graph.zig | 249 +++++++++++++++-------------------------- src/graph/resource.zig | 132 ++++++++++++++++++++++ src/graph/system.zig | 35 ++++++ src/graph/utils.zig | 63 +++++++++++ 4 files changed, 321 insertions(+), 158 deletions(-) create mode 100644 src/graph/resource.zig create mode 100644 src/graph/system.zig create mode 100644 src/graph/utils.zig diff --git a/src/graph.zig b/src/graph.zig index fc8de7c..b7e0114 100644 --- a/src/graph.zig +++ b/src/graph.zig @@ -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(); diff --git a/src/graph/resource.zig b/src/graph/resource.zig new file mode 100644 index 0000000..2aa2f54 --- /dev/null +++ b/src/graph/resource.zig @@ -0,0 +1,132 @@ +const std = @import("std"); +const utils = @import("utils.zig"); +const System = @import("system.zig"); +const Resource = @This(); + +const DEFAULT_CONTROLLER_CAPACITY = 8; + +/// Resource data +pointer: *anyopaque, +/// Pointer to the memory allocted for this resource +buffer: []u8, +alignment: u29, +hash: utils.Hash, + +pub fn deinit(self: *Resource, alloc: std.mem.Allocator) void { + alloc.free(self.buffer); +} + +pub const Controller = struct { + alloc: std.mem.Allocator, + command_buffer: std.ArrayListUnmanaged(Command), + error_state: ErrorState, + + pub const Command = union(enum) { + add_resource: Resource, + queue_system: System, + }; + pub const ErrorState = union(enum) { + ok: void, + recoverable: []const u8, + unrecoverable: void, + }; + + pub fn create(alloc: std.mem.Allocator) !Controller { + return .{ + .alloc = alloc, + .command_buffer = try std.ArrayListUnmanaged(Command).initCapacity(alloc, DEFAULT_CONTROLLER_CAPACITY), + .error_state = .ok, + }; + } + + /// Returns command queue, caller is responsible for freeing it's data + /// Call `clean()` afterwards, to clear the command queue + pub fn commands(self: *Controller) []const Command { + return self.command_buffer.items; + } + + /// Clears the command buffer, but does not deallocate it's contents + pub fn clear(self: *Controller) void { + self.command_buffer.clearRetainingCapacity(); + switch (self.error_state) { + .ok, .unrecoverable => {}, + .recoverable => |msg| self.alloc.free(msg), + } + self.error_state = .ok; + } + + /// Adds resource to the global storage, discarding any previously existing data + pub inline fn add_resource(self: *Controller, resource: anytype) void { + utils.validate_resource(@TypeOf(resource)); + + self.add_anyopaque_resource( + @ptrCast(&resource), + utils.hash_type(@TypeOf(resource)), + @sizeOf(@TypeOf(resource)), + @alignOf(@TypeOf(resource)), + ) catch |err| self.fail(err); + } + + pub fn queue_system(self: *Controller, comptime function: anytype) void { + utils.validate_system(function); + + self.queue_system_internal(function) catch |err| self.fail(err); + } + + fn queue_system_internal(self: *Controller, comptime function: anytype) !void { + var system = try System.from_function(function, self.alloc); + errdefer system.deinit(self.alloc); + + try self.command_buffer.append(self.alloc, .{ .queue_system = system }); + } + + /// `previous_output` is expected to be aligned accordingly + fn add_anyopaque_resource( + self: *Controller, + resource: *const anyopaque, + hash: utils.Hash, + size: usize, + align_to: u29, + ) !void { + // TODO: Review this shady function + const resource_buffer = try self.alloc.alloc(u8, size + align_to - 1); + 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], + ); + + try self.command_buffer.append(self.alloc, .{ .add_resource = .{ + .pointer = @ptrCast(resource_buffer[align_offset..]), + .buffer = resource_buffer, + .alignment = align_to, + .hash = hash, + } }); + } + + const ControllerError = std.mem.Allocator.Error; + fn fail(self: *Controller, err: ControllerError) void { + if (self.error_state == .unrecoverable) return; + if (self.error_state == .recoverable) self.alloc.free(self.error_state.recoverable); + switch (err) { + error.OutOfMemory => self.error_state = .unrecoverable, + } + } + + pub fn deinit(self: *Controller) void { + for (self.command_buffer.items) |*command| { + switch (command.*) { + .add_resource => |*resource| resource.deinit(self.alloc), + .queue_system => |*system| system.deinit(self.alloc), + } + } + self.clear(); + self.command_buffer.deinit(self.alloc); + } +}; diff --git a/src/graph/system.zig b/src/graph/system.zig new file mode 100644 index 0000000..f3cc5b9 --- /dev/null +++ b/src/graph/system.zig @@ -0,0 +1,35 @@ +const std = @import("std"); +const utils = @import("utils.zig"); +const Controller = @import("resource.zig").Controller; + +function_runner: *const fn ([]const *anyopaque) void, +requested_types: []const Request, + +pub const Request = union(enum) { + resource: utils.Hash, + controller: void, + // TODO: + // - Params +}; + +const Self = @This(); +pub fn from_function(comptime function: anytype, alloc: std.mem.Allocator) !Self { + utils.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) { + Controller => requests[i] = .controller, + else => |resource_type| requests[i] = .{ .resource = utils.hash_type(resource_type) }, + } + } + + return Self{ + .requested_types = try alloc.dupe(Request, &requests), + .function_runner = utils.generate_runner(function), + }; +} + +pub fn deinit(self: *const Self, alloc: std.mem.Allocator) void { + alloc.free(self.requested_types); +} diff --git a/src/graph/utils.zig b/src/graph/utils.zig new file mode 100644 index 0000000..271bc9f --- /dev/null +++ b/src/graph/utils.zig @@ -0,0 +1,63 @@ +const std = @import("std"); +const Resource = @import("resource.zig"); +const System = @import("system.zig"); + +pub const Hash = u32; +const HashAlgorithm = std.crypto.hash.blake2.Blake2s(@bitSizeOf(Hash)); + +pub inline fn hash_type(comptime h_type: type) Hash { + return hash_string(@typeName(h_type)); +} + +pub fn hash_string(comptime name: []const u8) Hash { + @setEvalBranchQuota(100000); + var output: [@divExact(@bitSizeOf(Hash), 8)]u8 = undefined; + + HashAlgorithm.hash(name, &output, .{}); + return std.mem.readInt( + Hash, + output[0..], + @import("builtin").cpu.arch.endian(), + ); +} + +pub fn validate_resource(comptime resource_type: type) void { + switch (@typeInfo(resource_type)) { + .Struct, .Enum, .Union => return, + else => @compileError("Invalid resource type \"" ++ @typeName(resource_type) ++ "\""), + } +} + +pub 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"); + + const controller_requests: usize = 0; + inline for (info.Fn.params) |param| { + if (@typeInfo(param.type.?) != .Pointer) @compileError("Systems can only have pointer parameters"); + switch (@typeInfo(param.type.?).Pointer.child) { + Resource.Controller => { + // controller_requests += 1; + // _ = &controller_requests; + }, + else => |t| validate_resource(t), + } + } + if (controller_requests > 1) @compileError("A system cannot request controller more than once"); +} + +pub 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; +}