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