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,