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 }