mach/src/Core.zig
Stephen Gutekanst 205a1f33db Core: support configuring window before it opens
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
2024-05-14 00:12:26 +02:00

239 lines
8 KiB
Zig

const std = @import("std");
const mach = @import("main.zig");
const gpu = mach.gpu;
// TODO(important): mach.core has a lot of standard Zig APIs, and some global variables, which are
// part of its old API design. We should elevate them into this module instead.
pub const name = .mach_core;
pub const Mod = mach.Mod(@This());
pub const systems = .{
.init = .{ .handler = init, .description =
\\ Send this once you've configured any options you want on e.g. the core.state().main_window
},
.start = .{ .handler = start, .description =
\\ Send this once your .app.init has been handled, you've sent .mach_core.init, and you are ready
\\ for .app.tick events.
},
.update = .{ .handler = update, .description =
\\ Send this when window entities have been updated and you want the new values respected.
},
.present_frame = .{ .handler = presentFrame, .description =
\\ Send this when rendering has finished and the swapchain should be presented.
},
.exit = .{ .handler = exit, .description =
\\ Send this when you would like to exit the application.
\\
\\ When the next .present_frame occurs, then .app.deinit will be sent giving your app a chance
\\ to deinitialize itself and .app.tick will no longer be sent. Once your app is done with
\\ deinitialization, you should send the final .mach_core.deinit event which will cause the
\\ application to finish.
},
.deinit = .{ .handler = deinit, .description =
\\ Send this once your app is fully deinitialized and ready to exit for good.
},
// TODO(important): need some way to tie event execution to a specific thread once we have a
// multithreaded dispatch implementation
.init_module = .{ .handler = initModule },
.main_thread_tick = .{ .handler = mainThreadTick },
.main_thread_tick_done = .{ .handler = fn () void },
};
pub const components = .{
.title = .{ .type = [:0]u8, .description =
\\ Window title slice. Can be set with a format string and arguments via:
\\
\\ ```
\\ try mach.Core.printTitle(core_mod, core_mod.state().main_window, "Hello, {s}!", .{"Mach"});
\\ ```
\\
\\ If setting this component yourself, ensure the buffer is allocated using core.state().allocator
\\ as it will be freed for you as part of the .deinit event.
},
.framebuffer_format = .{ .type = gpu.Texture.Format, .description =
\\ The texture format of the framebuffer
},
.framebuffer_width = .{ .type = u32, .description =
\\ The width of the framebuffer in texels
},
.framebuffer_height = .{ .type = u32, .description =
\\ The height of the framebuffer in texels
},
.width = .{ .type = u32, .description =
\\ The width of the window in virtual pixels
},
.height = .{ .type = u32, .description =
\\ The height of the window in virtual pixels
},
.fullscreen = .{ .type = bool, .description =
\\ Whether the window should be fullscreen (only respected at .start time)
},
};
/// Prints into the window title buffer using a format string and arguments. e.g.
///
/// ```
/// try mach.Core.printTitle(core_mod, core_mod.state().main_window, "Hello, {s}!", .{"Mach"});
/// ```
pub fn printTitle(
core: *Mod,
window_id: mach.EntityID,
comptime fmt: []const u8,
args: anytype,
) !void {
// Free any previous window title slice
if (core.get(window_id, .title)) |slice| core.state().allocator.free(slice);
// Allocate and assign a new window title slice.
const slice = try std.fmt.allocPrintZ(core.state().allocator, fmt, args);
try core.set(window_id, .title, slice);
}
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
allocator: std.mem.Allocator,
device: *mach.gpu.Device,
queue: *mach.gpu.Queue,
main_window: mach.EntityID,
run_state: enum {
initialized,
running,
exiting,
deinitializing,
exited,
} = .initialized,
fn start(core: *Mod) !void {
core.state().run_state = .running;
}
fn init(core: *Mod) !void {
try mach.core.init(.{
.display_mode = if (core.get(core.state().main_window, .fullscreen).?) .fullscreen else .windowed,
.size = .{
.width = core.get(core.state().main_window, .width).?,
.height = core.get(core.state().main_window, .height).?,
},
// TODO: expose this option
.power_preference = .high_performance,
});
// TODO(important): update this information upon framebuffer resize events
try core.set(core.state().main_window, .framebuffer_format, mach.core.descriptor.format);
try core.set(core.state().main_window, .framebuffer_width, mach.core.descriptor.width);
try core.set(core.state().main_window, .framebuffer_height, mach.core.descriptor.height);
try core.set(core.state().main_window, .width, mach.core.size().width);
try core.set(core.state().main_window, .height, mach.core.size().height);
core.state().allocator = mach.core.allocator;
core.state().device = mach.core.device;
core.state().queue = mach.core.device.getQueue();
}
fn initModule(entities: *mach.Entities.Mod, core: *Mod) !void {
mach.core.allocator = gpa.allocator(); // TODO: banish this global allocator
// Initialize GPU implementation
if (comptime !mach.use_sysgpu) try mach.wgpu.Impl.init(mach.core.allocator, .{});
if (comptime mach.use_sysgpu) try mach.sysgpu.Impl.init(mach.core.allocator, .{});
const main_window = try entities.new();
try core.set(main_window, .fullscreen, false);
try core.set(main_window, .width, 1920 / 2);
try core.set(main_window, .height, 1080 / 2);
core.init(.{
.allocator = undefined,
.device = undefined,
.queue = undefined,
.main_window = main_window,
});
mach.core.mods.schedule(.app, .init);
}
fn update(entities: *mach.Entities.Mod) !void {
var num_windows: usize = 0;
var q = try entities.query(.{
.ids = mach.Entities.Mod.read(.id),
.titles = Mod.read(.title),
});
while (q.next()) |v| {
for (v.ids, v.titles) |window_id, title| {
_ = window_id;
num_windows += 1;
try mach.core.printTitle("{s}", .{title});
}
}
if (num_windows > 1) @panic("mach: Core currently only supports a single window");
}
fn presentFrame(core: *Mod) !void {
switch (core.state().run_state) {
.running => {
mach.core.swap_chain.present();
// TODO(important): update this information in response to resize events rather than
// after frame submission
const main_window = core.state().main_window;
try core.set(main_window, .framebuffer_format, mach.core.descriptor.format);
try core.set(main_window, .framebuffer_width, mach.core.descriptor.width);
try core.set(main_window, .framebuffer_height, mach.core.descriptor.height);
try core.set(main_window, .width, mach.core.size().width);
try core.set(main_window, .height, mach.core.size().height);
// Signal that mainThreadTick is done
core.schedule(.main_thread_tick_done);
},
.exiting => {
// Exit opportunity is here, deinitialize now
core.state().run_state = .deinitializing;
mach.core.mods.schedule(.app, .deinit);
},
else => return,
}
}
fn deinit(entities: *mach.Entities.Mod, core: *Mod) !void {
core.state().queue.release();
mach.core.deinit();
var q = try entities.query(.{
.titles = Mod.read(.title),
});
while (q.next()) |v| {
for (v.titles) |title| {
core.state().allocator.free(title);
}
}
_ = gpa.deinit();
core.state().run_state = .exited;
// Signal that mainThreadTick is done
core.schedule(.main_thread_tick_done);
}
fn mainThreadTick(core: *Mod) !void {
if (core.state().run_state != .running) return;
_ = try mach.core.update(null);
mach.core.mods.schedule(.app, .tick);
}
fn exit(core: *Mod) void {
core.state().run_state = .exiting;
}