renderer/metal: deallocate unused image textures

This commit is contained in:
Mitchell Hashimoto
2023-08-21 22:11:58 -07:00
parent 20257c7a87
commit da4ead8f60
3 changed files with 107 additions and 21 deletions

View File

@ -24,6 +24,7 @@ const Terminal = terminal.Terminal;
const mtl = @import("metal/api.zig"); const mtl = @import("metal/api.zig");
const mtl_image = @import("metal/image.zig"); const mtl_image = @import("metal/image.zig");
const Image = mtl_image.Image; const Image = mtl_image.Image;
const ImageMap = mtl_image.ImageMap;
// Get native API access on certain platforms so we can do more customization. // Get native API access on certain platforms so we can do more customization.
const glfwNative = glfw.Native(.{ const glfwNative = glfw.Native(.{
@ -73,7 +74,7 @@ font_group: *font.GroupCache,
font_shaper: font.Shaper, font_shaper: font.Shaper,
/// The images that we may render. /// The images that we may render.
images: std.AutoHashMapUnmanaged(u32, Image) = .{}, images: ImageMap = .{},
/// Metal objects /// Metal objects
device: objc.Object, // MTLDevice device: objc.Object, // MTLDevice
@ -587,9 +588,9 @@ pub fn render(
const draw_cursor = self.cursor_visible and state.terminal.screen.viewportIsBottom(); const draw_cursor = self.cursor_visible and state.terminal.screen.viewportIsBottom();
// If we have Kitty graphics data, we enter a SLOW SLOW SLOW path. // If we have Kitty graphics data, we enter a SLOW SLOW SLOW path.
// This can be dramatically improved, this is basically a v1 effort // We only do this if the Kitty image state is dirty meaning only if
// to get it working. // it changes.
if (state.terminal.screen.kitty_images.placements.count() > 0) { if (state.terminal.screen.kitty_images.dirty) {
try self.prepKittyGraphics(&state.terminal.screen); try self.prepKittyGraphics(&state.terminal.screen);
} }
@ -632,8 +633,20 @@ pub fn render(
{ {
var image_it = self.images.iterator(); var image_it = self.images.iterator();
while (image_it.next()) |kv| { while (image_it.next()) |kv| {
if (kv.value_ptr.pending() == null) continue; switch (kv.value_ptr.*) {
try kv.value_ptr.upload(self.alloc, self.device); .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. /// the visible images are loaded on the GPU.
fn prepKittyGraphics( fn prepKittyGraphics(
self: *Metal, self: *Metal,
screen: *const terminal.Screen, screen: *terminal.Screen,
) !void { ) !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. // Go through the placements and ensure the image is loaded on the GPU.
var it = screen.kitty_images.placements.iterator(); var it = screen.kitty_images.placements.iterator();
while (it.next()) |kv| { while (it.next()) |kv| {

View File

@ -5,6 +5,9 @@ const objc = @import("objc");
const mtl = @import("api.zig"); 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 /// The state for a single image that is to be rendered. The image can be
/// pending upload or ready to use with a texture. /// pending upload or ready to use with a texture.
pub const Image = union(enum) { pub const Image = union(enum) {
@ -20,6 +23,10 @@ pub const Image = union(enum) {
/// The image is uploaded and ready to be used. /// The image is uploaded and ready to be used.
ready: objc.Object, // MTLTexture 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. /// Pending image data that needs to be uploaded to the GPU.
pub const Pending = struct { pub const Pending = struct {
height: u32, height: u32,
@ -42,27 +49,43 @@ pub const Image = union(enum) {
switch (self) { switch (self) {
.pending_rgb => |p| alloc.free(p.dataSlice(3)), .pending_rgb => |p| alloc.free(p.dataSlice(3)),
.pending_rgba => |p| alloc.free(p.dataSlice(4)), .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 /// Mark this image for unload whatever state it is in.
pub fn depth(self: Image) u32 { pub fn markForUnload(self: *Image) void {
return switch (self) { self.* = switch (self.*) {
.ready => unreachable, .unload_pending,
.pending_rgb => 3, .unload_ready,
.pending_rgba => 4, => 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. /// Returns true if this image is pending upload.
pub fn pending(self: Image) ?Pending { pub fn isPending(self: Image) bool {
return switch (self) { return self.pending() != null;
.ready => 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_rgb,
.pending_rgba, .pending_rgba,
=> |p| p, => false,
}; };
} }
@ -71,7 +94,10 @@ pub const Image = union(enum) {
/// no-op. /// no-op.
pub fn convert(self: *Image, alloc: Allocator) !void { pub fn convert(self: *Image, alloc: Allocator) !void {
switch (self.*) { switch (self.*) {
.ready => unreachable, // invalid .ready,
.unload_pending,
.unload_ready,
=> unreachable, // invalid
.pending_rgba => {}, // ready .pending_rgba => {}, // ready
@ -142,6 +168,26 @@ pub const Image = union(enum) {
self.* = .{ .ready = texture }; 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 { fn initTexture(p: Pending, device: objc.Object) !objc.Object {
// Create our descriptor // Create our descriptor
const desc = init: { const desc = init: {

View File

@ -20,6 +20,12 @@ pub const ImageStorage = struct {
const ImageMap = std.AutoHashMapUnmanaged(u32, Image); const ImageMap = std.AutoHashMapUnmanaged(u32, Image);
const PlacementMap = std.AutoHashMapUnmanaged(PlacementKey, Placement); 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 /// This is the next automatically assigned ID. We start mid-way
/// through the u32 range to avoid collisions with buggy programs. /// through the u32 range to avoid collisions with buggy programs.
next_id: u32 = 2147483647, next_id: u32 = 2147483647,
@ -70,6 +76,8 @@ pub const ImageStorage = struct {
// Write our new image // Write our new image
if (gop.found_existing) gop.value_ptr.deinit(alloc); if (gop.found_existing) gop.value_ptr.deinit(alloc);
gop.value_ptr.* = img; gop.value_ptr.* = img;
self.dirty = true;
} }
/// Add a placement for a given image. The caller must verify in advance /// 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 key: PlacementKey = .{ .image_id = image_id, .placement_id = placement_id };
const gop = try self.placements.getOrPut(alloc, key); const gop = try self.placements.getOrPut(alloc, key);
gop.value_ptr.* = p; gop.value_ptr.* = p;
self.dirty = true;
} }
/// Get an image by its ID. If the image doesn't exist, null is returned. /// 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) { .all => |delete_images| if (delete_images) {
// We just reset our entire state. // We just reset our entire state.
self.deinit(alloc); self.deinit(alloc);
self.* = .{}; self.* = .{ .dirty = true };
} else { } else {
// Delete all our placements // Delete all our placements
self.placements.deinit(alloc); self.placements.deinit(alloc);
self.placements = .{}; self.placements = .{};
self.dirty = true;
}, },
else => log.warn("unimplemented delete command: {}", .{cmd}), else => log.warn("unimplemented delete command: {}", .{cmd}),