1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
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 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 {
// The layer may be retained by the view after we release our reference.
// Clear the callback first so that a later display pass can't access the
// renderer that owned this wrapper after it has been freed.
self.setDisplayCallback(null, null);
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;
}
|