From 5a9bbcbc2d122d3f527b057d815ecdb4a976f0ce Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 22 Aug 2023 11:32:45 -0700 Subject: [PATCH] renderer/metal: clip image if necessary off top of viewport (scrolling) --- src/renderer/Metal.zig | 65 ++++++++++++++++++++++++++++----- src/renderer/metal/api.zig | 1 + src/renderer/metal/image.zig | 6 +++ src/renderer/metal/shaders.zig | 14 ++++++- src/renderer/shaders/cell.metal | 26 +++++++------ src/terminal/Screen.zig | 5 +++ 6 files changed, 96 insertions(+), 21 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 69bebb852..13947f2ec 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -736,6 +736,8 @@ fn drawImagePlacement( @as(f32, @floatFromInt(p.x)), @as(f32, @floatFromInt(p.y)), }, + + .offset_y = p.offset_y, }}); defer buf.deinit(); @@ -842,21 +844,65 @@ 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 bot = (terminal.point.Viewport{ + .x = screen.cols - 1, + .y = screen.rows - 1, + }).toScreen(screen); + // Go through the placements and ensure the image is loaded on the GPU. var it = screen.kitty_images.placements.iterator(); while (it.next()) |kv| { + // Find the image in storage + const image = screen.kitty_images.imageById(kv.key_ptr.image_id) orelse { + log.warn( + "missing image for placement, ignoring image_id={}", + .{kv.key_ptr.image_id}, + ); + 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. + 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_pixels = offset_cells * self.cell_size.height; + break :offset_y @intCast(offset_pixels); + } else 0; + // If we already know about this image then do nothing const gop = try self.images.getOrPut(self.alloc, kv.key_ptr.image_id); if (!gop.found_existing) { - // Find the image in storage - const image = screen.kitty_images.imageById(kv.key_ptr.image_id) orelse { - log.warn( - "missing image for placement, ignoring image_id={}", - .{kv.key_ptr.image_id}, - ); - continue; - }; - // Copy the data into the pending state. const data = try self.alloc.dupe(u8, image.data); errdefer self.alloc.free(data); @@ -883,6 +929,7 @@ fn prepKittyGraphics( .image_id = kv.key_ptr.image_id, .x = @intCast(viewport.x), .y = @intCast(viewport.y), + .offset_y = offset_y, }); } } diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index 92dc8794b..b4f7a9031 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -41,6 +41,7 @@ pub const MTLVertexFormat = enum(c_ulong) { uchar4 = 3, float2 = 29, int2 = 33, + uint = 36, uint2 = 37, uchar = 45, }; diff --git a/src/renderer/metal/image.zig b/src/renderer/metal/image.zig index 9aaed21ca..7971f3c5c 100644 --- a/src/renderer/metal/image.zig +++ b/src/renderer/metal/image.zig @@ -14,6 +14,12 @@ pub const Placement = struct { /// The grid x/y where this placement is located. x: u32, 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 map used for storing images. diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index c57613fd6..4119456bb 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -57,9 +57,10 @@ pub const Cell = extern struct { }; }; -/// Single parameter for the image shader. +/// Single parameter for the image shader. See shader for field details. pub const Image = extern struct { grid_pos: [2]f32, + offset_y: u32, }; /// The uniforms that are passed to the terminal cell shader. @@ -336,6 +337,17 @@ fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object { attr.setProperty("offset", @as(c_ulong, @offsetOf(Image, "grid_pos"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } + { + const attr = attrs.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 2)}, + ); + + attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uint)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(Image, "offset_y"))); + attr.setProperty("bufferIndex", @as(c_ulong, 0)); + } // The layout describes how and when we fetch the next vertex input. const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts")); diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index c0bf9eee8..5a405356c 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -194,6 +194,9 @@ struct ImageVertexIn { // The grid coordinates (x, y) where x < columns and y < rows where // the image will be rendered. It will be rendered from the top left. float2 grid_pos [[ attribute(1) ]]; + + // The offset for the texture coordinates. + uint offset_y [[ attribute(2) ]]; }; struct ImageVertexOut { @@ -207,9 +210,6 @@ vertex ImageVertexOut image_vertex( texture2d image [[ texture(0) ]], constant Uniforms &uniforms [[ buffer(1) ]] ) { - // The position of our image starts at the top-left of the grid cell. - float2 image_pos = uniforms.cell_size * input.grid_pos; - // The size of the image in pixels float2 image_size = float2(image.get_width(), image.get_height()); @@ -227,18 +227,22 @@ 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; + ImageVertexOut out; - // Our final position is our image position multiplied by the on/off - // position based on corners above. - image_pos = image_pos + image_size * position; + // The position of our image starts at the top-left of the grid cell. + float2 image_pos = uniforms.cell_size * input.grid_pos; + + // 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; - // Output position is just our cell top-left. out.position = uniforms.projection_matrix * float4(image_pos.x, image_pos.y, 0.0f, 1.0f); - - // Calculate the texture coordinate in pixels and normalize it to [0, 1] - out.tex_coord = position; - + out.tex_coord = tex_coord; return out; } diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index e53d1830c..976ccb10d 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1542,6 +1542,11 @@ pub const Scroll = union(enum) { /// want to do that yet (i.e. are they writing to the end of the screen /// or not). pub fn scroll(self: *Screen, behavior: Scroll) !void { + // No matter what, scrolling marks our image state as dirty since + // it could move placements. If there are no placements or no images + // this is still a very cheap operation. + self.kitty_images.dirty = true; + switch (behavior) { // Setting viewport offset to zero makes row 0 be at self.top // which is the top!