From d07506d285a24f6323ee4fde0eccc032df4332c9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Jan 2024 10:49:00 -0800 Subject: [PATCH 1/7] update zig-objc so enum types work for msgSend on x86_64 --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 43aab7d27..0404fa64a 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -13,8 +13,8 @@ .hash = "122071be402af6a317b66f007411678f8600559d5b2d5b00fc80d388a6ec27a43acc", }, .zig_objc = .{ - .url = "https://github.com/mitchellh/zig-objc/archive/294e0f3765a96613b45ff7dd594bf99e22409e96.tar.gz", - .hash = "1220ae28cc6af7600c6f23db1573b33ca3b8accd15e60a1fe1a2c979c20868f151fa", + .url = "https://github.com/mitchellh/zig-objc/archive/f6ed382b6db296626a9b56dadcf9d7e4fcba71d3.tar.gz", + .hash = "1220c94dbcdf5a799ce2b1571978ff3c97bab1341fe329084fcc3c06e5d6375469b9", }, .zig_js = .{ .url = "https://github.com/mitchellh/zig-js/archive/d4edb682733aef8dc3933683272bdf7c8b9fe658.tar.gz", From 0277a0fb4c4d2b076f072557e00309e85f6f16a5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Jan 2024 10:50:42 -0800 Subject: [PATCH 2/7] renderer/metal: detect frame commit failures and notify surface --- src/Surface.zig | 9 +++++++ src/apprt/surface.zig | 3 +++ src/renderer.zig | 8 ++++++ src/renderer/Metal.zig | 52 ++++++++++++++++++++++++++++++++++++++ src/renderer/metal/api.zig | 11 ++++++++ 5 files changed, 83 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index 8325b823e..bb38b0ad7 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -808,9 +808,18 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { const body = std.mem.sliceTo(¬ification.body, 0); 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. fn changeConfig(self: *Surface, config: *const configpkg.Config) !void { // Update our new derived config immediately diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 8e8fe01e0..463d2f906 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -54,6 +54,9 @@ pub const Message = union(enum) { /// Desktop notification body. body: [255:0]u8, }, + + /// Health status change for the renderer. + renderer_health: renderer.Health, }; /// A surface mailbox. diff --git a/src/renderer.zig b/src/renderer.zig index 9b2ebe71c..331773f31 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -52,6 +52,14 @@ pub const Renderer = switch (build_config.renderer) { .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(u8) { + healthy = 0, + unhealthy = 1, +}; + test { @import("std").testing.refAllDecls(@This()); } diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 9e9fbaea4..8e2e309e6 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -25,6 +25,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const Terminal = terminal.Terminal; +const Health = renderer.Health; const mtl = @import("metal/api.zig"); const mtl_buffer = @import("metal/buffer.zig"); @@ -121,6 +122,10 @@ texture_color: objc.Object, // MTLTexture /// Custom shader state. This is only set if we have custom shaders. 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 }, + pub const CustomShaderState = struct { /// The screen texture that we render the terminal to. If we don't have /// custom shaders, we render directly to the drawable. @@ -856,9 +861,56 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { } 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"), .{}); } +/// 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) return; + 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 = {} }); +} + fn drawPostShader( self: *Metal, encoder: objc.Object, diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index f92489374..0421a34a2 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -1,5 +1,16 @@ //! 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 pub const MTLLoadAction = enum(c_ulong) { dont_care = 0, From 376345dcae62167a27e4790b53fcb31f3d086cb5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Jan 2024 11:27:18 -0800 Subject: [PATCH 3/7] macos: detect renderer health failures and show error view --- include/ghostty.h | 7 +++ macos/Sources/Ghostty/Ghostty.App.swift | 16 +++++- macos/Sources/Ghostty/Ghostty.Config.swift | 18 ++++++ macos/Sources/Ghostty/Package.swift | 3 + macos/Sources/Ghostty/SurfaceView.swift | 66 ++++++++++++++++++++-- src/apprt/embedded.zig | 14 +++++ src/renderer.zig | 2 +- src/renderer/Metal.zig | 2 +- 8 files changed, 120 insertions(+), 8 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 6b058d78c..e9a5df125 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -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; //------------------------------------------------------------------- diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 3afbc0870..f988c553c 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -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?, body: UnsafePointer?) {} + 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? { diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 5cf05694b..cf402355c 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -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 } diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 9f8fe5237..5644ad5c0 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -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. diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 0fb4c212d..bca6c6687 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -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) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index a88aa55b0..5c45409ab 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -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 diff --git a/src/renderer.zig b/src/renderer.zig index 331773f31..72bba4c18 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -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, }; diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 8e2e309e6..b1369b007 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -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 From c28f2e36cc6ee1ee612e7ab8906d1aff6a26b6be Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Jan 2024 11:28:01 -0800 Subject: [PATCH 4/7] renderer/metal: do not be unhealthy by default :) --- src/renderer/Metal.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index b1369b007..8e2e309e6 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -896,7 +896,7 @@ fn bufferCompleted( const status = buffer.getProperty(mtl.MTLCommandBufferStatus, "status"); const health: Health = switch (status) { .@"error" => .unhealthy, - else => .unhealthy, + else => .healthy, }; // If our health value hasn't changed, then we do nothing. We don't From 87933cc63180160361a5feb4b34286c821f57c60 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Jan 2024 11:28:24 -0800 Subject: [PATCH 5/7] nix: update hash --- nix/zigCacheHash.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix index 63ca546e6..010a6fb51 100644 --- a/nix/zigCacheHash.nix +++ b/nix/zigCacheHash.nix @@ -1,3 +1,3 @@ # This file is auto-generated! check build-support/check-zig-cache-hash.sh for # more details. -"sha256-to0V9rCefIs8KcWsx+nopgQO4i7O3gb06LGNc6NXN2M=" +"sha256-+wU1PXMpR/THc7fBfh2ND34fteQCIUWDjN9xZTx5+bs=" From b806b45bae5e3e5d1aa21d92c4c6670aea65e3ab Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Jan 2024 11:57:21 -0800 Subject: [PATCH 6/7] macos: use UIColor for iOS --- macos/Sources/Ghostty/Ghostty.Config.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index cf402355c..641cca917 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -202,7 +202,13 @@ extension Ghostty { 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) From 90ea950d717b2b705f4c9c6e5317362e5937b3f0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Jan 2024 12:18:55 -0800 Subject: [PATCH 7/7] renderer/metal: use a semaphore to protect deinit while waiting for GPU --- src/renderer/Metal.zig | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 8e2e309e6..c881e7be0 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -126,6 +126,11 @@ custom_shader_state: ?CustomShaderState = null, /// 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 { /// The screen texture that we render the terminal to. If we don't have /// custom shaders, we render directly to the drawable. @@ -404,6 +409,12 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { } 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_bg.deinit(self.alloc); @@ -708,6 +719,10 @@ pub fn updateFrame( pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { _ = 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 (self.custom_shader_state) |*state| { const now = std.time.Instant.now() catch state.last_frame_time; @@ -901,14 +916,18 @@ fn bufferCompleted( // 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) return; - self.health.store(health, .SeqCst); + 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 = {} }); + // 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(