renderer/metal: extract helpers for shaders/buffers

This commit is contained in:
Mitchell Hashimoto
2023-08-22 08:49:34 -07:00
parent da4ead8f60
commit 5229cb93d2
3 changed files with 446 additions and 388 deletions

View File

@ -22,9 +22,14 @@ const Allocator = std.mem.Allocator;
const Terminal = terminal.Terminal; const Terminal = terminal.Terminal;
const mtl = @import("metal/api.zig"); const mtl = @import("metal/api.zig");
const mtl_buffer = @import("metal/buffer.zig");
const mtl_image = @import("metal/image.zig"); const mtl_image = @import("metal/image.zig");
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 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. // Get native API access on certain platforms so we can do more customization.
const glfwNative = glfw.Native(.{ 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 /// 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 /// but we keep this around so that we don't reallocate. Each set of
/// cells goes into a separate shader. /// cells goes into a separate shader.
cells_bg: std.ArrayListUnmanaged(GPUCell), cells_bg: std.ArrayListUnmanaged(mtl_shaders.Cell),
cells: std.ArrayListUnmanaged(GPUCell), cells: std.ArrayListUnmanaged(mtl_shaders.Cell),
/// The current GPU uniform values. /// The current GPU uniform values.
uniforms: GPUUniforms, uniforms: mtl_shaders.Uniforms,
/// The font structures. /// The font structures.
font_group: *font.GroupCache, font_group: *font.GroupCache,
@ -76,62 +81,19 @@ font_shaper: font.Shaper,
/// The images that we may render. /// The images that we may render.
images: ImageMap = .{}, 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 /// Metal objects
device: objc.Object, // MTLDevice device: objc.Object, // MTLDevice
queue: objc.Object, // MTLCommandQueue queue: objc.Object, // MTLCommandQueue
swapchain: objc.Object, // CAMetalLayer 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_greyscale: objc.Object, // MTLTexture
texture_color: 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 /// The configuration for this renderer that is derived from the main
/// configuration. This must be exported so that we don't need to /// configuration. This must be exported so that we don't need to
/// pass around Config pointers which makes memory management a pain. /// 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(); errdefer font_shaper.deinit();
// Initialize our Metal buffers // Vertex buffers
const buf_instance = buffer: { var buf_cells = try CellBuffer.init(device, 160 * 160);
const data = [6]u16{ errdefer buf_cells.deinit();
0, 1, 3, // Top-left triangle var buf_cells_bg = try CellBuffer.init(device, 160 * 160);
1, 2, 3, // Bottom-right triangle 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( // Initialize our shaders
objc.Object, var shaders = try Shaders.init(device);
objc.sel("newBufferWithBytes:length:options:"), errdefer shaders.deinit();
.{
@as(*const anyopaque, @ptrCast(&data)),
@as(c_ulong, @intCast(data.len * @sizeOf(u16))),
mtl.MTLResourceStorageModeShared,
},
);
};
const buf_cells = buffer: { // Font atlas textures
// 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);
const texture_greyscale = try initAtlasTexture(device, &options.font_group.atlas_greyscale); const texture_greyscale = try initAtlasTexture(device, &options.font_group.atlas_greyscale);
const texture_color = try initAtlasTexture(device, &options.font_group.atlas_color); 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_group = options.font_group,
.font_shaper = font_shaper, .font_shaper = font_shaper,
// Shaders
.shaders = shaders,
.buf_cells = buf_cells,
.buf_cells_bg = buf_cells_bg,
.buf_instance = buf_instance,
// Metal stuff // Metal stuff
.device = device, .device = device,
.queue = queue, .queue = queue,
.swapchain = swapchain, .swapchain = swapchain,
.buf_cells = buf_cells,
.buf_cells_bg = buf_cells_bg,
.buf_instance = buf_instance,
.pipeline = pipeline_state,
.texture_greyscale = texture_greyscale, .texture_greyscale = texture_greyscale,
.texture_color = texture_color, .texture_color = texture_color,
}; };
@ -355,13 +284,15 @@ pub fn deinit(self: *Metal) void {
self.images.deinit(self.alloc); self.images.deinit(self.alloc);
} }
deinitMTLResource(self.buf_cells_bg); self.buf_cells_bg.deinit();
deinitMTLResource(self.buf_cells); self.buf_cells.deinit();
deinitMTLResource(self.buf_instance); self.buf_instance.deinit();
deinitMTLResource(self.texture_greyscale); deinitMTLResource(self.texture_greyscale);
deinitMTLResource(self.texture_color); deinitMTLResource(self.texture_color);
self.queue.msgSend(void, objc.sel("release"), .{}); self.queue.msgSend(void, objc.sel("release"), .{});
self.shaders.deinit();
self.* = undefined; self.* = undefined;
} }
@ -699,11 +630,12 @@ pub fn render(
); );
defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); defer encoder.msgSend(void, objc.sel("endEncoding"), .{});
//do we need to do this?
//encoder.msgSend(void, objc.sel("setViewport:"), .{viewport});
// Use our shader pipeline // 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 // Set our buffers
encoder.msgSend( encoder.msgSend(
@ -749,14 +681,14 @@ pub fn render(
fn drawCells( fn drawCells(
self: *Metal, self: *Metal,
encoder: objc.Object, encoder: objc.Object,
buf: *objc.Object, buf: *CellBuffer,
cells: std.ArrayListUnmanaged(GPUCell), cells: std.ArrayListUnmanaged(mtl_shaders.Cell),
) !void { ) !void {
try self.syncCells(buf, cells); try buf.sync(self.device, cells.items);
encoder.msgSend( encoder.msgSend(
void, void,
objc.sel("setVertexBuffer:offset:atIndex:"), 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) { if (cells.items.len > 0) {
@ -767,7 +699,7 @@ fn drawCells(
@intFromEnum(mtl.MTLPrimitiveType.triangle), @intFromEnum(mtl.MTLPrimitiveType.triangle),
@as(c_ulong, 6), @as(c_ulong, 6),
@intFromEnum(mtl.MTLIndexType.uint16), @intFromEnum(mtl.MTLIndexType.uint16),
self.buf_instance.value, self.buf_instance.buffer.value,
@as(c_ulong, 0), @as(c_ulong, 0),
@as(c_ulong, cells.items.len), @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. // 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 // We keep track of it so that we can invert the colors so the character
// remains visible. // remains visible.
var cursor_cell: ?GPUCell = null; var cursor_cell: ?mtl_shaders.Cell = null;
// Build each cell // Build each cell
var rowIter = screen.rowIterator(.viewport); 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 try to base on the cursor cell but if its not there
// we use the actual cursor and if thats not there we give // we use the actual cursor and if thats not there we give
// up on preedit rendering. // up on preedit rendering.
var cell: GPUCell = cursor_cell orelse var cell: mtl_shaders.Cell = cursor_cell orelse
(real_cursor_cell orelse break :preedit).*; (real_cursor_cell orelse break :preedit).*;
cell.color = .{ 0, 0, 0, 255 }; 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 // 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 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, .text => .fg,
.emoji => .fg_color, .emoji => .fg_color,
}; };
@ -1249,7 +1181,7 @@ pub fn updateCell(
return true; 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 // Add the cursor
const cell = screen.getCell( const cell = screen.getCell(
.active, .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 /// Updates cell with the the given character. This returns true if the
/// cell was successfully updated. /// 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 // Get the font index for this codepoint
const font_index = if (self.font_group.indexForCodepoint( const font_index = if (self.font_group.indexForCodepoint(
self.alloc, self.alloc,
@ -1333,52 +1265,6 @@ fn updateCellChar(self: *Metal, cell: *GPUCell, cp: u21) bool {
return true; 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 /// 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 /// associated with the atlas to the given texture. If the atlas no longer
/// fits into the texture, the texture will be resized. /// 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. /// Initialize a MTLTexture object for the given atlas.
fn initAtlasTexture(device: objc.Object, atlas: *const font.Atlas) !objc.Object { fn initAtlasTexture(device: objc.Object, atlas: *const font.Atlas) !objc.Object {
// Determine our pixel format // 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 { fn deinitMTLResource(obj: objc.Object) void {
obj.msgSend(void, objc.sel("release"), .{}); 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;
}
}

View 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);
}
};
}

View 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));
}
}