aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/renderer/image.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/renderer/image.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/renderer/image.zig')
-rw-r--r--vendor/libghostty-vt/src/renderer/image.zig958
1 files changed, 958 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/renderer/image.zig b/vendor/libghostty-vt/src/renderer/image.zig
new file mode 100644
index 00000000..c43d2798
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/image.zig
@@ -0,0 +1,958 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = @import("../quirks.zig").inlineAssert;
+const wuffs = @import("wuffs");
+const terminal = @import("../terminal/main.zig");
+
+const Renderer = @import("../renderer.zig").Renderer;
+const GraphicsAPI = Renderer.API;
+const Texture = GraphicsAPI.Texture;
+const CellSize = @import("size.zig").CellSize;
+const Overlay = @import("Overlay.zig");
+
+const log = std.log.scoped(.renderer_image);
+
+/// Generic image rendering state for the renderer. This stores all
+/// images and their placements and exposes only a limited public API
+/// for adding images and placements and drawing them.
+pub const State = struct {
+ /// The full image state for the renderer that specifies what images
+ /// need to be uploaded, pruned, etc.
+ images: ImageMap,
+
+ /// The placements for the Kitty image protocol.
+ kitty_placements: std.ArrayListUnmanaged(Placement),
+
+ /// The end index (exclusive) for placements that should be
+ /// drawn below the background, below the text, etc.
+ kitty_bg_end: u32,
+ kitty_text_end: u32,
+
+ /// True if there are any virtual placements. This needs to be known
+ /// because virtual placements need to be recalculated more often
+ /// on frame builds and are generally more expensive to handle.
+ kitty_virtual: bool,
+
+ /// Overlays
+ overlay_placements: std.ArrayListUnmanaged(Placement),
+
+ pub const empty: State = .{
+ .images = .empty,
+ .kitty_placements = .empty,
+ .kitty_bg_end = 0,
+ .kitty_text_end = 0,
+ .kitty_virtual = false,
+ .overlay_placements = .empty,
+ };
+
+ pub fn deinit(self: *State, alloc: Allocator) void {
+ {
+ var it = self.images.iterator();
+ while (it.next()) |kv| kv.value_ptr.image.deinit(alloc);
+ self.images.deinit(alloc);
+ }
+ self.kitty_placements.deinit(alloc);
+ self.overlay_placements.deinit(alloc);
+ }
+
+ /// Upload any images to the GPU that need to be uploaded,
+ /// and remove any images that are no longer needed on the GPU.
+ ///
+ /// If any uploads fail, they are ignored. The return value
+ /// can be used to detect if upload was a total success (true)
+ /// or not (false).
+ pub fn upload(
+ self: *State,
+ alloc: Allocator,
+ api: *GraphicsAPI,
+ ) bool {
+ var success: bool = true;
+ var image_it = self.images.iterator();
+ while (image_it.next()) |kv| {
+ const img = &kv.value_ptr.image;
+ if (img.isUnloading()) {
+ img.deinit(alloc);
+ self.images.removeByPtr(kv.key_ptr);
+ continue;
+ }
+
+ if (img.isPending()) {
+ img.upload(
+ alloc,
+ api,
+ ) catch |err| {
+ log.warn("error uploading image to GPU err={}", .{err});
+ success = false;
+ };
+ }
+ }
+
+ return success;
+ }
+
+ pub const DrawPlacements = enum {
+ kitty_below_bg,
+ kitty_below_text,
+ kitty_above_text,
+ overlay,
+ };
+
+ /// Draw the given named set of placements.
+ ///
+ /// Any placements that have non-uploaded images are ignored. Any
+ /// graphics API errors during drawing are also ignored.
+ pub fn draw(
+ self: *State,
+ api: *GraphicsAPI,
+ pipeline: GraphicsAPI.Pipeline,
+ pass: *GraphicsAPI.RenderPass,
+ placement_type: DrawPlacements,
+ ) void {
+ const placements: []const Placement = switch (placement_type) {
+ .kitty_below_bg => self.kitty_placements.items[0..self.kitty_bg_end],
+ .kitty_below_text => self.kitty_placements.items[self.kitty_bg_end..self.kitty_text_end],
+ .kitty_above_text => self.kitty_placements.items[self.kitty_text_end..],
+ .overlay => self.overlay_placements.items,
+ };
+
+ for (placements) |p| {
+ // Look up the image
+ const image = self.images.get(p.image_id) orelse {
+ log.warn("image not found for placement image_id={}", .{p.image_id});
+ continue;
+ };
+
+ // Get the texture
+ const texture = switch (image.image) {
+ .ready,
+ .unload_ready,
+ => |t| t,
+ else => {
+ log.warn("image not ready for placement image_id={}", .{p.image_id});
+ continue;
+ },
+ };
+
+ // Create our vertex buffer, which is always exactly one item.
+ // future(mitchellh): we can group rendering multiple instances of a single image
+ var buf = GraphicsAPI.Buffer(GraphicsAPI.shaders.Image).initFill(
+ api.imageBufferOptions(),
+ &.{.{
+ .grid_pos = .{
+ @as(f32, @floatFromInt(p.x)),
+ @as(f32, @floatFromInt(p.y)),
+ },
+
+ .cell_offset = .{
+ @as(f32, @floatFromInt(p.cell_offset_x)),
+ @as(f32, @floatFromInt(p.cell_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)),
+ },
+
+ .dest_size = .{
+ @as(f32, @floatFromInt(p.width)),
+ @as(f32, @floatFromInt(p.height)),
+ },
+ }},
+ ) catch |err| {
+ log.warn("error creating image vertex buffer err={}", .{err});
+ continue;
+ };
+ defer buf.deinit();
+
+ pass.step(.{
+ .pipeline = pipeline,
+ .buffers = &.{buf.buffer},
+ .textures = &.{texture},
+ .draw = .{
+ .type = .triangle_strip,
+ .vertex_count = 4,
+ },
+ });
+ }
+ }
+
+ /// Update our overlay state. Null value deletes any existing overlay.
+ pub fn overlayUpdate(
+ self: *State,
+ alloc: Allocator,
+ overlay_: ?Overlay,
+ ) !void {
+ const overlay = overlay_ orelse {
+ // If we don't have an overlay, remove any existing one.
+ if (self.images.getPtr(.overlay)) |data| {
+ data.image.markForUnload();
+ }
+ return;
+ };
+
+ // For transmit time we always just use the current time
+ // and overwrite the overlay.
+ const transmit_time = try std.time.Instant.now();
+
+ // Ensure we have space for our overlay placement. Do this before
+ // we upload our image so we don't have to deal with cleaning
+ // that up.
+ self.overlay_placements.clearRetainingCapacity();
+ try self.overlay_placements.ensureUnusedCapacity(alloc, 1);
+
+ // Setup our image.
+ const pending = overlay.pendingImage();
+ try self.prepImage(
+ alloc,
+ .overlay,
+ transmit_time,
+ pending,
+ );
+ errdefer comptime unreachable;
+
+ // Setup our placement
+ self.overlay_placements.appendAssumeCapacity(.{
+ .image_id = .overlay,
+ .x = 0,
+ .y = 0,
+ .z = 0,
+ .width = pending.width,
+ .height = pending.height,
+ .cell_offset_x = 0,
+ .cell_offset_y = 0,
+ .source_x = 0,
+ .source_y = 0,
+ .source_width = pending.width,
+ .source_height = pending.height,
+ });
+ }
+
+ /// Returns true if the Kitty graphics state requires an update based
+ /// on the terminal state and our internal state.
+ ///
+ /// This does not read/write state used by drawing.
+ pub fn kittyRequiresUpdate(
+ self: *const State,
+ t: *const terminal.Terminal,
+ ) bool {
+ // If the terminal kitty image state is dirty, we must update.
+ if (t.screens.active.kitty_images.dirty) return true;
+
+ // 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 the virtual placements were removed, this will
+ // be set to false on the next update.
+ if (self.kitty_virtual) return true;
+
+ return false;
+ }
+
+ /// Update the Kitty graphics state from the terminal.
+ ///
+ /// This reads/writes state used by drawing.
+ pub fn kittyUpdate(
+ self: *State,
+ alloc: Allocator,
+ t: *const terminal.Terminal,
+ cell_size: CellSize,
+ ) void {
+ const storage = &t.screens.active.kitty_images;
+ defer storage.dirty = false;
+
+ // We always clear our previous placements no matter what because
+ // we rebuild them from scratch.
+ self.kitty_placements.clearRetainingCapacity();
+ self.kitty_virtual = false;
+
+ // Go through our known images and if there are any that are no longer
+ // in use then mark them to be freed.
+ //
+ // This never conflicts with the below because a placement can't
+ // reference an image that doesn't exist.
+ {
+ var it = self.images.iterator();
+ while (it.next()) |kv| {
+ switch (kv.key_ptr.*) {
+ // We're only looking at Kitty images
+ .kitty => |id| if (storage.imageById(id) == null) {
+ kv.value_ptr.image.markForUnload();
+ },
+
+ .overlay => {},
+ }
+ }
+ }
+
+ // The top-left and bottom-right corners of our viewport in screen
+ // points. This lets us determine offsets and containment of placements.
+ const top = t.screens.active.pages.getTopLeft(.viewport);
+ const bot = t.screens.active.pages.getBottomRight(.viewport).?;
+ const top_y = t.screens.active.pages.pointFromPin(.screen, top).?.screen.y;
+ const bot_y = t.screens.active.pages.pointFromPin(.screen, bot).?.screen.y;
+
+ // Go through the placements and ensure the image is
+ // on the GPU or else is ready to be sent to the GPU.
+ var it = storage.placements.iterator();
+ while (it.next()) |kv| {
+ 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.kitty_virtual = true;
+
+ // We also continue out because virtual placements are
+ // only triggered by the unicode placeholder, not by the
+ // placement itself.
+ continue;
+ },
+ }
+
+ // Get the image for the placement
+ const image = storage.imageById(kv.key_ptr.image_id) orelse {
+ log.warn(
+ "missing image for placement, ignoring image_id={}",
+ .{kv.key_ptr.image_id},
+ );
+ continue;
+ };
+
+ self.prepKittyPlacement(
+ alloc,
+ t,
+ top_y,
+ bot_y,
+ &image,
+ p,
+ ) catch |err| {
+ // For errors we log and continue. We try to place
+ // other placements even if one fails.
+ log.warn("error preparing kitty placement err={}", .{err});
+ };
+ }
+
+ // If we have virtual placements then we need to scan for placeholders.
+ if (self.kitty_virtual) {
+ var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot);
+ while (v_it.next()) |virtual_p| {
+ self.prepKittyVirtualPlacement(
+ alloc,
+ t,
+ &virtual_p,
+ cell_size,
+ ) catch |err| {
+ // For errors we log and continue. We try to place
+ // other placements even if one fails.
+ log.warn("error preparing kitty placement err={}", .{err});
+ };
+ }
+ }
+
+ // Sort the placements by their Z value.
+ std.mem.sortUnstable(
+ Placement,
+ self.kitty_placements.items,
+ {},
+ struct {
+ fn lessThan(
+ ctx: void,
+ lhs: Placement,
+ rhs: Placement,
+ ) bool {
+ _ = ctx;
+ return lhs.z < rhs.z or
+ (lhs.z == rhs.z and lhs.image_id.zLessThan(rhs.image_id));
+ }
+ }.lessThan,
+ );
+
+ // Find our indices. The values are sorted by z so we can
+ // find the first placement out of bounds to find the limits.
+ const bg_limit = std.math.minInt(i32) / 2;
+ var bg_end: ?u32 = null;
+ var text_end: ?u32 = null;
+ for (self.kitty_placements.items, 0..) |p, i| {
+ if (bg_end == null and p.z >= bg_limit) bg_end = @intCast(i);
+ if (text_end == null and p.z >= 0) text_end = @intCast(i);
+ }
+
+ // If we didn't see any images with a z > the bg limit,
+ // then our bg end is the end of our placement list.
+ self.kitty_bg_end =
+ bg_end orelse @intCast(self.kitty_placements.items.len);
+ // Same idea for the image_text_end.
+ self.kitty_text_end =
+ text_end orelse @intCast(self.kitty_placements.items.len);
+ }
+
+ const PrepImageError = error{
+ OutOfMemory,
+ ImageConversionError,
+ };
+
+ /// Get the viewport-relative position for this
+ /// placement and add it to the placements list.
+ fn prepKittyPlacement(
+ self: *State,
+ alloc: Allocator,
+ t: *const terminal.Terminal,
+ top_y: u32,
+ bot_y: u32,
+ image: *const terminal.kitty.graphics.Image,
+ p: *const terminal.kitty.graphics.ImageStorage.Placement,
+ ) PrepImageError!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;
+
+ // This is expensive but necessary.
+ const img_top_y = t.screens.active.pages.pointFromPin(.screen, rect.top_left).?.screen.y;
+ const img_bot_y = t.screens.active.pages.pointFromPin(.screen, rect.bottom_right).?.screen.y;
+
+ // If the selection isn't within our viewport then skip it.
+ if (img_top_y > bot_y) return;
+ if (img_bot_y < top_y) return;
+
+ // 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(alloc, image);
+
+ // Calculate the dimensions of our image, taking in to
+ // account the rows / columns specified by the placement.
+ const dest_size = p.calculatedSize(image.*, t);
+
+ // Calculate the source rectangle
+ const source_x = @min(image.width, p.source_x);
+ const source_y = @min(image.height, p.source_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 - source_y, p.source_height)
+ else
+ image.height;
+
+ // Get the viewport-relative Y position of the placement.
+ const y_pos: i32 = @as(i32, @intCast(img_top_y)) - @as(i32, @intCast(top_y));
+
+ // Accumulate the placement
+ if (dest_size.width > 0 and dest_size.height > 0) {
+ try self.kitty_placements.append(alloc, .{
+ .image_id = .{ .kitty = image.id },
+ .x = @intCast(rect.top_left.x),
+ .y = y_pos,
+ .z = p.z,
+ .width = dest_size.width,
+ .height = dest_size.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 prepKittyVirtualPlacement(
+ self: *State,
+ alloc: Allocator,
+ t: *const terminal.Terminal,
+ p: *const terminal.kitty.graphics.unicode.Placement,
+ cell_size: CellSize,
+ ) PrepImageError!void {
+ const storage = &t.screens.active.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,
+ cell_size.width,
+ cell_size.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.screens.active.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;
+ };
+
+ // Prepare the image for the GPU and store the placement.
+ try self.prepKittyImage(alloc, &image);
+ try self.kitty_placements.append(alloc, .{
+ .image_id = .{ .kitty = 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,
+ });
+ }
+
+ /// Prepare an image for upload to the GPU.
+ fn prepImage(
+ self: *State,
+ alloc: Allocator,
+ id: Id,
+ transmit_time: std.time.Instant,
+ pending: Image.Pending,
+ ) PrepImageError!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(alloc, id);
+ if (gop.found_existing and
+ gop.value_ptr.transmit_time.order(transmit_time) == .eq)
+ {
+ return;
+ }
+
+ // Copy the data so we own it.
+ const data = if (alloc.dupe(
+ u8,
+ pending.dataSlice(),
+ )) |v| v else |_| {
+ if (!gop.found_existing) {
+ // If this is a new entry we can just remove it since it
+ // was never sent to the GPU.
+ _ = self.images.remove(id);
+ } else {
+ // If this was an existing entry, it is invalid and
+ // we must unload it.
+ gop.value_ptr.image.markForUnload();
+ }
+
+ return error.OutOfMemory;
+ };
+ // Note: we don't need to errdefer free the data because it is
+ // put into the map immediately below and our errdefer to
+ // handle our map state will fix this up.
+
+ // Store it in the map
+ const new_image: Image = .{
+ .pending = .{
+ .width = pending.width,
+ .height = pending.height,
+ .pixel_format = pending.pixel_format,
+ .data = data.ptr,
+ },
+ };
+ if (!gop.found_existing) {
+ gop.value_ptr.* = .{
+ .image = new_image,
+ .transmit_time = undefined,
+ };
+ } else {
+ gop.value_ptr.image.markForReplace(
+ alloc,
+ new_image,
+ );
+ }
+
+ // If any error happens, we unload the image and it is invalid.
+ errdefer gop.value_ptr.image.markForUnload();
+
+ gop.value_ptr.image.prepForUpload(alloc) catch |err| {
+ log.warn("error preparing image for upload err={}", .{err});
+ return error.ImageConversionError;
+ };
+ gop.value_ptr.transmit_time = transmit_time;
+ }
+
+ /// Prepare the provided Kitty image for upload to the GPU by copying its
+ /// data with our allocator and setting it to the pending state.
+ fn prepKittyImage(
+ self: *State,
+ alloc: Allocator,
+ image: *const terminal.kitty.graphics.Image,
+ ) PrepImageError!void {
+ try self.prepImage(
+ alloc,
+ .{ .kitty = image.id },
+ image.transmit_time,
+ .{
+ .width = image.width,
+ .height = image.height,
+ .pixel_format = switch (image.format) {
+ .gray => .gray,
+ .gray_alpha => .gray_alpha,
+ .rgb => .rgb,
+ .rgba => .rgba,
+ .png => unreachable, // should be decoded by now
+ },
+
+ // constCasts are always gross but this one is safe is because
+ // the data is only read from here and copied into its own
+ // buffer.
+ .data = @constCast(image.data.ptr),
+ },
+ );
+ }
+};
+
+/// Represents a single image placement on the grid.
+/// A placement is a request to render an instance of an image.
+pub const Placement = struct {
+ /// The image being rendered. This MUST be in the image map.
+ image_id: Id,
+
+ /// The grid x/y where this placement is located.
+ x: i32,
+ y: i32,
+ z: i32,
+
+ /// The width/height of the placed image.
+ width: u32,
+ height: u32,
+
+ /// The offset in pixels from the top left of the cell.
+ /// This is clamped to the size of a cell.
+ cell_offset_x: u32,
+ cell_offset_y: u32,
+
+ /// The source rectangle of the placement.
+ source_x: u32,
+ source_y: u32,
+ source_width: u32,
+ source_height: u32,
+};
+
+/// Image identifier used to store and lookup images.
+///
+/// This is tagged by different image types to make it easier to
+/// store different kinds of images in the same map without having
+/// to worry about ID collisions.
+pub const Id = union(enum) {
+ /// Image sent to the terminal state via the kitty graphics protocol.
+ /// The value is the ID assigned by the terminal.
+ kitty: u32,
+
+ /// Debug overlay. This is always composited down to a single
+ /// image for now. In the future we can support layers here if we want.
+ overlay,
+
+ /// Z-ordering tie-breaker for images with the same z value.
+ pub fn zLessThan(lhs: Id, rhs: Id) bool {
+ // If our tags aren't the same, we sort by tag.
+ if (std.meta.activeTag(lhs) != std.meta.activeTag(rhs)) {
+ return switch (lhs) {
+ // Kitty images always sort before (lower z) non-kitty images.
+ .kitty => true,
+
+ .overlay => false,
+ };
+ }
+
+ switch (lhs) {
+ .kitty => |lhs_id| {
+ const rhs_id = rhs.kitty;
+ return lhs_id < rhs_id;
+ },
+
+ // No sensical ordering
+ .overlay => return false,
+ }
+ }
+};
+
+/// The map used for storing images.
+pub const ImageMap = std.AutoHashMapUnmanaged(Id, struct {
+ image: Image,
+ transmit_time: std.time.Instant,
+});
+
+/// The state for a single image that is to be rendered.
+pub const Image = union(enum) {
+ /// The image data is pending upload to the GPU.
+ ///
+ /// This data is owned by this union so it must be freed once uploaded.
+ pending: Pending,
+
+ /// This is the same as the pending states but there is
+ /// a texture already allocated that we want to replace.
+ replace: Replace,
+
+ /// The image is uploaded and ready to be used.
+ ready: Texture,
+
+ /// The image isn't uploaded yet but is scheduled to be unloaded.
+ unload_pending: Pending,
+ /// The image is uploaded and is scheduled to be unloaded.
+ unload_ready: Texture,
+ /// The image is uploaded and scheduled to be replaced
+ /// with new data, but it's also scheduled to be unloaded.
+ unload_replace: Replace,
+
+ pub const Replace = struct {
+ texture: Texture,
+ pending: Pending,
+ };
+
+ /// Pending image data that needs to be uploaded to the GPU.
+ pub const Pending = struct {
+ height: u32,
+ width: u32,
+ pixel_format: PixelFormat,
+
+ /// Data is always expected to be (width * height * bpp).
+ data: [*]u8,
+
+ pub fn dataSlice(self: Pending) []u8 {
+ return self.data[0..self.len()];
+ }
+
+ pub fn len(self: Pending) usize {
+ return self.width * self.height * self.pixel_format.bpp();
+ }
+
+ pub const PixelFormat = enum {
+ /// 1 byte per pixel grayscale.
+ gray,
+ /// 2 bytes per pixel grayscale + alpha.
+ gray_alpha,
+ /// 3 bytes per pixel RGB.
+ rgb,
+ /// 3 bytes per pixel BGR.
+ bgr,
+ /// 4 byte per pixel RGBA.
+ rgba,
+ /// 4 byte per pixel BGRA.
+ bgra,
+
+ /// Get bytes per pixel for this format.
+ pub inline fn bpp(self: PixelFormat) usize {
+ return switch (self) {
+ .gray => 1,
+ .gray_alpha => 2,
+ .rgb => 3,
+ .bgr => 3,
+ .rgba => 4,
+ .bgra => 4,
+ };
+ }
+ };
+ };
+
+ pub fn deinit(self: Image, alloc: Allocator) void {
+ switch (self) {
+ .pending,
+ .unload_pending,
+ => |p| alloc.free(p.dataSlice()),
+
+ .replace, .unload_replace => |r| {
+ alloc.free(r.pending.dataSlice());
+ r.texture.deinit();
+ },
+
+ .ready,
+ .unload_ready,
+ => |t| t.deinit(),
+ }
+ }
+
+ /// Mark this image for unload whatever state it is in.
+ pub fn markForUnload(self: *Image) void {
+ self.* = switch (self.*) {
+ .unload_pending,
+ .unload_replace,
+ .unload_ready,
+ => return,
+
+ .ready => |t| .{ .unload_ready = t },
+ .pending => |p| .{ .unload_pending = p },
+ .replace => |r| .{ .unload_replace = r },
+ };
+ }
+
+ /// Mark the current image to be replaced with a pending one. This will
+ /// attempt to update the existing texture if we have one, otherwise it
+ /// will act like a new upload.
+ pub fn markForReplace(self: *Image, alloc: Allocator, img: Image) void {
+ assert(img.isPending());
+
+ // If we have pending data right now, free it.
+ if (self.getPending()) |p| {
+ alloc.free(p.dataSlice());
+ }
+ // If we have an existing texture, use it in the replace.
+ if (self.getTexture()) |t| {
+ self.* = .{ .replace = .{
+ .texture = t,
+ .pending = img.getPending().?,
+ } };
+ return;
+ }
+ // Otherwise we just become a pending image.
+ self.* = .{ .pending = img.getPending().? };
+ }
+
+ /// Returns true if this image is pending upload.
+ pub fn isPending(self: Image) bool {
+ return self.getPending() != null;
+ }
+
+ /// Returns true if this image has an associated texture.
+ pub fn hasTexture(self: Image) bool {
+ return self.getTexture() != null;
+ }
+
+ /// Returns true if this image is marked for unload.
+ pub fn isUnloading(self: Image) bool {
+ return switch (self) {
+ .unload_pending,
+ .unload_replace,
+ .unload_ready,
+ => true,
+
+ .pending,
+ .replace,
+ .ready,
+ => false,
+ };
+ }
+
+ /// Converts the image data to a format that can be uploaded to the GPU.
+ /// If the data is already in a format that can be uploaded, this is a
+ /// no-op.
+ fn convert(self: *Image, alloc: Allocator) wuffs.Error!void {
+ const p = self.getPendingPointer().?;
+ // As things stand, we currently convert all images to RGBA before
+ // uploading to the GPU. This just makes things easier. In the future
+ // we may want to support other formats.
+ if (p.pixel_format == .rgba) return;
+ // If the pending data isn't RGBA we'll need to swizzle it.
+ const data = p.dataSlice();
+ const rgba = try switch (p.pixel_format) {
+ .gray => wuffs.swizzle.gToRgba(alloc, data),
+ .gray_alpha => wuffs.swizzle.gaToRgba(alloc, data),
+ .rgb => wuffs.swizzle.rgbToRgba(alloc, data),
+ .bgr => wuffs.swizzle.bgrToRgba(alloc, data),
+ .rgba => unreachable,
+ .bgra => wuffs.swizzle.bgraToRgba(alloc, data),
+ };
+ alloc.free(data);
+ p.data = rgba.ptr;
+ p.pixel_format = .rgba;
+ }
+
+ /// Prepare the pending image data for upload to the GPU.
+ /// This doesn't need GPU access so is safe to call any time.
+ fn prepForUpload(self: *Image, alloc: Allocator) wuffs.Error!void {
+ assert(self.isPending());
+ try self.convert(alloc);
+ }
+
+ /// Upload the pending image to the GPU and change the state of this
+ /// image to ready.
+ pub fn upload(
+ self: *Image,
+ alloc: Allocator,
+ api: *const GraphicsAPI,
+ ) (wuffs.Error || error{
+ /// Texture creation failed, usually a GPU memory issue.
+ UploadFailed,
+ })!void {
+ assert(self.isPending());
+
+ // No error recover is required after this call because it just
+ // converts in place and is idempotent.
+ try self.prepForUpload(alloc);
+
+ // Get our pending info
+ const p = self.getPending().?;
+
+ // Create our texture
+ const texture = Texture.init(
+ api.imageTextureOptions(.rgba, true),
+ @intCast(p.width),
+ @intCast(p.height),
+ p.dataSlice(),
+ ) catch return error.UploadFailed;
+ errdefer comptime unreachable;
+
+ // Uploaded. We can now clear our data and change our state.
+ //
+ // NOTE: For the `replace` state, this will free the old texture.
+ // We don't currently actually replace the existing texture
+ // in-place but that is an optimization we can do later.
+ self.deinit(alloc);
+ self.* = .{ .ready = texture };
+ }
+
+ /// Returns any pending image data for this image that requires upload.
+ ///
+ /// If there is no pending data to upload, returns null.
+ fn getPending(self: Image) ?Pending {
+ return switch (self) {
+ .pending,
+ .unload_pending,
+ => |p| p,
+
+ .replace,
+ .unload_replace,
+ => |r| r.pending,
+
+ else => null,
+ };
+ }
+
+ /// Returns the texture for this image.
+ ///
+ /// If there is no texture for it yet, returns null.
+ fn getTexture(self: Image) ?Texture {
+ return switch (self) {
+ .ready,
+ .unload_ready,
+ => |t| t,
+
+ .replace,
+ .unload_replace,
+ => |r| r.texture,
+
+ else => null,
+ };
+ }
+
+ // Same as getPending but returns a pointer instead of a copy.
+ fn getPendingPointer(self: *Image) ?*Pending {
+ return switch (self.*) {
+ .pending => return &self.pending,
+ .unload_pending => return &self.unload_pending,
+
+ .replace => return &self.replace.pending,
+ .unload_replace => return &self.unload_replace.pending,
+
+ else => null,
+ };
+ }
+};