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, &regions), @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 }