diff --git a/include/ghostty.h b/include/ghostty.h index 270dcff6e..22d42449c 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -542,6 +542,10 @@ uintptr_t ghostty_surface_pwd(ghostty_surface_t, char*, uintptr_t); bool ghostty_surface_has_selection(ghostty_surface_t); uintptr_t ghostty_surface_selection(ghostty_surface_t, char*, uintptr_t); +#ifdef __APPLE__ +void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t); +#endif + ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t); void ghostty_inspector_free(ghostty_surface_t); void ghostty_inspector_set_focus(ghostty_inspector_t, bool); diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 60f930c8a..6f3c4a357 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -112,6 +112,11 @@ extension Ghostty { selector: #selector(onUpdateRendererHealth), name: Ghostty.Notification.didUpdateRendererHealth, object: self) + center.addObserver( + self, + selector: #selector(windowDidChangeScreen), + name: NSWindow.didChangeScreenNotification, + object: nil) // Setup our surface. This will also initialize all the terminal IO. let surface_cfg = baseConfig ?? SurfaceConfiguration() @@ -322,6 +327,19 @@ extension Ghostty { healthy = health == GHOSTTY_RENDERER_HEALTH_OK } + @objc private func windowDidChangeScreen(notification: SwiftUI.Notification) { + guard let window = self.window else { return } + guard let object = notification.object as? NSWindow, window == object else { return } + guard let screen = window.screen else { return } + guard let surface = self.surface else { return } + + // When the window changes screens, we need to update libghostty with the screen + // ID. If vsync is enabled, this will be used with the CVDisplayLink to ensure + // the proper refresh rate is going. + let id = (screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as! NSNumber).uint32Value + ghostty_surface_set_display_id(surface, id) + } + // MARK: - NSView override func viewDidMoveToWindow() { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index a90934b1c..341de5eb7 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1737,6 +1737,15 @@ pub const CAPI = struct { // Inspector Metal APIs are only available on Apple systems usingnamespace if (builtin.target.isDarwin()) struct { + export fn ghostty_surface_set_display_id(ptr: *Surface, display_id: u32) void { + const surface = &ptr.core_surface; + _ = surface.renderer_thread.mailbox.push( + .{ .macos_display_id = display_id }, + .{ .forever = {} }, + ); + surface.renderer_thread.wakeup.notify() catch {}; + } + export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool { return ptr.initMetal(objc.Object.fromId(device)); } diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 72a0ab19c..36bd334d7 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -561,10 +561,14 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { 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, - }; + const display_link: ?DisplayLink = null; + // Note(mitchellh): if/when we ever want to add vsync, we can use this + // display link to trigger rendering. We don't need this if vsync is off + // because any change will trigger a redraw immediately. + // 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{ @@ -701,6 +705,16 @@ fn displayLinkCallback( }; } +/// Called when we get an updated display ID for our display link. +pub fn setMacOSDisplayID(self: *Metal, id: u32) !void { + if (comptime DisplayLink == void) return; + const display_link = self.display_link orelse return; + log.info("updating display link display id={}", .{id}); + display_link.setCurrentCGDisplay(id) catch |err| { + log.warn("error setting display link display id 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 2e4b03265..9345d8652 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -375,6 +375,12 @@ fn drainMailbox(self: *Thread) !void { }, .inspector => |v| self.flags.has_inspector = v, + + .macos_display_id => |v| { + if (@hasDecl(renderer.Renderer, "setMacOSDisplayID")) { + try self.renderer.setMacOSDisplayID(v); + } + }, } } } diff --git a/src/renderer/message.zig b/src/renderer/message.zig index c15854266..c2444b3c9 100644 --- a/src/renderer/message.zig +++ b/src/renderer/message.zig @@ -69,6 +69,9 @@ pub const Message = union(enum) { /// Activate or deactivate the inspector. inspector: bool, + /// The macOS display ID has changed for the window. + macos_display_id: u32, + /// Initialize a change_config message. pub fn initChangeConfig(alloc: Allocator, config: *const configpkg.Config) !Message { const thread_ptr = try alloc.create(renderer.Thread.DerivedConfig);