aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/renderer
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
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/renderer')
-rw-r--r--vendor/libghostty-vt/src/renderer/Metal.zig458
-rw-r--r--vendor/libghostty-vt/src/renderer/OpenGL.zig461
-rw-r--r--vendor/libghostty-vt/src/renderer/Options.zig24
-rw-r--r--vendor/libghostty-vt/src/renderer/Overlay.zig415
-rw-r--r--vendor/libghostty-vt/src/renderer/State.zig123
-rw-r--r--vendor/libghostty-vt/src/renderer/Thread.zig715
-rw-r--r--vendor/libghostty-vt/src/renderer/WebGL.zig2
-rw-r--r--vendor/libghostty-vt/src/renderer/backend.zig23
-rw-r--r--vendor/libghostty-vt/src/renderer/cell.zig680
-rw-r--r--vendor/libghostty-vt/src/renderer/cursor.zig154
-rw-r--r--vendor/libghostty-vt/src/renderer/generic.zig3374
-rw-r--r--vendor/libghostty-vt/src/renderer/image.zig958
-rw-r--r--vendor/libghostty-vt/src/renderer/link.zig322
-rw-r--r--vendor/libghostty-vt/src/renderer/message.zig111
-rw-r--r--vendor/libghostty-vt/src/renderer/metal/Frame.zig131
-rw-r--r--vendor/libghostty-vt/src/renderer/metal/IOSurfaceLayer.zig183
-rw-r--r--vendor/libghostty-vt/src/renderer/metal/Pipeline.zig204
-rw-r--r--vendor/libghostty-vt/src/renderer/metal/RenderPass.zig229
-rw-r--r--vendor/libghostty-vt/src/renderer/metal/Sampler.zig64
-rw-r--r--vendor/libghostty-vt/src/renderer/metal/Target.zig108
-rw-r--r--vendor/libghostty-vt/src/renderer/metal/Texture.zig202
-rw-r--r--vendor/libghostty-vt/src/renderer/metal/api.zig452
-rw-r--r--vendor/libghostty-vt/src/renderer/metal/buffer.zig185
-rw-r--r--vendor/libghostty-vt/src/renderer/metal/shaders.zig454
-rw-r--r--vendor/libghostty-vt/src/renderer/opengl/Frame.zig71
-rw-r--r--vendor/libghostty-vt/src/renderer/opengl/Pipeline.zig164
-rw-r--r--vendor/libghostty-vt/src/renderer/opengl/RenderPass.zig144
-rw-r--r--vendor/libghostty-vt/src/renderer/opengl/Sampler.zig45
-rw-r--r--vendor/libghostty-vt/src/renderer/opengl/Target.zig60
-rw-r--r--vendor/libghostty-vt/src/renderer/opengl/Texture.zig104
-rw-r--r--vendor/libghostty-vt/src/renderer/opengl/buffer.zig126
-rw-r--r--vendor/libghostty-vt/src/renderer/opengl/shaders.zig376
-rw-r--r--vendor/libghostty-vt/src/renderer/row.zig63
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/bg_color.f.glsl13
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/bg_image.f.glsl63
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/bg_image.v.glsl145
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/cell_bg.f.glsl61
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/cell_text.f.glsl100
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/cell_text.v.glsl146
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/common.glsl156
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/full_screen.v.glsl24
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/image.f.glsl21
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/image.v.glsl47
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/shaders.metal853
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/shadertoy_prefix.glsl52
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_crt.glsl59
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_focus.glsl41
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_invalid.glsl12
-rw-r--r--vendor/libghostty-vt/src/renderer/shadertoy.zig427
-rw-r--r--vendor/libghostty-vt/src/renderer/size.zig458
50 files changed, 13863 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/renderer/Metal.zig b/vendor/libghostty-vt/src/renderer/Metal.zig
new file mode 100644
index 00000000..6c7432d2
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/Metal.zig
@@ -0,0 +1,458 @@
+//! Graphics API wrapper for Metal.
+pub const Metal = @This();
+
+const std = @import("std");
+const assert = @import("../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+const builtin = @import("builtin");
+const objc = @import("objc");
+const macos = @import("macos");
+const graphics = macos.graphics;
+const apprt = @import("../apprt.zig");
+const font = @import("../font/main.zig");
+const configpkg = @import("../config.zig");
+const rendererpkg = @import("../renderer.zig");
+const Renderer = rendererpkg.GenericRenderer(Metal);
+const shadertoy = @import("shadertoy.zig");
+
+const mtl = @import("metal/api.zig");
+const IOSurfaceLayer = @import("metal/IOSurfaceLayer.zig");
+
+pub const GraphicsAPI = Metal;
+pub const Target = @import("metal/Target.zig");
+pub const Frame = @import("metal/Frame.zig");
+pub const RenderPass = @import("metal/RenderPass.zig");
+pub const Pipeline = @import("metal/Pipeline.zig");
+const bufferpkg = @import("metal/buffer.zig");
+pub const Buffer = bufferpkg.Buffer;
+pub const Sampler = @import("metal/Sampler.zig");
+pub const Texture = @import("metal/Texture.zig");
+pub const shaders = @import("metal/shaders.zig");
+
+pub const custom_shader_target: shadertoy.Target = .msl;
+// The fragCoord for Metal shaders is +Y = down.
+pub const custom_shader_y_is_down = true;
+
+/// Triple buffering.
+pub const swap_chain_count = 3;
+
+const log = std.log.scoped(.metal);
+
+layer: IOSurfaceLayer,
+
+/// MTLDevice
+device: objc.Object,
+/// MTLCommandQueue
+queue: objc.Object,
+
+/// Alpha blending mode
+blending: configpkg.Config.AlphaBlending,
+
+/// The default storage mode to use for resources created with our device.
+///
+/// This is based on whether the device is a discrete GPU or not, since
+/// discrete GPUs do not have unified memory and therefore do not support
+/// the "shared" storage mode, instead we have to use the "managed" mode.
+default_storage_mode: mtl.MTLResourceOptions.StorageMode,
+
+/// The maximum 2D texture width and height supported by the device.
+max_texture_size: u32,
+
+/// We start an AutoreleasePool before `drawFrame` and end it afterwards.
+autorelease_pool: ?*objc.AutoreleasePool = null,
+
+pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal {
+ comptime switch (builtin.os.tag) {
+ .macos, .ios => {},
+ else => @compileError("unsupported platform for Metal"),
+ };
+
+ _ = alloc;
+
+ // Choose our MTLDevice and create a MTLCommandQueue for that device.
+ const device = try chooseDevice();
+ errdefer device.release();
+ const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{});
+ errdefer queue.release();
+
+ // Grab metadata about the device.
+ const default_storage_mode: mtl.MTLResourceOptions.StorageMode = switch (comptime builtin.os.tag) {
+ // manage mode is not supported by iOS
+ .ios => .shared,
+ else => if (device.getProperty(bool, "hasUnifiedMemory")) .shared else .managed,
+ };
+ const max_texture_size = queryMaxTextureSize(device);
+ log.debug(
+ "device properties default_storage_mode={} max_texture_size={}",
+ .{ default_storage_mode, max_texture_size },
+ );
+
+ const ViewInfo = struct {
+ view: objc.Object,
+ scaleFactor: f64,
+ };
+
+ // Get the metadata about our underlying view that we'll be rendering to.
+ const info: ViewInfo = switch (apprt.runtime) {
+ apprt.embedded => .{
+ .scaleFactor = @floatCast(opts.rt_surface.content_scale.x),
+ .view = switch (opts.rt_surface.platform) {
+ .macos => |v| v.nsview,
+ .ios => |v| v.uiview,
+ },
+ },
+
+ else => @compileError("unsupported apprt for metal"),
+ };
+
+ // Create an IOSurfaceLayer which we can assign to the view to make
+ // it in to a "layer-hosting view", so that we can manually control
+ // the layer contents.
+ var layer = try IOSurfaceLayer.init();
+ errdefer layer.release();
+
+ // Add our layer to the view.
+ //
+ // On macOS we do this by making the view "layer-hosting"
+ // by assigning it to the view's `layer` property BEFORE
+ // setting `wantsLayer` to `true`.
+ //
+ // On iOS, views are always layer-backed, and `layer`
+ // is readonly, so instead we add it as a sublayer.
+ switch (comptime builtin.os.tag) {
+ .macos => {
+ info.view.setProperty("layer", layer.layer.value);
+ info.view.setProperty("wantsLayer", true);
+ },
+
+ .ios => {
+ const view_layer = objc.Object.fromId(info.view.getProperty(?*anyopaque, "layer"));
+ view_layer.msgSend(void, objc.sel("addSublayer:"), .{layer.layer.value});
+ },
+
+ else => @compileError("unsupported target for Metal"),
+ }
+
+ // Ensure that if our layer is oversized it
+ // does not overflow the bounds of the view.
+ info.view.setProperty("clipsToBounds", true);
+
+ // Ensure that our layer has a content scale set to
+ // match the scale factor of the window. This avoids
+ // magnification issues leading to blurry rendering.
+ layer.layer.setProperty("contentsScale", info.scaleFactor);
+
+ // This makes it so that our display callback will actually be called.
+ layer.layer.setProperty("needsDisplayOnBoundsChange", true);
+
+ return .{
+ .layer = layer,
+ .device = device,
+ .queue = queue,
+ .blending = opts.config.blending,
+ .default_storage_mode = default_storage_mode,
+ .max_texture_size = max_texture_size,
+ };
+}
+
+pub fn deinit(self: *Metal) void {
+ self.queue.release();
+ self.device.release();
+ self.layer.release();
+}
+
+pub fn loopEnter(self: *Metal) void {
+ const renderer: *align(1) Renderer = @fieldParentPtr("api", self);
+ self.layer.setDisplayCallback(
+ @ptrCast(&displayCallback),
+ @ptrCast(renderer),
+ );
+}
+
+fn displayCallback(renderer: *Renderer) align(8) void {
+ renderer.drawFrame(true) catch |err| {
+ log.warn("Error drawing frame in display callback, err={}", .{err});
+ };
+}
+
+/// Actions taken before doing anything in `drawFrame`.
+///
+/// Right now we use this to start an AutoreleasePool.
+pub fn drawFrameStart(self: *Metal) void {
+ assert(self.autorelease_pool == null);
+ self.autorelease_pool = .init();
+}
+
+/// Actions taken after `drawFrame` is done.
+///
+/// Right now we use this to end our AutoreleasePool.
+pub fn drawFrameEnd(self: *Metal) void {
+ assert(self.autorelease_pool != null);
+ self.autorelease_pool.?.deinit();
+ self.autorelease_pool = null;
+}
+
+pub fn initShaders(
+ self: *const Metal,
+ alloc: Allocator,
+ custom_shaders: []const [:0]const u8,
+) !shaders.Shaders {
+ return try shaders.Shaders.init(
+ alloc,
+ self.device,
+ custom_shaders,
+ // Using an `*_srgb` pixel format makes Metal gamma encode
+ // the pixels written to it *after* blending, which means
+ // we get linear alpha blending rather than gamma-incorrect
+ // blending.
+ if (self.blending.isLinear())
+ mtl.MTLPixelFormat.bgra8unorm_srgb
+ else
+ mtl.MTLPixelFormat.bgra8unorm,
+ );
+}
+
+/// Get the current size of the runtime surface.
+pub fn surfaceSize(self: *const Metal) !struct { width: u32, height: u32 } {
+ const bounds = self.layer.layer.getProperty(graphics.Rect, "bounds");
+ const scale = self.layer.layer.getProperty(f64, "contentsScale");
+
+ // We need to clamp our runtime surface size to the maximum
+ // possible texture size since we can't create a screen buffer (texture)
+ // larger than that.
+ return .{
+ .width = @min(
+ @as(u32, @intFromFloat(bounds.size.width * scale)),
+ self.max_texture_size,
+ ),
+ .height = @min(
+ @as(u32, @intFromFloat(bounds.size.height * scale)),
+ self.max_texture_size,
+ ),
+ };
+}
+
+/// Initialize a new render target which can be presented by this API.
+pub fn initTarget(self: *const Metal, width: usize, height: usize) !Target {
+ return Target.init(.{
+ .device = self.device,
+ // Using an `*_srgb` pixel format makes Metal gamma encode the pixels
+ // written to it *after* blending, which means we get linear alpha
+ // blending rather than gamma-incorrect blending.
+ .pixel_format = if (self.blending.isLinear())
+ .bgra8unorm_srgb
+ else
+ .bgra8unorm,
+ .storage_mode = self.default_storage_mode,
+ .width = width,
+ .height = height,
+ });
+}
+
+/// Present the provided target.
+pub inline fn present(self: *Metal, target: Target, sync: bool) !void {
+ if (sync) {
+ self.layer.setSurfaceSync(target.surface);
+ } else {
+ try self.layer.setSurface(target.surface);
+ }
+}
+
+/// Present the last presented target again. (noop for Metal)
+pub inline fn presentLastTarget(self: *Metal) !void {
+ _ = self;
+}
+
+/// Returns the options to use when constructing buffers.
+pub inline fn bufferOptions(self: Metal) bufferpkg.Options {
+ return .{
+ .device = self.device,
+ .resource_options = .{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = self.default_storage_mode,
+ },
+ };
+}
+
+pub const instanceBufferOptions = bufferOptions;
+pub const uniformBufferOptions = bufferOptions;
+pub const fgBufferOptions = bufferOptions;
+pub const bgBufferOptions = bufferOptions;
+pub const imageBufferOptions = bufferOptions;
+pub const bgImageBufferOptions = bufferOptions;
+
+/// Returns the options to use when constructing textures.
+pub inline fn textureOptions(self: Metal) Texture.Options {
+ return .{
+ .device = self.device,
+ // Using an `*_srgb` pixel format makes Metal gamma encode the pixels
+ // written to it *after* blending, which means we get linear alpha
+ // blending rather than gamma-incorrect blending.
+ .pixel_format = if (self.blending.isLinear())
+ .bgra8unorm_srgb
+ else
+ .bgra8unorm,
+ .resource_options = .{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = self.default_storage_mode,
+ },
+ .usage = .{
+ // textureOptions is currently only used for custom shaders,
+ // which require both the shader read (for when multiple shaders
+ // are chained) and render target (for the final output) usage.
+ // Disabling either of these will lead to metal validation
+ // errors in Xcode.
+ .shader_read = true,
+ .render_target = true,
+ },
+ };
+}
+
+pub inline fn samplerOptions(self: Metal) Sampler.Options {
+ return .{
+ .device = self.device,
+
+ // These parameters match Shadertoy behaviors.
+ .min_filter = .linear,
+ .mag_filter = .linear,
+ .s_address_mode = .clamp_to_edge,
+ .t_address_mode = .clamp_to_edge,
+ };
+}
+
+/// Pixel format for image texture options.
+pub const ImageTextureFormat = enum {
+ /// 1 byte per pixel grayscale.
+ gray,
+ /// 4 bytes per pixel RGBA.
+ rgba,
+ /// 4 bytes per pixel BGRA.
+ bgra,
+
+ fn toPixelFormat(
+ self: ImageTextureFormat,
+ srgb: bool,
+ ) mtl.MTLPixelFormat {
+ return switch (self) {
+ .gray => if (srgb) .r8unorm_srgb else .r8unorm,
+ .rgba => if (srgb) .rgba8unorm_srgb else .rgba8unorm,
+ .bgra => if (srgb) .bgra8unorm_srgb else .bgra8unorm,
+ };
+ }
+};
+
+/// Returns the options to use when constructing textures for images.
+pub inline fn imageTextureOptions(
+ self: Metal,
+ format: ImageTextureFormat,
+ srgb: bool,
+) Texture.Options {
+ return .{
+ .device = self.device,
+ .pixel_format = format.toPixelFormat(srgb),
+ .resource_options = .{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = self.default_storage_mode,
+ },
+ .usage = .{
+ // We only need to read from this texture from a shader.
+ .shader_read = true,
+ },
+ };
+}
+
+/// Initializes a Texture suitable for the provided font atlas.
+pub fn initAtlasTexture(
+ self: *const Metal,
+ atlas: *const font.Atlas,
+) Texture.Error!Texture {
+ const pixel_format: mtl.MTLPixelFormat = switch (atlas.format) {
+ .grayscale => .r8unorm,
+ .bgra => .bgra8unorm_srgb,
+ else => @panic("unsupported atlas format for Metal texture"),
+ };
+
+ return try Texture.init(
+ .{
+ .device = self.device,
+ .pixel_format = pixel_format,
+ .resource_options = .{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = self.default_storage_mode,
+ },
+ .usage = .{
+ // We only need to read from this texture from a shader.
+ .shader_read = true,
+ },
+ },
+ atlas.size,
+ atlas.size,
+ null,
+ );
+}
+
+/// Begin a frame.
+pub inline fn beginFrame(
+ self: *const Metal,
+ /// Once the frame has been completed, the `frameCompleted` method
+ /// on the renderer is called with the health status of the frame.
+ renderer: *Renderer,
+ /// The target is presented via the provided renderer's API when completed.
+ target: *Target,
+) !Frame {
+ return try Frame.begin(.{ .queue = self.queue }, renderer, target);
+}
+
+fn chooseDevice() error{NoMetalDevice}!objc.Object {
+ var chosen_device: ?objc.Object = null;
+
+ switch (comptime builtin.os.tag) {
+ .macos => {
+ const devices = objc.Object.fromId(mtl.MTLCopyAllDevices());
+ defer devices.release();
+
+ var iter = devices.iterate();
+ while (iter.next()) |device| {
+ // We want a GPU that’s connected to a display.
+ if (device.getProperty(bool, "isHeadless")) continue;
+ chosen_device = device;
+ // If the user has an eGPU plugged in, they probably want
+ // to use it. Otherwise, integrated GPUs are better for
+ // battery life and thermals.
+ if (device.getProperty(bool, "isRemovable") or
+ device.getProperty(bool, "isLowPower")) break;
+ }
+ },
+ .ios => {
+ chosen_device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice());
+ },
+ else => @compileError("unsupported target for Metal"),
+ }
+
+ const device = chosen_device orelse return error.NoMetalDevice;
+ return device.retain();
+}
+
+/// Determines the maximum 2D texture size supported by the device.
+/// We need to clamp our frame size to this if it's larger.
+fn queryMaxTextureSize(device: objc.Object) u32 {
+ // https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf
+
+ if (device.msgSend(
+ bool,
+ objc.sel("supportsFamily:"),
+ .{mtl.MTLGPUFamily.apple10},
+ )) return 32768;
+
+ if (device.msgSend(
+ bool,
+ objc.sel("supportsFamily:"),
+ .{mtl.MTLGPUFamily.apple3},
+ )) return 16384;
+
+ return 8192;
+}
diff --git a/vendor/libghostty-vt/src/renderer/OpenGL.zig b/vendor/libghostty-vt/src/renderer/OpenGL.zig
new file mode 100644
index 00000000..4b01da0c
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/OpenGL.zig
@@ -0,0 +1,461 @@
+//! Graphics API wrapper for OpenGL.
+pub const OpenGL = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const builtin = @import("builtin");
+const gl = @import("opengl");
+const shadertoy = @import("shadertoy.zig");
+const apprt = @import("../apprt.zig");
+const font = @import("../font/main.zig");
+const configpkg = @import("../config.zig");
+const rendererpkg = @import("../renderer.zig");
+const Renderer = rendererpkg.GenericRenderer(OpenGL);
+
+pub const GraphicsAPI = OpenGL;
+pub const Target = @import("opengl/Target.zig");
+pub const Frame = @import("opengl/Frame.zig");
+pub const RenderPass = @import("opengl/RenderPass.zig");
+pub const Pipeline = @import("opengl/Pipeline.zig");
+const bufferpkg = @import("opengl/buffer.zig");
+pub const Buffer = bufferpkg.Buffer;
+pub const Sampler = @import("opengl/Sampler.zig");
+pub const Texture = @import("opengl/Texture.zig");
+pub const shaders = @import("opengl/shaders.zig");
+
+pub const custom_shader_target: shadertoy.Target = .glsl;
+// The fragCoord for OpenGL shaders is +Y = up.
+pub const custom_shader_y_is_down = false;
+
+/// Because OpenGL's frame completion is always
+/// sync, we have no need for multi-buffering.
+pub const swap_chain_count = 1;
+
+const log = std.log.scoped(.opengl);
+
+/// We require at least OpenGL 4.3
+pub const MIN_VERSION_MAJOR = 4;
+pub const MIN_VERSION_MINOR = 3;
+
+alloc: std.mem.Allocator,
+
+/// Alpha blending mode
+blending: configpkg.Config.AlphaBlending,
+
+/// The most recently presented target, in case we need to present it again.
+last_target: ?Target = null,
+
+/// NOTE: This is an error{}!OpenGL instead of just OpenGL for parity with
+/// Metal, since it needs to be fallible so does this, even though it
+/// can't actually fail.
+pub fn init(alloc: Allocator, opts: rendererpkg.Options) error{}!OpenGL {
+ return .{
+ .alloc = alloc,
+ .blending = opts.config.blending,
+ };
+}
+
+pub fn deinit(self: *OpenGL) void {
+ self.* = undefined;
+}
+
+/// 32-bit windows cross-compilation breaks with `.c` for some reason, so...
+const gl_debug_proc_callconv =
+ @typeInfo(
+ @typeInfo(
+ @typeInfo(
+ gl.c.GLDEBUGPROC,
+ ).optional.child,
+ ).pointer.child,
+ ).@"fn".calling_convention;
+
+fn glDebugMessageCallback(
+ src: gl.c.GLenum,
+ typ: gl.c.GLenum,
+ id: gl.c.GLuint,
+ severity: gl.c.GLenum,
+ len: gl.c.GLsizei,
+ msg: [*c]const gl.c.GLchar,
+ user_param: ?*const anyopaque,
+) callconv(gl_debug_proc_callconv) void {
+ _ = user_param;
+
+ const src_str: []const u8 = switch (src) {
+ gl.c.GL_DEBUG_SOURCE_API => "OpenGL API",
+ gl.c.GL_DEBUG_SOURCE_WINDOW_SYSTEM => "Window System",
+ gl.c.GL_DEBUG_SOURCE_SHADER_COMPILER => "Shader Compiler",
+ gl.c.GL_DEBUG_SOURCE_THIRD_PARTY => "Third Party",
+ gl.c.GL_DEBUG_SOURCE_APPLICATION => "User",
+ gl.c.GL_DEBUG_SOURCE_OTHER => "Other",
+ else => "Unknown",
+ };
+
+ const typ_str: []const u8 = switch (typ) {
+ gl.c.GL_DEBUG_TYPE_ERROR => "Error",
+ gl.c.GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR => "Deprecated Behavior",
+ gl.c.GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR => "Undefined Behavior",
+ gl.c.GL_DEBUG_TYPE_PORTABILITY => "Portability Issue",
+ gl.c.GL_DEBUG_TYPE_PERFORMANCE => "Performance Issue",
+ gl.c.GL_DEBUG_TYPE_MARKER => "Marker",
+ gl.c.GL_DEBUG_TYPE_PUSH_GROUP => "Group Push",
+ gl.c.GL_DEBUG_TYPE_POP_GROUP => "Group Pop",
+ gl.c.GL_DEBUG_TYPE_OTHER => "Other",
+ else => "Unknown",
+ };
+
+ const msg_str = msg[0..@intCast(len)];
+
+ (switch (severity) {
+ gl.c.GL_DEBUG_SEVERITY_HIGH => log.err(
+ "[{d}] ({s}: {s}) {s}",
+ .{ id, src_str, typ_str, msg_str },
+ ),
+ gl.c.GL_DEBUG_SEVERITY_MEDIUM => log.warn(
+ "[{d}] ({s}: {s}) {s}",
+ .{ id, src_str, typ_str, msg_str },
+ ),
+ gl.c.GL_DEBUG_SEVERITY_LOW => log.info(
+ "[{d}] ({s}: {s}) {s}",
+ .{ id, src_str, typ_str, msg_str },
+ ),
+ gl.c.GL_DEBUG_SEVERITY_NOTIFICATION => log.debug(
+ "[{d}] ({s}: {s}) {s}",
+ .{ id, src_str, typ_str, msg_str },
+ ),
+ else => log.warn(
+ "UNKNOWN SEVERITY [{d}] ({s}: {s}) {s}",
+ .{ id, src_str, typ_str, msg_str },
+ ),
+ });
+}
+
+/// Prepares the provided GL context, loading it with glad.
+fn prepareContext(getProcAddress: anytype) !void {
+ const version = try gl.glad.load(getProcAddress);
+ const major = gl.glad.versionMajor(@intCast(version));
+ const minor = gl.glad.versionMinor(@intCast(version));
+ errdefer gl.glad.unload();
+ log.info("loaded OpenGL {}.{}", .{ major, minor });
+
+ // Need to check version before trying to enable it
+ if (major < MIN_VERSION_MAJOR or
+ (major == MIN_VERSION_MAJOR and minor < MIN_VERSION_MINOR))
+ {
+ log.warn(
+ "OpenGL version is too old. Ghostty requires OpenGL {d}.{d}",
+ .{ MIN_VERSION_MAJOR, MIN_VERSION_MINOR },
+ );
+ return error.OpenGLOutdated;
+ }
+
+ // Enable debug output for the context.
+ try gl.enable(gl.c.GL_DEBUG_OUTPUT);
+
+ // Register our debug message callback with the OpenGL context.
+ gl.glad.context.DebugMessageCallback.?(glDebugMessageCallback, null);
+
+ // Enable SRGB framebuffer for linear blending support.
+ try gl.enable(gl.c.GL_FRAMEBUFFER_SRGB);
+}
+
+/// This is called early right after surface creation.
+pub fn surfaceInit(surface: *apprt.Surface) !void {
+ _ = surface;
+
+ switch (apprt.runtime) {
+ else => @compileError("unsupported app runtime for OpenGL"),
+
+ // GTK uses global OpenGL context so we load from null.
+ apprt.gtk,
+ => try prepareContext(null),
+
+ apprt.embedded => {
+ // TODO(mitchellh): this does nothing today to allow libghostty
+ // to compile for OpenGL targets but libghostty is strictly
+ // broken for rendering on this platforms.
+ },
+ }
+
+ // These are very noisy so this is commented, but easy to uncomment
+ // whenever we need to check the OpenGL extension list
+ // if (builtin.mode == .Debug) {
+ // var ext_iter = try gl.ext.iterator();
+ // while (try ext_iter.next()) |ext| {
+ // log.debug("OpenGL extension available name={s}", .{ext});
+ // }
+ // }
+}
+
+/// This is called just prior to spinning up the renderer
+/// thread for final main thread setup requirements.
+pub fn finalizeSurfaceInit(self: *const OpenGL, surface: *apprt.Surface) !void {
+ _ = self;
+ _ = surface;
+}
+
+/// Callback called by renderer.Thread when it begins.
+pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void {
+ _ = self;
+ _ = surface;
+
+ switch (apprt.runtime) {
+ else => @compileError("unsupported app runtime for OpenGL"),
+
+ apprt.gtk => {
+ // GTK doesn't support threaded OpenGL operations as far as I can
+ // tell, so we use the renderer thread to setup all the state
+ // but then do the actual draws and texture syncs and all that
+ // on the main thread. As such, we don't do anything here.
+ },
+
+ apprt.embedded => {
+ // TODO(mitchellh): this does nothing today to allow libghostty
+ // to compile for OpenGL targets but libghostty is strictly
+ // broken for rendering on this platforms.
+ },
+ }
+}
+
+/// Callback called by renderer.Thread when it exits.
+pub fn threadExit(self: *const OpenGL) void {
+ _ = self;
+
+ switch (apprt.runtime) {
+ else => @compileError("unsupported app runtime for OpenGL"),
+
+ apprt.gtk => {
+ // We don't need to do any unloading for GTK because we may
+ // be sharing the global bindings with other windows.
+ },
+
+ apprt.embedded => {
+ // TODO: see threadEnter
+ },
+ }
+}
+
+pub fn displayRealized(self: *const OpenGL) void {
+ _ = self;
+
+ switch (apprt.runtime) {
+ apprt.gtk => prepareContext(null) catch |err| {
+ log.warn(
+ "Error preparing GL context in displayRealized, err={}",
+ .{err},
+ );
+ },
+
+ else => @compileError("only GTK should be calling displayRealized"),
+ }
+}
+
+/// Actions taken before doing anything in `drawFrame`.
+///
+/// Right now there's nothing we need to do for OpenGL.
+pub fn drawFrameStart(self: *OpenGL) void {
+ _ = self;
+}
+
+/// Actions taken after `drawFrame` is done.
+///
+/// Right now there's nothing we need to do for OpenGL.
+pub fn drawFrameEnd(self: *OpenGL) void {
+ _ = self;
+}
+
+pub fn initShaders(
+ self: *const OpenGL,
+ alloc: Allocator,
+ custom_shaders: []const [:0]const u8,
+) !shaders.Shaders {
+ _ = alloc;
+ return try shaders.Shaders.init(
+ self.alloc,
+ custom_shaders,
+ );
+}
+
+/// Get the current size of the runtime surface.
+pub fn surfaceSize(self: *const OpenGL) !struct { width: u32, height: u32 } {
+ _ = self;
+ var viewport: [4]gl.c.GLint = undefined;
+ gl.glad.context.GetIntegerv.?(gl.c.GL_VIEWPORT, &viewport);
+ return .{
+ .width = @intCast(viewport[2]),
+ .height = @intCast(viewport[3]),
+ };
+}
+
+/// Initialize a new render target which can be presented by this API.
+pub fn initTarget(self: *const OpenGL, width: usize, height: usize) !Target {
+ return Target.init(.{
+ .internal_format = if (self.blending.isLinear()) .srgba else .rgba,
+ .width = width,
+ .height = height,
+ });
+}
+
+/// Present the provided target.
+pub fn present(self: *OpenGL, target: Target) !void {
+ // In order to present a target we blit it to the default framebuffer.
+
+ // We disable GL_FRAMEBUFFER_SRGB while doing this blit, otherwise the
+ // values may be linearized as they're copied, but even though the draw
+ // framebuffer has a linear internal format, the values in it should be
+ // sRGB, not linear!
+ try gl.disable(gl.c.GL_FRAMEBUFFER_SRGB);
+ defer gl.enable(gl.c.GL_FRAMEBUFFER_SRGB) catch |err| {
+ log.err("Error re-enabling GL_FRAMEBUFFER_SRGB, err={}", .{err});
+ };
+
+ // Bind the target for reading.
+ const fbobind = try target.framebuffer.bind(.read);
+ defer fbobind.unbind();
+
+ // Blit
+ gl.glad.context.BlitFramebuffer.?(
+ 0,
+ 0,
+ @intCast(target.width),
+ @intCast(target.height),
+ 0,
+ 0,
+ @intCast(target.width),
+ @intCast(target.height),
+ gl.c.GL_COLOR_BUFFER_BIT,
+ gl.c.GL_NEAREST,
+ );
+
+ // Keep track of this target in case we need to repeat it.
+ self.last_target = target;
+}
+
+/// Present the last presented target again.
+pub fn presentLastTarget(self: *OpenGL) !void {
+ if (self.last_target) |target| try self.present(target);
+}
+
+/// Returns the options to use when constructing buffers.
+pub inline fn bufferOptions(self: OpenGL) bufferpkg.Options {
+ _ = self;
+ return .{
+ .target = .array,
+ .usage = .dynamic_draw,
+ };
+}
+
+pub const instanceBufferOptions = bufferOptions;
+pub const uniformBufferOptions = bufferOptions;
+pub const fgBufferOptions = bufferOptions;
+pub const bgBufferOptions = bufferOptions;
+pub const imageBufferOptions = bufferOptions;
+pub const bgImageBufferOptions = bufferOptions;
+
+/// Returns the options to use when constructing textures.
+pub inline fn textureOptions(self: OpenGL) Texture.Options {
+ _ = self;
+ return .{
+ .format = .rgba,
+ .internal_format = .srgba,
+ .target = .@"2D",
+ .min_filter = .linear,
+ .mag_filter = .linear,
+ .wrap_s = .clamp_to_edge,
+ .wrap_t = .clamp_to_edge,
+ };
+}
+
+/// Returns the options to use when constructing samplers.
+pub inline fn samplerOptions(self: OpenGL) Sampler.Options {
+ _ = self;
+ return .{
+ .min_filter = .linear,
+ .mag_filter = .linear,
+ .wrap_s = .clamp_to_edge,
+ .wrap_t = .clamp_to_edge,
+ };
+}
+
+/// Pixel format for image texture options.
+pub const ImageTextureFormat = enum {
+ /// 1 byte per pixel grayscale.
+ gray,
+ /// 4 bytes per pixel RGBA.
+ rgba,
+ /// 4 bytes per pixel BGRA.
+ bgra,
+
+ fn toPixelFormat(self: ImageTextureFormat) gl.Texture.Format {
+ return switch (self) {
+ .gray => .red,
+ .rgba => .rgba,
+ .bgra => .bgra,
+ };
+ }
+};
+
+/// Returns the options to use when constructing textures for images.
+pub inline fn imageTextureOptions(
+ self: OpenGL,
+ format: ImageTextureFormat,
+ srgb: bool,
+) Texture.Options {
+ _ = self;
+ return .{
+ .format = format.toPixelFormat(),
+ .internal_format = if (srgb) .srgba else .rgba,
+ .target = .@"2D",
+ // TODO: Generate mipmaps for image textures and use
+ // linear_mipmap_linear filtering so that they
+ // look good even when scaled way down.
+ .min_filter = .linear,
+ .mag_filter = .linear,
+ // TODO: Separate out background image options, use
+ // repeating coordinate modes so we don't have
+ // to do the modulus in the shader.
+ .wrap_s = .clamp_to_edge,
+ .wrap_t = .clamp_to_edge,
+ };
+}
+
+/// Initializes a Texture suitable for the provided font atlas.
+pub fn initAtlasTexture(
+ self: *const OpenGL,
+ atlas: *const font.Atlas,
+) Texture.Error!Texture {
+ _ = self;
+ const format: gl.Texture.Format, const internal_format: gl.Texture.InternalFormat =
+ switch (atlas.format) {
+ .grayscale => .{ .red, .red },
+ .bgra => .{ .bgra, .srgba },
+ else => @panic("unsupported atlas format for OpenGL texture"),
+ };
+
+ return try Texture.init(
+ .{
+ .format = format,
+ .internal_format = internal_format,
+ .target = .Rectangle,
+ .min_filter = .nearest,
+ .mag_filter = .nearest,
+ .wrap_s = .clamp_to_edge,
+ .wrap_t = .clamp_to_edge,
+ },
+ atlas.size,
+ atlas.size,
+ null,
+ );
+}
+
+/// Begin a frame.
+pub inline fn beginFrame(
+ self: *const OpenGL,
+ /// Once the frame has been completed, the `frameCompleted` method
+ /// on the renderer is called with the health status of the frame.
+ renderer: *Renderer,
+ /// The target is presented via the provided renderer's API when completed.
+ target: *Target,
+) !Frame {
+ _ = self;
+ return try Frame.begin(.{}, renderer, target);
+}
diff --git a/vendor/libghostty-vt/src/renderer/Options.zig b/vendor/libghostty-vt/src/renderer/Options.zig
new file mode 100644
index 00000000..948b31d2
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/Options.zig
@@ -0,0 +1,24 @@
+//! The options that are used to configure a renderer.
+
+const apprt = @import("../apprt.zig");
+const font = @import("../font/main.zig");
+const renderer = @import("../renderer.zig");
+
+/// The derived configuration for this renderer implementation.
+config: renderer.Renderer.DerivedConfig,
+
+/// The font grid that should be used along with the key for deref-ing.
+font_grid: *font.SharedGrid,
+
+/// The size of everything.
+size: renderer.Size,
+
+/// The mailbox for sending the surface messages. This is only valid
+/// once the thread has started and should not be used outside of the thread.
+surface_mailbox: apprt.surface.Mailbox,
+
+/// The apprt surface.
+rt_surface: *apprt.Surface,
+
+/// The renderer thread.
+thread: *renderer.Thread,
diff --git a/vendor/libghostty-vt/src/renderer/Overlay.zig b/vendor/libghostty-vt/src/renderer/Overlay.zig
new file mode 100644
index 00000000..62eb004b
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/Overlay.zig
@@ -0,0 +1,415 @@
+/// The debug overlay that can be drawn on top of the terminal
+/// during the rendering process.
+///
+/// This is implemented by doing all the drawing on the CPU via z2d,
+/// since the debug overlay isn't that common, z2d is pretty fast, and
+/// it simplifies our implementation quite a bit by not relying on us
+/// having a bunch of shaders that we have to write per-platform.
+///
+/// Initialize the overlay, apply features with `applyFeatures`, then
+/// get the resulting image with `pendingImage` to upload to the GPU.
+/// This works in concert with `renderer.image.State` to simplify. Draw
+/// it on the GPU as an image composited on top of the terminal output.
+const Overlay = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const z2d = @import("z2d");
+const terminal = @import("../terminal/main.zig");
+const size = @import("size.zig");
+const Size = size.Size;
+const CellSize = size.CellSize;
+const Image = @import("image.zig").Image;
+
+const log = std.log.scoped(.renderer_overlay);
+
+/// The colors we use for overlays.
+pub const Color = enum {
+ hyperlink, // light blue
+ semantic_prompt, // orange/gold
+ semantic_input, // cyan
+
+ pub fn rgb(self: Color) z2d.pixel.RGB {
+ return switch (self) {
+ .hyperlink => .{ .r = 180, .g = 180, .b = 255 },
+ .semantic_prompt => .{ .r = 255, .g = 200, .b = 64 },
+ .semantic_input => .{ .r = 64, .g = 200, .b = 255 },
+ };
+ }
+
+ /// The fill color for rectangles.
+ pub fn rectFill(self: Color) z2d.Pixel {
+ return self.alphaPixel(96);
+ }
+
+ /// The border color for rectangles.
+ pub fn rectBorder(self: Color) z2d.Pixel {
+ return self.alphaPixel(200);
+ }
+
+ /// The raw RGB as a pixel.
+ pub fn pixel(self: Color) z2d.Pixel {
+ return self.rgb().asPixel();
+ }
+
+ fn alphaPixel(self: Color, alpha: u8) z2d.Pixel {
+ var rgba: z2d.pixel.RGBA = .fromPixel(self.pixel());
+ rgba.a = alpha;
+ return rgba.multiply().asPixel();
+ }
+};
+
+/// The surface we're drawing our overlay to.
+surface: z2d.Surface,
+
+/// Cell size information so we can map grid coordinates to pixels.
+cell_size: CellSize,
+
+/// The set of available features and their configuration.
+pub const Feature = union(enum) {
+ highlight_hyperlinks,
+ semantic_prompts,
+};
+
+pub const InitError = Allocator.Error || error{
+ // The terminal dimensions are invalid to support an overlay.
+ // Either too small or too big.
+ InvalidDimensions,
+};
+
+/// Initialize a new, blank overlay.
+pub fn init(alloc: Allocator, sz: Size) InitError!Overlay {
+ // Our surface does NOT need to take into account padding because
+ // we render the overlay using the image subsystem and shaders which
+ // already take that into account.
+ const term_size = sz.terminal();
+ var sfc = z2d.Surface.initPixel(
+ .{ .rgba = .{ .r = 0, .g = 0, .b = 0, .a = 0 } },
+ alloc,
+ std.math.cast(i32, term_size.width) orelse
+ return error.InvalidDimensions,
+ std.math.cast(i32, term_size.height) orelse
+ return error.InvalidDimensions,
+ ) catch |err| switch (err) {
+ error.OutOfMemory => return error.OutOfMemory,
+ error.InvalidWidth, error.InvalidHeight => return error.InvalidDimensions,
+ };
+ errdefer sfc.deinit(alloc);
+
+ return .{
+ .surface = sfc,
+ .cell_size = sz.cell,
+ };
+}
+
+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 {
+ return .{
+ .width = @intCast(self.surface.getWidth()),
+ .height = @intCast(self.surface.getHeight()),
+ .pixel_format = .rgba,
+ .data = @ptrCast(self.surface.image_surface_rgba.buf.ptr),
+ };
+}
+
+/// Clear the overlay.
+pub fn reset(self: *Overlay) void {
+ self.surface.paintPixel(.{ .rgba = .{
+ .r = 0,
+ .g = 0,
+ .b = 0,
+ .a = 0,
+ } });
+}
+
+/// Apply the given features to this overlay. This will draw on top of
+/// any pre-existing content in the overlay.
+pub fn applyFeatures(
+ self: *Overlay,
+ alloc: Allocator,
+ state: *const terminal.RenderState,
+ features: []const Feature,
+) void {
+ for (features) |f| switch (f) {
+ .highlight_hyperlinks => self.highlightHyperlinks(
+ alloc,
+ state,
+ ),
+ .semantic_prompts => self.highlightSemanticPrompts(
+ alloc,
+ state,
+ ),
+ };
+}
+
+/// Add rectangles around contiguous hyperlinks in the render state.
+///
+/// Note: this currently doesn't take into account unique hyperlink IDs
+/// because the render state doesn't contain this. This will be added
+/// later.
+fn highlightHyperlinks(
+ self: *Overlay,
+ alloc: Allocator,
+ state: *const terminal.RenderState,
+) void {
+ const border_color = Color.hyperlink.rectBorder();
+ const fill_color = Color.hyperlink.rectFill();
+
+ const row_slice = state.row_data.slice();
+ const row_raw = row_slice.items(.raw);
+ const row_cells = row_slice.items(.cells);
+ for (row_raw, row_cells, 0..) |row, cells, y| {
+ if (!row.hyperlink) continue;
+
+ const cells_slice = cells.slice();
+ const raw_cells = cells_slice.items(.raw);
+
+ var x: usize = 0;
+ while (x < raw_cells.len) {
+ // Skip cells without hyperlinks
+ if (!raw_cells[x].hyperlink) {
+ x += 1;
+ continue;
+ }
+
+ // Found start of a hyperlink run
+ const start_x = x;
+
+ // Find end of contiguous hyperlink cells
+ while (x < raw_cells.len and raw_cells[x].hyperlink) x += 1;
+ const end_x = x;
+
+ self.highlightGridRect(
+ alloc,
+ start_x,
+ y,
+ end_x - start_x,
+ 1,
+ border_color,
+ fill_color,
+ ) catch |err| {
+ std.log.warn("Error drawing hyperlink border: {}", .{err});
+ };
+ }
+ }
+}
+
+fn highlightSemanticPrompts(
+ self: *Overlay,
+ alloc: Allocator,
+ state: *const terminal.RenderState,
+) void {
+ const row_slice = state.row_data.slice();
+ const row_raw = row_slice.items(.raw);
+ const row_cells = row_slice.items(.cells);
+
+ // Highlight the row-level semantic prompt bars. The prompts are easy
+ // because they're part of the row metadata.
+ {
+ const prompt_border = Color.semantic_prompt.rectBorder();
+ const prompt_fill = Color.semantic_prompt.rectFill();
+
+ var y: usize = 0;
+ while (y < row_raw.len) {
+ // If its not a semantic prompt row, skip it.
+ if (row_raw[y].semantic_prompt == .none) {
+ y += 1;
+ continue;
+ }
+
+ // Find the full length of the semantic prompt row by connecting
+ // all continuations.
+ const start_y = y;
+ y += 1;
+ while (y < row_raw.len and
+ row_raw[y].semantic_prompt == .prompt_continuation)
+ {
+ y += 1;
+ }
+ const end_y = y; // Exclusive
+
+ const bar_width = @min(@as(usize, 5), self.cell_size.width);
+ self.highlightPixelRect(
+ alloc,
+ 0,
+ start_y,
+ bar_width,
+ end_y - start_y,
+ prompt_border,
+ prompt_fill,
+ ) catch |err| {
+ log.warn("Error drawing semantic prompt bar: {}", .{err});
+ };
+ }
+ }
+
+ // Highlight contiguous semantic cells within rows.
+ for (row_cells, 0..) |cells, y| {
+ const cells_slice = cells.slice();
+ const raw_cells = cells_slice.items(.raw);
+
+ var x: usize = 0;
+ while (x < raw_cells.len) {
+ const cell = raw_cells[x];
+ const content = cell.semantic_content;
+ const start_x = x;
+
+ // We skip output because its just the rest of the non-prompt
+ // parts and it makes the overlay too noisy.
+ if (cell.semantic_content == .output) {
+ x += 1;
+ continue;
+ }
+
+ // Find the end of this content.
+ x += 1;
+ while (x < raw_cells.len) {
+ const next = raw_cells[x];
+ if (next.semantic_content != content) break;
+ x += 1;
+ }
+
+ const color: Color = switch (content) {
+ .prompt => .semantic_prompt,
+ .input => .semantic_input,
+ .output => unreachable,
+ };
+
+ self.highlightGridRect(
+ alloc,
+ start_x,
+ y,
+ x - start_x,
+ 1,
+ color.rectBorder(),
+ color.rectFill(),
+ ) catch |err| {
+ log.warn("Error drawing semantic content highlight: {}", .{err});
+ };
+ }
+ }
+}
+
+/// Creates a rectangle for highlighting a grid region. x/y/width/height
+/// are all in grid cells.
+fn highlightGridRect(
+ self: *Overlay,
+ alloc: Allocator,
+ x: usize,
+ y: usize,
+ width: usize,
+ height: usize,
+ border_color: z2d.Pixel,
+ fill_color: z2d.Pixel,
+) !void {
+ // All math below uses checked arithmetic to avoid overflows. The
+ // inputs aren't trusted and the path this is in isn't hot enough
+ // to wrarrant unsafe optimizations.
+
+ // Calculate our width/height in pixels.
+ const px_width = std.math.cast(i32, try std.math.mul(
+ usize,
+ width,
+ self.cell_size.width,
+ )) orelse return error.Overflow;
+ const px_height = std.math.cast(i32, try std.math.mul(
+ usize,
+ height,
+ self.cell_size.height,
+ )) orelse return error.Overflow;
+
+ // Calculate pixel coordinates
+ const start_x: f64 = @floatFromInt(std.math.cast(i32, try std.math.mul(
+ usize,
+ x,
+ self.cell_size.width,
+ )) orelse return error.Overflow);
+ const start_y: f64 = @floatFromInt(std.math.cast(i32, try std.math.mul(
+ usize,
+ y,
+ self.cell_size.height,
+ )) orelse return error.Overflow);
+ const end_x: f64 = start_x + @as(f64, @floatFromInt(px_width));
+ const end_y: f64 = start_y + @as(f64, @floatFromInt(px_height));
+
+ // Grab our context to draw
+ var ctx: z2d.Context = .init(alloc, &self.surface);
+ defer ctx.deinit();
+
+ // Don't need AA because we use sharp edges
+ ctx.setAntiAliasingMode(.none);
+ // Can use hairline since we have 1px borders
+ ctx.setHairline(true);
+
+ // Draw rectangle path
+ try ctx.moveTo(start_x, start_y);
+ try ctx.lineTo(end_x, start_y);
+ try ctx.lineTo(end_x, end_y);
+ try ctx.lineTo(start_x, end_y);
+ try ctx.closePath();
+
+ // Fill
+ ctx.setSourceToPixel(fill_color);
+ try ctx.fill();
+
+ // Border
+ ctx.setLineWidth(1);
+ ctx.setSourceToPixel(border_color);
+ try ctx.stroke();
+}
+
+/// Creates a rectangle for highlighting a region. x/y are grid cells and
+/// width/height are pixels.
+fn highlightPixelRect(
+ self: *Overlay,
+ alloc: Allocator,
+ x: usize,
+ y: usize,
+ width_px: usize,
+ height: usize,
+ border_color: z2d.Pixel,
+ fill_color: z2d.Pixel,
+) !void {
+ const px_width = std.math.cast(i32, width_px) orelse return error.Overflow;
+ const px_height = std.math.cast(i32, try std.math.mul(
+ usize,
+ height,
+ self.cell_size.height,
+ )) orelse return error.Overflow;
+
+ const start_x: f64 = @floatFromInt(std.math.cast(i32, try std.math.mul(
+ usize,
+ x,
+ self.cell_size.width,
+ )) orelse return error.Overflow);
+ const start_y: f64 = @floatFromInt(std.math.cast(i32, try std.math.mul(
+ usize,
+ y,
+ self.cell_size.height,
+ )) orelse return error.Overflow);
+ const end_x: f64 = start_x + @as(f64, @floatFromInt(px_width));
+ const end_y: f64 = start_y + @as(f64, @floatFromInt(px_height));
+
+ var ctx: z2d.Context = .init(alloc, &self.surface);
+ defer ctx.deinit();
+
+ ctx.setAntiAliasingMode(.none);
+ ctx.setHairline(true);
+
+ try ctx.moveTo(start_x, start_y);
+ try ctx.lineTo(end_x, start_y);
+ try ctx.lineTo(end_x, end_y);
+ try ctx.lineTo(start_x, end_y);
+ try ctx.closePath();
+
+ ctx.setSourceToPixel(fill_color);
+ try ctx.fill();
+
+ ctx.setLineWidth(1);
+ ctx.setSourceToPixel(border_color);
+ try ctx.stroke();
+}
diff --git a/vendor/libghostty-vt/src/renderer/State.zig b/vendor/libghostty-vt/src/renderer/State.zig
new file mode 100644
index 00000000..e882ddce
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/State.zig
@@ -0,0 +1,123 @@
+//! This is the render state that is given to a renderer.
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const Inspector = @import("../inspector/main.zig").Inspector;
+const terminalpkg = @import("../terminal/main.zig");
+const inputpkg = @import("../input.zig");
+const renderer = @import("../renderer.zig");
+
+/// The mutex that must be held while reading any of the data in the
+/// members of this state. Note that the state itself is NOT protected
+/// by the mutex and is NOT thread-safe, only the members values of the
+/// state (i.e. the terminal, devmode, etc. values).
+mutex: *std.Thread.Mutex,
+
+/// The terminal data.
+terminal: *terminalpkg.Terminal,
+
+/// The terminal inspector, if any. This will be null while the inspector
+/// is not active and will be set when it is active.
+inspector: ?*Inspector = null,
+
+/// Dead key state. This will render the current dead key preedit text
+/// over the cursor. This currently only ever renders a single codepoint.
+/// Preedit can in theory be multiple codepoints long but that is left as
+/// a future exercise.
+preedit: ?Preedit = null,
+
+/// Mouse state. This only contains state relevant to what renderers
+/// need about the mouse.
+mouse: Mouse = .{},
+
+pub const Mouse = struct {
+ /// The point on the viewport where the mouse currently is. We use
+ /// viewport points to avoid the complexity of mapping the mouse to
+ /// the renderer state.
+ point: ?terminalpkg.point.Coordinate = null,
+
+ /// The mods that are currently active for the last mouse event.
+ /// This could really just be mods in general and we probably will
+ /// move it out of mouse state at some point.
+ mods: inputpkg.Mods = .{},
+};
+
+/// The pre-edit state. See Surface.preeditCallback for more information.
+pub const Preedit = struct {
+ /// The codepoints to render as preedit text.
+ codepoints: []const Codepoint = &.{},
+
+ /// A single codepoint to render as preedit text.
+ pub const Codepoint = struct {
+ codepoint: u21,
+ wide: bool = false,
+ };
+
+ /// Deinit this preedit that was cre
+ pub fn deinit(self: *const Preedit, alloc: Allocator) void {
+ alloc.free(self.codepoints);
+ }
+
+ /// Allocate a copy of this preedit in the given allocator..
+ pub fn clone(self: *const Preedit, alloc: Allocator) !Preedit {
+ return .{
+ .codepoints = try alloc.dupe(Codepoint, self.codepoints),
+ };
+ }
+
+ /// The width in cells of all codepoints in the preedit.
+ pub fn width(self: *const Preedit) usize {
+ var result: usize = 0;
+ for (self.codepoints) |cp| {
+ result += if (cp.wide) 2 else 1;
+ }
+
+ return result;
+ }
+
+ /// Range returns the start and end x position of the preedit text
+ /// along with any codepoint offset necessary to fit the preedit
+ /// into the available space.
+ pub fn range(
+ self: *const Preedit,
+ start: terminalpkg.size.CellCountInt,
+ max: terminalpkg.size.CellCountInt,
+ ) struct {
+ start: terminalpkg.size.CellCountInt,
+ end: terminalpkg.size.CellCountInt,
+ cp_offset: usize,
+ } {
+ // If our width is greater than the number of cells we have
+ // then we need to adjust our codepoint start to a point where
+ // our width would be less than the number of cells we have.
+ const w, const cp_offset = width: {
+ // max is inclusive, so we need to add 1 to it.
+ const max_width = max - start + 1;
+
+ // Rebuild our width in reverse order. This is because we want
+ // to offset by the end cells, not the start cells (if we have to).
+ var w: terminalpkg.size.CellCountInt = 0;
+ for (0..self.codepoints.len) |i| {
+ const reverse_i = self.codepoints.len - i - 1;
+ const cp = self.codepoints[reverse_i];
+ w += if (cp.wide) 2 else 1;
+ if (w > max_width) {
+ break :width .{ w, reverse_i };
+ }
+ }
+
+ // Width fit in the max width so no offset necessary.
+ break :width .{ w, 0 };
+ };
+
+ // If our preedit goes off the end of the screen, we adjust it so
+ // that it shifts left.
+ const end = start + w;
+ const start_offset = if (end > max) end - max else 0;
+ return .{
+ .start = start -| start_offset,
+ .end = end -| start_offset,
+ .cp_offset = cp_offset,
+ };
+ }
+};
diff --git a/vendor/libghostty-vt/src/renderer/Thread.zig b/vendor/libghostty-vt/src/renderer/Thread.zig
new file mode 100644
index 00000000..50872137
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/Thread.zig
@@ -0,0 +1,715 @@
+//! Represents the renderer thread logic. The renderer thread is able to
+//! be woken up to render.
+pub const Thread = @This();
+
+const std = @import("std");
+const builtin = @import("builtin");
+const xev = @import("../global.zig").xev;
+const crash = @import("../crash/main.zig");
+const internal_os = @import("../os/main.zig");
+const rendererpkg = @import("../renderer.zig");
+const apprt = @import("../apprt.zig");
+const configpkg = @import("../config.zig");
+const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue;
+const App = @import("../App.zig");
+
+const Allocator = std.mem.Allocator;
+const log = std.log.scoped(.renderer_thread);
+
+const DRAW_INTERVAL = 8; // 120 FPS
+const CURSOR_BLINK_INTERVAL = 600;
+
+/// Whether calls to `drawFrame` must be done from the app thread.
+///
+/// If this is `true` then we send a `redraw_surface` message to the apprt
+/// whenever we need to draw instead of calling `drawFrame` directly.
+const must_draw_from_app_thread =
+ if (@hasDecl(apprt.App, "must_draw_from_app_thread"))
+ apprt.App.must_draw_from_app_thread
+ else
+ false;
+
+/// The type used for sending messages to the IO thread. For now this is
+/// hardcoded with a capacity. We can make this a comptime parameter in
+/// the future if we want it configurable.
+pub const Mailbox = BlockingQueue(rendererpkg.Message, 64);
+
+/// Allocator used for some state
+alloc: std.mem.Allocator,
+
+/// The main event loop for the application. The user data of this loop
+/// is always the allocator used to create the loop. This is a convenience
+/// so that users of the loop always have an allocator.
+loop: xev.Loop,
+
+/// This can be used to wake up the renderer and force a render safely from
+/// any thread.
+wakeup: xev.Async,
+wakeup_c: xev.Completion = .{},
+
+/// This can be used to stop the renderer on the next loop iteration.
+stop: xev.Async,
+stop_c: xev.Completion = .{},
+
+/// The timer used for rendering
+render_h: xev.Timer,
+render_c: xev.Completion = .{},
+
+/// The timer used for draw calls. Draw calls don't update from the
+/// terminal state so they're much cheaper. They're used for animation
+/// and are paused when the terminal is not focused.
+draw_h: xev.Timer,
+draw_c: xev.Completion = .{},
+draw_active: bool = false,
+
+/// This async is used to force a draw immediately. This does not
+/// coalesce like the wakeup does.
+draw_now: xev.Async,
+draw_now_c: xev.Completion = .{},
+
+/// The timer used for cursor blinking
+cursor_h: xev.Timer,
+cursor_c: xev.Completion = .{},
+cursor_c_cancel: xev.Completion = .{},
+
+/// The surface we're rendering to.
+surface: *apprt.Surface,
+
+/// The underlying renderer implementation.
+renderer: *rendererpkg.Renderer,
+
+/// Pointer to the shared state that is used to generate the final render.
+state: *rendererpkg.State,
+
+/// The mailbox that can be used to send this thread messages. Note
+/// this is a blocking queue so if it is full you will get errors (or block).
+mailbox: *Mailbox,
+
+/// Mailbox to send messages to the app thread
+app_mailbox: App.Mailbox,
+
+/// Configuration we need derived from the main config.
+config: DerivedConfig,
+
+flags: packed struct {
+ /// This is true when a blinking cursor should be visible and false
+ /// when it should not be visible. This is toggled on a timer by the
+ /// thread automatically.
+ cursor_blink_visible: bool = false,
+
+ /// This is true when the inspector is active.
+ has_inspector: bool = false,
+
+ /// This is true when the view is visible. This is used to determine
+ /// if we should be rendering or not.
+ visible: bool = true,
+
+ /// This is true when the view is focused. This defaults to true
+ /// and it is up to the apprt to set the correct value.
+ focused: bool = true,
+} = .{},
+
+pub const DerivedConfig = struct {
+ custom_shader_animation: configpkg.CustomShaderAnimation,
+
+ pub fn init(config: *const configpkg.Config) DerivedConfig {
+ return .{
+ .custom_shader_animation = config.@"custom-shader-animation",
+ };
+ }
+};
+
+/// Initialize the thread. This does not START the thread. This only sets
+/// up all the internal state necessary prior to starting the thread. It
+/// is up to the caller to start the thread with the threadMain entrypoint.
+pub fn init(
+ alloc: Allocator,
+ config: *const configpkg.Config,
+ surface: *apprt.Surface,
+ renderer_impl: *rendererpkg.Renderer,
+ state: *rendererpkg.State,
+ app_mailbox: App.Mailbox,
+) !Thread {
+ // Create our event loop.
+ var loop = try xev.Loop.init(.{});
+ errdefer loop.deinit();
+
+ // This async handle is used to "wake up" the renderer and force a render.
+ var wakeup_h = try xev.Async.init();
+ errdefer wakeup_h.deinit();
+
+ // This async handle is used to stop the loop and force the thread to end.
+ var stop_h = try xev.Async.init();
+ errdefer stop_h.deinit();
+
+ // The primary timer for rendering.
+ var render_h = try xev.Timer.init();
+ errdefer render_h.deinit();
+
+ // Draw timer, see comments.
+ var draw_h = try xev.Timer.init();
+ errdefer draw_h.deinit();
+
+ // Draw now async, see comments.
+ var draw_now = try xev.Async.init();
+ errdefer draw_now.deinit();
+
+ // Setup a timer for blinking the cursor
+ var cursor_timer = try xev.Timer.init();
+ errdefer cursor_timer.deinit();
+
+ // The mailbox for messaging this thread
+ var mailbox = try Mailbox.create(alloc);
+ errdefer mailbox.destroy(alloc);
+
+ return .{
+ .alloc = alloc,
+ .config = .init(config),
+ .loop = loop,
+ .wakeup = wakeup_h,
+ .stop = stop_h,
+ .render_h = render_h,
+ .draw_h = draw_h,
+ .draw_now = draw_now,
+ .cursor_h = cursor_timer,
+ .surface = surface,
+ .renderer = renderer_impl,
+ .state = state,
+ .mailbox = mailbox,
+ .app_mailbox = app_mailbox,
+ };
+}
+
+/// Clean up the thread. This is only safe to call once the thread
+/// completes executing; the caller must join prior to this.
+pub fn deinit(self: *Thread) void {
+ self.stop.deinit();
+ self.wakeup.deinit();
+ self.render_h.deinit();
+ self.draw_h.deinit();
+ self.draw_now.deinit();
+ self.cursor_h.deinit();
+ self.loop.deinit();
+
+ // Nothing can possibly access the mailbox anymore, destroy it.
+ self.mailbox.destroy(self.alloc);
+}
+
+/// The main entrypoint for the thread.
+pub fn threadMain(self: *Thread) void {
+ // Call child function so we can use errors...
+ self.threadMain_() catch |err| {
+ // In the future, we should expose this on the thread struct.
+ log.warn("error in renderer err={}", .{err});
+ };
+}
+
+fn threadMain_(self: *Thread) !void {
+ defer log.debug("renderer thread exited", .{});
+
+ // Right now, on Darwin, `std.Thread.setName` can only name the current
+ // thread, and we have no way to get the current thread from within it,
+ // so instead we use this code to name the thread instead.
+ if (builtin.os.tag.isDarwin()) {
+ internal_os.macos.pthread_setname_np(&"renderer".*);
+ }
+
+ // Setup our crash metadata
+ crash.sentry.thread_state = .{
+ .type = .renderer,
+ .surface = self.renderer.surface_mailbox.surface,
+ };
+ defer crash.sentry.thread_state = null;
+
+ // Setup our thread QoS
+ self.setQosClass();
+
+ // Run our loop start/end callbacks if the renderer cares.
+ const has_loop = @hasDecl(rendererpkg.Renderer, "loopEnter");
+ if (has_loop) try self.renderer.loopEnter(self);
+ defer if (has_loop) self.renderer.loopExit();
+
+ // Run our thread start/end callbacks. This is important because some
+ // renderers have to do per-thread setup. For example, OpenGL has to set
+ // some thread-local state since that is how it works.
+ try self.renderer.threadEnter(self.surface);
+ defer self.renderer.threadExit();
+
+ // Start the async handlers
+ self.wakeup.wait(&self.loop, &self.wakeup_c, Thread, self, wakeupCallback);
+ self.stop.wait(&self.loop, &self.stop_c, Thread, self, stopCallback);
+ self.draw_now.wait(&self.loop, &self.draw_now_c, Thread, self, drawNowCallback);
+
+ // Send an initial wakeup message so that we render right away.
+ try self.wakeup.notify();
+
+ // Start blinking the cursor.
+ self.cursor_h.run(
+ &self.loop,
+ &self.cursor_c,
+ cursorBlinkInterval(),
+ Thread,
+ self,
+ cursorTimerCallback,
+ );
+
+ // Start the draw timer
+ self.syncDrawTimer();
+
+ // Run
+ log.debug("starting renderer thread", .{});
+ defer log.debug("starting renderer thread shutdown", .{});
+ _ = try self.loop.run(.until_done);
+}
+
+fn setQosClass(self: *const Thread) void {
+ // Thread QoS classes are only relevant on macOS.
+ if (comptime !builtin.target.os.tag.isDarwin()) return;
+
+ const class: internal_os.macos.QosClass = class: {
+ // If we aren't visible (our view is fully occluded) then we
+ // always drop our rendering priority down because it's just
+ // mostly wasted work.
+ //
+ // The renderer itself should be doing this as well (for example
+ // Metal will stop our DisplayLink) but this also helps with
+ // general forced updates and CPU usage i.e. a rebuild cells call.
+ if (!self.flags.visible) break :class .utility;
+
+ // If we're not focused, but we're visible, then we set a higher
+ // than default priority because framerates still matter but it isn't
+ // as important as when we're focused.
+ if (!self.flags.focused) break :class .user_initiated;
+
+ // We are focused and visible, we are the definition of user interactive.
+ break :class .user_interactive;
+ };
+
+ if (internal_os.macos.setQosClass(class)) {
+ log.debug("thread QoS class set class={}", .{class});
+ } else |err| {
+ log.warn("error setting QoS class err={}", .{err});
+ }
+}
+
+fn syncDrawTimer(self: *Thread) void {
+ skip: {
+ // If our renderer supports animations and has them, then we
+ // can apply draw timer based on custom shader animation configuration.
+ if (@hasDecl(rendererpkg.Renderer, "hasAnimations") and
+ self.renderer.hasAnimations())
+ {
+ // If our config says to always animate, we do so.
+ switch (self.config.custom_shader_animation) {
+ // Always animate
+ .always => break :skip,
+ // Only when focused
+ .true => if (self.flags.focused) break :skip,
+ // Never animate
+ .false => {},
+ }
+ }
+
+ // We're skipping the draw timer. Stop it on the next iteration.
+ self.draw_active = false;
+ return;
+ }
+
+ // Set our active state so it knows we're running. We set this before
+ // even checking the active state in case we have a pending shutdown.
+ self.draw_active = true;
+
+ // If our draw timer is already active, then we don't have to do anything.
+ if (self.draw_c.state() == .active) return;
+
+ // Start the timer which loops
+ self.draw_h.run(
+ &self.loop,
+ &self.draw_c,
+ DRAW_INTERVAL,
+ Thread,
+ self,
+ drawCallback,
+ );
+}
+
+/// Drain the mailbox.
+fn drainMailbox(self: *Thread) !void {
+ // There's probably a more elegant way to do this...
+ //
+ // This is effectively an @autoreleasepool{} block, which we need in
+ // order to ensure that autoreleased objects are properly released.
+ const pool = if (builtin.os.tag.isDarwin())
+ @import("objc").AutoreleasePool.init()
+ else
+ void;
+ defer if (builtin.os.tag.isDarwin()) pool.deinit();
+
+ while (self.mailbox.pop()) |message| {
+ log.debug("mailbox message={}", .{message});
+ switch (message) {
+ .crash => @panic("crash request, crashing intentionally"),
+
+ .visible => |v| visible: {
+ // If our state didn't change we do nothing.
+ if (self.flags.visible == v) break :visible;
+
+ // Set our visible state
+ self.flags.visible = v;
+
+ // Visibility affects our QoS class
+ self.setQosClass();
+
+ // If we became visible then we immediately trigger a draw.
+ // We don't need to update frame data because that should
+ // still be happening.
+ if (v) self.drawFrame(false);
+
+ // Notify the renderer so it can update any state.
+ self.renderer.setVisible(v);
+
+ // Note that we're explicitly today not stopping any
+ // cursor timers, draw timers, etc. These things have very
+ // little resource cost and properly maintaining their active
+ // state across different transitions is going to be bug-prone,
+ // so its easier to just let them keep firing and have them
+ // check the visible state themselves to control their behavior.
+ },
+
+ .focus => |v| focus: {
+ // If our state didn't change we do nothing.
+ if (self.flags.focused == v) break :focus;
+
+ // Set our state
+ self.flags.focused = v;
+
+ // Focus affects our QoS class
+ self.setQosClass();
+
+ // Set it on the renderer
+ try self.renderer.setFocus(v);
+
+ // We always resync our draw timer (may disable it)
+ self.syncDrawTimer();
+
+ if (!v) {
+ // If we're not focused, then we stop the cursor blink
+ if (self.cursor_c.state() == .active and
+ self.cursor_c_cancel.state() == .dead)
+ {
+ self.cursor_h.cancel(
+ &self.loop,
+ &self.cursor_c,
+ &self.cursor_c_cancel,
+ void,
+ null,
+ cursorCancelCallback,
+ );
+ }
+ } else {
+ // If we're focused, we immediately show the cursor again
+ // and then restart the timer.
+ if (self.cursor_c.state() != .active) {
+ self.flags.cursor_blink_visible = true;
+ self.cursor_h.run(
+ &self.loop,
+ &self.cursor_c,
+ cursorBlinkInterval(),
+ Thread,
+ self,
+ cursorTimerCallback,
+ );
+ }
+ }
+ },
+
+ .reset_cursor_blink => {
+ self.flags.cursor_blink_visible = true;
+ if (self.cursor_c.state() == .active) {
+ self.cursor_h.reset(
+ &self.loop,
+ &self.cursor_c,
+ &self.cursor_c_cancel,
+ cursorBlinkInterval(),
+ Thread,
+ self,
+ cursorTimerCallback,
+ );
+ }
+ },
+
+ .font_grid => |grid| {
+ self.renderer.setFontGrid(grid.grid);
+ grid.set.deref(grid.old_key);
+ },
+
+ .resize => |v| self.renderer.setScreenSize(v),
+
+ .change_config => |config| {
+ defer config.alloc.destroy(config.thread);
+ defer config.alloc.destroy(config.impl);
+ try self.changeConfig(config.thread);
+ try self.renderer.changeConfig(config.impl);
+
+ // Stop and start the draw timer to capture the new
+ // hasAnimations value.
+ self.syncDrawTimer();
+ },
+
+ .search_viewport_matches => |v| {
+ // Note we don't free the new value because we expect our
+ // allocators to match.
+ if (self.renderer.search_matches) |*m| m.arena.deinit();
+ self.renderer.search_matches = v;
+ self.renderer.search_matches_dirty = true;
+ },
+
+ .search_selected_match => |v| {
+ // Note we don't free the new value because we expect our
+ // allocators to match.
+ if (self.renderer.search_selected_match) |*m| m.arena.deinit();
+ self.renderer.search_selected_match = v;
+ self.renderer.search_matches_dirty = true;
+ },
+
+ .inspector => |v| {
+ self.flags.has_inspector = v;
+ },
+
+ .macos_display_id => |v| {
+ if (@hasDecl(rendererpkg.Renderer, "setMacOSDisplayID")) {
+ try self.renderer.setMacOSDisplayID(v);
+ }
+ },
+ }
+ }
+}
+
+fn changeConfig(self: *Thread, config: *const DerivedConfig) !void {
+ self.config = config.*;
+}
+
+/// Trigger a draw. This will not update frame data or anything, it will
+/// just trigger a draw/paint.
+fn drawFrame(self: *Thread, now: bool) void {
+ // If we're invisible, we do not draw.
+ if (!self.flags.visible) return;
+
+ // If the renderer is managing a vsync on its own, we only draw
+ // when we're forced to via `now`.
+ if (!now and self.renderer.hasVsync()) return;
+
+ if (must_draw_from_app_thread) {
+ _ = self.app_mailbox.push(
+ .{ .redraw_surface = self.surface },
+ .{ .instant = {} },
+ );
+ } else {
+ self.renderer.drawFrame(false) catch |err|
+ log.warn("error drawing err={}", .{err});
+ }
+}
+
+fn wakeupCallback(
+ self_: ?*Thread,
+ _: *xev.Loop,
+ _: *xev.Completion,
+ r: xev.Async.WaitError!void,
+) xev.CallbackAction {
+ _ = r catch |err| {
+ log.err("error in wakeup err={}", .{err});
+ return .rearm;
+ };
+
+ const t = self_.?;
+
+ // When we wake up, we check the mailbox. Mailbox producers should
+ // wake up our thread after publishing.
+ t.drainMailbox() catch |err|
+ log.err("error draining mailbox err={}", .{err});
+
+ // Render immediately
+ _ = renderCallback(t, undefined, undefined, {});
+
+ // The below is not used anymore but if we ever want to introduce
+ // a configuration to introduce a delay to coalesce renders, we can
+ // use this.
+ //
+ // // If the timer is already active then we don't have to do anything.
+ // if (t.render_c.state() == .active) return .rearm;
+ //
+ // // Timer is not active, let's start it
+ // t.render_h.run(
+ // &t.loop,
+ // &t.render_c,
+ // 10,
+ // Thread,
+ // t,
+ // renderCallback,
+ // );
+
+ return .rearm;
+}
+
+fn drawNowCallback(
+ self_: ?*Thread,
+ _: *xev.Loop,
+ _: *xev.Completion,
+ r: xev.Async.WaitError!void,
+) xev.CallbackAction {
+ _ = r catch |err| {
+ log.err("error in draw now err={}", .{err});
+ return .rearm;
+ };
+
+ // Draw immediately
+ const t = self_.?;
+ t.drawFrame(true);
+
+ return .rearm;
+}
+
+fn drawCallback(
+ self_: ?*Thread,
+ _: *xev.Loop,
+ _: *xev.Completion,
+ r: xev.Timer.RunError!void,
+) xev.CallbackAction {
+ _ = r catch unreachable;
+ const t: *Thread = self_ orelse {
+ // This shouldn't happen so we log it.
+ log.warn("render callback fired without data set", .{});
+ return .disarm;
+ };
+
+ // Draw
+ t.drawFrame(false);
+
+ // Only continue if we're still active
+ if (t.draw_active) {
+ t.draw_h.run(&t.loop, &t.draw_c, DRAW_INTERVAL, Thread, t, drawCallback);
+ }
+
+ return .disarm;
+}
+
+fn renderCallback(
+ self_: ?*Thread,
+ _: *xev.Loop,
+ _: *xev.Completion,
+ r: xev.Timer.RunError!void,
+) xev.CallbackAction {
+ _ = r catch unreachable;
+ const t: *Thread = self_ orelse {
+ // This shouldn't happen so we log it.
+ log.warn("render callback fired without data set", .{});
+ return .disarm;
+ };
+
+ // Update our frame data
+ t.renderer.updateFrame(
+ t.state,
+ t.flags.cursor_blink_visible,
+ ) catch |err|
+ log.warn("error rendering err={}", .{err});
+
+ // Draw
+ t.drawFrame(false);
+
+ return .disarm;
+}
+
+fn cursorTimerCallback(
+ self_: ?*Thread,
+ _: *xev.Loop,
+ _: *xev.Completion,
+ r: xev.Timer.RunError!void,
+) xev.CallbackAction {
+ _ = r catch |err| switch (err) {
+ // This is sent when our timer is canceled. That's fine.
+ error.Canceled => return .disarm,
+
+ else => {
+ log.warn("error in cursor timer callback err={}", .{err});
+ unreachable;
+ },
+ };
+
+ const t: *Thread = self_ orelse {
+ // This shouldn't happen so we log it.
+ log.warn("render callback fired without data set", .{});
+ return .disarm;
+ };
+
+ t.flags.cursor_blink_visible = !t.flags.cursor_blink_visible;
+ t.wakeup.notify() catch {};
+
+ t.cursor_h.run(
+ &t.loop,
+ &t.cursor_c,
+ cursorBlinkInterval(),
+ Thread,
+ t,
+ cursorTimerCallback,
+ );
+ return .disarm;
+}
+
+fn cursorCancelCallback(
+ _: ?*void,
+ _: *xev.Loop,
+ _: *xev.Completion,
+ r: xev.Timer.CancelError!void,
+) xev.CallbackAction {
+ // This makes it easier to work across platforms where different platforms
+ // support different sets of errors, so we just unify it.
+ const CancelError = xev.Timer.CancelError || error{
+ Canceled,
+ NotFound,
+ Unexpected,
+ };
+
+ _ = r catch |err| switch (@as(CancelError, @errorCast(err))) {
+ error.Canceled => {}, // success
+ error.NotFound => {}, // completed before it could cancel
+ else => {
+ log.warn("error in cursor cancel callback err={}", .{err});
+ unreachable;
+ },
+ };
+
+ return .disarm;
+}
+
+// fn prepFrameCallback(h: *libuv.Prepare) void {
+// _ = h;
+//
+// tracy.frameMark();
+// }
+
+fn stopCallback(
+ self_: ?*Thread,
+ _: *xev.Loop,
+ _: *xev.Completion,
+ r: xev.Async.WaitError!void,
+) xev.CallbackAction {
+ _ = r catch unreachable;
+ self_.?.loop.stop();
+ return .disarm;
+}
+
+/// Returns the interval for the blinking cursor in milliseconds.
+fn cursorBlinkInterval() u64 {
+ if (std.valgrind.runningOnValgrind() > 0) {
+ // If we're running under Valgrind, the cursor blink adds enough
+ // churn that it makes some stalls annoying unless you're on a
+ // super powerful computer, so we delay it.
+ //
+ // This is a hack, we should change some of our cursor timer
+ // logic to be more efficient:
+ // https://github.com/ghostty-org/ghostty/issues/8003
+ return CURSOR_BLINK_INTERVAL * 5;
+ }
+
+ return CURSOR_BLINK_INTERVAL;
+}
diff --git a/vendor/libghostty-vt/src/renderer/WebGL.zig b/vendor/libghostty-vt/src/renderer/WebGL.zig
new file mode 100644
index 00000000..655f7a50
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/WebGL.zig
@@ -0,0 +1,2 @@
+//! Renderer implementation for WebGL in the browser.
+pub const WebGL = @This();
diff --git a/vendor/libghostty-vt/src/renderer/backend.zig b/vendor/libghostty-vt/src/renderer/backend.zig
new file mode 100644
index 00000000..942e1f0f
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/backend.zig
@@ -0,0 +1,23 @@
+const std = @import("std");
+const WasmTarget = @import("../os/wasm/target.zig").Target;
+
+/// Possible implementations, used for build options.
+pub const Backend = enum {
+ opengl,
+ metal,
+ webgl,
+
+ pub fn default(
+ target: std.Target,
+ wasm_target: WasmTarget,
+ ) Backend {
+ if (target.cpu.arch == .wasm32) {
+ return switch (wasm_target) {
+ .browser => .webgl,
+ };
+ }
+
+ if (target.os.tag.isDarwin()) return .metal;
+ return .opengl;
+ }
+};
diff --git a/vendor/libghostty-vt/src/renderer/cell.zig b/vendor/libghostty-vt/src/renderer/cell.zig
new file mode 100644
index 00000000..196ebb17
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/cell.zig
@@ -0,0 +1,680 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = @import("../quirks.zig").inlineAssert;
+const font = @import("../font/main.zig");
+const terminal = @import("../terminal/main.zig");
+const renderer = @import("../renderer.zig");
+const shaderpkg = renderer.Renderer.API.shaders;
+const ArrayListCollection = @import("../datastruct/array_list_collection.zig").ArrayListCollection;
+const symbols = @import("../unicode/symbols_table.zig").table;
+
+/// The possible cell content keys that exist.
+pub const Key = enum {
+ bg,
+ text,
+ underline,
+ strikethrough,
+ overline,
+
+ /// Returns the GPU vertex type for this key.
+ pub fn CellType(self: Key) type {
+ return switch (self) {
+ .bg => shaderpkg.CellBg,
+
+ .text,
+ .underline,
+ .strikethrough,
+ .overline,
+ => shaderpkg.CellText,
+ };
+ }
+};
+
+/// The contents of all the cells in the terminal.
+///
+/// The goal of this data structure is to allow for efficient row-wise
+/// clearing of data from the GPU buffers, to allow for row-wise dirty
+/// tracking to eliminate the overhead of rebuilding the GPU buffers
+/// each frame.
+///
+/// Must be initialized by resizing before calling any operations.
+pub const Contents = struct {
+ size: renderer.GridSize = .{ .rows = 0, .columns = 0 },
+
+ /// Flat array containing cell background colors for the terminal grid.
+ ///
+ /// Indexed as `bg_cells[row * size.columns + col]`.
+ ///
+ /// Prefer accessing with `Contents.bgCell(row, col).*` instead
+ /// of directly indexing in order to avoid integer size bugs.
+ bg_cells: []shaderpkg.CellBg = undefined,
+
+ /// The ArrayListCollection which holds all of the foreground cells. When
+ /// sized with Contents.resize the individual ArrayLists are given enough
+ /// room that they can hold a single row with #cols glyphs, underlines, and
+ /// strikethroughs; however, appendAssumeCapacity MUST NOT be used since
+ /// it is possible to exceed this with combining glyphs that add a glyph
+ /// but take up no column since they combine with the previous one, as
+ /// well as with fonts that perform multi-substitutions for glyphs, which
+ /// can result in a similar situation where multiple glyphs reside in the
+ /// same column.
+ ///
+ /// Allocations should nevertheless be exceedingly rare since hitting the
+ /// initial capacity of a list would require a row filled with underlined
+ /// struck through characters, at least one of which is a multi-glyph
+ /// composite.
+ ///
+ /// Rows are indexed as Contents.fg_rows[y + 1], because the first list in
+ /// the collection is reserved for the cursor, which must be the first item
+ /// in the buffer.
+ ///
+ /// Must be initialized by calling resize on the Contents struct before
+ /// calling any operations.
+ fg_rows: ArrayListCollection(shaderpkg.CellText) = .{ .lists = &.{} },
+
+ pub fn deinit(self: *Contents, alloc: Allocator) void {
+ alloc.free(self.bg_cells);
+ self.fg_rows.deinit(alloc);
+ }
+
+ /// Resize the cell contents for the given grid size. This will
+ /// always invalidate the entire cell contents.
+ pub fn resize(
+ self: *Contents,
+ alloc: Allocator,
+ size: renderer.GridSize,
+ ) Allocator.Error!void {
+ self.size = size;
+
+ const cell_count = @as(usize, size.columns) * @as(usize, size.rows);
+
+ const bg_cells = try alloc.alloc(shaderpkg.CellBg, cell_count);
+ errdefer alloc.free(bg_cells);
+ @memset(bg_cells, .{ 0, 0, 0, 0 });
+
+ // The foreground lists can hold 3 types of items:
+ // - Glyphs
+ // - Underlines
+ // - Strikethroughs
+ // So we give them an initial capacity of size.columns * 3, which will
+ // avoid any further allocations in the vast majority of cases. Sadly
+ // we can not assume capacity though, since with combining glyphs that
+ // form a single grapheme, and multi-substitutions in fonts, the number
+ // of glyphs in a row is theoretically unlimited.
+ //
+ // We have size.rows + 2 lists because indexes 0 and size.rows - 1 are
+ // used for special lists containing the cursor cell which need to
+ // be first and last in the buffer, respectively.
+ var fg_rows: ArrayListCollection(shaderpkg.CellText) = try .init(
+ alloc,
+ size.rows + 2,
+ size.columns * 3,
+ );
+ errdefer fg_rows.deinit(alloc);
+
+ // We don't need 3*cols worth of cells for the cursor lists, so we can
+ // replace them with smaller lists. This is technically a tiny bit of
+ // extra work but resize is not a hot function so it's worth it to not
+ // waste the memory.
+ fg_rows.lists[0].deinit(alloc);
+ fg_rows.lists[0] = try .initCapacity(alloc, 1);
+ fg_rows.lists[size.rows + 1].deinit(alloc);
+ fg_rows.lists[size.rows + 1] = try .initCapacity(alloc, 1);
+
+ // Perform the swap, no going back from here.
+ errdefer comptime unreachable;
+ alloc.free(self.bg_cells);
+ self.fg_rows.deinit(alloc);
+ self.bg_cells = bg_cells;
+ self.fg_rows = fg_rows;
+ }
+
+ /// Reset the cell contents to an empty state without resizing.
+ pub fn reset(self: *Contents) void {
+ @memset(self.bg_cells, .{ 0, 0, 0, 0 });
+ self.fg_rows.reset();
+ }
+
+ /// Set the cursor value. If the value is null then the cursor is hidden.
+ pub fn setCursor(
+ self: *Contents,
+ v: ?shaderpkg.CellText,
+ cursor_style: ?renderer.CursorStyle,
+ ) void {
+ if (self.size.rows == 0) return;
+ self.fg_rows.lists[0].clearRetainingCapacity();
+ self.fg_rows.lists[self.size.rows + 1].clearRetainingCapacity();
+
+ const cell = v orelse return;
+ const style = cursor_style orelse return;
+
+ switch (style) {
+ // Block cursors should be drawn first
+ .block => self.fg_rows.lists[0].appendAssumeCapacity(cell),
+ // Other cursor styles should be drawn last
+ .block_hollow, .bar, .underline, .lock => self.fg_rows.lists[self.size.rows + 1].appendAssumeCapacity(cell),
+ }
+ }
+
+ /// Returns the current cursor glyph if present, checking both cursor lists.
+ pub fn getCursorGlyph(self: *Contents) ?shaderpkg.CellText {
+ if (self.size.rows == 0) return null;
+ if (self.fg_rows.lists[0].items.len > 0) {
+ return self.fg_rows.lists[0].items[0];
+ }
+ if (self.fg_rows.lists[self.size.rows + 1].items.len > 0) {
+ return self.fg_rows.lists[self.size.rows + 1].items[0];
+ }
+ return null;
+ }
+
+ /// Access a background cell. Prefer this function over direct indexing
+ /// of `bg_cells` in order to avoid integer size bugs causing overflows.
+ pub inline fn bgCell(
+ self: *Contents,
+ row: usize,
+ col: usize,
+ ) *shaderpkg.CellBg {
+ return &self.bg_cells[row * self.size.columns + col];
+ }
+
+ /// Add a cell to the appropriate list. Adding the same cell twice will
+ /// result in duplication in the vertex buffer. The caller should clear
+ /// the corresponding row with Contents.clear to remove old cells first.
+ pub fn add(
+ self: *Contents,
+ alloc: Allocator,
+ comptime key: Key,
+ cell: key.CellType(),
+ ) Allocator.Error!void {
+ const y = cell.grid_pos[1];
+
+ assert(y < self.size.rows);
+
+ switch (key) {
+ .bg => comptime unreachable,
+
+ .text,
+ .underline,
+ .strikethrough,
+ .overline,
+ // We have a special list containing the cursor cell at the start
+ // of our fg row collection, so we need to add 1 to the y to get
+ // the correct index.
+ => try self.fg_rows.lists[y + 1].append(alloc, cell),
+ }
+ }
+
+ /// Clear all of the cell contents for a given row.
+ pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void {
+ assert(y < self.size.rows);
+
+ @memset(self.bg_cells[@as(usize, y) * self.size.columns ..][0..self.size.columns], .{ 0, 0, 0, 0 });
+
+ // We have a special list containing the cursor cell at the start
+ // of our fg row collection, so we need to add 1 to the y to get
+ // the correct index.
+ self.fg_rows.lists[y + 1].clearRetainingCapacity();
+ }
+};
+
+/// Returns true if a codepoint for a cell is a covering character. A covering
+/// character is a character that covers the entire cell. This is used to
+/// make window-padding-color=extend work better. See #2099.
+pub fn isCovering(cp: u21) bool {
+ return switch (cp) {
+ // U+2588 FULL BLOCK
+ 0x2588 => true,
+
+ else => false,
+ };
+}
+
+/// Returns true of the codepoint is a "symbol-like" character, which
+/// for now we define as anything in a private use area, and anything
+/// in several unicode blocks:
+/// - Arrows
+/// - Dingbats
+/// - Emoticons
+/// - Miscellaneous Symbols
+/// - Enclosed Alphanumerics
+/// - Enclosed Alphanumeric Supplement
+/// - Miscellaneous Symbols and Pictographs
+/// - Transport and Map Symbols
+///
+/// In the future it may be prudent to expand this to encompass more
+/// symbol-like characters, and/or exclude some PUA sections.
+pub fn isSymbol(cp: u21) bool {
+ return symbols.get(cp);
+}
+
+/// Returns the appropriate `constraint_width` for
+/// the provided cell when rendering its glyph(s).
+pub fn constraintWidth(
+ raw_slice: []const terminal.page.Cell,
+ x: usize,
+ cols: usize,
+) u2 {
+ const cell = raw_slice[x];
+ const cp = cell.codepoint();
+
+ const grid_width = cell.gridWidth();
+
+ // If the grid width of the cell is 2, the constraint
+ // width will always be 2, so we can just return early.
+ if (grid_width > 1) return grid_width;
+
+ // We allow "symbol-like" glyphs to extend to 2 cells wide if there's
+ // space, and if the previous glyph wasn't also a symbol. So if this
+ // codepoint isn't a symbol then we can return the grid width.
+ if (!isSymbol(cp)) return grid_width;
+
+ // If we are at the end of the screen it must be constrained to one cell.
+ if (x == cols - 1) return 1;
+
+ // If we have a previous cell and it was a symbol then we need
+ // to also constrain. This is so that multiple PUA glyphs align.
+ // This does not apply if the previous symbol is a graphics
+ // element such as a block element or Powerline glyph.
+ if (x > 0) {
+ const prev_cp = raw_slice[x - 1].codepoint();
+ if (isSymbol(prev_cp) and !isGraphicsElement(prev_cp)) {
+ return 1;
+ }
+ }
+
+ // If the next cell is whitespace, then we
+ // allow the glyph to be up to two cells wide.
+ const next_cp = raw_slice[x + 1].codepoint();
+ if (next_cp == 0 or isSpace(next_cp)) return 2;
+
+ // Otherwise, this has to be 1 cell wide.
+ return 1;
+}
+
+/// Whether min contrast should be disabled for a given glyph. True
+/// for graphics elements such as blocks and Powerline glyphs.
+pub fn noMinContrast(cp: u21) bool {
+ return isGraphicsElement(cp);
+}
+
+// Some general spaces, others intentionally kept
+// to force the font to render as a fixed width.
+fn isSpace(char: u21) bool {
+ return switch (char) {
+ 0x0020, // SPACE
+ 0x2002, // EN SPACE
+ => true,
+ else => false,
+ };
+}
+
+/// Returns true if the codepoint is used for terminal graphics, such
+/// as box drawing characters, block elements, and Powerline glyphs.
+fn isGraphicsElement(char: u21) bool {
+ return isBoxDrawing(char) or isBlockElement(char) or isLegacyComputing(char) or isPowerline(char);
+}
+
+// Returns true if the codepoint is a box drawing character.
+fn isBoxDrawing(char: u21) bool {
+ return switch (char) {
+ 0x2500...0x257F => true,
+ else => false,
+ };
+}
+
+// Returns true if the codepoint is a block element.
+fn isBlockElement(char: u21) bool {
+ return switch (char) {
+ 0x2580...0x259F => true,
+ else => false,
+ };
+}
+
+// Returns true if the codepoint is in a Symbols for Legacy
+// Computing block, including supplements.
+fn isLegacyComputing(char: u21) bool {
+ return switch (char) {
+ 0x1FB00...0x1FBFF => true,
+ 0x1CC00...0x1CEBF => true, // Supplement introduced in Unicode 16.0
+ else => false,
+ };
+}
+
+// Returns true if the codepoint is a part of the Powerline range.
+fn isPowerline(char: u21) bool {
+ return switch (char) {
+ 0xE0B0...0xE0D7 => true,
+ else => false,
+ };
+}
+
+test Contents {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const rows = 10;
+ const cols = 10;
+
+ var c: Contents = .{};
+ try c.resize(alloc, .{ .rows = rows, .columns = cols });
+ defer c.deinit(alloc);
+
+ // We should start off empty after resizing.
+ for (0..rows) |y| {
+ try testing.expect(c.fg_rows.lists[y + 1].items.len == 0);
+ for (0..cols) |x| {
+ try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(y, x).*);
+ }
+ }
+ // And the cursor row should have a capacity of 1 and also be empty.
+ try testing.expect(c.fg_rows.lists[0].capacity == 1);
+ try testing.expect(c.fg_rows.lists[0].items.len == 0);
+
+ // Add some contents.
+ const bg_cell: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
+ const fg_cell: shaderpkg.CellText = .{
+ .atlas = .grayscale,
+ .grid_pos = .{ 4, 1 },
+ .color = .{ 0, 0, 0, 1 },
+ };
+ c.bgCell(1, 4).* = bg_cell;
+ try c.add(alloc, .text, fg_cell);
+ try testing.expectEqual(bg_cell, c.bgCell(1, 4).*);
+ // The fg row index is offset by 1 because of the cursor list.
+ try testing.expectEqual(fg_cell, c.fg_rows.lists[2].items[0]);
+
+ // And we should be able to clear it.
+ c.clear(1);
+ for (0..rows) |y| {
+ try testing.expect(c.fg_rows.lists[y + 1].items.len == 0);
+ for (0..cols) |x| {
+ try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(y, x).*);
+ }
+ }
+
+ // Add a block cursor.
+ const cursor_cell: shaderpkg.CellText = .{
+ .atlas = .grayscale,
+ .bools = .{ .is_cursor_glyph = true },
+ .grid_pos = .{ 2, 3 },
+ .color = .{ 0, 0, 0, 1 },
+ };
+ c.setCursor(cursor_cell, .block);
+ try testing.expectEqual(cursor_cell, c.fg_rows.lists[0].items[0]);
+ try testing.expectEqual(cursor_cell, c.getCursorGlyph().?);
+
+ // And remove it.
+ c.setCursor(null, null);
+ try testing.expectEqual(0, c.fg_rows.lists[0].items.len);
+ try testing.expect(c.getCursorGlyph() == null);
+
+ // Add a hollow cursor.
+ c.setCursor(cursor_cell, .block_hollow);
+ try testing.expectEqual(cursor_cell, c.fg_rows.lists[rows + 1].items[0]);
+ try testing.expectEqual(cursor_cell, c.getCursorGlyph().?);
+}
+
+test "Contents clear retains other content" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const rows = 10;
+ const cols = 10;
+
+ var c: Contents = .{};
+ try c.resize(alloc, .{ .rows = rows, .columns = cols });
+ defer c.deinit(alloc);
+
+ // Set some contents
+ // bg and fg cells in row 1
+ const bg_cell_1: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
+ const fg_cell_1: shaderpkg.CellText = .{
+ .atlas = .grayscale,
+ .grid_pos = .{ 4, 1 },
+ .color = .{ 0, 0, 0, 1 },
+ };
+ c.bgCell(1, 4).* = bg_cell_1;
+ try c.add(alloc, .text, fg_cell_1);
+ // bg and fg cells in row 2
+ const bg_cell_2: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
+ const fg_cell_2: shaderpkg.CellText = .{
+ .atlas = .grayscale,
+ .grid_pos = .{ 4, 2 },
+ .color = .{ 0, 0, 0, 1 },
+ };
+ c.bgCell(2, 4).* = bg_cell_2;
+ try c.add(alloc, .text, fg_cell_2);
+
+ // Clear row 1, this should leave row 2 untouched
+ c.clear(1);
+
+ // Row 2 should still contain its cells.
+ try testing.expectEqual(bg_cell_2, c.bgCell(2, 4).*);
+ // Fg row index is +1 because of cursor list at start
+ try testing.expectEqual(fg_cell_2, c.fg_rows.lists[3].items[0]);
+}
+
+test "Contents clear last added content" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const rows = 10;
+ const cols = 10;
+
+ var c: Contents = .{};
+ try c.resize(alloc, .{ .rows = rows, .columns = cols });
+ defer c.deinit(alloc);
+
+ // Set some contents
+ // bg and fg cells in row 1
+ const bg_cell_1: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
+ const fg_cell_1: shaderpkg.CellText = .{
+ .atlas = .grayscale,
+ .grid_pos = .{ 4, 1 },
+ .color = .{ 0, 0, 0, 1 },
+ };
+ c.bgCell(1, 4).* = bg_cell_1;
+ try c.add(alloc, .text, fg_cell_1);
+ // bg and fg cells in row 2
+ const bg_cell_2: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
+ const fg_cell_2: shaderpkg.CellText = .{
+ .atlas = .grayscale,
+ .grid_pos = .{ 4, 2 },
+ .color = .{ 0, 0, 0, 1 },
+ };
+ c.bgCell(2, 4).* = bg_cell_2;
+ try c.add(alloc, .text, fg_cell_2);
+
+ // Clear row 2, this should leave row 1 untouched
+ c.clear(2);
+
+ // Row 1 should still contain its cells.
+ try testing.expectEqual(bg_cell_1, c.bgCell(1, 4).*);
+ // Fg row index is +1 because of cursor list at start
+ try testing.expectEqual(fg_cell_1, c.fg_rows.lists[2].items[0]);
+}
+
+test "Contents with zero-sized screen" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var c: Contents = .{};
+ defer c.deinit(alloc);
+
+ c.setCursor(null, null);
+ try testing.expect(c.getCursorGlyph() == null);
+}
+
+test "Cell constraint widths" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var t: terminal.Terminal = try .init(alloc, .{
+ .cols = 4,
+ .rows = 1,
+ });
+ defer t.deinit(alloc);
+
+ var s = t.vtStream();
+ defer s.deinit();
+
+ var state: terminal.RenderState = .empty;
+ defer state.deinit(alloc);
+
+ // for each case, the numbers in the comment denote expected
+ // constraint widths for the symbol-containing cells
+
+ // symbol->nothing: 2
+ {
+ t.fullReset();
+ s.nextSlice("");
+ try state.update(alloc, &t);
+ try testing.expectEqual(2, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ }
+
+ // symbol->character: 1
+ {
+ t.fullReset();
+ s.nextSlice("z");
+ try state.update(alloc, &t);
+ try testing.expectEqual(1, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ }
+
+ // symbol->space: 2
+ {
+ t.fullReset();
+ s.nextSlice(" z");
+ try state.update(alloc, &t);
+ try testing.expectEqual(2, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ }
+ // symbol->no-break space: 1
+ {
+ t.fullReset();
+ s.nextSlice("\u{00a0}z");
+ try state.update(alloc, &t);
+ try testing.expectEqual(1, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ }
+
+ // symbol->end of row: 1
+ {
+ t.fullReset();
+ s.nextSlice(" ");
+ try state.update(alloc, &t);
+ try testing.expectEqual(1, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 3,
+ state.cols,
+ ));
+ }
+
+ // character->symbol: 2
+ {
+ t.fullReset();
+ s.nextSlice("z");
+ try state.update(alloc, &t);
+ try testing.expectEqual(2, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 1,
+ state.cols,
+ ));
+ }
+
+ // symbol->symbol: 1,1
+ {
+ t.fullReset();
+ s.nextSlice("");
+ try state.update(alloc, &t);
+ try testing.expectEqual(1, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ try testing.expectEqual(1, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 1,
+ state.cols,
+ ));
+ }
+
+ // symbol->space->symbol: 2,2
+ {
+ t.fullReset();
+ s.nextSlice(" ");
+ try state.update(alloc, &t);
+ try testing.expectEqual(2, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ try testing.expectEqual(2, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 2,
+ state.cols,
+ ));
+ }
+
+ // symbol->powerline: 1 (dedicated test because powerline is special-cased in cellpkg)
+ {
+ t.fullReset();
+ s.nextSlice("");
+ try state.update(alloc, &t);
+ try testing.expectEqual(1, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ }
+
+ // powerline->symbol: 2 (dedicated test because powerline is special-cased in cellpkg)
+ {
+ t.fullReset();
+ s.nextSlice("");
+ try state.update(alloc, &t);
+ try testing.expectEqual(2, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 1,
+ state.cols,
+ ));
+ }
+
+ // powerline->nothing: 2 (dedicated test because powerline is special-cased in cellpkg)
+ {
+ t.fullReset();
+ s.nextSlice("");
+ try state.update(alloc, &t);
+ try testing.expectEqual(2, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ }
+
+ // powerline->space: 2 (dedicated test because powerline is special-cased in cellpkg)
+ {
+ t.fullReset();
+ s.nextSlice(" z");
+ try state.update(alloc, &t);
+ try testing.expectEqual(2, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ }
+}
diff --git a/vendor/libghostty-vt/src/renderer/cursor.zig b/vendor/libghostty-vt/src/renderer/cursor.zig
new file mode 100644
index 00000000..33992bc5
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/cursor.zig
@@ -0,0 +1,154 @@
+const std = @import("std");
+const terminal = @import("../terminal/main.zig");
+
+/// Available cursor styles for drawing that renderers must support.
+/// This is a superset of terminal cursor styles since the renderer supports
+/// some additional cursor states such as the hollow block.
+pub const Style = enum {
+ // Typical cursor input styles
+ block,
+ block_hollow,
+ bar,
+ underline,
+
+ // Special cursor styles
+ lock,
+
+ /// Create a cursor style from the terminal style request.
+ pub fn fromTerminal(term: terminal.CursorStyle) Style {
+ return switch (term) {
+ .bar => .bar,
+ .block => .block,
+ .block_hollow => .block_hollow,
+ .underline => .underline,
+ };
+ }
+};
+
+pub const StyleOptions = struct {
+ preedit: bool = false,
+ focused: bool = false,
+ blink_visible: bool = false,
+};
+
+/// Returns the cursor style to use for the current render state or null
+/// if a cursor should not be rendered at all.
+pub fn style(
+ state: *const terminal.RenderState,
+ opts: StyleOptions,
+) ?Style {
+ // Note the order of conditionals below is important. It represents
+ // a priority system of how we determine what state overrides cursor
+ // visibility and style.
+
+ // The cursor must be visible in the viewport to be rendered.
+ if (state.cursor.viewport == null) return null;
+
+ // If we are in preedit, then we always show the block cursor. We do
+ // this even if the cursor is explicitly not visible because it shows
+ // an important editing state to the user.
+ if (opts.preedit) return .block;
+
+ // If we're at a password input its always a lock.
+ if (state.cursor.password_input) return .lock;
+
+ // If the cursor is explicitly not visible by terminal mode, we don't render.
+ if (!state.cursor.visible) return null;
+
+ // If we're not focused, our cursor is always visible so that
+ // we can show the hollow box.
+ if (!opts.focused) return .block_hollow;
+
+ // If the cursor is blinking and our blink state is not visible,
+ // then we don't show the cursor.
+ if (state.cursor.blinking and !opts.blink_visible) return null;
+
+ // Otherwise, we use whatever style the terminal wants.
+ return .fromTerminal(state.cursor.visual_style);
+}
+
+test "cursor: default uses configured style" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ var term: terminal.Terminal = try .init(alloc, .{ .cols = 10, .rows = 10 });
+ defer term.deinit(alloc);
+
+ term.screens.active.cursor.cursor_style = .bar;
+ term.modes.set(.cursor_blinking, true);
+
+ var state: terminal.RenderState = .empty;
+ defer state.deinit(alloc);
+ try state.update(alloc, &term);
+
+ try testing.expect(style(&state, .{ .preedit = false, .focused = true, .blink_visible = true }) == .bar);
+ try testing.expect(style(&state, .{ .preedit = false, .focused = false, .blink_visible = true }) == .block_hollow);
+ try testing.expect(style(&state, .{ .preedit = false, .focused = false, .blink_visible = false }) == .block_hollow);
+ try testing.expect(style(&state, .{ .preedit = false, .focused = true, .blink_visible = false }) == null);
+}
+
+test "cursor: blinking disabled" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ var term = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 10 });
+ defer term.deinit(alloc);
+
+ term.screens.active.cursor.cursor_style = .bar;
+ term.modes.set(.cursor_blinking, false);
+
+ var state: terminal.RenderState = .empty;
+ defer state.deinit(alloc);
+ try state.update(alloc, &term);
+
+ try testing.expect(style(&state, .{ .focused = true, .blink_visible = true }) == .bar);
+ try testing.expect(style(&state, .{ .focused = true, .blink_visible = false }) == .bar);
+ try testing.expect(style(&state, .{ .focused = false, .blink_visible = true }) == .block_hollow);
+ try testing.expect(style(&state, .{ .focused = false, .blink_visible = false }) == .block_hollow);
+}
+
+test "cursor: explicitly not visible" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ var term = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 10 });
+ defer term.deinit(alloc);
+
+ term.screens.active.cursor.cursor_style = .bar;
+ term.modes.set(.cursor_visible, false);
+ term.modes.set(.cursor_blinking, false);
+
+ var state: terminal.RenderState = .empty;
+ defer state.deinit(alloc);
+ try state.update(alloc, &term);
+
+ try testing.expect(style(&state, .{ .focused = true, .blink_visible = true }) == null);
+ try testing.expect(style(&state, .{ .focused = true, .blink_visible = false }) == null);
+ try testing.expect(style(&state, .{ .focused = false, .blink_visible = true }) == null);
+ try testing.expect(style(&state, .{ .focused = false, .blink_visible = false }) == null);
+}
+
+test "cursor: always block with preedit" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ var term = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 10 });
+ defer term.deinit(alloc);
+
+ var state: terminal.RenderState = .empty;
+ defer state.deinit(alloc);
+ try state.update(alloc, &term);
+
+ // In any bool state
+ try testing.expect(style(&state, .{ .preedit = true, .focused = false, .blink_visible = false }) == .block);
+ try testing.expect(style(&state, .{ .preedit = true, .focused = true, .blink_visible = false }) == .block);
+ try testing.expect(style(&state, .{ .preedit = true, .focused = true, .blink_visible = true }) == .block);
+ try testing.expect(style(&state, .{ .preedit = true, .focused = false, .blink_visible = true }) == .block);
+
+ // If we're scrolled though, then we don't show the cursor.
+ for (0..100) |_| try term.index();
+ term.scrollViewport(.{ .top = {} });
+ try state.update(alloc, &term);
+
+ // In any bool state
+ try testing.expect(style(&state, .{ .preedit = true, .focused = false, .blink_visible = false }) == null);
+ try testing.expect(style(&state, .{ .preedit = true, .focused = true, .blink_visible = false }) == null);
+ try testing.expect(style(&state, .{ .preedit = true, .focused = true, .blink_visible = true }) == null);
+ try testing.expect(style(&state, .{ .preedit = true, .focused = false, .blink_visible = true }) == null);
+}
diff --git a/vendor/libghostty-vt/src/renderer/generic.zig b/vendor/libghostty-vt/src/renderer/generic.zig
new file mode 100644
index 00000000..0f4a294b
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/generic.zig
@@ -0,0 +1,3374 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const xev = @import("xev");
+const wuffs = @import("wuffs");
+const apprt = @import("../apprt.zig");
+const configpkg = @import("../config.zig");
+const font = @import("../font/main.zig");
+const inputpkg = @import("../input.zig");
+const os = @import("../os/main.zig");
+const terminal = @import("../terminal/main.zig");
+const renderer = @import("../renderer.zig");
+const math = @import("../math.zig");
+const Surface = @import("../Surface.zig");
+const link = @import("link.zig");
+const cellpkg = @import("cell.zig");
+const noMinContrast = cellpkg.noMinContrast;
+const constraintWidth = cellpkg.constraintWidth;
+const isCovering = cellpkg.isCovering;
+const rowNeverExtendBg = @import("row.zig").neverExtendBg;
+const Overlay = @import("Overlay.zig");
+const imagepkg = @import("image.zig");
+const ImageState = imagepkg.State;
+const shadertoy = @import("shadertoy.zig");
+const assert = @import("../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+const ArenaAllocator = std.heap.ArenaAllocator;
+const Terminal = terminal.Terminal;
+const Health = renderer.Health;
+
+const getConstraint = @import("../font/nerd_font_attributes.zig").getConstraint;
+
+const FileType = @import("../file_type.zig").FileType;
+
+const macos = switch (builtin.os.tag) {
+ .macos => @import("macos"),
+ else => void,
+};
+
+const DisplayLink = switch (builtin.os.tag) {
+ .macos => *macos.video.DisplayLink,
+ else => void,
+};
+
+const log = std.log.scoped(.generic_renderer);
+
+/// Create a renderer type with the provided graphics API wrapper.
+///
+/// The graphics API wrapper must provide the interface outlined below.
+/// Specific details for the interfaces are documented on the existing
+/// implementations (`Metal` and `OpenGL`).
+///
+/// Hierarchy of graphics abstractions:
+///
+/// [ GraphicsAPI ] - Responsible for configuring the runtime surface
+/// | | and providing render `Target`s that draw to it,
+/// | | as well as `Frame`s and `Pipeline`s.
+/// | V
+/// | [ Target ] - Represents an abstract target for rendering, which
+/// | could be a surface directly but is also used as an
+/// | abstraction for off-screen frame buffers.
+/// V
+/// [ Frame ] - Represents the context for drawing a given frame,
+/// | provides `RenderPass`es for issuing draw commands
+/// | to, and reports the frame health when complete.
+/// V
+/// [ RenderPass ] - Represents a render pass in a frame, consisting of
+/// : one or more `Step`s applied to the same target(s),
+/// [ Step ] - - - - each describing the input buffers and textures and
+/// : the vertex/fragment functions and geometry to use.
+/// :_ _ _ _ _ _ _ _ _ _/
+/// v
+/// [ Pipeline ] - Describes a vertex and fragment function to be used
+/// for a `Step`; the `GraphicsAPI` is responsible for
+/// these and they should be constructed and cached
+/// ahead of time.
+///
+/// [ Buffer ] - An abstraction over a GPU buffer.
+///
+/// [ Texture ] - An abstraction over a GPU texture.
+///
+pub fn Renderer(comptime GraphicsAPI: type) type {
+ return struct {
+ const Self = @This();
+
+ pub const API = GraphicsAPI;
+
+ const Target = GraphicsAPI.Target;
+ const Buffer = GraphicsAPI.Buffer;
+ const Sampler = GraphicsAPI.Sampler;
+ const Texture = GraphicsAPI.Texture;
+ const RenderPass = GraphicsAPI.RenderPass;
+
+ const shaderpkg = GraphicsAPI.shaders;
+ const Shaders = shaderpkg.Shaders;
+
+ /// Allocator that can be used
+ alloc: std.mem.Allocator,
+
+ /// This mutex must be held whenever any state used in `drawFrame` is
+ /// being modified, and also when it's being accessed in `drawFrame`.
+ draw_mutex: std.Thread.Mutex = .{},
+
+ /// The configuration we need derived from the main config.
+ config: DerivedConfig,
+
+ /// The mailbox for communicating with the window.
+ surface_mailbox: apprt.surface.Mailbox,
+
+ /// Current font metrics defining our grid.
+ grid_metrics: font.Metrics,
+
+ /// The size of everything.
+ size: renderer.Size,
+
+ /// True if the window is focused
+ focused: bool,
+
+ /// Flag to indicate that our focus state changed for custom
+ /// shaders to update their state.
+ custom_shader_focused_changed: bool = false,
+
+ /// The most recent scrollbar state. We use this as a cache to
+ /// determine if we need to notify the apprt that there was a
+ /// scrollbar change.
+ scrollbar: terminal.Scrollbar,
+ scrollbar_dirty: bool,
+
+ /// Tracks the last bottom-right pin of the screen to detect new output.
+ /// When the final line changes (node or y differs), new content was added.
+ /// Used for scroll-to-bottom on output feature.
+ last_bottom_node: ?usize,
+ last_bottom_y: terminal.size.CellCountInt,
+
+ /// The most recent viewport matches so that we can render search
+ /// matches in the visible frame. This is provided asynchronously
+ /// from the search thread so we have the dirty flag to also note
+ /// if we need to rebuild our cells to include search highlights.
+ ///
+ /// Note that the selections MAY BE INVALID (point to PageList nodes
+ /// that do not exist anymore). These must be validated prior to use.
+ search_matches: ?renderer.Message.SearchMatches,
+ search_selected_match: ?renderer.Message.SearchMatch,
+ search_matches_dirty: bool,
+
+ /// The current set of cells to render. This is rebuilt on every frame
+ /// but we keep this around so that we don't reallocate. Each set of
+ /// cells goes into a separate shader.
+ cells: cellpkg.Contents,
+
+ /// Set to true after rebuildCells is called. This can be used
+ /// to determine if any possible changes have been made to the
+ /// cells for the draw call.
+ cells_rebuilt: bool = false,
+
+ /// The current GPU uniform values.
+ uniforms: shaderpkg.Uniforms,
+
+ /// Custom shader uniform values.
+ custom_shader_uniforms: shadertoy.Uniforms,
+
+ /// Timestamp we rendered out first frame.
+ ///
+ /// This is used when updating custom shader uniforms.
+ first_frame_time: ?std.time.Instant = null,
+
+ /// Timestamp when we rendered out more recent frame.
+ ///
+ /// This is used when updating custom shader uniforms.
+ last_frame_time: ?std.time.Instant = null,
+
+ /// The font structures.
+ font_grid: *font.SharedGrid,
+ font_shaper: font.Shaper,
+ font_shaper_cache: font.ShaperCache,
+
+ /// The images that we may render.
+ images: ImageState = .empty,
+
+ /// Background image, if we have one.
+ bg_image: ?imagepkg.Image = null,
+ /// Set whenever the background image changes, signalling
+ /// that the new background image needs to be uploaded to
+ /// the GPU.
+ ///
+ /// This is initialized as true so that we load the image
+ /// on renderer initialization, not just on config change.
+ bg_image_changed: bool = true,
+ /// Background image vertex buffer.
+ bg_image_buffer: shaderpkg.BgImage,
+ /// This value is used to force-update the swap chain copy
+ /// of the background image buffer whenever we change it.
+ bg_image_buffer_modified: usize = 0,
+
+ /// Graphics API state.
+ api: GraphicsAPI,
+
+ /// The CVDisplayLink used to drive the rendering loop in
+ /// sync with the display. This is void on platforms that
+ /// don't support a display link.
+ display_link: ?DisplayLink = null,
+
+ /// Health of the most recently completed frame.
+ health: std.atomic.Value(Health) = .{ .raw = .healthy },
+
+ /// Our swap chain (multiple buffering)
+ swap_chain: SwapChain,
+
+ /// This value is used to force-update swap chain targets in the
+ /// event of a config change that requires it (such as blending mode).
+ target_config_modified: usize = 0,
+
+ /// If something happened that requires us to reinitialize our shaders,
+ /// this is set to true so that we can do that whenever possible.
+ reinitialize_shaders: bool = false,
+
+ /// Whether or not we have custom shaders.
+ has_custom_shaders: bool = false,
+
+ /// Our shader pipelines.
+ shaders: Shaders,
+
+ /// The render state we update per loop.
+ terminal_state: terminal.RenderState = .empty,
+
+ /// The number of frames since the last terminal state reset.
+ /// We reset the terminal state after ~100,000 frames (about 10 to
+ /// 15 minutes at 120Hz) to prevent wasted memory buildup from
+ /// a large screen.
+ terminal_state_frame_count: usize = 0,
+
+ /// Our overlay state, if any.
+ overlay: ?Overlay = null,
+
+ const HighlightTag = enum(u8) {
+ search_match,
+ search_match_selected,
+ };
+ /// Swap chain which maintains multiple copies of the state needed to
+ /// render a frame, so that we can start building the next frame while
+ /// the previous frame is still being processed on the GPU.
+ const SwapChain = struct {
+ // The count of buffers we use for double/triple buffering.
+ // If this is one then we don't do any double+ buffering at all.
+ // This is comptime because there isn't a good reason to change
+ // this at runtime and there is a lot of complexity to support it.
+ const buf_count = GraphicsAPI.swap_chain_count;
+
+ /// `buf_count` structs that can hold the
+ /// data needed by the GPU to draw a frame.
+ frames: [buf_count]FrameState,
+ /// Index of the most recently used frame state struct.
+ frame_index: std.math.IntFittingRange(0, buf_count) = 0,
+ /// Semaphore that we wait on to make sure we have an available
+ /// frame state struct so we can start working on a new frame.
+ frame_sema: std.Thread.Semaphore = .{ .permits = buf_count },
+
+ /// Set to true when deinited, if you try to deinit a defunct
+ /// swap chain it will just be ignored, to prevent double-free.
+ ///
+ /// This is required because of `displayUnrealized`, since it
+ /// `deinits` the swapchain, which leads to a double-free if
+ /// the renderer is deinited after that.
+ defunct: bool = false,
+
+ pub fn init(api: GraphicsAPI, custom_shaders: bool) !SwapChain {
+ var result: SwapChain = .{ .frames = undefined };
+
+ // Initialize all of our frame state.
+ for (&result.frames) |*frame| {
+ frame.* = try FrameState.init(api, custom_shaders);
+ }
+
+ return result;
+ }
+
+ pub fn deinit(self: *SwapChain) void {
+ if (self.defunct) return;
+ self.defunct = true;
+
+ // Wait for all of our inflight draws to complete
+ // so that we can cleanly deinit our GPU state.
+ for (0..buf_count) |_| self.frame_sema.wait();
+ for (&self.frames) |*frame| frame.deinit();
+ }
+
+ /// Get the next frame state to draw to. This will wait on the
+ /// semaphore to ensure that the frame is available. This must
+ /// always be paired with a call to releaseFrame.
+ pub fn nextFrame(self: *SwapChain) error{Defunct}!*FrameState {
+ if (self.defunct) return error.Defunct;
+
+ self.frame_sema.wait();
+ errdefer self.frame_sema.post();
+ self.frame_index = (self.frame_index + 1) % buf_count;
+ return &self.frames[self.frame_index];
+ }
+
+ /// This should be called when the frame has completed drawing.
+ pub fn releaseFrame(self: *SwapChain) void {
+ self.frame_sema.post();
+ }
+ };
+
+ /// State we need duplicated for every frame. Any state that could be
+ /// in a data race between the GPU and CPU while a frame is being drawn
+ /// should be in this struct.
+ ///
+ /// While a draw is in-process, we "lock" the state (via a semaphore)
+ /// and prevent the CPU from updating the state until our graphics API
+ /// reports that the frame is complete.
+ ///
+ /// This is used to implement double/triple buffering.
+ const FrameState = struct {
+ uniforms: UniformBuffer,
+ cells: CellTextBuffer,
+ cells_bg: CellBgBuffer,
+
+ grayscale: Texture,
+ grayscale_modified: usize = 0,
+ color: Texture,
+ color_modified: usize = 0,
+
+ target: Target,
+ /// See property of same name on Renderer for explanation.
+ target_config_modified: usize = 0,
+
+ /// Buffer with the vertex data for our background image.
+ ///
+ /// TODO: Make this an optional and only create it
+ /// if we actually have a background image.
+ bg_image_buffer: BgImageBuffer,
+ /// See property of same name on Renderer for explanation.
+ bg_image_buffer_modified: usize = 0,
+
+ /// Custom shader state, this is null if we have no custom shaders.
+ custom_shader_state: ?CustomShaderState = null,
+
+ const UniformBuffer = Buffer(shaderpkg.Uniforms);
+ const CellBgBuffer = Buffer(shaderpkg.CellBg);
+ const CellTextBuffer = Buffer(shaderpkg.CellText);
+ const BgImageBuffer = Buffer(shaderpkg.BgImage);
+
+ pub fn init(api: GraphicsAPI, custom_shaders: bool) !FrameState {
+ // Uniform buffer contains exactly 1 uniform struct. The
+ // uniform data will be undefined so this must be set before
+ // a frame is drawn.
+ var uniforms = try UniformBuffer.init(api.uniformBufferOptions(), 1);
+ errdefer uniforms.deinit();
+
+ // Create GPU buffers for our cells.
+ //
+ // We start them off with a size of 1, which will of course be
+ // too small, but they will be resized as needed. This is a bit
+ // wasteful but since it's a one-time thing it's not really a
+ // huge concern.
+ var cells = try CellTextBuffer.init(api.fgBufferOptions(), 1);
+ errdefer cells.deinit();
+ var cells_bg = try CellBgBuffer.init(api.bgBufferOptions(), 1);
+ errdefer cells_bg.deinit();
+
+ // Create a GPU buffer for our background image info.
+ var bg_image_buffer = try BgImageBuffer.init(
+ api.bgImageBufferOptions(),
+ 1,
+ );
+ errdefer bg_image_buffer.deinit();
+
+ // Initialize our textures for our font atlas.
+ //
+ // As with the buffers above, we start these off as small
+ // as possible since they'll inevitably be resized anyway.
+ const grayscale = try api.initAtlasTexture(&.{
+ .data = undefined,
+ .size = 1,
+ .format = .grayscale,
+ });
+ errdefer grayscale.deinit();
+ const color = try api.initAtlasTexture(&.{
+ .data = undefined,
+ .size = 1,
+ .format = .bgra,
+ });
+ errdefer color.deinit();
+
+ var custom_shader_state =
+ if (custom_shaders)
+ try CustomShaderState.init(api)
+ else
+ null;
+ errdefer if (custom_shader_state) |*state| state.deinit();
+
+ // Initialize the target. Just as with the other resources,
+ // start it off as small as we can since it'll be resized.
+ const target = try api.initTarget(1, 1);
+
+ return .{
+ .uniforms = uniforms,
+ .cells = cells,
+ .cells_bg = cells_bg,
+ .bg_image_buffer = bg_image_buffer,
+ .grayscale = grayscale,
+ .color = color,
+ .target = target,
+ .custom_shader_state = custom_shader_state,
+ };
+ }
+
+ pub fn deinit(self: *FrameState) void {
+ self.target.deinit();
+ self.uniforms.deinit();
+ self.cells.deinit();
+ self.cells_bg.deinit();
+ self.grayscale.deinit();
+ self.color.deinit();
+ self.bg_image_buffer.deinit();
+ if (self.custom_shader_state) |*state| state.deinit();
+ }
+
+ pub fn resize(
+ self: *FrameState,
+ api: GraphicsAPI,
+ width: usize,
+ height: usize,
+ ) !void {
+ if (self.custom_shader_state) |*state| {
+ try state.resize(api, width, height);
+ }
+ const target = try api.initTarget(width, height);
+ self.target.deinit();
+ self.target = target;
+ }
+ };
+
+ /// State relevant to our custom shaders if we have any.
+ const CustomShaderState = struct {
+ /// When we have a custom shader state, we maintain a front
+ /// and back texture which we use as a swap chain to render
+ /// between when multiple custom shaders are defined.
+ front_texture: Texture,
+ back_texture: Texture,
+
+ /// Shadertoy uses a sampler for accessing the various channel
+ /// textures. In Metal, we need to explicitly create these since
+ /// the glslang-to-msl compiler doesn't do it for us (as we
+ /// normally would in hand-written MSL). To keep it clean and
+ /// consistent, we just force all rendering APIs to provide an
+ /// explicit sampler.
+ ///
+ /// Samplers are immutable and describe sampling properties so
+ /// we can share the sampler across front/back textures (although
+ /// we only need it for the source texture at a time, we don't
+ /// need to "swap" it).
+ sampler: Sampler,
+
+ uniforms: UniformBuffer,
+
+ const UniformBuffer = Buffer(shadertoy.Uniforms);
+
+ /// Swap the front and back textures.
+ pub fn swap(self: *CustomShaderState) void {
+ std.mem.swap(Texture, &self.front_texture, &self.back_texture);
+ }
+
+ pub fn init(api: GraphicsAPI) !CustomShaderState {
+ // Create a GPU buffer to hold our uniforms.
+ var uniforms = try UniformBuffer.init(api.uniformBufferOptions(), 1);
+ errdefer uniforms.deinit();
+
+ // Initialize the front and back textures at 1x1 px, this
+ // is slightly wasteful but it's only done once so whatever.
+ const front_texture = try Texture.init(
+ api.textureOptions(),
+ 1,
+ 1,
+ null,
+ );
+ errdefer front_texture.deinit();
+ const back_texture = try Texture.init(
+ api.textureOptions(),
+ 1,
+ 1,
+ null,
+ );
+ errdefer back_texture.deinit();
+
+ const sampler = try Sampler.init(api.samplerOptions());
+ errdefer sampler.deinit();
+
+ return .{
+ .front_texture = front_texture,
+ .back_texture = back_texture,
+ .sampler = sampler,
+ .uniforms = uniforms,
+ };
+ }
+
+ pub fn deinit(self: *CustomShaderState) void {
+ self.front_texture.deinit();
+ self.back_texture.deinit();
+ self.sampler.deinit();
+ self.uniforms.deinit();
+ }
+
+ pub fn resize(
+ self: *CustomShaderState,
+ api: GraphicsAPI,
+ width: usize,
+ height: usize,
+ ) !void {
+ const front_texture = try Texture.init(
+ api.textureOptions(),
+ @intCast(width),
+ @intCast(height),
+ null,
+ );
+ errdefer front_texture.deinit();
+ const back_texture = try Texture.init(
+ api.textureOptions(),
+ @intCast(width),
+ @intCast(height),
+ null,
+ );
+ errdefer back_texture.deinit();
+
+ self.front_texture.deinit();
+ self.back_texture.deinit();
+
+ self.front_texture = front_texture;
+ self.back_texture = back_texture;
+ }
+ };
+
+ /// The configuration for this renderer that is derived from the main
+ /// configuration. This must be exported so that we don't need to
+ /// pass around Config pointers which makes memory management a pain.
+ pub const DerivedConfig = struct {
+ arena: ArenaAllocator,
+
+ font_thicken: bool,
+ font_thicken_strength: u8,
+ font_features: std.ArrayListUnmanaged([:0]const u8),
+ font_styles: font.CodepointResolver.StyleStatus,
+ font_shaping_break: configpkg.FontShapingBreak,
+ cursor_color: ?configpkg.Config.TerminalColor,
+ cursor_opacity: f64,
+ cursor_text: ?configpkg.Config.TerminalColor,
+ background: terminal.color.RGB,
+ background_opacity: f64,
+ background_opacity_cells: bool,
+ foreground: terminal.color.RGB,
+ selection_background: ?configpkg.Config.TerminalColor,
+ selection_foreground: ?configpkg.Config.TerminalColor,
+ search_background: configpkg.Config.TerminalColor,
+ search_foreground: configpkg.Config.TerminalColor,
+ search_selected_background: configpkg.Config.TerminalColor,
+ search_selected_foreground: configpkg.Config.TerminalColor,
+ bold_color: ?terminal.Style.BoldColor,
+ faint_opacity: u8,
+ min_contrast: f32,
+ padding_color: configpkg.WindowPaddingColor,
+ custom_shaders: configpkg.RepeatablePath,
+ bg_image: ?configpkg.Path,
+ bg_image_opacity: f32,
+ bg_image_position: configpkg.BackgroundImagePosition,
+ bg_image_fit: configpkg.BackgroundImageFit,
+ bg_image_repeat: bool,
+ links: link.Set,
+ vsync: bool,
+ colorspace: configpkg.Config.WindowColorspace,
+ blending: configpkg.Config.AlphaBlending,
+ background_blur: configpkg.Config.BackgroundBlur,
+ scroll_to_bottom_on_output: bool,
+
+ pub fn init(
+ alloc_gpa: Allocator,
+ config: *const configpkg.Config,
+ ) !DerivedConfig {
+ var arena = ArenaAllocator.init(alloc_gpa);
+ errdefer arena.deinit();
+ const alloc = arena.allocator();
+
+ // Copy our shaders
+ const custom_shaders = try config.@"custom-shader".clone(alloc);
+
+ // Copy our background image
+ const bg_image =
+ if (config.@"background-image") |bg|
+ try bg.clone(alloc)
+ else
+ null;
+
+ // Copy our font features
+ const font_features = try config.@"font-feature".clone(alloc);
+
+ // Get our font styles
+ var font_styles = font.CodepointResolver.StyleStatus.initFill(true);
+ font_styles.set(.bold, config.@"font-style-bold" != .false);
+ font_styles.set(.italic, config.@"font-style-italic" != .false);
+ font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false);
+
+ // Our link configs
+ const links = try link.Set.fromConfig(
+ alloc,
+ config.link.links.items,
+ );
+
+ return .{
+ .background_opacity = @max(0, @min(1, config.@"background-opacity")),
+ .background_opacity_cells = config.@"background-opacity-cells",
+ .font_thicken = config.@"font-thicken",
+ .font_thicken_strength = config.@"font-thicken-strength",
+ .font_features = font_features.list,
+ .font_styles = font_styles,
+ .font_shaping_break = config.@"font-shaping-break",
+
+ .cursor_color = config.@"cursor-color",
+ .cursor_text = config.@"cursor-text",
+ .cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")),
+
+ .background = config.background.toTerminalRGB(),
+ .foreground = config.foreground.toTerminalRGB(),
+ .bold_color = if (config.@"bold-color") |b| b.toTerminal() else null,
+ .faint_opacity = @intFromFloat(@ceil(config.@"faint-opacity" * 255)),
+
+ .min_contrast = @floatCast(config.@"minimum-contrast"),
+ .padding_color = config.@"window-padding-color",
+
+ .selection_background = config.@"selection-background",
+ .selection_foreground = config.@"selection-foreground",
+ .search_background = config.@"search-background",
+ .search_foreground = config.@"search-foreground",
+ .search_selected_background = config.@"search-selected-background",
+ .search_selected_foreground = config.@"search-selected-foreground",
+
+ .custom_shaders = custom_shaders,
+ .bg_image = bg_image,
+ .bg_image_opacity = config.@"background-image-opacity",
+ .bg_image_position = config.@"background-image-position",
+ .bg_image_fit = config.@"background-image-fit",
+ .bg_image_repeat = config.@"background-image-repeat",
+ .links = links,
+ .vsync = config.@"window-vsync",
+ .colorspace = config.@"window-colorspace",
+ .blending = config.@"alpha-blending",
+ .background_blur = config.@"background-blur",
+ .scroll_to_bottom_on_output = config.@"scroll-to-bottom".output,
+ .arena = arena,
+ };
+ }
+
+ pub fn deinit(self: *DerivedConfig) void {
+ const alloc = self.arena.allocator();
+ self.links.deinit(alloc);
+ self.arena.deinit();
+ }
+ };
+
+ pub fn init(alloc: Allocator, options: renderer.Options) !Self {
+ // Initialize our graphics API wrapper, this will prepare the
+ // surface provided by the apprt and set up any API-specific
+ // GPU resources.
+ var api = try GraphicsAPI.init(alloc, options);
+ errdefer api.deinit();
+
+ const has_custom_shaders = options.config.custom_shaders.value.items.len > 0;
+
+ // Prepare our swap chain
+ var swap_chain = try SwapChain.init(
+ api,
+ has_custom_shaders,
+ );
+ errdefer swap_chain.deinit();
+
+ // Create the font shaper.
+ var font_shaper = try font.Shaper.init(alloc, .{
+ .features = options.config.font_features.items,
+ });
+ errdefer font_shaper.deinit();
+
+ // Initialize all the data that requires a critical font section.
+ const font_critical: struct {
+ metrics: font.Metrics,
+ } = font_critical: {
+ const grid: *font.SharedGrid = options.font_grid;
+ grid.lock.lockShared();
+ defer grid.lock.unlockShared();
+ break :font_critical .{
+ .metrics = grid.metrics,
+ };
+ };
+
+ const display_link: ?DisplayLink = switch (builtin.os.tag) {
+ .macos => if (options.config.vsync)
+ try macos.video.DisplayLink.createWithActiveCGDisplays()
+ else
+ null,
+ else => null,
+ };
+ errdefer if (display_link) |v| v.release();
+
+ var result: Self = .{
+ .alloc = alloc,
+ .config = options.config,
+ .surface_mailbox = options.surface_mailbox,
+ .grid_metrics = font_critical.metrics,
+ .size = options.size,
+ .focused = true,
+ .scrollbar = .zero,
+ .scrollbar_dirty = false,
+ .last_bottom_node = null,
+ .last_bottom_y = 0,
+ .search_matches = null,
+ .search_selected_match = null,
+ .search_matches_dirty = false,
+
+ // Render state
+ .cells = .{},
+ .uniforms = .{
+ .projection_matrix = undefined,
+ .cell_size = undefined,
+ .grid_size = undefined,
+ .grid_padding = undefined,
+ .screen_size = undefined,
+ .padding_extend = .{},
+ .min_contrast = options.config.min_contrast,
+ .cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) },
+ .cursor_color = undefined,
+ .bg_color = .{
+ options.config.background.r,
+ options.config.background.g,
+ options.config.background.b,
+ // Note that if we're on macOS with glass effects
+ // we'll disable background opacity but we handle
+ // that in updateFrame.
+ @intFromFloat(@round(options.config.background_opacity * 255.0)),
+ },
+ .bools = .{
+ .cursor_wide = false,
+ .use_display_p3 = options.config.colorspace == .@"display-p3",
+ .use_linear_blending = options.config.blending.isLinear(),
+ .use_linear_correction = options.config.blending == .@"linear-corrected",
+ },
+ },
+ .custom_shader_uniforms = .{
+ .resolution = .{ 0, 0, 1 },
+ .time = 0,
+ .time_delta = 0,
+ .frame_rate = 60, // not currently updated
+ .frame = 0,
+ .channel_time = @splat(@splat(0)), // not currently updated
+ .channel_resolution = @splat(@splat(0)),
+ .mouse = @splat(0), // not currently updated
+ .date = @splat(0), // not currently updated
+ .sample_rate = 0, // N/A, we don't have any audio
+ .current_cursor = @splat(0),
+ .previous_cursor = @splat(0),
+ .current_cursor_color = @splat(0),
+ .previous_cursor_color = @splat(0),
+ .current_cursor_style = 0,
+ .previous_cursor_style = 0,
+ .cursor_visible = 0,
+ .cursor_change_time = 0,
+ .time_focus = 0,
+ .focus = 1, // assume focused initially
+ .palette = @splat(@splat(0)),
+ .background_color = @splat(0),
+ .foreground_color = @splat(0),
+ .cursor_color = @splat(0),
+ .cursor_text = @splat(0),
+ .selection_background_color = @splat(0),
+ .selection_foreground_color = @splat(0),
+ },
+ .bg_image_buffer = undefined,
+
+ // Fonts
+ .font_grid = options.font_grid,
+ .font_shaper = font_shaper,
+ .font_shaper_cache = font.ShaperCache.init(),
+
+ // Shaders (initialized below)
+ .shaders = undefined,
+
+ // Graphics API stuff
+ .api = api,
+ .swap_chain = swap_chain,
+ .display_link = display_link,
+ };
+
+ try result.initShaders();
+
+ // Ensure our undefined values above are correctly initialized.
+ result.updateFontGridUniforms();
+ result.updateScreenSizeUniforms();
+ result.updateBgImageBuffer();
+ try result.prepBackgroundImage();
+
+ return result;
+ }
+
+ pub fn deinit(self: *Self) void {
+ if (self.overlay) |*overlay| overlay.deinit(self.alloc);
+ self.terminal_state.deinit(self.alloc);
+ if (self.search_selected_match) |*m| m.arena.deinit();
+ if (self.search_matches) |*m| m.arena.deinit();
+ self.swap_chain.deinit();
+
+ if (DisplayLink != void) {
+ if (self.display_link) |display_link| {
+ display_link.stop() catch {};
+ display_link.release();
+ }
+ }
+
+ self.cells.deinit(self.alloc);
+
+ self.font_shaper.deinit();
+ self.font_shaper_cache.deinit(self.alloc);
+
+ self.config.deinit();
+
+ self.images.deinit(self.alloc);
+
+ if (self.bg_image) |img| img.deinit(self.alloc);
+
+ self.deinitShaders();
+
+ self.api.deinit();
+
+ self.* = undefined;
+ }
+
+ fn deinitShaders(self: *Self) void {
+ self.shaders.deinit(self.alloc);
+ }
+
+ fn initShaders(self: *Self) !void {
+ var arena = ArenaAllocator.init(self.alloc);
+ defer arena.deinit();
+ const arena_alloc = arena.allocator();
+
+ // Load our custom shaders
+ const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles(
+ arena_alloc,
+ self.config.custom_shaders,
+ GraphicsAPI.custom_shader_target,
+ ) catch |err| err: {
+ log.warn("error loading custom shaders err={}", .{err});
+ break :err &.{};
+ };
+
+ const has_custom_shaders = custom_shaders.len > 0;
+
+ var shaders = try self.api.initShaders(
+ self.alloc,
+ custom_shaders,
+ );
+ errdefer shaders.deinit(self.alloc);
+
+ self.shaders = shaders;
+ self.has_custom_shaders = has_custom_shaders;
+ }
+
+ /// This is called early right after surface creation.
+ pub fn surfaceInit(surface: *apprt.Surface) !void {
+ // If our API has to do things here, let it.
+ if (@hasDecl(GraphicsAPI, "surfaceInit")) {
+ try GraphicsAPI.surfaceInit(surface);
+ }
+ }
+
+ /// This is called just prior to spinning up the renderer thread for
+ /// final main thread setup requirements.
+ pub fn finalizeSurfaceInit(self: *Self, surface: *apprt.Surface) !void {
+ // If our API has to do things to finalize surface init, let it.
+ if (@hasDecl(GraphicsAPI, "finalizeSurfaceInit")) {
+ try self.api.finalizeSurfaceInit(surface);
+ }
+ }
+
+ /// Callback called by renderer.Thread when it begins.
+ pub fn threadEnter(self: *const Self, surface: *apprt.Surface) !void {
+ // If our API has to do things on thread enter, let it.
+ if (@hasDecl(GraphicsAPI, "threadEnter")) {
+ try self.api.threadEnter(surface);
+ }
+ }
+
+ /// Callback called by renderer.Thread when it exits.
+ pub fn threadExit(self: *const Self) void {
+ // If our API has to do things on thread exit, let it.
+ if (@hasDecl(GraphicsAPI, "threadExit")) {
+ self.api.threadExit();
+ }
+ }
+
+ /// Called by renderer.Thread when it starts the main loop.
+ pub fn loopEnter(self: *Self, thr: *renderer.Thread) !void {
+ // If our API has to do things on loop enter, let it.
+ if (@hasDecl(GraphicsAPI, "loopEnter")) {
+ self.api.loopEnter();
+ }
+
+ // If we don't support a display link we have no work to do.
+ if (comptime DisplayLink == void) return;
+
+ // This is when we know our "self" pointer is stable so we can
+ // setup the display link. To setup the display link we set our
+ // callback and we can start it immediately.
+ const display_link = self.display_link orelse return;
+ try display_link.setOutputCallback(
+ xev.Async,
+ &displayLinkCallback,
+ &thr.draw_now,
+ );
+ display_link.start() catch {};
+ }
+
+ /// Called by renderer.Thread when it exits the main loop.
+ pub fn loopExit(self: *Self) void {
+ // If our API has to do things on loop exit, let it.
+ if (@hasDecl(GraphicsAPI, "loopExit")) {
+ self.api.loopExit();
+ }
+
+ // If we don't support a display link we have no work to do.
+ if (comptime DisplayLink == void) return;
+
+ // Stop our display link. If this fails its okay it just means
+ // that we either never started it or the view its attached to
+ // is gone which is fine.
+ const display_link = self.display_link orelse return;
+ display_link.stop() catch {};
+ }
+
+ /// This is called by the GTK apprt after the surface is
+ /// reinitialized due to any of the events mentioned in
+ /// the doc comment for `displayUnrealized`.
+ pub fn displayRealized(self: *Self) !void {
+ // If our API has to do things on realize, let it.
+ if (@hasDecl(GraphicsAPI, "displayRealized")) {
+ self.api.displayRealized();
+ }
+
+ // Lock the draw mutex so that we can
+ // safely reinitialize our GPU resources.
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // We assume that the swap chain was deinited in
+ // `displayUnrealized`, in which case it should be
+ // marked defunct. If not, we have a problem.
+ assert(self.swap_chain.defunct);
+
+ // We reinitialize our shaders and our swap chain.
+ try self.initShaders();
+ self.swap_chain = try SwapChain.init(
+ self.api,
+ self.has_custom_shaders,
+ );
+ self.reinitialize_shaders = false;
+ self.target_config_modified = 1;
+ }
+
+ /// This is called by the GTK apprt when the surface is being destroyed.
+ /// This can happen because the surface is being closed but also when
+ /// moving the window between displays or splitting.
+ pub fn displayUnrealized(self: *Self) void {
+ // If our API has to do things on unrealize, let it.
+ if (@hasDecl(GraphicsAPI, "displayUnrealized")) {
+ self.api.displayUnrealized();
+ }
+
+ // Lock the draw mutex so that we can
+ // safely deinitialize our GPU resources.
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // We deinit our swap chain and shaders.
+ //
+ // This will mark them as defunct so that they
+ // can't be double-freed or used in draw calls.
+ self.swap_chain.deinit();
+ self.shaders.deinit(self.alloc);
+ }
+
+ fn displayLinkCallback(
+ _: *macos.video.DisplayLink,
+ ud: ?*xev.Async,
+ ) void {
+ const draw_now = ud orelse return;
+ draw_now.notify() catch |err| {
+ log.err("error notifying draw_now err={}", .{err});
+ };
+ }
+
+ /// Mark the full screen as dirty so that we redraw everything.
+ pub inline fn markDirty(self: *Self) void {
+ self.terminal_state.dirty = .full;
+ }
+
+ /// Called when we get an updated display ID for our display link.
+ pub fn setMacOSDisplayID(self: *Self, id: u32) !void {
+ if (comptime DisplayLink == void) return;
+ const display_link = self.display_link orelse return;
+ log.info("updating display link display id={}", .{id});
+ display_link.setCurrentCGDisplay(id) catch |err| {
+ log.warn("error setting display link display id err={}", .{err});
+ };
+ }
+
+ /// True if our renderer has animations so that a higher frequency
+ /// timer is used.
+ pub fn hasAnimations(self: *const Self) bool {
+ return self.has_custom_shaders;
+ }
+
+ /// True if our renderer is using vsync. If true, the renderer or apprt
+ /// is responsible for triggering draw_now calls to the render thread.
+ /// That is the only way to trigger a drawFrame.
+ pub fn hasVsync(self: *const Self) bool {
+ if (comptime DisplayLink == void) return false;
+ const display_link = self.display_link orelse return false;
+ return display_link.isRunning();
+ }
+
+ /// Callback when the focus changes for the terminal this is rendering.
+ ///
+ /// Must be called on the render thread.
+ pub fn setFocus(self: *Self, focus: bool) !void {
+ assert(self.focused != focus);
+
+ self.focused = focus;
+
+ // Flag that we need to update our custom shaders
+ self.custom_shader_focused_changed = true;
+
+ // If we're not focused, then we want to stop the display link
+ // because it is a waste of resources and we can move to pure
+ // change-driven updates.
+ if (comptime DisplayLink != void) link: {
+ const display_link = self.display_link orelse break :link;
+ if (focus) {
+ display_link.start() catch {};
+ } else {
+ display_link.stop() catch {};
+ }
+ }
+ }
+
+ /// Callback when the window is visible or occluded.
+ ///
+ /// Must be called on the render thread.
+ pub fn setVisible(self: *Self, visible: bool) void {
+ // If we're not visible, then we want to stop the display link
+ // because it is a waste of resources and we can move to pure
+ // change-driven updates.
+ if (comptime DisplayLink != void) link: {
+ const display_link = self.display_link orelse break :link;
+ if (visible and self.focused) {
+ display_link.start() catch {};
+ } else {
+ display_link.stop() catch {};
+ }
+ }
+ }
+
+ /// Set the new font grid.
+ ///
+ /// Must be called on the render thread.
+ pub fn setFontGrid(self: *Self, grid: *font.SharedGrid) void {
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // Update our grid
+ self.font_grid = grid;
+
+ // Update all our textures so that they sync on the next frame.
+ // We can modify this without a lock because the GPU does not
+ // touch this data.
+ for (&self.swap_chain.frames) |*frame| {
+ frame.grayscale_modified = 0;
+ frame.color_modified = 0;
+ }
+
+ // Get our metrics from the grid. This doesn't require a lock because
+ // the metrics are never recalculated.
+ const metrics = grid.metrics;
+ self.grid_metrics = metrics;
+
+ // Reset our shaper cache. If our font changed (not just the size) then
+ // the data in the shaper cache may be invalid and cannot be used, so we
+ // always clear the cache just in case.
+ const font_shaper_cache = font.ShaperCache.init();
+ self.font_shaper_cache.deinit(self.alloc);
+ self.font_shaper_cache = font_shaper_cache;
+
+ // Update cell size.
+ self.size.cell = .{
+ .width = metrics.cell_width,
+ .height = metrics.cell_height,
+ };
+
+ // Update relevant uniforms
+ self.updateFontGridUniforms();
+
+ // Force a full rebuild, because cached rows may still reference
+ // an outdated atlas from the old grid and this can cause garbage
+ // to be rendered.
+ self.markDirty();
+ }
+
+ /// Update uniforms that are based on the font grid.
+ ///
+ /// Caller must hold the draw mutex.
+ fn updateFontGridUniforms(self: *Self) void {
+ self.uniforms.cell_size = .{
+ @floatFromInt(self.grid_metrics.cell_width),
+ @floatFromInt(self.grid_metrics.cell_height),
+ };
+ }
+
+ /// Update the frame data.
+ pub fn updateFrame(
+ self: *Self,
+ state: *renderer.State,
+ cursor_blink_visible: bool,
+ ) Allocator.Error!void {
+ // const start = std.time.Instant.now() catch unreachable;
+ // const start_micro = std.time.microTimestamp();
+ // defer {
+ // const end = std.time.Instant.now() catch unreachable;
+ // log.warn(
+ // "[updateFrame time] start_micro={} duration={}ns",
+ // .{ start_micro, end.since(start) / std.time.ns_per_us },
+ // );
+ // }
+
+ // We fully deinit and reset the terminal state every so often
+ // so that a particularly large terminal state doesn't cause
+ // the renderer to hold on to retained memory.
+ //
+ // Frame count is ~12 minutes at 120Hz.
+ 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_frame_count += 1;
+
+ // Create an arena for all our temporary allocations while rebuilding
+ var arena = ArenaAllocator.init(self.alloc);
+ defer arena.deinit();
+ const arena_alloc = arena.allocator();
+
+ // Data we extract out of the critical area.
+ const Critical = struct {
+ links: terminal.RenderState.CellSet,
+ mouse: renderer.State.Mouse,
+ preedit: ?renderer.State.Preedit,
+ scrollbar: terminal.Scrollbar,
+ overlay_features: []const Overlay.Feature,
+ };
+
+ // Update all our data as tightly as possible within the mutex.
+ var critical: Critical = critical: {
+ // const start = try std.time.Instant.now();
+ // const start_micro = std.time.microTimestamp();
+ // defer {
+ // const end = std.time.Instant.now() catch unreachable;
+ // std.log.err("[updateFrame critical time] start={}\tduration={} us", .{ start_micro, end.since(start) / std.time.ns_per_us });
+ // }
+
+ state.mutex.lock();
+ defer state.mutex.unlock();
+
+ // If we're in a synchronized output state, we pause all rendering.
+ if (state.terminal.modes.get(.synchronized_output)) {
+ log.debug("synchronized output started, skipping render", .{});
+ return;
+ }
+
+ // If scroll-to-bottom on output is enabled, check if the final line
+ // changed by comparing the bottom-right pin. If the node pointer or
+ // y offset changed, new content was added to the screen.
+ // Update this BEFORE we update our render state so we can
+ // draw the new scrolled data immediately.
+ if (self.config.scroll_to_bottom_on_output) scroll: {
+ const br = state.terminal.screens.active.pages.getBottomRight(.screen) orelse break :scroll;
+
+ // If the pin hasn't changed, then don't scroll.
+ if (self.last_bottom_node == @intFromPtr(br.node) and
+ self.last_bottom_y == br.y) break :scroll;
+
+ // Update tracked pin state for next frame
+ self.last_bottom_node = @intFromPtr(br.node);
+ self.last_bottom_y = br.y;
+
+ // Scroll
+ state.terminal.scrollViewport(.bottom);
+ }
+
+ // Update our terminal state
+ try self.terminal_state.update(self.alloc, state.terminal);
+
+ // If our terminal state is dirty at all we need to redo
+ // the viewport search.
+ if (self.terminal_state.dirty != .false) {
+ state.terminal.flags.search_viewport_dirty = true;
+ }
+
+ // Get our scrollbar out of the terminal. We synchronize
+ // the scrollbar read with frame data updates because this
+ // naturally limits the number of calls to this method (it
+ // can be expensive) and also makes it so we don't need another
+ // cross-thread mailbox message within the IO path.
+ const scrollbar = state.terminal.screens.active.pages.scrollbar();
+
+ // Get our preedit state
+ const preedit: ?renderer.State.Preedit = preedit: {
+ const p = state.preedit orelse break :preedit null;
+ 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: {
+ // If our mouse isn't hovering, we have no links.
+ const vp = state.mouse.point orelse break :osc8 .empty;
+
+ // If the right mods aren't pressed, then we can't match.
+ if (!state.mouse.mods.equal(inputpkg.ctrlOrSuper(.{})))
+ break :osc8 .empty;
+
+ break :osc8 self.terminal_state.linkCells(
+ arena_alloc,
+ vp,
+ ) catch |err| {
+ log.warn("error searching for OSC8 links err={}", .{err});
+ break :osc8 .empty;
+ };
+ };
+
+ const overlay_features: []const Overlay.Feature = overlay: {
+ const insp = state.inspector orelse break :overlay &.{};
+ const renderer_info = insp.rendererInfo();
+ break :overlay renderer_info.overlayFeatures(
+ arena_alloc,
+ ) catch &.{};
+ };
+
+ break :critical .{
+ .links = links,
+ .mouse = state.mouse,
+ .preedit = preedit,
+ .scrollbar = scrollbar,
+ .overlay_features = overlay_features,
+ };
+ };
+
+ // Outside the critical area we can update our links to contain
+ // our regex results.
+ self.config.links.renderCellMap(
+ arena_alloc,
+ &critical.links,
+ &self.terminal_state,
+ state.mouse.point,
+ state.mouse.mods,
+ ) catch |err| {
+ log.warn("error searching for regex links err={}", .{err});
+ };
+
+ // Clear our highlight state and update.
+ if (self.search_matches_dirty or self.terminal_state.dirty != .false) {
+ self.search_matches_dirty = false;
+
+ // Clear the prior highlights
+ const row_data = self.terminal_state.row_data.slice();
+ var any_dirty: bool = false;
+ for (
+ row_data.items(.highlights),
+ row_data.items(.dirty),
+ ) |*highlights, *dirty| {
+ if (highlights.items.len > 0) {
+ highlights.clearRetainingCapacity();
+ dirty.* = true;
+ any_dirty = true;
+ }
+ }
+ if (any_dirty and self.terminal_state.dirty == .false) {
+ self.terminal_state.dirty = .partial;
+ }
+
+ // NOTE: The order below matters. Highlights added earlier
+ // will take priority.
+
+ if (self.search_selected_match) |m| {
+ self.terminal_state.updateHighlightsFlattened(
+ self.alloc,
+ @intFromEnum(HighlightTag.search_match_selected),
+ &.{m.match},
+ ) catch |err| {
+ // Not a critical error, we just won't show highlights.
+ log.warn("error updating search selected highlight err={}", .{err});
+ };
+ }
+
+ if (self.search_matches) |m| {
+ self.terminal_state.updateHighlightsFlattened(
+ self.alloc,
+ @intFromEnum(HighlightTag.search_match),
+ m.matches,
+ ) catch |err| {
+ // Not a critical error, we just won't show highlights.
+ log.warn("error updating search highlights err={}", .{err});
+ };
+ }
+ }
+
+ // From this point forward no more errors.
+ errdefer comptime unreachable;
+
+ // Reset our dirty state after updating.
+ defer self.terminal_state.dirty = .false;
+
+ // Rebuild the overlay image if we have one. We can do this
+ // outside of any critical areas.
+ self.rebuildOverlay(
+ critical.overlay_features,
+ ) catch |err| {
+ log.warn(
+ "error rebuilding overlay surface err={}",
+ .{err},
+ );
+ };
+
+ // Acquire the draw mutex for all remaining state updates.
+ {
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // Build our GPU cells
+ self.rebuildCells(
+ critical.preedit,
+ renderer.cursorStyle(&self.terminal_state, .{
+ .preedit = critical.preedit != null,
+ .focused = self.focused,
+ .blink_visible = cursor_blink_visible,
+ }),
+ &critical.links,
+ ) catch |err| {
+ // This means we weren't able to allocate our buffer
+ // to update the cells. In this case, we continue with
+ // our old buffer (frozen contents) and log it.
+ comptime assert(@TypeOf(err) == error{OutOfMemory});
+ log.warn("error rebuilding GPU cells err={}", .{err});
+ };
+
+ // The scrollbar is only emitted during draws so we also
+ // check the scrollbar cache here and update if needed.
+ // This is pretty fast.
+ if (!self.scrollbar.eql(critical.scrollbar)) {
+ self.scrollbar = critical.scrollbar;
+ self.scrollbar_dirty = true;
+ }
+
+ // Update our background color
+ self.uniforms.bg_color = .{
+ self.terminal_state.colors.background.r,
+ self.terminal_state.colors.background.g,
+ self.terminal_state.colors.background.b,
+ @intFromFloat(@round(self.config.background_opacity * 255.0)),
+ };
+
+ // If we're on macOS and have glass styles, we remove
+ // the background opacity because the glass effect handles
+ // it.
+ if (comptime builtin.os.tag == .macos) switch (self.config.background_blur) {
+ .@"macos-glass-regular",
+ .@"macos-glass-clear",
+ => self.uniforms.bg_color[3] = 0,
+
+ else => {},
+ };
+
+ // Prepare our overlay image for upload (or unload). This
+ // has to use our general allocator since it modifies
+ // state that survives frames.
+ self.images.overlayUpdate(
+ self.alloc,
+ self.overlay,
+ ) catch |err| {
+ log.warn("error updating overlay images err={}", .{err});
+ };
+
+ // Update custom shader uniforms that depend on terminal state.
+ self.updateCustomShaderUniformsFromState();
+ }
+
+ // Notify our shaper we're done for the frame. For some shapers,
+ // such as CoreText, this triggers off-thread cleanup logic.
+ self.font_shaper.endFrame();
+ }
+
+ /// Draw the frame to the screen.
+ ///
+ /// If `sync` is true, this will synchronously block until
+ /// the frame is finished drawing and has been presented.
+ pub fn drawFrame(
+ self: *Self,
+ sync: bool,
+ ) !void {
+ // const start = std.time.Instant.now() catch unreachable;
+ // const start_micro = std.time.microTimestamp();
+ // defer {
+ // const end = std.time.Instant.now() catch unreachable;
+ // log.warn(
+ // "[drawFrame time] start_micro={} duration={}ns",
+ // .{ start_micro, end.since(start) / std.time.ns_per_us },
+ // );
+ // }
+
+ // We hold a the draw mutex to prevent changes to any
+ // data we access while we're in the middle of drawing.
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // After the graphics API is complete (so we defer) we want to
+ // update our scrollbar state.
+ defer if (self.scrollbar_dirty) {
+ // Fail instantly if the surface mailbox if full, we'll just
+ // get it on the next frame.
+ if (self.surface_mailbox.push(.{
+ .scrollbar = self.scrollbar,
+ }, .instant) > 0) self.scrollbar_dirty = false;
+ };
+
+ // Let our graphics API do any bookkeeping, etc.
+ // that it needs to do before / after `drawFrame`.
+ self.api.drawFrameStart();
+ defer self.api.drawFrameEnd();
+
+ // Retrieve the most up-to-date surface size from the Graphics API
+ const surface_size = try self.api.surfaceSize();
+
+ // If either of our surface dimensions is zero
+ // then drawing is absurd, so we just return.
+ if (surface_size.width == 0 or surface_size.height == 0) return;
+
+ const size_changed =
+ self.size.screen.width != surface_size.width or
+ self.size.screen.height != surface_size.height;
+
+ // Conditions under which we need to draw the frame, otherwise we
+ // don't need to since the previous frame should be identical.
+ const needs_redraw =
+ size_changed or
+ self.cells_rebuilt or
+ self.hasAnimations() or
+ sync;
+
+ if (!needs_redraw) {
+ // We still need to present the last target again, because the
+ // apprt may be swapping buffers and display an outdated frame
+ // if we don't draw something new.
+ try self.api.presentLastTarget();
+ return;
+ }
+ self.cells_rebuilt = false;
+
+ // Wait for a frame to be available.
+ const frame = try self.swap_chain.nextFrame();
+ errdefer self.swap_chain.releaseFrame();
+ // log.debug("drawing frame index={}", .{self.swap_chain.frame_index});
+
+ // If we need to reinitialize our shaders, do so.
+ if (self.reinitialize_shaders) {
+ self.reinitialize_shaders = false;
+ self.shaders.deinit(self.alloc);
+ try self.initShaders();
+ }
+
+ // Our shaders should not be defunct at this point.
+ assert(!self.shaders.defunct);
+
+ // If we have custom shaders, make sure we have the
+ // custom shader state in our frame state, otherwise
+ // if we have a state but don't need it we remove it.
+ if (self.has_custom_shaders) {
+ if (frame.custom_shader_state == null) {
+ frame.custom_shader_state = try .init(self.api);
+ try frame.custom_shader_state.?.resize(
+ self.api,
+ surface_size.width,
+ surface_size.height,
+ );
+ }
+ } else if (frame.custom_shader_state) |*state| {
+ state.deinit();
+ frame.custom_shader_state = null;
+ }
+
+ // If our stored size doesn't match the
+ // surface size we need to update it.
+ if (size_changed) {
+ self.size.screen = .{
+ .width = surface_size.width,
+ .height = surface_size.height,
+ };
+ self.updateScreenSizeUniforms();
+ }
+
+ // If this frame's target isn't the correct size, or the target
+ // config has changed (such as when the blending mode changes),
+ // remove it and replace it with a new one with the right values.
+ if (frame.target.width != self.size.screen.width or
+ frame.target.height != self.size.screen.height or
+ frame.target_config_modified != self.target_config_modified)
+ {
+ try frame.resize(
+ self.api,
+ self.size.screen.width,
+ self.size.screen.height,
+ );
+ frame.target_config_modified = self.target_config_modified;
+ }
+
+ // Upload images to the GPU as necessary.
+ _ = self.images.upload(self.alloc, &self.api);
+
+ // Upload the background image to the GPU as necessary.
+ try self.uploadBackgroundImage();
+
+ // Update per-frame custom shader uniforms.
+ try self.updateCustomShaderUniformsForFrame();
+
+ // Setup our frame data
+ try frame.uniforms.sync(&.{self.uniforms});
+ try frame.cells_bg.sync(self.cells.bg_cells);
+ const fg_count = try frame.cells.syncFromArrayLists(self.cells.fg_rows.lists);
+
+ // If our background image buffer has changed, sync it.
+ if (frame.bg_image_buffer_modified != self.bg_image_buffer_modified) {
+ try frame.bg_image_buffer.sync(&.{self.bg_image_buffer});
+
+ frame.bg_image_buffer_modified = self.bg_image_buffer_modified;
+ }
+
+ // If our font atlas changed, sync the texture data
+ texture: {
+ const modified = self.font_grid.atlas_grayscale.modified.load(.monotonic);
+ if (modified <= frame.grayscale_modified) break :texture;
+ self.font_grid.lock.lockShared();
+ defer self.font_grid.lock.unlockShared();
+ frame.grayscale_modified = self.font_grid.atlas_grayscale.modified.load(.monotonic);
+ try self.syncAtlasTexture(&self.font_grid.atlas_grayscale, &frame.grayscale);
+ }
+ texture: {
+ const modified = self.font_grid.atlas_color.modified.load(.monotonic);
+ if (modified <= frame.color_modified) break :texture;
+ self.font_grid.lock.lockShared();
+ defer self.font_grid.lock.unlockShared();
+ frame.color_modified = self.font_grid.atlas_color.modified.load(.monotonic);
+ try self.syncAtlasTexture(&self.font_grid.atlas_color, &frame.color);
+ }
+
+ // Get a frame context from the graphics API.
+ var frame_ctx = try self.api.beginFrame(self, &frame.target);
+ defer frame_ctx.complete(sync);
+
+ {
+ var pass = frame_ctx.renderPass(&.{.{
+ .target = if (frame.custom_shader_state) |state|
+ .{ .texture = state.back_texture }
+ else
+ .{ .target = frame.target },
+ .clear_color = .{ 0.0, 0.0, 0.0, 0.0 },
+ }});
+ defer pass.complete();
+
+ // First we draw our background image, if we have one.
+ // The bg image shader also draws the main bg color.
+ //
+ // Otherwise, if we don't have a background image, we
+ // draw the background color by itself in its own step.
+ //
+ // NOTE: We don't use the clear_color for this because that
+ // would require us to do color space conversion on the
+ // CPU-side. In the future when we have utilities for
+ // that we should remove this step and use clear_color.
+ if (self.bg_image) |img| switch (img) {
+ .ready => |texture| pass.step(.{
+ .pipeline = self.shaders.pipelines.bg_image,
+ .uniforms = frame.uniforms.buffer,
+ .buffers = &.{frame.bg_image_buffer.buffer},
+ .textures = &.{texture},
+ .draw = .{ .type = .triangle, .vertex_count = 3 },
+ }),
+ else => {},
+ } else {
+ pass.step(.{
+ .pipeline = self.shaders.pipelines.bg_color,
+ .uniforms = frame.uniforms.buffer,
+ .buffers = &.{ null, frame.cells_bg.buffer },
+ .draw = .{ .type = .triangle, .vertex_count = 3 },
+ });
+ }
+
+ // Then we draw any kitty images that need
+ // to be behind text AND cell backgrounds.
+ self.images.draw(
+ &self.api,
+ self.shaders.pipelines.image,
+ &pass,
+ .kitty_below_bg,
+ );
+
+ // Then we draw any opaque cell backgrounds.
+ pass.step(.{
+ .pipeline = self.shaders.pipelines.cell_bg,
+ .uniforms = frame.uniforms.buffer,
+ .buffers = &.{ null, frame.cells_bg.buffer },
+ .draw = .{ .type = .triangle, .vertex_count = 3 },
+ });
+
+ // Kitty images between cell backgrounds and text.
+ self.images.draw(
+ &self.api,
+ self.shaders.pipelines.image,
+ &pass,
+ .kitty_below_text,
+ );
+
+ // Text.
+ pass.step(.{
+ .pipeline = self.shaders.pipelines.cell_text,
+ .uniforms = frame.uniforms.buffer,
+ .buffers = &.{
+ frame.cells.buffer,
+ frame.cells_bg.buffer,
+ },
+ .textures = &.{
+ frame.grayscale,
+ frame.color,
+ },
+ .draw = .{
+ .type = .triangle_strip,
+ .vertex_count = 4,
+ .instance_count = fg_count,
+ },
+ });
+
+ // Kitty images in front of text.
+ self.images.draw(
+ &self.api,
+ self.shaders.pipelines.image,
+ &pass,
+ .kitty_above_text,
+ );
+
+ // Debug overlay. We do this before any custom shader state
+ // because our debug overlay is aligned with the grid.
+ if (self.overlay != null) self.images.draw(
+ &self.api,
+ self.shaders.pipelines.image,
+ &pass,
+ .overlay,
+ );
+ }
+
+ // If we have custom shaders, then we render them.
+ if (frame.custom_shader_state) |*state| {
+ // Sync our uniforms.
+ try state.uniforms.sync(&.{self.custom_shader_uniforms});
+
+ for (self.shaders.post_pipelines, 0..) |pipeline, i| {
+ defer state.swap();
+
+ var pass = frame_ctx.renderPass(&.{.{
+ .target = if (i < self.shaders.post_pipelines.len - 1)
+ .{ .texture = state.front_texture }
+ else
+ .{ .target = frame.target },
+ .clear_color = .{ 0.0, 0.0, 0.0, 0.0 },
+ }});
+ defer pass.complete();
+
+ pass.step(.{
+ .pipeline = pipeline,
+ .uniforms = state.uniforms.buffer,
+ .textures = &.{state.back_texture},
+ .samplers = &.{state.sampler},
+ .draw = .{
+ .type = .triangle,
+ .vertex_count = 3,
+ },
+ });
+ }
+ }
+ }
+
+ // Callback from the graphics API when a frame is completed.
+ pub fn frameCompleted(
+ self: *Self,
+ health: Health,
+ ) void {
+ // If our health value hasn't changed, then we do nothing. We don't
+ // do a cmpxchg here because strict atomicity isn't important.
+ if (self.health.load(.seq_cst) != health) {
+ self.health.store(health, .seq_cst);
+
+ // Our health value changed, so we notify the surface so that it
+ // can do something about it.
+ _ = self.surface_mailbox.push(.{
+ .renderer_health = health,
+ }, .{ .forever = {} });
+ }
+
+ // Always release our semaphore
+ self.swap_chain.releaseFrame();
+ }
+
+ /// Call this any time the background image path changes.
+ ///
+ /// Caller must hold the draw mutex.
+ fn prepBackgroundImage(self: *Self) !void {
+ // Then we try to load the background image if we have a path.
+ if (self.config.bg_image) |p| load_background: {
+ const path = switch (p) {
+ .required, .optional => |slice| slice,
+ };
+
+ // Open the file
+ var file = std.fs.openFileAbsolute(path, .{}) catch |err| {
+ log.warn(
+ "error opening background image file \"{s}\": {}",
+ .{ path, err },
+ );
+ break :load_background;
+ };
+ defer file.close();
+
+ // Read it
+ const contents = file.readToEndAlloc(
+ self.alloc,
+ std.math.maxInt(u32), // Max size of 4 GiB, for now.
+ ) catch |err| {
+ log.warn(
+ "error reading background image file \"{s}\": {}",
+ .{ path, err },
+ );
+ break :load_background;
+ };
+ defer self.alloc.free(contents);
+
+ // Figure out what type it probably is.
+ const file_type = switch (FileType.detect(contents)) {
+ .unknown => FileType.guessFromExtension(
+ std.fs.path.extension(path),
+ ),
+ else => |t| t,
+ };
+
+ // Decode it if we know how.
+ const image_data = switch (file_type) {
+ .png => try wuffs.png.decode(self.alloc, contents),
+ .jpeg => try wuffs.jpeg.decode(self.alloc, contents),
+ .unknown => {
+ log.warn(
+ "Cannot determine file type for background image file \"{s}\"!",
+ .{path},
+ );
+ break :load_background;
+ },
+ else => |f| {
+ log.warn(
+ "Unsupported file type {} for background image file \"{s}\"!",
+ .{ f, path },
+ );
+ break :load_background;
+ },
+ };
+
+ const image: imagepkg.Image = .{
+ .pending = .{
+ .width = image_data.width,
+ .height = image_data.height,
+ .pixel_format = .rgba,
+ .data = image_data.data.ptr,
+ },
+ };
+
+ // 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);
+ } else {
+ self.bg_image = image;
+ }
+ } else {
+ // If we don't have a background image path, mark our
+ // background image for unload if we currently have one.
+ if (self.bg_image) |*img| img.markForUnload();
+ }
+ }
+
+ fn uploadBackgroundImage(self: *Self) !void {
+ // Make sure our bg image is uploaded if it needs to be.
+ if (self.bg_image) |*bg| {
+ if (bg.isUnloading()) {
+ bg.deinit(self.alloc);
+ self.bg_image = null;
+ return;
+ }
+ if (bg.isPending()) try bg.upload(self.alloc, &self.api);
+ }
+ }
+
+ /// Update the configuration.
+ pub fn changeConfig(self: *Self, config: *DerivedConfig) !void {
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // We always redo the font shaper in case font features changed. We
+ // could check to see if there was an actual config change but this is
+ // easier and rare enough to not cause performance issues.
+ {
+ var font_shaper = try font.Shaper.init(self.alloc, .{
+ .features = config.font_features.items,
+ });
+ errdefer font_shaper.deinit();
+ self.font_shaper.deinit();
+ self.font_shaper = font_shaper;
+ }
+
+ // We also need to reset the shaper cache so shaper info
+ // from the previous font isn't reused for the new font.
+ const font_shaper_cache = font.ShaperCache.init();
+ self.font_shaper_cache.deinit(self.alloc);
+ self.font_shaper_cache = font_shaper_cache;
+
+ // Set our new minimum contrast
+ self.uniforms.min_contrast = config.min_contrast;
+
+ // Set our new color space and blending
+ self.uniforms.bools.use_display_p3 = config.colorspace == .@"display-p3";
+ self.uniforms.bools.use_linear_blending = config.blending.isLinear();
+ self.uniforms.bools.use_linear_correction = config.blending == .@"linear-corrected";
+
+ const bg_image_config_changed =
+ self.config.bg_image_fit != config.bg_image_fit or
+ self.config.bg_image_position != config.bg_image_position or
+ self.config.bg_image_repeat != config.bg_image_repeat or
+ self.config.bg_image_opacity != config.bg_image_opacity;
+
+ const bg_image_changed =
+ if (self.config.bg_image) |old|
+ if (config.bg_image) |new|
+ !old.equal(new)
+ else
+ true
+ else
+ config.bg_image != null;
+
+ const old_blending = self.config.blending;
+ const custom_shaders_changed = !self.config.custom_shaders.equal(config.custom_shaders);
+
+ self.config.deinit();
+ self.config = config.*;
+
+ // If our background image path changed, prepare the new bg image.
+ if (bg_image_changed) try self.prepBackgroundImage();
+
+ // If our background image config changed, update the vertex buffer.
+ if (bg_image_config_changed) self.updateBgImageBuffer();
+
+ // Reset our viewport to force a rebuild, in case of a font change.
+ self.markDirty();
+
+ const blending_changed = old_blending != config.blending;
+
+ if (blending_changed) {
+ // We update our API's blending mode.
+ self.api.blending = config.blending;
+ // And indicate that we need to reinitialize our shaders.
+ self.reinitialize_shaders = true;
+ // And indicate that our swap chain targets need to
+ // be re-created to account for the new blending mode.
+ self.target_config_modified +%= 1;
+ }
+
+ if (custom_shaders_changed) {
+ self.reinitialize_shaders = true;
+ }
+ }
+
+ /// Resize the screen.
+ pub fn setScreenSize(
+ self: *Self,
+ size: renderer.Size,
+ ) void {
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // We only actually need the padding from this,
+ // everything else is derived elsewhere.
+ self.size.padding = size.padding;
+
+ self.updateScreenSizeUniforms();
+
+ log.debug("screen size size={}", .{size});
+ }
+
+ /// Update uniforms that are based on the screen size.
+ ///
+ /// Caller must hold the draw mutex.
+ fn updateScreenSizeUniforms(self: *Self) void {
+ const terminal_size = self.size.terminal();
+
+ // Blank space around the grid.
+ const blank: renderer.Padding = self.size.screen.blankPadding(
+ self.size.padding,
+ .{
+ .columns = self.cells.size.columns,
+ .rows = self.cells.size.rows,
+ },
+ .{
+ .width = self.grid_metrics.cell_width,
+ .height = self.grid_metrics.cell_height,
+ },
+ ).add(self.size.padding);
+
+ // Setup our uniforms
+ self.uniforms.projection_matrix = math.ortho2d(
+ -1 * @as(f32, @floatFromInt(self.size.padding.left)),
+ @floatFromInt(terminal_size.width + self.size.padding.right),
+ @floatFromInt(terminal_size.height + self.size.padding.bottom),
+ -1 * @as(f32, @floatFromInt(self.size.padding.top)),
+ );
+ self.uniforms.grid_padding = .{
+ @floatFromInt(blank.top),
+ @floatFromInt(blank.right),
+ @floatFromInt(blank.bottom),
+ @floatFromInt(blank.left),
+ };
+ self.uniforms.screen_size = .{
+ @floatFromInt(self.size.screen.width),
+ @floatFromInt(self.size.screen.height),
+ };
+ }
+
+ /// Update the background image vertex buffer (CPU-side).
+ ///
+ /// This should be called if and when configs change that
+ /// could affect the background image.
+ ///
+ /// Caller must hold the draw mutex.
+ fn updateBgImageBuffer(self: *Self) void {
+ self.bg_image_buffer = .{
+ .opacity = self.config.bg_image_opacity,
+ .info = .{
+ .position = switch (self.config.bg_image_position) {
+ .@"top-left" => .tl,
+ .@"top-center" => .tc,
+ .@"top-right" => .tr,
+ .@"center-left" => .ml,
+ .@"center-center", .center => .mc,
+ .@"center-right" => .mr,
+ .@"bottom-left" => .bl,
+ .@"bottom-center" => .bc,
+ .@"bottom-right" => .br,
+ },
+ .fit = switch (self.config.bg_image_fit) {
+ .contain => .contain,
+ .cover => .cover,
+ .stretch => .stretch,
+ .none => .none,
+ },
+ .repeat = self.config.bg_image_repeat,
+ },
+ };
+ // Signal that the buffer was modified.
+ self.bg_image_buffer_modified +%= 1;
+ }
+
+ /// Update custom shader uniforms that depend on terminal state.
+ ///
+ /// This should be called in `updateFrame` when terminal state changes.
+ fn updateCustomShaderUniformsFromState(self: *Self) void {
+ // We only need to do this if we have custom shaders.
+ if (!self.has_custom_shaders) return;
+
+ // Only update when terminal state is dirty.
+ if (self.terminal_state.dirty == .false) return;
+
+ const uniforms: *shadertoy.Uniforms = &self.custom_shader_uniforms;
+ const colors: *const terminal.RenderState.Colors = &self.terminal_state.colors;
+
+ // 256-color palette
+ for (colors.palette, 0..) |color, i| {
+ uniforms.palette[i] = .{
+ @as(f32, @floatFromInt(color.r)) / 255.0,
+ @as(f32, @floatFromInt(color.g)) / 255.0,
+ @as(f32, @floatFromInt(color.b)) / 255.0,
+ 1.0,
+ };
+ }
+
+ // Background color
+ uniforms.background_color = .{
+ @as(f32, @floatFromInt(colors.background.r)) / 255.0,
+ @as(f32, @floatFromInt(colors.background.g)) / 255.0,
+ @as(f32, @floatFromInt(colors.background.b)) / 255.0,
+ 1.0,
+ };
+
+ // Foreground color
+ uniforms.foreground_color = .{
+ @as(f32, @floatFromInt(colors.foreground.r)) / 255.0,
+ @as(f32, @floatFromInt(colors.foreground.g)) / 255.0,
+ @as(f32, @floatFromInt(colors.foreground.b)) / 255.0,
+ 1.0,
+ };
+
+ // Cursor color
+ if (colors.cursor) |cursor_color| {
+ uniforms.cursor_color = .{
+ @as(f32, @floatFromInt(cursor_color.r)) / 255.0,
+ @as(f32, @floatFromInt(cursor_color.g)) / 255.0,
+ @as(f32, @floatFromInt(cursor_color.b)) / 255.0,
+ 1.0,
+ };
+ }
+
+ // NOTE: the following could be optimized to follow a change in
+ // config for a slight optimization however this is only 12 bytes
+ // each being updated and likely isn't a cause for concern
+
+ // Cursor text color
+ if (self.config.cursor_text) |cursor_text| {
+ uniforms.cursor_text = .{
+ @as(f32, @floatFromInt(cursor_text.color.r)) / 255.0,
+ @as(f32, @floatFromInt(cursor_text.color.g)) / 255.0,
+ @as(f32, @floatFromInt(cursor_text.color.b)) / 255.0,
+ 1.0,
+ };
+ }
+
+ // Selection background color
+ if (self.config.selection_background) |selection_bg| {
+ uniforms.selection_background_color = .{
+ @as(f32, @floatFromInt(selection_bg.color.r)) / 255.0,
+ @as(f32, @floatFromInt(selection_bg.color.g)) / 255.0,
+ @as(f32, @floatFromInt(selection_bg.color.b)) / 255.0,
+ 1.0,
+ };
+ }
+
+ // Selection foreground color
+ if (self.config.selection_foreground) |selection_fg| {
+ uniforms.selection_foreground_color = .{
+ @as(f32, @floatFromInt(selection_fg.color.r)) / 255.0,
+ @as(f32, @floatFromInt(selection_fg.color.g)) / 255.0,
+ @as(f32, @floatFromInt(selection_fg.color.b)) / 255.0,
+ 1.0,
+ };
+ }
+
+ // Cursor visibility
+ uniforms.cursor_visible = @intFromBool(self.terminal_state.cursor.visible);
+
+ // Cursor style
+ const cursor_style: renderer.CursorStyle = .fromTerminal(self.terminal_state.cursor.visual_style);
+ uniforms.previous_cursor_style = uniforms.current_cursor_style;
+ uniforms.current_cursor_style = @as(i32, @intFromEnum(cursor_style));
+ }
+
+ /// Update per-frame custom shader uniforms.
+ ///
+ /// This should be called exactly once per frame, inside `drawFrame`.
+ fn updateCustomShaderUniformsForFrame(self: *Self) !void {
+ // We only need to do this if we have custom shaders.
+ if (!self.has_custom_shaders) return;
+
+ const uniforms: *shadertoy.Uniforms = &self.custom_shader_uniforms;
+
+ const now = try std.time.Instant.now();
+ defer self.last_frame_time = now;
+ const first_frame_time = self.first_frame_time orelse t: {
+ self.first_frame_time = now;
+ break :t now;
+ };
+ const last_frame_time = self.last_frame_time orelse now;
+
+ const since_ns: f32 = @floatFromInt(now.since(first_frame_time));
+ uniforms.time = since_ns / std.time.ns_per_s;
+
+ const delta_ns: f32 = @floatFromInt(now.since(last_frame_time));
+ uniforms.time_delta = delta_ns / std.time.ns_per_s;
+
+ uniforms.frame += 1;
+
+ const screen = self.size.screen;
+ const padding = self.size.padding;
+ const cell = self.size.cell;
+
+ uniforms.resolution = .{
+ @floatFromInt(screen.width),
+ @floatFromInt(screen.height),
+ 1,
+ };
+ uniforms.channel_resolution[0] = .{
+ @floatFromInt(screen.width),
+ @floatFromInt(screen.height),
+ 1,
+ 0,
+ };
+
+ if (self.cells.getCursorGlyph()) |cursor| {
+ const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]);
+ const cursor_height: f32 = @floatFromInt(cursor.glyph_size[1]);
+
+ // Left edge of the cell the cursor is in.
+ var pixel_x: f32 = @floatFromInt(
+ cursor.grid_pos[0] * cell.width + padding.left,
+ );
+ // Top edge, relative to the top of the
+ // screen, of the cell the cursor is in.
+ var pixel_y: f32 = @floatFromInt(
+ cursor.grid_pos[1] * cell.height + padding.top,
+ );
+
+ // If +Y is up in our shaders, we need to flip the coordinate
+ // so that it's instead the top edge of the cell relative to
+ // the *bottom* of the screen.
+ if (!GraphicsAPI.custom_shader_y_is_down) {
+ pixel_y = @as(f32, @floatFromInt(screen.height)) - pixel_y;
+ }
+
+ // Add the X bearing to get the -X (left) edge of the cursor.
+ pixel_x += @floatFromInt(cursor.bearings[0]);
+
+ // How we deal with the Y bearing depends on which direction
+ // is "up", since we want our final `pixel_y` value to be the
+ // +Y edge of the cursor.
+ if (GraphicsAPI.custom_shader_y_is_down) {
+ // As a reminder, the Y bearing is the distance from the
+ // bottom of the cell to the top of the glyph, so to get
+ // the +Y edge we need to add the cell height, subtract
+ // the Y bearing, and add the glyph height to get the +Y
+ // (bottom) edge of the cursor.
+ pixel_y += @floatFromInt(cell.height);
+ pixel_y -= @floatFromInt(cursor.bearings[1]);
+ pixel_y += @floatFromInt(cursor.glyph_size[1]);
+ } else {
+ // If the Y direction is reversed though, we instead want
+ // the *top* edge of the cursor, which means we just need
+ // to subtract the cell height and add the Y bearing.
+ pixel_y -= @floatFromInt(cell.height);
+ pixel_y += @floatFromInt(cursor.bearings[1]);
+ }
+
+ const new_cursor: [4]f32 = .{
+ pixel_x,
+ pixel_y,
+ cursor_width,
+ cursor_height,
+ };
+ const cursor_color: [4]f32 = .{
+ @as(f32, @floatFromInt(cursor.color[0])) / 255.0,
+ @as(f32, @floatFromInt(cursor.color[1])) / 255.0,
+ @as(f32, @floatFromInt(cursor.color[2])) / 255.0,
+ @as(f32, @floatFromInt(cursor.color[3])) / 255.0,
+ };
+
+ const cursor_changed: bool =
+ !std.meta.eql(new_cursor, uniforms.current_cursor) or
+ !std.meta.eql(cursor_color, uniforms.current_cursor_color);
+
+ if (cursor_changed) {
+ uniforms.previous_cursor = uniforms.current_cursor;
+ uniforms.previous_cursor_color = uniforms.current_cursor_color;
+ uniforms.current_cursor = new_cursor;
+ uniforms.current_cursor_color = cursor_color;
+ uniforms.cursor_change_time = uniforms.time;
+ }
+ }
+
+ // Update focus uniforms
+ uniforms.focus = @intFromBool(self.focused);
+
+ // If we need to update the time our focus state changed
+ // then update it to our current frame time. This may not be
+ // exactly correct since it is frame time, not exact focus
+ // time, but focus time on its own isn't exactly correct anyways
+ // since it comes async from a message.
+ if (self.custom_shader_focused_changed and self.focused) {
+ uniforms.time_focus = uniforms.time;
+ self.custom_shader_focused_changed = false;
+ }
+ }
+
+ /// Build the overlay as configured. Returns null if there is no
+ /// overlay currently configured.
+ fn rebuildOverlay(
+ self: *Self,
+ features: []const Overlay.Feature,
+ ) Overlay.InitError!void {
+ // const start = std.time.Instant.now() catch unreachable;
+ // const start_micro = std.time.microTimestamp();
+ // defer {
+ // const end = std.time.Instant.now() catch unreachable;
+ // log.warn(
+ // "[rebuildOverlay time] start_micro={} duration={}ns",
+ // .{ start_micro, end.since(start) / std.time.ns_per_us },
+ // );
+ // }
+
+ const alloc = self.alloc;
+
+ // If we have no features enabled, don't build an overlay.
+ // If we had a previous overlay, deallocate it.
+ if (features.len == 0) {
+ if (self.overlay) |*old| {
+ old.deinit(alloc);
+ self.overlay = null;
+ }
+
+ return;
+ }
+
+ // If we had a previous overlay, clear it. Otherwise, init.
+ const overlay: *Overlay = overlay: {
+ if (self.overlay) |*v| existing: {
+ // Verify that our overlay size matches our screen
+ // size as we know it now. If not, deinit and reinit.
+ // Note: these intCasts are always safe because z2d
+ // stores as i32 but we always init with a u32.
+ const width: u32 = @intCast(v.surface.getWidth());
+ const height: u32 = @intCast(v.surface.getHeight());
+ const term_size = self.size.terminal();
+ if (width != term_size.width or
+ height != term_size.height) break :existing;
+
+ // We also depend on cell size.
+ if (v.cell_size.width != self.size.cell.width or
+ v.cell_size.height != self.size.cell.height) break :existing;
+
+ // Everything matches, so we can just reset the surface
+ // and redraw.
+ v.reset();
+ break :overlay v;
+ }
+
+ // If we reached this point we want to reset our overlay.
+ if (self.overlay) |*v| {
+ v.deinit(alloc);
+ self.overlay = null;
+ }
+
+ assert(self.overlay == null);
+ const new: Overlay = try .init(alloc, self.size);
+ self.overlay = new;
+ break :overlay &self.overlay.?;
+ };
+ overlay.applyFeatures(
+ alloc,
+ &self.terminal_state,
+ features,
+ );
+ }
+
+ const PreeditRange = struct {
+ y: terminal.size.CellCountInt,
+ x: [2]terminal.size.CellCountInt,
+ cp_offset: usize,
+ };
+
+ /// Convert the terminal state to GPU cells stored in CPU memory. These
+ /// are then synced to the GPU in the next frame. This only updates CPU
+ /// memory and doesn't touch the GPU.
+ ///
+ /// This requires the draw mutex.
+ ///
+ /// Dirty state on terminal state won't be reset by this.
+ fn rebuildCells(
+ self: *Self,
+ preedit: ?renderer.State.Preedit,
+ cursor_style_: ?renderer.CursorStyle,
+ links: *const terminal.RenderState.CellSet,
+ ) Allocator.Error!void {
+ const state: *terminal.RenderState = &self.terminal_state;
+
+ // const start = try std.time.Instant.now();
+ // const start_micro = std.time.microTimestamp();
+ // defer {
+ // const end = std.time.Instant.now() catch unreachable;
+ // // "[rebuildCells time] <START us>\t<TIME_TAKEN us>"
+ // std.log.warn("[rebuildCells time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us});
+ // }
+
+ const grid_size_diff =
+ self.cells.size.rows != state.rows or
+ self.cells.size.columns != state.cols;
+
+ if (grid_size_diff) {
+ var new_size = self.cells.size;
+ new_size.rows = state.rows;
+ new_size.columns = state.cols;
+ try self.cells.resize(self.alloc, new_size);
+
+ // Update our uniforms accordingly, otherwise
+ // our background cells will be out of place.
+ self.uniforms.grid_size = .{ new_size.columns, new_size.rows };
+ }
+
+ const rebuild = state.dirty == .full or grid_size_diff;
+ if (rebuild) {
+ // If we are doing a full rebuild, then we clear the entire cell buffer.
+ self.cells.reset();
+
+ // We also reset our padding extension depending on the screen type
+ switch (self.config.padding_color) {
+ .background => {},
+
+ // For extension, assume we are extending in all directions.
+ // For "extend" this may be disabled due to heuristics below.
+ .extend, .@"extend-always" => {
+ self.uniforms.padding_extend = .{
+ .up = true,
+ .down = true,
+ .left = true,
+ .right = true,
+ };
+ },
+ }
+ }
+
+ // From this point on we never fail. We produce some kind of
+ // working terminal state, even if incorrect.
+ errdefer comptime unreachable;
+
+ // Get our row data from our state
+ const row_data = state.row_data.slice();
+ const row_raws = row_data.items(.raw);
+ const row_cells = row_data.items(.cells);
+ const row_dirty = row_data.items(.dirty);
+ const row_selection = row_data.items(.selection);
+ const row_highlights = row_data.items(.highlights);
+
+ // If our cell contents buffer is shorter than the screen viewport,
+ // we render the rows that fit, starting from the bottom. If instead
+ // the viewport is shorter than the cell contents buffer, we align
+ // the top of the viewport with the top of the contents buffer.
+ const row_len: usize = @min(
+ state.rows,
+ self.cells.size.rows,
+ );
+
+ // Determine our x/y range for preedit. We don't want to render anything
+ // here because we will render the preedit separately.
+ const preedit_range: ?PreeditRange = if (preedit) |preedit_v| preedit: {
+ // We base the preedit on the position of the cursor in the
+ // viewport. If the cursor isn't visible in the viewport we
+ // don't show it.
+ const cursor_vp = state.cursor.viewport orelse
+ break :preedit null;
+
+ // If our preedit row isn't dirty then we don't need the
+ // preedit range. This also avoids an issue later where we
+ // unconditionally add preedit cells when this is set.
+ if (!rebuild and !row_dirty[cursor_vp.y]) break :preedit null;
+
+ const range = preedit_v.range(
+ cursor_vp.x,
+ state.cols - 1,
+ );
+ break :preedit .{
+ .y = @intCast(cursor_vp.y),
+ .x = .{ range.start, range.end },
+ .cp_offset = range.cp_offset,
+ };
+ } else null;
+
+ for (
+ 0..,
+ row_raws[0..row_len],
+ row_cells[0..row_len],
+ row_dirty[0..row_len],
+ row_selection[0..row_len],
+ row_highlights[0..row_len],
+ ) |y_usize, row, *cells, *dirty, selection, *highlights| {
+ const y: terminal.size.CellCountInt = @intCast(y_usize);
+
+ if (!rebuild) {
+ // Only rebuild if we are doing a full rebuild or this row is dirty.
+ if (!dirty.*) continue;
+
+ // Clear the cells if the row is dirty
+ self.cells.clear(y);
+ }
+
+ // Unmark the dirty state in our render state.
+ dirty.* = false;
+
+ self.rebuildRow(
+ y,
+ row,
+ cells,
+ preedit_range,
+ selection,
+ highlights,
+ links,
+ ) catch |err| {
+ // This should never happen except under exceptional
+ // scenarios. In this case, we don't want to corrupt
+ // our render state so just clear this row and keep
+ // trying to finish it out.
+ log.warn("error building row y={} err={}", .{ y, err });
+ self.cells.clear(y);
+ };
+ }
+
+ // Setup our cursor rendering information.
+ cursor: {
+ // Clear our cursor by default.
+ self.cells.setCursor(null, null);
+ self.uniforms.cursor_pos = .{
+ std.math.maxInt(u16),
+ std.math.maxInt(u16),
+ };
+
+ // If the cursor isn't visible on the viewport, don't show
+ // a cursor. Otherwise, get our cursor cell, because we may
+ // need it for styling.
+ const cursor_vp = state.cursor.viewport orelse break :cursor;
+ const cursor_style: terminal.Style = cursor_style: {
+ const cells = state.row_data.items(.cells);
+ const cell = cells[cursor_vp.y].get(cursor_vp.x);
+ break :cursor_style if (cell.raw.hasStyling())
+ cell.style
+ else
+ .{};
+ };
+
+ // If we have preedit text, we don't setup a cursor
+ if (preedit != null) break :cursor;
+
+ // If there isn't a cursor visual style requested then
+ // we don't render a cursor.
+ const style = cursor_style_ orelse break :cursor;
+
+ // Determine the cursor color.
+ const cursor_color = cursor_color: {
+ // If an explicit cursor color was set by OSC 12, use that.
+ if (state.colors.cursor) |v| break :cursor_color v;
+
+ // Use our configured color if specified
+ if (self.config.cursor_color) |v| switch (v) {
+ .color => |color| break :cursor_color color.toTerminalRGB(),
+
+ inline .@"cell-foreground",
+ .@"cell-background",
+ => |_, tag| {
+ const fg_style = cursor_style.fg(.{
+ .default = state.colors.foreground,
+ .palette = &state.colors.palette,
+ .bold = self.config.bold_color,
+ });
+ const bg_style = cursor_style.bg(
+ &state.cursor.cell,
+ &state.colors.palette,
+ ) orelse state.colors.background;
+
+ break :cursor_color switch (tag) {
+ .color => unreachable,
+ .@"cell-foreground" => if (cursor_style.flags.inverse)
+ bg_style
+ else
+ fg_style,
+ .@"cell-background" => if (cursor_style.flags.inverse)
+ fg_style
+ else
+ bg_style,
+ };
+ },
+ };
+
+ break :cursor_color state.colors.foreground;
+ };
+
+ self.addCursor(
+ &state.cursor,
+ style,
+ cursor_color,
+ );
+
+ // If the cursor is visible then we set our uniforms.
+ if (style == .block) {
+ const wide = state.cursor.cell.wide;
+
+ self.uniforms.cursor_pos = .{
+ // If we are a spacer tail of a wide cell, our cursor needs
+ // to move back one cell. The saturate is to ensure we don't
+ // overflow but this shouldn't happen with well-formed input.
+ switch (wide) {
+ .narrow, .spacer_head, .wide => cursor_vp.x,
+ .spacer_tail => cursor_vp.x -| 1,
+ },
+ @intCast(cursor_vp.y),
+ };
+
+ self.uniforms.bools.cursor_wide = switch (wide) {
+ .narrow, .spacer_head => false,
+ .wide, .spacer_tail => true,
+ };
+
+ const uniform_color = if (self.config.cursor_text) |txt| blk: {
+ // If cursor-text is set, then compute the correct color.
+ // Otherwise, use the background color.
+ if (txt == .color) {
+ // Use the color set by cursor-text, if any.
+ break :blk txt.color.toTerminalRGB();
+ }
+
+ const fg_style = cursor_style.fg(.{
+ .default = state.colors.foreground,
+ .palette = &state.colors.palette,
+ .bold = self.config.bold_color,
+ });
+ const bg_style = cursor_style.bg(
+ &state.cursor.cell,
+ &state.colors.palette,
+ ) orelse state.colors.background;
+
+ break :blk switch (txt) {
+ // If the cell is reversed, use the opposite cell color instead.
+ .@"cell-foreground" => if (cursor_style.flags.inverse)
+ bg_style
+ else
+ fg_style,
+ .@"cell-background" => if (cursor_style.flags.inverse)
+ fg_style
+ else
+ bg_style,
+ else => unreachable,
+ };
+ } else state.colors.background;
+
+ self.uniforms.cursor_color = .{
+ uniform_color.r,
+ uniform_color.g,
+ uniform_color.b,
+ 255,
+ };
+ }
+ }
+
+ // Setup our preedit text.
+ if (preedit) |preedit_v| preedit: {
+ const range = preedit_range orelse break :preedit;
+ var x = range.x[0];
+ for (preedit_v.codepoints[range.cp_offset..]) |cp| {
+ self.addPreeditCell(
+ cp,
+ .{ .x = x, .y = range.y },
+ state.colors.foreground,
+ ) catch |err| {
+ log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{
+ x,
+ range.y,
+ err,
+ });
+ };
+
+ x += if (cp.wide) 2 else 1;
+ }
+ }
+
+ // Update that our cells rebuilt
+ self.cells_rebuilt = true;
+
+ // Log some things
+ // log.debug("rebuildCells complete cached_runs={}", .{
+ // self.font_shaper_cache.count(),
+ // });
+ }
+
+ fn rebuildRow(
+ self: *Self,
+ y: terminal.size.CellCountInt,
+ row: terminal.page.Row,
+ cells: *std.MultiArrayList(terminal.RenderState.Cell),
+ preedit_range: ?PreeditRange,
+ selection: ?[2]terminal.size.CellCountInt,
+ highlights: *const std.ArrayList(terminal.RenderState.Highlight),
+ links: *const terminal.RenderState.CellSet,
+ ) !void {
+ const state = &self.terminal_state;
+
+ // If our viewport is wider than our cell contents buffer,
+ // we still only process cells up to the width of the buffer.
+ const cells_slice = cells.slice();
+ const cells_len = @min(cells_slice.len, self.cells.size.columns);
+ const cells_raw = cells_slice.items(.raw);
+ const cells_style = cells_slice.items(.style);
+
+ // On primary screen, we still apply vertical padding
+ // extension under certain conditions we feel are safe.
+ //
+ // This helps make some scenarios look better while
+ // avoiding scenarios we know do NOT look good.
+ switch (self.config.padding_color) {
+ // These already have the correct values set above.
+ .background, .@"extend-always" => {},
+
+ // Apply heuristics for padding extension.
+ .extend => if (y == 0) {
+ self.uniforms.padding_extend.up = !rowNeverExtendBg(
+ row,
+ cells_raw,
+ cells_style,
+ &state.colors.palette,
+ state.colors.background,
+ );
+ } else if (y == self.cells.size.rows - 1) {
+ self.uniforms.padding_extend.down = !rowNeverExtendBg(
+ row,
+ cells_raw,
+ cells_style,
+ &state.colors.palette,
+ state.colors.background,
+ );
+ },
+ }
+
+ // Iterator of runs for shaping.
+ var run_iter_opts: font.shape.RunOptions = .{
+ .grid = self.font_grid,
+ .cells = cells_slice,
+ .selection = if (selection) |s| s else null,
+
+ // We want to do font shaping as long as the cursor is
+ // visible on this viewport.
+ .cursor_x = cursor_x: {
+ const vp = state.cursor.viewport orelse break :cursor_x null;
+ if (vp.y != y) break :cursor_x null;
+ break :cursor_x vp.x;
+ },
+ };
+ run_iter_opts.applyBreakConfig(self.config.font_shaping_break);
+ var run_iter = self.font_shaper.runIterator(run_iter_opts);
+ var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc);
+ var shaper_cells: ?[]const font.shape.Cell = null;
+ var shaper_cells_i: usize = 0;
+
+ for (
+ 0..,
+ cells_raw[0..cells_len],
+ cells_style[0..cells_len],
+ ) |x, *cell, *managed_style| {
+ // If this cell falls within our preedit range then we
+ // skip this because preedits are setup separately.
+ if (preedit_range) |range| preedit: {
+ // We're not on the preedit line, no actions necessary.
+ if (range.y != y) break :preedit;
+ // We're before the preedit range, no actions necessary.
+ if (x < range.x[0]) break :preedit;
+ // We're in the preedit range, skip this cell.
+ if (x <= range.x[1]) continue;
+ // After exiting the preedit range we need to catch
+ // the run position up because of the missed cells.
+ // In all other cases, no action is necessary.
+ if (x != range.x[1] + 1) break :preedit;
+
+ // Step the run iterator until we find a run that ends
+ // after the current cell, which will be the soonest run
+ // that might contain glyphs for our cell.
+ while (shaper_run) |run| {
+ if (run.offset + run.cells > x) break;
+ shaper_run = try run_iter.next(self.alloc);
+ shaper_cells = null;
+ shaper_cells_i = 0;
+ }
+
+ const run = shaper_run orelse break :preedit;
+
+ // If we haven't shaped this run, do so now.
+ shaper_cells = shaper_cells orelse
+ // Try to read the cells from the shaping cache if we can.
+ self.font_shaper_cache.get(run) orelse
+ cache: {
+ // Otherwise we have to shape them.
+ const new_cells = try self.font_shaper.shape(run);
+
+ // Try to cache them. If caching fails for any reason we
+ // continue because it is just a performance optimization,
+ // not a correctness issue.
+ self.font_shaper_cache.put(
+ self.alloc,
+ run,
+ new_cells,
+ ) catch |err| {
+ log.warn(
+ "error caching font shaping results err={}",
+ .{err},
+ );
+ };
+
+ // The cells we get from direct shaping are always owned
+ // by the shaper and valid until the next shaping call so
+ // we can safely use them.
+ break :cache new_cells;
+ };
+
+ // Advance our index until we reach or pass
+ // our current x position in the shaper cells.
+ const shaper_cells_unwrapped = shaper_cells.?;
+ while (run.offset + shaper_cells_unwrapped[shaper_cells_i].x < x) {
+ shaper_cells_i += 1;
+ }
+ }
+
+ const wide = cell.wide;
+ const style: terminal.Style = if (cell.hasStyling())
+ managed_style.*
+ else
+ .{};
+
+ // True if this cell is selected
+ const selected: enum {
+ false,
+ selection,
+ search,
+ search_selected,
+ } = selected: {
+ // Order below matters for precedence.
+
+ // Selection should take the highest precedence.
+ const x_compare = if (wide == .spacer_tail)
+ x -| 1
+ else
+ x;
+ if (selection) |sel| {
+ if (x_compare >= sel[0] and
+ x_compare <= sel[1]) break :selected .selection;
+ }
+
+ // If we're highlighted, then we're selected. In the
+ // future we want to use a different style for this
+ // but this to get started.
+ for (highlights.items) |hl| {
+ if (x_compare >= hl.range[0] and
+ x_compare <= hl.range[1])
+ {
+ const tag: HighlightTag = @enumFromInt(hl.tag);
+ break :selected switch (tag) {
+ .search_match => .search,
+ .search_match_selected => .search_selected,
+ };
+ }
+ }
+
+ break :selected .false;
+ };
+
+ // The `_style` suffixed values are the colors based on
+ // the cell style (SGR), before applying any additional
+ // configuration, inversions, selections, etc.
+ const bg_style = style.bg(
+ cell,
+ &state.colors.palette,
+ );
+ const fg_style = style.fg(.{
+ .default = state.colors.foreground,
+ .palette = &state.colors.palette,
+ .bold = self.config.bold_color,
+ });
+
+ // The final background color for the cell.
+ const bg = switch (selected) {
+ // If we have an explicit selection background color
+ // specified in the config, use that.
+ //
+ // If no configuration, then our selection background
+ // is our foreground color.
+ .selection => if (self.config.selection_background) |v| switch (v) {
+ .color => |color| color.toTerminalRGB(),
+ .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style,
+ .@"cell-background" => if (style.flags.inverse) fg_style else bg_style,
+ } else state.colors.foreground,
+
+ .search => switch (self.config.search_background) {
+ .color => |color| color.toTerminalRGB(),
+ .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style,
+ .@"cell-background" => if (style.flags.inverse) fg_style else bg_style,
+ },
+
+ .search_selected => switch (self.config.search_selected_background) {
+ .color => |color| color.toTerminalRGB(),
+ .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style,
+ .@"cell-background" => if (style.flags.inverse) fg_style else bg_style,
+ },
+
+ // Not selected
+ .false => if (style.flags.inverse != isCovering(cell.codepoint()))
+ // Two cases cause us to invert (use the fg color as the bg)
+ // - The "inverse" style flag.
+ // - A "covering" glyph; we use fg for bg in that
+ // case to help make sure that padding extension
+ // works correctly.
+ //
+ // If one of these is true (but not the other)
+ // then we use the fg style color for the bg.
+ fg_style
+ else
+ // Otherwise they cancel out.
+ bg_style,
+ };
+
+ const fg = fg: {
+ // Our happy-path non-selection background color
+ // is our style or our configured defaults.
+ const final_bg = bg_style orelse state.colors.background;
+
+ // Whether we need to use the bg color as our fg color:
+ // - Cell is selected, inverted, and set to cell-foreground
+ // - Cell is selected, not inverted, and set to cell-background
+ // - Cell is inverted and not selected
+ break :fg switch (selected) {
+ .selection => if (self.config.selection_foreground) |v| switch (v) {
+ .color => |color| color.toTerminalRGB(),
+ .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style,
+ .@"cell-background" => if (style.flags.inverse) fg_style else final_bg,
+ } else state.colors.background,
+
+ .search => switch (self.config.search_foreground) {
+ .color => |color| color.toTerminalRGB(),
+ .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style,
+ .@"cell-background" => if (style.flags.inverse) fg_style else final_bg,
+ },
+
+ .search_selected => switch (self.config.search_selected_foreground) {
+ .color => |color| color.toTerminalRGB(),
+ .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style,
+ .@"cell-background" => if (style.flags.inverse) fg_style else final_bg,
+ },
+
+ .false => if (style.flags.inverse)
+ final_bg
+ else
+ fg_style,
+ };
+ };
+
+ // Foreground alpha for this cell.
+ const alpha: u8 = if (style.flags.faint) self.config.faint_opacity else 255;
+
+ // Set the cell's background color.
+ {
+ const rgb = bg orelse state.colors.background;
+
+ // Determine our background alpha. If we have transparency configured
+ // then this is dynamic depending on some situations. This is all
+ // in an attempt to make transparency look the best for various
+ // situations. See inline comments.
+ const bg_alpha: u8 = bg_alpha: {
+ const default: u8 = 255;
+
+ // Cells that are selected should be fully opaque.
+ if (selected != .false) break :bg_alpha default;
+
+ // Cells that are reversed should be fully opaque.
+ if (style.flags.inverse) break :bg_alpha default;
+
+ // If the user requested to have opacity on all cells, apply it.
+ if (self.config.background_opacity_cells and bg_style != null) {
+ var opacity: f64 = @floatFromInt(default);
+ opacity *= self.config.background_opacity;
+ break :bg_alpha @intFromFloat(opacity);
+ }
+
+ // Cells that have an explicit bg color should be fully opaque.
+ if (bg_style != null) break :bg_alpha default;
+
+ // Otherwise, we won't draw the bg for this cell,
+ // we'll let the already-drawn background color
+ // show through.
+ break :bg_alpha 0;
+ };
+
+ self.cells.bgCell(y, x).* = .{
+ rgb.r, rgb.g, rgb.b, bg_alpha,
+ };
+ }
+
+ // If the invisible flag is set on this cell then we
+ // don't need to render any foreground elements, so
+ // we just skip all glyphs with this x coordinate.
+ //
+ // NOTE: This behavior matches xterm. Some other terminal
+ // emulators, e.g. Alacritty, still render text decorations
+ // and only make the text itself invisible. The decision
+ // has been made here to match xterm's behavior for this.
+ if (style.flags.invisible) {
+ continue;
+ }
+
+ // Give links a single underline, unless they already have
+ // an underline, in which case use a double underline to
+ // distinguish them.
+ const underline: terminal.Attribute.Underline = underline: {
+ if (links.contains(.{
+ .x = @intCast(x),
+ .y = @intCast(y),
+ })) {
+ break :underline if (style.flags.underline == .single)
+ .double
+ else
+ .single;
+ }
+ break :underline style.flags.underline;
+ };
+
+ // We draw underlines first so that they layer underneath text.
+ // This improves readability when a colored underline is used
+ // which intersects parts of the text (descenders).
+ if (underline != .none) self.addUnderline(
+ @intCast(x),
+ @intCast(y),
+ underline,
+ style.underlineColor(&state.colors.palette) orelse fg,
+ alpha,
+ ) catch |err| {
+ log.warn(
+ "error adding underline to cell, will be invalid x={} y={}, err={}",
+ .{ x, y, err },
+ );
+ };
+
+ if (style.flags.overline) self.addOverline(@intCast(x), @intCast(y), fg, alpha) catch |err| {
+ log.warn(
+ "error adding overline to cell, will be invalid x={} y={}, err={}",
+ .{ x, y, err },
+ );
+ };
+
+ // If we're at or past the end of our shaper run then
+ // we need to get the next run from the run iterator.
+ if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) {
+ shaper_run = try run_iter.next(self.alloc);
+ shaper_cells = null;
+ shaper_cells_i = 0;
+ }
+
+ if (shaper_run) |run| glyphs: {
+ // If we haven't shaped this run yet, do so.
+ shaper_cells = shaper_cells orelse
+ // Try to read the cells from the shaping cache if we can.
+ self.font_shaper_cache.get(run) orelse
+ cache: {
+ // Otherwise we have to shape them.
+ const new_cells = try self.font_shaper.shape(run);
+
+ // Try to cache them. If caching fails for any reason we
+ // continue because it is just a performance optimization,
+ // not a correctness issue.
+ self.font_shaper_cache.put(
+ self.alloc,
+ run,
+ new_cells,
+ ) catch |err| {
+ log.warn(
+ "error caching font shaping results err={}",
+ .{err},
+ );
+ };
+
+ // The cells we get from direct shaping are always owned
+ // by the shaper and valid until the next shaping call so
+ // we can safely use them.
+ break :cache new_cells;
+ };
+
+ const shaped_cells = shaper_cells orelse break :glyphs;
+
+ // If there are no shaper cells for this run, ignore it.
+ // This can occur for runs of empty cells, and is fine.
+ if (shaped_cells.len == 0) break :glyphs;
+
+ // If we encounter a shaper cell to the left of the current
+ // cell then we have some problems. This logic relies on x
+ // position monotonically increasing.
+ assert(run.offset + shaped_cells[shaper_cells_i].x >= x);
+
+ // NOTE: An assumption is made here that a single cell will never
+ // be present in more than one shaper run. If that assumption is
+ // violated, this logic breaks.
+
+ while (shaper_cells_i < shaped_cells.len and
+ run.offset + shaped_cells[shaper_cells_i].x == x) : ({
+ shaper_cells_i += 1;
+ }) {
+ self.addGlyph(
+ @intCast(x),
+ @intCast(y),
+ state.cols,
+ cells_raw,
+ shaped_cells[shaper_cells_i],
+ shaper_run.?,
+ fg,
+ alpha,
+ ) catch |err| {
+ log.warn(
+ "error adding glyph to cell, will be invalid x={} y={}, err={}",
+ .{ x, y, err },
+ );
+ };
+ }
+ }
+
+ // Finally, draw a strikethrough if necessary.
+ if (style.flags.strikethrough) self.addStrikethrough(
+ @intCast(x),
+ @intCast(y),
+ fg,
+ alpha,
+ ) catch |err| {
+ log.warn(
+ "error adding strikethrough to cell, will be invalid x={} y={}, err={}",
+ .{ x, y, err },
+ );
+ };
+ }
+ }
+
+ /// Add an underline decoration to the specified cell
+ fn addUnderline(
+ self: *Self,
+ x: terminal.size.CellCountInt,
+ y: terminal.size.CellCountInt,
+ style: terminal.Attribute.Underline,
+ color: terminal.color.RGB,
+ alpha: u8,
+ ) !void {
+ const sprite: font.Sprite = switch (style) {
+ .none => unreachable,
+ .single => .underline,
+ .double => .underline_double,
+ .dotted => .underline_dotted,
+ .dashed => .underline_dashed,
+ .curly => .underline_curly,
+ };
+
+ const render = try self.font_grid.renderGlyph(
+ self.alloc,
+ font.sprite_index,
+ @intFromEnum(sprite),
+ .{
+ .cell_width = 1,
+ .grid_metrics = self.grid_metrics,
+ },
+ );
+
+ try self.cells.add(self.alloc, .underline, .{
+ .atlas = .grayscale,
+ .grid_pos = .{ @intCast(x), @intCast(y) },
+ .color = .{ color.r, color.g, color.b, alpha },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x),
+ @intCast(render.glyph.offset_y),
+ },
+ });
+ }
+
+ /// Add a overline decoration to the specified cell
+ fn addOverline(
+ self: *Self,
+ x: terminal.size.CellCountInt,
+ y: terminal.size.CellCountInt,
+ color: terminal.color.RGB,
+ alpha: u8,
+ ) !void {
+ const render = try self.font_grid.renderGlyph(
+ self.alloc,
+ font.sprite_index,
+ @intFromEnum(font.Sprite.overline),
+ .{
+ .cell_width = 1,
+ .grid_metrics = self.grid_metrics,
+ },
+ );
+
+ try self.cells.add(self.alloc, .overline, .{
+ .atlas = .grayscale,
+ .grid_pos = .{ @intCast(x), @intCast(y) },
+ .color = .{ color.r, color.g, color.b, alpha },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x),
+ @intCast(render.glyph.offset_y),
+ },
+ });
+ }
+
+ /// Add a strikethrough decoration to the specified cell
+ fn addStrikethrough(
+ self: *Self,
+ x: terminal.size.CellCountInt,
+ y: terminal.size.CellCountInt,
+ color: terminal.color.RGB,
+ alpha: u8,
+ ) !void {
+ const render = try self.font_grid.renderGlyph(
+ self.alloc,
+ font.sprite_index,
+ @intFromEnum(font.Sprite.strikethrough),
+ .{
+ .cell_width = 1,
+ .grid_metrics = self.grid_metrics,
+ },
+ );
+
+ try self.cells.add(self.alloc, .strikethrough, .{
+ .atlas = .grayscale,
+ .grid_pos = .{ @intCast(x), @intCast(y) },
+ .color = .{ color.r, color.g, color.b, alpha },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x),
+ @intCast(render.glyph.offset_y),
+ },
+ });
+ }
+
+ // Add a glyph to the specified cell.
+ fn addGlyph(
+ self: *Self,
+ x: terminal.size.CellCountInt,
+ y: terminal.size.CellCountInt,
+ cols: usize,
+ cell_raws: []const terminal.page.Cell,
+ shaper_cell: font.shape.Cell,
+ shaper_run: font.shape.TextRun,
+ color: terminal.color.RGB,
+ alpha: u8,
+ ) !void {
+ const cell = cell_raws[x];
+ const cp = cell.codepoint();
+
+ // Render
+ const render = try self.font_grid.renderGlyph(
+ self.alloc,
+ shaper_run.font_index,
+ shaper_cell.glyph_index,
+ .{
+ .grid_metrics = self.grid_metrics,
+ .thicken = self.config.font_thicken,
+ .thicken_strength = self.config.font_thicken_strength,
+ .cell_width = cell.gridWidth(),
+ // If there's no Nerd Font constraint for this codepoint
+ // then, if it's a symbol, we constrain it to fit inside
+ // its cell(s), we don't modify the alignment at all.
+ .constraint = getConstraint(cp) orelse
+ if (cellpkg.isSymbol(cp)) .{
+ .size = .fit,
+ } else .none,
+ .constraint_width = constraintWidth(
+ cell_raws,
+ x,
+ cols,
+ ),
+ },
+ );
+
+ // If the glyph is 0 width or height, it will be invisible
+ // when drawn, so don't bother adding it to the buffer.
+ if (render.glyph.width == 0 or render.glyph.height == 0) {
+ return;
+ }
+
+ try self.cells.add(self.alloc, .text, .{
+ .atlas = switch (render.presentation) {
+ .emoji => .color,
+ .text => .grayscale,
+ },
+ .bools = .{ .no_min_contrast = noMinContrast(cp) },
+ .grid_pos = .{ @intCast(x), @intCast(y) },
+ .color = .{ color.r, color.g, color.b, alpha },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x + shaper_cell.x_offset),
+ @intCast(render.glyph.offset_y + shaper_cell.y_offset),
+ },
+ });
+ }
+
+ fn addCursor(
+ self: *Self,
+ cursor_state: *const terminal.RenderState.Cursor,
+ cursor_style: renderer.CursorStyle,
+ cursor_color: terminal.color.RGB,
+ ) void {
+ const cursor_vp = cursor_state.viewport orelse return;
+
+ // Add the cursor. We render the cursor over the wide character if
+ // we're on the wide character tail.
+ const wide, const x = cell: {
+ // The cursor goes over the screen cursor position.
+ if (!cursor_vp.wide_tail) break :cell .{
+ cursor_state.cell.wide == .wide,
+ cursor_vp.x,
+ };
+
+ // If we're part of a wide character, we move the cursor back
+ // to the actual character.
+ break :cell .{ true, cursor_vp.x - 1 };
+ };
+
+ const alpha: u8 = if (!self.focused) 255 else alpha: {
+ const alpha = 255 * self.config.cursor_opacity;
+ break :alpha @intFromFloat(@ceil(alpha));
+ };
+
+ const render = switch (cursor_style) {
+ .block,
+ .block_hollow,
+ .bar,
+ .underline,
+ => render: {
+ const sprite: font.Sprite = switch (cursor_style) {
+ .block => .cursor_rect,
+ .block_hollow => .cursor_hollow_rect,
+ .bar => .cursor_bar,
+ .underline => .cursor_underline,
+ .lock => unreachable,
+ };
+
+ break :render self.font_grid.renderGlyph(
+ self.alloc,
+ font.sprite_index,
+ @intFromEnum(sprite),
+ .{
+ .cell_width = if (wide) 2 else 1,
+ .grid_metrics = self.grid_metrics,
+ },
+ ) catch |err| {
+ log.warn("error rendering cursor glyph err={}", .{err});
+ return;
+ };
+ },
+
+ .lock => self.font_grid.renderCodepoint(
+ self.alloc,
+ 0xF023, // lock symbol
+ .regular,
+ .text,
+ .{
+ .cell_width = if (wide) 2 else 1,
+ .grid_metrics = self.grid_metrics,
+ },
+ ) catch |err| {
+ log.warn("error rendering cursor glyph err={}", .{err});
+ return;
+ } orelse {
+ // This should never happen because we embed nerd
+ // fonts so we just log and return instead of fallback.
+ log.warn("failed to find lock symbol for cursor codepoint=0xF023", .{});
+ return;
+ },
+ };
+
+ self.cells.setCursor(.{
+ .atlas = .grayscale,
+ .bools = .{ .is_cursor_glyph = true },
+ .grid_pos = .{ x, cursor_vp.y },
+ .color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x),
+ @intCast(render.glyph.offset_y),
+ },
+ }, cursor_style);
+ }
+
+ fn addPreeditCell(
+ self: *Self,
+ cp: renderer.State.Preedit.Codepoint,
+ coord: terminal.Coordinate,
+ screen_fg: terminal.color.RGB,
+ ) !void {
+ // Render the glyph for our preedit text
+ const render_ = self.font_grid.renderCodepoint(
+ self.alloc,
+ @intCast(cp.codepoint),
+ .regular,
+ .text,
+ .{ .grid_metrics = self.grid_metrics },
+ ) catch |err| {
+ log.warn("error rendering preedit glyph err={}", .{err});
+ return;
+ };
+ const render = render_ orelse {
+ log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint});
+ return;
+ };
+
+ // Add our text
+ try self.cells.add(self.alloc, .text, .{
+ .atlas = .grayscale,
+ .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
+ .color = .{ screen_fg.r, screen_fg.g, screen_fg.b, 255 },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x),
+ @intCast(render.glyph.offset_y),
+ },
+ });
+
+ // Add underline
+ try self.addUnderline(@intCast(coord.x), @intCast(coord.y), .single, screen_fg, 255);
+ if (cp.wide and coord.x < self.cells.size.columns - 1) {
+ try self.addUnderline(@intCast(coord.x + 1), @intCast(coord.y), .single, screen_fg, 255);
+ }
+ }
+
+ /// Sync the atlas data to the given texture. This copies the bytes
+ /// associated with the atlas to the given texture. If the atlas no
+ /// longer fits into the texture, the texture will be resized.
+ fn syncAtlasTexture(
+ self: *const Self,
+ atlas: *const font.Atlas,
+ texture: *Texture,
+ ) !void {
+ if (atlas.size > texture.width) {
+ // Free our old texture
+ texture.*.deinit();
+
+ // Reallocate
+ texture.* = try self.api.initAtlasTexture(atlas);
+ }
+
+ try texture.replaceRegion(0, 0, atlas.size, atlas.size, atlas.data);
+ }
+ };
+}
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,
+ };
+ }
+};
diff --git a/vendor/libghostty-vt/src/renderer/link.zig b/vendor/libghostty-vt/src/renderer/link.zig
new file mode 100644
index 00000000..c5de6157
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/link.zig
@@ -0,0 +1,322 @@
+const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const oni = @import("oniguruma");
+const inputpkg = @import("../input.zig");
+const terminal = @import("../terminal/main.zig");
+const point = terminal.point;
+const Screen = terminal.Screen;
+const Terminal = terminal.Terminal;
+
+const log = std.log.scoped(.renderer_link);
+
+/// The link configuration needed for renderers.
+pub const Link = struct {
+ /// The regular expression to match the link against.
+ regex: oni.Regex,
+
+ /// The situations in which the link should be highlighted.
+ highlight: inputpkg.Link.Highlight,
+
+ pub fn deinit(self: *Link) void {
+ self.regex.deinit();
+ }
+};
+
+/// A set of links. This provides a higher level API for renderers
+/// to match against a viewport and determine if cells are part of
+/// a link.
+pub const Set = struct {
+ links: []Link,
+
+ /// Returns the slice of links from the configuration.
+ pub fn fromConfig(
+ alloc: Allocator,
+ config: []const inputpkg.Link,
+ ) !Set {
+ var links: std.ArrayList(Link) = .empty;
+ defer links.deinit(alloc);
+
+ for (config) |link| {
+ var regex = try link.oniRegex();
+ errdefer regex.deinit();
+ try links.append(alloc, .{
+ .regex = regex,
+ .highlight = link.highlight,
+ });
+ }
+
+ return .{ .links = try links.toOwnedSlice(alloc) };
+ }
+
+ pub fn deinit(self: *Set, alloc: Allocator) void {
+ for (self.links) |*link| link.deinit();
+ alloc.free(self.links);
+ }
+
+ /// Fills matches with the matches from regex link matches.
+ pub fn renderCellMap(
+ self: *const Set,
+ alloc: Allocator,
+ result: *terminal.RenderState.CellSet,
+ render_state: *const terminal.RenderState,
+ mouse_viewport: ?point.Coordinate,
+ mouse_mods: inputpkg.Mods,
+ ) !void {
+ // Fast path, not very likely since we have default links.
+ if (self.links.len == 0) return;
+
+ // Convert our render state to a string + byte map.
+ var builder: std.Io.Writer.Allocating = .init(alloc);
+ defer builder.deinit();
+ var map: terminal.RenderState.StringMap = .empty;
+ defer map.deinit(alloc);
+ try render_state.string(&builder.writer, .{
+ .alloc = alloc,
+ .map = &map,
+ });
+
+ const str = builder.writer.buffered();
+
+ // Go through each link and see if we have any matches.
+ for (self.links) |*link| {
+ // Determine if our highlight conditions are met. We use a
+ // switch here instead of an if so that we can get a compile
+ // error if any other conditions are added.
+ switch (link.highlight) {
+ .always => {},
+ .always_mods => |v| if (!mouse_mods.equal(v)) continue,
+
+ // We check the hover points later.
+ .hover => if (mouse_viewport == null) continue,
+ .hover_mods => |v| {
+ if (mouse_viewport == null) continue;
+ if (!mouse_mods.equal(v)) continue;
+ },
+ }
+
+ var offset: usize = 0;
+ while (offset < str.len) {
+ var region = link.regex.search(
+ str[offset..],
+ .{},
+ ) catch |err| switch (err) {
+ error.Mismatch => break,
+ else => return err,
+ };
+ defer region.deinit();
+
+ // We have a match!
+ const offset_start: usize = @intCast(region.starts()[0]);
+ const offset_end: usize = @intCast(region.ends()[0]);
+ const start = offset + offset_start;
+ const end = offset + offset_end;
+
+ // Increment our offset by the number of bytes in the match.
+ // We defer this so that we can return the match before
+ // modifying the offset.
+ defer offset = end;
+
+ switch (link.highlight) {
+ .always, .always_mods => {},
+ .hover, .hover_mods => if (mouse_viewport) |vp| {
+ for (map.items[start..end]) |pt| {
+ if (pt.eql(vp)) break;
+ } else continue;
+ } else continue,
+ }
+
+ // Record the match
+ for (map.items[start..end]) |pt| {
+ try result.put(alloc, pt, {});
+ }
+ }
+ }
+ }
+};
+
+test "renderCellMap" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var t: terminal.Terminal = try .init(alloc, .{
+ .cols = 5,
+ .rows = 3,
+ });
+ defer t.deinit(alloc);
+
+ var s = t.vtStream();
+ defer s.deinit();
+ const str = "1ABCD2EFGH\r\n3IJKL";
+ s.nextSlice(str);
+
+ var state: terminal.RenderState = .empty;
+ defer state.deinit(alloc);
+ try state.update(alloc, &t);
+
+ // Get a set
+ var set = try Set.fromConfig(alloc, &.{
+ .{
+ .regex = "AB",
+ .action = .{ .open = {} },
+ .highlight = .{ .always = {} },
+ },
+
+ .{
+ .regex = "EF",
+ .action = .{ .open = {} },
+ .highlight = .{ .always = {} },
+ },
+ });
+ defer set.deinit(alloc);
+
+ // Get our matches
+ var result: terminal.RenderState.CellSet = .empty;
+ defer result.deinit(alloc);
+ try set.renderCellMap(
+ alloc,
+ &result,
+ &state,
+ null,
+ .{},
+ );
+ try testing.expect(!result.contains(.{ .x = 0, .y = 0 }));
+ try testing.expect(result.contains(.{ .x = 1, .y = 0 }));
+ try testing.expect(result.contains(.{ .x = 2, .y = 0 }));
+ try testing.expect(!result.contains(.{ .x = 3, .y = 0 }));
+ try testing.expect(result.contains(.{ .x = 1, .y = 1 }));
+ try testing.expect(!result.contains(.{ .x = 1, .y = 2 }));
+}
+
+test "renderCellMap hover links" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var t: terminal.Terminal = try .init(alloc, .{
+ .cols = 5,
+ .rows = 3,
+ });
+ defer t.deinit(alloc);
+
+ var s = t.vtStream();
+ defer s.deinit();
+ const str = "1ABCD2EFGH\r\n3IJKL";
+ s.nextSlice(str);
+
+ var state: terminal.RenderState = .empty;
+ defer state.deinit(alloc);
+ try state.update(alloc, &t);
+
+ // Get a set
+ var set = try Set.fromConfig(alloc, &.{
+ .{
+ .regex = "AB",
+ .action = .{ .open = {} },
+ .highlight = .{ .hover = {} },
+ },
+
+ .{
+ .regex = "EF",
+ .action = .{ .open = {} },
+ .highlight = .{ .always = {} },
+ },
+ });
+ defer set.deinit(alloc);
+
+ // Not hovering over the first link
+ {
+ var result: terminal.RenderState.CellSet = .empty;
+ defer result.deinit(alloc);
+ try set.renderCellMap(
+ alloc,
+ &result,
+ &state,
+ null,
+ .{},
+ );
+
+ // Test our matches
+ try testing.expect(!result.contains(.{ .x = 0, .y = 0 }));
+ try testing.expect(!result.contains(.{ .x = 1, .y = 0 }));
+ try testing.expect(!result.contains(.{ .x = 2, .y = 0 }));
+ try testing.expect(!result.contains(.{ .x = 3, .y = 0 }));
+ try testing.expect(result.contains(.{ .x = 1, .y = 1 }));
+ try testing.expect(!result.contains(.{ .x = 1, .y = 2 }));
+ }
+
+ // Hovering over the first link
+ {
+ var result: terminal.RenderState.CellSet = .empty;
+ defer result.deinit(alloc);
+ try set.renderCellMap(
+ alloc,
+ &result,
+ &state,
+ .{ .x = 1, .y = 0 },
+ .{},
+ );
+
+ // Test our matches
+ try testing.expect(!result.contains(.{ .x = 0, .y = 0 }));
+ try testing.expect(result.contains(.{ .x = 1, .y = 0 }));
+ try testing.expect(result.contains(.{ .x = 2, .y = 0 }));
+ try testing.expect(!result.contains(.{ .x = 3, .y = 0 }));
+ try testing.expect(result.contains(.{ .x = 1, .y = 1 }));
+ try testing.expect(!result.contains(.{ .x = 1, .y = 2 }));
+ }
+}
+
+test "renderCellMap mods no match" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var t: terminal.Terminal = try .init(alloc, .{
+ .cols = 5,
+ .rows = 3,
+ });
+ defer t.deinit(alloc);
+
+ var s = t.vtStream();
+ defer s.deinit();
+ const str = "1ABCD2EFGH\r\n3IJKL";
+ s.nextSlice(str);
+
+ var state: terminal.RenderState = .empty;
+ defer state.deinit(alloc);
+ try state.update(alloc, &t);
+
+ // Get a set
+ var set = try Set.fromConfig(alloc, &.{
+ .{
+ .regex = "AB",
+ .action = .{ .open = {} },
+ .highlight = .{ .always = {} },
+ },
+
+ .{
+ .regex = "EF",
+ .action = .{ .open = {} },
+ .highlight = .{ .always_mods = .{ .ctrl = true } },
+ },
+ });
+ defer set.deinit(alloc);
+
+ // Get our matches
+ var result: terminal.RenderState.CellSet = .empty;
+ defer result.deinit(alloc);
+ try set.renderCellMap(
+ alloc,
+ &result,
+ &state,
+ null,
+ .{},
+ );
+
+ // Test our matches
+ try testing.expect(!result.contains(.{ .x = 0, .y = 0 }));
+ try testing.expect(result.contains(.{ .x = 1, .y = 0 }));
+ try testing.expect(result.contains(.{ .x = 2, .y = 0 }));
+ try testing.expect(!result.contains(.{ .x = 3, .y = 0 }));
+ try testing.expect(!result.contains(.{ .x = 1, .y = 1 }));
+ try testing.expect(!result.contains(.{ .x = 1, .y = 2 }));
+}
diff --git a/vendor/libghostty-vt/src/renderer/message.zig b/vendor/libghostty-vt/src/renderer/message.zig
new file mode 100644
index 00000000..a47b9608
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/message.zig
@@ -0,0 +1,111 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const ArenaAllocator = std.heap.ArenaAllocator;
+const configpkg = @import("../config.zig");
+const font = @import("../font/main.zig");
+const renderer = @import("../renderer.zig");
+const terminal = @import("../terminal/main.zig");
+
+/// The messages that can be sent to a renderer thread.
+pub const Message = union(enum) {
+ /// Purposely crash the renderer. This is used for testing and debugging.
+ /// See the "crash" binding action.
+ crash,
+
+ /// A change in state in the window focus that this renderer is
+ /// rendering within. This is only sent when a change is detected so
+ /// the renderer is expected to handle all of these.
+ focus: bool,
+
+ /// A change in the view occlusion state. This can be used to determine
+ /// if the window is visible or not. A window can be not visible (occluded)
+ /// and still have focus.
+ visible: bool,
+
+ /// Reset the cursor blink by immediately showing the cursor then
+ /// restarting the timer.
+ reset_cursor_blink,
+
+ /// Change the font grid. This can happen for any number of reasons
+ /// including a font size change, family change, etc.
+ font_grid: struct {
+ grid: *font.SharedGrid,
+ set: *font.SharedGridSet,
+
+ // The key for the new grid. If adopting the new grid fails for any
+ // reason, the old grid should be kept but the new key should be
+ // dereferenced.
+ new_key: font.SharedGridSet.Key,
+
+ // After accepting the new grid, the old grid must be dereferenced
+ // using the fields below.
+ old_key: font.SharedGridSet.Key,
+ },
+
+ /// Changes the size. The screen size might change, padding, grid, etc.
+ resize: renderer.Size,
+
+ /// The derived configuration to update the renderer with.
+ change_config: struct {
+ alloc: Allocator,
+ thread: *renderer.Thread.DerivedConfig,
+ impl: *renderer.Renderer.DerivedConfig,
+ },
+
+ /// Matches for the current viewport from the search thread. These happen
+ /// async so they may be off for a frame or two from the actually rendered
+ /// viewport. The renderer must handle this gracefully.
+ search_viewport_matches: SearchMatches,
+
+ /// The selected match from the search thread. May be null to indicate
+ /// no match currently.
+ search_selected_match: ?SearchMatch,
+
+ /// Activate or deactivate the inspector.
+ inspector: bool,
+
+ /// The macOS display ID has changed for the window.
+ macos_display_id: u32,
+
+ pub const SearchMatches = struct {
+ arena: ArenaAllocator,
+ matches: []const terminal.highlight.Flattened,
+ };
+
+ pub const SearchMatch = struct {
+ arena: ArenaAllocator,
+ match: terminal.highlight.Flattened,
+ };
+
+ /// Initialize a change_config message.
+ pub fn initChangeConfig(alloc: Allocator, config: *const configpkg.Config) !Message {
+ const thread_ptr = try alloc.create(renderer.Thread.DerivedConfig);
+ errdefer alloc.destroy(thread_ptr);
+ const config_ptr = try alloc.create(renderer.Renderer.DerivedConfig);
+ errdefer alloc.destroy(config_ptr);
+
+ thread_ptr.* = renderer.Thread.DerivedConfig.init(config);
+ config_ptr.* = try renderer.Renderer.DerivedConfig.init(alloc, config);
+ errdefer config_ptr.deinit();
+
+ return .{
+ .change_config = .{
+ .alloc = alloc,
+ .thread = thread_ptr,
+ .impl = config_ptr,
+ },
+ };
+ }
+
+ pub fn deinit(self: *const Message) void {
+ switch (self.*) {
+ .change_config => |v| {
+ v.impl.deinit();
+ v.alloc.destroy(v.impl);
+ v.alloc.destroy(v.thread);
+ },
+
+ else => {},
+ }
+ }
+};
diff --git a/vendor/libghostty-vt/src/renderer/metal/Frame.zig b/vendor/libghostty-vt/src/renderer/metal/Frame.zig
new file mode 100644
index 00000000..388b4f9e
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/metal/Frame.zig
@@ -0,0 +1,131 @@
+//! Wrapper for handling render passes.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const objc = @import("objc");
+
+const mtl = @import("api.zig");
+const Renderer = @import("../generic.zig").Renderer(Metal);
+const Metal = @import("../Metal.zig");
+const Target = @import("Target.zig");
+const RenderPass = @import("RenderPass.zig");
+
+const Health = @import("../../renderer.zig").Health;
+
+const log = std.log.scoped(.metal);
+
+/// Options for beginning a frame.
+pub const Options = struct {
+ /// MTLCommandQueue
+ queue: objc.Object,
+};
+
+/// MTLCommandBuffer
+buffer: objc.Object,
+
+block: CompletionBlock.Context,
+
+/// Begin encoding a frame.
+pub fn begin(
+ opts: Options,
+ /// Once the frame has been completed, the `frameCompleted` method
+ /// on the renderer is called with the health status of the frame.
+ renderer: *Renderer,
+ /// The target is presented via the provided renderer's API when completed.
+ target: *Target,
+) !Self {
+ const buffer = opts.queue.msgSend(
+ objc.Object,
+ objc.sel("commandBuffer"),
+ .{},
+ );
+
+ // Create our block to register for completion updates.
+ // The block is deallocated by the objC runtime on success.
+ const block = CompletionBlock.init(
+ .{
+ .renderer = renderer,
+ .target = target,
+ .sync = false,
+ },
+ &bufferCompleted,
+ );
+
+ return .{ .buffer = buffer, .block = block };
+}
+
+/// This is the block type used for the addCompletedHandler callback.
+const CompletionBlock = objc.Block(struct {
+ renderer: *Renderer,
+ target: *Target,
+ sync: bool,
+}, .{
+ objc.c.id, // MTLCommandBuffer
+}, void);
+
+fn bufferCompleted(
+ block: *const CompletionBlock.Context,
+ buffer_id: objc.c.id,
+) callconv(.c) void {
+ const buffer = objc.Object.fromId(buffer_id);
+
+ // Get our command buffer status to pass back to the generic renderer.
+ const status = buffer.getProperty(mtl.MTLCommandBufferStatus, "status");
+ const health: Health = switch (status) {
+ .@"error" => .unhealthy,
+ else => .healthy,
+ };
+
+ // If the frame is healthy, present it.
+ if (health == .healthy) {
+ block.renderer.api.present(
+ block.target.*,
+ block.sync,
+ ) catch |err| {
+ log.err("Failed to present render target: err={}", .{err});
+ };
+ }
+
+ block.renderer.frameCompleted(health);
+}
+
+/// Add a render pass to this frame with the provided attachments.
+/// Returns a RenderPass which allows render steps to be added.
+pub inline fn renderPass(
+ self: *const Self,
+ attachments: []const RenderPass.Options.Attachment,
+) RenderPass {
+ return RenderPass.begin(.{
+ .attachments = attachments,
+ .command_buffer = self.buffer,
+ });
+}
+
+/// Complete this frame and present the target.
+///
+/// If `sync` is true, this will block until the frame is presented.
+pub inline fn complete(self: *Self, sync: bool) void {
+ // If we don't need to complete synchronously,
+ // we add our block as a completion handler.
+ //
+ // It will be copied when we add the handler, and then the
+ // copy will be deallocated by the objc runtime on success.
+ if (!sync) {
+ self.buffer.msgSend(
+ void,
+ objc.sel("addCompletedHandler:"),
+ .{&self.block},
+ );
+ }
+
+ self.buffer.msgSend(void, objc.sel("commit"), .{});
+
+ // If we need to complete synchronously, we wait until
+ // the buffer is completed and invoke the block directly.
+ if (sync) {
+ self.buffer.msgSend(void, "waitUntilCompleted", .{});
+ self.block.sync = true;
+ CompletionBlock.invoke(&self.block, .{self.buffer.value});
+ }
+}
diff --git a/vendor/libghostty-vt/src/renderer/metal/IOSurfaceLayer.zig b/vendor/libghostty-vt/src/renderer/metal/IOSurfaceLayer.zig
new file mode 100644
index 00000000..34fbfbed
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/metal/IOSurfaceLayer.zig
@@ -0,0 +1,183 @@
+//! A wrapper around a CALayer with a utility method
+//! for settings its `contents` to an IOSurface.
+const IOSurfaceLayer = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const objc = @import("objc");
+const macos = @import("macos");
+
+const IOSurface = macos.iosurface.IOSurface;
+
+const log = std.log.scoped(.IOSurfaceLayer);
+
+/// We subclass CALayer with a custom display handler, we only need
+/// to make the subclass once, and then we can use it as a singleton.
+var Subclass: ?objc.Class = null;
+
+/// The underlying CALayer
+layer: objc.Object,
+
+pub fn init() !IOSurfaceLayer {
+ // The layer returned by `[CALayer layer]` is autoreleased, which means
+ // that at the end of the current autorelease pool it will be deallocated
+ // if it isn't retained, so we retain it here manually an extra time.
+ const layer = (try getSubclass()).msgSend(
+ objc.Object,
+ objc.sel("layer"),
+ .{},
+ ).retain();
+ errdefer layer.release();
+
+ // The layer gravity is set to top-left so that the contents aren't
+ // stretched during resize operations before a new frame has been drawn.
+ layer.setProperty("contentsGravity", macos.animation.kCAGravityTopLeft);
+
+ layer.setInstanceVariable("display_cb", .{ .value = null });
+ layer.setInstanceVariable("display_ctx", .{ .value = null });
+
+ return .{ .layer = layer };
+}
+
+pub fn release(self: *IOSurfaceLayer) void {
+ self.layer.release();
+}
+
+/// Sets the layer's `contents` to the provided IOSurface.
+///
+/// Makes sure to do so on the main thread to avoid visual artifacts.
+pub inline fn setSurface(self: *IOSurfaceLayer, surface: *IOSurface) !void {
+ // We retain the surface to make sure it's not GC'd
+ // before we can set it as the contents of the layer.
+ //
+ // We release in the callback after setting the contents.
+ surface.retain();
+ // NOTE: Since `self.layer` is passed as an `objc.c.id`, it's
+ // automatically retained when the block is copied, so we
+ // don't need to retain it ourselves like with the surface.
+
+ var block = SetSurfaceBlock.init(.{
+ .layer = self.layer.value,
+ .surface = surface,
+ }, &setSurfaceCallback);
+
+ // We check if we're on the main thread and run the block directly if so.
+ const NSThread = objc.getClass("NSThread").?;
+ if (NSThread.msgSend(bool, "isMainThread", .{})) {
+ setSurfaceCallback(&block);
+ } else {
+ // NOTE: The block will be copied when we pass it to dispatch_async,
+ // and then automatically be deallocated by the objc runtime
+ // once it's executed.
+
+ macos.dispatch.dispatch_async(
+ @ptrCast(macos.dispatch.queue.getMain()),
+ @ptrCast(&block),
+ );
+ }
+}
+
+/// Sets the layer's `contents` to the provided IOSurface.
+///
+/// Does not ensure this happens on the main thread.
+pub inline fn setSurfaceSync(self: *IOSurfaceLayer, surface: *IOSurface) void {
+ self.layer.setProperty("contents", surface);
+}
+
+const SetSurfaceBlock = objc.Block(struct {
+ layer: objc.c.id,
+ surface: *IOSurface,
+}, .{}, void);
+
+fn setSurfaceCallback(
+ block: *const SetSurfaceBlock.Context,
+) callconv(.c) void {
+ const layer = objc.Object.fromId(block.layer);
+ const surface: *IOSurface = block.surface;
+
+ // See explanation of why we retain and release in `setSurface`.
+ defer surface.release();
+
+ // We check to see if the surface is the appropriate size for
+ // the layer, if it's not then we discard it. This is because
+ // asynchronously drawn frames can sometimes finish just after
+ // a synchronously drawn frame during a resize, and if we don't
+ // discard the improperly sized surface it creates jank.
+ const bounds = layer.getProperty(macos.graphics.Rect, "bounds");
+ const scale = layer.getProperty(f64, "contentsScale");
+ const width: usize = @intFromFloat(bounds.size.width * scale);
+ const height: usize = @intFromFloat(bounds.size.height * scale);
+ if (width != surface.getWidth() or height != surface.getHeight()) {
+ log.debug(
+ "setSurfaceCallback(): surface is wrong size for layer, discarding. surface = {d}x{d}, layer = {d}x{d}",
+ .{ surface.getWidth(), surface.getHeight(), width, height },
+ );
+ return;
+ }
+
+ layer.setProperty("contents", surface);
+}
+
+pub const DisplayCallback = ?*align(8) const fn (?*anyopaque) void;
+
+pub fn setDisplayCallback(
+ self: *IOSurfaceLayer,
+ display_cb: DisplayCallback,
+ display_ctx: ?*anyopaque,
+) void {
+ self.layer.setInstanceVariable(
+ "display_cb",
+ objc.Object.fromId(@constCast(display_cb)),
+ );
+ self.layer.setInstanceVariable(
+ "display_ctx",
+ objc.Object.fromId(display_ctx),
+ );
+}
+
+fn getSubclass() error{ObjCFailed}!objc.Class {
+ if (Subclass) |c| return c;
+
+ const CALayer =
+ objc.getClass("CALayer") orelse return error.ObjCFailed;
+
+ var subclass =
+ objc.allocateClassPair(CALayer, "IOSurfaceLayer") orelse return error.ObjCFailed;
+ errdefer objc.disposeClassPair(subclass);
+
+ if (!subclass.addIvar("display_cb")) return error.ObjCFailed;
+ if (!subclass.addIvar("display_ctx")) return error.ObjCFailed;
+
+ subclass.replaceMethod("display", struct {
+ fn display(target: objc.c.id, sel: objc.c.SEL) callconv(.c) void {
+ _ = sel;
+ const self = objc.Object.fromId(target);
+ const display_cb: DisplayCallback = @ptrFromInt(@intFromPtr(
+ self.getInstanceVariable("display_cb").value,
+ ));
+ if (display_cb) |cb| cb(
+ @ptrCast(self.getInstanceVariable("display_ctx").value),
+ );
+ }
+ }.display);
+
+ // Disable all animations for this layer by returning null for all actions.
+ subclass.replaceMethod("actionForKey:", struct {
+ fn actionForKey(
+ target: objc.c.id,
+ sel: objc.c.SEL,
+ key: objc.c.id,
+ ) callconv(.c) objc.c.id {
+ _ = target;
+ _ = sel;
+ _ = key;
+ return objc.getClass("NSNull").?.msgSend(objc.c.id, "null", .{});
+ }
+ }.actionForKey);
+
+ objc.registerClassPair(subclass);
+
+ Subclass = subclass;
+
+ return subclass;
+}
diff --git a/vendor/libghostty-vt/src/renderer/metal/Pipeline.zig b/vendor/libghostty-vt/src/renderer/metal/Pipeline.zig
new file mode 100644
index 00000000..9ba25c35
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/metal/Pipeline.zig
@@ -0,0 +1,204 @@
+//! Wrapper for handling render pipelines.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const macos = @import("macos");
+const objc = @import("objc");
+
+const mtl = @import("api.zig");
+
+const log = std.log.scoped(.metal);
+
+/// Options for initializing a render pipeline.
+pub const Options = struct {
+ /// MTLDevice
+ device: objc.Object,
+
+ /// Name of the vertex function
+ vertex_fn: []const u8,
+ /// Name of the fragment function
+ fragment_fn: []const u8,
+
+ /// MTLLibrary to get the vertex function from
+ vertex_library: objc.Object,
+ /// MTLLibrary to get the fragment function from
+ fragment_library: objc.Object,
+
+ /// Vertex step function
+ step_fn: mtl.MTLVertexStepFunction = .per_vertex,
+
+ /// Info about the color attachments used by this render pipeline.
+ attachments: []const Attachment,
+
+ /// Describes a color attachment.
+ pub const Attachment = struct {
+ pixel_format: mtl.MTLPixelFormat,
+ blending_enabled: bool = true,
+ };
+};
+
+/// MTLRenderPipelineState
+state: objc.Object,
+
+pub fn init(comptime VertexAttributes: ?type, opts: Options) !Self {
+ // Create our descriptor
+ const desc = init: {
+ const Class = objc.getClass("MTLRenderPipelineDescriptor").?;
+ const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
+ const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
+ break :init id_init;
+ };
+ defer desc.msgSend(void, objc.sel("release"), .{});
+
+ // Get our vertex and fragment functions and add them to the descriptor.
+ {
+ const str = try macos.foundation.String.createWithBytes(
+ opts.vertex_fn,
+ .utf8,
+ false,
+ );
+ defer str.release();
+
+ const ptr = opts.vertex_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
+ const func_vert = objc.Object.fromId(ptr.?);
+ defer func_vert.msgSend(void, objc.sel("release"), .{});
+
+ desc.setProperty("vertexFunction", func_vert);
+ }
+ {
+ const str = try macos.foundation.String.createWithBytes(
+ opts.fragment_fn,
+ .utf8,
+ false,
+ );
+ defer str.release();
+
+ const ptr = opts.fragment_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
+ const func_frag = objc.Object.fromId(ptr.?);
+ defer func_frag.msgSend(void, objc.sel("release"), .{});
+
+ desc.setProperty("fragmentFunction", func_frag);
+ }
+
+ // If we have vertex attributes, create and add a vertex descriptor.
+ if (VertexAttributes) |V| {
+ const vertex_desc = init: {
+ const Class = objc.getClass("MTLVertexDescriptor").?;
+ const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
+ const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
+ break :init id_init;
+ };
+ defer vertex_desc.msgSend(void, objc.sel("release"), .{});
+
+ // Our attributes are the fields of the input
+ const attrs = objc.Object.fromId(vertex_desc.getProperty(?*anyopaque, "attributes"));
+ autoAttribute(V, attrs);
+
+ // The layout describes how and when we fetch the next vertex input.
+ const layouts = objc.Object.fromId(vertex_desc.getProperty(?*anyopaque, "layouts"));
+ {
+ const layout = layouts.msgSend(
+ objc.Object,
+ objc.sel("objectAtIndexedSubscript:"),
+ .{@as(c_ulong, 0)},
+ );
+
+ layout.setProperty("stepFunction", @intFromEnum(opts.step_fn));
+ layout.setProperty("stride", @as(c_ulong, @sizeOf(V)));
+ }
+
+ desc.setProperty("vertexDescriptor", vertex_desc);
+ }
+
+ // Set our color attachment
+ const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments"));
+ for (opts.attachments, 0..) |at, i| {
+ const attachment = attachments.msgSend(
+ objc.Object,
+ objc.sel("objectAtIndexedSubscript:"),
+ .{@as(c_ulong, i)},
+ );
+
+ attachment.setProperty("pixelFormat", @intFromEnum(at.pixel_format));
+
+ attachment.setProperty("blendingEnabled", at.blending_enabled);
+ // We always use premultiplied alpha blending for now.
+ if (at.blending_enabled) {
+ attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
+ attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
+ attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
+ attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
+ attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
+ attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
+ }
+ }
+
+ // Make our state
+ var err: ?*anyopaque = null;
+ const pipeline_state = opts.device.msgSend(
+ objc.Object,
+ objc.sel("newRenderPipelineStateWithDescriptor:error:"),
+ .{ desc, &err },
+ );
+ try checkError(err);
+ errdefer pipeline_state.release();
+
+ return .{ .state = pipeline_state };
+}
+
+pub fn deinit(self: *const Self) void {
+ self.state.release();
+}
+
+fn autoAttribute(T: type, attrs: objc.Object) void {
+ inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| {
+ const offset = @offsetOf(T, field.name);
+
+ const FT = switch (@typeInfo(field.type)) {
+ .@"struct" => |e| e.backing_integer.?,
+ .@"enum" => |e| e.tag_type,
+ else => field.type,
+ };
+
+ // Very incomplete list, expand as necessary.
+ const format = switch (FT) {
+ [4]u8 => mtl.MTLVertexFormat.uchar4,
+ [2]u16 => mtl.MTLVertexFormat.ushort2,
+ [2]i16 => mtl.MTLVertexFormat.short2,
+ f32 => mtl.MTLVertexFormat.float,
+ [2]f32 => mtl.MTLVertexFormat.float2,
+ [4]f32 => mtl.MTLVertexFormat.float4,
+ i32 => mtl.MTLVertexFormat.int,
+ [2]i32 => mtl.MTLVertexFormat.int2,
+ [4]i32 => mtl.MTLVertexFormat.int2,
+ u32 => mtl.MTLVertexFormat.uint,
+ [2]u32 => mtl.MTLVertexFormat.uint2,
+ [4]u32 => mtl.MTLVertexFormat.uint4,
+ u8 => mtl.MTLVertexFormat.uchar,
+ i8 => mtl.MTLVertexFormat.char,
+ else => comptime unreachable,
+ };
+
+ const attr = attrs.msgSend(
+ objc.Object,
+ objc.sel("objectAtIndexedSubscript:"),
+ .{@as(c_ulong, i)},
+ );
+
+ attr.setProperty("format", @intFromEnum(format));
+ attr.setProperty("offset", @as(c_ulong, offset));
+ attr.setProperty("bufferIndex", @as(c_ulong, 0));
+ }
+}
+
+fn checkError(err_: ?*anyopaque) !void {
+ const nserr = objc.Object.fromId(err_ orelse return);
+ const str = @as(
+ *macos.foundation.String,
+ @ptrCast(nserr.getProperty(?*anyopaque, "localizedDescription").?),
+ );
+
+ log.err("metal error={s}", .{str.cstringPtr(.ascii).?});
+ return error.MetalFailed;
+}
diff --git a/vendor/libghostty-vt/src/renderer/metal/RenderPass.zig b/vendor/libghostty-vt/src/renderer/metal/RenderPass.zig
new file mode 100644
index 00000000..f204e177
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/metal/RenderPass.zig
@@ -0,0 +1,229 @@
+//! Wrapper for handling render passes.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const objc = @import("objc");
+
+const mtl = @import("api.zig");
+const Pipeline = @import("Pipeline.zig");
+const Sampler = @import("Sampler.zig");
+const Texture = @import("Texture.zig");
+const Target = @import("Target.zig");
+
+const log = std.log.scoped(.metal);
+
+/// Options for beginning a render pass.
+pub const Options = struct {
+ /// MTLCommandBuffer
+ command_buffer: objc.Object,
+ /// Color attachments for this render pass.
+ attachments: []const Attachment,
+
+ /// Describes a color attachment.
+ pub const Attachment = struct {
+ target: union(enum) {
+ texture: Texture,
+ target: Target,
+ },
+ clear_color: ?[4]f64 = null,
+ };
+};
+
+/// Describes a step in a render pass.
+pub const Step = struct {
+ pipeline: Pipeline,
+ /// MTLBuffer
+ uniforms: ?objc.Object = null,
+ /// MTLBuffer
+ buffers: []const ?objc.Object = &.{},
+ textures: []const ?Texture = &.{},
+ /// Set of samplers to use for this step. The index maps to an index
+ /// of a fragment texture, set via setFragmentSamplerState(_:index:).
+ samplers: []const ?Sampler = &.{},
+ draw: Draw,
+
+ /// Describes the draw call for this step.
+ pub const Draw = struct {
+ type: mtl.MTLPrimitiveType,
+ vertex_count: usize,
+ instance_count: usize = 1,
+ };
+};
+
+/// MTLRenderCommandEncoder
+encoder: objc.Object,
+
+/// Begin a render pass.
+pub fn begin(
+ opts: Options,
+) Self {
+ // Create a pass descriptor
+ const desc = desc: {
+ const MTLRenderPassDescriptor = objc.getClass("MTLRenderPassDescriptor").?;
+ const desc = MTLRenderPassDescriptor.msgSend(
+ objc.Object,
+ objc.sel("renderPassDescriptor"),
+ .{},
+ );
+
+ // Set our color attachment to be our drawable surface.
+ const attachments = objc.Object.fromId(
+ desc.getProperty(?*anyopaque, "colorAttachments"),
+ );
+ for (opts.attachments, 0..) |at, i| {
+ const attachment = attachments.msgSend(
+ objc.Object,
+ objc.sel("objectAtIndexedSubscript:"),
+ .{@as(c_ulong, i)},
+ );
+
+ attachment.setProperty(
+ "loadAction",
+ @intFromEnum(@as(
+ mtl.MTLLoadAction,
+ if (at.clear_color != null)
+ .clear
+ else
+ .load,
+ )),
+ );
+ attachment.setProperty(
+ "storeAction",
+ @intFromEnum(mtl.MTLStoreAction.store),
+ );
+ attachment.setProperty("texture", switch (at.target) {
+ .texture => |t| t.texture.value,
+ .target => |t| t.texture.value,
+ });
+ if (at.clear_color) |c| attachment.setProperty(
+ "clearColor",
+ mtl.MTLClearColor{
+ .red = c[0],
+ .green = c[1],
+ .blue = c[2],
+ .alpha = c[3],
+ },
+ );
+ }
+
+ break :desc desc;
+ };
+
+ // MTLRenderCommandEncoder
+ const encoder = opts.command_buffer.msgSend(
+ objc.Object,
+ objc.sel("renderCommandEncoderWithDescriptor:"),
+ .{desc.value},
+ );
+
+ return .{ .encoder = encoder };
+}
+
+/// Add a step to this render pass.
+pub fn step(self: *const Self, s: Step) void {
+ if (s.draw.instance_count == 0) return;
+
+ // Set pipeline state
+ self.encoder.msgSend(
+ void,
+ objc.sel("setRenderPipelineState:"),
+ .{s.pipeline.state.value},
+ );
+
+ if (s.buffers.len > 0) {
+ // We reserve index 0 for the vertex buffer, this isn't very
+ // flexible but it lines up with the API we have for OpenGL.
+ if (s.buffers[0]) |buf| {
+ self.encoder.msgSend(
+ void,
+ objc.sel("setVertexBuffer:offset:atIndex:"),
+ .{ buf.value, @as(c_ulong, 0), @as(c_ulong, 0) },
+ );
+ self.encoder.msgSend(
+ void,
+ objc.sel("setFragmentBuffer:offset:atIndex:"),
+ .{ buf.value, @as(c_ulong, 0), @as(c_ulong, 0) },
+ );
+ }
+
+ // Set the rest of the buffers starting at index 2, this is
+ // so that we can use index 1 for the uniforms if present.
+ //
+ // Also, we set buffers (and textures) for both stages.
+ //
+ // Again, not very flexible, but it's consistent and predictable,
+ // and we need to treat the uniforms as special because of OpenGL.
+ //
+ // TODO: Maybe in the future add info to the pipeline struct which
+ // allows it to define a mapping between provided buffers and
+ // what index they get set at for the vertex / fragment stage.
+ for (s.buffers[1..], 2..) |b, i| if (b) |buf| {
+ self.encoder.msgSend(
+ void,
+ objc.sel("setVertexBuffer:offset:atIndex:"),
+ .{ buf.value, @as(c_ulong, 0), @as(c_ulong, i) },
+ );
+ self.encoder.msgSend(
+ void,
+ objc.sel("setFragmentBuffer:offset:atIndex:"),
+ .{ buf.value, @as(c_ulong, 0), @as(c_ulong, i) },
+ );
+ };
+ }
+
+ // Set the uniforms as buffer index 1 if present.
+ if (s.uniforms) |buf| {
+ self.encoder.msgSend(
+ void,
+ objc.sel("setVertexBuffer:offset:atIndex:"),
+ .{ buf.value, @as(c_ulong, 0), @as(c_ulong, 1) },
+ );
+ self.encoder.msgSend(
+ void,
+ objc.sel("setFragmentBuffer:offset:atIndex:"),
+ .{ buf.value, @as(c_ulong, 0), @as(c_ulong, 1) },
+ );
+ }
+
+ // Set textures.
+ for (s.textures, 0..) |t, i| if (t) |tex| {
+ self.encoder.msgSend(
+ void,
+ objc.sel("setVertexTexture:atIndex:"),
+ .{ tex.texture.value, @as(c_ulong, i) },
+ );
+ self.encoder.msgSend(
+ void,
+ objc.sel("setFragmentTexture:atIndex:"),
+ .{ tex.texture.value, @as(c_ulong, i) },
+ );
+ };
+
+ // Set samplers.
+ for (s.samplers, 0..) |samp, i| if (samp) |sampler| {
+ self.encoder.msgSend(
+ void,
+ objc.sel("setFragmentSamplerState:atIndex:"),
+ .{ sampler.sampler.value, @as(c_ulong, i) },
+ );
+ };
+
+ // Draw!
+ self.encoder.msgSend(
+ void,
+ objc.sel("drawPrimitives:vertexStart:vertexCount:instanceCount:"),
+ .{
+ @intFromEnum(s.draw.type),
+ @as(c_ulong, 0),
+ @as(c_ulong, s.draw.vertex_count),
+ @as(c_ulong, s.draw.instance_count),
+ },
+ );
+}
+
+/// Complete this render pass.
+/// This struct can no longer be used after calling this.
+pub fn complete(self: *const Self) void {
+ self.encoder.msgSend(void, objc.sel("endEncoding"), .{});
+}
diff --git a/vendor/libghostty-vt/src/renderer/metal/Sampler.zig b/vendor/libghostty-vt/src/renderer/metal/Sampler.zig
new file mode 100644
index 00000000..593f9a86
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/metal/Sampler.zig
@@ -0,0 +1,64 @@
+//! Wrapper for handling samplers.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const objc = @import("objc");
+
+const mtl = @import("api.zig");
+const Metal = @import("../Metal.zig");
+
+const log = std.log.scoped(.metal);
+
+/// Options for initializing a sampler.
+pub const Options = struct {
+ /// MTLDevice
+ device: objc.Object,
+ min_filter: mtl.MTLSamplerMinMagFilter,
+ mag_filter: mtl.MTLSamplerMinMagFilter,
+ s_address_mode: mtl.MTLSamplerAddressMode,
+ t_address_mode: mtl.MTLSamplerAddressMode,
+};
+
+/// The underlying MTLSamplerState Object.
+sampler: objc.Object,
+
+pub const Error = error{
+ /// A Metal API call failed.
+ MetalFailed,
+};
+
+/// Initialize a sampler
+pub fn init(
+ opts: Options,
+) Error!Self {
+ // Create our descriptor
+ const desc = init: {
+ const Class = objc.getClass("MTLSamplerDescriptor").?;
+ const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
+ const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
+ break :init id_init;
+ };
+ defer desc.release();
+
+ // Properties
+ desc.setProperty("minFilter", opts.min_filter);
+ desc.setProperty("magFilter", opts.mag_filter);
+ desc.setProperty("sAddressMode", opts.s_address_mode);
+ desc.setProperty("tAddressMode", opts.t_address_mode);
+
+ // Create the sampler state
+ const id = opts.device.msgSend(
+ ?*anyopaque,
+ objc.sel("newSamplerStateWithDescriptor:"),
+ .{desc},
+ ) orelse return error.MetalFailed;
+
+ return .{
+ .sampler = objc.Object.fromId(id),
+ };
+}
+
+pub fn deinit(self: Self) void {
+ self.sampler.release();
+}
diff --git a/vendor/libghostty-vt/src/renderer/metal/Target.zig b/vendor/libghostty-vt/src/renderer/metal/Target.zig
new file mode 100644
index 00000000..f20bb0b7
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/metal/Target.zig
@@ -0,0 +1,108 @@
+//! Represents a render target.
+//!
+//! In this case, an IOSurface-backed MTLTexture.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const objc = @import("objc");
+const macos = @import("macos");
+const graphics = macos.graphics;
+const IOSurface = macos.iosurface.IOSurface;
+
+const mtl = @import("api.zig");
+
+const log = std.log.scoped(.metal);
+
+/// Options for initializing a Target
+pub const Options = struct {
+ /// MTLDevice
+ device: objc.Object,
+
+ /// Desired width
+ width: usize,
+ /// Desired height
+ height: usize,
+
+ /// Pixel format for the MTLTexture
+ pixel_format: mtl.MTLPixelFormat,
+ /// Storage mode for the MTLTexture
+ storage_mode: mtl.MTLResourceOptions.StorageMode,
+};
+
+/// The underlying IOSurface.
+surface: *IOSurface,
+
+/// The underlying MTLTexture.
+texture: objc.Object,
+
+/// Current width of this target.
+width: usize,
+/// Current height of this target.
+height: usize,
+
+pub fn init(opts: Options) !Self {
+ // We set our surface's color space to Display P3.
+ // This allows us to have "Apple-style" alpha blending,
+ // since it seems to be the case that Apple apps like
+ // Terminal and TextEdit render text in the display's
+ // color space using converted colors, which reduces,
+ // but does not fully eliminate blending artifacts.
+ const colorspace = try graphics.ColorSpace.createNamed(.displayP3);
+ defer colorspace.release();
+
+ const surface = try IOSurface.init(.{
+ .width = @intCast(opts.width),
+ .height = @intCast(opts.height),
+ .pixel_format = .@"32BGRA",
+ .bytes_per_element = 4,
+ .colorspace = colorspace,
+ });
+
+ // Create our descriptor
+ const desc = init: {
+ const Class = objc.getClass("MTLTextureDescriptor").?;
+ const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
+ const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
+ break :init id_init;
+ };
+ defer desc.release();
+
+ // Set our properties
+ desc.setProperty("width", @as(c_ulong, @intCast(opts.width)));
+ desc.setProperty("height", @as(c_ulong, @intCast(opts.height)));
+ desc.setProperty("pixelFormat", @intFromEnum(opts.pixel_format));
+ desc.setProperty("usage", mtl.MTLTextureUsage{ .render_target = true });
+ desc.setProperty(
+ "resourceOptions",
+ mtl.MTLResourceOptions{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = opts.storage_mode,
+ },
+ );
+
+ const id = opts.device.msgSend(
+ ?*anyopaque,
+ objc.sel("newTextureWithDescriptor:iosurface:plane:"),
+ .{
+ desc,
+ surface,
+ @as(c_ulong, 0),
+ },
+ ) orelse return error.MetalFailed;
+
+ const texture = objc.Object.fromId(id);
+
+ return .{
+ .surface = surface,
+ .texture = texture,
+ .width = opts.width,
+ .height = opts.height,
+ };
+}
+
+pub fn deinit(self: *Self) void {
+ self.surface.deinit();
+ self.texture.release();
+}
diff --git a/vendor/libghostty-vt/src/renderer/metal/Texture.zig b/vendor/libghostty-vt/src/renderer/metal/Texture.zig
new file mode 100644
index 00000000..5042919a
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/metal/Texture.zig
@@ -0,0 +1,202 @@
+//! Wrapper for handling textures.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = @import("../../quirks.zig").inlineAssert;
+const objc = @import("objc");
+
+const mtl = @import("api.zig");
+const Metal = @import("../Metal.zig");
+
+const log = std.log.scoped(.metal);
+
+/// Options for initializing a texture.
+pub const Options = struct {
+ /// MTLDevice
+ device: objc.Object,
+ pixel_format: mtl.MTLPixelFormat,
+ resource_options: mtl.MTLResourceOptions,
+ usage: mtl.MTLTextureUsage,
+};
+
+/// The underlying MTLTexture Object.
+texture: objc.Object,
+
+/// The width of this texture.
+width: usize,
+/// The height of this texture.
+height: usize,
+
+/// Bytes per pixel for this texture.
+bpp: usize,
+
+pub const Error = error{
+ /// A Metal API call failed.
+ MetalFailed,
+};
+
+/// Initialize a texture
+pub fn init(
+ opts: Options,
+ width: usize,
+ height: usize,
+ data: ?[]const u8,
+) Error!Self {
+ // Create our descriptor
+ const desc = init: {
+ const Class = objc.getClass("MTLTextureDescriptor").?;
+ const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
+ const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
+ break :init id_init;
+ };
+ defer desc.release();
+
+ // Set our properties
+ desc.setProperty("pixelFormat", @intFromEnum(opts.pixel_format));
+ desc.setProperty("width", @as(c_ulong, width));
+ desc.setProperty("height", @as(c_ulong, height));
+ desc.setProperty("resourceOptions", opts.resource_options);
+ desc.setProperty("usage", opts.usage);
+
+ // Initialize
+ const id = opts.device.msgSend(
+ ?*anyopaque,
+ objc.sel("newTextureWithDescriptor:"),
+ .{desc},
+ ) orelse return error.MetalFailed;
+
+ const self: Self = .{
+ .texture = objc.Object.fromId(id),
+ .width = width,
+ .height = height,
+ .bpp = bppOf(opts.pixel_format),
+ };
+
+ // If we have data, we set it here.
+ if (data) |d| {
+ assert(d.len == width * height * self.bpp);
+ try self.replaceRegion(0, 0, width, height, d);
+ }
+
+ return self;
+}
+
+pub fn deinit(self: Self) void {
+ self.texture.release();
+}
+
+/// Replace a region of the texture with the provided data.
+///
+/// Does NOT check the dimensions of the data to ensure correctness.
+pub fn replaceRegion(
+ self: Self,
+ x: usize,
+ y: usize,
+ width: usize,
+ height: usize,
+ data: []const u8,
+) error{}!void {
+ self.texture.msgSend(
+ void,
+ objc.sel("replaceRegion:mipmapLevel:withBytes:bytesPerRow:"),
+ .{
+ mtl.MTLRegion{
+ .origin = .{ .x = x, .y = y, .z = 0 },
+ .size = .{
+ .width = @intCast(width),
+ .height = @intCast(height),
+ .depth = 1,
+ },
+ },
+ @as(c_ulong, 0),
+ @as(*const anyopaque, data.ptr),
+ @as(c_ulong, self.bpp * width),
+ },
+ );
+}
+
+/// Returns the bytes per pixel for the provided pixel format
+fn bppOf(pixel_format: mtl.MTLPixelFormat) usize {
+ return switch (pixel_format) {
+ // Invalid
+ .invalid => @panic("invalid pixel format"),
+
+ // Weird formats I was too lazy to get the sizes of
+ else => @panic("pixel format size unknown (unlikely that this format was actually used, could be memory corruption)"),
+
+ // 8-bit pixel formats
+ .a8unorm,
+ .r8unorm,
+ .r8unorm_srgb,
+ .r8snorm,
+ .r8uint,
+ .r8sint,
+ .rg8unorm,
+ .rg8unorm_srgb,
+ .rg8snorm,
+ .rg8uint,
+ .rg8sint,
+ .stencil8,
+ => 1,
+
+ // 16-bit pixel formats
+ .r16unorm,
+ .r16snorm,
+ .r16uint,
+ .r16sint,
+ .r16float,
+ .rg16unorm,
+ .rg16snorm,
+ .rg16uint,
+ .rg16sint,
+ .rg16float,
+ .b5g6r5unorm,
+ .a1bgr5unorm,
+ .abgr4unorm,
+ .bgr5a1unorm,
+ .depth16unorm,
+ => 2,
+
+ // 32-bit pixel formats
+ .rgba8unorm,
+ .rgba8unorm_srgb,
+ .rgba8snorm,
+ .rgba8uint,
+ .rgba8sint,
+ .bgra8unorm,
+ .bgra8unorm_srgb,
+ .rgb10a2unorm,
+ .rgb10a2uint,
+ .rg11b10float,
+ .rgb9e5float,
+ .bgr10a2unorm,
+ .bgr10_xr,
+ .bgr10_xr_srgb,
+ .r32uint,
+ .r32sint,
+ .r32float,
+ .depth32float,
+ .depth24unorm_stencil8,
+ => 4,
+
+ // 64-bit pixel formats
+ .rg32uint,
+ .rg32sint,
+ .rg32float,
+ .rgba16unorm,
+ .rgba16snorm,
+ .rgba16uint,
+ .rgba16sint,
+ .rgba16float,
+ .bgra10_xr,
+ .bgra10_xr_srgb,
+ => 8,
+
+ // 128-bit pixel formats,
+ .rgba32uint,
+ .rgba32sint,
+ .rgba32float,
+ => 128,
+ };
+}
diff --git a/vendor/libghostty-vt/src/renderer/metal/api.zig b/vendor/libghostty-vt/src/renderer/metal/api.zig
new file mode 100644
index 00000000..a2d8a135
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/metal/api.zig
@@ -0,0 +1,452 @@
+//! This file contains the definitions of the Metal API that we use.
+//!
+//! Because the online Apple developer docs have recently (as of January 2025)
+//! been changed to hide enum values, `Metal-cpp` has been used as a reference
+//! source instead.
+//!
+//! Ref: https://developer.apple.com/metal/cpp/
+
+/// https://developer.apple.com/documentation/metal/mtlcommandbufferstatus?language=objc
+pub const MTLCommandBufferStatus = enum(c_ulong) {
+ not_enqueued = 0,
+ enqueued = 1,
+ committed = 2,
+ scheduled = 3,
+ completed = 4,
+ @"error" = 5,
+ _,
+};
+
+/// https://developer.apple.com/documentation/metal/mtlloadaction?language=objc
+pub const MTLLoadAction = enum(c_ulong) {
+ dont_care = 0,
+ load = 1,
+ clear = 2,
+};
+
+/// https://developer.apple.com/documentation/metal/mtlstoreaction?language=objc
+pub const MTLStoreAction = enum(c_ulong) {
+ dont_care = 0,
+ store = 1,
+ multisample_resolve = 2,
+ store_and_multisample_resolve = 3,
+ unknown = 4,
+ custom_sample_depth_store = 5,
+};
+
+/// https://developer.apple.com/documentation/metal/mtlresourceoptions?language=objc
+pub const MTLResourceOptions = packed struct(c_ulong) {
+ /// https://developer.apple.com/documentation/metal/mtlcpucachemode?language=objc
+ cpu_cache_mode: CPUCacheMode = .default,
+ /// https://developer.apple.com/documentation/metal/mtlstoragemode?language=objc
+ storage_mode: StorageMode,
+ /// https://developer.apple.com/documentation/metal/mtlhazardtrackingmode?language=objc
+ hazard_tracking_mode: HazardTrackingMode = .default,
+
+ _pad: @Type(.{
+ .int = .{ .signedness = .unsigned, .bits = @bitSizeOf(c_ulong) - 10 },
+ }) = 0,
+
+ pub const CPUCacheMode = enum(u4) {
+ default = 0,
+ write_combined = 1,
+ };
+
+ pub const StorageMode = enum(u4) {
+ shared = 0,
+ managed = 1,
+ private = 2,
+ memoryless = 3,
+ };
+
+ pub const HazardTrackingMode = enum(u2) {
+ default = 0,
+ untracked = 1,
+ tracked = 2,
+ };
+};
+
+/// https://developer.apple.com/documentation/metal/mtlprimitivetype?language=objc
+pub const MTLPrimitiveType = enum(c_ulong) {
+ point = 0,
+ line = 1,
+ line_strip = 2,
+ triangle = 3,
+ triangle_strip = 4,
+};
+
+/// https://developer.apple.com/documentation/metal/mtlindextype?language=objc
+pub const MTLIndexType = enum(c_ulong) {
+ uint16 = 0,
+ uint32 = 1,
+};
+
+/// https://developer.apple.com/documentation/metal/mtlvertexformat?language=objc
+pub const MTLVertexFormat = enum(c_ulong) {
+ invalid = 0,
+ uchar2 = 1,
+ uchar3 = 2,
+ uchar4 = 3,
+ char2 = 4,
+ char3 = 5,
+ char4 = 6,
+ uchar2normalized = 7,
+ uchar3normalized = 8,
+ uchar4normalized = 9,
+ char2normalized = 10,
+ char3normalized = 11,
+ char4normalized = 12,
+ ushort2 = 13,
+ ushort3 = 14,
+ ushort4 = 15,
+ short2 = 16,
+ short3 = 17,
+ short4 = 18,
+ ushort2normalized = 19,
+ ushort3normalized = 20,
+ ushort4normalized = 21,
+ short2normalized = 22,
+ short3normalized = 23,
+ short4normalized = 24,
+ half2 = 25,
+ half3 = 26,
+ half4 = 27,
+ float = 28,
+ float2 = 29,
+ float3 = 30,
+ float4 = 31,
+ int = 32,
+ int2 = 33,
+ int3 = 34,
+ int4 = 35,
+ uint = 36,
+ uint2 = 37,
+ uint3 = 38,
+ uint4 = 39,
+ int1010102normalized = 40,
+ uint1010102normalized = 41,
+ uchar4normalized_bgra = 42,
+ uchar = 45,
+ char = 46,
+ ucharnormalized = 47,
+ charnormalized = 48,
+ ushort = 49,
+ short = 50,
+ ushortnormalized = 51,
+ shortnormalized = 52,
+ half = 53,
+ floatrg11b10 = 54,
+ floatrgb9e5 = 55,
+};
+
+/// https://developer.apple.com/documentation/metal/mtlvertexstepfunction?language=objc
+pub const MTLVertexStepFunction = enum(c_ulong) {
+ constant = 0,
+ per_vertex = 1,
+ per_instance = 2,
+ per_patch = 3,
+ per_patch_control_point = 4,
+};
+
+/// https://developer.apple.com/documentation/metal/mtlpixelformat?language=objc
+pub const MTLPixelFormat = enum(c_ulong) {
+ invalid = 0,
+ a8unorm = 1,
+ r8unorm = 10,
+ r8unorm_srgb = 11,
+ r8snorm = 12,
+ r8uint = 13,
+ r8sint = 14,
+ r16unorm = 20,
+ r16snorm = 22,
+ r16uint = 23,
+ r16sint = 24,
+ r16float = 25,
+ rg8unorm = 30,
+ rg8unorm_srgb = 31,
+ rg8snorm = 32,
+ rg8uint = 33,
+ rg8sint = 34,
+ b5g6r5unorm = 40,
+ a1bgr5unorm = 41,
+ abgr4unorm = 42,
+ bgr5a1unorm = 43,
+ r32uint = 53,
+ r32sint = 54,
+ r32float = 55,
+ rg16unorm = 60,
+ rg16snorm = 62,
+ rg16uint = 63,
+ rg16sint = 64,
+ rg16float = 65,
+ rgba8unorm = 70,
+ rgba8unorm_srgb = 71,
+ rgba8snorm = 72,
+ rgba8uint = 73,
+ rgba8sint = 74,
+ bgra8unorm = 80,
+ bgra8unorm_srgb = 81,
+ rgb10a2unorm = 90,
+ rgb10a2uint = 91,
+ rg11b10float = 92,
+ rgb9e5float = 93,
+ bgr10a2unorm = 94,
+ bgr10_xr = 554,
+ bgr10_xr_srgb = 555,
+ rg32uint = 103,
+ rg32sint = 104,
+ rg32float = 105,
+ rgba16unorm = 110,
+ rgba16snorm = 112,
+ rgba16uint = 113,
+ rgba16sint = 114,
+ rgba16float = 115,
+ bgra10_xr = 552,
+ bgra10_xr_srgb = 553,
+ rgba32uint = 123,
+ rgba32sint = 124,
+ rgba32float = 125,
+ bc1_rgba = 130,
+ bc1_rgba_srgb = 131,
+ bc2_rgba = 132,
+ bc2_rgba_srgb = 133,
+ bc3_rgba = 134,
+ bc3_rgba_srgb = 135,
+ bc4_runorm = 140,
+ bc4_rsnorm = 141,
+ bc5_rgunorm = 142,
+ bc5_rgsnorm = 143,
+ bc6h_rgbfloat = 150,
+ bc6h_rgbufloat = 151,
+ bc7_rgbaunorm = 152,
+ bc7_rgbaunorm_srgb = 153,
+ pvrtc_rgb_2bpp = 160,
+ pvrtc_rgb_2bpp_srgb = 161,
+ pvrtc_rgb_4bpp = 162,
+ pvrtc_rgb_4bpp_srgb = 163,
+ pvrtc_rgba_2bpp = 164,
+ pvrtc_rgba_2bpp_srgb = 165,
+ pvrtc_rgba_4bpp = 166,
+ pvrtc_rgba_4bpp_srgb = 167,
+ eac_r11unorm = 170,
+ eac_r11snorm = 172,
+ eac_rg11unorm = 174,
+ eac_rg11snorm = 176,
+ eac_rgba8 = 178,
+ eac_rgba8_srgb = 179,
+ etc2_rgb8 = 180,
+ etc2_rgb8_srgb = 181,
+ etc2_rgb8a1 = 182,
+ etc2_rgb8a1_srgb = 183,
+ astc_4x4_srgb = 186,
+ astc_5x4_srgb = 187,
+ astc_5x5_srgb = 188,
+ astc_6x5_srgb = 189,
+ astc_6x6_srgb = 190,
+ astc_8x5_srgb = 192,
+ astc_8x6_srgb = 193,
+ astc_8x8_srgb = 194,
+ astc_10x5_srgb = 195,
+ astc_10x6_srgb = 196,
+ astc_10x8_srgb = 197,
+ astc_10x10_srgb = 198,
+ astc_12x10_srgb = 199,
+ astc_12x12_srgb = 200,
+ astc_4x4_ldr = 204,
+ astc_5x4_ldr = 205,
+ astc_5x5_ldr = 206,
+ astc_6x5_ldr = 207,
+ astc_6x6_ldr = 208,
+ astc_8x5_ldr = 210,
+ astc_8x6_ldr = 211,
+ astc_8x8_ldr = 212,
+ astc_10x5_ldr = 213,
+ astc_10x6_ldr = 214,
+ astc_10x8_ldr = 215,
+ astc_10x10_ldr = 216,
+ astc_12x10_ldr = 217,
+ astc_12x12_ldr = 218,
+ astc_4x4_hdr = 222,
+ astc_5x4_hdr = 223,
+ astc_5x5_hdr = 224,
+ astc_6x5_hdr = 225,
+ astc_6x6_hdr = 226,
+ astc_8x5_hdr = 228,
+ astc_8x6_hdr = 229,
+ astc_8x8_hdr = 230,
+ astc_10x5_hdr = 231,
+ astc_10x6_hdr = 232,
+ astc_10x8_hdr = 233,
+ astc_10x10_hdr = 234,
+ astc_12x10_hdr = 235,
+ astc_12x12_hdr = 236,
+ gbgr422 = 240,
+ bgrg422 = 241,
+ depth16unorm = 250,
+ depth32float = 252,
+ stencil8 = 253,
+ depth24unorm_stencil8 = 255,
+ depth32float_stencil8 = 260,
+ x32_stencil8 = 261,
+ x24_stencil8 = 262,
+};
+
+/// https://developer.apple.com/documentation/metal/mtlpurgeablestate?language=objc
+pub const MTLPurgeableState = enum(c_ulong) {
+ keep_current = 1,
+ non_volatile = 2,
+ @"volatile" = 3,
+ empty = 4,
+};
+
+/// https://developer.apple.com/documentation/metal/mtlsamplerminmagfilter?language=objc
+pub const MTLSamplerMinMagFilter = enum(c_ulong) {
+ nearest = 0,
+ linear = 1,
+};
+
+/// https://developer.apple.com/documentation/metal/mtlsampleraddressmode?language=objc
+pub const MTLSamplerAddressMode = enum(c_ulong) {
+ clamp_to_edge = 0,
+ mirror_clamp_to_edge = 1,
+ repeat = 2,
+ mirror_repeat = 3,
+ clamp_to_zero = 4,
+ clamp_to_border_color = 5,
+};
+
+/// https://developer.apple.com/documentation/metal/mtlblendfactor?language=objc
+pub const MTLBlendFactor = enum(c_ulong) {
+ zero = 0,
+ one = 1,
+ source_color = 2,
+ one_minus_source_color = 3,
+ source_alpha = 4,
+ one_minus_source_alpha = 5,
+ dest_color = 6,
+ one_minus_dest_color = 7,
+ dest_alpha = 8,
+ one_minus_dest_alpha = 9,
+ source_alpha_saturated = 10,
+ blend_color = 11,
+ one_minus_blend_color = 12,
+ blend_alpha = 13,
+ one_minus_blend_alpha = 14,
+ source_1_color = 15,
+ one_minus_source_1_color = 16,
+ source_1_alpha = 17,
+ one_minus_source_1_alpha = 18,
+};
+
+/// https://developer.apple.com/documentation/metal/mtlblendoperation?language=objc
+pub const MTLBlendOperation = enum(c_ulong) {
+ add = 0,
+ subtract = 1,
+ reverse_subtract = 2,
+ min = 3,
+ max = 4,
+};
+
+/// https://developer.apple.com/documentation/metal/mtltextureusage?language=objc
+pub const MTLTextureUsage = packed struct(c_ulong) {
+ /// https://developer.apple.com/documentation/metal/mtltextureusage/shaderread?language=objc
+ shader_read: bool = false, // TextureUsageShaderRead = 1,
+
+ /// https://developer.apple.com/documentation/metal/mtltextureusage/shaderwrite?language=objc
+ shader_write: bool = false, // TextureUsageShaderWrite = 2,
+
+ /// https://developer.apple.com/documentation/metal/mtltextureusage/rendertarget?language=objc
+ render_target: bool = false, // TextureUsageRenderTarget = 4,
+
+ _reserved: u1 = 0, // The enum skips from 4 to 16, 8 has no documented use.
+
+ /// https://developer.apple.com/documentation/metal/mtltextureusage/pixelformatview?language=objc
+ pixel_format_view: bool = false, // TextureUsagePixelFormatView = 16,
+
+ /// https://developer.apple.com/documentation/metal/mtltextureusage/shaderatomic?language=objc
+ shader_atomic: bool = false, // TextureUsageShaderAtomic = 32,
+
+ __reserved: @Type(.{ .int = .{
+ .signedness = .unsigned,
+ .bits = @bitSizeOf(c_ulong) - 6,
+ } }) = 0,
+
+ /// https://developer.apple.com/documentation/metal/mtltextureusage/unknown?language=objc
+ const unknown: MTLTextureUsage = @bitCast(0); // TextureUsageUnknown = 0,
+};
+
+/// https://developer.apple.com/documentation/metal/mtlbarrierscope?language=objc
+pub const MTLBarrierScope = enum(c_ulong) {
+ buffers = 1,
+ textures = 2,
+ render_targets = 4,
+};
+
+/// https://developer.apple.com/documentation/metal/mtlrenderstages?language=objc
+pub const MTLRenderStage = enum(c_ulong) {
+ vertex = 1,
+ fragment = 2,
+ tile = 4,
+ object = 8,
+ mesh = 16,
+};
+
+/// https://developer.apple.com/documentation/metal/mtlgpufamily?language=objc
+pub const MTLGPUFamily = enum(c_long) {
+ apple1 = 1001,
+ apple2 = 1002,
+ apple3 = 1003,
+ apple4 = 1004,
+ apple5 = 1005,
+ apple6 = 1006,
+ apple7 = 1007,
+ apple8 = 1008,
+ apple9 = 1009,
+ apple10 = 1010,
+
+ common1 = 3001,
+ common2 = 3002,
+ common3 = 3003,
+
+ metal3 = 5001,
+ metal4 = 5002,
+};
+
+pub const MTLClearColor = extern struct {
+ red: f64,
+ green: f64,
+ blue: f64,
+ alpha: f64,
+};
+
+pub const MTLViewport = extern struct {
+ x: f64,
+ y: f64,
+ width: f64,
+ height: f64,
+ znear: f64,
+ zfar: f64,
+};
+
+pub const MTLRegion = extern struct {
+ origin: MTLOrigin,
+ size: MTLSize,
+};
+
+pub const MTLOrigin = extern struct {
+ x: c_ulong,
+ y: c_ulong,
+ z: c_ulong,
+};
+
+pub const MTLSize = extern struct {
+ width: c_ulong,
+ height: c_ulong,
+ depth: c_ulong,
+};
+
+/// https://developer.apple.com/documentation/metal/1433367-mtlcopyalldevices
+pub extern "c" fn MTLCopyAllDevices() ?*anyopaque;
+
+/// https://developer.apple.com/documentation/metal/1433401-mtlcreatesystemdefaultdevice
+pub extern "c" fn MTLCreateSystemDefaultDevice() ?*anyopaque;
diff --git a/vendor/libghostty-vt/src/renderer/metal/buffer.zig b/vendor/libghostty-vt/src/renderer/metal/buffer.zig
new file mode 100644
index 00000000..f91f89e9
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/metal/buffer.zig
@@ -0,0 +1,185 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const objc = @import("objc");
+const macos = @import("macos");
+
+const mtl = @import("api.zig");
+const Metal = @import("../Metal.zig");
+
+const log = std.log.scoped(.metal);
+
+/// Options for initializing a buffer.
+pub const Options = struct {
+ /// MTLDevice
+ device: objc.Object,
+ resource_options: mtl.MTLResourceOptions,
+};
+
+/// Metal data storage for a certain set of equal types. This is usually
+/// used for vertex buffers, etc. This helpful wrapper makes it easy to
+/// prealloc, shrink, grow, sync, buffers with Metal.
+pub fn Buffer(comptime T: type) type {
+ return struct {
+ const Self = @This();
+
+ /// The options this buffer was initialized with.
+ opts: Options,
+
+ /// The underlying MTLBuffer object.
+ buffer: objc.Object,
+
+ /// The allocated length of the buffer.
+ /// Note that this is the number
+ /// of `T`s not the size in bytes.
+ len: usize,
+
+ /// Initialize a buffer with the given length pre-allocated.
+ pub fn init(opts: Options, len: usize) !Self {
+ const buffer = opts.device.msgSend(
+ objc.Object,
+ objc.sel("newBufferWithLength:options:"),
+ .{
+ @as(c_ulong, @intCast(len * @sizeOf(T))),
+ opts.resource_options,
+ },
+ );
+
+ return .{ .buffer = buffer, .opts = opts, .len = len };
+ }
+
+ /// Init the buffer filled with the given data.
+ pub fn initFill(opts: Options, data: []const T) !Self {
+ const buffer = opts.device.msgSend(
+ objc.Object,
+ objc.sel("newBufferWithBytes:length:options:"),
+ .{
+ @as(*const anyopaque, @ptrCast(data.ptr)),
+ @as(c_ulong, @intCast(data.len * @sizeOf(T))),
+ opts.resource_options,
+ },
+ );
+
+ return .{ .buffer = buffer, .opts = opts, .len = data.len };
+ }
+
+ pub fn deinit(self: *const Self) void {
+ self.buffer.msgSend(void, objc.sel("release"), .{});
+ }
+
+ /// Sync new contents to the buffer. The data is expected to be the
+ /// complete contents of the buffer. If the amount of data is larger
+ /// than the buffer length, the buffer will be reallocated.
+ ///
+ /// If the amount of data is smaller than the buffer length, the
+ /// remaining data in the buffer is left untouched.
+ pub fn sync(self: *Self, data: []const T) !void {
+ // If we need more bytes than our buffer has, we need to reallocate.
+ const req_bytes = data.len * @sizeOf(T);
+ const avail_bytes = self.buffer.getProperty(c_ulong, "length");
+ if (req_bytes > avail_bytes) {
+ // Deallocate previous buffer
+ self.buffer.msgSend(void, objc.sel("release"), .{});
+
+ // Allocate a new buffer with enough to hold double what we require.
+ const size = req_bytes * 2;
+ self.buffer = self.opts.device.msgSend(
+ objc.Object,
+ objc.sel("newBufferWithLength:options:"),
+ .{
+ @as(c_ulong, @intCast(size * @sizeOf(T))),
+ self.opts.resource_options,
+ },
+ );
+ }
+
+ // We can fit within the buffer so we can just replace bytes.
+ const dst = dst: {
+ const ptr = self.buffer.msgSend(?[*]u8, objc.sel("contents"), .{}) orelse {
+ log.warn("buffer contents ptr is null", .{});
+ return error.MetalFailed;
+ };
+
+ break :dst ptr[0..req_bytes];
+ };
+
+ const src = src: {
+ const ptr = @as([*]const u8, @ptrCast(data.ptr));
+ break :src ptr[0..req_bytes];
+ };
+
+ @memcpy(dst, src);
+
+ // If we're using the managed resource storage mode, then
+ // we need to signal Metal to synchronize the buffer data.
+ //
+ // Ref: https://developer.apple.com/documentation/metal/synchronizing-a-managed-resource-in-macos?language=objc
+ if (self.opts.resource_options.storage_mode == .managed) {
+ self.buffer.msgSend(
+ void,
+ "didModifyRange:",
+ .{macos.foundation.Range.init(0, req_bytes)},
+ );
+ }
+ }
+
+ /// Like Buffer.sync but takes data from an array of ArrayLists,
+ /// rather than a single array. Returns the number of items synced.
+ pub fn syncFromArrayLists(self: *Self, lists: []const std.ArrayListUnmanaged(T)) !usize {
+ var total_len: usize = 0;
+ for (lists) |list| {
+ total_len += list.items.len;
+ }
+
+ // If we need more bytes than our buffer has, we need to reallocate.
+ const req_bytes = total_len * @sizeOf(T);
+ const avail_bytes = self.buffer.getProperty(c_ulong, "length");
+ if (req_bytes > avail_bytes) {
+ // Deallocate previous buffer
+ self.buffer.msgSend(void, objc.sel("release"), .{});
+
+ // Allocate a new buffer with enough to hold double what we require.
+ const size = req_bytes * 2;
+ self.buffer = self.opts.device.msgSend(
+ objc.Object,
+ objc.sel("newBufferWithLength:options:"),
+ .{
+ @as(c_ulong, @intCast(size * @sizeOf(T))),
+ self.opts.resource_options,
+ },
+ );
+ }
+
+ // We can fit within the buffer so we can just replace bytes.
+ const dst = dst: {
+ const ptr = self.buffer.msgSend(?[*]u8, objc.sel("contents"), .{}) orelse {
+ log.warn("buffer contents ptr is null", .{});
+ return error.MetalFailed;
+ };
+
+ break :dst ptr[0..req_bytes];
+ };
+
+ var i: usize = 0;
+
+ for (lists) |list| {
+ const ptr = @as([*]const u8, @ptrCast(list.items.ptr));
+ @memcpy(dst[i..][0 .. list.items.len * @sizeOf(T)], ptr);
+ i += list.items.len * @sizeOf(T);
+ }
+
+ // If we're using the managed resource storage mode, then
+ // we need to signal Metal to synchronize the buffer data.
+ //
+ // Ref: https://developer.apple.com/documentation/metal/synchronizing-a-managed-resource-in-macos?language=objc
+ if (self.opts.resource_options.storage_mode == .managed) {
+ self.buffer.msgSend(
+ void,
+ "didModifyRange:",
+ .{macos.foundation.Range.init(0, req_bytes)},
+ );
+ }
+
+ return total_len;
+ }
+ };
+}
diff --git a/vendor/libghostty-vt/src/renderer/metal/shaders.zig b/vendor/libghostty-vt/src/renderer/metal/shaders.zig
new file mode 100644
index 00000000..0be02357
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/metal/shaders.zig
@@ -0,0 +1,454 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const macos = @import("macos");
+const objc = @import("objc");
+const math = @import("../../math.zig");
+
+const mtl = @import("api.zig");
+const Pipeline = @import("Pipeline.zig");
+
+const log = std.log.scoped(.metal);
+
+const pipeline_descs: []const struct { [:0]const u8, PipelineDescription } =
+ &.{
+ .{ "bg_color", .{
+ .vertex_fn = "full_screen_vertex",
+ .fragment_fn = "bg_color_fragment",
+ .blending_enabled = false,
+ } },
+ .{ "cell_bg", .{
+ .vertex_fn = "full_screen_vertex",
+ .fragment_fn = "cell_bg_fragment",
+ .blending_enabled = true,
+ } },
+ .{ "cell_text", .{
+ .vertex_attributes = CellText,
+ .vertex_fn = "cell_text_vertex",
+ .fragment_fn = "cell_text_fragment",
+ .step_fn = .per_instance,
+ .blending_enabled = true,
+ } },
+ .{ "image", .{
+ .vertex_attributes = Image,
+ .vertex_fn = "image_vertex",
+ .fragment_fn = "image_fragment",
+ .step_fn = .per_instance,
+ .blending_enabled = true,
+ } },
+ .{ "bg_image", .{
+ .vertex_attributes = BgImage,
+ .vertex_fn = "bg_image_vertex",
+ .fragment_fn = "bg_image_fragment",
+ .step_fn = .per_instance,
+ .blending_enabled = true,
+ } },
+ };
+
+/// All the comptime-known info about a pipeline, so that
+/// we can define them ahead-of-time in an ergonomic way.
+const PipelineDescription = struct {
+ vertex_attributes: ?type = null,
+ vertex_fn: []const u8,
+ fragment_fn: []const u8,
+ step_fn: mtl.MTLVertexStepFunction = .per_vertex,
+ blending_enabled: bool,
+
+ fn initPipeline(
+ self: PipelineDescription,
+ device: objc.Object,
+ library: objc.Object,
+ pixel_format: mtl.MTLPixelFormat,
+ ) !Pipeline {
+ return try .init(self.vertex_attributes, .{
+ .device = device,
+ .vertex_fn = self.vertex_fn,
+ .fragment_fn = self.fragment_fn,
+ .vertex_library = library,
+ .fragment_library = library,
+ .step_fn = self.step_fn,
+ .attachments = &.{.{
+ .pixel_format = pixel_format,
+ .blending_enabled = self.blending_enabled,
+ }},
+ });
+ }
+};
+
+/// We create a type for the pipeline collection based on our desc array.
+const PipelineCollection = t: {
+ var fields: [pipeline_descs.len]std.builtin.Type.StructField = undefined;
+ for (pipeline_descs, 0..) |pipeline, i| {
+ fields[i] = .{
+ .name = pipeline[0],
+ .type = Pipeline,
+ .default_value_ptr = null,
+ .is_comptime = false,
+ .alignment = @alignOf(Pipeline),
+ };
+ }
+ break :t @Type(.{ .@"struct" = .{
+ .layout = .auto,
+ .fields = &fields,
+ .decls = &.{},
+ .is_tuple = false,
+ } });
+};
+
+/// This contains the state for the shaders used by the Metal renderer.
+pub const Shaders = struct {
+ library: objc.Object,
+
+ /// Collection of available render pipelines.
+ pipelines: PipelineCollection,
+
+ /// Custom shaders to run against the final drawable texture. This
+ /// can be used to apply a lot of effects. Each shader is run in sequence
+ /// against the output of the previous shader.
+ post_pipelines: []const Pipeline,
+
+ /// Set to true when deinited, if you try to deinit a defunct set
+ /// of shaders it will just be ignored, to prevent double-free.
+ defunct: bool = false,
+
+ /// Initialize our shader set.
+ ///
+ /// "post_shaders" is an optional list of postprocess shaders to run
+ /// against the final drawable texture. This is an array of shader source
+ /// code, not file paths.
+ pub fn init(
+ alloc: Allocator,
+ device: objc.Object,
+ post_shaders: []const [:0]const u8,
+ pixel_format: mtl.MTLPixelFormat,
+ ) !Shaders {
+ const library = try initLibrary(device);
+ errdefer library.msgSend(void, objc.sel("release"), .{});
+
+ var pipelines: PipelineCollection = undefined;
+
+ var initialized_pipelines: usize = 0;
+
+ errdefer inline for (pipeline_descs, 0..) |pipeline, i| {
+ if (i < initialized_pipelines) {
+ @field(pipelines, pipeline[0]).deinit();
+ }
+ };
+
+ inline for (pipeline_descs) |pipeline| {
+ @field(pipelines, pipeline[0]) = try pipeline[1].initPipeline(
+ device,
+ library,
+ pixel_format,
+ );
+ initialized_pipelines += 1;
+ }
+
+ const post_pipelines: []const Pipeline = initPostPipelines(
+ alloc,
+ device,
+ library,
+ post_shaders,
+ pixel_format,
+ ) catch |err| err: {
+ // If an error happens while building postprocess shaders we
+ // want to just not use any postprocess shaders since we don't
+ // want to block Ghostty from working.
+ log.warn("error initializing postprocess shaders err={}", .{err});
+ break :err &.{};
+ };
+ errdefer if (post_pipelines.len > 0) {
+ for (post_pipelines) |pipeline| pipeline.deinit();
+ alloc.free(post_pipelines);
+ };
+
+ return .{
+ .library = library,
+ .pipelines = pipelines,
+ .post_pipelines = post_pipelines,
+ };
+ }
+
+ pub fn deinit(self: *Shaders, alloc: Allocator) void {
+ if (self.defunct) return;
+ self.defunct = true;
+
+ // Release our primary shaders
+ inline for (pipeline_descs) |pipeline| {
+ @field(self.pipelines, pipeline[0]).deinit();
+ }
+ self.library.msgSend(void, objc.sel("release"), .{});
+
+ // Release our postprocess shaders
+ if (self.post_pipelines.len > 0) {
+ for (self.post_pipelines) |pipeline| {
+ pipeline.deinit();
+ }
+ alloc.free(self.post_pipelines);
+ }
+ }
+};
+
+/// The uniforms that are passed to our shaders.
+pub const Uniforms = extern struct {
+ // Note: all of the explicit alignments are copied from the
+ // MSL developer reference just so that we can be sure that we got
+ // it all exactly right.
+
+ /// The projection matrix for turning world coordinates to normalized.
+ /// This is calculated based on the size of the screen.
+ projection_matrix: math.Mat align(16),
+
+ /// Size of the screen (render target) in pixels.
+ screen_size: [2]f32 align(8),
+
+ /// Size of a single cell in pixels, unscaled.
+ cell_size: [2]f32 align(8),
+
+ /// Size of the grid in columns and rows.
+ grid_size: [2]u16 align(4),
+
+ /// The padding around the terminal grid in pixels. In order:
+ /// top, right, bottom, left.
+ grid_padding: [4]f32 align(16),
+
+ /// Bit mask defining which directions to
+ /// extend cell colors in to the padding.
+ /// Order, LSB first: left, right, up, down
+ padding_extend: PaddingExtend align(1),
+
+ /// The minimum contrast ratio for text. The contrast ratio is calculated
+ /// according to the WCAG 2.0 spec.
+ min_contrast: f32 align(4),
+
+ /// The cursor position and color.
+ cursor_pos: [2]u16 align(4),
+ cursor_color: [4]u8 align(4),
+
+ /// The background color for the whole surface.
+ bg_color: [4]u8 align(4),
+
+ /// Various booleans.
+ ///
+ /// TODO: Maybe put these in a packed struct, like for OpenGL.
+ bools: extern struct {
+ /// Whether the cursor is 2 cells wide.
+ cursor_wide: bool align(1),
+
+ /// Indicates that colors provided to the shader are already in
+ /// the P3 color space, so they don't need to be converted from
+ /// sRGB.
+ use_display_p3: bool align(1),
+
+ /// Indicates that the color attachments for the shaders have
+ /// an `*_srgb` pixel format, which means the shaders need to
+ /// output linear RGB colors rather than gamma encoded colors,
+ /// since blending will be performed in linear space and then
+ /// Metal itself will re-encode the colors for storage.
+ use_linear_blending: bool align(1),
+
+ /// Enables a weight correction step that makes text rendered
+ /// with linear alpha blending have a similar apparent weight
+ /// (thickness) to gamma-incorrect blending.
+ use_linear_correction: bool align(1) = false,
+ },
+
+ const PaddingExtend = packed struct(u8) {
+ left: bool = false,
+ right: bool = false,
+ up: bool = false,
+ down: bool = false,
+ _padding: u4 = 0,
+ };
+};
+
+/// This is a single parameter for the terminal cell shader.
+pub const CellText = extern struct {
+ glyph_pos: [2]u32 align(8) = .{ 0, 0 },
+ glyph_size: [2]u32 align(8) = .{ 0, 0 },
+ bearings: [2]i16 align(4) = .{ 0, 0 },
+ grid_pos: [2]u16 align(4),
+ color: [4]u8 align(4),
+ atlas: Atlas align(1),
+ bools: packed struct(u8) {
+ no_min_contrast: bool = false,
+ is_cursor_glyph: bool = false,
+ _padding: u6 = 0,
+ } align(1) = .{},
+
+ pub const Atlas = enum(u8) {
+ grayscale = 0,
+ color = 1,
+ };
+
+ test {
+ // Minimizing the size of this struct is important,
+ // so we test it in order to be aware of any changes.
+ try std.testing.expectEqual(32, @sizeOf(CellText));
+ }
+};
+
+/// This is a single parameter for the cell bg shader.
+pub const CellBg = [4]u8;
+
+/// Single parameter for the image shader. See shader for field details.
+pub const Image = extern struct {
+ grid_pos: [2]f32,
+ cell_offset: [2]f32,
+ source_rect: [4]f32,
+ dest_size: [2]f32,
+};
+
+/// Single parameter for the bg image shader.
+pub const BgImage = extern struct {
+ opacity: f32 align(4),
+ info: Info align(1),
+
+ pub const Info = packed struct(u8) {
+ position: Position,
+ fit: Fit,
+ repeat: bool,
+ _padding: u1 = 0,
+
+ pub const Position = enum(u4) {
+ tl = 0,
+ tc = 1,
+ tr = 2,
+ ml = 3,
+ mc = 4,
+ mr = 5,
+ bl = 6,
+ bc = 7,
+ br = 8,
+ };
+
+ pub const Fit = enum(u2) {
+ contain = 0,
+ cover = 1,
+ stretch = 2,
+ none = 3,
+ };
+ };
+};
+
+/// Initialize the MTLLibrary. A MTLLibrary is a collection of shaders.
+fn initLibrary(device: objc.Object) !objc.Object {
+ const start = try std.time.Instant.now();
+
+ const data = try macos.dispatch.Data.create(
+ @embedFile("ghostty_metallib"),
+ macos.dispatch.queue.getMain(),
+ macos.dispatch.Data.DESTRUCTOR_DEFAULT,
+ );
+ defer data.release();
+
+ var err: ?*anyopaque = null;
+ const library = device.msgSend(
+ objc.Object,
+ objc.sel("newLibraryWithData:error:"),
+ .{
+ data,
+ &err,
+ },
+ );
+ try checkError(err);
+
+ const end = try std.time.Instant.now();
+ log.debug("shader library loaded time={}us", .{end.since(start) / std.time.ns_per_us});
+
+ return library;
+}
+
+/// Initialize our custom shader pipelines.
+///
+/// The shaders argument is a set of shader source code, not file paths.
+fn initPostPipelines(
+ alloc: Allocator,
+ device: objc.Object,
+ library: objc.Object,
+ shaders: []const [:0]const u8,
+ pixel_format: mtl.MTLPixelFormat,
+) ![]const Pipeline {
+ // If we have no shaders, do nothing.
+ if (shaders.len == 0) return &.{};
+
+ // Keeps track of how many shaders we successfully wrote.
+ var i: usize = 0;
+
+ // Initialize our result set. If any error happens, we undo everything.
+ var pipelines = try alloc.alloc(Pipeline, shaders.len);
+ errdefer {
+ for (pipelines[0..i]) |pipeline| {
+ pipeline.deinit();
+ }
+ alloc.free(pipelines);
+ }
+
+ // Build each shader. Note we don't use "0.." to build our index
+ // because we need to keep track of our length to clean up above.
+ for (shaders) |source| {
+ pipelines[i] = try initPostPipeline(
+ device,
+ library,
+ source,
+ pixel_format,
+ );
+ i += 1;
+ }
+
+ return pipelines;
+}
+
+/// Initialize a single custom shader pipeline from shader source.
+fn initPostPipeline(
+ device: objc.Object,
+ library: objc.Object,
+ data: [:0]const u8,
+ pixel_format: mtl.MTLPixelFormat,
+) !Pipeline {
+ // Create our library which has the shader source
+ const post_library = library: {
+ const source = try macos.foundation.String.createWithBytes(
+ data,
+ .utf8,
+ false,
+ );
+ defer source.release();
+
+ var err: ?*anyopaque = null;
+ const post_library = device.msgSend(
+ objc.Object,
+ objc.sel("newLibraryWithSource:options:error:"),
+ .{ source, @as(?*anyopaque, null), &err },
+ );
+ try checkError(err);
+ errdefer post_library.msgSend(void, objc.sel("release"), .{});
+
+ break :library post_library;
+ };
+ defer post_library.msgSend(void, objc.sel("release"), .{});
+
+ return try Pipeline.init(null, .{
+ .device = device,
+ .vertex_fn = "full_screen_vertex",
+ .fragment_fn = "main0",
+ .vertex_library = library,
+ .fragment_library = post_library,
+ .attachments = &.{
+ .{
+ .pixel_format = pixel_format,
+ .blending_enabled = false,
+ },
+ },
+ });
+}
+
+fn checkError(err_: ?*anyopaque) !void {
+ const nserr = objc.Object.fromId(err_ orelse return);
+ const str = @as(
+ *macos.foundation.String,
+ @ptrCast(nserr.getProperty(?*anyopaque, "localizedDescription").?),
+ );
+
+ log.err("metal error={s}", .{str.cstringPtr(.ascii).?});
+ return error.MetalFailed;
+}
diff --git a/vendor/libghostty-vt/src/renderer/opengl/Frame.zig b/vendor/libghostty-vt/src/renderer/opengl/Frame.zig
new file mode 100644
index 00000000..289413b0
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/opengl/Frame.zig
@@ -0,0 +1,71 @@
+//! Wrapper for handling render passes.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const gl = @import("opengl");
+
+const Renderer = @import("../generic.zig").Renderer(OpenGL);
+const OpenGL = @import("../OpenGL.zig");
+const Target = @import("Target.zig");
+const RenderPass = @import("RenderPass.zig");
+
+const Health = @import("../../renderer.zig").Health;
+
+const log = std.log.scoped(.opengl);
+
+/// Options for beginning a frame.
+pub const Options = struct {};
+
+renderer: *Renderer,
+target: *Target,
+
+/// Begin encoding a frame.
+pub fn begin(
+ opts: Options,
+ /// Once the frame has been completed, the `frameCompleted` method
+ /// on the renderer is called with the health status of the frame.
+ renderer: *Renderer,
+ /// The target is presented via the provided renderer's API when completed.
+ target: *Target,
+) !Self {
+ _ = opts;
+
+ return .{
+ .renderer = renderer,
+ .target = target,
+ };
+}
+
+/// Add a render pass to this frame with the provided attachments.
+/// Returns a RenderPass which allows render steps to be added.
+pub inline fn renderPass(
+ self: *const Self,
+ attachments: []const RenderPass.Options.Attachment,
+) RenderPass {
+ _ = self;
+ return RenderPass.begin(.{ .attachments = attachments });
+}
+
+/// Complete this frame and present the target.
+///
+/// If `sync` is true, this will block until the frame is presented.
+///
+/// NOTE: For OpenGL, `sync` is ignored and we always block.
+pub fn complete(self: *const Self, sync: bool) void {
+ _ = sync;
+ gl.finish();
+
+ // If there are any GL errors, consider the frame unhealthy.
+ const health: Health = if (gl.errors.getError()) .healthy else |_| .unhealthy;
+
+ // If the frame is healthy, present it.
+ if (health == .healthy) {
+ self.renderer.api.present(self.target.*) catch |err| {
+ log.err("Failed to present render target: err={}", .{err});
+ };
+ }
+
+ // Report the health to the renderer.
+ self.renderer.frameCompleted(health);
+}
diff --git a/vendor/libghostty-vt/src/renderer/opengl/Pipeline.zig b/vendor/libghostty-vt/src/renderer/opengl/Pipeline.zig
new file mode 100644
index 00000000..2469f45b
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/opengl/Pipeline.zig
@@ -0,0 +1,164 @@
+//! Wrapper for handling render pipelines.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const gl = @import("opengl");
+
+const log = std.log.scoped(.opengl);
+
+/// Options for initializing a render pipeline.
+pub const Options = struct {
+ /// GLSL source of the vertex function
+ vertex_fn: [:0]const u8,
+ /// GLSL source of the fragment function
+ fragment_fn: [:0]const u8,
+
+ /// Vertex step function
+ step_fn: StepFunction = .per_vertex,
+
+ /// Whether to enable blending.
+ blending_enabled: bool = true,
+
+ pub const StepFunction = enum {
+ constant,
+ per_vertex,
+ per_instance,
+ };
+};
+
+program: gl.Program,
+
+fbo: gl.Framebuffer,
+
+vao: gl.VertexArray,
+
+stride: usize,
+
+blending_enabled: bool,
+
+pub fn init(comptime VertexAttributes: ?type, opts: Options) !Self {
+ // Load and compile our shaders.
+ const program = try gl.Program.createVF(
+ opts.vertex_fn,
+ opts.fragment_fn,
+ );
+ errdefer program.destroy();
+
+ const pbind = try program.use();
+ defer pbind.unbind();
+
+ const fbo = try gl.Framebuffer.create();
+ errdefer fbo.destroy();
+ const fbobind = try fbo.bind(.framebuffer);
+ defer fbobind.unbind();
+
+ const vao = try gl.VertexArray.create();
+ errdefer vao.destroy();
+ const vaobind = try vao.bind();
+ defer vaobind.unbind();
+
+ if (VertexAttributes) |VA| try autoAttribute(VA, vaobind, opts.step_fn);
+
+ return .{
+ .program = program,
+ .fbo = fbo,
+ .vao = vao,
+ .stride = if (VertexAttributes) |VA| @sizeOf(VA) else 0,
+ .blending_enabled = opts.blending_enabled,
+ };
+}
+
+pub fn deinit(self: *const Self) void {
+ self.program.destroy();
+}
+
+fn autoAttribute(
+ T: type,
+ vaobind: gl.VertexArray.Binding,
+ step_fn: Options.StepFunction,
+) !void {
+ const divisor: gl.c.GLuint = switch (step_fn) {
+ .per_vertex => 0,
+ .per_instance => 1,
+ .constant => std.math.maxInt(gl.c.GLuint),
+ };
+
+ inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| {
+ try vaobind.enableAttribArray(i);
+ try vaobind.attributeBinding(i, 0);
+ try vaobind.bindingDivisor(i, divisor);
+
+ const offset = @offsetOf(T, field.name);
+
+ const FT = switch (@typeInfo(field.type)) {
+ .@"struct" => |s| s.backing_integer.?,
+ .@"enum" => |e| e.tag_type,
+ else => field.type,
+ };
+
+ const size, const IT = switch (@typeInfo(FT)) {
+ .array => |a| .{ a.len, a.child },
+ else => .{ 1, FT },
+ };
+
+ try switch (IT) {
+ u8 => vaobind.attributeIFormat(
+ i,
+ size,
+ gl.c.GL_UNSIGNED_BYTE,
+ offset,
+ ),
+ u16 => vaobind.attributeIFormat(
+ i,
+ size,
+ gl.c.GL_UNSIGNED_SHORT,
+ offset,
+ ),
+ u32 => vaobind.attributeIFormat(
+ i,
+ size,
+ gl.c.GL_UNSIGNED_INT,
+ offset,
+ ),
+ i8 => vaobind.attributeIFormat(
+ i,
+ size,
+ gl.c.GL_BYTE,
+ offset,
+ ),
+ i16 => vaobind.attributeIFormat(
+ i,
+ size,
+ gl.c.GL_SHORT,
+ offset,
+ ),
+ i32 => vaobind.attributeIFormat(
+ i,
+ size,
+ gl.c.GL_INT,
+ offset,
+ ),
+ f16 => vaobind.attributeFormat(
+ i,
+ size,
+ gl.c.GL_HALF_FLOAT,
+ false,
+ offset,
+ ),
+ f32 => vaobind.attributeFormat(
+ i,
+ size,
+ gl.c.GL_FLOAT,
+ false,
+ offset,
+ ),
+ f64 => vaobind.attributeLFormat(
+ i,
+ size,
+ offset,
+ ),
+ else => unreachable,
+ };
+ }
+}
diff --git a/vendor/libghostty-vt/src/renderer/opengl/RenderPass.zig b/vendor/libghostty-vt/src/renderer/opengl/RenderPass.zig
new file mode 100644
index 00000000..18066494
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/opengl/RenderPass.zig
@@ -0,0 +1,144 @@
+//! Wrapper for handling render passes.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const gl = @import("opengl");
+
+const Sampler = @import("Sampler.zig");
+const Target = @import("Target.zig");
+const Texture = @import("Texture.zig");
+const Pipeline = @import("Pipeline.zig");
+const Buffer = @import("buffer.zig").Buffer;
+
+/// Options for beginning a render pass.
+pub const Options = struct {
+ /// Color attachments for this render pass.
+ attachments: []const Attachment,
+
+ /// Describes a color attachment.
+ pub const Attachment = struct {
+ target: union(enum) {
+ texture: Texture,
+ target: Target,
+ },
+ clear_color: ?[4]f32 = null,
+ };
+};
+
+/// Describes a step in a render pass.
+pub const Step = struct {
+ pipeline: Pipeline,
+ uniforms: ?gl.Buffer = null,
+ buffers: []const ?gl.Buffer = &.{},
+ textures: []const ?Texture = &.{},
+ samplers: []const ?Sampler = &.{},
+ draw: Draw,
+
+ /// Describes the draw call for this step.
+ pub const Draw = struct {
+ type: gl.Primitive,
+ vertex_count: usize,
+ instance_count: usize = 1,
+ };
+};
+
+attachments: []const Options.Attachment,
+
+step_number: usize = 0,
+
+/// Begin a render pass.
+pub fn begin(
+ opts: Options,
+) Self {
+ return .{
+ .attachments = opts.attachments,
+ };
+}
+
+/// Add a step to this render pass.
+///
+/// TODO: Errors are silently ignored in this function, maybe they shouldn't be?
+pub fn step(self: *Self, s: Step) void {
+ if (s.draw.instance_count == 0) return;
+
+ const pbind = s.pipeline.program.use() catch return;
+ defer pbind.unbind();
+
+ const vaobind = s.pipeline.vao.bind() catch return;
+ defer vaobind.unbind();
+
+ const fbobind = switch (self.attachments[0].target) {
+ .target => |t| t.framebuffer.bind(.framebuffer) catch return,
+ .texture => |t| bind: {
+ const fbobind = s.pipeline.fbo.bind(.framebuffer) catch return;
+ fbobind.texture2D(.color0, t.target, t.texture, 0) catch {
+ fbobind.unbind();
+ return;
+ };
+ break :bind fbobind;
+ },
+ };
+ defer fbobind.unbind();
+
+ defer self.step_number += 1;
+
+ // If we have a clear color and this is the
+ // first step in the pass, go ahead and clear.
+ if (self.step_number == 0) if (self.attachments[0].clear_color) |c| {
+ gl.clearColor(c[0], c[1], c[2], c[3]);
+ gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
+ };
+
+ // Bind the uniform buffer we bind at index 1 to align with Metal.
+ if (s.uniforms) |ubo| {
+ _ = ubo.bindBase(.uniform, 1) catch return;
+ }
+
+ // Bind relevant texture units.
+ for (s.textures, 0..) |t, i| if (t) |tex| {
+ gl.Texture.active(@intCast(i)) catch return;
+ _ = tex.texture.bind(tex.target) catch return;
+ };
+
+ // Bind relevant samplers.
+ for (s.samplers, 0..) |s_, i| if (s_) |sampler| {
+ _ = sampler.sampler.bind(@intCast(i)) catch return;
+ };
+
+ // Bind 0th buffer as the vertex buffer,
+ // and bind the rest as storage buffers.
+ if (s.buffers.len > 0) {
+ if (s.buffers[0]) |vbo| vaobind.bindVertexBuffer(
+ 0,
+ vbo.id,
+ 0,
+ @intCast(s.pipeline.stride),
+ ) catch return;
+
+ for (s.buffers[1..], 1..) |b, i| if (b) |buf| {
+ _ = buf.bindBase(.storage, @intCast(i)) catch return;
+ };
+ }
+
+ if (s.pipeline.blending_enabled) {
+ gl.enable(gl.c.GL_BLEND) catch return;
+ gl.blendFunc(gl.c.GL_ONE, gl.c.GL_ONE_MINUS_SRC_ALPHA) catch return;
+ } else {
+ gl.disable(gl.c.GL_BLEND) catch return;
+ }
+
+ gl.drawArraysInstanced(
+ s.draw.type,
+ 0,
+ @intCast(s.draw.vertex_count),
+ @intCast(s.draw.instance_count),
+ ) catch return;
+}
+
+/// Complete this render pass.
+/// This struct can no longer be used after calling this.
+pub fn complete(self: *const Self) void {
+ _ = self;
+ gl.flush();
+}
diff --git a/vendor/libghostty-vt/src/renderer/opengl/Sampler.zig b/vendor/libghostty-vt/src/renderer/opengl/Sampler.zig
new file mode 100644
index 00000000..f4013c68
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/opengl/Sampler.zig
@@ -0,0 +1,45 @@
+//! Wrapper for handling samplers.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const gl = @import("opengl");
+
+const OpenGL = @import("../OpenGL.zig");
+
+const log = std.log.scoped(.opengl);
+
+/// Options for initializing a sampler.
+pub const Options = struct {
+ min_filter: gl.Texture.MinFilter,
+ mag_filter: gl.Texture.MagFilter,
+ wrap_s: gl.Texture.Wrap,
+ wrap_t: gl.Texture.Wrap,
+};
+
+sampler: gl.Sampler,
+
+pub const Error = error{
+ /// An OpenGL API call failed.
+ OpenGLFailed,
+};
+
+/// Initialize a sampler
+pub fn init(
+ opts: Options,
+) Error!Self {
+ const sampler = gl.Sampler.create() catch return error.OpenGLFailed;
+ errdefer sampler.destroy();
+ sampler.parameter(.WrapS, @intFromEnum(opts.wrap_s)) catch return error.OpenGLFailed;
+ sampler.parameter(.WrapT, @intFromEnum(opts.wrap_t)) catch return error.OpenGLFailed;
+ sampler.parameter(.MinFilter, @intFromEnum(opts.min_filter)) catch return error.OpenGLFailed;
+ sampler.parameter(.MagFilter, @intFromEnum(opts.mag_filter)) catch return error.OpenGLFailed;
+
+ return .{
+ .sampler = sampler,
+ };
+}
+
+pub fn deinit(self: Self) void {
+ self.sampler.destroy();
+}
diff --git a/vendor/libghostty-vt/src/renderer/opengl/Target.zig b/vendor/libghostty-vt/src/renderer/opengl/Target.zig
new file mode 100644
index 00000000..5c6d818f
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/opengl/Target.zig
@@ -0,0 +1,60 @@
+//! Represents a render target.
+//!
+//! In this case, an OpenGL renderbuffer-backed framebuffer.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const gl = @import("opengl");
+
+const log = std.log.scoped(.opengl);
+
+/// Options for initializing a Target
+pub const Options = struct {
+ /// Desired width
+ width: usize,
+ /// Desired height
+ height: usize,
+
+ /// Internal format for the renderbuffer.
+ internal_format: gl.Texture.InternalFormat,
+};
+
+/// The underlying `gl.Framebuffer` instance.
+framebuffer: gl.Framebuffer,
+
+/// The underlying `gl.Renderbuffer` instance.
+renderbuffer: gl.Renderbuffer,
+
+/// Current width of this target.
+width: usize,
+/// Current height of this target.
+height: usize,
+
+pub fn init(opts: Options) !Self {
+ const rbo = try gl.Renderbuffer.create();
+ const bound_rbo = try rbo.bind();
+ defer bound_rbo.unbind();
+ try bound_rbo.storage(
+ opts.internal_format,
+ @intCast(opts.width),
+ @intCast(opts.height),
+ );
+
+ const fbo = try gl.Framebuffer.create();
+ const bound_fbo = try fbo.bind(.framebuffer);
+ defer bound_fbo.unbind();
+ try bound_fbo.renderbuffer(.color0, rbo);
+
+ return .{
+ .framebuffer = fbo,
+ .renderbuffer = rbo,
+ .width = opts.width,
+ .height = opts.height,
+ };
+}
+
+pub fn deinit(self: *Self) void {
+ self.framebuffer.destroy();
+ self.renderbuffer.destroy();
+}
diff --git a/vendor/libghostty-vt/src/renderer/opengl/Texture.zig b/vendor/libghostty-vt/src/renderer/opengl/Texture.zig
new file mode 100644
index 00000000..c37ec686
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/opengl/Texture.zig
@@ -0,0 +1,104 @@
+//! Wrapper for handling textures.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const gl = @import("opengl");
+
+const OpenGL = @import("../OpenGL.zig");
+
+const log = std.log.scoped(.opengl);
+
+/// Options for initializing a texture.
+pub const Options = struct {
+ format: gl.Texture.Format,
+ internal_format: gl.Texture.InternalFormat,
+ target: gl.Texture.Target,
+ min_filter: gl.Texture.MinFilter,
+ mag_filter: gl.Texture.MagFilter,
+ wrap_s: gl.Texture.Wrap,
+ wrap_t: gl.Texture.Wrap,
+};
+
+texture: gl.Texture,
+
+/// The width of this texture.
+width: usize,
+/// The height of this texture.
+height: usize,
+
+/// Format for this texture.
+format: gl.Texture.Format,
+
+/// Target for this texture.
+target: gl.Texture.Target,
+
+pub const Error = error{
+ /// An OpenGL API call failed.
+ OpenGLFailed,
+};
+
+/// Initialize a texture
+pub fn init(
+ opts: Options,
+ width: usize,
+ height: usize,
+ data: ?[]const u8,
+) Error!Self {
+ const tex = gl.Texture.create() catch return error.OpenGLFailed;
+ errdefer tex.destroy();
+ {
+ const texbind = tex.bind(opts.target) catch return error.OpenGLFailed;
+ defer texbind.unbind();
+ texbind.parameter(.WrapS, @intFromEnum(opts.wrap_s)) catch return error.OpenGLFailed;
+ texbind.parameter(.WrapT, @intFromEnum(opts.wrap_t)) catch return error.OpenGLFailed;
+ texbind.parameter(.MinFilter, @intFromEnum(opts.min_filter)) catch return error.OpenGLFailed;
+ texbind.parameter(.MagFilter, @intFromEnum(opts.mag_filter)) catch return error.OpenGLFailed;
+ texbind.image2D(
+ 0,
+ opts.internal_format,
+ @intCast(width),
+ @intCast(height),
+ opts.format,
+ .UnsignedByte,
+ if (data) |d| @ptrCast(d.ptr) else null,
+ ) catch return error.OpenGLFailed;
+ }
+
+ return .{
+ .texture = tex,
+ .width = width,
+ .height = height,
+ .format = opts.format,
+ .target = opts.target,
+ };
+}
+
+pub fn deinit(self: Self) void {
+ self.texture.destroy();
+}
+
+/// Replace a region of the texture with the provided data.
+///
+/// Does NOT check the dimensions of the data to ensure correctness.
+pub fn replaceRegion(
+ self: Self,
+ x: usize,
+ y: usize,
+ width: usize,
+ height: usize,
+ data: []const u8,
+) Error!void {
+ const texbind = self.texture.bind(self.target) catch return error.OpenGLFailed;
+ defer texbind.unbind();
+ texbind.subImage2D(
+ 0,
+ @intCast(x),
+ @intCast(y),
+ @intCast(width),
+ @intCast(height),
+ self.format,
+ .UnsignedByte,
+ data.ptr,
+ ) catch return error.OpenGLFailed;
+}
diff --git a/vendor/libghostty-vt/src/renderer/opengl/buffer.zig b/vendor/libghostty-vt/src/renderer/opengl/buffer.zig
new file mode 100644
index 00000000..f9cbbceb
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/opengl/buffer.zig
@@ -0,0 +1,126 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const gl = @import("opengl");
+
+const OpenGL = @import("../OpenGL.zig");
+
+const log = std.log.scoped(.opengl);
+
+/// Options for initializing a buffer.
+pub const Options = struct {
+ target: gl.Buffer.Target = .array,
+ usage: gl.Buffer.Usage = .dynamic_draw,
+};
+
+/// OpenGL data storage for a certain set of equal types. This is usually
+/// used for vertex buffers, etc. This helpful wrapper makes it easy to
+/// prealloc, shrink, grow, sync, buffers with OpenGL.
+pub fn Buffer(comptime T: type) type {
+ return struct {
+ const Self = @This();
+
+ /// Underlying `gl.Buffer` instance.
+ buffer: gl.Buffer,
+
+ /// Options this buffer was allocated with.
+ opts: Options,
+
+ /// Current allocated length of the data store.
+ /// Note this is the number of `T`s, not the size in bytes.
+ len: usize,
+
+ /// Initialize a buffer with the given length pre-allocated.
+ pub fn init(opts: Options, len: usize) !Self {
+ const buffer = try gl.Buffer.create();
+ errdefer buffer.destroy();
+
+ const binding = try buffer.bind(opts.target);
+ defer binding.unbind();
+
+ try binding.setDataNullManual(len * @sizeOf(T), opts.usage);
+
+ return .{
+ .buffer = buffer,
+ .opts = opts,
+ .len = len,
+ };
+ }
+
+ /// Init the buffer filled with the given data.
+ pub fn initFill(opts: Options, data: []const T) !Self {
+ const buffer = try gl.Buffer.create();
+ errdefer buffer.destroy();
+
+ const binding = try buffer.bind(opts.target);
+ defer binding.unbind();
+
+ try binding.setData(data, opts.usage);
+
+ return .{
+ .buffer = buffer,
+ .opts = opts,
+ .len = data.len * @sizeOf(T),
+ };
+ }
+
+ pub fn deinit(self: Self) void {
+ self.buffer.destroy();
+ }
+
+ /// Sync new contents to the buffer. The data is expected to be the
+ /// complete contents of the buffer. If the amount of data is larger
+ /// than the buffer length, the buffer will be reallocated.
+ ///
+ /// If the amount of data is smaller than the buffer length, the
+ /// remaining data in the buffer is left untouched.
+ pub fn sync(self: *Self, data: []const T) !void {
+ const binding = try self.buffer.bind(self.opts.target);
+ defer binding.unbind();
+
+ // If we need more space than our buffer has, we need to reallocate.
+ if (data.len > self.len) {
+ // Reallocate the buffer to hold double what we require.
+ self.len = data.len * 2;
+ try binding.setDataNullManual(
+ self.len * @sizeOf(T),
+ self.opts.usage,
+ );
+ }
+
+ // We can fit within the buffer so we can just replace bytes.
+ try binding.setSubData(0, data);
+ }
+
+ /// Like Buffer.sync but takes data from an array of ArrayLists,
+ /// rather than a single array. Returns the number of items synced.
+ pub fn syncFromArrayLists(self: *Self, lists: []const std.ArrayListUnmanaged(T)) !usize {
+ const binding = try self.buffer.bind(self.opts.target);
+ defer binding.unbind();
+
+ var total_len: usize = 0;
+ for (lists) |list| {
+ total_len += list.items.len;
+ }
+
+ // If we need more space than our buffer has, we need to reallocate.
+ if (total_len > self.len) {
+ // Reallocate the buffer to hold double what we require.
+ self.len = total_len * 2;
+ try binding.setDataNullManual(
+ self.len * @sizeOf(T),
+ self.opts.usage,
+ );
+ }
+
+ // We can fit within the buffer so we can just replace bytes.
+ var i: usize = 0;
+
+ for (lists) |list| {
+ try binding.setSubData(i, list.items);
+ i += list.items.len * @sizeOf(T);
+ }
+
+ return total_len;
+ }
+ };
+}
diff --git a/vendor/libghostty-vt/src/renderer/opengl/shaders.zig b/vendor/libghostty-vt/src/renderer/opengl/shaders.zig
new file mode 100644
index 00000000..68c1f36a
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/opengl/shaders.zig
@@ -0,0 +1,376 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = @import("../../quirks.zig").inlineAssert;
+const math = @import("../../math.zig");
+
+const Pipeline = @import("Pipeline.zig");
+
+const log = std.log.scoped(.opengl);
+
+const pipeline_descs: []const struct { [:0]const u8, PipelineDescription } =
+ &.{
+ .{ "bg_color", .{
+ .vertex_fn = loadShaderCode("../shaders/glsl/full_screen.v.glsl"),
+ .fragment_fn = loadShaderCode("../shaders/glsl/bg_color.f.glsl"),
+ .blending_enabled = false,
+ } },
+ .{ "cell_bg", .{
+ .vertex_fn = loadShaderCode("../shaders/glsl/full_screen.v.glsl"),
+ .fragment_fn = loadShaderCode("../shaders/glsl/cell_bg.f.glsl"),
+ .blending_enabled = true,
+ } },
+ .{ "cell_text", .{
+ .vertex_attributes = CellText,
+ .vertex_fn = loadShaderCode("../shaders/glsl/cell_text.v.glsl"),
+ .fragment_fn = loadShaderCode("../shaders/glsl/cell_text.f.glsl"),
+ .step_fn = .per_instance,
+ .blending_enabled = true,
+ } },
+ .{ "image", .{
+ .vertex_attributes = Image,
+ .vertex_fn = loadShaderCode("../shaders/glsl/image.v.glsl"),
+ .fragment_fn = loadShaderCode("../shaders/glsl/image.f.glsl"),
+ .step_fn = .per_instance,
+ .blending_enabled = true,
+ } },
+ .{ "bg_image", .{
+ .vertex_attributes = BgImage,
+ .vertex_fn = loadShaderCode("../shaders/glsl/bg_image.v.glsl"),
+ .fragment_fn = loadShaderCode("../shaders/glsl/bg_image.f.glsl"),
+ .step_fn = .per_instance,
+ .blending_enabled = true,
+ } },
+ };
+
+/// All the comptime-known info about a pipeline, so that
+/// we can define them ahead-of-time in an ergonomic way.
+const PipelineDescription = struct {
+ vertex_attributes: ?type = null,
+ vertex_fn: [:0]const u8,
+ fragment_fn: [:0]const u8,
+ step_fn: Pipeline.Options.StepFunction = .per_vertex,
+ blending_enabled: bool = true,
+
+ fn initPipeline(self: PipelineDescription) !Pipeline {
+ return try .init(self.vertex_attributes, .{
+ .vertex_fn = self.vertex_fn,
+ .fragment_fn = self.fragment_fn,
+ .step_fn = self.step_fn,
+ .blending_enabled = self.blending_enabled,
+ });
+ }
+};
+
+/// We create a type for the pipeline collection based on our desc array.
+const PipelineCollection = t: {
+ var fields: [pipeline_descs.len]std.builtin.Type.StructField = undefined;
+ for (pipeline_descs, 0..) |pipeline, i| {
+ fields[i] = .{
+ .name = pipeline[0],
+ .type = Pipeline,
+ .default_value_ptr = null,
+ .is_comptime = false,
+ .alignment = @alignOf(Pipeline),
+ };
+ }
+ break :t @Type(.{ .@"struct" = .{
+ .layout = .auto,
+ .fields = &fields,
+ .decls = &.{},
+ .is_tuple = false,
+ } });
+};
+
+/// This contains the state for the shaders used by the Metal renderer.
+pub const Shaders = struct {
+ /// Collection of available render pipelines.
+ pipelines: PipelineCollection,
+
+ /// Custom shaders to run against the final drawable texture. This
+ /// can be used to apply a lot of effects. Each shader is run in sequence
+ /// against the output of the previous shader.
+ post_pipelines: []const Pipeline,
+
+ /// Set to true when deinited, if you try to deinit a defunct set
+ /// of shaders it will just be ignored, to prevent double-free.
+ defunct: bool = false,
+
+ /// Initialize our shader set.
+ ///
+ /// "post_shaders" is an optional list of postprocess shaders to run
+ /// against the final drawable texture. This is an array of shader source
+ /// code, not file paths.
+ pub fn init(
+ alloc: Allocator,
+ post_shaders: []const [:0]const u8,
+ ) !Shaders {
+ var pipelines: PipelineCollection = undefined;
+
+ var initialized_pipelines: usize = 0;
+
+ errdefer inline for (pipeline_descs, 0..) |pipeline, i| {
+ if (i < initialized_pipelines) {
+ @field(pipelines, pipeline[0]).deinit();
+ }
+ };
+
+ inline for (pipeline_descs) |pipeline| {
+ @field(pipelines, pipeline[0]) = try pipeline[1].initPipeline();
+ initialized_pipelines += 1;
+ }
+
+ const post_pipelines: []const Pipeline = initPostPipelines(
+ alloc,
+ post_shaders,
+ ) catch |err| err: {
+ // If an error happens while building postprocess shaders we
+ // want to just not use any postprocess shaders since we don't
+ // want to block Ghostty from working.
+ log.warn("error initializing postprocess shaders err={}", .{err});
+ break :err &.{};
+ };
+ errdefer if (post_pipelines.len > 0) {
+ for (post_pipelines) |pipeline| pipeline.deinit();
+ alloc.free(post_pipelines);
+ };
+
+ return .{
+ .pipelines = pipelines,
+ .post_pipelines = post_pipelines,
+ };
+ }
+
+ pub fn deinit(self: *Shaders, alloc: Allocator) void {
+ if (self.defunct) return;
+ self.defunct = true;
+
+ // Release our primary shaders
+ inline for (pipeline_descs) |pipeline| {
+ @field(self.pipelines, pipeline[0]).deinit();
+ }
+
+ // Release our postprocess shaders
+ if (self.post_pipelines.len > 0) {
+ for (self.post_pipelines) |pipeline| {
+ pipeline.deinit();
+ }
+ alloc.free(self.post_pipelines);
+ }
+ }
+};
+
+/// The uniforms that are passed to our shaders.
+pub const Uniforms = extern struct {
+ /// The projection matrix for turning world coordinates to normalized.
+ /// This is calculated based on the size of the screen.
+ projection_matrix: math.Mat align(16),
+
+ /// Size of the screen (render target) in pixels.
+ screen_size: [2]f32 align(8),
+
+ /// Size of a single cell in pixels, unscaled.
+ cell_size: [2]f32 align(8),
+
+ /// Size of the grid in columns and rows.
+ grid_size: [2]u16 align(4),
+
+ /// The padding around the terminal grid in pixels. In order:
+ /// top, right, bottom, left.
+ grid_padding: [4]f32 align(16),
+
+ /// Bit mask defining which directions to
+ /// extend cell colors in to the padding.
+ /// Order, LSB first: left, right, up, down
+ padding_extend: PaddingExtend align(4),
+
+ /// The minimum contrast ratio for text. The contrast ratio is calculated
+ /// according to the WCAG 2.0 spec.
+ min_contrast: f32 align(4),
+
+ /// The cursor position and color.
+ cursor_pos: [2]u16 align(4),
+ cursor_color: [4]u8 align(4),
+
+ /// The background color for the whole surface.
+ bg_color: [4]u8 align(4),
+
+ /// Various booleans, in a packed struct for space efficiency.
+ bools: Bools align(4),
+
+ const Bools = packed struct(u32) {
+ /// Whether the cursor is 2 cells wide.
+ cursor_wide: bool,
+
+ /// Indicates that colors provided to the shader are already in
+ /// the P3 color space, so they don't need to be converted from
+ /// sRGB.
+ use_display_p3: bool,
+
+ /// Indicates that the color attachments for the shaders have
+ /// an `*_srgb` pixel format, which means the shaders need to
+ /// output linear RGB colors rather than gamma encoded colors,
+ /// since blending will be performed in linear space and then
+ /// Metal itself will re-encode the colors for storage.
+ use_linear_blending: bool,
+
+ /// Enables a weight correction step that makes text rendered
+ /// with linear alpha blending have a similar apparent weight
+ /// (thickness) to gamma-incorrect blending.
+ use_linear_correction: bool = false,
+
+ _padding: u28 = 0,
+ };
+
+ const PaddingExtend = packed struct(u32) {
+ left: bool = false,
+ right: bool = false,
+ up: bool = false,
+ down: bool = false,
+ _padding: u28 = 0,
+ };
+};
+
+/// This is a single parameter for the terminal cell shader.
+pub const CellText = extern struct {
+ glyph_pos: [2]u32 align(8) = .{ 0, 0 },
+ glyph_size: [2]u32 align(8) = .{ 0, 0 },
+ bearings: [2]i16 align(4) = .{ 0, 0 },
+ grid_pos: [2]u16 align(4),
+ color: [4]u8 align(4),
+ atlas: Atlas align(1),
+ bools: packed struct(u8) {
+ no_min_contrast: bool = false,
+ is_cursor_glyph: bool = false,
+ _padding: u6 = 0,
+ } align(1) = .{},
+
+ pub const Atlas = enum(u8) {
+ grayscale = 0,
+ color = 1,
+ };
+
+ // test {
+ // // Minimizing the size of this struct is important,
+ // // so we test it in order to be aware of any changes.
+ // try std.testing.expectEqual(32, @sizeOf(CellText));
+ // }
+};
+
+/// This is a single parameter for the cell bg shader.
+pub const CellBg = [4]u8;
+
+/// Single parameter for the image shader. See shader for field details.
+pub const Image = extern struct {
+ grid_pos: [2]f32 align(8),
+ cell_offset: [2]f32 align(8),
+ source_rect: [4]f32 align(16),
+ dest_size: [2]f32 align(8),
+};
+
+/// Single parameter for the bg image shader.
+pub const BgImage = extern struct {
+ opacity: f32 align(4),
+ info: Info align(1),
+
+ pub const Info = packed struct(u8) {
+ position: Position,
+ fit: Fit,
+ repeat: bool,
+ _padding: u1 = 0,
+
+ pub const Position = enum(u4) {
+ tl = 0,
+ tc = 1,
+ tr = 2,
+ ml = 3,
+ mc = 4,
+ mr = 5,
+ bl = 6,
+ bc = 7,
+ br = 8,
+ };
+
+ pub const Fit = enum(u2) {
+ contain = 0,
+ cover = 1,
+ stretch = 2,
+ none = 3,
+ };
+ };
+};
+
+/// Initialize our custom shader pipelines. The shaders argument is a
+/// set of shader source code, not file paths.
+fn initPostPipelines(
+ alloc: Allocator,
+ shaders: []const [:0]const u8,
+) ![]const Pipeline {
+ // If we have no shaders, do nothing.
+ if (shaders.len == 0) return &.{};
+
+ // Keeps track of how many shaders we successfully wrote.
+ var i: usize = 0;
+
+ // Initialize our result set. If any error happens, we undo everything.
+ var pipelines = try alloc.alloc(Pipeline, shaders.len);
+ errdefer {
+ for (pipelines[0..i]) |pipeline| {
+ pipeline.deinit();
+ }
+ alloc.free(pipelines);
+ }
+
+ // Build each shader. Note we don't use "0.." to build our index
+ // because we need to keep track of our length to clean up above.
+ for (shaders) |source| {
+ pipelines[i] = try initPostPipeline(source);
+ i += 1;
+ }
+
+ return pipelines;
+}
+
+/// Initialize a single custom shader pipeline from shader source.
+fn initPostPipeline(data: [:0]const u8) !Pipeline {
+ return try Pipeline.init(null, .{
+ .vertex_fn = loadShaderCode("../shaders/glsl/full_screen.v.glsl"),
+ .fragment_fn = data,
+ });
+}
+
+/// Load shader code from the target path, processing `#include` directives.
+///
+/// Comptime only for now, this code is really sloppy and makes a bunch of
+/// assumptions about things being well formed and file names not containing
+/// quote marks. If we ever want to process `#include`s for custom shaders
+/// then we need to write something better than this for it.
+fn loadShaderCode(comptime path: []const u8) [:0]const u8 {
+ return comptime processIncludes(@embedFile(path), std.fs.path.dirname(path).?);
+}
+
+/// Used by loadShaderCode
+fn processIncludes(contents: [:0]const u8, basedir: []const u8) [:0]const u8 {
+ @setEvalBranchQuota(100_000);
+ var i: usize = 0;
+ while (i < contents.len) {
+ if (std.mem.startsWith(u8, contents[i..], "#include")) {
+ assert(std.mem.startsWith(u8, contents[i..], "#include \""));
+ const start = i + "#include \"".len;
+ const end = std.mem.indexOfScalarPos(u8, contents, start, '"').?;
+ return std.fmt.comptimePrint(
+ "{s}{s}{s}",
+ .{
+ contents[0..i],
+ @embedFile(basedir ++ "/" ++ contents[start..end]),
+ processIncludes(contents[end + 1 ..], basedir),
+ },
+ );
+ }
+ if (std.mem.indexOfPos(u8, contents, i, "\n#")) |j| {
+ i = (j + 1);
+ } else {
+ break;
+ }
+ }
+ return contents;
+}
diff --git a/vendor/libghostty-vt/src/renderer/row.zig b/vendor/libghostty-vt/src/renderer/row.zig
new file mode 100644
index 00000000..74a64101
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/row.zig
@@ -0,0 +1,63 @@
+const terminal = @import("../terminal/main.zig");
+
+// TODO: Test neverExtendBg function
+
+/// Returns true if the row of this pin should never have its background
+/// color extended for filling padding space in the renderer. This is
+/// a set of heuristics that help making our padding look better.
+pub fn neverExtendBg(
+ row: terminal.page.Row,
+ cells: []const terminal.page.Cell,
+ styles: []const terminal.Style,
+ palette: *const terminal.color.Palette,
+ default_background: terminal.color.RGB,
+) bool {
+ // Any semantic prompts should not have their background extended
+ // because prompts often contain special formatting (such as
+ // powerline) that looks bad when extended.
+ switch (row.semantic_prompt) {
+ .prompt, .prompt_continuation => return true,
+ .none => {},
+ }
+
+ for (0.., cells) |x, *cell| {
+ // If any cell has a default background color then we don't
+ // extend because the default background color probably looks
+ // good enough as an extension.
+ switch (cell.content_tag) {
+ // If it is a background color cell, we check the color.
+ .bg_color_palette, .bg_color_rgb => {
+ const s: terminal.Style = if (cell.hasStyling()) styles[x] else .{};
+ const bg = s.bg(cell, palette) orelse return true;
+ if (bg.eql(default_background)) return true;
+ },
+
+ // If its a codepoint cell we can check the style.
+ .codepoint, .codepoint_grapheme => {
+ // For codepoint containing, we also never extend bg
+ // if any cell has a powerline glyph because these are
+ // perfect-fit.
+ switch (cell.codepoint()) {
+ // Powerline
+ 0xE0B0...0xE0C8,
+ 0xE0CA,
+ 0xE0CC...0xE0D2,
+ 0xE0D4,
+ => return true,
+
+ else => {},
+ }
+
+ // Never extend a cell that has a default background.
+ // A default background is applied if there is no background
+ // on the style or the explicitly set background
+ // matches our default background.
+ const s: terminal.Style = if (cell.hasStyling()) styles[x] else .{};
+ const bg = s.bg(cell, palette) orelse return true;
+ if (bg.eql(default_background)) return true;
+ },
+ }
+ }
+
+ return false;
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_color.f.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_color.f.glsl
new file mode 100644
index 00000000..616c44b8
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_color.f.glsl
@@ -0,0 +1,13 @@
+#include "common.glsl"
+
+// Must declare this output for some versions of OpenGL.
+layout(location = 0) out vec4 out_FragColor;
+
+void main() {
+ bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
+
+ out_FragColor = load_color(
+ unpack4u8(bg_color_packed_4u8),
+ use_linear_blending
+ );
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_image.f.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_image.f.glsl
new file mode 100644
index 00000000..ee1195ef
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_image.f.glsl
@@ -0,0 +1,63 @@
+#include "common.glsl"
+
+// Position the FragCoord origin to the upper left
+// so as to align with our texture's directionality.
+layout(origin_upper_left) in vec4 gl_FragCoord;
+
+layout(binding = 0) uniform sampler2D image;
+
+flat in vec4 bg_color;
+flat in vec2 offset;
+flat in vec2 scale;
+flat in float opacity;
+flat in uint repeat;
+
+layout(location = 0) out vec4 out_FragColor;
+
+void main() {
+ bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
+
+ // Our texture coordinate is based on the screen position, offset by the
+ // dest rect origin, and scaled by the ratio between the dest rect size
+ // and the original texture size, which effectively scales the original
+ // size of the texture to the dest rect size.
+ vec2 tex_coord = (gl_FragCoord.xy - offset) * scale;
+
+ vec2 tex_size = textureSize(image, 0);
+
+ // If we need to repeat the texture, wrap the coordinates.
+ if (repeat != 0) {
+ tex_coord = mod(mod(tex_coord, tex_size) + tex_size, tex_size);
+ }
+
+ vec4 rgba;
+ // If we're out of bounds, we have no color,
+ // otherwise we sample the texture for it.
+ if (any(lessThan(tex_coord, vec2(0.0))) ||
+ any(greaterThan(tex_coord, tex_size)))
+ {
+ rgba = vec4(0.0);
+ } else {
+ // We divide by the texture size to normalize for sampling.
+ rgba = texture(image, tex_coord / tex_size);
+
+ if (!use_linear_blending) {
+ rgba = unlinearize(rgba);
+ }
+
+ rgba.rgb *= rgba.a;
+ }
+
+ // Multiply it by the configured opacity, but cap it at
+ // the value that will make it fully opaque relative to
+ // the background color alpha, so it isn't overexposed.
+ rgba *= min(opacity, 1.0 / bg_color.a);
+
+ // Blend it on to a fully opaque version of the background color.
+ rgba += max(vec4(0.0), vec4(bg_color.rgb, 1.0) * vec4(1.0 - rgba.a));
+
+ // Multiply everything by the background color alpha.
+ rgba *= bg_color.a;
+
+ out_FragColor = rgba;
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_image.v.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_image.v.glsl
new file mode 100644
index 00000000..d55aa174
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_image.v.glsl
@@ -0,0 +1,145 @@
+#include "common.glsl"
+
+layout(binding = 0) uniform sampler2D image;
+
+layout(location = 0) in float in_opacity;
+layout(location = 1) in uint info;
+
+// 4 bits of info.
+const uint BG_IMAGE_POSITION = 15u;
+const uint BG_IMAGE_TL = 0u;
+const uint BG_IMAGE_TC = 1u;
+const uint BG_IMAGE_TR = 2u;
+const uint BG_IMAGE_ML = 3u;
+const uint BG_IMAGE_MC = 4u;
+const uint BG_IMAGE_MR = 5u;
+const uint BG_IMAGE_BL = 6u;
+const uint BG_IMAGE_BC = 7u;
+const uint BG_IMAGE_BR = 8u;
+
+// 2 bits of info shifted 4.
+const uint BG_IMAGE_FIT = 3u << 4;
+const uint BG_IMAGE_CONTAIN = 0u << 4;
+const uint BG_IMAGE_COVER = 1u << 4;
+const uint BG_IMAGE_STRETCH = 2u << 4;
+const uint BG_IMAGE_NO_FIT = 3u << 4;
+
+// 1 bit of info shifted 6.
+const uint BG_IMAGE_REPEAT = 1u << 6;
+
+flat out vec4 bg_color;
+flat out vec2 offset;
+flat out vec2 scale;
+flat out float opacity;
+// We use a uint to pass the repeat value because
+// bools aren't allowed for vertex outputs in OpenGL.
+flat out uint repeat;
+
+void main() {
+ bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
+
+ vec4 position;
+ position.x = (gl_VertexID == 2) ? 3.0 : -1.0;
+ position.y = (gl_VertexID == 0) ? -3.0 : 1.0;
+ position.z = 1.0;
+ position.w = 1.0;
+
+ // Single triangle is clipped to viewport.
+ //
+ // X <- vid == 0: (-1, -3)
+ // |\
+ // | \
+ // | \
+ // |###\
+ // |#+# \ `+` is (0, 0). `#`s are viewport area.
+ // |### \
+ // X------X <- vid == 2: (3, 1)
+ // ^
+ // vid == 1: (-1, 1)
+
+ gl_Position = position;
+
+ opacity = in_opacity;
+
+ repeat = info & BG_IMAGE_REPEAT;
+
+ vec2 screen_size = screen_size;
+ vec2 tex_size = textureSize(image, 0);
+
+ vec2 dest_size = tex_size;
+ switch (info & BG_IMAGE_FIT) {
+ // For `contain` we scale by a factor that makes the image
+ // width match the screen width or makes the image height
+ // match the screen height, whichever is smaller.
+ case BG_IMAGE_CONTAIN: {
+ float scale = min(screen_size.x / tex_size.x, screen_size.y / tex_size.y);
+ dest_size = tex_size * scale;
+ } break;
+
+ // For `cover` we scale by a factor that makes the image
+ // width match the screen width or makes the image height
+ // match the screen height, whichever is larger.
+ case BG_IMAGE_COVER: {
+ float scale = max(screen_size.x / tex_size.x, screen_size.y / tex_size.y);
+ dest_size = tex_size * scale;
+ } break;
+
+ // For `stretch` we stretch the image to the size of
+ // the screen without worrying about aspect ratio.
+ case BG_IMAGE_STRETCH: {
+ dest_size = screen_size;
+ } break;
+
+ // For `none` we just use the original texture size.
+ case BG_IMAGE_NO_FIT: {
+ dest_size = tex_size;
+ } break;
+ }
+
+ vec2 start = vec2(0.0);
+ vec2 mid = (screen_size - dest_size) / vec2(2.0);
+ vec2 end = screen_size - dest_size;
+
+ vec2 dest_offset = mid;
+ switch (info & BG_IMAGE_POSITION) {
+ case BG_IMAGE_TL: {
+ dest_offset = vec2(start.x, start.y);
+ } break;
+ case BG_IMAGE_TC: {
+ dest_offset = vec2(mid.x, start.y);
+ } break;
+ case BG_IMAGE_TR: {
+ dest_offset = vec2(end.x, start.y);
+ } break;
+ case BG_IMAGE_ML: {
+ dest_offset = vec2(start.x, mid.y);
+ } break;
+ case BG_IMAGE_MC: {
+ dest_offset = vec2(mid.x, mid.y);
+ } break;
+ case BG_IMAGE_MR: {
+ dest_offset = vec2(end.x, mid.y);
+ } break;
+ case BG_IMAGE_BL: {
+ dest_offset = vec2(start.x, end.y);
+ } break;
+ case BG_IMAGE_BC: {
+ dest_offset = vec2(mid.x, end.y);
+ } break;
+ case BG_IMAGE_BR: {
+ dest_offset = vec2(end.x, end.y);
+ } break;
+ }
+
+ offset = dest_offset;
+ scale = tex_size / dest_size;
+
+ // We load a fully opaque version of the bg color and combine it with
+ // the alpha separately, because we need these as separate values in
+ // the framgment shader.
+ uvec4 u_bg_color = unpack4u8(bg_color_packed_4u8);
+ bg_color = vec4(load_color(
+ uvec4(u_bg_color.rgb, 255),
+ use_linear_blending
+ ).rgb, float(u_bg_color.a) / 255.0);
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_bg.f.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_bg.f.glsl
new file mode 100644
index 00000000..fa48c673
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_bg.f.glsl
@@ -0,0 +1,61 @@
+#include "common.glsl"
+
+// Position the origin to the upper left
+layout(origin_upper_left) in vec4 gl_FragCoord;
+
+// Must declare this output for some versions of OpenGL.
+layout(location = 0) out vec4 out_FragColor;
+
+layout(binding = 1, std430) readonly buffer bg_cells {
+ uint cells[];
+};
+
+vec4 cell_bg() {
+ uvec2 grid_size = unpack2u16(grid_size_packed_2u16);
+ ivec2 grid_pos = ivec2(floor((gl_FragCoord.xy - grid_padding.wx) / cell_size));
+ bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
+
+ vec4 bg = vec4(0.0);
+
+ // Clamp x position, extends edge bg colors in to padding on sides.
+ if (grid_pos.x < 0) {
+ if ((padding_extend & EXTEND_LEFT) != 0) {
+ grid_pos.x = 0;
+ } else {
+ return bg;
+ }
+ } else if (grid_pos.x > grid_size.x - 1) {
+ if ((padding_extend & EXTEND_RIGHT) != 0) {
+ grid_pos.x = int(grid_size.x) - 1;
+ } else {
+ return bg;
+ }
+ }
+
+ // Clamp y position if we should extend, otherwise discard if out of bounds.
+ if (grid_pos.y < 0) {
+ if ((padding_extend & EXTEND_UP) != 0) {
+ grid_pos.y = 0;
+ } else {
+ return bg;
+ }
+ } else if (grid_pos.y > grid_size.y - 1) {
+ if ((padding_extend & EXTEND_DOWN) != 0) {
+ grid_pos.y = int(grid_size.y) - 1;
+ } else {
+ return bg;
+ }
+ }
+
+ // Load the color for the cell.
+ vec4 cell_color = load_color(
+ unpack4u8(cells[grid_pos.y * grid_size.x + grid_pos.x]),
+ use_linear_blending
+ );
+
+ return cell_color;
+}
+
+void main() {
+ out_FragColor = cell_bg();
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_text.f.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_text.f.glsl
new file mode 100644
index 00000000..176efcbd
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_text.f.glsl
@@ -0,0 +1,100 @@
+#include "common.glsl"
+
+layout(binding = 0) uniform sampler2DRect atlas_grayscale;
+layout(binding = 1) uniform sampler2DRect atlas_color;
+
+in CellTextVertexOut {
+ flat uint atlas;
+ flat vec4 color;
+ flat vec4 bg_color;
+ vec2 tex_coord;
+} in_data;
+
+// Values `atlas` can take.
+const uint ATLAS_GRAYSCALE = 0u;
+const uint ATLAS_COLOR = 1u;
+
+// Must declare this output for some versions of OpenGL.
+layout(location = 0) out vec4 out_FragColor;
+
+void main() {
+ bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
+ bool use_linear_correction = (bools & USE_LINEAR_CORRECTION) != 0;
+
+ switch (in_data.atlas) {
+ default:
+ case ATLAS_GRAYSCALE:
+ {
+ // Our input color is always linear.
+ vec4 color = in_data.color;
+
+ // If we're not doing linear blending, then we need to
+ // re-apply the gamma encoding to our color manually.
+ //
+ // Since the alpha is premultiplied, we need to divide
+ // it out before unlinearizing and re-multiply it after.
+ if (!use_linear_blending) {
+ color.rgb /= vec3(color.a);
+ color = unlinearize(color);
+ color.rgb *= vec3(color.a);
+ }
+
+ // Fetch our alpha mask for this pixel.
+ float a = texture(atlas_grayscale, in_data.tex_coord).r;
+
+ // Linear blending weight correction corrects the alpha value to
+ // produce blending results which match gamma-incorrect blending.
+ if (use_linear_correction) {
+ // Short explanation of how this works:
+ //
+ // We get the luminances of the foreground and background colors,
+ // and then unlinearize them and perform blending on them. This
+ // gives us our desired luminance, which we derive our new alpha
+ // value from by mapping the range [bg_l, fg_l] to [0, 1], since
+ // our final blend will be a linear interpolation from bg to fg.
+ //
+ // This yields virtually identical results for grayscale blending,
+ // and very similar but non-identical results for color blending.
+ vec4 bg = in_data.bg_color;
+ float fg_l = luminance(color.rgb);
+ float bg_l = luminance(bg.rgb);
+ // To avoid numbers going haywire, we don't apply correction
+ // when the bg and fg luminances are within 0.001 of each other.
+ if (abs(fg_l - bg_l) > 0.001) {
+ float blend_l = linearize(unlinearize(fg_l) * a + unlinearize(bg_l) * (1.0 - a));
+ a = clamp((blend_l - bg_l) / (fg_l - bg_l), 0.0, 1.0);
+ }
+ }
+
+ // Multiply our whole color by the alpha mask.
+ // Since we use premultiplied alpha, this is
+ // the correct way to apply the mask.
+ color *= a;
+
+ out_FragColor = color;
+ return;
+ }
+
+ case ATLAS_COLOR:
+ {
+ // For now, we assume that color glyphs
+ // are already premultiplied linear colors.
+ vec4 color = texture(atlas_color, in_data.tex_coord);
+
+ // If we are doing linear blending, we can return this right away.
+ if (use_linear_blending) {
+ out_FragColor = color;
+ return;
+ }
+
+ // Otherwise we need to unlinearize the color. Since the alpha is
+ // premultiplied, we need to divide it out before unlinearizing.
+ color.rgb /= vec3(color.a);
+ color = unlinearize(color);
+ color.rgb *= vec3(color.a);
+
+ out_FragColor = color;
+ return;
+ }
+ }
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_text.v.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_text.v.glsl
new file mode 100644
index 00000000..7e38e2f0
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_text.v.glsl
@@ -0,0 +1,146 @@
+#include "common.glsl"
+
+// The position of the glyph in the texture (x, y)
+layout(location = 0) in uvec2 glyph_pos;
+
+// The size of the glyph in the texture (w, h)
+layout(location = 1) in uvec2 glyph_size;
+
+// The left and top bearings for the glyph (x, y)
+layout(location = 2) in ivec2 bearings;
+
+// The grid coordinates (x, y) where x < columns and y < rows
+layout(location = 3) in uvec2 grid_pos;
+
+// The color of the rendered text glyph.
+layout(location = 4) in uvec4 color;
+
+// Which atlas this glyph is in.
+layout(location = 5) in uint atlas;
+
+// Misc glyph properties.
+layout(location = 6) in uint glyph_bools;
+
+// Values `atlas` can take.
+const uint ATLAS_GRAYSCALE = 0u;
+const uint ATLAS_COLOR = 1u;
+
+// Masks for the `glyph_bools` attribute
+const uint NO_MIN_CONTRAST = 1u;
+const uint IS_CURSOR_GLYPH = 2u;
+
+out CellTextVertexOut {
+ flat uint atlas;
+ flat vec4 color;
+ flat vec4 bg_color;
+ vec2 tex_coord;
+} out_data;
+
+layout(binding = 1, std430) readonly buffer bg_cells {
+ uint bg_colors[];
+};
+
+void main() {
+ uvec2 grid_size = unpack2u16(grid_size_packed_2u16);
+ uvec2 cursor_pos = unpack2u16(cursor_pos_packed_2u16);
+ bool cursor_wide = (bools & CURSOR_WIDE) != 0;
+ bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
+
+ // Convert the grid x, y into world space x, y by accounting for cell size
+ vec2 cell_pos = cell_size * vec2(grid_pos);
+
+ int vid = gl_VertexID;
+
+ // We use a triangle strip with 4 vertices to render quads,
+ // so we determine which corner of the cell this vertex is in
+ // based on the vertex ID.
+ //
+ // 0 --> 1
+ // | .'|
+ // | / |
+ // | L |
+ // 2 --> 3
+ //
+ // 0 = top-left (0, 0)
+ // 1 = top-right (1, 0)
+ // 2 = bot-left (0, 1)
+ // 3 = bot-right (1, 1)
+ vec2 corner;
+ corner.x = float(vid == 1 || vid == 3);
+ corner.y = float(vid == 2 || vid == 3);
+
+ out_data.atlas = atlas;
+
+ // === Grid Cell ===
+ // +X
+ // 0,0--...->
+ // |
+ // . offset.x = bearings.x
+ // +Y. .|.
+ // . | |
+ // | cell_pos -> +-------+ _.
+ // v ._| |_. _|- offset.y = cell_size.y - bearings.y
+ // | | .###. | |
+ // | | #...# | |
+ // glyph_size.y -+ | ##### | |
+ // | | #.... | +- bearings.y
+ // |_| .#### | |
+ // | |_|
+ // +-------+
+ // |_._|
+ // |
+ // glyph_size.x
+ //
+ // In order to get the top left of the glyph, we compute an offset based on
+ // the bearings. The Y bearing is the distance from the bottom of the cell
+ // to the top of the glyph, so we subtract it from the cell height to get
+ // the y offset. The X bearing is the distance from the left of the cell
+ // to the left of the glyph, so it works as the x offset directly.
+
+ vec2 size = vec2(glyph_size);
+ vec2 offset = vec2(bearings);
+
+ offset.y = cell_size.y - offset.y;
+
+ // Calculate the final position of the cell which uses our glyph size
+ // and glyph offset to create the correct bounding box for the glyph.
+ cell_pos = cell_pos + size * corner + offset;
+ gl_Position = projection_matrix * vec4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
+
+ // Calculate the texture coordinate in pixels. This is NOT normalized
+ // (between 0.0 and 1.0), and does not need to be, since the texture will
+ // be sampled with pixel coordinate mode.
+ out_data.tex_coord = vec2(glyph_pos) + vec2(glyph_size) * corner;
+
+ // Get our color. We always fetch a linearized version to
+ // make it easier to handle minimum contrast calculations.
+ out_data.color = load_color(color, true);
+ // Get the BG color
+ out_data.bg_color = load_color(
+ unpack4u8(bg_colors[grid_pos.y * grid_size.x + grid_pos.x]),
+ true
+ );
+ // Blend it with the global bg color
+ vec4 global_bg = load_color(
+ unpack4u8(bg_color_packed_4u8),
+ true
+ );
+ out_data.bg_color += global_bg * vec4(1.0 - out_data.bg_color.a);
+
+ // If we have a minimum contrast, we need to check if we need to
+ // change the color of the text to ensure it has enough contrast
+ // with the background.
+ if (min_contrast > 1.0f && (glyph_bools & NO_MIN_CONTRAST) == 0) {
+ // Ensure our minimum contrast
+ out_data.color = contrasted_color(min_contrast, out_data.color, out_data.bg_color);
+ }
+
+ // Check if current position is under cursor (including wide cursor)
+ bool is_cursor_pos = ((grid_pos.x == cursor_pos.x) || (cursor_wide && (grid_pos.x == (cursor_pos.x + 1)))) && (grid_pos.y == cursor_pos.y);
+
+ // If this cell is the cursor cell, but we're not processing
+ // the cursor glyph itself, then we need to change the color.
+ if ((glyph_bools & IS_CURSOR_GLYPH) == 0 && is_cursor_pos) {
+ out_data.color = load_color(unpack4u8(cursor_color_packed_4u8), use_linear_blending);
+ }
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/common.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/common.glsl
new file mode 100644
index 00000000..1a7ea03a
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/common.glsl
@@ -0,0 +1,156 @@
+#version 430 core
+
+// These are common definitions to be shared across shaders, the first
+// line of any shader that needs these should be `#include "common.glsl"`.
+//
+// Included in this file are:
+// - The interface block for the global uniforms.
+// - Functions for unpacking values.
+// - Functions for working with colors.
+
+//----------------------------------------------------------------------------//
+// Global Uniforms
+//----------------------------------------------------------------------------//
+layout(binding = 1, std140) uniform Globals {
+ uniform mat4 projection_matrix;
+ uniform vec2 screen_size;
+ uniform vec2 cell_size;
+ uniform uint grid_size_packed_2u16;
+ uniform vec4 grid_padding;
+ uniform uint padding_extend;
+ uniform float min_contrast;
+ uniform uint cursor_pos_packed_2u16;
+ uniform uint cursor_color_packed_4u8;
+ uniform uint bg_color_packed_4u8;
+ uniform uint bools;
+};
+
+// Bools
+const uint CURSOR_WIDE = 1u;
+const uint USE_DISPLAY_P3 = 2u;
+const uint USE_LINEAR_BLENDING = 4u;
+const uint USE_LINEAR_CORRECTION = 8u;
+
+// Padding extend enum
+const uint EXTEND_LEFT = 1u;
+const uint EXTEND_RIGHT = 2u;
+const uint EXTEND_UP = 4u;
+const uint EXTEND_DOWN = 8u;
+
+//----------------------------------------------------------------------------//
+// Functions for Unpacking Values
+//----------------------------------------------------------------------------//
+// NOTE: These unpack functions assume little-endian.
+// If this ever becomes a problem... oh dear!
+
+uvec4 unpack4u8(uint packed_value) {
+ return uvec4(
+ uint(packed_value >> 0) & uint(0xFF),
+ uint(packed_value >> 8) & uint(0xFF),
+ uint(packed_value >> 16) & uint(0xFF),
+ uint(packed_value >> 24) & uint(0xFF)
+ );
+}
+
+uvec2 unpack2u16(uint packed_value) {
+ return uvec2(
+ uint(packed_value >> 0) & uint(0xFFFF),
+ uint(packed_value >> 16) & uint(0xFFFF)
+ );
+}
+
+ivec2 unpack2i16(int packed_value) {
+ return ivec2(
+ (packed_value << 16) >> 16,
+ (packed_value << 0) >> 16
+ );
+}
+
+//----------------------------------------------------------------------------//
+// Color Functions
+//----------------------------------------------------------------------------//
+
+// Compute the luminance of the provided color.
+//
+// Takes colors in linear RGB space. If your colors are gamma
+// encoded, linearize them before using them with this function.
+float luminance(vec3 color) {
+ return dot(color, vec3(0.2126f, 0.7152f, 0.0722f));
+}
+
+// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
+//
+// Takes colors in linear RGB space. If your colors are gamma
+// encoded, linearize them before using them with this function.
+float contrast_ratio(vec3 color1, vec3 color2) {
+ float luminance1 = luminance(color1) + 0.05;
+ float luminance2 = luminance(color2) + 0.05;
+ return max(luminance1, luminance2) / min(luminance1, luminance2);
+}
+
+// Return the fg if the contrast ratio is greater than min, otherwise
+// return a color that satisfies the contrast ratio. Currently, the color
+// is always white or black, whichever has the highest contrast ratio.
+//
+// Takes colors in linear RGB space. If your colors are gamma
+// encoded, linearize them before using them with this function.
+vec4 contrasted_color(float min_ratio, vec4 fg, vec4 bg) {
+ float ratio = contrast_ratio(fg.rgb, bg.rgb);
+ if (ratio < min_ratio) {
+ float white_ratio = contrast_ratio(vec3(1.0, 1.0, 1.0), bg.rgb);
+ float black_ratio = contrast_ratio(vec3(0.0, 0.0, 0.0), bg.rgb);
+ if (white_ratio > black_ratio) {
+ return vec4(1.0);
+ } else {
+ return vec4(0.0, 0.0, 0.0, 1.0);
+ }
+ }
+
+ return fg;
+}
+
+// Converts a color from sRGB gamma encoding to linear.
+vec4 linearize(vec4 srgb) {
+ bvec3 cutoff = lessThanEqual(srgb.rgb, vec3(0.04045));
+ vec3 higher = pow((srgb.rgb + vec3(0.055)) / vec3(1.055), vec3(2.4));
+ vec3 lower = srgb.rgb / vec3(12.92);
+
+ return vec4(mix(higher, lower, cutoff), srgb.a);
+}
+float linearize(float v) {
+ return v <= 0.04045 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4);
+}
+
+// Converts a color from linear to sRGB gamma encoding.
+vec4 unlinearize(vec4 linear) {
+ bvec3 cutoff = lessThanEqual(linear.rgb, vec3(0.0031308));
+ vec3 higher = pow(linear.rgb, vec3(1.0 / 2.4)) * vec3(1.055) - vec3(0.055);
+ vec3 lower = linear.rgb * vec3(12.92);
+
+ return vec4(mix(higher, lower, cutoff), linear.a);
+}
+float unlinearize(float v) {
+ return v <= 0.0031308 ? v * 12.92 : pow(v, 1.0 / 2.4) * 1.055 - 0.055;
+}
+
+// Load a 4 byte RGBA non-premultiplied color and linearize
+// and convert it as necessary depending on the provided info.
+//
+// `linear` controls whether the returned color is linear or gamma encoded.
+vec4 load_color(
+ uvec4 in_color,
+ bool linear
+) {
+ // 0 .. 255 -> 0.0 .. 1.0
+ vec4 color = vec4(in_color) / vec4(255.0f);
+
+ // Linearize if necessary.
+ if (linear) color = linearize(color);
+
+ // Premultiply our color by its alpha.
+ color.rgb *= color.a;
+
+ return color;
+}
+
+//----------------------------------------------------------------------------//
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/full_screen.v.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/full_screen.v.glsl
new file mode 100644
index 00000000..b89cedfa
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/full_screen.v.glsl
@@ -0,0 +1,24 @@
+#version 330 core
+
+void main() {
+ vec4 position;
+ position.x = (gl_VertexID == 2) ? 3.0 : -1.0;
+ position.y = (gl_VertexID == 0) ? -3.0 : 1.0;
+ position.z = 1.0;
+ position.w = 1.0;
+
+ // Single triangle is clipped to viewport.
+ //
+ // X <- vid == 0: (-1, -3)
+ // |\
+ // | \
+ // | \
+ // |###\
+ // |#+# \ `+` is (0, 0). `#`s are viewport area.
+ // |### \
+ // X------X <- vid == 2: (3, 1)
+ // ^
+ // vid == 1: (-1, 1)
+
+ gl_Position = position;
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/image.f.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/image.f.glsl
new file mode 100644
index 00000000..4f89d7a7
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/image.f.glsl
@@ -0,0 +1,21 @@
+#include "common.glsl"
+
+layout(binding = 0) uniform sampler2D image;
+
+in vec2 tex_coord;
+
+layout(location = 0) out vec4 out_FragColor;
+
+void main() {
+ bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
+
+ vec4 rgba = texture(image, tex_coord);
+
+ if (!use_linear_blending) {
+ rgba = unlinearize(rgba);
+ }
+
+ rgba.rgb *= vec3(rgba.a);
+
+ out_FragColor = rgba;
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/image.v.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/image.v.glsl
new file mode 100644
index 00000000..779fae32
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/image.v.glsl
@@ -0,0 +1,47 @@
+#include "common.glsl"
+
+layout(binding = 0) uniform sampler2D image;
+
+layout(location = 0) in vec2 grid_pos;
+layout(location = 1) in vec2 cell_offset;
+layout(location = 2) in vec4 source_rect;
+layout(location = 3) in vec2 dest_size;
+
+out vec2 tex_coord;
+
+void main() {
+ int vid = gl_VertexID;
+
+ // We use a triangle strip with 4 vertices to render quads,
+ // so we determine which corner of the cell this vertex is in
+ // based on the vertex ID.
+ //
+ // 0 --> 1
+ // | .'|
+ // | / |
+ // | L |
+ // 2 --> 3
+ //
+ // 0 = top-left (0, 0)
+ // 1 = top-right (1, 0)
+ // 2 = bot-left (0, 1)
+ // 3 = bot-right (1, 1)
+ vec2 corner;
+ corner.x = float(vid == 1 || vid == 3);
+ corner.y = float(vid == 2 || vid == 3);
+
+ // The texture coordinates start at our source x/y
+ // and add the width/height depending on the corner.
+ tex_coord = source_rect.xy;
+ tex_coord += source_rect.zw * corner;
+
+ // Normalize the coordinates.
+ tex_coord /= textureSize(image, 0);
+
+ // The position of our image starts at the top-left of the grid cell and
+ // adds the source rect width/height components.
+ vec2 image_pos = (cell_size * grid_pos) + cell_offset;
+ image_pos += dest_size * corner;
+
+ gl_Position = projection_matrix * vec4(image_pos.xy, 1.0, 1.0);
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/shaders.metal b/vendor/libghostty-vt/src/renderer/shaders/shaders.metal
new file mode 100644
index 00000000..4e02b633
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/shaders.metal
@@ -0,0 +1,853 @@
+#include <metal_stdlib>
+
+using namespace metal;
+
+enum Padding : uint8_t {
+ EXTEND_LEFT = 1u,
+ EXTEND_RIGHT = 2u,
+ EXTEND_UP = 4u,
+ EXTEND_DOWN = 8u,
+};
+
+struct Uniforms {
+ float4x4 projection_matrix;
+ float2 screen_size;
+ float2 cell_size;
+ ushort2 grid_size;
+ float4 grid_padding;
+ uint8_t padding_extend;
+ float min_contrast;
+ ushort2 cursor_pos;
+ uchar4 cursor_color;
+ uchar4 bg_color;
+ bool cursor_wide;
+ bool use_display_p3;
+ bool use_linear_blending;
+ bool use_linear_correction;
+};
+
+//-------------------------------------------------------------------
+// Color Functions
+//-------------------------------------------------------------------
+#pragma mark - Colors
+
+// D50-adapted sRGB to XYZ conversion matrix.
+// http://www.brucelindbloom.com/Eqn_RGB_XYZ_Matrix.html
+constant float3x3 sRGB_XYZ = transpose(float3x3(
+ 0.4360747, 0.3850649, 0.1430804,
+ 0.2225045, 0.7168786, 0.0606169,
+ 0.0139322, 0.0971045, 0.7141733
+));
+// XYZ to Display P3 conversion matrix.
+// http://endavid.com/index.php?entry=79
+constant float3x3 XYZ_DP3 = transpose(float3x3(
+ 2.40414768,-0.99010704,-0.39759019,
+ -0.84239098, 1.79905954, 0.01597023,
+ 0.04838763,-0.09752546, 1.27393636
+));
+// By composing the two above matrices we get
+// our sRGB to Display P3 conversion matrix.
+constant float3x3 sRGB_DP3 = XYZ_DP3 * sRGB_XYZ;
+
+// Converts a color in linear sRGB to linear Display P3
+//
+// TODO: The color matrix should probably be computed
+// dynamically and passed as a uniform, rather
+// than being hard coded above.
+float3 srgb_to_display_p3(float3 srgb) {
+ return sRGB_DP3 * srgb;
+}
+
+// Converts a color from sRGB gamma encoding to linear.
+float4 linearize(float4 srgb) {
+ bool3 cutoff = srgb.rgb <= 0.04045;
+ float3 lower = srgb.rgb / 12.92;
+ float3 higher = pow((srgb.rgb + 0.055) / 1.055, 2.4);
+ srgb.rgb = mix(higher, lower, float3(cutoff));
+
+ return srgb;
+}
+float linearize(float v) {
+ return v <= 0.04045 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4);
+}
+
+// Converts a color from linear to sRGB gamma encoding.
+float4 unlinearize(float4 linear) {
+ bool3 cutoff = linear.rgb <= 0.0031308;
+ float3 lower = linear.rgb * 12.92;
+ float3 higher = pow(linear.rgb, 1.0 / 2.4) * 1.055 - 0.055;
+ linear.rgb = mix(higher, lower, float3(cutoff));
+
+ return linear;
+}
+float unlinearize(float v) {
+ return v <= 0.0031308 ? v * 12.92 : pow(v, 1.0 / 2.4) * 1.055 - 0.055;
+}
+
+// Compute the luminance of the provided color.
+//
+// Takes colors in linear RGB space. If your colors are gamma
+// encoded, linearize them before using them with this function.
+float luminance(float3 color) {
+ return dot(color, float3(0.2126f, 0.7152f, 0.0722f));
+}
+
+// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
+//
+// Takes colors in linear RGB space. If your colors are gamma
+// encoded, linearize them before using them with this function.
+float contrast_ratio(float3 color1, float3 color2) {
+ float l1 = luminance(color1);
+ float l2 = luminance(color2);
+ return (max(l1, l2) + 0.05f) / (min(l1, l2) + 0.05f);
+}
+
+// Return the fg if the contrast ratio is greater than min, otherwise
+// return a color that satisfies the contrast ratio. Currently, the color
+// is always white or black, whichever has the highest contrast ratio.
+//
+// Takes colors in linear RGB space. If your colors are gamma
+// encoded, linearize them before using them with this function.
+float4 contrasted_color(float min, float4 fg, float4 bg) {
+ float ratio = contrast_ratio(fg.rgb, bg.rgb);
+ if (ratio < min) {
+ float white_ratio = contrast_ratio(float3(1.0f), bg.rgb);
+ float black_ratio = contrast_ratio(float3(0.0f), bg.rgb);
+ if (white_ratio > black_ratio) {
+ return float4(1.0f);
+ } else {
+ return float4(0.0f, 0.0f, 0.0f, 1.0f);
+ }
+ }
+
+ return fg;
+}
+
+// Load a 4 byte RGBA non-premultiplied color and linearize
+// and convert it as necessary depending on the provided info.
+//
+// Returns a color in the Display P3 color space.
+//
+// If `display_p3` is true, then the provided color is assumed to
+// already be in the Display P3 color space, otherwise it's treated
+// as an sRGB color and is appropriately converted to Display P3.
+//
+// `linear` controls whether the returned color is linear or gamma encoded.
+float4 load_color(
+ uchar4 in_color,
+ bool display_p3,
+ bool linear
+) {
+ // 0 .. 255 -> 0.0 .. 1.0
+ float4 color = float4(in_color) / 255.0f;
+
+ // If our color is already in Display P3 and
+ // we aren't doing linear blending, then we
+ // already have the correct color here and
+ // can premultiply and return it.
+ if (display_p3 && !linear) {
+ color.rgb *= color.a;
+ return color;
+ }
+
+ // The color is in either the sRGB or Display P3 color space,
+ // so in either case, it's a color space which uses the sRGB
+ // transfer function, so we can use one function in order to
+ // linearize it in either case.
+ //
+ // Even if we aren't doing linear blending, the color
+ // needs to be in linear space to convert color spaces.
+ color = linearize(color);
+
+ // If we're *NOT* using display P3 colors, then we're dealing
+ // with an sRGB color, in which case we need to convert it in
+ // to the Display P3 color space, since our output is always
+ // Display P3.
+ if (!display_p3) {
+ color.rgb = srgb_to_display_p3(color.rgb);
+ }
+
+ // If we're not doing linear blending, then we need to
+ // unlinearize after doing the color space conversion.
+ if (!linear) {
+ color = unlinearize(color);
+ }
+
+ // Premultiply our color by its alpha.
+ color.rgb *= color.a;
+
+ return color;
+}
+
+//-------------------------------------------------------------------
+// Full Screen Vertex Shader
+//-------------------------------------------------------------------
+#pragma mark - Full Screen Vertex Shader
+
+struct FullScreenVertexOut {
+ float4 position [[position]];
+};
+
+vertex FullScreenVertexOut full_screen_vertex(
+ uint vid [[vertex_id]]
+) {
+ FullScreenVertexOut out;
+
+ float4 position;
+ position.x = (vid == 2) ? 3.0 : -1.0;
+ position.y = (vid == 0) ? -3.0 : 1.0;
+ position.zw = 1.0;
+
+ // Single triangle is clipped to viewport.
+ //
+ // X <- vid == 0: (-1, -3)
+ // |\
+ // | \
+ // | \
+ // |###\
+ // |#+# \ `+` is (0, 0). `#`s are viewport area.
+ // |### \
+ // X------X <- vid == 2: (3, 1)
+ // ^
+ // vid == 1: (-1, 1)
+
+ out.position = position;
+
+ return out;
+}
+
+//-------------------------------------------------------------------
+// Background Color Shader
+//-------------------------------------------------------------------
+#pragma mark - BG Color Shader
+
+fragment float4 bg_color_fragment(
+ FullScreenVertexOut in [[stage_in]],
+ constant Uniforms& uniforms [[buffer(1)]]
+) {
+ return load_color(
+ uniforms.bg_color,
+ uniforms.use_display_p3,
+ uniforms.use_linear_blending
+ );
+}
+
+//-------------------------------------------------------------------
+// Background Image Shader
+//-------------------------------------------------------------------
+#pragma mark - BG Image Shader
+
+struct BgImageVertexIn {
+ float opacity [[attribute(0)]];
+ uint8_t info [[attribute(1)]];
+};
+
+enum BgImagePosition : uint8_t {
+ // 4 bits of info.
+ BG_IMAGE_POSITION = 15u,
+
+ BG_IMAGE_TL = 0u,
+ BG_IMAGE_TC = 1u,
+ BG_IMAGE_TR = 2u,
+ BG_IMAGE_ML = 3u,
+ BG_IMAGE_MC = 4u,
+ BG_IMAGE_MR = 5u,
+ BG_IMAGE_BL = 6u,
+ BG_IMAGE_BC = 7u,
+ BG_IMAGE_BR = 8u,
+};
+
+enum BgImageFit : uint8_t {
+ // 2 bits of info shifted 4.
+ BG_IMAGE_FIT = 3u << 4,
+
+ BG_IMAGE_CONTAIN = 0u << 4,
+ BG_IMAGE_COVER = 1u << 4,
+ BG_IMAGE_STRETCH = 2u << 4,
+ BG_IMAGE_NO_FIT = 3u << 4,
+};
+
+enum BgImageRepeat : uint8_t {
+ // 1 bit of info shifted 6.
+ BG_IMAGE_REPEAT = 1u << 6,
+};
+
+struct BgImageVertexOut {
+ float4 position [[position]];
+ float4 bg_color [[flat]];
+ float2 offset [[flat]];
+ float2 scale [[flat]];
+ float opacity [[flat]];
+ bool repeat [[flat]];
+};
+
+vertex BgImageVertexOut bg_image_vertex(
+ uint vid [[vertex_id]],
+ BgImageVertexIn in [[stage_in]],
+ texture2d<float> image [[texture(0)]],
+ constant Uniforms& uniforms [[buffer(1)]]
+) {
+ BgImageVertexOut out;
+
+ float4 position;
+ position.x = (vid == 2) ? 3.0 : -1.0;
+ position.y = (vid == 0) ? -3.0 : 1.0;
+ position.zw = 1.0;
+
+ // Single triangle is clipped to viewport.
+ //
+ // X <- vid == 0: (-1, -3)
+ // |\
+ // | \
+ // | \
+ // |###\
+ // |#+# \ `+` is (0, 0). `#`s are viewport area.
+ // |### \
+ // X------X <- vid == 2: (3, 1)
+ // ^
+ // vid == 1: (-1, 1)
+
+ out.position = position;
+
+ out.opacity = in.opacity;
+
+ out.repeat = (in.info & BG_IMAGE_REPEAT) == BG_IMAGE_REPEAT;
+
+ float2 screen_size = uniforms.screen_size;
+ float2 tex_size = float2(image.get_width(), image.get_height());
+
+ float2 dest_size = tex_size;
+ switch (in.info & BG_IMAGE_FIT) {
+ // For `contain` we scale by a factor that makes the image
+ // width match the screen width or makes the image height
+ // match the screen height, whichever is smaller.
+ case BG_IMAGE_CONTAIN: {
+ float scale = min(screen_size.x / tex_size.x, screen_size.y / tex_size.y);
+ dest_size = tex_size * scale;
+ } break;
+
+ // For `cover` we scale by a factor that makes the image
+ // width match the screen width or makes the image height
+ // match the screen height, whichever is larger.
+ case BG_IMAGE_COVER: {
+ float scale = max(screen_size.x / tex_size.x, screen_size.y / tex_size.y);
+ dest_size = tex_size * scale;
+ } break;
+
+ // For `stretch` we stretch the image to the size of
+ // the screen without worrying about aspect ratio.
+ case BG_IMAGE_STRETCH: {
+ dest_size = screen_size;
+ } break;
+
+ // For `none` we just use the original texture size.
+ case BG_IMAGE_NO_FIT: {
+ dest_size = tex_size;
+ } break;
+ }
+
+ float2 start = float2(0.0);
+ float2 mid = (screen_size - dest_size) / 2;
+ float2 end = screen_size - dest_size;
+
+ float2 dest_offset = mid;
+ switch (in.info & BG_IMAGE_POSITION) {
+ case BG_IMAGE_TL: {
+ dest_offset = float2(start.x, start.y);
+ } break;
+ case BG_IMAGE_TC: {
+ dest_offset = float2(mid.x, start.y);
+ } break;
+ case BG_IMAGE_TR: {
+ dest_offset = float2(end.x, start.y);
+ } break;
+ case BG_IMAGE_ML: {
+ dest_offset = float2(start.x, mid.y);
+ } break;
+ case BG_IMAGE_MC: {
+ dest_offset = float2(mid.x, mid.y);
+ } break;
+ case BG_IMAGE_MR: {
+ dest_offset = float2(end.x, mid.y);
+ } break;
+ case BG_IMAGE_BL: {
+ dest_offset = float2(start.x, end.y);
+ } break;
+ case BG_IMAGE_BC: {
+ dest_offset = float2(mid.x, end.y);
+ } break;
+ case BG_IMAGE_BR: {
+ dest_offset = float2(end.x, end.y);
+ } break;
+ }
+
+ out.offset = dest_offset;
+ out.scale = tex_size / dest_size;
+
+ // We load a fully opaque version of the bg color and combine it with
+ // the alpha separately, because we need these as separate values in
+ // the framgment shader.
+ out.bg_color = float4(load_color(
+ uchar4(uniforms.bg_color.rgb, 255),
+ uniforms.use_display_p3,
+ uniforms.use_linear_blending
+ ).rgb, float(uniforms.bg_color.a) / 255.0);
+
+ return out;
+}
+
+fragment float4 bg_image_fragment(
+ BgImageVertexOut in [[stage_in]],
+ texture2d<float> image [[texture(0)]],
+ constant Uniforms& uniforms [[buffer(1)]]
+) {
+ constexpr sampler textureSampler(
+ coord::pixel,
+ address::clamp_to_zero,
+ filter::linear
+ );
+
+ // Our texture coordinate is based on the screen position, offset by the
+ // dest rect origin, and scaled by the ratio between the dest rect size
+ // and the original texture size, which effectively scales the original
+ // size of the texture to the dest rect size.
+ float2 tex_coord = (in.position.xy - in.offset) * in.scale;
+
+ // If we need to repeat the texture, wrap the coordinates.
+ if (in.repeat) {
+ float2 tex_size = float2(image.get_width(), image.get_height());
+
+ tex_coord = fmod(fmod(tex_coord, tex_size) + tex_size, tex_size);
+ }
+
+ float4 rgba = image.sample(textureSampler, tex_coord);
+
+ if (!uniforms.use_linear_blending) {
+ rgba = unlinearize(rgba);
+ }
+
+ // Premultiply the bg image.
+ rgba.rgb *= rgba.a;
+
+ // Multiply it by the configured opacity, but cap it at
+ // the value that will make it fully opaque relative to
+ // the background color alpha, so it isn't overexposed.
+ rgba *= min(in.opacity, 1.0 / in.bg_color.a);
+
+ // Blend it on to a fully opaque version of the background color.
+ rgba += max(float4(0.0), float4(in.bg_color.rgb, 1.0) * (1.0 - rgba.a));
+
+ // Multiply everything by the background color alpha.
+ rgba *= in.bg_color.a;
+
+ return rgba;
+}
+
+//-------------------------------------------------------------------
+// Cell Background Shader
+//-------------------------------------------------------------------
+#pragma mark - Cell BG Shader
+
+fragment float4 cell_bg_fragment(
+ FullScreenVertexOut in [[stage_in]],
+ constant Uniforms& uniforms [[buffer(1)]],
+ constant uchar4 *cells [[buffer(2)]]
+) {
+ int2 grid_pos = int2(floor((in.position.xy - uniforms.grid_padding.wx) / uniforms.cell_size));
+
+ float4 bg = float4(0.0);
+
+ // Clamp x position, extends edge bg colors in to padding on sides.
+ if (grid_pos.x < 0) {
+ if (uniforms.padding_extend & EXTEND_LEFT) {
+ grid_pos.x = 0;
+ } else {
+ return bg;
+ }
+ } else if (grid_pos.x > uniforms.grid_size.x - 1) {
+ if (uniforms.padding_extend & EXTEND_RIGHT) {
+ grid_pos.x = uniforms.grid_size.x - 1;
+ } else {
+ return bg;
+ }
+ }
+
+ // Clamp y position if we should extend, otherwise discard if out of bounds.
+ if (grid_pos.y < 0) {
+ if (uniforms.padding_extend & EXTEND_UP) {
+ grid_pos.y = 0;
+ } else {
+ return bg;
+ }
+ } else if (grid_pos.y > uniforms.grid_size.y - 1) {
+ if (uniforms.padding_extend & EXTEND_DOWN) {
+ grid_pos.y = uniforms.grid_size.y - 1;
+ } else {
+ return bg;
+ }
+ }
+
+ // Load the color for the cell.
+ uchar4 cell_color = cells[grid_pos.y * uniforms.grid_size.x + grid_pos.x];
+
+ // Convert the color and return it.
+ //
+ // TODO: It might be a good idea to do a pass before this
+ // to convert all of the bg colors, so we don't waste
+ // a bunch of work converting the cell color in every
+ // fragment of each cell. It's not the most epxensive
+ // operation, but it is still wasted work.
+ return load_color(
+ cell_color,
+ uniforms.use_display_p3,
+ uniforms.use_linear_blending
+ );
+}
+
+//-------------------------------------------------------------------
+// Cell Text Shader
+//-------------------------------------------------------------------
+#pragma mark - Cell Text Shader
+
+enum CellTextAtlas : uint8_t {
+ ATLAS_GRAYSCALE = 0u,
+ ATLAS_COLOR = 1u,
+};
+
+// We use a packed struct of bools for misc properties of the glyph.
+enum CellTextBools : uint8_t {
+ // Don't apply min contrast to this glyph.
+ NO_MIN_CONTRAST = 1u,
+ // This is the cursor glyph.
+ IS_CURSOR_GLYPH = 2u,
+};
+
+struct CellTextVertexIn {
+ // The position of the glyph in the texture (x, y)
+ uint2 glyph_pos [[attribute(0)]];
+
+ // The size of the glyph in the texture (w, h)
+ uint2 glyph_size [[attribute(1)]];
+
+ // The left and top bearings for the glyph (x, y)
+ int2 bearings [[attribute(2)]];
+
+ // The grid coordinates (x, y) where x < columns and y < rows
+ ushort2 grid_pos [[attribute(3)]];
+
+ // The color of the rendered text glyph.
+ uchar4 color [[attribute(4)]];
+
+ // Which atlas to sample for our glyph.
+ uint8_t atlas [[attribute(5)]];
+
+ // Misc properties of the glyph.
+ uint8_t bools [[attribute(6)]];
+};
+
+struct CellTextVertexOut {
+ float4 position [[position]];
+ uint8_t atlas [[flat]];
+ float4 color [[flat]];
+ float4 bg_color [[flat]];
+ float2 tex_coord;
+};
+
+vertex CellTextVertexOut cell_text_vertex(
+ uint vid [[vertex_id]],
+ CellTextVertexIn in [[stage_in]],
+ constant Uniforms& uniforms [[buffer(1)]],
+ constant uchar4 *bg_colors [[buffer(2)]]
+) {
+ // Convert the grid x, y into world space x, y by accounting for cell size
+ float2 cell_pos = uniforms.cell_size * float2(in.grid_pos);
+
+ // We use a triangle strip with 4 vertices to render quads,
+ // so we determine which corner of the cell this vertex is in
+ // based on the vertex ID.
+ //
+ // 0 --> 1
+ // | .'|
+ // | / |
+ // | L |
+ // 2 --> 3
+ //
+ // 0 = top-left (0, 0)
+ // 1 = top-right (1, 0)
+ // 2 = bot-left (0, 1)
+ // 3 = bot-right (1, 1)
+ float2 corner;
+ corner.x = float(vid == 1 || vid == 3);
+ corner.y = float(vid == 2 || vid == 3);
+
+ CellTextVertexOut out;
+ out.atlas = in.atlas;
+
+ // === Grid Cell ===
+ // +X
+ // 0,0--...->
+ // |
+ // . offset.x = bearings.x
+ // +Y. .|.
+ // . | |
+ // | cell_pos -> +-------+ _.
+ // v ._| |_. _|- offset.y = cell_size.y - bearings.y
+ // | | .###. | |
+ // | | #...# | |
+ // glyph_size.y -+ | ##### | |
+ // | | #.... | +- bearings.y
+ // |_| .#### | |
+ // | |_|
+ // +-------+
+ // |_._|
+ // |
+ // glyph_size.x
+ //
+ // In order to get the top left of the glyph, we compute an offset based on
+ // the bearings. The Y bearing is the distance from the bottom of the cell
+ // to the top of the glyph, so we subtract it from the cell height to get
+ // the y offset. The X bearing is the distance from the left of the cell
+ // to the left of the glyph, so it works as the x offset directly.
+
+ float2 size = float2(in.glyph_size);
+ float2 offset = float2(in.bearings);
+
+ offset.y = uniforms.cell_size.y - offset.y;
+
+ // Calculate the final position of the cell which uses our glyph size
+ // and glyph offset to create the correct bounding box for the glyph.
+ cell_pos = cell_pos + size * corner + offset;
+ out.position =
+ uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
+
+ // Calculate the texture coordinate in pixels. This is NOT normalized
+ // (between 0.0 and 1.0), and does not need to be, since the texture will
+ // be sampled with pixel coordinate mode.
+ out.tex_coord = float2(in.glyph_pos) + float2(in.glyph_size) * corner;
+
+ // Get our color. We always fetch a linearized version to
+ // make it easier to handle minimum contrast calculations.
+ out.color = load_color(
+ in.color,
+ uniforms.use_display_p3,
+ true
+ );
+
+ // Get the BG color
+ out.bg_color = load_color(
+ bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x],
+ uniforms.use_display_p3,
+ true
+ );
+ // Blend it with the global bg color
+ float4 global_bg = load_color(
+ uniforms.bg_color,
+ uniforms.use_display_p3,
+ true
+ );
+ out.bg_color += global_bg * (1.0 - out.bg_color.a);
+
+ // If we have a minimum contrast, we need to check if we need to
+ // change the color of the text to ensure it has enough contrast
+ // with the background.
+ if (uniforms.min_contrast > 1.0f && (in.bools & NO_MIN_CONTRAST) == 0) {
+ // Ensure our minimum contrast
+ out.color = contrasted_color(uniforms.min_contrast, out.color, out.bg_color);
+ }
+
+ // Check if current position is under cursor (including wide cursor)
+ bool is_cursor_pos = (
+ in.grid_pos.x == uniforms.cursor_pos.x ||
+ uniforms.cursor_wide &&
+ in.grid_pos.x == uniforms.cursor_pos.x + 1
+ ) && in.grid_pos.y == uniforms.cursor_pos.y;
+
+ // If this cell is the cursor cell, but we're not processing
+ // the cursor glyph itself, then we need to change the color.
+ if ((in.bools & IS_CURSOR_GLYPH) == 0 && is_cursor_pos) {
+ out.color = load_color(
+ uniforms.cursor_color,
+ uniforms.use_display_p3,
+ true
+ );
+ }
+
+ return out;
+}
+
+fragment float4 cell_text_fragment(
+ CellTextVertexOut in [[stage_in]],
+ texture2d<float> textureGrayscale [[texture(0)]],
+ texture2d<float> textureColor [[texture(1)]],
+ constant Uniforms& uniforms [[buffer(1)]]
+) {
+ constexpr sampler textureSampler(
+ coord::pixel,
+ address::clamp_to_edge,
+ filter::nearest
+ );
+
+ switch (in.atlas) {
+ default:
+ case ATLAS_GRAYSCALE: {
+ // Our input color is always linear.
+ float4 color = in.color;
+
+ // If we're not doing linear blending, then we need to
+ // re-apply the gamma encoding to our color manually.
+ //
+ // Since the alpha is premultiplied, we need to divide
+ // it out before unlinearizing and re-multiply it after.
+ if (!uniforms.use_linear_blending) {
+ color.rgb /= color.a;
+ color = unlinearize(color);
+ color.rgb *= color.a;
+ }
+
+ // Fetch our alpha mask for this pixel.
+ float a = textureGrayscale.sample(textureSampler, in.tex_coord).r;
+
+ // Linear blending weight correction corrects the alpha value to
+ // produce blending results which match gamma-incorrect blending.
+ if (uniforms.use_linear_correction) {
+ // Short explanation of how this works:
+ //
+ // We get the luminances of the foreground and background colors,
+ // and then unlinearize them and perform blending on them. This
+ // gives us our desired luminance, which we derive our new alpha
+ // value from by mapping the range [bg_l, fg_l] to [0, 1], since
+ // our final blend will be a linear interpolation from bg to fg.
+ //
+ // This yields virtually identical results for grayscale blending,
+ // and very similar but non-identical results for color blending.
+ float4 bg = in.bg_color;
+ float fg_l = luminance(color.rgb);
+ float bg_l = luminance(bg.rgb);
+ // To avoid numbers going haywire, we don't apply correction
+ // when the bg and fg luminances are within 0.001 of each other.
+ if (abs(fg_l - bg_l) > 0.001) {
+ float blend_l = linearize(unlinearize(fg_l) * a + unlinearize(bg_l) * (1.0 - a));
+ a = clamp((blend_l - bg_l) / (fg_l - bg_l), 0.0, 1.0);
+ }
+ }
+
+ // Multiply our whole color by the alpha mask.
+ // Since we use premultiplied alpha, this is
+ // the correct way to apply the mask.
+ color *= a;
+
+ return color;
+ }
+
+ case ATLAS_COLOR: {
+ // For now, we assume that color glyphs
+ // are already premultiplied linear colors.
+ float4 color = textureColor.sample(textureSampler, in.tex_coord);
+
+ // If we're doing linear blending, we can return this right away.
+ if (uniforms.use_linear_blending) {
+ return color;
+ }
+
+ // Otherwise we need to unlinearize the color. Since the alpha is
+ // premultiplied, we need to divide it out before unlinearizing.
+ color.rgb /= color.a;
+ color = unlinearize(color);
+ color.rgb *= color.a;
+
+ return color;
+ }
+ }
+}
+//-------------------------------------------------------------------
+// Image Shader
+//-------------------------------------------------------------------
+#pragma mark - Image Shader
+
+struct ImageVertexIn {
+ // The grid coordinates (x, y) where x < columns and y < rows where
+ // the image will be rendered. It will be rendered from the top left.
+ float2 grid_pos [[attribute(0)]];
+
+ // Offset in pixels from the top-left of the cell to make the top-left
+ // corner of the image.
+ float2 cell_offset [[attribute(1)]];
+
+ // The source rectangle of the texture to sample from.
+ float4 source_rect [[attribute(2)]];
+
+ // The final width/height of the image in pixels.
+ float2 dest_size [[attribute(3)]];
+};
+
+struct ImageVertexOut {
+ float4 position [[position]];
+ float2 tex_coord;
+};
+
+vertex ImageVertexOut image_vertex(
+ uint vid [[vertex_id]],
+ ImageVertexIn in [[stage_in]],
+ texture2d<uint> image [[texture(0)]],
+ constant Uniforms& uniforms [[buffer(1)]]
+) {
+ // We use a triangle strip with 4 vertices to render quads,
+ // so we determine which corner of the cell this vertex is in
+ // based on the vertex ID.
+ //
+ // 0 --> 1
+ // | .'|
+ // | / |
+ // | L |
+ // 2 --> 3
+ //
+ // 0 = top-left (0, 0)
+ // 1 = top-right (1, 0)
+ // 2 = bot-left (0, 1)
+ // 3 = bot-right (1, 1)
+ float2 corner;
+ corner.x = float(vid == 1 || vid == 3);
+ corner.y = float(vid == 2 || vid == 3);
+
+ // The texture coordinates start at our source x/y
+ // and add the width/height depending on the corner.
+ //
+ // We don't need to normalize because we use pixel addressing for our sampler.
+ float2 tex_coord = in.source_rect.xy;
+ tex_coord += in.source_rect.zw * corner;
+
+ ImageVertexOut out;
+
+ // The position of our image starts at the top-left of the grid cell and
+ // adds the source rect width/height components.
+ float2 image_pos = (uniforms.cell_size * in.grid_pos) + in.cell_offset;
+ image_pos += in.dest_size * corner;
+
+ out.position =
+ uniforms.projection_matrix * float4(image_pos.x, image_pos.y, 0.0f, 1.0f);
+ out.tex_coord = tex_coord;
+ return out;
+}
+
+fragment float4 image_fragment(
+ ImageVertexOut in [[stage_in]],
+ texture2d<float> image [[texture(0)]],
+ constant Uniforms& uniforms [[buffer(1)]]
+) {
+ constexpr sampler textureSampler(
+ coord::pixel,
+ address::clamp_to_edge,
+ filter::linear
+ );
+
+ float4 rgba = image.sample(textureSampler, in.tex_coord);
+
+ if (!uniforms.use_linear_blending) {
+ rgba = unlinearize(rgba);
+ }
+
+ rgba.rgb *= rgba.a;
+
+ return rgba;
+}
+
diff --git a/vendor/libghostty-vt/src/renderer/shaders/shadertoy_prefix.glsl b/vendor/libghostty-vt/src/renderer/shaders/shadertoy_prefix.glsl
new file mode 100644
index 00000000..4b6d091b
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/shadertoy_prefix.glsl
@@ -0,0 +1,52 @@
+#version 430 core
+
+layout(binding = 1, std140) uniform Globals {
+ uniform vec3 iResolution;
+ uniform float iTime;
+ uniform float iTimeDelta;
+ uniform float iFrameRate;
+ uniform int iFrame;
+ uniform float iChannelTime[4];
+ uniform vec3 iChannelResolution[4];
+ uniform vec4 iMouse;
+ uniform vec4 iDate;
+ uniform float iSampleRate;
+ uniform vec4 iCurrentCursor;
+ uniform vec4 iPreviousCursor;
+ uniform vec4 iCurrentCursorColor;
+ uniform vec4 iPreviousCursorColor;
+ uniform int iCurrentCursorStyle;
+ uniform int iPreviousCursorStyle;
+ uniform int iCursorVisible;
+ uniform float iTimeCursorChange;
+ uniform float iTimeFocus;
+ uniform int iFocus;
+ uniform vec3 iPalette[256];
+ uniform vec3 iBackgroundColor;
+ uniform vec3 iForegroundColor;
+ uniform vec3 iCursorColor;
+ uniform vec3 iCursorText;
+ uniform vec3 iSelectionForegroundColor;
+ uniform vec3 iSelectionBackgroundColor;
+};
+
+#define CURSORSTYLE_BLOCK 0
+#define CURSORSTYLE_BLOCK_HOLLOW 1
+#define CURSORSTYLE_BAR 2
+#define CURSORSTYLE_UNDERLINE 3
+#define CURSORSTYLE_LOCK 4
+
+layout(binding = 0) uniform sampler2D iChannel0;
+
+// These are unused currently by Ghostty:
+// layout(binding = 1) uniform sampler2D iChannel1;
+// layout(binding = 2) uniform sampler2D iChannel2;
+// layout(binding = 3) uniform sampler2D iChannel3;
+
+layout(location = 0) in vec4 gl_FragCoord;
+layout(location = 0) out vec4 _fragColor;
+
+#define texture2D texture
+
+void mainImage( out vec4 fragColor, in vec2 fragCoord );
+void main() { mainImage (_fragColor, gl_FragCoord.xy); }
diff --git a/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_crt.glsl b/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_crt.glsl
new file mode 100644
index 00000000..b8d6dbb4
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_crt.glsl
@@ -0,0 +1,59 @@
+// This shader is NOT BUILD INTO Ghostty. This is only here for unit tests.
+
+// Loosely based on postprocessing shader by inigo quilez, License Creative
+// Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
+
+vec2 curve(vec2 uv)
+{
+ uv = (uv - 0.5) * 2.0;
+ uv *= 1.1;
+ uv.x *= 1.0 + pow((abs(uv.y) / 5.0), 2.0);
+ uv.y *= 1.0 + pow((abs(uv.x) / 4.0), 2.0);
+ uv = (uv / 2.0) + 0.5;
+ uv = uv *0.92 + 0.04;
+ return uv;
+}
+void mainImage( out vec4 fragColor, in vec2 fragCoord )
+{
+ vec2 q = fragCoord.xy / iResolution.xy;
+ vec2 uv = q;
+ uv = curve( uv );
+ vec3 oricol = texture( iChannel0, vec2(q.x,q.y) ).xyz;
+ vec3 col;
+ float x = sin(0.3*iTime+uv.y*21.0)*sin(0.7*iTime+uv.y*29.0)*sin(0.3+0.33*iTime+uv.y*31.0)*0.0017;
+
+ col.r = texture(iChannel0,vec2(x+uv.x+0.001,uv.y+0.001)).x+0.05;
+ col.g = texture(iChannel0,vec2(x+uv.x+0.000,uv.y-0.002)).y+0.05;
+ col.b = texture(iChannel0,vec2(x+uv.x-0.002,uv.y+0.000)).z+0.05;
+ col.r += 0.08*texture(iChannel0,0.75*vec2(x+0.025, -0.027)+vec2(uv.x+0.001,uv.y+0.001)).x;
+ col.g += 0.05*texture(iChannel0,0.75*vec2(x+-0.022, -0.02)+vec2(uv.x+0.000,uv.y-0.002)).y;
+ col.b += 0.08*texture(iChannel0,0.75*vec2(x+-0.02, -0.018)+vec2(uv.x-0.002,uv.y+0.000)).z;
+
+ col = clamp(col*0.6+0.4*col*col*1.0,0.0,1.0);
+
+ float vig = (0.0 + 1.0*16.0*uv.x*uv.y*(1.0-uv.x)*(1.0-uv.y));
+ col *= vec3(pow(vig,0.3));
+
+ col *= vec3(0.95,1.05,0.95);
+ col *= 2.8;
+
+ float scans = clamp( 0.35+0.35*sin(3.5*iTime+uv.y*iResolution.y*1.5), 0.0, 1.0);
+
+ float s = pow(scans,1.7);
+ col = col*vec3( 0.4+0.7*s) ;
+
+ col *= 1.0+0.01*sin(110.0*iTime);
+ if (uv.x < 0.0 || uv.x > 1.0)
+ col *= 0.0;
+ if (uv.y < 0.0 || uv.y > 1.0)
+ col *= 0.0;
+
+ col*=1.0-0.65*vec3(clamp((mod(fragCoord.x, 2.0)-1.0)*2.0,0.0,1.0));
+
+ float comp = smoothstep( 0.1, 0.9, sin(iTime) );
+
+ // Remove the next line to stop cross-fade between original and postprocess
+// col = mix( col, oricol, comp );
+
+ fragColor = vec4(col,1.0);
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_focus.glsl b/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_focus.glsl
new file mode 100644
index 00000000..9fc2304d
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_focus.glsl
@@ -0,0 +1,41 @@
+// Test shader for iTimeFocus and iFocus
+// Shows border when focused, green fade that restarts on each focus gain
+void mainImage(out vec4 fragColor, in vec2 fragCoord) {
+ vec2 uv = fragCoord / iResolution.xy;
+
+ // Sample the terminal content
+ vec4 terminal = texture2D(iChannel0, uv);
+ vec3 color = terminal.rgb;
+
+ if (iFocus > 0) {
+ // FOCUSED: Add border and fading green overlay
+
+ // Calculate time since focus was gained
+ float timeSinceFocus = iTime - iTimeFocus;
+
+ // Green fade: starts at 1.0 (full green), fades to 0.0 over 3 seconds
+ float fadeOut = max(0.0, 1.0 - (timeSinceFocus / 3.0));
+
+ // Add green overlay that fades out
+ color = mix(color, vec3(0.0, 1.0, 0.0), fadeOut * 0.4);
+
+ // Add border (5 pixels)
+ float borderSize = 5.0;
+ vec2 pixelCoord = fragCoord;
+ bool isBorder = pixelCoord.x < borderSize ||
+ pixelCoord.x > iResolution.x - borderSize ||
+ pixelCoord.y < borderSize ||
+ pixelCoord.y > iResolution.y - borderSize;
+
+ if (isBorder) {
+ // Bright cyan border that pulses subtly
+ float pulse = sin(timeSinceFocus * 2.0) * 0.1 + 0.9;
+ color = vec3(0.0, 1.0, 1.0) * pulse;
+ }
+ } else {
+ // UNFOCUSED: Solid red overlay (no border)
+ color = mix(color, vec3(1.0, 0.0, 0.0), 0.3);
+ }
+
+ fragColor = vec4(color, 1.0);
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_invalid.glsl b/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_invalid.glsl
new file mode 100644
index 00000000..1cff1c7c
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_invalid.glsl
@@ -0,0 +1,12 @@
+vec2 curve(vec2 uv)
+{
+ uv = (uv - 0.5) * 2.0;
+ uv *= 1.1;
+ uv.x *= 1.0 + pow((abs(uv.y) / 5.0), 2.0);
+ uv.y *= 1.0 + pow((abs(uv.x) / 4.0), 2.0);
+ uv = (uv / 2.0) + 0.5;
+ uv = uv *0.92 + 0.04;
+ return uv;
+}
+
+// Missing mainImage!
diff --git a/vendor/libghostty-vt/src/renderer/shadertoy.zig b/vendor/libghostty-vt/src/renderer/shadertoy.zig
new file mode 100644
index 00000000..556c2829
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shadertoy.zig
@@ -0,0 +1,427 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const Allocator = std.mem.Allocator;
+const ArenaAllocator = std.heap.ArenaAllocator;
+const glslang = @import("glslang");
+const spvcross = @import("spirv_cross");
+const configpkg = @import("../config.zig");
+
+const log = std.log.scoped(.shadertoy);
+
+/// The uniform struct used for shadertoy shaders.
+pub const Uniforms = extern struct {
+ resolution: [3]f32 align(16),
+ time: f32 align(4),
+ time_delta: f32 align(4),
+ frame_rate: f32 align(4),
+ frame: i32 align(4),
+ channel_time: [4][4]f32 align(16),
+ channel_resolution: [4][4]f32 align(16),
+ mouse: [4]f32 align(16),
+ date: [4]f32 align(16),
+ sample_rate: f32 align(4),
+ current_cursor: [4]f32 align(16),
+ previous_cursor: [4]f32 align(16),
+ current_cursor_color: [4]f32 align(16),
+ previous_cursor_color: [4]f32 align(16),
+ current_cursor_style: i32 align(4),
+ previous_cursor_style: i32 align(4),
+ cursor_visible: i32 align(4),
+ cursor_change_time: f32 align(4),
+ time_focus: f32 align(4),
+ focus: i32 align(4),
+ palette: [256][4]f32 align(16),
+ background_color: [4]f32 align(16),
+ foreground_color: [4]f32 align(16),
+ cursor_color: [4]f32 align(16),
+ cursor_text: [4]f32 align(16),
+ selection_background_color: [4]f32 align(16),
+ selection_foreground_color: [4]f32 align(16),
+};
+
+/// The target to load shaders for.
+pub const Target = enum { glsl, msl };
+
+/// Load a set of shaders from files and convert them to the target
+/// format. The shader order is preserved.
+pub fn loadFromFiles(
+ alloc_gpa: Allocator,
+ paths: configpkg.RepeatablePath,
+ target: Target,
+) ![]const [:0]const u8 {
+ var list: std.ArrayList([:0]const u8) = .empty;
+ defer list.deinit(alloc_gpa);
+ errdefer for (list.items) |shader| alloc_gpa.free(shader);
+
+ for (paths.value.items) |item| {
+ const path, const optional = switch (item) {
+ .optional => |path| .{ path, true },
+ .required => |path| .{ path, false },
+ };
+
+ const shader = loadFromFile(alloc_gpa, path, target) catch |err| {
+ if (err == error.FileNotFound and optional) {
+ continue;
+ }
+
+ return err;
+ };
+ log.info("loaded custom shader path={s}", .{path});
+ try list.append(alloc_gpa, shader);
+ }
+
+ return try list.toOwnedSlice(alloc_gpa);
+}
+
+/// Load a single shader from a file and convert it to the target language
+/// ready to be used with renderers.
+pub fn loadFromFile(
+ alloc_gpa: Allocator,
+ path: []const u8,
+ target: Target,
+) ![:0]const u8 {
+ var arena = ArenaAllocator.init(alloc_gpa);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ // Read it all into memory -- we don't expect shaders to be large.
+ const src = src: {
+ // Load the shader file
+ const cwd = std.fs.cwd();
+ const file = try cwd.openFile(path, .{});
+ defer file.close();
+
+ break :src try file.readToEndAlloc(
+ alloc,
+ 4 * 1024 * 1024, // 4MB
+ );
+ };
+
+ // Convert to full GLSL
+ const glsl: [:0]const u8 = glsl: {
+ var stream: std.Io.Writer.Allocating = .init(alloc);
+ try glslFromShader(&stream.writer, src);
+ try stream.writer.writeByte(0);
+ break :glsl stream.written()[0 .. stream.written().len - 1 :0];
+ };
+
+ // Convert to SPIR-V
+ const spirv: []const u8 = spirv: {
+ var stream: std.Io.Writer.Allocating = .init(alloc);
+ var errlog: SpirvLog = .{ .alloc = alloc };
+ defer errlog.deinit();
+ spirvFromGlsl(&stream.writer, &errlog, glsl) catch |err| {
+ if (errlog.info.len > 0 or errlog.debug.len > 0) {
+ log.warn("spirv error path={s} info={s} debug={s}", .{
+ path,
+ errlog.info,
+ errlog.debug,
+ });
+ }
+
+ return err;
+ };
+
+ // SpirV pointer must be aligned to 4 bytes since we expect
+ // a slice of words.
+ var list: std.ArrayListAligned(u8, .of(u32)) = .empty;
+ try list.appendSlice(alloc, stream.written());
+ break :spirv list.items;
+ };
+
+ // Convert to MSL
+ return switch (target) {
+ // Important: using the alloc_gpa here on purpose because this
+ // is the final result that will be returned to the caller.
+ .glsl => try glslFromSpv(alloc_gpa, spirv),
+ .msl => try mslFromSpv(alloc_gpa, spirv),
+ };
+}
+
+/// Convert a ShaderToy shader into valid GLSL.
+///
+/// ShaderToy shaders aren't full shaders, they're just implementing a
+/// mainImage function and don't define any of the uniforms. This function
+/// will convert the ShaderToy shader into a valid GLSL shader that can be
+/// compiled and linked.
+pub fn glslFromShader(writer: *std.Io.Writer, src: []const u8) !void {
+ const prefix = @embedFile("shaders/shadertoy_prefix.glsl");
+ try writer.writeAll(prefix);
+ try writer.writeAll("\n\n");
+ try writer.writeAll(src);
+}
+
+/// Convert a GLSL shader into SPIR-V assembly.
+pub fn spirvFromGlsl(
+ writer: *std.Io.Writer,
+ errlog: ?*SpirvLog,
+ src: [:0]const u8,
+) !void {
+ // So we can run unit tests without fear.
+ if (builtin.is_test) try glslang.testing.ensureInit();
+
+ const c = glslang.c;
+ const input: c.glslang_input_t = .{
+ .language = c.GLSLANG_SOURCE_GLSL,
+ .stage = c.GLSLANG_STAGE_FRAGMENT,
+ .client = c.GLSLANG_CLIENT_VULKAN,
+ .client_version = c.GLSLANG_TARGET_VULKAN_1_2,
+ .target_language = c.GLSLANG_TARGET_SPV,
+ .target_language_version = c.GLSLANG_TARGET_SPV_1_5,
+ .code = src.ptr,
+ .default_version = 100,
+ .default_profile = c.GLSLANG_NO_PROFILE,
+ .force_default_version_and_profile = 0,
+ .forward_compatible = 0,
+ .messages = c.GLSLANG_MSG_DEFAULT_BIT,
+ .resource = c.glslang_default_resource(),
+ };
+
+ const shader = try glslang.Shader.create(&input);
+ defer shader.delete();
+
+ shader.preprocess(&input) catch |err| {
+ if (errlog) |ptr| ptr.fromShader(shader) catch {};
+ return err;
+ };
+ shader.parse(&input) catch |err| {
+ if (errlog) |ptr| ptr.fromShader(shader) catch {};
+ return err;
+ };
+
+ const program = try glslang.Program.create();
+ defer program.delete();
+ program.addShader(shader);
+ program.link(
+ c.GLSLANG_MSG_SPV_RULES_BIT |
+ c.GLSLANG_MSG_VULKAN_RULES_BIT,
+ ) catch |err| {
+ if (errlog) |ptr| ptr.fromProgram(program) catch {};
+ return err;
+ };
+ program.spirvGenerate(c.GLSLANG_STAGE_FRAGMENT);
+ const size = program.spirvGetSize();
+ const ptr = try program.spirvGetPtr();
+ const ptr_u8: [*]u8 = @ptrCast(ptr);
+ const slice_u8: []u8 = ptr_u8[0 .. size * 4];
+ try writer.writeAll(slice_u8);
+}
+
+/// Retrieve errors from spirv compilation.
+pub const SpirvLog = struct {
+ alloc: Allocator,
+ info: [:0]const u8 = "",
+ debug: [:0]const u8 = "",
+
+ pub fn deinit(self: *const SpirvLog) void {
+ if (self.info.len > 0) self.alloc.free(self.info);
+ if (self.debug.len > 0) self.alloc.free(self.debug);
+ }
+
+ fn fromShader(self: *SpirvLog, shader: *glslang.Shader) !void {
+ const info = try shader.getInfoLog();
+ const debug = try shader.getDebugInfoLog();
+ self.info = "";
+ self.debug = "";
+ if (info.len > 0) self.info = try self.alloc.dupeZ(u8, info);
+ if (debug.len > 0) self.debug = try self.alloc.dupeZ(u8, debug);
+ }
+
+ fn fromProgram(self: *SpirvLog, program: *glslang.Program) !void {
+ const info = try program.getInfoLog();
+ const debug = try program.getDebugInfoLog();
+ self.info = "";
+ self.debug = "";
+ if (info.len > 0) self.info = try self.alloc.dupeZ(u8, info);
+ if (debug.len > 0) self.debug = try self.alloc.dupeZ(u8, debug);
+ }
+};
+
+/// Convert SPIR-V binary to MSL.
+pub fn mslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 {
+ const c = spvcross.c;
+ return try spvCross(alloc, spvcross.c.SPVC_BACKEND_MSL, spv, (struct {
+ fn setOptions(options: c.spvc_compiler_options) error{SpvcFailed}!void {
+ // We enable decoration binding, because we need this
+ // to properly locate the uniform block to index 1.
+ if (c.spvc_compiler_options_set_bool(
+ options,
+ c.SPVC_COMPILER_OPTION_MSL_ENABLE_DECORATION_BINDING,
+ c.SPVC_TRUE,
+ ) != c.SPVC_SUCCESS) {
+ return error.SpvcFailed;
+ }
+ }
+ }).setOptions);
+}
+
+/// Convert SPIR-V binary to GLSL.
+pub fn glslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 {
+ const GLSL_VERSION = 430;
+
+ const c = spvcross.c;
+ return try spvCross(alloc, c.SPVC_BACKEND_GLSL, spv, (struct {
+ fn setOptions(options: c.spvc_compiler_options) error{SpvcFailed}!void {
+ if (c.spvc_compiler_options_set_uint(
+ options,
+ c.SPVC_COMPILER_OPTION_GLSL_VERSION,
+ GLSL_VERSION,
+ ) != c.SPVC_SUCCESS) {
+ return error.SpvcFailed;
+ }
+ }
+ }).setOptions);
+}
+
+fn spvCross(
+ alloc: Allocator,
+ backend: spvcross.c.spvc_backend,
+ spv: []const u8,
+ comptime optionsFn_: ?*const fn (c: spvcross.c.spvc_compiler_options) error{SpvcFailed}!void,
+) ![:0]const u8 {
+ // Spir-V is always a multiple of 4 because it is written as a series of words
+ if (@mod(spv.len, 4) != 0) return error.SpirvInvalid;
+
+ // Compiler context
+ const c = spvcross.c;
+ var ctx: c.spvc_context = undefined;
+ if (c.spvc_context_create(&ctx) != c.SPVC_SUCCESS) return error.SpvcFailed;
+ defer c.spvc_context_destroy(ctx);
+
+ // It would be better to get this out into an output parameter to
+ // show users but for now we can just log it.
+ c.spvc_context_set_error_callback(ctx, @ptrCast(&(struct {
+ fn callback(_: ?*anyopaque, msg_ptr: [*c]const u8) callconv(.c) void {
+ const msg = std.mem.sliceTo(msg_ptr, 0);
+ std.log.warn("spirv-cross error message={s}", .{msg});
+ }
+ }).callback), null);
+
+ // Parse the Spir-V binary to an IR
+ var ir: c.spvc_parsed_ir = undefined;
+ if (c.spvc_context_parse_spirv(
+ ctx,
+ @ptrCast(@alignCast(spv.ptr)),
+ spv.len / 4,
+ &ir,
+ ) != c.SPVC_SUCCESS) {
+ return error.SpvcFailed;
+ }
+
+ // Build our compiler to GLSL
+ var compiler: c.spvc_compiler = undefined;
+ if (c.spvc_context_create_compiler(
+ ctx,
+ backend,
+ ir,
+ c.SPVC_CAPTURE_MODE_TAKE_OWNERSHIP,
+ &compiler,
+ ) != c.SPVC_SUCCESS) {
+ return error.SpvcFailed;
+ }
+
+ // Setup our options if we have any
+ if (optionsFn_) |optionsFn| {
+ var options: c.spvc_compiler_options = undefined;
+ if (c.spvc_compiler_create_compiler_options(compiler, &options) != c.SPVC_SUCCESS) {
+ return error.SpvcFailed;
+ }
+
+ try optionsFn(options);
+
+ if (c.spvc_compiler_install_compiler_options(compiler, options) != c.SPVC_SUCCESS) {
+ return error.SpvcFailed;
+ }
+ }
+
+ // Compile the resulting string. This string pointer is owned by the
+ // context so we don't need to free it.
+ var result: [*:0]const u8 = undefined;
+ if (c.spvc_compiler_compile(compiler, @ptrCast(&result)) != c.SPVC_SUCCESS) {
+ return error.SpvcFailed;
+ }
+
+ return try alloc.dupeZ(u8, std.mem.sliceTo(result, 0));
+}
+
+/// Convert ShaderToy shader to null-terminated glsl for testing.
+fn testGlslZ(alloc: Allocator, src: []const u8) ![:0]const u8 {
+ var buf: std.Io.Writer.Allocating = .init(alloc);
+ defer buf.deinit();
+ try glslFromShader(&buf.writer, src);
+ return try buf.toOwnedSliceSentinel(0);
+}
+
+test "spirv" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const src = try testGlslZ(alloc, test_crt);
+ defer alloc.free(src);
+
+ var buf: [4096 * 4]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ try spirvFromGlsl(&writer, null, src);
+}
+
+test "spirv invalid" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const src = try testGlslZ(alloc, test_invalid);
+ defer alloc.free(src);
+
+ var buf: [4096 * 4]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+
+ var errlog: SpirvLog = .{ .alloc = alloc };
+ defer errlog.deinit();
+ try testing.expectError(error.GlslangFailed, spirvFromGlsl(&writer, &errlog, src));
+ try testing.expect(errlog.info.len > 0);
+}
+
+test "shadertoy to msl" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const src = try testGlslZ(alloc, test_crt);
+ defer alloc.free(src);
+
+ var buf: std.Io.Writer.Allocating = .init(alloc);
+ defer buf.deinit();
+ try spirvFromGlsl(&buf.writer, null, src);
+
+ // TODO: Replace this with an aligned version of Writer.Allocating
+ var spvlist: std.ArrayListAligned(u8, .of(u32)) = .empty;
+ defer spvlist.deinit(alloc);
+ try spvlist.appendSlice(alloc, buf.written());
+
+ const msl = try mslFromSpv(alloc, spvlist.items);
+ defer alloc.free(msl);
+}
+
+test "shadertoy to glsl" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const src = try testGlslZ(alloc, test_crt);
+ defer alloc.free(src);
+
+ var buf: std.Io.Writer.Allocating = .init(alloc);
+ defer buf.deinit();
+ try spirvFromGlsl(&buf.writer, null, src);
+
+ // TODO: Replace this with an aligned version of Writer.Allocating
+ var spvlist: std.ArrayListAligned(u8, .of(u32)) = .empty;
+ defer spvlist.deinit(alloc);
+ try spvlist.appendSlice(alloc, buf.written());
+
+ const glsl = try glslFromSpv(alloc, spvlist.items);
+ defer alloc.free(glsl);
+
+ // log.warn("glsl={s}", .{glsl});
+}
+
+const test_crt = @embedFile("shaders/test_shadertoy_crt.glsl");
+const test_invalid = @embedFile("shaders/test_shadertoy_invalid.glsl");
+const test_focus = @embedFile("shaders/test_shadertoy_focus.glsl");
diff --git a/vendor/libghostty-vt/src/renderer/size.zig b/vendor/libghostty-vt/src/renderer/size.zig
new file mode 100644
index 00000000..6cf05f58
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/size.zig
@@ -0,0 +1,458 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const terminal_size = @import("../terminal/size.zig");
+
+const log = std.log.scoped(.renderer_size);
+
+/// Controls how extra whitespace around the terminal grid is distributed.
+pub const PaddingBalance = enum {
+ /// No balancing; padding is applied as specified explicitly.
+ false,
+ /// Balances padding but caps the top padding so the first row doesn't
+ /// drift too far from the top of the window. Excess vertical space is
+ /// shifted to the bottom.
+ true,
+ /// Distributes leftover space equally on all sides so the grid is
+ /// centered within the screen.
+ equal,
+};
+
+/// All relevant sizes for a rendered terminal. These are all the sizes that
+/// any functionality should need to know about the terminal in order to
+/// convert between any coordinate systems.
+///
+/// See the individual field type documentation for more information on each
+/// field. One important note is that any pixel values should already be scaled
+/// to the current DPI of the screen. If the DPI changes, the sizes should be
+/// recalculated and we expect this to be done by the caller.
+pub const Size = struct {
+ screen: ScreenSize,
+ cell: CellSize,
+ padding: Padding,
+
+ /// Return the grid size for this size. The grid size is calculated by
+ /// taking the screen size, removing padding, and dividing by the cell
+ /// dimensions.
+ pub fn grid(self: Size) GridSize {
+ return .init(self.screen.subPadding(self.padding), self.cell);
+ }
+
+ /// The size of the terminal. This is the same as the screen without
+ /// padding.
+ pub fn terminal(self: Size) ScreenSize {
+ return self.screen.subPadding(self.padding);
+ }
+
+ /// Set the padding to be balanced around the grid. The balanced
+ /// padding is calculated AFTER the explicit padding is taken
+ /// into account.
+ pub fn balancePadding(
+ self: *Size,
+ explicit: Padding,
+ mode: PaddingBalance,
+ ) void {
+ // This ensure grid() does the right thing
+ self.padding = explicit;
+
+ // Now we can calculate the balanced padding
+ self.padding = .balanced(
+ self.screen,
+ self.grid(),
+ self.cell,
+ );
+
+ switch (mode) {
+ .false => unreachable,
+ .equal => {},
+ .true => {
+ // Cap the top padding to avoid excessive space above the
+ // first row. The maximum is the balanced explicit horizontal
+ // padding plus half a cell width. Any excess is shifted to
+ // the bottom.
+ const max_top = (explicit.left + explicit.right + self.cell.width) / 2;
+ const vshift = self.padding.top -| max_top;
+ self.padding.top -= vshift;
+ self.padding.bottom += vshift;
+ },
+ }
+ }
+};
+
+/// A coordinate. This is defined as a tagged union to allow for different
+/// coordinate systems to be represented.
+///
+/// A coordinate is only valid within the context of a stable Size value.
+/// If any of the sizes in the Size struct change, the coordinate is no
+/// longer valid and must be recalculated. A conversion function is provided
+/// to migrate to a new Size (which may result in failure).
+///
+/// The coordinate systems are:
+///
+/// * surface: (0, 0) is the top-left of the surface (with padding). Negative
+/// values are allowed and are off the surface. Likewise, values greater
+/// than the surface size are off the surface. Units are pixels.
+///
+/// * terminal: (0, 0) is the top-left of the terminal grid. This is the
+/// same as the surface but with the padding removed. Negative values and
+/// values greater than the grid size are allowed and are off the terminal.
+/// Units are pixels.
+///
+/// * grid: (0, 0) is the top-left of the grid. Units are in cells. Negative
+/// values are not allowed but values greater than the grid size are
+/// possible and are off the grid.
+///
+pub const Coordinate = union(enum) {
+ surface: Surface,
+ terminal: Terminal,
+ grid: Grid,
+
+ pub const Tag = @typeInfo(Coordinate).@"union".tag_type.?;
+ pub const Surface = struct { x: f64, y: f64 };
+ pub const Terminal = struct { x: f64, y: f64 };
+ pub const Grid = struct { x: GridSize.Unit, y: GridSize.Unit };
+
+ /// Convert a coordinate to a different space within the same Size.
+ pub fn convert(self: Coordinate, to: Tag, size: Size) Coordinate {
+ // Unlikely fast-path but avoid work.
+ if (@as(Tag, self) == to) return self;
+
+ // To avoid the combinatorial explosion of conversion functions, we
+ // convert to the surface system first and then reconvert from there.
+ const surface = self.convertToSurface(size);
+
+ return switch (to) {
+ .surface => .{ .surface = surface },
+ .terminal => .{ .terminal = .{
+ .x = surface.x - @as(f64, @floatFromInt(size.padding.left)),
+ .y = surface.y - @as(f64, @floatFromInt(size.padding.top)),
+ } },
+ .grid => grid: {
+ // Get rid of the padding.
+ const term = (Coordinate{ .surface = surface }).convert(
+ .terminal,
+ size,
+ ).terminal;
+
+ // We need our grid to clamp
+ const grid = size.grid();
+
+ // Calculate the grid position.
+ const cell_width: f64 = @as(f64, @floatFromInt(size.cell.width));
+ const cell_height: f64 = @as(f64, @floatFromInt(size.cell.height));
+ const clamped_x: f64 = @max(0, term.x);
+ const clamped_y: f64 = @max(0, term.y);
+ const col: GridSize.Unit = @intFromFloat(clamped_x / cell_width);
+ const row: GridSize.Unit = @intFromFloat(clamped_y / cell_height);
+ const clamped_col: GridSize.Unit = @min(col, grid.columns - 1);
+ const clamped_row: GridSize.Unit = @min(row, grid.rows - 1);
+ break :grid .{ .grid = .{ .x = clamped_col, .y = clamped_row } };
+ },
+ };
+ }
+
+ /// Convert a coordinate to the surface coordinate system.
+ fn convertToSurface(self: Coordinate, size: Size) Surface {
+ return switch (self) {
+ .surface => |v| v,
+ .terminal => |v| .{
+ .x = v.x + @as(f64, @floatFromInt(size.padding.left)),
+ .y = v.y + @as(f64, @floatFromInt(size.padding.top)),
+ },
+ .grid => |v| grid: {
+ const col: f64 = @floatFromInt(v.x);
+ const row: f64 = @floatFromInt(v.y);
+ const cell_width: f64 = @floatFromInt(size.cell.width);
+ const cell_height: f64 = @floatFromInt(size.cell.height);
+ const padding_left: f64 = @floatFromInt(size.padding.left);
+ const padding_top: f64 = @floatFromInt(size.padding.top);
+ break :grid .{
+ .x = col * cell_width + padding_left,
+ .y = row * cell_height + padding_top,
+ };
+ },
+ };
+ }
+};
+
+/// The dimensions of a single "cell" in the terminal grid.
+///
+/// The dimensions are dependent on the current loaded set of font glyphs.
+/// We calculate the width based on the widest character and the height based
+/// on the height requirement for an underscore (the "lowest" -- visually --
+/// character).
+///
+/// The units for the width and height are in world space. They have to
+/// be normalized for any renderer implementation.
+pub const CellSize = extern struct {
+ width: u32,
+ height: u32,
+};
+
+/// The dimensions of the screen that the grid is rendered to. This is the
+/// terminal screen, so it is likely a subset of the window size. The dimensions
+/// should be in pixels.
+pub const ScreenSize = extern struct {
+ width: u32,
+ height: u32,
+
+ /// Subtract padding from the screen size.
+ pub fn subPadding(self: ScreenSize, padding: Padding) ScreenSize {
+ return .{
+ .width = self.width -| (padding.left + padding.right),
+ .height = self.height -| (padding.top + padding.bottom),
+ };
+ }
+
+ /// Calculates the amount of blank space around the grid. This is possible
+ /// when padding isn't balanced.
+ ///
+ /// The "self" screen size here should be the unpadded screen.
+ pub fn blankPadding(self: ScreenSize, padding: Padding, grid: GridSize, cell: CellSize) Padding {
+ const grid_width = grid.columns * cell.width;
+ const grid_height = grid.rows * cell.height;
+ const padded_width = grid_width + (padding.left + padding.right);
+ const padded_height = grid_height + (padding.top + padding.bottom);
+
+ // Note these have to use a saturating subtraction to avoid underflow
+ // because our padding can cause the padded sizes to be larger than
+ // our real screen if the screen is shrunk to a minimal size such
+ // as 1x1.
+ const leftover_width = self.width -| padded_width;
+ const leftover_height = self.height -| padded_height;
+
+ return .{
+ .top = 0,
+ .bottom = leftover_height,
+ .right = leftover_width,
+ .left = 0,
+ };
+ }
+
+ /// Returns true if two sizes are equal.
+ pub fn equals(self: ScreenSize, other: ScreenSize) bool {
+ return self.width == other.width and self.height == other.height;
+ }
+};
+
+/// The dimensions of the grid itself, in rows/columns units.
+pub const GridSize = extern struct {
+ pub const Unit = terminal_size.CellCountInt;
+
+ columns: Unit = 0,
+ rows: Unit = 0,
+
+ /// Initialize a grid size based on a screen and cell size.
+ pub fn init(screen: ScreenSize, cell: CellSize) GridSize {
+ var result: GridSize = undefined;
+ result.update(screen, cell);
+ return result;
+ }
+
+ /// Update the columns/rows for the grid based on the given screen and
+ /// cell size.
+ pub fn update(self: *GridSize, screen: ScreenSize, cell: CellSize) void {
+ const cell_width: f32 = @floatFromInt(cell.width);
+ const cell_height: f32 = @floatFromInt(cell.height);
+ const screen_width: f32 = @floatFromInt(screen.width);
+ const screen_height: f32 = @floatFromInt(screen.height);
+ const calc_cols: Unit = @intFromFloat(screen_width / cell_width);
+ const calc_rows: Unit = @intFromFloat(screen_height / cell_height);
+ self.columns = @max(1, calc_cols);
+ self.rows = @max(1, calc_rows);
+ }
+
+ /// Returns true if two sizes are equal.
+ pub fn equals(self: GridSize, other: GridSize) bool {
+ return self.columns == other.columns and self.rows == other.rows;
+ }
+};
+
+/// The padding to add to a screen.
+pub const Padding = extern struct {
+ top: u32 = 0,
+ bottom: u32 = 0,
+ right: u32 = 0,
+ left: u32 = 0,
+
+ /// Returns padding that balances the whitespace around the screen
+ /// for the given grid and cell sizes.
+ pub fn balanced(screen: ScreenSize, grid: GridSize, cell: CellSize) Padding {
+ // Turn our cell sizes into floats for the math
+ const cell_width: f32 = @floatFromInt(cell.width);
+ const cell_height: f32 = @floatFromInt(cell.height);
+
+ // The size of our full grid
+ const grid_width = @as(f32, @floatFromInt(grid.columns)) * cell_width;
+ const grid_height = @as(f32, @floatFromInt(grid.rows)) * cell_height;
+
+ // The empty space to the right of a line and bottom of the last row
+ const space_right = @as(f32, @floatFromInt(screen.width)) - grid_width;
+ const space_bot = @as(f32, @floatFromInt(screen.height)) - grid_height;
+
+ // The padding is split equally along both axes.
+ const padding_right = @floor(space_right / 2);
+ const padding_left = padding_right;
+
+ const padding_bot = @floor(space_bot / 2);
+ const padding_top = padding_bot;
+
+ const zero = @as(f32, 0);
+ return .{
+ .top = @intFromFloat(@max(zero, padding_top)),
+ .bottom = @intFromFloat(@max(zero, padding_bot)),
+ .right = @intFromFloat(@max(zero, padding_right)),
+ .left = @intFromFloat(@max(zero, padding_left)),
+ };
+ }
+
+ /// Add another padding to this one
+ pub fn add(self: Padding, other: Padding) Padding {
+ return .{
+ .top = self.top + other.top,
+ .bottom = self.bottom + other.bottom,
+ .right = self.right + other.right,
+ .left = self.left + other.left,
+ };
+ }
+
+ /// Equality test between two paddings.
+ pub fn eql(self: Padding, other: Padding) bool {
+ return self.top == other.top and
+ self.bottom == other.bottom and
+ self.right == other.right and
+ self.left == other.left;
+ }
+};
+
+test "Size.balancePadding equal distributes whitespace equally" {
+ const testing = std.testing;
+
+ // screen=1050x850, cell=10x20, explicit=4 each side
+ // grid: (1050-8)/10=104 cols, (850-8)/20=42 rows
+ // leftover: 1050-1040=10 horizontal, 850-840=10 vertical
+ // balanced: left=right=5, top=bottom=5
+ var size: Size = .{
+ .screen = .{ .width = 1050, .height = 850 },
+ .cell = .{ .width = 10, .height = 20 },
+ .padding = .{},
+ };
+ size.balancePadding(.{ .top = 4, .bottom = 4, .left = 4, .right = 4 }, .equal);
+ try testing.expectEqual(size.padding.left, size.padding.right);
+ try testing.expectEqual(size.padding.top, size.padding.bottom);
+ try testing.expect(size.padding.top > 0);
+}
+
+test "Size.balancePadding true shifts excess top to bottom" {
+ const testing = std.testing;
+
+ // screen=1090x1070, cell=20x40, explicit=0
+ // grid: 1090/20=54 cols, 1070/40=26 rows
+ // leftover: 1090-1080=10, 1070-1040=30
+ // balanced: left=right=5, top=bottom=15
+ // vshift cap: (0+0+20)/2=10, vshift=15-10=5
+ // result: top=10, bottom=20
+ var size: Size = .{
+ .screen = .{ .width = 1090, .height = 1070 },
+ .cell = .{ .width = 20, .height = 40 },
+ .padding = .{},
+ };
+ size.balancePadding(.{}, .true);
+ try testing.expectEqual(size.padding.left, size.padding.right);
+ try testing.expect(size.padding.top < size.padding.bottom);
+ try testing.expectEqual(@as(u32, 10), size.padding.top);
+ try testing.expectEqual(@as(u32, 20), size.padding.bottom);
+}
+
+test "Padding balanced on zero" {
+ // On some systems, our screen can be zero-sized for a bit, and we
+ // don't want to end up with negative padding.
+ const testing = std.testing;
+ const grid: GridSize = .{ .columns = 100, .rows = 37 };
+ const cell: CellSize = .{ .width = 10, .height = 20 };
+ const screen: ScreenSize = .{ .width = 0, .height = 0 };
+ const padding = Padding.balanced(screen, grid, cell);
+ try testing.expectEqual(Padding{}, padding);
+}
+
+test "GridSize update exact" {
+ const testing = std.testing;
+
+ var grid: GridSize = .{};
+ grid.update(.{
+ .width = 100,
+ .height = 40,
+ }, .{
+ .width = 5,
+ .height = 10,
+ });
+
+ try testing.expectEqual(@as(GridSize.Unit, 20), grid.columns);
+ try testing.expectEqual(@as(GridSize.Unit, 4), grid.rows);
+}
+
+test "GridSize update rounding" {
+ const testing = std.testing;
+
+ var grid: GridSize = .{};
+ grid.update(.{
+ .width = 20,
+ .height = 40,
+ }, .{
+ .width = 6,
+ .height = 15,
+ });
+
+ try testing.expectEqual(@as(GridSize.Unit, 3), grid.columns);
+ try testing.expectEqual(@as(GridSize.Unit, 2), grid.rows);
+}
+
+test "coordinate conversion" {
+ const testing = std.testing;
+
+ // A size for testing purposes. Purposely easy to calculate numbers.
+ const test_size: Size = .{
+ .screen = .{
+ .width = 100,
+ .height = 100,
+ },
+
+ .cell = .{
+ .width = 5,
+ .height = 10,
+ },
+
+ .padding = .{},
+ };
+
+ // Each pair is a test case of [expected, actual]. We only test
+ // one-way conversion because conversion can be lossy due to clamping
+ // and so on.
+ const table: []const [2]Coordinate = &.{
+ .{
+ .{ .grid = .{ .x = 0, .y = 0 } },
+ .{ .surface = .{ .x = 0, .y = 0 } },
+ },
+ .{
+ .{ .grid = .{ .x = 1, .y = 0 } },
+ .{ .surface = .{ .x = 6, .y = 0 } },
+ },
+ .{
+ .{ .grid = .{ .x = 1, .y = 1 } },
+ .{ .surface = .{ .x = 6, .y = 10 } },
+ },
+ .{
+ .{ .grid = .{ .x = 0, .y = 0 } },
+ .{ .surface = .{ .x = -10, .y = -10 } },
+ },
+ .{
+ .{ .grid = .{ .x = test_size.grid().columns - 1, .y = test_size.grid().rows - 1 } },
+ .{ .surface = .{ .x = 100_000, .y = 100_000 } },
+ },
+ };
+
+ for (table) |pair| {
+ const expected = pair[0];
+ const actual = pair[1].convert(@as(Coordinate.Tag, expected), test_size);
+ try testing.expectEqual(expected, actual);
+ }
+}