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.
This commit is contained in:
Qwerasd
2025-06-16 17:44:44 -06:00
parent 521872442a
commit 371d62a82c
68 changed files with 7088 additions and 7311 deletions

View File

@ -147,10 +147,6 @@ extension Ghostty {
// We need to support being a first responder so that we can get input events
override var acceptsFirstResponder: Bool { return true }
// I don't think we need this but this lets us know we should redraw our layer
// so we'll use that to tell ghostty to refresh.
override var wantsUpdateLayer: Bool { return true }
init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) {
self.markedText = NSMutableAttributedString()
self.uuid = uuid ?? .init()
@ -703,11 +699,6 @@ extension Ghostty {
setSurfaceSize(width: UInt32(fbFrame.size.width), height: UInt32(fbFrame.size.height))
}
override func updateLayer() {
guard let surface = self.surface else { return }
ghostty_surface_draw(surface);
}
override func mouseDown(with event: NSEvent) {
guard let surface = self.surface else { return }
let mods = Ghostty.ghosttyMods(event.modifierFlags)

View File

@ -2,6 +2,8 @@ pub const c = @import("animation/c.zig").c;
/// https://developer.apple.com/documentation/quartzcore/calayer/contents_gravity_values?language=objc
pub extern "c" const kCAGravityTopLeft: *anyopaque;
pub extern "c" const kCAGravityBottomLeft: *anyopaque;
pub extern "c" const kCAGravityCenter: *anyopaque;
test {
@import("std").testing.refAllDecls(@This());

View File

@ -33,6 +33,7 @@ pub fn build(b: *std.Build) !void {
lib.linkFramework("CoreText");
lib.linkFramework("CoreVideo");
lib.linkFramework("QuartzCore");
lib.linkFramework("IOSurface");
if (target.result.os.tag == .macos) {
lib.linkFramework("Carbon");
module.linkFramework("Carbon", .{});
@ -44,6 +45,7 @@ pub fn build(b: *std.Build) !void {
module.linkFramework("CoreText", .{});
module.linkFramework("CoreVideo", .{});
module.linkFramework("QuartzCore", .{});
module.linkFramework("IOSurface", .{});
try apple_sdk.addPaths(b, lib);
}

View File

@ -3,6 +3,16 @@ pub const data = @import("dispatch/data.zig");
pub const queue = @import("dispatch/queue.zig");
pub const Data = data.Data;
pub extern "c" fn dispatch_sync(
queue: *anyopaque,
block: *anyopaque,
) void;
pub extern "c" fn dispatch_async(
queue: *anyopaque,
block: *anyopaque,
) void;
test {
@import("std").testing.refAllDecls(@This());
}

View File

@ -30,6 +30,7 @@ pub const stringGetSurrogatePairForLongCharacter = string.stringGetSurrogatePair
pub const URL = url.URL;
pub const URLPathStyle = url.URLPathStyle;
pub const CFRelease = typepkg.CFRelease;
pub const CFRetain = typepkg.CFRetain;
test {
@import("std").testing.refAllDecls(@This());

View File

@ -1 +1,2 @@
pub extern "c" fn CFRelease(*anyopaque) void;
pub extern "c" fn CFRetain(*anyopaque) void;

8
pkg/macos/iosurface.zig Normal file
View File

@ -0,0 +1,8 @@
const iosurface = @import("iosurface/iosurface.zig");
pub const c = @import("iosurface/c.zig").c;
pub const IOSurface = iosurface.IOSurface;
test {
@import("std").testing.refAllDecls(@This());
}

View File

@ -0,0 +1 @@
pub const c = @import("../main.zig").c;

View File

@ -0,0 +1,136 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const c = @import("c.zig").c;
const foundation = @import("../foundation.zig");
const graphics = @import("../graphics.zig");
const video = @import("../video.zig");
pub const IOSurface = opaque {
pub const Error = error{
InvalidOperation,
};
pub const Properties = struct {
width: c_int,
height: c_int,
pixel_format: video.PixelFormat,
bytes_per_element: c_int,
colorspace: ?*graphics.ColorSpace,
};
pub fn init(properties: Properties) Allocator.Error!*IOSurface {
var w = try foundation.Number.create(.int, &properties.width);
defer w.release();
var h = try foundation.Number.create(.int, &properties.height);
defer h.release();
var pf = try foundation.Number.create(.int, &@as(c_int, @intFromEnum(properties.pixel_format)));
defer pf.release();
var bpe = try foundation.Number.create(.int, &properties.bytes_per_element);
defer bpe.release();
var properties_dict = try foundation.Dictionary.create(
&[_]?*const anyopaque{
c.kIOSurfaceWidth,
c.kIOSurfaceHeight,
c.kIOSurfacePixelFormat,
c.kIOSurfaceBytesPerElement,
},
&[_]?*const anyopaque{ w, h, pf, bpe },
);
defer properties_dict.release();
var surface = @as(?*IOSurface, @ptrFromInt(@intFromPtr(
c.IOSurfaceCreate(@ptrCast(properties_dict)),
))) orelse return error.OutOfMemory;
if (properties.colorspace) |space| {
surface.setColorSpace(space);
}
return surface;
}
pub fn deinit(self: *IOSurface) void {
// We mark it purgeable so that it is immediately unloaded, so that we
// don't have to wait for CoreFoundation garbage collection to trigger.
_ = c.IOSurfaceSetPurgeable(
@ptrCast(self),
c.kIOSurfacePurgeableEmpty,
null,
);
foundation.CFRelease(self);
}
pub fn retain(self: *IOSurface) void {
foundation.CFRetain(self);
}
pub fn release(self: *IOSurface) void {
foundation.CFRelease(self);
}
pub fn setColorSpace(self: *IOSurface, colorspace: *graphics.ColorSpace) void {
const serialized_colorspace = graphics.c.CGColorSpaceCopyPropertyList(
@ptrCast(colorspace),
).?;
defer foundation.CFRelease(@constCast(serialized_colorspace));
c.IOSurfaceSetValue(
@ptrCast(self),
c.kIOSurfaceColorSpace,
@ptrCast(serialized_colorspace),
);
}
pub inline fn lock(self: *IOSurface) void {
c.IOSurfaceLock(
@ptrCast(self),
0,
null,
);
}
pub inline fn unlock(self: *IOSurface) void {
c.IOSurfaceUnlock(
@ptrCast(self),
0,
null,
);
}
pub inline fn getAllocSize(self: *IOSurface) usize {
return c.IOSurfaceGetAllocSize(@ptrCast(self));
}
pub inline fn getWidth(self: *IOSurface) usize {
return c.IOSurfaceGetWidth(@ptrCast(self));
}
pub inline fn getHeight(self: *IOSurface) usize {
return c.IOSurfaceGetHeight(@ptrCast(self));
}
pub inline fn getBytesPerElement(self: *IOSurface) usize {
return c.IOSurfaceGetBytesPerElement(@ptrCast(self));
}
pub inline fn getBytesPerRow(self: *IOSurface) usize {
return c.IOSurfaceGetBytesPerRow(@ptrCast(self));
}
pub inline fn getBaseAddress(self: *IOSurface) ?[*]u8 {
return @ptrCast(c.IOSurfaceGetBaseAddress(@ptrCast(self)));
}
pub inline fn getElementWidth(self: *IOSurface) usize {
return c.IOSurfaceGetElementWidth(@ptrCast(self));
}
pub inline fn getElementHeight(self: *IOSurface) usize {
return c.IOSurfaceGetElementHeight(@ptrCast(self));
}
pub inline fn getPixelFormat(self: *IOSurface) video.PixelFormat {
return @enumFromInt(c.IOSurfaceGetPixelFormat(@ptrCast(self)));
}
};

View File

@ -8,6 +8,7 @@ pub const graphics = @import("graphics.zig");
pub const os = @import("os.zig");
pub const text = @import("text.zig");
pub const video = @import("video.zig");
pub const iosurface = @import("iosurface.zig");
// All of our C imports consolidated into one place. We used to
// import them one by one in each package but Zig 0.14 has some
@ -17,7 +18,9 @@ pub const c = @cImport({
@cInclude("CoreGraphics/CoreGraphics.h");
@cInclude("CoreText/CoreText.h");
@cInclude("CoreVideo/CoreVideo.h");
@cInclude("CoreVideo/CVPixelBuffer.h");
@cInclude("QuartzCore/CALayer.h");
@cInclude("IOSurface/IOSurfaceRef.h");
@cInclude("dispatch/dispatch.h");
@cInclude("os/log.h");

View File

@ -1,7 +1,9 @@
const display_link = @import("video/display_link.zig");
const pixel_format = @import("video/pixel_format.zig");
pub const c = @import("video/c.zig").c;
pub const DisplayLink = display_link.DisplayLink;
pub const PixelFormat = pixel_format.PixelFormat;
test {
@import("std").testing.refAllDecls(@This());

View File

@ -0,0 +1,171 @@
const c = @import("c.zig").c;
pub const PixelFormat = enum(c_int) {
/// 1 bit indexed
@"1Monochrome" = c.kCVPixelFormatType_1Monochrome,
/// 2 bit indexed
@"2Indexed" = c.kCVPixelFormatType_2Indexed,
/// 4 bit indexed
@"4Indexed" = c.kCVPixelFormatType_4Indexed,
/// 8 bit indexed
@"8Indexed" = c.kCVPixelFormatType_8Indexed,
/// 1 bit indexed gray, white is zero
@"1IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_1IndexedGray_WhiteIsZero,
/// 2 bit indexed gray, white is zero
@"2IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_2IndexedGray_WhiteIsZero,
/// 4 bit indexed gray, white is zero
@"4IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_4IndexedGray_WhiteIsZero,
/// 8 bit indexed gray, white is zero
@"8IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_8IndexedGray_WhiteIsZero,
/// 16 bit BE RGB 555
@"16BE555" = c.kCVPixelFormatType_16BE555,
/// 16 bit LE RGB 555
@"16LE555" = c.kCVPixelFormatType_16LE555,
/// 16 bit LE RGB 5551
@"16LE5551" = c.kCVPixelFormatType_16LE5551,
/// 16 bit BE RGB 565
@"16BE565" = c.kCVPixelFormatType_16BE565,
/// 16 bit LE RGB 565
@"16LE565" = c.kCVPixelFormatType_16LE565,
/// 24 bit RGB
@"24RGB" = c.kCVPixelFormatType_24RGB,
/// 24 bit BGR
@"24BGR" = c.kCVPixelFormatType_24BGR,
/// 32 bit ARGB
@"32ARGB" = c.kCVPixelFormatType_32ARGB,
/// 32 bit BGRA
@"32BGRA" = c.kCVPixelFormatType_32BGRA,
/// 32 bit ABGR
@"32ABGR" = c.kCVPixelFormatType_32ABGR,
/// 32 bit RGBA
@"32RGBA" = c.kCVPixelFormatType_32RGBA,
/// 64 bit ARGB, 16-bit big-endian samples
@"64ARGB" = c.kCVPixelFormatType_64ARGB,
/// 64 bit RGBA, 16-bit little-endian full-range (0-65535) samples
@"64RGBALE" = c.kCVPixelFormatType_64RGBALE,
/// 48 bit RGB, 16-bit big-endian samples
@"48RGB" = c.kCVPixelFormatType_48RGB,
/// 32 bit AlphaGray, 16-bit big-endian samples, black is zero
@"32AlphaGray" = c.kCVPixelFormatType_32AlphaGray,
/// 16 bit Grayscale, 16-bit big-endian samples, black is zero
@"16Gray" = c.kCVPixelFormatType_16Gray,
/// 30 bit RGB, 10-bit big-endian samples, 2 unused padding bits (at least significant end).
@"30RGB" = c.kCVPixelFormatType_30RGB,
/// 30 bit RGB, 10-bit big-endian samples, 2 unused padding bits (at most significant end), video-range (64-940).
@"30RGB_r210" = c.kCVPixelFormatType_30RGB_r210,
/// Component Y'CbCr 8-bit 4:2:2, ordered Cb Y'0 Cr Y'1
@"422YpCbCr8" = c.kCVPixelFormatType_422YpCbCr8,
/// Component Y'CbCrA 8-bit 4:4:4:4, ordered Cb Y' Cr A
@"4444YpCbCrA8" = c.kCVPixelFormatType_4444YpCbCrA8,
/// Component Y'CbCrA 8-bit 4:4:4:4, rendering format. full range alpha, zero biased YUV, ordered A Y' Cb Cr
@"4444YpCbCrA8R" = c.kCVPixelFormatType_4444YpCbCrA8R,
/// Component Y'CbCrA 8-bit 4:4:4:4, ordered A Y' Cb Cr, full range alpha, video range Y'CbCr.
@"4444AYpCbCr8" = c.kCVPixelFormatType_4444AYpCbCr8,
/// Component Y'CbCrA 16-bit 4:4:4:4, ordered A Y' Cb Cr, full range alpha, video range Y'CbCr, 16-bit little-endian samples.
@"4444AYpCbCr16" = c.kCVPixelFormatType_4444AYpCbCr16,
/// Component AY'CbCr single precision floating-point 4:4:4:4
@"4444AYpCbCrFloat" = c.kCVPixelFormatType_4444AYpCbCrFloat,
/// Component Y'CbCr 8-bit 4:4:4, ordered Cr Y' Cb, video range Y'CbCr
@"444YpCbCr8" = c.kCVPixelFormatType_444YpCbCr8,
/// Component Y'CbCr 10,12,14,16-bit 4:2:2
@"422YpCbCr16" = c.kCVPixelFormatType_422YpCbCr16,
/// Component Y'CbCr 10-bit 4:2:2
@"422YpCbCr10" = c.kCVPixelFormatType_422YpCbCr10,
/// Component Y'CbCr 10-bit 4:4:4
@"444YpCbCr10" = c.kCVPixelFormatType_444YpCbCr10,
/// Planar Component Y'CbCr 8-bit 4:2:0. baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrPlanar struct
@"420YpCbCr8Planar" = c.kCVPixelFormatType_420YpCbCr8Planar,
/// Planar Component Y'CbCr 8-bit 4:2:0, full range. baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrPlanar struct
@"420YpCbCr8PlanarFullRange" = c.kCVPixelFormatType_420YpCbCr8PlanarFullRange,
/// First plane: Video-range Component Y'CbCr 8-bit 4:2:2, ordered Cb Y'0 Cr Y'1; second plane: alpha 8-bit 0-255
@"422YpCbCr_4A_8BiPlanar" = c.kCVPixelFormatType_422YpCbCr_4A_8BiPlanar,
/// Bi-Planar Component Y'CbCr 8-bit 4:2:0, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
@"420YpCbCr8BiPlanarVideoRange" = c.kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
/// Bi-Planar Component Y'CbCr 8-bit 4:2:0, full-range (luma=[0,255] chroma=[1,255]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
@"420YpCbCr8BiPlanarFullRange" = c.kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
/// Bi-Planar Component Y'CbCr 8-bit 4:2:2, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
@"422YpCbCr8BiPlanarVideoRange" = c.kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange,
/// Bi-Planar Component Y'CbCr 8-bit 4:2:2, full-range (luma=[0,255] chroma=[1,255]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
@"422YpCbCr8BiPlanarFullRange" = c.kCVPixelFormatType_422YpCbCr8BiPlanarFullRange,
/// Bi-Planar Component Y'CbCr 8-bit 4:4:4, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
@"444YpCbCr8BiPlanarVideoRange" = c.kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange,
/// Bi-Planar Component Y'CbCr 8-bit 4:4:4, full-range (luma=[0,255] chroma=[1,255]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
@"444YpCbCr8BiPlanarFullRange" = c.kCVPixelFormatType_444YpCbCr8BiPlanarFullRange,
/// Component Y'CbCr 8-bit 4:2:2, ordered Y'0 Cb Y'1 Cr
@"422YpCbCr8_yuvs" = c.kCVPixelFormatType_422YpCbCr8_yuvs,
/// Component Y'CbCr 8-bit 4:2:2, full range, ordered Y'0 Cb Y'1 Cr
@"422YpCbCr8FullRange" = c.kCVPixelFormatType_422YpCbCr8FullRange,
/// 8 bit one component, black is zero
@"OneComponent8" = c.kCVPixelFormatType_OneComponent8,
/// 8 bit two component, black is zero
@"TwoComponent8" = c.kCVPixelFormatType_TwoComponent8,
/// little-endian RGB101010, 2 MSB are ignored, wide-gamut (384-895)
@"30RGBLEPackedWideGamut" = c.kCVPixelFormatType_30RGBLEPackedWideGamut,
/// little-endian ARGB2101010 full-range ARGB
@"ARGB2101010LEPacked" = c.kCVPixelFormatType_ARGB2101010LEPacked,
/// little-endian ARGB10101010, each 10 bits in the MSBs of 16bits, wide-gamut (384-895, including alpha)
@"40ARGBLEWideGamut" = c.kCVPixelFormatType_40ARGBLEWideGamut,
/// little-endian ARGB10101010, each 10 bits in the MSBs of 16bits, wide-gamut (384-895, including alpha). Alpha premultiplied
@"40ARGBLEWideGamutPremultiplied" = c.kCVPixelFormatType_40ARGBLEWideGamutPremultiplied,
/// 10 bit little-endian one component, stored as 10 MSBs of 16 bits, black is zero
@"OneComponent10" = c.kCVPixelFormatType_OneComponent10,
/// 12 bit little-endian one component, stored as 12 MSBs of 16 bits, black is zero
@"OneComponent12" = c.kCVPixelFormatType_OneComponent12,
/// 16 bit little-endian one component, black is zero
@"OneComponent16" = c.kCVPixelFormatType_OneComponent16,
/// 16 bit little-endian two component, black is zero
@"TwoComponent16" = c.kCVPixelFormatType_TwoComponent16,
/// 16 bit one component IEEE half-precision float, 16-bit little-endian samples
@"OneComponent16Half" = c.kCVPixelFormatType_OneComponent16Half,
/// 32 bit one component IEEE float, 32-bit little-endian samples
@"OneComponent32Float" = c.kCVPixelFormatType_OneComponent32Float,
/// 16 bit two component IEEE half-precision float, 16-bit little-endian samples
@"TwoComponent16Half" = c.kCVPixelFormatType_TwoComponent16Half,
/// 32 bit two component IEEE float, 32-bit little-endian samples
@"TwoComponent32Float" = c.kCVPixelFormatType_TwoComponent32Float,
/// 64 bit RGBA IEEE half-precision float, 16-bit little-endian samples
@"64RGBAHalf" = c.kCVPixelFormatType_64RGBAHalf,
/// 128 bit RGBA IEEE float, 32-bit little-endian samples
@"128RGBAFloat" = c.kCVPixelFormatType_128RGBAFloat,
/// Bayer 14-bit Little-Endian, packed in 16-bits, ordered G R G R... alternating with B G B G...
@"14Bayer_GRBG" = c.kCVPixelFormatType_14Bayer_GRBG,
/// Bayer 14-bit Little-Endian, packed in 16-bits, ordered R G R G... alternating with G B G B...
@"14Bayer_RGGB" = c.kCVPixelFormatType_14Bayer_RGGB,
/// Bayer 14-bit Little-Endian, packed in 16-bits, ordered B G B G... alternating with G R G R...
@"14Bayer_BGGR" = c.kCVPixelFormatType_14Bayer_BGGR,
/// Bayer 14-bit Little-Endian, packed in 16-bits, ordered G B G B... alternating with R G R G...
@"14Bayer_GBRG" = c.kCVPixelFormatType_14Bayer_GBRG,
/// IEEE754-2008 binary16 (half float), describing the normalized shift when comparing two images. Units are 1/meters: ( pixelShift / (pixelFocalLength * baselineInMeters) )
@"DisparityFloat16" = c.kCVPixelFormatType_DisparityFloat16,
/// IEEE754-2008 binary32 float, describing the normalized shift when comparing two images. Units are 1/meters: ( pixelShift / (pixelFocalLength * baselineInMeters) )
@"DisparityFloat32" = c.kCVPixelFormatType_DisparityFloat32,
/// IEEE754-2008 binary16 (half float), describing the depth (distance to an object) in meters
@"DepthFloat16" = c.kCVPixelFormatType_DepthFloat16,
/// IEEE754-2008 binary32 float, describing the depth (distance to an object) in meters
@"DepthFloat32" = c.kCVPixelFormatType_DepthFloat32,
/// 2 plane YCbCr10 4:2:0, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960])
@"420YpCbCr10BiPlanarVideoRange" = c.kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange,
/// 2 plane YCbCr10 4:2:2, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960])
@"422YpCbCr10BiPlanarVideoRange" = c.kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange,
/// 2 plane YCbCr10 4:4:4, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960])
@"444YpCbCr10BiPlanarVideoRange" = c.kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange,
/// 2 plane YCbCr10 4:2:0, each 10 bits in the MSBs of 16bits, full-range (Y range 0-1023)
@"420YpCbCr10BiPlanarFullRange" = c.kCVPixelFormatType_420YpCbCr10BiPlanarFullRange,
/// 2 plane YCbCr10 4:2:2, each 10 bits in the MSBs of 16bits, full-range (Y range 0-1023)
@"422YpCbCr10BiPlanarFullRange" = c.kCVPixelFormatType_422YpCbCr10BiPlanarFullRange,
/// 2 plane YCbCr10 4:4:4, each 10 bits in the MSBs of 16bits, full-range (Y range 0-1023)
@"444YpCbCr10BiPlanarFullRange" = c.kCVPixelFormatType_444YpCbCr10BiPlanarFullRange,
/// first and second planes as per 420YpCbCr8BiPlanarVideoRange (420v), alpha 8 bits in third plane full-range. No CVPlanarPixelBufferInfo struct.
@"420YpCbCr8VideoRange_8A_TriPlanar" = c.kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar,
/// Single plane Bayer 16-bit little-endian sensor element ("sensel".*) samples from full-size decoding of ProRes RAW images; Bayer pattern (sensel ordering) and other raw conversion information is described via buffer attachments
@"16VersatileBayer" = c.kCVPixelFormatType_16VersatileBayer,
/// Single plane 64-bit RGBA (16-bit little-endian samples) from downscaled decoding of ProRes RAW images; components--which may not be co-sited with one another--are sensel values and require raw conversion, information for which is described via buffer attachments
@"64RGBA_DownscaledProResRAW" = c.kCVPixelFormatType_64RGBA_DownscaledProResRAW,
/// 2 plane YCbCr16 4:2:2, video-range (luma=[4096,60160] chroma=[4096,61440])
@"422YpCbCr16BiPlanarVideoRange" = c.kCVPixelFormatType_422YpCbCr16BiPlanarVideoRange,
/// 2 plane YCbCr16 4:4:4, video-range (luma=[4096,60160] chroma=[4096,61440])
@"444YpCbCr16BiPlanarVideoRange" = c.kCVPixelFormatType_444YpCbCr16BiPlanarVideoRange,
/// 3 plane video-range YCbCr16 4:4:4 with 16-bit full-range alpha (luma=[4096,60160] chroma=[4096,61440] alpha=[0,65535]). No CVPlanarPixelBufferInfo struct.
@"444YpCbCr16VideoRange_16A_TriPlanar" = c.kCVPixelFormatType_444YpCbCr16VideoRange_16A_TriPlanar,
_,
};

View File

@ -51,7 +51,7 @@ pub const Binding = struct {
data: anytype,
usage: Usage,
) !void {
const info = dataInfo(&data);
const info = dataInfo(data);
glad.context.BufferData.?(
@intFromEnum(b.target),
info.size,
@ -136,10 +136,6 @@ pub const Binding = struct {
};
}
pub fn enableAttribArray(_: Binding, idx: c.GLuint) !void {
glad.context.EnableVertexAttribArray.?(idx);
}
/// Shorthand for vertexAttribPointer that is specialized towards the
/// common use case of specifying an array of homogeneous types that
/// don't need normalization. This also enables the attribute at idx.
@ -230,6 +226,7 @@ pub const Target = enum(c_uint) {
array = c.GL_ARRAY_BUFFER,
element_array = c.GL_ELEMENT_ARRAY_BUFFER,
uniform = c.GL_UNIFORM_BUFFER,
storage = c.GL_SHADER_STORAGE_BUFFER,
_,
};

View File

@ -5,6 +5,7 @@ const c = @import("c.zig").c;
const errors = @import("errors.zig");
const glad = @import("glad.zig");
const Texture = @import("Texture.zig");
const Renderbuffer = @import("Renderbuffer.zig");
id: c.GLuint,
@ -86,6 +87,29 @@ pub const Binding = struct {
try errors.getError();
}
pub fn renderbuffer(
self: Binding,
attachment: Attachment,
buffer: Renderbuffer,
) !void {
glad.context.FramebufferRenderbuffer.?(
@intFromEnum(self.target),
@intFromEnum(attachment),
c.GL_RENDERBUFFER,
buffer.id,
);
try errors.getError();
}
pub fn drawBuffers(
self: Binding,
bufs: []Attachment,
) !void {
_ = self;
glad.context.DrawBuffers.?(@intCast(bufs.len), bufs.ptr);
try errors.getError();
}
pub fn checkStatus(self: Binding) Status {
return @enumFromInt(glad.context.CheckFramebufferStatus.?(@intFromEnum(self.target)));
}

View File

@ -0,0 +1,56 @@
const Renderbuffer = @This();
const std = @import("std");
const c = @import("c.zig").c;
const errors = @import("errors.zig");
const glad = @import("glad.zig");
const Texture = @import("Texture.zig");
id: c.GLuint,
/// Create a single buffer.
pub fn create() !Renderbuffer {
var rbo: c.GLuint = undefined;
glad.context.GenRenderbuffers.?(1, &rbo);
return .{ .id = rbo };
}
pub fn destroy(v: Renderbuffer) void {
glad.context.DeleteRenderbuffers.?(1, &v.id);
}
pub fn bind(v: Renderbuffer) !Binding {
// Keep track of the previous binding so we can restore it in unbind.
var current: c.GLint = undefined;
glad.context.GetIntegerv.?(c.GL_RENDERBUFFER_BINDING, &current);
glad.context.BindRenderbuffer.?(c.GL_RENDERBUFFER, v.id);
return .{ .previous = @intCast(current) };
}
pub const Binding = struct {
previous: c.GLuint,
pub fn unbind(self: Binding) void {
glad.context.BindRenderbuffer.?(
c.GL_RENDERBUFFER,
self.previous,
);
}
pub fn storage(
self: Binding,
format: Texture.InternalFormat,
width: c.GLsizei,
height: c.GLsizei,
) !void {
_ = self;
glad.context.RenderbufferStorage.?(
c.GL_RENDERBUFFER,
@intCast(@intFromEnum(format)),
width,
height,
);
try errors.getError();
}
};

View File

@ -7,8 +7,8 @@ const glad = @import("glad.zig");
id: c.GLuint,
pub fn active(target: c.GLenum) !void {
glad.context.ActiveTexture.?(target);
pub fn active(index: c_uint) !void {
glad.context.ActiveTexture.?(index + c.GL_TEXTURE0);
try errors.getError();
}
@ -30,7 +30,7 @@ pub fn destroy(v: Texture) void {
glad.context.DeleteTextures.?(1, &v.id);
}
/// Enun for possible texture binding targets.
/// Enum for possible texture binding targets.
pub const Target = enum(c_uint) {
@"1D" = c.GL_TEXTURE_1D,
@"2D" = c.GL_TEXTURE_2D,
@ -67,11 +67,11 @@ pub const Parameter = enum(c_uint) {
/// Internal format enum for texture images.
pub const InternalFormat = enum(c_int) {
red = c.GL_RED,
rgb = c.GL_RGB,
rgba = c.GL_RGBA,
rgb = c.GL_RGB8,
rgba = c.GL_RGBA8,
srgb = c.GL_SRGB,
srgba = c.GL_SRGB_ALPHA,
srgb = c.GL_SRGB8,
srgba = c.GL_SRGB8_ALPHA8,
// There are so many more that I haven't filled in.
_,
@ -116,6 +116,7 @@ pub const Binding = struct {
),
else => unreachable,
}
try errors.getError();
}
pub fn image2D(
@ -140,6 +141,7 @@ pub const Binding = struct {
@intFromEnum(typ),
data,
);
try errors.getError();
}
pub fn subImage2D(
@ -164,6 +166,7 @@ pub const Binding = struct {
@intFromEnum(typ),
data,
);
try errors.getError();
}
pub fn copySubImage2D(
@ -176,6 +179,16 @@ pub const Binding = struct {
width: c.GLsizei,
height: c.GLsizei,
) !void {
glad.context.CopyTexSubImage2D.?(@intFromEnum(b.target), level, xoffset, yoffset, x, y, width, height);
glad.context.CopyTexSubImage2D.?(
@intFromEnum(b.target),
level,
xoffset,
yoffset,
x,
y,
width,
height,
);
try errors.getError();
}
};

View File

@ -29,4 +29,88 @@ pub const Binding = struct {
_ = self;
glad.context.BindVertexArray.?(0);
}
pub fn enableAttribArray(_: Binding, idx: c.GLuint) !void {
glad.context.EnableVertexAttribArray.?(idx);
try errors.getError();
}
pub fn bindingDivisor(_: Binding, idx: c.GLuint, divisor: c.GLuint) !void {
glad.context.VertexBindingDivisor.?(idx, divisor);
try errors.getError();
}
pub fn attributeBinding(
_: Binding,
attrib_idx: c.GLuint,
binding_idx: c.GLuint,
) !void {
glad.context.VertexAttribBinding.?(attrib_idx, binding_idx);
try errors.getError();
}
pub fn attributeFormat(
_: Binding,
idx: c.GLuint,
size: c.GLint,
typ: c.GLenum,
normalized: bool,
offset: c.GLuint,
) !void {
glad.context.VertexAttribFormat.?(
idx,
size,
typ,
@intCast(@intFromBool(normalized)),
offset,
);
try errors.getError();
}
pub fn attributeIFormat(
_: Binding,
idx: c.GLuint,
size: c.GLint,
typ: c.GLenum,
offset: c.GLuint,
) !void {
glad.context.VertexAttribIFormat.?(
idx,
size,
typ,
offset,
);
try errors.getError();
}
pub fn attributeLFormat(
_: Binding,
idx: c.GLuint,
size: c.GLint,
offset: c.GLuint,
) !void {
glad.context.VertexAttribLFormat.?(
idx,
size,
c.GL_DOUBLE,
offset,
);
try errors.getError();
}
pub fn bindVertexBuffer(
_: Binding,
idx: c.GLuint,
buffer: c.GLuint,
offset: c.GLintptr,
stride: c.GLsizei,
) !void {
glad.context.BindVertexBuffer.?(
idx,
buffer,
offset,
stride,
);
try errors.getError();
}
};

View File

@ -1,6 +1,7 @@
const c = @import("c.zig").c;
const errors = @import("errors.zig");
const glad = @import("glad.zig");
const Primitive = @import("primitives.zig").Primitive;
pub fn clearColor(r: f32, g: f32, b: f32, a: f32) void {
glad.context.ClearColor.?(r, g, b, a);
@ -15,6 +16,21 @@ pub fn drawArrays(mode: c.GLenum, first: c.GLint, count: c.GLsizei) !void {
try errors.getError();
}
pub fn drawArraysInstanced(
mode: Primitive,
first: c.GLint,
count: c.GLsizei,
primcount: c.GLsizei,
) !void {
glad.context.DrawArraysInstanced.?(
@intCast(@intFromEnum(mode)),
first,
count,
primcount,
);
try errors.getError();
}
pub fn drawElements(mode: c.GLenum, count: c.GLsizei, typ: c.GLenum, offset: usize) !void {
const offsetPtr = if (offset == 0) null else @as(*const anyopaque, @ptrFromInt(offset));
glad.context.DrawElements.?(mode, count, typ, offsetPtr);
@ -25,9 +41,15 @@ pub fn drawElementsInstanced(
mode: c.GLenum,
count: c.GLsizei,
typ: c.GLenum,
primcount: usize,
primcount: c.GLsizei,
) !void {
glad.context.DrawElementsInstanced.?(mode, count, typ, null, @intCast(primcount));
glad.context.DrawElementsInstanced.?(
mode,
count,
typ,
null,
primcount,
);
try errors.getError();
}
@ -36,6 +58,11 @@ pub fn enable(cap: c.GLenum) !void {
try errors.getError();
}
pub fn disable(cap: c.GLenum) !void {
glad.context.Disable.?(cap);
try errors.getError();
}
pub fn frontFace(mode: c.GLenum) !void {
glad.context.FrontFace.?(mode);
try errors.getError();
@ -57,3 +84,11 @@ pub fn pixelStore(mode: c.GLenum, value: anytype) !void {
}
try errors.getError();
}
pub fn finish() void {
glad.context.Finish.?();
}
pub fn flush() void {
glad.context.Flush.?();
}

View File

@ -16,20 +16,29 @@ pub const glad = @import("glad.zig");
pub const ext = @import("extensions.zig");
pub const Buffer = @import("Buffer.zig");
pub const Framebuffer = @import("Framebuffer.zig");
pub const Renderbuffer = @import("Renderbuffer.zig");
pub const Program = @import("Program.zig");
pub const Shader = @import("Shader.zig");
pub const Texture = @import("Texture.zig");
pub const VertexArray = @import("VertexArray.zig");
pub const errors = @import("errors.zig");
pub const Primitive = @import("primitives.zig").Primitive;
const draw = @import("draw.zig");
pub const blendFunc = draw.blendFunc;
pub const clear = draw.clear;
pub const clearColor = draw.clearColor;
pub const drawArrays = draw.drawArrays;
pub const drawArraysInstanced = draw.drawArraysInstanced;
pub const drawElements = draw.drawElements;
pub const drawElementsInstanced = draw.drawElementsInstanced;
pub const enable = draw.enable;
pub const disable = draw.disable;
pub const frontFace = draw.frontFace;
pub const pixelStore = draw.pixelStore;
pub const viewport = draw.viewport;
pub const flush = draw.flush;
pub const finish = draw.finish;

18
pkg/opengl/primitives.zig Normal file
View File

@ -0,0 +1,18 @@
pub const c = @import("c.zig").c;
pub const Primitive = enum(c_int) {
point = c.GL_POINTS,
line = c.GL_LINES,
line_strip = c.GL_LINE_STRIP,
triangle = c.GL_TRIANGLES,
triangle_strip = c.GL_TRIANGLE_STRIP,
// Commented out primitive types are excluded for parity with Metal.
//
// line_loop = c.GL_LINE_LOOP,
// line_adjacency = c.GL_LINES_ADJACENCY,
// line_strip_adjacency = c.GL_LINE_STRIP_ADJACENCY,
// triangle_fan = c.GL_TRIANGLE_FAN,
// triangle_adjacency = c.GL_TRIANGLES_ADJACENCY,
// triangle_strip_adjacency = c.GL_TRIANGLE_STRIP_ADJACENCY,
};

View File

@ -468,6 +468,7 @@ pub fn init(
.size = size,
.surface_mailbox = .{ .surface = self, .app = app_mailbox },
.rt_surface = rt_surface,
.thread = &self.renderer_thread,
});
errdefer renderer_impl.deinit();
@ -726,7 +727,9 @@ pub fn close(self: *Surface) void {
/// is in the middle of animation (such as a resize, etc.) or when
/// the render timer is managed manually by the apprt.
pub fn draw(self: *Surface) !void {
try self.renderer_thread.draw_now.notify();
// Renderers are required to support `drawFrame` being called from
// the main thread, so that they can update contents during resize.
try self.renderer.drawFrame(true);
}
/// Activate the inspector. This will begin collecting inspection data.

View File

@ -55,6 +55,11 @@ pub const c = @cImport({
const log = std.log.scoped(.gtk);
/// This is detected by the Renderer, in which case it sends a `redraw_surface`
/// message so that we can call `drawFrame` ourselves from the app thread,
/// because GTK's `GLArea` does not support drawing from a different thread.
pub const must_draw_from_app_thread = true;
pub const Options = struct {};
core_app: *CoreApp,

View File

@ -41,10 +41,6 @@ const adw_version = @import("adw_version.zig");
const log = std.log.scoped(.gtk_surface);
/// This is detected by the OpenGL renderer to move to a single-threaded
/// draw operation. This basically puts locks around our draw path.
pub const opengl_single_threaded_draw = true;
pub const Options = struct {
/// The parent surface to inherit settings such as font size, working
/// directory, etc. from.
@ -394,7 +390,10 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
// Various other GL properties
gl_area_widget.setCursorFromName("text");
gl_area.setRequiredVersion(3, 3);
gl_area.setRequiredVersion(
renderer.OpenGL.MIN_VERSION_MAJOR,
renderer.OpenGL.MIN_VERSION_MINOR,
);
gl_area.setHasStencilBuffer(0);
gl_area.setHasDepthBuffer(0);
gl_area.setUseEs(0);
@ -683,12 +682,13 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
fn realize(self: *Surface) !void {
// If this surface has already been realized, then we don't need to
// reinitialize. This can happen if a surface is moved from one GDK surface
// to another (i.e. a tab is pulled out into a window).
// reinitialize. This can happen if a surface is moved from one GDK
// surface to another (i.e. a tab is pulled out into a window).
if (self.realized) {
// If we have no OpenGL state though, we do need to reinitialize.
// We allow the renderer to figure that out
try self.core_surface.renderer.displayRealize();
// We allow the renderer to figure that out, and then queue a draw.
try self.core_surface.renderer.displayRealized();
self.redraw();
return;
}
@ -794,7 +794,7 @@ pub fn primaryWidget(self: *Surface) *gtk.Widget {
}
fn render(self: *Surface) !void {
try self.core_surface.renderer.drawFrame(self);
try self.core_surface.renderer.drawFrame(true);
}
/// Called by core surface to get the cgroup.

View File

@ -266,6 +266,9 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
/// This affects the appearance of text and of any images with transparency.
/// Additionally, custom shaders will receive colors in the configured space.
///
/// On macOS the default is `native`, on all other platforms the default is
/// `linear-corrected`.
///
/// Valid values:
///
/// * `native` - Perform alpha blending in the native color space for the OS.
@ -276,12 +279,15 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
/// when certain color combinations are used (e.g. red / green), but makes
/// dark text look much thinner than normal and light text much thicker.
/// This is also sometimes known as "gamma correction".
/// (Currently only supported on macOS. Has no effect on Linux.)
///
/// * `linear-corrected` - Same as `linear`, but with a correction step applied
/// for text that makes it look nearly or completely identical to `native`,
/// but without any of the darkening artifacts.
@"alpha-blending": AlphaBlending = .native,
@"alpha-blending": AlphaBlending =
if (builtin.os.tag == .macos)
.native
else
.@"linear-corrected",
/// All of the configurations behavior adjust various metrics determined by the
/// font. The values can be integers (1, -1, etc.) or a percentage (20%, -15%,

View File

@ -16,6 +16,7 @@ const cursor = @import("renderer/cursor.zig");
const message = @import("renderer/message.zig");
const size = @import("renderer/size.zig");
pub const shadertoy = @import("renderer/shadertoy.zig");
pub const GenericRenderer = @import("renderer/generic.zig").Renderer;
pub const Metal = @import("renderer/Metal.zig");
pub const OpenGL = @import("renderer/OpenGL.zig");
pub const WebGL = @import("renderer/WebGL.zig");
@ -56,8 +57,8 @@ pub const Impl = enum {
/// The implementation to use for the renderer. This is comptime chosen
/// so that every build has exactly one renderer implementation.
pub const Renderer = switch (build_config.renderer) {
.metal => Metal,
.opengl => OpenGL,
.metal => GenericRenderer(Metal),
.opengl => GenericRenderer(OpenGL),
.webgl => WebGL,
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -20,3 +20,6 @@ surface_mailbox: apprt.surface.Mailbox,
/// The apprt surface.
rt_surface: *apprt.Surface,
/// The renderer thread.
thread: *renderer.Thread,

View File

@ -20,6 +20,16 @@ const log = std.log.scoped(.renderer_thread);
const DRAW_INTERVAL = 8; // 120 FPS
const CURSOR_BLINK_INTERVAL = 600;
/// Whether calls to `drawFrame` must be done from the app thread.
///
/// If this is `true` then we send a `redraw_surface` message to the apprt
/// whenever we need to draw instead of calling `drawFrame` directly.
const must_draw_from_app_thread =
if (@hasDecl(apprt.App, "must_draw_from_app_thread"))
apprt.App.must_draw_from_app_thread
else
false;
/// The type used for sending messages to the IO thread. For now this is
/// hardcoded with a capacity. We can make this a comptime parameter in
/// the future if we want it configurable.
@ -314,6 +324,16 @@ fn stopDrawTimer(self: *Thread) void {
/// Drain the mailbox.
fn drainMailbox(self: *Thread) !void {
// There's probably a more elegant way to do this...
//
// This is effectively an @autoreleasepool{} block, which we need in
// order to ensure that autoreleased objects are properly released.
const pool = if (builtin.os.tag.isDarwin())
@import("objc").AutoreleasePool.init()
else
void;
defer if (builtin.os.tag.isDarwin()) pool.deinit();
while (self.mailbox.pop()) |message| {
log.debug("mailbox message={}", .{message});
switch (message) {
@ -432,7 +452,7 @@ fn drainMailbox(self: *Thread) !void {
self.renderer.markDirty();
},
.resize => |v| try self.renderer.setScreenSize(v),
.resize => {}, //|v| try self.renderer.setScreenSize(v),
.change_config => |config| {
defer config.alloc.destroy(config.thread);
@ -468,20 +488,16 @@ fn drawFrame(self: *Thread, now: bool) void {
if (!self.flags.visible) return;
// If the renderer is managing a vsync on its own, we only draw
// when we're forced to via now.
// when we're forced to via `now`.
if (!now and self.renderer.hasVsync()) return;
// If we're doing single-threaded GPU calls then we just wake up the
// app thread to redraw at this point.
if (rendererpkg.Renderer == rendererpkg.OpenGL and
rendererpkg.OpenGL.single_threaded_draw)
{
if (must_draw_from_app_thread) {
_ = self.app_mailbox.push(
.{ .redraw_surface = self.surface },
.{ .instant = {} },
);
} else {
self.renderer.drawFrame(self.surface) catch |err|
self.renderer.drawFrame(false) catch |err|
log.warn("error drawing err={}", .{err});
}
}

2866
src/renderer/generic.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,137 @@
//! Wrapper for handling render passes.
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 Renderer = @import("../generic.zig").Renderer(Metal);
const Metal = @import("../Metal.zig");
const Target = @import("Target.zig");
const Pipeline = @import("Pipeline.zig");
const RenderPass = @import("RenderPass.zig");
const Buffer = @import("buffer.zig").Buffer;
const Health = @import("../../renderer.zig").Health;
const log = std.log.scoped(.metal);
/// Options for beginning a frame.
pub const Options = struct {
/// MTLCommandQueue
queue: objc.Object,
};
/// MTLCommandBuffer
buffer: objc.Object,
block: CompletionBlock,
/// Begin encoding a frame.
pub fn begin(
opts: Options,
/// Once the frame has been completed, the `frameCompleted` method
/// on the renderer is called with the health status of the frame.
renderer: *Renderer,
/// The target is presented via the provided renderer's API when completed.
target: *Target,
) !Self {
const buffer = opts.queue.msgSend(
objc.Object,
objc.sel("commandBuffer"),
.{},
);
// Create our block to register for completion updates.
// The block is deallocated by the objC runtime on success.
const block = try CompletionBlock.init(
.{
.renderer = renderer,
.target = target,
.sync = false,
},
&bufferCompleted,
);
errdefer block.deinit();
return .{ .buffer = buffer, .block = block };
}
/// This is the block type used for the addCompletedHandler callback.
const CompletionBlock = objc.Block(struct {
renderer: *Renderer,
target: *Target,
sync: bool,
}, .{
objc.c.id, // MTLCommandBuffer
}, void);
fn bufferCompleted(
block: *const CompletionBlock.Context,
buffer_id: objc.c.id,
) callconv(.c) void {
const buffer = objc.Object.fromId(buffer_id);
// Get our command buffer status to pass back to the generic renderer.
const status = buffer.getProperty(mtl.MTLCommandBufferStatus, "status");
const health: Health = switch (status) {
.@"error" => .unhealthy,
else => .healthy,
};
// If the frame is healthy, present it.
if (health == .healthy) {
block.renderer.api.present(
block.target.*,
block.sync,
) catch |err| {
log.err("Failed to present render target: err={}", .{err});
};
}
block.renderer.frameCompleted(health);
}
/// Add a render pass to this frame with the provided attachments.
/// Returns a RenderPass which allows render steps to be added.
pub inline fn renderPass(
self: *const Self,
attachments: []const RenderPass.Options.Attachment,
) RenderPass {
return RenderPass.begin(.{
.attachments = attachments,
.command_buffer = self.buffer,
});
}
/// Complete this frame and present the target.
///
/// If `sync` is true, this will block until the frame is presented.
pub inline fn complete(self: *Self, sync: bool) void {
// If we don't need to complete synchronously,
// we add our block as a completion handler.
//
// It will be deallocated by the objc runtime on success.
if (!sync) {
self.buffer.msgSend(
void,
objc.sel("addCompletedHandler:"),
.{self.block.context},
);
}
self.buffer.msgSend(void, objc.sel("commit"), .{});
// If we need to complete synchronously, we wait until
// the buffer is completed and call the callback directly,
// deiniting the block after we're done.
if (sync) {
self.buffer.msgSend(void, "waitUntilCompleted", .{});
self.block.context.sync = true;
bufferCompleted(self.block.context, self.buffer.value);
self.block.deinit();
}
}

View File

@ -0,0 +1,187 @@
//! A wrapper around a CALayer with a utility method
//! for settings its `contents` to an IOSurface.
const IOSurfaceLayer = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const objc = @import("objc");
const macos = @import("macos");
const IOSurface = macos.iosurface.IOSurface;
const log = std.log.scoped(.IOSurfaceLayer);
/// We subclass CALayer with a custom display handler, we only need
/// to make the subclass once, and then we can use it as a singleton.
var Subclass: ?objc.Class = null;
/// The underlying CALayer
layer: objc.Object,
pub fn init() !IOSurfaceLayer {
const layer = (try getSubclass()).msgSend(
objc.Object,
objc.sel("layer"),
.{},
);
errdefer layer.release();
// The layer gravity is set to top-left so that the contents aren't
// stretched during resize operations before a new frame has been drawn.
layer.setProperty("contentsGravity", macos.animation.kCAGravityTopLeft);
layer.setInstanceVariable("display_cb", .{ .value = null });
layer.setInstanceVariable("display_ctx", .{ .value = null });
return .{ .layer = layer };
}
pub fn release(self: *IOSurfaceLayer) void {
self.layer.release();
}
/// Sets the layer's `contents` to the provided IOSurface.
///
/// Makes sure to do so on the main thread to avoid visual artifacts.
pub inline fn setSurface(self: *IOSurfaceLayer, surface: *IOSurface) !void {
// We retain the surface to make sure it's not GC'd
// before we can set it as the contents of the layer.
//
// We release in the callback after setting the contents.
surface.retain();
// We also need to retain the layer itself to make sure it
// isn't destroyed before the callback completes, since if
// that happens it will try to interact with a deallocated
// object.
_ = self.layer.retain();
var block = try SetSurfaceBlock.init(.{
.layer = self.layer.value,
.surface = surface,
}, &setSurfaceCallback);
// We check if we're on the main thread and run the block directly if so.
const NSThread = objc.getClass("NSThread").?;
if (NSThread.msgSend(bool, "isMainThread", .{})) {
setSurfaceCallback(block.context);
block.deinit();
} else {
// NOTE: The block will automatically be deallocated by the objc
// runtime once it's executed, so there's no need to deinit it.
macos.dispatch.dispatch_async(
@ptrCast(macos.dispatch.queue.getMain()),
@ptrCast(block.context),
);
}
}
/// Sets the layer's `contents` to the provided IOSurface.
///
/// Does not ensure this happens on the main thread.
pub inline fn setSurfaceSync(self: *IOSurfaceLayer, surface: *IOSurface) void {
self.layer.setProperty("contents", surface);
}
const SetSurfaceBlock = objc.Block(struct {
layer: objc.c.id,
surface: *IOSurface,
}, .{}, void);
fn setSurfaceCallback(
block: *const SetSurfaceBlock.Context,
) callconv(.c) void {
const layer = objc.Object.fromId(block.layer);
const surface: *IOSurface = block.surface;
// See explanation of why we retain and release in `setSurface`.
defer {
surface.release();
layer.release();
}
// We check to see if the surface is the appropriate size for
// the layer, if it's not then we discard it. This is because
// asynchronously drawn frames can sometimes finish just after
// a synchronously drawn frame during a resize, and if we don't
// discard the improperly sized surface it creates jank.
const bounds = layer.getProperty(macos.graphics.Rect, "bounds");
const scale = layer.getProperty(f64, "contentsScale");
const width: usize = @intFromFloat(bounds.size.width * scale);
const height: usize = @intFromFloat(bounds.size.height * scale);
if (width != surface.getWidth() or height != surface.getHeight()) {
log.debug(
"setSurfaceCallback(): surface is wrong size for layer, discarding. surface = {d}x{d}, layer = {d}x{d}",
.{ surface.getWidth(), surface.getHeight(), width, height },
);
return;
}
layer.setProperty("contents", surface);
}
pub const DisplayCallback = ?*align(8) const fn (?*anyopaque) void;
pub fn setDisplayCallback(
self: *IOSurfaceLayer,
display_cb: DisplayCallback,
display_ctx: ?*anyopaque,
) void {
self.layer.setInstanceVariable(
"display_cb",
objc.Object.fromId(@constCast(display_cb)),
);
self.layer.setInstanceVariable(
"display_ctx",
objc.Object.fromId(display_ctx),
);
}
fn getSubclass() error{ObjCFailed}!objc.Class {
if (Subclass) |c| return c;
const CALayer =
objc.getClass("CALayer") orelse return error.ObjCFailed;
var subclass =
objc.allocateClassPair(CALayer, "IOSurfaceLayer") orelse return error.ObjCFailed;
errdefer objc.disposeClassPair(subclass);
if (!subclass.addIvar("display_cb")) return error.ObjCFailed;
if (!subclass.addIvar("display_ctx")) return error.ObjCFailed;
subclass.replaceMethod("display", struct {
fn display(target: objc.c.id, sel: objc.c.SEL) callconv(.c) void {
_ = sel;
const self = objc.Object.fromId(target);
const display_cb: DisplayCallback = @ptrFromInt(@intFromPtr(
self.getInstanceVariable("display_cb").value,
));
if (display_cb) |cb| cb(
@ptrCast(self.getInstanceVariable("display_ctx").value),
);
}
}.display);
// Disable all animations for this layer by returning null for all actions.
subclass.replaceMethod("actionForKey:", struct {
fn actionForKey(
target: objc.c.id,
sel: objc.c.SEL,
key: objc.c.id,
) callconv(.c) objc.c.id {
_ = target;
_ = sel;
_ = key;
return objc.getClass("NSNull").?.msgSend(objc.c.id, "null", .{});
}
}.actionForKey);
objc.registerClassPair(subclass);
Subclass = subclass;
return subclass;
}

View File

@ -0,0 +1,203 @@
//! Wrapper for handling render pipelines.
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const macos = @import("macos");
const objc = @import("objc");
const mtl = @import("api.zig");
const Texture = @import("Texture.zig");
const Metal = @import("../Metal.zig");
const log = std.log.scoped(.metal);
/// Options for initializing a render pipeline.
pub const Options = struct {
/// MTLDevice
device: objc.Object,
/// Name of the vertex function
vertex_fn: []const u8,
/// Name of the fragment function
fragment_fn: []const u8,
/// MTLLibrary to get the vertex function from
vertex_library: objc.Object,
/// MTLLibrary to get the fragment function from
fragment_library: objc.Object,
/// Vertex step function
step_fn: mtl.MTLVertexStepFunction = .per_vertex,
/// Info about the color attachments used by this render pipeline.
attachments: []const Attachment,
/// Describes a color attachment.
pub const Attachment = struct {
pixel_format: mtl.MTLPixelFormat,
blending_enabled: bool = true,
};
};
/// MTLRenderPipelineState
state: objc.Object,
pub fn init(comptime VertexAttributes: ?type, opts: Options) !Self {
// Create our descriptor
const desc = init: {
const Class = objc.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;
};
defer desc.msgSend(void, objc.sel("release"), .{});
// Get our vertex and fragment functions and add them to the descriptor.
{
const str = try macos.foundation.String.createWithBytes(
opts.vertex_fn,
.utf8,
false,
);
defer str.release();
const ptr = opts.vertex_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
const func_vert = objc.Object.fromId(ptr.?);
defer func_vert.msgSend(void, objc.sel("release"), .{});
desc.setProperty("vertexFunction", func_vert);
}
{
const str = try macos.foundation.String.createWithBytes(
opts.fragment_fn,
.utf8,
false,
);
defer str.release();
const ptr = opts.fragment_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
const func_frag = objc.Object.fromId(ptr.?);
defer func_frag.msgSend(void, objc.sel("release"), .{});
desc.setProperty("fragmentFunction", func_frag);
}
// If we have vertex attributes, create and add a vertex descriptor.
if (VertexAttributes) |V| {
const vertex_desc = init: {
const Class = objc.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;
};
defer vertex_desc.msgSend(void, objc.sel("release"), .{});
// Our attributes are the fields of the input
const attrs = objc.Object.fromId(vertex_desc.getProperty(?*anyopaque, "attributes"));
autoAttribute(V, attrs);
// The layout describes how and when we fetch the next vertex input.
const layouts = objc.Object.fromId(vertex_desc.getProperty(?*anyopaque, "layouts"));
{
const layout = layouts.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, 0)},
);
layout.setProperty("stepFunction", @intFromEnum(opts.step_fn));
layout.setProperty("stride", @as(c_ulong, @sizeOf(V)));
}
desc.setProperty("vertexDescriptor", vertex_desc);
}
// Set our color attachment
const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments"));
for (opts.attachments, 0..) |at, i| {
const attachment = attachments.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, i)},
);
attachment.setProperty("pixelFormat", @intFromEnum(at.pixel_format));
attachment.setProperty("blendingEnabled", at.blending_enabled);
// We always use premultiplied alpha blending for now.
if (at.blending_enabled) {
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 = opts.device.msgSend(
objc.Object,
objc.sel("newRenderPipelineStateWithDescriptor:error:"),
.{ desc, &err },
);
try checkError(err);
errdefer pipeline_state.release();
return .{ .state = pipeline_state };
}
pub fn deinit(self: *const Self) void {
self.state.release();
}
fn autoAttribute(T: type, attrs: objc.Object) void {
inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| {
const offset = @offsetOf(T, field.name);
const FT = switch (@typeInfo(field.type)) {
.@"enum" => |e| e.tag_type,
else => field.type,
};
// Very incomplete list, expand as necessary.
const format = switch (FT) {
[4]u8 => mtl.MTLVertexFormat.uchar4,
[2]u16 => mtl.MTLVertexFormat.ushort2,
[2]i16 => mtl.MTLVertexFormat.short2,
[2]f32 => mtl.MTLVertexFormat.float2,
[4]f32 => mtl.MTLVertexFormat.float4,
[2]i32 => mtl.MTLVertexFormat.int2,
u32 => mtl.MTLVertexFormat.uint,
[2]u32 => mtl.MTLVertexFormat.uint2,
[4]u32 => mtl.MTLVertexFormat.uint4,
u8 => mtl.MTLVertexFormat.uchar,
else => comptime unreachable,
};
const attr = attrs.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, i)},
);
attr.setProperty("format", @intFromEnum(format));
attr.setProperty("offset", @as(c_ulong, offset));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
}
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;
}

View File

@ -0,0 +1,220 @@
//! Wrapper for handling render passes.
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 Pipeline = @import("Pipeline.zig");
const Texture = @import("Texture.zig");
const Target = @import("Target.zig");
const Metal = @import("../Metal.zig");
const Buffer = @import("buffer.zig").Buffer;
const log = std.log.scoped(.metal);
/// Options for beginning a render pass.
pub const Options = struct {
/// MTLCommandBuffer
command_buffer: objc.Object,
/// Color attachments for this render pass.
attachments: []const Attachment,
/// Describes a color attachment.
pub const Attachment = struct {
target: union(enum) {
texture: Texture,
target: Target,
},
clear_color: ?[4]f64 = null,
};
};
/// Describes a step in a render pass.
pub const Step = struct {
pipeline: Pipeline,
/// MTLBuffer
uniforms: ?objc.Object = null,
/// MTLBuffer
buffers: []const ?objc.Object = &.{},
textures: []const ?Texture = &.{},
draw: Draw,
/// Describes the draw call for this step.
pub const Draw = struct {
type: mtl.MTLPrimitiveType,
vertex_count: usize,
instance_count: usize = 1,
};
};
/// MTLRenderCommandEncoder
encoder: objc.Object,
/// Begin a render pass.
pub fn begin(
opts: Options,
) Self {
// Create a pass descriptor
const desc = desc: {
const MTLRenderPassDescriptor = objc.getClass("MTLRenderPassDescriptor").?;
const desc = MTLRenderPassDescriptor.msgSend(
objc.Object,
objc.sel("renderPassDescriptor"),
.{},
);
// Set our color attachment to be our drawable surface.
const attachments = objc.Object.fromId(
desc.getProperty(?*anyopaque, "colorAttachments"),
);
for (opts.attachments, 0..) |at, i| {
const attachment = attachments.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, i)},
);
attachment.setProperty(
"loadAction",
@intFromEnum(@as(
mtl.MTLLoadAction,
if (at.clear_color != null)
.clear
else
.load,
)),
);
attachment.setProperty(
"storeAction",
@intFromEnum(mtl.MTLStoreAction.store),
);
attachment.setProperty("texture", switch (at.target) {
.texture => |t| t.texture.value,
.target => |t| t.texture.value,
});
if (at.clear_color) |c| attachment.setProperty(
"clearColor",
mtl.MTLClearColor{
.red = c[0],
.green = c[1],
.blue = c[2],
.alpha = c[3],
},
);
}
break :desc desc;
};
// MTLRenderCommandEncoder
const encoder = opts.command_buffer.msgSend(
objc.Object,
objc.sel("renderCommandEncoderWithDescriptor:"),
.{desc.value},
);
return .{ .encoder = encoder };
}
/// Add a step to this render pass.
pub fn step(self: *const Self, s: Step) void {
if (s.draw.instance_count == 0) return;
// Set pipeline state
self.encoder.msgSend(
void,
objc.sel("setRenderPipelineState:"),
.{s.pipeline.state.value},
);
if (s.buffers.len > 0) {
// We reserve index 0 for the vertex buffer, this isn't very
// flexible but it lines up with the API we have for OpenGL.
if (s.buffers[0]) |buf| {
self.encoder.msgSend(
void,
objc.sel("setVertexBuffer:offset:atIndex:"),
.{ buf.value, @as(c_ulong, 0), @as(c_ulong, 0) },
);
self.encoder.msgSend(
void,
objc.sel("setFragmentBuffer:offset:atIndex:"),
.{ buf.value, @as(c_ulong, 0), @as(c_ulong, 0) },
);
}
// Set the rest of the buffers starting at index 2, this is
// so that we can use index 1 for the uniforms if present.
//
// Also, we set buffers (and textures) for both stages.
//
// Again, not very flexible, but it's consistent and predictable,
// and we need to treat the uniforms as special because of OpenGL.
//
// TODO: Maybe in the future add info to the pipeline struct which
// allows it to define a mapping between provided buffers and
// what index they get set at for the vertex / fragment stage.
for (s.buffers[1..], 2..) |b, i| if (b) |buf| {
self.encoder.msgSend(
void,
objc.sel("setVertexBuffer:offset:atIndex:"),
.{ buf.value, @as(c_ulong, 0), @as(c_ulong, i) },
);
self.encoder.msgSend(
void,
objc.sel("setFragmentBuffer:offset:atIndex:"),
.{ buf.value, @as(c_ulong, 0), @as(c_ulong, i) },
);
};
}
// Set the uniforms as buffer index 1 if present.
if (s.uniforms) |buf| {
self.encoder.msgSend(
void,
objc.sel("setVertexBuffer:offset:atIndex:"),
.{ buf.value, @as(c_ulong, 0), @as(c_ulong, 1) },
);
self.encoder.msgSend(
void,
objc.sel("setFragmentBuffer:offset:atIndex:"),
.{ buf.value, @as(c_ulong, 0), @as(c_ulong, 1) },
);
}
// Set textures.
for (s.textures, 0..) |t, i| if (t) |tex| {
self.encoder.msgSend(
void,
objc.sel("setVertexTexture:atIndex:"),
.{ tex.texture.value, @as(c_ulong, i) },
);
self.encoder.msgSend(
void,
objc.sel("setFragmentTexture:atIndex:"),
.{ tex.texture.value, @as(c_ulong, i) },
);
};
// Draw!
self.encoder.msgSend(
void,
objc.sel("drawPrimitives:vertexStart:vertexCount:instanceCount:"),
.{
@intFromEnum(s.draw.type),
@as(c_ulong, 0),
@as(c_ulong, s.draw.vertex_count),
@as(c_ulong, s.draw.instance_count),
},
);
}
/// Complete this render pass.
/// This struct can no longer be used after calling this.
pub fn complete(self: *const Self) void {
self.encoder.msgSend(void, objc.sel("endEncoding"), .{});
}

View File

@ -0,0 +1,110 @@
//! Represents a render target.
//!
//! In this case, an IOSurface-backed MTLTexture.
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 macos = @import("macos");
const graphics = macos.graphics;
const IOSurface = macos.iosurface.IOSurface;
const mtl = @import("api.zig");
const log = std.log.scoped(.metal);
/// Options for initializing a Target
pub const Options = struct {
/// MTLDevice
device: objc.Object,
/// Desired width
width: usize,
/// Desired height
height: usize,
/// Pixel format for the MTLTexture
pixel_format: mtl.MTLPixelFormat,
/// Storage mode for the MTLTexture
storage_mode: mtl.MTLResourceOptions.StorageMode,
};
/// The underlying IOSurface.
surface: *IOSurface,
/// The underlying MTLTexture.
texture: objc.Object,
/// Current width of this target.
width: usize,
/// Current height of this target.
height: usize,
pub fn init(opts: Options) !Self {
// We set our surface's color space to Display P3.
// This allows us to have "Apple-style" alpha blending,
// since it seems to be the case that Apple apps like
// Terminal and TextEdit render text in the display's
// color space using converted colors, which reduces,
// but does not fully eliminate blending artifacts.
const colorspace = try graphics.ColorSpace.createNamed(.displayP3);
defer colorspace.release();
const surface = try IOSurface.init(.{
.width = @intCast(opts.width),
.height = @intCast(opts.height),
.pixel_format = .@"32BGRA",
.bytes_per_element = 4,
.colorspace = colorspace,
});
// 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("width", @as(c_ulong, @intCast(opts.width)));
desc.setProperty("height", @as(c_ulong, @intCast(opts.height)));
desc.setProperty("pixelFormat", @intFromEnum(opts.pixel_format));
desc.setProperty("usage", mtl.MTLTextureUsage{ .render_target = true });
desc.setProperty(
"resourceOptions",
mtl.MTLResourceOptions{
// Indicate that the CPU writes to this resource but never reads it.
.cpu_cache_mode = .write_combined,
.storage_mode = opts.storage_mode,
},
);
const id = opts.device.msgSend(
?*anyopaque,
objc.sel("newTextureWithDescriptor:iosurface:plane:"),
.{
desc,
surface,
@as(c_ulong, 0),
},
) orelse return error.MetalFailed;
const texture = objc.Object.fromId(id);
return .{
.surface = surface,
.texture = texture,
.width = opts.width,
.height = opts.height,
};
}
pub fn deinit(self: *Self) void {
self.surface.deinit();
self.texture.release();
}

View File

@ -0,0 +1,196 @@
//! 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,
/// Initialize a texture
pub fn init(
opts: Options,
width: usize,
height: usize,
data: ?[]const u8,
) !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,
) !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,
};
}

View File

@ -366,7 +366,7 @@ pub const MTLTextureUsage = packed struct(c_ulong) {
/// https://developer.apple.com/documentation/metal/mtltextureusage/shaderatomic?language=objc
shader_atomic: bool = false, // TextureUsageShaderAtomic = 32,
__reserved: @Type(.{ .Int = .{
__reserved: @Type(.{ .int = .{
.signedness = .unsigned,
.bits = @bitSizeOf(c_ulong) - 6,
} }) = 0,
@ -375,6 +375,22 @@ pub const MTLTextureUsage = packed struct(c_ulong) {
const unknown: MTLTextureUsage = @bitCast(0); // TextureUsageUnknown = 0,
};
/// https://developer.apple.com/documentation/metal/mtlbarrierscope?language=objc
pub const MTLBarrierScope = enum(c_ulong) {
buffers = 1,
textures = 2,
render_targets = 4,
};
/// https://developer.apple.com/documentation/metal/mtlrenderstages?language=objc
pub const MTLRenderStage = enum(c_ulong) {
vertex = 1,
fragment = 2,
tile = 4,
object = 8,
mesh = 16,
};
pub const MTLClearColor = extern struct {
red: f64,
green: f64,

View File

@ -5,9 +5,17 @@ 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.
@ -15,74 +23,57 @@ pub fn Buffer(comptime T: type) type {
return struct {
const Self = @This();
/// The resource options for this buffer.
options: mtl.MTLResourceOptions,
/// The options this buffer was initialized with.
opts: Options,
buffer: objc.Object, // MTLBuffer
/// 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(
device: objc.Object,
len: usize,
options: mtl.MTLResourceOptions,
) !Self {
const buffer = device.msgSend(
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))),
options,
opts.resource_options,
},
);
return .{ .buffer = buffer, .options = options };
return .{ .buffer = buffer, .opts = opts, .len = len };
}
/// Init the buffer filled with the given data.
pub fn initFill(
device: objc.Object,
data: []const T,
options: mtl.MTLResourceOptions,
) !Self {
const buffer = device.msgSend(
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))),
options,
opts.resource_options,
},
);
return .{ .buffer = buffer, .options = options };
return .{ .buffer = buffer, .opts = opts, .len = data.len };
}
pub fn deinit(self: *Self) void {
pub fn deinit(self: *const 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 {
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");
@ -92,12 +83,12 @@ pub fn Buffer(comptime T: type) type {
// Allocate a new buffer with enough to hold double what we require.
const size = req_bytes * 2;
self.buffer = device.msgSend(
self.buffer = self.opts.device.msgSend(
objc.Object,
objc.sel("newBufferWithLength:options:"),
.{
@as(c_ulong, @intCast(size * @sizeOf(T))),
self.options,
self.opts.resource_options,
},
);
}
@ -123,7 +114,7 @@ pub fn Buffer(comptime T: type) type {
// 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.options.storage_mode == .managed) {
if (self.opts.resource_options.storage_mode == .managed) {
self.buffer.msgSend(
void,
"didModifyRange:",
@ -134,7 +125,7 @@ pub fn Buffer(comptime T: type) type {
/// 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 {
pub fn syncFromArrayLists(self: *Self, lists: []const std.ArrayListUnmanaged(T)) !usize {
var total_len: usize = 0;
for (lists) |list| {
total_len += list.items.len;
@ -149,12 +140,12 @@ pub fn Buffer(comptime T: type) type {
// Allocate a new buffer with enough to hold double what we require.
const size = req_bytes * 2;
self.buffer = device.msgSend(
self.buffer = self.opts.device.msgSend(
objc.Object,
objc.sel("newBufferWithLength:options:"),
.{
@as(c_ulong, @intCast(size * @sizeOf(T))),
self.options,
self.opts.resource_options,
},
);
}
@ -181,7 +172,7 @@ pub fn Buffer(comptime T: type) type {
// 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.options.storage_mode == .managed) {
if (self.opts.resource_options.storage_mode == .managed) {
self.buffer.msgSend(
void,
"didModifyRange:",

View File

@ -4,6 +4,9 @@ const assert = std.debug.assert;
const objc = @import("objc");
const wuffs = @import("wuffs");
const Metal = @import("../Metal.zig");
const Texture = Metal.Texture;
const mtl = @import("api.zig");
/// Represents a single image placement on the grid. A placement is a
@ -61,15 +64,15 @@ pub const Image = union(enum) {
replace_rgba: Replace,
/// The image is uploaded and ready to be used.
ready: objc.Object, // MTLTexture
ready: Texture,
/// The image is uploaded but is scheduled to be unloaded.
unload_pending: []u8,
unload_ready: objc.Object, // MTLTexture
unload_replace: struct { []u8, objc.Object },
unload_ready: Texture,
unload_replace: struct { []u8, Texture },
pub const Replace = struct {
texture: objc.Object,
texture: Texture,
pending: Pending,
};
@ -101,32 +104,32 @@ pub const Image = union(enum) {
.replace_gray => |r| {
alloc.free(r.pending.dataSlice(1));
r.texture.msgSend(void, objc.sel("release"), .{});
r.texture.deinit();
},
.replace_gray_alpha => |r| {
alloc.free(r.pending.dataSlice(2));
r.texture.msgSend(void, objc.sel("release"), .{});
r.texture.deinit();
},
.replace_rgb => |r| {
alloc.free(r.pending.dataSlice(3));
r.texture.msgSend(void, objc.sel("release"), .{});
r.texture.deinit();
},
.replace_rgba => |r| {
alloc.free(r.pending.dataSlice(4));
r.texture.msgSend(void, objc.sel("release"), .{});
r.texture.deinit();
},
.unload_replace => |r| {
alloc.free(r[0]);
r[1].msgSend(void, objc.sel("release"), .{});
r[1].deinit();
},
.ready,
.unload_ready,
=> |obj| obj.msgSend(void, objc.sel("release"), .{}),
=> |t| t.deinit(),
}
}
@ -170,7 +173,7 @@ pub const Image = union(enum) {
// Get our existing texture. This switch statement will also handle
// scenarios where there is no existing texture and we can modify
// the self pointer directly.
const existing: objc.Object = switch (self.*) {
const existing: Texture = switch (self.*) {
// For pending, we can free the old data and become pending
// ourselves.
.pending_gray => |p| {
@ -357,10 +360,11 @@ pub const Image = union(enum) {
pub fn upload(
self: *Image,
alloc: Allocator,
device: objc.Object,
/// Storage mode for the MTLTexture object
storage_mode: mtl.MTLResourceOptions.StorageMode,
metal: *const Metal,
) !void {
const device = metal.device;
const storage_mode = metal.default_storage_mode;
// Convert our data if we have to
try self.convert(alloc);
@ -368,27 +372,19 @@ pub const Image = union(enum) {
const p = self.pending().?;
// Create our texture
const texture = try initTexture(p, device, storage_mode);
errdefer texture.msgSend(void, objc.sel("release"), .{});
// Upload our data
const d = self.depth();
texture.msgSend(
void,
objc.sel("replaceRegion:mipmapLevel:withBytes:bytesPerRow:"),
const texture = try Texture.init(
.{
mtl.MTLRegion{
.origin = .{ .x = 0, .y = 0, .z = 0 },
.size = .{
.width = @intCast(p.width),
.height = @intCast(p.height),
.depth = 1,
.device = device,
.pixel_format = .rgba8unorm_srgb,
.resource_options = .{
// Indicate that the CPU writes to this resource but never reads it.
.cpu_cache_mode = .write_combined,
.storage_mode = storage_mode,
},
},
@as(c_ulong, 0),
@as(*const anyopaque, p.data),
@as(c_ulong, d * p.width),
},
@intCast(p.width),
@intCast(p.height),
p.data[0 .. p.width * p.height * self.depth()],
);
// Uploaded. We can now clear our data and change our state.
@ -425,42 +421,4 @@ pub const Image = union(enum) {
else => null,
};
}
fn initTexture(
p: Pending,
device: objc.Object,
/// Storage mode for the MTLTexture object
storage_mode: mtl.MTLResourceOptions.StorageMode,
) !objc.Object {
// 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;
};
// Set our properties
desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.rgba8unorm_srgb));
desc.setProperty("width", @as(c_ulong, @intCast(p.width)));
desc.setProperty("height", @as(c_ulong, @intCast(p.height)));
desc.setProperty(
"resourceOptions",
mtl.MTLResourceOptions{
// Indicate that the CPU writes to this resource but never reads it.
.cpu_cache_mode = .write_combined,
.storage_mode = storage_mode,
},
);
// Initialize
const id = device.msgSend(
?*anyopaque,
objc.sel("newTextureWithDescriptor:"),
.{desc},
) orelse return error.MetalFailed;
return objc.Object.fromId(id);
}
};

View File

@ -1,38 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const objc = @import("objc");
const mtl = @import("api.zig");
pub const Sampler = struct {
sampler: objc.Object,
pub fn init(device: objc.Object) !Sampler {
const desc = init: {
const Class = objc.getClass("MTLSamplerDescriptor").?;
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;
};
defer desc.msgSend(void, objc.sel("release"), .{});
desc.setProperty("rAddressMode", @intFromEnum(mtl.MTLSamplerAddressMode.clamp_to_edge));
desc.setProperty("sAddressMode", @intFromEnum(mtl.MTLSamplerAddressMode.clamp_to_edge));
desc.setProperty("tAddressMode", @intFromEnum(mtl.MTLSamplerAddressMode.clamp_to_edge));
desc.setProperty("minFilter", @intFromEnum(mtl.MTLSamplerMinMagFilter.linear));
desc.setProperty("magFilter", @intFromEnum(mtl.MTLSamplerMinMagFilter.linear));
const sampler = device.msgSend(
objc.Object,
objc.sel("newSamplerStateWithDescriptor:"),
.{desc},
);
errdefer sampler.msgSend(void, objc.sel("release"), .{});
return .{ .sampler = sampler };
}
pub fn deinit(self: *Sampler) void {
self.sampler.msgSend(void, objc.sel("release"), .{});
}
};

View File

@ -6,6 +6,7 @@ const objc = @import("objc");
const math = @import("../../math.zig");
const mtl = @import("api.zig");
const Pipeline = @import("Pipeline.zig");
const log = std.log.scoped(.metal);
@ -14,20 +15,24 @@ pub const Shaders = struct {
library: objc.Object,
/// Renders cell foreground elements (text, decorations).
cell_text_pipeline: objc.Object,
cell_text_pipeline: Pipeline,
/// The cell background shader is the shader used to render the
/// background of terminal cells.
cell_bg_pipeline: objc.Object,
cell_bg_pipeline: Pipeline,
/// The image shader is the shader used to render images for things
/// like the Kitty image protocol.
image_pipeline: objc.Object,
image_pipeline: Pipeline,
/// Custom shaders to run against the final drawable texture. This
/// can be used to apply a lot of effects. Each shader is run in sequence
/// against the output of the previous shader.
post_pipelines: []const objc.Object,
post_pipelines: []const Pipeline,
/// Set to true when deinited, if you try to deinit a defunct set
/// of shaders it will just be ignored, to prevent double-free.
defunct: bool = false,
/// Initialize our shader set.
///
@ -44,15 +49,15 @@ pub const Shaders = struct {
errdefer library.msgSend(void, objc.sel("release"), .{});
const cell_text_pipeline = try initCellTextPipeline(device, library, pixel_format);
errdefer cell_text_pipeline.msgSend(void, objc.sel("release"), .{});
errdefer cell_text_pipeline.deinit();
const cell_bg_pipeline = try initCellBgPipeline(device, library, pixel_format);
errdefer cell_bg_pipeline.msgSend(void, objc.sel("release"), .{});
errdefer cell_bg_pipeline.deinit();
const image_pipeline = try initImagePipeline(device, library, pixel_format);
errdefer image_pipeline.msgSend(void, objc.sel("release"), .{});
errdefer image_pipeline.deinit();
const post_pipelines: []const objc.Object = initPostPipelines(
const post_pipelines: []const Pipeline = initPostPipelines(
alloc,
device,
library,
@ -66,7 +71,7 @@ pub const Shaders = struct {
break :err &.{};
};
errdefer if (post_pipelines.len > 0) {
for (post_pipelines) |pipeline| pipeline.msgSend(void, objc.sel("release"), .{});
for (post_pipelines) |pipeline| pipeline.deinit();
alloc.free(post_pipelines);
};
@ -80,16 +85,19 @@ pub const Shaders = struct {
}
pub fn deinit(self: *Shaders, alloc: Allocator) void {
if (self.defunct) return;
self.defunct = true;
// Release our primary shaders
self.cell_text_pipeline.msgSend(void, objc.sel("release"), .{});
self.cell_bg_pipeline.msgSend(void, objc.sel("release"), .{});
self.image_pipeline.msgSend(void, objc.sel("release"), .{});
self.cell_text_pipeline.deinit();
self.cell_bg_pipeline.deinit();
self.image_pipeline.deinit();
self.library.msgSend(void, objc.sel("release"), .{});
// Release our postprocess shaders
if (self.post_pipelines.len > 0) {
for (self.post_pipelines) |pipeline| {
pipeline.msgSend(void, objc.sel("release"), .{});
pipeline.deinit();
}
alloc.free(self.post_pipelines);
}
@ -140,6 +148,10 @@ pub const Uniforms = extern struct {
/// The background color for the whole surface.
bg_color: [4]u8 align(4),
/// Various booleans.
///
/// TODO: Maybe put these in a packed struct, like for OpenGL.
bools: extern struct {
/// Whether the cursor is 2 cells wide.
cursor_wide: bool align(1),
@ -159,6 +171,7 @@ pub const Uniforms = extern struct {
/// with linear alpha blending have a similar apparent weight
/// (thickness) to gamma-incorrect blending.
use_linear_correction: bool align(1) = false,
},
const PaddingExtend = packed struct(u8) {
left: bool = false,
@ -214,15 +227,16 @@ fn initLibrary(device: objc.Object) !objc.Object {
return library;
}
/// Initialize our custom shader pipelines. The shaders argument is a
/// set of shader source code, not file paths.
/// Initialize our custom shader pipelines.
///
/// The shaders argument is a set of shader source code, not file paths.
fn initPostPipelines(
alloc: Allocator,
device: objc.Object,
library: objc.Object,
shaders: []const [:0]const u8,
pixel_format: mtl.MTLPixelFormat,
) ![]const objc.Object {
) ![]const Pipeline {
// If we have no shaders, do nothing.
if (shaders.len == 0) return &.{};
@ -230,10 +244,10 @@ fn initPostPipelines(
var i: usize = 0;
// Initialize our result set. If any error happens, we undo everything.
var pipelines = try alloc.alloc(objc.Object, shaders.len);
var pipelines = try alloc.alloc(Pipeline, shaders.len);
errdefer {
for (pipelines[0..i]) |pipeline| {
pipeline.msgSend(void, objc.sel("release"), .{});
pipeline.deinit();
}
alloc.free(pipelines);
}
@ -259,7 +273,7 @@ fn initPostPipeline(
library: objc.Object,
data: [:0]const u8,
pixel_format: mtl.MTLPixelFormat,
) !objc.Object {
) !Pipeline {
// Create our library which has the shader source
const post_library = library: {
const source = try macos.foundation.String.createWithBytes(
@ -282,16 +296,19 @@ fn initPostPipeline(
};
defer post_library.msgSend(void, objc.sel("release"), .{});
return (Pipeline{
return try Pipeline.init(null, .{
.device = device,
.vertex_fn = "full_screen_vertex",
.fragment_fn = "main0",
.vertex_library = library,
.fragment_library = post_library,
.attachments = &.{
.{
.pixel_format = pixel_format,
.blending_enabled = false,
}).init(
device,
library,
post_library,
pixel_format,
);
},
},
});
}
/// This is a single parameter for the terminal cell shader.
@ -324,19 +341,21 @@ fn initCellTextPipeline(
device: objc.Object,
library: objc.Object,
pixel_format: mtl.MTLPixelFormat,
) !objc.Object {
return (Pipeline{
) !Pipeline {
return try Pipeline.init(CellText, .{
.device = device,
.vertex_fn = "cell_text_vertex",
.fragment_fn = "cell_text_fragment",
.Vertex = CellText,
.vertex_library = library,
.fragment_library = library,
.step_fn = .per_instance,
.attachments = &.{
.{
.pixel_format = pixel_format,
.blending_enabled = true,
}).init(
device,
library,
library,
pixel_format,
);
},
},
});
}
/// This is a single parameter for the cell bg shader.
@ -347,17 +366,20 @@ fn initCellBgPipeline(
device: objc.Object,
library: objc.Object,
pixel_format: mtl.MTLPixelFormat,
) !objc.Object {
return (Pipeline{
) !Pipeline {
return try Pipeline.init(null, .{
.device = device,
.vertex_fn = "cell_bg_vertex",
.fragment_fn = "cell_bg_fragment",
.vertex_library = library,
.fragment_library = library,
.attachments = &.{
.{
.pixel_format = pixel_format,
.blending_enabled = false,
}).init(
device,
library,
library,
pixel_format,
);
},
},
});
}
/// Initialize the image render pipeline for our shader library.
@ -365,182 +387,21 @@ fn initImagePipeline(
device: objc.Object,
library: objc.Object,
pixel_format: mtl.MTLPixelFormat,
) !objc.Object {
return (Pipeline{
) !Pipeline {
return try Pipeline.init(Image, .{
.device = device,
.vertex_fn = "image_vertex",
.fragment_fn = "image_fragment",
.Vertex = Image,
.vertex_library = library,
.fragment_library = library,
.step_fn = .per_instance,
.attachments = &.{
.{
.pixel_format = pixel_format,
.blending_enabled = true,
}).init(
device,
library,
library,
pixel_format,
);
}
/// A struct with all the necessary info to initialize a pipeline.
const Pipeline = struct {
/// Name of the vertex function
vertex_fn: []const u8,
/// Name of the fragment function
fragment_fn: []const u8,
/// Vertex attribute struct
Vertex: ?type = null,
/// Vertex step function
step_fn: mtl.MTLVertexStepFunction = .per_vertex,
/// Whether blending is enabled for the color attachment
blending_enabled: bool = true,
fn init(
self: *const Pipeline,
device: objc.Object,
vertex_library: objc.Object,
fragment_library: objc.Object,
pixel_format: mtl.MTLPixelFormat,
) !objc.Object {
// Create our descriptor
const desc = init: {
const Class = objc.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;
};
defer desc.msgSend(void, objc.sel("release"), .{});
// Get our vertex and fragment functions and add them to the descriptor.
{
const str = try macos.foundation.String.createWithBytes(
self.vertex_fn,
.utf8,
false,
);
defer str.release();
const ptr = vertex_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
const func_vert = objc.Object.fromId(ptr.?);
defer func_vert.msgSend(void, objc.sel("release"), .{});
desc.setProperty("vertexFunction", func_vert);
}
{
const str = try macos.foundation.String.createWithBytes(
self.fragment_fn,
.utf8,
false,
);
defer str.release();
const ptr = fragment_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
const func_frag = objc.Object.fromId(ptr.?);
defer func_frag.msgSend(void, objc.sel("release"), .{});
desc.setProperty("fragmentFunction", func_frag);
}
// If we have vertex attributes, create and add a vertex descriptor.
if (self.Vertex) |V| {
const vertex_desc = init: {
const Class = objc.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;
};
defer vertex_desc.msgSend(void, objc.sel("release"), .{});
// Our attributes are the fields of the input
const attrs = objc.Object.fromId(vertex_desc.getProperty(?*anyopaque, "attributes"));
autoAttribute(V, attrs);
// The layout describes how and when we fetch the next vertex input.
const layouts = objc.Object.fromId(vertex_desc.getProperty(?*anyopaque, "layouts"));
{
const layout = layouts.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, 0)},
);
layout.setProperty("stepFunction", @intFromEnum(self.step_fn));
layout.setProperty("stride", @as(c_ulong, @sizeOf(V)));
}
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)},
);
attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
attachment.setProperty("blendingEnabled", self.blending_enabled);
// We always use premultiplied alpha blending for now.
if (self.blending_enabled) {
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);
errdefer pipeline_state.msgSend(void, objc.sel("release"), .{});
return pipeline_state;
}
};
fn autoAttribute(T: type, attrs: objc.Object) void {
inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| {
const offset = @offsetOf(T, field.name);
const FT = switch (@typeInfo(field.type)) {
.@"enum" => |e| e.tag_type,
else => field.type,
};
const format = switch (FT) {
[4]u8 => mtl.MTLVertexFormat.uchar4,
[2]u16 => mtl.MTLVertexFormat.ushort2,
[2]i16 => mtl.MTLVertexFormat.short2,
[2]f32 => mtl.MTLVertexFormat.float2,
[4]f32 => mtl.MTLVertexFormat.float4,
[2]i32 => mtl.MTLVertexFormat.int2,
u32 => mtl.MTLVertexFormat.uint,
[2]u32 => mtl.MTLVertexFormat.uint2,
[4]u32 => mtl.MTLVertexFormat.uint4,
u8 => mtl.MTLVertexFormat.uchar,
else => comptime unreachable,
};
const attr = attrs.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, i)},
);
attr.setProperty("format", @intFromEnum(format));
attr.setProperty("offset", @as(c_ulong, offset));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
},
},
});
}
fn checkError(err_: ?*anyopaque) !void {

View File

@ -1,196 +0,0 @@
/// The OpenGL program for rendering terminal cells.
const CellProgram = @This();
const std = @import("std");
const gl = @import("opengl");
program: gl.Program,
vao: gl.VertexArray,
ebo: gl.Buffer,
vbo: gl.Buffer,
/// The raw structure that maps directly to the buffer sent to the vertex shader.
/// This must be "extern" so that the field order is not reordered by the
/// Zig compiler.
pub const Cell = extern struct {
/// vec2 grid_coord
grid_col: u16,
grid_row: u16,
/// vec2 glyph_pos
glyph_x: u32 = 0,
glyph_y: u32 = 0,
/// vec2 glyph_size
glyph_width: u32 = 0,
glyph_height: u32 = 0,
/// vec2 glyph_offset
glyph_offset_x: i32 = 0,
glyph_offset_y: i32 = 0,
/// vec4 color_in
r: u8,
g: u8,
b: u8,
a: u8,
/// vec4 bg_color_in
bg_r: u8,
bg_g: u8,
bg_b: u8,
bg_a: u8,
/// uint mode
mode: CellMode,
/// The width in grid cells that a rendering takes.
grid_width: u8,
};
pub const CellMode = enum(u8) {
bg = 1,
fg = 2,
fg_constrained = 3,
fg_color = 7,
fg_powerline = 15,
// Non-exhaustive because masks change it
_,
/// Apply a mask to the mode.
pub fn mask(self: CellMode, m: CellMode) CellMode {
return @enumFromInt(@intFromEnum(self) | @intFromEnum(m));
}
pub fn isFg(self: CellMode) bool {
// Since we use bit tricks below, we want to ensure the enum
// doesn't change without us looking at this logic again.
comptime {
const info = @typeInfo(CellMode).@"enum";
std.debug.assert(info.fields.len == 5);
}
return @intFromEnum(self) & @intFromEnum(@as(CellMode, .fg)) != 0;
}
};
pub fn init() !CellProgram {
// Load and compile our shaders.
const program = try gl.Program.createVF(
@embedFile("../shaders/cell.v.glsl"),
@embedFile("../shaders/cell.f.glsl"),
);
errdefer program.destroy();
// Set our cell dimensions
const pbind = try program.use();
defer pbind.unbind();
// Set all of our texture indexes
try program.setUniform("text", 0);
try program.setUniform("text_color", 1);
// Setup our VAO
const vao = try gl.VertexArray.create();
errdefer vao.destroy();
const vaobind = try vao.bind();
defer vaobind.unbind();
// Element buffer (EBO)
const ebo = try gl.Buffer.create();
errdefer ebo.destroy();
var ebobind = try ebo.bind(.element_array);
defer ebobind.unbind();
try ebobind.setData([6]u8{
0, 1, 3, // Top-left triangle
1, 2, 3, // Bottom-right triangle
}, .static_draw);
// Vertex buffer (VBO)
const vbo = try gl.Buffer.create();
errdefer vbo.destroy();
var vbobind = try vbo.bind(.array);
defer vbobind.unbind();
var offset: usize = 0;
try vbobind.attributeAdvanced(0, 2, gl.c.GL_UNSIGNED_SHORT, false, @sizeOf(Cell), offset);
offset += 2 * @sizeOf(u16);
try vbobind.attributeAdvanced(1, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Cell), offset);
offset += 2 * @sizeOf(u32);
try vbobind.attributeAdvanced(2, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Cell), offset);
offset += 2 * @sizeOf(u32);
try vbobind.attributeAdvanced(3, 2, gl.c.GL_INT, false, @sizeOf(Cell), offset);
offset += 2 * @sizeOf(i32);
try vbobind.attributeAdvanced(4, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(Cell), offset);
offset += 4 * @sizeOf(u8);
try vbobind.attributeAdvanced(5, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(Cell), offset);
offset += 4 * @sizeOf(u8);
try vbobind.attributeIAdvanced(6, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Cell), offset);
offset += 1 * @sizeOf(u8);
try vbobind.attributeIAdvanced(7, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Cell), offset);
try vbobind.enableAttribArray(0);
try vbobind.enableAttribArray(1);
try vbobind.enableAttribArray(2);
try vbobind.enableAttribArray(3);
try vbobind.enableAttribArray(4);
try vbobind.enableAttribArray(5);
try vbobind.enableAttribArray(6);
try vbobind.enableAttribArray(7);
try vbobind.attributeDivisor(0, 1);
try vbobind.attributeDivisor(1, 1);
try vbobind.attributeDivisor(2, 1);
try vbobind.attributeDivisor(3, 1);
try vbobind.attributeDivisor(4, 1);
try vbobind.attributeDivisor(5, 1);
try vbobind.attributeDivisor(6, 1);
try vbobind.attributeDivisor(7, 1);
return .{
.program = program,
.vao = vao,
.ebo = ebo,
.vbo = vbo,
};
}
pub fn bind(self: CellProgram) !Binding {
const program = try self.program.use();
errdefer program.unbind();
const vao = try self.vao.bind();
errdefer vao.unbind();
const ebo = try self.ebo.bind(.element_array);
errdefer ebo.unbind();
const vbo = try self.vbo.bind(.array);
errdefer vbo.unbind();
return .{
.program = program,
.vao = vao,
.ebo = ebo,
.vbo = vbo,
};
}
pub fn deinit(self: CellProgram) void {
self.vbo.destroy();
self.ebo.destroy();
self.vao.destroy();
self.program.destroy();
}
pub const Binding = struct {
program: gl.Program.Binding,
vao: gl.VertexArray.Binding,
ebo: gl.Buffer.Binding,
vbo: gl.Buffer.Binding,
pub fn unbind(self: Binding) void {
self.vbo.unbind();
self.ebo.unbind();
self.vao.unbind();
self.program.unbind();
}
};

View File

@ -0,0 +1,75 @@
//! Wrapper for handling render passes.
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const gl = @import("opengl");
const Renderer = @import("../generic.zig").Renderer(OpenGL);
const OpenGL = @import("../OpenGL.zig");
const Target = @import("Target.zig");
const Pipeline = @import("Pipeline.zig");
const RenderPass = @import("RenderPass.zig");
const Buffer = @import("buffer.zig").Buffer;
const Health = @import("../../renderer.zig").Health;
const log = std.log.scoped(.opengl);
/// Options for beginning a frame.
pub const Options = struct {};
renderer: *Renderer,
target: *Target,
/// Begin encoding a frame.
pub fn begin(
opts: Options,
/// Once the frame has been completed, the `frameCompleted` method
/// on the renderer is called with the health status of the frame.
renderer: *Renderer,
/// The target is presented via the provided renderer's API when completed.
target: *Target,
) !Self {
_ = opts;
return .{
.renderer = renderer,
.target = target,
};
}
/// Add a render pass to this frame with the provided attachments.
/// Returns a RenderPass which allows render steps to be added.
pub inline fn renderPass(
self: *const Self,
attachments: []const RenderPass.Options.Attachment,
) RenderPass {
_ = self;
return RenderPass.begin(.{ .attachments = attachments });
}
/// Complete this frame and present the target.
///
/// If `sync` is true, this will block until the frame is presented.
///
/// NOTE: For OpenGL, `sync` is ignored and we always block.
pub fn complete(self: *const Self, sync: bool) void {
_ = sync;
gl.finish();
// If there are any GL errors, consider the frame unhealthy.
const health: Health = if (gl.errors.getError()) .healthy else |_| .unhealthy;
// If the frame is healthy, present it.
if (health == .healthy) {
self.renderer.api.present(self.target.*) catch |err| {
log.err("Failed to present render target: err={}", .{err});
};
}
// Report the health to the renderer.
self.renderer.frameCompleted(health);
}

View File

@ -1,134 +0,0 @@
/// The OpenGL program for rendering terminal cells.
const ImageProgram = @This();
const std = @import("std");
const gl = @import("opengl");
program: gl.Program,
vao: gl.VertexArray,
ebo: gl.Buffer,
vbo: gl.Buffer,
pub const Input = extern struct {
/// vec2 grid_coord
grid_col: i32,
grid_row: i32,
/// vec2 cell_offset
cell_offset_x: u32 = 0,
cell_offset_y: u32 = 0,
/// vec4 source_rect
source_x: u32 = 0,
source_y: u32 = 0,
source_width: u32 = 0,
source_height: u32 = 0,
/// vec2 dest_size
dest_width: u32 = 0,
dest_height: u32 = 0,
};
pub fn init() !ImageProgram {
// Load and compile our shaders.
const program = try gl.Program.createVF(
@embedFile("../shaders/image.v.glsl"),
@embedFile("../shaders/image.f.glsl"),
);
errdefer program.destroy();
// Set our program uniforms
const pbind = try program.use();
defer pbind.unbind();
// Set all of our texture indexes
try program.setUniform("image", 0);
// Setup our VAO
const vao = try gl.VertexArray.create();
errdefer vao.destroy();
const vaobind = try vao.bind();
defer vaobind.unbind();
// Element buffer (EBO)
const ebo = try gl.Buffer.create();
errdefer ebo.destroy();
var ebobind = try ebo.bind(.element_array);
defer ebobind.unbind();
try ebobind.setData([6]u8{
0, 1, 3, // Top-left triangle
1, 2, 3, // Bottom-right triangle
}, .static_draw);
// Vertex buffer (VBO)
const vbo = try gl.Buffer.create();
errdefer vbo.destroy();
var vbobind = try vbo.bind(.array);
defer vbobind.unbind();
var offset: usize = 0;
try vbobind.attributeAdvanced(0, 2, gl.c.GL_INT, false, @sizeOf(Input), offset);
offset += 2 * @sizeOf(i32);
try vbobind.attributeAdvanced(1, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Input), offset);
offset += 2 * @sizeOf(u32);
try vbobind.attributeAdvanced(2, 4, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Input), offset);
offset += 4 * @sizeOf(u32);
try vbobind.attributeAdvanced(3, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Input), offset);
offset += 2 * @sizeOf(u32);
try vbobind.enableAttribArray(0);
try vbobind.enableAttribArray(1);
try vbobind.enableAttribArray(2);
try vbobind.enableAttribArray(3);
try vbobind.attributeDivisor(0, 1);
try vbobind.attributeDivisor(1, 1);
try vbobind.attributeDivisor(2, 1);
try vbobind.attributeDivisor(3, 1);
return .{
.program = program,
.vao = vao,
.ebo = ebo,
.vbo = vbo,
};
}
pub fn bind(self: ImageProgram) !Binding {
const program = try self.program.use();
errdefer program.unbind();
const vao = try self.vao.bind();
errdefer vao.unbind();
const ebo = try self.ebo.bind(.element_array);
errdefer ebo.unbind();
const vbo = try self.vbo.bind(.array);
errdefer vbo.unbind();
return .{
.program = program,
.vao = vao,
.ebo = ebo,
.vbo = vbo,
};
}
pub fn deinit(self: ImageProgram) void {
self.vbo.destroy();
self.ebo.destroy();
self.vao.destroy();
self.program.destroy();
}
pub const Binding = struct {
program: gl.Program.Binding,
vao: gl.VertexArray.Binding,
ebo: gl.Buffer.Binding,
vbo: gl.Buffer.Binding,
pub fn unbind(self: Binding) void {
self.vbo.unbind();
self.ebo.unbind();
self.vao.unbind();
self.program.unbind();
}
};

View File

@ -0,0 +1,170 @@
//! Wrapper for handling render pipelines.
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const gl = @import("opengl");
const OpenGL = @import("../OpenGL.zig");
const Texture = @import("Texture.zig");
const Buffer = @import("buffer.zig").Buffer;
const log = std.log.scoped(.opengl);
/// Options for initializing a render pipeline.
pub const Options = struct {
/// GLSL source of the vertex function
vertex_fn: [:0]const u8,
/// GLSL source of the fragment function
fragment_fn: [:0]const u8,
/// Vertex step function
step_fn: StepFunction = .per_vertex,
/// Whether to enable blending.
blending_enabled: bool = true,
pub const StepFunction = enum {
constant,
per_vertex,
per_instance,
};
};
program: gl.Program,
fbo: gl.Framebuffer,
vao: gl.VertexArray,
stride: usize,
blending_enabled: bool,
pub fn init(comptime VertexAttributes: ?type, opts: Options) !Self {
// Load and compile our shaders.
const program = try gl.Program.createVF(
opts.vertex_fn,
opts.fragment_fn,
);
errdefer program.destroy();
const pbind = try program.use();
defer pbind.unbind();
const fbo = try gl.Framebuffer.create();
errdefer fbo.destroy();
const fbobind = try fbo.bind(.framebuffer);
defer fbobind.unbind();
const vao = try gl.VertexArray.create();
errdefer vao.destroy();
const vaobind = try vao.bind();
defer vaobind.unbind();
if (VertexAttributes) |VA| try autoAttribute(VA, vaobind, opts.step_fn);
return .{
.program = program,
.fbo = fbo,
.vao = vao,
.stride = if (VertexAttributes) |VA| @sizeOf(VA) else 0,
.blending_enabled = opts.blending_enabled,
};
}
pub fn deinit(self: *const Self) void {
self.program.destroy();
}
fn autoAttribute(
T: type,
vaobind: gl.VertexArray.Binding,
step_fn: Options.StepFunction,
) !void {
const divisor: gl.c.GLuint = switch (step_fn) {
.per_vertex => 0,
.per_instance => 1,
.constant => std.math.maxInt(gl.c.GLuint),
};
inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| {
try vaobind.enableAttribArray(i);
try vaobind.attributeBinding(i, 0);
try vaobind.bindingDivisor(i, divisor);
const offset = @offsetOf(T, field.name);
const FT = switch (@typeInfo(field.type)) {
.@"enum" => |e| e.tag_type,
else => field.type,
};
const size, const IT = switch (@typeInfo(FT)) {
.array => |a| .{ a.len, a.child },
else => .{ 1, FT },
};
try switch (IT) {
u8 => vaobind.attributeIFormat(
i,
size,
gl.c.GL_UNSIGNED_BYTE,
offset,
),
u16 => vaobind.attributeIFormat(
i,
size,
gl.c.GL_UNSIGNED_SHORT,
offset,
),
u32 => vaobind.attributeIFormat(
i,
size,
gl.c.GL_UNSIGNED_INT,
offset,
),
i8 => vaobind.attributeIFormat(
i,
size,
gl.c.GL_BYTE,
offset,
),
i16 => vaobind.attributeIFormat(
i,
size,
gl.c.GL_SHORT,
offset,
),
i32 => vaobind.attributeIFormat(
i,
size,
gl.c.GL_INT,
offset,
),
f16 => vaobind.attributeFormat(
i,
size,
gl.c.GL_HALF_FLOAT,
false,
offset,
),
f32 => vaobind.attributeFormat(
i,
size,
gl.c.GL_FLOAT,
false,
offset,
),
f64 => vaobind.attributeLFormat(
i,
size,
offset,
),
else => unreachable,
};
}
}

View File

@ -0,0 +1,141 @@
//! Wrapper for handling render passes.
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const gl = @import("opengl");
const OpenGL = @import("../OpenGL.zig");
const Target = @import("Target.zig");
const Texture = @import("Texture.zig");
const Pipeline = @import("Pipeline.zig");
const RenderPass = @import("RenderPass.zig");
const Buffer = @import("buffer.zig").Buffer;
/// Options for beginning a render pass.
pub const Options = struct {
/// Color attachments for this render pass.
attachments: []const Attachment,
/// Describes a color attachment.
pub const Attachment = struct {
target: union(enum) {
texture: Texture,
target: Target,
},
clear_color: ?[4]f32 = null,
};
};
/// Describes a step in a render pass.
pub const Step = struct {
pipeline: Pipeline,
uniforms: ?gl.Buffer = null,
buffers: []const ?gl.Buffer = &.{},
textures: []const ?Texture = &.{},
draw: Draw,
/// Describes the draw call for this step.
pub const Draw = struct {
type: gl.Primitive,
vertex_count: usize,
instance_count: usize = 1,
};
};
attachments: []const Options.Attachment,
step_number: usize = 0,
/// Begin a render pass.
pub fn begin(
opts: Options,
) Self {
return .{
.attachments = opts.attachments,
};
}
/// Add a step to this render pass.
///
/// TODO: Errors are silently ignored in this function, maybe they shouldn't be?
pub fn step(self: *Self, s: Step) void {
if (s.draw.instance_count == 0) return;
const pbind = s.pipeline.program.use() catch return;
defer pbind.unbind();
const vaobind = s.pipeline.vao.bind() catch return;
defer vaobind.unbind();
const fbobind = switch (self.attachments[0].target) {
.target => |t| t.framebuffer.bind(.framebuffer) catch return,
.texture => |t| bind: {
const fbobind = s.pipeline.fbo.bind(.framebuffer) catch return;
fbobind.texture2D(.color0, t.target, t.texture, 0) catch {
fbobind.unbind();
return;
};
break :bind fbobind;
},
};
defer fbobind.unbind();
defer self.step_number += 1;
// If we have a clear color and this is the
// first step in the pass, go ahead and clear.
if (self.step_number == 0) if (self.attachments[0].clear_color) |c| {
gl.clearColor(c[0], c[1], c[2], c[3]);
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
};
// Bind the uniform buffer we bind at index 1 to align with Metal.
if (s.uniforms) |ubo| {
_ = ubo.bindBase(.uniform, 1) catch return;
}
// Bind relevant texture units.
for (s.textures, 0..) |t, i| if (t) |tex| {
gl.Texture.active(@intCast(i)) catch return;
_ = tex.texture.bind(tex.target) catch return;
};
// Bind 0th buffer as the vertex buffer,
// and bind the rest as storage buffers.
if (s.buffers.len > 0) {
if (s.buffers[0]) |vbo| vaobind.bindVertexBuffer(
0,
vbo.id,
0,
@intCast(s.pipeline.stride),
) catch return;
for (s.buffers[1..], 1..) |b, i| if (b) |buf| {
_ = buf.bindBase(.storage, @intCast(i)) catch return;
};
}
if (s.pipeline.blending_enabled) {
gl.enable(gl.c.GL_BLEND) catch return;
gl.blendFunc(gl.c.GL_ONE, gl.c.GL_ONE_MINUS_SRC_ALPHA) catch return;
} else {
gl.disable(gl.c.GL_BLEND) catch return;
}
gl.drawArraysInstanced(
s.draw.type,
0,
@intCast(s.draw.vertex_count),
@intCast(s.draw.instance_count),
) catch return;
}
/// Complete this render pass.
/// This struct can no longer be used after calling this.
pub fn complete(self: *const Self) void {
_ = self;
gl.flush();
}

View File

@ -0,0 +1,62 @@
//! Represents a render target.
//!
//! In this case, an OpenGL renderbuffer-backed framebuffer.
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const gl = @import("opengl");
const log = std.log.scoped(.opengl);
/// Options for initializing a Target
pub const Options = struct {
/// Desired width
width: usize,
/// Desired height
height: usize,
/// Internal format for the renderbuffer.
internal_format: gl.Texture.InternalFormat,
};
/// The underlying `gl.Framebuffer` instance.
framebuffer: gl.Framebuffer,
/// The underlying `gl.Renderbuffer` instance.
renderbuffer: gl.Renderbuffer,
/// Current width of this target.
width: usize,
/// Current height of this target.
height: usize,
pub fn init(opts: Options) !Self {
const rbo = try gl.Renderbuffer.create();
const bound_rbo = try rbo.bind();
defer bound_rbo.unbind();
try bound_rbo.storage(
opts.internal_format,
@intCast(opts.width),
@intCast(opts.height),
);
const fbo = try gl.Framebuffer.create();
const bound_fbo = try fbo.bind(.framebuffer);
defer bound_fbo.unbind();
try bound_fbo.renderbuffer(.color0, rbo);
return .{
.framebuffer = fbo,
.renderbuffer = rbo,
.width = opts.width,
.height = opts.height,
};
}
pub fn deinit(self: *Self) void {
self.framebuffer.destroy();
self.renderbuffer.destroy();
}

View File

@ -0,0 +1,99 @@
//! 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 gl = @import("opengl");
const OpenGL = @import("../OpenGL.zig");
const log = std.log.scoped(.opengl);
/// Options for initializing a texture.
pub const Options = struct {
format: gl.Texture.Format,
internal_format: gl.Texture.InternalFormat,
target: gl.Texture.Target,
};
texture: gl.Texture,
/// The width of this texture.
width: usize,
/// The height of this texture.
height: usize,
/// Format for this texture.
format: gl.Texture.Format,
/// Target for this texture.
target: gl.Texture.Target,
/// Initialize a texture
pub fn init(
opts: Options,
width: usize,
height: usize,
data: ?[]const u8,
) !Self {
const tex = try gl.Texture.create();
errdefer tex.destroy();
{
const texbind = try tex.bind(opts.target);
defer texbind.unbind();
try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
try texbind.parameter(.MinFilter, gl.c.GL_LINEAR);
try texbind.parameter(.MagFilter, gl.c.GL_LINEAR);
try texbind.image2D(
0,
opts.internal_format,
@intCast(width),
@intCast(height),
0,
opts.format,
.UnsignedByte,
if (data) |d| @ptrCast(d.ptr) else null,
);
}
return .{
.texture = tex,
.width = width,
.height = height,
.format = opts.format,
.target = opts.target,
};
}
pub fn deinit(self: Self) void {
self.texture.destroy();
}
/// 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,
) !void {
const texbind = try self.texture.bind(self.target);
defer texbind.unbind();
try texbind.subImage2D(
0,
@intCast(x),
@intCast(y),
@intCast(width),
@intCast(height),
self.format,
.UnsignedByte,
data.ptr,
);
}

View File

@ -0,0 +1,127 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const gl = @import("opengl");
const OpenGL = @import("../OpenGL.zig");
const log = std.log.scoped(.opengl);
/// Options for initializing a buffer.
pub const Options = struct {
target: gl.Buffer.Target = .array,
usage: gl.Buffer.Usage = .dynamic_draw,
};
/// OpenGL 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 OpenGL.
pub fn Buffer(comptime T: type) type {
return struct {
const Self = @This();
/// Underlying `gl.Buffer` instance.
buffer: gl.Buffer,
/// Options this buffer was allocated with.
opts: Options,
/// Current allocated length of the data store.
/// Note 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 = try gl.Buffer.create();
errdefer buffer.destroy();
const binding = try buffer.bind(opts.target);
defer binding.unbind();
try binding.setDataNullManual(len * @sizeOf(T), opts.usage);
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 = try gl.Buffer.create();
errdefer buffer.destroy();
const binding = try buffer.bind(opts.target);
defer binding.unbind();
try binding.setData(data, opts.usage);
return .{
.buffer = buffer,
.opts = opts,
.len = data.len * @sizeOf(T),
};
}
pub fn deinit(self: Self) void {
self.buffer.destroy();
}
/// 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 {
const binding = try self.buffer.bind(self.opts.target);
defer binding.unbind();
// If we need more space than our buffer has, we need to reallocate.
if (data.len > self.len) {
// Reallocate the buffer to hold double what we require.
self.len = data.len * 2;
try binding.setDataNullManual(
self.len * @sizeOf(T),
self.opts.usage,
);
}
// We can fit within the buffer so we can just replace bytes.
try binding.setSubData(0, data);
}
/// 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 {
const binding = try self.buffer.bind(self.opts.target);
defer binding.unbind();
var total_len: usize = 0;
for (lists) |list| {
total_len += list.items.len;
}
// If we need more space than our buffer has, we need to reallocate.
if (total_len > self.len) {
// Reallocate the buffer to hold double what we require.
self.len = total_len * 2;
try binding.setDataNullManual(
self.len * @sizeOf(T),
self.opts.usage,
);
}
// We can fit within the buffer so we can just replace bytes.
var i: usize = 0;
for (lists) |list| {
try binding.setSubData(i, list.items);
i += list.items.len * @sizeOf(T);
}
return total_len;
}
};
}

View File

@ -0,0 +1,220 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const renderer = @import("../../renderer.zig");
const terminal = @import("../../terminal/main.zig");
const shaderpkg = @import("shaders.zig");
/// The possible cell content keys that exist.
pub const Key = enum {
bg,
text,
underline,
strikethrough,
overline,
/// Returns the GPU vertex type for this key.
pub fn CellType(self: Key) type {
return switch (self) {
.bg => shaderpkg.CellBg,
.text,
.underline,
.strikethrough,
.overline,
=> shaderpkg.CellText,
};
}
};
/// A pool of ArrayLists with methods for bulk operations.
fn ArrayListPool(comptime T: type) type {
return struct {
const Self = ArrayListPool(T);
const ArrayListT = std.ArrayListUnmanaged(T);
// An array containing the lists that belong to this pool.
lists: []ArrayListT = &[_]ArrayListT{},
// The pool will be initialized with empty ArrayLists.
pub fn init(alloc: Allocator, list_count: usize, initial_capacity: usize) !Self {
const self: Self = .{
.lists = try alloc.alloc(ArrayListT, list_count),
};
for (self.lists) |*list| {
list.* = try ArrayListT.initCapacity(alloc, initial_capacity);
}
return self;
}
pub fn deinit(self: *Self, alloc: Allocator) void {
for (self.lists) |*list| {
list.deinit(alloc);
}
alloc.free(self.lists);
}
/// Clear all lists in the pool.
pub fn reset(self: *Self) void {
for (self.lists) |*list| {
list.clearRetainingCapacity();
}
}
};
}
/// The contents of all the cells in the terminal.
///
/// The goal of this data structure is to allow for efficient row-wise
/// clearing of data from the GPU buffers, to allow for row-wise dirty
/// tracking to eliminate the overhead of rebuilding the GPU buffers
/// each frame.
///
/// Must be initialized by resizing before calling any operations.
pub const Contents = struct {
size: renderer.GridSize = .{ .rows = 0, .columns = 0 },
/// Flat array containing cell background colors for the terminal grid.
///
/// Indexed as `bg_cells[row * size.columns + col]`.
///
/// Prefer accessing with `Contents.bgCell(row, col).*` instead
/// of directly indexing in order to avoid integer size bugs.
bg_cells: []shaderpkg.CellBg = undefined,
/// The ArrayListPool which holds all of the foreground cells. When sized
/// with Contents.resize the individual ArrayLists are given enough room
/// that they can hold a single row with #cols glyphs, underlines, and
/// strikethroughs; however, appendAssumeCapacity MUST NOT be used since
/// it is possible to exceed this with combining glyphs that add a glyph
/// but take up no column since they combine with the previous one, as
/// well as with fonts that perform multi-substitutions for glyphs, which
/// can result in a similar situation where multiple glyphs reside in the
/// same column.
///
/// Allocations should nevertheless be exceedingly rare since hitting the
/// initial capacity of a list would require a row filled with underlined
/// struck through characters, at least one of which is a multi-glyph
/// composite.
///
/// Rows are indexed as Contents.fg_rows[y + 1], because the first list in
/// the pool is reserved for the cursor, which must be the first item in
/// the buffer.
///
/// Must be initialized by calling resize on the Contents struct before
/// calling any operations.
fg_rows: ArrayListPool(shaderpkg.CellText) = .{},
pub fn deinit(self: *Contents, alloc: Allocator) void {
alloc.free(self.bg_cells);
self.fg_rows.deinit(alloc);
}
/// Resize the cell contents for the given grid size. This will
/// always invalidate the entire cell contents.
pub fn resize(
self: *Contents,
alloc: Allocator,
size: renderer.GridSize,
) !void {
self.size = size;
const cell_count = @as(usize, size.columns) * @as(usize, size.rows);
const bg_cells = try alloc.alloc(shaderpkg.CellBg, cell_count);
errdefer alloc.free(bg_cells);
@memset(bg_cells, .{ 0, 0, 0, 0 });
// The foreground lists can hold 3 types of items:
// - Glyphs
// - Underlines
// - Strikethroughs
// So we give them an initial capacity of size.columns * 3, which will
// avoid any further allocations in the vast majority of cases. Sadly
// we can not assume capacity though, since with combining glyphs that
// form a single grapheme, and multi-substitutions in fonts, the number
// of glyphs in a row is theoretically unlimited.
//
// We have size.rows + 1 lists because index 0 is used for a special
// list containing the cursor cell which needs to be first in the buffer.
var fg_rows = try ArrayListPool(shaderpkg.CellText).init(alloc, size.rows + 1, size.columns * 3);
errdefer fg_rows.deinit(alloc);
alloc.free(self.bg_cells);
self.fg_rows.deinit(alloc);
self.bg_cells = bg_cells;
self.fg_rows = fg_rows;
// We don't need 3*cols worth of cells for the cursor list, so we can
// replace it with a smaller list. This is technically a tiny bit of
// extra work but resize is not a hot function so it's worth it to not
// waste the memory.
self.fg_rows.lists[0].deinit(alloc);
self.fg_rows.lists[0] = try std.ArrayListUnmanaged(shaderpkg.CellText).initCapacity(alloc, 1);
}
/// Reset the cell contents to an empty state without resizing.
pub fn reset(self: *Contents) void {
@memset(self.bg_cells, .{ 0, 0, 0, 0 });
self.fg_rows.reset();
}
/// Set the cursor value. If the value is null then the cursor is hidden.
pub fn setCursor(self: *Contents, v: ?shaderpkg.CellText) void {
self.fg_rows.lists[0].clearRetainingCapacity();
if (v) |cell| {
self.fg_rows.lists[0].appendAssumeCapacity(cell);
}
}
/// Access a background cell. Prefer this function over direct indexing
/// of `bg_cells` in order to avoid integer size bugs causing overflows.
pub inline fn bgCell(self: *Contents, row: usize, col: usize) *shaderpkg.CellBg {
return &self.bg_cells[row * self.size.columns + col];
}
/// Add a cell to the appropriate list. Adding the same cell twice will
/// result in duplication in the vertex buffer. The caller should clear
/// the corresponding row with Contents.clear to remove old cells first.
pub fn add(
self: *Contents,
alloc: Allocator,
comptime key: Key,
cell: key.CellType(),
) !void {
const y = cell.grid_pos[1];
assert(y < self.size.rows);
switch (key) {
.bg => comptime unreachable,
.text,
.underline,
.strikethrough,
.overline,
// We have a special list containing the cursor cell at the start
// of our fg row pool, so we need to add 1 to the y to get the
// correct index.
=> try self.fg_rows.lists[y + 1].append(alloc, cell),
}
}
/// Clear all of the cell contents for a given row.
pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void {
assert(y < self.size.rows);
@memset(self.bg_cells[@as(usize, y) * self.size.columns ..][0..self.size.columns], .{ 0, 0, 0, 0 });
// We have a special list containing the cursor cell at the start
// of our fg row pool, so we need to add 1 to the y to get the
// correct index.
self.fg_rows.lists[y + 1].clearRetainingCapacity();
}
};

View File

@ -1,310 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const gl = @import("opengl");
const Size = @import("../size.zig").Size;
const log = std.log.scoped(.opengl_custom);
/// The "INDEX" is the index into the global GL state and the
/// "BINDING" is the binding location in the shader.
const UNIFORM_INDEX: gl.c.GLuint = 0;
const UNIFORM_BINDING: gl.c.GLuint = 0;
/// Global uniforms for custom shaders.
pub const Uniforms = extern struct {
resolution: [3]f32 align(16) = .{ 0, 0, 0 },
time: f32 align(4) = 1,
time_delta: f32 align(4) = 1,
frame_rate: f32 align(4) = 1,
frame: i32 align(4) = 1,
channel_time: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
channel_resolution: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
mouse: [4]f32 align(16) = .{ 0, 0, 0, 0 },
date: [4]f32 align(16) = .{ 0, 0, 0, 0 },
sample_rate: f32 align(4) = 1,
};
/// The state associated with custom shaders. This should only be initialized
/// if there is at least one custom shader.
///
/// To use this, the main terminal shader should render to the framebuffer
/// specified by "fbo". The resulting "fb_texture" will contain the color
/// attachment. This is then used as the iChannel0 input to the custom
/// shader.
pub const State = struct {
/// The uniform data
uniforms: Uniforms,
/// The OpenGL buffers
fbo: gl.Framebuffer,
ubo: gl.Buffer,
vao: gl.VertexArray,
ebo: gl.Buffer,
fb_texture: gl.Texture,
/// The set of programs for the custom shaders.
programs: []const Program,
/// The first time a frame was drawn. This is used to update
/// the time uniform.
first_frame_time: std.time.Instant,
/// The last time a frame was drawn. This is used to update
/// the time uniform.
last_frame_time: std.time.Instant,
pub fn init(
alloc: Allocator,
srcs: []const [:0]const u8,
) !State {
if (srcs.len == 0) return error.OneCustomShaderRequired;
// Create our programs
var programs = std.ArrayList(Program).init(alloc);
defer programs.deinit();
errdefer for (programs.items) |p| p.deinit();
for (srcs) |src| {
try programs.append(try Program.init(src));
}
// Create the texture for the framebuffer
const fb_tex = try gl.Texture.create();
errdefer fb_tex.destroy();
{
const texbind = try fb_tex.bind(.@"2D");
try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
try texbind.parameter(.MinFilter, gl.c.GL_LINEAR);
try texbind.parameter(.MagFilter, gl.c.GL_LINEAR);
try texbind.image2D(
0,
.rgb,
1,
1,
0,
.rgb,
.UnsignedByte,
null,
);
}
// Create our framebuffer for rendering off screen.
// The shader prior to custom shaders should use this
// framebuffer.
const fbo = try gl.Framebuffer.create();
errdefer fbo.destroy();
const fbbind = try fbo.bind(.framebuffer);
defer fbbind.unbind();
try fbbind.texture2D(.color0, .@"2D", fb_tex, 0);
const fbstatus = fbbind.checkStatus();
if (fbstatus != .complete) {
log.warn(
"framebuffer is not complete state={}",
.{fbstatus},
);
return error.InvalidFramebuffer;
}
// Create our uniform buffer that is shared across all
// custom shaders
const ubo = try gl.Buffer.create();
errdefer ubo.destroy();
{
var ubobind = try ubo.bind(.uniform);
defer ubobind.unbind();
try ubobind.setDataNull(Uniforms, .static_draw);
}
// Setup our VAO for the custom shader.
const vao = try gl.VertexArray.create();
errdefer vao.destroy();
const vaobind = try vao.bind();
defer vaobind.unbind();
// Element buffer (EBO)
const ebo = try gl.Buffer.create();
errdefer ebo.destroy();
var ebobind = try ebo.bind(.element_array);
defer ebobind.unbind();
try ebobind.setData([6]u8{
0, 1, 3, // Top-left triangle
1, 2, 3, // Bottom-right triangle
}, .static_draw);
return .{
.programs = try programs.toOwnedSlice(),
.uniforms = .{},
.fbo = fbo,
.ubo = ubo,
.vao = vao,
.ebo = ebo,
.fb_texture = fb_tex,
.first_frame_time = try std.time.Instant.now(),
.last_frame_time = try std.time.Instant.now(),
};
}
pub fn deinit(self: *const State, alloc: Allocator) void {
for (self.programs) |p| p.deinit();
alloc.free(self.programs);
self.ubo.destroy();
self.ebo.destroy();
self.vao.destroy();
self.fb_texture.destroy();
self.fbo.destroy();
}
pub fn setScreenSize(self: *State, size: Size) !void {
// Update our uniforms
self.uniforms.resolution = .{
@floatFromInt(size.screen.width),
@floatFromInt(size.screen.height),
1,
};
try self.syncUniforms();
// Update our texture
const texbind = try self.fb_texture.bind(.@"2D");
try texbind.image2D(
0,
.rgb,
@intCast(size.screen.width),
@intCast(size.screen.height),
0,
.rgb,
.UnsignedByte,
null,
);
}
/// Call this prior to drawing a frame to update the time
/// and synchronize the uniforms. This synchronizes uniforms
/// so you should make changes to uniforms prior to calling
/// this.
pub fn newFrame(self: *State) !void {
// Update our frame time
const now = std.time.Instant.now() catch self.first_frame_time;
const since_ns: f32 = @floatFromInt(now.since(self.first_frame_time));
const delta_ns: f32 = @floatFromInt(now.since(self.last_frame_time));
self.uniforms.time = since_ns / std.time.ns_per_s;
self.uniforms.time_delta = delta_ns / std.time.ns_per_s;
self.last_frame_time = now;
// Sync our uniform changes
try self.syncUniforms();
}
fn syncUniforms(self: *State) !void {
var ubobind = try self.ubo.bind(.uniform);
defer ubobind.unbind();
try ubobind.setData(self.uniforms, .static_draw);
}
/// Call this to bind all the necessary OpenGL resources for
/// all custom shaders. Each individual shader needs to be bound
/// one at a time too.
pub fn bind(self: *const State) !Binding {
// Move our uniform buffer into proper global index. Note that
// in theory we can do this globally once and never worry about
// it again. I don't think we're high-performance enough at all
// to worry about that and this makes it so you can just move
// around CustomProgram usage without worrying about clobbering
// the global state.
try self.ubo.bindBase(.uniform, UNIFORM_INDEX);
// Bind our texture that is shared amongst all
try gl.Texture.active(gl.c.GL_TEXTURE0);
var texbind = try self.fb_texture.bind(.@"2D");
errdefer texbind.unbind();
const vao = try self.vao.bind();
errdefer vao.unbind();
const ebo = try self.ebo.bind(.element_array);
errdefer ebo.unbind();
return .{
.vao = vao,
.ebo = ebo,
.fb_texture = texbind,
};
}
/// Copy the fbo's attached texture to the backbuffer.
pub fn copyFramebuffer(self: *State) !void {
const texbind = try self.fb_texture.bind(.@"2D");
errdefer texbind.unbind();
try texbind.copySubImage2D(
0,
0,
0,
0,
0,
@intFromFloat(self.uniforms.resolution[0]),
@intFromFloat(self.uniforms.resolution[1]),
);
}
pub const Binding = struct {
vao: gl.VertexArray.Binding,
ebo: gl.Buffer.Binding,
fb_texture: gl.Texture.Binding,
pub fn unbind(self: Binding) void {
self.ebo.unbind();
self.vao.unbind();
self.fb_texture.unbind();
}
};
};
/// A single OpenGL program (combined shaders) for custom shaders.
pub const Program = struct {
program: gl.Program,
pub fn init(src: [:0]const u8) !Program {
const program = try gl.Program.createVF(
@embedFile("../shaders/custom.v.glsl"),
src,
);
errdefer program.destroy();
// Map our uniform buffer to the global GL state
try program.uniformBlockBinding(UNIFORM_INDEX, UNIFORM_BINDING);
return .{ .program = program };
}
pub fn deinit(self: *const Program) void {
self.program.destroy();
}
/// Bind the program for use. This should be called so that draw can
/// be called.
pub fn bind(self: *const Program) !Binding {
const program = try self.program.use();
errdefer program.unbind();
return .{
.program = program,
};
}
pub const Binding = struct {
program: gl.Program.Binding,
pub fn unbind(self: Binding) void {
self.program.unbind();
}
pub fn draw(self: Binding) !void {
_ = self;
try gl.drawElementsInstanced(
gl.c.GL_TRIANGLES,
6,
gl.c.GL_UNSIGNED_BYTE,
1,
);
}
};
};

View File

@ -3,6 +3,8 @@ const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const gl = @import("opengl");
const wuffs = @import("wuffs");
const OpenGL = @import("../OpenGL.zig");
const Texture = OpenGL.Texture;
/// Represents a single image placement on the grid. A placement is a
/// request to render an instance of an image.
@ -59,15 +61,15 @@ pub const Image = union(enum) {
replace_rgba: Replace,
/// The image is uploaded and ready to be used.
ready: gl.Texture,
ready: Texture,
/// The image is uploaded but is scheduled to be unloaded.
unload_pending: []u8,
unload_ready: gl.Texture,
unload_replace: struct { []u8, gl.Texture },
unload_ready: Texture,
unload_replace: struct { []u8, Texture },
pub const Replace = struct {
texture: gl.Texture,
texture: Texture,
pending: Pending,
};
@ -99,32 +101,32 @@ pub const Image = union(enum) {
.replace_gray => |r| {
alloc.free(r.pending.dataSlice(1));
r.texture.destroy();
r.texture.deinit();
},
.replace_gray_alpha => |r| {
alloc.free(r.pending.dataSlice(2));
r.texture.destroy();
r.texture.deinit();
},
.replace_rgb => |r| {
alloc.free(r.pending.dataSlice(3));
r.texture.destroy();
r.texture.deinit();
},
.replace_rgba => |r| {
alloc.free(r.pending.dataSlice(4));
r.texture.destroy();
r.texture.deinit();
},
.unload_replace => |r| {
alloc.free(r[0]);
r[1].destroy();
r[1].deinit();
},
.ready,
.unload_ready,
=> |tex| tex.destroy(),
=> |tex| tex.deinit(),
}
}
@ -168,7 +170,7 @@ pub const Image = union(enum) {
// Get our existing texture. This switch statement will also handle
// scenarios where there is no existing texture and we can modify
// the self pointer directly.
const existing: gl.Texture = switch (self.*) {
const existing: Texture = switch (self.*) {
// For pending, we can free the old data and become pending ourselves.
.pending_gray => |p| {
alloc.free(p.dataSlice(1));
@ -356,7 +358,10 @@ pub const Image = union(enum) {
pub fn upload(
self: *Image,
alloc: Allocator,
opengl: *const OpenGL,
) !void {
_ = opengl;
// Convert our data if we have to
try self.convert(alloc);
@ -374,23 +379,15 @@ pub const Image = union(enum) {
};
// Create our texture
const tex = try gl.Texture.create();
errdefer tex.destroy();
const texbind = try tex.bind(.@"2D");
try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
try texbind.parameter(.MinFilter, gl.c.GL_LINEAR);
try texbind.parameter(.MagFilter, gl.c.GL_LINEAR);
try texbind.image2D(
0,
formats.internal,
const tex = try Texture.init(
.{
.format = formats.format,
.internal_format = formats.internal,
.target = .Rectangle,
},
@intCast(p.width),
@intCast(p.height),
0,
formats.format,
.UnsignedByte,
p.data,
p.data[0 .. p.width * p.height * self.depth()],
);
// Uploaded. We can now clear our data and change our state.

View File

@ -0,0 +1,310 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const math = @import("../../math.zig");
const Pipeline = @import("Pipeline.zig");
const log = std.log.scoped(.opengl);
/// This contains the state for the shaders used by the Metal renderer.
pub const Shaders = struct {
/// Renders cell foreground elements (text, decorations).
cell_text_pipeline: Pipeline,
/// The cell background shader is the shader used to render the
/// background of terminal cells.
cell_bg_pipeline: Pipeline,
/// The image shader is the shader used to render images for things
/// like the Kitty image protocol.
image_pipeline: Pipeline,
/// Custom shaders to run against the final drawable texture. This
/// can be used to apply a lot of effects. Each shader is run in sequence
/// against the output of the previous shader.
post_pipelines: []const Pipeline,
/// Set to true when deinited, if you try to deinit a defunct set
/// of shaders it will just be ignored, to prevent double-free.
defunct: bool = false,
/// Initialize our shader set.
///
/// "post_shaders" is an optional list of postprocess shaders to run
/// against the final drawable texture. This is an array of shader source
/// code, not file paths.
pub fn init(
alloc: Allocator,
post_shaders: []const [:0]const u8,
) !Shaders {
const cell_text_pipeline = try initCellTextPipeline();
errdefer cell_text_pipeline.deinit();
const cell_bg_pipeline = try initCellBgPipeline();
errdefer cell_bg_pipeline.deinit();
const image_pipeline = try initImagePipeline();
errdefer image_pipeline.deinit();
const post_pipelines: []const Pipeline = initPostPipelines(
alloc,
post_shaders,
) catch |err| err: {
// If an error happens while building postprocess shaders we
// want to just not use any postprocess shaders since we don't
// want to block Ghostty from working.
log.warn("error initializing postprocess shaders err={}", .{err});
break :err &.{};
};
errdefer if (post_pipelines.len > 0) {
for (post_pipelines) |pipeline| pipeline.deinit();
alloc.free(post_pipelines);
};
return .{
.cell_text_pipeline = cell_text_pipeline,
.cell_bg_pipeline = cell_bg_pipeline,
.image_pipeline = image_pipeline,
.post_pipelines = post_pipelines,
};
}
pub fn deinit(self: *Shaders, alloc: Allocator) void {
if (self.defunct) return;
self.defunct = true;
// Release our primary shaders
self.cell_text_pipeline.deinit();
self.cell_bg_pipeline.deinit();
self.image_pipeline.deinit();
// Release our postprocess shaders
if (self.post_pipelines.len > 0) {
for (self.post_pipelines) |pipeline| {
pipeline.deinit();
}
alloc.free(self.post_pipelines);
}
}
};
/// Single parameter for the image shader. See shader for field details.
pub const Image = extern struct {
grid_pos: [2]f32 align(8),
cell_offset: [2]f32 align(8),
source_rect: [4]f32 align(16),
dest_size: [2]f32 align(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 align(16),
/// Size of a single cell in pixels, unscaled.
cell_size: [2]f32 align(8),
/// Size of the grid in columns and rows.
grid_size: [2]u16 align(4),
/// The padding around the terminal grid in pixels. In order:
/// top, right, bottom, left.
grid_padding: [4]f32 align(16),
/// Bit mask defining which directions to
/// extend cell colors in to the padding.
/// Order, LSB first: left, right, up, down
padding_extend: PaddingExtend align(4),
/// The minimum contrast ratio for text. The contrast ratio is calculated
/// according to the WCAG 2.0 spec.
min_contrast: f32 align(4),
/// The cursor position and color.
cursor_pos: [2]u16 align(4),
cursor_color: [4]u8 align(4),
/// The background color for the whole surface.
bg_color: [4]u8 align(4),
/// Various booleans, in a packed struct for space efficiency.
bools: Bools align(4),
const Bools = packed struct(u32) {
/// Whether the cursor is 2 cells wide.
cursor_wide: bool,
/// Indicates that colors provided to the shader are already in
/// the P3 color space, so they don't need to be converted from
/// sRGB.
use_display_p3: bool,
/// Indicates that the color attachments for the shaders have
/// an `*_srgb` pixel format, which means the shaders need to
/// output linear RGB colors rather than gamma encoded colors,
/// since blending will be performed in linear space and then
/// Metal itself will re-encode the colors for storage.
use_linear_blending: bool,
/// Enables a weight correction step that makes text rendered
/// with linear alpha blending have a similar apparent weight
/// (thickness) to gamma-incorrect blending.
use_linear_correction: bool = false,
_padding: u28 = 0,
};
const PaddingExtend = packed struct(u32) {
left: bool = false,
right: bool = false,
up: bool = false,
down: bool = false,
_padding: u28 = 0,
};
};
/// The uniforms used for custom postprocess shaders.
pub const PostUniforms = extern struct {
resolution: [3]f32 align(16),
time: f32 align(4),
time_delta: f32 align(4),
frame_rate: f32 align(4),
frame: i32 align(4),
channel_time: [4][4]f32 align(16),
channel_resolution: [4][4]f32 align(16),
mouse: [4]f32 align(16),
date: [4]f32 align(16),
sample_rate: f32 align(4),
};
/// Initialize our custom shader pipelines. The shaders argument is a
/// set of shader source code, not file paths.
fn initPostPipelines(
alloc: Allocator,
shaders: []const [:0]const u8,
) ![]const Pipeline {
// If we have no shaders, do nothing.
if (shaders.len == 0) return &.{};
// Keeps track of how many shaders we successfully wrote.
var i: usize = 0;
// Initialize our result set. If any error happens, we undo everything.
var pipelines = try alloc.alloc(Pipeline, shaders.len);
errdefer {
for (pipelines[0..i]) |pipeline| {
pipeline.deinit();
}
alloc.free(pipelines);
}
// Build each shader. Note we don't use "0.." to build our index
// because we need to keep track of our length to clean up above.
for (shaders) |source| {
pipelines[i] = try initPostPipeline(source);
i += 1;
}
return pipelines;
}
/// Initialize a single custom shader pipeline from shader source.
fn initPostPipeline(data: [:0]const u8) !Pipeline {
return try Pipeline.init(null, .{
.vertex_fn = loadShaderCode("../shaders/glsl/full_screen.v.glsl"),
.fragment_fn = data,
});
}
/// This is a single parameter for the terminal cell shader.
pub const CellText = extern struct {
glyph_pos: [2]u32 align(8) = .{ 0, 0 },
glyph_size: [2]u32 align(8) = .{ 0, 0 },
bearings: [2]i16 align(4) = .{ 0, 0 },
grid_pos: [2]u16 align(4),
color: [4]u8 align(4),
mode: Mode align(4),
constraint_width: u32 align(4) = 0,
pub const Mode = enum(u32) {
fg = 1,
fg_constrained = 2,
fg_color = 3,
cursor = 4,
fg_powerline = 5,
};
// test {
// // Minimizing the size of this struct is important,
// // so we test it in order to be aware of any changes.
// try std.testing.expectEqual(32, @sizeOf(CellText));
// }
};
/// Initialize the cell render pipeline.
fn initCellTextPipeline() !Pipeline {
return try Pipeline.init(CellText, .{
.vertex_fn = loadShaderCode("../shaders/glsl/cell_text.v.glsl"),
.fragment_fn = loadShaderCode("../shaders/glsl/cell_text.f.glsl"),
.step_fn = .per_instance,
});
}
/// This is a single parameter for the cell bg shader.
pub const CellBg = [4]u8;
/// Initialize the cell background render pipeline.
fn initCellBgPipeline() !Pipeline {
return try Pipeline.init(null, .{
.vertex_fn = loadShaderCode("../shaders/glsl/full_screen.v.glsl"),
.fragment_fn = loadShaderCode("../shaders/glsl/cell_bg.f.glsl"),
});
}
/// Initialize the image render pipeline.
fn initImagePipeline() !Pipeline {
return try Pipeline.init(Image, .{
.vertex_fn = loadShaderCode("../shaders/glsl/image.v.glsl"),
.fragment_fn = loadShaderCode("../shaders/glsl/image.f.glsl"),
.step_fn = .per_instance,
});
}
/// Load shader code from the target path, processing `#include` directives.
///
/// Comptime only for now, this code is really sloppy and makes a bunch of
/// assumptions about things being well formed and file names not containing
/// quote marks. If we ever want to process `#include`s for custom shaders
/// then we need to write something better than this for it.
fn loadShaderCode(comptime path: []const u8) [:0]const u8 {
return comptime processIncludes(@embedFile(path), std.fs.path.dirname(path).?);
}
/// Used by loadShaderCode
fn processIncludes(contents: [:0]const u8, basedir: []const u8) [:0]const u8 {
@setEvalBranchQuota(100_000);
var i: usize = 0;
while (i < contents.len) {
if (std.mem.startsWith(u8, contents[i..], "#include")) {
assert(std.mem.startsWith(u8, contents[i..], "#include \""));
const start = i + "#include \"".len;
const end = std.mem.indexOfScalarPos(u8, contents, start, '"').?;
return std.fmt.comptimePrint(
"{s}{s}{s}",
.{
contents[0..i],
@embedFile(basedir ++ .{std.fs.path.sep} ++ contents[start..end]),
processIncludes(contents[end + 1 ..], basedir),
},
);
}
if (std.mem.indexOfPos(u8, contents, i, "\n#")) |j| {
i = (j + 1);
} else {
break;
}
}
return contents;
}

View File

@ -1,53 +0,0 @@
#version 330 core
in vec2 glyph_tex_coords;
flat in uint mode;
// The color for this cell. If this is a background pass this is the
// background color. Otherwise, this is the foreground color.
flat in vec4 color;
// The position of the cells top-left corner.
flat in vec2 screen_cell_pos;
// Position the fragment coordinate to the upper left
layout(origin_upper_left) in vec4 gl_FragCoord;
// Must declare this output for some versions of OpenGL.
layout(location = 0) out vec4 out_FragColor;
// Font texture
uniform sampler2D text;
uniform sampler2D text_color;
// Dimensions of the cell
uniform vec2 cell_size;
// See vertex shader
const uint MODE_BG = 1u;
const uint MODE_FG = 2u;
const uint MODE_FG_CONSTRAINED = 3u;
const uint MODE_FG_COLOR = 7u;
const uint MODE_FG_POWERLINE = 15u;
void main() {
float a;
switch (mode) {
case MODE_BG:
out_FragColor = color;
break;
case MODE_FG:
case MODE_FG_CONSTRAINED:
case MODE_FG_POWERLINE:
a = texture(text, glyph_tex_coords).r;
vec3 premult = color.rgb * color.a;
out_FragColor = vec4(premult.rgb*a, a);
break;
case MODE_FG_COLOR:
out_FragColor = texture(text_color, glyph_tex_coords);
break;
}
}

View File

@ -249,20 +249,12 @@ vertex CellBgVertexOut cell_bg_vertex(
fragment float4 cell_bg_fragment(
CellBgVertexOut in [[stage_in]],
constant uchar4 *cells [[buffer(0)]],
constant Uniforms& uniforms [[buffer(1)]]
constant Uniforms& uniforms [[buffer(1)]],
constant uchar4 *cells [[buffer(2)]]
) {
int2 grid_pos = int2(floor((in.position.xy - uniforms.grid_padding.wx) / uniforms.cell_size));
float4 bg = float4(0.0);
// If we have any background transparency then we render bg-colored cells as
// fully transparent, since the background is handled by the layer bg color
// and we don't want to double up our bg color, but if our bg color is fully
// opaque then our layer is opaque and can't handle transparency, so we need
// to return the bg color directly instead.
if (uniforms.bg_color.a == 255) {
bg = in.bg_color;
}
float4 bg = in.bg_color;
// Clamp x position, extends edge bg colors in to padding on sides.
if (grid_pos.x < 0) {
@ -374,19 +366,23 @@ vertex CellTextVertexOut cell_text_vertex(
// Convert the grid x, y into world space x, y by accounting for cell size
float2 cell_pos = uniforms.cell_size * float2(in.grid_pos);
// Turn the cell position into a vertex point depending on the
// vertex ID. Since we use instanced drawing, we have 4 vertices
// for each corner of the cell. We can use vertex ID to determine
// which one we're looking at. Using this, we can use 1 or 0 to keep
// or discard the value for the vertex.
// We use a triangle strip with 4 vertices to render quads,
// so we determine which corner of the cell this vertex is in
// based on the vertex ID.
//
// 0 = top-right
// 1 = bot-right
// 2 = bot-left
// 3 = top-left
// 0 --> 1
// | .'|
// | / |
// | L |
// 2 --> 3
//
// 0 = top-left (0, 0)
// 1 = top-right (1, 0)
// 2 = bot-left (0, 1)
// 3 = bot-right (1, 1)
float2 corner;
corner.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f;
corner.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f;
corner.x = float(vid == 1 || vid == 3);
corner.y = float(vid == 2 || vid == 3);
CellTextVertexOut out;
out.mode = in.mode;
@ -502,7 +498,7 @@ fragment float4 cell_text_fragment(
CellTextVertexOut in [[stage_in]],
texture2d<float> textureGrayscale [[texture(0)]],
texture2d<float> textureColor [[texture(1)]],
constant Uniforms& uniforms [[buffer(2)]]
constant Uniforms& uniforms [[buffer(1)]]
) {
constexpr sampler textureSampler(
coord::pixel,
@ -621,19 +617,23 @@ vertex ImageVertexOut image_vertex(
texture2d<uint> image [[texture(0)]],
constant Uniforms& uniforms [[buffer(1)]]
) {
// Turn the image position into a vertex point depending on the
// vertex ID. Since we use instanced drawing, we have 4 vertices
// for each corner of the cell. We can use vertex ID to determine
// which one we're looking at. Using this, we can use 1 or 0 to keep
// or discard the value for the vertex.
// We use a triangle strip with 4 vertices to render quads,
// so we determine which corner of the cell this vertex is in
// based on the vertex ID.
//
// 0 = top-right
// 1 = bot-right
// 2 = bot-left
// 3 = top-left
// 0 --> 1
// | .'|
// | / |
// | L |
// 2 --> 3
//
// 0 = top-left (0, 0)
// 1 = top-right (1, 0)
// 2 = bot-left (0, 1)
// 3 = bot-right (1, 1)
float2 corner;
corner.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f;
corner.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f;
corner.x = float(vid == 1 || vid == 3);
corner.y = float(vid == 2 || vid == 3);
// The texture coordinates start at our source x/y
// and add the width/height depending on the corner.

View File

@ -1,258 +0,0 @@
#version 330 core
// These are the possible modes that "mode" can be set to. This is
// used to multiplex multiple render modes into a single shader.
//
// NOTE: this must be kept in sync with the fragment shader
const uint MODE_BG = 1u;
const uint MODE_FG = 2u;
const uint MODE_FG_CONSTRAINED = 3u;
const uint MODE_FG_COLOR = 7u;
const uint MODE_FG_POWERLINE = 15u;
// The grid coordinates (x, y) where x < columns and y < rows
layout (location = 0) in vec2 grid_coord;
// Position of the glyph in the texture.
layout (location = 1) in vec2 glyph_pos;
// Width/height of the glyph
layout (location = 2) in vec2 glyph_size;
// Offset of the top-left corner of the glyph when rendered in a rect.
layout (location = 3) in vec2 glyph_offset;
// The color for this cell in RGBA (0 to 1.0). Background or foreground
// depends on mode.
layout (location = 4) in vec4 color_in;
// Only set for MODE_FG, this is the background color of the FG text.
// This is used to detect minimal contrast for the text.
layout (location = 5) in vec4 bg_color_in;
// The mode of this shader. The mode determines what fields are used,
// what the output will be, etc. This shader is capable of executing in
// multiple "modes" so that we can share some logic and so that we can draw
// the entire terminal grid in a single GPU pass.
layout (location = 6) in uint mode_in;
// The width in cells of this item.
layout (location = 7) in uint grid_width;
// The background or foreground color for the fragment, depending on
// whether this is a background or foreground pass.
flat out vec4 color;
// The x/y coordinate for the glyph representing the font.
out vec2 glyph_tex_coords;
// The position of the cell top-left corner in screen cords. z and w
// are width and height.
flat out vec2 screen_cell_pos;
// Pass the mode forward to the fragment shader.
flat out uint mode;
uniform sampler2D text;
uniform sampler2D text_color;
uniform vec2 cell_size;
uniform vec2 grid_size;
uniform vec4 grid_padding;
uniform bool padding_vertical_top;
uniform bool padding_vertical_bottom;
uniform mat4 projection;
uniform float min_contrast;
/********************************************************************
* Modes
*
*-------------------------------------------------------------------
* MODE_BG
*
* In MODE_BG, this shader renders only the background color for the
* cell. This is a simple mode where we generate a simple rectangle
* made up of 4 vertices and then it is filled. In this mode, the output
* "color" is the fill color for the bg.
*
*-------------------------------------------------------------------
* MODE_FG
*
* In MODE_FG, the shader renders the glyph onto this cell and utilizes
* the glyph texture "text". In this mode, the output "color" is the
* fg color to use for the glyph.
*
*/
//-------------------------------------------------------------------
// Color Functions
//-------------------------------------------------------------------
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
float luminance_component(float c) {
if (c <= 0.03928) {
return c / 12.92;
} else {
return pow((c + 0.055) / 1.055, 2.4);
}
}
float relative_luminance(vec3 color) {
vec3 color_adjusted = vec3(
luminance_component(color.r),
luminance_component(color.g),
luminance_component(color.b)
);
vec3 weights = vec3(0.2126, 0.7152, 0.0722);
return dot(color_adjusted, weights);
}
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
float contrast_ratio(vec3 color1, vec3 color2) {
float luminance1 = relative_luminance(color1) + 0.05;
float luminance2 = relative_luminance(color2) + 0.05;
return max(luminance1, luminance2) / min(luminance1, luminance2);
}
// Return the fg if the contrast ratio is greater than min, otherwise
// return a color that satisfies the contrast ratio. Currently, the color
// is always white or black, whichever has the highest contrast ratio.
vec4 contrasted_color(float min_ratio, vec4 fg, vec4 bg) {
vec3 fg_premult = fg.rgb * fg.a;
vec3 bg_premult = bg.rgb * bg.a;
float ratio = contrast_ratio(fg_premult, bg_premult);
if (ratio < min_ratio) {
float white_ratio = contrast_ratio(vec3(1.0, 1.0, 1.0), bg_premult);
float black_ratio = contrast_ratio(vec3(0.0, 0.0, 0.0), bg_premult);
if (white_ratio > black_ratio) {
return vec4(1.0, 1.0, 1.0, fg.a);
} else {
return vec4(0.0, 0.0, 0.0, fg.a);
}
}
return fg;
}
//-------------------------------------------------------------------
// Main
//-------------------------------------------------------------------
void main() {
// We always forward our mode unmasked because the fragment
// shader doesn't use any of the masks.
mode = mode_in;
// Top-left cell coordinates converted to world space
// Example: (1,0) with a 30 wide cell is converted to (30,0)
vec2 cell_pos = cell_size * grid_coord;
// Our Z value. For now we just use grid_z directly but we pull it
// out here so the variable name is more uniform to our cell_pos and
// in case we want to do any other math later.
float cell_z = 0.0;
// Turn the cell position into a vertex point depending on the
// gl_VertexID. Since we use instanced drawing, we have 4 vertices
// for each corner of the cell. We can use gl_VertexID to determine
// which one we're looking at. Using this, we can use 1 or 0 to keep
// or discard the value for the vertex.
//
// 0 = top-right
// 1 = bot-right
// 2 = bot-left
// 3 = top-left
vec2 position;
position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? 1. : 0.;
position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 0. : 1.;
// Scaled for wide chars
vec2 cell_size_scaled = cell_size;
cell_size_scaled.x = cell_size_scaled.x * grid_width;
switch (mode) {
case MODE_BG:
// If we're at the edge of the grid, we add our padding to the background
// to extend it. Note: grid_padding is top/right/bottom/left.
if (grid_coord.y == 0 && padding_vertical_top) {
cell_pos.y -= grid_padding.r;
cell_size_scaled.y += grid_padding.r;
} else if (grid_coord.y == grid_size.y - 1 && padding_vertical_bottom) {
cell_size_scaled.y += grid_padding.b;
}
if (grid_coord.x == 0) {
cell_pos.x -= grid_padding.a;
cell_size_scaled.x += grid_padding.a;
} else if (grid_coord.x == grid_size.x - 1) {
cell_size_scaled.x += grid_padding.g;
}
// Calculate the final position of our cell in world space.
// We have to add our cell size since our vertices are offset
// one cell up and to the left. (Do the math to verify yourself)
cell_pos = cell_pos + cell_size_scaled * position;
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
color = color_in / 255.0;
break;
case MODE_FG:
case MODE_FG_CONSTRAINED:
case MODE_FG_COLOR:
case MODE_FG_POWERLINE:
vec2 glyph_offset_calc = glyph_offset;
// The glyph_offset.y is the y bearing, a y value that when added
// to the baseline is the offset (+y is up). Our grid goes down.
// So we flip it with `cell_size.y - glyph_offset.y`.
glyph_offset_calc.y = cell_size_scaled.y - glyph_offset_calc.y;
// If this is a constrained mode, we need to constrain it!
vec2 glyph_size_calc = glyph_size;
if (mode == MODE_FG_CONSTRAINED) {
if (glyph_size.x > cell_size_scaled.x) {
float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x);
glyph_offset_calc.y = glyph_offset_calc.y + ((glyph_size.y - new_y) / 2);
glyph_size_calc.y = new_y;
glyph_size_calc.x = cell_size_scaled.x;
}
}
// Calculate the final position of the cell.
cell_pos = cell_pos + (glyph_size_calc * position) + glyph_offset_calc;
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
// We need to convert our texture position and size to normalized
// device coordinates (0 to 1.0) by dividing by the size of the texture.
ivec2 text_size;
switch(mode) {
case MODE_FG_CONSTRAINED:
case MODE_FG_POWERLINE:
case MODE_FG:
text_size = textureSize(text, 0);
break;
case MODE_FG_COLOR:
text_size = textureSize(text_color, 0);
break;
}
vec2 glyph_tex_pos = glyph_pos / text_size;
vec2 glyph_tex_size = glyph_size / text_size;
glyph_tex_coords = glyph_tex_pos + glyph_tex_size * position;
// If we have a minimum contrast, we need to check if we need to
// change the color of the text to ensure it has enough contrast
// with the background.
// We only apply this adjustment to "normal" text with MODE_FG,
// since we want color glyphs to appear in their original color
// and Powerline glyphs to be unaffected (else parts of the line would
// have different colors as some parts are displayed via background colors).
vec4 color_final = color_in / 255.0;
if (min_contrast > 1.0 && mode == MODE_FG) {
vec4 bg_color = bg_color_in / 255.0;
color_final = contrasted_color(min_contrast, color_final, bg_color);
}
color = color_final;
break;
}
}

View File

@ -1,8 +0,0 @@
#version 330 core
void main(){
vec2 position;
position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? -1. : 1.;
position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 1. : -1.;
gl_Position = vec4(position.xy, 0.0f, 1.0f);
}

View File

@ -0,0 +1,61 @@
#include "common.glsl"
// Position the origin to the upper left
layout(origin_upper_left, pixel_center_integer) in vec4 gl_FragCoord;
// Must declare this output for some versions of OpenGL.
layout(location = 0) out vec4 out_FragColor;
layout(binding = 1, std430) readonly buffer bg_cells {
uint cells[];
};
vec4 cell_bg() {
uvec2 grid_size = unpack2u16(grid_size_packed_2u16);
ivec2 grid_pos = ivec2(floor((gl_FragCoord.xy - grid_padding.wx) / cell_size));
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
vec4 bg = load_color(unpack4u8(bg_color_packed_4u8), use_linear_blending);
// Clamp x position, extends edge bg colors in to padding on sides.
if (grid_pos.x < 0) {
if ((padding_extend & EXTEND_LEFT) != 0) {
grid_pos.x = 0;
} else {
return bg;
}
} else if (grid_pos.x > grid_size.x - 1) {
if ((padding_extend & EXTEND_RIGHT) != 0) {
grid_pos.x = int(grid_size.x) - 1;
} else {
return bg;
}
}
// Clamp y position if we should extend, otherwise discard if out of bounds.
if (grid_pos.y < 0) {
if ((padding_extend & EXTEND_UP) != 0) {
grid_pos.y = 0;
} else {
return bg;
}
} else if (grid_pos.y > grid_size.y - 1) {
if ((padding_extend & EXTEND_DOWN) != 0) {
grid_pos.y = int(grid_size.y) - 1;
} else {
return bg;
}
}
// Load the color for the cell.
vec4 cell_color = load_color(
unpack4u8(cells[grid_pos.y * grid_size.x + grid_pos.x]),
use_linear_blending
);
return cell_color;
}
void main() {
out_FragColor = cell_bg();
}

View File

@ -0,0 +1,109 @@
#include "common.glsl"
layout(binding = 0) uniform sampler2DRect atlas_grayscale;
layout(binding = 1) uniform sampler2DRect atlas_color;
in CellTextVertexOut {
flat uint mode;
flat vec4 color;
flat vec4 bg_color;
vec2 tex_coord;
} in_data;
// These are the possible modes that "mode" can be set to. This is
// used to multiplex multiple render modes into a single shader.
//
// NOTE: this must be kept in sync with the fragment shader
const uint MODE_TEXT = 1u;
const uint MODE_TEXT_CONSTRAINED = 2u;
const uint MODE_TEXT_COLOR = 3u;
const uint MODE_TEXT_CURSOR = 4u;
const uint MODE_TEXT_POWERLINE = 5u;
// Must declare this output for some versions of OpenGL.
layout(location = 0) out vec4 out_FragColor;
void main() {
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
bool use_linear_correction = (bools & USE_LINEAR_CORRECTION) != 0;
switch (in_data.mode) {
default:
case MODE_TEXT_CURSOR:
case MODE_TEXT_CONSTRAINED:
case MODE_TEXT_POWERLINE:
case MODE_TEXT:
{
// Our input color is always linear.
vec4 color = in_data.color;
// If we're not doing linear blending, then we need to
// re-apply the gamma encoding to our color manually.
//
// Since the alpha is premultiplied, we need to divide
// it out before unlinearizing and re-multiply it after.
if (!use_linear_blending) {
color.rgb /= vec3(color.a);
color = unlinearize(color);
color.rgb *= vec3(color.a);
}
// Fetch our alpha mask for this pixel.
float a = texture(atlas_grayscale, in_data.tex_coord).r;
// Linear blending weight correction corrects the alpha value to
// produce blending results which match gamma-incorrect blending.
if (use_linear_correction) {
// Short explanation of how this works:
//
// We get the luminances of the foreground and background colors,
// and then unlinearize them and perform blending on them. This
// gives us our desired luminance, which we derive our new alpha
// value from by mapping the range [bg_l, fg_l] to [0, 1], since
// our final blend will be a linear interpolation from bg to fg.
//
// This yields virtually identical results for grayscale blending,
// and very similar but non-identical results for color blending.
vec4 bg = in_data.bg_color;
float fg_l = luminance(color.rgb);
float bg_l = luminance(bg.rgb);
// To avoid numbers going haywire, we don't apply correction
// when the bg and fg luminances are within 0.001 of each other.
if (abs(fg_l - bg_l) > 0.001) {
float blend_l = linearize(unlinearize(fg_l) * a + unlinearize(bg_l) * (1.0 - a));
a = clamp((blend_l - bg_l) / (fg_l - bg_l), 0.0, 1.0);
}
}
// Multiply our whole color by the alpha mask.
// Since we use premultiplied alpha, this is
// the correct way to apply the mask.
color *= a;
out_FragColor = color;
return;
}
case MODE_TEXT_COLOR:
{
// For now, we assume that color glyphs
// are already premultiplied sRGB colors.
vec4 color = texture(atlas_color, in_data.tex_coord);
// If we aren't doing linear blending, we can return this right away.
if (!use_linear_blending) {
out_FragColor = color;
return;
}
// Otherwise we need to linearize the color. Since the alpha is
// premultiplied, we need to divide it out before linearizing.
color.rgb /= vec3(color.a);
color = linearize(color);
color.rgb *= vec3(color.a);
out_FragColor = color;
return;
}
}
}

View File

@ -0,0 +1,162 @@
#include "common.glsl"
// The position of the glyph in the texture (x, y)
layout(location = 0) in uvec2 glyph_pos;
// The size of the glyph in the texture (w, h)
layout(location = 1) in uvec2 glyph_size;
// The left and top bearings for the glyph (x, y)
layout(location = 2) in ivec2 bearings;
// The grid coordinates (x, y) where x < columns and y < rows
layout(location = 3) in uvec2 grid_pos;
// The color of the rendered text glyph.
layout(location = 4) in uvec4 color;
// The mode for this cell.
layout(location = 5) in uint mode;
// The width to constrain the glyph to, in cells, or 0 for no constraint.
layout(location = 6) in uint constraint_width;
// These are the possible modes that "mode" can be set to. This is
// used to multiplex multiple render modes into a single shader.
const uint MODE_TEXT = 1u;
const uint MODE_TEXT_CONSTRAINED = 2u;
const uint MODE_TEXT_COLOR = 3u;
const uint MODE_TEXT_CURSOR = 4u;
const uint MODE_TEXT_POWERLINE = 5u;
out CellTextVertexOut {
flat uint mode;
flat vec4 color;
flat vec4 bg_color;
vec2 tex_coord;
} out_data;
layout(binding = 1, std430) readonly buffer bg_cells {
uint bg_colors[];
};
void main() {
uvec2 grid_size = unpack2u16(grid_size_packed_2u16);
uvec2 cursor_pos = unpack2u16(cursor_pos_packed_2u16);
bool cursor_wide = (bools & CURSOR_WIDE) != 0;
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
// Convert the grid x, y into world space x, y by accounting for cell size
vec2 cell_pos = cell_size * vec2(grid_pos);
int vid = gl_VertexID;
// We use a triangle strip with 4 vertices to render quads,
// so we determine which corner of the cell this vertex is in
// based on the vertex ID.
//
// 0 --> 1
// | .'|
// | / |
// | L |
// 2 --> 3
//
// 0 = top-left (0, 0)
// 1 = top-right (1, 0)
// 2 = bot-left (0, 1)
// 3 = bot-right (1, 1)
vec2 corner;
corner.x = float(vid == 1 || vid == 3);
corner.y = float(vid == 2 || vid == 3);
out_data.mode = mode;
// === Grid Cell ===
// +X
// 0,0--...->
// |
// . offset.x = bearings.x
// +Y. .|.
// . | |
// | cell_pos -> +-------+ _.
// v ._| |_. _|- offset.y = cell_size.y - bearings.y
// | | .###. | |
// | | #...# | |
// glyph_size.y -+ | ##### | |
// | | #.... | +- bearings.y
// |_| .#### | |
// | |_|
// +-------+
// |_._|
// |
// glyph_size.x
//
// In order to get the top left of the glyph, we compute an offset based on
// the bearings. The Y bearing is the distance from the bottom of the cell
// to the top of the glyph, so we subtract it from the cell height to get
// the y offset. The X bearing is the distance from the left of the cell
// to the left of the glyph, so it works as the x offset directly.
vec2 size = vec2(glyph_size);
vec2 offset = vec2(bearings);
offset.y = cell_size.y - offset.y;
// If we're constrained then we need to scale the glyph.
if (mode == MODE_TEXT_CONSTRAINED) {
float max_width = cell_size.x * constraint_width;
// If this glyph is wider than the constraint width,
// fit it to the width and remove its horizontal offset.
if (size.x > max_width) {
float new_y = size.y * (max_width / size.x);
offset.y += (size.y - new_y) / 2.0;
offset.x = 0.0;
size.y = new_y;
size.x = max_width;
} else if (max_width - size.x > offset.x) {
// However, if it does fit in the constraint width, make
// sure the offset is small enough to not push it over the
// right edge of the constraint width.
offset.x = max_width - size.x;
}
}
// Calculate the final position of the cell which uses our glyph size
// and glyph offset to create the correct bounding box for the glyph.
cell_pos = cell_pos + size * corner + offset;
gl_Position = projection_matrix * vec4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
// Calculate the texture coordinate in pixels. This is NOT normalized
// (between 0.0 and 1.0), and does not need to be, since the texture will
// be sampled with pixel coordinate mode.
out_data.tex_coord = vec2(glyph_pos) + vec2(glyph_size) * corner;
// Get our color. We always fetch a linearized version to
// make it easier to handle minimum contrast calculations.
out_data.color = load_color(color, true);
// Get the BG color
out_data.bg_color = load_color(
unpack4u8(bg_colors[grid_pos.y * grid_size.x + grid_pos.x]),
true
);
// If we have a minimum contrast, we need to check if we need to
// change the color of the text to ensure it has enough contrast
// with the background.
// We only apply this adjustment to "normal" text with MODE_TEXT,
// since we want color glyphs to appear in their original color
// and Powerline glyphs to be unaffected (else parts of the line would
// have different colors as some parts are displayed via background colors).
if (min_contrast > 1.0f && mode == MODE_TEXT) {
// Ensure our minimum contrast
out_data.color = contrasted_color(min_contrast, out_data.color, out_data.bg_color);
}
// Check if current position is under cursor (including wide cursor)
bool is_cursor_pos = ((grid_pos.x == cursor_pos.x) || (cursor_wide && (grid_pos.x == (cursor_pos.x + 1)))) && (grid_pos.y == cursor_pos.y);
// If this cell is the cursor cell, then we need to change the color.
if (mode != MODE_TEXT_CURSOR && is_cursor_pos) {
out_data.color = load_color(unpack4u8(cursor_color_packed_4u8), use_linear_blending);
}
}

View File

@ -0,0 +1,155 @@
#version 430 core
// These are common definitions to be shared across shaders, the first
// line of any shader that needs these should be `#include "common.glsl"`.
//
// Included in this file are:
// - The interface block for the global uniforms.
// - Functions for unpacking values.
// - Functions for working with colors.
//----------------------------------------------------------------------------//
// Global Uniforms
//----------------------------------------------------------------------------//
layout(binding = 1, std140) uniform Globals {
uniform mat4 projection_matrix;
uniform vec2 cell_size;
uniform uint grid_size_packed_2u16;
uniform vec4 grid_padding;
uniform uint padding_extend;
uniform float min_contrast;
uniform uint cursor_pos_packed_2u16;
uniform uint cursor_color_packed_4u8;
uniform uint bg_color_packed_4u8;
uniform uint bools;
};
// Bools
const uint CURSOR_WIDE = 1u;
const uint USE_DISPLAY_P3 = 2u;
const uint USE_LINEAR_BLENDING = 4u;
const uint USE_LINEAR_CORRECTION = 8u;
// Padding extend enum
const uint EXTEND_LEFT = 1u;
const uint EXTEND_RIGHT = 2u;
const uint EXTEND_UP = 4u;
const uint EXTEND_DOWN = 8u;
//----------------------------------------------------------------------------//
// Functions for Unpacking Values
//----------------------------------------------------------------------------//
// NOTE: These unpack functions assume little-endian.
// If this ever becomes a problem... oh dear!
uvec4 unpack4u8(uint packed_value) {
return uvec4(
uint(packed_value >> 0) & uint(0xFF),
uint(packed_value >> 8) & uint(0xFF),
uint(packed_value >> 16) & uint(0xFF),
uint(packed_value >> 24) & uint(0xFF)
);
}
uvec2 unpack2u16(uint packed_value) {
return uvec2(
uint(packed_value >> 0) & uint(0xFFFF),
uint(packed_value >> 16) & uint(0xFFFF)
);
}
ivec2 unpack2i16(int packed_value) {
return ivec2(
(packed_value << 16) >> 16,
(packed_value << 0) >> 16
);
}
//----------------------------------------------------------------------------//
// Color Functions
//----------------------------------------------------------------------------//
// Compute the luminance of the provided color.
//
// Takes colors in linear RGB space. If your colors are gamma
// encoded, linearize them before using them with this function.
float luminance(vec3 color) {
return dot(color, vec3(0.2126f, 0.7152f, 0.0722f));
}
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
//
// Takes colors in linear RGB space. If your colors are gamma
// encoded, linearize them before using them with this function.
float contrast_ratio(vec3 color1, vec3 color2) {
float luminance1 = luminance(color1) + 0.05;
float luminance2 = luminance(color2) + 0.05;
return max(luminance1, luminance2) / min(luminance1, luminance2);
}
// Return the fg if the contrast ratio is greater than min, otherwise
// return a color that satisfies the contrast ratio. Currently, the color
// is always white or black, whichever has the highest contrast ratio.
//
// Takes colors in linear RGB space. If your colors are gamma
// encoded, linearize them before using them with this function.
vec4 contrasted_color(float min_ratio, vec4 fg, vec4 bg) {
float ratio = contrast_ratio(fg.rgb, bg.rgb);
if (ratio < min_ratio) {
float white_ratio = contrast_ratio(vec3(1.0, 1.0, 1.0), bg.rgb);
float black_ratio = contrast_ratio(vec3(0.0, 0.0, 0.0), bg.rgb);
if (white_ratio > black_ratio) {
return vec4(1.0);
} else {
return vec4(0.0);
}
}
return fg;
}
// Converts a color from sRGB gamma encoding to linear.
vec4 linearize(vec4 srgb) {
bvec3 cutoff = lessThanEqual(srgb.rgb, vec3(0.04045));
vec3 higher = pow((srgb.rgb + vec3(0.055)) / vec3(1.055), vec3(2.4));
vec3 lower = srgb.rgb / vec3(12.92);
return vec4(mix(higher, lower, cutoff), srgb.a);
}
float linearize(float v) {
return v <= 0.04045 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4);
}
// Converts a color from linear to sRGB gamma encoding.
vec4 unlinearize(vec4 linear) {
bvec3 cutoff = lessThanEqual(linear.rgb, vec3(0.0031308));
vec3 higher = pow(linear.rgb, vec3(1.0 / 2.4)) * vec3(1.055) - vec3(0.055);
vec3 lower = linear.rgb * vec3(12.92);
return vec4(mix(higher, lower, cutoff), linear.a);
}
float unlinearize(float v) {
return v <= 0.0031308 ? v * 12.92 : pow(v, 1.0 / 2.4) * 1.055 - 0.055;
}
// Load a 4 byte RGBA non-premultiplied color and linearize
// and convert it as necessary depending on the provided info.
//
// `linear` controls whether the returned color is linear or gamma encoded.
vec4 load_color(
uvec4 in_color,
bool linear
) {
// 0 .. 255 -> 0.0 .. 1.0
vec4 color = vec4(in_color) / vec4(255.0f);
// Linearize if necessary.
if (linear) color = linearize(color);
// Premultiply our color by its alpha.
color.rgb *= color.a;
return color;
}
//----------------------------------------------------------------------------//

View File

@ -0,0 +1,24 @@
#version 330 core
void main() {
vec4 position;
position.x = (gl_VertexID == 2) ? 3.0 : -1.0;
position.y = (gl_VertexID == 0) ? -3.0 : 1.0;
position.z = 1.0;
position.w = 1.0;
// Single triangle is clipped to viewport.
//
// X <- vid == 0: (-1, -3)
// |\
// | \
// | \
// |###\
// |#+# \ `+` is (0, 0). `#`s are viewport area.
// |### \
// X------X <- vid == 2: (3, 1)
// ^
// vid == 1: (-1, 1)
gl_Position = position;
}

View File

@ -0,0 +1,21 @@
#include "common.glsl"
layout(binding = 0) uniform sampler2DRect image;
in vec2 tex_coord;
layout(location = 0) out vec4 out_FragColor;
void main() {
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
vec4 rgba = texture(image, tex_coord);
if (!use_linear_blending) {
rgba = unlinearize(rgba);
}
rgba.rgb *= vec3(rgba.a);
out_FragColor = rgba;
}

View File

@ -0,0 +1,46 @@
#include "common.glsl"
layout(binding = 0) uniform sampler2DRect image;
layout(location = 0) in vec2 grid_pos;
layout(location = 1) in vec2 cell_offset;
layout(location = 2) in vec4 source_rect;
layout(location = 3) in vec2 dest_size;
out vec2 tex_coord;
void main() {
int vid = gl_VertexID;
// We use a triangle strip with 4 vertices to render quads,
// so we determine which corner of the cell this vertex is in
// based on the vertex ID.
//
// 0 --> 1
// | .'|
// | / |
// | L |
// 2 --> 3
//
// 0 = top-left (0, 0)
// 1 = top-right (1, 0)
// 2 = bot-left (0, 1)
// 3 = bot-right (1, 1)
vec2 corner;
corner.x = float(vid == 1 || vid == 3);
corner.y = float(vid == 2 || vid == 3);
// The texture coordinates start at our source x/y
// and add the width/height depending on the corner.
//
// We don't need to normalize because we use pixel addressing for our sampler.
tex_coord = source_rect.xy;
tex_coord += source_rect.zw * corner;
// The position of our image starts at the top-left of the grid cell and
// adds the source rect width/height components.
vec2 image_pos = (cell_size * grid_pos) + cell_offset;
image_pos += dest_size * corner;
gl_Position = projection_matrix * vec4(image_pos.xy, 1.0, 1.0);
}

View File

@ -1,29 +0,0 @@
#version 330 core
in vec2 tex_coord;
layout(location = 0) out vec4 out_FragColor;
uniform sampler2D image;
// Converts a color from linear to sRGB gamma encoding.
vec4 unlinearize(vec4 linear) {
bvec3 cutoff = lessThan(linear.rgb, vec3(0.0031308));
vec3 higher = pow(linear.rgb, vec3(1.0/2.4)) * vec3(1.055) - vec3(0.055);
vec3 lower = linear.rgb * vec3(12.92);
return vec4(mix(higher, lower, cutoff), linear.a);
}
void main() {
vec4 color = texture(image, tex_coord);
// Our texture is stored with an sRGB internal format,
// which means that the values are linearized when we
// sample the texture, but for now we actually want to
// output the color with gamma compression, so we do
// that.
color = unlinearize(color);
out_FragColor = vec4(color.rgb * color.a, color.a);
}

View File

@ -1,44 +0,0 @@
#version 330 core
layout (location = 0) in vec2 grid_pos;
layout (location = 1) in vec2 cell_offset;
layout (location = 2) in vec4 source_rect;
layout (location = 3) in vec2 dest_size;
out vec2 tex_coord;
uniform sampler2D image;
uniform vec2 cell_size;
uniform mat4 projection;
void main() {
// The size of the image in pixels
vec2 image_size = textureSize(image, 0);
// Turn the cell position into a vertex point depending on the
// gl_VertexID. Since we use instanced drawing, we have 4 vertices
// for each corner of the cell. We can use gl_VertexID to determine
// which one we're looking at. Using this, we can use 1 or 0 to keep
// or discard the value for the vertex.
//
// 0 = top-right
// 1 = bot-right
// 2 = bot-left
// 3 = top-left
vec2 position;
position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? 1. : 0.;
position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 0. : 1.;
// The texture coordinates start at our source x/y, then add the width/height
// as enabled by our instance id, then normalize to [0, 1]
tex_coord = source_rect.xy;
tex_coord += source_rect.zw * position;
tex_coord /= image_size;
// The position of our image starts at the top-left of the grid cell and
// adds the source rect width/height components.
vec2 image_pos = (cell_size * grid_pos) + cell_offset;
image_pos += dest_size * position;
gl_Position = projection * vec4(image_pos.xy, 0, 1.0);
}

View File

@ -1,6 +1,6 @@
#version 430 core
layout(binding = 0) uniform Globals {
layout(binding = 1, std140) uniform Globals {
uniform vec3 iResolution;
uniform float iTime;
uniform float iTimeDelta;

View File

@ -205,18 +205,25 @@ pub const SpirvLog = struct {
/// Convert SPIR-V binary to MSL.
pub fn mslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 {
return try spvCross(alloc, spvcross.c.SPVC_BACKEND_MSL, spv, null);
const c = spvcross.c;
return try spvCross(alloc, spvcross.c.SPVC_BACKEND_MSL, spv, (struct {
fn setOptions(options: c.spvc_compiler_options) error{SpvcFailed}!void {
// We enable decoration binding, because we need this
// to properly locate the uniform block to index 1.
if (c.spvc_compiler_options_set_bool(
options,
c.SPVC_COMPILER_OPTION_MSL_ENABLE_DECORATION_BINDING,
c.SPVC_TRUE,
) != c.SPVC_SUCCESS) {
return error.SpvcFailed;
}
}
}).setOptions);
}
/// Convert SPIR-V binary to GLSL..
/// Convert SPIR-V binary to GLSL.
pub fn glslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 {
// Our minimum version for shadertoy shaders is OpenGL 4.2 because
// Spirv-Cross generates binding locations for uniforms which is
// only supported in OpenGL 4.2 and above.
//
// If we can figure out a way to NOT do this then we can lower this
// version.
const GLSL_VERSION = 420;
const GLSL_VERSION = 430;
const c = spvcross.c;
return try spvCross(alloc, c.SPVC_BACKEND_GLSL, spv, (struct {