mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
terminal/kitty-gfx: process source rectangle display params
This commit is contained in:
@ -528,7 +528,7 @@ pub fn render(
|
|||||||
// We only do this if the Kitty image state is dirty meaning only if
|
// We only do this if the Kitty image state is dirty meaning only if
|
||||||
// it changes.
|
// it changes.
|
||||||
if (state.terminal.screen.kitty_images.dirty) {
|
if (state.terminal.screen.kitty_images.dirty) {
|
||||||
try self.prepKittyGraphics(&state.terminal.screen);
|
try self.prepKittyGraphics(state.terminal);
|
||||||
}
|
}
|
||||||
|
|
||||||
break :critical .{
|
break :critical .{
|
||||||
@ -742,7 +742,12 @@ fn drawImagePlacement(
|
|||||||
@as(f32, @floatFromInt(p.cell_offset_y)),
|
@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();
|
defer buf.deinit();
|
||||||
|
|
||||||
@ -827,9 +832,10 @@ 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: *terminal.Screen,
|
t: *terminal.Terminal,
|
||||||
) !void {
|
) !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 always clear our previous placements no matter what because
|
||||||
// we rebuild them from scratch.
|
// we rebuild them from scratch.
|
||||||
@ -843,7 +849,7 @@ fn prepKittyGraphics(
|
|||||||
{
|
{
|
||||||
var it = self.images.iterator();
|
var it = self.images.iterator();
|
||||||
while (it.next()) |kv| {
|
while (it.next()) |kv| {
|
||||||
if (screen.kitty_images.imageById(kv.key_ptr.*) == null) {
|
if (storage.imageById(kv.key_ptr.*) == null) {
|
||||||
kv.value_ptr.markForUnload();
|
kv.value_ptr.markForUnload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -851,17 +857,18 @@ fn prepKittyGraphics(
|
|||||||
|
|
||||||
// The top-left and bottom-right corners of our viewport in screen
|
// The top-left and bottom-right corners of our viewport in screen
|
||||||
// points. This lets us determine offsets and containment of placements.
|
// 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{
|
const bot = (terminal.point.Viewport{
|
||||||
.x = screen.cols - 1,
|
.x = t.screen.cols - 1,
|
||||||
.y = screen.rows - 1,
|
.y = t.screen.rows - 1,
|
||||||
}).toScreen(screen);
|
}).toScreen(&t.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 = storage.placements.iterator();
|
||||||
while (it.next()) |kv| {
|
while (it.next()) |kv| {
|
||||||
// Find the image in storage
|
// 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(
|
log.warn(
|
||||||
"missing image for placement, ignoring image_id={}",
|
"missing image for placement, ignoring image_id={}",
|
||||||
.{kv.key_ptr.image_id},
|
.{kv.key_ptr.image_id},
|
||||||
@ -869,38 +876,14 @@ fn prepKittyGraphics(
|
|||||||
continue;
|
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 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 (!image_sel.within(top, bot)) continue;
|
||||||
|
|
||||||
// If the top left is outside the viewport we need to calc an offset
|
// 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.
|
// 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_y: u32 = if (image_sel.start.y < t.screen.viewport) offset_y: {
|
||||||
const offset_cells = screen.viewport - image_sel.start.y;
|
const offset_cells = t.screen.viewport - image_sel.start.y;
|
||||||
const offset_pixels = offset_cells * self.cell_size.height;
|
const offset_pixels = offset_cells * self.cell_size.height;
|
||||||
break :offset_y @intCast(offset_pixels);
|
break :offset_y @intCast(offset_pixels);
|
||||||
} else 0;
|
} else 0;
|
||||||
@ -913,31 +896,48 @@ fn prepKittyGraphics(
|
|||||||
errdefer self.alloc.free(data);
|
errdefer self.alloc.free(data);
|
||||||
|
|
||||||
// Store it in the map
|
// Store it in the map
|
||||||
const p: Image.Pending = .{
|
const pending: Image.Pending = .{
|
||||||
.width = image.width,
|
.width = image.width,
|
||||||
.height = image.height,
|
.height = image.height,
|
||||||
.data = data.ptr,
|
.data = data.ptr,
|
||||||
};
|
};
|
||||||
|
|
||||||
gop.value_ptr.* = switch (image.format) {
|
gop.value_ptr.* = switch (image.format) {
|
||||||
.rgb => .{ .pending_rgb = p },
|
.rgb => .{ .pending_rgb = pending },
|
||||||
.rgba => .{ .pending_rgba = p },
|
.rgba => .{ .pending_rgba = pending },
|
||||||
.png => unreachable, // should be decoded by now
|
.png => unreachable, // should be decoded by now
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert our screen point to a viewport point
|
// 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
|
// Accumulate the placement
|
||||||
try self.image_placements.append(self.alloc, .{
|
if (image.width > 0 and image.height > 0) {
|
||||||
.image_id = kv.key_ptr.image_id,
|
try self.image_placements.append(self.alloc, .{
|
||||||
.x = @intCast(kv.value_ptr.point.x),
|
.image_id = kv.key_ptr.image_id,
|
||||||
.y = @intCast(viewport.y),
|
.x = @intCast(kv.value_ptr.point.x),
|
||||||
.cell_offset_x = kv.value_ptr.x_offset,
|
.y = @intCast(viewport.y),
|
||||||
.cell_offset_y = kv.value_ptr.y_offset,
|
.cell_offset_x = kv.value_ptr.x_offset,
|
||||||
.offset_y = offset_y,
|
.cell_offset_y = kv.value_ptr.y_offset,
|
||||||
});
|
.source_x = source_x,
|
||||||
|
.source_y = source_y,
|
||||||
|
.source_width = source_width,
|
||||||
|
.source_height = source_height,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ pub const MTLIndexType = enum(c_ulong) {
|
|||||||
pub const MTLVertexFormat = enum(c_ulong) {
|
pub const MTLVertexFormat = enum(c_ulong) {
|
||||||
uchar4 = 3,
|
uchar4 = 3,
|
||||||
float2 = 29,
|
float2 = 29,
|
||||||
|
float4 = 31,
|
||||||
int2 = 33,
|
int2 = 33,
|
||||||
uint = 36,
|
uint = 36,
|
||||||
uint2 = 37,
|
uint2 = 37,
|
||||||
|
@ -20,11 +20,11 @@ pub const Placement = struct {
|
|||||||
cell_offset_x: u32,
|
cell_offset_x: u32,
|
||||||
cell_offset_y: u32,
|
cell_offset_y: u32,
|
||||||
|
|
||||||
/// The offset of the top of the image texture in case we are clipping
|
/// The source rectangle of the placement.
|
||||||
/// the top. We don't need an offset_x because we don't support any
|
source_x: u32,
|
||||||
/// horizontal scrolling so the width is never clipped from the left.
|
source_y: u32,
|
||||||
/// Clipping from the bottom/right is handled by the shader.
|
source_width: u32,
|
||||||
offset_y: u32,
|
source_height: u32,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The map used for storing images.
|
/// The map used for storing images.
|
||||||
|
@ -61,7 +61,7 @@ pub const Cell = extern struct {
|
|||||||
pub const Image = extern struct {
|
pub const Image = extern struct {
|
||||||
grid_pos: [2]f32,
|
grid_pos: [2]f32,
|
||||||
cell_offset: [2]f32,
|
cell_offset: [2]f32,
|
||||||
offset_y: u32,
|
source_rect: [4]f32,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The uniforms that are passed to the terminal cell shader.
|
/// 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)},
|
.{@as(c_ulong, 3)},
|
||||||
);
|
);
|
||||||
|
|
||||||
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uint));
|
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.float4));
|
||||||
attr.setProperty("offset", @as(c_ulong, @offsetOf(Image, "offset_y")));
|
attr.setProperty("offset", @as(c_ulong, @offsetOf(Image, "source_rect")));
|
||||||
attr.setProperty("bufferIndex", @as(c_ulong, 0));
|
attr.setProperty("bufferIndex", @as(c_ulong, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,8 +199,8 @@ struct ImageVertexIn {
|
|||||||
// corner of the image.
|
// corner of the image.
|
||||||
float2 cell_offset [[ attribute(2) ]];
|
float2 cell_offset [[ attribute(2) ]];
|
||||||
|
|
||||||
// The offset for the texture coordinates.
|
// The source rectangle of the texture to sample from.
|
||||||
uint offset_y [[ attribute(3) ]];
|
float4 source_rect [[ attribute(3) ]];
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ImageVertexOut {
|
struct ImageVertexOut {
|
||||||
@ -231,19 +231,18 @@ 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)
|
// The texture coordinates start at our source x/y, then add the width/height
|
||||||
// then we need to offset the y by offset_y for clipping.
|
// as enabled by our instance id, then normalize to [0, 1]
|
||||||
float2 tex_coord = position;
|
float2 tex_coord = input.source_rect.xy;
|
||||||
if (tex_coord.y == 0) tex_coord.y = input.offset_y / image_size.y;
|
tex_coord += input.source_rect.zw * position;
|
||||||
|
tex_coord /= image_size;
|
||||||
|
|
||||||
ImageVertexOut out;
|
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;
|
float2 image_pos = (uniforms.cell_size * input.grid_pos) + input.cell_offset;
|
||||||
|
image_pos += input.source_rect.zw * 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;
|
|
||||||
|
|
||||||
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;
|
out.tex_coord = tex_coord;
|
||||||
|
@ -172,6 +172,10 @@ fn display(
|
|||||||
.point = placement_point,
|
.point = placement_point,
|
||||||
.x_offset = d.x_offset,
|
.x_offset = d.x_offset,
|
||||||
.y_offset = d.y_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| {
|
storage.addPlacement(alloc, img.id, d.placement_id, p) catch |err| {
|
||||||
encodeError(&result, err);
|
encodeError(&result, err);
|
||||||
@ -184,23 +188,17 @@ fn display(
|
|||||||
.after => {
|
.after => {
|
||||||
const p_sel = p.selection(img, terminal);
|
const p_sel = p.selection(img, terminal);
|
||||||
|
|
||||||
// If we are moving beneath the screen we need to scroll.
|
// We can do better by doing this with pure internal screen state
|
||||||
// TODO: handle scroll regions
|
// but this handles scroll regions.
|
||||||
var new_y = p_sel.end.y + 1;
|
const height = p_sel.end.y - p_sel.start.y + 1;
|
||||||
if (new_y >= terminal.rows) {
|
for (0..height) |_| terminal.index() catch |err| {
|
||||||
const scroll_amount = (new_y + 1) - terminal.rows;
|
log.warn("failed to move cursor: {}", .{err});
|
||||||
terminal.screen.scroll(.{ .screen = @intCast(scroll_amount) }) catch |err| {
|
break;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move the cursor
|
|
||||||
terminal.setCursorPos(
|
terminal.setCursorPos(
|
||||||
new_y,
|
terminal.screen.cursor.y + 1,
|
||||||
p_sel.end.x,
|
p_sel.end.x + 1,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -160,6 +160,12 @@ pub const ImageStorage = struct {
|
|||||||
x_offset: u32 = 0,
|
x_offset: u32 = 0,
|
||||||
y_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
|
/// Returns a selection of the entire rectangle this placement
|
||||||
/// occupies within the screen.
|
/// occupies within the screen.
|
||||||
pub fn selection(
|
pub fn selection(
|
||||||
@ -175,9 +181,13 @@ pub const ImageStorage = struct {
|
|||||||
const cell_width_f64 = terminal_width_f64 / grid_columns_f64;
|
const cell_width_f64 = terminal_width_f64 / grid_columns_f64;
|
||||||
const cell_height_f64 = terminal_height_f64 / grid_rows_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
|
// Calculate our image size in grid cells
|
||||||
const width_f64: f64 = @floatFromInt(image.width);
|
const width_f64: f64 = @floatFromInt(width_px);
|
||||||
const height_f64: f64 = @floatFromInt(image.height);
|
const height_f64: f64 = @floatFromInt(height_px);
|
||||||
const width_cells: u32 = @intFromFloat(@ceil(width_f64 / cell_width_f64));
|
const width_cells: u32 = @intFromFloat(@ceil(width_f64 / cell_width_f64));
|
||||||
const height_cells: u32 = @intFromFloat(@ceil(height_f64 / cell_height_f64));
|
const height_cells: u32 = @intFromFloat(@ceil(height_f64 / cell_height_f64));
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user