lib/windowing/src/wayland/present/dmabuf.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 //! Buffers another device renders, handed to the compositor as dma-bufs with explicit
2 //! synchronization. The compositor describes which formats and modifiers it can import, and from
3 //! which device, through dma-buf feedback (`zwp_linux_dmabuf_feedback_v1`). The caller imports each
4 //! of its images once as a `wl_buffer` and each image's DRM timeline syncobj once as a
5 //! `wp_linux_drm_syncobj_timeline_v1`. Every commit then names an acquire point the compositor
6 //! waits for before reading the buffer and a release point it signals once it has stopped.
7 //!
8 //! The package keeps no GPU state. The caller owns the dma-bufs and timelines. Each descriptor it
9 //! passes is duplicated for the compositor, and the caller keeps its own.
10
11 const builtin = @import("builtin");
12 const std = @import("std");
13 const sys = @import("sys");
14 const native = @import("wayland");
15 const windowing = @import("../../root.zig");
16
17 const core = native.protocol.core;
18 const desktop = native.protocol.desktop;
19 const runtime = native.runtime;
20
21 const linux_dmabuf = desktop.zwp_linux_dmabuf_v1;
22 const buffer_params = desktop.zwp_linux_buffer_params_v1;
23 const feedback_protocol = desktop.zwp_linux_dmabuf_feedback_v1;
24 const syncobj_manager = desktop.wp_linux_drm_syncobj_manager_v1;
25 const syncobj_surface = desktop.wp_linux_drm_syncobj_surface_v1;
26 const syncobj_timeline = desktop.wp_linux_drm_syncobj_timeline_v1;
27
28 /// The lowest `zwp_linux_dmabuf_v1` version with feedback, and the highest this module speaks.
29 pub const minimum_dmabuf_version: u32 = 4;
30 pub const maximum_dmabuf_version: u32 = 5;
31
32 /// Memory planes one buffer may have.
33 pub const max_planes: u32 = 4;
34 /// Buffers imported at once, enough for two rings of four.
35 pub const max_buffers: u32 = 8;
36 /// Timelines imported at once, one per buffer.
37 pub const max_timelines: u32 = max_buffers;
38 /// Tranches one feedback update may carry.
39 pub const max_tranches: u32 = 8;
40 /// Entries of the format table kept. The table's wire form spends 16 bytes on each.
41 pub const max_table_entries: u32 = 4096;
42 /// Table indices kept across all tranches of one update.
43 pub const max_tranche_indices: u32 = 4096;
44
45 pub const Error = error{
46 /// The compositor lacks dma-buf feedback or explicit synchronization.
47 Unsupported,
48 /// Every buffer or timeline slot is in use.
49 CapacityExceeded,
50 /// A plane count, extent or descriptor the request cannot carry.
51 InvalidBuffer,
52 Unexpected,
53 };
54
55 /// One format table entry: a DRM fourcc and a modifier.
56 pub const FormatModifier = extern struct {
57 format: u32,
58 pad: u32 = 0,
59 modifier: u64,
60 };
61
62 /// A preference group of the compositor's feedback, highest preference first.
63 pub const Tranche = struct {
64 /// The device, as a Linux `dev_t`, whose buffers the tranche describes.
65 device: u64,
66 /// `scanout` (1) when the compositor can put buffers of this tranche straight on screen.
67 flags: u32,
68 first_index: u32,
69 index_count: u32,
70 };
71
72 /// The compositor's latest complete dma-buf feedback for the surface.
73 pub const Feedback = struct {
74 main_device: u64 = 0,
75 table: []FormatModifier = &.{},
76 table_len: u32 = 0,
77 indices: []u16 = &.{},
78 index_count: u32 = 0,
79 tranches: [max_tranches]Tranche = undefined,
80 tranche_count: u32 = 0,
81 /// Updates received whole, so a caller can see when the compositor changed its mind.
82 generation: u64 = 0,
83 /// Whether a table entry, index or tranche was dropped because it exceeded the capacity.
84 truncated: bool = false,
85
86 pub fn trancheSlice(self: *const Feedback) []const Tranche {
87 return self.tranches[0..self.tranche_count];
88 }
89
90 pub fn entries(self: *const Feedback, tranche: Tranche) []const u16 {
91 return self.indices[tranche.first_index..][0..tranche.index_count];
92 }
93
94 pub fn entry(self: *const Feedback, index: u16) ?FormatModifier {
95 if (index >= self.table_len) return null;
96 return self.table[index];
97 }
98
99 /// The modifiers of `format` in every tranche for `device`, in preference order and without
100 /// repeats. Returns how many it wrote to `out`.
101 pub fn modifiers(self: *const Feedback, format: u32, device: u64, out: []u64) usize {
102 var count: usize = 0;
103 for (self.trancheSlice()) |tranche| {
104 if (tranche.device != device) continue;
105 for (self.entries(tranche)) |index| {
106 const value = self.entry(index) orelse continue;
107 if (value.format != format) continue;
108 if (std.mem.indexOfScalar(u64, out[0..count], value.modifier) != null) continue;
109 if (count == out.len) return count;
110 out[count] = value.modifier;
111 count += 1;
112 }
113 }
114 return count;
115 }
116 };
117
118 pub const Plane = struct {
119 /// Borrowed. The request carries a duplicate.
120 fd: sys.fd.Descriptor,
121 offset: u32,
122 stride: u32,
123 };
124
125 pub const BufferDesc = struct {
126 width: u32,
127 height: u32,
128 /// DRM fourcc.
129 format: u32,
130 modifier: u64,
131 planes: []const Plane,
132 };
133
134 /// An imported dma-buf, named by its `wl_buffer` object.
135 pub const Buffer = struct { id: u32 };
136
137 /// An imported DRM timeline syncobj.
138 pub const Timeline = struct { id: u32 };
139
140 /// The points one commit names on a timeline.
141 pub const SyncPoints = struct {
142 timeline: Timeline,
143 acquire: u64,
144 release: u64,
145 };
146
147 /// One dma-buf frame: the buffer, its extent, the changed rectangle and its sync points.
148 pub const Frame = struct {
149 buffer: Buffer,
150 width: u32,
151 height: u32,
152 region: windowing.PresentRegion,
153 sync: SyncPoints,
154 };
155
156 pub const PresentError = Error || error{
157 /// The compositor has not asked for a frame since the last one.
158 FrameNotReady,
159 /// The window is hidden, so the compositor would not show the frame.
160 NotVisible,
161 /// The frame's extent is not the window's framebuffer extent.
162 UnsupportedFormat,
163 };
164
165 /// Feedback being received, which replaces `Feedback` when its `done` arrives.
166 const PendingFeedback = struct {
167 open: bool = false,
168 main_device: u64 = 0,
169 table_len: ?u32 = null,
170 index_count: u32 = 0,
171 tranche_count: u32 = 0,
172 tranche: Tranche = .{ .device = 0, .flags = 0, .first_index = 0, .index_count = 0 },
173 };
174
175 pub const Dmabuf = struct {
176 allocator: std.mem.Allocator,
177 client: *runtime.Client,
178 dmabuf_id: u32,
179 manager_id: u32,
180 surface_id: u32,
181 feedback_id: ?u32 = null,
182 /// Made at the first explicit commit. Afterwards every commit of the surface needs sync points.
183 sync_surface_id: ?u32 = null,
184 feedback: Feedback = .{},
185 pending: PendingFeedback = .{},
186 pending_tranches: [max_tranches]Tranche = undefined,
187 pending_table: []FormatModifier = &.{},
188 pending_indices: []u16 = &.{},
189 buffers: [max_buffers]?u32 = @splat(null),
190 timelines: [max_timelines]?u32 = @splat(null),
191
192 /// Asks the compositor for the surface's feedback, which arrives with the next roundtrip.
193 pub fn open(
194 allocator: std.mem.Allocator,
195 client: *runtime.Client,
196 dmabuf_id: u32,
197 manager_id: u32,
198 surface_id: u32,
199 ) (Error || error{OutOfMemory})!Dmabuf {
200 const dmabuf_object = client.object(dmabuf_id) orelse return error.Unsupported;
201 if (dmabuf_object.version < minimum_dmabuf_version) return error.Unsupported;
202 var self = Dmabuf{
203 .allocator = allocator,
204 .client = client,
205 .dmabuf_id = dmabuf_id,
206 .manager_id = manager_id,
207 .surface_id = surface_id,
208 };
209 self.feedback.table = try allocator.alloc(FormatModifier, max_table_entries);
210 errdefer allocator.free(self.feedback.table);
211 self.feedback.indices = try allocator.alloc(u16, max_tranche_indices);
212 errdefer allocator.free(self.feedback.indices);
213 self.pending_table = try allocator.alloc(FormatModifier, max_table_entries);
214 errdefer allocator.free(self.pending_table);
215 self.pending_indices = try allocator.alloc(u16, max_tranche_indices);
216 errdefer allocator.free(self.pending_indices);
217 var created: [1]u32 = undefined;
218 client.request(
219 dmabuf_id,
220 linux_dmabuf.requests.get_surface_feedback.opcode,
221 &.{ .{ .new_id = .fixed }, .{ .object = surface_id } },
222 &created,
223 ) catch return error.Unexpected;
224 self.feedback_id = created[0];
225 return self;
226 }
227
228 pub fn deinit(self: *Dmabuf) void {
229 for (&self.buffers) |*slot| if (slot.*) |id| {
230 self.destroyObject(id, core.wl_buffer.requests.destroy.opcode);
231 slot.* = null;
232 };
233 for (&self.timelines) |*slot| if (slot.*) |id| {
234 self.destroyObject(id, syncobj_timeline.requests.destroy.opcode);
235 slot.* = null;
236 };
237 if (self.sync_surface_id) |id| self.destroyObject(id, syncobj_surface.requests.destroy.opcode);
238 if (self.feedback_id) |id| self.destroyObject(id, feedback_protocol.requests.destroy.opcode);
239 self.allocator.free(self.feedback.table);
240 self.allocator.free(self.feedback.indices);
241 self.allocator.free(self.pending_table);
242 self.allocator.free(self.pending_indices);
243 self.* = undefined;
244 }
245
246 /// The `zwp_linux_dmabuf_v1` version bound, 4 or later.
247 pub fn version(self: *const Dmabuf) u32 {
248 const object = self.client.object(self.dmabuf_id) orelse return 0;
249 return object.version;
250 }
251
252 /// The latest complete feedback, or null before the first one arrives.
253 pub fn currentFeedback(self: *const Dmabuf) ?*const Feedback {
254 if (self.feedback.generation == 0) return null;
255 return &self.feedback;
256 }
257
258 /// Imports a dma-buf as a `wl_buffer`. The compositor refuses a buffer it cannot import with a
259 /// protocol error, so a caller picks the format and modifier from the feedback.
260 pub fn importBuffer(self: *Dmabuf, desc: BufferDesc) Error!Buffer {
261 if (desc.planes.len == 0 or desc.planes.len > max_planes) return error.InvalidBuffer;
262 if (desc.width == 0 or desc.height == 0) return error.InvalidBuffer;
263 if (desc.width > std.math.maxInt(i32) or desc.height > std.math.maxInt(i32)) return error.InvalidBuffer;
264 const slot = freeSlot(&self.buffers) orelse return error.CapacityExceeded;
265 var params: [1]u32 = undefined;
266 self.client.request(self.dmabuf_id, linux_dmabuf.requests.create_params.opcode, &.{.{ .new_id = .fixed }}, ¶ms) catch
267 return error.Unexpected;
268 defer self.destroyObject(params[0], buffer_params.requests.destroy.opcode);
269 const modifier_hi: u32 = @truncate(desc.modifier >> 32);
270 const modifier_lo: u32 = @truncate(desc.modifier);
271 for (desc.planes, 0..) |plane, index| {
272 const duplicate = sys.fd.duplicate(plane.fd) catch return error.InvalidBuffer;
273 self.client.request(params[0], buffer_params.requests.add.opcode, &.{
274 .{ .descriptor_owned = duplicate },
275 .{ .uint = @intCast(index) },
276 .{ .uint = plane.offset },
277 .{ .uint = plane.stride },
278 .{ .uint = modifier_hi },
279 .{ .uint = modifier_lo },
280 }, &.{}) catch {
281 sys.fd.close(duplicate);
282 return error.Unexpected;
283 };
284 }
285 var created: [1]u32 = undefined;
286 self.client.request(params[0], buffer_params.requests.create_immed.opcode, &.{
287 .{ .new_id = .fixed },
288 .{ .int = @intCast(desc.width) },
289 .{ .int = @intCast(desc.height) },
290 .{ .uint = desc.format },
291 .{ .uint = 0 },
292 }, &created) catch return error.Unexpected;
293 slot.* = created[0];
294 return .{ .id = created[0] };
295 }
296
297 pub fn releaseBuffer(self: *Dmabuf, buffer: Buffer) void {
298 const slot = findSlot(&self.buffers, buffer.id) orelse return;
299 self.destroyObject(buffer.id, core.wl_buffer.requests.destroy.opcode);
300 slot.* = null;
301 }
302
303 /// Imports a DRM timeline syncobj from its descriptor.
304 pub fn importTimeline(self: *Dmabuf, fd: sys.fd.Descriptor) Error!Timeline {
305 const slot = freeSlot(&self.timelines) orelse return error.CapacityExceeded;
306 const duplicate = sys.fd.duplicate(fd) catch return error.InvalidBuffer;
307 var created: [1]u32 = undefined;
308 self.client.request(self.manager_id, syncobj_manager.requests.import_timeline.opcode, &.{
309 .{ .new_id = .fixed },
310 .{ .descriptor_owned = duplicate },
311 }, &created) catch {
312 sys.fd.close(duplicate);
313 return error.Unexpected;
314 };
315 slot.* = created[0];
316 return .{ .id = created[0] };
317 }
318
319 pub fn destroyTimeline(self: *Dmabuf, timeline: Timeline) void {
320 const slot = findSlot(&self.timelines, timeline.id) orelse return;
321 self.destroyObject(timeline.id, syncobj_timeline.requests.destroy.opcode);
322 slot.* = null;
323 }
324
325 pub fn ownsBuffer(self: *const Dmabuf, buffer: Buffer) bool {
326 return std.mem.indexOfScalar(?u32, &self.buffers, buffer.id) != null;
327 }
328
329 pub fn ownsTimeline(self: *const Dmabuf, timeline: Timeline) bool {
330 return std.mem.indexOfScalar(?u32, &self.timelines, timeline.id) != null;
331 }
332
333 /// Sends the sync points of the next commit, making the surface's syncobj object first.
334 pub fn setSyncPoints(self: *Dmabuf, sync: SyncPoints) Error!void {
335 if (sync.acquire >= sync.release) return error.InvalidBuffer;
336 if (!self.ownsTimeline(sync.timeline)) return error.InvalidBuffer;
337 const surface = self.sync_surface_id orelse blk: {
338 var created: [1]u32 = undefined;
339 self.client.request(self.manager_id, syncobj_manager.requests.get_surface.opcode, &.{
340 .{ .new_id = .fixed },
341 .{ .object = self.surface_id },
342 }, &created) catch return error.Unexpected;
343 self.sync_surface_id = created[0];
344 break :blk created[0];
345 };
346 inline for (.{
347 .{ syncobj_surface.requests.set_acquire_point.opcode, sync.acquire },
348 .{ syncobj_surface.requests.set_release_point.opcode, sync.release },
349 }) |point| {
350 self.client.request(surface, point[0], &.{
351 .{ .object = sync.timeline.id },
352 .{ .uint = @truncate(point[1] >> 32) },
353 .{ .uint = @truncate(point[1]) },
354 }, &.{}) catch return error.Unexpected;
355 }
356 }
357
358 /// Handles feedback events and the events of imported buffers. The compositor's
359 /// `wl_buffer.release` means nothing under explicit synchronization and is dropped.
360 pub fn dispatch(self: *Dmabuf, event: *runtime.RoutedView) !bool {
361 if (self.feedback_id == event.object_id) {
362 try self.dispatchFeedback(event);
363 return true;
364 }
365 return std.mem.indexOfScalar(?u32, &self.buffers, event.object_id) != null;
366 }
367
368 fn dispatchFeedback(self: *Dmabuf, event: *runtime.RoutedView) !void {
369 const events = feedback_protocol.events;
370 const pending = &self.pending;
371 if (!pending.open) {
372 pending.* = .{ .open = true, .main_device = self.feedback.main_device };
373 }
374 var decoder = try event.borrowedDecoder();
375 switch (event.metadata.opcode) {
376 events.format_table.opcode => {
377 _ = try decoder.descriptor();
378 const size = try decoder.unsigned();
379 try decoder.finish();
380 const fd = try event.takeDescriptor(0);
381 defer sys.fd.close(fd);
382 pending.table_len = try self.readTable(fd, size);
383 },
384 events.main_device.opcode => {
385 pending.main_device = try deviceNumber(try decoder.array());
386 try decoder.finish();
387 },
388 events.tranche_target_device.opcode => {
389 pending.tranche.device = try deviceNumber(try decoder.array());
390 try decoder.finish();
391 },
392 events.tranche_flags.opcode => {
393 pending.tranche.flags = try decoder.unsigned();
394 try decoder.finish();
395 },
396 events.tranche_formats.opcode => {
397 const bytes = try decoder.array();
398 try decoder.finish();
399 if (bytes.len % 2 != 0) return error.InvalidEvent;
400 var offset: usize = 0;
401 while (offset < bytes.len) : (offset += 2) {
402 if (pending.index_count == max_tranche_indices) {
403 self.feedback.truncated = true;
404 break;
405 }
406 self.pending_indices[pending.index_count] = std.mem.readInt(u16, bytes[offset..][0..2], native_endian);
407 pending.index_count += 1;
408 pending.tranche.index_count += 1;
409 }
410 },
411 events.tranche_done.opcode => {
412 try decoder.finish();
413 if (pending.tranche_count == max_tranches) {
414 self.feedback.truncated = true;
415 } else {
416 self.pending_tranches[pending.tranche_count] = pending.tranche;
417 pending.tranche_count += 1;
418 }
419 pending.tranche = .{ .device = 0, .flags = 0, .first_index = pending.index_count, .index_count = 0 };
420 },
421 events.done.opcode => {
422 try decoder.finish();
423 self.commitFeedback();
424 },
425 else => return error.InvalidEvent,
426 }
427 }
428
429 fn commitFeedback(self: *Dmabuf) void {
430 const pending = &self.pending;
431 const feedback = &self.feedback;
432 if (pending.table_len) |len| {
433 @memcpy(feedback.table[0..len], self.pending_table[0..len]);
434 feedback.table_len = len;
435 }
436 feedback.main_device = pending.main_device;
437 @memcpy(feedback.indices[0..pending.index_count], self.pending_indices[0..pending.index_count]);
438 feedback.index_count = pending.index_count;
439 @memcpy(feedback.tranches[0..pending.tranche_count], self.pending_tranches[0..pending.tranche_count]);
440 feedback.tranche_count = pending.tranche_count;
441 feedback.generation += 1;
442 pending.* = .{};
443 }
444
445 /// Copies the format table the compositor shares through `fd` into pending storage.
446 fn readTable(self: *Dmabuf, fd: sys.fd.Descriptor, size: u32) !u32 {
447 if (size == 0 or size % @sizeOf(FormatModifier) != 0) return error.InvalidEvent;
448 const mapping = sys.memory.mapPrivateFile(fd, size, .{ .read = true }, 0) catch return error.InvalidEvent;
449 defer sys.memory.unmap(mapping);
450 const available: u32 = size / @sizeOf(FormatModifier);
451 const kept = @min(available, max_table_entries);
452 if (kept < available) self.feedback.truncated = true;
453 @memcpy(std.mem.sliceAsBytes(self.pending_table[0..kept]), mapping[0 .. kept * @sizeOf(FormatModifier)]);
454 return kept;
455 }
456
457 fn destroyObject(self: *Dmabuf, id: u32, opcode: u16) void {
458 self.client.request(id, opcode, &.{}, &.{}) catch {};
459 }
460 };
461
462 const native_endian = builtin.cpu.arch.endian();
463
464 /// A `dev_t` the compositor sends as an array in native byte order.
465 fn deviceNumber(bytes: []const u8) !u64 {
466 if (bytes.len != @sizeOf(u64)) return error.InvalidEvent;
467 return std.mem.readInt(u64, bytes[0..8], native_endian);
468 }
469
470 fn freeSlot(slots: []?u32) ?*?u32 {
471 for (slots) |*slot| if (slot.* == null) return slot;
472 return null;
473 }
474
475 fn findSlot(slots: []?u32, id: u32) ?*?u32 {
476 for (slots) |*slot| if (slot.* == id) return slot;
477 return null;
478 }
479
480 test "feedback lists a format's modifiers for a device in tranche order without repeats" {
481 var table = [_]FormatModifier{
482 .{ .format = 0x34324258, .modifier = 0 },
483 .{ .format = 0x34324258, .modifier = 0x0200000000401b03 },
484 .{ .format = 0x34325258, .modifier = 0 },
485 };
486 var indices = [_]u16{ 1, 0, 2, 0, 9 };
487 var feedback = Feedback{ .table = &table, .table_len = table.len, .indices = &indices, .index_count = indices.len, .generation = 1 };
488 feedback.tranches[0] = .{ .device = 0xE280, .flags = 1, .first_index = 0, .index_count = 3 };
489 feedback.tranches[1] = .{ .device = 0xE280, .flags = 0, .first_index = 3, .index_count = 2 };
490 feedback.tranche_count = 2;
491 var out: [4]u64 = undefined;
492 const count = feedback.modifiers(0x34324258, 0xE280, &out);
493 try std.testing.expectEqualSlices(u64, &.{ 0x0200000000401b03, 0 }, out[0..count]);
494 try std.testing.expectEqual(@as(usize, 0), feedback.modifiers(0x34324258, 0xE281, &out));
495 try std.testing.expectEqual(@as(?FormatModifier, null), feedback.entry(9));
496 }
497
498 test "a device number decodes from the native-order array" {
499 var encoded: [8]u8 = undefined;
500 std.mem.writeInt(u64, &encoded, 0xE280, native_endian);
501 try std.testing.expectEqual(@as(u64, 0xE280), try deviceNumber(&encoded));
502 try std.testing.expectError(error.InvalidEvent, deviceNumber(&.{ 0x80, 0xE2 }));
503 }