ghostty/src/renderer/metal/Texture.zig
2025-06-20 15:18:41 -06:00

202 lines
4.8 KiB
Zig

//! Wrapper for handling textures.
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const objc = @import("objc");
const mtl = @import("api.zig");
const Metal = @import("../Metal.zig");
const log = std.log.scoped(.metal);
/// Options for initializing a texture.
pub const Options = struct {
/// MTLDevice
device: objc.Object,
pixel_format: mtl.MTLPixelFormat,
resource_options: mtl.MTLResourceOptions,
};
/// The underlying MTLTexture Object.
texture: objc.Object,
/// The width of this texture.
width: usize,
/// The height of this texture.
height: usize,
/// Bytes per pixel for this texture.
bpp: usize,
pub const Error = error{
/// A Metal API call failed.
MetalFailed,
};
/// Initialize a texture
pub fn init(
opts: Options,
width: usize,
height: usize,
data: ?[]const u8,
) Error!Self {
// Create our descriptor
const desc = init: {
const Class = objc.getClass("MTLTextureDescriptor").?;
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;
};
errdefer desc.msgSend(void, objc.sel("release"), .{});
// Set our properties
desc.setProperty("pixelFormat", @intFromEnum(opts.pixel_format));
desc.setProperty("width", @as(c_ulong, width));
desc.setProperty("height", @as(c_ulong, height));
desc.setProperty("resourceOptions", opts.resource_options);
// Initialize
const id = opts.device.msgSend(
?*anyopaque,
objc.sel("newTextureWithDescriptor:"),
.{desc},
) orelse return error.MetalFailed;
const self: Self = .{
.texture = objc.Object.fromId(id),
.width = width,
.height = height,
.bpp = bppOf(opts.pixel_format),
};
// If we have data, we set it here.
if (data) |d| {
assert(d.len == width * height * self.bpp);
try self.replaceRegion(0, 0, width, height, d);
}
return self;
}
pub fn deinit(self: Self) void {
self.texture.release();
}
/// Replace a region of the texture with the provided data.
///
/// Does NOT check the dimensions of the data to ensure correctness.
pub fn replaceRegion(
self: Self,
x: usize,
y: usize,
width: usize,
height: usize,
data: []const u8,
) error{}!void {
self.texture.msgSend(
void,
objc.sel("replaceRegion:mipmapLevel:withBytes:bytesPerRow:"),
.{
mtl.MTLRegion{
.origin = .{ .x = x, .y = y, .z = 0 },
.size = .{
.width = @intCast(width),
.height = @intCast(height),
.depth = 1,
},
},
@as(c_ulong, 0),
@as(*const anyopaque, data.ptr),
@as(c_ulong, self.bpp * width),
},
);
}
/// Returns the bytes per pixel for the provided pixel format
fn bppOf(pixel_format: mtl.MTLPixelFormat) usize {
return switch (pixel_format) {
// Invalid
.invalid => @panic("invalid pixel format"),
// Weird formats I was too lazy to get the sizes of
else => @panic("pixel format size unknown (unlikely that this format was actually used, could be memory corruption)"),
// 8-bit pixel formats
.a8unorm,
.r8unorm,
.r8unorm_srgb,
.r8snorm,
.r8uint,
.r8sint,
.rg8unorm,
.rg8unorm_srgb,
.rg8snorm,
.rg8uint,
.rg8sint,
.stencil8,
=> 1,
// 16-bit pixel formats
.r16unorm,
.r16snorm,
.r16uint,
.r16sint,
.r16float,
.rg16unorm,
.rg16snorm,
.rg16uint,
.rg16sint,
.rg16float,
.b5g6r5unorm,
.a1bgr5unorm,
.abgr4unorm,
.bgr5a1unorm,
.depth16unorm,
=> 2,
// 32-bit pixel formats
.rgba8unorm,
.rgba8unorm_srgb,
.rgba8snorm,
.rgba8uint,
.rgba8sint,
.bgra8unorm,
.bgra8unorm_srgb,
.rgb10a2unorm,
.rgb10a2uint,
.rg11b10float,
.rgb9e5float,
.bgr10a2unorm,
.bgr10_xr,
.bgr10_xr_srgb,
.r32uint,
.r32sint,
.r32float,
.depth32float,
.depth24unorm_stencil8,
=> 4,
// 64-bit pixel formats
.rg32uint,
.rg32sint,
.rg32float,
.rgba16unorm,
.rgba16snorm,
.rgba16uint,
.rgba16sint,
.rgba16float,
.bgra10_xr,
.bgra10_xr_srgb,
=> 8,
// 128-bit pixel formats,
.rgba32uint,
.rgba32sint,
.rgba32float,
=> 128,
};
}