From 371d62a82ce26a6dd7c61d0175759f2d53771065 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 16 Jun 2025 17:44:44 -0600 Subject: [PATCH] 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. --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 9 - pkg/macos/animation.zig | 2 + pkg/macos/build.zig | 2 + pkg/macos/dispatch.zig | 10 + pkg/macos/foundation.zig | 1 + pkg/macos/foundation/type.zig | 1 + pkg/macos/iosurface.zig | 8 + pkg/macos/iosurface/c.zig | 1 + pkg/macos/iosurface/iosurface.zig | 136 + pkg/macos/main.zig | 3 + pkg/macos/video.zig | 2 + pkg/macos/video/pixel_format.zig | 171 + pkg/opengl/Buffer.zig | 7 +- pkg/opengl/Framebuffer.zig | 24 + pkg/opengl/Renderbuffer.zig | 56 + pkg/opengl/Texture.zig | 29 +- pkg/opengl/VertexArray.zig | 84 + pkg/opengl/draw.zig | 39 +- pkg/opengl/main.zig | 9 + pkg/opengl/primitives.zig | 18 + src/Surface.zig | 5 +- src/apprt/gtk/App.zig | 5 + src/apprt/gtk/Surface.zig | 20 +- src/config/Config.zig | 10 +- src/renderer.zig | 5 +- src/renderer/Metal.zig | 3441 ++--------------- src/renderer/OpenGL.zig | 2799 ++------------ src/renderer/Options.zig | 3 + src/renderer/Thread.zig | 32 +- src/renderer/generic.zig | 2866 ++++++++++++++ src/renderer/metal/Frame.zig | 137 + src/renderer/metal/IOSurfaceLayer.zig | 187 + src/renderer/metal/Pipeline.zig | 203 + src/renderer/metal/RenderPass.zig | 220 ++ src/renderer/metal/Target.zig | 110 + src/renderer/metal/Texture.zig | 196 + src/renderer/metal/api.zig | 18 +- src/renderer/metal/buffer.zig | 77 +- src/renderer/metal/image.zig | 98 +- src/renderer/metal/sampler.zig | 38 - src/renderer/metal/shaders.zig | 331 +- src/renderer/opengl/CellProgram.zig | 196 - src/renderer/opengl/Frame.zig | 75 + src/renderer/opengl/ImageProgram.zig | 134 - src/renderer/opengl/Pipeline.zig | 170 + src/renderer/opengl/RenderPass.zig | 141 + src/renderer/opengl/Target.zig | 62 + src/renderer/opengl/Texture.zig | 99 + src/renderer/opengl/buffer.zig | 127 + src/renderer/opengl/cell.zig | 220 ++ src/renderer/opengl/custom.zig | 310 -- src/renderer/opengl/image.zig | 49 +- src/renderer/opengl/shaders.zig | 310 ++ src/renderer/shaders/cell.f.glsl | 53 - src/renderer/shaders/cell.metal | 68 +- src/renderer/shaders/cell.v.glsl | 258 -- src/renderer/shaders/custom.v.glsl | 8 - src/renderer/shaders/glsl/cell_bg.f.glsl | 61 + src/renderer/shaders/glsl/cell_text.f.glsl | 109 + src/renderer/shaders/glsl/cell_text.v.glsl | 162 + src/renderer/shaders/glsl/common.glsl | 155 + src/renderer/shaders/glsl/full_screen.v.glsl | 24 + src/renderer/shaders/glsl/image.f.glsl | 21 + src/renderer/shaders/glsl/image.v.glsl | 46 + src/renderer/shaders/image.f.glsl | 29 - src/renderer/shaders/image.v.glsl | 44 - src/renderer/shaders/shadertoy_prefix.glsl | 30 +- src/renderer/shadertoy.zig | 25 +- 68 files changed, 7088 insertions(+), 7311 deletions(-) create mode 100644 pkg/macos/iosurface.zig create mode 100644 pkg/macos/iosurface/c.zig create mode 100644 pkg/macos/iosurface/iosurface.zig create mode 100644 pkg/macos/video/pixel_format.zig create mode 100644 pkg/opengl/Renderbuffer.zig create mode 100644 pkg/opengl/primitives.zig create mode 100644 src/renderer/generic.zig create mode 100644 src/renderer/metal/Frame.zig create mode 100644 src/renderer/metal/IOSurfaceLayer.zig create mode 100644 src/renderer/metal/Pipeline.zig create mode 100644 src/renderer/metal/RenderPass.zig create mode 100644 src/renderer/metal/Target.zig create mode 100644 src/renderer/metal/Texture.zig delete mode 100644 src/renderer/metal/sampler.zig delete mode 100644 src/renderer/opengl/CellProgram.zig create mode 100644 src/renderer/opengl/Frame.zig delete mode 100644 src/renderer/opengl/ImageProgram.zig create mode 100644 src/renderer/opengl/Pipeline.zig create mode 100644 src/renderer/opengl/RenderPass.zig create mode 100644 src/renderer/opengl/Target.zig create mode 100644 src/renderer/opengl/Texture.zig create mode 100644 src/renderer/opengl/buffer.zig create mode 100644 src/renderer/opengl/cell.zig delete mode 100644 src/renderer/opengl/custom.zig create mode 100644 src/renderer/opengl/shaders.zig delete mode 100644 src/renderer/shaders/cell.f.glsl delete mode 100644 src/renderer/shaders/cell.v.glsl delete mode 100644 src/renderer/shaders/custom.v.glsl create mode 100644 src/renderer/shaders/glsl/cell_bg.f.glsl create mode 100644 src/renderer/shaders/glsl/cell_text.f.glsl create mode 100644 src/renderer/shaders/glsl/cell_text.v.glsl create mode 100644 src/renderer/shaders/glsl/common.glsl create mode 100644 src/renderer/shaders/glsl/full_screen.v.glsl create mode 100644 src/renderer/shaders/glsl/image.f.glsl create mode 100644 src/renderer/shaders/glsl/image.v.glsl delete mode 100644 src/renderer/shaders/image.f.glsl delete mode 100644 src/renderer/shaders/image.v.glsl diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index a47dbdaca..7be249b1a 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -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) diff --git a/pkg/macos/animation.zig b/pkg/macos/animation.zig index 5c3c8fd30..247f97605 100644 --- a/pkg/macos/animation.zig +++ b/pkg/macos/animation.zig @@ -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()); diff --git a/pkg/macos/build.zig b/pkg/macos/build.zig index df76da9b4..3e0a97d1a 100644 --- a/pkg/macos/build.zig +++ b/pkg/macos/build.zig @@ -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); } diff --git a/pkg/macos/dispatch.zig b/pkg/macos/dispatch.zig index 2bc7e8396..3add9c0e9 100644 --- a/pkg/macos/dispatch.zig +++ b/pkg/macos/dispatch.zig @@ -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()); } diff --git a/pkg/macos/foundation.zig b/pkg/macos/foundation.zig index 85562faf0..d4f634091 100644 --- a/pkg/macos/foundation.zig +++ b/pkg/macos/foundation.zig @@ -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()); diff --git a/pkg/macos/foundation/type.zig b/pkg/macos/foundation/type.zig index e3ee150f2..45bd09054 100644 --- a/pkg/macos/foundation/type.zig +++ b/pkg/macos/foundation/type.zig @@ -1 +1,2 @@ pub extern "c" fn CFRelease(*anyopaque) void; +pub extern "c" fn CFRetain(*anyopaque) void; diff --git a/pkg/macos/iosurface.zig b/pkg/macos/iosurface.zig new file mode 100644 index 000000000..9d2e750cf --- /dev/null +++ b/pkg/macos/iosurface.zig @@ -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()); +} diff --git a/pkg/macos/iosurface/c.zig b/pkg/macos/iosurface/c.zig new file mode 100644 index 000000000..1a7d1627e --- /dev/null +++ b/pkg/macos/iosurface/c.zig @@ -0,0 +1 @@ +pub const c = @import("../main.zig").c; diff --git a/pkg/macos/iosurface/iosurface.zig b/pkg/macos/iosurface/iosurface.zig new file mode 100644 index 000000000..37f8712ba --- /dev/null +++ b/pkg/macos/iosurface/iosurface.zig @@ -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))); + } +}; diff --git a/pkg/macos/main.zig b/pkg/macos/main.zig index d094b987e..42253ba48 100644 --- a/pkg/macos/main.zig +++ b/pkg/macos/main.zig @@ -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"); diff --git a/pkg/macos/video.zig b/pkg/macos/video.zig index 0f5cbc4d6..d0b1125ab 100644 --- a/pkg/macos/video.zig +++ b/pkg/macos/video.zig @@ -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()); diff --git a/pkg/macos/video/pixel_format.zig b/pkg/macos/video/pixel_format.zig new file mode 100644 index 000000000..30f11881e --- /dev/null +++ b/pkg/macos/video/pixel_format.zig @@ -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, + _, +}; diff --git a/pkg/opengl/Buffer.zig b/pkg/opengl/Buffer.zig index 3e55410b7..609342958 100644 --- a/pkg/opengl/Buffer.zig +++ b/pkg/opengl/Buffer.zig @@ -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, _, }; diff --git a/pkg/opengl/Framebuffer.zig b/pkg/opengl/Framebuffer.zig index c5d659f98..ea1f0d2ba 100644 --- a/pkg/opengl/Framebuffer.zig +++ b/pkg/opengl/Framebuffer.zig @@ -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))); } diff --git a/pkg/opengl/Renderbuffer.zig b/pkg/opengl/Renderbuffer.zig new file mode 100644 index 000000000..ef21287f7 --- /dev/null +++ b/pkg/opengl/Renderbuffer.zig @@ -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(); + } +}; diff --git a/pkg/opengl/Texture.zig b/pkg/opengl/Texture.zig index fa5cf770b..833a9bb4d 100644 --- a/pkg/opengl/Texture.zig +++ b/pkg/opengl/Texture.zig @@ -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(); } }; diff --git a/pkg/opengl/VertexArray.zig b/pkg/opengl/VertexArray.zig index 4a6a37576..44bf31621 100644 --- a/pkg/opengl/VertexArray.zig +++ b/pkg/opengl/VertexArray.zig @@ -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(); + } }; diff --git a/pkg/opengl/draw.zig b/pkg/opengl/draw.zig index 866511c32..50110f605 100644 --- a/pkg/opengl/draw.zig +++ b/pkg/opengl/draw.zig @@ -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.?(); +} diff --git a/pkg/opengl/main.zig b/pkg/opengl/main.zig index 19cd750d0..7165ad3ab 100644 --- a/pkg/opengl/main.zig +++ b/pkg/opengl/main.zig @@ -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; diff --git a/pkg/opengl/primitives.zig b/pkg/opengl/primitives.zig new file mode 100644 index 000000000..e12f51a66 --- /dev/null +++ b/pkg/opengl/primitives.zig @@ -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, +}; diff --git a/src/Surface.zig b/src/Surface.zig index 41d40125a..a25b200f7 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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. diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 099a051a4..0299cc1ff 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -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, diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 1e5b1bfe8..cf8d651dd 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -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. diff --git a/src/config/Config.zig b/src/config/Config.zig index 2df66ba45..f7a197184 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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%, diff --git a/src/renderer.zig b/src/renderer.zig index 61d9a4e53..e3ed070b6 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -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, }; diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 639ef354b..766cbefa5 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1,547 +1,80 @@ -//! Renderer implementation for Metal. -//! -//! Open questions: -//! +//! Graphics API wrapper for Metal. pub const Metal = @This(); const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; const builtin = @import("builtin"); const glfw = @import("glfw"); const objc = @import("objc"); const macos = @import("macos"); -const imgui = @import("imgui"); -const glslang = @import("glslang"); -const xev = @import("../global.zig").xev; -const apprt = @import("../apprt.zig"); -const configpkg = @import("../config.zig"); -const font = @import("../font/main.zig"); -const os = @import("../os/main.zig"); -const terminal = @import("../terminal/main.zig"); -const renderer = @import("../renderer.zig"); -const math = @import("../math.zig"); -const Surface = @import("../Surface.zig"); -const link = @import("link.zig"); const graphics = macos.graphics; -const fgMode = @import("cell.zig").fgMode; -const isCovering = @import("cell.zig").isCovering; +const apprt = @import("../apprt.zig"); +const font = @import("../font/main.zig"); +const configpkg = @import("../config.zig"); +const rendererpkg = @import("../renderer.zig"); +const Renderer = rendererpkg.GenericRenderer(Metal); const shadertoy = @import("shadertoy.zig"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const ArenaAllocator = std.heap.ArenaAllocator; -const CFReleaseThread = os.CFReleaseThread; -const Terminal = terminal.Terminal; -const Health = renderer.Health; const mtl = @import("metal/api.zig"); -const mtl_buffer = @import("metal/buffer.zig"); -const mtl_cell = @import("metal/cell.zig"); -const mtl_image = @import("metal/image.zig"); -const mtl_sampler = @import("metal/sampler.zig"); -const mtl_shaders = @import("metal/shaders.zig"); -const Image = mtl_image.Image; -const ImageMap = mtl_image.ImageMap; -const Shaders = mtl_shaders.Shaders; +const IOSurfaceLayer = @import("metal/IOSurfaceLayer.zig"); -const ImageBuffer = mtl_buffer.Buffer(mtl_shaders.Image); -const InstanceBuffer = mtl_buffer.Buffer(u16); +pub const GraphicsAPI = Metal; +pub const Target = @import("metal/Target.zig"); +pub const Frame = @import("metal/Frame.zig"); +pub const RenderPass = @import("metal/RenderPass.zig"); +pub const Pipeline = @import("metal/Pipeline.zig"); +const bufferpkg = @import("metal/buffer.zig"); +pub const Buffer = bufferpkg.Buffer; +pub const Texture = @import("metal/Texture.zig"); +pub const shaders = @import("metal/shaders.zig"); -const ImagePlacementList = std.ArrayListUnmanaged(mtl_image.Placement); +pub const cellpkg = @import("metal/cell.zig"); +pub const imagepkg = @import("metal/image.zig"); -const DisplayLink = switch (builtin.os.tag) { - .macos => *macos.video.DisplayLink, - else => void, -}; +pub const custom_shader_target: shadertoy.Target = .msl; + +const log = std.log.scoped(.metal); // Get native API access on certain platforms so we can do more customization. const glfwNative = glfw.Native(.{ .cocoa = builtin.os.tag == .macos, }); -const log = std.log.scoped(.metal); +layer: IOSurfaceLayer, -/// Allocator that can be used -alloc: std.mem.Allocator, +/// MTLDevice +device: objc.Object, +/// MTLCommandQueue +queue: objc.Object, -/// The configuration we need derived from the main config. -config: DerivedConfig, +/// Alpha blending mode +blending: configpkg.Config.AlphaBlending, -/// The mailbox for communicating with the window. -surface_mailbox: apprt.surface.Mailbox, - -/// Current font metrics defining our grid. -grid_metrics: font.Metrics, - -/// The size of everything. -size: renderer.Size, - -/// True if the window is focused -focused: bool, - -/// The foreground color set by an OSC 10 sequence. If unset then -/// default_foreground_color is used. -foreground_color: ?terminal.color.RGB, - -/// Foreground color set in the user's config file. -default_foreground_color: terminal.color.RGB, - -/// The background color set by an OSC 11 sequence. If unset then -/// default_background_color is used. -background_color: ?terminal.color.RGB, - -/// Background color set in the user's config file. -default_background_color: terminal.color.RGB, - -/// The cursor color set by an OSC 12 sequence. If unset then -/// default_cursor_color is used. -cursor_color: ?terminal.color.RGB, - -/// Default cursor color when no color is set explicitly by an OSC 12 command. -/// This is cursor color as set in the user's config, if any. If no cursor color -/// is set in the user's config, then the cursor color is determined by the -/// current foreground color. -default_cursor_color: ?terminal.color.RGB, - -/// When `cursor_color` is null, swap the foreground and background colors of -/// the cell under the cursor for the cursor color. Otherwise, use the default -/// foreground color as the cursor color. -cursor_invert: bool, - -/// The current set of cells to render. This is rebuilt on every frame -/// but we keep this around so that we don't reallocate. Each set of -/// cells goes into a separate shader. -cells: mtl_cell.Contents, - -/// The last viewport that we based our rebuild off of. If this changes, -/// then we do a full rebuild of the cells. The pointer values in this pin -/// are NOT SAFE to read because they may be modified, freed, etc from the -/// termio thread. We treat the pointers as integers for comparison only. -cells_viewport: ?terminal.Pin = null, - -/// Set to true after rebuildCells is called. This can be used -/// to determine if any possible changes have been made to the -/// cells for the draw call. -cells_rebuilt: bool = false, - -/// The current GPU uniform values. -uniforms: mtl_shaders.Uniforms, - -/// The font structures. -font_grid: *font.SharedGrid, -font_shaper: font.Shaper, -font_shaper_cache: font.ShaperCache, - -/// The images that we may render. -images: ImageMap = .{}, -image_placements: ImagePlacementList = .{}, -image_bg_end: u32 = 0, -image_text_end: u32 = 0, -image_virtual: bool = false, - -/// Metal state -shaders: Shaders, // Compiled shaders - -/// Metal objects -layer: objc.Object, // CAMetalLayer - -/// The CVDisplayLink used to drive the rendering loop in sync -/// with the display. This is void on platforms that don't support -/// a display link. -display_link: ?DisplayLink = null, - -/// The `CGColorSpace` that represents our current terminal color space -terminal_colorspace: *graphics.ColorSpace, - -/// Custom shader state. This is only set if we have custom shaders. -custom_shader_state: ?CustomShaderState = null, - -/// Health of the last frame. Note that when we do double/triple buffering -/// this will have to be part of the frame state. -health: std.atomic.Value(Health) = .{ .raw = .healthy }, - -/// Our GPU state -gpu_state: GPUState, - -/// State we need for the GPU that is shared between all frames. -pub const GPUState = struct { - // The count of buffers we use for double/triple buffering. If - // this is one then we don't do any double+ buffering at all. This - // is comptime because there isn't a good reason to change this at - // runtime and there is a lot of complexity to support it. For comptime, - // this is useful for debugging. - const BufferCount = 3; - - /// The frame data, the current frame index, and the semaphore protecting - /// the frame data. This is used to implement double/triple/etc. buffering. - frames: [BufferCount]FrameState, - frame_index: std.math.IntFittingRange(0, BufferCount) = 0, - frame_sema: std.Thread.Semaphore = .{ .permits = BufferCount }, - - device: objc.Object, // MTLDevice - queue: objc.Object, // MTLCommandQueue - - /// This buffer is written exactly once so we can use it globally. - instance: InstanceBuffer, // MTLBuffer - - /// The default storage mode to use for resources created with our device. - /// - /// This is based on whether the device is a discrete GPU or not, since - /// discrete GPUs do not have unified memory and therefore do not support - /// the "shared" storage mode, instead we have to use the "managed" mode. - default_storage_mode: mtl.MTLResourceOptions.StorageMode, - - pub fn init() !GPUState { - const device = try chooseDevice(); - const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); - errdefer queue.release(); - - // We determine whether our device is a discrete GPU based on these: - // - We're on macOS (iOS, iPadOS, etc. are guaranteed to be integrated). - // - We're not on aarch64 (Apple Silicon, therefore integrated). - // - The device reports that it does not have unified memory. - const is_discrete = - builtin.target.os.tag == .macos and - builtin.target.cpu.arch != .aarch64 and - !device.getProperty(bool, "hasUnifiedMemory"); - - const default_storage_mode: mtl.MTLResourceOptions.StorageMode = - if (is_discrete) .managed else .shared; - - var instance = try InstanceBuffer.initFill(device, &.{ - 0, 1, 3, // Top-left triangle - 1, 2, 3, // Bottom-right triangle - }, .{ .storage_mode = default_storage_mode }); - errdefer instance.deinit(); - - var result: GPUState = .{ - .device = device, - .queue = queue, - .instance = instance, - .frames = undefined, - .default_storage_mode = default_storage_mode, - }; - - // Initialize all of our frame state. - for (&result.frames) |*frame| { - frame.* = try FrameState.init(result.device, default_storage_mode); - } - - return result; - } - - fn chooseDevice() error{NoMetalDevice}!objc.Object { - var chosen_device: ?objc.Object = null; - - switch (comptime builtin.os.tag) { - .macos => { - const devices = objc.Object.fromId(mtl.MTLCopyAllDevices()); - defer devices.release(); - - var iter = devices.iterate(); - while (iter.next()) |device| { - // We want a GPU that’s connected to a display. - if (device.getProperty(bool, "isHeadless")) continue; - chosen_device = device; - // If the user has an eGPU plugged in, they probably want - // to use it. Otherwise, integrated GPUs are better for - // battery life and thermals. - if (device.getProperty(bool, "isRemovable") or - device.getProperty(bool, "isLowPower")) break; - } - }, - .ios => { - chosen_device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice()); - }, - else => @compileError("unsupported target for Metal"), - } - - const device = chosen_device orelse return error.NoMetalDevice; - return device.retain(); - } - - pub fn deinit(self: *GPUState) void { - // Wait for all of our inflight draws to complete so that - // we can cleanly deinit our GPU state. - for (0..BufferCount) |_| self.frame_sema.wait(); - for (&self.frames) |*frame| frame.deinit(); - self.instance.deinit(); - self.queue.release(); - self.device.release(); - } - - /// Get the next frame state to draw to. This will wait on the - /// semaphore to ensure that the frame is available. This must - /// always be paired with a call to releaseFrame. - pub fn nextFrame(self: *GPUState) *FrameState { - self.frame_sema.wait(); - errdefer self.frame_sema.post(); - self.frame_index = (self.frame_index + 1) % BufferCount; - return &self.frames[self.frame_index]; - } - - /// This should be called when the frame has completed drawing. - pub fn releaseFrame(self: *GPUState) void { - self.frame_sema.post(); - } -}; - -/// State we need duplicated for every frame. Any state that could be -/// in a data race between the GPU and CPU while a frame is being -/// drawn should be in this struct. +/// The default storage mode to use for resources created with our device. /// -/// While a draw is in-process, we "lock" the state (via a semaphore) -/// and prevent the CPU from updating the state until Metal reports -/// that the frame is complete. -/// -/// This is used to implement double/triple buffering. -pub const FrameState = struct { - uniforms: UniformBuffer, - cells: CellTextBuffer, - cells_bg: CellBgBuffer, +/// This is based on whether the device is a discrete GPU or not, since +/// discrete GPUs do not have unified memory and therefore do not support +/// the "shared" storage mode, instead we have to use the "managed" mode. +default_storage_mode: mtl.MTLResourceOptions.StorageMode, - grayscale: objc.Object, // MTLTexture - grayscale_modified: usize = 0, - color: objc.Object, // MTLTexture - color_modified: usize = 0, - - /// A buffer containing the uniform data. - const UniformBuffer = mtl_buffer.Buffer(mtl_shaders.Uniforms); - const CellBgBuffer = mtl_buffer.Buffer(mtl_shaders.CellBg); - const CellTextBuffer = mtl_buffer.Buffer(mtl_shaders.CellText); - - pub fn init( - device: objc.Object, - /// Storage mode for buffers and textures. - storage_mode: mtl.MTLResourceOptions.StorageMode, - ) !FrameState { - // Uniform buffer contains exactly 1 uniform struct. The - // uniform data will be undefined so this must be set before - // a frame is drawn. - var uniforms = try UniformBuffer.init( - device, - 1, - .{ - // Indicate that the CPU writes to this resource but never reads it. - .cpu_cache_mode = .write_combined, - .storage_mode = storage_mode, - }, - ); - errdefer uniforms.deinit(); - - // Create the buffers for our vertex data. The preallocation size - // is likely too small but our first frame update will resize it. - var cells = try CellTextBuffer.init( - device, - 10 * 10, - .{ - // Indicate that the CPU writes to this resource but never reads it. - .cpu_cache_mode = .write_combined, - .storage_mode = storage_mode, - }, - ); - errdefer cells.deinit(); - var cells_bg = try CellBgBuffer.init( - device, - 10 * 10, - .{ - // Indicate that the CPU writes to this resource but never reads it. - .cpu_cache_mode = .write_combined, - .storage_mode = storage_mode, - }, - ); - - errdefer cells_bg.deinit(); - - // Initialize our textures for our font atlas. - const grayscale = try initAtlasTexture(device, &.{ - .data = undefined, - .size = 8, - .format = .grayscale, - }, storage_mode); - errdefer grayscale.release(); - const color = try initAtlasTexture(device, &.{ - .data = undefined, - .size = 8, - .format = .rgba, - }, storage_mode); - errdefer color.release(); - - return .{ - .uniforms = uniforms, - .cells = cells, - .cells_bg = cells_bg, - .grayscale = grayscale, - .color = color, - }; - } - - pub fn deinit(self: *FrameState) void { - self.uniforms.deinit(); - self.cells.deinit(); - self.cells_bg.deinit(); - self.grayscale.release(); - self.color.release(); - } -}; - -pub const CustomShaderState = struct { - /// When we have a custom shader state, we maintain a front - /// and back texture which we use as a swap chain to render - /// between when multiple custom shaders are defined. - front_texture: objc.Object, // MTLTexture - back_texture: objc.Object, // MTLTexture - - sampler: mtl_sampler.Sampler, - uniforms: mtl_shaders.PostUniforms, - - /// 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, - - /// Swap the front and back textures. - pub fn swap(self: *CustomShaderState) void { - std.mem.swap(objc.Object, &self.front_texture, &self.back_texture); - } - - pub fn deinit(self: *CustomShaderState) void { - self.front_texture.release(); - self.back_texture.release(); - self.sampler.deinit(); - } -}; - -/// The configuration for this renderer that is derived from the main -/// configuration. This must be exported so that we don't need to -/// pass around Config pointers which makes memory management a pain. -pub const DerivedConfig = struct { - arena: ArenaAllocator, - - font_thicken: bool, - font_thicken_strength: u8, - font_features: std.ArrayListUnmanaged([:0]const u8), - font_styles: font.CodepointResolver.StyleStatus, - cursor_color: ?terminal.color.RGB, - cursor_invert: bool, - cursor_opacity: f64, - cursor_text: ?terminal.color.RGB, - background: terminal.color.RGB, - background_opacity: f64, - foreground: terminal.color.RGB, - selection_background: ?terminal.color.RGB, - selection_foreground: ?terminal.color.RGB, - invert_selection_fg_bg: bool, - bold_is_bright: bool, - min_contrast: f32, - padding_color: configpkg.WindowPaddingColor, - custom_shaders: configpkg.RepeatablePath, - links: link.Set, - vsync: bool, - colorspace: configpkg.Config.WindowColorspace, - blending: configpkg.Config.AlphaBlending, - - pub fn init( - alloc_gpa: Allocator, - config: *const configpkg.Config, - ) !DerivedConfig { - var arena = ArenaAllocator.init(alloc_gpa); - errdefer arena.deinit(); - const alloc = arena.allocator(); - - // Copy our shaders - const custom_shaders = try config.@"custom-shader".clone(alloc); - - // Copy our font features - const font_features = try config.@"font-feature".clone(alloc); - - // Get our font styles - var font_styles = font.CodepointResolver.StyleStatus.initFill(true); - font_styles.set(.bold, config.@"font-style-bold" != .false); - font_styles.set(.italic, config.@"font-style-italic" != .false); - font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); - - // Our link configs - const links = try link.Set.fromConfig( - alloc, - config.link.links.items, - ); - - const cursor_invert = config.@"cursor-invert-fg-bg"; - - return .{ - .background_opacity = @max(0, @min(1, config.@"background-opacity")), - .font_thicken = config.@"font-thicken", - .font_thicken_strength = config.@"font-thicken-strength", - .font_features = font_features.list, - .font_styles = font_styles, - - .cursor_color = if (!cursor_invert and config.@"cursor-color" != null) - config.@"cursor-color".?.toTerminalRGB() - else - null, - - .cursor_invert = cursor_invert, - - .cursor_text = if (config.@"cursor-text") |txt| - txt.toTerminalRGB() - else - null, - - .cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")), - - .background = config.background.toTerminalRGB(), - .foreground = config.foreground.toTerminalRGB(), - .invert_selection_fg_bg = config.@"selection-invert-fg-bg", - .bold_is_bright = config.@"bold-is-bright", - .min_contrast = @floatCast(config.@"minimum-contrast"), - .padding_color = config.@"window-padding-color", - - .selection_background = if (config.@"selection-background") |bg| - bg.toTerminalRGB() - else - null, - - .selection_foreground = if (config.@"selection-foreground") |bg| - bg.toTerminalRGB() - else - null, - - .custom_shaders = custom_shaders, - .links = links, - .vsync = config.@"window-vsync", - .colorspace = config.@"window-colorspace", - .blending = config.@"alpha-blending", - .arena = arena, - }; - } - - pub fn deinit(self: *DerivedConfig) void { - const alloc = self.arena.allocator(); - self.links.deinit(alloc); - self.arena.deinit(); - } -}; - -/// Returns the hints that we want for this -pub fn glfwWindowHints(config: *const configpkg.Config) glfw.Window.Hints { - return .{ - .client_api = .no_api, - .transparent_framebuffer = config.@"background-opacity" < 1, +pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal { + comptime switch (builtin.os.tag) { + .macos, .ios => {}, + else => @compileError("unsupported platform for Metal"), }; -} -/// This is called early right after window creation to setup our -/// window surface as necessary. -pub fn surfaceInit(surface: *apprt.Surface) !void { - _ = surface; + _ = alloc; - // We don't do anything else here because we want to set everything - // else up during actual initialization. -} + // Choose our MTLDevice and create a MTLCommandQueue for that device. + const device = try chooseDevice(); + errdefer device.release(); + const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); + errdefer queue.release(); + + const default_storage_mode: mtl.MTLResourceOptions.StorageMode = + if (device.getProperty(bool, "hasUnifiedMemory")) .shared else .managed; -pub fn init(alloc: Allocator, options: renderer.Options) !Metal { const ViewInfo = struct { view: objc.Object, scaleFactor: f64, @@ -553,7 +86,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { // Everything in glfw is window-oriented so we grab the backing // window, then derive everything from that. const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow( - options.rt_surface.window, + opts.rt_surface.window, ).?); const contentView = objc.Object.fromId( @@ -571,8 +104,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }, apprt.embedded => .{ - .scaleFactor = @floatCast(options.rt_surface.content_scale.x), - .view = switch (options.rt_surface.platform) { + .scaleFactor = @floatCast(opts.rt_surface.content_scale.x), + .view = switch (opts.rt_surface.platform) { .macos => |v| v.nsview, .ios => |v| v.uiview, }, @@ -581,2772 +114,236 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { else => @compileError("unsupported apprt for metal"), }; - // Initialize our metal stuff - var gpu_state = try GPUState.init(); - errdefer gpu_state.deinit(); + // Create an IOSurfaceLayer which we can assign to the view to make + // it in to a "layer-hosting view", so that we can manually control + // the layer contents. + var layer = try IOSurfaceLayer.init(); + errdefer layer.release(); - // Get our CAMetalLayer - const layer: objc.Object = switch (builtin.os.tag) { - .macos => layer: { - const CAMetalLayer = objc.getClass("CAMetalLayer").?; - break :layer CAMetalLayer.msgSend(objc.Object, objc.sel("layer"), .{}); + // Add our layer to the view. + // + // On macOS we do this by making the view "layer-hosting" + // by assigning it to the view's `layer` property BEFORE + // setting `wantsLayer` to `true`. + // + // On iOS, views are always layer-backed, and `layer` + // is readonly, so instead we add it as a sublayer. + switch (comptime builtin.os.tag) { + .macos => { + info.view.setProperty("layer", layer.layer.value); + info.view.setProperty("wantsLayer", true); }, - // iOS is always layer-backed so we don't need to do anything here. - .ios => info.view.getProperty(objc.Object, "layer"), + .ios => { + info.view.msgSend(void, objc.sel("addSublayer"), .{layer.layer.value}); + }, else => @compileError("unsupported target for Metal"), - }; - layer.setProperty("device", gpu_state.device.value); - layer.setProperty("opaque", options.config.background_opacity >= 1); - layer.setProperty("displaySyncEnabled", options.config.vsync); - - // Set our layer's pixel format appropriately. - layer.setProperty( - "pixelFormat", - // Using an `*_srgb` pixel format makes Metal gamma encode - // the pixels written to it *after* blending, which means - // we get linear alpha blending rather than gamma-incorrect - // blending. - if (options.config.blending.isLinear()) - @intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb) - else - @intFromEnum(mtl.MTLPixelFormat.bgra8unorm), - ); - - // Set our layer'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(); - layer.setProperty("colorspace", colorspace); - - // Create a colorspace the represents our terminal colors - // this will allow us to create e.g. `CGColor`s for things - // like the current background color. - const terminal_colorspace = try graphics.ColorSpace.createNamed( - switch (options.config.colorspace) { - .@"display-p3" => .displayP3, - .srgb => .sRGB, - }, - ); - errdefer terminal_colorspace.release(); - - // Make our view layer-backed with our Metal layer. On iOS views are - // always layer backed so we don't need to do this. But on iOS the - // caller MUST be sure to set the layerClass to CAMetalLayer. - if (comptime builtin.os.tag == .macos) { - info.view.setProperty("layer", layer.value); - info.view.setProperty("wantsLayer", true); - - // The layer gravity is set to top-left so that when we resize - // the view, the contents aren't stretched before a redraw. - layer.setProperty("contentsGravity", macos.animation.kCAGravityTopLeft); } - // Ensure that our metal layer has a content scale set to match the - // scale factor of the window. This avoids magnification issues leading - // to blurry rendering. - layer.setProperty("contentsScale", info.scaleFactor); + // Ensure that if our layer is oversized it + // does not overflow the bounds of the view. + info.view.setProperty("clipsToBounds", true); - // Create the font shaper. We initially create a shaper that can support - // a width of 160 which is a common width for modern screens to help - // avoid allocations later. - var font_shaper = try font.Shaper.init(alloc, .{ - .features = options.config.font_features.items, - }); - errdefer font_shaper.deinit(); + // Ensure that our layer has a content scale set to + // match the scale factor of the window. This avoids + // magnification issues leading to blurry rendering. + layer.layer.setProperty("contentsScale", info.scaleFactor); - // Initialize all the data that requires a critical font section. - const font_critical: struct { - metrics: font.Metrics, - } = font_critical: { - const grid = options.font_grid; - grid.lock.lockShared(); - defer grid.lock.unlockShared(); - break :font_critical .{ - .metrics = grid.metrics, - }; - }; + // This makes it so that our display callback will actually be called. + layer.layer.setProperty("needsDisplayOnBoundsChange", true); - const display_link: ?DisplayLink = switch (builtin.os.tag) { - .macos => if (options.config.vsync) - try macos.video.DisplayLink.createWithActiveCGDisplays() - else - null, - else => null, - }; - errdefer if (display_link) |v| v.release(); - - var result: Metal = .{ - .alloc = alloc, - .config = options.config, - .surface_mailbox = options.surface_mailbox, - .grid_metrics = font_critical.metrics, - .size = options.size, - .focused = true, - .foreground_color = null, - .default_foreground_color = options.config.foreground, - .background_color = null, - .default_background_color = options.config.background, - .cursor_color = null, - .default_cursor_color = options.config.cursor_color, - .cursor_invert = options.config.cursor_invert, - - // Render state - .cells = .{}, - .uniforms = .{ - .projection_matrix = undefined, - .cell_size = undefined, - .grid_size = undefined, - .grid_padding = undefined, - .padding_extend = .{}, - .min_contrast = options.config.min_contrast, - .cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) }, - .cursor_color = undefined, - .bg_color = .{ - options.config.background.r, - options.config.background.g, - options.config.background.b, - @intFromFloat(@round(options.config.background_opacity * 255.0)), - }, - .cursor_wide = false, - .use_display_p3 = options.config.colorspace == .@"display-p3", - .use_linear_blending = options.config.blending.isLinear(), - .use_linear_correction = options.config.blending == .@"linear-corrected", - }, - - // Fonts - .font_grid = options.font_grid, - .font_shaper = font_shaper, - .font_shaper_cache = font.ShaperCache.init(), - - // Shaders (initialized below) - .shaders = undefined, - - // Metal stuff + return .{ .layer = layer, - .display_link = display_link, - .terminal_colorspace = terminal_colorspace, - .custom_shader_state = null, - .gpu_state = gpu_state, + .device = device, + .queue = queue, + .blending = opts.config.blending, + .default_storage_mode = default_storage_mode, }; - - try result.initShaders(); - - // Do an initialize screen size setup to ensure our undefined values - // above are initialized. - try result.setScreenSize(result.size); - - return result; } pub fn deinit(self: *Metal) void { - self.gpu_state.deinit(); + self.queue.release(); + self.device.release(); - if (DisplayLink != void) { - if (self.display_link) |display_link| { - display_link.stop() catch {}; - display_link.release(); - } - } - - self.terminal_colorspace.release(); - - self.cells.deinit(self.alloc); - - self.font_shaper.deinit(); - self.font_shaper_cache.deinit(self.alloc); - - self.config.deinit(); - - { - var it = self.images.iterator(); - while (it.next()) |kv| kv.value_ptr.image.deinit(self.alloc); - self.images.deinit(self.alloc); - } - self.image_placements.deinit(self.alloc); - - self.deinitShaders(); - - self.* = undefined; + // NOTE: We don't release the layer here because that should be taken + // care of automatically when the hosting view is destroyed. } -fn deinitShaders(self: *Metal) void { - if (self.custom_shader_state) |*state| state.deinit(); - - self.shaders.deinit(self.alloc); +pub fn loopEnter(self: *Metal) void { + const renderer: *align(1) Renderer = @fieldParentPtr("api", self); + self.layer.setDisplayCallback( + @ptrCast(&displayCallback), + @ptrCast(renderer), + ); } -fn initShaders(self: *Metal) !void { - var arena = ArenaAllocator.init(self.alloc); - defer arena.deinit(); - const arena_alloc = arena.allocator(); - - // Load our custom shaders - const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles( - arena_alloc, - self.config.custom_shaders, - .msl, - ) catch |err| err: { - log.warn("error loading custom shaders err={}", .{err}); - break :err &.{}; +fn displayCallback(renderer: *Renderer) align(8) void { + renderer.drawFrame(true) catch |err| { + log.warn("Error drawing frame in display callback, err={}", .{err}); }; +} - var custom_shader_state: ?CustomShaderState = state: { - if (custom_shaders.len == 0) break :state null; - - // Build our sampler for our texture - var sampler = try mtl_sampler.Sampler.init(self.gpu_state.device); - errdefer sampler.deinit(); - - break :state .{ - // Resolution and screen textures will be fixed up by first - // call to setScreenSize. Draw calls will bail out early if - // the screen size hasn't been set yet, so it won't error. - .front_texture = undefined, - .back_texture = undefined, - .sampler = sampler, - .uniforms = .{ - .resolution = .{ 0, 0, 1 }, - .time = 1, - .time_delta = 1, - .frame_rate = 1, - .frame = 1, - .channel_time = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, - .channel_resolution = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, - .mouse = .{ 0, 0, 0, 0 }, - .date = .{ 0, 0, 0, 0 }, - .sample_rate = 1, - }, - - .first_frame_time = try std.time.Instant.now(), - .last_frame_time = try std.time.Instant.now(), - }; - }; - errdefer if (custom_shader_state) |*state| state.deinit(); - - var shaders = try Shaders.init( - self.alloc, - self.gpu_state.device, +pub fn initShaders( + self: *const Metal, + alloc: Allocator, + custom_shaders: []const [:0]const u8, +) !shaders.Shaders { + return try shaders.Shaders.init( + alloc, + self.device, custom_shaders, // Using an `*_srgb` pixel format makes Metal gamma encode // the pixels written to it *after* blending, which means // we get linear alpha blending rather than gamma-incorrect // blending. - if (self.config.blending.isLinear()) + if (self.blending.isLinear()) mtl.MTLPixelFormat.bgra8unorm_srgb else mtl.MTLPixelFormat.bgra8unorm, ); - errdefer shaders.deinit(self.alloc); - - self.shaders = shaders; - self.custom_shader_state = custom_shader_state; } -/// This is called just prior to spinning up the renderer thread for -/// final main thread setup requirements. -pub fn finalizeSurfaceInit(self: *Metal, surface: *apprt.Surface) !void { - _ = self; - _ = surface; - - // Metal doesn't have to do anything here. OpenGL has to do things - // like release the context but Metal doesn't have anything like that. -} - -/// Callback called by renderer.Thread when it begins. -pub fn threadEnter(self: *const Metal, surface: *apprt.Surface) !void { - _ = self; - _ = surface; - - // Metal requires no per-thread state. -} - -/// Callback called by renderer.Thread when it exits. -pub fn threadExit(self: *const Metal) void { - _ = self; - - // Metal requires no per-thread state. -} - -/// Called by renderer.Thread when it starts the main loop. -pub fn loopEnter(self: *Metal, thr: *renderer.Thread) !void { - // If we don't support a display link we have no work to do. - if (comptime DisplayLink == void) return; - - // This is when we know our "self" pointer is stable so we can - // setup the display link. To setup the display link we set our - // callback and we can start it immediately. - const display_link = self.display_link orelse return; - try display_link.setOutputCallback( - xev.Async, - &displayLinkCallback, - &thr.draw_now, - ); - display_link.start() catch {}; -} - -/// Called by renderer.Thread when it exits the main loop. -pub fn loopExit(self: *Metal) void { - // If we don't support a display link we have no work to do. - if (comptime DisplayLink == void) return; - - // Stop our display link. If this fails its okay it just means - // that we either never started it or the view its attached to - // is gone which is fine. - const display_link = self.display_link orelse return; - display_link.stop() catch {}; -} - -fn displayLinkCallback( - _: *macos.video.DisplayLink, - ud: ?*xev.Async, -) void { - const draw_now = ud orelse return; - draw_now.notify() catch |err| { - log.err("error notifying draw_now err={}", .{err}); +/// Get the current size of the runtime surface. +pub fn surfaceSize(self: *const Metal) !struct { width: u32, height: u32 } { + const bounds = self.layer.layer.getProperty(graphics.Rect, "bounds"); + const scale = self.layer.layer.getProperty(f64, "contentsScale"); + return .{ + .width = @intFromFloat(bounds.size.width * scale), + .height = @intFromFloat(bounds.size.height * scale), }; } -/// Mark the full screen as dirty so that we redraw everything. -pub fn markDirty(self: *Metal) void { - // This is how we force a full rebuild with metal. - self.cells_viewport = null; -} - -/// Called when we get an updated display ID for our display link. -pub fn setMacOSDisplayID(self: *Metal, id: u32) !void { - if (comptime DisplayLink == void) return; - const display_link = self.display_link orelse return; - log.info("updating display link display id={}", .{id}); - display_link.setCurrentCGDisplay(id) catch |err| { - log.warn("error setting display link display id err={}", .{err}); - }; -} - -/// True if our renderer has animations so that a higher frequency -/// timer is used. -pub fn hasAnimations(self: *const Metal) bool { - return self.custom_shader_state != null; -} - -/// True if our renderer is using vsync. If true, the renderer or apprt -/// is responsible for triggering draw_now calls to the render thread. That -/// is the only way to trigger a drawFrame. -pub fn hasVsync(self: *const Metal) bool { - if (comptime DisplayLink == void) return false; - const display_link = self.display_link orelse return false; - return display_link.isRunning(); -} - -/// Callback when the focus changes for the terminal this is rendering. -/// -/// Must be called on the render thread. -pub fn setFocus(self: *Metal, focus: bool) !void { - self.focused = focus; - - // If we're not focused, then we want to stop the display link - // because it is a waste of resources and we can move to pure - // change-driven updates. - if (comptime DisplayLink != void) link: { - const display_link = self.display_link orelse break :link; - if (focus) { - display_link.start() catch {}; - } else { - display_link.stop() catch {}; - } - } -} - -/// Callback when the window is visible or occluded. -/// -/// Must be called on the render thread. -pub fn setVisible(self: *Metal, visible: bool) void { - // If we're not visible, then we want to stop the display link - // because it is a waste of resources and we can move to pure - // change-driven updates. - if (comptime DisplayLink != void) link: { - const display_link = self.display_link orelse break :link; - if (visible and self.focused) { - display_link.start() catch {}; - } else { - display_link.stop() catch {}; - } - } -} - -/// Set the new font grid. -/// -/// Must be called on the render thread. -pub fn setFontGrid(self: *Metal, grid: *font.SharedGrid) void { - // Update our grid - self.font_grid = grid; - - // Update all our textures so that they sync on the next frame. - // We can modify this without a lock because the GPU does not - // touch this data. - for (&self.gpu_state.frames) |*frame| { - frame.grayscale_modified = 0; - frame.color_modified = 0; - } - - // Get our metrics from the grid. This doesn't require a lock because - // the metrics are never recalculated. - const metrics = grid.metrics; - self.grid_metrics = metrics; - - // Reset our shaper cache. If our font changed (not just the size) then - // the data in the shaper cache may be invalid and cannot be used, so we - // always clear the cache just in case. - const font_shaper_cache = font.ShaperCache.init(); - self.font_shaper_cache.deinit(self.alloc); - self.font_shaper_cache = font_shaper_cache; - - // Run a screen size update since this handles a lot of our uniforms - // that are grid size dependent and changing the font grid can change - // the grid size. - // - // If the screen size isn't set, it will be eventually so that'll call - // the setScreenSize automatically. - self.setScreenSize(self.size) catch |err| { - // The setFontGrid function can't fail but resizing our cell - // buffer definitely can fail. If it does, our renderer is probably - // screwed but let's just log it and continue until we can figure - // out a better way to handle this. - log.err("error resizing cells buffer err={}", .{err}); - }; - - // Reset our viewport to force a rebuild, since `setScreenSize` only - // does this when the number of cells changes, which isn't guaranteed. - self.cells_viewport = null; -} - -/// Update the frame data. -pub fn updateFrame( - self: *Metal, - surface: *apprt.Surface, - state: *renderer.State, - cursor_blink_visible: bool, -) !void { - _ = surface; - - // Data we extract out of the critical area. - const Critical = struct { - bg: terminal.color.RGB, - screen: terminal.Screen, - screen_type: terminal.ScreenType, - mouse: renderer.State.Mouse, - preedit: ?renderer.State.Preedit, - cursor_style: ?renderer.CursorStyle, - color_palette: terminal.color.Palette, - viewport_pin: terminal.Pin, - - /// If true, rebuild the full screen. - full_rebuild: bool, - }; - - // Update all our data as tightly as possible within the mutex. - var critical: Critical = critical: { - // const start = try std.time.Instant.now(); - // const start_micro = std.time.microTimestamp(); - // defer { - // const end = std.time.Instant.now() catch unreachable; - // // "[updateFrame critical time] \t" - // std.log.err("[updateFrame critical time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us}); - // } - - state.mutex.lock(); - defer state.mutex.unlock(); - - // If we're in a synchronized output state, we pause all rendering. - if (state.terminal.modes.get(.synchronized_output)) { - log.debug("synchronized output started, skipping render", .{}); - return; - } - - // Swap bg/fg if the terminal is reversed - const bg = self.background_color orelse self.default_background_color; - const fg = self.foreground_color orelse self.default_foreground_color; - defer { - if (self.background_color) |*c| { - c.* = bg; - } else { - self.default_background_color = bg; - } - - if (self.foreground_color) |*c| { - c.* = fg; - } else { - self.default_foreground_color = fg; - } - } - - if (state.terminal.modes.get(.reverse_colors)) { - if (self.background_color) |*c| { - c.* = fg; - } else { - self.default_background_color = fg; - } - - if (self.foreground_color) |*c| { - c.* = bg; - } else { - self.default_foreground_color = bg; - } - } - - // Get the viewport pin so that we can compare it to the current. - const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?; - - // We used to share terminal state, but we've since learned through - // analysis that it is faster to copy the terminal state than to - // hold the lock while rebuilding GPU cells. - var screen_copy = try state.terminal.screen.clone( - self.alloc, - .{ .viewport = .{} }, - null, - ); - errdefer screen_copy.deinit(); - - // Whether to draw our cursor or not. - const cursor_style = if (state.terminal.flags.password_input) - .lock +/// Initialize a new render target which can be presented by this API. +pub fn initTarget(self: *const Metal, width: usize, height: usize) !Target { + return Target.init(.{ + .device = self.device, + // Using an `*_srgb` pixel format makes Metal gamma encode the pixels + // written to it *after* blending, which means we get linear alpha + // blending rather than gamma-incorrect blending. + .pixel_format = if (self.blending.isLinear()) + .bgra8unorm_srgb else - renderer.cursorStyle( - state, - self.focused, - cursor_blink_visible, - ); - - // Get our preedit state - const preedit: ?renderer.State.Preedit = preedit: { - if (cursor_style == null) break :preedit null; - const p = state.preedit orelse break :preedit null; - break :preedit try p.clone(self.alloc); - }; - errdefer if (preedit) |p| p.deinit(self.alloc); - - // If we have Kitty graphics data, we enter a SLOW SLOW SLOW path. - // We only do this if the Kitty image state is dirty meaning only if - // it changes. - // - // If we have any virtual references, we must also rebuild our - // kitty state on every frame because any cell change can move - // an image. - if (state.terminal.screen.kitty_images.dirty or - self.image_virtual) - { - try self.prepKittyGraphics(state.terminal); - } - - // If we have any terminal dirty flags set then we need to rebuild - // the entire screen. This can be optimized in the future. - const full_rebuild: bool = rebuild: { - { - const Int = @typeInfo(terminal.Terminal.Dirty).@"struct".backing_integer.?; - const v: Int = @bitCast(state.terminal.flags.dirty); - if (v > 0) break :rebuild true; - } - { - const Int = @typeInfo(terminal.Screen.Dirty).@"struct".backing_integer.?; - const v: Int = @bitCast(state.terminal.screen.dirty); - if (v > 0) break :rebuild true; - } - - // If our viewport changed then we need to rebuild the entire - // screen because it means we scrolled. If we have no previous - // viewport then we must rebuild. - const prev_viewport = self.cells_viewport orelse break :rebuild true; - if (!prev_viewport.eql(viewport_pin)) break :rebuild true; - - break :rebuild false; - }; - - // Reset the dirty flags in the terminal and screen. We assume - // that our rebuild will be successful since so we optimize for - // success and reset while we hold the lock. This is much easier - // than coordinating row by row or as changes are persisted. - state.terminal.flags.dirty = .{}; - state.terminal.screen.dirty = .{}; - { - var it = state.terminal.screen.pages.pageIterator( - .right_down, - .{ .screen = .{} }, - null, - ); - while (it.next()) |chunk| { - var dirty_set = chunk.node.data.dirtyBitSet(); - dirty_set.unsetAll(); - } - } - - break :critical .{ - .bg = self.background_color orelse self.default_background_color, - .screen = screen_copy, - .screen_type = state.terminal.active_screen, - .mouse = state.mouse, - .preedit = preedit, - .cursor_style = cursor_style, - .color_palette = state.terminal.color_palette.colors, - .viewport_pin = viewport_pin, - .full_rebuild = full_rebuild, - }; - }; - defer { - critical.screen.deinit(); - if (critical.preedit) |p| p.deinit(self.alloc); - } - - // Build our GPU cells - try self.rebuildCells( - critical.full_rebuild, - &critical.screen, - critical.screen_type, - critical.mouse, - critical.preedit, - critical.cursor_style, - &critical.color_palette, - ); - - // Notify our shaper we're done for the frame. For some shapers like - // CoreText this triggers off-thread cleanup logic. - self.font_shaper.endFrame(); - - // Update our viewport pin - self.cells_viewport = critical.viewport_pin; - - // Update our background color - self.uniforms.bg_color = .{ - critical.bg.r, - critical.bg.g, - critical.bg.b, - @intFromFloat(@round(self.config.background_opacity * 255.0)), - }; - - // Update the background color on our layer - // - // TODO: Is this expensive? Should we be checking if our - // bg color has changed first before doing this work? - { - const color = graphics.c.CGColorCreate( - @ptrCast(self.terminal_colorspace), - &[4]f64{ - @as(f64, @floatFromInt(critical.bg.r)) / 255.0, - @as(f64, @floatFromInt(critical.bg.g)) / 255.0, - @as(f64, @floatFromInt(critical.bg.b)) / 255.0, - self.config.background_opacity, - }, - ); - defer graphics.c.CGColorRelease(color); - - // We use a CATransaction so that Core Animation knows that we - // updated the background color property. Otherwise it behaves - // weird, not updating the color until we resize. - const CATransaction = objc.getClass("CATransaction").?; - CATransaction.msgSend(void, "begin", .{}); - defer CATransaction.msgSend(void, "commit", .{}); - - self.layer.setProperty("backgroundColor", color); - } - - // Go through our images and see if we need to setup any textures. - { - var image_it = self.images.iterator(); - while (image_it.next()) |kv| { - switch (kv.value_ptr.image) { - .ready => {}, - - .pending_gray, - .pending_gray_alpha, - .pending_rgb, - .pending_rgba, - .replace_gray, - .replace_gray_alpha, - .replace_rgb, - .replace_rgba, - => try kv.value_ptr.image.upload( - self.alloc, - self.gpu_state.device, - self.gpu_state.default_storage_mode, - ), - - .unload_pending, - .unload_replace, - .unload_ready, - => { - kv.value_ptr.image.deinit(self.alloc); - self.images.removeByPtr(kv.key_ptr); - }, - } - } - } -} - -/// Draw the frame to the screen. -pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { - _ = surface; - - // If we have no cells rebuilt we can usually skip drawing since there - // is no changed data. However, if we have active animations we still - // need to draw so that we can update the time uniform and render the - // changes. - if (!self.cells_rebuilt and !self.hasAnimations()) return; - self.cells_rebuilt = false; - - // Wait for a frame to be available. - const frame = self.gpu_state.nextFrame(); - errdefer self.gpu_state.releaseFrame(); - // log.debug("drawing frame index={}", .{self.gpu_state.frame_index}); - - // Setup our frame data - try frame.uniforms.sync(self.gpu_state.device, &.{self.uniforms}); - try frame.cells_bg.sync(self.gpu_state.device, self.cells.bg_cells); - const fg_count = try frame.cells.syncFromArrayLists(self.gpu_state.device, self.cells.fg_rows.lists); - - // If we have custom shaders, update the animation time. - if (self.custom_shader_state) |*state| { - const now = std.time.Instant.now() catch state.first_frame_time; - const since_ns: f32 = @floatFromInt(now.since(state.first_frame_time)); - const delta_ns: f32 = @floatFromInt(now.since(state.last_frame_time)); - state.uniforms.time = since_ns / std.time.ns_per_s; - state.uniforms.time_delta = delta_ns / std.time.ns_per_s; - state.last_frame_time = now; - } - - // @autoreleasepool {} - const pool = objc.AutoreleasePool.init(); - defer pool.deinit(); - - // Get our drawable (CAMetalDrawable) - const drawable = self.layer.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); - - // Get our screen texture. If we don't have a dedicated screen texture - // then we just use the drawable texture. - const screen_texture = if (self.custom_shader_state) |state| - state.back_texture - else tex: { - const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{}); - break :tex objc.Object.fromId(texture); - }; - - // If our font atlas changed, sync the texture data - texture: { - const modified = self.font_grid.atlas_grayscale.modified.load(.monotonic); - if (modified <= frame.grayscale_modified) break :texture; - self.font_grid.lock.lockShared(); - defer self.font_grid.lock.unlockShared(); - frame.grayscale_modified = self.font_grid.atlas_grayscale.modified.load(.monotonic); - try syncAtlasTexture( - self.gpu_state.device, - &self.font_grid.atlas_grayscale, - &frame.grayscale, - self.gpu_state.default_storage_mode, - ); - } - texture: { - const modified = self.font_grid.atlas_color.modified.load(.monotonic); - if (modified <= frame.color_modified) break :texture; - self.font_grid.lock.lockShared(); - defer self.font_grid.lock.unlockShared(); - frame.color_modified = self.font_grid.atlas_color.modified.load(.monotonic); - try syncAtlasTexture( - self.gpu_state.device, - &self.font_grid.atlas_color, - &frame.color, - self.gpu_state.default_storage_mode, - ); - } - - // Command buffer (MTLCommandBuffer) - const buffer = self.gpu_state.queue.msgSend(objc.Object, objc.sel("commandBuffer"), .{}); - - { - // MTLRenderPassDescriptor - 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")); - { - const attachment = attachments.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 0)}, - ); - - attachment.setProperty("loadAction", @intFromEnum(mtl.MTLLoadAction.clear)); - attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store)); - attachment.setProperty("texture", screen_texture.value); - attachment.setProperty("clearColor", mtl.MTLClearColor{ - .red = 0.0, - .green = 0.0, - .blue = 0.0, - .alpha = 0.0, - }); - } - - break :desc desc; - }; - - // MTLRenderCommandEncoder - const encoder = buffer.msgSend( - objc.Object, - objc.sel("renderCommandEncoderWithDescriptor:"), - .{desc.value}, - ); - defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); - - // Draw background images first - try self.drawImagePlacements(encoder, frame, self.image_placements.items[0..self.image_bg_end]); - - // Then draw background cells - try self.drawCellBgs(encoder, frame); - - // Then draw images under text - try self.drawImagePlacements(encoder, frame, self.image_placements.items[self.image_bg_end..self.image_text_end]); - - // Then draw fg cells - try self.drawCellFgs(encoder, frame, fg_count); - - // Then draw remaining images - try self.drawImagePlacements(encoder, frame, self.image_placements.items[self.image_text_end..]); - } - - // If we have custom shaders, then we render them. - if (self.custom_shader_state) |*state| { - // MTLRenderPassDescriptor - const desc = desc: { - const MTLRenderPassDescriptor = objc.getClass("MTLRenderPassDescriptor").?; - const desc = MTLRenderPassDescriptor.msgSend( - objc.Object, - objc.sel("renderPassDescriptor"), - .{}, - ); - - break :desc desc; - }; - - // Prepare our color attachment (output). - const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); - const attachment = attachments.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 0)}, - ); - attachment.setProperty("loadAction", @intFromEnum(mtl.MTLLoadAction.clear)); - attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store)); - attachment.setProperty("clearColor", mtl.MTLClearColor{ - .red = 0, - .green = 0, - .blue = 0, - .alpha = 1, - }); - - const post_len = self.shaders.post_pipelines.len; - - for (self.shaders.post_pipelines[0 .. post_len - 1]) |pipeline| { - // Set our color attachment to be our front texture. - attachment.setProperty("texture", state.front_texture.value); - - // MTLRenderCommandEncoder - const encoder = buffer.msgSend( - objc.Object, - objc.sel("renderCommandEncoderWithDescriptor:"), - .{desc.value}, - ); - defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); - - // Draw shader - try self.drawPostShader(encoder, pipeline, state); - // Swap the front and back textures. - state.swap(); - } - - // Draw the final shader directly to the drawable. - { - // Set our color attachment to be our drawable. - // - // Texture is a property of CAMetalDrawable but if you run - // Ghostty in XCode in debug mode it returns a CaptureMTLDrawable - // which ironically doesn't implement CAMetalDrawable as a - // property so we just send a message. - const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{}); - attachment.setProperty("texture", texture); - - // MTLRenderCommandEncoder - const encoder = buffer.msgSend( - objc.Object, - objc.sel("renderCommandEncoderWithDescriptor:"), - .{desc.value}, - ); - defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); - - try self.drawPostShader( - encoder, - self.shaders.post_pipelines[post_len - 1], - state, - ); - } - } - - buffer.msgSend(void, objc.sel("presentDrawable:"), .{drawable.value}); - - // Create our block to register for completion updates. This is used - // so we can detect failures. The block is deallocated by the objC - // runtime on success. - const block = try CompletionBlock.init(.{ .self = self }, &bufferCompleted); - errdefer block.deinit(); - buffer.msgSend(void, objc.sel("addCompletedHandler:"), .{block.context}); - - buffer.msgSend(void, objc.sel("commit"), .{}); -} - -/// This is the block type used for the addCompletedHandler call.back. -const CompletionBlock = objc.Block(struct { self: *Metal }, .{ - objc.c.id, // MTLCommandBuffer -}, void); - -/// This is the callback called by the CompletionBlock invocation for -/// addCompletedHandler. -/// -/// Note: this is USUALLY called on a separate thread because the renderer -/// thread and the Apple event loop threads are usually different. Therefore, -/// we need to be mindful of thread safety here. -fn bufferCompleted( - block: *const CompletionBlock.Context, - buffer_id: objc.c.id, -) callconv(.c) void { - const self = block.self; - const buffer = objc.Object.fromId(buffer_id); - - // Get our command buffer status. If it is anything other than error - // then we don't care and just return right away. We're looking for - // errors so that we can log them. - const status = buffer.getProperty(mtl.MTLCommandBufferStatus, "status"); - const health: Health = switch (status) { - .@"error" => .unhealthy, - else => .healthy, - }; - - // If our health value hasn't changed, then we do nothing. We don't - // do a cmpxchg here because strict atomicity isn't important. - if (self.health.load(.seq_cst) != health) { - self.health.store(health, .seq_cst); - - // Our health value changed, so we notify the surface so that it - // can do something about it. - _ = self.surface_mailbox.push(.{ - .renderer_health = health, - }, .{ .forever = {} }); - } - - // Always release our semaphore - self.gpu_state.releaseFrame(); -} - -fn drawPostShader( - self: *Metal, - encoder: objc.Object, - pipeline: objc.Object, - state: *const CustomShaderState, -) !void { - _ = self; - - // Use our custom shader pipeline - encoder.msgSend( - void, - objc.sel("setRenderPipelineState:"), - .{pipeline.value}, - ); - - // Set our sampler - encoder.msgSend( - void, - objc.sel("setFragmentSamplerState:atIndex:"), - .{ state.sampler.sampler.value, @as(c_ulong, 0) }, - ); - - // Set our uniforms - encoder.msgSend( - void, - objc.sel("setFragmentBytes:length:atIndex:"), - .{ - @as(*const anyopaque, @ptrCast(&state.uniforms)), - @as(c_ulong, @sizeOf(@TypeOf(state.uniforms))), - @as(c_ulong, 0), - }, - ); - - // Screen texture - encoder.msgSend( - void, - objc.sel("setFragmentTexture:atIndex:"), - .{ - state.back_texture.value, - @as(c_ulong, 0), - }, - ); - - // Draw! - encoder.msgSend( - void, - objc.sel("drawPrimitives:vertexStart:vertexCount:"), - .{ - @intFromEnum(mtl.MTLPrimitiveType.triangle), - @as(c_ulong, 0), - @as(c_ulong, 3), - }, - ); -} - -fn drawImagePlacements( - self: *Metal, - encoder: objc.Object, - frame: *const FrameState, - placements: []const mtl_image.Placement, -) !void { - if (placements.len == 0) return; - - // Use our image shader pipeline - encoder.msgSend( - void, - objc.sel("setRenderPipelineState:"), - .{self.shaders.image_pipeline.value}, - ); - - // Set our uniforms - encoder.msgSend( - void, - objc.sel("setVertexBuffer:offset:atIndex:"), - .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, - ); - encoder.msgSend( - void, - objc.sel("setFragmentBuffer:offset:atIndex:"), - .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, - ); - - for (placements) |placement| { - try self.drawImagePlacement(encoder, placement); - } -} - -fn drawImagePlacement( - self: *Metal, - encoder: objc.Object, - p: mtl_image.Placement, -) !void { - // Look up the image - const image = self.images.get(p.image_id) orelse { - log.warn("image not found for placement image_id={}", .{p.image_id}); - return; - }; - - // Get the texture - const texture = switch (image.image) { - .ready => |t| t, - else => { - log.warn("image not ready for placement image_id={}", .{p.image_id}); - return; - }, - }; - - // Create our vertex buffer, which is always exactly one item. - // future(mitchellh): we can group rendering multiple instances of a single image - const Buffer = mtl_buffer.Buffer(mtl_shaders.Image); - var buf = try Buffer.initFill(self.gpu_state.device, &.{.{ - .grid_pos = .{ - @as(f32, @floatFromInt(p.x)), - @as(f32, @floatFromInt(p.y)), - }, - - .cell_offset = .{ - @as(f32, @floatFromInt(p.cell_offset_x)), - @as(f32, @floatFromInt(p.cell_offset_y)), - }, - - .source_rect = .{ - @as(f32, @floatFromInt(p.source_x)), - @as(f32, @floatFromInt(p.source_y)), - @as(f32, @floatFromInt(p.source_width)), - @as(f32, @floatFromInt(p.source_height)), - }, - - .dest_size = .{ - @as(f32, @floatFromInt(p.width)), - @as(f32, @floatFromInt(p.height)), - }, - }}, .{ - // Indicate that the CPU writes to this resource but never reads it. - .cpu_cache_mode = .write_combined, - .storage_mode = self.gpu_state.default_storage_mode, - }); - defer buf.deinit(); - - // Set our buffer - encoder.msgSend( - void, - objc.sel("setVertexBuffer:offset:atIndex:"), - .{ buf.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, - ); - - // Set our texture - encoder.msgSend( - void, - objc.sel("setVertexTexture:atIndex:"), - .{ - texture.value, - @as(c_ulong, 0), - }, - ); - encoder.msgSend( - void, - objc.sel("setFragmentTexture:atIndex:"), - .{ - texture.value, - @as(c_ulong, 0), - }, - ); - - // Draw! - encoder.msgSend( - void, - objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"), - .{ - @intFromEnum(mtl.MTLPrimitiveType.triangle), - @as(c_ulong, 6), - @intFromEnum(mtl.MTLIndexType.uint16), - self.gpu_state.instance.buffer.value, - @as(c_ulong, 0), - @as(c_ulong, 1), - }, - ); - - // log.debug("drawImagePlacement: {}", .{p}); -} - -/// Draw the cell backgrounds. -fn drawCellBgs( - self: *Metal, - encoder: objc.Object, - frame: *const FrameState, -) !void { - // Use our shader pipeline - encoder.msgSend( - void, - objc.sel("setRenderPipelineState:"), - .{self.shaders.cell_bg_pipeline.value}, - ); - - // Set our buffers - encoder.msgSend( - void, - objc.sel("setVertexBuffer:offset:atIndex:"), - .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, - ); - encoder.msgSend( - void, - objc.sel("setFragmentBuffer:offset:atIndex:"), - .{ frame.cells_bg.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, - ); - encoder.msgSend( - void, - objc.sel("setFragmentBuffer:offset:atIndex:"), - .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, - ); - - encoder.msgSend( - void, - objc.sel("drawPrimitives:vertexStart:vertexCount:"), - .{ - @intFromEnum(mtl.MTLPrimitiveType.triangle), - @as(c_ulong, 0), - @as(c_ulong, 3), - }, - ); -} - -/// Draw the cell foregrounds using the text shader. -fn drawCellFgs( - self: *Metal, - encoder: objc.Object, - frame: *const FrameState, - len: usize, -) !void { - // This triggers an assertion in the Metal API if we try to draw - // with an instance count of 0 so just bail. - if (len == 0) return; - - // Use our shader pipeline - encoder.msgSend( - void, - objc.sel("setRenderPipelineState:"), - .{self.shaders.cell_text_pipeline.value}, - ); - - // Set our buffers - encoder.msgSend( - void, - objc.sel("setVertexBuffer:offset:atIndex:"), - .{ frame.cells.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, - ); - encoder.msgSend( - void, - objc.sel("setVertexBuffer:offset:atIndex:"), - .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, - ); - encoder.msgSend( - void, - objc.sel("setVertexBuffer:offset:atIndex:"), - .{ frame.cells_bg.buffer.value, @as(c_ulong, 0), @as(c_ulong, 2) }, - ); - encoder.msgSend( - void, - objc.sel("setFragmentTexture:atIndex:"), - .{ frame.grayscale.value, @as(c_ulong, 0) }, - ); - encoder.msgSend( - void, - objc.sel("setFragmentTexture:atIndex:"), - .{ frame.color.value, @as(c_ulong, 1) }, - ); - encoder.msgSend( - void, - objc.sel("setFragmentBuffer:offset:atIndex:"), - .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 2) }, - ); - - encoder.msgSend( - void, - objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"), - .{ - @intFromEnum(mtl.MTLPrimitiveType.triangle), - @as(c_ulong, 6), - @intFromEnum(mtl.MTLIndexType.uint16), - self.gpu_state.instance.buffer.value, - @as(c_ulong, 0), - @as(c_ulong, len), - }, - ); -} - -/// This goes through the Kitty graphic placements and accumulates the -/// placements we need to render on our viewport. It also ensures that -/// the visible images are loaded on the GPU. -fn prepKittyGraphics( - self: *Metal, - t: *terminal.Terminal, -) !void { - const storage = &t.screen.kitty_images; - defer storage.dirty = false; - - // We always clear our previous placements no matter what because - // we rebuild them from scratch. - self.image_placements.clearRetainingCapacity(); - self.image_virtual = false; - - // Go through our known images and if there are any that are no longer - // in use then mark them to be freed. - // - // This never conflicts with the below because a placement can't - // reference an image that doesn't exist. - { - var it = self.images.iterator(); - while (it.next()) |kv| { - if (storage.imageById(kv.key_ptr.*) == null) { - kv.value_ptr.image.markForUnload(); - } - } - } - - // The top-left and bottom-right corners of our viewport in screen - // points. This lets us determine offsets and containment of placements. - const top = t.screen.pages.getTopLeft(.viewport); - const bot = t.screen.pages.getBottomRight(.viewport).?; - const top_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y; - const bot_y = t.screen.pages.pointFromPin(.screen, bot).?.screen.y; - - // Go through the placements and ensure the image is loaded on the GPU. - var it = storage.placements.iterator(); - while (it.next()) |kv| { - const p = kv.value_ptr; - - // Special logic based on location - switch (p.location) { - .pin => {}, - .virtual => { - // We need to mark virtual placements on our renderer so that - // we know to rebuild in more scenarios since cell changes can - // now trigger placement changes. - self.image_virtual = true; - - // We also continue out because virtual placements are - // only triggered by the unicode placeholder, not by the - // placement itself. - continue; - }, - } - - // Get the image for the placement - const image = storage.imageById(kv.key_ptr.image_id) orelse { - log.warn( - "missing image for placement, ignoring image_id={}", - .{kv.key_ptr.image_id}, - ); - continue; - }; - - try self.prepKittyPlacement(t, top_y, bot_y, &image, p); - } - - // If we have virtual placements then we need to scan for placeholders. - if (self.image_virtual) { - var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot); - while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement( - t, - &virtual_p, - ); - } - - // Sort the placements by their Z value. - std.mem.sortUnstable( - mtl_image.Placement, - self.image_placements.items, - {}, - struct { - fn lessThan( - ctx: void, - lhs: mtl_image.Placement, - rhs: mtl_image.Placement, - ) bool { - _ = ctx; - return lhs.z < rhs.z or (lhs.z == rhs.z and lhs.image_id < rhs.image_id); - } - }.lessThan, - ); - - // Find our indices. The values are sorted by z so we can find the - // first placement out of bounds to find the limits. - var bg_end: ?u32 = null; - var text_end: ?u32 = null; - const bg_limit = std.math.minInt(i32) / 2; - for (self.image_placements.items, 0..) |p, i| { - if (bg_end == null and p.z >= bg_limit) { - bg_end = @intCast(i); - } - if (text_end == null and p.z >= 0) { - text_end = @intCast(i); - } - } - - self.image_bg_end = bg_end orelse 0; - self.image_text_end = text_end orelse self.image_bg_end; -} - -fn prepKittyVirtualPlacement( - self: *Metal, - t: *terminal.Terminal, - p: *const terminal.kitty.graphics.unicode.Placement, -) !void { - const storage = &t.screen.kitty_images; - const image = storage.imageById(p.image_id) orelse { - log.warn( - "missing image for virtual placement, ignoring image_id={}", - .{p.image_id}, - ); - return; - }; - - const rp = p.renderPlacement( - storage, - &image, - self.grid_metrics.cell_width, - self.grid_metrics.cell_height, - ) catch |err| { - log.warn("error rendering virtual placement err={}", .{err}); - return; - }; - - // If our placement is zero sized then we don't do anything. - if (rp.dest_width == 0 or rp.dest_height == 0) return; - - const viewport: terminal.point.Point = t.screen.pages.pointFromPin( - .viewport, - rp.top_left, - ) orelse { - // This is unreachable with virtual placements because we should - // only ever be looking at virtual placements that are in our - // viewport in the renderer and virtual placements only ever take - // up one row. - unreachable; - }; - - // Send our image to the GPU and store the placement for rendering. - try self.prepKittyImage(&image); - try self.image_placements.append(self.alloc, .{ - .image_id = image.id, - .x = @intCast(rp.top_left.x), - .y = @intCast(viewport.viewport.y), - .z = -1, - .width = rp.dest_width, - .height = rp.dest_height, - .cell_offset_x = rp.offset_x, - .cell_offset_y = rp.offset_y, - .source_x = rp.source_x, - .source_y = rp.source_y, - .source_width = rp.source_width, - .source_height = rp.source_height, + .bgra8unorm, + .storage_mode = self.default_storage_mode, + .width = width, + .height = height, }); } -fn prepKittyPlacement( - self: *Metal, - t: *terminal.Terminal, - top_y: u32, - bot_y: u32, - image: *const terminal.kitty.graphics.Image, - p: *const terminal.kitty.graphics.ImageStorage.Placement, -) !void { - // Get the rect for the placement. If this placement doesn't have - // a rect then its virtual or something so skip it. - const rect = p.rect(image.*, t) orelse return; - - // This is expensive but necessary. - const img_top_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y; - const img_bot_y = t.screen.pages.pointFromPin(.screen, rect.bottom_right).?.screen.y; - - // If the selection isn't within our viewport then skip it. - if (img_top_y > bot_y) return; - if (img_bot_y < top_y) return; - - // We need to prep this image for upload if it isn't in the cache OR - // it is in the cache but the transmit time doesn't match meaning this - // image is different. - try self.prepKittyImage(image); - - // Calculate the dimensions of our image, taking in to - // account the rows / columns specified by the placement. - const dest_size = p.calculatedSize(image.*, t); - - // Calculate the source rectangle - const source_x = @min(image.width, p.source_x); - const source_y = @min(image.height, p.source_y); - const source_width = if (p.source_width > 0) - @min(image.width - source_x, p.source_width) - else - image.width; - const source_height = if (p.source_height > 0) - @min(image.height - source_y, p.source_height) - else - image.height; - - // Get the viewport-relative Y position of the placement. - const y_pos: i32 = @as(i32, @intCast(img_top_y)) - @as(i32, @intCast(top_y)); - - // Accumulate the placement - if (dest_size.width > 0 and dest_size.height > 0) { - try self.image_placements.append(self.alloc, .{ - .image_id = image.id, - .x = @intCast(rect.top_left.x), - .y = y_pos, - .z = p.z, - .width = dest_size.width, - .height = dest_size.height, - .cell_offset_x = p.x_offset, - .cell_offset_y = p.y_offset, - .source_x = source_x, - .source_y = source_y, - .source_width = source_width, - .source_height = source_height, - }); - } -} - -fn prepKittyImage( - self: *Metal, - image: *const terminal.kitty.graphics.Image, -) !void { - // If this image exists and its transmit time is the same we assume - // it is the identical image so we don't need to send it to the GPU. - const gop = try self.images.getOrPut(self.alloc, image.id); - if (gop.found_existing and - gop.value_ptr.transmit_time.order(image.transmit_time) == .eq) - { - return; - } - - // Copy the data into the pending state. - const data = try self.alloc.dupe(u8, image.data); - errdefer self.alloc.free(data); - - // Store it in the map - const pending: Image.Pending = .{ - .width = image.width, - .height = image.height, - .data = data.ptr, - }; - - const new_image: Image = switch (image.format) { - .gray => .{ .pending_gray = pending }, - .gray_alpha => .{ .pending_gray_alpha = pending }, - .rgb => .{ .pending_rgb = pending }, - .rgba => .{ .pending_rgba = pending }, - .png => unreachable, // should be decoded by now - }; - - if (!gop.found_existing) { - gop.value_ptr.* = .{ - .image = new_image, - .transmit_time = undefined, - }; +/// Present the provided target. +pub inline fn present(self: *Metal, target: Target, sync: bool) !void { + if (sync) { + self.layer.setSurfaceSync(target.surface); } else { - try gop.value_ptr.image.markForReplace( - self.alloc, - new_image, - ); - } - - gop.value_ptr.transmit_time = image.transmit_time; -} - -/// Update the configuration. -pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void { - // We always redo the font shaper in case font features changed. We - // could check to see if there was an actual config change but this is - // easier and rare enough to not cause performance issues. - { - var font_shaper = try font.Shaper.init(self.alloc, .{ - .features = config.font_features.items, - }); - errdefer font_shaper.deinit(); - self.font_shaper.deinit(); - self.font_shaper = font_shaper; - } - - // We also need to reset the shaper cache so shaper info - // from the previous font isn't re-used for the new font. - const font_shaper_cache = font.ShaperCache.init(); - self.font_shaper_cache.deinit(self.alloc); - self.font_shaper_cache = font_shaper_cache; - - // Set our new minimum contrast - self.uniforms.min_contrast = config.min_contrast; - - // Set our new color space and blending - self.uniforms.use_display_p3 = config.colorspace == .@"display-p3"; - self.uniforms.use_linear_blending = config.blending.isLinear(); - self.uniforms.use_linear_correction = config.blending == .@"linear-corrected"; - - // Set our new colors - self.default_background_color = config.background; - self.default_foreground_color = config.foreground; - self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null; - self.cursor_invert = config.cursor_invert; - - // Update our layer's opaqueness and display sync in case they changed. - { - // We use a CATransaction so that Core Animation knows that we - // updated the opaque property. Otherwise it behaves weird, not - // properly going from opaque to transparent unless we resize. - const CATransaction = objc.getClass("CATransaction").?; - CATransaction.msgSend(void, "begin", .{}); - defer CATransaction.msgSend(void, "commit", .{}); - - self.layer.setProperty("opaque", config.background_opacity >= 1); - self.layer.setProperty("displaySyncEnabled", config.vsync); - } - - // Update our terminal colorspace if it changed - if (self.config.colorspace != config.colorspace) { - const terminal_colorspace = try graphics.ColorSpace.createNamed( - switch (config.colorspace) { - .@"display-p3" => .displayP3, - .srgb => .sRGB, - }, - ); - errdefer terminal_colorspace.release(); - self.terminal_colorspace.release(); - self.terminal_colorspace = terminal_colorspace; - } - - const old_blending = self.config.blending; - const old_custom_shaders = self.config.custom_shaders; - - self.config.deinit(); - self.config = config.*; - - // Reset our viewport to force a rebuild, in case of a font change. - self.cells_viewport = null; - - // We reinitialize our shaders if our - // blending or custom shaders changed. - if (old_blending != config.blending or - !old_custom_shaders.equal(config.custom_shaders)) - { - self.deinitShaders(); - try self.initShaders(); - // We call setScreenSize to reinitialize - // the textures used for custom shaders. - if (self.custom_shader_state != null) { - try self.setScreenSize(self.size); - } - // And we update our layer's pixel format appropriately. - self.layer.setProperty( - "pixelFormat", - if (config.blending.isLinear()) - @intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb) - else - @intFromEnum(mtl.MTLPixelFormat.bgra8unorm), - ); + try self.layer.setSurface(target.surface); } } -/// Resize the screen. -pub fn setScreenSize( - self: *Metal, - size: renderer.Size, -) !void { - // Store our sizes - self.size = size; - const grid_size = size.grid(); - const terminal_size = size.terminal(); - - // Blank space around the grid. - const blank: renderer.Padding = size.screen.blankPadding( - size.padding, - grid_size, - size.cell, - ).add(size.padding); - - var padding_extend = self.uniforms.padding_extend; - switch (self.config.padding_color) { - .extend => { - // If padding extension is enabled, we extend left and right always - // because there is no downside to this. Up/down is dependent - // on some heuristics (see rebuildCells). - padding_extend.left = true; - padding_extend.right = true; - }, - - .@"extend-always" => { - padding_extend.up = true; - padding_extend.down = true; - padding_extend.left = true; - padding_extend.right = true; - }, - - .background => { - // Otherwise, disable all padding extension. - padding_extend = .{}; - }, - } - - // Set the size of the drawable surface to the bounds - self.layer.setProperty("drawableSize", graphics.Size{ - .width = @floatFromInt(size.screen.width), - .height = @floatFromInt(size.screen.height), - }); - - // Setup our uniforms - const old = self.uniforms; - self.uniforms = .{ - .projection_matrix = math.ortho2d( - -1 * @as(f32, @floatFromInt(size.padding.left)), - @floatFromInt(terminal_size.width + size.padding.right), - @floatFromInt(terminal_size.height + size.padding.bottom), - -1 * @as(f32, @floatFromInt(size.padding.top)), - ), - .cell_size = .{ - @floatFromInt(self.grid_metrics.cell_width), - @floatFromInt(self.grid_metrics.cell_height), - }, - .grid_size = .{ - grid_size.columns, - grid_size.rows, - }, - .grid_padding = .{ - @floatFromInt(blank.top), - @floatFromInt(blank.right), - @floatFromInt(blank.bottom), - @floatFromInt(blank.left), - }, - .padding_extend = padding_extend, - .min_contrast = old.min_contrast, - .cursor_pos = old.cursor_pos, - .cursor_color = old.cursor_color, - .bg_color = old.bg_color, - .cursor_wide = old.cursor_wide, - .use_display_p3 = old.use_display_p3, - .use_linear_blending = old.use_linear_blending, - .use_linear_correction = old.use_linear_correction, - }; - - // Reset our cell contents if our grid size has changed. - if (!self.cells.size.equals(grid_size)) { - try self.cells.resize(self.alloc, grid_size); - - // Reset our viewport to force a rebuild - self.cells_viewport = null; - } - - // If we have custom shaders then we update the state - if (self.custom_shader_state) |*state| { - // Only free our previous texture if this isn't our first - // time setting the custom shader state. - if (state.uniforms.resolution[0] > 0) { - state.front_texture.release(); - state.back_texture.release(); - } - - state.uniforms.resolution = .{ - @floatFromInt(size.screen.width), - @floatFromInt(size.screen.height), - 1, - }; - - state.front_texture = texture: { - // This texture is the size of our drawable but supports being a - // render target AND reading so that the custom shaders can read from it. - 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; - }; - desc.setProperty( - "pixelFormat", - // Using an `*_srgb` pixel format makes Metal gamma encode - // the pixels written to it *after* blending, which means - // we get linear alpha blending rather than gamma-incorrect - // blending. - if (self.config.blending.isLinear()) - @intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb) - else - @intFromEnum(mtl.MTLPixelFormat.bgra8unorm), - ); - desc.setProperty("width", @as(c_ulong, @intCast(size.screen.width))); - desc.setProperty("height", @as(c_ulong, @intCast(size.screen.height))); - desc.setProperty( - "usage", - mtl.MTLTextureUsage{ - .render_target = true, - .shader_read = true, - .shader_write = true, - }, - ); - - // If we fail to create the texture, then we just don't have a screen - // texture and our custom shaders won't run. - const id = self.gpu_state.device.msgSend( - ?*anyopaque, - objc.sel("newTextureWithDescriptor:"), - .{desc}, - ) orelse return error.MetalFailed; - - break :texture objc.Object.fromId(id); - }; - - state.back_texture = texture: { - // This texture is the size of our drawable but supports being a - // render target AND reading so that the custom shaders can read from it. - 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; - }; - desc.setProperty( - "pixelFormat", - // Using an `*_srgb` pixel format makes Metal gamma encode - // the pixels written to it *after* blending, which means - // we get linear alpha blending rather than gamma-incorrect - // blending. - if (self.config.blending.isLinear()) - @intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb) - else - @intFromEnum(mtl.MTLPixelFormat.bgra8unorm), - ); - desc.setProperty("width", @as(c_ulong, @intCast(size.screen.width))); - desc.setProperty("height", @as(c_ulong, @intCast(size.screen.height))); - desc.setProperty( - "usage", - mtl.MTLTextureUsage{ - .render_target = true, - .shader_read = true, - .shader_write = true, - }, - ); - - // If we fail to create the texture, then we just don't have a screen - // texture and our custom shaders won't run. - const id = self.gpu_state.device.msgSend( - ?*anyopaque, - objc.sel("newTextureWithDescriptor:"), - .{desc}, - ) orelse return error.MetalFailed; - - break :texture objc.Object.fromId(id); - }; - } - - log.debug("screen size size={}", .{size}); +/// Present the last presented target again. (noop for Metal) +pub inline fn repeat(self: *Metal) !void { + _ = self; } -/// Convert the terminal state to GPU cells stored in CPU memory. These -/// are then synced to the GPU in the next frame. This only updates CPU -/// memory and doesn't touch the GPU. -fn rebuildCells( - self: *Metal, - rebuild: bool, - screen: *terminal.Screen, - screen_type: terminal.ScreenType, - mouse: renderer.State.Mouse, - preedit: ?renderer.State.Preedit, - cursor_style_: ?renderer.CursorStyle, - color_palette: *const terminal.color.Palette, -) !void { - // const start = try std.time.Instant.now(); - // const start_micro = std.time.microTimestamp(); - // defer { - // const end = std.time.Instant.now() catch unreachable; - // // "[rebuildCells time] \t" - // std.log.warn("[rebuildCells time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us}); - // } - - _ = screen_type; // we might use this again later so not deleting it yet - - // Create an arena for all our temporary allocations while rebuilding - var arena = ArenaAllocator.init(self.alloc); - defer arena.deinit(); - const arena_alloc = arena.allocator(); - - // Create our match set for the links. - var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet( - arena_alloc, - screen, - mouse_pt, - mouse.mods, - ) else .{}; - - // Determine our x/y range for preedit. We don't want to render anything - // here because we will render the preedit separately. - const preedit_range: ?struct { - y: terminal.size.CellCountInt, - x: [2]terminal.size.CellCountInt, - cp_offset: usize, - } = if (preedit) |preedit_v| preedit: { - const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1); - break :preedit .{ - .y = screen.cursor.y, - .x = .{ range.start, range.end }, - .cp_offset = range.cp_offset, - }; - } else null; - - if (rebuild) { - // If we are doing a full rebuild, then we clear the entire cell buffer. - self.cells.reset(); - - // We also reset our padding extension depending on the screen type - switch (self.config.padding_color) { - .background => {}, - - // For extension, assume we are extending in all directions. - // For "extend" this may be disabled due to heuristics below. - .extend, .@"extend-always" => { - self.uniforms.padding_extend = .{ - .up = true, - .down = true, - .left = true, - .right = true, - }; - }, - } - } - - // We rebuild the cells row-by-row because we - // do font shaping and dirty tracking by row. - var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null); - // If our cell contents buffer is shorter than the screen viewport, - // we render the rows that fit, starting from the bottom. If instead - // the viewport is shorter than the cell contents buffer, we align - // the top of the viewport with the top of the contents buffer. - var y: terminal.size.CellCountInt = @min( - screen.pages.rows, - self.cells.size.rows, - ); - while (row_it.next()) |row| { - // The viewport may have more rows than our cell contents, - // so we need to break from the loop early if we hit y = 0. - if (y == 0) break; - - y -= 1; - - if (!rebuild) { - // Only rebuild if we are doing a full rebuild or this row is dirty. - if (!row.isDirty()) continue; - - // Clear the cells if the row is dirty - self.cells.clear(y); - } - - // True if we want to do font shaping around the cursor. We want to - // do font shaping as long as the cursor is enabled. - const shape_cursor = screen.viewportIsBottom() and - y == screen.cursor.y; - - // We need to get this row's selection if there is one for proper - // run splitting. - const row_selection = sel: { - const sel = screen.selection orelse break :sel null; - const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse - break :sel null; - break :sel sel.containedRow(screen, pin) orelse null; - }; - - // On primary screen, we still apply vertical padding extension - // under certain conditions we feel are safe. This helps make some - // scenarios look better while avoiding scenarios we know do NOT look - // good. - switch (self.config.padding_color) { - // These already have the correct values set above. - .background, .@"extend-always" => {}, - - // Apply heuristics for padding extension. - .extend => if (y == 0) { - self.uniforms.padding_extend.up = !row.neverExtendBg( - color_palette, - self.background_color orelse self.default_background_color, - ); - } else if (y == self.cells.size.rows - 1) { - self.uniforms.padding_extend.down = !row.neverExtendBg( - color_palette, - self.background_color orelse self.default_background_color, - ); - }, - } - - // Iterator of runs for shaping. - var run_iter = self.font_shaper.runIterator( - self.font_grid, - screen, - row, - row_selection, - if (shape_cursor) screen.cursor.x else null, - ); - var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); - var shaper_cells: ?[]const font.shape.Cell = null; - var shaper_cells_i: usize = 0; - - const row_cells_all = row.cells(.all); - - // If our viewport is wider than our cell contents buffer, - // we still only process cells up to the width of the buffer. - const row_cells = row_cells_all[0..@min(row_cells_all.len, self.cells.size.columns)]; - - for (row_cells, 0..) |*cell, x| { - // If this cell falls within our preedit range then we - // skip this because preedits are setup separately. - if (preedit_range) |range| preedit: { - // We're not on the preedit line, no actions necessary. - if (range.y != y) break :preedit; - // We're before the preedit range, no actions necessary. - if (x < range.x[0]) break :preedit; - // We're in the preedit range, skip this cell. - if (x <= range.x[1]) continue; - // After exiting the preedit range we need to catch - // the run position up because of the missed cells. - // In all other cases, no action is necessary. - if (x != range.x[1] + 1) break :preedit; - - // Step the run iterator until we find a run that ends - // after the current cell, which will be the soonest run - // that might contain glyphs for our cell. - while (shaper_run) |run| { - if (run.offset + run.cells > x) break; - shaper_run = try run_iter.next(self.alloc); - shaper_cells = null; - shaper_cells_i = 0; - } - - const run = shaper_run orelse break :preedit; - - // If we haven't shaped this run, do so now. - shaper_cells = shaper_cells orelse - // Try to read the cells from the shaping cache if we can. - self.font_shaper_cache.get(run) orelse - cache: { - // Otherwise we have to shape them. - const cells = try self.font_shaper.shape(run); - - // Try to cache them. If caching fails for any reason we - // continue because it is just a performance optimization, - // not a correctness issue. - self.font_shaper_cache.put( - self.alloc, - run, - cells, - ) catch |err| { - log.warn( - "error caching font shaping results err={}", - .{err}, - ); - }; - - // The cells we get from direct shaping are always owned - // by the shaper and valid until the next shaping call so - // we can safely use them. - break :cache cells; - }; - - // Advance our index until we reach or pass - // our current x position in the shaper cells. - while (shaper_cells.?[shaper_cells_i].x < x) { - shaper_cells_i += 1; - } - } - - const wide = cell.wide; - - const style = row.style(cell); - - const cell_pin: terminal.Pin = cell: { - var copy = row; - copy.x = @intCast(x); - break :cell copy; - }; - - // True if this cell is selected - const selected: bool = if (screen.selection) |sel| - sel.contains(screen, .{ - .node = row.node, - .y = row.y, - .x = @intCast( - // Spacer tails should show the selection - // state of the wide cell they belong to. - if (wide == .spacer_tail) - x -| 1 - else - x, - ), - }) - else - false; - - const bg_style = style.bg(cell, color_palette); - const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color; - - // The final background color for the cell. - const bg = bg: { - if (selected) { - break :bg if (self.config.invert_selection_fg_bg) - if (style.flags.inverse) - // Cell is selected with invert selection fg/bg - // enabled, and the cell has the inverse style - // flag, so they cancel out and we get the normal - // bg color. - bg_style - else - // If it doesn't have the inverse style - // flag then we use the fg color instead. - fg_style - else - // If we don't have invert selection fg/bg set then we - // just use the selection background if set, otherwise - // the default fg color. - break :bg self.config.selection_background orelse self.foreground_color orelse self.default_foreground_color; - } - - // Not selected - break :bg if (style.flags.inverse != isCovering(cell.codepoint())) - // Two cases cause us to invert (use the fg color as the bg) - // - The "inverse" style flag. - // - A "covering" glyph; we use fg for bg in that case to - // help make sure that padding extension works correctly. - // If one of these is true (but not the other) - // then we use the fg style color for the bg. - fg_style - else - // Otherwise they cancel out. - bg_style; - }; - - const fg = fg: { - if (selected and !self.config.invert_selection_fg_bg) { - // If we don't have invert selection fg/bg set - // then we just use the selection foreground if - // set, otherwise the default bg color. - break :fg self.config.selection_foreground orelse self.background_color orelse self.default_background_color; - } - - // Whether we need to use the bg color as our fg color: - // - Cell is inverted and not selected - // - Cell is selected and not inverted - // Note: if selected then invert sel fg / bg must be - // false since we separately handle it if true above. - break :fg if (style.flags.inverse != selected) - bg_style orelse self.background_color orelse self.default_background_color - else - fg_style; - }; - - // Foreground alpha for this cell. - const alpha: u8 = if (style.flags.faint) 175 else 255; - - // Set the cell's background color. - { - const rgb = bg orelse self.background_color orelse self.default_background_color; - - // Determine our background alpha. If we have transparency configured - // then this is dynamic depending on some situations. This is all - // in an attempt to make transparency look the best for various - // situations. See inline comments. - const bg_alpha: u8 = bg_alpha: { - const default: u8 = 255; - - if (self.config.background_opacity >= 1) break :bg_alpha default; - - // Cells that are selected should be fully opaque. - if (selected) break :bg_alpha default; - - // Cells that are reversed should be fully opaque. - if (style.flags.inverse) break :bg_alpha default; - - // Cells that have an explicit bg color should be fully opaque. - if (bg_style != null) { - break :bg_alpha default; - } - - // Otherwise, we use the configured background opacity. - break :bg_alpha @intFromFloat(@round(self.config.background_opacity * 255.0)); - }; - - self.cells.bgCell(y, x).* = .{ - rgb.r, rgb.g, rgb.b, bg_alpha, - }; - } - - // If the invisible flag is set on this cell then we - // don't need to render any foreground elements, so - // we just skip all glyphs with this x coordinate. - // - // NOTE: This behavior matches xterm. Some other terminal - // emulators, e.g. Alacritty, still render text decorations - // and only make the text itself invisible. The decision - // has been made here to match xterm's behavior for this. - if (style.flags.invisible) { - continue; - } - - // Give links a single underline, unless they already have - // an underline, in which case use a double underline to - // distinguish them. - const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin)) - if (style.flags.underline == .single) - .double - else - .single - else - style.flags.underline; - - // We draw underlines first so that they layer underneath text. - // This improves readability when a colored underline is used - // which intersects parts of the text (descenders). - if (underline != .none) self.addUnderline( - @intCast(x), - @intCast(y), - underline, - style.underlineColor(color_palette) orelse fg, - alpha, - ) catch |err| { - log.warn( - "error adding underline to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - - if (style.flags.overline) self.addOverline(@intCast(x), @intCast(y), fg, alpha) catch |err| { - log.warn( - "error adding overline to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - - // If we're at or past the end of our shaper run then - // we need to get the next run from the run iterator. - if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) { - shaper_run = try run_iter.next(self.alloc); - shaper_cells = null; - shaper_cells_i = 0; - } - - if (shaper_run) |run| glyphs: { - // If we haven't shaped this run yet, do so. - shaper_cells = shaper_cells orelse - // Try to read the cells from the shaping cache if we can. - self.font_shaper_cache.get(run) orelse - cache: { - // Otherwise we have to shape them. - const cells = try self.font_shaper.shape(run); - - // Try to cache them. If caching fails for any reason we - // continue because it is just a performance optimization, - // not a correctness issue. - self.font_shaper_cache.put( - self.alloc, - run, - cells, - ) catch |err| { - log.warn( - "error caching font shaping results err={}", - .{err}, - ); - }; - - // The cells we get from direct shaping are always owned - // by the shaper and valid until the next shaping call so - // we can safely use them. - break :cache cells; - }; - - const cells = shaper_cells orelse break :glyphs; - - // If there are no shaper cells for this run, ignore it. - // This can occur for runs of empty cells, and is fine. - if (cells.len == 0) break :glyphs; - - // If we encounter a shaper cell to the left of the current - // cell then we have some problems. This logic relies on x - // position monotonically increasing. - assert(cells[shaper_cells_i].x >= x); - - // NOTE: An assumption is made here that a single cell will never - // be present in more than one shaper run. If that assumption is - // violated, this logic breaks. - - while (shaper_cells_i < cells.len and cells[shaper_cells_i].x == x) : ({ - shaper_cells_i += 1; - }) { - self.addGlyph( - @intCast(x), - @intCast(y), - cell_pin, - cells[shaper_cells_i], - shaper_run.?, - fg, - alpha, - ) catch |err| { - log.warn( - "error adding glyph to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - } - } - - // Finally, draw a strikethrough if necessary. - if (style.flags.strikethrough) self.addStrikethrough( - @intCast(x), - @intCast(y), - fg, - alpha, - ) catch |err| { - log.warn( - "error adding strikethrough to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - } - } - - // Setup our cursor rendering information. - cursor: { - // By default, we don't handle cursor inversion on the shader. - self.cells.setCursor(null); - self.uniforms.cursor_pos = .{ - std.math.maxInt(u16), - std.math.maxInt(u16), - }; - - // If we have preedit text, we don't setup a cursor - if (preedit != null) break :cursor; - - // Prepare the cursor cell contents. - const style = cursor_style_ orelse break :cursor; - const cursor_color = self.cursor_color orelse self.default_cursor_color orelse color: { - if (self.cursor_invert) { - // Use the foreground color from the cell under the cursor, if any. - const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); - break :color if (sty.flags.inverse) - // If the cell is reversed, use background color instead. - (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color) - else - (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color); - } else { - break :color self.foreground_color orelse self.default_foreground_color; - } - }; - - self.addCursor(screen, style, cursor_color); - - // If the cursor is visible then we set our uniforms. - if (style == .block and screen.viewportIsBottom()) { - const wide = screen.cursor.page_cell.wide; - - self.uniforms.cursor_pos = .{ - // If we are a spacer tail of a wide cell, our cursor needs - // to move back one cell. The saturate is to ensure we don't - // overflow but this shouldn't happen with well-formed input. - switch (wide) { - .narrow, .spacer_head, .wide => screen.cursor.x, - .spacer_tail => screen.cursor.x -| 1, - }, - screen.cursor.y, - }; - - self.uniforms.cursor_wide = switch (wide) { - .narrow, .spacer_head => false, - .wide, .spacer_tail => true, - }; - - const uniform_color = if (self.cursor_invert) blk: { - // Use the background color from the cell under the cursor, if any. - const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); - break :blk if (sty.flags.inverse) - // If the cell is reversed, use foreground color instead. - (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color) - else - (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color); - } else if (self.config.cursor_text) |txt| - txt - else - self.background_color orelse self.default_background_color; - - self.uniforms.cursor_color = .{ - uniform_color.r, - uniform_color.g, - uniform_color.b, - 255, - }; - } - } - - // Setup our preedit text. - if (preedit) |preedit_v| { - const range = preedit_range.?; - var x = range.x[0]; - for (preedit_v.codepoints[range.cp_offset..]) |cp| { - self.addPreeditCell(cp, .{ .x = x, .y = range.y }) catch |err| { - log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ - x, - range.y, - err, - }); - }; - - x += if (cp.wide) 2 else 1; - } - } - - // Update that our cells rebuilt - self.cells_rebuilt = true; - - // Log some things - // log.debug("rebuildCells complete cached_runs={}", .{ - // self.font_shaper_cache.count(), - // }); -} - -/// Add an underline decoration to the specified cell -fn addUnderline( - self: *Metal, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, - style: terminal.Attribute.Underline, - color: terminal.color.RGB, - alpha: u8, -) !void { - const sprite: font.Sprite = switch (style) { - .none => unreachable, - .single => .underline, - .double => .underline_double, - .dotted => .underline_dotted, - .dashed => .underline_dashed, - .curly => .underline_curly, - }; - - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(sprite), - .{ - .cell_width = 1, - .grid_metrics = self.grid_metrics, - }, - ); - - try self.cells.add(self.alloc, .underline, .{ - .mode = .fg, - .grid_pos = .{ @intCast(x), @intCast(y) }, - .constraint_width = 1, - .color = .{ color.r, color.g, color.b, alpha }, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .bearings = .{ - @intCast(render.glyph.offset_x), - @intCast(render.glyph.offset_y), - }, - }); -} - -/// Add a overline decoration to the specified cell -fn addOverline( - self: *Metal, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, - color: terminal.color.RGB, - alpha: u8, -) !void { - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(font.Sprite.overline), - .{ - .cell_width = 1, - .grid_metrics = self.grid_metrics, - }, - ); - - try self.cells.add(self.alloc, .overline, .{ - .mode = .fg, - .grid_pos = .{ @intCast(x), @intCast(y) }, - .constraint_width = 1, - .color = .{ color.r, color.g, color.b, alpha }, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .bearings = .{ - @intCast(render.glyph.offset_x), - @intCast(render.glyph.offset_y), - }, - }); -} - -/// Add a strikethrough decoration to the specified cell -fn addStrikethrough( - self: *Metal, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, - color: terminal.color.RGB, - alpha: u8, -) !void { - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(font.Sprite.strikethrough), - .{ - .cell_width = 1, - .grid_metrics = self.grid_metrics, - }, - ); - - try self.cells.add(self.alloc, .strikethrough, .{ - .mode = .fg, - .grid_pos = .{ @intCast(x), @intCast(y) }, - .constraint_width = 1, - .color = .{ color.r, color.g, color.b, alpha }, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .bearings = .{ - @intCast(render.glyph.offset_x), - @intCast(render.glyph.offset_y), - }, - }); -} - -// Add a glyph to the specified cell. -fn addGlyph( - self: *Metal, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, - cell_pin: terminal.Pin, - shaper_cell: font.shape.Cell, - shaper_run: font.shape.TextRun, - color: terminal.color.RGB, - alpha: u8, -) !void { - const rac = cell_pin.rowAndCell(); - const cell = rac.cell; - - // Render - const render = try self.font_grid.renderGlyph( - self.alloc, - shaper_run.font_index, - shaper_cell.glyph_index, - .{ - .grid_metrics = self.grid_metrics, - .thicken = self.config.font_thicken, - .thicken_strength = self.config.font_thicken_strength, - }, - ); - - // If the glyph is 0 width or height, it will be invisible - // when drawn, so don't bother adding it to the buffer. - if (render.glyph.width == 0 or render.glyph.height == 0) { - return; - } - - const mode: mtl_shaders.CellText.Mode = switch (try fgMode( - render.presentation, - cell_pin, - )) { - .normal => .fg, - .color => .fg_color, - .constrained => .fg_constrained, - .powerline => .fg_powerline, - }; - - try self.cells.add(self.alloc, .text, .{ - .mode = mode, - .grid_pos = .{ @intCast(x), @intCast(y) }, - .constraint_width = cell.gridWidth(), - .color = .{ color.r, color.g, color.b, alpha }, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .bearings = .{ - @intCast(render.glyph.offset_x + shaper_cell.x_offset), - @intCast(render.glyph.offset_y + shaper_cell.y_offset), - }, - }); -} - -fn addCursor( - self: *Metal, - screen: *terminal.Screen, - cursor_style: renderer.CursorStyle, - cursor_color: terminal.color.RGB, -) void { - // Add the cursor. We render the cursor over the wide character if - // we're on the wide character tail. - const wide, const x = cell: { - // The cursor goes over the screen cursor position. - const cell = screen.cursor.page_cell; - if (cell.wide != .spacer_tail or screen.cursor.x == 0) - break :cell .{ cell.wide == .wide, screen.cursor.x }; - - // If we're part of a wide character, we move the cursor back to - // the actual character. - const prev_cell = screen.cursorCellLeft(1); - break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 }; - }; - - const alpha: u8 = if (!self.focused) 255 else alpha: { - const alpha = 255 * self.config.cursor_opacity; - break :alpha @intFromFloat(@ceil(alpha)); - }; - - const render = switch (cursor_style) { - .block, - .block_hollow, - .bar, - .underline, - => render: { - const sprite: font.Sprite = switch (cursor_style) { - .block => .cursor_rect, - .block_hollow => .cursor_hollow_rect, - .bar => .cursor_bar, - .underline => .underline, - .lock => unreachable, - }; - - break :render self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(sprite), - .{ - .cell_width = if (wide) 2 else 1, - .grid_metrics = self.grid_metrics, - }, - ) catch |err| { - log.warn("error rendering cursor glyph err={}", .{err}); - return; - }; - }, - - .lock => self.font_grid.renderCodepoint( - self.alloc, - 0xF023, // lock symbol - .regular, - .text, - .{ - .cell_width = if (wide) 2 else 1, - .grid_metrics = self.grid_metrics, - }, - ) catch |err| { - log.warn("error rendering cursor glyph err={}", .{err}); - return; - } orelse { - // This should never happen because we embed nerd - // fonts so we just log and return instead of fallback. - log.warn("failed to find lock symbol for cursor codepoint=0xF023", .{}); - return; +/// Returns the options to use when constructing buffers. +pub inline fn bufferOptions(self: Metal) bufferpkg.Options { + return .{ + .device = self.device, + .resource_options = .{ + // Indicate that the CPU writes to this resource but never reads it. + .cpu_cache_mode = .write_combined, + .storage_mode = self.default_storage_mode, }, }; - - self.cells.setCursor(.{ - .mode = .cursor, - .grid_pos = .{ x, screen.cursor.y }, - .color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha }, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .bearings = .{ - @intCast(render.glyph.offset_x), - @intCast(render.glyph.offset_y), - }, - }); } -fn addPreeditCell( - self: *Metal, - cp: renderer.State.Preedit.Codepoint, - coord: terminal.Coordinate, -) !void { - // Preedit is rendered inverted - const bg = self.foreground_color orelse self.default_foreground_color; - const fg = self.background_color orelse self.default_background_color; +pub const instanceBufferOptions = bufferOptions; +pub const uniformBufferOptions = bufferOptions; +pub const fgBufferOptions = bufferOptions; +pub const bgBufferOptions = bufferOptions; +pub const imageBufferOptions = bufferOptions; - // Render the glyph for our preedit text - const render_ = self.font_grid.renderCodepoint( - self.alloc, - @intCast(cp.codepoint), - .regular, - .text, - .{ .grid_metrics = self.grid_metrics }, - ) catch |err| { - log.warn("error rendering preedit glyph err={}", .{err}); - return; - }; - const render = render_ orelse { - log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint}); - return; - }; - - // Add our opaque background cell - self.cells.bgCell(coord.y, coord.x).* = .{ - bg.r, bg.g, bg.b, 255, - }; - if (cp.wide and coord.x < self.cells.size.columns - 1) { - self.cells.bgCell(coord.y, coord.x + 1).* = .{ - bg.r, bg.g, bg.b, 255, - }; - } - - // Add our text - try self.cells.add(self.alloc, .text, .{ - .mode = .fg, - .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, - .color = .{ fg.r, fg.g, fg.b, 255 }, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .bearings = .{ - @intCast(render.glyph.offset_x), - @intCast(render.glyph.offset_y), +/// Returns the options to use when constructing textures. +pub inline fn textureOptions(self: Metal) Texture.Options { + return .{ + .device = self.device, + // Using an `*_srgb` pixel format makes Metal gamma encode the pixels + // written to it *after* blending, which means we get linear alpha + // blending rather than gamma-incorrect blending. + .pixel_format = if (self.blending.isLinear()) + .bgra8unorm_srgb + else + .bgra8unorm, + .resource_options = .{ + // Indicate that the CPU writes to this resource but never reads it. + .cpu_cache_mode = .write_combined, + .storage_mode = self.default_storage_mode, }, - }); + }; } -/// Sync the atlas data to the given texture. This copies the bytes -/// associated with the atlas to the given texture. If the atlas no longer -/// fits into the texture, the texture will be resized. -fn syncAtlasTexture( - device: objc.Object, - atlas: *const font.Atlas, - texture: *objc.Object, - /// Storage mode for the MTLTexture object - storage_mode: mtl.MTLResourceOptions.StorageMode, -) !void { - const width = texture.getProperty(c_ulong, "width"); - if (atlas.size > width) { - // Free our old texture - texture.*.release(); - - // Reallocate - texture.* = try initAtlasTexture(device, atlas, storage_mode); - } - - texture.msgSend( - void, - objc.sel("replaceRegion:mipmapLevel:withBytes:bytesPerRow:"), - .{ - mtl.MTLRegion{ - .origin = .{ .x = 0, .y = 0, .z = 0 }, - .size = .{ - .width = @intCast(atlas.size), - .height = @intCast(atlas.size), - .depth = 1, - }, - }, - @as(c_ulong, 0), - @as(*const anyopaque, atlas.data.ptr), - @as(c_ulong, atlas.format.depth() * atlas.size), - }, - ); -} - -/// Initialize a MTLTexture object for the given atlas. -fn initAtlasTexture( - device: objc.Object, - atlas: *const font.Atlas, - /// Storage mode for the MTLTexture object - storage_mode: mtl.MTLResourceOptions.StorageMode, -) !objc.Object { - // Determine our pixel format +/// Initializes a Texture suitable for the provided font atlas. +pub fn initAtlasTexture(self: *const Metal, atlas: *const font.Atlas) !Texture { const pixel_format: mtl.MTLPixelFormat = switch (atlas.format) { .grayscale => .r8unorm, .rgba => .bgra8unorm, else => @panic("unsupported atlas format for Metal texture"), }; - // 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(pixel_format)); - desc.setProperty("width", @as(c_ulong, @intCast(atlas.size))); - desc.setProperty("height", @as(c_ulong, @intCast(atlas.size))); - - 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, + return Texture.init( + .{ + .device = self.device, + .pixel_format = pixel_format, + .resource_options = .{ + // Indicate that the CPU writes to this resource but never reads it. + .cpu_cache_mode = .write_combined, + .storage_mode = self.default_storage_mode, + }, }, + atlas.size, + atlas.size, + null, ); - - // Initialize - const id = device.msgSend( - ?*anyopaque, - objc.sel("newTextureWithDescriptor:"), - .{desc}, - ) orelse return error.MetalFailed; - - return objc.Object.fromId(id); } -test { - _ = mtl_cell; +/// Begin a frame. +pub inline fn beginFrame( + self: *const Metal, + /// 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, +) !Frame { + return try Frame.begin(.{ .queue = self.queue }, renderer, target); +} + +fn chooseDevice() error{NoMetalDevice}!objc.Object { + var chosen_device: ?objc.Object = null; + + switch (comptime builtin.os.tag) { + .macos => { + const devices = objc.Object.fromId(mtl.MTLCopyAllDevices()); + defer devices.release(); + + var iter = devices.iterate(); + while (iter.next()) |device| { + // We want a GPU that’s connected to a display. + if (device.getProperty(bool, "isHeadless")) continue; + chosen_device = device; + // If the user has an eGPU plugged in, they probably want + // to use it. Otherwise, integrated GPUs are better for + // battery life and thermals. + if (device.getProperty(bool, "isRemovable") or + device.getProperty(bool, "isLowPower")) break; + } + }, + .ios => { + chosen_device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice()); + }, + else => @compileError("unsupported target for Metal"), + } + + const device = chosen_device orelse return error.NoMetalDevice; + return device.retain(); } diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index d0222a390..c2f8bd652 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -1,452 +1,159 @@ -//! Rendering implementation for OpenGL. +//! Graphics API wrapper for OpenGL. pub const OpenGL = @This(); const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; const builtin = @import("builtin"); const glfw = @import("glfw"); -const assert = std.debug.assert; -const testing = std.testing; -const Allocator = std.mem.Allocator; -const ArenaAllocator = std.heap.ArenaAllocator; -const link = @import("link.zig"); -const isCovering = @import("cell.zig").isCovering; -const fgMode = @import("cell.zig").fgMode; +const gl = @import("opengl"); const shadertoy = @import("shadertoy.zig"); const apprt = @import("../apprt.zig"); -const configpkg = @import("../config.zig"); const font = @import("../font/main.zig"); -const imgui = @import("imgui"); -const renderer = @import("../renderer.zig"); -const terminal = @import("../terminal/main.zig"); -const Terminal = terminal.Terminal; -const gl = @import("opengl"); -const math = @import("../math.zig"); -const Surface = @import("../Surface.zig"); +const configpkg = @import("../config.zig"); +const rendererpkg = @import("../renderer.zig"); +const Renderer = rendererpkg.GenericRenderer(OpenGL); -const CellProgram = @import("opengl/CellProgram.zig"); -const ImageProgram = @import("opengl/ImageProgram.zig"); -const gl_image = @import("opengl/image.zig"); -const custom = @import("opengl/custom.zig"); -const Image = gl_image.Image; -const ImageMap = gl_image.ImageMap; -const ImagePlacementList = std.ArrayListUnmanaged(gl_image.Placement); +pub const GraphicsAPI = OpenGL; +pub const Target = @import("opengl/Target.zig"); +pub const Frame = @import("opengl/Frame.zig"); +pub const RenderPass = @import("opengl/RenderPass.zig"); +pub const Pipeline = @import("opengl/Pipeline.zig"); +const bufferpkg = @import("opengl/buffer.zig"); +pub const Buffer = bufferpkg.Buffer; +pub const Texture = @import("opengl/Texture.zig"); +pub const shaders = @import("opengl/shaders.zig"); -const log = std.log.scoped(.grid); +pub const cellpkg = @import("opengl/cell.zig"); +pub const imagepkg = @import("opengl/image.zig"); -/// The runtime can request a single-threaded draw by setting this boolean -/// to true. In this case, the renderer.draw() call is expected to be called -/// from the runtime. -pub const single_threaded_draw = if (@hasDecl(apprt.Surface, "opengl_single_threaded_draw")) - apprt.Surface.opengl_single_threaded_draw -else - false; -const DrawMutex = if (single_threaded_draw) std.Thread.Mutex else void; -const drawMutexZero: DrawMutex = if (DrawMutex == void) void{} else .{}; +pub const custom_shader_target: shadertoy.Target = .glsl; + +const log = std.log.scoped(.opengl); + +/// We require at least OpenGL 4.3 +pub const MIN_VERSION_MAJOR = 4; +pub const MIN_VERSION_MINOR = 3; alloc: std.mem.Allocator, -/// The configuration we need derived from the main config. -config: DerivedConfig, +/// Alpha blending mode +blending: configpkg.Config.AlphaBlending, -/// Current font metrics defining our grid. -grid_metrics: font.Metrics, +/// The most recently presented target, in case we need to present it again. +last_target: ?Target = null, -/// The size of everything. -size: renderer.Size, - -/// The current set of cells to render. Each set of cells goes into -/// a separate shader call. -cells_bg: std.ArrayListUnmanaged(CellProgram.Cell), -cells: std.ArrayListUnmanaged(CellProgram.Cell), - -/// The last viewport that we based our rebuild off of. If this changes, -/// then we do a full rebuild of the cells. The pointer values in this pin -/// are NOT SAFE to read because they may be modified, freed, etc from the -/// termio thread. We treat the pointers as integers for comparison only. -cells_viewport: ?terminal.Pin = null, - -/// The size of the cells list that was sent to the GPU. This is used -/// to detect when the cells array was reallocated/resized and handle that -/// accordingly. -gl_cells_size: usize = 0, - -/// The last length of the cells that was written to the GPU. This is used to -/// determine what data needs to be rewritten on the GPU. -gl_cells_written: usize = 0, - -/// Shader program for cell rendering. -gl_state: ?GLState = null, - -/// The font structures. -font_grid: *font.SharedGrid, -font_shaper: font.Shaper, -font_shaper_cache: font.ShaperCache, -texture_grayscale_modified: usize = 0, -texture_grayscale_resized: usize = 0, -texture_color_modified: usize = 0, -texture_color_resized: usize = 0, - -/// True if the window is focused -focused: bool, - -/// The foreground color set by an OSC 10 sequence. If unset then the default -/// value from the config file is used. -foreground_color: ?terminal.color.RGB, - -/// Foreground color set in the user's config file. -default_foreground_color: terminal.color.RGB, - -/// The background color set by an OSC 11 sequence. If unset then the default -/// value from the config file is used. -background_color: ?terminal.color.RGB, - -/// Background color set in the user's config file. -default_background_color: terminal.color.RGB, - -/// The cursor color set by an OSC 12 sequence. If unset then -/// default_cursor_color is used. -cursor_color: ?terminal.color.RGB, - -/// Default cursor color when no color is set explicitly by an OSC 12 command. -/// This is cursor color as set in the user's config, if any. If no cursor color -/// is set in the user's config, then the cursor color is determined by the -/// current foreground color. -default_cursor_color: ?terminal.color.RGB, - -/// When `cursor_color` is null, swap the foreground and background colors of -/// the cell under the cursor for the cursor color. Otherwise, use the default -/// foreground color as the cursor color. -cursor_invert: bool, - -/// The mailbox for communicating with the window. -surface_mailbox: apprt.surface.Mailbox, - -/// Deferred operations. This is used to apply changes to the OpenGL context. -/// Some runtimes (GTK) do not support multi-threading so to keep our logic -/// simple we apply all OpenGL context changes in the render() call. -deferred_screen_size: ?SetScreenSize = null, -deferred_font_size: ?SetFontSize = null, -deferred_config: ?SetConfig = null, - -/// If we're drawing with single threaded operations -draw_mutex: DrawMutex = drawMutexZero, - -/// Current background to draw. This may not match self.background if the -/// terminal is in reversed mode. -draw_background: terminal.color.RGB, - -/// Whether we're doing padding extension for vertical sides. -padding_extend_top: bool = true, -padding_extend_bottom: bool = true, - -/// The images that we may render. -images: ImageMap = .{}, -image_placements: ImagePlacementList = .{}, -image_bg_end: u32 = 0, -image_text_end: u32 = 0, -image_virtual: bool = false, - -/// Deferred OpenGL operation to update the screen size. -const SetScreenSize = struct { - size: renderer.Size, - - fn apply(self: SetScreenSize, r: *OpenGL) !void { - const gl_state: *GLState = if (r.gl_state) |*v| - v - else - return error.OpenGLUninitialized; - - // Apply our padding - const grid_size = self.size.grid(); - const terminal_size = self.size.terminal(); - - // Blank space around the grid. - const blank: renderer.Padding = switch (r.config.padding_color) { - // We can use zero padding because the background color is our - // clear color. - .background => .{}, - - .extend, .@"extend-always" => self.size.screen.blankPadding( - self.size.padding, - grid_size, - self.size.cell, - ).add(self.size.padding), - }; - - // Update our viewport for this context to be the entire window. - // OpenGL works in pixels, so we have to use the pixel size. - try gl.viewport( - 0, - 0, - @intCast(self.size.screen.width), - @intCast(self.size.screen.height), - ); - - // Update the projection uniform within our shader - inline for (.{ "cell_program", "image_program" }) |name| { - const program = @field(gl_state, name); - const bind = try program.program.use(); - defer bind.unbind(); - try program.program.setUniform( - "projection", - - // 2D orthographic projection with the full w/h - math.ortho2d( - -1 * @as(f32, @floatFromInt(self.size.padding.left)), - @floatFromInt(terminal_size.width + self.size.padding.right), - @floatFromInt(terminal_size.height + self.size.padding.bottom), - -1 * @as(f32, @floatFromInt(self.size.padding.top)), - ), - ); - } - - // Setup our grid padding - { - const program = gl_state.cell_program; - const bind = try program.program.use(); - defer bind.unbind(); - try program.program.setUniform( - "grid_padding", - @Vector(4, f32){ - @floatFromInt(blank.top), - @floatFromInt(blank.right), - @floatFromInt(blank.bottom), - @floatFromInt(blank.left), - }, - ); - try program.program.setUniform( - "grid_size", - @Vector(2, f32){ - @floatFromInt(grid_size.columns), - @floatFromInt(grid_size.rows), - }, - ); - } - - // Update our custom shader resolution - if (gl_state.custom) |*custom_state| { - try custom_state.setScreenSize(self.size); - } - } -}; - -const SetFontSize = struct { - metrics: font.Metrics, - - fn apply(self: SetFontSize, r: *const OpenGL) !void { - const gl_state = r.gl_state orelse return error.OpenGLUninitialized; - - inline for (.{ "cell_program", "image_program" }) |name| { - const program = @field(gl_state, name); - const bind = try program.program.use(); - defer bind.unbind(); - try program.program.setUniform( - "cell_size", - @Vector(2, f32){ - @floatFromInt(self.metrics.cell_width), - @floatFromInt(self.metrics.cell_height), - }, - ); - } - } -}; - -const SetConfig = struct { - fn apply(self: SetConfig, r: *const OpenGL) !void { - _ = self; - const gl_state = r.gl_state orelse return error.OpenGLUninitialized; - - const bind = try gl_state.cell_program.program.use(); - defer bind.unbind(); - try gl_state.cell_program.program.setUniform( - "min_contrast", - r.config.min_contrast, - ); - } -}; - -/// The configuration for this renderer that is derived from the main -/// configuration. This must be exported so that we don't need to -/// pass around Config pointers which makes memory management a pain. -pub const DerivedConfig = struct { - arena: ArenaAllocator, - - font_thicken: bool, - font_thicken_strength: u8, - font_features: std.ArrayListUnmanaged([:0]const u8), - font_styles: font.CodepointResolver.StyleStatus, - cursor_color: ?terminal.color.RGB, - cursor_invert: bool, - cursor_text: ?terminal.color.RGB, - cursor_opacity: f64, - background: terminal.color.RGB, - background_opacity: f64, - foreground: terminal.color.RGB, - selection_background: ?terminal.color.RGB, - selection_foreground: ?terminal.color.RGB, - invert_selection_fg_bg: bool, - bold_is_bright: bool, - min_contrast: f32, - padding_color: configpkg.WindowPaddingColor, - custom_shaders: configpkg.RepeatablePath, - links: link.Set, - - pub fn init( - alloc_gpa: Allocator, - config: *const configpkg.Config, - ) !DerivedConfig { - var arena = ArenaAllocator.init(alloc_gpa); - errdefer arena.deinit(); - const alloc = arena.allocator(); - - // Copy our shaders - const custom_shaders = try config.@"custom-shader".clone(alloc); - - // Copy our font features - const font_features = try config.@"font-feature".clone(alloc); - - // Get our font styles - var font_styles = font.CodepointResolver.StyleStatus.initFill(true); - font_styles.set(.bold, config.@"font-style-bold" != .false); - font_styles.set(.italic, config.@"font-style-italic" != .false); - font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); - - // Our link configs - const links = try link.Set.fromConfig( - alloc, - config.link.links.items, - ); - - const cursor_invert = config.@"cursor-invert-fg-bg"; - - return .{ - .background_opacity = @max(0, @min(1, config.@"background-opacity")), - .font_thicken = config.@"font-thicken", - .font_thicken_strength = config.@"font-thicken-strength", - .font_features = font_features.list, - .font_styles = font_styles, - - .cursor_color = if (!cursor_invert and config.@"cursor-color" != null) - config.@"cursor-color".?.toTerminalRGB() - else - null, - - .cursor_invert = cursor_invert, - - .cursor_text = if (config.@"cursor-text") |txt| - txt.toTerminalRGB() - else - null, - - .cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")), - - .background = config.background.toTerminalRGB(), - .foreground = config.foreground.toTerminalRGB(), - .invert_selection_fg_bg = config.@"selection-invert-fg-bg", - .bold_is_bright = config.@"bold-is-bright", - .min_contrast = @floatCast(config.@"minimum-contrast"), - .padding_color = config.@"window-padding-color", - - .selection_background = if (config.@"selection-background") |bg| - bg.toTerminalRGB() - else - null, - - .selection_foreground = if (config.@"selection-foreground") |bg| - bg.toTerminalRGB() - else - null, - - .custom_shaders = custom_shaders, - .links = links, - - .arena = arena, - }; - } - - pub fn deinit(self: *DerivedConfig) void { - const alloc = self.arena.allocator(); - self.links.deinit(alloc); - self.arena.deinit(); - } -}; - -pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { - // Create the initial font shaper - var shaper = try font.Shaper.init(alloc, .{ - .features = options.config.font_features.items, - }); - errdefer shaper.deinit(); - - // For the remainder of the setup we lock our font grid data because - // we're reading it. - const grid = options.font_grid; - grid.lock.lockShared(); - defer grid.lock.unlockShared(); - - var gl_state = try GLState.init(alloc, options.config, grid); - errdefer gl_state.deinit(); - - return OpenGL{ +pub fn init(alloc: Allocator, opts: rendererpkg.Options) !OpenGL { + return .{ .alloc = alloc, - .config = options.config, - .cells_bg = .{}, - .cells = .{}, - .grid_metrics = grid.metrics, - .size = options.size, - .gl_state = gl_state, - .font_grid = grid, - .font_shaper = shaper, - .font_shaper_cache = font.ShaperCache.init(), - .draw_background = options.config.background, - .focused = true, - .foreground_color = null, - .default_foreground_color = options.config.foreground, - .background_color = null, - .default_background_color = options.config.background, - .cursor_color = null, - .default_cursor_color = options.config.cursor_color, - .cursor_invert = options.config.cursor_invert, - .surface_mailbox = options.surface_mailbox, - .deferred_font_size = .{ .metrics = grid.metrics }, - .deferred_config = .{}, + .blending = opts.config.blending, }; } pub fn deinit(self: *OpenGL) void { - self.font_shaper.deinit(); - self.font_shaper_cache.deinit(self.alloc); - - { - var it = self.images.iterator(); - while (it.next()) |kv| kv.value_ptr.image.deinit(self.alloc); - self.images.deinit(self.alloc); - } - self.image_placements.deinit(self.alloc); - - if (self.gl_state) |*v| v.deinit(self.alloc); - - self.cells.deinit(self.alloc); - self.cells_bg.deinit(self.alloc); - - self.config.deinit(); - self.* = undefined; } /// Returns the hints that we want for this pub fn glfwWindowHints(config: *const configpkg.Config) glfw.Window.Hints { + _ = config; return .{ - .context_version_major = 3, - .context_version_minor = 3, + .context_version_major = MIN_VERSION_MAJOR, + .context_version_minor = MIN_VERSION_MINOR, .opengl_profile = .opengl_core_profile, .opengl_forward_compat = true, - .cocoa_graphics_switching = builtin.os.tag == .macos, - .cocoa_retina_framebuffer = true, - .transparent_framebuffer = config.@"background-opacity" < 1, + .transparent_framebuffer = true, }; } +fn glDebugMessageCallback( + src: gl.c.GLenum, + typ: gl.c.GLenum, + id: gl.c.GLuint, + severity: gl.c.GLenum, + len: gl.c.GLsizei, + msg: [*c]const gl.c.GLchar, + user_param: ?*const anyopaque, +) callconv(.c) void { + _ = user_param; + + const src_str: []const u8 = switch (src) { + gl.c.GL_DEBUG_SOURCE_API => "OpenGL API", + gl.c.GL_DEBUG_SOURCE_WINDOW_SYSTEM => "Window System", + gl.c.GL_DEBUG_SOURCE_SHADER_COMPILER => "Shader Compiler", + gl.c.GL_DEBUG_SOURCE_THIRD_PARTY => "Third Party", + gl.c.GL_DEBUG_SOURCE_APPLICATION => "User", + gl.c.GL_DEBUG_SOURCE_OTHER => "Other", + else => "Unknown", + }; + + const typ_str: []const u8 = switch (typ) { + gl.c.GL_DEBUG_TYPE_ERROR => "Error", + gl.c.GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR => "Deprecated Behavior", + gl.c.GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR => "Undefined Behavior", + gl.c.GL_DEBUG_TYPE_PORTABILITY => "Portability Issue", + gl.c.GL_DEBUG_TYPE_PERFORMANCE => "Performance Issue", + gl.c.GL_DEBUG_TYPE_MARKER => "Marker", + gl.c.GL_DEBUG_TYPE_PUSH_GROUP => "Group Push", + gl.c.GL_DEBUG_TYPE_POP_GROUP => "Group Pop", + gl.c.GL_DEBUG_TYPE_OTHER => "Other", + else => "Unknown", + }; + + const msg_str = msg[0..@intCast(len)]; + + (switch (severity) { + gl.c.GL_DEBUG_SEVERITY_HIGH => log.err( + "[{d}] ({s}: {s}) {s}", + .{ id, src_str, typ_str, msg_str }, + ), + gl.c.GL_DEBUG_SEVERITY_MEDIUM => log.warn( + "[{d}] ({s}: {s}) {s}", + .{ id, src_str, typ_str, msg_str }, + ), + gl.c.GL_DEBUG_SEVERITY_LOW => log.info( + "[{d}] ({s}: {s}) {s}", + .{ id, src_str, typ_str, msg_str }, + ), + gl.c.GL_DEBUG_SEVERITY_NOTIFICATION => log.debug( + "[{d}] ({s}: {s}) {s}", + .{ id, src_str, typ_str, msg_str }, + ), + else => log.warn( + "UNKNOWN SEVERITY [{d}] ({s}: {s}) {s}", + .{ id, src_str, typ_str, msg_str }, + ), + }); +} + +/// Prepares the provided GL context, loading it with glad. +fn prepareContext(getProcAddress: anytype) !void { + const version = try gl.glad.load(getProcAddress); + const major = gl.glad.versionMajor(@intCast(version)); + const minor = gl.glad.versionMinor(@intCast(version)); + errdefer gl.glad.unload(); + log.info("loaded OpenGL {}.{}", .{ major, minor }); + + // Enable debug output for the context. + try gl.enable(gl.c.GL_DEBUG_OUTPUT); + + // Register our debug message callback with the OpenGL context. + gl.glad.context.DebugMessageCallback.?(glDebugMessageCallback, null); + + // Enable SRGB framebuffer for linear blending support. + try gl.enable(gl.c.GL_FRAMEBUFFER_SRGB); + + if (major < MIN_VERSION_MAJOR or + (major == MIN_VERSION_MAJOR and minor < MIN_VERSION_MINOR)) + { + log.warn( + "OpenGL version is too old. Ghostty requires OpenGL {d}.{d}", + .{ MIN_VERSION_MAJOR, MIN_VERSION_MINOR }, + ); + return error.OpenGLOutdated; + } +} + /// This is called early right after surface creation. pub fn surfaceInit(surface: *apprt.Surface) !void { // Treat this like a thread entry @@ -455,20 +162,8 @@ pub fn surfaceInit(surface: *apprt.Surface) !void { switch (apprt.runtime) { else => @compileError("unsupported app runtime for OpenGL"), - apprt.gtk => { - // GTK uses global OpenGL context so we load from null. - const version = try gl.glad.load(null); - const major = gl.glad.versionMajor(@intCast(version)); - const minor = gl.glad.versionMinor(@intCast(version)); - errdefer gl.glad.unload(); - log.info("loaded OpenGL {}.{}", .{ major, minor }); - - // We require at least OpenGL 3.3 - if (major < 3 or (major == 3 and minor < 3)) { - log.warn("OpenGL version is too old. Ghostty requires OpenGL 3.3", .{}); - return error.OpenGLOutdated; - } - }, + // GTK uses global OpenGL context so we load from null. + apprt.gtk => try prepareContext(null), apprt.glfw => try self.threadEnter(surface), @@ -489,69 +184,19 @@ pub fn surfaceInit(surface: *apprt.Surface) !void { // } } -/// This is called just prior to spinning up the renderer thread for -/// final main thread setup requirements. +/// This is called just prior to spinning up the renderer +/// thread for final main thread setup requirements. pub fn finalizeSurfaceInit(self: *const OpenGL, surface: *apprt.Surface) !void { _ = self; _ = surface; - // For GLFW, we grabbed the OpenGL context in surfaceInit and we - // need to release it before we start the renderer thread. + // For GLFW, we grabbed the OpenGL context in surfaceInit and + // we need to release it before we start the renderer thread. if (apprt.runtime == apprt.glfw) { glfw.makeContextCurrent(null); } } -/// Called when the OpenGL context is made invalid, so we need to free -/// all previous resources and stop rendering. -pub fn displayUnrealized(self: *OpenGL) void { - if (single_threaded_draw) self.draw_mutex.lock(); - defer if (single_threaded_draw) self.draw_mutex.unlock(); - - if (self.gl_state) |*v| { - v.deinit(self.alloc); - self.gl_state = null; - } -} - -/// Called when the OpenGL is ready to be initialized. -pub fn displayRealize(self: *OpenGL) !void { - if (single_threaded_draw) self.draw_mutex.lock(); - defer if (single_threaded_draw) self.draw_mutex.unlock(); - - // Make our new state - var gl_state = gl_state: { - self.font_grid.lock.lockShared(); - defer self.font_grid.lock.unlockShared(); - break :gl_state try GLState.init( - self.alloc, - self.config, - self.font_grid, - ); - }; - errdefer gl_state.deinit(); - - // Unrealize if we have to - if (self.gl_state) |*v| v.deinit(self.alloc); - - // Set our new state - self.gl_state = gl_state; - - // Make sure we invalidate all the fields so that we - // reflush everything - self.gl_cells_size = 0; - self.gl_cells_written = 0; - self.texture_grayscale_modified = 0; - self.texture_color_modified = 0; - self.texture_grayscale_resized = 0; - self.texture_color_resized = 0; - - // We need to reset our uniforms - self.deferred_screen_size = .{ .size = self.size }; - self.deferred_font_size = .{ .metrics = self.grid_metrics }; - self.deferred_config = .{}; -} - /// Callback called by renderer.Thread when it begins. pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void { _ = self; @@ -568,22 +213,17 @@ pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void { apprt.glfw => { // We need to make the OpenGL context current. OpenGL requires - // that a single thread own the a single OpenGL context (if any). This - // ensures that the context switches over to our thread. Important: - // the prior thread MUST have detached the context prior to calling - // this entrypoint. + // that a single thread own the a single OpenGL context (if any). + // This ensures that the context switches over to our thread. + // Important: the prior thread MUST have detached the context + // prior to calling this entrypoint. glfw.makeContextCurrent(surface.window); errdefer glfw.makeContextCurrent(null); glfw.swapInterval(1); // Load OpenGL bindings. This API is context-aware so this sets // a threadlocal context for these pointers. - const version = try gl.glad.load(&glfw.getProcAddress); - errdefer gl.glad.unload(); - log.info("loaded OpenGL {}.{}", .{ - gl.glad.versionMajor(@intCast(version)), - gl.glad.versionMinor(@intCast(version)), - }); + try prepareContext(&glfw.getProcAddress); }, apprt.embedded => { @@ -617,2068 +257,149 @@ pub fn threadExit(self: *const OpenGL) void { } } -/// True if our renderer has animations so that a higher frequency -/// timer is used. -pub fn hasAnimations(self: *const OpenGL) bool { - const state = self.gl_state orelse return false; - return state.custom != null; -} - -/// See Metal -pub fn hasVsync(self: *const OpenGL) bool { +pub fn displayRealized(self: *const OpenGL) void { _ = self; - // OpenGL currently never has vsync - return false; -} - -/// See Metal. -pub fn markDirty(self: *OpenGL) void { - // Do nothing, we don't have dirty tracking yet. - _ = self; -} - -/// Callback when the focus changes for the terminal this is rendering. -/// -/// Must be called on the render thread. -pub fn setFocus(self: *OpenGL, focus: bool) !void { - self.focused = focus; -} - -/// Callback when the window is visible or occluded. -/// -/// Must be called on the render thread. -pub fn setVisible(self: *OpenGL, visible: bool) void { - _ = self; - _ = visible; -} - -/// Set the new font grid. -/// -/// Must be called on the render thread. -pub fn setFontGrid(self: *OpenGL, grid: *font.SharedGrid) void { - if (single_threaded_draw) self.draw_mutex.lock(); - defer if (single_threaded_draw) self.draw_mutex.unlock(); - - // Reset our font grid - self.font_grid = grid; - self.grid_metrics = grid.metrics; - self.texture_grayscale_modified = 0; - self.texture_grayscale_resized = 0; - self.texture_color_modified = 0; - self.texture_color_resized = 0; - - // Reset our shaper cache. If our font changed (not just the size) then - // the data in the shaper cache may be invalid and cannot be used, so we - // always clear the cache just in case. - const font_shaper_cache = font.ShaperCache.init(); - self.font_shaper_cache.deinit(self.alloc); - self.font_shaper_cache = font_shaper_cache; - - // Update our screen size because the font grid can affect grid - // metrics which update uniforms. - self.deferred_screen_size = .{ .size = self.size }; - - // Defer our GPU updates - self.deferred_font_size = .{ .metrics = grid.metrics }; -} - -/// The primary render callback that is completely thread-safe. -pub fn updateFrame( - self: *OpenGL, - surface: *apprt.Surface, - state: *renderer.State, - cursor_blink_visible: bool, -) !void { - _ = surface; - - // Data we extract out of the critical area. - const Critical = struct { - full_rebuild: bool, - gl_bg: terminal.color.RGB, - screen: terminal.Screen, - screen_type: terminal.ScreenType, - mouse: renderer.State.Mouse, - preedit: ?renderer.State.Preedit, - cursor_style: ?renderer.CursorStyle, - color_palette: terminal.color.Palette, - }; - - // Update all our data as tightly as possible within the mutex. - var critical: Critical = critical: { - state.mutex.lock(); - defer state.mutex.unlock(); - - // If we're in a synchronized output state, we pause all rendering. - if (state.terminal.modes.get(.synchronized_output)) { - log.debug("synchronized output started, skipping render", .{}); - return; - } - - // Swap bg/fg if the terminal is reversed - const bg = self.background_color orelse self.default_background_color; - const fg = self.foreground_color orelse self.default_foreground_color; - defer { - if (self.background_color) |*c| { - c.* = bg; - } else { - self.default_background_color = bg; - } - - if (self.foreground_color) |*c| { - c.* = fg; - } else { - self.default_foreground_color = fg; - } - } - - if (state.terminal.modes.get(.reverse_colors)) { - if (self.background_color) |*c| { - c.* = fg; - } else { - self.default_background_color = fg; - } - - if (self.foreground_color) |*c| { - c.* = bg; - } else { - self.default_foreground_color = bg; - } - } - - // Get the viewport pin so that we can compare it to the current. - const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?; - - // We used to share terminal state, but we've since learned through - // analysis that it is faster to copy the terminal state than to - // hold the lock while rebuilding GPU cells. - var screen_copy = try state.terminal.screen.clone( - self.alloc, - .{ .viewport = .{} }, - null, - ); - errdefer screen_copy.deinit(); - - // Whether to draw our cursor or not. - const cursor_style = if (state.terminal.flags.password_input) - .lock - else - renderer.cursorStyle( - state, - self.focused, - cursor_blink_visible, - ); - - // Get our preedit state - const preedit: ?renderer.State.Preedit = preedit: { - if (cursor_style == null) break :preedit null; - const p = state.preedit orelse break :preedit null; - break :preedit try p.clone(self.alloc); - }; - errdefer if (preedit) |p| p.deinit(self.alloc); - - // If we have Kitty graphics data, we enter a SLOW SLOW SLOW path. - // We only do this if the Kitty image state is dirty meaning only if - // it changes. - // - // If we have any virtual references, we must also rebuild our - // kitty state on every frame because any cell change can move - // an image. - if (state.terminal.screen.kitty_images.dirty or - self.image_virtual) - { - // prepKittyGraphics touches self.images which is also used - // in drawFrame so if we're drawing on a separate thread we need - // to lock this. - if (single_threaded_draw) self.draw_mutex.lock(); - defer if (single_threaded_draw) self.draw_mutex.unlock(); - try self.prepKittyGraphics(state.terminal); - } - - // If we have any terminal dirty flags set then we need to rebuild - // the entire screen. This can be optimized in the future. - const full_rebuild: bool = rebuild: { - { - const Int = @typeInfo(terminal.Terminal.Dirty).@"struct".backing_integer.?; - const v: Int = @bitCast(state.terminal.flags.dirty); - if (v > 0) break :rebuild true; - } - { - const Int = @typeInfo(terminal.Screen.Dirty).@"struct".backing_integer.?; - const v: Int = @bitCast(state.terminal.screen.dirty); - if (v > 0) break :rebuild true; - } - - // If our viewport changed then we need to rebuild the entire - // screen because it means we scrolled. If we have no previous - // viewport then we must rebuild. - const prev_viewport = self.cells_viewport orelse break :rebuild true; - if (!prev_viewport.eql(viewport_pin)) break :rebuild true; - - break :rebuild false; - }; - - // Reset the dirty flags in the terminal and screen. We assume - // that our rebuild will be successful since so we optimize for - // success and reset while we hold the lock. This is much easier - // than coordinating row by row or as changes are persisted. - state.terminal.flags.dirty = .{}; - state.terminal.screen.dirty = .{}; - { - var it = state.terminal.screen.pages.pageIterator( - .right_down, - .{ .screen = .{} }, - null, - ); - while (it.next()) |chunk| { - var dirty_set = chunk.node.data.dirtyBitSet(); - dirty_set.unsetAll(); - } - } - - // Update our viewport pin for dirty tracking - self.cells_viewport = viewport_pin; - - break :critical .{ - .full_rebuild = full_rebuild, - .gl_bg = self.background_color orelse self.default_background_color, - .screen = screen_copy, - .screen_type = state.terminal.active_screen, - .mouse = state.mouse, - .preedit = preedit, - .cursor_style = cursor_style, - .color_palette = state.terminal.color_palette.colors, - }; - }; - defer { - critical.screen.deinit(); - if (critical.preedit) |p| p.deinit(self.alloc); - } - - // Grab our draw mutex if we have it and update our data - { - if (single_threaded_draw) self.draw_mutex.lock(); - defer if (single_threaded_draw) self.draw_mutex.unlock(); - - // Set our draw data - self.draw_background = critical.gl_bg; - - // Build our GPU cells - try self.rebuildCells( - critical.full_rebuild, - &critical.screen, - critical.screen_type, - critical.mouse, - critical.preedit, - critical.cursor_style, - &critical.color_palette, - ); - - // Notify our shaper we're done for the frame. For some shapers like - // CoreText this triggers off-thread cleanup logic. - self.font_shaper.endFrame(); - } -} - -/// This goes through the Kitty graphic placements and accumulates the -/// placements we need to render on our viewport. It also ensures that -/// the visible images are loaded on the GPU. -fn prepKittyGraphics( - self: *OpenGL, - t: *terminal.Terminal, -) !void { - const storage = &t.screen.kitty_images; - defer storage.dirty = false; - - // We always clear our previous placements no matter what because - // we rebuild them from scratch. - self.image_placements.clearRetainingCapacity(); - self.image_virtual = false; - - // Go through our known images and if there are any that are no longer - // in use then mark them to be freed. - // - // This never conflicts with the below because a placement can't - // reference an image that doesn't exist. - { - var it = self.images.iterator(); - while (it.next()) |kv| { - if (storage.imageById(kv.key_ptr.*) == null) { - kv.value_ptr.image.markForUnload(); - } - } - } - - // The top-left and bottom-right corners of our viewport in screen - // points. This lets us determine offsets and containment of placements. - const top = t.screen.pages.getTopLeft(.viewport); - const bot = t.screen.pages.getBottomRight(.viewport).?; - const top_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y; - const bot_y = t.screen.pages.pointFromPin(.screen, bot).?.screen.y; - - // Go through the placements and ensure the image is loaded on the GPU. - var it = storage.placements.iterator(); - while (it.next()) |kv| { - // Find the image in storage - const p = kv.value_ptr; - - // Special logic based on location - switch (p.location) { - .pin => {}, - .virtual => { - // We need to mark virtual placements on our renderer so that - // we know to rebuild in more scenarios since cell changes can - // now trigger placement changes. - self.image_virtual = true; - - // We also continue out because virtual placements are - // only triggered by the unicode placeholder, not by the - // placement itself. - continue; - }, - } - - const image = storage.imageById(kv.key_ptr.image_id) orelse { - log.warn( - "missing image for placement, ignoring image_id={}", - .{kv.key_ptr.image_id}, - ); - continue; - }; - - try self.prepKittyPlacement(t, top_y, bot_y, &image, p); - } - - // If we have virtual placements then we need to scan for placeholders. - if (self.image_virtual) { - var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot); - while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement( - t, - &virtual_p, - ); - } - - // Sort the placements by their Z value. - std.mem.sortUnstable( - gl_image.Placement, - self.image_placements.items, - {}, - struct { - fn lessThan( - ctx: void, - lhs: gl_image.Placement, - rhs: gl_image.Placement, - ) bool { - _ = ctx; - return lhs.z < rhs.z or (lhs.z == rhs.z and lhs.image_id < rhs.image_id); - } - }.lessThan, - ); - - // Find our indices. The values are sorted by z so we can find the - // first placement out of bounds to find the limits. - var bg_end: ?u32 = null; - var text_end: ?u32 = null; - const bg_limit = std.math.minInt(i32) / 2; - for (self.image_placements.items, 0..) |p, i| { - if (bg_end == null and p.z >= bg_limit) { - bg_end = @intCast(i); - } - if (text_end == null and p.z >= 0) { - text_end = @intCast(i); - } - } - - self.image_bg_end = bg_end orelse 0; - self.image_text_end = text_end orelse self.image_bg_end; -} - -fn prepKittyVirtualPlacement( - self: *OpenGL, - t: *terminal.Terminal, - p: *const terminal.kitty.graphics.unicode.Placement, -) !void { - const storage = &t.screen.kitty_images; - const image = storage.imageById(p.image_id) orelse { - log.warn( - "missing image for virtual placement, ignoring image_id={}", - .{p.image_id}, - ); - return; - }; - - const rp = p.renderPlacement( - storage, - &image, - self.grid_metrics.cell_width, - self.grid_metrics.cell_height, - ) catch |err| { - log.warn("error rendering virtual placement err={}", .{err}); - return; - }; - - // If our placement is zero sized then we don't do anything. - if (rp.dest_width == 0 or rp.dest_height == 0) return; - - const viewport: terminal.point.Point = t.screen.pages.pointFromPin( - .viewport, - rp.top_left, - ) orelse { - // This is unreachable with virtual placements because we should - // only ever be looking at virtual placements that are in our - // viewport in the renderer and virtual placements only ever take - // up one row. - unreachable; - }; - - // Send our image to the GPU and store the placement for rendering. - try self.prepKittyImage(&image); - try self.image_placements.append(self.alloc, .{ - .image_id = image.id, - .x = @intCast(rp.top_left.x), - .y = @intCast(viewport.viewport.y), - .z = -1, - .width = rp.dest_width, - .height = rp.dest_height, - .cell_offset_x = rp.offset_x, - .cell_offset_y = rp.offset_y, - .source_x = rp.source_x, - .source_y = rp.source_y, - .source_width = rp.source_width, - .source_height = rp.source_height, - }); -} - -fn prepKittyPlacement( - self: *OpenGL, - t: *terminal.Terminal, - top_y: u32, - bot_y: u32, - image: *const terminal.kitty.graphics.Image, - p: *const terminal.kitty.graphics.ImageStorage.Placement, -) !void { - // Get the rect for the placement. If this placement doesn't have - // a rect then its virtual or something so skip it. - const rect = p.rect(image.*, t) orelse return; - - // This is expensive but necessary. - const img_top_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y; - const img_bot_y = t.screen.pages.pointFromPin(.screen, rect.bottom_right).?.screen.y; - - // If the selection isn't within our viewport then skip it. - if (img_top_y > bot_y) return; - if (img_bot_y < top_y) return; - - // We need to prep this image for upload if it isn't in the cache OR - // it is in the cache but the transmit time doesn't match meaning this - // image is different. - try self.prepKittyImage(image); - - // Calculate the dimensions of our image, taking in to - // account the rows / columns specified by the placement. - const dest_size = p.calculatedSize(image.*, t); - - // Calculate the source rectangle - const source_x = @min(image.width, p.source_x); - const source_y = @min(image.height, p.source_y); - const source_width = if (p.source_width > 0) - @min(image.width - source_x, p.source_width) - else - image.width; - const source_height = if (p.source_height > 0) - @min(image.height - source_y, p.source_height) - else - image.height; - - // Get the viewport-relative Y position of the placement. - const y_pos: i32 = @as(i32, @intCast(img_top_y)) - @as(i32, @intCast(top_y)); - - // Accumulate the placement - if (dest_size.width > 0 and dest_size.height > 0) { - try self.image_placements.append(self.alloc, .{ - .image_id = image.id, - .x = @intCast(rect.top_left.x), - .y = y_pos, - .z = p.z, - .width = dest_size.width, - .height = dest_size.height, - .cell_offset_x = p.x_offset, - .cell_offset_y = p.y_offset, - .source_x = source_x, - .source_y = source_y, - .source_width = source_width, - .source_height = source_height, - }); - } -} - -fn prepKittyImage( - self: *OpenGL, - image: *const terminal.kitty.graphics.Image, -) !void { - // We need to prep this image for upload if it isn't in the cache OR - // it is in the cache but the transmit time doesn't match meaning this - // image is different. - const gop = try self.images.getOrPut(self.alloc, image.id); - if (gop.found_existing and - gop.value_ptr.transmit_time.order(image.transmit_time) == .eq) - { - return; - } - - // Copy the data into the pending state. - const data = try self.alloc.dupe(u8, image.data); - errdefer self.alloc.free(data); - - // Store it in the map - const pending: Image.Pending = .{ - .width = image.width, - .height = image.height, - .data = data.ptr, - }; - - const new_image: Image = switch (image.format) { - .gray => .{ .pending_gray = pending }, - .gray_alpha => .{ .pending_gray_alpha = pending }, - .rgb => .{ .pending_rgb = pending }, - .rgba => .{ .pending_rgba = pending }, - .png => unreachable, // should be decoded by now - }; - - if (!gop.found_existing) { - gop.value_ptr.* = .{ - .image = new_image, - .transmit_time = undefined, - }; - } else { - try gop.value_ptr.image.markForReplace( - self.alloc, - new_image, - ); - } - - gop.value_ptr.transmit_time = image.transmit_time; -} - -/// rebuildCells rebuilds all the GPU cells from our CPU state. This is a -/// slow operation but ensures that the GPU state exactly matches the CPU state. -/// In steady-state operation, we use some GPU tricks to send down stale data -/// that is ignored. This accumulates more memory; rebuildCells clears it. -/// -/// Note this doesn't have to typically be manually called. Internally, -/// the renderer will do this when it needs more memory space. -pub fn rebuildCells( - self: *OpenGL, - rebuild: bool, - screen: *terminal.Screen, - screen_type: terminal.ScreenType, - mouse: renderer.State.Mouse, - preedit: ?renderer.State.Preedit, - cursor_style_: ?renderer.CursorStyle, - color_palette: *const terminal.color.Palette, -) !void { - _ = screen_type; - - // Bg cells at most will need space for the visible screen size - self.cells_bg.clearRetainingCapacity(); - self.cells.clearRetainingCapacity(); - - // Create an arena for all our temporary allocations while rebuilding - var arena = ArenaAllocator.init(self.alloc); - defer arena.deinit(); - const arena_alloc = arena.allocator(); - - // We've written no data to the GPU, refresh it all - self.gl_cells_written = 0; - - // Create our match set for the links. - var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet( - arena_alloc, - screen, - mouse_pt, - mouse.mods, - ) else .{}; - - // Determine our x/y range for preedit. We don't want to render anything - // here because we will render the preedit separately. - const preedit_range: ?struct { - y: terminal.size.CellCountInt, - x: [2]terminal.size.CellCountInt, - cp_offset: usize, - } = if (preedit) |preedit_v| preedit: { - const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1); - break :preedit .{ - .y = screen.cursor.y, - .x = .{ range.start, range.end }, - .cp_offset = range.cp_offset, - }; - } else null; - - // These are all the foreground cells underneath the cursor. - // - // We keep track of these so that we can invert the colors and move them - // in front of the block cursor so that the character remains visible. - // - // We init with a capacity of 4 to account for decorations such - // as underline and strikethrough, as well as combining chars. - var cursor_cells = try std.ArrayListUnmanaged(CellProgram.Cell).initCapacity(arena_alloc, 4); - defer cursor_cells.deinit(arena_alloc); - - if (rebuild) { - switch (self.config.padding_color) { - .background => {}, - - .extend, .@"extend-always" => { - self.padding_extend_top = true; - self.padding_extend_bottom = true; - }, - } - } - - const grid_size = self.size.grid(); - - // We rebuild the cells row-by-row because we do font shaping by row. - var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null); - // If our cell contents buffer is shorter than the screen viewport, - // we render the rows that fit, starting from the bottom. If instead - // the viewport is shorter than the cell contents buffer, we align - // the top of the viewport with the top of the contents buffer. - var y: terminal.size.CellCountInt = @min( - screen.pages.rows, - grid_size.rows, - ); - while (row_it.next()) |row| { - // The viewport may have more rows than our cell contents, - // so we need to break from the loop early if we hit y = 0. - if (y == 0) break; - - y -= 1; - - // True if we want to do font shaping around the cursor. We want to - // do font shaping as long as the cursor is enabled. - const shape_cursor = screen.viewportIsBottom() and - y == screen.cursor.y; - - // If this is the row with our cursor, then we may have to modify - // the cell with the cursor. - const start_i: usize = self.cells.items.len; - defer if (shape_cursor and cursor_style_ == .block) { - const x = screen.cursor.x; - const wide = row.cells(.all)[x].wide; - const min_x = switch (wide) { - .narrow, .spacer_head, .wide => x, - .spacer_tail => x -| 1, - }; - const max_x = switch (wide) { - .narrow, .spacer_head, .spacer_tail => x, - .wide => x +| 1, - }; - for (self.cells.items[start_i..]) |cell| { - if (cell.grid_col < min_x or cell.grid_col > max_x) continue; - if (cell.mode.isFg()) { - cursor_cells.append(arena_alloc, cell) catch { - // We silently ignore if this fails because - // worst case scenario some combining glyphs - // aren't visible under the cursor '\_('-')_/' - }; - } - } - }; - - // We need to get this row's selection if there is one for proper - // run splitting. - const row_selection = sel: { - const sel = screen.selection orelse break :sel null; - const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse - break :sel null; - break :sel sel.containedRow(screen, pin) orelse null; - }; - - // On primary screen, we still apply vertical padding extension - // under certain conditions we feel are safe. This helps make some - // scenarios look better while avoiding scenarios we know do NOT look - // good. - switch (self.config.padding_color) { - // These already have the correct values set above. - .background, .@"extend-always" => {}, - - // Apply heuristics for padding extension. - .extend => if (y == 0) { - self.padding_extend_top = !row.neverExtendBg( - color_palette, - self.background_color orelse self.default_background_color, - ); - } else if (y == self.size.grid().rows - 1) { - self.padding_extend_bottom = !row.neverExtendBg( - color_palette, - self.background_color orelse self.default_background_color, - ); - }, - } - - // Iterator of runs for shaping. - var run_iter = self.font_shaper.runIterator( - self.font_grid, - screen, - row, - row_selection, - if (shape_cursor) screen.cursor.x else null, - ); - var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); - var shaper_cells: ?[]const font.shape.Cell = null; - var shaper_cells_i: usize = 0; - - const row_cells_all = row.cells(.all); - - // If our viewport is wider than our cell contents buffer, - // we still only process cells up to the width of the buffer. - const row_cells = row_cells_all[0..@min(row_cells_all.len, grid_size.columns)]; - - for (row_cells, 0..) |*cell, x| { - // If this cell falls within our preedit range then we - // skip this because preedits are setup separately. - if (preedit_range) |range| preedit: { - // We're not on the preedit line, no actions necessary. - if (range.y != y) break :preedit; - // We're before the preedit range, no actions necessary. - if (x < range.x[0]) break :preedit; - // We're in the preedit range, skip this cell. - if (x <= range.x[1]) continue; - // After exiting the preedit range we need to catch - // the run position up because of the missed cells. - // In all other cases, no action is necessary. - if (x != range.x[1] + 1) break :preedit; - - // Step the run iterator until we find a run that ends - // after the current cell, which will be the soonest run - // that might contain glyphs for our cell. - while (shaper_run) |run| { - if (run.offset + run.cells > x) break; - shaper_run = try run_iter.next(self.alloc); - shaper_cells = null; - shaper_cells_i = 0; - } - - const run = shaper_run orelse break :preedit; - - // If we haven't shaped this run, do so now. - shaper_cells = shaper_cells orelse - // Try to read the cells from the shaping cache if we can. - self.font_shaper_cache.get(run) orelse - cache: { - // Otherwise we have to shape them. - const cells = try self.font_shaper.shape(run); - - // Try to cache them. If caching fails for any reason we - // continue because it is just a performance optimization, - // not a correctness issue. - self.font_shaper_cache.put( - self.alloc, - run, - cells, - ) catch |err| { - log.warn( - "error caching font shaping results err={}", - .{err}, - ); - }; - - // The cells we get from direct shaping are always owned - // by the shaper and valid until the next shaping call so - // we can safely use them. - break :cache cells; - }; - - // Advance our index until we reach or pass - // our current x position in the shaper cells. - while (shaper_cells.?[shaper_cells_i].x < x) { - shaper_cells_i += 1; - } - } - - const wide = cell.wide; - - const style = row.style(cell); - - const cell_pin: terminal.Pin = cell: { - var copy = row; - copy.x = @intCast(x); - break :cell copy; - }; - - // True if this cell is selected - const selected: bool = if (screen.selection) |sel| - sel.contains(screen, .{ - .node = row.node, - .y = row.y, - .x = @intCast( - // Spacer tails should show the selection - // state of the wide cell they belong to. - if (wide == .spacer_tail) - x -| 1 - else - x, - ), - }) - else - false; - - const bg_style = style.bg(cell, color_palette); - const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color; - - // The final background color for the cell. - const bg = bg: { - if (selected) { - break :bg if (self.config.invert_selection_fg_bg) - if (style.flags.inverse) - // Cell is selected with invert selection fg/bg - // enabled, and the cell has the inverse style - // flag, so they cancel out and we get the normal - // bg color. - bg_style - else - // If it doesn't have the inverse style - // flag then we use the fg color instead. - fg_style - else - // If we don't have invert selection fg/bg set then we - // just use the selection background if set, otherwise - // the default fg color. - break :bg self.config.selection_background orelse self.foreground_color orelse self.default_foreground_color; - } - - // Not selected - break :bg if (style.flags.inverse != isCovering(cell.codepoint())) - // Two cases cause us to invert (use the fg color as the bg) - // - The "inverse" style flag. - // - A "covering" glyph; we use fg for bg in that case to - // help make sure that padding extension works correctly. - // If one of these is true (but not the other) - // then we use the fg style color for the bg. - fg_style - else - // Otherwise they cancel out. - bg_style; - }; - - const fg = fg: { - if (selected and !self.config.invert_selection_fg_bg) { - // If we don't have invert selection fg/bg set - // then we just use the selection foreground if - // set, otherwise the default bg color. - break :fg self.config.selection_foreground orelse self.background_color orelse self.default_background_color; - } - - // Whether we need to use the bg color as our fg color: - // - Cell is inverted and not selected - // - Cell is selected and not inverted - // Note: if selected then invert sel fg / bg must be - // false since we separately handle it if true above. - break :fg if (style.flags.inverse != selected) - bg_style orelse self.background_color orelse self.default_background_color - else - fg_style; - }; - - // Foreground alpha for this cell. - const alpha: u8 = if (style.flags.faint) 175 else 255; - - // If the cell has a background color, set it. - const bg_color: [4]u8 = if (bg) |rgb| bg: { - // Determine our background alpha. If we have transparency configured - // then this is dynamic depending on some situations. This is all - // in an attempt to make transparency look the best for various - // situations. See inline comments. - const bg_alpha: u8 = bg_alpha: { - const default: u8 = 255; - - if (self.config.background_opacity >= 1) break :bg_alpha default; - - // If we're selected, we do not apply background opacity - if (selected) break :bg_alpha default; - - // If we're reversed, do not apply background opacity - if (style.flags.inverse) break :bg_alpha default; - - // If we have a background and its not the default background - // then we apply background opacity - if (style.bg(cell, color_palette) != null and !rgb.eql(self.background_color orelse self.default_background_color)) { - break :bg_alpha default; - } - - // We apply background opacity. - var bg_alpha: f64 = @floatFromInt(default); - bg_alpha *= self.config.background_opacity; - bg_alpha = @ceil(bg_alpha); - break :bg_alpha @intFromFloat(bg_alpha); - }; - - try self.cells_bg.append(self.alloc, .{ - .mode = .bg, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = cell.gridWidth(), - .glyph_x = 0, - .glyph_y = 0, - .glyph_width = 0, - .glyph_height = 0, - .glyph_offset_x = 0, - .glyph_offset_y = 0, - .r = rgb.r, - .g = rgb.g, - .b = rgb.b, - .a = bg_alpha, - .bg_r = 0, - .bg_g = 0, - .bg_b = 0, - .bg_a = 0, - }); - - break :bg .{ - rgb.r, rgb.g, rgb.b, bg_alpha, - }; - } else .{ - self.draw_background.r, - self.draw_background.g, - self.draw_background.b, - @intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))), - }; - - // If the invisible flag is set on this cell then we - // don't need to render any foreground elements, so - // we just skip all glyphs with this x coordinate. - // - // NOTE: This behavior matches xterm. Some other terminal - // emulators, e.g. Alacritty, still render text decorations - // and only make the text itself invisible. The decision - // has been made here to match xterm's behavior for this. - if (style.flags.invisible) { - continue; - } - - // Give links a single underline, unless they already have - // an underline, in which case use a double underline to - // distinguish them. - const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin)) - if (style.flags.underline == .single) - .double - else - .single - else - style.flags.underline; - - // We draw underlines first so that they layer underneath text. - // This improves readability when a colored underline is used - // which intersects parts of the text (descenders). - if (underline != .none) self.addUnderline( - @intCast(x), - @intCast(y), - underline, - style.underlineColor(color_palette) orelse fg, - alpha, - bg_color, - ) catch |err| { - log.warn( - "error adding underline to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - - if (style.flags.overline) self.addOverline( - @intCast(x), - @intCast(y), - fg, - alpha, - bg_color, - ) catch |err| { - log.warn( - "error adding overline to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - - // If we're at or past the end of our shaper run then - // we need to get the next run from the run iterator. - if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) { - shaper_run = try run_iter.next(self.alloc); - shaper_cells = null; - shaper_cells_i = 0; - } - - if (shaper_run) |run| glyphs: { - // If we haven't shaped this run yet, do so. - shaper_cells = shaper_cells orelse - // Try to read the cells from the shaping cache if we can. - self.font_shaper_cache.get(run) orelse - cache: { - // Otherwise we have to shape them. - const cells = try self.font_shaper.shape(run); - - // Try to cache them. If caching fails for any reason we - // continue because it is just a performance optimization, - // not a correctness issue. - self.font_shaper_cache.put( - self.alloc, - run, - cells, - ) catch |err| { - log.warn( - "error caching font shaping results err={}", - .{err}, - ); - }; - - // The cells we get from direct shaping are always owned - // by the shaper and valid until the next shaping call so - // we can safely use them. - break :cache cells; - }; - - const cells = shaper_cells orelse break :glyphs; - - // If there are no shaper cells for this run, ignore it. - // This can occur for runs of empty cells, and is fine. - if (cells.len == 0) break :glyphs; - - // If we encounter a shaper cell to the left of the current - // cell then we have some problems. This logic relies on x - // position monotonically increasing. - assert(cells[shaper_cells_i].x >= x); - - // NOTE: An assumption is made here that a single cell will never - // be present in more than one shaper run. If that assumption is - // violated, this logic breaks. - - while (shaper_cells_i < cells.len and cells[shaper_cells_i].x == x) : ({ - shaper_cells_i += 1; - }) { - self.addGlyph( - @intCast(x), - @intCast(y), - cell_pin, - cells[shaper_cells_i], - shaper_run.?, - fg, - alpha, - bg_color, - ) catch |err| { - log.warn( - "error adding glyph to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - } - } - - // Finally, draw a strikethrough if necessary. - if (style.flags.strikethrough) self.addStrikethrough( - @intCast(x), - @intCast(y), - fg, - alpha, - bg_color, - ) catch |err| { - log.warn( - "error adding strikethrough to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - } - } - - // Add the cursor at the end so that it overlays everything. If we have - // a cursor cell then we invert the colors on that and add it in so - // that we can always see it. - if (cursor_style_) |cursor_style| cursor_style: { - // If we have a preedit, we try to render the preedit text on top - // of the cursor. - if (preedit) |preedit_v| { - const range = preedit_range.?; - var x = range.x[0]; - for (preedit_v.codepoints[range.cp_offset..]) |cp| { - self.addPreeditCell(cp, x, range.y) catch |err| { - log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ - x, - range.y, - err, - }); - }; - - x += if (cp.wide) 2 else 1; - } - - // Preedit hides the cursor - break :cursor_style; - } - - const cursor_color = self.cursor_color orelse self.default_cursor_color orelse color: { - if (self.cursor_invert) { - // Use the foreground color from the cell under the cursor, if any. - const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); - break :color if (sty.flags.inverse) - // If the cell is reversed, use background color instead. - (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color) - else - (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color); - } else { - break :color self.foreground_color orelse self.default_foreground_color; - } - }; - - _ = try self.addCursor(screen, cursor_style, cursor_color); - for (cursor_cells.items) |*cell| { - if (cell.mode.isFg() and cell.mode != .fg_color) { - const cell_color = if (self.cursor_invert) blk: { - // Use the background color from the cell under the cursor, if any. - const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); - break :blk if (sty.flags.inverse) - // If the cell is reversed, use foreground color instead. - (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color) - else - (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color); - } else if (self.config.cursor_text) |txt| - txt - else - self.background_color orelse self.default_background_color; - - cell.r = cell_color.r; - cell.g = cell_color.g; - cell.b = cell_color.b; - cell.a = 255; - } - try self.cells.append(self.alloc, cell.*); - } - } - - // Free up memory, generally in case where surface has shrunk. - // If more than half of the capacity is unused, remove all unused capacity. - if (self.cells.items.len * 2 < self.cells.capacity) { - self.cells.shrinkAndFree(self.alloc, self.cells.items.len); - } - if (self.cells_bg.items.len * 2 < self.cells_bg.capacity) { - self.cells_bg.shrinkAndFree(self.alloc, self.cells_bg.items.len); - } - - // Some debug mode safety checks - if (std.debug.runtime_safety) { - for (self.cells_bg.items) |cell| assert(cell.mode == .bg); - for (self.cells.items) |cell| assert(cell.mode != .bg); - } -} - -fn addPreeditCell( - self: *OpenGL, - cp: renderer.State.Preedit.Codepoint, - x: usize, - y: usize, -) !void { - // Preedit is rendered inverted - const bg = self.foreground_color orelse self.default_foreground_color; - const fg = self.background_color orelse self.default_background_color; - - // Render the glyph for our preedit text - const render_ = self.font_grid.renderCodepoint( - self.alloc, - @intCast(cp.codepoint), - .regular, - .text, - .{ .grid_metrics = self.grid_metrics }, - ) catch |err| { - log.warn("error rendering preedit glyph err={}", .{err}); - return; - }; - const render = render_ orelse { - log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint}); - return; - }; - - // Add our opaque background cell - try self.cells_bg.append(self.alloc, .{ - .mode = .bg, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = if (cp.wide) 2 else 1, - .glyph_x = 0, - .glyph_y = 0, - .glyph_width = 0, - .glyph_height = 0, - .glyph_offset_x = 0, - .glyph_offset_y = 0, - .r = bg.r, - .g = bg.g, - .b = bg.b, - .a = 255, - .bg_r = 0, - .bg_g = 0, - .bg_b = 0, - .bg_a = 0, - }); - - // Add our text - try self.cells.append(self.alloc, .{ - .mode = .fg, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = if (cp.wide) 2 else 1, - .glyph_x = render.glyph.atlas_x, - .glyph_y = render.glyph.atlas_y, - .glyph_width = render.glyph.width, - .glyph_height = render.glyph.height, - .glyph_offset_x = render.glyph.offset_x, - .glyph_offset_y = render.glyph.offset_y, - .r = fg.r, - .g = fg.g, - .b = fg.b, - .a = 255, - .bg_r = bg.r, - .bg_g = bg.g, - .bg_b = bg.b, - .bg_a = 255, - }); -} - -fn addCursor( - self: *OpenGL, - screen: *terminal.Screen, - cursor_style: renderer.CursorStyle, - cursor_color: terminal.color.RGB, -) !?*const CellProgram.Cell { - // Add the cursor. We render the cursor over the wide character if - // we're on the wide character tail. - const wide, const x = cell: { - // The cursor goes over the screen cursor position. - const cell = screen.cursor.page_cell; - if (cell.wide != .spacer_tail or screen.cursor.x == 0) - break :cell .{ cell.wide == .wide, screen.cursor.x }; - - // If we're part of a wide character, we move the cursor back to - // the actual character. - const prev_cell = screen.cursorCellLeft(1); - break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 }; - }; - - const alpha: u8 = if (!self.focused) 255 else alpha: { - const alpha = 255 * self.config.cursor_opacity; - break :alpha @intFromFloat(@ceil(alpha)); - }; - - const render = switch (cursor_style) { - .block, - .block_hollow, - .bar, - .underline, - => render: { - const sprite: font.Sprite = switch (cursor_style) { - .block => .cursor_rect, - .block_hollow => .cursor_hollow_rect, - .bar => .cursor_bar, - .underline => .underline, - .lock => unreachable, - }; - - break :render self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(sprite), - .{ - .cell_width = if (wide) 2 else 1, - .grid_metrics = self.grid_metrics, - }, - ) catch |err| { - log.warn("error rendering cursor glyph err={}", .{err}); - return null; - }; - }, - - .lock => self.font_grid.renderCodepoint( - self.alloc, - 0xF023, // lock symbol - .regular, - .text, - .{ - .cell_width = if (wide) 2 else 1, - .grid_metrics = self.grid_metrics, - }, - ) catch |err| { - log.warn("error rendering cursor glyph err={}", .{err}); - return null; - } orelse { - // This should never happen because we embed nerd - // fonts so we just log and return instead of fallback. - log.warn("failed to find lock symbol for cursor codepoint=0xF023", .{}); - return null; - }, - }; - - try self.cells.append(self.alloc, .{ - .mode = .fg, - .grid_col = @intCast(x), - .grid_row = @intCast(screen.cursor.y), - .grid_width = if (wide) 2 else 1, - .r = cursor_color.r, - .g = cursor_color.g, - .b = cursor_color.b, - .a = alpha, - .bg_r = 0, - .bg_g = 0, - .bg_b = 0, - .bg_a = 0, - .glyph_x = render.glyph.atlas_x, - .glyph_y = render.glyph.atlas_y, - .glyph_width = render.glyph.width, - .glyph_height = render.glyph.height, - .glyph_offset_x = render.glyph.offset_x, - .glyph_offset_y = render.glyph.offset_y, - }); - - return &self.cells.items[self.cells.items.len - 1]; -} - -/// Add an underline decoration to the specified cell -fn addUnderline( - self: *OpenGL, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, - style: terminal.Attribute.Underline, - color: terminal.color.RGB, - alpha: u8, - bg: [4]u8, -) !void { - const sprite: font.Sprite = switch (style) { - .none => unreachable, - .single => .underline, - .double => .underline_double, - .dotted => .underline_dotted, - .dashed => .underline_dashed, - .curly => .underline_curly, - }; - - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(sprite), - .{ - .cell_width = 1, - .grid_metrics = self.grid_metrics, - }, - ); - - try self.cells.append(self.alloc, .{ - .mode = .fg, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = 1, - .glyph_x = render.glyph.atlas_x, - .glyph_y = render.glyph.atlas_y, - .glyph_width = render.glyph.width, - .glyph_height = render.glyph.height, - .glyph_offset_x = render.glyph.offset_x, - .glyph_offset_y = render.glyph.offset_y, - .r = color.r, - .g = color.g, - .b = color.b, - .a = alpha, - .bg_r = bg[0], - .bg_g = bg[1], - .bg_b = bg[2], - .bg_a = bg[3], - }); -} - -/// Add an overline decoration to the specified cell -fn addOverline( - self: *OpenGL, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, - color: terminal.color.RGB, - alpha: u8, - bg: [4]u8, -) !void { - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(font.Sprite.overline), - .{ - .cell_width = 1, - .grid_metrics = self.grid_metrics, - }, - ); - - try self.cells.append(self.alloc, .{ - .mode = .fg, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = 1, - .glyph_x = render.glyph.atlas_x, - .glyph_y = render.glyph.atlas_y, - .glyph_width = render.glyph.width, - .glyph_height = render.glyph.height, - .glyph_offset_x = render.glyph.offset_x, - .glyph_offset_y = render.glyph.offset_y, - .r = color.r, - .g = color.g, - .b = color.b, - .a = alpha, - .bg_r = bg[0], - .bg_g = bg[1], - .bg_b = bg[2], - .bg_a = bg[3], - }); -} - -/// Add a strikethrough decoration to the specified cell -fn addStrikethrough( - self: *OpenGL, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, - color: terminal.color.RGB, - alpha: u8, - bg: [4]u8, -) !void { - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(font.Sprite.strikethrough), - .{ - .cell_width = 1, - .grid_metrics = self.grid_metrics, - }, - ); - - try self.cells.append(self.alloc, .{ - .mode = .fg, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = 1, - .glyph_x = render.glyph.atlas_x, - .glyph_y = render.glyph.atlas_y, - .glyph_width = render.glyph.width, - .glyph_height = render.glyph.height, - .glyph_offset_x = render.glyph.offset_x, - .glyph_offset_y = render.glyph.offset_y, - .r = color.r, - .g = color.g, - .b = color.b, - .a = alpha, - .bg_r = bg[0], - .bg_g = bg[1], - .bg_b = bg[2], - .bg_a = bg[3], - }); -} - -// Add a glyph to the specified cell. -fn addGlyph( - self: *OpenGL, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, - cell_pin: terminal.Pin, - shaper_cell: font.shape.Cell, - shaper_run: font.shape.TextRun, - color: terminal.color.RGB, - alpha: u8, - bg: [4]u8, -) !void { - const rac = cell_pin.rowAndCell(); - const cell = rac.cell; - - // Render - const render = try self.font_grid.renderGlyph( - self.alloc, - shaper_run.font_index, - shaper_cell.glyph_index, - .{ - .cell_width = if (cell.wide == .wide) 2 else 1, - .grid_metrics = self.grid_metrics, - .thicken = self.config.font_thicken, - .thicken_strength = self.config.font_thicken_strength, - }, - ); - - // If the glyph is 0 width or height, it will be invisible - // when drawn, so don't bother adding it to the buffer. - if (render.glyph.width == 0 or render.glyph.height == 0) { - return; - } - - // If we're rendering a color font, we use the color atlas - const mode: CellProgram.CellMode = switch (try fgMode( - render.presentation, - cell_pin, - )) { - .normal => .fg, - .color => .fg_color, - .constrained => .fg_constrained, - .powerline => .fg_powerline, - }; - - try self.cells.append(self.alloc, .{ - .mode = mode, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = cell.gridWidth(), - .glyph_x = render.glyph.atlas_x, - .glyph_y = render.glyph.atlas_y, - .glyph_width = render.glyph.width, - .glyph_height = render.glyph.height, - .glyph_offset_x = render.glyph.offset_x + shaper_cell.x_offset, - .glyph_offset_y = render.glyph.offset_y + shaper_cell.y_offset, - .r = color.r, - .g = color.g, - .b = color.b, - .a = alpha, - .bg_r = bg[0], - .bg_g = bg[1], - .bg_b = bg[2], - .bg_a = bg[3], - }); -} - -/// Update the configuration. -pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void { - // We always redo the font shaper in case font features changed. We - // could check to see if there was an actual config change but this is - // easier and rare enough to not cause performance issues. - { - var font_shaper = try font.Shaper.init(self.alloc, .{ - .features = config.font_features.items, - }); - errdefer font_shaper.deinit(); - self.font_shaper.deinit(); - self.font_shaper = font_shaper; - } - - // We also need to reset the shaper cache so shaper info - // from the previous font isn't re-used for the new font. - const font_shaper_cache = font.ShaperCache.init(); - self.font_shaper_cache.deinit(self.alloc); - self.font_shaper_cache = font_shaper_cache; - - // Set our new colors - self.default_background_color = config.background; - self.default_foreground_color = config.foreground; - self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null; - self.cursor_invert = config.cursor_invert; - - // Update our uniforms - self.deferred_config = .{}; - - self.config.deinit(); - self.config = config.*; -} - -/// Set the screen size for rendering. This will update the projection -/// used for the shader so that the scaling of the grid is correct. -pub fn setScreenSize( - self: *OpenGL, - size: renderer.Size, -) !void { - if (single_threaded_draw) self.draw_mutex.lock(); - defer if (single_threaded_draw) self.draw_mutex.unlock(); - - // Store our screen size - self.size = size; - - // Defer our OpenGL updates - self.deferred_screen_size = .{ .size = size }; - - log.debug("screen size size={}", .{size}); -} - -/// Updates the font texture atlas if it is dirty. -fn flushAtlas(self: *OpenGL) !void { - const gl_state = self.gl_state orelse return; - try flushAtlasSingle( - &self.font_grid.lock, - gl_state.texture, - &self.font_grid.atlas_grayscale, - &self.texture_grayscale_modified, - &self.texture_grayscale_resized, - .red, - .red, - ); - try flushAtlasSingle( - &self.font_grid.lock, - gl_state.texture_color, - &self.font_grid.atlas_color, - &self.texture_color_modified, - &self.texture_color_resized, - .rgba, - .bgra, - ); -} - -/// Flush a single atlas, grabbing all necessary locks, checking for -/// changes, etc. -fn flushAtlasSingle( - lock: *std.Thread.RwLock, - texture: gl.Texture, - atlas: *font.Atlas, - modified: *usize, - resized: *usize, - internal_format: gl.Texture.InternalFormat, - format: gl.Texture.Format, -) !void { - // If the texture isn't modified we do nothing - const new_modified = atlas.modified.load(.monotonic); - if (new_modified <= modified.*) return; - - // If it is modified we need to grab a read-lock - lock.lockShared(); - defer lock.unlockShared(); - - var texbind = try texture.bind(.@"2D"); - defer texbind.unbind(); - - const new_resized = atlas.resized.load(.monotonic); - if (new_resized > resized.*) { - try texbind.image2D( - 0, - internal_format, - @intCast(atlas.size), - @intCast(atlas.size), - 0, - format, - .UnsignedByte, - atlas.data.ptr, - ); - - // Only update the resized number after successful resize - resized.* = new_resized; - } else { - try texbind.subImage2D( - 0, - 0, - 0, - @intCast(atlas.size), - @intCast(atlas.size), - format, - .UnsignedByte, - atlas.data.ptr, - ); - } - - // Update our modified tracker after successful update - modified.* = atlas.modified.load(.monotonic); -} - -/// Render renders the current cell state. This will not modify any of -/// the cells. -pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { - // If we're in single-threaded more we grab a lock since we use shared data. - if (single_threaded_draw) self.draw_mutex.lock(); - defer if (single_threaded_draw) self.draw_mutex.unlock(); - const gl_state: *GLState = if (self.gl_state) |*v| v else return; - - // Go through our images and see if we need to setup any textures. - { - var image_it = self.images.iterator(); - while (image_it.next()) |kv| { - switch (kv.value_ptr.image) { - .ready => {}, - - .pending_gray, - .pending_gray_alpha, - .pending_rgb, - .pending_rgba, - .replace_gray, - .replace_gray_alpha, - .replace_rgb, - .replace_rgba, - => try kv.value_ptr.image.upload(self.alloc), - - .unload_pending, - .unload_replace, - .unload_ready, - => { - kv.value_ptr.image.deinit(self.alloc); - self.images.removeByPtr(kv.key_ptr); - }, - } - } - } - - // In the "OpenGL Programming Guide for Mac" it explains that: "When you - // use an NSOpenGLView object with OpenGL calls that are issued from a - // thread other than the main one, you must set up mutex locking." - // This locks the context and avoids crashes that can happen due to - // races with the underlying Metal layer that Apple is using to - // implement OpenGL. - const is_darwin = builtin.target.os.tag.isDarwin(); - const ogl = if (comptime is_darwin) @cImport({ - @cInclude("OpenGL/OpenGL.h"); - }) else {}; - const cgl_ctx = if (comptime is_darwin) ogl.CGLGetCurrentContext(); - if (comptime is_darwin) _ = ogl.CGLLockContext(cgl_ctx); - defer _ = if (comptime is_darwin) ogl.CGLUnlockContext(cgl_ctx); - - // If our viewport size doesn't match the saved screen size then - // we need to update it. We rely on this over setScreenSize because - // we can pull it directly from the OpenGL context instead of relying - // on the eventual message. - { - var viewport: [4]gl.c.GLint = undefined; - gl.glad.context.GetIntegerv.?(gl.c.GL_VIEWPORT, &viewport); - const screen: renderer.ScreenSize = .{ - .width = @intCast(viewport[2]), - .height = @intCast(viewport[3]), - }; - if (!screen.equals(self.size.screen)) { - self.size.screen = screen; - self.deferred_screen_size = .{ .size = self.size }; - } - } - - // Draw our terminal cells - try self.drawCellProgram(gl_state); - - // Draw our custom shaders - if (gl_state.custom) |*custom_state| { - try self.drawCustomPrograms(custom_state); - } - - // Swap our window buffers switch (apprt.runtime) { - apprt.glfw => surface.window.swapBuffers(), - apprt.gtk => {}, - apprt.embedded => {}, - else => @compileError("unsupported runtime"), + apprt.gtk => prepareContext(null) catch |err| { + log.warn( + "Error preparing GL context in displayRealized, err={}", + .{err}, + ); + }, + + else => @compileError("only GTK should be calling displayRealized"), } } -/// Draw the custom shaders. -fn drawCustomPrograms(self: *OpenGL, custom_state: *custom.State) !void { +pub fn initShaders( + self: *const OpenGL, + alloc: Allocator, + custom_shaders: []const [:0]const u8, +) !shaders.Shaders { + _ = alloc; + return try shaders.Shaders.init( + self.alloc, + custom_shaders, + ); +} + +/// Get the current size of the runtime surface. +pub fn surfaceSize(self: *const OpenGL) !struct { width: u32, height: u32 } { _ = self; - assert(custom_state.programs.len > 0); - - // Bind our state that is global to all custom shaders - const custom_bind = try custom_state.bind(); - defer custom_bind.unbind(); - - // Setup the new frame - try custom_state.newFrame(); - - // Go through each custom shader and draw it. - for (custom_state.programs) |program| { - const bind = try program.bind(); - defer bind.unbind(); - try bind.draw(); - try custom_state.copyFramebuffer(); - } -} - -/// Runs the cell program (shaders) to draw the terminal grid. -fn drawCellProgram( - self: *OpenGL, - gl_state: *const GLState, -) !void { - // Try to flush our atlas, this will only do something if there - // are changes to the atlas. - try self.flushAtlas(); - - // If we have custom shaders, then we draw to the custom - // shader framebuffer. - const fbobind: ?gl.Framebuffer.Binding = fbobind: { - const state = gl_state.custom orelse break :fbobind null; - break :fbobind try state.fbo.bind(.framebuffer); + var viewport: [4]gl.c.GLint = undefined; + gl.glad.context.GetIntegerv.?(gl.c.GL_VIEWPORT, &viewport); + return .{ + .width = @intCast(viewport[2]), + .height = @intCast(viewport[3]), }; - defer if (fbobind) |v| v.unbind(); +} - // Clear the surface - gl.clearColor( - @floatCast(@as(f32, @floatFromInt(self.draw_background.r)) / 255 * self.config.background_opacity), - @floatCast(@as(f32, @floatFromInt(self.draw_background.g)) / 255 * self.config.background_opacity), - @floatCast(@as(f32, @floatFromInt(self.draw_background.b)) / 255 * self.config.background_opacity), - @floatCast(self.config.background_opacity), - ); - gl.clear(gl.c.GL_COLOR_BUFFER_BIT); +/// Initialize a new render target which can be presented by this API. +pub fn initTarget(self: *const OpenGL, width: usize, height: usize) !Target { + return Target.init(.{ + .internal_format = if (self.blending.isLinear()) .srgba else .rgba, + .width = width, + .height = height, + }); +} - // If we have deferred operations, run them. - if (self.deferred_screen_size) |v| { - try v.apply(self); - self.deferred_screen_size = null; - } - if (self.deferred_font_size) |v| { - try v.apply(self); - self.deferred_font_size = null; - } - if (self.deferred_config) |v| { - try v.apply(self); - self.deferred_config = null; - } +/// Present the provided target. +pub fn present(self: *OpenGL, target: Target) !void { + // In order to present a target we blit it to the default framebuffer. - // Apply our padding extension fields - { - const program = gl_state.cell_program; - const bind = try program.program.use(); - defer bind.unbind(); - try program.program.setUniform( - "padding_vertical_top", - self.padding_extend_top, - ); - try program.program.setUniform( - "padding_vertical_bottom", - self.padding_extend_bottom, - ); - } + // We disable GL_FRAMEBUFFER_SRGB while doing this blit, otherwise the + // values may be linearized as they're copied, but even though the draw + // framebuffer has a linear internal format, the values in it should be + // sRGB, not linear! + try gl.disable(gl.c.GL_FRAMEBUFFER_SRGB); + defer gl.enable(gl.c.GL_FRAMEBUFFER_SRGB) catch |err| { + log.err("Error re-enabling GL_FRAMEBUFFER_SRGB, err={}", .{err}); + }; - // Draw background images first - try self.drawImages( - gl_state, - self.image_placements.items[0..self.image_bg_end], + // Bind the target for reading. + const fbobind = try target.framebuffer.bind(.read); + defer fbobind.unbind(); + + // Blit + gl.glad.context.BlitFramebuffer.?( + 0, + 0, + @intCast(target.width), + @intCast(target.height), + 0, + 0, + @intCast(target.width), + @intCast(target.height), + gl.c.GL_COLOR_BUFFER_BIT, + gl.c.GL_NEAREST, ); - // Draw our background - try self.drawCells(gl_state, self.cells_bg); + // Keep track of this target in case we need to repeat it. + self.last_target = target; +} - // Then draw images under text - try self.drawImages( - gl_state, - self.image_placements.items[self.image_bg_end..self.image_text_end], - ); +/// Present the last presented target again. +pub fn repeat(self: *OpenGL) !void { + if (self.last_target) |target| try self.present(target); +} - // Drag foreground - try self.drawCells(gl_state, self.cells); +/// Returns the options to use when constructing buffers. +pub inline fn bufferOptions(self: OpenGL) bufferpkg.Options { + _ = self; + return .{ + .target = .array, + .usage = .dynamic_draw, + }; +} - // Draw remaining images - try self.drawImages( - gl_state, - self.image_placements.items[self.image_text_end..], +pub const instanceBufferOptions = bufferOptions; +pub const uniformBufferOptions = bufferOptions; +pub const fgBufferOptions = bufferOptions; +pub const bgBufferOptions = bufferOptions; +pub const imageBufferOptions = bufferOptions; + +/// Returns the options to use when constructing textures. +pub inline fn textureOptions(self: OpenGL) Texture.Options { + _ = self; + return .{ + .format = .rgba, + .internal_format = .srgba, + .target = .@"2D", + }; +} + +/// Initializes a Texture suitable for the provided font atlas. +pub fn initAtlasTexture(self: *const OpenGL, atlas: *const font.Atlas) !Texture { + _ = self; + const format: gl.Texture.Format, const internal_format: gl.Texture.InternalFormat = + switch (atlas.format) { + .grayscale => .{ .red, .red }, + .rgba => .{ .rgba, .srgba }, + else => @panic("unsupported atlas format for OpenGL texture"), + }; + + return Texture.init( + .{ + .format = format, + .internal_format = internal_format, + .target = .Rectangle, + }, + atlas.size, + atlas.size, + null, ); } -/// Runs the image program to draw images. -fn drawImages( - self: *OpenGL, - gl_state: *const GLState, - placements: []const gl_image.Placement, -) !void { - if (placements.len == 0) return; - - // Bind our image program - const bind = try gl_state.image_program.bind(); - defer bind.unbind(); - - // For each placement we need to bind the texture - for (placements) |p| { - // Get the image and image texture - const image = self.images.get(p.image_id) orelse { - log.warn("image not found for placement image_id={}", .{p.image_id}); - continue; - }; - - const texture = switch (image.image) { - .ready => |t| t, - else => { - log.warn("image not ready for placement image_id={}", .{p.image_id}); - continue; - }, - }; - - // Bind the texture - try gl.Texture.active(gl.c.GL_TEXTURE0); - var texbind = try texture.bind(.@"2D"); - defer texbind.unbind(); - - // Setup our data - try bind.vbo.setData(ImageProgram.Input{ - .grid_col = p.x, - .grid_row = p.y, - .cell_offset_x = p.cell_offset_x, - .cell_offset_y = p.cell_offset_y, - .source_x = p.source_x, - .source_y = p.source_y, - .source_width = p.source_width, - .source_height = p.source_height, - .dest_width = p.width, - .dest_height = p.height, - }, .static_draw); - - try gl.drawElementsInstanced( - gl.c.GL_TRIANGLES, - 6, - gl.c.GL_UNSIGNED_BYTE, - 1, - ); - } +/// Begin a frame. +pub inline fn beginFrame( + self: *const OpenGL, + /// 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, +) !Frame { + _ = self; + return try Frame.begin(.{}, renderer, target); } - -/// Loads some set of cell data into our buffer and issues a draw call. -/// This expects all the OpenGL state to be setup. -/// -/// Future: when we move to multiple shaders, this will go away and -/// we'll have a draw call per-shader. -fn drawCells( - self: *OpenGL, - gl_state: *const GLState, - cells: std.ArrayListUnmanaged(CellProgram.Cell), -) !void { - // If we have no cells to render, then we render nothing. - if (cells.items.len == 0) return; - - // Todo: get rid of this completely - self.gl_cells_written = 0; - - // Bind our cell program state, buffers - const bind = try gl_state.cell_program.bind(); - defer bind.unbind(); - - // Bind our textures - try gl.Texture.active(gl.c.GL_TEXTURE0); - var texbind = try gl_state.texture.bind(.@"2D"); - defer texbind.unbind(); - - try gl.Texture.active(gl.c.GL_TEXTURE1); - var texbind1 = try gl_state.texture_color.bind(.@"2D"); - defer texbind1.unbind(); - - // Our allocated buffer on the GPU is smaller than our capacity. - // We reallocate a new buffer with the full new capacity. - if (self.gl_cells_size < cells.capacity) { - log.info("reallocating GPU buffer old={} new={}", .{ - self.gl_cells_size, - cells.capacity, - }); - - try bind.vbo.setDataNullManual( - @sizeOf(CellProgram.Cell) * cells.capacity, - .static_draw, - ); - - self.gl_cells_size = cells.capacity; - self.gl_cells_written = 0; - } - - // If we have data to write to the GPU, send it. - if (self.gl_cells_written < cells.items.len) { - const data = cells.items[self.gl_cells_written..]; - // log.info("sending {} cells to GPU", .{data.len}); - try bind.vbo.setSubData(self.gl_cells_written * @sizeOf(CellProgram.Cell), data); - - self.gl_cells_written += data.len; - assert(data.len > 0); - assert(self.gl_cells_written <= cells.items.len); - } - - try gl.drawElementsInstanced( - gl.c.GL_TRIANGLES, - 6, - gl.c.GL_UNSIGNED_BYTE, - cells.items.len, - ); -} - -/// The OpenGL objects that are associated with a renderer. This makes it -/// easy to create/destroy these as a set in situations i.e. where the -/// OpenGL context is replaced. -const GLState = struct { - cell_program: CellProgram, - image_program: ImageProgram, - texture: gl.Texture, - texture_color: gl.Texture, - custom: ?custom.State, - - pub fn init( - alloc: Allocator, - config: DerivedConfig, - font_grid: *font.SharedGrid, - ) !GLState { - var arena = ArenaAllocator.init(alloc); - defer arena.deinit(); - const arena_alloc = arena.allocator(); - - // Load our custom shaders - const custom_state: ?custom.State = custom: { - const shaders: []const [:0]const u8 = shadertoy.loadFromFiles( - arena_alloc, - config.custom_shaders, - .glsl, - ) catch |err| err: { - log.warn("error loading custom shaders err={}", .{err}); - break :err &.{}; - }; - if (shaders.len == 0) break :custom null; - - break :custom custom.State.init( - alloc, - shaders, - ) catch |err| err: { - log.warn("error initializing custom shaders err={}", .{err}); - break :err null; - }; - }; - - // Blending for text. We use GL_ONE here because we should be using - // premultiplied alpha for all our colors in our fragment shaders. - // This avoids having a blurry border where transparency is expected on - // pixels. - try gl.enable(gl.c.GL_BLEND); - try gl.blendFunc(gl.c.GL_ONE, gl.c.GL_ONE_MINUS_SRC_ALPHA); - - // Build 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, - .red, - @intCast(font_grid.atlas_grayscale.size), - @intCast(font_grid.atlas_grayscale.size), - 0, - .red, - .UnsignedByte, - font_grid.atlas_grayscale.data.ptr, - ); - } - - // Build our color texture - const tex_color = try gl.Texture.create(); - errdefer tex_color.destroy(); - { - const texbind = try tex_color.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, - .rgba, - @intCast(font_grid.atlas_color.size), - @intCast(font_grid.atlas_color.size), - 0, - .bgra, - .UnsignedByte, - font_grid.atlas_color.data.ptr, - ); - } - - // Build our cell renderer - const cell_program = try CellProgram.init(); - errdefer cell_program.deinit(); - - // Build our image renderer - const image_program = try ImageProgram.init(); - errdefer image_program.deinit(); - - return .{ - .cell_program = cell_program, - .image_program = image_program, - .texture = tex, - .texture_color = tex_color, - .custom = custom_state, - }; - } - - pub fn deinit(self: *GLState, alloc: Allocator) void { - if (self.custom) |v| v.deinit(alloc); - self.texture.destroy(); - self.texture_color.destroy(); - self.image_program.deinit(); - self.cell_program.deinit(); - } -}; diff --git a/src/renderer/Options.zig b/src/renderer/Options.zig index e7d9b3a42..85ff8e310 100644 --- a/src/renderer/Options.zig +++ b/src/renderer/Options.zig @@ -20,3 +20,6 @@ surface_mailbox: apprt.surface.Mailbox, /// The apprt surface. rt_surface: *apprt.Surface, + +/// The renderer thread. +thread: *renderer.Thread, diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 52f599549..03ca7b5e1 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -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}); } } diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig new file mode 100644 index 000000000..359f5f1b3 --- /dev/null +++ b/src/renderer/generic.zig @@ -0,0 +1,2866 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const glfw = @import("glfw"); +const xev = @import("xev"); +const apprt = @import("../apprt.zig"); +const configpkg = @import("../config.zig"); +const font = @import("../font/main.zig"); +const os = @import("../os/main.zig"); +const terminal = @import("../terminal/main.zig"); +const renderer = @import("../renderer.zig"); +const math = @import("../math.zig"); +const Surface = @import("../Surface.zig"); +const link = @import("link.zig"); +const fgMode = @import("cell.zig").fgMode; +const isCovering = @import("cell.zig").isCovering; +const shadertoy = @import("shadertoy.zig"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const Terminal = terminal.Terminal; +const Health = renderer.Health; + +const macos = switch (builtin.os.tag) { + .macos => @import("macos"), + else => void, +}; + +const DisplayLink = switch (builtin.os.tag) { + .macos => *macos.video.DisplayLink, + else => void, +}; + +const log = std.log.scoped(.generic_renderer); + +/// Create a renderer type with the provided graphics API wrapper. +/// +/// The graphics API wrapper must provide the interface outlined below. +/// Specific details for the interfaces are documented on the existing +/// implementations (`Metal` and `OpenGL`). +/// +/// Hierarchy of graphics abstractions: +/// +/// [ GraphicsAPI ] - Responsible for configuring the runtime surface +/// | | and providing render `Target`s that draw to it, +/// | | as well as `Frame`s and `Pipeline`s. +/// | V +/// | [ Target ] - Represents an abstract target for rendering, which +/// | could be a surface directly but is also used as an +/// | abstraction for off-screen frame buffers. +/// V +/// [ Frame ] - Represents the context for drawing a given frame, +/// | provides `RenderPass`es for issuing draw commands +/// | to, and reports the frame health when complete. +/// V +/// [ RenderPass ] - Represents a render pass in a frame, consisting of +/// : one or more `Step`s applied to the same target(s), +/// [ Step ] - - - - each describing the input buffers and textures and +/// : the vertex/fragment functions and geometry to use. +/// :_ _ _ _ _ _ _ _ _ _/ +/// v +/// [ Pipeline ] - Describes a vertex and fragment function to be used +/// for a `Step`; the `GraphicsAPI` is responsible for +/// these and they should be constructed and cached +/// ahead of time. +/// +/// [ Buffer ] - An abstraction over a GPU buffer. +/// +/// [ Texture ] - An abstraction over a GPU texture. +/// +pub fn Renderer(comptime GraphicsAPI: type) type { + const Target = GraphicsAPI.Target; + const Buffer = GraphicsAPI.Buffer; + const Texture = GraphicsAPI.Texture; + const RenderPass = GraphicsAPI.RenderPass; + const shaderpkg = GraphicsAPI.shaders; + + const cellpkg = GraphicsAPI.cellpkg; + const imagepkg = GraphicsAPI.imagepkg; + const Image = imagepkg.Image; + const ImageMap = imagepkg.ImageMap; + + const Shaders = shaderpkg.Shaders; + + const ImagePlacementList = std.ArrayListUnmanaged(imagepkg.Placement); + + return struct { + const Self = @This(); + + /// Allocator that can be used + alloc: std.mem.Allocator, + + /// This mutex must be held whenever any state used in `drawFrame` is + /// being modified, and also when it's being accessed in `drawFrame`. + draw_mutex: std.Thread.Mutex = .{}, + + /// The configuration we need derived from the main config. + config: DerivedConfig, + + /// The mailbox for communicating with the window. + surface_mailbox: apprt.surface.Mailbox, + + /// Current font metrics defining our grid. + grid_metrics: font.Metrics, + + /// The size of everything. + size: renderer.Size, + + /// True if the window is focused + focused: bool, + + /// The foreground color set by an OSC 10 sequence. If unset then + /// default_foreground_color is used. + foreground_color: ?terminal.color.RGB, + + /// Foreground color set in the user's config file. + default_foreground_color: terminal.color.RGB, + + /// The background color set by an OSC 11 sequence. If unset then + /// default_background_color is used. + background_color: ?terminal.color.RGB, + + /// Background color set in the user's config file. + default_background_color: terminal.color.RGB, + + /// The cursor color set by an OSC 12 sequence. If unset then + /// default_cursor_color is used. + cursor_color: ?terminal.color.RGB, + + /// Default cursor color when no color is set explicitly by an OSC 12 command. + /// This is cursor color as set in the user's config, if any. If no cursor color + /// is set in the user's config, then the cursor color is determined by the + /// current foreground color. + default_cursor_color: ?terminal.color.RGB, + + /// When `cursor_color` is null, swap the foreground and background colors of + /// the cell under the cursor for the cursor color. Otherwise, use the default + /// foreground color as the cursor color. + cursor_invert: bool, + + /// The current set of cells to render. This is rebuilt on every frame + /// but we keep this around so that we don't reallocate. Each set of + /// cells goes into a separate shader. + cells: cellpkg.Contents, + + /// The last viewport that we based our rebuild off of. If this changes, + /// then we do a full rebuild of the cells. The pointer values in this pin + /// are NOT SAFE to read because they may be modified, freed, etc from the + /// termio thread. We treat the pointers as integers for comparison only. + cells_viewport: ?terminal.Pin = null, + + /// Set to true after rebuildCells is called. This can be used + /// to determine if any possible changes have been made to the + /// cells for the draw call. + cells_rebuilt: bool = false, + + /// The current GPU uniform values. + uniforms: shaderpkg.Uniforms, + + /// The font structures. + font_grid: *font.SharedGrid, + font_shaper: font.Shaper, + font_shaper_cache: font.ShaperCache, + + /// The images that we may render. + images: ImageMap = .{}, + image_placements: ImagePlacementList = .{}, + image_bg_end: u32 = 0, + image_text_end: u32 = 0, + image_virtual: bool = false, + + /// Graphics API state. + api: GraphicsAPI, + + /// The CVDisplayLink used to drive the rendering loop in + /// sync with the display. This is void on platforms that + /// don't support a display link. + display_link: ?DisplayLink = null, + + /// Health of the most recently completed frame. + health: std.atomic.Value(Health) = .{ .raw = .healthy }, + + /// Our swap chain (multiple buffering) + swap_chain: SwapChain, + + /// This value is used to force-update swap chain targets in the + /// event of a config change that requires it (such as blending mode). + target_config_modified: usize = 0, + + /// If something happened that requires us to reinitialize our shaders, + /// this is set to true so that we can do that whenever possible. + reinitialize_shaders: bool = false, + + /// Whether or not we have custom shaders. + has_custom_shaders: bool = false, + + /// Our shader pipelines. + shaders: Shaders, + + /// Swap chain which maintains multiple copies of the state needed to + /// render a frame, so that we can start building the next frame while + /// the previous frame is still being processed on the GPU. + const SwapChain = struct { + // The count of buffers we use for double/triple buffering. + // If this is one then we don't do any double+ buffering at all. + // This is comptime because there isn't a good reason to change + // this at runtime and there is a lot of complexity to support it. + // For comptime, this is useful for debugging. + const buf_count = 3; + + /// `buf_count` structs that can hold the + /// data needed by the GPU to draw a frame. + frames: [buf_count]FrameState, + /// Index of the most recently used frame state struct. + frame_index: std.math.IntFittingRange(0, buf_count) = 0, + /// Semaphore that we wait on to make sure we have an available + /// frame state struct so we can start working on a new frame. + frame_sema: std.Thread.Semaphore = .{ .permits = buf_count }, + + /// Set to true when deinited, if you try to deinit a defunct + /// swap chain it will just be ignored, to prevent double-free. + defunct: bool = false, + + pub fn init(api: GraphicsAPI, custom_shaders: bool) !SwapChain { + var result: SwapChain = .{ .frames = undefined }; + + // Initialize all of our frame state. + for (&result.frames) |*frame| { + frame.* = try FrameState.init(api, custom_shaders); + } + + return result; + } + + pub fn deinit(self: *SwapChain) void { + if (self.defunct) return; + self.defunct = true; + + // Wait for all of our inflight draws to complete + // so that we can cleanly deinit our GPU state. + for (0..buf_count) |_| self.frame_sema.wait(); + for (&self.frames) |*frame| frame.deinit(); + } + + /// Get the next frame state to draw to. This will wait on the + /// semaphore to ensure that the frame is available. This must + /// always be paired with a call to releaseFrame. + pub fn nextFrame(self: *SwapChain) error{Defunct}!*FrameState { + if (self.defunct) return error.Defunct; + + self.frame_sema.wait(); + errdefer self.frame_sema.post(); + self.frame_index = (self.frame_index + 1) % buf_count; + return &self.frames[self.frame_index]; + } + + /// This should be called when the frame has completed drawing. + pub fn releaseFrame(self: *SwapChain) void { + self.frame_sema.post(); + } + }; + + /// State we need duplicated for every frame. Any state that could be + /// in a data race between the GPU and CPU while a frame is being drawn + /// should be in this struct. + /// + /// While a draw is in-process, we "lock" the state (via a semaphore) + /// and prevent the CPU from updating the state until our graphics API + /// reports that the frame is complete. + /// + /// This is used to implement double/triple buffering. + const FrameState = struct { + uniforms: UniformBuffer, + cells: CellTextBuffer, + cells_bg: CellBgBuffer, + + grayscale: Texture, + grayscale_modified: usize = 0, + color: Texture, + color_modified: usize = 0, + + target: Target, + /// See property of same name on Renderer for explanation. + target_config_modified: usize = 0, + + /// Custom shader state, this is null if we have no custom shaders. + custom_shader_state: ?CustomShaderState = null, + + /// A buffer containing the uniform data. + const UniformBuffer = Buffer(shaderpkg.Uniforms); + const CellBgBuffer = Buffer(shaderpkg.CellBg); + const CellTextBuffer = Buffer(shaderpkg.CellText); + + pub fn init(api: GraphicsAPI, custom_shaders: bool) !FrameState { + // Uniform buffer contains exactly 1 uniform struct. The + // uniform data will be undefined so this must be set before + // a frame is drawn. + var uniforms = try UniformBuffer.init(api.uniformBufferOptions(), 1); + errdefer uniforms.deinit(); + + // Create the buffers for our vertex data. The preallocation size + // is likely too small but our first frame update will resize it. + var cells = try CellTextBuffer.init(api.fgBufferOptions(), 10 * 10); + errdefer cells.deinit(); + var cells_bg = try CellBgBuffer.init(api.bgBufferOptions(), 10 * 10); + errdefer cells_bg.deinit(); + + // Initialize our textures for our font atlas. + const grayscale = try api.initAtlasTexture(&.{ + .data = undefined, + .size = 8, + .format = .grayscale, + }); + errdefer grayscale.deinit(); + const color = try api.initAtlasTexture(&.{ + .data = undefined, + .size = 8, + .format = .rgba, + }); + errdefer color.deinit(); + + var custom_shader_state = + if (custom_shaders) + try CustomShaderState.init(api) + else + null; + errdefer if (custom_shader_state) |*state| state.deinit(); + + // Initialize the target at 1x1 px, this is slightly + // wasteful but it's only done once so whatever. + const target = try api.initTarget(1, 1); + + return .{ + .uniforms = uniforms, + .cells = cells, + .cells_bg = cells_bg, + .grayscale = grayscale, + .color = color, + .target = target, + .custom_shader_state = custom_shader_state, + }; + } + + pub fn deinit(self: *FrameState) void { + self.uniforms.deinit(); + self.cells.deinit(); + self.cells_bg.deinit(); + self.grayscale.deinit(); + self.color.deinit(); + if (self.custom_shader_state) |*state| state.deinit(); + } + + pub fn resize( + self: *FrameState, + api: GraphicsAPI, + width: usize, + height: usize, + ) !void { + if (self.custom_shader_state) |*state| { + try state.resize(api, width, height); + } + const target = try api.initTarget(width, height); + self.target.deinit(); + self.target = target; + } + }; + + /// State relevant to our custom shaders if we have any. + const CustomShaderState = struct { + /// When we have a custom shader state, we maintain a front + /// and back texture which we use as a swap chain to render + /// between when multiple custom shaders are defined. + front_texture: Texture, + back_texture: Texture, + + uniforms: shaderpkg.PostUniforms, + + /// 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, + + /// Swap the front and back textures. + pub fn swap(self: *CustomShaderState) void { + std.mem.swap(Texture, &self.front_texture, &self.back_texture); + } + + pub fn init(api: GraphicsAPI) !CustomShaderState { + // Initialize the front and back textures at 1x1 px, this + // is slightly wasteful but it's only done once so whatever. + const front_texture = try Texture.init( + api.textureOptions(), + 1, + 1, + null, + ); + errdefer front_texture.deinit(); + const back_texture = try Texture.init( + api.textureOptions(), + 1, + 1, + null, + ); + errdefer back_texture.deinit(); + return .{ + .front_texture = front_texture, + .back_texture = back_texture, + + .uniforms = .{ + .resolution = .{ 0, 0, 1 }, + .time = 1, + .time_delta = 1, + .frame_rate = 1, + .frame = 1, + .channel_time = @splat(@splat(0)), + .channel_resolution = @splat(@splat(0)), + .mouse = .{ 0, 0, 0, 0 }, + .date = .{ 0, 0, 0, 0 }, + .sample_rate = 1, + }, + + .first_frame_time = try std.time.Instant.now(), + .last_frame_time = try std.time.Instant.now(), + }; + } + + pub fn deinit(self: *CustomShaderState) void { + self.front_texture.deinit(); + self.back_texture.deinit(); + } + + pub fn resize( + self: *CustomShaderState, + api: GraphicsAPI, + width: usize, + height: usize, + ) !void { + const front_texture = try Texture.init( + api.textureOptions(), + @intCast(width), + @intCast(height), + null, + ); + errdefer front_texture.deinit(); + const back_texture = try Texture.init( + api.textureOptions(), + @intCast(width), + @intCast(height), + null, + ); + errdefer back_texture.deinit(); + + self.front_texture.deinit(); + self.back_texture.deinit(); + + self.front_texture = front_texture; + self.back_texture = back_texture; + + self.uniforms.resolution = .{ + @floatFromInt(width), + @floatFromInt(height), + 1, + }; + self.uniforms.channel_resolution[0] = .{ + @floatFromInt(width), + @floatFromInt(height), + 1, + 0, + }; + } + }; + + /// The configuration for this renderer that is derived from the main + /// configuration. This must be exported so that we don't need to + /// pass around Config pointers which makes memory management a pain. + pub const DerivedConfig = struct { + arena: ArenaAllocator, + + font_thicken: bool, + font_thicken_strength: u8, + font_features: std.ArrayListUnmanaged([:0]const u8), + font_styles: font.CodepointResolver.StyleStatus, + cursor_color: ?terminal.color.RGB, + cursor_invert: bool, + cursor_opacity: f64, + cursor_text: ?terminal.color.RGB, + background: terminal.color.RGB, + background_opacity: f64, + foreground: terminal.color.RGB, + selection_background: ?terminal.color.RGB, + selection_foreground: ?terminal.color.RGB, + invert_selection_fg_bg: bool, + bold_is_bright: bool, + min_contrast: f32, + padding_color: configpkg.WindowPaddingColor, + custom_shaders: configpkg.RepeatablePath, + links: link.Set, + vsync: bool, + colorspace: configpkg.Config.WindowColorspace, + blending: configpkg.Config.AlphaBlending, + + pub fn init( + alloc_gpa: Allocator, + config: *const configpkg.Config, + ) !DerivedConfig { + var arena = ArenaAllocator.init(alloc_gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + // Copy our shaders + const custom_shaders = try config.@"custom-shader".clone(alloc); + + // Copy our font features + const font_features = try config.@"font-feature".clone(alloc); + + // Get our font styles + var font_styles = font.CodepointResolver.StyleStatus.initFill(true); + font_styles.set(.bold, config.@"font-style-bold" != .false); + font_styles.set(.italic, config.@"font-style-italic" != .false); + font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); + + // Our link configs + const links = try link.Set.fromConfig( + alloc, + config.link.links.items, + ); + + const cursor_invert = config.@"cursor-invert-fg-bg"; + + return .{ + .background_opacity = @max(0, @min(1, config.@"background-opacity")), + .font_thicken = config.@"font-thicken", + .font_thicken_strength = config.@"font-thicken-strength", + .font_features = font_features.list, + .font_styles = font_styles, + + .cursor_color = if (!cursor_invert and config.@"cursor-color" != null) + config.@"cursor-color".?.toTerminalRGB() + else + null, + + .cursor_invert = cursor_invert, + + .cursor_text = if (config.@"cursor-text") |txt| + txt.toTerminalRGB() + else + null, + + .cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")), + + .background = config.background.toTerminalRGB(), + .foreground = config.foreground.toTerminalRGB(), + .invert_selection_fg_bg = config.@"selection-invert-fg-bg", + .bold_is_bright = config.@"bold-is-bright", + .min_contrast = @floatCast(config.@"minimum-contrast"), + .padding_color = config.@"window-padding-color", + + .selection_background = if (config.@"selection-background") |bg| + bg.toTerminalRGB() + else + null, + + .selection_foreground = if (config.@"selection-foreground") |bg| + bg.toTerminalRGB() + else + null, + + .custom_shaders = custom_shaders, + .links = links, + .vsync = config.@"window-vsync", + .colorspace = config.@"window-colorspace", + .blending = config.@"alpha-blending", + .arena = arena, + }; + } + + pub fn deinit(self: *DerivedConfig) void { + const alloc = self.arena.allocator(); + self.links.deinit(alloc); + self.arena.deinit(); + } + }; + + /// Returns the hints that we want for this window. + pub fn glfwWindowHints(config: *const configpkg.Config) glfw.Window.Hints { + // If our graphics API provides hints, use them, + // otherwise fall back to generic hints. + if (@hasDecl(GraphicsAPI, "glfwWindowHints")) { + return GraphicsAPI.glfwWindowHints(config); + } + + return .{ + .client_api = .no_api, + .transparent_framebuffer = config.@"background-opacity" < 1, + }; + } + + pub fn init(alloc: Allocator, options: renderer.Options) !Self { + // Initialize our graphics API wrapper, this will prepare the + // surface provided by the apprt and set up any API-specific + // GPU resources. + var api = try GraphicsAPI.init(alloc, options); + errdefer api.deinit(); + + const has_custom_shaders = options.config.custom_shaders.value.items.len > 0; + + // Prepare our swap chain + var swap_chain = try SwapChain.init( + api, + has_custom_shaders, + ); + errdefer swap_chain.deinit(); + + // Create the font shaper. + var font_shaper = try font.Shaper.init(alloc, .{ + .features = options.config.font_features.items, + }); + errdefer font_shaper.deinit(); + + // Initialize all the data that requires a critical font section. + const font_critical: struct { + metrics: font.Metrics, + } = font_critical: { + const grid = options.font_grid; + grid.lock.lockShared(); + defer grid.lock.unlockShared(); + break :font_critical .{ + .metrics = grid.metrics, + }; + }; + + const display_link: ?DisplayLink = switch (builtin.os.tag) { + .macos => if (options.config.vsync) + try macos.video.DisplayLink.createWithActiveCGDisplays() + else + null, + else => null, + }; + errdefer if (display_link) |v| v.release(); + + var result: Self = .{ + .alloc = alloc, + .config = options.config, + .surface_mailbox = options.surface_mailbox, + .grid_metrics = font_critical.metrics, + .size = options.size, + .focused = true, + .foreground_color = null, + .default_foreground_color = options.config.foreground, + .background_color = null, + .default_background_color = options.config.background, + .cursor_color = null, + .default_cursor_color = options.config.cursor_color, + .cursor_invert = options.config.cursor_invert, + + // Render state + .cells = .{}, + .uniforms = .{ + .projection_matrix = undefined, + .cell_size = undefined, + .grid_size = undefined, + .grid_padding = undefined, + .padding_extend = .{}, + .min_contrast = options.config.min_contrast, + .cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) }, + .cursor_color = undefined, + .bg_color = .{ + options.config.background.r, + options.config.background.g, + options.config.background.b, + @intFromFloat(@round(options.config.background_opacity * 255.0)), + }, + .bools = .{ + .cursor_wide = false, + .use_display_p3 = options.config.colorspace == .@"display-p3", + .use_linear_blending = options.config.blending.isLinear(), + .use_linear_correction = options.config.blending == .@"linear-corrected", + }, + }, + + // Fonts + .font_grid = options.font_grid, + .font_shaper = font_shaper, + .font_shaper_cache = font.ShaperCache.init(), + + // Shaders (initialized below) + .shaders = undefined, + + // Graphics API stuff + .api = api, + .swap_chain = swap_chain, + .display_link = display_link, + }; + + try result.initShaders(); + + // Ensure our undefined values above are correctly initialized. + result.updateFontGridUniforms(); + result.updateScreenSizeUniforms(); + + return result; + } + + pub fn deinit(self: *Self) void { + self.swap_chain.deinit(); + + if (DisplayLink != void) { + if (self.display_link) |display_link| { + display_link.stop() catch {}; + display_link.release(); + } + } + + self.cells.deinit(self.alloc); + + self.font_shaper.deinit(); + self.font_shaper_cache.deinit(self.alloc); + + self.config.deinit(); + + { + var it = self.images.iterator(); + while (it.next()) |kv| kv.value_ptr.image.deinit(self.alloc); + self.images.deinit(self.alloc); + } + self.image_placements.deinit(self.alloc); + + self.deinitShaders(); + + self.api.deinit(); + + self.* = undefined; + } + + fn deinitShaders(self: *Self) void { + self.shaders.deinit(self.alloc); + } + + fn initShaders(self: *Self) !void { + var arena = ArenaAllocator.init(self.alloc); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + + // Load our custom shaders + const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles( + arena_alloc, + self.config.custom_shaders, + GraphicsAPI.custom_shader_target, + ) catch |err| err: { + log.warn("error loading custom shaders err={}", .{err}); + break :err &.{}; + }; + + const has_custom_shaders = custom_shaders.len > 0; + + var shaders = try self.api.initShaders( + self.alloc, + custom_shaders, + ); + errdefer shaders.deinit(self.alloc); + + self.shaders = shaders; + self.has_custom_shaders = has_custom_shaders; + } + + /// This is called early right after surface creation. + pub fn surfaceInit(surface: *apprt.Surface) !void { + // If our API has to do things here, let it. + if (@hasDecl(GraphicsAPI, "surfaceInit")) { + try GraphicsAPI.surfaceInit(surface); + } + } + + /// This is called just prior to spinning up the renderer thread for + /// final main thread setup requirements. + pub fn finalizeSurfaceInit(self: *Self, surface: *apprt.Surface) !void { + // If our API has to do things to finalize surface init, let it. + if (@hasDecl(GraphicsAPI, "finalizeSurfaceInit")) { + try self.api.finalizeSurfaceInit(surface); + } + } + + /// Callback called by renderer.Thread when it begins. + pub fn threadEnter(self: *const Self, surface: *apprt.Surface) !void { + // If our API has to do things on thread enter, let it. + if (@hasDecl(GraphicsAPI, "threadEnter")) { + try self.api.threadEnter(surface); + } + } + + /// Callback called by renderer.Thread when it exits. + pub fn threadExit(self: *const Self) void { + // If our API has to do things on thread exit, let it. + if (@hasDecl(GraphicsAPI, "threadExit")) { + self.api.threadExit(); + } + } + + /// Called by renderer.Thread when it starts the main loop. + pub fn loopEnter(self: *Self, thr: *renderer.Thread) !void { + // If our API has to do things on loop enter, let it. + if (@hasDecl(GraphicsAPI, "loopEnter")) { + self.api.loopEnter(); + } + + // If we don't support a display link we have no work to do. + if (comptime DisplayLink == void) return; + + // This is when we know our "self" pointer is stable so we can + // setup the display link. To setup the display link we set our + // callback and we can start it immediately. + const display_link = self.display_link orelse return; + try display_link.setOutputCallback( + xev.Async, + &displayLinkCallback, + &thr.draw_now, + ); + display_link.start() catch {}; + } + + /// Called by renderer.Thread when it exits the main loop. + pub fn loopExit(self: *Self) void { + // If our API has to do things on loop exit, let it. + if (@hasDecl(GraphicsAPI, "loopExit")) { + self.api.loopExit(); + } + + // If we don't support a display link we have no work to do. + if (comptime DisplayLink == void) return; + + // Stop our display link. If this fails its okay it just means + // that we either never started it or the view its attached to + // is gone which is fine. + const display_link = self.display_link orelse return; + display_link.stop() catch {}; + } + + /// This is called by the GTK apprt after the surface is + /// reinitialized due to any of the events mentioned in + /// the doc comment for `displayUnrealized`. + pub fn displayRealized(self: *Self) !void { + // If our API has to do things on realize, let it. + if (@hasDecl(GraphicsAPI, "displayRealized")) { + self.api.displayRealized(); + } + + // Lock the draw mutex so that we can + // safely reinitialize our GPU resources. + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + // We assume that the swap chain was deinited in + // `displayUnrealized`, in which case it should be + // marked defunct. If not, we have a problem. + assert(self.swap_chain.defunct); + + // We reinitialize our shaders and our swap chain. + try self.initShaders(); + self.swap_chain = try SwapChain.init( + self.api, + self.has_custom_shaders, + ); + self.reinitialize_shaders = false; + self.target_config_modified = 1; + } + + /// This is called by the GTK apprt when the surface is being destroyed. + /// This can happen because the surface is being closed but also when + /// moving the window between displays or splitting. + pub fn displayUnrealized(self: *Self) void { + // If our API has to do things on unrealize, let it. + if (@hasDecl(GraphicsAPI, "displayUnrealized")) { + self.api.displayUnrealized(); + } + + // Lock the draw mutex so that we can + // safely deinitialize our GPU resources. + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + // We deinit our swap chain and shaders. + // + // This will mark them as defunct so that they + // can't be double-freed or used in draw calls. + self.swap_chain.deinit(); + self.shaders.deinit(self.alloc); + } + + fn displayLinkCallback( + _: *macos.video.DisplayLink, + ud: ?*xev.Async, + ) void { + const draw_now = ud orelse return; + draw_now.notify() catch |err| { + log.err("error notifying draw_now err={}", .{err}); + }; + } + + /// Mark the full screen as dirty so that we redraw everything. + pub fn markDirty(self: *Self) void { + self.cells_viewport = null; + } + + /// Called when we get an updated display ID for our display link. + pub fn setMacOSDisplayID(self: *Self, id: u32) !void { + if (comptime DisplayLink == void) return; + const display_link = self.display_link orelse return; + log.info("updating display link display id={}", .{id}); + display_link.setCurrentCGDisplay(id) catch |err| { + log.warn("error setting display link display id err={}", .{err}); + }; + } + + /// True if our renderer has animations so that a higher frequency + /// timer is used. + pub fn hasAnimations(self: *const Self) bool { + return self.has_custom_shaders; + } + + /// True if our renderer is using vsync. If true, the renderer or apprt + /// is responsible for triggering draw_now calls to the render thread. + /// That is the only way to trigger a drawFrame. + pub fn hasVsync(self: *const Self) bool { + if (comptime DisplayLink == void) return false; + const display_link = self.display_link orelse return false; + return display_link.isRunning(); + } + + /// Callback when the focus changes for the terminal this is rendering. + /// + /// Must be called on the render thread. + pub fn setFocus(self: *Self, focus: bool) !void { + self.focused = focus; + + // If we're not focused, then we want to stop the display link + // because it is a waste of resources and we can move to pure + // change-driven updates. + if (comptime DisplayLink != void) link: { + const display_link = self.display_link orelse break :link; + if (focus) { + display_link.start() catch {}; + } else { + display_link.stop() catch {}; + } + } + } + + /// Callback when the window is visible or occluded. + /// + /// Must be called on the render thread. + pub fn setVisible(self: *Self, visible: bool) void { + // If we're not visible, then we want to stop the display link + // because it is a waste of resources and we can move to pure + // change-driven updates. + if (comptime DisplayLink != void) link: { + const display_link = self.display_link orelse break :link; + if (visible and self.focused) { + display_link.start() catch {}; + } else { + display_link.stop() catch {}; + } + } + } + + /// Set the new font grid. + /// + /// Must be called on the render thread. + pub fn setFontGrid(self: *Self, grid: *font.SharedGrid) void { + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + // Update our grid + self.font_grid = grid; + + // Update all our textures so that they sync on the next frame. + // We can modify this without a lock because the GPU does not + // touch this data. + for (&self.swap_chain.frames) |*frame| { + frame.grayscale_modified = 0; + frame.color_modified = 0; + } + + // Get our metrics from the grid. This doesn't require a lock because + // the metrics are never recalculated. + const metrics = grid.metrics; + self.grid_metrics = metrics; + + // Reset our shaper cache. If our font changed (not just the size) then + // the data in the shaper cache may be invalid and cannot be used, so we + // always clear the cache just in case. + const font_shaper_cache = font.ShaperCache.init(); + self.font_shaper_cache.deinit(self.alloc); + self.font_shaper_cache = font_shaper_cache; + + // Update cell size. + self.size.cell = .{ + .width = metrics.cell_width, + .height = metrics.cell_height, + }; + + // Update relevant uniforms + self.updateFontGridUniforms(); + } + + /// Update uniforms that are based on the font grid. + /// + /// Caller must hold the draw mutex. + fn updateFontGridUniforms(self: *Self) void { + self.uniforms.cell_size = .{ + @floatFromInt(self.grid_metrics.cell_width), + @floatFromInt(self.grid_metrics.cell_height), + }; + } + + /// Update the frame data. + pub fn updateFrame( + self: *Self, + surface: *apprt.Surface, + state: *renderer.State, + cursor_blink_visible: bool, + ) !void { + _ = surface; + + // Data we extract out of the critical area. + const Critical = struct { + bg: terminal.color.RGB, + screen: terminal.Screen, + screen_type: terminal.ScreenType, + mouse: renderer.State.Mouse, + preedit: ?renderer.State.Preedit, + cursor_style: ?renderer.CursorStyle, + color_palette: terminal.color.Palette, + + /// If true, rebuild the full screen. + full_rebuild: bool, + }; + + // Update all our data as tightly as possible within the mutex. + var critical: Critical = critical: { + // const start = try std.time.Instant.now(); + // const start_micro = std.time.microTimestamp(); + // defer { + // const end = std.time.Instant.now() catch unreachable; + // // "[updateFrame critical time] \t" + // std.log.err("[updateFrame critical time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us}); + // } + + state.mutex.lock(); + defer state.mutex.unlock(); + + // If we're in a synchronized output state, we pause all rendering. + if (state.terminal.modes.get(.synchronized_output)) { + log.debug("synchronized output started, skipping render", .{}); + return; + } + + // Swap bg/fg if the terminal is reversed + const bg = self.background_color orelse self.default_background_color; + const fg = self.foreground_color orelse self.default_foreground_color; + defer { + if (self.background_color) |*c| { + c.* = bg; + } else { + self.default_background_color = bg; + } + + if (self.foreground_color) |*c| { + c.* = fg; + } else { + self.default_foreground_color = fg; + } + } + + if (state.terminal.modes.get(.reverse_colors)) { + if (self.background_color) |*c| { + c.* = fg; + } else { + self.default_background_color = fg; + } + + if (self.foreground_color) |*c| { + c.* = bg; + } else { + self.default_foreground_color = bg; + } + } + + // Get the viewport pin so that we can compare it to the current. + const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?; + + // We used to share terminal state, but we've since learned through + // analysis that it is faster to copy the terminal state than to + // hold the lock while rebuilding GPU cells. + var screen_copy = try state.terminal.screen.clone( + self.alloc, + .{ .viewport = .{} }, + null, + ); + errdefer screen_copy.deinit(); + + // Whether to draw our cursor or not. + const cursor_style = if (state.terminal.flags.password_input) + .lock + else + renderer.cursorStyle( + state, + self.focused, + cursor_blink_visible, + ); + + // Get our preedit state + const preedit: ?renderer.State.Preedit = preedit: { + if (cursor_style == null) break :preedit null; + const p = state.preedit orelse break :preedit null; + break :preedit try p.clone(self.alloc); + }; + errdefer if (preedit) |p| p.deinit(self.alloc); + + // If we have Kitty graphics data, we enter a SLOW SLOW SLOW path. + // We only do this if the Kitty image state is dirty meaning only if + // it changes. + // + // If we have any virtual references, we must also rebuild our + // kitty state on every frame because any cell change can move + // an image. + if (state.terminal.screen.kitty_images.dirty or + self.image_virtual) + { + try self.prepKittyGraphics(state.terminal); + } + + // If we have any terminal dirty flags set then we need to rebuild + // the entire screen. This can be optimized in the future. + const full_rebuild: bool = rebuild: { + { + const Int = @typeInfo(terminal.Terminal.Dirty).@"struct".backing_integer.?; + const v: Int = @bitCast(state.terminal.flags.dirty); + if (v > 0) break :rebuild true; + } + { + const Int = @typeInfo(terminal.Screen.Dirty).@"struct".backing_integer.?; + const v: Int = @bitCast(state.terminal.screen.dirty); + if (v > 0) break :rebuild true; + } + + // If our viewport changed then we need to rebuild the entire + // screen because it means we scrolled. If we have no previous + // viewport then we must rebuild. + const prev_viewport = self.cells_viewport orelse break :rebuild true; + if (!prev_viewport.eql(viewport_pin)) break :rebuild true; + + break :rebuild false; + }; + + // Reset the dirty flags in the terminal and screen. We assume + // that our rebuild will be successful since so we optimize for + // success and reset while we hold the lock. This is much easier + // than coordinating row by row or as changes are persisted. + state.terminal.flags.dirty = .{}; + state.terminal.screen.dirty = .{}; + { + var it = state.terminal.screen.pages.pageIterator( + .right_down, + .{ .screen = .{} }, + null, + ); + while (it.next()) |chunk| { + var dirty_set = chunk.node.data.dirtyBitSet(); + dirty_set.unsetAll(); + } + } + + // Update our viewport pin + self.cells_viewport = viewport_pin; + + break :critical .{ + .bg = self.background_color orelse self.default_background_color, + .screen = screen_copy, + .screen_type = state.terminal.active_screen, + .mouse = state.mouse, + .preedit = preedit, + .cursor_style = cursor_style, + .color_palette = state.terminal.color_palette.colors, + .full_rebuild = full_rebuild, + }; + }; + defer { + critical.screen.deinit(); + if (critical.preedit) |p| p.deinit(self.alloc); + } + + // Build our GPU cells + try self.rebuildCells( + critical.full_rebuild, + &critical.screen, + critical.screen_type, + critical.mouse, + critical.preedit, + critical.cursor_style, + &critical.color_palette, + ); + + // Notify our shaper we're done for the frame. For some shapers, + // such as CoreText, this triggers off-thread cleanup logic. + self.font_shaper.endFrame(); + + // Acquire the draw mutex because we're modifying state here. + { + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + // Update our background color + self.uniforms.bg_color = .{ + critical.bg.r, + critical.bg.g, + critical.bg.b, + @intFromFloat(@round(self.config.background_opacity * 255.0)), + }; + } + } + + /// Draw the frame to the screen. + /// + /// If `sync` is true, this will synchronously block until + /// the frame is finished drawing and has been presented. + pub fn drawFrame( + self: *Self, + sync: bool, + ) !void { + // We hold a the draw mutex to prevent changes to any + // data we access while we're in the middle of drawing. + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + // 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(); + + // Retrieve the most up-to-date surface size from the Graphics API + const surface_size = try self.api.surfaceSize(); + + // If either of our surface dimensions is zero + // then drawing is absurd, so we just return. + if (surface_size.width == 0 or surface_size.height == 0) return; + + const size_changed = + self.size.screen.width != surface_size.width or + self.size.screen.height != surface_size.height; + + // Conditions under which we need to draw the frame, otherwise we + // don't need to since the previous frame should be identical. + const needs_redraw = + size_changed or + self.cells_rebuilt or + self.hasAnimations() or + sync; + + if (!needs_redraw) { + // We still need to present the last target again, because the + // apprt may be swapping buffers and display an outdated frame + // if we don't draw something new. + try self.api.repeat(); + return; + } + self.cells_rebuilt = false; + + // Wait for a frame to be available. + const frame = try self.swap_chain.nextFrame(); + errdefer self.swap_chain.releaseFrame(); + // log.debug("drawing frame index={}", .{self.swap_chain.frame_index}); + + // If we need to reinitialize our shaders, do so. + if (self.reinitialize_shaders) { + self.reinitialize_shaders = false; + self.shaders.deinit(self.alloc); + try self.initShaders(); + } + + // Our shaders should not be defunct at this point. + assert(!self.shaders.defunct); + + // If we have custom shaders, make sure we have the + // custom shader state in our frame state, otherwise + // if we have a state but don't need it we remove it. + if (self.has_custom_shaders) { + if (frame.custom_shader_state == null) { + frame.custom_shader_state = try .init(self.api); + try frame.custom_shader_state.?.resize( + self.api, + surface_size.width, + surface_size.height, + ); + } + } else if (frame.custom_shader_state) |*state| { + state.deinit(); + frame.custom_shader_state = null; + } + + // If our stored size doesn't match the + // surface size we need to update it. + if (size_changed) { + self.size.screen = .{ + .width = surface_size.width, + .height = surface_size.height, + }; + self.updateScreenSizeUniforms(); + } + + // If this frame's target isn't the correct size, or the target + // config has changed (such as when the blending mode changes), + // remove it and replace it with a new one with the right values. + if (frame.target.width != self.size.screen.width or + frame.target.height != self.size.screen.height or + frame.target_config_modified != self.target_config_modified) + { + try frame.resize( + self.api, + self.size.screen.width, + self.size.screen.height, + ); + frame.target_config_modified = self.target_config_modified; + } + + // Upload images to the GPU as necessary. + { + var image_it = self.images.iterator(); + while (image_it.next()) |kv| { + switch (kv.value_ptr.image) { + .ready => {}, + + .pending_gray, + .pending_gray_alpha, + .pending_rgb, + .pending_rgba, + .replace_gray, + .replace_gray_alpha, + .replace_rgb, + .replace_rgba, + => try kv.value_ptr.image.upload(self.alloc, &self.api), + + .unload_pending, + .unload_replace, + .unload_ready, + => { + kv.value_ptr.image.deinit(self.alloc); + self.images.removeByPtr(kv.key_ptr); + }, + } + } + } + + // Setup our frame data + try frame.uniforms.sync(&.{self.uniforms}); + try frame.cells_bg.sync(self.cells.bg_cells); + const fg_count = try frame.cells.syncFromArrayLists(self.cells.fg_rows.lists); + + // If we have custom shaders, update the animation time. + if (frame.custom_shader_state) |*state| { + const now = std.time.Instant.now() catch state.first_frame_time; + const since_ns: f32 = @floatFromInt(now.since(state.first_frame_time)); + const delta_ns: f32 = @floatFromInt(now.since(state.last_frame_time)); + state.uniforms.time = since_ns / std.time.ns_per_s; + state.uniforms.time_delta = delta_ns / std.time.ns_per_s; + state.last_frame_time = now; + } + + // If our font atlas changed, sync the texture data + texture: { + const modified = self.font_grid.atlas_grayscale.modified.load(.monotonic); + if (modified <= frame.grayscale_modified) break :texture; + self.font_grid.lock.lockShared(); + defer self.font_grid.lock.unlockShared(); + frame.grayscale_modified = self.font_grid.atlas_grayscale.modified.load(.monotonic); + try self.syncAtlasTexture(&self.font_grid.atlas_grayscale, &frame.grayscale); + } + texture: { + const modified = self.font_grid.atlas_color.modified.load(.monotonic); + if (modified <= frame.color_modified) break :texture; + self.font_grid.lock.lockShared(); + defer self.font_grid.lock.unlockShared(); + frame.color_modified = self.font_grid.atlas_color.modified.load(.monotonic); + try self.syncAtlasTexture(&self.font_grid.atlas_color, &frame.color); + } + + // Get a frame context from the graphics API. + var frame_ctx = try self.api.beginFrame(self, &frame.target); + defer frame_ctx.complete(sync); + + { + var pass = frame_ctx.renderPass(&.{.{ + .target = if (frame.custom_shader_state) |state| + .{ .texture = state.back_texture } + else + .{ .target = frame.target }, + .clear_color = .{ 0.0, 0.0, 0.0, 0.0 }, + }}); + defer pass.complete(); + + // bg images + try self.drawImagePlacements(&pass, self.image_placements.items[0..self.image_bg_end]); + // bg + pass.step(.{ + .pipeline = self.shaders.cell_bg_pipeline, + .uniforms = frame.uniforms.buffer, + .buffers = &.{ null, frame.cells_bg.buffer }, + .draw = .{ + .type = .triangle, + .vertex_count = 3, + }, + }); + // mg images + try self.drawImagePlacements(&pass, self.image_placements.items[self.image_bg_end..self.image_text_end]); + // text + pass.step(.{ + .pipeline = self.shaders.cell_text_pipeline, + .uniforms = frame.uniforms.buffer, + .buffers = &.{ + frame.cells.buffer, + frame.cells_bg.buffer, + }, + .textures = &.{ + frame.grayscale, + frame.color, + }, + .draw = .{ + .type = .triangle_strip, + .vertex_count = 4, + .instance_count = fg_count, + }, + }); + // fg images + try self.drawImagePlacements(&pass, self.image_placements.items[self.image_text_end..]); + } + + // If we have custom shaders, then we render them. + if (frame.custom_shader_state) |*state| { + // We create a buffer on the GPU for our post uniforms. + // TODO: This should be a part of the frame state tbqh. + const PostBuffer = Buffer(shaderpkg.PostUniforms); + const uniform_buffer = try PostBuffer.initFill( + self.api.bufferOptions(), + &.{state.uniforms}, + ); + defer uniform_buffer.deinit(); + + for (self.shaders.post_pipelines, 0..) |pipeline, i| { + defer state.swap(); + + var pass = frame_ctx.renderPass(&.{.{ + .target = if (i < self.shaders.post_pipelines.len - 1) + .{ .texture = state.front_texture } + else + .{ .target = frame.target }, + .clear_color = .{ 0.0, 0.0, 0.0, 0.0 }, + }}); + defer pass.complete(); + + pass.step(.{ + .pipeline = pipeline, + .uniforms = uniform_buffer.buffer, + .textures = &.{state.back_texture}, + .draw = .{ + .type = .triangle, + .vertex_count = 3, + }, + }); + } + } + } + + // Callback from the graphics API when a frame is completed. + pub fn frameCompleted( + self: *Self, + health: Health, + ) void { + // If our health value hasn't changed, then we do nothing. We don't + // do a cmpxchg here because strict atomicity isn't important. + if (self.health.load(.seq_cst) != health) { + self.health.store(health, .seq_cst); + + // Our health value changed, so we notify the surface so that it + // can do something about it. + _ = self.surface_mailbox.push(.{ + .renderer_health = health, + }, .{ .forever = {} }); + } + + // Always release our semaphore + self.swap_chain.releaseFrame(); + } + + fn drawImagePlacements( + self: *Self, + pass: *RenderPass, + placements: []const imagepkg.Placement, + ) !void { + if (placements.len == 0) return; + + for (placements) |p| { + + // Look up the image + const image = self.images.get(p.image_id) orelse { + log.warn("image not found for placement image_id={}", .{p.image_id}); + return; + }; + + // Get the texture + const texture = switch (image.image) { + .ready => |t| t, + else => { + log.warn("image not ready for placement image_id={}", .{p.image_id}); + return; + }, + }; + + // Create our vertex buffer, which is always exactly one item. + // future(mitchellh): we can group rendering multiple instances of a single image + var buf = try Buffer(shaderpkg.Image).initFill( + self.api.imageBufferOptions(), + &.{.{ + .grid_pos = .{ + @as(f32, @floatFromInt(p.x)), + @as(f32, @floatFromInt(p.y)), + }, + + .cell_offset = .{ + @as(f32, @floatFromInt(p.cell_offset_x)), + @as(f32, @floatFromInt(p.cell_offset_y)), + }, + + .source_rect = .{ + @as(f32, @floatFromInt(p.source_x)), + @as(f32, @floatFromInt(p.source_y)), + @as(f32, @floatFromInt(p.source_width)), + @as(f32, @floatFromInt(p.source_height)), + }, + + .dest_size = .{ + @as(f32, @floatFromInt(p.width)), + @as(f32, @floatFromInt(p.height)), + }, + }}, + ); + defer buf.deinit(); + + pass.step(.{ + .pipeline = self.shaders.image_pipeline, + .buffers = &.{buf.buffer}, + .textures = &.{texture}, + .draw = .{ + .type = .triangle_strip, + .vertex_count = 4, + }, + }); + } + } + + /// This goes through the Kitty graphic placements and accumulates the + /// placements we need to render on our viewport. It also ensures that + /// the visible images are loaded on the GPU. + fn prepKittyGraphics( + self: *Self, + t: *terminal.Terminal, + ) !void { + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + const storage = &t.screen.kitty_images; + defer storage.dirty = false; + + // We always clear our previous placements no matter what because + // we rebuild them from scratch. + self.image_placements.clearRetainingCapacity(); + self.image_virtual = false; + + // Go through our known images and if there are any that are no longer + // in use then mark them to be freed. + // + // This never conflicts with the below because a placement can't + // reference an image that doesn't exist. + { + var it = self.images.iterator(); + while (it.next()) |kv| { + if (storage.imageById(kv.key_ptr.*) == null) { + kv.value_ptr.image.markForUnload(); + } + } + } + + // The top-left and bottom-right corners of our viewport in screen + // points. This lets us determine offsets and containment of placements. + const top = t.screen.pages.getTopLeft(.viewport); + const bot = t.screen.pages.getBottomRight(.viewport).?; + const top_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y; + const bot_y = t.screen.pages.pointFromPin(.screen, bot).?.screen.y; + + // Go through the placements and ensure the image is loaded on the GPU. + var it = storage.placements.iterator(); + while (it.next()) |kv| { + const p = kv.value_ptr; + + // Special logic based on location + switch (p.location) { + .pin => {}, + .virtual => { + // We need to mark virtual placements on our renderer so that + // we know to rebuild in more scenarios since cell changes can + // now trigger placement changes. + self.image_virtual = true; + + // We also continue out because virtual placements are + // only triggered by the unicode placeholder, not by the + // placement itself. + continue; + }, + } + + // Get the image for the placement + const image = storage.imageById(kv.key_ptr.image_id) orelse { + log.warn( + "missing image for placement, ignoring image_id={}", + .{kv.key_ptr.image_id}, + ); + continue; + }; + + try self.prepKittyPlacement(t, top_y, bot_y, &image, p); + } + + // If we have virtual placements then we need to scan for placeholders. + if (self.image_virtual) { + var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot); + while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement( + t, + &virtual_p, + ); + } + + // Sort the placements by their Z value. + std.mem.sortUnstable( + imagepkg.Placement, + self.image_placements.items, + {}, + struct { + fn lessThan( + ctx: void, + lhs: imagepkg.Placement, + rhs: imagepkg.Placement, + ) bool { + _ = ctx; + return lhs.z < rhs.z or (lhs.z == rhs.z and lhs.image_id < rhs.image_id); + } + }.lessThan, + ); + + // Find our indices. The values are sorted by z so we can find the + // first placement out of bounds to find the limits. + var bg_end: ?u32 = null; + var text_end: ?u32 = null; + const bg_limit = std.math.minInt(i32) / 2; + for (self.image_placements.items, 0..) |p, i| { + if (bg_end == null and p.z >= bg_limit) { + bg_end = @intCast(i); + } + if (text_end == null and p.z >= 0) { + text_end = @intCast(i); + } + } + + self.image_bg_end = bg_end orelse 0; + self.image_text_end = text_end orelse self.image_bg_end; + } + + fn prepKittyVirtualPlacement( + self: *Self, + t: *terminal.Terminal, + p: *const terminal.kitty.graphics.unicode.Placement, + ) !void { + const storage = &t.screen.kitty_images; + const image = storage.imageById(p.image_id) orelse { + log.warn( + "missing image for virtual placement, ignoring image_id={}", + .{p.image_id}, + ); + return; + }; + + const rp = p.renderPlacement( + storage, + &image, + self.grid_metrics.cell_width, + self.grid_metrics.cell_height, + ) catch |err| { + log.warn("error rendering virtual placement err={}", .{err}); + return; + }; + + // If our placement is zero sized then we don't do anything. + if (rp.dest_width == 0 or rp.dest_height == 0) return; + + const viewport: terminal.point.Point = t.screen.pages.pointFromPin( + .viewport, + rp.top_left, + ) orelse { + // This is unreachable with virtual placements because we should + // only ever be looking at virtual placements that are in our + // viewport in the renderer and virtual placements only ever take + // up one row. + unreachable; + }; + + // Send our image to the GPU and store the placement for rendering. + try self.prepKittyImage(&image); + try self.image_placements.append(self.alloc, .{ + .image_id = image.id, + .x = @intCast(rp.top_left.x), + .y = @intCast(viewport.viewport.y), + .z = -1, + .width = rp.dest_width, + .height = rp.dest_height, + .cell_offset_x = rp.offset_x, + .cell_offset_y = rp.offset_y, + .source_x = rp.source_x, + .source_y = rp.source_y, + .source_width = rp.source_width, + .source_height = rp.source_height, + }); + } + + fn prepKittyPlacement( + self: *Self, + t: *terminal.Terminal, + top_y: u32, + bot_y: u32, + image: *const terminal.kitty.graphics.Image, + p: *const terminal.kitty.graphics.ImageStorage.Placement, + ) !void { + // Get the rect for the placement. If this placement doesn't have + // a rect then its virtual or something so skip it. + const rect = p.rect(image.*, t) orelse return; + + // This is expensive but necessary. + const img_top_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y; + const img_bot_y = t.screen.pages.pointFromPin(.screen, rect.bottom_right).?.screen.y; + + // If the selection isn't within our viewport then skip it. + if (img_top_y > bot_y) return; + if (img_bot_y < top_y) return; + + // We need to prep this image for upload if it isn't in the cache OR + // it is in the cache but the transmit time doesn't match meaning this + // image is different. + try self.prepKittyImage(image); + + // Calculate the dimensions of our image, taking in to + // account the rows / columns specified by the placement. + const dest_size = p.calculatedSize(image.*, t); + + // Calculate the source rectangle + const source_x = @min(image.width, p.source_x); + const source_y = @min(image.height, p.source_y); + const source_width = if (p.source_width > 0) + @min(image.width - source_x, p.source_width) + else + image.width; + const source_height = if (p.source_height > 0) + @min(image.height - source_y, p.source_height) + else + image.height; + + // Get the viewport-relative Y position of the placement. + const y_pos: i32 = @as(i32, @intCast(img_top_y)) - @as(i32, @intCast(top_y)); + + // Accumulate the placement + if (dest_size.width > 0 and dest_size.height > 0) { + try self.image_placements.append(self.alloc, .{ + .image_id = image.id, + .x = @intCast(rect.top_left.x), + .y = y_pos, + .z = p.z, + .width = dest_size.width, + .height = dest_size.height, + .cell_offset_x = p.x_offset, + .cell_offset_y = p.y_offset, + .source_x = source_x, + .source_y = source_y, + .source_width = source_width, + .source_height = source_height, + }); + } + } + + fn prepKittyImage( + self: *Self, + image: *const terminal.kitty.graphics.Image, + ) !void { + // If this image exists and its transmit time is the same we assume + // it is the identical image so we don't need to send it to the GPU. + const gop = try self.images.getOrPut(self.alloc, image.id); + if (gop.found_existing and + gop.value_ptr.transmit_time.order(image.transmit_time) == .eq) + { + return; + } + + // Copy the data into the pending state. + const data = try self.alloc.dupe(u8, image.data); + errdefer self.alloc.free(data); + + // Store it in the map + const pending: Image.Pending = .{ + .width = image.width, + .height = image.height, + .data = data.ptr, + }; + + const new_image: Image = switch (image.format) { + .gray => .{ .pending_gray = pending }, + .gray_alpha => .{ .pending_gray_alpha = pending }, + .rgb => .{ .pending_rgb = pending }, + .rgba => .{ .pending_rgba = pending }, + .png => unreachable, // should be decoded by now + }; + + if (!gop.found_existing) { + gop.value_ptr.* = .{ + .image = new_image, + .transmit_time = undefined, + }; + } else { + try gop.value_ptr.image.markForReplace( + self.alloc, + new_image, + ); + } + + gop.value_ptr.transmit_time = image.transmit_time; + } + + /// Update the configuration. + pub fn changeConfig(self: *Self, config: *DerivedConfig) !void { + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + // We always redo the font shaper in case font features changed. We + // could check to see if there was an actual config change but this is + // easier and rare enough to not cause performance issues. + { + var font_shaper = try font.Shaper.init(self.alloc, .{ + .features = config.font_features.items, + }); + errdefer font_shaper.deinit(); + self.font_shaper.deinit(); + self.font_shaper = font_shaper; + } + + // We also need to reset the shaper cache so shaper info + // from the previous font isn't re-used for the new font. + const font_shaper_cache = font.ShaperCache.init(); + self.font_shaper_cache.deinit(self.alloc); + self.font_shaper_cache = font_shaper_cache; + + // Set our new minimum contrast + self.uniforms.min_contrast = config.min_contrast; + + // Set our new color space and blending + self.uniforms.bools.use_display_p3 = config.colorspace == .@"display-p3"; + self.uniforms.bools.use_linear_blending = config.blending.isLinear(); + self.uniforms.bools.use_linear_correction = config.blending == .@"linear-corrected"; + + // Set our new colors + self.default_background_color = config.background; + self.default_foreground_color = config.foreground; + self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null; + self.cursor_invert = config.cursor_invert; + + const old_blending = self.config.blending; + const custom_shaders_changed = !self.config.custom_shaders.equal(config.custom_shaders); + + self.config.deinit(); + self.config = config.*; + + // Reset our viewport to force a rebuild, in case of a font change. + self.cells_viewport = null; + + const blending_changed = old_blending != config.blending; + + if (blending_changed) { + // We update our API's blending mode. + self.api.blending = config.blending; + // And indicate that we need to reinitialize our shaders. + self.reinitialize_shaders = true; + // And indicate that our swap chain targets need to + // be re-created to account for the new blending mode. + self.target_config_modified +%= 1; + } + + if (custom_shaders_changed) { + self.reinitialize_shaders = true; + } + } + + /// Resize the screen. + pub fn setScreenSize( + self: *Self, + size: renderer.Size, + ) void { + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + // We only actually need the padding from this, + // everything else is derived elsewhere. + self.size.padding = size.padding; + + self.updateScreenSizeUniforms(); + + log.debug("screen size size={}", .{size}); + } + + /// Update uniforms that are based on the screen size. + /// + /// Caller must hold the draw mutex. + fn updateScreenSizeUniforms(self: *Self) void { + const terminal_size = self.size.terminal(); + + // Blank space around the grid. + const blank: renderer.Padding = self.size.screen.blankPadding( + self.size.padding, + .{ + .columns = self.cells.size.columns, + .rows = self.cells.size.rows, + }, + .{ + .width = self.grid_metrics.cell_width, + .height = self.grid_metrics.cell_height, + }, + ).add(self.size.padding); + + // Setup our uniforms + self.uniforms.projection_matrix = math.ortho2d( + -1 * @as(f32, @floatFromInt(self.size.padding.left)), + @floatFromInt(terminal_size.width + self.size.padding.right), + @floatFromInt(terminal_size.height + self.size.padding.bottom), + -1 * @as(f32, @floatFromInt(self.size.padding.top)), + ); + self.uniforms.grid_padding = .{ + @floatFromInt(blank.top), + @floatFromInt(blank.right), + @floatFromInt(blank.bottom), + @floatFromInt(blank.left), + }; + } + + /// Convert the terminal state to GPU cells stored in CPU memory. These + /// are then synced to the GPU in the next frame. This only updates CPU + /// memory and doesn't touch the GPU. + fn rebuildCells( + self: *Self, + wants_rebuild: bool, + screen: *terminal.Screen, + screen_type: terminal.ScreenType, + mouse: renderer.State.Mouse, + preedit: ?renderer.State.Preedit, + cursor_style_: ?renderer.CursorStyle, + color_palette: *const terminal.color.Palette, + ) !void { + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + // const start = try std.time.Instant.now(); + // const start_micro = std.time.microTimestamp(); + // defer { + // const end = std.time.Instant.now() catch unreachable; + // // "[rebuildCells time] \t" + // std.log.warn("[rebuildCells time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us}); + // } + + _ = screen_type; // we might use this again later so not deleting it yet + + // Create an arena for all our temporary allocations while rebuilding + var arena = ArenaAllocator.init(self.alloc); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + + // Create our match set for the links. + var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet( + arena_alloc, + screen, + mouse_pt, + mouse.mods, + ) else .{}; + + // Determine our x/y range for preedit. We don't want to render anything + // here because we will render the preedit separately. + const preedit_range: ?struct { + y: terminal.size.CellCountInt, + x: [2]terminal.size.CellCountInt, + cp_offset: usize, + } = if (preedit) |preedit_v| preedit: { + const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1); + break :preedit .{ + .y = screen.cursor.y, + .x = .{ range.start, range.end }, + .cp_offset = range.cp_offset, + }; + } else null; + + const grid_size_diff = + self.cells.size.rows != screen.pages.rows or + self.cells.size.columns != screen.pages.cols; + + if (grid_size_diff) { + var new_size = self.cells.size; + new_size.rows = screen.pages.rows; + new_size.columns = screen.pages.cols; + try self.cells.resize(self.alloc, new_size); + + // Update our uniforms accordingly, otherwise + // our background cells will be out of place. + self.uniforms.grid_size = .{ new_size.columns, new_size.rows }; + } + + const rebuild = wants_rebuild or grid_size_diff; + + if (rebuild) { + // If we are doing a full rebuild, then we clear the entire cell buffer. + self.cells.reset(); + + // We also reset our padding extension depending on the screen type + switch (self.config.padding_color) { + .background => {}, + + // For extension, assume we are extending in all directions. + // For "extend" this may be disabled due to heuristics below. + .extend, .@"extend-always" => { + self.uniforms.padding_extend = .{ + .up = true, + .down = true, + .left = true, + .right = true, + }; + }, + } + } + + // We rebuild the cells row-by-row because we + // do font shaping and dirty tracking by row. + var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null); + // If our cell contents buffer is shorter than the screen viewport, + // we render the rows that fit, starting from the bottom. If instead + // the viewport is shorter than the cell contents buffer, we align + // the top of the viewport with the top of the contents buffer. + var y: terminal.size.CellCountInt = @min( + screen.pages.rows, + self.cells.size.rows, + ); + while (row_it.next()) |row| { + // The viewport may have more rows than our cell contents, + // so we need to break from the loop early if we hit y = 0. + if (y == 0) break; + + y -= 1; + + if (!rebuild) { + // Only rebuild if we are doing a full rebuild or this row is dirty. + if (!row.isDirty()) continue; + + // Clear the cells if the row is dirty + self.cells.clear(y); + } + + // True if we want to do font shaping around the cursor. + // We want to do font shaping as long as the cursor is enabled. + const shape_cursor = screen.viewportIsBottom() and + y == screen.cursor.y; + + // We need to get this row's selection, if + // there is one, for proper run splitting. + const row_selection = sel: { + const sel = screen.selection orelse break :sel null; + const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse + break :sel null; + break :sel sel.containedRow(screen, pin) orelse null; + }; + + // On primary screen, we still apply vertical padding + // extension under certain conditions we feel are safe. + // + // This helps make some scenarios look better while + // avoiding scenarios we know do NOT look good. + switch (self.config.padding_color) { + // These already have the correct values set above. + .background, .@"extend-always" => {}, + + // Apply heuristics for padding extension. + .extend => if (y == 0) { + self.uniforms.padding_extend.up = !row.neverExtendBg( + color_palette, + self.background_color orelse self.default_background_color, + ); + } else if (y == self.cells.size.rows - 1) { + self.uniforms.padding_extend.down = !row.neverExtendBg( + color_palette, + self.background_color orelse self.default_background_color, + ); + }, + } + + // Iterator of runs for shaping. + var run_iter = self.font_shaper.runIterator( + self.font_grid, + screen, + row, + row_selection, + if (shape_cursor) screen.cursor.x else null, + ); + var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); + var shaper_cells: ?[]const font.shape.Cell = null; + var shaper_cells_i: usize = 0; + + const row_cells_all = row.cells(.all); + + // If our viewport is wider than our cell contents buffer, + // we still only process cells up to the width of the buffer. + const row_cells = row_cells_all[0..@min(row_cells_all.len, self.cells.size.columns)]; + + for (row_cells, 0..) |*cell, x| { + // If this cell falls within our preedit range then we + // skip this because preedits are setup separately. + if (preedit_range) |range| preedit: { + // We're not on the preedit line, no actions necessary. + if (range.y != y) break :preedit; + // We're before the preedit range, no actions necessary. + if (x < range.x[0]) break :preedit; + // We're in the preedit range, skip this cell. + if (x <= range.x[1]) continue; + // After exiting the preedit range we need to catch + // the run position up because of the missed cells. + // In all other cases, no action is necessary. + if (x != range.x[1] + 1) break :preedit; + + // Step the run iterator until we find a run that ends + // after the current cell, which will be the soonest run + // that might contain glyphs for our cell. + while (shaper_run) |run| { + if (run.offset + run.cells > x) break; + shaper_run = try run_iter.next(self.alloc); + shaper_cells = null; + shaper_cells_i = 0; + } + + const run = shaper_run orelse break :preedit; + + // If we haven't shaped this run, do so now. + shaper_cells = shaper_cells orelse + // Try to read the cells from the shaping cache if we can. + self.font_shaper_cache.get(run) orelse + cache: { + // Otherwise we have to shape them. + const cells = try self.font_shaper.shape(run); + + // Try to cache them. If caching fails for any reason we + // continue because it is just a performance optimization, + // not a correctness issue. + self.font_shaper_cache.put( + self.alloc, + run, + cells, + ) catch |err| { + log.warn( + "error caching font shaping results err={}", + .{err}, + ); + }; + + // The cells we get from direct shaping are always owned + // by the shaper and valid until the next shaping call so + // we can safely use them. + break :cache cells; + }; + + // Advance our index until we reach or pass + // our current x position in the shaper cells. + while (shaper_cells.?[shaper_cells_i].x < x) { + shaper_cells_i += 1; + } + } + + const wide = cell.wide; + + const style = row.style(cell); + + const cell_pin: terminal.Pin = cell: { + var copy = row; + copy.x = @intCast(x); + break :cell copy; + }; + + // True if this cell is selected + const selected: bool = if (screen.selection) |sel| + sel.contains(screen, .{ + .node = row.node, + .y = row.y, + .x = @intCast( + // Spacer tails should show the selection + // state of the wide cell they belong to. + if (wide == .spacer_tail) + x -| 1 + else + x, + ), + }) + else + false; + + const bg_style = style.bg(cell, color_palette); + const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color; + + // The final background color for the cell. + const bg = bg: { + if (selected) { + break :bg if (self.config.invert_selection_fg_bg) + if (style.flags.inverse) + // Cell is selected with invert selection fg/bg + // enabled, and the cell has the inverse style + // flag, so they cancel out and we get the normal + // bg color. + bg_style + else + // If it doesn't have the inverse style + // flag then we use the fg color instead. + fg_style + else + // If we don't have invert selection fg/bg set then we + // just use the selection background if set, otherwise + // the default fg color. + break :bg self.config.selection_background orelse self.foreground_color orelse self.default_foreground_color; + } + + // Not selected + break :bg if (style.flags.inverse != isCovering(cell.codepoint())) + // Two cases cause us to invert (use the fg color as the bg) + // - The "inverse" style flag. + // - A "covering" glyph; we use fg for bg in that + // case to help make sure that padding extension + // works correctly. + // + // If one of these is true (but not the other) + // then we use the fg style color for the bg. + fg_style + else + // Otherwise they cancel out. + bg_style; + }; + + const fg = fg: { + if (selected and !self.config.invert_selection_fg_bg) { + // If we don't have invert selection fg/bg set + // then we just use the selection foreground if + // set, otherwise the default bg color. + break :fg self.config.selection_foreground orelse self.background_color orelse self.default_background_color; + } + + // Whether we need to use the bg color as our fg color: + // - Cell is inverted and not selected + // - Cell is selected and not inverted + // Note: if selected then invert sel fg / bg must be + // false since we separately handle it if true above. + break :fg if (style.flags.inverse != selected) + bg_style orelse self.background_color orelse self.default_background_color + else + fg_style; + }; + + // Foreground alpha for this cell. + const alpha: u8 = if (style.flags.faint) 175 else 255; + + // Set the cell's background color. + { + const rgb = bg orelse self.background_color orelse self.default_background_color; + + // Determine our background alpha. If we have transparency configured + // then this is dynamic depending on some situations. This is all + // in an attempt to make transparency look the best for various + // situations. See inline comments. + const bg_alpha: u8 = bg_alpha: { + const default: u8 = 255; + + if (self.config.background_opacity >= 1) break :bg_alpha default; + + // Cells that are selected should be fully opaque. + if (selected) break :bg_alpha default; + + // Cells that are reversed should be fully opaque. + if (style.flags.inverse) break :bg_alpha default; + + // Cells that have an explicit bg color should be fully opaque. + if (bg_style != null) { + break :bg_alpha default; + } + + // Otherwise, we use the configured background opacity. + break :bg_alpha @intFromFloat(@round(self.config.background_opacity * 255.0)); + }; + + self.cells.bgCell(y, x).* = .{ + rgb.r, rgb.g, rgb.b, bg_alpha, + }; + } + + // If the invisible flag is set on this cell then we + // don't need to render any foreground elements, so + // we just skip all glyphs with this x coordinate. + // + // NOTE: This behavior matches xterm. Some other terminal + // emulators, e.g. Alacritty, still render text decorations + // and only make the text itself invisible. The decision + // has been made here to match xterm's behavior for this. + if (style.flags.invisible) { + continue; + } + + // Give links a single underline, unless they already have + // an underline, in which case use a double underline to + // distinguish them. + const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin)) + if (style.flags.underline == .single) + .double + else + .single + else + style.flags.underline; + + // We draw underlines first so that they layer underneath text. + // This improves readability when a colored underline is used + // which intersects parts of the text (descenders). + if (underline != .none) self.addUnderline( + @intCast(x), + @intCast(y), + underline, + style.underlineColor(color_palette) orelse fg, + alpha, + ) catch |err| { + log.warn( + "error adding underline to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + + if (style.flags.overline) self.addOverline(@intCast(x), @intCast(y), fg, alpha) catch |err| { + log.warn( + "error adding overline to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + + // If we're at or past the end of our shaper run then + // we need to get the next run from the run iterator. + if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) { + shaper_run = try run_iter.next(self.alloc); + shaper_cells = null; + shaper_cells_i = 0; + } + + if (shaper_run) |run| glyphs: { + // If we haven't shaped this run yet, do so. + shaper_cells = shaper_cells orelse + // Try to read the cells from the shaping cache if we can. + self.font_shaper_cache.get(run) orelse + cache: { + // Otherwise we have to shape them. + const cells = try self.font_shaper.shape(run); + + // Try to cache them. If caching fails for any reason we + // continue because it is just a performance optimization, + // not a correctness issue. + self.font_shaper_cache.put( + self.alloc, + run, + cells, + ) catch |err| { + log.warn( + "error caching font shaping results err={}", + .{err}, + ); + }; + + // The cells we get from direct shaping are always owned + // by the shaper and valid until the next shaping call so + // we can safely use them. + break :cache cells; + }; + + const cells = shaper_cells orelse break :glyphs; + + // If there are no shaper cells for this run, ignore it. + // This can occur for runs of empty cells, and is fine. + if (cells.len == 0) break :glyphs; + + // If we encounter a shaper cell to the left of the current + // cell then we have some problems. This logic relies on x + // position monotonically increasing. + assert(cells[shaper_cells_i].x >= x); + + // NOTE: An assumption is made here that a single cell will never + // be present in more than one shaper run. If that assumption is + // violated, this logic breaks. + + while (shaper_cells_i < cells.len and cells[shaper_cells_i].x == x) : ({ + shaper_cells_i += 1; + }) { + self.addGlyph( + @intCast(x), + @intCast(y), + cell_pin, + cells[shaper_cells_i], + shaper_run.?, + fg, + alpha, + ) catch |err| { + log.warn( + "error adding glyph to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + } + } + + // Finally, draw a strikethrough if necessary. + if (style.flags.strikethrough) self.addStrikethrough( + @intCast(x), + @intCast(y), + fg, + alpha, + ) catch |err| { + log.warn( + "error adding strikethrough to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + } + } + + // Setup our cursor rendering information. + cursor: { + // By default, we don't handle cursor inversion on the shader. + self.cells.setCursor(null); + self.uniforms.cursor_pos = .{ + std.math.maxInt(u16), + std.math.maxInt(u16), + }; + + // If we have preedit text, we don't setup a cursor + if (preedit != null) break :cursor; + + // Prepare the cursor cell contents. + const style = cursor_style_ orelse break :cursor; + const cursor_color = self.cursor_color orelse self.default_cursor_color orelse color: { + if (self.cursor_invert) { + // Use the foreground color from the cell under the cursor, if any. + const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); + break :color if (sty.flags.inverse) + // If the cell is reversed, use background color instead. + (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color) + else + (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color); + } else { + break :color self.foreground_color orelse self.default_foreground_color; + } + }; + + self.addCursor(screen, style, cursor_color); + + // If the cursor is visible then we set our uniforms. + if (style == .block and screen.viewportIsBottom()) { + const wide = screen.cursor.page_cell.wide; + + self.uniforms.cursor_pos = .{ + // If we are a spacer tail of a wide cell, our cursor needs + // to move back one cell. The saturate is to ensure we don't + // overflow but this shouldn't happen with well-formed input. + switch (wide) { + .narrow, .spacer_head, .wide => screen.cursor.x, + .spacer_tail => screen.cursor.x -| 1, + }, + screen.cursor.y, + }; + + self.uniforms.bools.cursor_wide = switch (wide) { + .narrow, .spacer_head => false, + .wide, .spacer_tail => true, + }; + + const uniform_color = if (self.cursor_invert) blk: { + // Use the background color from the cell under the cursor, if any. + const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); + break :blk if (sty.flags.inverse) + // If the cell is reversed, use foreground color instead. + (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color) + else + (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color); + } else if (self.config.cursor_text) |txt| + txt + else + self.background_color orelse self.default_background_color; + + self.uniforms.cursor_color = .{ + uniform_color.r, + uniform_color.g, + uniform_color.b, + 255, + }; + } + } + + // Setup our preedit text. + if (preedit) |preedit_v| { + const range = preedit_range.?; + var x = range.x[0]; + for (preedit_v.codepoints[range.cp_offset..]) |cp| { + self.addPreeditCell(cp, .{ .x = x, .y = range.y }) catch |err| { + log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ + x, + range.y, + err, + }); + }; + + x += if (cp.wide) 2 else 1; + } + } + + // Update that our cells rebuilt + self.cells_rebuilt = true; + + // Log some things + // log.debug("rebuildCells complete cached_runs={}", .{ + // self.font_shaper_cache.count(), + // }); + } + + /// Add an underline decoration to the specified cell + fn addUnderline( + self: *Self, + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, + style: terminal.Attribute.Underline, + color: terminal.color.RGB, + alpha: u8, + ) !void { + const sprite: font.Sprite = switch (style) { + .none => unreachable, + .single => .underline, + .double => .underline_double, + .dotted => .underline_dotted, + .dashed => .underline_dashed, + .curly => .underline_curly, + }; + + const render = try self.font_grid.renderGlyph( + self.alloc, + font.sprite_index, + @intFromEnum(sprite), + .{ + .cell_width = 1, + .grid_metrics = self.grid_metrics, + }, + ); + + try self.cells.add(self.alloc, .underline, .{ + .mode = .fg, + .grid_pos = .{ @intCast(x), @intCast(y) }, + .constraint_width = 1, + .color = .{ color.r, color.g, color.b, alpha }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .bearings = .{ + @intCast(render.glyph.offset_x), + @intCast(render.glyph.offset_y), + }, + }); + } + + /// Add a overline decoration to the specified cell + fn addOverline( + self: *Self, + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, + color: terminal.color.RGB, + alpha: u8, + ) !void { + const render = try self.font_grid.renderGlyph( + self.alloc, + font.sprite_index, + @intFromEnum(font.Sprite.overline), + .{ + .cell_width = 1, + .grid_metrics = self.grid_metrics, + }, + ); + + try self.cells.add(self.alloc, .overline, .{ + .mode = .fg, + .grid_pos = .{ @intCast(x), @intCast(y) }, + .constraint_width = 1, + .color = .{ color.r, color.g, color.b, alpha }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .bearings = .{ + @intCast(render.glyph.offset_x), + @intCast(render.glyph.offset_y), + }, + }); + } + + /// Add a strikethrough decoration to the specified cell + fn addStrikethrough( + self: *Self, + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, + color: terminal.color.RGB, + alpha: u8, + ) !void { + const render = try self.font_grid.renderGlyph( + self.alloc, + font.sprite_index, + @intFromEnum(font.Sprite.strikethrough), + .{ + .cell_width = 1, + .grid_metrics = self.grid_metrics, + }, + ); + + try self.cells.add(self.alloc, .strikethrough, .{ + .mode = .fg, + .grid_pos = .{ @intCast(x), @intCast(y) }, + .constraint_width = 1, + .color = .{ color.r, color.g, color.b, alpha }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .bearings = .{ + @intCast(render.glyph.offset_x), + @intCast(render.glyph.offset_y), + }, + }); + } + + // Add a glyph to the specified cell. + fn addGlyph( + self: *Self, + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, + cell_pin: terminal.Pin, + shaper_cell: font.shape.Cell, + shaper_run: font.shape.TextRun, + color: terminal.color.RGB, + alpha: u8, + ) !void { + const rac = cell_pin.rowAndCell(); + const cell = rac.cell; + + // Render + const render = try self.font_grid.renderGlyph( + self.alloc, + shaper_run.font_index, + shaper_cell.glyph_index, + .{ + .grid_metrics = self.grid_metrics, + .thicken = self.config.font_thicken, + .thicken_strength = self.config.font_thicken_strength, + }, + ); + + // If the glyph is 0 width or height, it will be invisible + // when drawn, so don't bother adding it to the buffer. + if (render.glyph.width == 0 or render.glyph.height == 0) { + return; + } + + const mode: shaderpkg.CellText.Mode = switch (try fgMode( + render.presentation, + cell_pin, + )) { + .normal => .fg, + .color => .fg_color, + .constrained => .fg_constrained, + .powerline => .fg_powerline, + }; + + try self.cells.add(self.alloc, .text, .{ + .mode = mode, + .grid_pos = .{ @intCast(x), @intCast(y) }, + .constraint_width = cell.gridWidth(), + .color = .{ color.r, color.g, color.b, alpha }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .bearings = .{ + @intCast(render.glyph.offset_x + shaper_cell.x_offset), + @intCast(render.glyph.offset_y + shaper_cell.y_offset), + }, + }); + } + + fn addCursor( + self: *Self, + screen: *terminal.Screen, + cursor_style: renderer.CursorStyle, + cursor_color: terminal.color.RGB, + ) void { + // Add the cursor. We render the cursor over the wide character if + // we're on the wide character tail. + const wide, const x = cell: { + // The cursor goes over the screen cursor position. + const cell = screen.cursor.page_cell; + if (cell.wide != .spacer_tail or screen.cursor.x == 0) + break :cell .{ cell.wide == .wide, screen.cursor.x }; + + // If we're part of a wide character, we move the cursor back to + // the actual character. + const prev_cell = screen.cursorCellLeft(1); + break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 }; + }; + + const alpha: u8 = if (!self.focused) 255 else alpha: { + const alpha = 255 * self.config.cursor_opacity; + break :alpha @intFromFloat(@ceil(alpha)); + }; + + const render = switch (cursor_style) { + .block, + .block_hollow, + .bar, + .underline, + => render: { + const sprite: font.Sprite = switch (cursor_style) { + .block => .cursor_rect, + .block_hollow => .cursor_hollow_rect, + .bar => .cursor_bar, + .underline => .underline, + .lock => unreachable, + }; + + break :render self.font_grid.renderGlyph( + self.alloc, + font.sprite_index, + @intFromEnum(sprite), + .{ + .cell_width = if (wide) 2 else 1, + .grid_metrics = self.grid_metrics, + }, + ) catch |err| { + log.warn("error rendering cursor glyph err={}", .{err}); + return; + }; + }, + + .lock => self.font_grid.renderCodepoint( + self.alloc, + 0xF023, // lock symbol + .regular, + .text, + .{ + .cell_width = if (wide) 2 else 1, + .grid_metrics = self.grid_metrics, + }, + ) catch |err| { + log.warn("error rendering cursor glyph err={}", .{err}); + return; + } orelse { + // This should never happen because we embed nerd + // fonts so we just log and return instead of fallback. + log.warn("failed to find lock symbol for cursor codepoint=0xF023", .{}); + return; + }, + }; + + self.cells.setCursor(.{ + .mode = .cursor, + .grid_pos = .{ x, screen.cursor.y }, + .color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .bearings = .{ + @intCast(render.glyph.offset_x), + @intCast(render.glyph.offset_y), + }, + }); + } + + fn addPreeditCell( + self: *Self, + cp: renderer.State.Preedit.Codepoint, + coord: terminal.Coordinate, + ) !void { + // Preedit is rendered inverted + const bg = self.foreground_color orelse self.default_foreground_color; + const fg = self.background_color orelse self.default_background_color; + + // Render the glyph for our preedit text + const render_ = self.font_grid.renderCodepoint( + self.alloc, + @intCast(cp.codepoint), + .regular, + .text, + .{ .grid_metrics = self.grid_metrics }, + ) catch |err| { + log.warn("error rendering preedit glyph err={}", .{err}); + return; + }; + const render = render_ orelse { + log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint}); + return; + }; + + // Add our opaque background cell + self.cells.bgCell(coord.y, coord.x).* = .{ + bg.r, bg.g, bg.b, 255, + }; + if (cp.wide and coord.x < self.cells.size.columns - 1) { + self.cells.bgCell(coord.y, coord.x + 1).* = .{ + bg.r, bg.g, bg.b, 255, + }; + } + + // Add our text + try self.cells.add(self.alloc, .text, .{ + .mode = .fg, + .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, + .color = .{ fg.r, fg.g, fg.b, 255 }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .bearings = .{ + @intCast(render.glyph.offset_x), + @intCast(render.glyph.offset_y), + }, + }); + } + + /// Sync the atlas data to the given texture. This copies the bytes + /// associated with the atlas to the given texture. If the atlas no + /// longer fits into the texture, the texture will be resized. + fn syncAtlasTexture( + self: *const Self, + atlas: *const font.Atlas, + texture: *Texture, + ) !void { + if (atlas.size > texture.width) { + // Free our old texture + texture.*.deinit(); + + // Reallocate + texture.* = try self.api.initAtlasTexture(atlas); + } + + try texture.replaceRegion(0, 0, atlas.size, atlas.size, atlas.data); + } + }; +} diff --git a/src/renderer/metal/Frame.zig b/src/renderer/metal/Frame.zig new file mode 100644 index 000000000..81b38e7b6 --- /dev/null +++ b/src/renderer/metal/Frame.zig @@ -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(); + } +} diff --git a/src/renderer/metal/IOSurfaceLayer.zig b/src/renderer/metal/IOSurfaceLayer.zig new file mode 100644 index 000000000..4c51a55c2 --- /dev/null +++ b/src/renderer/metal/IOSurfaceLayer.zig @@ -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; +} diff --git a/src/renderer/metal/Pipeline.zig b/src/renderer/metal/Pipeline.zig new file mode 100644 index 000000000..f72aeb2e1 --- /dev/null +++ b/src/renderer/metal/Pipeline.zig @@ -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; +} diff --git a/src/renderer/metal/RenderPass.zig b/src/renderer/metal/RenderPass.zig new file mode 100644 index 000000000..e48bc4c00 --- /dev/null +++ b/src/renderer/metal/RenderPass.zig @@ -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"), .{}); +} diff --git a/src/renderer/metal/Target.zig b/src/renderer/metal/Target.zig new file mode 100644 index 000000000..fa62d3014 --- /dev/null +++ b/src/renderer/metal/Target.zig @@ -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(); +} diff --git a/src/renderer/metal/Texture.zig b/src/renderer/metal/Texture.zig new file mode 100644 index 000000000..6e3ae78c7 --- /dev/null +++ b/src/renderer/metal/Texture.zig @@ -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, + }; +} diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index 90a1a65ab..e1daa6848 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -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, diff --git a/src/renderer/metal/buffer.zig b/src/renderer/metal/buffer.zig index 4128e297b..43320a60b 100644 --- a/src/renderer/metal/buffer.zig +++ b/src/renderer/metal/buffer.zig @@ -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:", diff --git a/src/renderer/metal/image.zig b/src/renderer/metal/image.zig index 7d2599308..1bfa3c621 100644 --- a/src/renderer/metal/image.zig +++ b/src/renderer/metal/image.zig @@ -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); - } }; diff --git a/src/renderer/metal/sampler.zig b/src/renderer/metal/sampler.zig deleted file mode 100644 index c7a04df3a..000000000 --- a/src/renderer/metal/sampler.zig +++ /dev/null @@ -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"), .{}); - } -}; diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index ff5f1e6bd..68994882e 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -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 { diff --git a/src/renderer/opengl/CellProgram.zig b/src/renderer/opengl/CellProgram.zig deleted file mode 100644 index c4da8e233..000000000 --- a/src/renderer/opengl/CellProgram.zig +++ /dev/null @@ -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(); - } -}; diff --git a/src/renderer/opengl/Frame.zig b/src/renderer/opengl/Frame.zig new file mode 100644 index 000000000..4c23fe106 --- /dev/null +++ b/src/renderer/opengl/Frame.zig @@ -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); +} diff --git a/src/renderer/opengl/ImageProgram.zig b/src/renderer/opengl/ImageProgram.zig deleted file mode 100644 index ff6794085..000000000 --- a/src/renderer/opengl/ImageProgram.zig +++ /dev/null @@ -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(); - } -}; diff --git a/src/renderer/opengl/Pipeline.zig b/src/renderer/opengl/Pipeline.zig new file mode 100644 index 000000000..127d689f5 --- /dev/null +++ b/src/renderer/opengl/Pipeline.zig @@ -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, + }; + } +} diff --git a/src/renderer/opengl/RenderPass.zig b/src/renderer/opengl/RenderPass.zig new file mode 100644 index 000000000..0f5bd89e7 --- /dev/null +++ b/src/renderer/opengl/RenderPass.zig @@ -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(); +} diff --git a/src/renderer/opengl/Target.zig b/src/renderer/opengl/Target.zig new file mode 100644 index 000000000..1b3a13ed0 --- /dev/null +++ b/src/renderer/opengl/Target.zig @@ -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(); +} diff --git a/src/renderer/opengl/Texture.zig b/src/renderer/opengl/Texture.zig new file mode 100644 index 000000000..84a1ae9bc --- /dev/null +++ b/src/renderer/opengl/Texture.zig @@ -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, + ); +} + diff --git a/src/renderer/opengl/buffer.zig b/src/renderer/opengl/buffer.zig new file mode 100644 index 000000000..48b6f410e --- /dev/null +++ b/src/renderer/opengl/buffer.zig @@ -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; + } + }; +} diff --git a/src/renderer/opengl/cell.zig b/src/renderer/opengl/cell.zig new file mode 100644 index 000000000..abdbaa0e8 --- /dev/null +++ b/src/renderer/opengl/cell.zig @@ -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(); + } +}; diff --git a/src/renderer/opengl/custom.zig b/src/renderer/opengl/custom.zig deleted file mode 100644 index 859277ce5..000000000 --- a/src/renderer/opengl/custom.zig +++ /dev/null @@ -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, - ); - } - }; -}; diff --git a/src/renderer/opengl/image.zig b/src/renderer/opengl/image.zig index 26cd90736..77779fb8a 100644 --- a/src/renderer/opengl/image.zig +++ b/src/renderer/opengl/image.zig @@ -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. diff --git a/src/renderer/opengl/shaders.zig b/src/renderer/opengl/shaders.zig new file mode 100644 index 000000000..253ae8719 --- /dev/null +++ b/src/renderer/opengl/shaders.zig @@ -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; +} diff --git a/src/renderer/shaders/cell.f.glsl b/src/renderer/shaders/cell.f.glsl deleted file mode 100644 index f9c1ce2b1..000000000 --- a/src/renderer/shaders/cell.f.glsl +++ /dev/null @@ -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; - } -} diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index 5b3875221..039c600ed 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -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 textureGrayscale [[texture(0)]], texture2d 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 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. diff --git a/src/renderer/shaders/cell.v.glsl b/src/renderer/shaders/cell.v.glsl deleted file mode 100644 index f37e69adc..000000000 --- a/src/renderer/shaders/cell.v.glsl +++ /dev/null @@ -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; - } -} diff --git a/src/renderer/shaders/custom.v.glsl b/src/renderer/shaders/custom.v.glsl deleted file mode 100644 index 653e1800e..000000000 --- a/src/renderer/shaders/custom.v.glsl +++ /dev/null @@ -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); -} diff --git a/src/renderer/shaders/glsl/cell_bg.f.glsl b/src/renderer/shaders/glsl/cell_bg.f.glsl new file mode 100644 index 000000000..cfd598f95 --- /dev/null +++ b/src/renderer/shaders/glsl/cell_bg.f.glsl @@ -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(); +} diff --git a/src/renderer/shaders/glsl/cell_text.f.glsl b/src/renderer/shaders/glsl/cell_text.f.glsl new file mode 100644 index 000000000..fda552424 --- /dev/null +++ b/src/renderer/shaders/glsl/cell_text.f.glsl @@ -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; + } + } +} diff --git a/src/renderer/shaders/glsl/cell_text.v.glsl b/src/renderer/shaders/glsl/cell_text.v.glsl new file mode 100644 index 000000000..76ede1082 --- /dev/null +++ b/src/renderer/shaders/glsl/cell_text.v.glsl @@ -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); + } +} diff --git a/src/renderer/shaders/glsl/common.glsl b/src/renderer/shaders/glsl/common.glsl new file mode 100644 index 000000000..0450d0c06 --- /dev/null +++ b/src/renderer/shaders/glsl/common.glsl @@ -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; +} + +//----------------------------------------------------------------------------// diff --git a/src/renderer/shaders/glsl/full_screen.v.glsl b/src/renderer/shaders/glsl/full_screen.v.glsl new file mode 100644 index 000000000..b89cedfa5 --- /dev/null +++ b/src/renderer/shaders/glsl/full_screen.v.glsl @@ -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; +} diff --git a/src/renderer/shaders/glsl/image.f.glsl b/src/renderer/shaders/glsl/image.f.glsl new file mode 100644 index 000000000..cd93cf666 --- /dev/null +++ b/src/renderer/shaders/glsl/image.f.glsl @@ -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; +} diff --git a/src/renderer/shaders/glsl/image.v.glsl b/src/renderer/shaders/glsl/image.v.glsl new file mode 100644 index 000000000..55b12ed68 --- /dev/null +++ b/src/renderer/shaders/glsl/image.v.glsl @@ -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); +} diff --git a/src/renderer/shaders/image.f.glsl b/src/renderer/shaders/image.f.glsl deleted file mode 100644 index e4aa9ef8e..000000000 --- a/src/renderer/shaders/image.f.glsl +++ /dev/null @@ -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); -} diff --git a/src/renderer/shaders/image.v.glsl b/src/renderer/shaders/image.v.glsl deleted file mode 100644 index e3d07ca9e..000000000 --- a/src/renderer/shaders/image.v.glsl +++ /dev/null @@ -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); -} diff --git a/src/renderer/shaders/shadertoy_prefix.glsl b/src/renderer/shaders/shadertoy_prefix.glsl index a1a220bd4..5bc25bc03 100644 --- a/src/renderer/shaders/shadertoy_prefix.glsl +++ b/src/renderer/shaders/shadertoy_prefix.glsl @@ -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; diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 45d86cbfe..68171a23e 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -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 {