diff --git a/build.zig b/build.zig index 1c21e84..1a2a0ee 100644 --- a/build.zig +++ b/build.zig @@ -208,9 +208,9 @@ pub fn build(b: *std.Build) !void { }); const ona_module = try project.add_module(b, "ona", .{}); - const input_module = try project.add_module(b, "input", .{}); + const hid_module = try project.add_module(b, "hid", .{}); - const coral_module = try project.add_module(b, "coral", .{ + const gfx_module = try project.add_module(b, "gfx", .{ .imports = &.{ .{ .name = "sokol", @@ -224,14 +224,14 @@ pub fn build(b: *std.Build) !void { .{ .name = "input", - .module = input_module, + .module = hid_module, }, }, }); - coral_module.addIncludePath(b.path("ext/")); + gfx_module.addIncludePath(b.path("ext/")); - coral_module.linkLibrary(spirv_cross: { + gfx_module.linkLibrary(spirv_cross: { const dir = "ext/spirv-cross/"; const sources = [_][]const u8{ @@ -274,12 +274,12 @@ pub fn build(b: *std.Build) !void { break: spirv_cross lib; }); - coral_module.linkSystemLibrary("SDL2", .{ + gfx_module.linkSystemLibrary("SDL2", .{ .needed = true, .preferred_link_mode = .dynamic, }); - coral_module.link_libc = true; + gfx_module.link_libc = true; try project.find_tests(b); try project.find_demos(b); diff --git a/demos/effects.zig b/demos/effects.zig index 3d0a2c2..5fc70d0 100644 --- a/demos/effects.zig +++ b/demos/effects.zig @@ -1,4 +1,4 @@ -const coral = @import("coral"); +const gfx = @import("gfx"); const ona = @import("ona"); @@ -12,17 +12,11 @@ const CRT = extern struct { }; const Effects = struct { - render_texture: coral.Texture = .default, - crt_effect: coral.Effect = .default, + render_texture: gfx.Texture = .default, + crt_effect: gfx.Effect = .default, }; -pub const main = ona.start(setup, .{ - .tick_rate = 60, - .execution = .{.thread_share = 0.1}, - .middleware = &.{coral.setup}, -}); - -fn load(display: ona.Write(coral.Display), actors: ona.Write(Effects), assets: ona.Write(coral.Assets)) !void { +fn load(display: ona.Write(gfx.Display), actors: ona.Write(Effects), assets: ona.Write(gfx.Assets)) !void { display.state.width, display.state.height = .{1280, 720}; actors.state.render_texture = try assets.state.load_texture(.{ @@ -39,10 +33,16 @@ fn load(display: ona.Write(coral.Display), actors: ona.Write(Effects), assets: o actors.state.crt_effect = try assets.state.load_effect_file(ona.files.bundle, "./crt.frag.spv"); } -fn render(commands: coral.Commands, effects: ona.Write(Effects), app: ona.Read(ona.App), display: ona.Write(coral.Display)) !void { +pub const main = ona.App.setup. + with_module(gfx). + with_state(Effects{}). + with_system(.load, ona.system_fn(load), .{.label = "load effects"}). + with_system(.render, ona.system_fn(render), .{.label = "render effects"}).build(); + +fn render(commands: gfx.Commands, effects: ona.Write(Effects), app: ona.Read(ona.App), display: ona.Write(gfx.Display)) !void { try commands.set_target(.{ .texture = effects.state.render_texture, - .clear_color = coral.colors.black, + .clear_color = gfx.colors.black, .clear_depth = 0, .clear_stencil = 0, }); @@ -50,7 +50,7 @@ fn render(commands: coral.Commands, effects: ona.Write(Effects), app: ona.Read(o const display_width: f32 = @floatFromInt(display.state.width); const display_height: f32 = @floatFromInt(display.state.height); - const display_transform = coral.Transform2D{ + const display_transform = gfx.Transform2D{ .origin = .{display_width / 2, display_height / 2}, .xbasis = .{display_width, 0}, .ybasis = .{0, display_height}, @@ -84,9 +84,3 @@ fn render(commands: coral.Commands, effects: ona.Write(Effects), app: ona.Read(o }); } -fn setup(world: *ona.World, events: ona.App.Events) !void { - try world.set_state(Effects{}); - - try world.on_event(events.load, ona.system_fn(load), .{.label = "load"}); - try world.on_event(events.render, ona.system_fn(render), .{.label = "render actors"}); -} diff --git a/readme.md b/readme.md index 4b93ccf..12e78fa 100644 --- a/readme.md +++ b/readme.md @@ -44,11 +44,11 @@ As the project evolves, dependencies on libraries external to the project codeba After the repository has finished cloning, you will then want to also clone all Git submodule dependencies via the `git submodule update --init --recursive` command. If you are using a Git front-end, this may be a menu option or is handled *automagically*, it varies depending on the front-end. -Once all third-party tools and dependencies are satisfied, navigate to the root project folder and run `zig build demos` to build the demo executables. The resulting binaries will be placed in `/demos` will a name respective to the source file it was built from. +Once all third-party tools and dependencies are satisfied, navigate to the root project folder and run `zig build demos` to build the demo executables. The resulting binaries will be placed in `/demos` with a name respective to the source file it was built from. To experiment without creating your own Zig project, you can create a new uniquely named source file in this directory and the `zig build demos` step will (re)build it along with everything else. -The unit testing suite for the engine modules (`src/ona`, `src/coral`, etc.) may be built and ran via `zig build tests`. +The unit testing suite for the engine modules (`src/ona`, `src/gfx`, etc.) may be built and ran via `zig build tests`. Tests are ran by our continuous integration host so should these shouldn't fail locally without user intervention. If they do, feel free to report an issue via my email linked in my Sauce Control bio or through any other means of public communication I have. @@ -103,6 +103,6 @@ const app = b.addExecutable(.{ }); app.root_module.addImport("ona", ona_dependency.module("ona")); -app.root_module.addImport("coral", ona_dependency.module("coral")); -app.root_module.addImport("input", ona_dependency.module("input")); +app.root_module.addImport("gfx", ona_dependency.module("gfx")); +app.root_module.addImport("hid", ona_dependency.module("hid")); ``` diff --git a/src/coral/colors.zig b/src/coral/colors.zig deleted file mode 100644 index ddc6624..0000000 --- a/src/coral/colors.zig +++ /dev/null @@ -1,13 +0,0 @@ -const coral = @import("./coral.zig"); - -pub const black = greyscale(0); - -pub const white = greyscale(1); - -pub fn greyscale(v: f32) coral.Color { - return .{v, v, v, 1}; -} - -pub fn rgb(r: f32, g: f32, b: f32) coral.Color { - return .{r, g, b, 1}; -} diff --git a/src/coral/Resources.zig b/src/gfx/Resources.zig similarity index 93% rename from src/coral/Resources.zig rename to src/gfx/Resources.zig index 2c929b7..a566c65 100644 --- a/src/coral/Resources.zig +++ b/src/gfx/Resources.zig @@ -1,4 +1,4 @@ -const coral = @import("./coral.zig"); +const gfx = @import("./gfx.zig"); const ona = @import("ona"); @@ -24,7 +24,7 @@ pub const Effect = struct { self.* = undefined; } - pub fn init(desc: coral.Effect.Desc) spirv.Error!Effect { + pub fn init(desc: gfx.Effect.Desc) spirv.Error!Effect { var spirv_arena = std.heap.ArenaAllocator.init(ona.heap.allocator); defer { @@ -282,7 +282,7 @@ pub const Texture = struct { self.* = undefined; } - pub fn init(desc: coral.Texture.Desc) std.mem.Allocator.Error!Texture { + pub fn init(desc: gfx.Texture.Desc) std.mem.Allocator.Error!Texture { const pixel_format = switch (desc.format) { .rgba8 => sokol.gfx.PixelFormat.RGBA8, .bgra8 => sokol.gfx.PixelFormat.BGRA8, @@ -386,7 +386,7 @@ pub const Texture = struct { const TexturePool = ona.Pool(Texture); -pub fn create_effect(self: *Self, desc: coral.Effect.Desc) !coral.Effect { +pub fn create_effect(self: *Self, desc: gfx.Effect.Desc) !gfx.Effect { var effect = try Effect.init(desc); errdefer effect.deinit(); @@ -394,7 +394,7 @@ pub fn create_effect(self: *Self, desc: coral.Effect.Desc) !coral.Effect { return @enumFromInt(try self.effects.insert(effect)); } -pub fn create_texture(self: *Self, desc: coral.Texture.Desc) !coral.Texture { +pub fn create_texture(self: *Self, desc: gfx.Texture.Desc) !gfx.Texture { var texture = try Texture.init(desc); errdefer texture.deinit(); @@ -422,7 +422,7 @@ pub fn deinit(self: *Self) void { self.* = undefined; } -pub fn destroy_effect(self: *Self, handle: coral.Effect) bool { +pub fn destroy_effect(self: *Self, handle: gfx.Effect) bool { switch (handle) { .default => {}, @@ -438,7 +438,7 @@ pub fn destroy_effect(self: *Self, handle: coral.Effect) bool { return true; } -pub fn destroy_texture(self: *Self, handle: coral.Texture) bool { +pub fn destroy_texture(self: *Self, handle: gfx.Texture) bool { switch (handle) { .default => {}, @@ -454,11 +454,11 @@ pub fn destroy_texture(self: *Self, handle: coral.Texture) bool { return true; } -pub fn get_effect(self: *Self, handle: coral.Effect) ?*Effect { +pub fn get_effect(self: *Self, handle: gfx.Effect) ?*Effect { return self.effects.get(@intFromEnum(handle)); } -pub fn get_texture(self: *Self, handle: coral.Texture) ?*Texture { +pub fn get_texture(self: *Self, handle: gfx.Texture) ?*Texture { return self.textures.get(@intFromEnum(handle)); } @@ -478,11 +478,11 @@ pub fn init() !Self { } }; - assert.is_handle(coral.Effect.default, try pools.create_effect(.{ + assert.is_handle(gfx.Effect.default, try pools.create_effect(.{ .fragment_spirv_ops = &spirv.to_ops(@embedFile("./shaders/2d_default.frag.spv")), })); - assert.is_handle(coral.Texture.default, try pools.create_texture(.{ + assert.is_handle(gfx.Texture.default, try pools.create_texture(.{ .format = .rgba8, .access = .{ @@ -503,7 +503,7 @@ pub fn init() !Self { }, })); - assert.is_handle(coral.Texture.backbuffer, try pools.create_texture(.{ + assert.is_handle(gfx.Texture.backbuffer, try pools.create_texture(.{ .format = .rgba8, .access = .{ diff --git a/src/gfx/colors.zig b/src/gfx/colors.zig new file mode 100644 index 0000000..6f4d887 --- /dev/null +++ b/src/gfx/colors.zig @@ -0,0 +1,13 @@ +const gfx = @import("./gfx.zig"); + +pub const black = greyscale(0); + +pub const white = greyscale(1); + +pub fn greyscale(v: f32) gfx.Color { + return .{v, v, v, 1}; +} + +pub fn rgb(r: f32, g: f32, b: f32) gfx.Color { + return .{r, g, b, 1}; +} diff --git a/src/coral/coral.zig b/src/gfx/gfx.zig similarity index 99% rename from src/coral/coral.zig rename to src/gfx/gfx.zig index 5b61d1c..5612145 100644 --- a/src/coral/coral.zig +++ b/src/gfx/gfx.zig @@ -477,9 +477,9 @@ pub fn setup(world: *ona.World, events: ona.App.Events) (error {Unsupported} || } try world.set_state(Display{}); - try world.on_event(events.pre_update, ona.system_fn(poll), .{.label = "poll coral"}); - try world.on_event(events.exit, ona.system_fn(stop), .{.label = "stop coral"}); - try world.on_event(events.finish, ona.system_fn(synchronize), .{.label = "synchronize coral"}); + try world.on_event(events.pre_update, ona.system_fn(poll), .{.label = "poll gfx"}); + try world.on_event(events.exit, ona.system_fn(stop), .{.label = "stop gfx"}); + try world.on_event(events.finish, ona.system_fn(synchronize), .{.label = "synchronize gfx"}); } pub fn stop(assets: ona.Write(Assets)) void { diff --git a/src/coral/rendering.zig b/src/gfx/rendering.zig similarity index 95% rename from src/coral/rendering.zig rename to src/gfx/rendering.zig index 2e4182f..3c9775b 100644 --- a/src/coral/rendering.zig +++ b/src/gfx/rendering.zig @@ -1,6 +1,6 @@ const Resources = @import("./Resources.zig"); -const coral = @import("./coral.zig"); +const gfx = @import("./gfx.zig"); const ext = @cImport({ @cInclude("SDL2/SDL.h"); @@ -20,12 +20,12 @@ const Frame = struct { quad_vertex_buffer: sokol.gfx.Buffer, drawn_count: usize = 0, flushed_count: usize = 0, - current_source_texture: coral.Texture = .default, - current_target_texture: coral.Texture = .backbuffer, - current_effect: coral.Effect = .default, + current_source_texture: gfx.Texture = .default, + current_target_texture: gfx.Texture = .backbuffer, + current_effect: gfx.Effect = .default, const DrawTexture = extern struct { - transform: coral.Transform2D, + transform: gfx.Transform2D, tint: @Vector(4, u8) = @splat(std.math.maxInt(u8)), depth: f32 = 0, texture_offset: @Vector(2, f32) = @splat(0), @@ -73,7 +73,7 @@ const Frame = struct { }; } - pub fn draw_texture(self: *Frame, resources: *Resources, command: coral.Commands.DrawTextureCommand) !void { + pub fn draw_texture(self: *Frame, resources: *Resources, command: gfx.Commands.DrawTextureCommand) !void { if (command.texture != self.current_source_texture) { self.flush(resources); } @@ -173,7 +173,7 @@ const Frame = struct { } } - pub fn set_effect(self: *Frame, resources: *Resources, command: coral.Commands.SetEffectCommand) void { + pub fn set_effect(self: *Frame, resources: *Resources, command: gfx.Commands.SetEffectCommand) void { if (command.effect != self.current_effect) { self.flush(resources); } @@ -185,7 +185,7 @@ const Frame = struct { } } - pub fn set_target(self: *Frame, resources: *Resources, command: coral.Commands.SetTargetCommand) void { + pub fn set_target(self: *Frame, resources: *Resources, command: gfx.Commands.SetTargetCommand) void { sokol.gfx.endPass(); var pass = sokol.gfx.Pass{ @@ -237,7 +237,7 @@ const vertex_indices = .{ .instance = 1, }; -fn orthographic_projection(near: f32, far: f32, viewport: coral.Rect) Matrix(4, f32) { +fn orthographic_projection(near: f32, far: f32, viewport: gfx.Rect) Matrix(4, f32) { const width = viewport.right - viewport.left; const height = viewport.bottom - viewport.top; @@ -249,7 +249,7 @@ fn orthographic_projection(near: f32, far: f32, viewport: coral.Rect) Matrix(4, }; } -pub fn process_work(pending_work: *coral.Assets.WorkQueue, window: *ext.SDL_Window) !void { +pub fn process_work(pending_work: *gfx.Assets.WorkQueue, window: *ext.SDL_Window) !void { const context = configure_and_create: { var result = @as(c_int, 0); diff --git a/src/coral/shaders/2d_default.frag b/src/gfx/shaders/2d_default.frag similarity index 100% rename from src/coral/shaders/2d_default.frag rename to src/gfx/shaders/2d_default.frag diff --git a/src/coral/shaders/2d_default.vert b/src/gfx/shaders/2d_default.vert similarity index 100% rename from src/coral/shaders/2d_default.vert rename to src/gfx/shaders/2d_default.vert diff --git a/src/coral/spirv.zig b/src/gfx/spirv.zig similarity index 99% rename from src/coral/spirv.zig rename to src/gfx/spirv.zig index 09254d3..dfe55ab 100644 --- a/src/coral/spirv.zig +++ b/src/gfx/spirv.zig @@ -1,4 +1,4 @@ -const coral = @import("./coral.zig"); +const gfx = @import("./gfx.zig"); const ext = @cImport({ @cInclude("spirv-cross/spirv_cross_c.h"); diff --git a/src/input/input.zig b/src/hid/hid.zig similarity index 100% rename from src/input/input.zig rename to src/hid/hid.zig diff --git a/src/ona/World.zig b/src/ona/World.zig index 208eb3b..989ff16 100644 --- a/src/ona/World.zig +++ b/src/ona/World.zig @@ -128,11 +128,17 @@ const Schedule = struct { graph: Graph, arena: std.heap.ArenaAllocator, system_id_nodes: ona.map.Hashed(usize, NodeBundle, ona.map.usize_traits), + inserted_systems: ona.stack.Sequential(InsertedSystem) = .{}, read_write_resource_id_nodes: ResourceNodeBundle, read_only_resource_id_nodes: ResourceNodeBundle, parallel_work_bundles: ParallelNodeBundles, blocking_work: NodeBundle, + const InsertedSystem = struct { + info: *const ona.SystemInfo, + order: ona.SystemOrder, + }; + pub fn deinit(self: *Schedule, world: *Self) void { { var nodes = self.system_id_nodes.entries(); @@ -184,12 +190,91 @@ const Schedule = struct { self.system_id_nodes.deinit(); self.read_write_resource_id_nodes.deinit(); self.read_only_resource_id_nodes.deinit(); + self.inserted_systems.deinit(); self.arena.deinit(); } pub fn run(self: *Schedule, world: *Self) anyerror!void { if (self.is_invalidated()) { const work = struct { + fn bind_systems(schedule: *Schedule, world_: *Self) !void { + for (schedule.inserted_systems.values) |system| { + const nodes = lazily_create: { + const system_id = @intFromPtr(system.info); + + break: lazily_create schedule.system_id_nodes.get(system_id) orelse insert: { + std.debug.assert(try schedule.system_id_nodes.emplace(system_id, .{ + .allocator = schedule.system_id_nodes.allocator, + })); + + break: insert schedule.system_id_nodes.get(system_id).?; + }; + }; + + const dependencies = init: { + const total_run_orders = system.order.run_after.len + system.order.run_before.len; + const dependencies = try ona.heap.allocator.alloc(Dependency, total_run_orders); + var dependencies_written = @as(usize, 0); + + for (system.order.run_after) |after_system| { + dependencies[dependencies_written] = .{ + .id = @intFromPtr(after_system), + .kind = .after, + }; + + dependencies_written += 1; + } + + for (system.order.run_before) |before_system| { + dependencies[dependencies_written] = .{ + .id = @intFromPtr(before_system), + .kind = .before, + }; + + dependencies_written += 1; + } + + break: init dependencies; + }; + + errdefer ona.heap.allocator.free(dependencies); + + const label = try ona.heap.allocator.dupeZ(u8, if (system.order.label.len == 0) "anonymous system" else system.order.label); + + errdefer ona.heap.allocator.free(label); + + const node = try schedule.graph.append(.{ + .info = system.info, + .label = label, + .dependencies = dependencies, + .resource_accesses = .{.allocator = ona.heap.allocator}, + }); + + const system_node = schedule.graph.get_ptr(node).?; + const system_parameter_states = system_node.parameter_states[0 .. system.info.parameter_count]; + + errdefer { + for (system.info.used_parameters(), system_parameter_states) |parameter, state| { + parameter.unbind(schedule.arena.allocator(), state, .{ + .world = world_, + }); + } + + std.debug.assert(schedule.graph.remove_node(node) != null); + } + + for (system_parameter_states, system.info.used_parameters()) |*state, parameter| { + state.* = try parameter.bind(schedule.arena.allocator(), .{ + .world = world_, + .node = node, + .systems = schedule, + }); + } + + try nodes.push_grow(node); + } + } + fn regenerate_graph(schedule: *Schedule) !void { schedule.graph.clear_edges(); @@ -311,6 +396,7 @@ const Schedule = struct { } }; + try work.bind_systems(self, world); try work.regenerate_graph(self); try work.sort(self); } @@ -375,7 +461,12 @@ const Schedule = struct { }; } - pub fn invalidate_work(self: *Schedule) void { + pub fn insert(self: *Schedule, info: *const ona.SystemInfo, order: ona.SystemOrder) std.mem.Allocator.Error!void { + try self.inserted_systems.push_grow(.{ + .info = info, + .order = order, + }); + self.blocking_work.clear(); for (self.parallel_work_bundles.values) |*bundle| { @@ -388,83 +479,6 @@ const Schedule = struct { pub fn is_invalidated(self: Schedule) bool { return self.parallel_work_bundles.is_empty() and self.blocking_work.is_empty(); } - - pub fn then(self: *Schedule, world: *Self, info: *const ona.SystemInfo, order: ona.SystemOrder) std.mem.Allocator.Error!void { - const nodes = lazily_create: { - const system_id = @intFromPtr(info); - - break: lazily_create self.system_id_nodes.get(system_id) orelse insert: { - std.debug.assert(try self.system_id_nodes.emplace(system_id, .{ - .allocator = self.system_id_nodes.allocator, - })); - - break: insert self.system_id_nodes.get(system_id).?; - }; - }; - - const dependencies = init: { - const total_run_orders = order.run_after.len + order.run_before.len; - const dependencies = try ona.heap.allocator.alloc(Dependency, total_run_orders); - var dependencies_written = @as(usize, 0); - - for (order.run_after) |after_system| { - dependencies[dependencies_written] = .{ - .id = @intFromPtr(after_system), - .kind = .after, - }; - - dependencies_written += 1; - } - - for (order.run_before) |before_system| { - dependencies[dependencies_written] = .{ - .id = @intFromPtr(before_system), - .kind = .before, - }; - - dependencies_written += 1; - } - - break: init dependencies; - }; - - errdefer ona.heap.allocator.free(dependencies); - - const label = try ona.heap.allocator.dupeZ(u8, if (order.label.len == 0) "anonymous system" else order.label); - - errdefer ona.heap.allocator.free(label); - - const node = try self.graph.append(.{ - .info = info, - .label = label, - .dependencies = dependencies, - .resource_accesses = .{.allocator = ona.heap.allocator}, - }); - - const system = self.graph.get_ptr(node).?; - - errdefer { - for (info.used_parameters(), system.parameter_states[0 .. info.parameter_count]) |parameter, state| { - parameter.unbind(self.arena.allocator(), state, .{ - .world = world, - }); - } - - std.debug.assert(self.graph.remove_node(node) != null); - } - - for (system.parameter_states[0 .. info.parameter_count], info.used_parameters()) |*state, parameter| { - state.* = try parameter.bind(self.arena.allocator(), .{ - .world = world, - .node = node, - .systems = self, - }); - } - - try nodes.push_grow(node); - - self.invalidate_work(); - } }; const Self = @This(); @@ -598,7 +612,7 @@ pub fn init(thread_count: u32) std.Thread.SpawnError!Self { } pub fn on_event(self: *Self, event: Event, action: *const ona.SystemInfo, order: ona.SystemOrder) std.mem.Allocator.Error!void { - try self.event_systems.values[@intFromEnum(event)].then(self, action, order); + try self.event_systems.values[@intFromEnum(event)].insert(action, order); } pub fn run_event(self: *Self, event: Event) anyerror!void { diff --git a/src/ona/ona.zig b/src/ona/ona.zig index 2dab086..55ef7f2 100644 --- a/src/ona/ona.zig +++ b/src/ona/ona.zig @@ -44,6 +44,169 @@ pub const App = struct { exit: World.Event, }; + pub const Setup = struct { + pub const Event = enum { + load, + pre_update, + update, + post_update, + render, + finish, + exit, + }; + + step: fn (*World, Events) anyerror!void = noop, + + fn noop(_: *World, _: Events) !void {} + + pub fn build(self: Setup) fn () anyerror!void { + const Start = struct { + fn main() anyerror!void { + defer { + heap.trace_leaks(); + } + + parse_args: for (std.os.argv[1 ..]) |arg| { + const arg_span = std.mem.span(arg); + const arg_split_index = std.mem.indexOfScalar(u8, arg_span, '=') orelse arg_span.len; + const arg_name = arg_span[0 .. arg_split_index]; + + for (LaunchFlag.values) |value| { + const name = @tagName(value); + + if (!std.mem.eql(u8, arg_name, name)) { + continue; + } + + LaunchFlag.args[@intFromEnum(value)] = + if (arg_split_index == arg_span.len) + name + else + arg_span[arg_split_index ..]; + + continue: parse_args; + } + } + + const cpu_count = @as(u32, @intCast(std.math.clamp(std.Thread.getCpuCount() catch |cpu_count_error| { + @panic(switch (cpu_count_error) { + error.PermissionDenied => "permission denied retrieving CPU count", + error.SystemResources => "system resources are preventing retrieval of the CPU count", + error.Unexpected => "unexpected failure retrieving CPU count", + }); + }, 0, std.math.maxInt(u32)))); + + var world = try World.init(scalars.fractional(cpu_count, @as(f32, 0.75)) orelse 0); + + defer world.deinit(); + + const events = App.Events{ + .load = try world.create_event("load"), + .pre_update = try world.create_event("pre-update"), + .update = try world.create_event("update"), + .post_update = try world.create_event("post-update"), + .render = try world.create_event("render"), + .finish = try world.create_event("finish"), + .exit = try world.create_event("exit"), + }; + + const app = try world.set_get_state(App{ + .events = &events, + .target_frame_time = 1.0 / 60.0, + .elapsed_time = 0, + .is_running = true, + }); + + try self.step(&world, events); + try world.run_event(events.load); + + const ticks_initial = std.time.milliTimestamp(); + var ticks_previous = ticks_initial; + var accumulated_time = @as(f64, 0); + + while (app.is_running) { + const ticks_current = std.time.milliTimestamp(); + const milliseconds_per_second = 1000.0; + const delta_time = @as(f64, @floatFromInt(ticks_current - ticks_previous)) / milliseconds_per_second; + + app.elapsed_time = @as(f64, @floatFromInt(ticks_current - ticks_initial)) / milliseconds_per_second; + ticks_previous = ticks_current; + accumulated_time += delta_time; + + try world.run_event(events.pre_update); + + while (accumulated_time >= app.target_frame_time) : (accumulated_time -= app.target_frame_time) { + try world.run_event(events.update); + } + + try world.run_event(events.post_update); + try world.run_event(events.render); + try world.run_event(events.finish); + } + + try world.run_event(events.exit); + } + }; + + return Start.main; + } + + pub fn with_module(self: Setup, comptime Module: type) Setup { + if (!@hasDecl(Module, "setup")) { + @compileError("`Module` argument must have a setup fn declaration"); + } + + const WithState = struct { + fn step(world: *World, events: App.Events) !void { + try Module.setup(world, events); + try self.step(world, events); + } + }; + + return .{ + .step = WithState.step, + }; + } + + pub fn with_state(self: Setup, comptime state: anytype) Setup { + const WithState = struct { + fn step(world: *World, events: App.Events) !void { + try world.set_state(state); + try self.step(world, events); + } + }; + + return .{ + .step = WithState.step, + }; + } + + pub fn with_system(self: Setup, comptime event: Event, comptime info: *const SystemInfo, comptime order: SystemOrder) Setup { + const WithState = struct { + fn step(world: *World, events: App.Events) !void { + const app_event = switch (event) { + .load => events.load, + .pre_update => events.pre_update, + .update => events.update, + .post_update => events.post_update, + .render => events.render, + .finish => events.finish, + .exit => events.exit, + }; + + try world.on_event(app_event, info, order); + try self.step(world, events); + } + }; + + return .{ + .step = WithState.step, + }; + } + }; + + pub const setup = Setup{}; + pub fn quit(self: *App) void { self.is_running = false; } @@ -161,7 +324,7 @@ pub fn Exclusive(comptime values: []const type) type { .Optional => has_state, else => has_state orelse { - @panic(std.fmt.comptimePrint("attempt to use exclusive {s} that has not yet been set", .{ + @panic(std.fmt.comptimePrint("attempt to use exclusive {s} that has not been set yet", .{ @typeName(Value), })); }, @@ -190,17 +353,6 @@ pub const LaunchFlag = enum { const values = std.enums.values(LaunchFlag); }; -pub const Options = struct { - tick_rate: u64, - execution: Execution, - middleware: []const *const Setup, - - pub const Execution = union (enum) { - single_threaded, - thread_share: f32, - }; -}; - pub fn Params(comptime Value: type) type { if (!@hasDecl(Value, "Param")) { @compileError("System parameters must have a Params type declaration"); @@ -430,8 +582,6 @@ pub fn Send(comptime Message: type) type { }; } -pub const Setup = fn (*World, App.Events) anyerror!void; - pub const ShareInfo = struct { thread_restriction: ThreadRestriction, read_only: bool, @@ -497,7 +647,7 @@ pub fn Shared(comptime Value: type, comptime info: ShareInfo) type { .Optional => state, else => state orelse { - @panic(std.fmt.comptimePrint("attempt to use {s}{s} {s} that has not yet been set", .{ + @panic(std.fmt.comptimePrint("attempt to use {s}{s} {s} that has not been set yet", .{ thread_restriction_name, if (info.read_only) "read-only" else "read-write", @typeName(Value), @@ -688,108 +838,6 @@ fn parameter_type(comptime Value: type) *const SystemInfo.Parameter { }; } -pub fn start(comptime setup: Setup, comptime options: Options) fn () anyerror!void { - const Start = struct { - fn main() anyerror!void { - defer { - heap.trace_leaks(); - } - - parse_args: for (std.os.argv[1 ..]) |arg| { - const arg_span = std.mem.span(arg); - const arg_split_index = std.mem.indexOfScalar(u8, arg_span, '=') orelse arg_span.len; - const arg_name = arg_span[0 .. arg_split_index]; - - for (LaunchFlag.values) |value| { - const name = @tagName(value); - - if (!std.mem.eql(u8, arg_name, name)) { - continue; - } - - LaunchFlag.args[@intFromEnum(value)] = - if (arg_split_index == arg_span.len) - name - else - arg_span[arg_split_index ..]; - - continue: parse_args; - } - } - - var world = try switch (options.execution) { - .single_threaded => World.init(0), - - .thread_share => |thread_share| init: { - const cpu_count = @as(u32, @intCast(std.math.clamp(std.Thread.getCpuCount() catch |cpu_count_error| { - @panic(switch (cpu_count_error) { - error.PermissionDenied => "permission denied retrieving CPU count", - error.SystemResources => "system resources are preventing retrieval of the CPU count", - error.Unexpected => "unexpected failure retrieving CPU count", - }); - }, 0, std.math.maxInt(u32)))); - - break: init World.init(scalars.fractional(cpu_count, thread_share) orelse 0); - }, - }; - - defer world.deinit(); - - const events = App.Events{ - .load = try world.create_event("load"), - .pre_update = try world.create_event("pre-update"), - .update = try world.create_event("update"), - .post_update = try world.create_event("post-update"), - .render = try world.create_event("render"), - .finish = try world.create_event("finish"), - .exit = try world.create_event("exit"), - }; - - const app = try world.set_get_state(App{ - .events = &events, - .target_frame_time = 1.0 / @as(f64, @floatFromInt(options.tick_rate)), - .elapsed_time = 0, - .is_running = true, - }); - - inline for (options.middleware) |setup_middleware| { - try setup_middleware(&world, events); - } - - try setup(&world, events); - try world.run_event(events.load); - - const ticks_initial = std.time.milliTimestamp(); - var ticks_previous = ticks_initial; - var accumulated_time = @as(f64, 0); - - while (app.is_running) { - const ticks_current = std.time.milliTimestamp(); - const milliseconds_per_second = 1000.0; - const delta_time = @as(f64, @floatFromInt(ticks_current - ticks_previous)) / milliseconds_per_second; - - app.elapsed_time = @as(f64, @floatFromInt(ticks_current - ticks_initial)) / milliseconds_per_second; - ticks_previous = ticks_current; - accumulated_time += delta_time; - - try world.run_event(events.pre_update); - - while (accumulated_time >= app.target_frame_time) : (accumulated_time -= app.target_frame_time) { - try world.run_event(events.update); - } - - try world.run_event(events.post_update); - try world.run_event(events.render); - try world.run_event(events.finish); - } - - try world.run_event(events.exit); - } - }; - - return Start.main; -} - pub fn system_fn(comptime call: anytype) *const SystemInfo { const Call = @TypeOf(call);