diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index b7be691b4..ba7fa1240 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -528,7 +528,7 @@ pub fn render( // 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); + try self.prepKittyGraphics(state.terminal); } break :critical .{ @@ -742,7 +742,12 @@ fn drawImagePlacement( @as(f32, @floatFromInt(p.cell_offset_y)), }, - .offset_y = p.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)), + }, }}); defer buf.deinit(); @@ -827,9 +832,10 @@ fn drawCells( /// the visible images are loaded on the GPU. fn prepKittyGraphics( self: *Metal, - screen: *terminal.Screen, + t: *terminal.Terminal, ) !void { - defer screen.kitty_images.dirty = false; + const storage = &t.screen.kitty_images; + defer storage.dirty = false; // We always clear our previous placements no matter what because // we rebuild them from scratch. @@ -843,7 +849,7 @@ fn prepKittyGraphics( { var it = self.images.iterator(); while (it.next()) |kv| { - if (screen.kitty_images.imageById(kv.key_ptr.*) == null) { + if (storage.imageById(kv.key_ptr.*) == null) { kv.value_ptr.markForUnload(); } } @@ -851,17 +857,18 @@ fn prepKittyGraphics( // The top-left and bottom-right corners of our viewport in screen // points. This lets us determine offsets and containment of placements. - const top = (terminal.point.Viewport{}).toScreen(screen); + const top = (terminal.point.Viewport{}).toScreen(&t.screen); const bot = (terminal.point.Viewport{ - .x = screen.cols - 1, - .y = screen.rows - 1, - }).toScreen(screen); + .x = t.screen.cols - 1, + .y = t.screen.rows - 1, + }).toScreen(&t.screen); // Go through the placements and ensure the image is loaded on the GPU. - var it = screen.kitty_images.placements.iterator(); + var it = storage.placements.iterator(); while (it.next()) |kv| { // Find the image in storage - const image = screen.kitty_images.imageById(kv.key_ptr.image_id) orelse { + const p = kv.value_ptr; + const image = storage.imageById(kv.key_ptr.image_id) orelse { log.warn( "missing image for placement, ignoring image_id={}", .{kv.key_ptr.image_id}, @@ -869,38 +876,14 @@ fn prepKittyGraphics( continue; }; - // We want the width/height of the image in cells to figure out - // if this image is within our viewport. We use floats here because - // we want to round UP so that if any part of the image is in a cell, - // we count the cell. - const image_grid_size: renderer.GridSize = grid_size: { - const width_f64: f64 = @floatFromInt(image.width); - const height_f64: f64 = @floatFromInt(image.height); - const cell_width_f64: f64 = @floatFromInt(self.cell_size.width); - const cell_height_f64: f64 = @floatFromInt(self.cell_size.height); - const width_cells: u32 = @intFromFloat(@ceil(width_f64 / cell_width_f64)); - const height_cells: u32 = @intFromFloat(@ceil(height_f64 / cell_height_f64)); - break :grid_size .{ .columns = width_cells, .rows = height_cells }; - }; - - // Create a "selection" across the image. This is how we detect - // whether the image is in our viewport by detecting whether the - // selection is in our viewport. - const image_sel: terminal.Selection = .{ - .start = kv.value_ptr.point, - .end = .{ - .x = kv.value_ptr.point.x + image_grid_size.columns, - .y = kv.value_ptr.point.y + image_grid_size.rows, - }, - }; - // If the selection isn't within our viewport then skip it. + const image_sel = kv.value_ptr.selection(image, t); if (!image_sel.within(top, bot)) continue; // If the top left is outside the viewport we need to calc an offset // so that we render (0, 0) with some offset for the texture. - const offset_y: u32 = if (image_sel.start.y < screen.viewport) offset_y: { - const offset_cells = screen.viewport - image_sel.start.y; + const offset_y: u32 = if (image_sel.start.y < t.screen.viewport) offset_y: { + const offset_cells = t.screen.viewport - image_sel.start.y; const offset_pixels = offset_cells * self.cell_size.height; break :offset_y @intCast(offset_pixels); } else 0; @@ -913,31 +896,48 @@ fn prepKittyGraphics( errdefer self.alloc.free(data); // Store it in the map - const p: Image.Pending = .{ + const pending: Image.Pending = .{ .width = image.width, .height = image.height, .data = data.ptr, }; gop.value_ptr.* = switch (image.format) { - .rgb => .{ .pending_rgb = p }, - .rgba => .{ .pending_rgba = p }, + .rgb => .{ .pending_rgb = pending }, + .rgba => .{ .pending_rgba = pending }, .png => unreachable, // should be decoded by now }; } // Convert our screen point to a viewport point - const viewport = kv.value_ptr.point.toViewport(screen); + const viewport = kv.value_ptr.point.toViewport(&t.screen); + + // Calculate the source rectangle + const source_x = @min(image.width, p.source_x); + const source_y = @min(image.height, p.source_y + offset_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, p.source_height) + else + image.height -| offset_y; // Accumulate the placement - try self.image_placements.append(self.alloc, .{ - .image_id = kv.key_ptr.image_id, - .x = @intCast(kv.value_ptr.point.x), - .y = @intCast(viewport.y), - .cell_offset_x = kv.value_ptr.x_offset, - .cell_offset_y = kv.value_ptr.y_offset, - .offset_y = offset_y, - }); + if (image.width > 0 and image.height > 0) { + try self.image_placements.append(self.alloc, .{ + .image_id = kv.key_ptr.image_id, + .x = @intCast(kv.value_ptr.point.x), + .y = @intCast(viewport.y), + .cell_offset_x = kv.value_ptr.x_offset, + .cell_offset_y = kv.value_ptr.y_offset, + .source_x = source_x, + .source_y = source_y, + .source_width = source_width, + .source_height = source_height, + }); + } } } diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index b4f7a9031..f3dc2f835 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -40,6 +40,7 @@ pub const MTLIndexType = enum(c_ulong) { pub const MTLVertexFormat = enum(c_ulong) { uchar4 = 3, float2 = 29, + float4 = 31, int2 = 33, uint = 36, uint2 = 37, diff --git a/src/renderer/metal/image.zig b/src/renderer/metal/image.zig index 3a028f9e9..353dd510a 100644 --- a/src/renderer/metal/image.zig +++ b/src/renderer/metal/image.zig @@ -20,11 +20,11 @@ pub const Placement = struct { cell_offset_x: u32, cell_offset_y: u32, - /// The offset of the top of the image texture in case we are clipping - /// the top. We don't need an offset_x because we don't support any - /// horizontal scrolling so the width is never clipped from the left. - /// Clipping from the bottom/right is handled by the shader. - offset_y: u32, + /// The source rectangle of the placement. + source_x: u32, + source_y: u32, + source_width: u32, + source_height: u32, }; /// The map used for storing images. diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index b080bfb89..663be1bf9 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -61,7 +61,7 @@ pub const Cell = extern struct { pub const Image = extern struct { grid_pos: [2]f32, cell_offset: [2]f32, - offset_y: u32, + source_rect: [4]f32, }; /// The uniforms that are passed to the terminal cell shader. @@ -356,8 +356,8 @@ fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object { .{@as(c_ulong, 3)}, ); - attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uint)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Image, "offset_y"))); + attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.float4)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(Image, "source_rect"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index cc533bf42..22004f6e5 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -199,8 +199,8 @@ struct ImageVertexIn { // corner of the image. float2 cell_offset [[ attribute(2) ]]; - // The offset for the texture coordinates. - uint offset_y [[ attribute(3) ]]; + // The source rectangle of the texture to sample from. + float4 source_rect [[ attribute(3) ]]; }; struct ImageVertexOut { @@ -231,19 +231,18 @@ vertex ImageVertexOut image_vertex( position.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f; position.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f; - // The texture coordinates are in [0, 1]. If we're at top y (y == 0) - // then we need to offset the y by offset_y for clipping. - float2 tex_coord = position; - if (tex_coord.y == 0) tex_coord.y = input.offset_y / image_size.y; + // The texture coordinates start at our source x/y, then add the width/height + // as enabled by our instance id, then normalize to [0, 1] + float2 tex_coord = input.source_rect.xy; + tex_coord += input.source_rect.zw * position; + tex_coord /= image_size; ImageVertexOut out; - // The position of our image starts at the top-left of the grid cell. + // The position of our image starts at the top-left of the grid cell and + // adds the source rect width/height components. float2 image_pos = (uniforms.cell_size * input.grid_pos) + input.cell_offset; - - // We need to adjust the bottom y of the image by offset y otherwise - // as we scroll the full image will be rendered and stretched. - image_pos += float2(image_size.x, image_size.y - input.offset_y) * position; + image_pos += input.source_rect.zw * position; out.position = uniforms.projection_matrix * float4(image_pos.x, image_pos.y, 0.0f, 1.0f); out.tex_coord = tex_coord; diff --git a/src/terminal/kitty/graphics_exec.zig b/src/terminal/kitty/graphics_exec.zig index c37ee2f54..cddf63853 100644 --- a/src/terminal/kitty/graphics_exec.zig +++ b/src/terminal/kitty/graphics_exec.zig @@ -172,6 +172,10 @@ fn display( .point = placement_point, .x_offset = d.x_offset, .y_offset = d.y_offset, + .source_x = d.x, + .source_y = d.y, + .source_width = d.width, + .source_height = d.height, }; storage.addPlacement(alloc, img.id, d.placement_id, p) catch |err| { encodeError(&result, err); @@ -184,23 +188,17 @@ fn display( .after => { const p_sel = p.selection(img, terminal); - // If we are moving beneath the screen we need to scroll. - // TODO: handle scroll regions - var new_y = p_sel.end.y + 1; - if (new_y >= terminal.rows) { - const scroll_amount = (new_y + 1) - terminal.rows; - terminal.screen.scroll(.{ .screen = @intCast(scroll_amount) }) catch |err| { - // If this failed we just warn, the screen will just be in a - // weird state but nothing fatal. - log.warn("scroll for image failed: {}", .{err}); - }; - new_y = terminal.rows - 1; - } + // We can do better by doing this with pure internal screen state + // but this handles scroll regions. + const height = p_sel.end.y - p_sel.start.y + 1; + for (0..height) |_| terminal.index() catch |err| { + log.warn("failed to move cursor: {}", .{err}); + break; + }; - // Move the cursor terminal.setCursorPos( - new_y, - p_sel.end.x, + terminal.screen.cursor.y + 1, + p_sel.end.x + 1, ); }, } diff --git a/src/terminal/kitty/graphics_storage.zig b/src/terminal/kitty/graphics_storage.zig index 6d14e7757..c7baa7c75 100644 --- a/src/terminal/kitty/graphics_storage.zig +++ b/src/terminal/kitty/graphics_storage.zig @@ -160,6 +160,12 @@ pub const ImageStorage = struct { x_offset: u32 = 0, y_offset: u32 = 0, + /// Source rectangle for the image to pull from + source_x: u32 = 0, + source_y: u32 = 0, + source_width: u32 = 0, + source_height: u32 = 0, + /// Returns a selection of the entire rectangle this placement /// occupies within the screen. pub fn selection( @@ -175,9 +181,13 @@ pub const ImageStorage = struct { const cell_width_f64 = terminal_width_f64 / grid_columns_f64; const cell_height_f64 = terminal_height_f64 / grid_rows_f64; + // Our image width + const width_px = if (self.source_width > 0) self.source_width else image.width; + const height_px = if (self.source_height > 0) self.source_height else image.height; + // Calculate our image size in grid cells - const width_f64: f64 = @floatFromInt(image.width); - const height_f64: f64 = @floatFromInt(image.height); + const width_f64: f64 = @floatFromInt(width_px); + const height_f64: f64 = @floatFromInt(height_px); const width_cells: u32 = @intFromFloat(@ceil(width_f64 / cell_width_f64)); const height_cells: u32 = @intFromFloat(@ceil(height_f64 / cell_height_f64));