mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
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:
@ -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)
|
||||
|
@ -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());
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -1 +1,2 @@
|
||||
pub extern "c" fn CFRelease(*anyopaque) void;
|
||||
pub extern "c" fn CFRetain(*anyopaque) void;
|
||||
|
8
pkg/macos/iosurface.zig
Normal file
8
pkg/macos/iosurface.zig
Normal 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());
|
||||
}
|
1
pkg/macos/iosurface/c.zig
Normal file
1
pkg/macos/iosurface/c.zig
Normal file
@ -0,0 +1 @@
|
||||
pub const c = @import("../main.zig").c;
|
136
pkg/macos/iosurface/iosurface.zig
Normal file
136
pkg/macos/iosurface/iosurface.zig
Normal 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)));
|
||||
}
|
||||
};
|
@ -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");
|
||||
|
||||
|
@ -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());
|
||||
|
171
pkg/macos/video/pixel_format.zig
Normal file
171
pkg/macos/video/pixel_format.zig
Normal 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,
|
||||
_,
|
||||
};
|
@ -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,
|
||||
_,
|
||||
};
|
||||
|
||||
|
@ -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)));
|
||||
}
|
||||
|
56
pkg/opengl/Renderbuffer.zig
Normal file
56
pkg/opengl/Renderbuffer.zig
Normal 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, ¤t);
|
||||
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();
|
||||
}
|
||||
};
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
@ -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.?();
|
||||
}
|
||||
|
@ -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
18
pkg/opengl/primitives.zig
Normal 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,
|
||||
};
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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.
|
||||
|
@ -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%,
|
||||
|
@ -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
@ -20,3 +20,6 @@ surface_mailbox: apprt.surface.Mailbox,
|
||||
|
||||
/// The apprt surface.
|
||||
rt_surface: *apprt.Surface,
|
||||
|
||||
/// The renderer thread.
|
||||
thread: *renderer.Thread,
|
||||
|
@ -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
2866
src/renderer/generic.zig
Normal file
File diff suppressed because it is too large
Load Diff
137
src/renderer/metal/Frame.zig
Normal file
137
src/renderer/metal/Frame.zig
Normal 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();
|
||||
}
|
||||
}
|
187
src/renderer/metal/IOSurfaceLayer.zig
Normal file
187
src/renderer/metal/IOSurfaceLayer.zig
Normal 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;
|
||||
}
|
203
src/renderer/metal/Pipeline.zig
Normal file
203
src/renderer/metal/Pipeline.zig
Normal 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;
|
||||
}
|
220
src/renderer/metal/RenderPass.zig
Normal file
220
src/renderer/metal/RenderPass.zig
Normal 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"), .{});
|
||||
}
|
110
src/renderer/metal/Target.zig
Normal file
110
src/renderer/metal/Target.zig
Normal 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();
|
||||
}
|
196
src/renderer/metal/Texture.zig
Normal file
196
src/renderer/metal/Texture.zig
Normal 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,
|
||||
};
|
||||
}
|
@ -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,
|
||||
|
@ -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:",
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -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"), .{});
|
||||
}
|
||||
};
|
@ -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,25 +148,30 @@ pub const Uniforms = extern struct {
|
||||
/// The background color for the whole surface.
|
||||
bg_color: [4]u8 align(4),
|
||||
|
||||
/// Whether the cursor is 2 cells wide.
|
||||
cursor_wide: bool align(1),
|
||||
/// 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),
|
||||
|
||||
/// 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 align(1),
|
||||
/// 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 align(1),
|
||||
|
||||
/// 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 align(1),
|
||||
/// 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 align(1),
|
||||
|
||||
/// 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 align(1) = false,
|
||||
/// 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 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",
|
||||
.blending_enabled = false,
|
||||
}).init(
|
||||
device,
|
||||
library,
|
||||
post_library,
|
||||
pixel_format,
|
||||
);
|
||||
.vertex_library = library,
|
||||
.fragment_library = post_library,
|
||||
.attachments = &.{
|
||||
.{
|
||||
.pixel_format = pixel_format,
|
||||
.blending_enabled = false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// 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,
|
||||
.blending_enabled = true,
|
||||
}).init(
|
||||
device,
|
||||
library,
|
||||
library,
|
||||
pixel_format,
|
||||
);
|
||||
.attachments = &.{
|
||||
.{
|
||||
.pixel_format = pixel_format,
|
||||
.blending_enabled = true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// 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",
|
||||
.blending_enabled = false,
|
||||
}).init(
|
||||
device,
|
||||
library,
|
||||
library,
|
||||
pixel_format,
|
||||
);
|
||||
.vertex_library = library,
|
||||
.fragment_library = library,
|
||||
.attachments = &.{
|
||||
.{
|
||||
.pixel_format = pixel_format,
|
||||
.blending_enabled = false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// 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,
|
||||
.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));
|
||||
}
|
||||
.attachments = &.{
|
||||
.{
|
||||
.pixel_format = pixel_format,
|
||||
.blending_enabled = true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fn checkError(err_: ?*anyopaque) !void {
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
75
src/renderer/opengl/Frame.zig
Normal file
75
src/renderer/opengl/Frame.zig
Normal 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);
|
||||
}
|
@ -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();
|
||||
}
|
||||
};
|
170
src/renderer/opengl/Pipeline.zig
Normal file
170
src/renderer/opengl/Pipeline.zig
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
141
src/renderer/opengl/RenderPass.zig
Normal file
141
src/renderer/opengl/RenderPass.zig
Normal 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();
|
||||
}
|
62
src/renderer/opengl/Target.zig
Normal file
62
src/renderer/opengl/Target.zig
Normal 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();
|
||||
}
|
99
src/renderer/opengl/Texture.zig
Normal file
99
src/renderer/opengl/Texture.zig
Normal 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,
|
||||
);
|
||||
}
|
||||
|
127
src/renderer/opengl/buffer.zig
Normal file
127
src/renderer/opengl/buffer.zig
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
220
src/renderer/opengl/cell.zig
Normal file
220
src/renderer/opengl/cell.zig
Normal 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();
|
||||
}
|
||||
};
|
@ -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,
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
@ -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.
|
||||
|
310
src/renderer/opengl/shaders.zig
Normal file
310
src/renderer/opengl/shaders.zig
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
61
src/renderer/shaders/glsl/cell_bg.f.glsl
Normal file
61
src/renderer/shaders/glsl/cell_bg.f.glsl
Normal 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();
|
||||
}
|
109
src/renderer/shaders/glsl/cell_text.f.glsl
Normal file
109
src/renderer/shaders/glsl/cell_text.f.glsl
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
162
src/renderer/shaders/glsl/cell_text.v.glsl
Normal file
162
src/renderer/shaders/glsl/cell_text.v.glsl
Normal 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);
|
||||
}
|
||||
}
|
155
src/renderer/shaders/glsl/common.glsl
Normal file
155
src/renderer/shaders/glsl/common.glsl
Normal 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;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------//
|
24
src/renderer/shaders/glsl/full_screen.v.glsl
Normal file
24
src/renderer/shaders/glsl/full_screen.v.glsl
Normal 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;
|
||||
}
|
21
src/renderer/shaders/glsl/image.f.glsl
Normal file
21
src/renderer/shaders/glsl/image.f.glsl
Normal 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;
|
||||
}
|
46
src/renderer/shaders/glsl/image.v.glsl
Normal file
46
src/renderer/shaders/glsl/image.v.glsl
Normal 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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -1,24 +1,24 @@
|
||||
#version 430 core
|
||||
|
||||
layout(binding = 0) uniform Globals {
|
||||
uniform vec3 iResolution;
|
||||
uniform float iTime;
|
||||
uniform float iTimeDelta;
|
||||
uniform float iFrameRate;
|
||||
uniform int iFrame;
|
||||
uniform float iChannelTime[4];
|
||||
uniform vec3 iChannelResolution[4];
|
||||
uniform vec4 iMouse;
|
||||
uniform vec4 iDate;
|
||||
uniform float iSampleRate;
|
||||
layout(binding = 1, std140) uniform Globals {
|
||||
uniform vec3 iResolution;
|
||||
uniform float iTime;
|
||||
uniform float iTimeDelta;
|
||||
uniform float iFrameRate;
|
||||
uniform int iFrame;
|
||||
uniform float iChannelTime[4];
|
||||
uniform vec3 iChannelResolution[4];
|
||||
uniform vec4 iMouse;
|
||||
uniform vec4 iDate;
|
||||
uniform float iSampleRate;
|
||||
};
|
||||
|
||||
layout(binding = 0) uniform sampler2D iChannel0;
|
||||
layout(binding = 0) uniform sampler2D iChannel0;
|
||||
|
||||
// These are unused currently by Ghostty:
|
||||
// layout(binding = 1) uniform sampler2D iChannel1;
|
||||
// layout(binding = 2) uniform sampler2D iChannel2;
|
||||
// layout(binding = 3) uniform sampler2D iChannel3;
|
||||
// layout(binding = 1) uniform sampler2D iChannel1;
|
||||
// layout(binding = 2) uniform sampler2D iChannel2;
|
||||
// layout(binding = 3) uniform sampler2D iChannel3;
|
||||
|
||||
layout(location = 0) in vec4 gl_FragCoord;
|
||||
layout(location = 0) out vec4 _fragColor;
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user