Files
att_wm/src/shm.zig
T

196 lines
6.6 KiB
Zig

//! 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;
}
};