aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/renderer/metal/IOSurfaceLayer.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/renderer/metal/IOSurfaceLayer.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/renderer/metal/IOSurfaceLayer.zig')
-rw-r--r--vendor/libghostty-vt/src/renderer/metal/IOSurfaceLayer.zig183
1 files changed, 183 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/renderer/metal/IOSurfaceLayer.zig b/vendor/libghostty-vt/src/renderer/metal/IOSurfaceLayer.zig
new file mode 100644
index 00000000..34fbfbed
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/metal/IOSurfaceLayer.zig
@@ -0,0 +1,183 @@
+//! 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 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 {
+ // The layer returned by `[CALayer layer]` is autoreleased, which means
+ // that at the end of the current autorelease pool it will be deallocated
+ // if it isn't retained, so we retain it here manually an extra time.
+ const layer = (try getSubclass()).msgSend(
+ objc.Object,
+ objc.sel("layer"),
+ .{},
+ ).retain();
+ 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();
+ // NOTE: Since `self.layer` is passed as an `objc.c.id`, it's
+ // automatically retained when the block is copied, so we
+ // don't need to retain it ourselves like with the surface.
+
+ var block = 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);
+ } else {
+ // NOTE: The block will be copied when we pass it to dispatch_async,
+ // and then automatically be deallocated by the objc runtime
+ // once it's executed.
+
+ macos.dispatch.dispatch_async(
+ @ptrCast(macos.dispatch.queue.getMain()),
+ @ptrCast(&block),
+ );
+ }
+}
+
+/// 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();
+
+ // 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;
+}