Core: add .present_frame event replacing swapbuffers call

Note that on e.g. web platform, swapbuffers is not an explicit call.
We also need a signal that the frame has been submitted and finished,
and this is it.

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2024-04-21 23:53:09 -07:00 committed by Stephen Gutekanst
parent 431e0dbbe1
commit 656b0202f2
9 changed files with 79 additions and 38 deletions

View file

@ -104,7 +104,7 @@ fn tick(core: *mach.Core.Mod, game: *Mod) !void {
core.state().queue.submit(&[_]*gpu.CommandBuffer{command}); core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
// Present the frame // Present the frame
mach.core.swap_chain.present(); core.send(.present_frame, .{});
// update the window title every second // update the window title every second
if (game.state().title_timer.read() >= 1.0) { if (game.state().title_timer.read() >= 1.0) {

View file

@ -47,10 +47,6 @@ fn init(
renderer: *Renderer.Mod, renderer: *Renderer.Mod,
game: *Mod, game: *Mod,
) !void { ) !void {
// The Mach .core is where we set window options, etc.
// TODO(important): replace this API with something better
mach.core.setTitle("Hello, ECS!");
// Create our player entity. // Create our player entity.
const player = try core.newEntity(); const player = try core.newEntity();

View file

@ -123,22 +123,29 @@ fn tick(
renderer: *Mod, renderer: *Mod,
) !void { ) !void {
const device = core.state().device; const device = core.state().device;
_ = device; // autofix
// Begin our render pass // Grab the back buffer of the swapchain
const back_buffer_view = mach.core.swap_chain.getCurrentTextureView().?; const back_buffer_view = mach.core.swap_chain.getCurrentTextureView().?;
const color_attachment = gpu.RenderPassColorAttachment{ defer back_buffer_view.release();
// Create a command encoder
const label = @tagName(name) ++ ".tick";
const encoder = core.state().device.createCommandEncoder(&.{ .label = label });
defer encoder.release();
// Begin render pass
const sky_blue_background = gpu.Color{ .r = 0.776, .g = 0.988, .b = 1, .a = 1 };
const color_attachments = [_]gpu.RenderPassColorAttachment{.{
.view = back_buffer_view, .view = back_buffer_view,
.clear_value = std.mem.zeroes(gpu.Color), .clear_value = sky_blue_background,
.load_op = .clear, .load_op = .clear,
.store_op = .store, .store_op = .store,
}; }};
const render_pass = encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
const label = @tagName(name) ++ ".tick";
const encoder = device.createCommandEncoder(&.{ .label = label });
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.label = label, .label = label,
.color_attachments = &.{color_attachment}, .color_attachments = &color_attachments,
}); }));
// Update uniform buffer // Update uniform buffer
var archetypes_iter = core.entities.query(.{ .all = &.{ var archetypes_iter = core.entities.query(.{ .all = &.{
@ -161,20 +168,21 @@ fn tick(
} }
} }
const pass = encoder.beginRenderPass(&render_pass_info); // Draw
for (renderer.state().bind_groups[0..num_entities]) |bind_group| { for (renderer.state().bind_groups[0..num_entities]) |bind_group| {
pass.setPipeline(renderer.state().pipeline); render_pass.setPipeline(renderer.state().pipeline);
pass.setBindGroup(0, bind_group, &.{0}); render_pass.setBindGroup(0, bind_group, &.{0});
pass.draw(3, 1, 0, 0); render_pass.draw(3, 1, 0, 0);
} }
pass.end();
pass.release();
// Finish render pass
render_pass.end();
// Submit our commands to the queue
var command = encoder.finish(&.{ .label = label }); var command = encoder.finish(&.{ .label = label });
encoder.release(); defer command.release();
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
renderer.state().queue.submit(&[_]*gpu.CommandBuffer{command}); // Present the frame
command.release(); core.send(.present_frame, .{});
mach.core.swap_chain.present();
back_buffer_view.release();
} }

View file

@ -238,7 +238,7 @@ fn endFrame(game: *Mod, core: *mach.Core.Mod) !void {
core.state().queue.submit(&[_]*gpu.CommandBuffer{command}); core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
// Present the frame // Present the frame
mach.core.swap_chain.present(); core.send(.present_frame, .{});
// Every second, update the window title with the FPS // Every second, update the window title with the FPS
if (game.state().fps_timer.read() >= 1.0) { if (game.state().fps_timer.read() >= 1.0) {

View file

@ -12,6 +12,7 @@ const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const mach = @import("mach"); const mach = @import("mach");
const gpu = mach.gpu;
const math = mach.math; const math = mach.math;
const sysaudio = mach.sysaudio; const sysaudio = mach.sysaudio;
@ -109,10 +110,40 @@ fn tick(
} }
} }
// Grab the back buffer of the swapchain
const back_buffer_view = mach.core.swap_chain.getCurrentTextureView().?; const back_buffer_view = mach.core.swap_chain.getCurrentTextureView().?;
defer back_buffer_view.release();
mach.core.swap_chain.present(); // Create a command encoder
back_buffer_view.release(); const label = @tagName(name) ++ ".tick";
const encoder = core.state().device.createCommandEncoder(&.{ .label = label });
defer encoder.release();
// Begin render pass
const sky_blue_background = gpu.Color{ .r = 0.776, .g = 0.988, .b = 1, .a = 1 };
const color_attachments = [_]gpu.RenderPassColorAttachment{.{
.view = back_buffer_view,
.clear_value = sky_blue_background,
.load_op = .clear,
.store_op = .store,
}};
const render_pass = encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
.label = label,
.color_attachments = &color_attachments,
}));
// Draw nothing
// Finish render pass
render_pass.end();
// Submit our commands to the queue
var command = encoder.finish(&.{ .label = label });
defer command.release();
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
// Present the frame
core.send(.present_frame, .{});
} }
fn fillTone(audio: *mach.Audio.Mod, frequency: f32) ![]const f32 { fn fillTone(audio: *mach.Audio.Mod, frequency: f32) ![]const f32 {

View file

@ -222,7 +222,7 @@ fn endFrame(game: *Mod, core: *mach.Core.Mod) !void {
core.state().queue.submit(&[_]*gpu.CommandBuffer{command}); core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
// Present the frame // Present the frame
mach.core.swap_chain.present(); core.send(.present_frame, .{});
// Every second, update the window title with the FPS // Every second, update the window title with the FPS
if (game.state().fps_timer.read() >= 1.0) { if (game.state().fps_timer.read() >= 1.0) {

View file

@ -63,9 +63,6 @@ fn init(
text_style: *gfx.TextStyle.Mod, text_style: *gfx.TextStyle.Mod,
game: *Mod, game: *Mod,
) !void { ) !void {
// The Mach .core is where we set window options, etc.
mach.core.setTitle("gfx.Text example");
// TODO: a better way to initialize entities with default values // TODO: a better way to initialize entities with default values
// TODO(text): most of these style options are not respected yet. // TODO(text): most of these style options are not respected yet.
const style1 = try core.newEntity(); const style1 = try core.newEntity();
@ -268,7 +265,7 @@ fn endFrame(game: *Mod, text: *gfx.Text.Mod, core: *mach.Core.Mod) !void {
core.state().queue.submit(&[_]*gpu.CommandBuffer{command}); core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
// Present the frame // Present the frame
mach.core.swap_chain.present(); core.send(.present_frame, .{});
// Every second, update the window title with the FPS // Every second, update the window title with the FPS
if (game.state().fps_timer.read() >= 1.0) { if (game.state().fps_timer.read() >= 1.0) {

View file

@ -19,6 +19,10 @@ pub const local_events = .{
\\ Send this when window entities have been updated and you want the new values respected. \\ 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.
},
.init = .{ .handler = init }, .init = .{ .handler = init },
.init_done = .{ .handler = fn () void }, .init_done = .{ .handler = fn () void },
@ -49,7 +53,7 @@ pub const components = .{
/// try mach.Core.printTitle(core_mod, core_mod.state().main_window, "Hello, {s}!", .{"Mach"}); /// try mach.Core.printTitle(core_mod, core_mod.state().main_window, "Hello, {s}!", .{"Mach"});
/// ``` /// ```
pub fn printTitle( pub fn printTitle(
core: *mach.Core.Mod, core: *Mod,
window_id: mach.EntityID, window_id: mach.EntityID,
comptime fmt: []const u8, comptime fmt: []const u8,
args: anytype, args: anytype,
@ -106,6 +110,13 @@ fn update(core: *Mod) !void {
if (num_windows > 1) @panic("mach: Core currently only supports a single window"); if (num_windows > 1) @panic("mach: Core currently only supports a single window");
} }
fn presentFrame(core: *Mod) !void {
mach.core.swap_chain.present();
// Signal that mainThreadTick is done
core.send(.main_thread_tick_done, .{});
}
fn deinit(core: *Mod) void { fn deinit(core: *Mod) void {
core.state().queue.release(); core.state().queue.release();
// TODO: this triggers a device loss error, which we should handle correctly // TODO: this triggers a device loss error, which we should handle correctly
@ -129,9 +140,6 @@ fn mainThreadTick(core: *Mod) !void {
// Send .tick to anyone interested // Send .tick to anyone interested
core.sendGlobal(.tick, .{}); core.sendGlobal(.tick, .{});
// Signal that mainThreadTick is done
core.send(.main_thread_tick_done, .{});
} }
fn exit(core: *Mod) void { fn exit(core: *Mod) void {

View file

@ -342,6 +342,7 @@ pub fn Modules(comptime modules: anytype) type {
options: DispatchOptions, options: DispatchOptions,
injectable: anytype, injectable: anytype,
) !void { ) !void {
@setEvalBranchQuota(10000);
// TODO: optimize to reduce send contention // TODO: optimize to reduce send contention
// TODO: parallel / multi-threaded dispatch // TODO: parallel / multi-threaded dispatch
// TODO: PGO // TODO: PGO