diff --git a/build.zig b/build.zig index 882b04a79..4c77fb92a 100644 --- a/build.zig +++ b/build.zig @@ -223,7 +223,10 @@ fn addDeps( _ = try utf8proc.link(b, step); // Glfw - const glfw_opts: glfw.Options = .{ .metal = false, .opengl = false }; + const glfw_opts: glfw.Options = .{ + .metal = step.target.isDarwin(), + .opengl = false, + }; try glfw.link(b, step, glfw_opts); // Imgui, we have to do this later since we need some information diff --git a/pkg/objc/object.zig b/pkg/objc/object.zig index 11f152b67..b69b0ee22 100644 --- a/pkg/objc/object.zig +++ b/pkg/objc/object.zig @@ -54,7 +54,7 @@ pub const Object = struct { break :getter objc.sel(val); } else objc.sel(n); - self.msgSend(T, getter, .{}); + return self.msgSend(T, getter, .{}); } }; diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 53765bb21..6b5f2ea7d 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -2,12 +2,19 @@ pub const Metal = @This(); const std = @import("std"); +const builtin = @import("builtin"); const glfw = @import("glfw"); +const objc = @import("objc"); const font = @import("../font/main.zig"); const terminal = @import("../terminal/main.zig"); const renderer = @import("../renderer.zig"); const Allocator = std.mem.Allocator; +// Get native API access on certain platforms so we can do more customization. +const glfwNative = glfw.Native(.{ + .cocoa = builtin.os.tag == .macos, +}); + const log = std.log.scoped(.metal); /// Current cell dimensions for this grid. @@ -19,6 +26,11 @@ foreground: terminal.color.RGB, /// Default background color background: terminal.color.RGB, +/// Metal objects +device: objc.Object, // MTLDevice +queue: objc.Object, // MTLCommandQueue +swapchain: objc.Object, // CAMetalLayer + /// Returns the hints that we want for this pub fn windowHints() glfw.Window.Hints { return .{ @@ -32,9 +44,23 @@ pub fn windowHints() glfw.Window.Hints { /// window surface as necessary. pub fn windowInit(window: glfw.Window) !void { _ = window; + + // We don't do anything else here because we want to set everything + // else up during actual initialization. } pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { + // Initialize our metal stuff + const device = objc.Object.fromId(MTLCreateSystemDefaultDevice()); + const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); + const swapchain = swapchain: { + const CAMetalLayer = objc.Class.getClass("CAMetalLayer").?; + const swapchain = CAMetalLayer.msgSend(objc.Object, objc.sel("layer"), .{}); + swapchain.setProperty("device", device.value); + swapchain.setProperty("opaque", true); + break :swapchain swapchain; + }; + // Get our cell metrics based on a regular font ascii 'M'. Why 'M'? // Doesn't matter, any normal ASCII will do we're just trying to make // sure we use the regular font. @@ -49,6 +75,9 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { .cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }, .background = .{ .r = 0, .g = 0, .b = 0 }, .foreground = .{ .r = 255, .g = 255, .b = 255 }, + .device = device, + .queue = queue, + .swapchain = swapchain, }; } @@ -59,19 +88,26 @@ pub fn deinit(self: *Metal) void { /// This is called just prior to spinning up the renderer thread for /// final main thread setup requirements. pub fn finalizeInit(self: *const Metal, window: glfw.Window) !void { - _ = self; - _ = window; + // Set our window backing layer to be our swapchain + const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(window).?); + const contentView = objc.Object.fromId(nswindow.getProperty(?*anyopaque, "contentView").?); + contentView.setProperty("layer", self.swapchain.value); + contentView.setProperty("wantsLayer", true); } /// Callback called by renderer.Thread when it begins. pub fn threadEnter(self: *const Metal, window: glfw.Window) !void { _ = self; _ = window; + + // Metal requires no per-thread state. } /// Callback called by renderer.Thread when it exits. pub fn threadExit(self: *const Metal) void { _ = self; + + // Metal requires no per-thread state. } /// The primary render callback that is completely thread-safe. @@ -80,7 +116,109 @@ pub fn render( window: glfw.Window, state: *renderer.State, ) !void { - _ = self; _ = window; - _ = state; + + // Data we extract out of the critical area. + const Critical = struct { + bg: terminal.color.RGB, + }; + + // Update all our data as tightly as possible within the mutex. + const critical: Critical = critical: { + state.mutex.lock(); + defer state.mutex.unlock(); + + // Swap bg/fg if the terminal is reversed + const bg = self.background; + const fg = self.foreground; + defer { + self.background = bg; + self.foreground = fg; + } + if (state.terminal.modes.reverse_colors) { + self.background = fg; + self.foreground = bg; + } + + break :critical .{ + .bg = self.background, + }; + }; + + // @autoreleasepool {} + const pool = objc_autoreleasePoolPush(); + defer objc_autoreleasePoolPop(pool); + + // Get our surface (CAMetalDrawable) + const surface = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); + + // MTLRenderPassDescriptor + const MTLRenderPassDescriptor = objc.Class.getClass("MTLRenderPassDescriptor").?; + const desc = desc: { + const desc = MTLRenderPassDescriptor.msgSend( + objc.Object, + objc.sel("renderPassDescriptor"), + .{}, + ); + + // Set our color attachment to be our drawable surface. + const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); + { + const attachment = attachments.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 0)}, + ); + + attachment.setProperty("loadAction", @enumToInt(MTLLoadAction.clear)); + attachment.setProperty("storeAction", @enumToInt(MTLStoreAction.store)); + attachment.setProperty("texture", surface.getProperty(objc.c.id, "texture").?); + attachment.setProperty("clearColor", MTLClearColor{ + .red = @intToFloat(f32, critical.bg.r) / 255, + .green = @intToFloat(f32, critical.bg.g) / 255, + .blue = @intToFloat(f32, critical.bg.b) / 255, + .alpha = 1.0, + }); + } + + break :desc desc; + }; + + // Command buffer (MTLCommandBuffer) + const buffer = self.queue.msgSend(objc.Object, objc.sel("commandBuffer"), .{}); + + // MTLRenderCommandEncoder + const encoder = buffer.msgSend( + objc.Object, + objc.sel("renderCommandEncoderWithDescriptor:"), + .{desc.value}, + ); + encoder.msgSend(void, objc.sel("endEncoding"), .{}); + + buffer.msgSend(void, objc.sel("presentDrawable:"), .{surface.value}); + buffer.msgSend(void, objc.sel("commit"), .{}); } + +/// https://developer.apple.com/documentation/metal/mtlloadaction?language=objc +const MTLLoadAction = enum(c_ulong) { + dont_care = 0, + load = 1, + clear = 2, +}; + +/// https://developer.apple.com/documentation/metal/mtlstoreaction?language=objc +const MTLStoreAction = enum(c_ulong) { + dont_care = 0, + store = 1, +}; + +const MTLClearColor = extern struct { + red: f64, + green: f64, + blue: f64, + alpha: f64, +}; + +extern "c" fn MTLCreateSystemDefaultDevice() ?*anyopaque; +extern "c" fn objc_autoreleasePoolPush() ?*anyopaque; +extern "c" fn objc_autoreleasePoolPop(?*anyopaque) void;