Add deferred system operations
Some checks reported errors
continuous-integration/drone/push Build was killed

This commit is contained in:
kayomn 2025-08-11 10:13:21 +01:00
parent 340f843740
commit 89d975f668
10 changed files with 578 additions and 348 deletions

View File

@ -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;
}

View File

@ -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)})),
};
}

View File

@ -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,

View File

@ -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,47 +178,12 @@ pub fn Binary(comptime Key: type, comptime Value: type, comptime traits: Traits(
}
};
const Self = @This();
const Pool = struct {
has_node: ?*Node = null,
pub fn clear(self: *Self) void {
var free_nodes: ?*Node = null;
if (self.active_nodes) |root| {
// Push root onto stack
root.has_parent = free_nodes;
free_nodes = root;
self.active_nodes = null;
}
while (free_nodes) |node| {
// Pop node from stack
free_nodes = node.has_parent;
// Push children onto stack
if (node.has_left) |left| {
left.has_parent = free_nodes;
free_nodes = left;
}
if (node.has_right) |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;
}
}
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;
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;
@ -136,23 +197,67 @@ pub fn Binary(comptime Key: type, comptime Value: type, comptime traits: Traits(
return created_node;
}
pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
if (self.active_nodes) |node| {
node.deinit(allocator);
allocator.destroy(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.has_root) |root| {
// Push root onto stack
root.has_parent = free_nodes;
free_nodes = root;
self.has_root = null;
}
self.active_nodes = undefined;
while (free_nodes) |node| {
// Pop node from stack
free_nodes = node.has_parent;
// Push children onto stack
if (node.has_lesser) |left| {
left.has_parent = free_nodes;
free_nodes = left;
}
if (node.has_greater) |right| {
right.has_parent = free_nodes;
free_nodes = right;
}
self.free_nodes.destroy(node);
}
}
pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
if (self.has_root) |root| {
root.deinit(allocator);
allocator.destroy(root);
}
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 => {
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,
};
}
};
}

View File

@ -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 {

View File

@ -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();
}
}
inline for (call_fn.params) |call_param| {
const behavior_param = comptime Param.init(call_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,
});
}
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);
}
fn onRun(app: *ona.App, states: []const coral.Box) void {
var call_args: std.meta.ArgsTuple(Call) = undefined;
std.debug.assert(states.len == call_args.len);
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;
if (param.uses_bind) {
init_args[0] = states[i].has(@TypeOf(init_args[0].*)).?;
}
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].*);
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)}));
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");
};
} else {
const InitArg = @TypeOf(init_args[arg_index].*);
init_args[arg_index] = app.hasState(InitArg);
if (coral.meta.hasFn(Param, "bind")) |bind_fn| {
param_state_types[i] = coral.meta.UnwrappedError(bind_fn.return_type.?);
}
}
call_args[i] = coral.expect(fn_param.type.?.init, init_args);
}
const Tuple = std.meta.Tuple(&param_state_types);
if (coral.meta.Unwrapped(call_fn.return_type.?) != void) {
@compileError(std.fmt.comptimePrint("parameter `call` must return some variation of void", .{
@typeName(Call),
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,
}));
}
coral.expect(call, call_args);
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,
}));
}
const instance = Self{
.label = @typeName(Call),
.onInsertion = onInsertion,
.onRun = onRun,
.is_blocking = check: {
for (call_fn.params) |fn_param| {
const param = Param.init(fn_param);
if (param.is_blocking) {
break :check true;
self.params[i].deinit();
}
}
}
break :check false;
},
};
};
return &behaviors.instance;
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,
}));
}
fn usesTrait(comptime SystemParam: type, name: []const u8) bool {
if (@hasDecl(SystemParam, "traits")) {
const Traits = @TypeOf(SystemParam.traits);
if (coral.meta.UnwrappedError(apply_fn.return_type.?) != void) {
const param_state_name = @typeName(ParamState);
const traits_struct = switch (@typeInfo(Traits)) {
.@"struct" => |@"struct"| @"struct",
@compileError(std.fmt.comptimePrint("Fn {s}.apply is expected to return type void or error-wrapped void", .{
param_state_name,
}));
}
else => @compileError(std.fmt.comptimePrint("{s}.traits must be a struct type", .{
@typeName(SystemParam),
coral.expect(ParamState.apply, .{ &param_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),
})),
};
try systems.dependOnType(app, behavior, .{
.is_read_only = init_param_pointer.is_const,
.id = .of(init_param_pointer.child),
});
}
}
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 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;
inline for (0..function_args.len) |i| {
const Param = function_fn.params[i].type orelse {
@compileError("Behavior fns may not have generic parameters");
};
var init_args: std.meta.ArgsTuple(@TypeOf(Param.init)) = undefined;
const uses_state = @TypeOf(param_states.params[i]) != void;
if (uses_state) {
init_args[0] = &param_states.params[i];
}
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)}));
};
inline for (@intFromBool(uses_state)..init_args.len, init_fn.params[@intFromBool(uses_state)..]) |arg_index, init_param| {
const InitParam = init_param.type.?;
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 *");
},
}
}
function_args[i] = coral.expect(Param.init, init_args);
}
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(function, function_args);
}
const instance = Self{
.label = @typeName(Function),
.apply = apply,
.bind = bind,
.call = call,
.is_blocking = is_blocking,
};
const is_blocking = infer: {
for (function_fn.params) |function_param| {
const Param = function_param.type orelse {
@compileError("Behavior fns may not have generic parameters");
};
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 :infer false;
};
};
return &behavior.instance;
}
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(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;
}
}

View File

@ -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 applyDeferred(self: *Self, app: *ona.App) void {
for (self.parallel_work.items.slice()) |work| {
work.behavior.apply(work.local_state, app);
}
for (self.blocking_work.items.slice()) |work| {
work.behavior.apply(work.local_state, app);
}
}
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, .{})) |edge| {
edge.param_states = try behavior.onInsertion(behavior, coral.heap.allocator, app, self);
}
if (try self.edges.insert(coral.heap.allocator, behavior, undefined)) |edge| {
errdefer {
std.debug.assert(self.edges.remove(behavior) != null);
}
fn parallelRun(app: *ona.App, works: []const Work) void {
for (works) |work| {
work.behavior.onRun(app, work.param_states);
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
}
}
{
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];
try tasks.execute(coral.asio.CallTask(parallelRun).init(.{ app, work }));
try tasks.execute(coral.asio.CallTask(runWorkGroup).init(.{
app,
parallel_work[parallel_work_offset .. 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);
}
}

View File

@ -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));
}

View File

@ -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);

View File

@ -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,