ghostty/src/renderer/metal/buffer.zig
2024-08-05 13:56:57 +02:00

160 lines
6.0 KiB
Zig

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"), .{});
}
/// Get the buffer contents as a slice of T. The contents are
/// mutable. The contents may or may not be automatically synced
/// depending on the buffer storage mode. See the Metal docs.
pub fn contents(self: *Self) ![]T {
const len_bytes = self.buffer.getProperty(c_ulong, "length");
assert(@mod(len_bytes, @sizeOf(T)) == 0);
const len = @divExact(len_bytes, @sizeOf(T));
const ptr = self.buffer.msgSend(
?[*]T,
objc.sel("contents"),
.{},
).?;
return ptr[0..len];
}
/// 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, 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);
}
/// 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, device: objc.Object, lists: []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 = 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];
};
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);
}
return total_len;
}
};
}