From 718a66497c6d3c469ce8ebc3b1c2c320c55904f7 Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Sun, 6 Mar 2022 15:25:35 -0700 Subject: [PATCH] gpu: add gpu-hello-triangle (dawn) example Signed-off-by: Stephen Gutekanst --- gpu/build.zig | 15 ++- gpu/examples/c.zig | 5 + gpu/examples/main.zig | 234 +++++++++++++++++++++++++++++++++ gpu/examples/sample_utils.zig | 236 ++++++++++++++++++++++++++++++++++ gpu/libs/mach-glfw | 1 + gpu/libs/mach-gpu-dawn | 1 + 6 files changed, 490 insertions(+), 2 deletions(-) create mode 100644 gpu/examples/c.zig create mode 100644 gpu/examples/main.zig create mode 100644 gpu/examples/sample_utils.zig create mode 120000 gpu/libs/mach-glfw create mode 120000 gpu/libs/mach-gpu-dawn diff --git a/gpu/build.zig b/gpu/build.zig index 8938d02f..f91a1a7e 100644 --- a/gpu/build.zig +++ b/gpu/build.zig @@ -1,17 +1,28 @@ const std = @import("std"); +const gpu_dawn = @import("libs/mach-gpu-dawn/build.zig"); +const glfw = @import("libs/mach-glfw/build.zig"); pub fn build(b: *std.build.Builder) void { - // Standard release options allow the person running `zig build` to select - // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. const mode = b.standardReleaseOptions(); + const target = b.standardTargetOptions(.{}); const lib = b.addStaticLibrary("gpu", "src/main.zig"); lib.setBuildMode(mode); lib.install(); + gpu_dawn.link(b, lib, .{}); const main_tests = b.addTest("src/main.zig"); main_tests.setBuildMode(mode); const test_step = b.step("test", "Run library tests"); test_step.dependOn(&main_tests.step); + + const example = b.addExecutable("gpu-hello-triangle", "examples/main.zig"); + example.setTarget(target); + example.setBuildMode(mode); + example.install(); + example.linkLibC(); + example.addPackagePath("glfw", "libs/mach-glfw/src/main.zig"); + glfw.link(b, example, .{}); + gpu_dawn.link(b, example, .{}); } diff --git a/gpu/examples/c.zig b/gpu/examples/c.zig new file mode 100644 index 00000000..2648fad7 --- /dev/null +++ b/gpu/examples/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/gpu/examples/main.zig b/gpu/examples/main.zig new file mode 100644 index 00000000..508c5bc8 --- /dev/null +++ b/gpu/examples/main.zig @@ -0,0 +1,234 @@ +const std = @import("std"); +const sample_utils = @import("sample_utils.zig"); +const c = @import("c.zig").c; +const glfw = @import("glfw"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var allocator = gpa.allocator(); + + const setup = try sample_utils.setup(allocator); + const queue = c.wgpuDeviceGetQueue(setup.device); + const framebuffer_size = try setup.window.getFramebufferSize(); + + const window_data = try allocator.create(WindowData); + window_data.* = .{ + .surface = null, + .swap_chain = null, + .swap_chain_format = undefined, + .current_desc = undefined, + .target_desc = undefined, + }; + setup.window.setUserPointer(window_data); + + // If targetting 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 = setup.backend_type == c.WGPUBackendType_OpenGL or setup.backend_type == c.WGPUBackendType_OpenGLES; + var descriptor: c.WGPUSwapChainDescriptor = undefined; + if (!use_legacy_api) { + window_data.swap_chain_format = c.WGPUTextureFormat_BGRA8Unorm; + descriptor = c.WGPUSwapChainDescriptor{ + .nextInChain = null, + .label = "basic swap chain", + .usage = c.WGPUTextureUsage_RenderAttachment, + .format = window_data.swap_chain_format, + .width = framebuffer_size.width, + .height = framebuffer_size.height, + .presentMode = c.WGPUPresentMode_Fifo, + .implementation = 0, + }; + window_data.surface = sample_utils.createSurfaceForWindow( + setup.instance, + setup.window, + comptime sample_utils.detectGLFWOptions(), + ); + } else { + const binding = c.machUtilsCreateBinding(setup.backend_type, @ptrCast(*c.GLFWwindow, setup.window.handle), setup.device); + if (binding == null) { + @panic("failed to create Dawn backend binding"); + } + descriptor = std.mem.zeroes(c.WGPUSwapChainDescriptor); + descriptor.implementation = c.machUtilsBackendBinding_getSwapChainImplementation(binding); + window_data.swap_chain = c.wgpuDeviceCreateSwapChain(setup.device, null, &descriptor); + + window_data.swap_chain_format = c.machUtilsBackendBinding_getPreferredSwapChainTextureFormat(binding); + c.wgpuSwapChainConfigure( + window_data.swap_chain.?, + window_data.swap_chain_format, + c.WGPUTextureUsage_RenderAttachment, + framebuffer_size.width, + framebuffer_size.height, + ); + } + window_data.current_desc = descriptor; + window_data.target_desc = descriptor; + + const vs = + \\ @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); + \\ } + ; + var vs_wgsl_descriptor = try allocator.create(c.WGPUShaderModuleWGSLDescriptor); + vs_wgsl_descriptor.chain.next = null; + vs_wgsl_descriptor.chain.sType = c.WGPUSType_ShaderModuleWGSLDescriptor; + vs_wgsl_descriptor.source = vs; + const vs_shader_descriptor = c.WGPUShaderModuleDescriptor{ + .nextInChain = @ptrCast(*const c.WGPUChainedStruct, vs_wgsl_descriptor), + .label = "my vertex shader", + }; + const vs_module = c.wgpuDeviceCreateShaderModule(setup.device, &vs_shader_descriptor); + + const fs = + \\ @stage(fragment) fn main() -> @location(0) vec4 { + \\ return vec4(1.0, 0.0, 0.0, 1.0); + \\ } + ; + var fs_wgsl_descriptor = try allocator.create(c.WGPUShaderModuleWGSLDescriptor); + fs_wgsl_descriptor.chain.next = null; + fs_wgsl_descriptor.chain.sType = c.WGPUSType_ShaderModuleWGSLDescriptor; + fs_wgsl_descriptor.source = fs; + const fs_shader_descriptor = c.WGPUShaderModuleDescriptor{ + .nextInChain = @ptrCast(*const c.WGPUChainedStruct, fs_wgsl_descriptor), + .label = "my fragment shader", + }; + const fs_module = c.wgpuDeviceCreateShaderModule(setup.device, &fs_shader_descriptor); + + // Fragment state + var blend = std.mem.zeroes(c.WGPUBlendState); + blend.color.operation = c.WGPUBlendOperation_Add; + blend.color.srcFactor = c.WGPUBlendFactor_One; + blend.color.dstFactor = c.WGPUBlendFactor_One; + blend.alpha.operation = c.WGPUBlendOperation_Add; + blend.alpha.srcFactor = c.WGPUBlendFactor_One; + blend.alpha.dstFactor = c.WGPUBlendFactor_One; + + var color_target = std.mem.zeroes(c.WGPUColorTargetState); + color_target.format = window_data.swap_chain_format; + color_target.blend = &blend; + color_target.writeMask = c.WGPUColorWriteMask_All; + + var fragment = std.mem.zeroes(c.WGPUFragmentState); + fragment.module = fs_module; + fragment.entryPoint = "main"; + fragment.targetCount = 1; + fragment.targets = &color_target; + + var pipeline_descriptor = std.mem.zeroes(c.WGPURenderPipelineDescriptor); + pipeline_descriptor.fragment = &fragment; + + // Other state + pipeline_descriptor.layout = null; + pipeline_descriptor.depthStencil = null; + + pipeline_descriptor.vertex.module = vs_module; + pipeline_descriptor.vertex.entryPoint = "main"; + pipeline_descriptor.vertex.bufferCount = 0; + pipeline_descriptor.vertex.buffers = null; + + pipeline_descriptor.multisample.count = 1; + pipeline_descriptor.multisample.mask = 0xFFFFFFFF; + pipeline_descriptor.multisample.alphaToCoverageEnabled = false; + + pipeline_descriptor.primitive.frontFace = c.WGPUFrontFace_CCW; + pipeline_descriptor.primitive.cullMode = c.WGPUCullMode_None; + pipeline_descriptor.primitive.topology = c.WGPUPrimitiveTopology_TriangleList; + pipeline_descriptor.primitive.stripIndexFormat = c.WGPUIndexFormat_Undefined; + + const pipeline = c.wgpuDeviceCreateRenderPipeline(setup.device, &pipeline_descriptor); + + c.wgpuShaderModuleRelease(vs_module); + c.wgpuShaderModuleRelease(fs_module); + + // Reconfigure the swap chain with the new framebuffer width/height, otherwise e.g. the Vulkan + // device would be lost after a resize. + setup.window.setFramebufferSizeCallback((struct { + fn callback(window: glfw.Window, width: u32, height: u32) void { + const pl = window.getUserPointer(WindowData); + pl.?.target_desc.width = width; + pl.?.target_desc.height = height; + } + }).callback); + + while (!setup.window.shouldClose()) { + try frame(.{ + .window = setup.window, + .device = setup.device, + .pipeline = pipeline, + .queue = queue, + }); + std.time.sleep(16 * std.time.ns_per_ms); + } +} + +const WindowData = struct { + surface: ?c.WGPUSurface, + swap_chain: ?c.WGPUSwapChain, + swap_chain_format: c.WGPUTextureFormat, + current_desc: c.WGPUSwapChainDescriptor, + target_desc: c.WGPUSwapChainDescriptor, +}; + +const FrameParams = struct { + window: glfw.Window, + device: c.WGPUDevice, + pipeline: c.WGPURenderPipeline, + queue: c.WGPUQueue, +}; + +fn isDescriptorEqual(a: c.WGPUSwapChainDescriptor, b: c.WGPUSwapChainDescriptor) bool { + return a.usage == b.usage and a.format == b.format and a.width == b.width and a.height == b.height and a.presentMode == b.presentMode; +} + +fn frame(params: FrameParams) !void { + try glfw.pollEvents(); + const pl = params.window.getUserPointer(WindowData).?; + if (pl.swap_chain == null or !isDescriptorEqual(pl.current_desc, pl.target_desc)) { + const use_legacy_api = pl.surface == null; + if (!use_legacy_api) { + pl.swap_chain = c.wgpuDeviceCreateSwapChain(params.device, pl.surface.?, &pl.target_desc); + } else { + c.wgpuSwapChainConfigure( + pl.swap_chain.?, + pl.swap_chain_format, + c.WGPUTextureUsage_RenderAttachment, + @intCast(u32, pl.target_desc.width), + @intCast(u32, pl.target_desc.height), + ); + } + pl.current_desc = pl.target_desc; + } + + const back_buffer_view = c.wgpuSwapChainGetCurrentTextureView(pl.swap_chain.?); + var render_pass_info = std.mem.zeroes(c.WGPURenderPassDescriptor); + var color_attachment = std.mem.zeroes(c.WGPURenderPassColorAttachment); + color_attachment.view = back_buffer_view; + color_attachment.resolveTarget = null; + color_attachment.clearValue = c.WGPUColor{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 0.0 }; + color_attachment.loadOp = c.WGPULoadOp_Clear; + color_attachment.storeOp = c.WGPUStoreOp_Store; + render_pass_info.colorAttachmentCount = 1; + render_pass_info.colorAttachments = &color_attachment; + render_pass_info.depthStencilAttachment = null; + + const encoder = c.wgpuDeviceCreateCommandEncoder(params.device, null); + const pass = c.wgpuCommandEncoderBeginRenderPass(encoder, &render_pass_info); + c.wgpuRenderPassEncoderSetPipeline(pass, params.pipeline); + c.wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); + c.wgpuRenderPassEncoderEnd(pass); + c.wgpuRenderPassEncoderRelease(pass); + + const commands = c.wgpuCommandEncoderFinish(encoder, null); + c.wgpuCommandEncoderRelease(encoder); + + c.wgpuQueueSubmit(params.queue, 1, &commands); + c.wgpuCommandBufferRelease(commands); + c.wgpuSwapChainPresent(pl.swap_chain.?); + c.wgpuTextureViewRelease(back_buffer_view); +} diff --git a/gpu/examples/sample_utils.zig b/gpu/examples/sample_utils.zig new file mode 100644 index 00000000..d4e7e84a --- /dev/null +++ b/gpu/examples/sample_utils.zig @@ -0,0 +1,236 @@ +const std = @import("std"); +const assert = std.debug.assert; +const glfw = @import("glfw"); +const c = @import("c.zig").c; +const objc = @cImport({ + @cInclude("objc/message.h"); +}); + +fn printDeviceError(error_type: c.WGPUErrorType, message: [*c]const u8, _: ?*anyopaque) callconv(.C) void { + switch (error_type) { + c.WGPUErrorType_Validation => std.debug.print("dawn: validation error: {s}\n", .{message}), + c.WGPUErrorType_OutOfMemory => std.debug.print("dawn: out of memory: {s}\n", .{message}), + c.WGPUErrorType_Unknown => std.debug.print("dawn: unknown error: {s}\n", .{message}), + c.WGPUErrorType_DeviceLost => std.debug.print("dawn: device lost: {s}\n", .{message}), + else => unreachable, + } +} + +const Setup = struct { + instance: c.WGPUInstance, + backend_type: c.WGPUBackendType, + device: c.WGPUDevice, + window: glfw.Window, +}; + +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, + }; +} + +fn detectBackendType(allocator: std.mem.Allocator) !c.WGPUBackendType { + const WGPU_BACKEND = try getEnvVarOwned(allocator, "WGPU_BACKEND"); + if (WGPU_BACKEND) |backend| { + defer allocator.free(backend); + if (std.ascii.eqlIgnoreCase(backend, "opengl")) return c.WGPUBackendType_OpenGL; + if (std.ascii.eqlIgnoreCase(backend, "opengles")) return c.WGPUBackendType_OpenGLES; + if (std.ascii.eqlIgnoreCase(backend, "d3d11")) return c.WGPUBackendType_D3D11; + if (std.ascii.eqlIgnoreCase(backend, "d3d12")) return c.WGPUBackendType_D3D12; + if (std.ascii.eqlIgnoreCase(backend, "metal")) return c.WGPUBackendType_Metal; + if (std.ascii.eqlIgnoreCase(backend, "null")) return c.WGPUBackendType_Null; + if (std.ascii.eqlIgnoreCase(backend, "vulkan")) return c.WGPUBackendType_Vulkan; + @panic("unknown BACKEND type"); + } + + const target = @import("builtin").target; + if (target.isDarwin()) return c.WGPUBackendType_Metal; + if (target.os.tag == .windows) return c.WGPUBackendType_D3D12; + return c.WGPUBackendType_Vulkan; +} + +fn backendTypeString(t: c.WGPUBackendType) []const u8 { + return switch (t) { + c.WGPUBackendType_OpenGL => "OpenGL", + c.WGPUBackendType_OpenGLES => "OpenGLES", + c.WGPUBackendType_D3D11 => "D3D11", + c.WGPUBackendType_D3D12 => "D3D12", + c.WGPUBackendType_Metal => "Metal", + c.WGPUBackendType_Null => "Null", + c.WGPUBackendType_Vulkan => "Vulkan", + else => unreachable, + }; +} + +pub fn setup(allocator: std.mem.Allocator) !Setup { + const backend_type = try detectBackendType(allocator); + + try glfw.init(.{}); + + // Create the test window and discover adapters using it (esp. for OpenGL) + var hints = glfwWindowHintsForBackend(backend_type); + hints.cocoa_retina_framebuffer = false; + const window = try glfw.Window.create(640, 480, "Dawn window", null, null, hints); + + const instance = c.machDawnNativeInstance_init(); + try discoverAdapter(instance, window, backend_type); + + const adapters = c.machDawnNativeInstance_getAdapters(instance); + var backend_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); + if (c.machDawnNativeAdapterProperties_getBackendType(properties) == backend_type) { + const name = c.machDawnNativeAdapterProperties_getName(properties); + const driver_description = c.machDawnNativeAdapterProperties_getDriverDescription(properties); + std.debug.print("found {s} adapter: {s}, {s}\n", .{ backendTypeString(backend_type), name, driver_description }); + backend_adapter = adapter; + } + } + assert(backend_adapter != null); + + const backend_device = c.machDawnNativeAdapter_createDevice(backend_adapter.?, null); + const backend_procs = c.machDawnNativeGetProcs(); + + c.dawnProcSetProcs(backend_procs); + backend_procs.*.deviceSetUncapturedErrorCallback.?(backend_device, printDeviceError, null); + return Setup{ + .instance = c.machDawnNativeInstance_get(instance), + .backend_type = backend_type, + .device = backend_device, + .window = window, + }; +} + +fn glfwWindowHintsForBackend(backend: c.WGPUBackendType) glfw.Window.Hints { + return switch (backend) { + c.WGPUBackendType_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, + }, + c.WGPUBackendType_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, + }, + }; +} + +fn discoverAdapter(instance: c.MachDawnNativeInstance, window: glfw.Window, typ: c.WGPUBackendType) !void { + if (typ == c.WGPUBackendType_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, typ, &adapter_options); + } else if (typ == c.WGPUBackendType_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, 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( + instance: c.WGPUInstance, + window: glfw.Window, + comptime glfw_options: glfw.BackendOptions, +) c.WGPUSurface { + const glfw_native = glfw.Native(glfw_options); + if (glfw_options.win32) { + var desc: c.WGPUSurfaceDescriptorFromWindowsHWND = undefined; + desc.chain.next = null; + desc.chain.sType = c.WGPUSType_SurfaceDescriptorFromWindowsHWND; + + desc.hinstance = std.os.windows.kernel32.GetModuleHandleW(null); + desc.hwnd = glfw_native.getWin32Window(window); + + var descriptor: c.WGPUSurfaceDescriptor = undefined; + descriptor.nextInChain = @ptrCast(*c.WGPUChainedStruct, &desc); + descriptor.label = "basic surface"; + return c.wgpuInstanceCreateSurface(instance, &descriptor); + } else if (glfw_options.x11) { + var desc: c.WGPUSurfaceDescriptorFromXlibWindow = undefined; + desc.chain.next = null; + desc.chain.sType = c.WGPUSType_SurfaceDescriptorFromXlibWindow; + + desc.display = glfw_native.getX11Display(); + desc.window = glfw_native.getX11Window(window); + + var descriptor: c.WGPUSurfaceDescriptor = undefined; + descriptor.nextInChain = @ptrCast(*c.WGPUChainedStruct, &desc); + descriptor.label = "basic surface"; + return c.wgpuInstanceCreateSurface(instance, &descriptor); + } else if (glfw_options.cocoa) { + var desc: c.WGPUSurfaceDescriptorFromMetalLayer = undefined; + desc.chain.next = null; + desc.chain.sType = c.WGPUSType_SurfaceDescriptorFromMetalLayer; + + 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] + + desc.layer = layer.?; + + var descriptor: c.WGPUSurfaceDescriptor = undefined; + descriptor.nextInChain = @ptrCast(*c.WGPUChainedStruct, &desc); + descriptor.label = "basic surface"; + return c.wgpuInstanceCreateSurface(instance, &descriptor); + } 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; +} + +// Borrowed from https://github.com/hazeycode/zig-objcrt +pub 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); +} diff --git a/gpu/libs/mach-glfw b/gpu/libs/mach-glfw new file mode 120000 index 00000000..0d47f286 --- /dev/null +++ b/gpu/libs/mach-glfw @@ -0,0 +1 @@ +../../glfw \ No newline at end of file diff --git a/gpu/libs/mach-gpu-dawn b/gpu/libs/mach-gpu-dawn new file mode 120000 index 00000000..c033c781 --- /dev/null +++ b/gpu/libs/mach-gpu-dawn @@ -0,0 +1 @@ +../../gpu-dawn \ No newline at end of file