ghostty/src/renderer/metal/buffer.zig
Qwerasd 371d62a82c renderer: big rework, graphics API abstraction layers, unified logic
This commit is very large, representing about a month of work with many
interdependent changes that don't separate cleanly in to atomic commits.

The main change here is unifying the renderer logic to a single generic
renderer, implemented on top of an abstraction layer over OpenGL/Metal.

I'll write a more complete summary of the changes in the description of
the PR.
2025-06-20 15:18:41 -06:00

187 lines
7.0 KiB
Zig

const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const objc = @import("objc");
const macos = @import("macos");
const mtl = @import("api.zig");
const Metal = @import("../Metal.zig");
const log = std.log.scoped(.metal);
/// Options for initializing a buffer.
pub const Options = struct {
/// MTLDevice
device: objc.Object,
resource_options: mtl.MTLResourceOptions,
};
/// 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();
/// The options this buffer was initialized with.
opts: Options,
/// The underlying MTLBuffer object.
buffer: objc.Object,
/// The allocated length of the buffer.
/// Note that this is the number
/// of `T`s not the size in bytes.
len: usize,
/// Initialize a buffer with the given length pre-allocated.
pub fn init(opts: Options, len: usize) !Self {
const buffer = opts.device.msgSend(
objc.Object,
objc.sel("newBufferWithLength:options:"),
.{
@as(c_ulong, @intCast(len * @sizeOf(T))),
opts.resource_options,
},
);
return .{ .buffer = buffer, .opts = opts, .len = len };
}
/// Init the buffer filled with the given data.
pub fn initFill(opts: Options, data: []const T) !Self {
const buffer = opts.device.msgSend(
objc.Object,
objc.sel("newBufferWithBytes:length:options:"),
.{
@as(*const anyopaque, @ptrCast(data.ptr)),
@as(c_ulong, @intCast(data.len * @sizeOf(T))),
opts.resource_options,
},
);
return .{ .buffer = buffer, .opts = opts, .len = data.len };
}
pub fn deinit(self: *const Self) void {
self.buffer.msgSend(void, objc.sel("release"), .{});
}
/// Sync new contents to the buffer. The data is expected to be the
/// complete contents of the buffer. If the amount of data is larger
/// than the buffer length, the buffer will be reallocated.
///
/// If the amount of data is smaller than the buffer length, the
/// remaining data in the buffer is left untouched.
pub fn sync(self: *Self, 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 = self.opts.device.msgSend(
objc.Object,
objc.sel("newBufferWithLength:options:"),
.{
@as(c_ulong, @intCast(size * @sizeOf(T))),
self.opts.resource_options,
},
);
}
// 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);
// If we're using the managed resource storage mode, then
// we need to signal Metal to synchronize the buffer data.
//
// Ref: https://developer.apple.com/documentation/metal/synchronizing-a-managed-resource-in-macos?language=objc
if (self.opts.resource_options.storage_mode == .managed) {
self.buffer.msgSend(
void,
"didModifyRange:",
.{macos.foundation.Range.init(0, req_bytes)},
);
}
}
/// Like Buffer.sync but takes data from an array of ArrayLists,
/// rather than a single array. Returns the number of items synced.
pub fn syncFromArrayLists(self: *Self, lists: []const std.ArrayListUnmanaged(T)) !usize {
var total_len: usize = 0;
for (lists) |list| {
total_len += list.items.len;
}
// If we need more bytes than our buffer has, we need to reallocate.
const req_bytes = total_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 = self.opts.device.msgSend(
objc.Object,
objc.sel("newBufferWithLength:options:"),
.{
@as(c_ulong, @intCast(size * @sizeOf(T))),
self.opts.resource_options,
},
);
}
// 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];
};
var i: usize = 0;
for (lists) |list| {
const ptr = @as([*]const u8, @ptrCast(list.items.ptr));
@memcpy(dst[i..][0 .. list.items.len * @sizeOf(T)], ptr);
i += list.items.len * @sizeOf(T);
}
// If we're using the managed resource storage mode, then
// we need to signal Metal to synchronize the buffer data.
//
// Ref: https://developer.apple.com/documentation/metal/synchronizing-a-managed-resource-in-macos?language=objc
if (self.opts.resource_options.storage_mode == .managed) {
self.buffer.msgSend(
void,
"didModifyRange:",
.{macos.foundation.Range.init(0, req_bytes)},
);
}
return total_len;
}
};
}