diff --git a/src/coral/asio.zig b/src/coral/asio.zig index a39d899..0b2e72a 100644 --- a/src/coral/asio.zig +++ b/src/coral/asio.zig @@ -16,19 +16,19 @@ pub fn CallTask(comptime function: anytype) type { return struct { args: Args, - @"return": Future(function_fn.return_type.?), + returned: Future(function_fn.return_type.?), const Self = @This(); pub fn init(args: Args) Self { return .{ .args = args, - .@"return" = .empty, + .returned = .empty, }; } pub fn run(self: *Self) void { - std.debug.assert(self.@"return".resolve(@call(.auto, function, self.args))); + std.debug.assert(self.returned.resolve(@call(.auto, function, self.args))); } }; } @@ -145,7 +145,7 @@ pub const TaskQueue = struct { while (true) { const runner = self.pending.dequeue(); - if (runner.is_fn(poison_pill)) { + if (runner.isFn(poison_pill)) { break; } diff --git a/src/coral/coral.zig b/src/coral/coral.zig index 120e950..8781ad4 100644 --- a/src/coral/coral.zig +++ b/src/coral/coral.zig @@ -26,16 +26,15 @@ pub const utf8 = @import("./utf8.zig"); pub fn Callable(comptime Output: type, comptime input_types: []const type) type { const InputTuple = std.meta.Tuple(input_types); - const fn_context: *allowzero anyopaque = @ptrFromInt(0); return struct { - context: *allowzero anyopaque, - call_with_context: *const fn (*anyopaque, InputTuple) Output, + has_context: ?*anyopaque, + call_with_context: *const fn (?*anyopaque, InputTuple) Output, fn FnCall(comptime invoke: anytype) type { return struct { - fn call(context: *anyopaque, inputs: InputTuple) Output { - std.debug.assert(context == fn_context); + fn call(context: ?*anyopaque, inputs: InputTuple) Output { + std.debug.assert(context == null); return @call(.auto, invoke, inputs); } @@ -47,16 +46,14 @@ pub fn Callable(comptime Output: type, comptime input_types: []const type) type const is_zero_aligned = @typeInfo(ContextPtr).pointer.alignment == 0; return struct { - fn invoke_concrete(context: *anyopaque, inputs: InputTuple) Output { - std.debug.assert(context != fn_context); - + fn invoke_concrete(context: ?*anyopaque, inputs: InputTuple) Output { if (is_zero_aligned) { - return @call(.auto, invoke, .{@as(ContextPtr, @ptrCast(context))} ++ inputs); + return @call(.auto, invoke, .{@as(ContextPtr, @ptrCast(context.?))} ++ inputs); } var args: std.meta.ArgsTuple(Invoke) = undefined; - args[0] = @as(ContextPtr, @ptrCast(@alignCast(context))); + args[0] = @as(ContextPtr, @ptrCast(@alignCast(context.?))); inline for (1..args.len, &inputs) |i, input| { args[i] = input; @@ -73,29 +70,29 @@ pub fn Callable(comptime Output: type, comptime input_types: []const type) type const Self = @This(); pub fn call(self: Self, inputs: InputTuple) Output { - return self.call_with_context(self.context, inputs); + return self.call_with_context(self.has_context, inputs); } pub fn initFn(comptime invoke: anytype) Self { return .{ - .context = fn_context, + .has_context = null, .call_with_context = FnCall(invoke).call, }; } pub fn initRef(context_ptr: anytype, comptime invoke: anytype) Self { return .{ - .context = @ptrCast(context_ptr), + .has_context = @ptrCast(context_ptr), .call_with_context = RefCall(@TypeOf(context_ptr), invoke).invoke_concrete, }; } - pub fn is_fn(self: Self, invoke: anytype) bool { + pub fn isFn(self: Self, invoke: anytype) bool { const Invoke = @TypeOf(invoke); return switch (@typeInfo(Invoke)) { - .pointer => self.is_fn(invoke.*), - .@"fn" => (self.context == fn_context) and (self.call_with_context == FnCall(invoke).call), + .pointer => self.isFn(invoke.*), + .@"fn" => (self.has_context == null) and (self.call_with_context == FnCall(invoke).call), else => @compileError(std.fmt.comptimePrint("parameter `fn` must be a function type or pointer to one, not {s}", .{@typeName(Invoke)})), }; } diff --git a/src/coral/meta.zig b/src/coral/meta.zig index e64fced..7d8e447 100644 --- a/src/coral/meta.zig +++ b/src/coral/meta.zig @@ -8,6 +8,20 @@ pub fn Unwrapped(comptime Value: type) type { }; } +pub fn UnwrappedError(comptime Value: type) type { + return switch (@typeInfo(Value)) { + .error_union => |error_union| error_union.payload, + else => Value, + }; +} + +pub fn UnwrappedOptional(comptime Value: type) type { + return switch (@typeInfo(Value)) { + .optional => |optional| optional.child, + else => Value, + }; +} + pub fn isContainer(@"type": std.builtin.Type) bool { return switch (@"type") { .@"struct", .@"union", .@"enum", .@"opaque" => true, diff --git a/src/coral/tree.zig b/src/coral/tree.zig index 1188d3f..0a2edc0 100644 --- a/src/coral/tree.zig +++ b/src/coral/tree.zig @@ -4,32 +4,128 @@ const std = @import("std"); pub fn Binary(comptime Key: type, comptime Value: type, comptime traits: Traits(Key)) type { return struct { - free_nodes: ?*Node = null, - active_nodes: ?*Node = null, + free_nodes: Pool = .{}, + has_root: ?*Node = null, const Node = struct { key: Key, value: Value, - has_left: ?*Node = null, - has_right: ?*Node = null, + has_lesser: ?*Node = null, + has_greater: ?*Node = null, has_parent: ?*Node = null, fn deinit(self: *Node, allocator: std.mem.Allocator) void { - self.has_parent = undefined; - - if (self.has_left) |left| { + if (self.has_lesser) |left| { left.deinit(allocator); allocator.destroy(left); } - self.has_left = undefined; - - if (self.has_right) |right| { + if (self.has_greater) |right| { right.deinit(allocator); allocator.destroy(right); } - self.has_right = undefined; + self.* = undefined; + } + + fn find(self: *Node, key: Key) ?*Node { + var node = self; + + while (true) { + const next_node = switch (traits.compare(key, node.key)) { + .lesser => node.has_lesser, + .greater => node.has_greater, + + .equal => { + return node; + }, + }; + + node = next_node orelse { + return null; + }; + } + } + + fn getMax(self: *Node) *Node { + var node = self; + + while (self.has_lesser) |lesser| { + node = lesser; + } + + return node; + } + + fn getMin(self: *Node) *Node { + var node = self; + + while (self.has_lesser) |lesser| { + node = lesser; + } + + return node; + } + + fn remove(self: *Node, key: Key) ?*Node { + const node = self.find(key) orelse { + return null; + }; + + if (node.has_lesser == null) { + std.debug.assert(self.transplant(node.has_greater)); + } else if (node.has_greater == null) { + std.debug.assert(self.transplant(node.has_lesser)); + } else { + var successor = node.has_greater.?.getMin(); + + if (successor.has_parent != node) { + // Move successor up: replace successor with its right child first + std.debug.assert(successor.transplant(successor.has_greater)); + + // Attach node.right to successor + successor.has_greater = node.has_greater; + + if (successor.has_greater) |g| { + g.has_parent = successor; + } + } + + // Replace node with successor + std.debug.assert(node.transplant(successor)); + + // Attach node.left to successor + successor.has_lesser = node.has_lesser; + + if (successor.has_lesser) |l| { + l.has_parent = successor; + } + } + + // Detach the removed node completely and return it + node.has_parent = null; + node.has_lesser = null; + node.has_greater = null; + + return node; + } + + fn transplant(self: *Node, has_node: ?*Node) bool { + const parent = self.has_parent orelse { + return false; + }; + + if (parent.has_lesser == self) { + parent.has_lesser = has_node; + } else { + parent.has_greater = has_node; + } + + if (has_node) |node| { + node.has_parent = self.has_parent; + } + + return true; } }; @@ -40,8 +136,8 @@ pub fn Binary(comptime Key: type, comptime Value: type, comptime traits: Traits( var nodes = self.nodes; while (nodes) |node| { - const left = node.has_left orelse { - self.nodes = node.has_right; + const left = node.has_lesser orelse { + self.nodes = node.has_greater; return .{ .key = node.key, @@ -51,13 +147,13 @@ pub fn Binary(comptime Key: type, comptime Value: type, comptime traits: Traits( // Find the rightmost node in left subtree or link back to current var pred = left; - while (pred.has_right != null and pred.has_right != node) { - pred = pred.has_right.?; + while (pred.has_greater != null and pred.has_greater != node) { + pred = pred.has_greater.?; } - if (pred.has_right != null) { - pred.has_right = null; - self.nodes = node.has_right; + if (pred.has_greater != null) { + pred.has_greater = null; + self.nodes = node.has_greater; return .{ .key = node.key, @@ -65,8 +161,8 @@ pub fn Binary(comptime Key: type, comptime Value: type, comptime traits: Traits( }; } - pred.has_right = node; - self.nodes = node.has_left; + pred.has_greater = node; + self.nodes = node.has_lesser; nodes = self.nodes; } @@ -82,16 +178,48 @@ pub fn Binary(comptime Key: type, comptime Value: type, comptime traits: Traits( } }; + const Pool = struct { + has_node: ?*Node = null, + + fn create(self: *Pool, allocator: std.mem.Allocator, node: Node) std.mem.Allocator.Error!*Node { + if (self.has_node) |free_node| { + self.has_node = free_node.has_parent; + + free_node.* = node; + + return free_node; + } + + const created_node = try allocator.create(Node); + + created_node.* = node; + + return created_node; + } + + fn destroy(self: *Pool, node: *Node) void { + node.* = .{ + .key = undefined, + .value = undefined, + .has_parent = self.has_node, + .has_lesser = null, + .has_greater = null, + }; + + self.has_node = node; + } + }; + const Self = @This(); pub fn clear(self: *Self) void { var free_nodes: ?*Node = null; - if (self.active_nodes) |root| { + if (self.has_root) |root| { // Push root onto stack root.has_parent = free_nodes; free_nodes = root; - self.active_nodes = null; + self.has_root = null; } while (free_nodes) |node| { @@ -99,60 +227,37 @@ pub fn Binary(comptime Key: type, comptime Value: type, comptime traits: Traits( free_nodes = node.has_parent; // Push children onto stack - if (node.has_left) |left| { + if (node.has_lesser) |left| { left.has_parent = free_nodes; free_nodes = left; } - if (node.has_right) |right| { + if (node.has_greater) |right| { right.has_parent = free_nodes; free_nodes = right; } - // Add node to free list - node.has_left = null; - node.has_right = null; - node.has_parent = null; - node.key = undefined; - node.value = undefined; - node.has_right = self.free_nodes; - self.free_nodes = node; + self.free_nodes.destroy(node); } } - pub fn createNode(self: *Self, allocator: std.mem.Allocator, node: Node) std.mem.Allocator.Error!*Node { - if (self.free_nodes) |free_node| { - self.free_nodes = free_node.has_parent; - - free_node.* = node; - - return free_node; - } - - const created_node = try allocator.create(Node); - - created_node.* = node; - - return created_node; - } - pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { - if (self.active_nodes) |node| { - node.deinit(allocator); - allocator.destroy(node); + if (self.has_root) |root| { + root.deinit(allocator); + allocator.destroy(root); } - self.active_nodes = undefined; + self.has_root = undefined; } pub fn insert(self: *Self, allocator: std.mem.Allocator, key: Key, value: Value) std.mem.Allocator.Error!?*Value { - var node = self.active_nodes orelse { - self.active_nodes = try self.createNode(allocator, .{ + var node = self.has_root orelse { + self.has_root = try self.free_nodes.create(allocator, .{ .key = key, .value = value, }); - return &self.active_nodes.?.value; + return &self.has_root.?.value; }; while (true) { @@ -162,26 +267,26 @@ pub fn Binary(comptime Key: type, comptime Value: type, comptime traits: Traits( }, .lesser => { - node = node.has_left orelse { - node.has_left = try self.createNode(allocator, .{ + node = node.has_lesser orelse { + node.has_lesser = try self.free_nodes.create(allocator, .{ .key = key, .value = value, .has_parent = node, }); - return &node.has_left.?.value; + return &node.has_lesser.?.value; }; }, .greater => { - node = node.has_right orelse { - node.has_right = try self.createNode(allocator, .{ + node = node.has_greater orelse { + node.has_greater = try self.free_nodes.create(allocator, .{ .key = key, .value = value, .has_parent = node, }); - return &node.has_right.?.value; + return &node.has_greater.?.value; }; }, } @@ -189,49 +294,54 @@ pub fn Binary(comptime Key: type, comptime Value: type, comptime traits: Traits( } pub const empty = Self{ - .active_nodes = null, + .has_root = null, }; pub fn get(self: Self, key: Key) ?*Value { - var nodes = self.active_nodes; - - while (nodes) |node| { - nodes = switch (traits.compare(key, node.key)) { - .lesser => node.has_left, - .greater => node.has_right, - - .equal => { - return &node.value; - }, - }; + if (self.has_root) |root| { + if (root.find(key)) |node| { + return &node.value; + } } return null; } pub fn getKey(self: Self, key: Key) ?Key { - var nodes = self.active_nodes; - - while (nodes) |node| { - nodes = switch (traits.compare(key, node.key)) { - .lesser => node.has_left, - .greater => node.has_right, - - .equal => { - return node.key; - }, - }; + if (self.has_root) |root| { + if (root.find(key)) |node| { + return &node.key; + } } return null; } pub fn isEmpty(self: *Self) bool { - return self.active_nodes == null; + return self.has_root == null; } pub fn keyValues(self: *const Self) KeyValues { - return .{ .nodes = self.active_nodes }; + return .{ .nodes = self.has_root }; + } + + pub fn remove(self: *Self, key: Key) ?coral.KeyValuePair(Key, Value) { + const root = self.has_root orelse { + return null; + }; + + const node = root.remove(key) orelse { + return null; + }; + + defer { + self.free_nodes.destroy(node); + } + + return .{ + .key = node.key, + .value = node.value, + }; } }; } diff --git a/src/ona/App.zig b/src/ona/App.zig index fb75e33..9ef9ea1 100644 --- a/src/ona/App.zig +++ b/src/ona/App.zig @@ -57,7 +57,7 @@ fn scheduleName(comptime schedule: anytype) [:0]const u8 { }; } -pub fn hasState(self: *Self, comptime State: type) ?*State { +pub fn hasState(self: *const Self, comptime State: type) ?*State { if (self.initialized_states.get(.of(State))) |boxed_state| { return boxed_state.has(State).?; } @@ -92,6 +92,8 @@ pub fn run(self: *Self, tasks: *coral.asio.TaskQueue, comptime schedule: anytype const systems = self.named_systems.get(schedule_name) orelse (try self.named_systems.insert(coral.heap.allocator, schedule_name, .empty)).?; try systems.run(self, tasks); + + systems.applyDeferred(self); } pub fn setState(self: *Self, state: anytype) error{OutOfMemory}!void { diff --git a/src/ona/App/Behavior.zig b/src/ona/App/Behavior.zig index 57e4181..7b45732 100644 --- a/src/ona/App/Behavior.zig +++ b/src/ona/App/Behavior.zig @@ -7,261 +7,279 @@ const ona = @import("../ona.zig"); const std = @import("std"); label: [*:0]const u8, -onInsertion: *const fn (*const Self, std.mem.Allocator, *ona.App, *SystemGraph) std.mem.Allocator.Error![]coral.Box, -onRun: *const fn (*ona.App, []const coral.Box) void, +apply: *const fn (coral.Box, *ona.App) void, +bind: *const fn (*const Self, *ona.App, *SystemGraph) std.mem.Allocator.Error!coral.Box, +call: *const fn (coral.Box, *const ona.App) void, is_blocking: bool, -const Param = struct { - uses_bind: bool, - init_params: []const InitParam, - is_blocking: bool, - - const InitParam = struct { - type_id: *const coral.TypeId, - is_required: bool, - is_read_only: bool, - }; - - fn getInitParams(params: []const std.builtin.Type.Fn.Param) []const InitParam { - const init_params = struct { - const instance = get: { - var buffer: [params.len]InitParam = undefined; - - for (&buffer, params) |*init_param, fn_param| { - const FnParam = fn_param.type orelse { - @compileError("generic params on Param.init fns are disallowed"); - }; - - init_param.* = switch (@typeInfo(FnParam)) { - .pointer => |pointer| .{ - .type_id = .of(pointer.child), - .is_required = true, - .is_read_only = pointer.is_const, - }, - - .optional => |optional| switch (@typeInfo(optional.child)) { - .pointer => |pointer| .{ - .type_id = .of(pointer.child), - .is_required = false, - .is_read_only = pointer.is_const, - }, - - else => @compileError("non-pointer optional params on Param.init fns are disallowed"), - }, - - else => @compileError("params on Param.init fns must be optionals or pointers"), - }; - } - - break :get buffer; - }; - }; - - return &init_params.instance; - } - - pub fn init(comptime fn_param: std.builtin.Type.Fn.Param) Param { - const FnParam = fn_param.type orelse { - @compileError("generic behavior params are not permitted"); - }; - - const init_fn = coral.meta.hasFn(FnParam, "init") orelse { - @compileError(std.fmt.comptimePrint("{s} must contain a .init declaration", .{@typeName(FnParam)})); - }; - - comptime var uses_bind = false; - - if (coral.meta.hasFn(FnParam, "bind")) |bind_fn| { - if (bind_fn.params.len != 0) { - @compileError(std.fmt.comptimePrint("{s}.bind cannot accept any parameters", .{@typeName(FnParam)})); - } - - const State = switch (@typeInfo(bind_fn.return_type.?)) { - .error_union => |error_union| error_union.payload, - else => bind_fn.return_type.?, - }; - - if (init_fn.params.len == 0 or init_fn.params[0].type != *State) { - const fn_param_name = @typeName(FnParam); - - @compileError(std.fmt.comptimePrint("The first parameter of fn {s}.init must be of type {s} while fn {s}.bind is present", .{ - fn_param_name, - @typeName(*State), - fn_param_name, - })); - } - - uses_bind = true; - } - - if (coral.meta.Unwrapped(init_fn.return_type.?) != FnParam) { - const param_type_name = @typeName(FnParam); - - @compileError(std.fmt.comptimePrint("Fn {s}.init must return some variation of {s}", .{ - param_type_name, - param_type_name, - })); - } - - return .{ - .init_params = getInitParams(init_fn.params[@intFromBool(uses_bind)..]), - .is_blocking = usesTrait(FnParam, "blocking"), - .uses_bind = uses_bind, - }; - } -}; - const Self = @This(); -pub fn after(comptime self: *const Self, comptime dependency: *const Self) *const Self { - const afters = struct { - fn on_insertion(behavior: *const Self, app: *ona.App, systems: *SystemGraph) std.mem.Allocator.Error!void { - try self.onInsertion(behavior, app, systems); - try systems.depend_on_behavior(app, behavior, dependency); - } +pub fn of(comptime function: anytype) *const Self { + const Function = @TypeOf(function); - fn on_run(app: *ona.App) void { - self.onRun(app); - } - - const instance = Self{ - .label = std.fmt.comptimePrint("({s} after {s})", .{ self.label, dependency.label }), - .onInsertion = on_insertion, - .onRun = on_run, - }; - }; - - return &afters.instance; -} - -pub fn before(comptime self: *const Self, comptime dependant: *const Self) *const Self { - const afters = struct { - fn on_insertion(behavior: *const Self, app: *ona.App, systems: *SystemGraph) std.mem.Allocator.Error!void { - try systems.depend_on_behavior(app, dependant, behavior); - try systems.insert(app, behavior); - } - - const instance = Self{ - .label = std.fmt.comptimePrint("({s} before {s})", .{ self.label, dependant.label }), - .onInsertion = on_insertion, - }; - }; - - return &afters.instance; -} - -pub fn of(comptime call: anytype) *const Self { - const Call = @TypeOf(call); - - const call_fn = switch (@typeInfo(Call)) { + const function_fn = switch (@typeInfo(Function)) { .@"fn" => |@"fn"| @"fn", - else => @compileError("`call` parameter must be an fn type"), + else => @compileError("`function` parameter must be an fn type"), }; - const behaviors = struct { - fn onInsertion(behavior: *const Self, allocator: std.mem.Allocator, app: *ona.App, systems: *SystemGraph) std.mem.Allocator.Error![]coral.Box { - var states: [call_fn.params.len]coral.Box = undefined; - comptime var states_written = 0; + const behavior = struct { + const ParamStates = define: { + var param_state_types = [_]type{void} ** function_fn.params.len; - errdefer { - inline for (states[states_written..]) |*state| { - state.deinit(); + for (0..param_state_types.len, function_fn.params) |i, function_param| { + const Param = function_param.type orelse { + @compileError("Behavior fns may not have generic parameters"); + }; + + if (coral.meta.hasFn(Param, "bind")) |bind_fn| { + param_state_types[i] = coral.meta.UnwrappedError(bind_fn.return_type.?); } } - inline for (call_fn.params) |call_param| { - const behavior_param = comptime Param.init(call_param); + const Tuple = std.meta.Tuple(¶m_state_types); + + break :define struct { + params: Tuple, + + pub fn deinit(self: *@This()) void { + inline for (0..self.params.len) |i| { + const ParamState = @TypeOf(self.params[i]); + + if (coral.meta.hasFn(ParamState, "deinit")) |deinit_fn| { + if (deinit_fn.params.len != 1 or deinit_fn.params[0].type != *ParamState) { + const param_state_name = @typeName(ParamState); + + @compileError(std.fmt.comptimePrint("Fn {s}.deinit expects only 1 argument and it must be of type {s}", .{ + param_state_name, + param_state_name, + })); + } + + if (deinit_fn.return_type != void) { + const param_state_name = @typeName(ParamState); + + @compileError(std.fmt.comptimePrint("Fn {s}.deinit is expected to return type void", .{ + param_state_name, + })); + } + + self.params[i].deinit(); + } + } + } + }; + }; + + fn apply(local_state: coral.Box, app: *ona.App) void { + var param_states = local_state.has(ParamStates).?; + + inline for (0..param_states.params.len) |i| { + const ParamState = @TypeOf(param_states.params[i]); + + if (coral.meta.hasFn(ParamState, "apply")) |apply_fn| { + if (apply_fn.params.len != 2 or (apply_fn.params[0].type != *ParamState and apply_fn.params[1].type != *ona.App)) { + const param_state_name = @typeName(ParamState); + + @compileError(std.fmt.comptimePrint("Fn {s}.apply expects 2 arguments and they must be of types ({s}, {s})", .{ + param_state_name, + @typeInfo(*ona.App), + param_state_name, + })); + } + + if (coral.meta.UnwrappedError(apply_fn.return_type.?) != void) { + const param_state_name = @typeName(ParamState); + + @compileError(std.fmt.comptimePrint("Fn {s}.apply is expected to return type void or error-wrapped void", .{ + param_state_name, + })); + } + + coral.expect(ParamState.apply, .{ ¶m_states.params[i], app }); + } + } + } + + fn bind(behavior: *const Self, app: *ona.App, systems: *SystemGraph) std.mem.Allocator.Error!coral.Box { + inline for (function_fn.params) |function_param| { + const Param = function_param.type orelse { + @compileError("Behavior fns may not have generic parameters"); + }; + + const init_fn = coral.meta.hasFn(Param, "init") orelse { + @compileError(std.fmt.comptimePrint("{s} must have a .init fn to be used as a behavior param", .{@typeName(Param)})); + }; + + const has_bind_fn = coral.meta.hasFn(Param, "bind"); + + inline for (init_fn.params[@intFromBool(has_bind_fn != null)..]) |init_param| { + const InitParam = coral.meta.UnwrappedOptional(init_param.type orelse { + @compileError(std.fmt.comptimePrint("fn {s}.init may not have generic parameters", .{ + @typeName(Param), + })); + }); + + const init_param_pointer = switch (@typeInfo(InitParam)) { + .pointer => |pointer| pointer, + + else => @compileError(std.fmt.comptimePrint("Behavior param fn {s}.init expects arguments as pointer types to state only", .{ + @typeName(Param), + })), + }; - inline for (behavior_param.init_params) |init_param| { try systems.dependOnType(app, behavior, .{ - .id = init_param.type_id, - .is_read_only = init_param.is_read_only, + .is_read_only = init_param_pointer.is_const, + .id = .of(init_param_pointer.child), }); } - - states[states_written] = try .init(allocator, if (behavior_param.uses_bind) call_param.type.?.bind() else {}); - states_written += 1; } - return allocator.dupe(coral.Box, &states); + var param_states: ParamStates = undefined; + + inline for (0..param_states.params.len, function_fn.params) |i, param| { + param_states.params[i] = switch (@TypeOf(param_states.params[i])) { + void => {}, + else => coral.expect(param.type.?.bind, .{}), + }; + } + + // TODO: Errdefer deinit the param states. + + return .init(coral.heap.allocator, param_states); } - fn onRun(app: *ona.App, states: []const coral.Box) void { - var call_args: std.meta.ArgsTuple(Call) = undefined; + fn call(local_state: coral.Box, app: *const ona.App) void { + var param_states = local_state.has(ParamStates).?; + var function_args: std.meta.ArgsTuple(Function) = undefined; - std.debug.assert(states.len == call_args.len); + inline for (0..function_args.len) |i| { + const Param = function_fn.params[i].type orelse { + @compileError("Behavior fns may not have generic parameters"); + }; - inline for (0..call_args.len) |i| { - const fn_param = call_fn.params[i]; - const param = comptime Param.init(fn_param); - var init_args: std.meta.ArgsTuple(@TypeOf(fn_param.type.?.init)) = undefined; + var init_args: std.meta.ArgsTuple(@TypeOf(Param.init)) = undefined; + const uses_state = @TypeOf(param_states.params[i]) != void; - if (param.uses_bind) { - init_args[0] = states[i].has(@TypeOf(init_args[0].*)).?; + if (uses_state) { + init_args[0] = ¶m_states.params[i]; } - inline for (@intFromBool(param.uses_bind)..init_args.len, param.init_params) |arg_index, init_param| { - if (init_param.is_required) { - const InitArg = @TypeOf(init_args[arg_index].*); + const init_fn = coral.meta.hasFn(Param, "init") orelse { + @compileError(std.fmt.comptimePrint("{s} must have a .init fn to be used as a behavior param", .{@typeName(Param)})); + }; - init_args[arg_index] = app.hasState(InitArg) orelse { - @panic(std.fmt.comptimePrint("{s} is a required state but not present in the App", .{@typeName(InitArg)})); - }; - } else { - const InitArg = @TypeOf(init_args[arg_index].*); + inline for (@intFromBool(uses_state)..init_args.len, init_fn.params[@intFromBool(uses_state)..]) |arg_index, init_param| { + const InitParam = init_param.type.?; - init_args[arg_index] = app.hasState(InitArg); + switch (@typeInfo(InitParam)) { + .optional => |optional| { + const child_pointer = switch (@typeInfo(optional.child)) { + .pointer => |pointer| pointer, + else => @compileError(""), + }; + + init_args[arg_index] = app.hasState(child_pointer.child); + }, + + .pointer => |pointer| { + init_args[arg_index] = app.hasState(pointer.child) orelse { + @panic(std.fmt.comptimePrint("{s} is a required state but not present in the App", .{ + @typeName(pointer.child), + })); + }; + }, + + else => { + @compileError("? or *"); + }, } } - call_args[i] = coral.expect(fn_param.type.?.init, init_args); + function_args[i] = coral.expect(Param.init, init_args); } - if (coral.meta.Unwrapped(call_fn.return_type.?) != void) { - @compileError(std.fmt.comptimePrint("parameter `call` must return some variation of void", .{ - @typeName(Call), + if (coral.meta.Unwrapped(function_fn.return_type.?) != void) { + @compileError(std.fmt.comptimePrint("parameter `function` must return some variation of void", .{ + @typeName(Function), })); } - coral.expect(call, call_args); + coral.expect(function, function_args); } const instance = Self{ - .label = @typeName(Call), - .onInsertion = onInsertion, - .onRun = onRun, + .label = @typeName(Function), + .apply = apply, + .bind = bind, + .call = call, + .is_blocking = is_blocking, + }; - .is_blocking = check: { - for (call_fn.params) |fn_param| { - const param = Param.init(fn_param); + const is_blocking = infer: { + for (function_fn.params) |function_param| { + const Param = function_param.type orelse { + @compileError("Behavior fns may not have generic parameters"); + }; - if (param.is_blocking) { - break :check true; + for (getInitParams(Param)) |init_param| { + const InitParam = coral.meta.UnwrappedOptional(init_param.type orelse { + @compileError(std.fmt.comptimePrint("fn {s}.init may not have generic parameters", .{ + @typeName(Param), + })); + }); + + const init_param_pointer = switch (@typeInfo(InitParam)) { + .pointer => |pointer| pointer, + + else => @compileError(std.fmt.comptimePrint("Behavior param fn {s}.init expects arguments as pointer types to state only", .{ + @typeName(Param), + })), + }; + + if (usesTrait(init_param_pointer.child, "blocking")) { + break :infer true; } } + } - break :check false; - }, + break :infer false; }; }; - return &behaviors.instance; + return &behavior.instance; } -fn usesTrait(comptime SystemParam: type, name: []const u8) bool { - if (@hasDecl(SystemParam, "traits")) { - const Traits = @TypeOf(SystemParam.traits); +fn getInitParams(comptime Param: type) []const std.builtin.Type.Fn.Param { + const init_fn = coral.meta.hasFn(Param, "init") orelse { + @compileError(std.fmt.comptimePrint("{s} must have a .init fn to be used as a behavior param", .{ + @typeName(Param), + })); + }; + + if (coral.meta.UnwrappedError(init_fn.return_type.?) != Param) { + const param_name = @typeName(Param); + + @compileError(std.fmt.comptimePrint("{s}.init must return {s} or an error-wrapped {s} to be used as a behavior param", .{ + param_name, + param_name, + param_name, + })); + } + + return init_fn.params; +} + +fn usesTrait(comptime Param: type, name: []const u8) bool { + if (@hasDecl(Param, "traits")) { + const Traits = @TypeOf(Param.traits); const traits_struct = switch (@typeInfo(Traits)) { .@"struct" => |@"struct"| @"struct", - - else => @compileError(std.fmt.comptimePrint("{s}.traits must be a struct type", .{ - @typeName(SystemParam), - })), + else => @compileError(std.fmt.comptimePrint("{s}.traits must be a struct type", .{@typeName(Param)})), }; if (!traits_struct.is_tuple) { - @compileError(std.fmt.comptimePrint("{s}.traits must be a tuple", .{@typeName(SystemParam)})); + @compileError(std.fmt.comptimePrint("{s}.traits must be a tuple", .{@typeName(Param)})); } for (traits_struct.fields) |field| { @@ -269,7 +287,7 @@ fn usesTrait(comptime SystemParam: type, name: []const u8) bool { @compileError("All members of tuple {s}.traits must be enum literals"); } - if (std.mem.eql(u8, @tagName(@field(SystemParam.traits, field.name)), name)) { + if (std.mem.eql(u8, @tagName(@field(Param.traits, field.name)), name)) { return true; } } diff --git a/src/ona/App/SystemGraph.zig b/src/ona/App/SystemGraph.zig index 6aa4af6..0bfd098 100644 --- a/src/ona/App/SystemGraph.zig +++ b/src/ona/App/SystemGraph.zig @@ -18,17 +18,13 @@ const BehaviorSet = coral.stack.Sequential(*const ona.App.Behavior); const Edge = struct { dependencies: BehaviorSet = .empty, - param_states: []coral.Box = &.{}, + local_state: coral.Box, pub fn deinit(self: *Edge, allocator: std.mem.Allocator) void { - for (self.param_states) |*param_state| { - param_state.deinit(); - } - - allocator.free(self.param_states); + self.local_state.deinit(); self.dependencies.deinit(allocator); - self.param_states = undefined; + self.* = undefined; } }; @@ -42,14 +38,14 @@ const Processed = struct { const Work = struct { behavior: *const ona.App.Behavior, - param_states: []const coral.Box = &.{}, + local_state: coral.Box, }; const WorkSet = coral.stack.Sequential(Work); const Self = @This(); -pub const RunError = error{ +pub const RunError = std.mem.Allocator.Error || error{ MissingDependency, }; @@ -59,26 +55,28 @@ pub const TypeDependency = struct { }; pub fn deinit(self: *Self) void { - self.processed.deinit(coral.heap.allocator); - self.parallel_work.deinit(coral.heap.allocator); - self.parallel_work_ranges.deinit(coral.heap.allocator); - self.blocking_work.deinit(coral.heap.allocator); + const allocator = coral.heap.allocator; + + self.processed.deinit(allocator); + self.parallel_work.deinit(allocator); + self.parallel_work_ranges.deinit(allocator); + self.blocking_work.deinit(allocator); inline for (.{ &self.edges, &self.state_readers, &self.state_writers }) |map| { var key_values = map.keyValues(); while (key_values.nextValue()) |value| { - value.deinit(coral.heap.allocator); + value.deinit(allocator); } - map.deinit(coral.heap.allocator); + map.deinit(allocator); } } pub fn dependOnBehavior(self: *Self, app: *ona.App, dependant: *const ona.App.Behavior, dependency: *const ona.App.Behavior) std.mem.Allocator.Error!void { try self.insert(app, dependant); - const edges = self.edges.get(dependant) orelse (try self.edges.insert(coral.heap.allocator, dependant, .{})).?; + const edges = self.edges.get(dependant).?; if (std.mem.indexOfScalar(*const ona.App.Behavior, edges.dependencies.items.slice(), dependency) == null) { try edges.dependencies.pushGrow(coral.heap.allocator, dependency); @@ -135,17 +133,28 @@ pub const empty = Self{ .parallel_work_ranges = .empty, }; -pub fn insert(self: *Self, app: *ona.App, behavior: *const ona.App.Behavior) std.mem.Allocator.Error!void { - self.processed.clear(); +pub fn applyDeferred(self: *Self, app: *ona.App) void { + for (self.parallel_work.items.slice()) |work| { + work.behavior.apply(work.local_state, app); + } - if (try self.edges.insert(coral.heap.allocator, behavior, .{})) |edge| { - edge.param_states = try behavior.onInsertion(behavior, coral.heap.allocator, app, self); + for (self.blocking_work.items.slice()) |work| { + work.behavior.apply(work.local_state, app); } } -fn parallelRun(app: *ona.App, works: []const Work) void { - for (works) |work| { - work.behavior.onRun(app, work.param_states); +pub fn insert(self: *Self, app: *ona.App, behavior: *const ona.App.Behavior) std.mem.Allocator.Error!void { + self.processed.clear(); + + if (try self.edges.insert(coral.heap.allocator, behavior, undefined)) |edge| { + errdefer { + std.debug.assert(self.edges.remove(behavior) != null); + } + + edge.* = .{ + .local_state = try behavior.bind(behavior, app, self), + .dependencies = .empty, + }; } } @@ -164,12 +173,12 @@ fn process(self: *Self, behavior: *const ona.App.Behavior, edge: Edge) !Processe try switch (processed.is_blocking) { true => self.blocking_work.pushGrow(coral.heap.allocator, .{ .behavior = behavior, - .param_states = edge.param_states, + .local_state = edge.local_state, }), false => self.parallel_work.pushGrow(coral.heap.allocator, .{ .behavior = behavior, - .param_states = edge.param_states, + .local_state = edge.local_state, }), }; } @@ -177,7 +186,7 @@ fn process(self: *Self, behavior: *const ona.App.Behavior, edge: Edge) !Processe return processed; } -pub fn run(self: *Self, app: *ona.App, tasks: *coral.asio.TaskQueue) (std.mem.Allocator.Error || RunError)!void { +pub fn run(self: *Self, app: *const ona.App, tasks: *coral.asio.TaskQueue) RunError!void { if (self.processed.isEmpty()) { errdefer { self.processed.clear(); @@ -205,19 +214,26 @@ pub fn run(self: *Self, app: *ona.App, tasks: *coral.asio.TaskQueue) (std.mem.Al } } - var parallel_work_offset: usize = 0; + { + const parallel_work = self.parallel_work.items.slice(); + var parallel_work_offset: usize = 0; - for (self.parallel_work_ranges.items.slice()) |parallel_work_range| { - const work = self.parallel_work.items.slice()[parallel_work_offset .. parallel_work_offset + parallel_work_range]; + for (self.parallel_work_ranges.items.slice()) |parallel_work_range| { + try tasks.execute(coral.asio.CallTask(runWorkGroup).init(.{ + app, + parallel_work[parallel_work_offset .. parallel_work_offset + parallel_work_range], + })); - try tasks.execute(coral.asio.CallTask(parallelRun).init(.{ app, work })); - - parallel_work_offset += parallel_work_range; + parallel_work_offset += parallel_work_range; + } } tasks.finish(); + runWorkGroup(app, self.blocking_work.items.slice()); +} - for (self.blocking_work.items.slice()) |work| { - work.behavior.onRun(app, work.param_states); +fn runWorkGroup(app: *const ona.App, parallel_work: []const Work) void { + for (parallel_work) |work| { + work.behavior.call(work.local_state, app); } } diff --git a/src/ona/gfx.zig b/src/ona/gfx.zig index ea16a8a..dcc46f4 100644 --- a/src/ona/gfx.zig +++ b/src/ona/gfx.zig @@ -29,8 +29,17 @@ pub const Display = struct { is_hidden: bool, }; -pub fn queueSynchronize(context: ona.Exclusive(Context), display: ona.Read(Display)) !void { - context.ptr.shared.swapBuffers(display.ptr.*); +pub fn synchronize(commands: ona.Commands) !void { + const buffer_swap = struct { + fn apply(app: *ona.App) void { + const context = app.hasState(Context).?; + const display = app.hasState(Display).?; + + context.shared.swapBuffers(display.*); + } + }; + + try commands.push(buffer_swap.apply); } pub fn setup(app: *ona.App) !void { @@ -66,5 +75,5 @@ pub fn setup(app: *ona.App) !void { try app.setState(context); } - try app.on(.render, .of(queueSynchronize)); + try app.on(.render, .of(synchronize)); } diff --git a/src/ona/gfx/Context.zig b/src/ona/gfx/Context.zig index 189442d..a1e3637 100644 --- a/src/ona/gfx/Context.zig +++ b/src/ona/gfx/Context.zig @@ -19,7 +19,7 @@ pub const Queue = struct { commands: coral.stack.Sequential(Command) = .empty, has_next: ?*Queue = null, - pub fn cancel(self: *Queue) void { + pub fn reset(self: *Queue) void { self.commands.clear(); } @@ -184,7 +184,7 @@ const Shared = struct { while (self.swap_buffers[@intFromBool(!self.is_swapped)].dequeue()) |queue| { defer { - queue.cancel(); + queue.reset(); queue.has_next = self.has_pooled_queue.load(.acquire); diff --git a/src/ona/ona.zig b/src/ona/ona.zig index 09dfe60..5ceb398 100644 --- a/src/ona/ona.zig +++ b/src/ona/ona.zig @@ -158,6 +158,70 @@ fn Channel(comptime Message: type) type { }; } +pub const Commands = struct { + pending: *Buffer, + + const Action = coral.Callable(void, &.{*App}); + + const Buffer = struct { + arena: std.heap.ArenaAllocator, + actions: coral.stack.Sequential(Action), + + pub fn apply(self: *Buffer, app: *App) void { + for (self.actions.items.slice()) |action| { + action.call(.{app}); + } + + self.reset(); + } + + pub fn reset(self: *Buffer) void { + if (!self.arena.reset(.retain_capacity)) { + std.log.warn("Failed to retain command buffer arena capacity", .{}); + } + + self.actions.clear(); + } + + pub fn deinit(self: *Buffer) void { + self.actions.deinit(coral.heap.allocator); + self.arena.deinit(); + } + }; + + pub fn bind() Buffer { + return .{ + .arena = .init(coral.heap.allocator), + .actions = .empty, + }; + } + + pub fn init(buffer: *Buffer) Commands { + return .{ + .pending = buffer, + }; + } + + pub fn push(self: Commands, comptime command: anytype) std.mem.Allocator.Error!void { + const Command = @TypeOf(command); + + switch (Command) { + fn (*App) void => { + try self.pending.actions.pushGrow(coral.heap.allocator, .initFn(command)); + }, + + else => { + const arena_allocator = self.pending.arena.allocator(); + const context = try arena_allocator.create(Command); + + context.* = .{}; + + try self.pending.actions.pushGrow(coral.heap.allocator, .initRef(command)); + }, + } + } +}; + pub fn Exclusive(comptime State: type) type { return struct { ptr: *State,