From 5229cb93d2e03cc7eb927450f26842536471a762 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 22 Aug 2023 08:49:34 -0700 Subject: [PATCH] renderer/metal: extract helpers for shaders/buffers --- src/renderer/Metal.zig | 442 ++++----------------------------- src/renderer/metal/buffer.zig | 91 +++++++ src/renderer/metal/shaders.zig | 301 ++++++++++++++++++++++ 3 files changed, 446 insertions(+), 388 deletions(-) create mode 100644 src/renderer/metal/buffer.zig create mode 100644 src/renderer/metal/shaders.zig diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 8f346f3db..758f749db 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -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{ - 0, 1, 3, // Top-left triangle - 1, 2, 3, // Bottom-right triangle - }; + // 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; - } -} diff --git a/src/renderer/metal/buffer.zig b/src/renderer/metal/buffer.zig new file mode 100644 index 000000000..eb5c1d193 --- /dev/null +++ b/src/renderer/metal/buffer.zig @@ -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); + } + }; +} diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig new file mode 100644 index 000000000..cf1125a51 --- /dev/null +++ b/src/renderer/metal/shaders.zig @@ -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)); + } +}