lib/wayland/src/shm/buffer.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const builtin = @import("builtin");
  2 const std = @import("std");
  3 const sys = @import("sys");
  4 const wayland = @import("../root.zig");
  5 
  6 const core = wayland.protocol.core;
  7 const runtime = wayland.runtime;
  8 
  9 pub const Format = enum(u32) {
 10     argb8888 = 0,
 11     xrgb8888 = 1,
 12     abgr8888 = 0x3432_4241,
 13     xbgr8888 = 0x3432_4258,
 14 
 15     pub fn fromRaw(raw: u32) ?Format {
 16         return std.enums.fromInt(Format, raw);
 17     }
 18 };
 19 
 20 pub const Region = struct {
 21     x: u32,
 22     y: u32,
 23     width: u32,
 24     height: u32,
 25 };
 26 
 27 pub const Error = error{
 28     OutOfMemory,
 29     DimensionsTooLarge,
 30     CreateFailed,
 31 };
 32 
 33 pub const PixelError = error{
 34     InvalidRegion,
 35     DestinationTooSmall,
 36 };
 37 
 38 pub const Buffer = struct {
 39     object_id: u32,
 40     mapping: []align(std.heap.page_size_min) u8,
 41     byte_len: usize,
 42     width: u32,
 43     height: u32,
 44     stride: u32,
 45     format: Format,
 46     busy: bool = false,
 47 
 48     pub fn open(
 49         client: *runtime.Client,
 50         shm: u32,
 51         width: u32,
 52         height: u32,
 53         format: Format,
 54     ) Error!Buffer {
 55         const dimensions = try Dimensions.init(width, height);
 56         const descriptor = try createDescriptor();
 57         var descriptor_owned = true;
 58         defer if (descriptor_owned) sys.fd.close(descriptor);
 59         try truncate(descriptor, dimensions.byte_len);
 60 
 61         const mapping = try mapDescriptor(descriptor, dimensions.byte_len);
 62         errdefer sys.memory.unmap(mapping);
 63 
 64         const pool = try createPool(client, shm, descriptor, dimensions.byte_len);
 65         descriptor_owned = false;
 66         var pool_live = true;
 67         defer if (pool_live) destroy(client, pool, core.wl_shm_pool.requests.destroy.opcode);
 68 
 69         const buffer_id = try createBuffer(
 70             client,
 71             pool,
 72             width,
 73             height,
 74             dimensions.stride,
 75             format,
 76         );
 77         errdefer destroy(
 78             client,
 79             buffer_id,
 80             core.wl_buffer.requests.destroy.opcode,
 81         );
 82         try closePool(client, pool);
 83         pool_live = false;
 84 
 85         return .{
 86             .object_id = buffer_id,
 87             .mapping = mapping,
 88             .byte_len = dimensions.byte_len,
 89             .width = width,
 90             .height = height,
 91             .stride = dimensions.stride,
 92             .format = format,
 93         };
 94     }
 95 
 96     pub fn deinit(self: *Buffer, client: *runtime.Client) void {
 97         destroy(client, self.object_id, core.wl_buffer.requests.destroy.opcode);
 98         sys.memory.unmap(self.mapping);
 99         self.* = undefined;
100     }
101 
102     pub fn dispatch(self: *Buffer, event: *runtime.RoutedView) !void {
103         if (event.object_id != self.object_id or
104             event.metadata.opcode != core.wl_buffer.events.release.opcode)
105         {
106             return error.InvalidBufferEvent;
107         }
108         var decoder = try event.borrowedDecoder();
109         try decoder.finish();
110         self.busy = false;
111     }
112 
113     /// Converts the pixels of the rectangle `region` of the full frame `rgba8`
114     /// into the buffer, reordering each pixel's bytes into the buffer's format.
115     /// The slice `rgba8` covers the whole buffer at the buffer's extent and
116     /// stride, so a rectangle sits at the same byte offset in both. A present
117     /// calls this to copy the changed part of a frame into the buffer the
118     /// compositor will read. A present of the whole image passes the whole
119     /// buffer as the rectangle, and a present of one change passes that
120     /// change's rectangle and touches those rows alone. A source shorter than
121     /// the buffer fails with `error.DestinationTooSmall`. A rectangle with zero
122     /// width or height, or one reaching past the buffer's extent, fails with
123     /// `error.InvalidRegion`.
124     pub fn writeRgba8(self: *Buffer, rgba8: []const u8, region: Region) PixelError!void {
125         if (rgba8.len < self.byte_len) return error.DestinationTooSmall;
126         _ = try regionByteLen(self, region);
127         std.debug.assert(self.stride == self.width * 4);
128         const row_byte_len = @as(usize, region.width) * 4;
129         var row: u32 = 0;
130         while (row < region.height) : (row += 1) {
131             const offset =
132                 @as(usize, region.y + row) * self.stride +
133                 @as(usize, region.x) * 4;
134             convertToShm(
135                 self.mapping[offset..][0..row_byte_len],
136                 rgba8[offset..][0..row_byte_len],
137                 self.format,
138             );
139         }
140     }
141 
142     /// Returns the rectangle covering the whole buffer, so a caller that sends
143     /// the whole image passes this rectangle to `writeRgba8` for a present with
144     /// no damage list.
145     pub fn fullRegion(self: *const Buffer) Region {
146         return .{ .x = 0, .y = 0, .width = self.width, .height = self.height };
147     }
148 
149     pub fn copyRegionRgba8(
150         self: *const Buffer,
151         region: Region,
152         destination: []u8,
153     ) PixelError!void {
154         const byte_len = try regionByteLen(self, region);
155         if (destination.len < byte_len) return error.DestinationTooSmall;
156         var row: u32 = 0;
157         while (row < region.height) : (row += 1) {
158             const source_offset =
159                 @as(usize, region.y + row) * self.stride +
160                 @as(usize, region.x) * 4;
161             const destination_offset = @as(usize, row) * region.width * 4;
162             const row_byte_len = @as(usize, region.width) * 4;
163             convertFromShm(
164                 destination[destination_offset..][0..row_byte_len],
165                 self.mapping[source_offset..][0..row_byte_len],
166                 self.format,
167             );
168         }
169     }
170 };
171 
172 const Dimensions = struct {
173     stride: u32,
174     byte_len: usize,
175 
176     fn init(width: u32, height: u32) Error!Dimensions {
177         if (width == 0 or height == 0) return error.DimensionsTooLarge;
178         const stride = std.math.mul(
179             u32,
180             width,
181             4,
182         ) catch return error.DimensionsTooLarge;
183         const byte_len_u64 = @as(u64, stride) * height;
184         if (width > std.math.maxInt(i32) or
185             height > std.math.maxInt(i32) or
186             stride > std.math.maxInt(i32) or
187             byte_len_u64 > std.math.maxInt(i32))
188         {
189             return error.DimensionsTooLarge;
190         }
191         return .{ .stride = stride, .byte_len = @intCast(byte_len_u64) };
192     }
193 };
194 
195 fn createDescriptor() Error!sys.fd.Descriptor {
196     if (comptime builtin.os.tag != .linux) return error.CreateFailed;
197     return sys.fs.createAnonymousFile("tiny-wayland") catch |err| switch (err) {
198         error.OutOfMemory => error.OutOfMemory,
199         else => error.CreateFailed,
200     };
201 }
202 
203 fn mapDescriptor(
204     descriptor: sys.fd.Descriptor,
205     byte_len: usize,
206 ) Error![]align(std.heap.page_size_min) u8 {
207     return sys.memory.mapSharedFile(
208         descriptor,
209         byte_len,
210         .{ .read = true, .write = true },
211         0,
212     ) catch |err| switch (err) {
213         error.OutOfMemory => error.OutOfMemory,
214         else => error.CreateFailed,
215     };
216 }
217 
218 fn createPool(
219     client: *runtime.Client,
220     shm: u32,
221     descriptor: sys.fd.Descriptor,
222     byte_len: usize,
223 ) Error!u32 {
224     var created: [1]u32 = undefined;
225     client.request(
226         shm,
227         core.wl_shm.requests.create_pool.opcode,
228         &.{
229             .{ .new_id = .fixed },
230             .{ .descriptor_owned = descriptor },
231             .{ .int = @intCast(byte_len) },
232         },
233         &created,
234     ) catch return error.CreateFailed;
235     return created[0];
236 }
237 
238 fn createBuffer(
239     client: *runtime.Client,
240     pool: u32,
241     width: u32,
242     height: u32,
243     stride: u32,
244     format: Format,
245 ) Error!u32 {
246     var created: [1]u32 = undefined;
247     client.request(
248         pool,
249         core.wl_shm_pool.requests.create_buffer.opcode,
250         &.{
251             .{ .new_id = .fixed },
252             .{ .int = 0 },
253             .{ .int = @intCast(width) },
254             .{ .int = @intCast(height) },
255             .{ .int = @intCast(stride) },
256             .{ .uint = @backingInt(format) },
257         },
258         &created,
259     ) catch return error.CreateFailed;
260     return created[0];
261 }
262 
263 fn closePool(client: *runtime.Client, pool: u32) Error!void {
264     client.request(
265         pool,
266         core.wl_shm_pool.requests.destroy.opcode,
267         &.{},
268         &.{},
269     ) catch return error.CreateFailed;
270 }
271 
272 fn regionByteLen(buffer: *const Buffer, region: Region) PixelError!usize {
273     if (region.width == 0 or region.height == 0) return error.InvalidRegion;
274     if (region.x > buffer.width or region.y > buffer.height) {
275         return error.InvalidRegion;
276     }
277     if (region.width > buffer.width - region.x or
278         region.height > buffer.height - region.y)
279     {
280         return error.InvalidRegion;
281     }
282     const pixels = std.math.mul(
283         usize,
284         region.width,
285         region.height,
286     ) catch return error.InvalidRegion;
287     return std.math.mul(usize, pixels, 4) catch return error.InvalidRegion;
288 }
289 
290 const lane_steps = [_]comptime_int{ 16, 4 };
291 
292 fn swizzleLanes(
293     comptime format: Format,
294     comptime lanes: comptime_int,
295     words: @Vector(lanes, u32),
296 ) @Vector(lanes, u32) {
297     const Words = @Vector(lanes, u32);
298     const half: @Vector(lanes, u5) = @splat(16);
299     const blue: Words = @splat(0x0000_00ff);
300     const green: Words = @splat(0x0000_ff00);
301     const red: Words = @splat(0x00ff_0000);
302     const alpha: Words = @splat(0xff00_0000);
303     const colour: Words = @splat(0x00ff_ffff);
304     return switch (format) {
305         .argb8888 => ((words >> half) & blue) | (words & green) |
306             ((words << half) & red) | (words & alpha),
307         .xrgb8888 => ((words >> half) & blue) | (words & green) |
308             ((words << half) & red) | alpha,
309         .abgr8888 => words,
310         .xbgr8888 => (words & colour) | alpha,
311     };
312 }
313 
314 fn swizzleSpan(
315     comptime format: Format,
316     destination: []u8,
317     source: []const u8,
318 ) void {
319     std.debug.assert(destination.len == source.len);
320     std.debug.assert(destination.len % 4 == 0);
321     var offset: usize = 0;
322     inline for (lane_steps) |lanes| {
323         const Words = @Vector(lanes, u32);
324         const step = lanes * 4;
325         comptime std.debug.assert(@sizeOf(Words) == step);
326         while (offset + step <= destination.len) : (offset += step) {
327             const read: *align(1) const Words = @ptrCast(source[offset..][0..step]);
328             const write: *align(1) Words = @ptrCast(destination[offset..][0..step]);
329             write.* = swizzleLanes(format, lanes, read.*);
330         }
331     }
332     while (offset + 4 <= destination.len) : (offset += 4) {
333         const read: *align(1) const u32 = @ptrCast(source[offset..][0..4]);
334         const write: *align(1) u32 = @ptrCast(destination[offset..][0..4]);
335         const word: @Vector(1, u32) = @splat(read.*);
336         write.* = swizzleLanes(format, 1, word)[0];
337     }
338     std.debug.assert(offset == destination.len);
339 }
340 
341 fn swizzle(destination: []u8, source: []const u8, format: Format) void {
342     std.debug.assert(destination.len == source.len);
343     std.debug.assert(destination.len % 4 == 0);
344     switch (format) {
345         inline else => |tag| swizzleSpan(tag, destination, source),
346     }
347 }
348 
349 fn convertToShm(destination: []u8, rgba8: []const u8, format: Format) void {
350     swizzle(destination, rgba8, format);
351 }
352 
353 fn convertFromShm(destination: []u8, source: []const u8, format: Format) void {
354     swizzle(destination, source, format);
355 }
356 
357 fn destroy(client: *runtime.Client, object_id: u32, opcode: u16) void {
358     client.request(object_id, opcode, &.{}, &.{}) catch {};
359 }
360 
361 fn truncate(descriptor: sys.fd.Descriptor, byte_len: usize) Error!void {
362     return sys.fs.truncateDescriptor(descriptor, byte_len) catch |err| switch (err) {
363         error.OutOfMemory => error.OutOfMemory,
364         error.UnsupportedPlatform, error.TruncateFailed => error.CreateFailed,
365     };
366 }
367 
368 test "shared memory dimensions enforce protocol integer bounds" {
369     try std.testing.expectEqual(
370         Dimensions{ .stride = 256, .byte_len = 8192 },
371         try Dimensions.init(64, 32),
372     );
373     try std.testing.expectError(
374         error.DimensionsTooLarge,
375         Dimensions.init(0, 32),
376     );
377     try std.testing.expectError(
378         error.DimensionsTooLarge,
379         Dimensions.init(std.math.maxInt(u32), 2),
380     );
381 }
382 
383 test "shared memory pixel formats round trip canonical rgba" {
384     const rgba = [_]u8{ 0x11, 0x22, 0x33, 0x44 };
385     inline for (std.meta.tags(Format)) |format| {
386         var shared: [4]u8 = undefined;
387         var decoded: [4]u8 = undefined;
388         convertToShm(&shared, &rgba, format);
389         convertFromShm(&decoded, &shared, format);
390         try std.testing.expectEqualSlices(u8, rgba[0..3], decoded[0..3]);
391         const expected_alpha: u8 = switch (format) {
392             .argb8888, .abgr8888 => rgba[3],
393             .xrgb8888, .xbgr8888 => 0xff,
394         };
395         try std.testing.expectEqual(expected_alpha, decoded[3]);
396     }
397 }
398 
399 test "a region write swizzles its own rows and preserves every other byte" {
400     const width: u32 = 4;
401     const height: u32 = 3;
402     var mapping: [width * height * 4]u8 align(std.heap.page_size_min) = @splat(0x5a);
403     var value = Buffer{
404         .object_id = 11,
405         .mapping = @alignCast(mapping[0..]),
406         .byte_len = mapping.len,
407         .width = width,
408         .height = height,
409         .stride = width * 4,
410         .format = .xrgb8888,
411     };
412     var frame: [width * height * 4]u8 = undefined;
413     for (&frame, 0..) |*byte, index| byte.* = @truncate(index *% 7 +% 1);
414 
415     try value.writeRgba8(&frame, .{ .x = 1, .y = 1, .width = 2, .height = 1 });
416 
417     var mutated: usize = 0;
418     for (mapping, 0..) |byte, index| {
419         const row = index / (width * 4);
420         const column = (index % (width * 4)) / 4;
421         const inside = row == 1 and column >= 1 and column < 3;
422         if (inside) {
423             const channel = index % 4;
424             const expected: u8 = switch (channel) {
425                 0 => frame[index + 2],
426                 1 => frame[index],
427                 2 => frame[index - 2],
428                 else => 0xff,
429             };
430             try std.testing.expectEqual(expected, byte);
431             mutated += 1;
432         } else {
433             try std.testing.expectEqual(@as(u8, 0x5a), byte);
434         }
435     }
436     try std.testing.expectEqual(@as(usize, 8), mutated);
437 }
438 
439 test "a full region write equals the region writes that cover it" {
440     const width: u32 = 4;
441     const height: u32 = 4;
442     var whole: [width * height * 4]u8 align(std.heap.page_size_min) = @splat(0);
443     var split: [width * height * 4]u8 align(std.heap.page_size_min) = @splat(0);
444     var frame: [width * height * 4]u8 = undefined;
445     for (&frame, 0..) |*byte, index| byte.* = @truncate(index *% 13 +% 5);
446 
447     var full = Buffer{
448         .object_id = 12,
449         .mapping = @alignCast(whole[0..]),
450         .byte_len = whole.len,
451         .width = width,
452         .height = height,
453         .stride = width * 4,
454         .format = .argb8888,
455     };
456     var partial = full;
457     partial.object_id = 13;
458     partial.mapping = @alignCast(split[0..]);
459 
460     try full.writeRgba8(&frame, full.fullRegion());
461     try partial.writeRgba8(&frame, .{ .x = 0, .y = 0, .width = 4, .height = 2 });
462     try partial.writeRgba8(&frame, .{ .x = 0, .y = 2, .width = 4, .height = 2 });
463     try std.testing.expectEqualSlices(u8, &whole, &split);
464 
465     try std.testing.expectError(
466         error.InvalidRegion,
467         full.writeRgba8(&frame, .{ .x = 3, .y = 0, .width = 2, .height = 1 }),
468     );
469     try std.testing.expectError(
470         error.DestinationTooSmall,
471         full.writeRgba8(frame[0 .. frame.len - 1], full.fullRegion()),
472     );
473 }