From e1908f7cc7992216567f303d5b09228a3772ae37 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 13 Feb 2024 09:21:04 -0800 Subject: [PATCH 1/9] renderer: handle renderer pause/redraw on occlusion --- src/Surface.zig | 8 ++--- src/renderer/Thread.zig | 69 ++++++++++++++++++++++++++-------------- src/renderer/message.zig | 5 +++ 3 files changed, 54 insertions(+), 28 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 61e881cf4..53981eb70 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1563,10 +1563,10 @@ pub fn textCallback(self: *Surface, text: []const u8) !void { /// of focus state. This is used to pause rendering when the surface /// is not visible, and also re-render when it becomes visible again. pub fn occlusionCallback(self: *Surface, visible: bool) !void { - // If we became visible, then we queue a render. This helps scenarios - // where the apprt pauses rendering when the surface is not visible, - // i.e. macOS with Metal (see issue #1510). - if (visible) try self.queueRender(); + _ = self.renderer_thread.mailbox.push(.{ + .visible = visible, + }, .{ .forever = {} }); + try self.queueRender(); } pub fn focusCallback(self: *Surface, focused: bool) !void { diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 2ad4145cf..113f6761c 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -82,6 +82,10 @@ flags: packed struct { /// This is true when the inspector is active. has_inspector: bool = false, + + /// This is true when the view is visible. This is used to determine + /// if we should be rendering or not. + visible: bool = true, } = .{}, pub const DerivedConfig = struct { @@ -242,6 +246,23 @@ fn drainMailbox(self: *Thread) !void { while (self.mailbox.pop()) |message| { log.debug("mailbox message={}", .{message}); switch (message) { + .visible => |v| { + // Set our visible state + self.flags.visible = v; + + // If we became visible then we immediately trigger a draw. + // We don't need to update frame data because that should + // still be happening. + if (v) self.drawFrame(); + + // Note that we're explicitly today not stopping any + // cursor timers, draw timers, etc. These things have very + // little resource cost and properly maintaining their active + // state across different transitions is going to be bug-prone, + // so its easier to just let them keep firing and have them + // check the visible state themselves to control their behavior. + }, + .focus => |v| { // Set it on the renderer try self.renderer.setFocus(v); @@ -341,6 +362,27 @@ fn changeConfig(self: *Thread, config: *const DerivedConfig) !void { self.config = config.*; } +/// Trigger a draw. This will not update frame data or anything, it will +/// just trigger a draw/paint. +fn drawFrame(self: *Thread) void { + // If we're invisible, we do not draw. + if (!self.flags.visible) return; + + // If we're doing single-threaded GPU calls then we just wake up the + // app thread to redraw at this point. + if (renderer.Renderer == renderer.OpenGL and + renderer.OpenGL.single_threaded_draw) + { + _ = self.app_mailbox.push( + .{ .redraw_surface = self.surface }, + .{ .instant = {} }, + ); + } else { + self.renderer.drawFrame(self.surface) catch |err| + log.warn("error drawing err={}", .{err}); + } +} + fn wakeupCallback( self_: ?*Thread, _: *xev.Loop, @@ -388,19 +430,8 @@ fn drawCallback( return .disarm; }; - // If we're doing single-threaded GPU calls then we just wake up the - // app thread to redraw at this point. - if (renderer.Renderer == renderer.OpenGL and - renderer.OpenGL.single_threaded_draw) - { - _ = t.app_mailbox.push( - .{ .redraw_surface = t.surface }, - .{ .instant = {} }, - ); - } else { - t.renderer.drawFrame(t.surface) catch |err| - log.warn("error drawing err={}", .{err}); - } + // Draw + t.drawFrame(); // Only continue if we're still active if (t.draw_active) { @@ -436,18 +467,8 @@ fn renderCallback( ) catch |err| log.warn("error rendering err={}", .{err}); - // If we're doing single-threaded GPU calls then we also wake up the - // app thread to redraw at this point. - if (renderer.Renderer == renderer.OpenGL and - renderer.OpenGL.single_threaded_draw) - { - _ = t.app_mailbox.push(.{ .redraw_surface = t.surface }, .{ .instant = {} }); - return .disarm; - } - // Draw - t.renderer.drawFrame(t.surface) catch |err| - log.warn("error drawing err={}", .{err}); + t.drawFrame(); return .disarm; } diff --git a/src/renderer/message.zig b/src/renderer/message.zig index 73faa0ad7..b215bd4d2 100644 --- a/src/renderer/message.zig +++ b/src/renderer/message.zig @@ -13,6 +13,11 @@ pub const Message = union(enum) { /// the renderer is expected to handle all of these. focus: bool, + /// A change in the view occlusion state. This can be used to determine + /// if the window is visible or not. A window can be not visible (occluded) + /// and still have focus. + visible: bool, + /// Reset the cursor blink by immediately showing the cursor then /// restarting the timer. reset_cursor_blink: void, From 0c1d288142a0d0bf31be13999e89bdc08bd18fb9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 13 Feb 2024 09:44:40 -0800 Subject: [PATCH 2/9] renderer/metal: start extracting "visible" resources --- src/renderer/Metal.zig | 107 +++++++++++++++++++++++++++++------------ 1 file changed, 77 insertions(+), 30 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 0866e9a50..c0e2dcecd 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -116,12 +116,14 @@ buf_instance: InstanceBuffer, // MTLBuffer device: objc.Object, // MTLDevice queue: objc.Object, // MTLCommandQueue layer: objc.Object, // CAMetalLayer -texture_greyscale: objc.Object, // MTLTexture -texture_color: objc.Object, // MTLTexture /// Custom shader state. This is only set if we have custom shaders. custom_shader_state: ?CustomShaderState = null, +/// The allocated resources required when the view is visible and free-able +/// when the view is not visible. These are required to draw. +visible_resources: ?VisibleResources = 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 }, @@ -427,10 +429,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { var shaders = try Shaders.init(alloc, device, custom_shaders); errdefer shaders.deinit(alloc); - // Font atlas textures - const texture_greyscale = try initAtlasTexture(device, &options.font_group.atlas_greyscale); - const texture_color = try initAtlasTexture(device, &options.font_group.atlas_color); - return Metal{ .alloc = alloc, .config = options.config, @@ -469,8 +467,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .device = device, .queue = queue, .layer = layer, - .texture_greyscale = texture_greyscale, - .texture_color = texture_color, .custom_shader_state = custom_shader_state, }; } @@ -499,8 +495,7 @@ pub fn deinit(self: *Metal) void { self.buf_cells_bg.deinit(); self.buf_cells.deinit(); self.buf_instance.deinit(); - deinitMTLResource(self.texture_greyscale); - deinitMTLResource(self.texture_color); + if (self.visible_resources) |*v| v.deinit(); self.queue.msgSend(void, objc.sel("release"), .{}); if (self.custom_shader_state) |*state| state.deinit(); @@ -761,6 +756,18 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { self.inflight.wait(); errdefer self.inflight.post(); + // Get our cached resources. If we don't have them, then we need to + // create them. If we fail to create them, mark the renderer as unhealthy. + const resources: *VisibleResources = if (self.visible_resources) |*v| v else resources: { + const resources = VisibleResources.init(self) catch |err| { + self.setHealth(.unhealthy); + return err; + }; + + self.visible_resources = resources; + break :resources &self.visible_resources.?; + }; + // If we have custom shaders, update the animation time. if (self.custom_shader_state) |*state| { const now = std.time.Instant.now() catch state.first_frame_time; @@ -789,11 +796,19 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { // If our font atlas changed, sync the texture data if (self.font_group.atlas_greyscale.modified) { - try syncAtlasTexture(self.device, &self.font_group.atlas_greyscale, &self.texture_greyscale); + try syncAtlasTexture( + self.device, + &self.font_group.atlas_greyscale, + &resources.texture_greyscale, + ); self.font_group.atlas_greyscale.modified = false; } if (self.font_group.atlas_color.modified) { - try syncAtlasTexture(self.device, &self.font_group.atlas_color, &self.texture_color); + try syncAtlasTexture( + self.device, + &self.font_group.atlas_color, + &resources.texture_color, + ); self.font_group.atlas_color.modified = false; } @@ -850,13 +865,13 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]); // Then draw background cells - try self.drawCells(encoder, &self.buf_cells_bg, self.cells_bg); + try self.drawCells(encoder, &self.buf_cells_bg, self.cells_bg, resources); // Then draw images under text try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); // Then draw fg cells - try self.drawCells(encoder, &self.buf_cells, self.cells); + try self.drawCells(encoder, &self.buf_cells, self.cells, resources); // Then draw remaining images try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]); @@ -949,27 +964,30 @@ fn bufferCompleted( // 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) { + self.setHealth(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(); } +/// Set the health state for the renderer. If the state changes, notify +/// the surface. +fn setHealth(self: *Metal, health: Health) void { + // 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, @@ -1155,6 +1173,7 @@ fn drawCells( encoder: objc.Object, buf: *CellBuffer, cells: std.ArrayListUnmanaged(mtl_shaders.Cell), + resources: *const VisibleResources, ) !void { if (cells.items.len == 0) return; @@ -1181,7 +1200,7 @@ fn drawCells( void, objc.sel("setFragmentTexture:atIndex:"), .{ - self.texture_greyscale.value, + resources.texture_greyscale.value, @as(c_ulong, 0), }, ); @@ -1189,7 +1208,7 @@ fn drawCells( void, objc.sel("setFragmentTexture:atIndex:"), .{ - self.texture_color.value, + resources.texture_color.value, @as(c_ulong, 1), }, ); @@ -2201,3 +2220,31 @@ fn initAtlasTexture(device: objc.Object, atlas: *const font.Atlas) !objc.Object fn deinitMTLResource(obj: objc.Object) void { obj.msgSend(void, objc.sel("release"), .{}); } + +/// VisibleResources are the resources that we only need if our view is +/// currently visible, and can be safely released and rebuilt when we +/// need to free up memory. +/// +/// This is a performance optimization that makes it so that Ghostty +/// only uses GPU resources for views that are currently visible. +const VisibleResources = struct { + texture_greyscale: objc.Object, // MTLTexture + texture_color: objc.Object, // MTLTexture + + pub fn init(m: *Metal) !VisibleResources { + const texture_greyscale = try initAtlasTexture(m.device, &m.font_group.atlas_greyscale); + errdefer deinitMTLResource(texture_greyscale); + const texture_color = try initAtlasTexture(m.device, &m.font_group.atlas_color); + errdefer deinitMTLResource(texture_color); + + return .{ + .texture_greyscale = texture_greyscale, + .texture_color = texture_color, + }; + } + + pub fn deinit(self: *VisibleResources) void { + deinitMTLResource(self.texture_greyscale); + deinitMTLResource(self.texture_color); + } +}; From 9f6db11b95f1bfed79f9b653f2ffb2be81cfc158 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 13 Feb 2024 19:10:58 -0800 Subject: [PATCH 3/9] renderer/metal: cache buf cells --- src/renderer/Metal.zig | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index c0e2dcecd..925267867 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -108,8 +108,6 @@ image_text_end: u32 = 0, /// Metal state shaders: Shaders, // Compiled shaders -buf_cells: CellBuffer, // Vertex buffer for cells -buf_cells_bg: CellBuffer, // Vertex buffer for background cells buf_instance: InstanceBuffer, // MTLBuffer /// Metal objects @@ -373,10 +371,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { errdefer font_shaper.deinit(); // Vertex buffers - var buf_cells = try CellBuffer.init(device, 160 * 160); - errdefer buf_cells.deinit(); - var buf_cells_bg = try CellBuffer.init(device, 160 * 160); - errdefer buf_cells_bg.deinit(); var buf_instance = try InstanceBuffer.initFill(device, &.{ 0, 1, 3, // Top-left triangle 1, 2, 3, // Bottom-right triangle @@ -459,8 +453,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { // Shaders .shaders = shaders, - .buf_cells = buf_cells, - .buf_cells_bg = buf_cells_bg, .buf_instance = buf_instance, // Metal stuff @@ -492,8 +484,6 @@ pub fn deinit(self: *Metal) void { } self.image_placements.deinit(self.alloc); - self.buf_cells_bg.deinit(); - self.buf_cells.deinit(); self.buf_instance.deinit(); if (self.visible_resources) |*v| v.deinit(); self.queue.msgSend(void, objc.sel("release"), .{}); @@ -865,13 +855,13 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]); // Then draw background cells - try self.drawCells(encoder, &self.buf_cells_bg, self.cells_bg, resources); + try self.drawCells(encoder, &resources.buf_cells_bg, self.cells_bg, resources); // Then draw images under text try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); // Then draw fg cells - try self.drawCells(encoder, &self.buf_cells, self.cells, resources); + try self.drawCells(encoder, &resources.buf_cells, self.cells, resources); // Then draw remaining images try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]); @@ -2228,22 +2218,33 @@ fn deinitMTLResource(obj: objc.Object) void { /// This is a performance optimization that makes it so that Ghostty /// only uses GPU resources for views that are currently visible. const VisibleResources = struct { + buf_cells: CellBuffer, // Vertex buffer for cells + buf_cells_bg: CellBuffer, // Vertex buffer for background cells texture_greyscale: objc.Object, // MTLTexture texture_color: objc.Object, // MTLTexture pub fn init(m: *Metal) !VisibleResources { + var buf_cells = try CellBuffer.init(m.device, 160 * 160); + errdefer buf_cells.deinit(); + var buf_cells_bg = try CellBuffer.init(m.device, 160 * 160); + errdefer buf_cells_bg.deinit(); + const texture_greyscale = try initAtlasTexture(m.device, &m.font_group.atlas_greyscale); errdefer deinitMTLResource(texture_greyscale); const texture_color = try initAtlasTexture(m.device, &m.font_group.atlas_color); errdefer deinitMTLResource(texture_color); return .{ + .buf_cells = buf_cells, + .buf_cells_bg = buf_cells_bg, .texture_greyscale = texture_greyscale, .texture_color = texture_color, }; } pub fn deinit(self: *VisibleResources) void { + self.buf_cells.deinit(); + self.buf_cells_bg.deinit(); deinitMTLResource(self.texture_greyscale); deinitMTLResource(self.texture_color); } From bef83446d1cff42946c2a242e3c645350d00c574 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 13 Feb 2024 19:37:10 -0800 Subject: [PATCH 4/9] renderer/metal: move shaders to cached state --- src/renderer/Metal.zig | 96 ++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 925267867..4a1f0f131 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -106,10 +106,6 @@ image_placements: ImagePlacementList = .{}, image_bg_end: u32 = 0, image_text_end: u32 = 0, -/// Metal state -shaders: Shaders, // Compiled shaders -buf_instance: InstanceBuffer, // MTLBuffer - /// Metal objects device: objc.Object, // MTLDevice queue: objc.Object, // MTLCommandQueue @@ -263,10 +259,6 @@ pub fn surfaceInit(surface: *apprt.Surface) !void { } pub fn init(alloc: Allocator, options: renderer.Options) !Metal { - var arena = ArenaAllocator.init(alloc); - defer arena.deinit(); - const arena_alloc = arena.allocator(); - const ViewInfo = struct { view: objc.Object, scaleFactor: f64, @@ -370,26 +362,9 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }); errdefer font_shaper.deinit(); - // Vertex buffers - var buf_instance = try InstanceBuffer.initFill(device, &.{ - 0, 1, 3, // Top-left triangle - 1, 2, 3, // Bottom-right triangle - }); - errdefer buf_instance.deinit(); - - // Load our custom shaders - const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles( - arena_alloc, - options.config.custom_shaders.items, - .msl, - ) catch |err| err: { - log.warn("error loading custom shaders err={}", .{err}); - break :err &.{}; - }; - // If we have custom shaders then setup our state var custom_shader_state: ?CustomShaderState = state: { - if (custom_shaders.len == 0) break :state null; + if (options.config.custom_shaders.items.len == 0) break :state null; // Build our sampler for our texture var sampler = try mtl_sampler.Sampler.init(device); @@ -419,10 +394,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }; errdefer if (custom_shader_state) |*state| state.deinit(); - // Initialize our shaders - var shaders = try Shaders.init(alloc, device, custom_shaders); - errdefer shaders.deinit(alloc); - return Metal{ .alloc = alloc, .config = options.config, @@ -451,10 +422,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .font_group = options.font_group, .font_shaper = font_shaper, - // Shaders - .shaders = shaders, - .buf_instance = buf_instance, - // Metal stuff .device = device, .queue = queue, @@ -484,13 +451,9 @@ pub fn deinit(self: *Metal) void { } self.image_placements.deinit(self.alloc); - self.buf_instance.deinit(); - if (self.visible_resources) |*v| v.deinit(); - self.queue.msgSend(void, objc.sel("release"), .{}); - if (self.custom_shader_state) |*state| state.deinit(); - - self.shaders.deinit(self.alloc); + if (self.visible_resources) |*v| v.deinit(self.alloc); + self.queue.msgSend(void, objc.sel("release"), .{}); self.* = undefined; } @@ -852,19 +815,19 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); // Draw background images first - try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]); + try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end], resources); // Then draw background cells try self.drawCells(encoder, &resources.buf_cells_bg, self.cells_bg, resources); // Then draw images under text - try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); + try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end], resources); // Then draw fg cells try self.drawCells(encoder, &resources.buf_cells, self.cells, resources); // Then draw remaining images - try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]); + try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..], resources); } // If we have custom shaders AND we have a screen texture, then we @@ -915,7 +878,7 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { ); defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); - for (self.shaders.post_pipelines) |pipeline| { + for (resources.shaders.post_pipelines) |pipeline| { try self.drawPostShader(encoder, pipeline, &state); } } @@ -1037,6 +1000,7 @@ fn drawImagePlacements( self: *Metal, encoder: objc.Object, placements: []const mtl_image.Placement, + resources: *const VisibleResources, ) !void { if (placements.len == 0) return; @@ -1044,7 +1008,7 @@ fn drawImagePlacements( encoder.msgSend( void, objc.sel("setRenderPipelineState:"), - .{self.shaders.image_pipeline.value}, + .{resources.shaders.image_pipeline.value}, ); // Set our uniform, which is the only shared buffer @@ -1059,7 +1023,7 @@ fn drawImagePlacements( ); for (placements) |placement| { - try self.drawImagePlacement(encoder, placement); + try self.drawImagePlacement(encoder, placement, resources); } } @@ -1067,6 +1031,7 @@ fn drawImagePlacement( self: *Metal, encoder: objc.Object, p: mtl_image.Placement, + resources: *const VisibleResources, ) !void { // Look up the image const image = self.images.get(p.image_id) orelse { @@ -1144,7 +1109,7 @@ fn drawImagePlacement( @intFromEnum(mtl.MTLPrimitiveType.triangle), @as(c_ulong, 6), @intFromEnum(mtl.MTLIndexType.uint16), - self.buf_instance.buffer.value, + resources.buf_instance.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1), }, @@ -1173,7 +1138,7 @@ fn drawCells( encoder.msgSend( void, objc.sel("setRenderPipelineState:"), - .{self.shaders.cell_pipeline.value}, + .{resources.shaders.cell_pipeline.value}, ); // Set our buffers @@ -1215,7 +1180,7 @@ fn drawCells( @intFromEnum(mtl.MTLPrimitiveType.triangle), @as(c_ulong, 6), @intFromEnum(mtl.MTLIndexType.uint16), - self.buf_instance.buffer.value, + resources.buf_instance.buffer.value, @as(c_ulong, 0), @as(c_ulong, cells.items.len), }, @@ -2218,34 +2183,65 @@ fn deinitMTLResource(obj: objc.Object) void { /// This is a performance optimization that makes it so that Ghostty /// only uses GPU resources for views that are currently visible. const VisibleResources = struct { + shaders: Shaders, // Compiled shaders buf_cells: CellBuffer, // Vertex buffer for cells buf_cells_bg: CellBuffer, // Vertex buffer for background cells + buf_instance: InstanceBuffer, // MTLBuffer texture_greyscale: objc.Object, // MTLTexture texture_color: objc.Object, // MTLTexture pub fn init(m: *Metal) !VisibleResources { + var arena = ArenaAllocator.init(m.alloc); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + + // Load our custom shaders + const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles( + arena_alloc, + m.config.custom_shaders.items, + .msl, + ) catch |err| err: { + log.warn("error loading custom shaders err={}", .{err}); + break :err &.{}; + }; + + // Shaders + var shaders = try Shaders.init(m.alloc, m.device, custom_shaders); + errdefer shaders.deinit(m.alloc); + + // Buffers var buf_cells = try CellBuffer.init(m.device, 160 * 160); errdefer buf_cells.deinit(); var buf_cells_bg = try CellBuffer.init(m.device, 160 * 160); errdefer buf_cells_bg.deinit(); + var buf_instance = try InstanceBuffer.initFill(m.device, &.{ + 0, 1, 3, // Top-left triangle + 1, 2, 3, // Bottom-right triangle + }); + errdefer buf_instance.deinit(); + // Textures const texture_greyscale = try initAtlasTexture(m.device, &m.font_group.atlas_greyscale); errdefer deinitMTLResource(texture_greyscale); const texture_color = try initAtlasTexture(m.device, &m.font_group.atlas_color); errdefer deinitMTLResource(texture_color); return .{ + .shaders = shaders, .buf_cells = buf_cells, .buf_cells_bg = buf_cells_bg, + .buf_instance = buf_instance, .texture_greyscale = texture_greyscale, .texture_color = texture_color, }; } - pub fn deinit(self: *VisibleResources) void { + pub fn deinit(self: *VisibleResources, alloc: Allocator) void { self.buf_cells.deinit(); self.buf_cells_bg.deinit(); + self.buf_instance.deinit(); deinitMTLResource(self.texture_greyscale); deinitMTLResource(self.texture_color); + self.shaders.deinit(alloc); } }; From b5d543705de4ea42407db9e2d49ae03f3ae5cf14 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 13 Feb 2024 20:07:41 -0800 Subject: [PATCH 5/9] renderer/metal: free resources when occluded --- src/renderer/Metal.zig | 18 ++++++++++++++++++ src/renderer/Thread.zig | 3 +++ 2 files changed, 21 insertions(+) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 4a1f0f131..8296176d4 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -509,6 +509,19 @@ pub fn setFocus(self: *Metal, focus: bool) !void { self.focused = focus; } +/// Callback when the view visibility changes, i.e. if a window is on +/// another workspace, behind a window, etc. +pub fn setVisible(self: *Metal, visible: bool) !void { + //if (true) return; + if (!visible) { + if (self.visible_resources) |*v| { + log.debug("view occluded, deallocating GPU resources", .{}); + v.deinit(self.alloc); + self.visible_resources = null; + } + } +} + /// Set the new font size. /// /// Must be called on the render thread. @@ -712,6 +725,7 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { // Get our cached resources. If we don't have them, then we need to // create them. If we fail to create them, mark the renderer as unhealthy. const resources: *VisibleResources = if (self.visible_resources) |*v| v else resources: { + log.debug("view is visible, allocating GPU resources", .{}); const resources = VisibleResources.init(self) catch |err| { self.setHealth(.unhealthy); return err; @@ -2226,6 +2240,10 @@ const VisibleResources = struct { const texture_color = try initAtlasTexture(m.device, &m.font_group.atlas_color); errdefer deinitMTLResource(texture_color); + // Mark our atlas as modified so the textures are synced + m.font_group.atlas_greyscale.modified = true; + m.font_group.atlas_color.modified = true; + return .{ .shaders = shaders, .buf_cells = buf_cells, diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 113f6761c..84f1940f5 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -250,6 +250,9 @@ fn drainMailbox(self: *Thread) !void { // Set our visible state self.flags.visible = v; + // Set it on the renderer + try self.renderer.setVisible(v); + // If we became visible then we immediately trigger a draw. // We don't need to update frame data because that should // still be happening. From e10e45a93579b28b090bf491ee56a237cdd68eb8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 13 Feb 2024 21:57:46 -0800 Subject: [PATCH 6/9] Revert "renderer/metal: free resources when occluded" This reverts commit b5d543705de4ea42407db9e2d49ae03f3ae5cf14. --- src/renderer/Metal.zig | 18 ------------------ src/renderer/Thread.zig | 3 --- 2 files changed, 21 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 8296176d4..4a1f0f131 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -509,19 +509,6 @@ pub fn setFocus(self: *Metal, focus: bool) !void { self.focused = focus; } -/// Callback when the view visibility changes, i.e. if a window is on -/// another workspace, behind a window, etc. -pub fn setVisible(self: *Metal, visible: bool) !void { - //if (true) return; - if (!visible) { - if (self.visible_resources) |*v| { - log.debug("view occluded, deallocating GPU resources", .{}); - v.deinit(self.alloc); - self.visible_resources = null; - } - } -} - /// Set the new font size. /// /// Must be called on the render thread. @@ -725,7 +712,6 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { // Get our cached resources. If we don't have them, then we need to // create them. If we fail to create them, mark the renderer as unhealthy. const resources: *VisibleResources = if (self.visible_resources) |*v| v else resources: { - log.debug("view is visible, allocating GPU resources", .{}); const resources = VisibleResources.init(self) catch |err| { self.setHealth(.unhealthy); return err; @@ -2240,10 +2226,6 @@ const VisibleResources = struct { const texture_color = try initAtlasTexture(m.device, &m.font_group.atlas_color); errdefer deinitMTLResource(texture_color); - // Mark our atlas as modified so the textures are synced - m.font_group.atlas_greyscale.modified = true; - m.font_group.atlas_color.modified = true; - return .{ .shaders = shaders, .buf_cells = buf_cells, diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 84f1940f5..113f6761c 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -250,9 +250,6 @@ fn drainMailbox(self: *Thread) !void { // Set our visible state self.flags.visible = v; - // Set it on the renderer - try self.renderer.setVisible(v); - // If we became visible then we immediately trigger a draw. // We don't need to update frame data because that should // still be happening. From b602eba61bae68f3fbe6683fd4940ba81f1f30e6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 13 Feb 2024 21:57:49 -0800 Subject: [PATCH 7/9] Revert "renderer/metal: move shaders to cached state" This reverts commit bef83446d1cff42946c2a242e3c645350d00c574. --- src/renderer/Metal.zig | 96 ++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 4a1f0f131..925267867 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -106,6 +106,10 @@ image_placements: ImagePlacementList = .{}, image_bg_end: u32 = 0, image_text_end: u32 = 0, +/// Metal state +shaders: Shaders, // Compiled shaders +buf_instance: InstanceBuffer, // MTLBuffer + /// Metal objects device: objc.Object, // MTLDevice queue: objc.Object, // MTLCommandQueue @@ -259,6 +263,10 @@ pub fn surfaceInit(surface: *apprt.Surface) !void { } pub fn init(alloc: Allocator, options: renderer.Options) !Metal { + var arena = ArenaAllocator.init(alloc); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + const ViewInfo = struct { view: objc.Object, scaleFactor: f64, @@ -362,9 +370,26 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }); errdefer font_shaper.deinit(); + // Vertex buffers + var buf_instance = try InstanceBuffer.initFill(device, &.{ + 0, 1, 3, // Top-left triangle + 1, 2, 3, // Bottom-right triangle + }); + errdefer buf_instance.deinit(); + + // Load our custom shaders + const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles( + arena_alloc, + options.config.custom_shaders.items, + .msl, + ) catch |err| err: { + log.warn("error loading custom shaders err={}", .{err}); + break :err &.{}; + }; + // If we have custom shaders then setup our state var custom_shader_state: ?CustomShaderState = state: { - if (options.config.custom_shaders.items.len == 0) break :state null; + if (custom_shaders.len == 0) break :state null; // Build our sampler for our texture var sampler = try mtl_sampler.Sampler.init(device); @@ -394,6 +419,10 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }; errdefer if (custom_shader_state) |*state| state.deinit(); + // Initialize our shaders + var shaders = try Shaders.init(alloc, device, custom_shaders); + errdefer shaders.deinit(alloc); + return Metal{ .alloc = alloc, .config = options.config, @@ -422,6 +451,10 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .font_group = options.font_group, .font_shaper = font_shaper, + // Shaders + .shaders = shaders, + .buf_instance = buf_instance, + // Metal stuff .device = device, .queue = queue, @@ -451,10 +484,14 @@ pub fn deinit(self: *Metal) void { } self.image_placements.deinit(self.alloc); - if (self.custom_shader_state) |*state| state.deinit(); - if (self.visible_resources) |*v| v.deinit(self.alloc); + self.buf_instance.deinit(); + if (self.visible_resources) |*v| v.deinit(); self.queue.msgSend(void, objc.sel("release"), .{}); + if (self.custom_shader_state) |*state| state.deinit(); + + self.shaders.deinit(self.alloc); + self.* = undefined; } @@ -815,19 +852,19 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); // Draw background images first - try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end], resources); + try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]); // Then draw background cells try self.drawCells(encoder, &resources.buf_cells_bg, self.cells_bg, resources); // Then draw images under text - try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end], resources); + try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); // Then draw fg cells try self.drawCells(encoder, &resources.buf_cells, self.cells, resources); // Then draw remaining images - try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..], resources); + try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]); } // If we have custom shaders AND we have a screen texture, then we @@ -878,7 +915,7 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { ); defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); - for (resources.shaders.post_pipelines) |pipeline| { + for (self.shaders.post_pipelines) |pipeline| { try self.drawPostShader(encoder, pipeline, &state); } } @@ -1000,7 +1037,6 @@ fn drawImagePlacements( self: *Metal, encoder: objc.Object, placements: []const mtl_image.Placement, - resources: *const VisibleResources, ) !void { if (placements.len == 0) return; @@ -1008,7 +1044,7 @@ fn drawImagePlacements( encoder.msgSend( void, objc.sel("setRenderPipelineState:"), - .{resources.shaders.image_pipeline.value}, + .{self.shaders.image_pipeline.value}, ); // Set our uniform, which is the only shared buffer @@ -1023,7 +1059,7 @@ fn drawImagePlacements( ); for (placements) |placement| { - try self.drawImagePlacement(encoder, placement, resources); + try self.drawImagePlacement(encoder, placement); } } @@ -1031,7 +1067,6 @@ fn drawImagePlacement( self: *Metal, encoder: objc.Object, p: mtl_image.Placement, - resources: *const VisibleResources, ) !void { // Look up the image const image = self.images.get(p.image_id) orelse { @@ -1109,7 +1144,7 @@ fn drawImagePlacement( @intFromEnum(mtl.MTLPrimitiveType.triangle), @as(c_ulong, 6), @intFromEnum(mtl.MTLIndexType.uint16), - resources.buf_instance.buffer.value, + self.buf_instance.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1), }, @@ -1138,7 +1173,7 @@ fn drawCells( encoder.msgSend( void, objc.sel("setRenderPipelineState:"), - .{resources.shaders.cell_pipeline.value}, + .{self.shaders.cell_pipeline.value}, ); // Set our buffers @@ -1180,7 +1215,7 @@ fn drawCells( @intFromEnum(mtl.MTLPrimitiveType.triangle), @as(c_ulong, 6), @intFromEnum(mtl.MTLIndexType.uint16), - resources.buf_instance.buffer.value, + self.buf_instance.buffer.value, @as(c_ulong, 0), @as(c_ulong, cells.items.len), }, @@ -2183,65 +2218,34 @@ fn deinitMTLResource(obj: objc.Object) void { /// This is a performance optimization that makes it so that Ghostty /// only uses GPU resources for views that are currently visible. const VisibleResources = struct { - shaders: Shaders, // Compiled shaders buf_cells: CellBuffer, // Vertex buffer for cells buf_cells_bg: CellBuffer, // Vertex buffer for background cells - buf_instance: InstanceBuffer, // MTLBuffer texture_greyscale: objc.Object, // MTLTexture texture_color: objc.Object, // MTLTexture pub fn init(m: *Metal) !VisibleResources { - var arena = ArenaAllocator.init(m.alloc); - defer arena.deinit(); - const arena_alloc = arena.allocator(); - - // Load our custom shaders - const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles( - arena_alloc, - m.config.custom_shaders.items, - .msl, - ) catch |err| err: { - log.warn("error loading custom shaders err={}", .{err}); - break :err &.{}; - }; - - // Shaders - var shaders = try Shaders.init(m.alloc, m.device, custom_shaders); - errdefer shaders.deinit(m.alloc); - - // Buffers var buf_cells = try CellBuffer.init(m.device, 160 * 160); errdefer buf_cells.deinit(); var buf_cells_bg = try CellBuffer.init(m.device, 160 * 160); errdefer buf_cells_bg.deinit(); - var buf_instance = try InstanceBuffer.initFill(m.device, &.{ - 0, 1, 3, // Top-left triangle - 1, 2, 3, // Bottom-right triangle - }); - errdefer buf_instance.deinit(); - // Textures const texture_greyscale = try initAtlasTexture(m.device, &m.font_group.atlas_greyscale); errdefer deinitMTLResource(texture_greyscale); const texture_color = try initAtlasTexture(m.device, &m.font_group.atlas_color); errdefer deinitMTLResource(texture_color); return .{ - .shaders = shaders, .buf_cells = buf_cells, .buf_cells_bg = buf_cells_bg, - .buf_instance = buf_instance, .texture_greyscale = texture_greyscale, .texture_color = texture_color, }; } - pub fn deinit(self: *VisibleResources, alloc: Allocator) void { + pub fn deinit(self: *VisibleResources) void { self.buf_cells.deinit(); self.buf_cells_bg.deinit(); - self.buf_instance.deinit(); deinitMTLResource(self.texture_greyscale); deinitMTLResource(self.texture_color); - self.shaders.deinit(alloc); } }; From 387e5b242ecaeed86c3fb67531a73cca527b4386 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 13 Feb 2024 21:57:50 -0800 Subject: [PATCH 8/9] Revert "renderer/metal: cache buf cells" This reverts commit 9f6db11b95f1bfed79f9b653f2ffb2be81cfc158. --- src/renderer/Metal.zig | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 925267867..c0e2dcecd 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -108,6 +108,8 @@ image_text_end: u32 = 0, /// Metal state shaders: Shaders, // Compiled shaders +buf_cells: CellBuffer, // Vertex buffer for cells +buf_cells_bg: CellBuffer, // Vertex buffer for background cells buf_instance: InstanceBuffer, // MTLBuffer /// Metal objects @@ -371,6 +373,10 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { errdefer font_shaper.deinit(); // Vertex buffers + var buf_cells = try CellBuffer.init(device, 160 * 160); + errdefer buf_cells.deinit(); + var buf_cells_bg = try CellBuffer.init(device, 160 * 160); + errdefer buf_cells_bg.deinit(); var buf_instance = try InstanceBuffer.initFill(device, &.{ 0, 1, 3, // Top-left triangle 1, 2, 3, // Bottom-right triangle @@ -453,6 +459,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { // Shaders .shaders = shaders, + .buf_cells = buf_cells, + .buf_cells_bg = buf_cells_bg, .buf_instance = buf_instance, // Metal stuff @@ -484,6 +492,8 @@ pub fn deinit(self: *Metal) void { } self.image_placements.deinit(self.alloc); + self.buf_cells_bg.deinit(); + self.buf_cells.deinit(); self.buf_instance.deinit(); if (self.visible_resources) |*v| v.deinit(); self.queue.msgSend(void, objc.sel("release"), .{}); @@ -855,13 +865,13 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]); // Then draw background cells - try self.drawCells(encoder, &resources.buf_cells_bg, self.cells_bg, resources); + try self.drawCells(encoder, &self.buf_cells_bg, self.cells_bg, resources); // Then draw images under text try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); // Then draw fg cells - try self.drawCells(encoder, &resources.buf_cells, self.cells, resources); + try self.drawCells(encoder, &self.buf_cells, self.cells, resources); // Then draw remaining images try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]); @@ -2218,33 +2228,22 @@ fn deinitMTLResource(obj: objc.Object) void { /// This is a performance optimization that makes it so that Ghostty /// only uses GPU resources for views that are currently visible. const VisibleResources = struct { - buf_cells: CellBuffer, // Vertex buffer for cells - buf_cells_bg: CellBuffer, // Vertex buffer for background cells texture_greyscale: objc.Object, // MTLTexture texture_color: objc.Object, // MTLTexture pub fn init(m: *Metal) !VisibleResources { - var buf_cells = try CellBuffer.init(m.device, 160 * 160); - errdefer buf_cells.deinit(); - var buf_cells_bg = try CellBuffer.init(m.device, 160 * 160); - errdefer buf_cells_bg.deinit(); - const texture_greyscale = try initAtlasTexture(m.device, &m.font_group.atlas_greyscale); errdefer deinitMTLResource(texture_greyscale); const texture_color = try initAtlasTexture(m.device, &m.font_group.atlas_color); errdefer deinitMTLResource(texture_color); return .{ - .buf_cells = buf_cells, - .buf_cells_bg = buf_cells_bg, .texture_greyscale = texture_greyscale, .texture_color = texture_color, }; } pub fn deinit(self: *VisibleResources) void { - self.buf_cells.deinit(); - self.buf_cells_bg.deinit(); deinitMTLResource(self.texture_greyscale); deinitMTLResource(self.texture_color); } From 239a5177103f9c8798f03f14ee5213b256a07aa1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 13 Feb 2024 21:57:55 -0800 Subject: [PATCH 9/9] Revert "renderer/metal: start extracting "visible" resources" This reverts commit 0c1d288142a0d0bf31be13999e89bdc08bd18fb9. --- src/renderer/Metal.zig | 107 ++++++++++++----------------------------- 1 file changed, 30 insertions(+), 77 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index c0e2dcecd..0866e9a50 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -116,14 +116,12 @@ buf_instance: InstanceBuffer, // MTLBuffer device: objc.Object, // MTLDevice queue: objc.Object, // MTLCommandQueue layer: objc.Object, // CAMetalLayer +texture_greyscale: objc.Object, // MTLTexture +texture_color: objc.Object, // MTLTexture /// Custom shader state. This is only set if we have custom shaders. custom_shader_state: ?CustomShaderState = null, -/// The allocated resources required when the view is visible and free-able -/// when the view is not visible. These are required to draw. -visible_resources: ?VisibleResources = 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 }, @@ -429,6 +427,10 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { var shaders = try Shaders.init(alloc, device, custom_shaders); errdefer shaders.deinit(alloc); + // Font atlas textures + const texture_greyscale = try initAtlasTexture(device, &options.font_group.atlas_greyscale); + const texture_color = try initAtlasTexture(device, &options.font_group.atlas_color); + return Metal{ .alloc = alloc, .config = options.config, @@ -467,6 +469,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .device = device, .queue = queue, .layer = layer, + .texture_greyscale = texture_greyscale, + .texture_color = texture_color, .custom_shader_state = custom_shader_state, }; } @@ -495,7 +499,8 @@ pub fn deinit(self: *Metal) void { self.buf_cells_bg.deinit(); self.buf_cells.deinit(); self.buf_instance.deinit(); - if (self.visible_resources) |*v| v.deinit(); + deinitMTLResource(self.texture_greyscale); + deinitMTLResource(self.texture_color); self.queue.msgSend(void, objc.sel("release"), .{}); if (self.custom_shader_state) |*state| state.deinit(); @@ -756,18 +761,6 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { self.inflight.wait(); errdefer self.inflight.post(); - // Get our cached resources. If we don't have them, then we need to - // create them. If we fail to create them, mark the renderer as unhealthy. - const resources: *VisibleResources = if (self.visible_resources) |*v| v else resources: { - const resources = VisibleResources.init(self) catch |err| { - self.setHealth(.unhealthy); - return err; - }; - - self.visible_resources = resources; - break :resources &self.visible_resources.?; - }; - // If we have custom shaders, update the animation time. if (self.custom_shader_state) |*state| { const now = std.time.Instant.now() catch state.first_frame_time; @@ -796,19 +789,11 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { // If our font atlas changed, sync the texture data if (self.font_group.atlas_greyscale.modified) { - try syncAtlasTexture( - self.device, - &self.font_group.atlas_greyscale, - &resources.texture_greyscale, - ); + try syncAtlasTexture(self.device, &self.font_group.atlas_greyscale, &self.texture_greyscale); self.font_group.atlas_greyscale.modified = false; } if (self.font_group.atlas_color.modified) { - try syncAtlasTexture( - self.device, - &self.font_group.atlas_color, - &resources.texture_color, - ); + try syncAtlasTexture(self.device, &self.font_group.atlas_color, &self.texture_color); self.font_group.atlas_color.modified = false; } @@ -865,13 +850,13 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]); // Then draw background cells - try self.drawCells(encoder, &self.buf_cells_bg, self.cells_bg, resources); + try self.drawCells(encoder, &self.buf_cells_bg, self.cells_bg); // Then draw images under text try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); // Then draw fg cells - try self.drawCells(encoder, &self.buf_cells, self.cells, resources); + try self.drawCells(encoder, &self.buf_cells, self.cells); // Then draw remaining images try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]); @@ -964,30 +949,27 @@ fn bufferCompleted( // 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"); - self.setHealth(switch (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(); } -/// Set the health state for the renderer. If the state changes, notify -/// the surface. -fn setHealth(self: *Metal, health: Health) void { - // 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, @@ -1173,7 +1155,6 @@ fn drawCells( encoder: objc.Object, buf: *CellBuffer, cells: std.ArrayListUnmanaged(mtl_shaders.Cell), - resources: *const VisibleResources, ) !void { if (cells.items.len == 0) return; @@ -1200,7 +1181,7 @@ fn drawCells( void, objc.sel("setFragmentTexture:atIndex:"), .{ - resources.texture_greyscale.value, + self.texture_greyscale.value, @as(c_ulong, 0), }, ); @@ -1208,7 +1189,7 @@ fn drawCells( void, objc.sel("setFragmentTexture:atIndex:"), .{ - resources.texture_color.value, + self.texture_color.value, @as(c_ulong, 1), }, ); @@ -2220,31 +2201,3 @@ fn initAtlasTexture(device: objc.Object, atlas: *const font.Atlas) !objc.Object fn deinitMTLResource(obj: objc.Object) void { obj.msgSend(void, objc.sel("release"), .{}); } - -/// VisibleResources are the resources that we only need if our view is -/// currently visible, and can be safely released and rebuilt when we -/// need to free up memory. -/// -/// This is a performance optimization that makes it so that Ghostty -/// only uses GPU resources for views that are currently visible. -const VisibleResources = struct { - texture_greyscale: objc.Object, // MTLTexture - texture_color: objc.Object, // MTLTexture - - pub fn init(m: *Metal) !VisibleResources { - const texture_greyscale = try initAtlasTexture(m.device, &m.font_group.atlas_greyscale); - errdefer deinitMTLResource(texture_greyscale); - const texture_color = try initAtlasTexture(m.device, &m.font_group.atlas_color); - errdefer deinitMTLResource(texture_color); - - return .{ - .texture_greyscale = texture_greyscale, - .texture_color = texture_color, - }; - } - - pub fn deinit(self: *VisibleResources) void { - deinitMTLResource(self.texture_greyscale); - deinitMTLResource(self.texture_color); - } -};