aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/renderer/opengl
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/opengl
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/renderer/opengl')
-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
8 files changed, 1090 insertions, 0 deletions
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;
+}