lib/windowing/src/wayland/present/buffer.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const native = @import("wayland");
3 const windowing = @import("../../root.zig");
4
5 pub const damage = @import("damage.zig");
6
7 const runtime = native.runtime;
8
9 const Buffer = native.shm.Buffer;
10 pub const Format = native.shm.Format;
11 pub const SwapchainError = native.shm.Error ||
12 error{RetiredCapacityExceeded};
13
14 const BufferIndex = u32;
15 const no_buffer = std.math.maxInt(BufferIndex);
16
17 pub const CapacityError = error{
18 ActiveBuffersEmpty,
19 RetiredBuffersEmpty,
20 BufferSlotsTooMany,
21 CapacityOverflow,
22 };
23
24 pub const Capacity = struct {
25 active_buffer_count: usize,
26 retired_buffer_count: usize,
27 buffer_slot_count: usize,
28 buffer_slot_bytes: usize,
29 active_index_bytes: usize,
30 replacement_index_bytes: usize,
31 retired_index_bytes: usize,
32 dirty_region_bytes: usize,
33 metadata_storage_bytes: usize,
34
35 pub fn derive(
36 active_buffer_count: usize,
37 retired_buffer_count: usize,
38 ) CapacityError!Capacity {
39 if (active_buffer_count == 0) return error.ActiveBuffersEmpty;
40 if (retired_buffer_count == 0) return error.RetiredBuffersEmpty;
41 const working_buffer_count = std.math.mul(
42 usize,
43 active_buffer_count,
44 2,
45 ) catch return error.CapacityOverflow;
46 const buffer_slot_count = std.math.add(
47 usize,
48 working_buffer_count,
49 retired_buffer_count,
50 ) catch return error.CapacityOverflow;
51 if (buffer_slot_count > std.math.maxInt(BufferIndex)) {
52 return error.BufferSlotsTooMany;
53 }
54 const buffer_slot_bytes = std.math.mul(
55 usize,
56 buffer_slot_count,
57 @sizeOf(?Buffer),
58 ) catch return error.CapacityOverflow;
59 const active_index_bytes = try indexBytes(active_buffer_count);
60 const replacement_index_bytes = active_index_bytes;
61 const retired_index_bytes = try indexBytes(retired_buffer_count);
62 const dirty_region_bytes = std.math.mul(
63 usize,
64 buffer_slot_count,
65 @sizeOf(damage.Region),
66 ) catch return error.CapacityOverflow;
67 const metadata_storage_bytes = try sum(&.{
68 buffer_slot_bytes,
69 dirty_region_bytes,
70 active_index_bytes,
71 replacement_index_bytes,
72 retired_index_bytes,
73 });
74 return .{
75 .active_buffer_count = active_buffer_count,
76 .retired_buffer_count = retired_buffer_count,
77 .buffer_slot_count = buffer_slot_count,
78 .buffer_slot_bytes = buffer_slot_bytes,
79 .active_index_bytes = active_index_bytes,
80 .replacement_index_bytes = replacement_index_bytes,
81 .retired_index_bytes = retired_index_bytes,
82 .dirty_region_bytes = dirty_region_bytes,
83 .metadata_storage_bytes = metadata_storage_bytes,
84 };
85 }
86 };
87
88 fn indexBytes(count: usize) CapacityError!usize {
89 return std.math.mul(usize, count, @sizeOf(BufferIndex)) catch
90 error.CapacityOverflow;
91 }
92
93 fn sum(terms: []const usize) CapacityError!usize {
94 var total: usize = 0;
95 for (terms) |term| {
96 total = std.math.add(usize, total, term) catch return error.CapacityOverflow;
97 }
98 return total;
99 }
100
101 const Storage = struct {
102 buffers: []?Buffer,
103 dirty: []damage.Region,
104 working_indices: []BufferIndex,
105 retired: []BufferIndex,
106 active_buffer_count: usize,
107 retired_count: usize = 0,
108
109 fn init(
110 allocator: std.mem.Allocator,
111 capacity: Capacity,
112 ) std.mem.Allocator.Error!Storage {
113 std.debug.assert(capacity.active_buffer_count > 0);
114 std.debug.assert(capacity.retired_buffer_count > 0);
115 std.debug.assert(capacity.buffer_slot_count <= std.math.maxInt(BufferIndex));
116 std.debug.assert(capacity.active_buffer_count <= std.math.maxInt(usize) / 2);
117 std.debug.assert(
118 capacity.active_buffer_count * 2 <=
119 std.math.maxInt(usize) - capacity.retired_buffer_count,
120 );
121 std.debug.assert(
122 capacity.active_buffer_count * 2 + capacity.retired_buffer_count ==
123 capacity.buffer_slot_count,
124 );
125 std.debug.assert(capacity.buffer_slot_bytes % @sizeOf(?Buffer) == 0);
126 std.debug.assert(
127 capacity.buffer_slot_bytes / @sizeOf(?Buffer) == capacity.buffer_slot_count,
128 );
129 std.debug.assert(capacity.active_index_bytes % @sizeOf(BufferIndex) == 0);
130 std.debug.assert(
131 capacity.active_index_bytes / @sizeOf(BufferIndex) ==
132 capacity.active_buffer_count,
133 );
134 std.debug.assert(capacity.replacement_index_bytes == capacity.active_index_bytes);
135 std.debug.assert(capacity.retired_index_bytes % @sizeOf(BufferIndex) == 0);
136 std.debug.assert(
137 capacity.retired_index_bytes / @sizeOf(BufferIndex) ==
138 capacity.retired_buffer_count,
139 );
140
141 const buffers = try allocator.alloc(?Buffer, capacity.buffer_slot_count);
142 errdefer allocator.free(buffers);
143 @memset(buffers, null);
144 const working_indices = try allocator.alloc(
145 BufferIndex,
146 capacity.active_buffer_count * 2,
147 );
148 errdefer allocator.free(working_indices);
149 @memset(working_indices, no_buffer);
150 const retired = try allocator.alloc(BufferIndex, capacity.retired_buffer_count);
151 errdefer allocator.free(retired);
152 const dirty = try allocator.alloc(damage.Region, capacity.buffer_slot_count);
153 @memset(dirty, .{});
154 return .{
155 .buffers = buffers,
156 .dirty = dirty,
157 .working_indices = working_indices,
158 .retired = retired,
159 .active_buffer_count = capacity.active_buffer_count,
160 };
161 }
162
163 fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
164 std.debug.assert(self.retired_count <= self.retired.len);
165 for (self.buffers) |slot| std.debug.assert(slot == null);
166 allocator.free(self.dirty);
167 allocator.free(self.retired);
168 allocator.free(self.working_indices);
169 allocator.free(self.buffers);
170 self.* = undefined;
171 }
172
173 fn active(self: *Storage) []BufferIndex {
174 return self.working_indices[0..self.active_buffer_count];
175 }
176
177 fn replacement(self: *Storage) []BufferIndex {
178 return self.working_indices[self.active_buffer_count..];
179 }
180 };
181
182 /// A pair of one buffer the compositor is done with and that buffer's
183 /// accumulator. A present takes the pair, writes the listed rectangles into the
184 /// buffer, and then sends the buffer.
185 pub const Acquired = struct {
186 buffer: *Buffer,
187 dirty: *damage.Region,
188
189 /// Copies every rectangle the buffer owes out of `source` into the buffer,
190 /// converting each pixel to the compositor's byte order, so a present
191 /// brings the buffer up to the logical frame. `source` holds the whole
192 /// frame at the buffer's extent, and each rectangle is read from the same
193 /// offsets it is written to. The call empties the accumulator once every
194 /// rectangle is written, so the buffer owes nothing until the next change
195 /// arrives. The call returns four bytes for each pixel of the listed
196 /// rectangles. The accumulator has to hold at least one rectangle when the
197 /// call starts. A failure from the pixel copy comes back as the call's
198 /// error, with the accumulator left as it was.
199 pub fn write(self: Acquired, source: []const u8) native.shm.PixelError!usize {
200 std.debug.assert(!self.dirty.isEmpty());
201 var written: usize = 0;
202 for (self.dirty.slice()) |region| {
203 try self.buffer.writeRgba8(source, .{
204 .x = region.x,
205 .y = region.y,
206 .width = region.width,
207 .height = region.height,
208 });
209 written += region.pixelCount() * 4;
210 }
211 std.debug.assert(written == self.dirty.pixelCount() * 4);
212 self.dirty.clear();
213 return written;
214 }
215 };
216
217 pub const Swapchain = struct {
218 allocator: std.mem.Allocator,
219 client: *runtime.Client,
220 storage: Storage,
221 shm: u32 = 0,
222 format: Format = .xrgb8888,
223 configured: bool = false,
224 width: u32 = 0,
225 height: u32 = 0,
226 retired_capacity_rejection_count: u64 = 0,
227
228 pub fn init(
229 allocator: std.mem.Allocator,
230 client: *runtime.Client,
231 capacity: Capacity,
232 ) std.mem.Allocator.Error!Swapchain {
233 return .{
234 .allocator = allocator,
235 .client = client,
236 .storage = try Storage.init(allocator, capacity),
237 };
238 }
239
240 pub fn configure(self: *Swapchain, shm: u32, format: Format) void {
241 std.debug.assert(!self.configured);
242 self.shm = shm;
243 self.format = format;
244 self.configured = true;
245 }
246
247 pub fn deinit(self: *Swapchain) void {
248 for (self.storage.buffers) |*slot| {
249 if (slot.*) |*value| value.deinit(self.client);
250 slot.* = null;
251 }
252 self.storage.deinit(self.allocator);
253 self.* = undefined;
254 }
255
256 pub fn dispatch(self: *Swapchain, event: *runtime.RoutedView) !bool {
257 for (self.storage.buffers) |*slot| {
258 if (slot.*) |*value| {
259 if (value.object_id != event.object_id) continue;
260 try value.dispatch(event);
261 return true;
262 }
263 }
264 return false;
265 }
266
267 pub fn ensure(self: *Swapchain, width: u32, height: u32) SwapchainError!void {
268 std.debug.assert(self.configured);
269 self.collectRetired();
270 const active = self.storage.active();
271 if (self.width == width and self.height == height and active[0] != no_buffer) return;
272
273 var busy_count: usize = 0;
274 for (active) |index| {
275 if (index != no_buffer and self.buffer(index).busy) busy_count += 1;
276 }
277 if (!self.admitRetirement(busy_count)) return error.RetiredCapacityExceeded;
278
279 errdefer self.clearReplacement();
280 for (self.storage.replacement()) |*slot| {
281 std.debug.assert(slot.* == no_buffer);
282 const index = self.findFreeBuffer() orelse unreachable;
283 const value = try Buffer.open(
284 self.client,
285 self.shm,
286 width,
287 height,
288 self.format,
289 );
290 self.installBuffer(index, value, width, height);
291 slot.* = index;
292 }
293
294 self.commitReplacement();
295 self.width = width;
296 self.height = height;
297 }
298
299 /// Destroys every retired buffer the compositor has released. The call
300 /// hands back the first active buffer that is free, together with that
301 /// buffer's accumulator, so a present finds a buffer it may write now. The
302 /// returned accumulator lists where that buffer differs from the logical
303 /// frame, so a present writes those rectangles and then clears the list.
304 /// The call returns null when the compositor holds every active buffer, and
305 /// the presenter then keeps the frame for a later send.
306 pub fn acquire(self: *Swapchain) ?Acquired {
307 self.collectRetired();
308 for (self.storage.active()) |index| {
309 if (index == no_buffer) continue;
310 const value = self.buffer(index);
311 if (value.busy) continue;
312 return .{ .buffer = value, .dirty = &self.storage.dirty[@intCast(index)] };
313 }
314 return null;
315 }
316
317 /// Adds the rectangle `region` to the accumulator of every active buffer,
318 /// busy or free, because the change applies to all of them. The presenter
319 /// calls this at every present so each buffer learns what it will have to
320 /// rewrite when its turn comes. The call answers `.collapsed` when at least
321 /// one of those accumulators ran out of room, and `.exact` when none did.
322 pub fn markDamage(self: *Swapchain, region: windowing.PresentRegion) damage.Addition {
323 var addition: damage.Addition = .exact;
324 for (self.storage.active()) |index| {
325 if (index == no_buffer) continue;
326 if (self.storage.dirty[@intCast(index)].add(region) == .collapsed) {
327 addition = .collapsed;
328 }
329 }
330 return addition;
331 }
332
333 pub fn collectRetired(self: *Swapchain) void {
334 var position = self.storage.retired_count;
335 while (position > 0) {
336 position -= 1;
337 const index = self.storage.retired[position];
338 if (self.buffer(index).busy) continue;
339 self.storage.retired_count -= 1;
340 self.storage.retired[position] = self.storage.retired[self.storage.retired_count];
341 self.destroy(index);
342 }
343 }
344
345 pub fn retiredCapacityRejectionCount(self: *const Swapchain) u64 {
346 return self.retired_capacity_rejection_count;
347 }
348
349 fn admitRetirement(self: *Swapchain, busy_count: usize) bool {
350 std.debug.assert(self.storage.retired_count <= self.storage.retired.len);
351 if (busy_count <= self.storage.retired.len - self.storage.retired_count) return true;
352 self.retired_capacity_rejection_count +|= 1;
353 return false;
354 }
355
356 fn commitReplacement(self: *Swapchain) void {
357 const active = self.storage.active();
358 for (active) |*slot| {
359 const index = slot.*;
360 if (index == no_buffer) continue;
361 if (self.buffer(index).busy) {
362 std.debug.assert(self.storage.retired_count < self.storage.retired.len);
363 self.storage.retired[self.storage.retired_count] = index;
364 self.storage.retired_count += 1;
365 } else {
366 self.destroy(index);
367 }
368 slot.* = no_buffer;
369 }
370
371 const replacement = self.storage.replacement();
372 for (active, replacement) |*active_slot, *replacement_slot| {
373 std.debug.assert(replacement_slot.* != no_buffer);
374 active_slot.* = replacement_slot.*;
375 replacement_slot.* = no_buffer;
376 }
377 }
378
379 fn clearReplacement(self: *Swapchain) void {
380 for (self.storage.replacement()) |*slot| {
381 if (slot.* != no_buffer) self.destroy(slot.*);
382 slot.* = no_buffer;
383 }
384 }
385
386 /// Stores a newly created buffer in an empty slot and covers that slot's
387 /// accumulator with the whole extent, so the first present into that buffer
388 /// writes all of it. A new buffer holds none of the frame's pixels yet,
389 /// which is why it owes every one of them. The slot has to be empty.
390 fn installBuffer(
391 self: *Swapchain,
392 index: BufferIndex,
393 value: Buffer,
394 width: u32,
395 height: u32,
396 ) void {
397 std.debug.assert(self.storage.buffers[@intCast(index)] == null);
398 self.storage.buffers[@intCast(index)] = value;
399 self.storage.dirty[@intCast(index)].cover(width, height);
400 }
401
402 fn findFreeBuffer(self: *const Swapchain) ?BufferIndex {
403 for (self.storage.buffers, 0..) |slot, index| {
404 if (slot == null) return @intCast(index);
405 }
406 return null;
407 }
408
409 fn buffer(self: *Swapchain, index: BufferIndex) *Buffer {
410 std.debug.assert(index != no_buffer);
411 if (self.storage.buffers[@intCast(index)]) |*value| return value;
412 unreachable;
413 }
414
415 fn destroy(self: *Swapchain, index: BufferIndex) void {
416 const value = self.buffer(index);
417 value.deinit(self.client);
418 self.storage.buffers[@intCast(index)] = null;
419 self.storage.dirty[@intCast(index)].clear();
420 }
421 };
422
423 test "swapchain capacity derives exact transient metadata and rejects invalid limits" {
424 const capacity = try Capacity.derive(3, 5);
425 try std.testing.expectEqual(@as(usize, 3), capacity.active_buffer_count);
426 try std.testing.expectEqual(@as(usize, 5), capacity.retired_buffer_count);
427 try std.testing.expectEqual(@as(usize, 11), capacity.buffer_slot_count);
428 try std.testing.expectEqual(11 * @sizeOf(?Buffer), capacity.buffer_slot_bytes);
429 try std.testing.expectEqual(3 * @sizeOf(BufferIndex), capacity.active_index_bytes);
430 try std.testing.expectEqual(3 * @sizeOf(BufferIndex), capacity.replacement_index_bytes);
431 try std.testing.expectEqual(5 * @sizeOf(BufferIndex), capacity.retired_index_bytes);
432 try std.testing.expectEqual(11 * @sizeOf(damage.Region), capacity.dirty_region_bytes);
433 try std.testing.expectEqual(
434 capacity.buffer_slot_bytes +
435 capacity.dirty_region_bytes +
436 capacity.active_index_bytes +
437 capacity.replacement_index_bytes +
438 capacity.retired_index_bytes,
439 capacity.metadata_storage_bytes,
440 );
441 try std.testing.expectError(error.ActiveBuffersEmpty, Capacity.derive(0, 1));
442 try std.testing.expectError(error.RetiredBuffersEmpty, Capacity.derive(1, 0));
443 if (@sizeOf(usize) > @sizeOf(BufferIndex)) {
444 try std.testing.expectError(
445 error.BufferSlotsTooMany,
446 Capacity.derive(std.math.maxInt(BufferIndex) / 2, 2),
447 );
448 }
449 try std.testing.expectError(
450 error.CapacityOverflow,
451 Capacity.derive(std.math.maxInt(usize) / 2 + 1, 1),
452 );
453 }
454
455 test "swapchain metadata acquires every region at initialization" {
456 const capacity = try Capacity.derive(2, 2);
457 const client: *runtime.Client = undefined;
458 for (0..4) |fail_index| {
459 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{
460 .fail_index = fail_index,
461 });
462 try std.testing.expectError(
463 error.OutOfMemory,
464 Swapchain.init(failing.allocator(), client, capacity),
465 );
466 }
467
468 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{
469 .fail_index = 4,
470 });
471 var swapchain = try Swapchain.init(failing.allocator(), client, capacity);
472 defer swapchain.deinit();
473 try std.testing.expectEqual(@as(usize, 4), failing.allocations);
474 try std.testing.expectEqual(@as(usize, 2), swapchain.storage.active().len);
475 try std.testing.expectEqual(@as(usize, 2), swapchain.storage.replacement().len);
476 try std.testing.expectEqual(@as(usize, 2), swapchain.storage.retired.len);
477 }
478
479 test "swapchain metadata commits one full retirement and rejects max plus one" {
480 const capacity = try Capacity.derive(2, 2);
481 const client: *runtime.Client = undefined;
482 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{
483 .fail_index = 4,
484 });
485 var swapchain = try Swapchain.init(failing.allocator(), client, capacity);
486 defer swapchain.deinit();
487 try std.testing.expect(swapchain.admitRetirement(2));
488 var mapped: [1]u8 align(std.heap.page_size_min) = undefined;
489 for (swapchain.storage.buffers[0..4], 0..) |*slot, index| {
490 slot.* = Buffer{
491 .object_id = @intCast(index + 1),
492 .mapping = mapped[0..],
493 .byte_len = 1,
494 .width = 1,
495 .height = 1,
496 .stride = 4,
497 .format = .xbgr8888,
498 .busy = index < 2,
499 };
500 }
501 swapchain.storage.active()[0] = 0;
502 swapchain.storage.active()[1] = 1;
503 swapchain.storage.replacement()[0] = 2;
504 swapchain.storage.replacement()[1] = 3;
505 swapchain.commitReplacement();
506 try std.testing.expectEqualSlices(BufferIndex, &.{ 2, 3 }, swapchain.storage.active());
507 try std.testing.expectEqualSlices(
508 BufferIndex,
509 &.{ no_buffer, no_buffer },
510 swapchain.storage.replacement(),
511 );
512 try std.testing.expectEqual(@as(usize, 2), swapchain.storage.retired_count);
513 try std.testing.expectEqualSlices(BufferIndex, &.{ 0, 1 }, swapchain.storage.retired);
514
515 const active_before = [2]BufferIndex{
516 swapchain.storage.active()[0],
517 swapchain.storage.active()[1],
518 };
519 try std.testing.expect(!swapchain.admitRetirement(1));
520 try std.testing.expectEqualSlices(BufferIndex, &active_before, swapchain.storage.active());
521 try std.testing.expectEqual(@as(usize, 2), swapchain.storage.retired_count);
522 try std.testing.expectEqual(@as(u64, 1), swapchain.retiredCapacityRejectionCount());
523 swapchain.retired_capacity_rejection_count = std.math.maxInt(u64);
524 try std.testing.expect(!swapchain.admitRetirement(1));
525 try std.testing.expectEqual(
526 std.math.maxInt(u64),
527 swapchain.retiredCapacityRejectionCount(),
528 );
529 try std.testing.expectEqual(@as(usize, 4), failing.allocations);
530
531 @memset(swapchain.storage.buffers, null);
532 @memset(swapchain.storage.working_indices, no_buffer);
533 swapchain.storage.retired_count = 0;
534 }
535
536 test "swapchain metadata reuses a released buffer slot" {
537 const capacity = try Capacity.derive(2, 2);
538 const client: *runtime.Client = undefined;
539 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{
540 .fail_index = 4,
541 });
542 var swapchain = try Swapchain.init(failing.allocator(), client, capacity);
543 defer swapchain.deinit();
544 var mapped: [1]u8 align(std.heap.page_size_min) = undefined;
545 const first = swapchain.findFreeBuffer().?;
546 swapchain.storage.buffers[@intCast(first)] = Buffer{
547 .object_id = 9,
548 .mapping = mapped[0..],
549 .byte_len = 1,
550 .width = 1,
551 .height = 1,
552 .stride = 4,
553 .format = .xbgr8888,
554 };
555 try std.testing.expect(swapchain.findFreeBuffer().? != first);
556 swapchain.storage.buffers[@intCast(first)] = null;
557 try std.testing.expectEqual(first, swapchain.findFreeBuffer().?);
558 try std.testing.expectEqual(@as(usize, 4), failing.allocations);
559 }
560
561 test "XRGB conversion writes protocol byte order" {
562 var bytes: [8]u8 align(std.heap.page_size_min) = undefined;
563 var value = Buffer{
564 .object_id = 10,
565 .mapping = @alignCast(bytes[0..]),
566 .byte_len = bytes.len,
567 .width = 2,
568 .height = 1,
569 .stride = 8,
570 .format = .xrgb8888,
571 };
572 try value.writeRgba8(&.{ 1, 2, 3, 4, 10, 20, 30, 40 }, value.fullRegion());
573 try std.testing.expectEqualSlices(u8, &.{ 3, 2, 1, 255, 30, 20, 10, 255 }, &bytes);
574 }
575
576 const rotation_width: u32 = 16;
577 const rotation_height: u32 = 12;
578 const rotation_bytes: usize = rotation_width * rotation_height * 4;
579 const rotation_slots: usize = 3;
580
581 /// A test harness that builds a swapchain of three buffers whose pixel storage
582 /// comes from the test allocator. Tests use the harness to drive a present
583 /// rotation over real pixel storage and compare the buffers byte for byte. Each
584 /// buffer starts zeroed and owes its whole extent. `use` marks one buffer free
585 /// and the other two busy, so a test picks the buffer the next acquire returns.
586 /// A rotation driven this way needs no compositor and no socket, and the tests
587 /// pass a client pointer they leave unread.
588 const Rotation = struct {
589 swapchain: Swapchain,
590 allocator: std.mem.Allocator,
591
592 fn init(
593 allocator: std.mem.Allocator,
594 client: *runtime.Client,
595 width: u32,
596 height: u32,
597 ) !Rotation {
598 const capacity = try Capacity.derive(rotation_slots, 2);
599 var chain = try Swapchain.init(allocator, client, capacity);
600 chain.configured = true;
601 chain.width = width;
602 chain.height = height;
603 const byte_len = @as(usize, width) * height * 4;
604 for (chain.storage.active()) |*slot| {
605 const index = chain.findFreeBuffer().?;
606 const mapping = try allocator.alignedAlloc(
607 u8,
608 .fromByteUnits(std.heap.page_size_min),
609 byte_len,
610 );
611 @memset(mapping, 0);
612 chain.installBuffer(index, .{
613 .object_id = @as(u32, index) + 1,
614 .mapping = mapping,
615 .byte_len = byte_len,
616 .width = width,
617 .height = height,
618 .stride = width * 4,
619 .format = .xrgb8888,
620 }, width, height);
621 slot.* = index;
622 }
623 return .{ .swapchain = chain, .allocator = allocator };
624 }
625
626 fn deinit(self: *Rotation) void {
627 for (self.swapchain.storage.buffers) |*slot| {
628 if (slot.*) |value| self.allocator.free(value.mapping);
629 slot.* = null;
630 }
631 self.swapchain.storage.deinit(self.allocator);
632 self.* = undefined;
633 }
634
635 fn use(self: *Rotation, position: usize) Acquired {
636 for (self.swapchain.storage.active(), 0..) |index, slot| {
637 self.swapchain.buffer(index).busy = slot != position;
638 }
639 return self.swapchain.acquire().?;
640 }
641 };
642
643 fn paintRegion(frame: []u8, region: windowing.PresentRegion, seed: u8) void {
644 var row: u32 = 0;
645 while (row < region.height) : (row += 1) {
646 var column: u32 = 0;
647 while (column < region.width) : (column += 1) {
648 const offset =
649 (@as(usize, region.y + row) * rotation_width + region.x + column) * 4;
650 frame[offset] = seed +% @as(u8, @truncate(offset));
651 frame[offset + 1] = seed *% 3 +% @as(u8, @truncate(row));
652 frame[offset + 2] = seed *% 7 +% @as(u8, @truncate(column));
653 frame[offset + 3] = 0xff;
654 }
655 }
656 }
657
658 fn nextRegion(random: std.Random) windowing.PresentRegion {
659 const x = random.uintLessThan(u32, rotation_width);
660 const y = random.uintLessThan(u32, rotation_height);
661 return .{
662 .x = x,
663 .y = y,
664 .width = 1 + random.uintLessThan(u32, rotation_width - x),
665 .height = 1 + random.uintLessThan(u32, rotation_height - y),
666 };
667 }
668
669 test "region presents leave every slot equal to the full presents they replace" {
670 const allocator = std.testing.allocator;
671 const client: *runtime.Client = undefined;
672 var region = try Rotation.init(allocator, client, rotation_width, rotation_height);
673 defer region.deinit();
674 var whole = try Rotation.init(allocator, client, rotation_width, rotation_height);
675 defer whole.deinit();
676
677 var frame: [rotation_bytes]u8 = @splat(0);
678 var prng = std.Random.DefaultPrng.init(0x9e3779b97f4a7c15);
679 const random = prng.random();
680 const schedule = [_]usize{ 0, 1, 2, 0, 0, 1, 2, 0 };
681 var region_written: usize = 0;
682 var whole_written: usize = 0;
683 for (schedule, 0..) |position, step| {
684 const dirty = nextRegion(random);
685 paintRegion(&frame, dirty, @truncate(step *% 37 +% 11));
686 _ = region.swapchain.markDamage(dirty);
687 const partial = region.use(position);
688 region_written += try partial.write(&frame);
689 const full = whole.use(position);
690 full.dirty.cover(rotation_width, rotation_height);
691 whole_written += try full.write(&frame);
692 try std.testing.expectEqualSlices(
693 u8,
694 full.buffer.mapping,
695 partial.buffer.mapping,
696 );
697 }
698 try std.testing.expectEqual(@as(usize, 8 * rotation_bytes), whole_written);
699 try std.testing.expect(region_written < whole_written);
700 }
701
702 test "three disjoint region presents leave every slot able to reach the frame" {
703 const allocator = std.testing.allocator;
704 const client: *runtime.Client = undefined;
705 var rotation = try Rotation.init(allocator, client, rotation_width, rotation_height);
706 defer rotation.deinit();
707 var reference = try Rotation.init(allocator, client, rotation_width, rotation_height);
708 defer reference.deinit();
709
710 var frame: [rotation_bytes]u8 = @splat(0x07);
711 for (0..rotation_slots) |position| {
712 const slot = rotation.use(position);
713 try std.testing.expectEqual(rotation_bytes, try slot.write(&frame));
714 }
715
716 const disjoint = [_]windowing.PresentRegion{
717 .{ .x = 0, .y = 0, .width = 3, .height = 2 },
718 .{ .x = 10, .y = 8, .width = 4, .height = 3 },
719 .{ .x = 6, .y = 1, .width = 2, .height = 2 },
720 };
721 for (disjoint, 0..) |dirty, step| {
722 paintRegion(&frame, dirty, @truncate(step *% 53 +% 29));
723 _ = rotation.swapchain.markDamage(dirty);
724 const slot = rotation.use(step);
725 _ = try slot.write(&frame);
726 }
727
728 const whole = reference.use(0);
729 whole.dirty.cover(rotation_width, rotation_height);
730 _ = try whole.write(&frame);
731 for (0..rotation_slots) |position| {
732 const slot = rotation.use(position);
733 if (!slot.dirty.isEmpty()) _ = try slot.write(&frame);
734 try std.testing.expectEqualSlices(
735 u8,
736 whole.buffer.mapping,
737 slot.buffer.mapping,
738 );
739 }
740 }
741
742 const cell_columns: u32 = 8;
743 const cell_extent: u32 = 2;
744 const listed_round_count: usize = 6;
745 const listed_frame_count: usize = 48;
746 const OwedMask = [rotation_width * rotation_height]bool;
747
748 fn markOwed(owed: *OwedMask, region: windowing.PresentRegion) void {
749 var row: u32 = 0;
750 while (row < region.height) : (row += 1) {
751 const start = (region.y + row) * rotation_width + region.x;
752 @memset(owed[start..][0..region.width], true);
753 }
754 }
755
756 fn drawDisjoint(
757 random: std.Random,
758 regions: *[damage.max_rects]windowing.PresentRegion,
759 ) []const windowing.PresentRegion {
760 comptime std.debug.assert(cell_columns * cell_extent == rotation_width);
761 comptime std.debug.assert(damage.max_rects / cell_columns * cell_extent == rotation_height);
762 var cells: [damage.max_rects]u8 = undefined;
763 for (&cells, 0..) |*cell, index| cell.* = @intCast(index);
764 random.shuffle(u8, &cells);
765 const limit: usize = if (random.boolean()) 12 else damage.max_rects;
766 const count = 1 + random.uintLessThan(usize, limit);
767 for (regions[0..count], cells[0..count]) |*region, cell| {
768 const width = 1 + random.uintLessThan(u32, cell_extent);
769 const height = 1 + random.uintLessThan(u32, cell_extent);
770 const x_slack = random.uintLessThan(u32, cell_extent - width + 1);
771 const y_slack = random.uintLessThan(u32, cell_extent - height + 1);
772 region.* = .{
773 .x = (cell % cell_columns) * cell_extent + x_slack,
774 .y = (cell / cell_columns) * cell_extent + y_slack,
775 .width = width,
776 .height = height,
777 };
778 }
779 return regions[0..count];
780 }
781
782 const Differential = struct {
783 listed: *Rotation,
784 whole: *Rotation,
785 frame: [rotation_bytes]u8 = @splat(0),
786 owed: [rotation_slots]OwedMask = @splat(@splat(false)),
787 collapsed: [rotation_slots]bool = @splat(false),
788 written: [rotation_slots]bool = @splat(false),
789 last: [rotation_slots]usize = @splat(0),
790 ages: [5]bool = @splat(false),
791 exact_writes: usize = 0,
792 collapsed_writes: usize = 0,
793
794 fn mark(self: *Differential, regions: []const windowing.PresentRegion, seed: u8) void {
795 for (regions) |region| {
796 paintRegion(&self.frame, region, seed);
797 const addition = self.listed.swapchain.markDamage(region);
798 for (&self.owed, &self.collapsed) |*owed, *collapsed| {
799 markOwed(owed, region);
800 if (addition == .collapsed) collapsed.* = true;
801 }
802 }
803 }
804
805 fn pick(self: *const Differential, random: std.Random, step: usize) usize {
806 for (self.written, self.last, 0..) |written, last, position| {
807 if (!written or step - last == 4) return position;
808 }
809 return random.uintLessThan(usize, rotation_slots);
810 }
811
812 fn present(self: *Differential, position: usize, step: usize) !void {
813 try self.expectSlotReaches(position);
814 if (self.written[position]) {
815 const age = step - self.last[position];
816 std.debug.assert(age >= 1);
817 std.debug.assert(age <= 4);
818 self.ages[age] = true;
819 }
820 self.written[position] = true;
821 self.last[position] = step;
822 }
823
824 fn expectSlotReaches(self: *Differential, position: usize) !void {
825 const partial = self.listed.use(position);
826 try self.expectOwed(position, partial.dirty);
827 if (!partial.dirty.isEmpty()) _ = try partial.write(&self.frame);
828 self.owed[position] = @splat(false);
829 self.collapsed[position] = false;
830 const full = self.whole.use(position);
831 full.dirty.cover(rotation_width, rotation_height);
832 _ = try full.write(&self.frame);
833 try std.testing.expectEqualSlices(u8, full.buffer.mapping, partial.buffer.mapping);
834 }
835
836 fn expectOwed(self: *Differential, position: usize, dirty: *const damage.Region) !void {
837 if (!self.written[position]) return;
838 var covered: OwedMask = @splat(false);
839 for (dirty.slice()) |region| markOwed(&covered, region);
840 if (self.collapsed[position]) {
841 for (self.owed[position], covered) |owes, listed| {
842 if (owes) try std.testing.expect(listed);
843 }
844 self.collapsed_writes += 1;
845 } else {
846 try std.testing.expectEqualSlices(bool, &self.owed[position], &covered);
847 self.exact_writes += 1;
848 }
849 }
850 };
851
852 test "listed region presents match full presents at every slot age from one to four" {
853 const allocator = std.testing.allocator;
854 const client: *runtime.Client = undefined;
855 var prng = std.Random.DefaultPrng.init(0x5eed_7e96_d4a3_0048);
856 const random = prng.random();
857 var ages: [5]bool = @splat(false);
858 var exact_writes: usize = 0;
859 var collapsed_writes: usize = 0;
860 var regions: [damage.max_rects]windowing.PresentRegion = undefined;
861 for (0..listed_round_count) |_| {
862 var listed = try Rotation.init(allocator, client, rotation_width, rotation_height);
863 defer listed.deinit();
864 var whole = try Rotation.init(allocator, client, rotation_width, rotation_height);
865 defer whole.deinit();
866 var run = Differential{ .listed = &listed, .whole = &whole };
867 for (1..listed_frame_count + 1) |step| {
868 run.mark(drawDisjoint(random, ®ions), @truncate(step *% 41 +% 7));
869 try run.present(run.pick(random, step), step);
870 }
871 for (0..rotation_slots) |position| try run.expectSlotReaches(position);
872 for (&ages, run.ages) |*seen, reached| seen.* = seen.* or reached;
873 exact_writes += run.exact_writes;
874 collapsed_writes += run.collapsed_writes;
875 }
876 try std.testing.expect(!ages[0]);
877 for (ages[1..]) |seen| try std.testing.expect(seen);
878 try std.testing.expect(exact_writes > 0);
879 try std.testing.expect(collapsed_writes > 0);
880 }
881
882 test "a slot recreated at a new extent owes every byte of its buffer" {
883 const allocator = std.testing.allocator;
884 const client: *runtime.Client = undefined;
885 var rotation = try Rotation.init(allocator, client, rotation_width, rotation_height);
886 defer rotation.deinit();
887
888 var frame: [rotation_bytes]u8 = @splat(0x21);
889 for (0..rotation_slots) |position| {
890 const slot = rotation.use(position);
891 _ = try slot.write(&frame);
892 try std.testing.expect(slot.dirty.isEmpty());
893 }
894
895 _ = rotation.swapchain.markDamage(.{ .x = 1, .y = 1, .width = 2, .height = 2 });
896 for (rotation.swapchain.storage.active()) |index| {
897 const value = rotation.swapchain.buffer(index).*;
898 rotation.swapchain.storage.buffers[@intCast(index)] = null;
899 rotation.swapchain.installBuffer(index, value, rotation_width, rotation_height);
900 try std.testing.expectEqualSlices(
901 windowing.PresentRegion,
902 &.{windowing.PresentRegion.full(rotation_width, rotation_height)},
903 rotation.swapchain.storage.dirty[@intCast(index)].slice(),
904 );
905 }
906 const slot = rotation.use(0);
907 try std.testing.expectEqual(rotation_bytes, try slot.write(&frame));
908 }
909
910 test "a slot that missed presents owes each rectangle it missed" {
911 const allocator = std.testing.allocator;
912 const client: *runtime.Client = undefined;
913 var rotation = try Rotation.init(allocator, client, rotation_width, rotation_height);
914 defer rotation.deinit();
915
916 var frame: [rotation_bytes]u8 = @splat(0x33);
917 for (0..rotation_slots) |position| {
918 const slot = rotation.use(position);
919 _ = try slot.write(&frame);
920 }
921
922 const first = windowing.PresentRegion{ .x = 1, .y = 1, .width = 2, .height = 2 };
923 const second = windowing.PresentRegion{ .x = 9, .y = 7, .width = 3, .height = 2 };
924 try std.testing.expectEqual(damage.Addition.exact, rotation.swapchain.markDamage(first));
925 try std.testing.expectEqual(damage.Addition.exact, rotation.swapchain.markDamage(second));
926 for (rotation.swapchain.storage.active()) |index| {
927 try std.testing.expectEqualSlices(
928 windowing.PresentRegion,
929 &.{ first, second },
930 rotation.swapchain.storage.dirty[@intCast(index)].slice(),
931 );
932 }
933 const slot = rotation.use(0);
934 const owed_bytes = (first.pixelCount() + second.pixelCount()) * 4;
935 try std.testing.expectEqual(owed_bytes, try slot.write(&frame));
936 try std.testing.expect(slot.dirty.isEmpty());
937 }
938
939 fn spacedPixel(index: usize) windowing.PresentRegion {
940 std.debug.assert(index < damage.max_rects);
941 return .{
942 .x = @intCast(2 * (index % 8)),
943 .y = @intCast(2 * (index / 8)),
944 .width = 1,
945 .height = 1,
946 };
947 }
948
949 test "a slot that missed forty nine rectangles owes their bounding box" {
950 const allocator = std.testing.allocator;
951 const client: *runtime.Client = undefined;
952 var rotation = try Rotation.init(allocator, client, rotation_width, rotation_height);
953 defer rotation.deinit();
954 var frame: [rotation_bytes]u8 = @splat(0x44);
955 for (0..rotation_slots) |position| _ = try rotation.use(position).write(&frame);
956
957 for (0..damage.max_rects) |index| {
958 const addition = rotation.swapchain.markDamage(spacedPixel(index));
959 try std.testing.expectEqual(damage.Addition.exact, addition);
960 }
961 const listed = rotation.use(0);
962 try std.testing.expectEqual(damage.max_rects, listed.dirty.slice().len);
963 try std.testing.expectEqual(damage.max_rects * 4, try listed.write(&frame));
964
965 const corner = windowing.PresentRegion{ .x = 15, .y = 11, .width = 1, .height = 1 };
966 try std.testing.expectEqual(damage.Addition.collapsed, rotation.swapchain.markDamage(corner));
967 const fresh = rotation.use(0);
968 try std.testing.expectEqualSlices(windowing.PresentRegion, &.{corner}, fresh.dirty.slice());
969 try std.testing.expectEqual(@as(usize, 4), try fresh.write(&frame));
970 const stale = rotation.use(1);
971 const whole = windowing.PresentRegion.full(rotation_width, rotation_height);
972 try std.testing.expectEqualSlices(windowing.PresentRegion, &.{whole}, stale.dirty.slice());
973 try std.testing.expectEqual(rotation_bytes, try stale.write(&frame));
974 }
975
976 test "a caret and a clock at opposite corners write two rectangles per present" {
977 const allocator = std.testing.allocator;
978 const client: *runtime.Client = undefined;
979 const width: u32 = 3840;
980 const height: u32 = 2160;
981 var rotation = try Rotation.init(allocator, client, width, height);
982 defer rotation.deinit();
983 const frame = try allocator.alloc(u8, @as(usize, width) * height * 4);
984 defer allocator.free(frame);
985 @memset(frame, 0x5c);
986 for (0..rotation_slots) |position| _ = try rotation.use(position).write(frame);
987
988 const caret = windowing.PresentRegion{ .x = 0, .y = 0, .width = 64, .height = 64 };
989 const clock = windowing.PresentRegion{
990 .x = width - 64,
991 .y = height - 64,
992 .width = 64,
993 .height = 64,
994 };
995 const tile_bytes: usize = 16 * 1024;
996 try std.testing.expectEqual(tile_bytes, caret.pixelCount() * 4);
997 try std.testing.expectEqual(damage.Addition.exact, rotation.swapchain.markDamage(caret));
998 try std.testing.expectEqual(tile_bytes, try rotation.use(0).write(frame));
999
1000 for (1..13) |step| {
1001 const region = if (step % 2 == 1) clock else caret;
1002 try std.testing.expectEqual(damage.Addition.exact, rotation.swapchain.markDamage(region));
1003 const slot = rotation.use(step % rotation_slots);
1004 const owed = slot.dirty.slice();
1005 try std.testing.expectEqual(@as(usize, 2), owed.len);
1006 try std.testing.expect(std.meta.eql(owed[0], caret) or std.meta.eql(owed[1], caret));
1007 try std.testing.expect(std.meta.eql(owed[0], clock) or std.meta.eql(owed[1], clock));
1008 try std.testing.expectEqual(2 * tile_bytes, try slot.write(frame));
1009 }
1010 }