diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/renderer/metal/shaders.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/renderer/metal/shaders.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/renderer/metal/shaders.zig | 454 |
1 files changed, 454 insertions, 0 deletions
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; +} |
