renderer/opengl: unicode placeholder support

This commit is contained in:
Mitchell Hashimoto
2024-07-29 19:15:42 -07:00
parent bf30753657
commit 765254e784
2 changed files with 210 additions and 98 deletions

View File

@ -932,7 +932,6 @@ pub fn updateFrame(
// If we have any virtual references, we must also rebuild our
// kitty state on every frame because any cell change can move
// an image.
// TODO(mitchellh): integrate with row dirty flags
if (state.terminal.screen.kitty_images.dirty or
self.image_virtual)
{

View File

@ -120,6 +120,7 @@ images: ImageMap = .{},
image_placements: ImagePlacementList = .{},
image_bg_end: u32 = 0,
image_text_end: u32 = 0,
image_virtual: bool = false,
/// Defererred OpenGL operation to update the screen size.
const SetScreenSize = struct {
@ -693,7 +694,13 @@ pub fn updateFrame(
// If we have Kitty graphics data, we enter a SLOW SLOW SLOW path.
// We only do this if the Kitty image state is dirty meaning only if
// it changes.
if (state.terminal.screen.kitty_images.dirty) {
//
// If we have any virtual references, we must also rebuild our
// kitty state on every frame because any cell change can move
// an image.
if (state.terminal.screen.kitty_images.dirty or
self.image_virtual)
{
// prepKittyGraphics touches self.images which is also used
// in drawFrame so if we're drawing on a separate thread we need
// to lock this.
@ -752,6 +759,7 @@ fn prepKittyGraphics(
// We always clear our previous placements no matter what because
// we rebuild them from scratch.
self.image_placements.clearRetainingCapacity();
self.image_virtual = false;
// Go through our known images and if there are any that are no longer
// in use then mark them to be freed.
@ -777,6 +785,23 @@ fn prepKittyGraphics(
while (it.next()) |kv| {
// Find the image in storage
const p = kv.value_ptr;
// Special logic based on location
switch (p.location) {
.pin => {},
.virtual => {
// We need to mark virtual placements on our renderer so that
// we know to rebuild in more scenarios since cell changes can
// now trigger placement changes.
self.image_virtual = true;
// We also continue out because virtual placements are
// only triggered by the unicode placeholder, not by the
// placement itself.
continue;
},
}
const image = storage.imageById(kv.key_ptr.image_id) orelse {
log.warn(
"missing image for placement, ignoring image_id={}",
@ -785,103 +810,16 @@ fn prepKittyGraphics(
continue;
};
// Get the rect for the placement. If this placement doesn't have
// a rect then its virtual or something so skip it.
const rect = p.rect(image, t) orelse continue;
try self.prepKittyPlacement(t, &top, &bot, &image, p);
}
// If the selection isn't within our viewport then skip it.
if (bot.before(rect.top_left)) continue;
if (rect.bottom_right.before(top)) 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 (rect.top_left.before(top)) offset_y: {
const vp_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y;
const img_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y;
const offset_cells = vp_y - img_y;
const offset_pixels = offset_cells * self.grid_metrics.cell_height;
break :offset_y @intCast(offset_pixels);
} else 0;
// 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
// image is different.
const gop = try self.images.getOrPut(self.alloc, kv.key_ptr.image_id);
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
const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
.viewport,
rect.top_left,
) orelse .{ .viewport = .{} };
// 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 -| source_y;
// Calculate the width/height of our image.
const dest_width = if (p.columns > 0) p.columns * self.grid_metrics.cell_width else source_width;
const dest_height = if (p.rows > 0) p.rows * self.grid_metrics.cell_height else source_height;
// Accumulate the placement
if (image.width > 0 and image.height > 0) {
try self.image_placements.append(self.alloc, .{
.image_id = kv.key_ptr.image_id,
.x = @intCast(rect.top_left.x),
.y = @intCast(viewport.viewport.y),
.z = p.z,
.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,
});
}
// If we have virtual placements then we need to scan for placeholders.
if (self.image_virtual) {
var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot);
while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement(
t,
&virtual_p,
);
}
// Sort the placements by their Z value.
@ -918,6 +856,181 @@ fn prepKittyGraphics(
}
}
fn prepKittyVirtualPlacement(
self: *OpenGL,
t: *terminal.Terminal,
p: *const terminal.kitty.graphics.unicode.Placement,
) !void {
const storage = &t.screen.kitty_images;
const image = storage.imageById(p.image_id) orelse {
log.warn(
"missing image for virtual placement, ignoring image_id={}",
.{p.image_id},
);
return;
};
const rp = p.renderPlacement(
storage,
&image,
self.grid_metrics.cell_width,
self.grid_metrics.cell_height,
) catch |err| {
log.warn("error rendering virtual placement err={}", .{err});
return;
};
// If our placement is zero sized then we don't do anything.
if (rp.dest_width == 0 or rp.dest_height == 0) return;
const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
.viewport,
rp.top_left,
) orelse {
// This is unreachable with virtual placements because we should
// only ever be looking at virtual placements that are in our
// viewport in the renderer and virtual placements only ever take
// up one row.
unreachable;
};
// Send our image to the GPU and store the placement for rendering.
try self.prepKittyImage(&image);
try self.image_placements.append(self.alloc, .{
.image_id = image.id,
.x = @intCast(rp.top_left.x),
.y = @intCast(viewport.viewport.y),
.z = -1,
.width = rp.dest_width,
.height = rp.dest_height,
.cell_offset_x = rp.offset_x,
.cell_offset_y = rp.offset_y,
.source_x = rp.source_x,
.source_y = rp.source_y,
.source_width = rp.source_width,
.source_height = rp.source_height,
});
}
fn prepKittyPlacement(
self: *OpenGL,
t: *terminal.Terminal,
top: *const terminal.Pin,
bot: *const terminal.Pin,
image: *const terminal.kitty.graphics.Image,
p: *const terminal.kitty.graphics.ImageStorage.Placement,
) !void {
// Get the rect for the placement. If this placement doesn't have
// a rect then its virtual or something so skip it.
const rect = p.rect(image.*, t) orelse return;
// If the selection isn't within our viewport then skip it.
if (bot.before(rect.top_left)) return;
if (rect.bottom_right.before(top.*)) return;
// 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 (rect.top_left.before(top.*)) offset_y: {
const vp_y = t.screen.pages.pointFromPin(.screen, top.*).?.screen.y;
const img_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y;
const offset_cells = vp_y - img_y;
const offset_pixels = offset_cells * self.grid_metrics.cell_height;
break :offset_y @intCast(offset_pixels);
} else 0;
// 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
// image is different.
try self.prepKittyImage(image);
// Convert our screen point to a viewport point
const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
.viewport,
rect.top_left,
) orelse .{ .viewport = .{} };
// 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 -| source_y;
// Calculate the width/height of our image.
const dest_width = if (p.columns > 0) p.columns * self.grid_metrics.cell_width else source_width;
const dest_height = if (p.rows > 0) p.rows * self.grid_metrics.cell_height else source_height;
// Accumulate the placement
if (image.width > 0 and image.height > 0) {
try self.image_placements.append(self.alloc, .{
.image_id = image.id,
.x = @intCast(rect.top_left.x),
.y = @intCast(viewport.viewport.y),
.z = p.z,
.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,
});
}
}
fn prepKittyImage(
self: *OpenGL,
image: *const terminal.kitty.graphics.Image,
) !void {
// 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
// image is different.
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;
}
/// rebuildCells rebuilds all the GPU cells from our CPU state. This is a
/// slow operation but ensures that the GPU state exactly matches the CPU state.
/// In steady-state operation, we use some GPU tricks to send down stale data