From 66e56f037b1479f708888fd16c3ff02952ceb7c4 Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Sun, 18 Aug 2024 16:45:37 -0700 Subject: [PATCH] initial macOS backend (#1249) Signed-off-by: Stephen Gutekanst Co-authored-by: Michael Bradshaw --- build.zig | 35 ++++-- build.zig.zon | 4 +- src/Core.zig | 3 +- src/core/darwin.zig | 254 +++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 30 +++-- src/sysgpu/metal.zig | 7 +- 6 files changed, 308 insertions(+), 25 deletions(-) create mode 100644 src/core/darwin.zig diff --git a/build.zig b/build.zig index b6dcd674..cc50cacd 100644 --- a/build.zig +++ b/build.zig @@ -136,6 +136,12 @@ pub fn build(b: *std.Build) !void { module.linkLibrary(dep.artifact("wayland-headers")); lib.linkLibrary(dep.artifact("wayland-headers")); } + if (target.result.isDarwin()) { + if (b.lazyDependency("mach_objc", .{ + .target = target, + .optimize = optimize, + })) |dep| module.addImport("objc", dep.module("mach-objc")); + } } } if (want_sysaudio) { @@ -167,10 +173,12 @@ pub fn build(b: *std.Build) !void { example_run_step.dependOn(&example_run_cmd.step); } } - if (b.lazyDependency("mach_objc", .{ - .target = target, - .optimize = optimize, - })) |dep| module.addImport("objc", dep.module("mach-objc")); + if (target.result.isDarwin()) { + if (b.lazyDependency("mach_objc", .{ + .target = target, + .optimize = optimize, + })) |dep| module.addImport("objc", dep.module("mach-objc")); + } } if (target.result.isDarwin()) { @@ -222,10 +230,13 @@ pub fn build(b: *std.Build) !void { } if (want_sysgpu) { if (b.lazyDependency("vulkan_zig_generated", .{})) |dep| module.addImport("vulkan", dep.module("vulkan-zig-generated")); - if (b.lazyDependency("mach_objc", .{ - .target = target, - .optimize = optimize, - })) |dep| module.addImport("objc", dep.module("mach-objc")); + if (target.result.isDarwin()) { + if (b.lazyDependency("mach_objc", .{ + .target = target, + .optimize = optimize, + })) |dep| module.addImport("objc", dep.module("mach-objc")); + } + linkSysgpu(b, module); if (want_libs) { @@ -277,10 +288,12 @@ pub const Platform = enum { wayland, web, win32, + darwin, null, pub fn fromTarget(target: std.Target) Platform { if (target.cpu.arch == .wasm32) return .web; + if (target.os.tag.isDarwin()) return .darwin; if (target.os.tag == .windows) return .win32; return .x11; } @@ -292,7 +305,11 @@ fn linkSysgpu(b: *std.Build, module: *std.Build.Module) void { if (target.cpu.arch != .wasm32) module.link_libc = true; if (target.isDarwin()) { module.linkSystemLibrary("objc", .{}); - module.linkFramework("AppKit", .{}); + if (target.os.tag == .macos) { + module.linkFramework("AppKit", .{}); + } else { + module.linkFramework("UIKit", .{}); + } module.linkFramework("CoreGraphics", .{}); module.linkFramework("Foundation", .{}); module.linkFramework("Metal", .{}); diff --git a/build.zig.zon b/build.zig.zon index 10e4bc5b..62d25762 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -22,8 +22,8 @@ .lazy = true, }, .mach_objc = .{ - .url = "https://pkg.machengine.org/mach-objc/055619a732ae50ba24eb04399613be87f8432f0f.tar.gz", - .hash = "1220211475420584a552b461dd569f891d29cb8e9e485b97053be604680c8d6e2a3e", + .url = "https://pkg.machengine.org/mach-objc/9f4635396dc8805247bab86c1decb7531b5f0309.tar.gz", + .hash = "1220b2997b164f3a6c0be62264c79c7feae2ca005d7f5c405315def5a9af887be5ba", .lazy = true, }, .xcode_frameworks = .{ diff --git a/src/Core.zig b/src/Core.zig index 11c06484..8c538cf4 100644 --- a/src/Core.zig +++ b/src/Core.zig @@ -12,11 +12,12 @@ pub const sysjs = @import("mach-sysjs"); pub const Timer = @import("core/Timer.zig"); const Frequency = @import("core/Frequency.zig"); -const Platform = switch (build_options.core_platform) { +pub const Platform = switch (build_options.core_platform) { .x11 => @import("core/X11.zig"), .wayland => @import("core/Wayland.zig"), .web => @panic("TODO: revive wasm backend"), .win32 => @import("core/win32.zig"), + .darwin => @import("core/Darwin.zig"), .null => @import("core/Null.zig"), }; diff --git a/src/core/darwin.zig b/src/core/darwin.zig new file mode 100644 index 00000000..9f8e4901 --- /dev/null +++ b/src/core/darwin.zig @@ -0,0 +1,254 @@ +const std = @import("std"); +const mach = @import("../main.zig"); +const Core = @import("../Core.zig"); +const InputState = @import("InputState.zig"); +const Frequency = @import("Frequency.zig"); +const unicode = @import("unicode.zig"); +const detectBackendType = @import("common.zig").detectBackendType; +const gpu = mach.gpu; +const InitOptions = Core.InitOptions; +const Event = Core.Event; +const KeyEvent = Core.KeyEvent; +const MouseButtonEvent = Core.MouseButtonEvent; +const MouseButton = Core.MouseButton; +const Size = Core.Size; +const DisplayMode = Core.DisplayMode; +const CursorShape = Core.CursorShape; +const VSyncMode = Core.VSyncMode; +const CursorMode = Core.CursorMode; +const Position = Core.Position; +const Key = Core.Key; +const KeyMods = Core.KeyMods; +const Joystick = Core.Joystick; +const objc = @import("objc"); + +const log = std.log.scoped(.mach); + +const EventQueue = std.fifo.LinearFifo(Event, .Dynamic); +pub const EventIterator = struct { + queue: *EventQueue, + + pub inline fn next(self: *EventIterator) ?Event { + return self.queue.readItem(); + } +}; + +pub const Darwin = @This(); + +allocator: std.mem.Allocator, +core: *Core, + +events: EventQueue, +input_state: InputState, +// modifiers: KeyMods, + +title: [:0]const u8, +display_mode: DisplayMode, +vsync_mode: VSyncMode, +cursor_mode: CursorMode, +cursor_shape: CursorShape, +border: bool, +headless: bool, +refresh_rate: u32, +size: Size, +surface_descriptor: gpu.Surface.Descriptor, +window: ?*objc.app_kit.ns.Window, + +pub fn run(comptime on_each_update_fn: anytype, args_tuple: std.meta.ArgsTuple(@TypeOf(on_each_update_fn))) noreturn { + objc.avf_audio.avaudio.init(); + objc.foundation.ns.init(); + objc.metal.mtl.init(); + objc.quartz_core.ca.init(); + objc.app_kit.ns.init(); + + const Args = @TypeOf(args_tuple); + const args_bytes = std.mem.asBytes(&args_tuple); + const ArgsBytes = @TypeOf(args_bytes.*); + const Helper = struct { + // TODO: port libdispatch and use it instead of doing this directly. + extern "System" fn dispatch_async(queue: *anyopaque, block: *objc.foundation.ns.Block(fn () void)) void; + extern "System" var _dispatch_main_q: anyopaque; + pub fn cCallback(block: *objc.foundation.ns.BlockLiteral(ArgsBytes)) callconv(.C) void { + const args: *Args = @ptrCast(&block.context); + if (@call(.auto, on_each_update_fn, args.*) catch false) { + dispatch_async(&_dispatch_main_q, block.asBlockWithSignature(fn () void)); + } else { + // We copied the block when we called `setRunBlock()`, so we release it here when the looping will end. + block.release(); + } + } + }; + var block_literal = objc.foundation.ns.stackBlockLiteral(Helper.cCallback, args_bytes.*, null, null); + + // `NSApplicationMain()` and `UIApplicationMain()` never return, so there's no point in trying to add any kind of cleanup work here. + const ns_app = objc.app_kit.ns.Application.sharedApplication(); + const delegate = objc.mach.AppDelegate.allocInit(); + delegate.setRunBlock(block_literal.asBlock().copy()); + ns_app.setDelegate(@ptrCast(delegate)); + _ = objc.app_kit.ns.applicationMain(0, undefined); + + unreachable; + // TODO: support UIKit. +} + +// Called on the main thread +pub fn init(darwin: *Darwin, options: InitOptions) !void { + var surface_descriptor = gpu.Surface.Descriptor{}; + + // TODO: support UIKit. + var window: ?*objc.appkit.ns.Window = null; + if (!options.headless) { + const metal_descriptor = try options.allocator.create(gpu.Surface.DescriptorFromMetalLayer); + const layer = objc.quartz_core.ca.MetalLayer.new(); + defer layer.release(); + metal_descriptor.* = .{ + .layer = layer, + }; + surface_descriptor.next_in_chain = .{ .from_metal_layer = metal_descriptor }; + + const screen = objc.appkit.ns.Screen.mainScreen(); + const rect = objc.core_graphics.cg.Rect{ // TODO: use a meaningful rect + .origin = .{ .x = 100, .y = 100 }, + .size = .{ .width = 480, .height = 270 }, + }; + const window_style = + (if (options.display_mode == .fullscreen) objc.appkit.ns.WindowStyleMaskFullScreen else 0) | + (if (options.display_mode == .windowed) objc.appkit.ns.WindowStyleMaskTitled else 0) | + (if (options.display_mode == .windowed) objc.appkit.ns.WindowStyleMaskClosable else 0) | + (if (options.display_mode == .windowed) objc.appkit.ns.WindowStyleMaskMiniaturizable else 0) | + (if (options.display_mode == .windowed) objc.appkit.ns.WindowStyleMaskResizable else 0); + window = objc.appkit.ns.Window.alloc().initWithContentRect_styleMask_backing_defer_screen(rect, window_style, objc.appkit.ns.BackingStoreBuffered, true, screen); + window.?.setReleasedWhenClosed(false); + if (window.?.contentView()) |view| { + view.setLayer(@ptrCast(layer)); + } + window.?.setIsVisible(true); + window.?.makeKeyAndOrderFront(null); + } + + var events = EventQueue.init(options.allocator); + try events.ensureTotalCapacity(2048); + + darwin.* = .{ + .allocator = options.allocator, + .core = @fieldParentPtr("platform", darwin), + .events = events, + .input_state = .{}, + .title = options.title, + .display_mode = options.display_mode, + .vsync_mode = .none, + .cursor_mode = .normal, + .cursor_shape = .arrow, + .border = options.border, + .headless = options.headless, + .refresh_rate = 60, // TODO: set to something meaningful + .size = options.size, + .surface_descriptor = surface_descriptor, + .window = window, + }; +} + +pub fn deinit(darwin: *Darwin) void { + if (darwin.window) |w| @as(*objc.foundation.ns.ObjectProtocol, @ptrCast(w)).release(); + return; +} + +// Called on the main thread +pub fn update(_: *Darwin) !void { + return; +} + +// May be called from any thread. +pub inline fn pollEvents(n: *Darwin) EventIterator { + return EventIterator{ .queue = &n.events }; +} + +// May be called from any thread. +pub fn setTitle(_: *Darwin, _: [:0]const u8) void { + return; +} + +// May be called from any thread. +pub fn setDisplayMode(_: *Darwin, _: DisplayMode) void { + return; +} + +// May be called from any thread. +pub fn setBorder(_: *Darwin, _: bool) void { + return; +} + +// May be called from any thread. +pub fn setHeadless(_: *Darwin, _: bool) void { + return; +} + +// May be called from any thread. +pub fn setVSync(_: *Darwin, _: VSyncMode) void { + return; +} + +// May be called from any thread. +pub fn setSize(_: *Darwin, _: Size) void { + return; +} + +// May be called from any thread. +pub fn size(_: *Darwin) Size { + return Size{ .width = 100, .height = 100 }; +} + +// May be called from any thread. +pub fn setCursorMode(_: *Darwin, _: CursorMode) void { + return; +} + +// May be called from any thread. +pub fn setCursorShape(_: *Darwin, _: CursorShape) void { + return; +} + +// May be called from any thread. +pub fn joystickPresent(_: *Darwin, _: Joystick) bool { + return false; +} + +// May be called from any thread. +pub fn joystickName(_: *Darwin, _: Joystick) ?[:0]const u8 { + return null; +} + +// May be called from any thread. +pub fn joystickButtons(_: *Darwin, _: Joystick) ?[]const bool { + return null; +} + +// May be called from any thread. +pub fn joystickAxes(_: *Darwin, _: Joystick) ?[]const f32 { + return null; +} + +// May be called from any thread. +pub fn keyPressed(_: *Darwin, _: Key) bool { + return false; +} + +// May be called from any thread. +pub fn keyReleased(_: *Darwin, _: Key) bool { + return true; +} + +// May be called from any thread. +pub fn mousePressed(_: *Darwin, _: MouseButton) bool { + return false; +} + +// May be called from any thread. +pub fn mouseReleased(_: *Darwin, _: MouseButton) bool { + return true; +} + +// May be called from any thread. +pub fn mousePosition(_: *Darwin) Position { + return Position{ .x = 0, .y = 0 }; +} diff --git a/src/main.zig b/src/main.zig index 9898f21a..393e7a9f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -72,18 +72,28 @@ pub const App = struct { app.mods.schedule(app.main_mod, .init); // Main loop - while (!app.mods.mod.mach_core.state().should_close) { - // Dispatch events until queue is empty - try app.mods.dispatch(&stack_space, .{}); - // Run `update` when `init` and all other systems are exectued - app.mods.schedule(app.main_mod, .update); + if (comptime builtin.target.isDarwin()) { + Core.Platform.run(on_each_update, .{app, &stack_space}); + } else { + while (try app.on_each_update(&stack_space)) {} + } + } + + fn on_each_update(app: *App, stack_space: []u8) !bool { + if (app.mods.mod.mach_core.state().should_close) { + // Final Dispatch to deinitalize resources + app.mods.schedule(app.main_mod, .deinit); + try app.mods.dispatch(stack_space, .{}); + app.mods.schedule(.mach_core, .deinit); + try app.mods.dispatch(stack_space, .{}); + return false; } - // Final Dispatch to deinitalize resources - app.mods.schedule(app.main_mod, .deinit); - try app.mods.dispatch(&stack_space, .{}); - app.mods.schedule(.mach_core, .deinit); - try app.mods.dispatch(&stack_space, .{}); + // Dispatch events until queue is empty + try app.mods.dispatch(stack_space, .{}); + // Run `update` when `init` and all other systems are executed + app.mods.schedule(app.main_mod, .update); + return true; } }; diff --git a/src/sysgpu/metal.zig b/src/sysgpu/metal.zig index d3033aa3..b5a4b0a1 100644 --- a/src/sysgpu/metal.zig +++ b/src/sysgpu/metal.zig @@ -2214,13 +2214,14 @@ pub const Queue = struct { .queue = queue, .fence_value = queue.fence_value, }; - mtl_command_buffer.addCompletedHandler(ctx, completedHandler); + var handler = ns.stackBlockLiteral(completedHandler, ctx, null, null); + mtl_command_buffer.addCompletedHandler(handler.asBlock()); mtl_command_buffer.commit(); } - fn completedHandler(ctx: CompletedContext, mtl_command_buffer: *mtl.CommandBuffer) void { + fn completedHandler(block: *ns.BlockLiteral(CompletedContext), mtl_command_buffer: *mtl.CommandBuffer) callconv(.C) void { _ = mtl_command_buffer; - ctx.queue.completed_value.store(ctx.fence_value, .release); + block.context.queue.completed_value.store(block.context.fence_value, .release); } };