lib/sys/src/drm.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 //! Linux DRM render nodes and the synchronization objects (syncobjs) they own, through the kernel
2 //! uAPI of `include/uapi/drm/drm.h`. A syncobj is a kernel handle to a fence or, as a timeline, to
3 //! a sequence of fences numbered by 64-bit points. Compositors and GPU drivers exchange timeline
4 //! syncobjs to say when a buffer's contents are ready and when its reader is done with it.
5 //!
6 //! A render node is `/dev/dri/renderD<minor>`. Syncobj handles are local to the open file that
7 //! created or imported them, so every call below takes the node that owns the handle.
8
9 const builtin = @import("builtin");
10 const std = @import("std");
11
12 pub const supported = builtin.os.tag == .linux;
13
14 const linux = std.os.linux;
15
16 pub const Error = error{
17 UnsupportedPlatform,
18 OpenFailed,
19 AccessDenied,
20 /// The node is not the device that was asked for.
21 DeviceMismatch,
22 /// The kernel rejected the request's arguments or does not know the request.
23 Unsupported,
24 InvalidHandle,
25 OutOfMemory,
26 /// The deadline passed before the point signalled.
27 Timeout,
28 Failed,
29 };
30
31 /// A Linux device number, as `stat` reports it in `st_rdev` and as Wayland's dmabuf feedback sends
32 /// it in native byte order.
33 pub const DeviceNumber = struct {
34 major: u32,
35 minor: u32,
36
37 /// Decodes the kernel's `dev_t` layout, which splits each number across two fields.
38 pub fn fromRaw(encoded: u64) DeviceNumber {
39 return .{
40 .major = @intCast(((encoded >> 8) & 0xfff) | ((encoded >> 32) & 0xffff_f000)),
41 .minor = @intCast((encoded & 0xff) | ((encoded >> 12) & 0xffff_ff00)),
42 };
43 }
44
45 pub fn raw(self: DeviceNumber) u64 {
46 const major: u64 = self.major;
47 const minor: u64 = self.minor;
48 return (minor & 0xff) | ((major & 0xfff) << 8) | ((minor & ~@as(u64, 0xff)) << 12) | ((major & ~@as(u64, 0xfff)) << 32);
49 }
50 };
51
52 /// A timeline syncobj handle, valid on the render node that created or imported it.
53 pub const Syncobj = struct {
54 handle: u32,
55 };
56
57 const ioctl_base: u32 = 'd';
58
59 fn iowr(number: u8, comptime T: type) u32 {
60 return (3 << 30) | (@as(u32, @sizeOf(T)) << 16) | (ioctl_base << 8) | number;
61 }
62
63 const SyncobjCreate = extern struct {
64 handle: u32 = 0,
65 flags: u32 = 0,
66 };
67
68 const SyncobjDestroy = extern struct {
69 handle: u32,
70 pad: u32 = 0,
71 };
72
73 const SyncobjHandle = extern struct {
74 handle: u32 = 0,
75 flags: u32 = 0,
76 fd: i32 = -1,
77 pad: u32 = 0,
78 point: u64 = 0,
79 };
80
81 const SyncobjTimelineWait = extern struct {
82 handles: u64,
83 points: u64,
84 timeout_nsec: i64,
85 count_handles: u32,
86 flags: u32,
87 first_signaled: u32 = 0,
88 pad: u32 = 0,
89 deadline_nsec: u64 = 0,
90 };
91
92 const SyncobjTimelineArray = extern struct {
93 handles: u64,
94 points: u64,
95 count_handles: u32,
96 flags: u32 = 0,
97 };
98
99 const ioctl_syncobj_create = iowr(0xBF, SyncobjCreate);
100 const ioctl_syncobj_destroy = iowr(0xC0, SyncobjDestroy);
101 const ioctl_syncobj_handle_to_fd = iowr(0xC1, SyncobjHandle);
102 const ioctl_syncobj_fd_to_handle = iowr(0xC2, SyncobjHandle);
103 const ioctl_syncobj_timeline_wait = iowr(0xCA, SyncobjTimelineWait);
104 const ioctl_syncobj_query = iowr(0xCB, SyncobjTimelineArray);
105 const ioctl_syncobj_timeline_signal = iowr(0xCD, SyncobjTimelineArray);
106
107 const fd_to_handle_import_sync_file: u32 = 1 << 0;
108 const fd_to_handle_timeline: u32 = 1 << 1;
109 const wait_for_submit: u32 = 1 << 1;
110
111 pub const RenderNode = struct {
112 fd: i32,
113
114 /// Opens `/dev/dri/renderD<minor>` and checks that it is `device`, so a caller that learned a
115 /// device number from a compositor or a driver reaches that device and no other.
116 pub fn open(device: DeviceNumber) Error!RenderNode {
117 if (comptime !supported) return error.UnsupportedPlatform;
118 var path_buffer: [32]u8 = undefined;
119 const path = std.fmt.bufPrintSentinel(&path_buffer, "/dev/dri/renderD{d}", .{device.minor}, 0) catch
120 return error.OpenFailed;
121 const rc = linux.open(path, .{ .ACCMODE = .RDWR, .CLOEXEC = true }, 0);
122 const fd: i32 = switch (linux.errno(rc)) {
123 .SUCCESS => @intCast(rc),
124 .ACCES, .PERM => return error.AccessDenied,
125 else => return error.OpenFailed,
126 };
127 errdefer _ = linux.close(fd);
128 var status = std.mem.zeroes(linux.Statx);
129 const result = linux.statx(fd, "", linux.AT.EMPTY_PATH, .{ .TYPE = true }, &status);
130 if (linux.errno(result) != .SUCCESS) return error.OpenFailed;
131 if (status.rdev_major != device.major or status.rdev_minor != device.minor) return error.DeviceMismatch;
132 return .{ .fd = fd };
133 }
134
135 pub fn close(self: RenderNode) void {
136 if (comptime !supported) return;
137 _ = linux.close(self.fd);
138 }
139
140 /// Creates a syncobj with no fence. As a timeline its last signalled point starts at 0.
141 pub fn createSyncobj(self: RenderNode) Error!Syncobj {
142 var request = SyncobjCreate{};
143 try self.call(ioctl_syncobj_create, &request);
144 return .{ .handle = request.handle };
145 }
146
147 pub fn destroySyncobj(self: RenderNode, syncobj: Syncobj) void {
148 var request = SyncobjDestroy{ .handle = syncobj.handle };
149 self.call(ioctl_syncobj_destroy, &request) catch {};
150 }
151
152 /// A new file descriptor for the whole syncobj, which another process imports as the same
153 /// object. The caller owns the descriptor.
154 pub fn exportSyncobj(self: RenderNode, syncobj: Syncobj) Error!i32 {
155 var request = SyncobjHandle{ .handle = syncobj.handle };
156 try self.call(ioctl_syncobj_handle_to_fd, &request);
157 return request.fd;
158 }
159
160 /// Attaches the fence of `sync_file` to `point` of a timeline syncobj, so the point signals
161 /// when that fence does. The caller keeps ownership of `sync_file`.
162 pub fn importSyncFile(self: RenderNode, syncobj: Syncobj, point: u64, sync_file: i32) Error!void {
163 std.debug.assert(point != 0);
164 var request = SyncobjHandle{
165 .handle = syncobj.handle,
166 .flags = fd_to_handle_import_sync_file | fd_to_handle_timeline,
167 .fd = sync_file,
168 .point = point,
169 };
170 try self.call(ioctl_syncobj_fd_to_handle, &request);
171 }
172
173 /// Waits until `point` has a fence and that fence has signalled, or until `deadline_ns` on
174 /// `CLOCK_MONOTONIC` passes. A point nobody has attached a fence to yet counts as unsignalled.
175 pub fn waitPoint(self: RenderNode, syncobj: Syncobj, point: u64, deadline_ns: i64) Error!void {
176 var handle = syncobj.handle;
177 var wanted = point;
178 var request = SyncobjTimelineWait{
179 .handles = @intFromPtr(&handle),
180 .points = @intFromPtr(&wanted),
181 .timeout_nsec = deadline_ns,
182 .count_handles = 1,
183 .flags = wait_for_submit,
184 };
185 try self.call(ioctl_syncobj_timeline_wait, &request);
186 }
187
188 /// The highest point of the timeline that has signalled.
189 pub fn signalledPoint(self: RenderNode, syncobj: Syncobj) Error!u64 {
190 var handle = syncobj.handle;
191 var point: u64 = 0;
192 var request = SyncobjTimelineArray{
193 .handles = @intFromPtr(&handle),
194 .points = @intFromPtr(&point),
195 .count_handles = 1,
196 };
197 try self.call(ioctl_syncobj_query, &request);
198 return point;
199 }
200
201 /// Signals `point` from the host, as a reader that finished on the CPU does.
202 pub fn signalPoint(self: RenderNode, syncobj: Syncobj, point: u64) Error!void {
203 var handle = syncobj.handle;
204 var signalled = point;
205 var request = SyncobjTimelineArray{
206 .handles = @intFromPtr(&handle),
207 .points = @intFromPtr(&signalled),
208 .count_handles = 1,
209 };
210 try self.call(ioctl_syncobj_timeline_signal, &request);
211 }
212
213 fn call(self: RenderNode, request_code: u32, argument: anytype) Error!void {
214 if (comptime !supported) return error.UnsupportedPlatform;
215 while (true) {
216 const rc = linux.ioctl(self.fd, request_code, @intFromPtr(argument));
217 return switch (linux.errno(rc)) {
218 .SUCCESS => {},
219 .INTR, .AGAIN => continue,
220 .TIME => error.Timeout,
221 .NOENT => error.InvalidHandle,
222 .NOMEM => error.OutOfMemory,
223 .INVAL, .NOTTY, .OPNOTSUPP => error.Unsupported,
224 else => error.Failed,
225 };
226 }
227 }
228 };
229
230 /// The current `CLOCK_MONOTONIC` time, the clock syncobj deadlines are measured on.
231 pub fn monotonicNow() i64 {
232 var now: linux.timespec = undefined;
233 std.debug.assert(linux.errno(linux.clock_gettime(.MONOTONIC, &now)) == .SUCCESS);
234 return @as(i64, now.sec) * std.time.ns_per_s + now.nsec;
235 }
236
237 test "device numbers round trip through the kernel dev_t layout" {
238 const render = DeviceNumber{ .major = 226, .minor = 128 };
239 try std.testing.expectEqual(@as(u64, 0xE280), render.raw());
240 try std.testing.expectEqual(render, DeviceNumber.fromRaw(0xE280));
241 const wide = DeviceNumber{ .major = 0x12345, .minor = 0x6789a };
242 try std.testing.expectEqual(wide, DeviceNumber.fromRaw(wide.raw()));
243 }
244
245 test "syncobj requests match the uAPI structure sizes" {
246 try std.testing.expectEqual(@as(usize, 8), @sizeOf(SyncobjCreate));
247 try std.testing.expectEqual(@as(usize, 24), @sizeOf(SyncobjHandle));
248 try std.testing.expectEqual(@as(usize, 48), @sizeOf(SyncobjTimelineWait));
249 try std.testing.expectEqual(@as(usize, 24), @sizeOf(SyncobjTimelineArray));
250 try std.testing.expectEqual(@as(u32, 0xC00864BF), ioctl_syncobj_create);
251 try std.testing.expectEqual(@as(u32, 0xC01864C2), ioctl_syncobj_fd_to_handle);
252 try std.testing.expectEqual(@as(u32, 0xC03064CA), ioctl_syncobj_timeline_wait);
253 }
254
255 test "a timeline point signalled from the host ends a wait and a missing point times out" {
256 if (comptime !supported) return error.SkipZigTest;
257 const node = RenderNode.open(.{ .major = 226, .minor = 128 }) catch return error.SkipZigTest;
258 defer node.close();
259 const syncobj = try node.createSyncobj();
260 defer node.destroySyncobj(syncobj);
261 try std.testing.expectEqual(@as(u64, 0), try node.signalledPoint(syncobj));
262 try std.testing.expectError(error.Timeout, node.waitPoint(syncobj, 1, monotonicNow() + std.time.ns_per_ms));
263 try node.signalPoint(syncobj, 1);
264 try node.waitPoint(syncobj, 1, monotonicNow() + std.time.ns_per_s);
265 try std.testing.expectEqual(@as(u64, 1), try node.signalledPoint(syncobj));
266 const exported = try node.exportSyncobj(syncobj);
267 _ = linux.close(exported);
268 }