mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
renderer/metal: first pass at an image shader
This commit is contained in:
@ -28,7 +28,9 @@ const mtl_shaders = @import("metal/shaders.zig");
|
||||
const Image = mtl_image.Image;
|
||||
const ImageMap = mtl_image.ImageMap;
|
||||
const Shaders = mtl_shaders.Shaders;
|
||||
|
||||
const CellBuffer = mtl_buffer.Buffer(mtl_shaders.Cell);
|
||||
const ImageBuffer = mtl_buffer.Buffer(mtl_shaders.Image);
|
||||
const InstanceBuffer = mtl_buffer.Buffer(u16);
|
||||
|
||||
// Get native API access on certain platforms so we can do more customization.
|
||||
|
@ -13,6 +13,7 @@ const log = std.log.scoped(.metal);
|
||||
pub const Shaders = struct {
|
||||
library: objc.Object,
|
||||
cell_pipeline: objc.Object,
|
||||
image_pipeline: objc.Object,
|
||||
|
||||
pub fn init(device: objc.Object) !Shaders {
|
||||
const library = try initLibrary(device);
|
||||
@ -21,14 +22,19 @@ pub const Shaders = struct {
|
||||
const cell_pipeline = try initCellPipeline(device, library);
|
||||
errdefer cell_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||
|
||||
const image_pipeline = try initImagePipeline(device, library);
|
||||
errdefer image_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||
|
||||
return .{
|
||||
.library = library,
|
||||
.cell_pipeline = cell_pipeline,
|
||||
.image_pipeline = image_pipeline,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Shaders) void {
|
||||
self.cell_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||
self.image_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||
self.library.msgSend(void, objc.sel("release"), .{});
|
||||
}
|
||||
};
|
||||
@ -51,6 +57,11 @@ pub const Cell = extern struct {
|
||||
};
|
||||
};
|
||||
|
||||
/// Single parameter for the image shader.
|
||||
pub const Image = extern struct {
|
||||
grid_pos: [2]f32,
|
||||
};
|
||||
|
||||
/// The uniforms that are passed to the terminal cell shader.
|
||||
pub const Uniforms = extern struct {
|
||||
/// The projection matrix for turning world coordinates to normalized.
|
||||
@ -117,7 +128,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
|
||||
break :func_frag objc.Object.fromId(ptr.?);
|
||||
};
|
||||
|
||||
// Create the vertex descriptor. The vertex descriptor describves the
|
||||
// Create the vertex descriptor. The vertex descriptor describes the
|
||||
// data layout of the vertex inputs. We use indexed (or "instanced")
|
||||
// rendering, so this makes it so that each instance gets a single
|
||||
// Cell as input.
|
||||
@ -274,6 +285,123 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
|
||||
return pipeline_state;
|
||||
}
|
||||
|
||||
/// Initialize the image render pipeline for our shader library.
|
||||
fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object {
|
||||
// Get our vertex and fragment functions
|
||||
const func_vert = func_vert: {
|
||||
const str = try macos.foundation.String.createWithBytes(
|
||||
"image_vertex",
|
||||
.utf8,
|
||||
false,
|
||||
);
|
||||
defer str.release();
|
||||
|
||||
const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
|
||||
break :func_vert objc.Object.fromId(ptr.?);
|
||||
};
|
||||
const func_frag = func_frag: {
|
||||
const str = try macos.foundation.String.createWithBytes(
|
||||
"image_fragment",
|
||||
.utf8,
|
||||
false,
|
||||
);
|
||||
defer str.release();
|
||||
|
||||
const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
|
||||
break :func_frag objc.Object.fromId(ptr.?);
|
||||
};
|
||||
|
||||
// Create the vertex descriptor. The vertex descriptor describes the
|
||||
// data layout of the vertex inputs. We use indexed (or "instanced")
|
||||
// rendering, so this makes it so that each instance gets a single
|
||||
// Image as input.
|
||||
const vertex_desc = vertex_desc: {
|
||||
const desc = init: {
|
||||
const Class = objc.Class.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;
|
||||
};
|
||||
|
||||
// Our attributes are the fields of the input
|
||||
const attrs = objc.Object.fromId(desc.getProperty(?*anyopaque, "attributes"));
|
||||
{
|
||||
const attr = attrs.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("objectAtIndexedSubscript:"),
|
||||
.{@as(c_ulong, 1)},
|
||||
);
|
||||
|
||||
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.float2));
|
||||
attr.setProperty("offset", @as(c_ulong, @offsetOf(Image, "grid_pos")));
|
||||
attr.setProperty("bufferIndex", @as(c_ulong, 0));
|
||||
}
|
||||
|
||||
// The layout describes how and when we fetch the next vertex input.
|
||||
const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts"));
|
||||
{
|
||||
const layout = layouts.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("objectAtIndexedSubscript:"),
|
||||
.{@as(c_ulong, 0)},
|
||||
);
|
||||
|
||||
// Access each Image per instance, not per vertex.
|
||||
layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance));
|
||||
layout.setProperty("stride", @as(c_ulong, @sizeOf(Image)));
|
||||
}
|
||||
|
||||
break :vertex_desc desc;
|
||||
};
|
||||
|
||||
// Create our descriptor
|
||||
const desc = init: {
|
||||
const Class = objc.Class.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;
|
||||
};
|
||||
|
||||
// Set our properties
|
||||
desc.setProperty("vertexFunction", func_vert);
|
||||
desc.setProperty("fragmentFunction", func_frag);
|
||||
desc.setProperty("vertexDescriptor", vertex_desc);
|
||||
|
||||
// Set our color attachment
|
||||
const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments"));
|
||||
{
|
||||
const attachment = attachments.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("objectAtIndexedSubscript:"),
|
||||
.{@as(c_ulong, 0)},
|
||||
);
|
||||
|
||||
// Value is MTLPixelFormatBGRA8Unorm
|
||||
attachment.setProperty("pixelFormat", @as(c_ulong, 80));
|
||||
|
||||
// Blending. This is required so that our text we render on top
|
||||
// of our drawable properly blends into the bg.
|
||||
attachment.setProperty("blendingEnabled", true);
|
||||
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 = device.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("newRenderPipelineStateWithDescriptor:error:"),
|
||||
.{ desc, &err },
|
||||
);
|
||||
try checkError(err);
|
||||
|
||||
return pipeline_state;
|
||||
}
|
||||
|
||||
fn checkError(err_: ?*anyopaque) !void {
|
||||
const nserr = objc.Object.fromId(err_ orelse return);
|
||||
const str = @as(
|
||||
|
@ -49,6 +49,11 @@ struct VertexOut {
|
||||
float2 tex_coord;
|
||||
};
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Terminal Grid Cell Shader
|
||||
//-------------------------------------------------------------------
|
||||
#pragma mark - Terminal Grid Cell Shader
|
||||
|
||||
vertex VertexOut uber_vertex(
|
||||
unsigned int vid [[ vertex_id ]],
|
||||
VertexIn input [[ stage_in ]],
|
||||
@ -179,3 +184,68 @@ fragment float4 uber_fragment(
|
||||
return in.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(1) ]];
|
||||
};
|
||||
|
||||
struct ImageVertexOut {
|
||||
float4 position [[ position ]];
|
||||
float2 tex_coord;
|
||||
};
|
||||
|
||||
vertex ImageVertexOut image_vertex(
|
||||
unsigned int vid [[ vertex_id ]],
|
||||
ImageVertexIn input [[ stage_in ]],
|
||||
texture2d<float> image [[ texture(0) ]],
|
||||
constant Uniforms &uniforms [[ buffer(1) ]]
|
||||
) {
|
||||
// The position of our image starts at the top-left of the grid cell.
|
||||
float2 image_pos = uniforms.cell_size * input.grid_pos;
|
||||
|
||||
// The size of the image in pixels
|
||||
float2 image_size = float2(image.get_width(), image.get_height());
|
||||
|
||||
// Turn the image position into a vertex point depending on the
|
||||
// vertex ID. Since we use instanced drawing, we have 4 vertices
|
||||
// for each corner of the cell. We can use vertex ID to determine
|
||||
// which one we're looking at. Using this, we can use 1 or 0 to keep
|
||||
// or discard the value for the vertex.
|
||||
//
|
||||
// 0 = top-right
|
||||
// 1 = bot-right
|
||||
// 2 = bot-left
|
||||
// 3 = top-left
|
||||
float2 position;
|
||||
position.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f;
|
||||
position.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f;
|
||||
|
||||
ImageVertexOut out;
|
||||
|
||||
// Our final position is our image position multiplied by the on/off
|
||||
// position based on corners above.
|
||||
image_pos = image_pos + image_size * position;
|
||||
|
||||
// Output position is just our cell top-left.
|
||||
out.position = uniforms.projection_matrix * float4(image_pos.x, image_pos.y, 0.0f, 1.0f);
|
||||
|
||||
// Calculate the texture coordinate in pixels and normalize it to [0, 1]
|
||||
out.tex_coord = position;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
fragment float4 image_fragment(
|
||||
ImageVertexOut in [[ stage_in ]],
|
||||
texture2d<float> image [[ texture(0) ]]
|
||||
) {
|
||||
constexpr sampler textureSampler(address::clamp_to_edge, filter::linear);
|
||||
return image.sample(textureSampler, in.tex_coord);
|
||||
}
|
||||
|
Reference in New Issue
Block a user