ghostty/src/renderer/metal/shaders.zig
Qwerasd ec20c455c7 renderer: remove all gpu-side glyph constraint logic
Now that it's done at the rasterization stage, we don't need to handle
it on the GPU. This also means that we can switch to nearest neighbor
interpolation in the Metal shader since we're guaranteed to be pixel
perfect. Accidentally, we were already nearest neighbor in the OpenGL
shaders because I used the Rectangle texture mode in the big renderer
rework, which doesn't support interpolation- anyway, that's no longer
problematic since we won't be scaling glyphs on the GPU anymore.
2025-07-04 15:47:35 -06:00

456 lines
14 KiB
Zig

const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
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;
}