diff --git a/build.zig b/build.zig index 94025c98..6adc5d6a 100644 --- a/build.zig +++ b/build.zig @@ -627,6 +627,7 @@ fn buildExamples( deps: []const Dependency = &.{}, std_platform_only: bool = false, has_assets: bool = false, + use_module_api: bool = false, }{ .{ .name = "sysaudio", .deps = &.{} }, .{ @@ -634,6 +635,7 @@ fn buildExamples( .deps = &.{ .zigimg, .freetype, .assets }, .std_platform_only = true, }, + .{ .name = "core-custom-entrypoint", .deps = &.{}, .use_module_api = true }, .{ .name = "custom-renderer", .deps = &.{} }, .{ .name = "sprite", @@ -658,38 +660,61 @@ fn buildExamples( if (target.result.cpu.arch == .wasm32) break; - var deps = std.ArrayList(std.Build.Module.Import).init(b.allocator); - for (example.deps) |d| try deps.append(d.dependency(b, target, optimize)); - const app = try App.init( - b, - .{ + if (example.use_module_api) { + const exe = b.addExecutable(.{ .name = example.name, - .src = "examples/" ++ example.name ++ "/main.zig", + .root_source_file = .{ .path = "examples/" ++ example.name ++ "/main.zig" }, .target = target, .optimize = optimize, - .deps = deps.items, - .res_dirs = if (example.has_assets) &.{example.name ++ "/assets"} else null, - .watch_paths = &.{"examples/" ++ example.name}, - .mach_builder = b, - .mach_mod = mach_mod, - }, - ); + }); + exe.root_module.addImport("mach", mach_mod); + addPaths(&exe.root_module); + link(b, exe, &exe.root_module); + b.installArtifact(exe); - try app.link(); + const compile_step = b.step(example.name, "Compile " ++ example.name); + compile_step.dependOn(b.getInstallStep()); - for (example.deps) |dep| switch (dep) { - .model3d => app.compile.linkLibrary(b.dependency("mach_model3d", .{ - .target = target, - .optimize = optimize, - }).artifact("mach-model3d")), - else => {}, - }; + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); - const compile_step = b.step(example.name, "Compile " ++ example.name); - compile_step.dependOn(&app.install.step); + const run_step = b.step("run-" ++ example.name, "Run " ++ example.name); + run_step.dependOn(&run_cmd.step); + } else { + var deps = std.ArrayList(std.Build.Module.Import).init(b.allocator); + for (example.deps) |d| try deps.append(d.dependency(b, target, optimize)); + const app = try App.init( + b, + .{ + .name = example.name, + .src = "examples/" ++ example.name ++ "/main.zig", + .target = target, + .optimize = optimize, + .deps = deps.items, + .res_dirs = if (example.has_assets) &.{example.name ++ "/assets"} else null, + .watch_paths = &.{"examples/" ++ example.name}, + .mach_builder = b, + .mach_mod = mach_mod, + }, + ); - const run_step = b.step("run-" ++ example.name, "Run " ++ example.name); - run_step.dependOn(&app.run.step); + try app.link(); + + for (example.deps) |dep| switch (dep) { + .model3d => app.compile.linkLibrary(b.dependency("mach_model3d", .{ + .target = target, + .optimize = optimize, + }).artifact("mach-model3d")), + else => {}, + }; + + const compile_step = b.step(example.name, "Compile " ++ example.name); + compile_step.dependOn(&app.install.step); + + const run_step = b.step("run-" ++ example.name, "Run " ++ example.name); + run_step.dependOn(&app.run.step); + } } } diff --git a/examples/core-custom-entrypoint/Game.zig b/examples/core-custom-entrypoint/Game.zig new file mode 100644 index 00000000..dcedc655 --- /dev/null +++ b/examples/core-custom-entrypoint/Game.zig @@ -0,0 +1,105 @@ +const std = @import("std"); +const mach = @import("mach"); +const gpu = mach.gpu; + +pub const name = .game; +pub const Mod = mach.Mod(@This()); + +pub const global_events = .{ + .init = .{ .handler = init }, + .tick = .{ .handler = tick }, +}; + +title_timer: mach.Timer, +pipeline: *gpu.RenderPipeline, + +fn init(game: *Mod) !void { + const shader_module = mach.core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + defer shader_module.release(); + + // Fragment state + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = mach.core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .vertex = gpu.VertexState{ + .module = shader_module, + .entry_point = "vertex_main", + }, + }; + const pipeline = mach.core.device.createRenderPipeline(&pipeline_descriptor); + + game.init(.{ + .title_timer = try mach.Timer.start(), + .pipeline = pipeline, + }); + try updateWindowTitle(); +} + +pub fn deinit(game: *Mod) void { + game.state().pipeline.release(); +} + +// TODO(important): remove need for returning an error here +fn tick( + core: *mach.Core.Mod, + game: *Mod, +) !void { + // TODO(important): event polling should occur in mach.Core module and get fired as ECS event. + var iter = mach.core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .close => core.send(.exit, .{}), // Tell mach.Core to exit the app + else => {}, + } + } + + const queue = mach.core.queue; + const back_buffer_view = mach.core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + + const encoder = mach.core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(game.state().pipeline); + pass.draw(3, 1, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + mach.core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (game.state().title_timer.read() >= 1.0) { + game.state().title_timer.reset(); + try updateWindowTitle(); + } +} + +fn updateWindowTitle() !void { + try mach.core.printTitle("mach.Core - custom entrypoint [ {d}fps ] [ Input {d}hz ]", .{ + mach.core.frameRate(), + mach.core.inputRate(), + }); +} diff --git a/examples/core-custom-entrypoint/main.zig b/examples/core-custom-entrypoint/main.zig new file mode 100644 index 00000000..f795b772 --- /dev/null +++ b/examples/core-custom-entrypoint/main.zig @@ -0,0 +1,20 @@ +const std = @import("std"); + +const mach = @import("mach"); +const Game = @import("Game.zig"); + +// The global list of Mach modules registered for use in our application. +pub const modules = .{ + mach.Core, + Game, +}; + +pub const GPUInterface = mach.core.wgpu.dawn.Interface; + +pub fn main() !void { + // Initialize mach.Core + try mach.core.initModule(); + + // Main loop + while (try mach.core.tick()) {} +} diff --git a/examples/core-custom-entrypoint/shader.wgsl b/examples/core-custom-entrypoint/shader.wgsl new file mode 100644 index 00000000..429d87e0 --- /dev/null +++ b/examples/core-custom-entrypoint/shader.wgsl @@ -0,0 +1,14 @@ +@vertex fn vertex_main( + @builtin(vertex_index) VertexIndex : u32 +) -> @builtin(position) vec4 { + var pos = array, 3>( + vec2( 0.0, 0.5), + vec2(-0.5, -0.5), + vec2( 0.5, -0.5) + ); + return vec4(pos[VertexIndex], 0.0, 1.0); +} + +@fragment fn frag_main() -> @location(0) vec4 { + return vec4(1.0, 0.0, 0.0, 1.0); +} diff --git a/src/Core.zig b/src/Core.zig new file mode 100644 index 00000000..043f980e --- /dev/null +++ b/src/Core.zig @@ -0,0 +1,67 @@ +const std = @import("std"); +const mach = @import("main.zig"); + +pub const name = .mach_core; + +pub const Mod = mach.Mod(@This()); + +pub const global_events = .{ + .init = .{ .handler = fn () void }, + .deinit = .{ .handler = fn () void }, + .tick = .{ .handler = fn () void }, +}; + +pub const local_events = .{ + .init = .{ .handler = init }, + + // TODO(important): need some way to tie event execution to a specific thread once we have a + // multithreaded dispatch implementation + .main_thread_tick = .{ .handler = mainThreadTick }, + .main_thread_tick_done = .{ .handler = fn () void }, + .deinit = .{ .handler = deinit }, + .exit = .{ .handler = exit }, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +device: *mach.gpu.Device, +queue: *mach.gpu.Queue, +should_exit: bool = false, + +fn init(core: *Mod) !void { + // Initialize GPU implementation + if (comptime mach.core.options.use_wgpu) try mach.core.wgpu.Impl.init(mach.core.allocator, .{}); + if (comptime mach.core.options.use_sysgpu) try mach.core.sysgpu.Impl.init(mach.core.allocator, .{}); + + mach.core.allocator = gpa.allocator(); // TODO: banish this global allocator + try mach.core.init(.{}); + + core.init(.{ + .device = mach.core.device, + .queue = mach.core.device.getQueue(), + }); + + core.sendGlobal(.init, .{}); +} + +fn deinit(core: *Mod) void { + core.state().queue.release(); + // TODO: this triggers a device loss error, which we should handle correctly + // core.state().device.release(); + mach.core.deinit(); + _ = gpa.deinit(); +} + +fn mainThreadTick(core: *Mod) !void { + _ = try mach.core.update(null); + + // Send .tick to anyone interested + core.sendGlobal(.tick, .{}); + + // Signal that mainThreadTick is done + core.send(.main_thread_tick_done, .{}); +} + +fn exit(core: *Mod) void { + core.state().should_exit = true; +} diff --git a/src/core/main.zig b/src/core/main.zig index 043f1a6b..4d356e38 100644 --- a/src/core/main.zig +++ b/src/core/main.zig @@ -7,6 +7,30 @@ pub const Timer = @import("Timer.zig"); const Frequency = @import("Frequency.zig"); const platform = @import("platform.zig"); +const mach = @import("../main.zig"); +pub var mods: mach.Modules = undefined; + +pub fn initModule() !void { + // Initialize the global set of Mach modules used in the program. + try mods.init(std.heap.c_allocator); + mods.mod.mach_core.send(.init, .{}); +} + +/// Tick runs a single step of the main loop on the main OS thread. +/// +/// Returns true if tick() should be called again, false if the application should exit. +pub fn tick() !bool { + mods.mod.mach_core.send(.main_thread_tick, .{}); + + // Dispatch events until this .mach_core.main_thread_tick_done is sent + try mods.dispatch(.{ .until = .{ + .module_name = mods.moduleNameToID(.mach_core), + .local_event = mods.localEventToID(.mach_core, .main_thread_tick_done), + } }); + + return !mods.mod.mach_core.state().should_exit; +} + /// Returns the error set that the function F returns. fn ErrorSet(comptime F: type) type { return @typeInfo(@typeInfo(F).Fn.return_type.?).ErrorUnion.error_set; diff --git a/src/core/platform/glfw/Core.zig b/src/core/platform/glfw/Core.zig index 8dbe9032..d45a56a8 100644 --- a/src/core/platform/glfw/Core.zig +++ b/src/core/platform/glfw/Core.zig @@ -581,13 +581,16 @@ pub fn appUpdateThreadTick(self: *Core, app: anytype) bool { }); } - if (app.update() catch unreachable) { - self.done.set(); + const use_app = @typeInfo(@TypeOf(app)) == .Pointer; + if (use_app) { + if (app.update() catch unreachable) { + self.done.set(); - // Wake the main thread from any event handling, so there is not e.g. a one second delay - // in exiting the application. - glfw.postEmptyEvent(); - return false; + // Wake the main thread from any event handling, so there is not e.g. a one second delay + // in exiting the application. + glfw.postEmptyEvent(); + return false; + } } self.gpu_device.tick(); self.gpu_device.machWaitForCommandsToBeScheduled(); @@ -605,10 +608,13 @@ pub fn appUpdateThread(self: *Core, app: anytype) void { // Called on the main thread pub fn update(self: *Core, app: anytype) !bool { if (self.done.isSet()) return true; - if (!self.app_update_thread_started) { - self.app_update_thread_started = true; - const thread = try std.Thread.spawn(.{}, appUpdateThread, .{ self, app }); - thread.detach(); + const use_app = @typeInfo(@TypeOf(app)) == .Pointer; + if (use_app) { + if (!self.app_update_thread_started) { + self.app_update_thread_started = true; + const thread = try std.Thread.spawn(.{}, appUpdateThread, .{ self, app }); + thread.detach(); + } } if (self.state_update.isSet()) { @@ -748,14 +754,18 @@ pub fn update(self: *Core, app: anytype) !bool { } } - const frequency_delay = @as(f32, @floatFromInt(self.input.delay_ns)) / @as(f32, @floatFromInt(std.time.ns_per_s)); - glfw.waitEventsTimeout(frequency_delay); + if (use_app) { + const frequency_delay = @as(f32, @floatFromInt(self.input.delay_ns)) / @as(f32, @floatFromInt(std.time.ns_per_s)); + glfw.waitEventsTimeout(frequency_delay); - if (@hasDecl(std.meta.Child(@TypeOf(app)), "updateMainThread")) { - if (app.updateMainThread() catch unreachable) { - self.done.set(); - return true; + if (@hasDecl(std.meta.Child(@TypeOf(app)), "updateMainThread")) { + if (app.updateMainThread() catch unreachable) { + self.done.set(); + return true; + } } + } else { + glfw.pollEvents(); } glfw.getErrorCode() catch |err| switch (err) { @@ -764,6 +774,8 @@ pub fn update(self: *Core, app: anytype) !bool { else => unreachable, }; self.input.tick(); + + if (!use_app) return !self.appUpdateThreadTick(app); return false; } diff --git a/src/main.zig b/src/main.zig index 468c9a1d..6284629e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,6 +6,7 @@ pub const core = if (build_options.want_core) @import("core/main.zig") else stru pub const Timer = if (build_options.want_core) core.Timer else struct {}; pub const gpu = if (build_options.want_core) core.gpu else struct {}; pub const sysjs = if (build_options.want_core) @import("mach-sysjs") else struct {}; +pub const Core = if (build_options.want_core) @import("Core.zig") else struct {}; // Mach standard library // gamemode requires libc on linux