diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index e75171721..6c083b6c2 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -18,9 +18,7 @@ const constraintWidth = cellpkg.constraintWidth; const isCovering = cellpkg.isCovering; const rowNeverExtendBg = @import("row.zig").neverExtendBg; const imagepkg = @import("image.zig"); -const Image = imagepkg.Image; -const ImageMap = imagepkg.ImageMap; -const ImagePlacementList = std.ArrayListUnmanaged(imagepkg.Placement); +const ImageState = imagepkg.State; const shadertoy = @import("shadertoy.zig"); const assert = @import("../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; @@ -169,11 +167,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { font_shaper_cache: font.ShaperCache, /// The images that we may render. - images: ImageMap = .{}, - image_placements: ImagePlacementList = .{}, - image_bg_end: u32 = 0, - image_text_end: u32 = 0, - image_virtual: bool = false, + images: ImageState = .empty, /// Background image, if we have one. bg_image: ?imagepkg.Image = null, @@ -806,12 +800,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self.config.deinit(); - { - var it = self.images.iterator(); - while (it.next()) |kv| kv.value_ptr.image.deinit(self.alloc); - self.images.deinit(self.alloc); - } - self.image_placements.deinit(self.alloc); + self.images.deinit(self.alloc); if (self.bg_image) |img| img.deinit(self.alloc); @@ -1190,10 +1179,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // If we have any virtual references, we must also rebuild our // kitty state on every frame because any cell change can move // an image. - if (state.terminal.screens.active.kitty_images.dirty or - self.image_virtual) - { - self.prepKittyGraphics(state.terminal); + if (self.images.kittyRequiresUpdate(state.terminal)) { + self.images.kittyUpdate( + self.alloc, + state.terminal, + .{ + .width = self.grid_metrics.cell_width, + .height = self.grid_metrics.cell_height, + }, + ); } // Get our OSC8 links we're hovering if we have a mouse. @@ -1460,7 +1454,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } // Upload images to the GPU as necessary. - try self.uploadKittyImages(); + _ = self.images.upload(self.alloc, &self.api); // Upload the background image to the GPU as necessary. try self.uploadBackgroundImage(); @@ -1542,9 +1536,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Then we draw any kitty images that need // to be behind text AND cell backgrounds. - try self.drawImagePlacements( + self.images.draw( + &self.api, + self.shaders.pipelines.image, &pass, - self.image_placements.items[0..self.image_bg_end], + .kitty_below_bg, ); // Then we draw any opaque cell backgrounds. @@ -1556,9 +1552,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }); // Kitty images between cell backgrounds and text. - try self.drawImagePlacements( + self.images.draw( + &self.api, + self.shaders.pipelines.image, &pass, - self.image_placements.items[self.image_bg_end..self.image_text_end], + .kitty_below_text, ); // Text. @@ -1581,9 +1579,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }); // Kitty images in front of text. - try self.drawImagePlacements( + self.images.draw( + &self.api, + self.shaders.pipelines.image, &pass, - self.image_placements.items[self.image_text_end..], + .kitty_above_text, ); } @@ -1707,358 +1707,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } } - /// This goes through the Kitty graphic placements and accumulates the - /// placements we need to render on our viewport. - fn prepKittyGraphics( - self: *Self, - t: *terminal.Terminal, - ) void { - self.draw_mutex.lock(); - defer self.draw_mutex.unlock(); - - const storage = &t.screens.active.kitty_images; - defer storage.dirty = false; - - // We always clear our previous placements no matter what because - // we rebuild them from scratch. - self.image_placements.clearRetainingCapacity(); - self.image_virtual = 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 (storage.imageById(kv.key_ptr.*) == null) { - kv.value_ptr.image.markForUnload(); - } - } - } - - // The top-left and bottom-right corners of our viewport in screen - // points. This lets us determine offsets and containment of placements. - const top = t.screens.active.pages.getTopLeft(.viewport); - const bot = t.screens.active.pages.getBottomRight(.viewport).?; - const top_y = t.screens.active.pages.pointFromPin(.screen, top).?.screen.y; - const bot_y = t.screens.active.pages.pointFromPin(.screen, bot).?.screen.y; - - // Go through the placements and ensure the image is - // on the GPU or else is ready to be sent to the GPU. - var it = storage.placements.iterator(); - while (it.next()) |kv| { - const p = kv.value_ptr; - - // Special logic based on location - switch (p.location) { - .pin => {}, - .virtual => { - // We need to mark virtual placements on our renderer so that - // we know to rebuild in more scenarios since cell changes can - // now trigger placement changes. - self.image_virtual = true; - - // We also continue out because virtual placements are - // only triggered by the unicode placeholder, not by the - // placement itself. - continue; - }, - } - - // Get the image for the placement - const image = storage.imageById(kv.key_ptr.image_id) orelse { - log.warn( - "missing image for placement, ignoring image_id={}", - .{kv.key_ptr.image_id}, - ); - continue; - }; - - self.prepKittyPlacement( - t, - top_y, - bot_y, - &image, - p, - ) catch |err| { - // For errors we log and continue. We try to place - // other placements even if one fails. - log.warn("error preparing kitty placement err={}", .{err}); - }; - } - - // If we have virtual placements then we need to scan for placeholders. - if (self.image_virtual) { - var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot); - while (v_it.next()) |virtual_p| { - self.prepKittyVirtualPlacement( - t, - &virtual_p, - ) catch |err| { - // For errors we log and continue. We try to place - // other placements even if one fails. - log.warn("error preparing kitty placement err={}", .{err}); - }; - } - } - - // Sort the placements by their Z value. - std.mem.sortUnstable( - imagepkg.Placement, - self.image_placements.items, - {}, - struct { - fn lessThan( - ctx: void, - lhs: imagepkg.Placement, - rhs: imagepkg.Placement, - ) bool { - _ = ctx; - return lhs.z < rhs.z or (lhs.z == rhs.z and lhs.image_id < rhs.image_id); - } - }.lessThan, - ); - - // Find our indices. The values are sorted by z so we can - // find the first placement out of bounds to find the limits. - var bg_end: ?u32 = null; - var text_end: ?u32 = null; - const bg_limit = std.math.minInt(i32) / 2; - for (self.image_placements.items, 0..) |p, i| { - if (bg_end == null and p.z >= bg_limit) { - bg_end = @intCast(i); - } - if (text_end == null and p.z >= 0) { - text_end = @intCast(i); - } - } - - // If we didn't see any images with a z > the bg limit, - // then our bg end is the end of our placement list. - self.image_bg_end = - bg_end orelse @intCast(self.image_placements.items.len); - - // Same idea for the image_text_end. - self.image_text_end = - text_end orelse @intCast(self.image_placements.items.len); - } - - fn prepKittyVirtualPlacement( - self: *Self, - t: *terminal.Terminal, - p: *const terminal.kitty.graphics.unicode.Placement, - ) PrepKittyImageError!void { - const storage = &t.screens.active.kitty_images; - const image = storage.imageById(p.image_id) orelse { - log.warn( - "missing image for virtual placement, ignoring image_id={}", - .{p.image_id}, - ); - return; - }; - - const rp = p.renderPlacement( - storage, - &image, - self.grid_metrics.cell_width, - self.grid_metrics.cell_height, - ) catch |err| { - log.warn("error rendering virtual placement err={}", .{err}); - return; - }; - - // If our placement is zero sized then we don't do anything. - if (rp.dest_width == 0 or rp.dest_height == 0) return; - - const viewport: terminal.point.Point = t.screens.active.pages.pointFromPin( - .viewport, - rp.top_left, - ) orelse { - // This is unreachable with virtual placements because we should - // only ever be looking at virtual placements that are in our - // viewport in the renderer and virtual placements only ever take - // up one row. - unreachable; - }; - - // Prepare the image for the GPU and store the placement. - try self.prepKittyImage(&image); - try self.image_placements.append(self.alloc, .{ - .image_id = image.id, - .x = @intCast(rp.top_left.x), - .y = @intCast(viewport.viewport.y), - .z = -1, - .width = rp.dest_width, - .height = rp.dest_height, - .cell_offset_x = rp.offset_x, - .cell_offset_y = rp.offset_y, - .source_x = rp.source_x, - .source_y = rp.source_y, - .source_width = rp.source_width, - .source_height = rp.source_height, - }); - } - - /// Get the viewport-relative position for this - /// placement and add it to the placements list. - fn prepKittyPlacement( - self: *Self, - t: *terminal.Terminal, - top_y: u32, - bot_y: u32, - image: *const terminal.kitty.graphics.Image, - p: *const terminal.kitty.graphics.ImageStorage.Placement, - ) PrepKittyImageError!void { - // Get the rect for the placement. If this placement doesn't have - // a rect then its virtual or something so skip it. - const rect = p.rect(image.*, t) orelse return; - - // This is expensive but necessary. - const img_top_y = t.screens.active.pages.pointFromPin(.screen, rect.top_left).?.screen.y; - const img_bot_y = t.screens.active.pages.pointFromPin(.screen, rect.bottom_right).?.screen.y; - - // If the selection isn't within our viewport then skip it. - if (img_top_y > bot_y) return; - if (img_bot_y < top_y) return; - - // We need to prep this image for upload if it isn't in the - // cache OR it is in the cache but the transmit time doesn't - // match meaning this image is different. - try self.prepKittyImage(image); - - // Calculate the dimensions of our image, taking in to - // account the rows / columns specified by the placement. - const dest_size = p.calculatedSize(image.*, t); - - // Calculate the source rectangle - const source_x = @min(image.width, p.source_x); - const source_y = @min(image.height, p.source_y); - const source_width = if (p.source_width > 0) - @min(image.width - source_x, p.source_width) - else - image.width; - const source_height = if (p.source_height > 0) - @min(image.height - source_y, p.source_height) - else - image.height; - - // Get the viewport-relative Y position of the placement. - const y_pos: i32 = @as(i32, @intCast(img_top_y)) - @as(i32, @intCast(top_y)); - - // Accumulate the placement - if (dest_size.width > 0 and dest_size.height > 0) { - try self.image_placements.append(self.alloc, .{ - .image_id = image.id, - .x = @intCast(rect.top_left.x), - .y = y_pos, - .z = p.z, - .width = dest_size.width, - .height = dest_size.height, - .cell_offset_x = p.x_offset, - .cell_offset_y = p.y_offset, - .source_x = source_x, - .source_y = source_y, - .source_width = source_width, - .source_height = source_height, - }); - } - } - - const PrepKittyImageError = error{ - OutOfMemory, - ImageConversionError, - }; - - /// Prepare the provided image for upload to the GPU by copying its - /// data with our allocator and setting it to the pending state. - fn prepKittyImage( - self: *Self, - image: *const terminal.kitty.graphics.Image, - ) PrepKittyImageError!void { - // If this image exists and its transmit time is the same we assume - // it is the identical image so we don't need to send it to the GPU. - const gop = try self.images.getOrPut(self.alloc, image.id); - if (gop.found_existing and - gop.value_ptr.transmit_time.order(image.transmit_time) == .eq) - { - return; - } - - // Copy the data into the pending state. - const data = if (self.alloc.dupe( - u8, - image.data, - )) |v| v else |_| { - if (!gop.found_existing) { - // If this is a new entry we can just remove it since it - // was never sent to the GPU. - _ = self.images.remove(image.id); - } else { - // If this was an existing entry, it is invalid and - // we must unload it. - gop.value_ptr.image.markForUnload(); - } - - return error.OutOfMemory; - }; - // Note: we don't need to errdefer free the data because it is - // put into the map immediately below and our errdefer to - // handle our map state will fix this up. - - // Store it in the map - const new_image: Image = .{ - .pending = .{ - .width = image.width, - .height = image.height, - .pixel_format = switch (image.format) { - .gray => .gray, - .gray_alpha => .gray_alpha, - .rgb => .rgb, - .rgba => .rgba, - .png => unreachable, // should be decoded by now - }, - .data = data.ptr, - }, - }; - if (!gop.found_existing) { - gop.value_ptr.* = .{ - .image = new_image, - .transmit_time = undefined, - }; - } else { - gop.value_ptr.image.markForReplace( - self.alloc, - new_image, - ); - } - - // If any error happens, we unload the image and it is invalid. - errdefer gop.value_ptr.image.markForUnload(); - - gop.value_ptr.image.prepForUpload(self.alloc) catch |err| { - log.warn("error preparing kitty image for upload err={}", .{err}); - return error.ImageConversionError; - }; - gop.value_ptr.transmit_time = image.transmit_time; - } - - /// Upload any images to the GPU that need to be uploaded, - /// and remove any images that are no longer needed on the GPU. - fn uploadKittyImages(self: *Self) !void { - var image_it = self.images.iterator(); - while (image_it.next()) |kv| { - const img = &kv.value_ptr.image; - if (img.isUnloading()) { - img.deinit(self.alloc); - self.images.removeByPtr(kv.key_ptr); - continue; - } - if (img.isPending()) try img.upload(self.alloc, &self.api); - } - } - /// Call this any time the background image path changes. /// /// Caller must hold the draw mutex. diff --git a/src/renderer/image.zig b/src/renderer/image.zig index bf0f7b736..dd5d8bed9 100644 --- a/src/renderer/image.zig +++ b/src/renderer/image.zig @@ -2,16 +2,546 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = @import("../quirks.zig").inlineAssert; const wuffs = @import("wuffs"); +const terminal = @import("../terminal/main.zig"); const Renderer = @import("../renderer.zig").Renderer; const GraphicsAPI = Renderer.API; const Texture = GraphicsAPI.Texture; +const CellSize = @import("size.zig").CellSize; + +const log = std.log.scoped(.renderer_image); + +/// Generic image rendering state for the renderer. This stores all +/// images and their placements and exposes only a limited public API +/// for adding images and placements and drawing them. +pub const State = struct { + /// The full image state for the renderer that specifies what images + /// need to be uploaded, pruned, etc. + images: ImageMap, + + /// The placements for the Kitty image protocol. + kitty_placements: std.ArrayListUnmanaged(Placement), + + /// The end index (exclusive) for placements that should be + /// drawn below the background, below the text, etc. + kitty_bg_end: u32, + kitty_text_end: u32, + + /// True if there are any virtual placements. This needs to be known + /// because virtual placements need to be recalculated more often + /// on frame builds and are generally more expensive to handle. + kitty_virtual: bool, + + pub const empty: State = .{ + .images = .empty, + .kitty_placements = .empty, + .kitty_bg_end = 0, + .kitty_text_end = 0, + .kitty_virtual = false, + }; + + pub fn deinit(self: *State, alloc: Allocator) void { + { + var it = self.images.iterator(); + while (it.next()) |kv| kv.value_ptr.image.deinit(alloc); + self.images.deinit(alloc); + } + self.kitty_placements.deinit(alloc); + } + + /// Upload any images to the GPU that need to be uploaded, + /// and remove any images that are no longer needed on the GPU. + /// + /// If any uploads fail, they are ignored. The return value + /// can be used to detect if upload was a total success (true) + /// or not (false). + pub fn upload( + self: *State, + alloc: Allocator, + api: *GraphicsAPI, + ) bool { + var success: bool = true; + var image_it = self.images.iterator(); + while (image_it.next()) |kv| { + const img = &kv.value_ptr.image; + if (img.isUnloading()) { + img.deinit(alloc); + self.images.removeByPtr(kv.key_ptr); + continue; + } + + if (img.isPending()) { + img.upload( + alloc, + api, + ) catch |err| { + log.warn("error uploading image to GPU err={}", .{err}); + success = false; + }; + } + } + + return success; + } + + pub const DrawPlacements = enum { + kitty_below_bg, + kitty_below_text, + kitty_above_text, + }; + + /// Draw the given named set of placements. + /// + /// Any placements that have non-uploaded images are ignored. Any + /// graphics API errors during drawing are also ignored. + pub fn draw( + self: *State, + api: *GraphicsAPI, + pipeline: GraphicsAPI.Pipeline, + pass: *GraphicsAPI.RenderPass, + placement_type: DrawPlacements, + ) void { + const placements: []const Placement = switch (placement_type) { + .kitty_below_bg => self.kitty_placements.items[0..self.kitty_bg_end], + .kitty_below_text => self.kitty_placements.items[self.kitty_bg_end..self.kitty_text_end], + .kitty_above_text => self.kitty_placements.items[self.kitty_text_end..], + }; + + for (placements) |p| { + // Look up the image + const image = self.images.get(p.image_id) orelse { + log.warn("image not found for placement image_id={}", .{p.image_id}); + continue; + }; + + // Get the texture + const texture = switch (image.image) { + .ready, + .unload_ready, + => |t| t, + else => { + log.warn("image not ready for placement image_id={}", .{p.image_id}); + continue; + }, + }; + + // Create our vertex buffer, which is always exactly one item. + // future(mitchellh): we can group rendering multiple instances of a single image + var buf = GraphicsAPI.Buffer(GraphicsAPI.shaders.Image).initFill( + api.imageBufferOptions(), + &.{.{ + .grid_pos = .{ + @as(f32, @floatFromInt(p.x)), + @as(f32, @floatFromInt(p.y)), + }, + + .cell_offset = .{ + @as(f32, @floatFromInt(p.cell_offset_x)), + @as(f32, @floatFromInt(p.cell_offset_y)), + }, + + .source_rect = .{ + @as(f32, @floatFromInt(p.source_x)), + @as(f32, @floatFromInt(p.source_y)), + @as(f32, @floatFromInt(p.source_width)), + @as(f32, @floatFromInt(p.source_height)), + }, + + .dest_size = .{ + @as(f32, @floatFromInt(p.width)), + @as(f32, @floatFromInt(p.height)), + }, + }}, + ) catch |err| { + log.warn("error creating image vertex buffer err={}", .{err}); + continue; + }; + defer buf.deinit(); + + pass.step(.{ + .pipeline = pipeline, + .buffers = &.{buf.buffer}, + .textures = &.{texture}, + .draw = .{ + .type = .triangle_strip, + .vertex_count = 4, + }, + }); + } + } + + /// Returns true if the Kitty graphics state requires an update based + /// on the terminal state and our internal state. + /// + /// This does not read/write state used by drawing. + pub fn kittyRequiresUpdate( + self: *const State, + t: *const terminal.Terminal, + ) bool { + // If the terminal kitty image state is dirty, we must update. + if (t.screens.active.kitty_images.dirty) return true; + + // If we have any virtual references, we must also rebuild our + // kitty state on every frame because any cell change can move + // an image. If the virtual placements were removed, this will + // be set to false on the next update. + if (self.kitty_virtual) return true; + + return false; + } + + /// Update the Kitty graphics state from the terminal. + /// + /// This reads/writes state used by drawing. + pub fn kittyUpdate( + self: *State, + alloc: Allocator, + t: *const terminal.Terminal, + cell_size: CellSize, + ) void { + const storage = &t.screens.active.kitty_images; + defer storage.dirty = false; + + // We always clear our previous placements no matter what because + // we rebuild them from scratch. + self.kitty_placements.clearRetainingCapacity(); + self.kitty_virtual = 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| { + switch (kv.key_ptr.*) { + // We're only looking at Kitty images + .kitty => |id| if (storage.imageById(id) == null) { + kv.value_ptr.image.markForUnload(); + }, + } + } + } + + // The top-left and bottom-right corners of our viewport in screen + // points. This lets us determine offsets and containment of placements. + const top = t.screens.active.pages.getTopLeft(.viewport); + const bot = t.screens.active.pages.getBottomRight(.viewport).?; + const top_y = t.screens.active.pages.pointFromPin(.screen, top).?.screen.y; + const bot_y = t.screens.active.pages.pointFromPin(.screen, bot).?.screen.y; + + // Go through the placements and ensure the image is + // on the GPU or else is ready to be sent to the GPU. + var it = storage.placements.iterator(); + while (it.next()) |kv| { + const p = kv.value_ptr; + + // Special logic based on location + switch (p.location) { + .pin => {}, + .virtual => { + // We need to mark virtual placements on our renderer so that + // we know to rebuild in more scenarios since cell changes can + // now trigger placement changes. + self.kitty_virtual = true; + + // We also continue out because virtual placements are + // only triggered by the unicode placeholder, not by the + // placement itself. + continue; + }, + } + + // Get the image for the placement + const image = storage.imageById(kv.key_ptr.image_id) orelse { + log.warn( + "missing image for placement, ignoring image_id={}", + .{kv.key_ptr.image_id}, + ); + continue; + }; + + self.prepKittyPlacement( + alloc, + t, + top_y, + bot_y, + &image, + p, + ) catch |err| { + // For errors we log and continue. We try to place + // other placements even if one fails. + log.warn("error preparing kitty placement err={}", .{err}); + }; + } + + // If we have virtual placements then we need to scan for placeholders. + if (self.kitty_virtual) { + var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot); + while (v_it.next()) |virtual_p| { + self.prepKittyVirtualPlacement( + alloc, + t, + &virtual_p, + cell_size, + ) catch |err| { + // For errors we log and continue. We try to place + // other placements even if one fails. + log.warn("error preparing kitty placement err={}", .{err}); + }; + } + } + + // Sort the placements by their Z value. + std.mem.sortUnstable( + Placement, + self.kitty_placements.items, + {}, + struct { + fn lessThan( + ctx: void, + lhs: Placement, + rhs: Placement, + ) bool { + _ = ctx; + return lhs.z < rhs.z or + (lhs.z == rhs.z and lhs.image_id.zLessThan(rhs.image_id)); + } + }.lessThan, + ); + + // Find our indices. The values are sorted by z so we can + // find the first placement out of bounds to find the limits. + const bg_limit = std.math.minInt(i32) / 2; + var bg_end: ?u32 = null; + var text_end: ?u32 = null; + for (self.kitty_placements.items, 0..) |p, i| { + if (bg_end == null and p.z >= bg_limit) bg_end = @intCast(i); + if (text_end == null and p.z >= 0) text_end = @intCast(i); + } + + // If we didn't see any images with a z > the bg limit, + // then our bg end is the end of our placement list. + self.kitty_bg_end = + bg_end orelse @intCast(self.kitty_placements.items.len); + // Same idea for the image_text_end. + self.kitty_text_end = + text_end orelse @intCast(self.kitty_placements.items.len); + } + + const PrepKittyImageError = error{ + OutOfMemory, + ImageConversionError, + }; + + /// Get the viewport-relative position for this + /// placement and add it to the placements list. + fn prepKittyPlacement( + self: *State, + alloc: Allocator, + t: *const terminal.Terminal, + top_y: u32, + bot_y: u32, + image: *const terminal.kitty.graphics.Image, + p: *const terminal.kitty.graphics.ImageStorage.Placement, + ) PrepKittyImageError!void { + // Get the rect for the placement. If this placement doesn't have + // a rect then its virtual or something so skip it. + const rect = p.rect(image.*, t) orelse return; + + // This is expensive but necessary. + const img_top_y = t.screens.active.pages.pointFromPin(.screen, rect.top_left).?.screen.y; + const img_bot_y = t.screens.active.pages.pointFromPin(.screen, rect.bottom_right).?.screen.y; + + // If the selection isn't within our viewport then skip it. + if (img_top_y > bot_y) return; + if (img_bot_y < top_y) return; + + // We need to prep this image for upload if it isn't in the + // cache OR it is in the cache but the transmit time doesn't + // match meaning this image is different. + try self.prepKittyImage(alloc, image); + + // Calculate the dimensions of our image, taking in to + // account the rows / columns specified by the placement. + const dest_size = p.calculatedSize(image.*, t); + + // Calculate the source rectangle + const source_x = @min(image.width, p.source_x); + const source_y = @min(image.height, p.source_y); + const source_width = if (p.source_width > 0) + @min(image.width - source_x, p.source_width) + else + image.width; + const source_height = if (p.source_height > 0) + @min(image.height - source_y, p.source_height) + else + image.height; + + // Get the viewport-relative Y position of the placement. + const y_pos: i32 = @as(i32, @intCast(img_top_y)) - @as(i32, @intCast(top_y)); + + // Accumulate the placement + if (dest_size.width > 0 and dest_size.height > 0) { + try self.kitty_placements.append(alloc, .{ + .image_id = .{ .kitty = image.id }, + .x = @intCast(rect.top_left.x), + .y = y_pos, + .z = p.z, + .width = dest_size.width, + .height = dest_size.height, + .cell_offset_x = p.x_offset, + .cell_offset_y = p.y_offset, + .source_x = source_x, + .source_y = source_y, + .source_width = source_width, + .source_height = source_height, + }); + } + } + + fn prepKittyVirtualPlacement( + self: *State, + alloc: Allocator, + t: *const terminal.Terminal, + p: *const terminal.kitty.graphics.unicode.Placement, + cell_size: CellSize, + ) PrepKittyImageError!void { + const storage = &t.screens.active.kitty_images; + const image = storage.imageById(p.image_id) orelse { + log.warn( + "missing image for virtual placement, ignoring image_id={}", + .{p.image_id}, + ); + return; + }; + + const rp = p.renderPlacement( + storage, + &image, + cell_size.width, + cell_size.height, + ) catch |err| { + log.warn("error rendering virtual placement err={}", .{err}); + return; + }; + + // If our placement is zero sized then we don't do anything. + if (rp.dest_width == 0 or rp.dest_height == 0) return; + + const viewport: terminal.point.Point = t.screens.active.pages.pointFromPin( + .viewport, + rp.top_left, + ) orelse { + // This is unreachable with virtual placements because we should + // only ever be looking at virtual placements that are in our + // viewport in the renderer and virtual placements only ever take + // up one row. + unreachable; + }; + + // Prepare the image for the GPU and store the placement. + try self.prepKittyImage(alloc, &image); + try self.kitty_placements.append(alloc, .{ + .image_id = .{ .kitty = image.id }, + .x = @intCast(rp.top_left.x), + .y = @intCast(viewport.viewport.y), + .z = -1, + .width = rp.dest_width, + .height = rp.dest_height, + .cell_offset_x = rp.offset_x, + .cell_offset_y = rp.offset_y, + .source_x = rp.source_x, + .source_y = rp.source_y, + .source_width = rp.source_width, + .source_height = rp.source_height, + }); + } + + /// Prepare the provided image for upload to the GPU by copying its + /// data with our allocator and setting it to the pending state. + fn prepKittyImage( + self: *State, + alloc: Allocator, + image: *const terminal.kitty.graphics.Image, + ) PrepKittyImageError!void { + // If this image exists and its transmit time is the same we assume + // it is the identical image so we don't need to send it to the GPU. + const gop = try self.images.getOrPut( + alloc, + .{ .kitty = image.id }, + ); + if (gop.found_existing and + gop.value_ptr.transmit_time.order(image.transmit_time) == .eq) + { + return; + } + + // Copy the data into the pending state. + const data = if (alloc.dupe( + u8, + image.data, + )) |v| v else |_| { + if (!gop.found_existing) { + // If this is a new entry we can just remove it since it + // was never sent to the GPU. + _ = self.images.remove(.{ .kitty = image.id }); + } else { + // If this was an existing entry, it is invalid and + // we must unload it. + gop.value_ptr.image.markForUnload(); + } + + return error.OutOfMemory; + }; + // Note: we don't need to errdefer free the data because it is + // put into the map immediately below and our errdefer to + // handle our map state will fix this up. + + // Store it in the map + const new_image: Image = .{ + .pending = .{ + .width = image.width, + .height = image.height, + .pixel_format = switch (image.format) { + .gray => .gray, + .gray_alpha => .gray_alpha, + .rgb => .rgb, + .rgba => .rgba, + .png => unreachable, // should be decoded by now + }, + .data = data.ptr, + }, + }; + if (!gop.found_existing) { + gop.value_ptr.* = .{ + .image = new_image, + .transmit_time = undefined, + }; + } else { + gop.value_ptr.image.markForReplace( + alloc, + new_image, + ); + } + + // If any error happens, we unload the image and it is invalid. + errdefer gop.value_ptr.image.markForUnload(); + + gop.value_ptr.image.prepForUpload(alloc) catch |err| { + log.warn("error preparing kitty image for upload err={}", .{err}); + return error.ImageConversionError; + }; + gop.value_ptr.transmit_time = image.transmit_time; + } +}; /// Represents a single image placement on the grid. /// A placement is a request to render an instance of an image. pub const Placement = struct { /// The image being rendered. This MUST be in the image map. - image_id: u32, + image_id: Id, /// The grid x/y where this placement is located. x: i32, @@ -34,8 +564,37 @@ pub const Placement = struct { source_height: u32, }; +/// Image identifier used to store and lookup images. +/// +/// This is tagged by different image types to make it easier to +/// store different kinds of images in the same map without having +/// to worry about ID collisions. +pub const Id = union(enum) { + /// Image sent to the terminal state via the kitty graphics protocol. + /// The value is the ID assigned by the terminal. + kitty: u32, + + /// Z-ordering tie-breaker for images with the same z value. + pub fn zLessThan(lhs: Id, rhs: Id) bool { + // If our tags aren't the same, we sort by tag. + if (std.meta.activeTag(lhs) != std.meta.activeTag(rhs)) { + return switch (lhs) { + // Kitty images always sort before (lower z) non-kitty images. + .kitty => false, + }; + } + + switch (lhs) { + .kitty => |lhs_id| { + const rhs_id = rhs.kitty; + return lhs_id < rhs_id; + }, + } + } +}; + /// The map used for storing images. -pub const ImageMap = std.AutoHashMapUnmanaged(u32, struct { +pub const ImageMap = std.AutoHashMapUnmanaged(Id, struct { image: Image, transmit_time: std.time.Instant, }); @@ -221,27 +780,33 @@ pub const Image = union(enum) { try self.convert(alloc); } - /// Upload the pending image to the GPU and - /// change the state of this image to ready. + /// Upload the pending image to the GPU and change the state of this + /// image to ready. pub fn upload( self: *Image, alloc: Allocator, api: *const GraphicsAPI, - ) !void { + ) (wuffs.Error || error{ + /// Texture creation failed, usually a GPU memory issue. + UploadFailed, + })!void { assert(self.isPending()); + // No error recover is required after this call because it just + // converts in place and is idempotent. try self.prepForUpload(alloc); // Get our pending info const p = self.getPending().?; // Create our texture - const texture = try Texture.init( + const texture = Texture.init( api.imageTextureOptions(.rgba, true), @intCast(p.width), @intCast(p.height), p.dataSlice(), - ); + ) catch return error.UploadFailed; + errdefer comptime unreachable; // Uploaded. We can now clear our data and change our state. //