mach: implement App struct in terms of unified entry point
This commit is contained in:
parent
3e87b383d2
commit
2aedc4ca01
6 changed files with 313 additions and 333 deletions
|
|
@ -103,7 +103,7 @@ const MachApp = struct {
|
||||||
b: *std.build.Builder,
|
b: *std.build.Builder,
|
||||||
|
|
||||||
pub fn createApplication(b: *std.build.Builder, name: []const u8, src: []const u8, deps: []const Pkg) MachApp {
|
pub fn createApplication(b: *std.build.Builder, name: []const u8, src: []const u8, deps: []const Pkg) MachApp {
|
||||||
const exe = b.addExecutable(name, "src/entry/native.zig");
|
const exe = b.addExecutable(name, "src/entry_native.zig");
|
||||||
exe.addPackage(.{
|
exe.addPackage(.{
|
||||||
.name = "app",
|
.name = "app",
|
||||||
.path = .{ .path = src },
|
.path = .{ .path = src },
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,19 @@ const std = @import("std");
|
||||||
const mach = @import("mach");
|
const mach = @import("mach");
|
||||||
const gpu = @import("gpu");
|
const gpu = @import("gpu");
|
||||||
|
|
||||||
const App = mach.App(*FrameParams, .{});
|
pub const options: mach.Engine.Options = .{};
|
||||||
|
|
||||||
pub fn init() !void {
|
pipeline: gpu.RenderPipeline,
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
queue: gpu.Queue,
|
||||||
var allocator = gpa.allocator();
|
|
||||||
|
|
||||||
const ctx = try allocator.create(FrameParams);
|
const Self = @This();
|
||||||
var app = try App.init(allocator, ctx, .{});
|
pub fn init(self: *Self, engine: *mach.Engine) !void {
|
||||||
|
const vs_module = engine.gpu_driver.device.createShaderModule(&.{
|
||||||
const vs_module = app.device.createShaderModule(&.{
|
|
||||||
.label = "my vertex shader",
|
.label = "my vertex shader",
|
||||||
.code = .{ .wgsl = @embedFile("vert.wgsl") },
|
.code = .{ .wgsl = @embedFile("vert.wgsl") },
|
||||||
});
|
});
|
||||||
|
|
||||||
const fs_module = app.device.createShaderModule(&.{
|
const fs_module = engine.gpu_driver.device.createShaderModule(&.{
|
||||||
.label = "my fragment shader",
|
.label = "my fragment shader",
|
||||||
.code = .{ .wgsl = @embedFile("frag.wgsl") },
|
.code = .{ .wgsl = @embedFile("frag.wgsl") },
|
||||||
});
|
});
|
||||||
|
|
@ -35,7 +33,7 @@ pub fn init() !void {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const color_target = gpu.ColorTargetState{
|
const color_target = gpu.ColorTargetState{
|
||||||
.format = app.swap_chain_format,
|
.format = engine.gpu_driver.swap_chain_format,
|
||||||
.blend = &blend,
|
.blend = &blend,
|
||||||
.write_mask = gpu.ColorWriteMask.all,
|
.write_mask = gpu.ColorWriteMask.all,
|
||||||
};
|
};
|
||||||
|
|
@ -67,30 +65,17 @@ pub fn init() !void {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.* = FrameParams{
|
self.pipeline = engine.gpu_driver.device.createRenderPipeline(&pipeline_descriptor);
|
||||||
.pipeline = app.device.createRenderPipeline(&pipeline_descriptor),
|
self.queue = engine.gpu_driver.device.getQueue();
|
||||||
.queue = app.device.getQueue(),
|
|
||||||
};
|
|
||||||
|
|
||||||
vs_module.release();
|
vs_module.release();
|
||||||
fs_module.release();
|
fs_module.release();
|
||||||
|
|
||||||
try app.run(.{ .frame = frame });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update() !bool {
|
pub fn deinit(_: *Self, _: *mach.Engine) void {}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit() void {}
|
pub fn update(self: *Self, engine: *mach.Engine) !bool {
|
||||||
|
const back_buffer_view = engine.gpu_driver.swap_chain.?.getCurrentTextureView();
|
||||||
const FrameParams = struct {
|
|
||||||
pipeline: gpu.RenderPipeline,
|
|
||||||
queue: gpu.Queue,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn frame(app: *App, params: *FrameParams) !void {
|
|
||||||
const back_buffer_view = app.swap_chain.?.getCurrentTextureView();
|
|
||||||
const color_attachment = gpu.RenderPassColorAttachment{
|
const color_attachment = gpu.RenderPassColorAttachment{
|
||||||
.view = back_buffer_view,
|
.view = back_buffer_view,
|
||||||
.resolve_target = null,
|
.resolve_target = null,
|
||||||
|
|
@ -99,13 +84,13 @@ fn frame(app: *App, params: *FrameParams) !void {
|
||||||
.store_op = .store,
|
.store_op = .store,
|
||||||
};
|
};
|
||||||
|
|
||||||
const encoder = app.device.createCommandEncoder(null);
|
const encoder = engine.gpu_driver.device.createCommandEncoder(null);
|
||||||
const render_pass_info = gpu.RenderPassEncoder.Descriptor{
|
const render_pass_info = gpu.RenderPassEncoder.Descriptor{
|
||||||
.color_attachments = &.{color_attachment},
|
.color_attachments = &.{color_attachment},
|
||||||
.depth_stencil_attachment = null,
|
.depth_stencil_attachment = null,
|
||||||
};
|
};
|
||||||
const pass = encoder.beginRenderPass(&render_pass_info);
|
const pass = encoder.beginRenderPass(&render_pass_info);
|
||||||
pass.setPipeline(params.pipeline);
|
pass.setPipeline(self.pipeline);
|
||||||
pass.draw(3, 1, 0, 0);
|
pass.draw(3, 1, 0, 0);
|
||||||
pass.end();
|
pass.end();
|
||||||
pass.release();
|
pass.release();
|
||||||
|
|
@ -113,8 +98,10 @@ fn frame(app: *App, params: *FrameParams) !void {
|
||||||
var command = encoder.finish(null);
|
var command = encoder.finish(null);
|
||||||
encoder.release();
|
encoder.release();
|
||||||
|
|
||||||
params.queue.submit(&.{command});
|
self.queue.submit(&.{command});
|
||||||
command.release();
|
command.release();
|
||||||
app.swap_chain.?.present();
|
engine.gpu_driver.swap_chain.?.present();
|
||||||
back_buffer_view.release();
|
back_buffer_view.release();
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
81
src/Engine.zig
Normal file
81
src/Engine.zig
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const glfw = @import("glfw");
|
||||||
|
const gpu = @import("gpu");
|
||||||
|
|
||||||
|
pub const VSyncMode = enum {
|
||||||
|
/// Potential screen tearing.
|
||||||
|
/// No synchronization with monitor, render frames as fast as possible.
|
||||||
|
none,
|
||||||
|
|
||||||
|
/// No tearing, synchronizes rendering with monitor refresh rate, rendering frames when ready.
|
||||||
|
///
|
||||||
|
/// Tries to stay one frame ahead of the monitor, so when it's ready for the next frame it is
|
||||||
|
/// already prepared.
|
||||||
|
double,
|
||||||
|
|
||||||
|
/// No tearing, synchronizes rendering with monitor refresh rate, rendering frames when ready.
|
||||||
|
///
|
||||||
|
/// Tries to stay two frames ahead of the monitor, so when it's ready for the next frame it is
|
||||||
|
/// already prepared.
|
||||||
|
triple,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Application options that can be configured at init time.
|
||||||
|
pub const Options = struct {
|
||||||
|
/// The title of the window.
|
||||||
|
title: [*:0]const u8 = "Mach engine",
|
||||||
|
|
||||||
|
/// The width of the window.
|
||||||
|
width: u32 = 640,
|
||||||
|
|
||||||
|
/// The height of the window.
|
||||||
|
height: u32 = 480,
|
||||||
|
|
||||||
|
/// Monitor synchronization modes.
|
||||||
|
vsync: VSyncMode = .double,
|
||||||
|
|
||||||
|
/// GPU features required by the application.
|
||||||
|
required_features: ?[]gpu.Feature = null,
|
||||||
|
|
||||||
|
/// GPU limits required by the application.
|
||||||
|
required_limits: ?gpu.Limits = null,
|
||||||
|
|
||||||
|
/// Whether the application has a preference for low power or high performance GPU.
|
||||||
|
power_preference: gpu.PowerPreference = .none,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Window, events, inputs etc.
|
||||||
|
core: Core,
|
||||||
|
|
||||||
|
/// WebGPU driver - stores device, swap chains, targets and more
|
||||||
|
gpu_driver: GpuDriver,
|
||||||
|
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
/// The amount of time (in seconds) that has passed since the last frame was rendered.
|
||||||
|
///
|
||||||
|
/// For example, if you are animating a cube which should rotate 360 degrees every second,
|
||||||
|
/// instead of writing (360.0 / 60.0) and assuming the frame rate is 60hz, write
|
||||||
|
/// (360.0 * engine.delta_time)
|
||||||
|
delta_time: f64 = 0,
|
||||||
|
delta_time_ns: u64 = 0,
|
||||||
|
timer: std.time.Timer,
|
||||||
|
|
||||||
|
pub const Core = struct {
|
||||||
|
internal: union {
|
||||||
|
window: glfw.Window,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const GpuDriver = struct {
|
||||||
|
device: gpu.Device,
|
||||||
|
backend_type: gpu.Adapter.BackendType,
|
||||||
|
swap_chain: ?gpu.SwapChain,
|
||||||
|
swap_chain_format: gpu.Texture.Format,
|
||||||
|
|
||||||
|
native_instance: gpu.NativeInstance,
|
||||||
|
surface: ?gpu.Surface,
|
||||||
|
current_desc: gpu.SwapChain.Descriptor,
|
||||||
|
target_desc: gpu.SwapChain.Descriptor,
|
||||||
|
};
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
const app = @import("app");
|
|
||||||
|
|
||||||
pub fn main() !void {
|
|
||||||
try app.init();
|
|
||||||
defer app.deinit();
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const success = try app.update();
|
|
||||||
if (!success)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
211
src/entry_native.zig
Normal file
211
src/entry_native.zig
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const App = @import("app");
|
||||||
|
|
||||||
|
const glfw = @import("glfw");
|
||||||
|
const gpu = @import("gpu");
|
||||||
|
const util = @import("util.zig");
|
||||||
|
const c = @import("c.zig").c;
|
||||||
|
|
||||||
|
const Engine = @import("Engine.zig");
|
||||||
|
const Options = Engine.Options;
|
||||||
|
|
||||||
|
/// Default GLFW error handling callback
|
||||||
|
fn glfwErrorCallback(error_code: glfw.Error, description: [:0]const u8) void {
|
||||||
|
std.debug.print("glfw: {}: {s}\n", .{ error_code, description });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(allocator: Allocator, options: Options) !Engine {
|
||||||
|
const backend_type = try util.detectBackendType(allocator);
|
||||||
|
|
||||||
|
glfw.setErrorCallback(glfwErrorCallback);
|
||||||
|
try glfw.init(.{});
|
||||||
|
|
||||||
|
// Create the test window and discover adapters using it (esp. for OpenGL)
|
||||||
|
var hints = util.glfwWindowHintsForBackend(backend_type);
|
||||||
|
hints.cocoa_retina_framebuffer = true;
|
||||||
|
const window = try glfw.Window.create(
|
||||||
|
options.width,
|
||||||
|
options.height,
|
||||||
|
options.title,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
hints,
|
||||||
|
);
|
||||||
|
|
||||||
|
const backend_procs = c.machDawnNativeGetProcs();
|
||||||
|
c.dawnProcSetProcs(backend_procs);
|
||||||
|
|
||||||
|
const instance = c.machDawnNativeInstance_init();
|
||||||
|
var native_instance = gpu.NativeInstance.wrap(c.machDawnNativeInstance_get(instance).?);
|
||||||
|
|
||||||
|
// Discover e.g. OpenGL adapters.
|
||||||
|
try util.discoverAdapters(instance, window, backend_type);
|
||||||
|
|
||||||
|
// Request an adapter.
|
||||||
|
//
|
||||||
|
// TODO: It would be nice if we could use gpu_interface.waitForAdapter here, however the webgpu.h
|
||||||
|
// API does not yet have a way to specify what type of backend you want (vulkan, opengl, etc.)
|
||||||
|
// In theory, I suppose we shouldn't need to and Dawn should just pick the best adapter - but in
|
||||||
|
// practice if Vulkan is not supported today waitForAdapter/requestAdapter merely generates an error.
|
||||||
|
//
|
||||||
|
// const gpu_interface = native_instance.interface();
|
||||||
|
// const backend_adapter = switch (gpu_interface.waitForAdapter(&.{
|
||||||
|
// .power_preference = .high_performance,
|
||||||
|
// })) {
|
||||||
|
// .adapter => |v| v,
|
||||||
|
// .err => |err| {
|
||||||
|
// std.debug.print("mach: failed to get adapter: error={} {s}\n", .{ err.code, err.message });
|
||||||
|
// std.process.exit(1);
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
const adapters = c.machDawnNativeInstance_getAdapters(instance);
|
||||||
|
var dawn_adapter: ?c.MachDawnNativeAdapter = null;
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < c.machDawnNativeAdapters_length(adapters)) : (i += 1) {
|
||||||
|
const adapter = c.machDawnNativeAdapters_index(adapters, i);
|
||||||
|
const properties = c.machDawnNativeAdapter_getProperties(adapter);
|
||||||
|
const found_backend_type = @intToEnum(gpu.Adapter.BackendType, c.machDawnNativeAdapterProperties_getBackendType(properties));
|
||||||
|
if (found_backend_type == backend_type) {
|
||||||
|
dawn_adapter = adapter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dawn_adapter == null) {
|
||||||
|
std.debug.print("mach: no matching adapter found for {s}", .{@tagName(backend_type)});
|
||||||
|
std.debug.print("-> maybe try GPU_BACKEND=opengl ?\n", .{});
|
||||||
|
std.process.exit(1);
|
||||||
|
}
|
||||||
|
std.debug.assert(dawn_adapter != null);
|
||||||
|
const backend_adapter = gpu.NativeInstance.fromWGPUAdapter(c.machDawnNativeAdapter_get(dawn_adapter.?).?);
|
||||||
|
|
||||||
|
// Print which adapter we are going to use.
|
||||||
|
const props = backend_adapter.properties;
|
||||||
|
std.debug.print("mach: found {s} backend on {s} adapter: {s}, {s}\n", .{
|
||||||
|
gpu.Adapter.backendTypeName(props.backend_type),
|
||||||
|
gpu.Adapter.typeName(props.adapter_type),
|
||||||
|
props.name,
|
||||||
|
props.driver_description,
|
||||||
|
});
|
||||||
|
|
||||||
|
const device = switch (backend_adapter.waitForDevice(&.{
|
||||||
|
.required_features = options.required_features,
|
||||||
|
.required_limits = options.required_limits,
|
||||||
|
})) {
|
||||||
|
.device => |v| v,
|
||||||
|
.err => |err| {
|
||||||
|
// TODO: return a proper error type
|
||||||
|
std.debug.print("mach: failed to get device: error={} {s}\n", .{ err.code, err.message });
|
||||||
|
std.process.exit(1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var framebuffer_size = try window.getFramebufferSize();
|
||||||
|
|
||||||
|
// If targeting OpenGL, we can't use the newer WGPUSurface API. Instead, we need to use the
|
||||||
|
// older Dawn-specific API. https://bugs.chromium.org/p/dawn/issues/detail?id=269&q=surface&can=2
|
||||||
|
const use_legacy_api = backend_type == .opengl or backend_type == .opengles;
|
||||||
|
var descriptor: gpu.SwapChain.Descriptor = undefined;
|
||||||
|
var swap_chain: ?gpu.SwapChain = null;
|
||||||
|
var swap_chain_format: gpu.Texture.Format = undefined;
|
||||||
|
var surface: ?gpu.Surface = null;
|
||||||
|
if (!use_legacy_api) {
|
||||||
|
swap_chain_format = .bgra8_unorm;
|
||||||
|
descriptor = .{
|
||||||
|
.label = "basic swap chain",
|
||||||
|
.usage = .{ .render_attachment = true },
|
||||||
|
.format = swap_chain_format,
|
||||||
|
.width = framebuffer_size.width,
|
||||||
|
.height = framebuffer_size.height,
|
||||||
|
.present_mode = switch (options.vsync) {
|
||||||
|
.none => .immediate,
|
||||||
|
.double => .fifo,
|
||||||
|
.triple => .mailbox,
|
||||||
|
},
|
||||||
|
.implementation = 0,
|
||||||
|
};
|
||||||
|
surface = util.createSurfaceForWindow(
|
||||||
|
&native_instance,
|
||||||
|
window,
|
||||||
|
comptime util.detectGLFWOptions(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const binding = c.machUtilsCreateBinding(@enumToInt(backend_type), @ptrCast(*c.GLFWwindow, window.handle), @ptrCast(c.WGPUDevice, device.ptr));
|
||||||
|
if (binding == null) {
|
||||||
|
@panic("failed to create Dawn backend binding");
|
||||||
|
}
|
||||||
|
descriptor = std.mem.zeroes(gpu.SwapChain.Descriptor);
|
||||||
|
descriptor.implementation = c.machUtilsBackendBinding_getSwapChainImplementation(binding);
|
||||||
|
swap_chain = device.nativeCreateSwapChain(null, &descriptor);
|
||||||
|
|
||||||
|
swap_chain_format = @intToEnum(gpu.Texture.Format, @intCast(u32, c.machUtilsBackendBinding_getPreferredSwapChainTextureFormat(binding)));
|
||||||
|
swap_chain.?.configure(
|
||||||
|
swap_chain_format,
|
||||||
|
.{ .render_attachment = true },
|
||||||
|
framebuffer_size.width,
|
||||||
|
framebuffer_size.height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
device.setUncapturedErrorCallback(&util.printUnhandledErrorCallback);
|
||||||
|
return Engine{
|
||||||
|
.allocator = allocator,
|
||||||
|
.timer = try std.time.Timer.start(),
|
||||||
|
.core = .{ .internal = .{
|
||||||
|
.window = window,
|
||||||
|
} },
|
||||||
|
.gpu_driver = .{
|
||||||
|
.device = device,
|
||||||
|
.backend_type = backend_type,
|
||||||
|
.native_instance = native_instance,
|
||||||
|
.surface = surface,
|
||||||
|
.swap_chain = swap_chain,
|
||||||
|
.swap_chain_format = swap_chain_format,
|
||||||
|
.current_desc = descriptor,
|
||||||
|
.target_desc = descriptor,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
var engine = try init(allocator, App.options);
|
||||||
|
var app: App = undefined;
|
||||||
|
|
||||||
|
try app.init(&engine);
|
||||||
|
defer app.deinit(&engine);
|
||||||
|
|
||||||
|
const window = engine.core.internal.window;
|
||||||
|
while (!window.shouldClose()) {
|
||||||
|
try glfw.pollEvents();
|
||||||
|
|
||||||
|
engine.delta_time_ns = engine.timer.lap();
|
||||||
|
engine.delta_time = @intToFloat(f64, engine.delta_time_ns) / @intToFloat(f64, std.time.ns_per_s);
|
||||||
|
|
||||||
|
var framebuffer_size = try window.getFramebufferSize();
|
||||||
|
engine.gpu_driver.target_desc.width = framebuffer_size.width;
|
||||||
|
engine.gpu_driver.target_desc.height = framebuffer_size.height;
|
||||||
|
|
||||||
|
if (engine.gpu_driver.swap_chain == null or !engine.gpu_driver.current_desc.equal(&engine.gpu_driver.target_desc)) {
|
||||||
|
const use_legacy_api = engine.gpu_driver.surface == null;
|
||||||
|
if (!use_legacy_api) {
|
||||||
|
engine.gpu_driver.swap_chain = engine.gpu_driver.device.nativeCreateSwapChain(engine.gpu_driver.surface, &engine.gpu_driver.target_desc);
|
||||||
|
} else engine.gpu_driver.swap_chain.?.configure(
|
||||||
|
engine.gpu_driver.swap_chain_format,
|
||||||
|
.{ .render_attachment = true },
|
||||||
|
engine.gpu_driver.target_desc.width,
|
||||||
|
engine.gpu_driver.target_desc.height,
|
||||||
|
);
|
||||||
|
|
||||||
|
//if (funcs.resize) |f| {
|
||||||
|
// try f(app, app.context, app.target_desc.width, app.target_desc.height);
|
||||||
|
//}
|
||||||
|
engine.gpu_driver.current_desc = engine.gpu_driver.target_desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = try app.update(&engine);
|
||||||
|
if (!success)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
289
src/main.zig
289
src/main.zig
|
|
@ -1,289 +1,2 @@
|
||||||
const std = @import("std");
|
pub const Engine = @import("Engine.zig");
|
||||||
const testing = std.testing;
|
|
||||||
|
|
||||||
const glfw = @import("glfw");
|
|
||||||
const gpu = @import("gpu");
|
|
||||||
const util = @import("util.zig");
|
|
||||||
const c = @import("c.zig").c;
|
|
||||||
|
|
||||||
/// For now, this contains nothing. In the future, this will include application configuration that
|
|
||||||
/// can only be specified at compile-time.
|
|
||||||
pub const AppConfig = struct {};
|
|
||||||
|
|
||||||
pub const VSyncMode = enum {
|
|
||||||
/// Potential screen tearing.
|
|
||||||
/// No synchronization with monitor, render frames as fast as possible.
|
|
||||||
none,
|
|
||||||
|
|
||||||
/// No tearing, synchronizes rendering with monitor refresh rate, rendering frames when ready.
|
|
||||||
///
|
|
||||||
/// Tries to stay one frame ahead of the monitor, so when it's ready for the next frame it is
|
|
||||||
/// already prepared.
|
|
||||||
double,
|
|
||||||
|
|
||||||
/// No tearing, synchronizes rendering with monitor refresh rate, rendering frames when ready.
|
|
||||||
///
|
|
||||||
/// Tries to stay two frames ahead of the monitor, so when it's ready for the next frame it is
|
|
||||||
/// already prepared.
|
|
||||||
triple,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Application options that can be configured at init time.
|
|
||||||
pub const Options = struct {
|
|
||||||
/// The title of the window.
|
|
||||||
title: [*:0]const u8 = "Mach engine",
|
|
||||||
|
|
||||||
/// The width of the window.
|
|
||||||
width: u32 = 640,
|
|
||||||
|
|
||||||
/// The height of the window.
|
|
||||||
height: u32 = 480,
|
|
||||||
|
|
||||||
/// Monitor synchronization modes.
|
|
||||||
vsync: VSyncMode = .double,
|
|
||||||
|
|
||||||
/// GPU features required by the application.
|
|
||||||
required_features: ?[]gpu.Feature = null,
|
|
||||||
|
|
||||||
/// GPU limits required by the application.
|
|
||||||
required_limits: ?gpu.Limits = null,
|
|
||||||
|
|
||||||
/// Whether the application has a preference for low power or high performance GPU.
|
|
||||||
power_preference: gpu.PowerPreference = .none,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Default GLFW error handling callback
|
|
||||||
fn glfwErrorCallback(error_code: glfw.Error, description: [:0]const u8) void {
|
|
||||||
std.debug.print("glfw: {}: {s}\n", .{ error_code, description });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A Mach application.
|
|
||||||
///
|
|
||||||
/// The Context type is your own data type which can later be accessed via app.context from within
|
|
||||||
/// the frame function you pass to run().
|
|
||||||
pub fn App(comptime Context: type, comptime config: AppConfig) type {
|
|
||||||
_ = config;
|
|
||||||
return struct {
|
|
||||||
context: Context,
|
|
||||||
device: gpu.Device,
|
|
||||||
window: glfw.Window,
|
|
||||||
backend_type: gpu.Adapter.BackendType,
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
swap_chain: ?gpu.SwapChain,
|
|
||||||
swap_chain_format: gpu.Texture.Format,
|
|
||||||
|
|
||||||
/// The amount of time (in seconds) that has passed since the last frame was rendered.
|
|
||||||
///
|
|
||||||
/// For example, if you are animating a cube which should rotate 360 degrees every second,
|
|
||||||
/// instead of writing (360.0 / 60.0) and assuming the frame rate is 60hz, write
|
|
||||||
/// (360.0 * app.delta_time)
|
|
||||||
delta_time: f64 = 0,
|
|
||||||
delta_time_ns: u64 = 0,
|
|
||||||
|
|
||||||
// Internals
|
|
||||||
native_instance: gpu.NativeInstance,
|
|
||||||
surface: ?gpu.Surface,
|
|
||||||
current_desc: gpu.SwapChain.Descriptor,
|
|
||||||
target_desc: gpu.SwapChain.Descriptor,
|
|
||||||
timer: std.time.Timer,
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, context: Context, options: Options) !Self {
|
|
||||||
const backend_type = try util.detectBackendType(allocator);
|
|
||||||
|
|
||||||
glfw.setErrorCallback(glfwErrorCallback);
|
|
||||||
try glfw.init(.{});
|
|
||||||
|
|
||||||
// Create the test window and discover adapters using it (esp. for OpenGL)
|
|
||||||
var hints = util.glfwWindowHintsForBackend(backend_type);
|
|
||||||
hints.cocoa_retina_framebuffer = true;
|
|
||||||
const window = try glfw.Window.create(
|
|
||||||
options.width,
|
|
||||||
options.height,
|
|
||||||
options.title,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
hints,
|
|
||||||
);
|
|
||||||
|
|
||||||
const backend_procs = c.machDawnNativeGetProcs();
|
|
||||||
c.dawnProcSetProcs(backend_procs);
|
|
||||||
|
|
||||||
const instance = c.machDawnNativeInstance_init();
|
|
||||||
var native_instance = gpu.NativeInstance.wrap(c.machDawnNativeInstance_get(instance).?);
|
|
||||||
|
|
||||||
// Discover e.g. OpenGL adapters.
|
|
||||||
try util.discoverAdapters(instance, window, backend_type);
|
|
||||||
|
|
||||||
// Request an adapter.
|
|
||||||
//
|
|
||||||
// TODO: It would be nice if we could use gpu_interface.waitForAdapter here, however the webgpu.h
|
|
||||||
// API does not yet have a way to specify what type of backend you want (vulkan, opengl, etc.)
|
|
||||||
// In theory, I suppose we shouldn't need to and Dawn should just pick the best adapter - but in
|
|
||||||
// practice if Vulkan is not supported today waitForAdapter/requestAdapter merely generates an error.
|
|
||||||
//
|
|
||||||
// const gpu_interface = native_instance.interface();
|
|
||||||
// const backend_adapter = switch (gpu_interface.waitForAdapter(&.{
|
|
||||||
// .power_preference = .high_performance,
|
|
||||||
// })) {
|
|
||||||
// .adapter => |v| v,
|
|
||||||
// .err => |err| {
|
|
||||||
// std.debug.print("mach: failed to get adapter: error={} {s}\n", .{ err.code, err.message });
|
|
||||||
// std.process.exit(1);
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
const adapters = c.machDawnNativeInstance_getAdapters(instance);
|
|
||||||
var dawn_adapter: ?c.MachDawnNativeAdapter = null;
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < c.machDawnNativeAdapters_length(adapters)) : (i += 1) {
|
|
||||||
const adapter = c.machDawnNativeAdapters_index(adapters, i);
|
|
||||||
const properties = c.machDawnNativeAdapter_getProperties(adapter);
|
|
||||||
const found_backend_type = @intToEnum(gpu.Adapter.BackendType, c.machDawnNativeAdapterProperties_getBackendType(properties));
|
|
||||||
if (found_backend_type == backend_type) {
|
|
||||||
dawn_adapter = adapter;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dawn_adapter == null) {
|
|
||||||
std.debug.print("mach: no matching adapter found for {s}", .{@tagName(backend_type)});
|
|
||||||
std.debug.print("-> maybe try GPU_BACKEND=opengl ?\n", .{});
|
|
||||||
std.process.exit(1);
|
|
||||||
}
|
|
||||||
std.debug.assert(dawn_adapter != null);
|
|
||||||
const backend_adapter = gpu.NativeInstance.fromWGPUAdapter(c.machDawnNativeAdapter_get(dawn_adapter.?).?);
|
|
||||||
|
|
||||||
// Print which adapter we are going to use.
|
|
||||||
const props = backend_adapter.properties;
|
|
||||||
std.debug.print("mach: found {s} backend on {s} adapter: {s}, {s}\n", .{
|
|
||||||
gpu.Adapter.backendTypeName(props.backend_type),
|
|
||||||
gpu.Adapter.typeName(props.adapter_type),
|
|
||||||
props.name,
|
|
||||||
props.driver_description,
|
|
||||||
});
|
|
||||||
|
|
||||||
const device = switch (backend_adapter.waitForDevice(&.{
|
|
||||||
.required_features = options.required_features,
|
|
||||||
.required_limits = options.required_limits,
|
|
||||||
})) {
|
|
||||||
.device => |v| v,
|
|
||||||
.err => |err| {
|
|
||||||
// TODO: return a proper error type
|
|
||||||
std.debug.print("mach: failed to get device: error={} {s}\n", .{ err.code, err.message });
|
|
||||||
std.process.exit(1);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var framebuffer_size = try window.getFramebufferSize();
|
|
||||||
|
|
||||||
// If targeting OpenGL, we can't use the newer WGPUSurface API. Instead, we need to use the
|
|
||||||
// older Dawn-specific API. https://bugs.chromium.org/p/dawn/issues/detail?id=269&q=surface&can=2
|
|
||||||
const use_legacy_api = backend_type == .opengl or backend_type == .opengles;
|
|
||||||
var descriptor: gpu.SwapChain.Descriptor = undefined;
|
|
||||||
var swap_chain: ?gpu.SwapChain = null;
|
|
||||||
var swap_chain_format: gpu.Texture.Format = undefined;
|
|
||||||
var surface: ?gpu.Surface = null;
|
|
||||||
if (!use_legacy_api) {
|
|
||||||
swap_chain_format = .bgra8_unorm;
|
|
||||||
descriptor = .{
|
|
||||||
.label = "basic swap chain",
|
|
||||||
.usage = .{ .render_attachment = true },
|
|
||||||
.format = swap_chain_format,
|
|
||||||
.width = framebuffer_size.width,
|
|
||||||
.height = framebuffer_size.height,
|
|
||||||
.present_mode = switch (options.vsync) {
|
|
||||||
.none => .immediate,
|
|
||||||
.double => .fifo,
|
|
||||||
.triple => .mailbox,
|
|
||||||
},
|
|
||||||
.implementation = 0,
|
|
||||||
};
|
|
||||||
surface = util.createSurfaceForWindow(
|
|
||||||
&native_instance,
|
|
||||||
window,
|
|
||||||
comptime util.detectGLFWOptions(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const binding = c.machUtilsCreateBinding(@enumToInt(backend_type), @ptrCast(*c.GLFWwindow, window.handle), @ptrCast(c.WGPUDevice, device.ptr));
|
|
||||||
if (binding == null) {
|
|
||||||
@panic("failed to create Dawn backend binding");
|
|
||||||
}
|
|
||||||
descriptor = std.mem.zeroes(gpu.SwapChain.Descriptor);
|
|
||||||
descriptor.implementation = c.machUtilsBackendBinding_getSwapChainImplementation(binding);
|
|
||||||
swap_chain = device.nativeCreateSwapChain(null, &descriptor);
|
|
||||||
|
|
||||||
swap_chain_format = @intToEnum(gpu.Texture.Format, @intCast(u32, c.machUtilsBackendBinding_getPreferredSwapChainTextureFormat(binding)));
|
|
||||||
swap_chain.?.configure(
|
|
||||||
swap_chain_format,
|
|
||||||
.{ .render_attachment = true },
|
|
||||||
framebuffer_size.width,
|
|
||||||
framebuffer_size.height,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
device.setUncapturedErrorCallback(&util.printUnhandledErrorCallback);
|
|
||||||
return Self{
|
|
||||||
.context = context,
|
|
||||||
.device = device,
|
|
||||||
.window = window,
|
|
||||||
.backend_type = backend_type,
|
|
||||||
.allocator = allocator,
|
|
||||||
|
|
||||||
.native_instance = native_instance,
|
|
||||||
.surface = surface,
|
|
||||||
.swap_chain = swap_chain,
|
|
||||||
.swap_chain_format = swap_chain_format,
|
|
||||||
.timer = try std.time.Timer.start(),
|
|
||||||
.current_desc = descriptor,
|
|
||||||
.target_desc = descriptor,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const Funcs = struct {
|
|
||||||
// Run once per frame
|
|
||||||
frame: fn (app: *Self, ctx: Context) error{OutOfMemory}!void,
|
|
||||||
// Run once at the start, and whenever the swapchain is recreated
|
|
||||||
resize: ?fn (app: *Self, ctx: Context, width: u32, height: u32) error{OutOfMemory}!void = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn run(app: *Self, funcs: Funcs) !void {
|
|
||||||
if (app.swap_chain != null and funcs.resize != null) {
|
|
||||||
try funcs.resize.?(app, app.context, app.current_desc.width, app.current_desc.height);
|
|
||||||
}
|
|
||||||
while (!app.window.shouldClose()) {
|
|
||||||
try glfw.pollEvents();
|
|
||||||
|
|
||||||
app.delta_time_ns = app.timer.lap();
|
|
||||||
app.delta_time = @intToFloat(f64, app.delta_time_ns) / @intToFloat(f64, std.time.ns_per_s);
|
|
||||||
|
|
||||||
var framebuffer_size = try app.window.getFramebufferSize();
|
|
||||||
app.target_desc.width = framebuffer_size.width;
|
|
||||||
app.target_desc.height = framebuffer_size.height;
|
|
||||||
|
|
||||||
if (app.swap_chain == null or !app.current_desc.equal(&app.target_desc)) {
|
|
||||||
const use_legacy_api = app.surface == null;
|
|
||||||
if (!use_legacy_api) {
|
|
||||||
app.swap_chain = app.device.nativeCreateSwapChain(app.surface, &app.target_desc);
|
|
||||||
} else app.swap_chain.?.configure(
|
|
||||||
app.swap_chain_format,
|
|
||||||
.{ .render_attachment = true },
|
|
||||||
app.target_desc.width,
|
|
||||||
app.target_desc.height,
|
|
||||||
);
|
|
||||||
if (funcs.resize) |f| {
|
|
||||||
try f(app, app.context, app.target_desc.width, app.target_desc.height);
|
|
||||||
}
|
|
||||||
app.current_desc = app.target_desc;
|
|
||||||
}
|
|
||||||
|
|
||||||
try funcs.frame(app, app.context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test "glfw_basic" {
|
|
||||||
_ = Options;
|
|
||||||
_ = App;
|
|
||||||
glfw.basicTest() catch unreachable;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue