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 }