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 }