Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions example/c-vt-kitty-graphics/src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ int main() {
&cols, &rows) == GHOSTTY_SUCCESS) {
printf(" grid size: %u cols x %u rows\n", cols, rows);
}
ghostty_kitty_graphics_image_free(image);
}
printf("Total placements: %d\n", placement_count);
ghostty_kitty_graphics_placement_iterator_free(iter);
Expand Down
51 changes: 41 additions & 10 deletions include/ghostty/vt/kitty_graphics.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,13 @@ extern "C" {
* @ref GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID), call
* ghostty_kitty_graphics_image() to get a @ref GhosttyKittyGraphicsImage
* handle. From this handle, ghostty_kitty_graphics_image_get() provides
* the image dimensions, pixel format, compression, and a borrowed pointer
* to the raw pixel data.
* the image dimensions, pixel format, compression, and a pointer to the raw
* pixel data owned by and valid for the lifetime of the image handle. The
* pointer carries no additional ownership.
*
* GhosttyKittyGraphicsImage is an owned handle that remains valid across
* terminal mutations. Release every returned handle with
* ghostty_kitty_graphics_image_free().
*
* ## Rendering Helpers
*
Expand Down Expand Up @@ -108,11 +113,13 @@ extern "C" {
*
* ## Lifetime and Thread Safety
*
* All handles borrowed from the terminal (GhosttyKittyGraphics,
* GhosttyKittyGraphicsImage) are invalidated by any mutating terminal
* call. The placement iterator is independently owned and must be freed
* by the caller, but the data it yields is only valid while the
* underlying terminal is not mutated.
* GhosttyKittyGraphics is borrowed from the terminal and invalidated by any
* mutating terminal call. GhosttyKittyGraphicsImage is independently owned
* and keeps its immutable image data valid across terminal mutations.
*
* The placement iterator is independently owned and must be freed by the
* caller, but the data it yields is only valid while the underlying terminal
* is not mutated.
*
* ## Example
*
Expand Down Expand Up @@ -389,8 +396,8 @@ typedef enum GHOSTTY_ENUM_TYPED {
GHOSTTY_KITTY_IMAGE_DATA_COMPRESSION = 6,

/**
* Borrowed pointer to the raw pixel data. Valid as long as the
* underlying terminal is not mutated.
* Pointer to raw pixel data owned by the image handle. It remains valid until
* the image handle is freed and carries no additional ownership.
*
* The data is always fully decoded, uncompressed pixels in the
* format reported by GHOSTTY_KITTY_IMAGE_DATA_FORMAT: zlib payloads
Expand Down Expand Up @@ -502,14 +509,38 @@ GHOSTTY_API GhosttyResult ghostty_kitty_graphics_get(
*
* @param graphics The kitty graphics handle
* @param image_id The image ID to look up
* @return An opaque image handle, or NULL if not found
* @return An owned image handle, or NULL if not found. The caller must free
* non-NULL handles with ghostty_kitty_graphics_image_free().
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyKittyGraphicsImage ghostty_kitty_graphics_image(
GhosttyKittyGraphics graphics,
uint32_t image_id);

/**
* Increment an image handle's reference count.
*
* @param image The image to retain, or NULL
* @return The same image handle, or NULL if image is NULL
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyKittyGraphicsImage ghostty_kitty_graphics_image_retain(
GhosttyKittyGraphicsImage image);

/**
* Release an owned Kitty graphics image handle.
*
* Passing NULL is allowed and is a no-op.
*
* @param image The owned image handle to free, or NULL
*
* @ingroup kitty_graphics
*/
GHOSTTY_API void ghostty_kitty_graphics_image_free(
GhosttyKittyGraphicsImage image);

/**
* Get data from a Kitty graphics image.
*
Expand Down
7 changes: 3 additions & 4 deletions include/ghostty/vt/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,10 @@ typedef struct GhosttyTrackedGridRefImpl* GhosttyTrackedGridRef;
typedef struct GhosttyKittyGraphicsImpl* GhosttyKittyGraphics;

/**
* Opaque handle to a Kitty graphics image.
* Owned handle to a Kitty graphics image.
*
* Obtained via ghostty_kitty_graphics_image() with an image ID. The
* pointer is borrowed from the storage and remains valid until the next
* mutating terminal call.
* Images remain valid across terminal mutations. Every non-NULL handle must
* be released with ghostty_kitty_graphics_image_free().
*
* @ingroup kitty_graphics
*/
Expand Down
2 changes: 2 additions & 0 deletions src/lib_vt.zig
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ comptime {
@export(&c.terminal_point_from_grid_ref, .{ .name = "ghostty_terminal_point_from_grid_ref" });
@export(&c.kitty_graphics_get, .{ .name = "ghostty_kitty_graphics_get" });
@export(&c.kitty_graphics_image, .{ .name = "ghostty_kitty_graphics_image" });
@export(&c.kitty_graphics_image_retain, .{ .name = "ghostty_kitty_graphics_image_retain" });
@export(&c.kitty_graphics_image_free, .{ .name = "ghostty_kitty_graphics_image_free" });
@export(&c.kitty_graphics_image_get, .{ .name = "ghostty_kitty_graphics_image_get" });
@export(&c.kitty_graphics_image_get_multi, .{ .name = "ghostty_kitty_graphics_image_get_multi" });
@export(&c.kitty_graphics_placement_iterator_new, .{ .name = "ghostty_kitty_graphics_placement_iterator_new" });
Expand Down
23 changes: 20 additions & 3 deletions src/renderer/Overlay.zig
Original file line number Diff line number Diff line change
Expand Up @@ -106,16 +106,33 @@ pub fn deinit(self: *Overlay, alloc: Allocator) void {
self.surface.deinit(alloc);
}

/// Returns a pending image that can be used to copy, convert, upload, etc.
pub fn pendingImage(self: *const Overlay) Image.Pending {
/// Returns a borrowed image source that the renderer copies before storing.
pub fn pendingImage(self: *const Overlay) Image.Source {
return .{
.width = @intCast(self.surface.getWidth()),
.height = @intCast(self.surface.getHeight()),
.pixel_format = .rgba,
.data = @ptrCast(self.surface.image_surface_rgba.buf.ptr),
.data = std.mem.sliceAsBytes(self.surface.image_surface_rgba.buf),
};
}

test "pending image exposes all RGBA bytes" {
const testing = std.testing;
const alloc = testing.allocator;

var overlay = try Overlay.init(alloc, .{
.screen = .{ .width = 7, .height = 5 },
.cell = .{ .width = 1, .height = 1 },
.padding = .{},
});
defer overlay.deinit(alloc);

const source = overlay.pendingImage();
try testing.expectEqual(@as(u32, 7), source.width);
try testing.expectEqual(@as(u32, 5), source.height);
try testing.expectEqual(@as(usize, 7 * 5 * 4), source.data.len);
}

/// Clear the overlay.
pub fn reset(self: *Overlay) void {
self.surface.paintPixel(.{ .rgba = .{
Expand Down
52 changes: 18 additions & 34 deletions src/renderer/generic.zig
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
shaders: Shaders,

/// The render state we update per loop.
terminal_state: terminal.RenderState = .empty,
terminal_state: terminal.RenderState = terminal.RenderState.init(.{ .kitty_graphics = true }),

/// The number of frames since the last terminal state reset.
/// We reset the terminal state after ~100,000 frames (about 10 to
Expand Down Expand Up @@ -820,7 +820,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {

self.images.deinit(self.alloc);

if (self.bg_image) |img| img.deinit(self.alloc);
if (self.bg_image) |*img| img.deinit(self.alloc);

self.deinitShaders();

Expand Down Expand Up @@ -1143,7 +1143,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
const max_terminal_state_frame_count = 100_000;
if (self.terminal_state_frame_count >= max_terminal_state_frame_count) {
self.terminal_state.deinit(self.alloc);
self.terminal_state = .empty;
self.terminal_state = terminal.RenderState.init(.{ .kitty_graphics = true });
}
self.terminal_state_frame_count += 1;

Expand Down Expand Up @@ -1230,28 +1230,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
break :preedit try p.clone(arena_alloc);
};

// 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 we have any virtual references, we must also rebuild our
// kitty state on every frame because any cell change can move
// an image.
if (self.images.kittyRequiresUpdate(state.terminal)) {
// We need to grab the draw mutex since this updates
// our image state that drawFrame uses.
self.draw_mutex.lock();
defer self.draw_mutex.unlock();
self.images.kittyUpdate(
self.alloc,
state.terminal,
.{
.width = self.grid_metrics.cell_width,
.height = self.grid_metrics.cell_height,
},
);
}

// Get our OSC8 links we're hovering if we have a mouse.
// This requires terminal state because of URLs.
const links: terminal.RenderState.CellSet = osc8: {
Expand Down Expand Up @@ -1293,6 +1271,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// render state (e.g. rebuildCells).
self.terminal_state.endUpdate();

if (self.terminal_state.kitty.dirty) {
self.draw_mutex.lock();
defer self.draw_mutex.unlock();
if (self.images.kittyUpdate(self.alloc, &self.terminal_state)) {
self.terminal_state.kitty.dirty = false;
}
}

// Outside the critical area we can update our links to contain
// our regex results.
self.config.links.renderCellMap(
Expand Down Expand Up @@ -1816,21 +1802,19 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
},
};

const image: imagepkg.Image = .{
.pending = .{
.width = image_data.width,
.height = image_data.height,
.pixel_format = .rgba,
.data = image_data.data.ptr,
},
var pending: imagepkg.Image.Pending = .{
.width = image_data.width,
.height = image_data.height,
.pixel_format = .rgba,
.backing = .{ .renderer_owned = image_data.data },
};

// If we have an existing background image, replace it.
// Otherwise, set this as our background image directly.
if (self.bg_image) |*img| {
img.markForReplace(self.alloc, image);
img.markForReplace(self.alloc, &pending);
} else {
self.bg_image = image;
self.bg_image = .{ .pending = pending.take() };
}
} else {
// If we don't have a background image path, mark our
Expand Down
Loading
Loading