From da4ead8f6084e56a59825da5db9457d620c95f80 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Aug 2023 22:11:58 -0700 Subject: [PATCH] renderer/metal: deallocate unused image textures --- src/renderer/Metal.zig | 43 ++++++++++++--- src/renderer/metal/image.zig | 72 ++++++++++++++++++++----- src/terminal/kitty/graphics_storage.zig | 13 ++++- 3 files changed, 107 insertions(+), 21 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 3f8d98a7f..8f346f3db 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -24,6 +24,7 @@ const Terminal = terminal.Terminal; const mtl = @import("metal/api.zig"); const mtl_image = @import("metal/image.zig"); const Image = mtl_image.Image; +const ImageMap = mtl_image.ImageMap; // Get native API access on certain platforms so we can do more customization. const glfwNative = glfw.Native(.{ @@ -73,7 +74,7 @@ font_group: *font.GroupCache, font_shaper: font.Shaper, /// The images that we may render. -images: std.AutoHashMapUnmanaged(u32, Image) = .{}, +images: ImageMap = .{}, /// Metal objects device: objc.Object, // MTLDevice @@ -587,9 +588,9 @@ pub fn render( const draw_cursor = self.cursor_visible and state.terminal.screen.viewportIsBottom(); // If we have Kitty graphics data, we enter a SLOW SLOW SLOW path. - // This can be dramatically improved, this is basically a v1 effort - // to get it working. - if (state.terminal.screen.kitty_images.placements.count() > 0) { + // We only do this if the Kitty image state is dirty meaning only if + // it changes. + if (state.terminal.screen.kitty_images.dirty) { try self.prepKittyGraphics(&state.terminal.screen); } @@ -632,8 +633,20 @@ pub fn render( { var image_it = self.images.iterator(); while (image_it.next()) |kv| { - if (kv.value_ptr.pending() == null) continue; - try kv.value_ptr.upload(self.alloc, self.device); + switch (kv.value_ptr.*) { + .ready => {}, + + .pending_rgb, + .pending_rgba, + => try kv.value_ptr.upload(self.alloc, self.device), + + .unload_pending, + .unload_ready, + => { + kv.value_ptr.deinit(self.alloc); + self.images.removeByPtr(kv.key_ptr); + }, + } } } @@ -767,8 +780,24 @@ fn drawCells( /// the visible images are loaded on the GPU. fn prepKittyGraphics( self: *Metal, - screen: *const terminal.Screen, + screen: *terminal.Screen, ) !void { + defer screen.kitty_images.dirty = false; + + // Go through our known images and if there are any that are no longer + // in use then mark them to be freed. + // + // This never conflicts with the below because a placement can't + // reference an image that doesn't exist. + { + var it = self.images.iterator(); + while (it.next()) |kv| { + if (screen.kitty_images.imageById(kv.key_ptr.*) == null) { + kv.value_ptr.markForUnload(); + } + } + } + // Go through the placements and ensure the image is loaded on the GPU. var it = screen.kitty_images.placements.iterator(); while (it.next()) |kv| { diff --git a/src/renderer/metal/image.zig b/src/renderer/metal/image.zig index 75449040b..d24739c25 100644 --- a/src/renderer/metal/image.zig +++ b/src/renderer/metal/image.zig @@ -5,6 +5,9 @@ const objc = @import("objc"); const mtl = @import("api.zig"); +/// The map used for storing images. +pub const ImageMap = std.AutoHashMapUnmanaged(u32, Image); + /// The state for a single image that is to be rendered. The image can be /// pending upload or ready to use with a texture. pub const Image = union(enum) { @@ -20,6 +23,10 @@ pub const Image = union(enum) { /// The image is uploaded and ready to be used. ready: objc.Object, // MTLTexture + /// The image is uploaded but is scheduled to be unloaded. + unload_pending: []u8, + unload_ready: objc.Object, // MTLTexture + /// Pending image data that needs to be uploaded to the GPU. pub const Pending = struct { height: u32, @@ -42,27 +49,43 @@ pub const Image = union(enum) { switch (self) { .pending_rgb => |p| alloc.free(p.dataSlice(3)), .pending_rgba => |p| alloc.free(p.dataSlice(4)), - .ready => |obj| obj.msgSend(void, objc.sel("release"), .{}), + .unload_pending => |data| alloc.free(data), + + .ready, + .unload_ready, + => |obj| obj.msgSend(void, objc.sel("release"), .{}), } } - /// Our pixel depth - pub fn depth(self: Image) u32 { - return switch (self) { - .ready => unreachable, - .pending_rgb => 3, - .pending_rgba => 4, + /// Mark this image for unload whatever state it is in. + pub fn markForUnload(self: *Image) void { + self.* = switch (self.*) { + .unload_pending, + .unload_ready, + => return, + + .ready => |obj| .{ .unload_ready = obj }, + .pending_rgb => |p| .{ .unload_pending = p.dataSlice(3) }, + .pending_rgba => |p| .{ .unload_pending = p.dataSlice(4) }, }; } - /// Returns true if this image is in a pending state and requires upload. - pub fn pending(self: Image) ?Pending { - return switch (self) { - .ready => null, + /// Returns true if this image is pending upload. + pub fn isPending(self: Image) bool { + return self.pending() != null; + } + /// Returns true if this image is pending an unload. + pub fn isUnloading(self: Image) bool { + return switch (self) { + .unload_pending, + .unload_ready, + => true, + + .ready, .pending_rgb, .pending_rgba, - => |p| p, + => false, }; } @@ -71,7 +94,10 @@ pub const Image = union(enum) { /// no-op. pub fn convert(self: *Image, alloc: Allocator) !void { switch (self.*) { - .ready => unreachable, // invalid + .ready, + .unload_pending, + .unload_ready, + => unreachable, // invalid .pending_rgba => {}, // ready @@ -142,6 +168,26 @@ pub const Image = union(enum) { self.* = .{ .ready = texture }; } + /// Our pixel depth + fn depth(self: Image) u32 { + return switch (self) { + .pending_rgb => 3, + .pending_rgba => 4, + else => unreachable, + }; + } + + /// Returns true if this image is in a pending state and requires upload. + fn pending(self: Image) ?Pending { + return switch (self) { + .pending_rgb, + .pending_rgba, + => |p| p, + + else => null, + }; + } + fn initTexture(p: Pending, device: objc.Object) !objc.Object { // Create our descriptor const desc = init: { diff --git a/src/terminal/kitty/graphics_storage.zig b/src/terminal/kitty/graphics_storage.zig index 0bdfa6766..ce5cdd2a3 100644 --- a/src/terminal/kitty/graphics_storage.zig +++ b/src/terminal/kitty/graphics_storage.zig @@ -20,6 +20,12 @@ pub const ImageStorage = struct { const ImageMap = std.AutoHashMapUnmanaged(u32, Image); const PlacementMap = std.AutoHashMapUnmanaged(PlacementKey, Placement); + /// Dirty is set to true if placements or images change. This is + /// purely informational for the renderer and doesn't affect the + /// correctness of the program. The renderer must set this to false + /// if it cares about this value. + dirty: bool = false, + /// This is the next automatically assigned ID. We start mid-way /// through the u32 range to avoid collisions with buggy programs. next_id: u32 = 2147483647, @@ -70,6 +76,8 @@ pub const ImageStorage = struct { // Write our new image if (gop.found_existing) gop.value_ptr.deinit(alloc); gop.value_ptr.* = img; + + self.dirty = true; } /// Add a placement for a given image. The caller must verify in advance @@ -91,6 +99,8 @@ pub const ImageStorage = struct { const key: PlacementKey = .{ .image_id = image_id, .placement_id = placement_id }; const gop = try self.placements.getOrPut(alloc, key); gop.value_ptr.* = p; + + self.dirty = true; } /// Get an image by its ID. If the image doesn't exist, null is returned. @@ -121,11 +131,12 @@ pub const ImageStorage = struct { .all => |delete_images| if (delete_images) { // We just reset our entire state. self.deinit(alloc); - self.* = .{}; + self.* = .{ .dirty = true }; } else { // Delete all our placements self.placements.deinit(alloc); self.placements = .{}; + self.dirty = true; }, else => log.warn("unimplemented delete command: {}", .{cmd}),