mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
renderer/metal: clip image if necessary off top of viewport (scrolling)
This commit is contained in:
@ -736,6 +736,8 @@ fn drawImagePlacement(
|
|||||||
@as(f32, @floatFromInt(p.x)),
|
@as(f32, @floatFromInt(p.x)),
|
||||||
@as(f32, @floatFromInt(p.y)),
|
@as(f32, @floatFromInt(p.y)),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.offset_y = p.offset_y,
|
||||||
}});
|
}});
|
||||||
defer buf.deinit();
|
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.
|
// 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| {
|
||||||
|
// 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
|
// If we already know about this image then do nothing
|
||||||
const gop = try self.images.getOrPut(self.alloc, kv.key_ptr.image_id);
|
const gop = try self.images.getOrPut(self.alloc, kv.key_ptr.image_id);
|
||||||
if (!gop.found_existing) {
|
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.
|
// Copy the data into the pending state.
|
||||||
const data = try self.alloc.dupe(u8, image.data);
|
const data = try self.alloc.dupe(u8, image.data);
|
||||||
errdefer self.alloc.free(data);
|
errdefer self.alloc.free(data);
|
||||||
@ -883,6 +929,7 @@ fn prepKittyGraphics(
|
|||||||
.image_id = kv.key_ptr.image_id,
|
.image_id = kv.key_ptr.image_id,
|
||||||
.x = @intCast(viewport.x),
|
.x = @intCast(viewport.x),
|
||||||
.y = @intCast(viewport.y),
|
.y = @intCast(viewport.y),
|
||||||
|
.offset_y = offset_y,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ pub const MTLVertexFormat = enum(c_ulong) {
|
|||||||
uchar4 = 3,
|
uchar4 = 3,
|
||||||
float2 = 29,
|
float2 = 29,
|
||||||
int2 = 33,
|
int2 = 33,
|
||||||
|
uint = 36,
|
||||||
uint2 = 37,
|
uint2 = 37,
|
||||||
uchar = 45,
|
uchar = 45,
|
||||||
};
|
};
|
||||||
|
@ -14,6 +14,12 @@ pub const Placement = struct {
|
|||||||
/// The grid x/y where this placement is located.
|
/// The grid x/y where this placement is located.
|
||||||
x: u32,
|
x: u32,
|
||||||
y: 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.
|
/// The map used for storing images.
|
||||||
|
@ -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 {
|
pub const Image = extern struct {
|
||||||
grid_pos: [2]f32,
|
grid_pos: [2]f32,
|
||||||
|
offset_y: u32,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The uniforms that are passed to the terminal cell shader.
|
/// 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("offset", @as(c_ulong, @offsetOf(Image, "grid_pos")));
|
||||||
attr.setProperty("bufferIndex", @as(c_ulong, 0));
|
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.
|
// The layout describes how and when we fetch the next vertex input.
|
||||||
const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts"));
|
const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts"));
|
||||||
|
@ -194,6 +194,9 @@ struct ImageVertexIn {
|
|||||||
// The grid coordinates (x, y) where x < columns and y < rows where
|
// 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.
|
// the image will be rendered. It will be rendered from the top left.
|
||||||
float2 grid_pos [[ attribute(1) ]];
|
float2 grid_pos [[ attribute(1) ]];
|
||||||
|
|
||||||
|
// The offset for the texture coordinates.
|
||||||
|
uint offset_y [[ attribute(2) ]];
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ImageVertexOut {
|
struct ImageVertexOut {
|
||||||
@ -207,9 +210,6 @@ vertex ImageVertexOut image_vertex(
|
|||||||
texture2d<uint> image [[ texture(0) ]],
|
texture2d<uint> image [[ texture(0) ]],
|
||||||
constant Uniforms &uniforms [[ buffer(1) ]]
|
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
|
// The size of the image in pixels
|
||||||
float2 image_size = float2(image.get_width(), image.get_height());
|
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.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f;
|
||||||
position.y = (vid == 0 || vid == 3) ? 0.0f : 1.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;
|
ImageVertexOut out;
|
||||||
|
|
||||||
// Our final position is our image position multiplied by the on/off
|
// The position of our image starts at the top-left of the grid cell.
|
||||||
// position based on corners above.
|
float2 image_pos = uniforms.cell_size * input.grid_pos;
|
||||||
image_pos = image_pos + image_size * position;
|
|
||||||
|
// 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);
|
out.position = uniforms.projection_matrix * float4(image_pos.x, image_pos.y, 0.0f, 1.0f);
|
||||||
|
out.tex_coord = tex_coord;
|
||||||
// Calculate the texture coordinate in pixels and normalize it to [0, 1]
|
|
||||||
out.tex_coord = position;
|
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
/// want to do that yet (i.e. are they writing to the end of the screen
|
||||||
/// or not).
|
/// or not).
|
||||||
pub fn scroll(self: *Screen, behavior: Scroll) !void {
|
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) {
|
switch (behavior) {
|
||||||
// Setting viewport offset to zero makes row 0 be at self.top
|
// Setting viewport offset to zero makes row 0 be at self.top
|
||||||
// which is the top!
|
// which is the top!
|
||||||
|
Reference in New Issue
Block a user