mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
renderer/metal: rewrite kitty placeholder handling
This commit is contained in:
@ -1679,6 +1679,9 @@ fn prepKittyVirtualPlacement(
|
|||||||
bot: *const terminal.Pin,
|
bot: *const terminal.Pin,
|
||||||
p: *const terminal.kitty.graphics.unicode.Placement,
|
p: *const terminal.kitty.graphics.unicode.Placement,
|
||||||
) !void {
|
) !void {
|
||||||
|
_ = top;
|
||||||
|
_ = bot;
|
||||||
|
|
||||||
const storage = &t.screen.kitty_images;
|
const storage = &t.screen.kitty_images;
|
||||||
const image = storage.imageById(p.image_id) orelse {
|
const image = storage.imageById(p.image_id) orelse {
|
||||||
log.warn(
|
log.warn(
|
||||||
@ -1716,18 +1719,23 @@ fn prepKittyVirtualPlacement(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate our grid size for the placement. If it is isn't explicitly
|
// Calculate the grid size for the placement. For virtual placements,
|
||||||
// provided by the placement we try to calculate it to fit in the
|
// we use the requested row/cols. If either isn't specified, we choose
|
||||||
// grid as closely as possible.
|
// the best size based on the image size to fit the entire image in its
|
||||||
|
// original size.
|
||||||
|
//
|
||||||
|
// This part of the code does NOT do preserve any aspect ratios. Its
|
||||||
|
// dumbly fitting the image into the grid size -- possibly user specified.
|
||||||
const img_grid: renderer.GridSize = grid: {
|
const img_grid: renderer.GridSize = grid: {
|
||||||
|
// Use requested rows/columns if specified
|
||||||
var rows = placement.rows;
|
var rows = placement.rows;
|
||||||
var columns = placement.columns;
|
var columns = placement.columns;
|
||||||
|
|
||||||
|
// For unspecified rows/columns, calculate based on the image size.
|
||||||
if (rows == 0) {
|
if (rows == 0) {
|
||||||
const cell_height = self.grid_metrics.cell_height;
|
const cell_height = self.grid_metrics.cell_height;
|
||||||
rows = (image.height + cell_height - 1) / cell_height;
|
rows = (image.height + cell_height - 1) / cell_height;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (columns == 0) {
|
if (columns == 0) {
|
||||||
const cell_width = self.grid_metrics.cell_width;
|
const cell_width = self.grid_metrics.cell_width;
|
||||||
columns = (image.width + cell_width - 1) / cell_width;
|
columns = (image.width + cell_width - 1) / cell_width;
|
||||||
@ -1751,11 +1759,17 @@ fn prepKittyVirtualPlacement(
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// The image is fit into the placement grid size. We need to calculate
|
// Next we have to fit the source image into the grid size while preserving
|
||||||
// various offsets in order to center the image vertically/horizontally
|
// aspect ratio. We will center the image horizontally/vertically if
|
||||||
// into the grid size while preserving the aspect ratio.
|
// necessary.
|
||||||
|
|
||||||
|
// The offsets are the pixel offsets from the top-left of the top-left
|
||||||
|
// grid cell in order to center the image as best as possible.
|
||||||
var x_offset: f64 = 0;
|
var x_offset: f64 = 0;
|
||||||
var y_offset: f64 = 0;
|
var y_offset: f64 = 0;
|
||||||
|
|
||||||
|
// The scale factors are the scaling factors applied to the original
|
||||||
|
// image size in order to fit it into our placement grid size.
|
||||||
var x_scale: f64 = 0;
|
var x_scale: f64 = 0;
|
||||||
var y_scale: f64 = 0;
|
var y_scale: f64 = 0;
|
||||||
const rows_px: f64 = @floatFromInt(img_grid.rows * self.grid_metrics.cell_height);
|
const rows_px: f64 = @floatFromInt(img_grid.rows * self.grid_metrics.cell_height);
|
||||||
@ -1773,76 +1787,93 @@ fn prepKittyVirtualPlacement(
|
|||||||
x_scale = y_scale;
|
x_scale = y_scale;
|
||||||
x_offset = (cols_px - img_width_f64 * x_scale) / 2;
|
x_offset = (cols_px - img_width_f64 * x_scale) / 2;
|
||||||
}
|
}
|
||||||
|
log.warn("x_offset={}, y_offset={}, x_scale={}, y_scale={}", .{
|
||||||
|
x_offset, y_offset, x_scale, y_scale,
|
||||||
|
});
|
||||||
|
|
||||||
// A bunch of math to map the placement and image to virtual placeholder
|
// At this point, we have the following information:
|
||||||
// grid. This is ported as closely as possible from Kitty so we get this
|
// - image.width/height - The original image width and height.
|
||||||
// as right as possible. I EXPECT there are some rounding bugs in here
|
// - img_grid.rows/columns - The requested grid size for the placement.
|
||||||
// so compared to Kitty we may be off by 1px here or there. If someone
|
// - offset/scale - The offset and scale to fit the image into the
|
||||||
// can show that to be true let's modify it.
|
// placement grid.
|
||||||
//
|
//
|
||||||
// This code is purposely not super Zig-like because I want to keep it
|
// For our run requested coordinates and size we now need to map
|
||||||
// as close to the Kitty implementation as possible os its easy to
|
// the original image down into our grid cells honoring the offsets
|
||||||
// compare and modify.
|
// calculated for the best fit.
|
||||||
var pin: terminal.Pin = p.pin;
|
|
||||||
var cols: u32 = p.width;
|
const img_x_offset: f64 = x_offset / x_scale;
|
||||||
var rows: u32 = p.height;
|
const img_y_offset: f64 = y_offset / y_scale;
|
||||||
const x_dst: f64 = @floatFromInt(p.col * self.grid_metrics.cell_width);
|
const img_width_padded: f64 = img_width_f64 + (img_x_offset * 2);
|
||||||
const y_dst: f64 = @floatFromInt(p.row * self.grid_metrics.cell_height);
|
const img_height_padded: f64 = img_height_f64 + (img_y_offset * 2);
|
||||||
const w_dst: f64 = @floatFromInt(p.width * self.grid_metrics.cell_width);
|
log.warn("padded_width={}, padded_height={} original_width={}, original_height={}", .{
|
||||||
const h_dst: f64 = @floatFromInt(p.height * self.grid_metrics.cell_height);
|
img_width_padded, img_height_padded, img_width_f64, img_height_f64,
|
||||||
var cell_x_off: u32 = 0;
|
});
|
||||||
var cell_y_off: u32 = 0;
|
|
||||||
var src_x: f64 = (x_dst - x_offset) / x_scale;
|
const source_width_f64: f64 = img_width_padded * (@as(f64, @floatFromInt(p.width)) / @as(f64, @floatFromInt(img_grid.columns)));
|
||||||
var src_y: f64 = (y_dst - y_offset) / y_scale;
|
var source_height_f64: f64 = img_height_padded * (@as(f64, @floatFromInt(p.height)) / @as(f64, @floatFromInt(img_grid.rows)));
|
||||||
var src_w: f64 = w_dst / x_scale;
|
const source_x_f64: f64 = img_width_padded * (@as(f64, @floatFromInt(p.col)) / @as(f64, @floatFromInt(img_grid.columns)));
|
||||||
var src_h: f64 = h_dst / y_scale;
|
var source_y_f64: f64 = img_height_padded * (@as(f64, @floatFromInt(p.row)) / @as(f64, @floatFromInt(img_grid.rows)));
|
||||||
if (src_x < 0) {
|
|
||||||
src_w += src_x;
|
const p_x_offset_f64: f64 = 0;
|
||||||
cell_x_off = @intFromFloat(@round(-src_x * x_scale));
|
var p_y_offset_f64: f64 = 0;
|
||||||
src_x = 0;
|
const dst_width_f64: f64 = @floatFromInt(p.width * self.grid_metrics.cell_width);
|
||||||
const col_off: u32 = cell_x_off / self.grid_metrics.cell_width;
|
var dst_height_f64: f64 = @floatFromInt(p.height * self.grid_metrics.cell_height);
|
||||||
cell_x_off %= self.grid_metrics.cell_width;
|
|
||||||
pin = pin.right(col_off);
|
// If our y is in our top offset area, we need to adjust the source to
|
||||||
if (cols <= col_off) return;
|
// be shorter, and offset it into the cell.
|
||||||
cols -= col_off;
|
if (source_y_f64 < img_y_offset) {
|
||||||
}
|
const offset: f64 = img_y_offset - source_y_f64;
|
||||||
if (src_y < 0) {
|
source_height_f64 -= offset;
|
||||||
src_h += src_y;
|
p_y_offset_f64 = offset;
|
||||||
cell_y_off = @intFromFloat(@round(-src_y * y_scale));
|
dst_height_f64 -= offset * y_scale;
|
||||||
src_y = 0;
|
source_y_f64 = 0;
|
||||||
const row_off: u32 = cell_y_off / self.grid_metrics.cell_height;
|
|
||||||
cell_y_off %= self.grid_metrics.cell_height;
|
|
||||||
pin = pin.down(row_off) orelse return;
|
|
||||||
if (rows <= row_off) return;
|
|
||||||
rows -= row_off;
|
|
||||||
}
|
|
||||||
if (src_x + src_w > img_width_f64) {
|
|
||||||
const redundant_px = src_x + src_w - img_width_f64;
|
|
||||||
const redundant_cells = @as(u32, @intFromFloat(redundant_px * x_scale)) / self.grid_metrics.cell_width;
|
|
||||||
if (cols <= redundant_cells) return;
|
|
||||||
cols -= redundant_cells;
|
|
||||||
src_w -= @as(f64, @floatFromInt(redundant_cells * self.grid_metrics.cell_width)) / x_scale;
|
|
||||||
}
|
|
||||||
if (src_y + src_h > img_height_f64) {
|
|
||||||
const redundant_px = src_y + src_h - img_height_f64;
|
|
||||||
const redundant_cells = @as(u32, @intFromFloat(redundant_px * y_scale)) / self.grid_metrics.cell_height;
|
|
||||||
if (rows <= redundant_cells) return;
|
|
||||||
rows -= redundant_cells;
|
|
||||||
src_h -= @as(f64, @floatFromInt(redundant_cells * self.grid_metrics.cell_height)) / y_scale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build our real placement
|
// if our y is in our bottom offset area, we need to shorten the
|
||||||
try self.prepKittyPlacement(t, top, bot, &image, &.{
|
// source to fit in the cell.
|
||||||
.location = .{ .pin = &pin },
|
if (source_y_f64 + source_height_f64 > img_height_padded - img_y_offset) {
|
||||||
.x_offset = cell_x_off,
|
source_y_f64 -= img_y_offset;
|
||||||
.y_offset = cell_y_off,
|
source_height_f64 = img_height_padded - img_y_offset - source_y_f64;
|
||||||
.source_x = @intFromFloat(@round(src_x)),
|
source_height_f64 -= img_y_offset;
|
||||||
.source_y = @intFromFloat(@round(src_y)),
|
dst_height_f64 = source_height_f64 * y_scale;
|
||||||
.source_width = @intFromFloat(@round(src_w)),
|
}
|
||||||
.source_height = @intFromFloat(@round(src_h)),
|
|
||||||
.columns = cols,
|
const source_width: u32 = @intFromFloat(@round(source_width_f64));
|
||||||
.rows = rows,
|
const source_height: u32 = @intFromFloat(@round(source_height_f64));
|
||||||
.z = -1, // Render behind cursor
|
const source_x: u32 = @intFromFloat(@round(source_x_f64));
|
||||||
|
const source_y: u32 = @intFromFloat(@round(source_y_f64));
|
||||||
|
const p_x_offset: u32 = @intFromFloat(@round(p_x_offset_f64 * x_scale));
|
||||||
|
const p_y_offset: u32 = @intFromFloat(@round(p_y_offset_f64 * y_scale));
|
||||||
|
const dest_width: u32 = @intFromFloat(@round(dst_width_f64));
|
||||||
|
const dest_height: u32 = @intFromFloat(@round(dst_height_f64));
|
||||||
|
|
||||||
|
log.warn("source_x={}, source_y={}, source_width={}, source_height={}", .{
|
||||||
|
source_x, source_y, source_width, source_height,
|
||||||
|
});
|
||||||
|
log.warn("p_x_offset={}, p_y_offset={}", .{ p_x_offset, p_y_offset });
|
||||||
|
log.warn("dest_width={}, dest_height={}", .{ dest_width, dest_height });
|
||||||
|
|
||||||
|
// Send our image to the GPU
|
||||||
|
try self.prepKittyImage(&image);
|
||||||
|
|
||||||
|
const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
|
||||||
|
.viewport,
|
||||||
|
p.pin,
|
||||||
|
) orelse @panic("TODO: unreachable?");
|
||||||
|
|
||||||
|
try self.image_placements.append(self.alloc, .{
|
||||||
|
.image_id = image.id,
|
||||||
|
.x = @intCast(p.pin.x),
|
||||||
|
.y = @intCast(viewport.viewport.y),
|
||||||
|
.z = -1,
|
||||||
|
.width = dest_width,
|
||||||
|
.height = dest_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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1875,42 +1906,7 @@ fn prepKittyPlacement(
|
|||||||
// We need to prep this image for upload if it isn't in the cache OR
|
// 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
|
// it is in the cache but the transmit time doesn't match meaning this
|
||||||
// image is different.
|
// image is different.
|
||||||
const gop = try self.images.getOrPut(self.alloc, image.id);
|
try self.prepKittyImage(image);
|
||||||
if (!gop.found_existing or
|
|
||||||
gop.value_ptr.transmit_time.order(image.transmit_time) != .eq)
|
|
||||||
{
|
|
||||||
// Copy the data into the pending state.
|
|
||||||
const data = try self.alloc.dupe(u8, image.data);
|
|
||||||
errdefer self.alloc.free(data);
|
|
||||||
|
|
||||||
// Store it in the map
|
|
||||||
const pending: Image.Pending = .{
|
|
||||||
.width = image.width,
|
|
||||||
.height = image.height,
|
|
||||||
.data = data.ptr,
|
|
||||||
};
|
|
||||||
|
|
||||||
const new_image: Image = switch (image.format) {
|
|
||||||
.grey_alpha => .{ .pending_grey_alpha = pending },
|
|
||||||
.rgb => .{ .pending_rgb = pending },
|
|
||||||
.rgba => .{ .pending_rgba = pending },
|
|
||||||
.png => unreachable, // should be decoded by now
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!gop.found_existing) {
|
|
||||||
gop.value_ptr.* = .{
|
|
||||||
.image = new_image,
|
|
||||||
.transmit_time = undefined,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
try gop.value_ptr.image.markForReplace(
|
|
||||||
self.alloc,
|
|
||||||
new_image,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
gop.value_ptr.transmit_time = image.transmit_time;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert our screen point to a viewport point
|
// Convert our screen point to a viewport point
|
||||||
const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
|
const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
|
||||||
@ -1953,6 +1949,52 @@ fn prepKittyPlacement(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prepKittyImage(
|
||||||
|
self: *Metal,
|
||||||
|
image: *const terminal.kitty.graphics.Image,
|
||||||
|
) !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 = try self.alloc.dupe(u8, image.data);
|
||||||
|
errdefer self.alloc.free(data);
|
||||||
|
|
||||||
|
// Store it in the map
|
||||||
|
const pending: Image.Pending = .{
|
||||||
|
.width = image.width,
|
||||||
|
.height = image.height,
|
||||||
|
.data = data.ptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
const new_image: Image = switch (image.format) {
|
||||||
|
.grey_alpha => .{ .pending_grey_alpha = pending },
|
||||||
|
.rgb => .{ .pending_rgb = pending },
|
||||||
|
.rgba => .{ .pending_rgba = pending },
|
||||||
|
.png => unreachable, // should be decoded by now
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!gop.found_existing) {
|
||||||
|
gop.value_ptr.* = .{
|
||||||
|
.image = new_image,
|
||||||
|
.transmit_time = undefined,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
try gop.value_ptr.image.markForReplace(
|
||||||
|
self.alloc,
|
||||||
|
new_image,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
gop.value_ptr.transmit_time = image.transmit_time;
|
||||||
|
}
|
||||||
|
|
||||||
/// Update the configuration.
|
/// Update the configuration.
|
||||||
pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
|
pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
|
||||||
// We always redo the font shaper in case font features changed. We
|
// We always redo the font shaper in case font features changed. We
|
||||||
|
Reference in New Issue
Block a user