ghostty/src/renderer/image.zig

303 lines
9.0 KiB
Zig

const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const wuffs = @import("wuffs");
const Renderer = @import("../renderer.zig").Renderer;
const GraphicsAPI = Renderer.API;
const Texture = GraphicsAPI.Texture;
/// 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: u32,
/// 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,
};
/// The map used for storing images.
pub const ImageMap = std.AutoHashMapUnmanaged(u32, 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.
pub 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.
pub fn prepForUpload(self: *Image, alloc: Allocator) !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,
) !void {
assert(self.isPending());
try self.prepForUpload(alloc);
// Get our pending info
const p = self.getPending().?;
// Create our texture
const texture = try Texture.init(
api.imageTextureOptions(.rgba, true),
@intCast(p.width),
@intCast(p.height),
p.dataSlice(),
);
// 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,
};
}
};