core: add mach.Core module API

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2024-04-08 22:54:13 -07:00 committed by Stephen Gutekanst
parent 69b749879d
commit 013546b189
8 changed files with 309 additions and 41 deletions

View file

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

View file

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

View file

@ -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()) {}
}

View file

@ -0,0 +1,14 @@
@vertex fn vertex_main(
@builtin(vertex_index) VertexIndex : u32
) -> @builtin(position) vec4<f32> {
var pos = array<vec2<f32>, 3>(
vec2<f32>( 0.0, 0.5),
vec2<f32>(-0.5, -0.5),
vec2<f32>( 0.5, -0.5)
);
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}
@fragment fn frag_main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}

67
src/Core.zig Normal file
View file

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

View file

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

View file

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

View file

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