diff --git a/build.zig b/build.zig index 8a46d67c..c18a4ea4 100644 --- a/build.zig +++ b/build.zig @@ -103,7 +103,7 @@ const MachApp = struct { b: *std.build.Builder, 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(.{ .name = "app", .path = .{ .path = src }, diff --git a/examples/triangle/main.zig b/examples/triangle/main.zig index cc8c1cc0..f466966c 100644 --- a/examples/triangle/main.zig +++ b/examples/triangle/main.zig @@ -2,21 +2,19 @@ const std = @import("std"); const mach = @import("mach"); const gpu = @import("gpu"); -const App = mach.App(*FrameParams, .{}); +pub const options: mach.Engine.Options = .{}; -pub fn init() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - var allocator = gpa.allocator(); +pipeline: gpu.RenderPipeline, +queue: gpu.Queue, - const ctx = try allocator.create(FrameParams); - var app = try App.init(allocator, ctx, .{}); - - const vs_module = app.device.createShaderModule(&.{ +const Self = @This(); +pub fn init(self: *Self, engine: *mach.Engine) !void { + const vs_module = engine.gpu_driver.device.createShaderModule(&.{ .label = "my vertex shader", .code = .{ .wgsl = @embedFile("vert.wgsl") }, }); - const fs_module = app.device.createShaderModule(&.{ + const fs_module = engine.gpu_driver.device.createShaderModule(&.{ .label = "my fragment shader", .code = .{ .wgsl = @embedFile("frag.wgsl") }, }); @@ -35,7 +33,7 @@ pub fn init() !void { }, }; const color_target = gpu.ColorTargetState{ - .format = app.swap_chain_format, + .format = engine.gpu_driver.swap_chain_format, .blend = &blend, .write_mask = gpu.ColorWriteMask.all, }; @@ -67,30 +65,17 @@ pub fn init() !void { }, }; - ctx.* = FrameParams{ - .pipeline = app.device.createRenderPipeline(&pipeline_descriptor), - .queue = app.device.getQueue(), - }; + self.pipeline = engine.gpu_driver.device.createRenderPipeline(&pipeline_descriptor); + self.queue = engine.gpu_driver.device.getQueue(); vs_module.release(); fs_module.release(); - - try app.run(.{ .frame = frame }); } -pub fn update() !bool { - return false; -} +pub fn deinit(_: *Self, _: *mach.Engine) void {} -pub fn deinit() void {} - -const FrameParams = struct { - pipeline: gpu.RenderPipeline, - queue: gpu.Queue, -}; - -fn frame(app: *App, params: *FrameParams) !void { - const back_buffer_view = app.swap_chain.?.getCurrentTextureView(); +pub fn update(self: *Self, engine: *mach.Engine) !bool { + const back_buffer_view = engine.gpu_driver.swap_chain.?.getCurrentTextureView(); const color_attachment = gpu.RenderPassColorAttachment{ .view = back_buffer_view, .resolve_target = null, @@ -99,13 +84,13 @@ fn frame(app: *App, params: *FrameParams) !void { .store_op = .store, }; - const encoder = app.device.createCommandEncoder(null); + const encoder = engine.gpu_driver.device.createCommandEncoder(null); const render_pass_info = gpu.RenderPassEncoder.Descriptor{ .color_attachments = &.{color_attachment}, .depth_stencil_attachment = null, }; const pass = encoder.beginRenderPass(&render_pass_info); - pass.setPipeline(params.pipeline); + pass.setPipeline(self.pipeline); pass.draw(3, 1, 0, 0); pass.end(); pass.release(); @@ -113,8 +98,10 @@ fn frame(app: *App, params: *FrameParams) !void { var command = encoder.finish(null); encoder.release(); - params.queue.submit(&.{command}); + self.queue.submit(&.{command}); command.release(); - app.swap_chain.?.present(); + engine.gpu_driver.swap_chain.?.present(); back_buffer_view.release(); + + return true; } diff --git a/src/Engine.zig b/src/Engine.zig new file mode 100644 index 00000000..a50891e8 --- /dev/null +++ b/src/Engine.zig @@ -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, +}; diff --git a/src/entry/native.zig b/src/entry/native.zig deleted file mode 100644 index 498ffa71..00000000 --- a/src/entry/native.zig +++ /dev/null @@ -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; - } -} diff --git a/src/entry_native.zig b/src/entry_native.zig new file mode 100644 index 00000000..b92ba2fb --- /dev/null +++ b/src/entry_native.zig @@ -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; + } +} diff --git a/src/main.zig b/src/main.zig index 956fe466..796b98c8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,289 +1,2 @@ -const std = @import("std"); -const testing = std.testing; +pub const Engine = @import("Engine.zig"); -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; -}