lib/windowing/src/wayland/presentation.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const builtin = @import("builtin");
  3 const sys = @import("sys");
  4 const native = @import("wayland");
  5 const windowing = @import("../root.zig");
  6 const present = @import("present/root.zig");
  7 
  8 const buffer = present.buffer;
  9 const desktop = native.protocol.desktop;
 10 const runtime = native.runtime;
 11 
 12 const Timestamp = windowing.presentation.Timestamp;
 13 const Submission = windowing.presentation.Submission;
 14 const Outcome = windowing.presentation.Outcome;
 15 
 16 pub const CapacityError = error{
 17     PendingFeedbackEmpty,
 18     RetainedOutcomesEmpty,
 19     ActiveBuffersEmpty,
 20     RetiredBuffersEmpty,
 21     BufferSlotsTooMany,
 22     FrameStorageEmpty,
 23     CapacityOverflow,
 24 };
 25 
 26 pub const Capacity = struct {
 27     frames: windowing.presentation.Capacity,
 28     pending_feedback_count: usize,
 29     retained_outcome_count: usize,
 30     state_bytes: usize,
 31     pending_feedback_bytes: usize,
 32     retained_outcome_bytes: usize,
 33     swapchain: buffer.Capacity,
 34     swapchain_metadata_bytes: usize,
 35     pending_frame_bytes: usize,
 36     maximum_foreign_mapped_frame_bytes: usize,
 37     total_heap_bytes: usize,
 38 
 39     pub fn derive(limits: windowing.presentation.Limits) CapacityError!Capacity {
 40         if (limits.pending_feedback_count == 0) return error.PendingFeedbackEmpty;
 41         if (limits.retained_outcome_count == 0) return error.RetainedOutcomesEmpty;
 42         const frames = try windowing.presentation.Capacity.derive(limits);
 43         const swapchain = try buffer.Capacity.derive(
 44             limits.active_buffer_count,
 45             limits.retired_buffer_count,
 46         );
 47 
 48         const pending_feedback_bytes = std.math.mul(
 49             usize,
 50             limits.pending_feedback_count,
 51             @sizeOf(PendingFeedback),
 52         ) catch return error.CapacityOverflow;
 53         const retained_outcome_bytes = std.math.mul(
 54             usize,
 55             limits.retained_outcome_count,
 56             @sizeOf(Outcome),
 57         ) catch return error.CapacityOverflow;
 58         const storage_bytes = std.math.add(
 59             usize,
 60             pending_feedback_bytes,
 61             retained_outcome_bytes,
 62         ) catch return error.CapacityOverflow;
 63         const metadata_and_frame_bytes = std.math.add(
 64             usize,
 65             swapchain.metadata_storage_bytes,
 66             frames.wayland_pending_frame_bytes,
 67         ) catch return error.CapacityOverflow;
 68         const owner_storage_bytes = std.math.add(
 69             usize,
 70             storage_bytes,
 71             metadata_and_frame_bytes,
 72         ) catch return error.CapacityOverflow;
 73         const total_heap_bytes = std.math.add(
 74             usize,
 75             @sizeOf(State),
 76             owner_storage_bytes,
 77         ) catch return error.CapacityOverflow;
 78         const maximum_foreign_mapped_frame_bytes = std.math.mul(
 79             usize,
 80             swapchain.buffer_slot_count,
 81             frames.retained_frame_byte_count,
 82         ) catch return error.CapacityOverflow;
 83         return .{
 84             .frames = frames,
 85             .pending_feedback_count = limits.pending_feedback_count,
 86             .retained_outcome_count = limits.retained_outcome_count,
 87             .state_bytes = @sizeOf(State),
 88             .pending_feedback_bytes = pending_feedback_bytes,
 89             .retained_outcome_bytes = retained_outcome_bytes,
 90             .swapchain = swapchain,
 91             .swapchain_metadata_bytes = swapchain.metadata_storage_bytes,
 92             .pending_frame_bytes = frames.wayland_pending_frame_bytes,
 93             .maximum_foreign_mapped_frame_bytes = maximum_foreign_mapped_frame_bytes,
 94             .total_heap_bytes = total_heap_bytes,
 95         };
 96     }
 97 };
 98 
 99 pub const CallbackError = error{
100     DuplicateClockId,
101     InvalidEvent,
102     InvalidTimestamp,
103 };
104 
105 pub const InitError = error{OutOfMemory};
106 
107 pub const SubmitError = error{
108     CreateFailed,
109     TooManyPending,
110     SubmissionIdsExhausted,
111     ClockUnavailable,
112 };
113 
114 pub const Presentation = struct {
115     state: *State,
116 
117     pub fn init(
118         allocator: std.mem.Allocator,
119         client: *runtime.Client,
120         object_id: u32,
121         capacity: Capacity,
122     ) InitError!Presentation {
123         const state = allocator.create(State) catch return error.OutOfMemory;
124         errdefer allocator.destroy(state);
125         state.* = .{
126             .allocator = allocator,
127             .client = client,
128             .object_id = object_id,
129             .storage = Storage.init(allocator, capacity) catch return error.OutOfMemory,
130         };
131         return .{ .state = state };
132     }
133 
134     pub fn deinit(self: *Presentation) void {
135         const state = self.state;
136         state.client.request(
137             state.object_id,
138             desktop.wp_presentation.requests.destroy.opcode,
139             &.{},
140             &.{},
141         ) catch {};
142         state.storage.deinit(state.allocator);
143         state.allocator.destroy(state);
144         self.* = undefined;
145     }
146 
147     pub fn dispatch(self: *Presentation, event: *runtime.RoutedView) !bool {
148         if (event.object_id == self.state.object_id) {
149             try self.dispatchPresentation(event);
150             return true;
151         }
152         const feedback_index = self.state.findFeedback(event.object_id) orelse return false;
153         try self.dispatchFeedback(feedback_index, event);
154         return true;
155     }
156 
157     pub fn submit(self: *Presentation, surface: u32) SubmitError!u64 {
158         const clock_id = self.state.clock_id orelse return error.ClockUnavailable;
159         const submitted_at = try sampleClock(clock_id);
160         return self.submitAt(surface, clock_id, submitted_at);
161     }
162 
163     fn submitAt(
164         self: *Presentation,
165         surface: u32,
166         clock_id: sys.time.ExternalClockId,
167         submitted_at: Timestamp,
168     ) SubmitError!u64 {
169         const state = self.state;
170         const feedback_index = try state.storage.reserveFeedback();
171         errdefer state.storage.cancelFeedbackReservation(feedback_index);
172         if (state.next_submission_id == std.math.maxInt(u64)) return error.SubmissionIdsExhausted;
173 
174         var created: [1]u32 = undefined;
175         state.client.request(
176             state.object_id,
177             desktop.wp_presentation.requests.feedback.opcode,
178             &.{ .{ .object = surface }, .{ .new_id = .fixed } },
179             &created,
180         ) catch return error.CreateFailed;
181 
182         const id = state.next_submission_id;
183         state.storage.commitFeedback(feedback_index, .{
184             .object_id = created[0],
185             .submission = .{
186                 .id = id,
187                 .clock_id = clock_id.toNative(),
188                 .timestamp = submitted_at,
189             },
190         });
191         state.next_submission_id += 1;
192         return id;
193     }
194 
195     pub fn clockId(self: *const Presentation) ?sys.time.ExternalClockId {
196         return self.state.clock_id;
197     }
198 
199     pub fn pendingCount(self: *const Presentation) usize {
200         return self.state.storage.pending_count;
201     }
202 
203     pub fn outcomeCount(self: *const Presentation) usize {
204         return self.state.storage.outcomes.len;
205     }
206 
207     pub fn nextOutcome(self: *Presentation) ?Outcome {
208         return self.state.storage.outcomes.pop();
209     }
210 
211     pub fn droppedOutcomeCount(self: *const Presentation) u64 {
212         return self.state.storage.outcomes.dropped;
213     }
214 
215     pub fn takeCallbackError(self: *Presentation) ?CallbackError {
216         const result = self.state.callback_error;
217         self.state.callback_error = null;
218         return result;
219     }
220 
221     fn dispatchPresentation(self: *Presentation, event: *runtime.RoutedView) !void {
222         if (event.metadata.opcode != desktop.wp_presentation.events.clock_id.opcode) {
223             self.state.recordError(error.InvalidEvent);
224             return;
225         }
226         var decoder = try event.borrowedDecoder();
227         const clock_id = sys.time.ExternalClockId.fromNative(try decoder.unsigned());
228         try decoder.finish();
229         self.state.setClockId(clock_id);
230     }
231 
232     fn dispatchFeedback(self: *Presentation, feedback_index: usize, event: *runtime.RoutedView) !void {
233         const state = self.state;
234         var decoder = try event.borrowedDecoder();
235         switch (event.metadata.opcode) {
236             desktop.wp_presentation_feedback.events.sync_output.opcode => {
237                 _ = try decoder.object();
238                 try decoder.finish();
239                 state.storage.pending[feedback_index].synchronized_outputs +|= 1;
240             },
241             desktop.wp_presentation_feedback.events.presented.opcode => {
242                 const seconds_high = try decoder.unsigned();
243                 const seconds_low = try decoder.unsigned();
244                 const nanoseconds = try decoder.unsigned();
245                 const refresh = try decoder.unsigned();
246                 const sequence_high = try decoder.unsigned();
247                 const sequence_low = try decoder.unsigned();
248                 const flags = try decoder.unsigned();
249                 try decoder.finish();
250                 const timestamp = timestampFromWire(seconds_high, seconds_low, nanoseconds) catch {
251                     state.reject(feedback_index, error.InvalidTimestamp);
252                     return;
253                 };
254                 const feedback = state.storage.pending[feedback_index];
255                 state.resolve(feedback_index, .{ .presented = .{
256                     .submission = feedback.submission,
257                     .timestamp = timestamp,
258                     .refresh_nanoseconds = refresh,
259                     .sequence = (@as(u64, sequence_high) << 32) | sequence_low,
260                     .kind = @bitCast(flags),
261                     .synchronized_outputs = feedback.synchronized_outputs,
262                 } });
263             },
264             desktop.wp_presentation_feedback.events.discarded.opcode => {
265                 try decoder.finish();
266                 const submission = state.storage.pending[feedback_index].submission;
267                 state.resolve(feedback_index, .{ .discarded = .{ .submission = submission } });
268             },
269             else => {
270                 self.state.recordError(error.InvalidEvent);
271                 return error.InvalidEvent;
272             },
273         }
274     }
275 };
276 
277 fn sampleClock(clock_id: sys.time.ExternalClockId) error{ClockUnavailable}!Timestamp {
278     const nanoseconds = sys.time.externalClockNanoseconds(clock_id) catch
279         return error.ClockUnavailable;
280     const value = std.math.cast(u64, nanoseconds) orelse
281         return error.ClockUnavailable;
282     return Timestamp.fromNanoseconds(value);
283 }
284 
285 const State = struct {
286     allocator: std.mem.Allocator,
287     client: *runtime.Client,
288     object_id: u32,
289     storage: Storage,
290     clock_id: ?sys.time.ExternalClockId = null,
291     callback_error: ?CallbackError = null,
292     next_submission_id: u64 = 1,
293 
294     fn setClockId(self: *State, clock_id: sys.time.ExternalClockId) void {
295         if (self.clock_id != null) {
296             self.recordError(error.DuplicateClockId);
297             return;
298         }
299         self.clock_id = clock_id;
300     }
301 
302     fn recordError(self: *State, failure: CallbackError) void {
303         if (self.callback_error == null) self.callback_error = failure;
304     }
305 
306     fn findFeedback(self: *const State, object_id: u32) ?usize {
307         return self.storage.findFeedback(object_id);
308     }
309 
310     fn reject(self: *State, feedback_index: usize, failure: CallbackError) void {
311         self.recordError(failure);
312         _ = self.storage.removeFeedback(feedback_index);
313     }
314 
315     fn resolve(self: *State, feedback_index: usize, outcome: Outcome) void {
316         self.storage.resolve(feedback_index, outcome);
317     }
318 };
319 
320 const PendingFeedback = struct {
321     object_id: u32,
322     submission: Submission,
323     synchronized_outputs: u32 = 0,
324 };
325 
326 const Storage = struct {
327     pending: []PendingFeedback,
328     pending_count: usize = 0,
329     feedback_reserved: bool = false,
330     outcomes: OutcomeQueue,
331 
332     fn init(allocator: std.mem.Allocator, capacity: Capacity) std.mem.Allocator.Error!Storage {
333         const pending = try allocator.alloc(PendingFeedback, capacity.pending_feedback_count);
334         errdefer allocator.free(pending);
335         const outcomes = try allocator.alloc(Outcome, capacity.retained_outcome_count);
336         return .{
337             .pending = pending,
338             .outcomes = OutcomeQueue.init(outcomes),
339         };
340     }
341 
342     fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
343         allocator.free(self.outcomes.items);
344         allocator.free(self.pending);
345         self.* = undefined;
346     }
347 
348     fn reserveFeedback(self: *Storage) error{TooManyPending}!usize {
349         if (self.feedback_reserved or self.pending_count == self.pending.len) return error.TooManyPending;
350         self.feedback_reserved = true;
351         return self.pending_count;
352     }
353 
354     fn cancelFeedbackReservation(self: *Storage, index: usize) void {
355         std.debug.assert(self.feedback_reserved);
356         std.debug.assert(index == self.pending_count);
357         self.feedback_reserved = false;
358     }
359 
360     fn commitFeedback(self: *Storage, index: usize, feedback: PendingFeedback) void {
361         std.debug.assert(self.feedback_reserved);
362         std.debug.assert(index == self.pending_count);
363         self.pending[index] = feedback;
364         self.pending_count += 1;
365         self.feedback_reserved = false;
366     }
367 
368     fn findFeedback(self: *const Storage, object_id: u32) ?usize {
369         for (self.pending[0..self.pending_count], 0..) |feedback, index| {
370             if (feedback.object_id == object_id) return index;
371         }
372         return null;
373     }
374 
375     fn removeFeedback(self: *Storage, index: usize) PendingFeedback {
376         std.debug.assert(!self.feedback_reserved);
377         std.debug.assert(index < self.pending_count);
378         const removed = self.pending[index];
379         self.pending_count -= 1;
380         if (index != self.pending_count) self.pending[index] = self.pending[self.pending_count];
381         return removed;
382     }
383 
384     fn resolve(self: *Storage, index: usize, outcome: Outcome) void {
385         _ = self.removeFeedback(index);
386         self.outcomes.push(outcome);
387     }
388 };
389 
390 const OutcomeQueue = struct {
391     items: []Outcome,
392     head: usize = 0,
393     len: usize = 0,
394     dropped: u64 = 0,
395 
396     fn init(items: []Outcome) OutcomeQueue {
397         std.debug.assert(items.len > 0);
398         return .{ .items = items };
399     }
400 
401     fn push(self: *OutcomeQueue, outcome: Outcome) void {
402         if (self.len == self.items.len) {
403             self.items[self.head] = outcome;
404             self.head = (self.head + 1) % self.items.len;
405             self.dropped = self.dropped +| 1;
406             return;
407         }
408         const tail = (self.head + self.len) % self.items.len;
409         self.items[tail] = outcome;
410         self.len += 1;
411     }
412 
413     fn pop(self: *OutcomeQueue) ?Outcome {
414         if (self.len == 0) return null;
415         const outcome = self.items[self.head];
416         self.head = (self.head + 1) % self.items.len;
417         self.len -= 1;
418         return outcome;
419     }
420 };
421 
422 fn timestampFromWire(seconds_high: u32, seconds_low: u32, nanoseconds: u32) windowing.presentation.TimestampError!Timestamp {
423     const seconds = (@as(u64, seconds_high) << 32) | seconds_low;
424     return Timestamp.init(seconds, nanoseconds);
425 }
426 
427 test "presentation timestamps preserve wire precision" {
428     const timestamp = try timestampFromWire(0x0123_4567, 0x89ab_cdef, 987_654_321);
429     try std.testing.expectEqual(@as(u64, 0x0123_4567_89ab_cdef), timestamp.seconds);
430     try std.testing.expectEqual(@as(u32, 987_654_321), timestamp.nanoseconds);
431     try std.testing.expectError(error.InvalidTimestamp, timestampFromWire(0, 0, std.time.ns_per_s));
432 }
433 
434 test "presentation capacity derives exact default storage and rejects invalid limits" {
435     const capacity = try Capacity.derive(.{});
436     try std.testing.expectEqual(@as(usize, 256), capacity.pending_feedback_count);
437     try std.testing.expectEqual(@as(usize, 256), capacity.retained_outcome_count);
438     try std.testing.expectEqual(@sizeOf(State), capacity.state_bytes);
439     try std.testing.expectEqual(256 * @sizeOf(PendingFeedback), capacity.pending_feedback_bytes);
440     try std.testing.expectEqual(256 * @sizeOf(Outcome), capacity.retained_outcome_bytes);
441     try std.testing.expectEqual(@as(usize, 3), capacity.swapchain.active_buffer_count);
442     try std.testing.expectEqual(@as(usize, 3), capacity.swapchain.retired_buffer_count);
443     try std.testing.expectEqual(
444         capacity.swapchain.metadata_storage_bytes,
445         capacity.swapchain_metadata_bytes,
446     );
447     try std.testing.expectEqual(
448         capacity.state_bytes +
449             capacity.pending_feedback_bytes +
450             capacity.retained_outcome_bytes +
451             capacity.swapchain_metadata_bytes +
452             capacity.pending_frame_bytes,
453         capacity.total_heap_bytes,
454     );
455     try std.testing.expectEqual(
456         capacity.swapchain.buffer_slot_count * capacity.frames.retained_frame_byte_count,
457         capacity.maximum_foreign_mapped_frame_bytes,
458     );
459     try std.testing.expectError(error.PendingFeedbackEmpty, Capacity.derive(.{
460         .pending_feedback_count = 0,
461     }));
462     try std.testing.expectError(error.RetainedOutcomesEmpty, Capacity.derive(.{
463         .retained_outcome_count = 0,
464     }));
465     try std.testing.expectError(error.ActiveBuffersEmpty, Capacity.derive(.{
466         .active_buffer_count = 0,
467     }));
468     try std.testing.expectError(error.RetiredBuffersEmpty, Capacity.derive(.{
469         .retired_buffer_count = 0,
470     }));
471     try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
472         .pending_feedback_count = std.math.maxInt(usize) / @sizeOf(PendingFeedback) + 1,
473     }));
474     try std.testing.expectError(error.FrameStorageEmpty, Capacity.derive(.{
475         .retained_frame_byte_count = 0,
476     }));
477     try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
478         .retained_frame_byte_count = std.math.maxInt(usize) / 9 + 1,
479     }));
480 }
481 
482 test "presentation outcome retention stays bounded and keeps newest evidence" {
483     var items: [3]Outcome = undefined;
484     var queue = OutcomeQueue.init(&items);
485     for (0..items.len + 3) |index| {
486         queue.push(.{ .discarded = .{ .submission = .{
487             .id = @intCast(index),
488             .clock_id = 1,
489             .timestamp = Timestamp.fromNanoseconds(@intCast(index)),
490         } } });
491     }
492     try std.testing.expectEqual(items.len, queue.len);
493     try std.testing.expectEqual(@as(u64, 3), queue.dropped);
494     try std.testing.expectEqual(@as(u64, 3), queue.pop().?.submission().id);
495 }
496 
497 test "presentation storage admits to its limit and allocates only during initialization" {
498     const capacity = try Capacity.derive(.{
499         .pending_feedback_count = 2,
500         .retained_outcome_count = 2,
501     });
502     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 2 });
503     const allocator = failing.allocator();
504     var storage = try Storage.init(allocator, capacity);
505     defer storage.deinit(allocator);
506     try std.testing.expectEqual(@as(usize, 2), failing.allocations);
507 
508     const first = try storage.reserveFeedback();
509     storage.commitFeedback(first, .{
510         .object_id = 11,
511         .submission = .{ .id = 1, .clock_id = 7, .timestamp = Timestamp.fromNanoseconds(10) },
512     });
513     const second = try storage.reserveFeedback();
514     storage.commitFeedback(second, .{
515         .object_id = 12,
516         .submission = .{ .id = 2, .clock_id = 7, .timestamp = Timestamp.fromNanoseconds(20) },
517     });
518     try std.testing.expectError(error.TooManyPending, storage.reserveFeedback());
519     try std.testing.expectEqual(@as(?usize, 0), storage.findFeedback(11));
520     try std.testing.expectEqual(@as(?usize, 1), storage.findFeedback(12));
521 
522     storage.resolve(0, .{ .discarded = .{ .submission = .{
523         .id = 1,
524         .clock_id = 7,
525         .timestamp = Timestamp.fromNanoseconds(10),
526     } } });
527     try std.testing.expectEqual(@as(usize, 1), storage.pending_count);
528     try std.testing.expectEqual(@as(?usize, 0), storage.findFeedback(12));
529     const cancelled = try storage.reserveFeedback();
530     storage.cancelFeedbackReservation(cancelled);
531     try std.testing.expectEqual(@as(usize, 1), storage.pending_count);
532     const reused = try storage.reserveFeedback();
533     storage.commitFeedback(reused, .{
534         .object_id = 13,
535         .submission = .{ .id = 3, .clock_id = 7, .timestamp = Timestamp.fromNanoseconds(30) },
536     });
537 
538     storage.outcomes.push(.{ .discarded = .{ .submission = .{
539         .id = 4,
540         .clock_id = 7,
541         .timestamp = Timestamp.fromNanoseconds(40),
542     } } });
543     storage.outcomes.push(.{ .discarded = .{ .submission = .{
544         .id = 5,
545         .clock_id = 7,
546         .timestamp = Timestamp.fromNanoseconds(50),
547     } } });
548     try std.testing.expectEqual(@as(u64, 1), storage.outcomes.dropped);
549     try std.testing.expectEqual(@as(u64, 4), storage.outcomes.pop().?.submission().id);
550     try std.testing.expectEqual(@as(usize, 2), failing.allocations);
551 }
552 
553 test "presentation samples the compositor-selected POSIX clock" {
554     if (builtin.os.tag != .linux) return error.SkipZigTest;
555     const realtime = try sampleClock(sys.time.real_clock_id);
556     const monotonic = try sampleClock(sys.time.awake_clock_id);
557     try std.testing.expect(realtime.seconds > monotonic.seconds);
558     try std.testing.expectError(
559         error.ClockUnavailable,
560         sampleClock(.fromNative(std.math.maxInt(u32))),
561     );
562 }