mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +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 Image = mtl_image.Image;
|
||||||
const ImageMap = mtl_image.ImageMap;
|
const ImageMap = mtl_image.ImageMap;
|
||||||
const Shaders = mtl_shaders.Shaders;
|
const Shaders = mtl_shaders.Shaders;
|
||||||
|
|
||||||
const CellBuffer = mtl_buffer.Buffer(mtl_shaders.Cell);
|
const CellBuffer = mtl_buffer.Buffer(mtl_shaders.Cell);
|
||||||
|
const ImageBuffer = mtl_buffer.Buffer(mtl_shaders.Image);
|
||||||
const InstanceBuffer = mtl_buffer.Buffer(u16);
|
const InstanceBuffer = mtl_buffer.Buffer(u16);
|
||||||
|
|
||||||
// Get native API access on certain platforms so we can do more customization.
|
// 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 {
|
pub const Shaders = struct {
|
||||||
library: objc.Object,
|
library: objc.Object,
|
||||||
cell_pipeline: objc.Object,
|
cell_pipeline: objc.Object,
|
||||||
|
image_pipeline: objc.Object,
|
||||||
|
|
||||||
pub fn init(device: objc.Object) !Shaders {
|
pub fn init(device: objc.Object) !Shaders {
|
||||||
const library = try initLibrary(device);
|
const library = try initLibrary(device);
|
||||||
@ -21,14 +22,19 @@ pub const Shaders = struct {
|
|||||||
const cell_pipeline = try initCellPipeline(device, library);
|
const cell_pipeline = try initCellPipeline(device, library);
|
||||||
errdefer cell_pipeline.msgSend(void, objc.sel("release"), .{});
|
errdefer cell_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||||
|
|
||||||
|
const image_pipeline = try initImagePipeline(device, library);
|
||||||
|
errdefer image_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.library = library,
|
.library = library,
|
||||||
.cell_pipeline = cell_pipeline,
|
.cell_pipeline = cell_pipeline,
|
||||||
|
.image_pipeline = image_pipeline,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Shaders) void {
|
pub fn deinit(self: *Shaders) void {
|
||||||
self.cell_pipeline.msgSend(void, objc.sel("release"), .{});
|
self.cell_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||||
|
self.image_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||||
self.library.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.
|
/// The uniforms that are passed to the terminal cell shader.
|
||||||
pub const Uniforms = extern struct {
|
pub const Uniforms = extern struct {
|
||||||
/// The projection matrix for turning world coordinates to normalized.
|
/// 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.?);
|
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")
|
// data layout of the vertex inputs. We use indexed (or "instanced")
|
||||||
// rendering, so this makes it so that each instance gets a single
|
// rendering, so this makes it so that each instance gets a single
|
||||||
// Cell as input.
|
// Cell as input.
|
||||||
@ -274,6 +285,123 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
|
|||||||
return pipeline_state;
|
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 {
|
fn checkError(err_: ?*anyopaque) !void {
|
||||||
const nserr = objc.Object.fromId(err_ orelse return);
|
const nserr = objc.Object.fromId(err_ orelse return);
|
||||||
const str = @as(
|
const str = @as(
|
||||||
|
@ -49,6 +49,11 @@ struct VertexOut {
|
|||||||
float2 tex_coord;
|
float2 tex_coord;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
// Terminal Grid Cell Shader
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
#pragma mark - Terminal Grid Cell Shader
|
||||||
|
|
||||||
vertex VertexOut uber_vertex(
|
vertex VertexOut uber_vertex(
|
||||||
unsigned int vid [[ vertex_id ]],
|
unsigned int vid [[ vertex_id ]],
|
||||||
VertexIn input [[ stage_in ]],
|
VertexIn input [[ stage_in ]],
|
||||||
@ -179,3 +184,68 @@ fragment float4 uber_fragment(
|
|||||||
return in.color;
|
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