diff --git a/build.zig b/build.zig index 6546db60..90f51179 100644 --- a/build.zig +++ b/build.zig @@ -1,23 +1,73 @@ -const Builder = @import("std").build.Builder; +const std = @import("std"); +const gpu = @import("gpu/build.zig"); +const gpu_dawn = @import("gpu-dawn/build.zig"); const glfw = @import("glfw/build.zig"); -pub fn build(b: *Builder) void { +pub fn build(b: *std.build.Builder) void { const mode = b.standardReleaseOptions(); const target = b.standardTargetOptions(.{}); - const lib = b.addStaticLibrary("engine", "src/main.zig"); - lib.setBuildMode(mode); - lib.setTarget(target); - lib.addPackagePath("glfw", "glfw/src/main.zig"); - glfw.link(b, lib, .{}); - lib.install(); + const gpu_dawn_options = gpu_dawn.Options{ + .from_source = b.option(bool, "dawn-from-source", "Build Dawn from source") orelse false, + }; + const options = Options {.gpu_dawn_options = gpu_dawn_options}; const main_tests = b.addTest("src/main.zig"); main_tests.setBuildMode(mode); main_tests.setTarget(target); - main_tests.addPackagePath("glfw", "glfw/src/main.zig"); - glfw.link(b, main_tests, .{}); + main_tests.addPackage(pkg); + main_tests.addPackage(gpu.pkg); + link(b, main_tests, options); const test_step = b.step("test", "Run library tests"); test_step.dependOn(&main_tests.step); + + const example = b.addExecutable("hello-triangle", "examples/main.zig"); + example.setTarget(target); + example.setBuildMode(mode); + example.addPackage(pkg); + example.addPackage(gpu.pkg); + link(b, example, options); + example.install(); + + const example_run_cmd = example.run(); + example_run_cmd.step.dependOn(b.getInstallStep()); + const example_run_step = b.step("run-example", "Run the example"); + example_run_step.dependOn(&example_run_cmd.step); +} + +pub const Options = struct { + glfw_options: glfw.Options = .{}, + gpu_dawn_options: gpu_dawn.Options = .{}, +}; + +pub const pkg = .{ + .name = "mach", + .path = .{ .path = thisDir() ++ "/src/main.zig" }, + .dependencies = &.{ gpu.pkg, glfw.pkg }, +}; + +pub fn link(b: *std.build.Builder, step: *std.build.LibExeObjStep, options: Options) void { + const gpu_options = gpu.Options{ + .glfw_options = @bitCast(@import("gpu/libs/mach-glfw/build.zig").Options, options.glfw_options), + .gpu_dawn_options = @bitCast(@import("gpu/libs/mach-gpu-dawn/build.zig").Options, options.gpu_dawn_options), + }; + + const main_abs = std.fs.path.join(b.allocator, &.{ thisDir(), "src/main.zig" }) catch unreachable; + const lib = b.addStaticLibrary("mach", main_abs); + lib.setBuildMode(step.build_mode); + lib.setTarget(step.target); + lib.addPackage(gpu.pkg); + lib.addPackage(glfw.pkg); + + glfw.link(b, lib, options.glfw_options); + gpu.link(b, lib, gpu_options); + lib.install(); + + glfw.link(b, step, options.glfw_options); + gpu.link(b, step, gpu_options); +} + +fn thisDir() []const u8 { + return std.fs.path.dirname(@src().file) orelse "."; } diff --git a/examples/frag.wgsl b/examples/frag.wgsl new file mode 100644 index 00000000..bb3d9f80 --- /dev/null +++ b/examples/frag.wgsl @@ -0,0 +1,3 @@ +@stage(fragment) fn main() -> @location(0) vec4 { + return vec4(1.0, 0.0, 0.0, 1.0); +} diff --git a/examples/main.zig b/examples/main.zig new file mode 100644 index 00000000..2701f32b --- /dev/null +++ b/examples/main.zig @@ -0,0 +1,114 @@ +const std = @import("std"); +const mach = @import("mach"); +const gpu = @import("gpu"); + +const App = mach.App(*FrameParams, .{}); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var allocator = gpa.allocator(); + + const ctx = try allocator.create(FrameParams); + var app = try App.init(allocator, ctx, .{}); + + const vs_module = app.device.createShaderModule(&.{ + .label = "my vertex shader", + .code = .{ .wgsl = @embedFile("vert.wgsl") }, + }); + + const fs_module = app.device.createShaderModule(&.{ + .label = "my fragment shader", + .code = .{ .wgsl = @embedFile("frag.wgsl") }, + }); + + // Fragment state + const blend = gpu.BlendState{ + .color = .{ + .operation = .add, + .src_factor = .one, + .dst_factor = .one, + }, + .alpha = .{ + .operation = .add, + .src_factor = .one, + .dst_factor = .one, + }, + }; + const color_target = gpu.ColorTargetState{ + .format = app.swap_chain_format, + .blend = &blend, + .write_mask = .all, + }; + const fragment = gpu.FragmentState{ + .module = fs_module, + .entry_point = "main", + .targets = &.{color_target}, + .constants = null, + }; + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = null, + .depth_stencil = null, + .vertex = .{ + .module = vs_module, + .entry_point = "main", + .buffers = null, + }, + .multisample = .{ + .count = 1, + .mask = 0xFFFFFFFF, + .alpha_to_coverage_enabled = false, + }, + .primitive = .{ + .front_face = .ccw, + .cull_mode = .none, + .topology = .triangle_list, + .strip_index_format = .none, + }, + }; + + ctx.* = FrameParams{ + .pipeline = app.device.createRenderPipeline(&pipeline_descriptor), + .queue = app.device.getQueue(), + }; + + vs_module.release(); + fs_module.release(); + + try app.run(frame); +} + +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{ + .view = back_buffer_view, + .resolve_target = null, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + + const encoder = app.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.draw(3, 1, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + params.queue.submit(&.{command}); + command.release(); + app.swap_chain.?.present(); + back_buffer_view.release(); +} diff --git a/examples/vert.wgsl b/examples/vert.wgsl new file mode 100644 index 00000000..32131db0 --- /dev/null +++ b/examples/vert.wgsl @@ -0,0 +1,10 @@ +@stage(vertex) fn main( + @builtin(vertex_index) VertexIndex : u32 +) -> @builtin(position) vec4 { + var pos = array, 3>( + vec2( 0.0, 0.5), + vec2(-0.5, -0.5), + vec2( 0.5, -0.5) + ); + return vec4(pos[VertexIndex], 0.0, 1.0); +} diff --git a/src/c.zig b/src/c.zig new file mode 100644 index 00000000..2648fad7 --- /dev/null +++ b/src/c.zig @@ -0,0 +1,5 @@ +pub const c = @cImport({ + @cInclude("dawn/webgpu.h"); + @cInclude("dawn/dawn_proc.h"); + @cInclude("dawn_native_mach.h"); +}); diff --git a/src/main.zig b/src/main.zig index 728ff2e2..283086d0 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,7 +2,233 @@ const std = @import("std"); 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 {}; + +/// 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, + + /// 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, +}; + +/// 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, + + // Internals + native_instance: gpu.NativeInstance, + surface: ?gpu.Surface, + current_desc: gpu.SwapChain.Descriptor, + target_desc: gpu.SwapChain.Descriptor, + + const Self = @This(); + + pub fn init(allocator: std.mem.Allocator, context: Context, options: Options) !Self { + const backend_type = try util.detectBackendType(allocator); + + 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, + .format = swap_chain_format, + .width = framebuffer_size.width, + .height = framebuffer_size.height, + .present_mode = .fifo, + .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, + 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, + .current_desc = descriptor, + .target_desc = descriptor, + }; + } + + const FrameFunc = fn (app: *Self, ctx: Context) error{OutOfMemory}!void; + + pub fn run(app: *Self, frame: FrameFunc) !void { + while (!app.window.shouldClose()) { + try glfw.pollEvents(); + + 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, + app.target_desc.width, + app.target_desc.height, + ); + app.current_desc = app.target_desc; + } + + try frame(app, app.context); + std.time.sleep(16 * std.time.ns_per_ms); // TODO: this is very naive + } + } + }; +} test "glfw_basic" { + _ = Options; + _ = App; glfw.basicTest() catch unreachable; } diff --git a/src/util.zig b/src/util.zig new file mode 100644 index 00000000..0e7282ee --- /dev/null +++ b/src/util.zig @@ -0,0 +1,168 @@ +const std = @import("std"); + +const glfw = @import("glfw"); +const gpu = @import("gpu"); +const c = @import("c.zig").c; +const objc = @cImport({ + @cInclude("objc/message.h"); +}); + +fn printUnhandledError(_: void, typ: gpu.ErrorType, message: [*:0]const u8) void { + switch (typ) { + .validation => std.debug.print("gpu: validation error: {s}\n", .{message}), + .out_of_memory => std.debug.print("gpu: out of memory: {s}\n", .{message}), + .device_lost => std.debug.print("gpu: device lost: {s}\n", .{message}), + .unknown => std.debug.print("gpu: unknown error: {s}\n", .{message}), + else => unreachable, + } + std.os.exit(1); +} +pub var printUnhandledErrorCallback = gpu.ErrorCallback.init(void, {}, printUnhandledError); + +fn getEnvVarOwned(allocator: std.mem.Allocator, key: []const u8) error{ OutOfMemory, InvalidUtf8 }!?[]u8 { + return std.process.getEnvVarOwned(allocator, key) catch |err| switch (err) { + error.EnvironmentVariableNotFound => @as(?[]u8, null), + else => |e| e, + }; +} + +pub fn detectBackendType(allocator: std.mem.Allocator) !gpu.Adapter.BackendType { + const GPU_BACKEND = try getEnvVarOwned(allocator, "GPU_BACKEND"); + if (GPU_BACKEND) |backend| { + defer allocator.free(backend); + if (std.ascii.eqlIgnoreCase(backend, "opengl")) return .opengl; + if (std.ascii.eqlIgnoreCase(backend, "opengles")) return .opengles; + if (std.ascii.eqlIgnoreCase(backend, "d3d11")) return .d3d11; + if (std.ascii.eqlIgnoreCase(backend, "d3d12")) return .d3d12; + if (std.ascii.eqlIgnoreCase(backend, "metal")) return .metal; + if (std.ascii.eqlIgnoreCase(backend, "null")) return .nul; + if (std.ascii.eqlIgnoreCase(backend, "vulkan")) return .vulkan; + @panic("unknown GPU_BACKEND type"); + } + + const target = @import("builtin").target; + if (target.isDarwin()) return .metal; + if (target.os.tag == .windows) return .d3d12; + return .vulkan; +} + +pub fn glfwWindowHintsForBackend(backend: gpu.Adapter.BackendType) glfw.Window.Hints { + return switch (backend) { + .opengl => .{ + // Ask for OpenGL 4.4 which is what the GL backend requires for compute shaders and + // texture views. + .context_version_major = 4, + .context_version_minor = 4, + .opengl_forward_compat = true, + .opengl_profile = .opengl_core_profile, + }, + .opengles => .{ + .context_version_major = 3, + .context_version_minor = 1, + .client_api = .opengl_es_api, + .context_creation_api = .egl_context_api, + }, + else => .{ + // Without this GLFW will initialize a GL context on the window, which prevents using + // the window with other APIs (by crashing in weird ways). + .client_api = .no_api, + }, + }; +} + +pub fn discoverAdapters(instance: c.MachDawnNativeInstance, window: glfw.Window, typ: gpu.Adapter.BackendType) !void { + switch (typ) { + .opengl => { + try glfw.makeContextCurrent(window); + const adapter_options = c.MachDawnNativeAdapterDiscoveryOptions_OpenGL{ + .getProc = @ptrCast(fn ([*c]const u8) callconv(.C) ?*anyopaque, glfw.getProcAddress), + }; + _ = c.machDawnNativeInstance_discoverAdapters(instance, @enumToInt(typ), &adapter_options); + }, + .opengles => { + try glfw.makeContextCurrent(window); + const adapter_options = c.MachDawnNativeAdapterDiscoveryOptions_OpenGLES{ + .getProc = @ptrCast(fn ([*c]const u8) callconv(.C) ?*anyopaque, glfw.getProcAddress), + }; + _ = c.machDawnNativeInstance_discoverAdapters(instance, @enumToInt(typ), &adapter_options); + }, + else => { + c.machDawnNativeInstance_discoverDefaultAdapters(instance); + }, + } +} + +pub fn detectGLFWOptions() glfw.BackendOptions { + const target = @import("builtin").target; + if (target.isDarwin()) return .{ .cocoa = true }; + return switch (target.os.tag) { + .windows => .{ .win32 = true }, + .linux => .{ .x11 = true }, + else => .{}, + }; +} + +pub fn createSurfaceForWindow( + native_instance: *const gpu.NativeInstance, + window: glfw.Window, + comptime glfw_options: glfw.BackendOptions, +) gpu.Surface { + const glfw_native = glfw.Native(glfw_options); + const descriptor = if (glfw_options.win32) gpu.Surface.Descriptor{ + .windows_hwnd = .{ + .label = "basic surface", + .hinstance = std.os.windows.kernel32.GetModuleHandleW(null).?, + .hwnd = glfw_native.getWin32Window(window), + }, + } else if (glfw_options.x11) gpu.Surface.Descriptor{ + .xlib = .{ + .label = "basic surface", + .display = glfw_native.getX11Display(), + .window = glfw_native.getX11Window(window), + }, + } else if (glfw_options.cocoa) blk: { + const ns_window = glfw_native.getCocoaWindow(window); + const ns_view = msgSend(ns_window, "contentView", .{}, *anyopaque); // [nsWindow contentView] + + // Create a CAMetalLayer that covers the whole window that will be passed to CreateSurface. + msgSend(ns_view, "setWantsLayer:", .{true}, void); // [view setWantsLayer:YES] + const layer = msgSend(objc.objc_getClass("CAMetalLayer"), "layer", .{}, ?*anyopaque); // [CAMetalLayer layer] + if (layer == null) @panic("failed to create Metal layer"); + msgSend(ns_view, "setLayer:", .{layer.?}, void); // [view setLayer:layer] + + // Use retina if the window was created with retina support. + const scale_factor = msgSend(ns_window, "backingScaleFactor", .{}, f64); // [ns_window backingScaleFactor] + msgSend(layer.?, "setContentsScale:", .{scale_factor}, void); // [layer setContentsScale:scale_factor] + + break :blk gpu.Surface.Descriptor{ + .metal_layer = .{ + .label = "basic surface", + .layer = layer.?, + }, + }; + } else if (glfw_options.wayland) { + @panic("Dawn does not yet have Wayland support, see https://bugs.chromium.org/p/dawn/issues/detail?id=1246&q=surface&can=2"); + } else unreachable; + + return native_instance.createSurface(&descriptor); +} + +// Borrowed from https://github.com/hazeycode/zig-objcrt +fn msgSend(obj: anytype, sel_name: [:0]const u8, args: anytype, comptime ReturnType: type) ReturnType { + const args_meta = @typeInfo(@TypeOf(args)).Struct.fields; + + const FnType = switch (args_meta.len) { + 0 => fn (@TypeOf(obj), objc.SEL) callconv(.C) ReturnType, + 1 => fn (@TypeOf(obj), objc.SEL, args_meta[0].field_type) callconv(.C) ReturnType, + 2 => fn (@TypeOf(obj), objc.SEL, args_meta[0].field_type, args_meta[1].field_type) callconv(.C) ReturnType, + 3 => fn (@TypeOf(obj), objc.SEL, args_meta[0].field_type, args_meta[1].field_type, args_meta[2].field_type) callconv(.C) ReturnType, + 4 => fn (@TypeOf(obj), objc.SEL, args_meta[0].field_type, args_meta[1].field_type, args_meta[2].field_type, args_meta[3].field_type) callconv(.C) ReturnType, + else => @compileError("Unsupported number of args"), + }; + + // NOTE: func is a var because making it const causes a compile error which I believe is a compiler bug + var func = @ptrCast(FnType, objc.objc_msgSend); + const sel = objc.sel_getUid(sel_name); + + return @call(.{}, func, .{ obj, sel } ++ args); +}