mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macos: detect renderer health failures and show error view
This commit is contained in:
@ -331,6 +331,11 @@ typedef enum {
|
||||
GHOSTTY_BUILD_MODE_RELEASE_SMALL,
|
||||
} ghostty_build_mode_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_RENDERER_HEALTH_OK,
|
||||
GHOSTTY_RENDERER_HEALTH_UNHEALTHY,
|
||||
} ghostty_renderer_health_e;
|
||||
|
||||
// Fully defined types. This MUST be kept in sync with equivalent Zig
|
||||
// structs. To find the Zig struct, grep for this type name. The documentation
|
||||
// for all of these types is available in the Zig source.
|
||||
@ -376,6 +381,7 @@ typedef void (*ghostty_runtime_set_initial_window_size_cb)(void *, uint32_t, uin
|
||||
typedef void (*ghostty_runtime_render_inspector_cb)(void *);
|
||||
typedef void (*ghostty_runtime_set_cell_size_cb)(void *, uint32_t, uint32_t);
|
||||
typedef void (*ghostty_runtime_show_desktop_notification_cb)(void *, const char *, const char *);
|
||||
typedef void (*ghostty_runtime_update_renderer_health)(void *, ghostty_renderer_health_e);
|
||||
|
||||
typedef struct {
|
||||
void *userdata;
|
||||
@ -404,6 +410,7 @@ typedef struct {
|
||||
ghostty_runtime_render_inspector_cb render_inspector_cb;
|
||||
ghostty_runtime_set_cell_size_cb set_cell_size_cb;
|
||||
ghostty_runtime_show_desktop_notification_cb show_desktop_notification_cb;
|
||||
ghostty_runtime_update_renderer_health update_renderer_health_cb;
|
||||
} ghostty_runtime_config_s;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
|
@ -92,8 +92,8 @@ extension Ghostty {
|
||||
render_inspector_cb: { userdata in App.renderInspector(userdata) },
|
||||
set_cell_size_cb: { userdata, width, height in App.setCellSize(userdata, width: width, height: height) },
|
||||
show_desktop_notification_cb: { userdata, title, body in
|
||||
App.showUserNotification(userdata, title: title, body: body)
|
||||
}
|
||||
App.showUserNotification(userdata, title: title, body: body) },
|
||||
update_renderer_health_cb: { userdata, health in App.updateRendererHealth(userdata, health: health) }
|
||||
)
|
||||
|
||||
// Create the ghostty app.
|
||||
@ -289,6 +289,7 @@ extension Ghostty {
|
||||
static func renderInspector(_ userdata: UnsafeMutableRawPointer?) {}
|
||||
static func setCellSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {}
|
||||
static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer<CChar>?, body: UnsafePointer<CChar>?) {}
|
||||
static func updateRendererHealth(_ userdata: UnsafeMutableRawPointer?, health: ghostty_renderer_health_e) {}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
@ -611,6 +612,17 @@ extension Ghostty {
|
||||
"mode": mode,
|
||||
])
|
||||
}
|
||||
|
||||
static func updateRendererHealth(_ userdata: UnsafeMutableRawPointer?, health: ghostty_renderer_health_e) {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.didUpdateRendererHealth,
|
||||
object: surface,
|
||||
userInfo: [
|
||||
"health": health,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the GhosttyState from the given userdata value.
|
||||
static private func appState(fromView view: SurfaceView) -> App? {
|
||||
|
@ -197,6 +197,24 @@ extension Ghostty {
|
||||
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
||||
return v
|
||||
}
|
||||
|
||||
var backgroundColor: Color {
|
||||
var rgb: UInt32 = 0
|
||||
let bg_key = "background"
|
||||
if (!ghostty_config_get(config, &rgb, bg_key, UInt(bg_key.count))) {
|
||||
return Color(NSColor.windowBackgroundColor)
|
||||
}
|
||||
|
||||
let red = Double(rgb & 0xff)
|
||||
let green = Double((rgb >> 8) & 0xff)
|
||||
let blue = Double((rgb >> 16) & 0xff)
|
||||
|
||||
return Color(
|
||||
red: red / 255,
|
||||
green: green / 255,
|
||||
blue: blue / 255
|
||||
)
|
||||
}
|
||||
|
||||
var backgroundOpacity: Double {
|
||||
guard let config = self.config else { return 1 }
|
||||
|
@ -232,6 +232,9 @@ extension Ghostty.Notification {
|
||||
|
||||
/// Notification sent to the split root to equalize split sizes
|
||||
static let didEqualizeSplits = Notification.Name("com.mitchellh.ghostty.didEqualizeSplits")
|
||||
|
||||
/// Notification that renderer health changed
|
||||
static let didUpdateRendererHealth = Notification.Name("com.mitchellh.ghostty.didUpdateRendererHealth")
|
||||
}
|
||||
|
||||
// Make the input enum hashable.
|
||||
|
@ -56,6 +56,8 @@ extension Ghostty {
|
||||
private var hasFocus: Bool { surfaceFocus && windowFocus }
|
||||
|
||||
var body: some View {
|
||||
let center = NotificationCenter.default
|
||||
|
||||
ZStack {
|
||||
// We use a GeometryReader to get the frame bounds so that our metal surface
|
||||
// is up to date. See TerminalSurfaceView for why we don't use the NSView
|
||||
@ -63,9 +65,9 @@ extension Ghostty {
|
||||
GeometryReader { geo in
|
||||
// We use these notifications to determine when the window our surface is
|
||||
// attached to is or is not focused.
|
||||
let pubBecomeFocused = NotificationCenter.default.publisher(for: Notification.didBecomeFocusedSurface, object: surfaceView)
|
||||
let pubBecomeKey = NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification)
|
||||
let pubResign = NotificationCenter.default.publisher(for: NSWindow.didResignKeyNotification)
|
||||
let pubBecomeFocused = center.publisher(for: Notification.didBecomeFocusedSurface, object: surfaceView)
|
||||
let pubBecomeKey = center.publisher(for: NSWindow.didBecomeKeyNotification)
|
||||
let pubResign = center.publisher(for: NSWindow.didResignKeyNotification)
|
||||
|
||||
Surface(view: surfaceView, hasFocus: hasFocus, size: geo.size)
|
||||
.focused($surfaceFocus)
|
||||
@ -141,6 +143,12 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
.ghosttySurfaceView(surfaceView)
|
||||
|
||||
// If our surface is not healthy, then we render an error view over it.
|
||||
if (!surfaceView.healthy) {
|
||||
Rectangle().fill(ghostty.config.backgroundColor)
|
||||
SurfaceUnhealthyView()
|
||||
}
|
||||
|
||||
// If we're part of a split view and don't have focus, we put a semi-transparent
|
||||
// rectangle above our view to make it look unfocused. We use "surfaceFocus"
|
||||
@ -158,6 +166,29 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SurfaceUnhealthyView: View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image("AppIconImage")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 128, height: 128)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("Oh, no. 😭").font(.title)
|
||||
Text("""
|
||||
The renderer has failed. This is usually due to exhausting
|
||||
available GPU memory. Please free up available resources.
|
||||
""".replacingOccurrences(of: "\n", with: " ")
|
||||
)
|
||||
.frame(maxWidth: 350)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A surface is terminology in Ghostty for a terminal surface, or a place where a terminal is actually drawn
|
||||
/// and interacted with. The word "surface" is used because a surface may represent a window, a tab,
|
||||
@ -248,6 +279,10 @@ extension Ghostty {
|
||||
// when the font size changes). This is used to allow windows to be
|
||||
// resized in discrete steps of a single cell.
|
||||
@Published var cellSize: NSSize = .zero
|
||||
|
||||
// The health state of the surface. This currently only reflects the
|
||||
// renderer health. In the future we may want to make this an enum.
|
||||
@Published var healthy: Bool = true
|
||||
|
||||
// An initial size to request for a window. This will only affect
|
||||
// then the view is moved to a new window.
|
||||
@ -327,7 +362,16 @@ extension Ghostty {
|
||||
// is non-zero so that our layer bounds are non-zero so that our renderer
|
||||
// can do SOMETHING.
|
||||
super.init(frame: NSMakeRect(0, 0, 800, 600))
|
||||
|
||||
|
||||
// Before we initialize the surface we want to register our notifications
|
||||
// so there is no window where we can't receive them.
|
||||
let center = NotificationCenter.default
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onUpdateRendererHealth),
|
||||
name: Ghostty.Notification.didUpdateRendererHealth,
|
||||
object: self)
|
||||
|
||||
// Setup our surface. This will also initialize all the terminal IO.
|
||||
let surface_cfg = baseConfig ?? SurfaceConfiguration()
|
||||
var surface_cfg_c = surface_cfg.ghosttyConfig(view: self)
|
||||
@ -346,6 +390,10 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Remove all of our notificationcenter subscriptions
|
||||
let center = NotificationCenter.default
|
||||
center.removeObserver(self)
|
||||
|
||||
// Whenever the surface is removed, we need to note that our restorable
|
||||
// state is invalid to prevent the surface from being restored.
|
||||
invalidateRestorableState()
|
||||
@ -503,6 +551,16 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
@objc private func onUpdateRendererHealth(notification: SwiftUI.Notification) {
|
||||
guard let healthAny = notification.userInfo?["health"] else { return }
|
||||
guard let health = healthAny as? ghostty_renderer_health_e else { return }
|
||||
healthy = health == GHOSTTY_RENDERER_HEALTH_OK
|
||||
}
|
||||
|
||||
// MARK: - NSView
|
||||
|
||||
override func viewDidMoveToWindow() {
|
||||
// Set our background blur if requested
|
||||
setWindowBackgroundBlur(window)
|
||||
|
@ -11,6 +11,7 @@ const Allocator = std.mem.Allocator;
|
||||
const objc = @import("objc");
|
||||
const apprt = @import("../apprt.zig");
|
||||
const input = @import("../input.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const CoreApp = @import("../App.zig");
|
||||
const CoreInspector = @import("../inspector/main.zig").Inspector;
|
||||
@ -123,6 +124,9 @@ pub const App = struct {
|
||||
|
||||
/// Show a desktop notification to the user.
|
||||
show_desktop_notification: ?*const fn (SurfaceUD, [*:0]const u8, [*:0]const u8) void = null,
|
||||
|
||||
/// Called when the health of the renderer changes.
|
||||
update_renderer_health: ?*const fn (SurfaceUD, renderer.Health) void = null,
|
||||
};
|
||||
|
||||
/// Special values for the goto_tab callback.
|
||||
@ -960,6 +964,16 @@ pub const Surface = struct {
|
||||
|
||||
func(self.opts.userdata, title, body);
|
||||
}
|
||||
|
||||
/// Update the health of the renderer.
|
||||
pub fn updateRendererHealth(self: *const Surface, health: renderer.Health) void {
|
||||
const func = self.app.opts.update_renderer_health orelse {
|
||||
log.info("runtime embedder does not support update_renderer_health", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
func(self.opts.userdata, health);
|
||||
}
|
||||
};
|
||||
|
||||
/// Inspector is the state required for the terminal inspector. A terminal
|
||||
|
@ -55,7 +55,7 @@ pub const Renderer = switch (build_config.renderer) {
|
||||
/// The health status of a renderer. These must be shared across all
|
||||
/// renderers even if some states aren't reachable so that our API users
|
||||
/// can use the same enum for all renderers.
|
||||
pub const Health = enum(u8) {
|
||||
pub const Health = enum(c_int) {
|
||||
healthy = 0,
|
||||
unhealthy = 1,
|
||||
};
|
||||
|
@ -896,7 +896,7 @@ fn bufferCompleted(
|
||||
const status = buffer.getProperty(mtl.MTLCommandBufferStatus, "status");
|
||||
const health: Health = switch (status) {
|
||||
.@"error" => .unhealthy,
|
||||
else => .healthy,
|
||||
else => .unhealthy,
|
||||
};
|
||||
|
||||
// If our health value hasn't changed, then we do nothing. We don't
|
||||
|
Reference in New Issue
Block a user