//! Minimal wl_shm buffer pool for the tab bar. //! //! The tab bar is the only thing att_wm draws itself, and it draws nothing but //! solid rectangles and text in the bitmap font from `font.zig`, so this //! deliberately stops at "memfd, mmap, fill" rather than pulling in pixman. const std = @import("std"); const posix = std.posix; const Allocator = std.mem.Allocator; const sys = @import("sys.zig"); const wayland = @import("wayland"); const wl = wayland.client.wl; const font = @import("font.zig"); const layout = @import("layout.zig"); const Box = layout.Box; /// Two buffers is enough: we redraw at most once per render sequence and the /// compositor releases the previous one promptly. const buffer_count = 2; pub const Buffer = struct { wl_buffer: *wl.Buffer, data: []align(std.heap.page_size_min) u8, width: i32, height: i32, /// Held by the compositor; must not be drawn into until released. busy: bool = false, fn onRelease(_: *wl.Buffer, event: wl.Buffer.Event, self: *Buffer) void { switch (event) { .release => self.busy = false, } } pub fn pixels(self: *Buffer) []u32 { const count: usize = @intCast(self.width * self.height); const ptr: [*]u32 = @ptrCast(@alignCast(self.data.ptr)); return ptr[0..count]; } /// Fill a rectangle, in buffer-local coordinates, clipped to the buffer. pub fn fill(self: *Buffer, rect: Box, argb: u32) void { const x0 = @max(0, rect.x); const y0 = @max(0, rect.y); const x1 = @min(self.width, rect.x + rect.width); const y1 = @min(self.height, rect.y + rect.height); if (x1 <= x0 or y1 <= y0) return; const px = self.pixels(); const stride: usize = @intCast(self.width); var y: i32 = y0; while (y < y1) : (y += 1) { const row_start = @as(usize, @intCast(y)) * stride; const from = row_start + @as(usize, @intCast(x0)); const to = row_start + @as(usize, @intCast(x1)); @memset(px[from..to], argb); } } /// Fill a rectangle, clipped to `clip` as well as to the buffer. pub fn fillClipped(self: *Buffer, rect: Box, clip: Box, argb: u32) void { const x0 = @max(rect.x, clip.x); const y0 = @max(rect.y, clip.y); const x1 = @min(rect.x + rect.width, clip.x + clip.width); const y1 = @min(rect.y + rect.height, clip.y + clip.height); if (x1 <= x0 or y1 <= y0) return; self.fill(.{ .x = x0, .y = y0, .width = x1 - x0, .height = y1 - y0 }, argb); } /// Draw text with the top left of its first cell at (x, y), clipped to /// `clip`, and return the x the next cell would start at. The font has no /// antialiasing, so lit pixels are simply written: nothing to blend. pub fn drawText(self: *Buffer, text: []const u8, x: i32, y: i32, scale: i32, argb: u32, clip: Box) i32 { var pen = x; var it: font.Iterator = .{ .bytes = text }; while (it.next()) |cp| { // Stop as soon as the pen leaves the clip rather than walking the // rest of a long title a pixel at a time. if (pen >= clip.x + clip.width) break; self.drawGlyph(cp, pen, y, scale, argb, clip); pen += font.advance * scale; } return pen; } pub fn drawGlyph(self: *Buffer, cp: u21, x: i32, y: i32, scale: i32, argb: u32, clip: Box) void { const rows = font.glyph(cp); for (rows, 0..) |bits, row| { if (bits == 0) continue; const py = y + @as(i32, @intCast(row)) * scale; var col: u3 = 0; while (col < font.cell_width) : (col += 1) { const mask = @as(u8, 1) << @intCast(font.cell_width - 1 - col); if (bits & mask == 0) continue; self.fillClipped(.{ .x = x + @as(i32, col) * scale, .y = py, .width = scale, .height = scale, }, clip, argb); } } } fn deinit(self: *Buffer, gpa: Allocator) void { self.wl_buffer.destroy(); posix.munmap(self.data); gpa.destroy(self); } }; pub const Pool = struct { gpa: Allocator, shm: *wl.Shm, buffers: [buffer_count]?*Buffer = @splat(null), pub fn init(gpa: Allocator, shm: *wl.Shm) Pool { return .{ .gpa = gpa, .shm = shm }; } pub fn deinit(self: *Pool) void { for (&self.buffers) |*slot| { if (slot.*) |buf| buf.deinit(self.gpa); slot.* = null; } } /// Return a buffer of the requested size that the compositor is not /// currently reading from, creating or resizing one as needed. pub fn acquire(self: *Pool, width: i32, height: i32) !*Buffer { if (width <= 0 or height <= 0) return error.InvalidSize; // Reuse an idle buffer that is already the right size. for (self.buffers) |maybe| { if (maybe) |buf| { if (!buf.busy and buf.width == width and buf.height == height) return buf; } } // Otherwise take a free slot, evicting an idle wrong-sized buffer. for (&self.buffers) |*slot| { if (slot.* == null) { slot.* = try self.create(width, height); return slot.*.?; } } for (&self.buffers) |*slot| { const buf = slot.*.?; if (!buf.busy) { buf.deinit(self.gpa); slot.* = try self.create(width, height); return slot.*.?; } } return error.AllBuffersBusy; } fn create(self: *Pool, width: i32, height: i32) !*Buffer { const stride = width * 4; const size: usize = @intCast(stride * height); const fd = try posix.memfd_create("att_wm-shm", std.os.linux.MFD.CLOEXEC); defer sys.close(fd); try sys.ftruncate(fd, size); const data = try posix.mmap( null, size, .{ .READ = true, .WRITE = true }, .{ .TYPE = .SHARED }, fd, 0, ); errdefer posix.munmap(data); const shm_pool = try self.shm.createPool(fd, @intCast(size)); defer shm_pool.destroy(); const wl_buffer = try shm_pool.createBuffer(0, width, height, stride, .argb8888); errdefer wl_buffer.destroy(); const buf = try self.gpa.create(Buffer); buf.* = .{ .wl_buffer = wl_buffer, .data = data, .width = width, .height = height, }; wl_buffer.setListener(*Buffer, Buffer.onRelease, buf); return buf; } };