diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 0d05b7983..73e0bef01 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -11,6 +11,7 @@ const objc = @import("objc"); const macos = @import("macos"); const imgui = @import("imgui"); const glslang = @import("glslang"); +const xev = @import("xev"); const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); const font = @import("../font/main.zig"); @@ -42,6 +43,11 @@ const InstanceBuffer = mtl_buffer.Buffer(u16); const ImagePlacementList = std.ArrayListUnmanaged(mtl_image.Placement); +const DisplayLink = switch (builtin.os.tag) { + .macos => *macos.video.DisplayLink, + else => void, +}; + // Get native API access on certain platforms so we can do more customization. const glfwNative = glfw.Native(.{ .cocoa = builtin.os.tag == .macos, @@ -115,6 +121,11 @@ shaders: Shaders, // Compiled shaders /// Metal objects layer: objc.Object, // CAMetalLayer +/// The CVDisplayLink used to drive the rendering loop in sync +/// with the display. This is void on platforms that don't support +/// a display link. +display_link: ?DisplayLink = null, + /// Custom shader state. This is only set if we have custom shaders. custom_shader_state: ?CustomShaderState = null, @@ -545,9 +556,15 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }; }; - const cells = try mtl_cell.Contents.init(alloc); + var cells = try mtl_cell.Contents.init(alloc); errdefer cells.deinit(alloc); + const display_link: ?DisplayLink = switch (builtin.os.tag) { + .macos => try macos.video.DisplayLink.createWithActiveCGDisplays(), + else => null, + }; + errdefer if (display_link) |v| v.release(); + return Metal{ .alloc = alloc, .config = options.config, @@ -581,6 +598,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { // Metal stuff .layer = layer, + .display_link = display_link, .custom_shader_state = custom_shader_state, .gpu_state = gpu_state, }; @@ -589,6 +607,13 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { pub fn deinit(self: *Metal) void { self.gpu_state.deinit(); + if (DisplayLink != void) { + if (self.display_link) |display_link| { + display_link.stop() catch {}; + display_link.release(); + } + } + self.cells.deinit(self.alloc); self.font_shaper.deinit(); @@ -635,6 +660,45 @@ pub fn threadExit(self: *const Metal) void { // Metal requires no per-thread state. } +/// Called by renderer.Thread when it starts the main loop. +pub fn loopEnter(self: *Metal, thr: *renderer.Thread) !void { + // If we don't support a display link we have no work to do. + if (comptime DisplayLink == void) return; + + // This is when we know our "self" pointer is stable so we can + // setup the display link. To setup the display link we set our + // callback and we can start it immediately. + const display_link = self.display_link orelse return; + try display_link.setOutputCallback( + xev.Async, + &displayLinkCallback, + &thr.draw_now, + ); + display_link.start() catch {}; +} + +/// Called by renderer.Thread when it exits the main loop. +pub fn loopExit(self: *const Metal) void { + // If we don't support a display link we have no work to do. + if (comptime DisplayLink == void) return; + + // Stop our display link. If this fails its okay it just means + // that we either never started it or the view its attached to + // is gone which is fine. + const display_link = self.display_link orelse return; + display_link.stop() catch {}; +} + +fn displayLinkCallback( + _: *macos.video.DisplayLink, + ud: ?*xev.Async, +) void { + const draw_now = ud orelse return; + draw_now.notify() catch |err| { + log.err("error notifying draw_now err={}", .{err}); + }; +} + /// True if our renderer has animations so that a higher frequency /// timer is used. pub fn hasAnimations(self: *const Metal) bool { diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index c4342ef4c..ce7fc4d7c 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -146,7 +146,7 @@ pub fn init( var mailbox = try Mailbox.create(alloc); errdefer mailbox.destroy(alloc); - return Thread{ + return .{ .alloc = alloc, .config = DerivedConfig.init(config), .loop = loop, @@ -191,6 +191,11 @@ pub fn threadMain(self: *Thread) void { fn threadMain_(self: *Thread) !void { defer log.debug("renderer thread exited", .{}); + // Run our loop start/end callbacks if the renderer cares. + const has_loop = @hasDecl(renderer.Renderer, "loopEnter"); + if (has_loop) try self.renderer.loopEnter(self); + defer if (has_loop) self.renderer.loopExit(); + // Run our thread start/end callbacks. This is important because some // renderers have to do per-thread setup. For example, OpenGL has to set // some thread-local state since that is how it works.