macos: tolerate display link creation failures

Fixes #13293

Treat Core Video display link creation as optional when macOS has no
active displays. The previous error path reported every creation
failure as out of memory and aborted renderer initialization.

This also resyncs the display link on any display change so when
a display becomes available it re-adds itself.

Tabs created while the session is locked now initialize normally and
fall back to event-driven rendering without vsync.
This commit is contained in:
Mitchell Hashimoto
2026-08-05 11:31:16 -07:00
parent 54fe8e1885
commit a177ba90af
3 changed files with 68 additions and 53 deletions

View File

@@ -1,21 +1,19 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const c = @import("c.zig").c;
pub const DisplayLink = opaque {
pub const Error = error{
CreationFailed,
InvalidOperation,
};
pub fn createWithActiveCGDisplays() Allocator.Error!*DisplayLink {
pub fn createWithActiveCGDisplays() Error!*DisplayLink {
var result: ?*DisplayLink = null;
if (c.CVDisplayLinkCreateWithActiveCGDisplays(
@ptrCast(&result),
) != c.kCVReturnSuccess)
return error.OutOfMemory;
return error.CreationFailed;
return result orelse error.OutOfMemory;
return result orelse error.CreationFailed;
}
pub fn release(self: *DisplayLink) void {

View File

@@ -501,7 +501,7 @@ fn drainMailbox(self: *Thread) !void {
.macos_display_id => |v| {
if (@hasDecl(rendererpkg.Renderer, "setMacOSDisplayID")) {
try self.renderer.setMacOSDisplayID(v);
try self.renderer.setMacOSDisplayID(v, &self.draw_now);
}
},
}

View File

@@ -1,6 +1,7 @@
const std = @import("std");
const builtin = @import("builtin");
const xev = @import("xev");
const global = @import("../global.zig");
const xev = global.xev;
const wuffs = @import("wuffs");
const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig");
@@ -27,7 +28,6 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const Terminal = terminal.Terminal;
const Health = renderer.Health;
const compat_file = @import("../lib/compat/file.zig");
const global = @import("../global.zig");
const getConstraint = @import("../font/nerd_font_attributes.zig").getConstraint;
@@ -117,6 +117,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// True if the window is focused
focused: bool,
/// True if the window is visible.
visible: bool,
/// Flag to indicate that our focus state changed for custom
/// shaders to update their state.
custom_shader_focused_changed: bool = false,
@@ -693,15 +696,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
};
};
const display_link: ?DisplayLink = switch (builtin.os.tag) {
.macos => if (options.config.vsync)
try macos.video.DisplayLink.createWithActiveCGDisplays()
else
null,
else => null,
};
errdefer if (display_link) |v| v.release();
var result: Self = .{
.alloc = alloc,
.config = options.config,
@@ -709,6 +703,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.grid_metrics = font_critical.metrics,
.size = options.size,
.focused = true,
.visible = true,
.scrollbar = .zero,
.scrollbar_dirty = false,
.last_bottom_node = null,
@@ -787,7 +782,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Graphics API stuff
.api = api,
.swap_chain = swap_chain,
.display_link = display_link,
};
try result.initShaders();
@@ -907,16 +901,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// 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 {};
self.syncDisplayLink(null, &thr.draw_now);
}
/// Called by renderer.Thread when it exits the main loop.
@@ -1003,13 +988,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
/// Called when we get an updated display ID for our display link.
pub fn setMacOSDisplayID(self: *Self, id: u32) !void {
pub fn setMacOSDisplayID(
self: *Self,
id: u32,
draw_now: *xev.Async,
) !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});
};
self.syncDisplayLink(id, draw_now);
}
/// True if our renderer has animations so that a higher frequency
@@ -1038,33 +1023,65 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Flag that we need to update our custom shaders
self.custom_shader_focused_changed = true;
// If we're not focused, then we want to stop the display link
// because it is a waste of resources and we can move to pure
// change-driven updates.
if (comptime DisplayLink != void) link: {
const display_link = self.display_link orelse break :link;
if (focus) {
display_link.start() catch {};
} else {
display_link.stop() catch {};
}
}
self.syncDisplayLink(null, null);
}
/// Callback when the window is visible or occluded.
///
/// Must be called on the render thread.
pub fn setVisible(self: *Self, visible: bool) void {
self.visible = visible;
self.syncDisplayLink(null, null);
}
/// Create or update the display link and match it to the current
/// surface state.
fn syncDisplayLink(
self: *Self,
display_id: ?u32,
draw_now: ?*xev.Async,
) void {
if (comptime DisplayLink == void) return;
const display_link = self.display_link orelse display_link: {
if (!self.config.vsync) return;
const callback = draw_now orelse return;
const result = macos.video.DisplayLink.createWithActiveCGDisplays() catch |err| {
// A locked macOS session can temporarily have no active
// displays. Rendering can continue without vsync and a
// later display update will retry this method.
log.warn("error creating display link; using fallback rendering err={}", .{err});
return;
};
result.setOutputCallback(
xev.Async,
&displayLinkCallback,
callback,
) catch |err| {
log.warn("error configuring display link err={}", .{err});
result.release();
return;
};
self.display_link = result;
log.info("created display link", .{});
break :display_link result;
};
if (display_id) |id| {
log.info("updating display link display id={}", .{id});
display_link.setCurrentCGDisplay(id) catch |err| {
log.warn("error setting display link display id err={}", .{err});
};
}
// If we're not visible, then we want to stop the display link
// because it is a waste of resources and we can move to pure
// change-driven updates.
if (comptime DisplayLink != void) link: {
const display_link = self.display_link orelse break :link;
if (visible and self.focused) {
display_link.start() catch {};
} else {
display_link.stop() catch {};
}
if (self.visible and self.focused) {
display_link.start() catch {};
} else {
display_link.stop() catch {};
}
}