mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-22 19:56:08 +03:00
renderer/metal: extract helpers for shaders/buffers
This commit is contained in:
@ -22,9 +22,14 @@ const Allocator = std.mem.Allocator;
|
||||
const Terminal = terminal.Terminal;
|
||||
|
||||
const mtl = @import("metal/api.zig");
|
||||
const mtl_buffer = @import("metal/buffer.zig");
|
||||
const mtl_image = @import("metal/image.zig");
|
||||
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 InstanceBuffer = mtl_buffer.Buffer(u16);
|
||||
|
||||
// Get native API access on certain platforms so we can do more customization.
|
||||
const glfwNative = glfw.Native(.{
|
||||
@ -63,11 +68,11 @@ cursor_style: renderer.CursorStyle,
|
||||
/// The current set of cells to render. This is rebuilt on every frame
|
||||
/// but we keep this around so that we don't reallocate. Each set of
|
||||
/// cells goes into a separate shader.
|
||||
cells_bg: std.ArrayListUnmanaged(GPUCell),
|
||||
cells: std.ArrayListUnmanaged(GPUCell),
|
||||
cells_bg: std.ArrayListUnmanaged(mtl_shaders.Cell),
|
||||
cells: std.ArrayListUnmanaged(mtl_shaders.Cell),
|
||||
|
||||
/// The current GPU uniform values.
|
||||
uniforms: GPUUniforms,
|
||||
uniforms: mtl_shaders.Uniforms,
|
||||
|
||||
/// The font structures.
|
||||
font_group: *font.GroupCache,
|
||||
@ -76,62 +81,19 @@ font_shaper: font.Shaper,
|
||||
/// The images that we may render.
|
||||
images: ImageMap = .{},
|
||||
|
||||
/// Metal state
|
||||
shaders: Shaders, // Compiled shaders
|
||||
buf_cells: CellBuffer, // Vertex buffer for cells
|
||||
buf_cells_bg: CellBuffer, // Vertex buffer for background cells
|
||||
buf_instance: InstanceBuffer, // MTLBuffer
|
||||
|
||||
/// Metal objects
|
||||
device: objc.Object, // MTLDevice
|
||||
queue: objc.Object, // MTLCommandQueue
|
||||
swapchain: objc.Object, // CAMetalLayer
|
||||
buf_cells_bg: objc.Object, // MTLBuffer
|
||||
buf_cells: objc.Object, // MTLBuffer
|
||||
buf_instance: objc.Object, // MTLBuffer
|
||||
pipeline: objc.Object, // MTLRenderPipelineState
|
||||
texture_greyscale: objc.Object, // MTLTexture
|
||||
texture_color: objc.Object, // MTLTexture
|
||||
|
||||
const GPUCell = extern struct {
|
||||
mode: GPUCellMode,
|
||||
grid_pos: [2]f32,
|
||||
glyph_pos: [2]u32 = .{ 0, 0 },
|
||||
glyph_size: [2]u32 = .{ 0, 0 },
|
||||
glyph_offset: [2]i32 = .{ 0, 0 },
|
||||
color: [4]u8,
|
||||
cell_width: u8,
|
||||
};
|
||||
|
||||
// Intel macOS 13 doesn't like it when any field in a vertex buffer is not
|
||||
// aligned on the alignment of the struct. I don't understand it, I think
|
||||
// this must be some macOS 13 Metal GPU driver bug because it doesn't matter
|
||||
// on macOS 12 or Apple Silicon macOS 13.
|
||||
//
|
||||
// To be safe, we put this test in here.
|
||||
test "GPUCell offsets" {
|
||||
const testing = std.testing;
|
||||
const alignment = @alignOf(GPUCell);
|
||||
inline for (@typeInfo(GPUCell).Struct.fields) |field| {
|
||||
const offset = @offsetOf(GPUCell, field.name);
|
||||
try testing.expectEqual(0, @mod(offset, alignment));
|
||||
}
|
||||
}
|
||||
|
||||
const GPUUniforms = 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,
|
||||
|
||||
/// Size of a single cell in pixels, unscaled.
|
||||
cell_size: [2]f32,
|
||||
|
||||
/// Metrics for underline/strikethrough
|
||||
strikethrough_position: f32,
|
||||
strikethrough_thickness: f32,
|
||||
};
|
||||
|
||||
const GPUCellMode = enum(u8) {
|
||||
bg = 1,
|
||||
fg = 2,
|
||||
fg_color = 7,
|
||||
strikethrough = 8,
|
||||
};
|
||||
|
||||
/// The configuration for this renderer that is derived from the main
|
||||
/// configuration. This must be exported so that we don't need to
|
||||
/// pass around Config pointers which makes memory management a pain.
|
||||
@ -248,57 +210,22 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
});
|
||||
errdefer font_shaper.deinit();
|
||||
|
||||
// Initialize our Metal buffers
|
||||
const buf_instance = buffer: {
|
||||
const data = [6]u16{
|
||||
// Vertex buffers
|
||||
var buf_cells = try CellBuffer.init(device, 160 * 160);
|
||||
errdefer buf_cells.deinit();
|
||||
var buf_cells_bg = try CellBuffer.init(device, 160 * 160);
|
||||
errdefer buf_cells_bg.deinit();
|
||||
var buf_instance = try InstanceBuffer.initFill(device, &.{
|
||||
0, 1, 3, // Top-left triangle
|
||||
1, 2, 3, // Bottom-right triangle
|
||||
};
|
||||
});
|
||||
errdefer buf_instance.deinit();
|
||||
|
||||
break :buffer device.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("newBufferWithBytes:length:options:"),
|
||||
.{
|
||||
@as(*const anyopaque, @ptrCast(&data)),
|
||||
@as(c_ulong, @intCast(data.len * @sizeOf(u16))),
|
||||
mtl.MTLResourceStorageModeShared,
|
||||
},
|
||||
);
|
||||
};
|
||||
// Initialize our shaders
|
||||
var shaders = try Shaders.init(device);
|
||||
errdefer shaders.deinit();
|
||||
|
||||
const buf_cells = buffer: {
|
||||
// Preallocate for 160x160 grid with 3 modes (bg, fg, text). This
|
||||
// should handle most terminals well, and we can avoid a resize later.
|
||||
const prealloc = 160 * 160 * 3;
|
||||
|
||||
break :buffer device.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("newBufferWithLength:options:"),
|
||||
.{
|
||||
@as(c_ulong, @intCast(prealloc * @sizeOf(GPUCell))),
|
||||
mtl.MTLResourceStorageModeShared,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const buf_cells_bg = buffer: {
|
||||
// Preallocate for 160x160 grid with 3 modes (bg, fg, text). This
|
||||
// should handle most terminals well, and we can avoid a resize later.
|
||||
const prealloc = 160 * 160;
|
||||
|
||||
break :buffer device.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("newBufferWithLength:options:"),
|
||||
.{
|
||||
@as(c_ulong, @intCast(prealloc * @sizeOf(GPUCell))),
|
||||
mtl.MTLResourceStorageModeShared,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// Initialize our shader (MTLLibrary)
|
||||
const library = try initLibrary(device, @embedFile("shaders/cell.metal"));
|
||||
const pipeline_state = try initPipelineState(device, library);
|
||||
// Font atlas textures
|
||||
const texture_greyscale = try initAtlasTexture(device, &options.font_group.atlas_greyscale);
|
||||
const texture_color = try initAtlasTexture(device, &options.font_group.atlas_color);
|
||||
|
||||
@ -327,14 +254,16 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
.font_group = options.font_group,
|
||||
.font_shaper = font_shaper,
|
||||
|
||||
// Shaders
|
||||
.shaders = shaders,
|
||||
.buf_cells = buf_cells,
|
||||
.buf_cells_bg = buf_cells_bg,
|
||||
.buf_instance = buf_instance,
|
||||
|
||||
// Metal stuff
|
||||
.device = device,
|
||||
.queue = queue,
|
||||
.swapchain = swapchain,
|
||||
.buf_cells = buf_cells,
|
||||
.buf_cells_bg = buf_cells_bg,
|
||||
.buf_instance = buf_instance,
|
||||
.pipeline = pipeline_state,
|
||||
.texture_greyscale = texture_greyscale,
|
||||
.texture_color = texture_color,
|
||||
};
|
||||
@ -355,13 +284,15 @@ pub fn deinit(self: *Metal) void {
|
||||
self.images.deinit(self.alloc);
|
||||
}
|
||||
|
||||
deinitMTLResource(self.buf_cells_bg);
|
||||
deinitMTLResource(self.buf_cells);
|
||||
deinitMTLResource(self.buf_instance);
|
||||
self.buf_cells_bg.deinit();
|
||||
self.buf_cells.deinit();
|
||||
self.buf_instance.deinit();
|
||||
deinitMTLResource(self.texture_greyscale);
|
||||
deinitMTLResource(self.texture_color);
|
||||
self.queue.msgSend(void, objc.sel("release"), .{});
|
||||
|
||||
self.shaders.deinit();
|
||||
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
@ -699,11 +630,12 @@ pub fn render(
|
||||
);
|
||||
defer encoder.msgSend(void, objc.sel("endEncoding"), .{});
|
||||
|
||||
//do we need to do this?
|
||||
//encoder.msgSend(void, objc.sel("setViewport:"), .{viewport});
|
||||
|
||||
// Use our shader pipeline
|
||||
encoder.msgSend(void, objc.sel("setRenderPipelineState:"), .{self.pipeline.value});
|
||||
encoder.msgSend(
|
||||
void,
|
||||
objc.sel("setRenderPipelineState:"),
|
||||
.{self.shaders.cell_pipeline.value},
|
||||
);
|
||||
|
||||
// Set our buffers
|
||||
encoder.msgSend(
|
||||
@ -749,14 +681,14 @@ pub fn render(
|
||||
fn drawCells(
|
||||
self: *Metal,
|
||||
encoder: objc.Object,
|
||||
buf: *objc.Object,
|
||||
cells: std.ArrayListUnmanaged(GPUCell),
|
||||
buf: *CellBuffer,
|
||||
cells: std.ArrayListUnmanaged(mtl_shaders.Cell),
|
||||
) !void {
|
||||
try self.syncCells(buf, cells);
|
||||
try buf.sync(self.device, cells.items);
|
||||
encoder.msgSend(
|
||||
void,
|
||||
objc.sel("setVertexBuffer:offset:atIndex:"),
|
||||
.{ buf.value, @as(c_ulong, 0), @as(c_ulong, 0) },
|
||||
.{ buf.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) },
|
||||
);
|
||||
|
||||
if (cells.items.len > 0) {
|
||||
@ -767,7 +699,7 @@ fn drawCells(
|
||||
@intFromEnum(mtl.MTLPrimitiveType.triangle),
|
||||
@as(c_ulong, 6),
|
||||
@intFromEnum(mtl.MTLIndexType.uint16),
|
||||
self.buf_instance.value,
|
||||
self.buf_instance.buffer.value,
|
||||
@as(c_ulong, 0),
|
||||
@as(c_ulong, cells.items.len),
|
||||
},
|
||||
@ -938,7 +870,7 @@ fn rebuildCells(
|
||||
// This is the cell that has [mode == .fg] and is underneath our cursor.
|
||||
// We keep track of it so that we can invert the colors so the character
|
||||
// remains visible.
|
||||
var cursor_cell: ?GPUCell = null;
|
||||
var cursor_cell: ?mtl_shaders.Cell = null;
|
||||
|
||||
// Build each cell
|
||||
var rowIter = screen.rowIterator(.viewport);
|
||||
@ -1043,7 +975,7 @@ fn rebuildCells(
|
||||
// We try to base on the cursor cell but if its not there
|
||||
// we use the actual cursor and if thats not there we give
|
||||
// up on preedit rendering.
|
||||
var cell: GPUCell = cursor_cell orelse
|
||||
var cell: mtl_shaders.Cell = cursor_cell orelse
|
||||
(real_cursor_cell orelse break :preedit).*;
|
||||
cell.color = .{ 0, 0, 0, 255 };
|
||||
|
||||
@ -1191,7 +1123,7 @@ pub fn updateCell(
|
||||
|
||||
// If we're rendering a color font, we use the color atlas
|
||||
const presentation = try self.font_group.group.presentationFromIndex(shaper_run.font_index);
|
||||
const mode: GPUCellMode = switch (presentation) {
|
||||
const mode: mtl_shaders.Cell.Mode = switch (presentation) {
|
||||
.text => .fg,
|
||||
.emoji => .fg_color,
|
||||
};
|
||||
@ -1249,7 +1181,7 @@ pub fn updateCell(
|
||||
return true;
|
||||
}
|
||||
|
||||
fn addCursor(self: *Metal, screen: *terminal.Screen) ?*const GPUCell {
|
||||
fn addCursor(self: *Metal, screen: *terminal.Screen) ?*const mtl_shaders.Cell {
|
||||
// Add the cursor
|
||||
const cell = screen.getCell(
|
||||
.active,
|
||||
@ -1297,7 +1229,7 @@ fn addCursor(self: *Metal, screen: *terminal.Screen) ?*const GPUCell {
|
||||
|
||||
/// Updates cell with the the given character. This returns true if the
|
||||
/// cell was successfully updated.
|
||||
fn updateCellChar(self: *Metal, cell: *GPUCell, cp: u21) bool {
|
||||
fn updateCellChar(self: *Metal, cell: *mtl_shaders.Cell, cp: u21) bool {
|
||||
// Get the font index for this codepoint
|
||||
const font_index = if (self.font_group.indexForCodepoint(
|
||||
self.alloc,
|
||||
@ -1333,52 +1265,6 @@ fn updateCellChar(self: *Metal, cell: *GPUCell, cp: u21) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Sync the vertex buffer inputs to the GPU. This will attempt to reuse
|
||||
/// the existing buffer (of course!) but will allocate a new buffer if
|
||||
/// our cells don't fit in it.
|
||||
fn syncCells(
|
||||
self: *Metal,
|
||||
target: *objc.Object,
|
||||
cells: std.ArrayListUnmanaged(GPUCell),
|
||||
) !void {
|
||||
const req_bytes = cells.items.len * @sizeOf(GPUCell);
|
||||
const avail_bytes = target.getProperty(c_ulong, "length");
|
||||
|
||||
// If we need more bytes than our buffer has, we need to reallocate.
|
||||
if (req_bytes > avail_bytes) {
|
||||
// Deallocate previous buffer
|
||||
deinitMTLResource(target.*);
|
||||
|
||||
// Allocate a new buffer with enough to hold double what we require.
|
||||
const size = req_bytes * 2;
|
||||
target.* = self.device.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("newBufferWithLength:options:"),
|
||||
.{
|
||||
@as(c_ulong, @intCast(size * @sizeOf(GPUCell))),
|
||||
mtl.MTLResourceStorageModeShared,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// We can fit within the vertex buffer so we can just replace bytes.
|
||||
const dst = dst: {
|
||||
const ptr = target.msgSend(?[*]u8, objc.sel("contents"), .{}) orelse {
|
||||
log.warn("buf_cells contents ptr is null", .{});
|
||||
return error.MetalFailed;
|
||||
};
|
||||
|
||||
break :dst ptr[0..req_bytes];
|
||||
};
|
||||
|
||||
const src = src: {
|
||||
const ptr = @as([*]const u8, @ptrCast(cells.items.ptr));
|
||||
break :src ptr[0..req_bytes];
|
||||
};
|
||||
|
||||
@memcpy(dst, src);
|
||||
}
|
||||
|
||||
/// Sync the atlas data to the given texture. This copies the bytes
|
||||
/// associated with the atlas to the given texture. If the atlas no longer
|
||||
/// fits into the texture, the texture will be resized.
|
||||
@ -1411,213 +1297,6 @@ fn syncAtlasTexture(device: objc.Object, atlas: *const font.Atlas, texture: *obj
|
||||
);
|
||||
}
|
||||
|
||||
/// Initialize the shader library.
|
||||
fn initLibrary(device: objc.Object, data: []const u8) !objc.Object {
|
||||
const source = try macos.foundation.String.createWithBytes(
|
||||
data,
|
||||
.utf8,
|
||||
false,
|
||||
);
|
||||
defer source.release();
|
||||
|
||||
var err: ?*anyopaque = null;
|
||||
const library = device.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("newLibraryWithSource:options:error:"),
|
||||
.{
|
||||
source,
|
||||
@as(?*anyopaque, null),
|
||||
&err,
|
||||
},
|
||||
);
|
||||
try checkError(err);
|
||||
|
||||
return library;
|
||||
}
|
||||
|
||||
/// Initialize the render pipeline for our shader library.
|
||||
fn initPipelineState(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(
|
||||
"uber_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(
|
||||
"uber_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 describves the
|
||||
// data layout of the vertex inputs. We use indexed (or "instanced")
|
||||
// rendering, so this makes it so that each instance gets a single
|
||||
// GPUCell 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, 0)},
|
||||
);
|
||||
|
||||
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar));
|
||||
attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "mode")));
|
||||
attr.setProperty("bufferIndex", @as(c_ulong, 0));
|
||||
}
|
||||
{
|
||||
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(GPUCell, "grid_pos")));
|
||||
attr.setProperty("bufferIndex", @as(c_ulong, 0));
|
||||
}
|
||||
{
|
||||
const attr = attrs.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("objectAtIndexedSubscript:"),
|
||||
.{@as(c_ulong, 2)},
|
||||
);
|
||||
|
||||
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uint2));
|
||||
attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "glyph_pos")));
|
||||
attr.setProperty("bufferIndex", @as(c_ulong, 0));
|
||||
}
|
||||
{
|
||||
const attr = attrs.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("objectAtIndexedSubscript:"),
|
||||
.{@as(c_ulong, 3)},
|
||||
);
|
||||
|
||||
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uint2));
|
||||
attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "glyph_size")));
|
||||
attr.setProperty("bufferIndex", @as(c_ulong, 0));
|
||||
}
|
||||
{
|
||||
const attr = attrs.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("objectAtIndexedSubscript:"),
|
||||
.{@as(c_ulong, 4)},
|
||||
);
|
||||
|
||||
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.int2));
|
||||
attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "glyph_offset")));
|
||||
attr.setProperty("bufferIndex", @as(c_ulong, 0));
|
||||
}
|
||||
{
|
||||
const attr = attrs.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("objectAtIndexedSubscript:"),
|
||||
.{@as(c_ulong, 5)},
|
||||
);
|
||||
|
||||
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4));
|
||||
attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "color")));
|
||||
attr.setProperty("bufferIndex", @as(c_ulong, 0));
|
||||
}
|
||||
{
|
||||
const attr = attrs.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("objectAtIndexedSubscript:"),
|
||||
.{@as(c_ulong, 6)},
|
||||
);
|
||||
|
||||
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar));
|
||||
attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "cell_width")));
|
||||
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 GPUCell per instance, not per vertex.
|
||||
layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance));
|
||||
layout.setProperty("stride", @as(c_ulong, @sizeOf(GPUCell)));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// Initialize a MTLTexture object for the given atlas.
|
||||
fn initAtlasTexture(device: objc.Object, atlas: *const font.Atlas) !objc.Object {
|
||||
// Determine our pixel format
|
||||
@ -1655,16 +1334,3 @@ fn initAtlasTexture(device: objc.Object, atlas: *const font.Atlas) !objc.Object
|
||||
fn deinitMTLResource(obj: objc.Object) void {
|
||||
obj.msgSend(void, objc.sel("release"), .{});
|
||||
}
|
||||
|
||||
fn checkError(err_: ?*anyopaque) !void {
|
||||
if (err_) |err| {
|
||||
const nserr = objc.Object.fromId(err);
|
||||
const str = @as(
|
||||
*macos.foundation.String,
|
||||
@ptrCast(nserr.getProperty(?*anyopaque, "localizedDescription").?),
|
||||
);
|
||||
|
||||
log.err("metal error={s}", .{str.cstringPtr(.ascii).?});
|
||||
return error.MetalFailed;
|
||||
}
|
||||
}
|
||||
|
91
src/renderer/metal/buffer.zig
Normal file
91
src/renderer/metal/buffer.zig
Normal file
@ -0,0 +1,91 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const objc = @import("objc");
|
||||
|
||||
const mtl = @import("api.zig");
|
||||
|
||||
const log = std.log.scoped(.metal);
|
||||
|
||||
/// Metal data storage for a certain set of equal types. This is usually
|
||||
/// used for vertex buffers, etc. This helpful wrapper makes it easy to
|
||||
/// prealloc, shrink, grow, sync, buffers with Metal.
|
||||
pub fn Buffer(comptime T: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
buffer: objc.Object, // MTLBuffer
|
||||
|
||||
/// Initialize a buffer with the given length pre-allocated.
|
||||
pub fn init(device: objc.Object, len: usize) !Self {
|
||||
const buffer = device.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("newBufferWithLength:options:"),
|
||||
.{
|
||||
@as(c_ulong, @intCast(len * @sizeOf(T))),
|
||||
mtl.MTLResourceStorageModeShared,
|
||||
},
|
||||
);
|
||||
|
||||
return .{ .buffer = buffer };
|
||||
}
|
||||
|
||||
/// Init the buffer filled with the given data.
|
||||
pub fn initFill(device: objc.Object, data: []const T) !Self {
|
||||
const buffer = device.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("newBufferWithBytes:length:options:"),
|
||||
.{
|
||||
@as(*const anyopaque, @ptrCast(data.ptr)),
|
||||
@as(c_ulong, @intCast(data.len * @sizeOf(T))),
|
||||
mtl.MTLResourceStorageModeShared,
|
||||
},
|
||||
);
|
||||
|
||||
return .{ .buffer = buffer };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.buffer.msgSend(void, objc.sel("release"), .{});
|
||||
}
|
||||
|
||||
/// Sync new contents to the buffer.
|
||||
pub fn sync(self: *Self, device: objc.Object, data: []const T) !void {
|
||||
// If we need more bytes than our buffer has, we need to reallocate.
|
||||
const req_bytes = data.len * @sizeOf(T);
|
||||
const avail_bytes = self.buffer.getProperty(c_ulong, "length");
|
||||
if (req_bytes > avail_bytes) {
|
||||
// Deallocate previous buffer
|
||||
self.buffer.msgSend(void, objc.sel("release"), .{});
|
||||
|
||||
// Allocate a new buffer with enough to hold double what we require.
|
||||
const size = req_bytes * 2;
|
||||
self.buffer = device.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("newBufferWithLength:options:"),
|
||||
.{
|
||||
@as(c_ulong, @intCast(size * @sizeOf(T))),
|
||||
mtl.MTLResourceStorageModeShared,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// We can fit within the buffer so we can just replace bytes.
|
||||
const dst = dst: {
|
||||
const ptr = self.buffer.msgSend(?[*]u8, objc.sel("contents"), .{}) orelse {
|
||||
log.warn("buffer contents ptr is null", .{});
|
||||
return error.MetalFailed;
|
||||
};
|
||||
|
||||
break :dst ptr[0..req_bytes];
|
||||
};
|
||||
|
||||
const src = src: {
|
||||
const ptr = @as([*]const u8, @ptrCast(data.ptr));
|
||||
break :src ptr[0..req_bytes];
|
||||
};
|
||||
|
||||
@memcpy(dst, src);
|
||||
}
|
||||
};
|
||||
}
|
301
src/renderer/metal/shaders.zig
Normal file
301
src/renderer/metal/shaders.zig
Normal file
@ -0,0 +1,301 @@
|
||||
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 log = std.log.scoped(.metal);
|
||||
|
||||
/// This contains the state for the shaders used by the Metal renderer.
|
||||
pub const Shaders = struct {
|
||||
library: objc.Object,
|
||||
cell_pipeline: objc.Object,
|
||||
|
||||
pub fn init(device: objc.Object) !Shaders {
|
||||
const library = try initLibrary(device);
|
||||
errdefer library.msgSend(void, objc.sel("release"), .{});
|
||||
|
||||
const cell_pipeline = try initCellPipeline(device, library);
|
||||
errdefer cell_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||
|
||||
return .{
|
||||
.library = library,
|
||||
.cell_pipeline = cell_pipeline,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Shaders) void {
|
||||
self.cell_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||
self.library.msgSend(void, objc.sel("release"), .{});
|
||||
}
|
||||
};
|
||||
|
||||
/// This is a single parameter for the terminal cell shader.
|
||||
pub const Cell = extern struct {
|
||||
mode: Mode,
|
||||
grid_pos: [2]f32,
|
||||
glyph_pos: [2]u32 = .{ 0, 0 },
|
||||
glyph_size: [2]u32 = .{ 0, 0 },
|
||||
glyph_offset: [2]i32 = .{ 0, 0 },
|
||||
color: [4]u8,
|
||||
cell_width: u8,
|
||||
|
||||
pub const Mode = enum(u8) {
|
||||
bg = 1,
|
||||
fg = 2,
|
||||
fg_color = 7,
|
||||
strikethrough = 8,
|
||||
};
|
||||
};
|
||||
|
||||
/// The uniforms that are passed to the terminal cell shader.
|
||||
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,
|
||||
|
||||
/// Size of a single cell in pixels, unscaled.
|
||||
cell_size: [2]f32,
|
||||
|
||||
/// Metrics for underline/strikethrough
|
||||
strikethrough_position: f32,
|
||||
strikethrough_thickness: f32,
|
||||
};
|
||||
|
||||
/// Initialize the MTLLibrary. A MTLLibrary is a collection of shaders.
|
||||
fn initLibrary(device: objc.Object) !objc.Object {
|
||||
// Hardcoded since this file isn't meant to be reusable.
|
||||
const data = @embedFile("../shaders/cell.metal");
|
||||
const source = try macos.foundation.String.createWithBytes(
|
||||
data,
|
||||
.utf8,
|
||||
false,
|
||||
);
|
||||
defer source.release();
|
||||
|
||||
var err: ?*anyopaque = null;
|
||||
const library = device.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("newLibraryWithSource:options:error:"),
|
||||
.{
|
||||
source,
|
||||
@as(?*anyopaque, null),
|
||||
&err,
|
||||
},
|
||||
);
|
||||
try checkError(err);
|
||||
|
||||
return library;
|
||||
}
|
||||
|
||||
/// Initialize the cell render pipeline for our shader library.
|
||||
fn initCellPipeline(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(
|
||||
"uber_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(
|
||||
"uber_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 describves 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.
|
||||
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, 0)},
|
||||
);
|
||||
|
||||
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar));
|
||||
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "mode")));
|
||||
attr.setProperty("bufferIndex", @as(c_ulong, 0));
|
||||
}
|
||||
{
|
||||
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(Cell, "grid_pos")));
|
||||
attr.setProperty("bufferIndex", @as(c_ulong, 0));
|
||||
}
|
||||
{
|
||||
const attr = attrs.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("objectAtIndexedSubscript:"),
|
||||
.{@as(c_ulong, 2)},
|
||||
);
|
||||
|
||||
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uint2));
|
||||
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "glyph_pos")));
|
||||
attr.setProperty("bufferIndex", @as(c_ulong, 0));
|
||||
}
|
||||
{
|
||||
const attr = attrs.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("objectAtIndexedSubscript:"),
|
||||
.{@as(c_ulong, 3)},
|
||||
);
|
||||
|
||||
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uint2));
|
||||
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "glyph_size")));
|
||||
attr.setProperty("bufferIndex", @as(c_ulong, 0));
|
||||
}
|
||||
{
|
||||
const attr = attrs.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("objectAtIndexedSubscript:"),
|
||||
.{@as(c_ulong, 4)},
|
||||
);
|
||||
|
||||
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.int2));
|
||||
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "glyph_offset")));
|
||||
attr.setProperty("bufferIndex", @as(c_ulong, 0));
|
||||
}
|
||||
{
|
||||
const attr = attrs.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("objectAtIndexedSubscript:"),
|
||||
.{@as(c_ulong, 5)},
|
||||
);
|
||||
|
||||
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4));
|
||||
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "color")));
|
||||
attr.setProperty("bufferIndex", @as(c_ulong, 0));
|
||||
}
|
||||
{
|
||||
const attr = attrs.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("objectAtIndexedSubscript:"),
|
||||
.{@as(c_ulong, 6)},
|
||||
);
|
||||
|
||||
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar));
|
||||
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "cell_width")));
|
||||
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 Cell per instance, not per vertex.
|
||||
layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance));
|
||||
layout.setProperty("stride", @as(c_ulong, @sizeOf(Cell)));
|
||||
}
|
||||
|
||||
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(
|
||||
*macos.foundation.String,
|
||||
@ptrCast(nserr.getProperty(?*anyopaque, "localizedDescription").?),
|
||||
);
|
||||
|
||||
log.err("metal error={s}", .{str.cstringPtr(.ascii).?});
|
||||
return error.MetalFailed;
|
||||
}
|
||||
|
||||
// Intel macOS 13 doesn't like it when any field in a vertex buffer is not
|
||||
// aligned on the alignment of the struct. I don't understand it, I think
|
||||
// this must be some macOS 13 Metal GPU driver bug because it doesn't matter
|
||||
// on macOS 12 or Apple Silicon macOS 13.
|
||||
//
|
||||
// To be safe, we put this test in here.
|
||||
test "Cell offsets" {
|
||||
const testing = std.testing;
|
||||
const alignment = @alignOf(Cell);
|
||||
inline for (@typeInfo(Cell).Struct.fields) |field| {
|
||||
const offset = @offsetOf(Cell, field.name);
|
||||
try testing.expectEqual(0, @mod(offset, alignment));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user