aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/renderer/opengl/shaders.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/renderer/opengl/shaders.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/renderer/opengl/shaders.zig')
-rw-r--r--vendor/libghostty-vt/src/renderer/opengl/shaders.zig376
1 files changed, 376 insertions, 0 deletions
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;
+}