mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 17:26:09 +03:00
Merge pull request #1315 from mitchellh/mtl-fail
macOS: detect Metal renderer failures (i.e. GPU memory exhaustion) and show an error message
This commit is contained in:
@ -13,8 +13,8 @@
|
|||||||
.hash = "122071be402af6a317b66f007411678f8600559d5b2d5b00fc80d388a6ec27a43acc",
|
.hash = "122071be402af6a317b66f007411678f8600559d5b2d5b00fc80d388a6ec27a43acc",
|
||||||
},
|
},
|
||||||
.zig_objc = .{
|
.zig_objc = .{
|
||||||
.url = "https://github.com/mitchellh/zig-objc/archive/294e0f3765a96613b45ff7dd594bf99e22409e96.tar.gz",
|
.url = "https://github.com/mitchellh/zig-objc/archive/f6ed382b6db296626a9b56dadcf9d7e4fcba71d3.tar.gz",
|
||||||
.hash = "1220ae28cc6af7600c6f23db1573b33ca3b8accd15e60a1fe1a2c979c20868f151fa",
|
.hash = "1220c94dbcdf5a799ce2b1571978ff3c97bab1341fe329084fcc3c06e5d6375469b9",
|
||||||
},
|
},
|
||||||
.zig_js = .{
|
.zig_js = .{
|
||||||
.url = "https://github.com/mitchellh/zig-js/archive/d4edb682733aef8dc3933683272bdf7c8b9fe658.tar.gz",
|
.url = "https://github.com/mitchellh/zig-js/archive/d4edb682733aef8dc3933683272bdf7c8b9fe658.tar.gz",
|
||||||
|
@ -331,6 +331,11 @@ typedef enum {
|
|||||||
GHOSTTY_BUILD_MODE_RELEASE_SMALL,
|
GHOSTTY_BUILD_MODE_RELEASE_SMALL,
|
||||||
} ghostty_build_mode_e;
|
} 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
|
// 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
|
// structs. To find the Zig struct, grep for this type name. The documentation
|
||||||
// for all of these types is available in the Zig source.
|
// 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_render_inspector_cb)(void *);
|
||||||
typedef void (*ghostty_runtime_set_cell_size_cb)(void *, uint32_t, uint32_t);
|
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_show_desktop_notification_cb)(void *, const char *, const char *);
|
||||||
|
typedef void (*ghostty_runtime_update_renderer_health)(void *, ghostty_renderer_health_e);
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
void *userdata;
|
void *userdata;
|
||||||
@ -404,6 +410,7 @@ typedef struct {
|
|||||||
ghostty_runtime_render_inspector_cb render_inspector_cb;
|
ghostty_runtime_render_inspector_cb render_inspector_cb;
|
||||||
ghostty_runtime_set_cell_size_cb set_cell_size_cb;
|
ghostty_runtime_set_cell_size_cb set_cell_size_cb;
|
||||||
ghostty_runtime_show_desktop_notification_cb show_desktop_notification_cb;
|
ghostty_runtime_show_desktop_notification_cb show_desktop_notification_cb;
|
||||||
|
ghostty_runtime_update_renderer_health update_renderer_health_cb;
|
||||||
} ghostty_runtime_config_s;
|
} ghostty_runtime_config_s;
|
||||||
|
|
||||||
//-------------------------------------------------------------------
|
//-------------------------------------------------------------------
|
||||||
|
@ -92,8 +92,8 @@ extension Ghostty {
|
|||||||
render_inspector_cb: { userdata in App.renderInspector(userdata) },
|
render_inspector_cb: { userdata in App.renderInspector(userdata) },
|
||||||
set_cell_size_cb: { userdata, width, height in App.setCellSize(userdata, width: width, height: height) },
|
set_cell_size_cb: { userdata, width, height in App.setCellSize(userdata, width: width, height: height) },
|
||||||
show_desktop_notification_cb: { userdata, title, body in
|
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.
|
// Create the ghostty app.
|
||||||
@ -289,6 +289,7 @@ extension Ghostty {
|
|||||||
static func renderInspector(_ userdata: UnsafeMutableRawPointer?) {}
|
static func renderInspector(_ userdata: UnsafeMutableRawPointer?) {}
|
||||||
static func setCellSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {}
|
static func setCellSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {}
|
||||||
static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer<CChar>?, body: UnsafePointer<CChar>?) {}
|
static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer<CChar>?, body: UnsafePointer<CChar>?) {}
|
||||||
|
static func updateRendererHealth(_ userdata: UnsafeMutableRawPointer?, health: ghostty_renderer_health_e) {}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@ -612,6 +613,17 @@ extension Ghostty {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
/// Returns the GhosttyState from the given userdata value.
|
||||||
static private func appState(fromView view: SurfaceView) -> App? {
|
static private func appState(fromView view: SurfaceView) -> App? {
|
||||||
guard let surface = view.surface else { return nil }
|
guard let surface = view.surface else { return nil }
|
||||||
|
@ -198,6 +198,30 @@ extension Ghostty {
|
|||||||
return v
|
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))) {
|
||||||
|
#if os(macOS)
|
||||||
|
return Color(NSColor.windowBackgroundColor)
|
||||||
|
#elseif os(iOS)
|
||||||
|
return Color(UIColor.systemBackground)
|
||||||
|
#else
|
||||||
|
#error("unsupported")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
var backgroundOpacity: Double {
|
||||||
guard let config = self.config else { return 1 }
|
guard let config = self.config else { return 1 }
|
||||||
var v: Double = 1
|
var v: Double = 1
|
||||||
|
@ -232,6 +232,9 @@ extension Ghostty.Notification {
|
|||||||
|
|
||||||
/// Notification sent to the split root to equalize split sizes
|
/// Notification sent to the split root to equalize split sizes
|
||||||
static let didEqualizeSplits = Notification.Name("com.mitchellh.ghostty.didEqualizeSplits")
|
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.
|
// Make the input enum hashable.
|
||||||
|
@ -56,6 +56,8 @@ extension Ghostty {
|
|||||||
private var hasFocus: Bool { surfaceFocus && windowFocus }
|
private var hasFocus: Bool { surfaceFocus && windowFocus }
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
let center = NotificationCenter.default
|
||||||
|
|
||||||
ZStack {
|
ZStack {
|
||||||
// We use a GeometryReader to get the frame bounds so that our metal surface
|
// 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
|
// is up to date. See TerminalSurfaceView for why we don't use the NSView
|
||||||
@ -63,9 +65,9 @@ extension Ghostty {
|
|||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
// We use these notifications to determine when the window our surface is
|
// We use these notifications to determine when the window our surface is
|
||||||
// attached to is or is not focused.
|
// attached to is or is not focused.
|
||||||
let pubBecomeFocused = NotificationCenter.default.publisher(for: Notification.didBecomeFocusedSurface, object: surfaceView)
|
let pubBecomeFocused = center.publisher(for: Notification.didBecomeFocusedSurface, object: surfaceView)
|
||||||
let pubBecomeKey = NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification)
|
let pubBecomeKey = center.publisher(for: NSWindow.didBecomeKeyNotification)
|
||||||
let pubResign = NotificationCenter.default.publisher(for: NSWindow.didResignKeyNotification)
|
let pubResign = center.publisher(for: NSWindow.didResignKeyNotification)
|
||||||
|
|
||||||
Surface(view: surfaceView, hasFocus: hasFocus, size: geo.size)
|
Surface(view: surfaceView, hasFocus: hasFocus, size: geo.size)
|
||||||
.focused($surfaceFocus)
|
.focused($surfaceFocus)
|
||||||
@ -142,6 +144,12 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
.ghosttySurfaceView(surfaceView)
|
.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
|
// 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"
|
// rectangle above our view to make it look unfocused. We use "surfaceFocus"
|
||||||
// because we want to keep our focused surface dark even if we don't have window
|
// because we want to keep our focused surface dark even if we don't have window
|
||||||
@ -159,6 +167,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
|
/// 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,
|
/// and interacted with. The word "surface" is used because a surface may represent a window, a tab,
|
||||||
/// a split, a small preview pane, etc. It is ANYTHING that has a terminal drawn to it.
|
/// a split, a small preview pane, etc. It is ANYTHING that has a terminal drawn to it.
|
||||||
@ -249,6 +280,10 @@ extension Ghostty {
|
|||||||
// resized in discrete steps of a single cell.
|
// resized in discrete steps of a single cell.
|
||||||
@Published var cellSize: NSSize = .zero
|
@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
|
// An initial size to request for a window. This will only affect
|
||||||
// then the view is moved to a new window.
|
// then the view is moved to a new window.
|
||||||
var initialSize: NSSize? = nil
|
var initialSize: NSSize? = nil
|
||||||
@ -328,6 +363,15 @@ extension Ghostty {
|
|||||||
// can do SOMETHING.
|
// can do SOMETHING.
|
||||||
super.init(frame: NSMakeRect(0, 0, 800, 600))
|
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.
|
// Setup our surface. This will also initialize all the terminal IO.
|
||||||
let surface_cfg = baseConfig ?? SurfaceConfiguration()
|
let surface_cfg = baseConfig ?? SurfaceConfiguration()
|
||||||
var surface_cfg_c = surface_cfg.ghosttyConfig(view: self)
|
var surface_cfg_c = surface_cfg.ghosttyConfig(view: self)
|
||||||
@ -346,6 +390,10 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
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
|
// Whenever the surface is removed, we need to note that our restorable
|
||||||
// state is invalid to prevent the surface from being restored.
|
// state is invalid to prevent the surface from being restored.
|
||||||
invalidateRestorableState()
|
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() {
|
override func viewDidMoveToWindow() {
|
||||||
// Set our background blur if requested
|
// Set our background blur if requested
|
||||||
setWindowBackgroundBlur(window)
|
setWindowBackgroundBlur(window)
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
|
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
|
||||||
# more details.
|
# more details.
|
||||||
"sha256-to0V9rCefIs8KcWsx+nopgQO4i7O3gb06LGNc6NXN2M="
|
"sha256-+wU1PXMpR/THc7fBfh2ND34fteQCIUWDjN9xZTx5+bs="
|
||||||
|
@ -808,9 +808,18 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
|||||||
const body = std.mem.sliceTo(¬ification.body, 0);
|
const body = std.mem.sliceTo(¬ification.body, 0);
|
||||||
try self.showDesktopNotification(title, body);
|
try self.showDesktopNotification(title, body);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.renderer_health => |health| self.updateRendererHealth(health),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called when our renderer health state changes.
|
||||||
|
fn updateRendererHealth(self: *Surface, health: renderer.Health) void {
|
||||||
|
log.warn("renderer health status change status={}", .{health});
|
||||||
|
if (!@hasDecl(apprt.runtime.Surface, "updateRendererHealth")) return;
|
||||||
|
self.rt_surface.updateRendererHealth(health);
|
||||||
|
}
|
||||||
|
|
||||||
/// Update our configuration at runtime.
|
/// Update our configuration at runtime.
|
||||||
fn changeConfig(self: *Surface, config: *const configpkg.Config) !void {
|
fn changeConfig(self: *Surface, config: *const configpkg.Config) !void {
|
||||||
// Update our new derived config immediately
|
// Update our new derived config immediately
|
||||||
|
@ -11,6 +11,7 @@ const Allocator = std.mem.Allocator;
|
|||||||
const objc = @import("objc");
|
const objc = @import("objc");
|
||||||
const apprt = @import("../apprt.zig");
|
const apprt = @import("../apprt.zig");
|
||||||
const input = @import("../input.zig");
|
const input = @import("../input.zig");
|
||||||
|
const renderer = @import("../renderer.zig");
|
||||||
const terminal = @import("../terminal/main.zig");
|
const terminal = @import("../terminal/main.zig");
|
||||||
const CoreApp = @import("../App.zig");
|
const CoreApp = @import("../App.zig");
|
||||||
const CoreInspector = @import("../inspector/main.zig").Inspector;
|
const CoreInspector = @import("../inspector/main.zig").Inspector;
|
||||||
@ -123,6 +124,9 @@ pub const App = struct {
|
|||||||
|
|
||||||
/// Show a desktop notification to the user.
|
/// Show a desktop notification to the user.
|
||||||
show_desktop_notification: ?*const fn (SurfaceUD, [*:0]const u8, [*:0]const u8) void = null,
|
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.
|
/// Special values for the goto_tab callback.
|
||||||
@ -960,6 +964,16 @@ pub const Surface = struct {
|
|||||||
|
|
||||||
func(self.opts.userdata, title, body);
|
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
|
/// Inspector is the state required for the terminal inspector. A terminal
|
||||||
|
@ -54,6 +54,9 @@ pub const Message = union(enum) {
|
|||||||
/// Desktop notification body.
|
/// Desktop notification body.
|
||||||
body: [255:0]u8,
|
body: [255:0]u8,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Health status change for the renderer.
|
||||||
|
renderer_health: renderer.Health,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A surface mailbox.
|
/// A surface mailbox.
|
||||||
|
@ -52,6 +52,14 @@ pub const Renderer = switch (build_config.renderer) {
|
|||||||
.webgl => WebGL,
|
.webgl => WebGL,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// 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(c_int) {
|
||||||
|
healthy = 0,
|
||||||
|
unhealthy = 1,
|
||||||
|
};
|
||||||
|
|
||||||
test {
|
test {
|
||||||
@import("std").testing.refAllDecls(@This());
|
@import("std").testing.refAllDecls(@This());
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ const assert = std.debug.assert;
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
const Terminal = terminal.Terminal;
|
const Terminal = terminal.Terminal;
|
||||||
|
const Health = renderer.Health;
|
||||||
|
|
||||||
const mtl = @import("metal/api.zig");
|
const mtl = @import("metal/api.zig");
|
||||||
const mtl_buffer = @import("metal/buffer.zig");
|
const mtl_buffer = @import("metal/buffer.zig");
|
||||||
@ -121,6 +122,15 @@ texture_color: objc.Object, // MTLTexture
|
|||||||
/// Custom shader state. This is only set if we have custom shaders.
|
/// Custom shader state. This is only set if we have custom shaders.
|
||||||
custom_shader_state: ?CustomShaderState = null,
|
custom_shader_state: ?CustomShaderState = null,
|
||||||
|
|
||||||
|
/// Health of the last frame. Note that when we do double/triple buffering
|
||||||
|
/// this will have to be part of the frame state.
|
||||||
|
health: std.atomic.Value(Health) = .{ .raw = .healthy },
|
||||||
|
|
||||||
|
/// Sempahore blocking our in-flight buffer updates. For now this is just
|
||||||
|
/// one but in the future if we implement double/triple-buffering this
|
||||||
|
/// will be incremented.
|
||||||
|
inflight: std.Thread.Semaphore = .{ .permits = 1 },
|
||||||
|
|
||||||
pub const CustomShaderState = struct {
|
pub const CustomShaderState = struct {
|
||||||
/// The screen texture that we render the terminal to. If we don't have
|
/// The screen texture that we render the terminal to. If we don't have
|
||||||
/// custom shaders, we render directly to the drawable.
|
/// custom shaders, we render directly to the drawable.
|
||||||
@ -399,6 +409,12 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Metal) void {
|
pub fn deinit(self: *Metal) void {
|
||||||
|
// If we have inflight buffers, wait for completion. This ensures that
|
||||||
|
// any pending GPU operations are completed before we start deallocating
|
||||||
|
// everything. This is important because our completion callbacks access
|
||||||
|
// "self"
|
||||||
|
self.inflight.wait();
|
||||||
|
|
||||||
self.cells.deinit(self.alloc);
|
self.cells.deinit(self.alloc);
|
||||||
self.cells_bg.deinit(self.alloc);
|
self.cells_bg.deinit(self.alloc);
|
||||||
|
|
||||||
@ -703,6 +719,10 @@ pub fn updateFrame(
|
|||||||
pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
|
pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
|
||||||
_ = surface;
|
_ = surface;
|
||||||
|
|
||||||
|
// Wait for a buffer to be available.
|
||||||
|
self.inflight.wait();
|
||||||
|
errdefer self.inflight.post();
|
||||||
|
|
||||||
// If we have custom shaders, update the animation time.
|
// If we have custom shaders, update the animation time.
|
||||||
if (self.custom_shader_state) |*state| {
|
if (self.custom_shader_state) |*state| {
|
||||||
const now = std.time.Instant.now() catch state.last_frame_time;
|
const now = std.time.Instant.now() catch state.last_frame_time;
|
||||||
@ -856,9 +876,60 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buffer.msgSend(void, objc.sel("presentDrawable:"), .{drawable.value});
|
buffer.msgSend(void, objc.sel("presentDrawable:"), .{drawable.value});
|
||||||
|
|
||||||
|
// Create our block to register for completion updates. This is used
|
||||||
|
// so we can detect failures. The block is deallocated by the objC
|
||||||
|
// runtime on success.
|
||||||
|
const block = try CompletionBlock.init(.{ .self = self }, &bufferCompleted);
|
||||||
|
errdefer block.deinit();
|
||||||
|
buffer.msgSend(void, objc.sel("addCompletedHandler:"), .{block.context});
|
||||||
|
|
||||||
buffer.msgSend(void, objc.sel("commit"), .{});
|
buffer.msgSend(void, objc.sel("commit"), .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This is the block type used for the addCompletedHandler call.back.
|
||||||
|
const CompletionBlock = objc.Block(struct { self: *Metal }, .{
|
||||||
|
objc.c.id, // MTLCommandBuffer
|
||||||
|
}, void);
|
||||||
|
|
||||||
|
/// This is the callback called by the CompletionBlock invocation for
|
||||||
|
/// addCompletedHandler.
|
||||||
|
///
|
||||||
|
/// Note: this is USUALLY called on a separate thread because the renderer
|
||||||
|
/// thread and the Apple event loop threads are usually different. Therefore,
|
||||||
|
/// we need to be mindful of thread safety here.
|
||||||
|
fn bufferCompleted(
|
||||||
|
block: *const CompletionBlock.Context,
|
||||||
|
buffer_id: objc.c.id,
|
||||||
|
) callconv(.C) void {
|
||||||
|
const self = block.self;
|
||||||
|
const buffer = objc.Object.fromId(buffer_id);
|
||||||
|
|
||||||
|
// Get our command buffer status. If it is anything other than error
|
||||||
|
// then we don't care and just return right away. We're looking for
|
||||||
|
// errors so that we can log them.
|
||||||
|
const status = buffer.getProperty(mtl.MTLCommandBufferStatus, "status");
|
||||||
|
const health: Health = switch (status) {
|
||||||
|
.@"error" => .unhealthy,
|
||||||
|
else => .healthy,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If our health value hasn't changed, then we do nothing. We don't
|
||||||
|
// do a cmpxchg here because strict atomicity isn't important.
|
||||||
|
if (self.health.load(.SeqCst) != health) {
|
||||||
|
self.health.store(health, .SeqCst);
|
||||||
|
|
||||||
|
// Our health value changed, so we notify the surface so that it
|
||||||
|
// can do something about it.
|
||||||
|
_ = self.surface_mailbox.push(.{
|
||||||
|
.renderer_health = health,
|
||||||
|
}, .{ .forever = {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always release our semaphore
|
||||||
|
self.inflight.post();
|
||||||
|
}
|
||||||
|
|
||||||
fn drawPostShader(
|
fn drawPostShader(
|
||||||
self: *Metal,
|
self: *Metal,
|
||||||
encoder: objc.Object,
|
encoder: objc.Object,
|
||||||
|
@ -1,5 +1,16 @@
|
|||||||
//! This file contains the definitions of the Metal API that we use.
|
//! This file contains the definitions of the Metal API that we use.
|
||||||
|
|
||||||
|
/// https://developer.apple.com/documentation/metal/mtlcommandbufferstatus?language=objc
|
||||||
|
pub const MTLCommandBufferStatus = enum(c_ulong) {
|
||||||
|
not_enqueued = 0,
|
||||||
|
enqueued = 1,
|
||||||
|
committed = 2,
|
||||||
|
scheduled = 3,
|
||||||
|
completed = 4,
|
||||||
|
@"error" = 5,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
/// https://developer.apple.com/documentation/metal/mtlloadaction?language=objc
|
/// https://developer.apple.com/documentation/metal/mtlloadaction?language=objc
|
||||||
pub const MTLLoadAction = enum(c_ulong) {
|
pub const MTLLoadAction = enum(c_ulong) {
|
||||||
dont_care = 0,
|
dont_care = 0,
|
||||||
|
Reference in New Issue
Block a user