Something went wrong. Try again.
A Wayland client for Zig 0.16 with no libwayland: protocol scanner, sans-I/O wire protocol, and socket client.
Something went wrong. Try again.
17 kB · 425 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Buffers to draw into, which are never handed out while the compositor//! still holds them.//!//! A buffer goes through three states. `acquire` hands out a *free* one as//! *acquired*; presenting it makes it *busy*, because the compositor may read//! it until it sends `wl_buffer.release`; the release makes it free again.//! Two buffers are usually enough -- one on screen, one being drawn -- and a//! third is made when both are busy, up to `Options.max_buffers`. When every//! buffer is busy and no more may be made, `acquire` returns null and the//! caller dispatches until a release arrives.//!//! Asking for a different size or format is a resize. Free buffers of the old//! shape are destroyed at once; busy ones are marked stale and destroyed when//! the compositor releases them, since destroying a buffer the compositor may//! still be showing invites a glitch on some compositors.//!//! A pool has one of two backings, chosen when it is made://!//! - **Shared memory** (`create`). Each buffer is its own `memfd`, mapped//! here and shared with the compositor through its own `wl_shm_pool`,//! which is destroyed straight away -- the buffer keeps what it needs of//! the pool alive. One pool per buffer costs a file descriptor each and//! makes a resize nothing more than a new buffer. `Buffer.pixels` is the//! mapping.//! - **dma-buf** (`createDmabuf`). The memory comes from a//! `Dmabuf.BufferAllocator` -- a GPU driver, typically -- offered the//! modifiers the compositor accepts for the format. The pool turns what it//! allocated into a `wl_buffer` with `zwp_linux_buffer_params_v1`'s//! asynchronous `create`, so a buffer the compositor refuses is//! `error.BufferCreationFailed`, which a caller can fall back from, rather//! than a protocol error that ends the connection. That waits for the//! compositor's answer, dispatching as it does, so `acquire` on such a pool//! must not be called from a listener.//!//! Under explicit sync (`Presenter.enableExplicitSync`), the compositor no//! longer promises `wl_buffer.release`, so a buffer presented with sync points//! stays busy until the consumer, having seen the release point signal, calls//! `released`.
const std = @import("std");const linux = std.os.linux;const Allocator = std.mem.Allocator;const Connection = @import("client").Connection;const Session = @import("protocol").Session;const protocols = @import("wayland-protocols");const wl = protocols.wl;const Format = @import("format.zig").Format;const Shm = @import("Shm.zig");const bindings = @import("bindings.zig");const Dmabuf = if (bindings.has_dmabuf) @import("Dmabuf.zig") else void;
const BufferPool = @This();
gpa: Allocator,conn: *Connection,backing: Backing,options: Options,buffers: std.ArrayList(*Buffer) = .empty,/// What the dma-buf allocator returned when `acquire` last failed with/// `error.AllocationFailed`.allocator_error: ?anyerror = null,
pub const Backing = union(enum) { shm: *Shm, dmabuf: if (bindings.has_dmabuf) DmabufBacking else void,};
const DmabufBacking = if (bindings.has_dmabuf) struct { dmabuf: *Dmabuf, allocator: Dmabuf.BufferAllocator,} else void;
pub const Options = struct { /// How many buffers of the current shape may exist at once. Two is the /// least that lets drawing overlap with the compositor reading; three /// covers a compositor that holds on to the previous buffer for a frame. max_buffers: u8 = 3,};
/// One buffer: the compositor's `wl_buffer`, and what backs it.pub const Buffer = struct { pool: *BufferPool, wl_buffer: wl.Buffer, width: u32, height: u32, /// Bytes from the start of one row to the start of the next. stride: u32, format: Format, state: State = .acquired, /// Of a shape the pool no longer hands out; destroyed on release. stale: bool = false, /// Last presented with explicit sync, so `wl_buffer.release` is not to be /// waited for, and `BufferPool.released` says when it is free. explicit_release: bool = false, storage: Storage,
pub const State = enum { free, acquired, busy };
/// What the buffer's pixels live in. pub const Storage = union(enum) { shm: struct { memory: []align(std.heap.page_size_min) u8 }, /// The allocator's handle, and what it allocated. No CPU mapping: /// the pixels are wherever the allocator put them. dmabuf: struct { handle: ?*anyopaque, modifier: u64, planes: [4]Plane, plane_count: u3, }, };
pub const Plane = struct { fd: std.posix.fd_t, offset: u32, stride: u32, };
/// The pixels, `stride * height` bytes, mapped and writable. Only a /// buffer in shared memory has any; see `mapped` for one that may not. /// Valid until the buffer is destroyed. pub fn pixels(b: *const Buffer) []u8 { return b.mapped().?; }
/// The pixels if the buffer is mapped -- in shared memory -- or null. pub fn mapped(b: *const Buffer) ?[]u8 { return switch (b.storage) { .shm => |shm| shm.memory, .dmabuf => null, }; }};
/// The errors `acquire` raises itself. It returns `anyerror` because a/// dma-buf pool dispatches while it waits for the compositor, and a listener/// dispatched then may return anything.pub const AcquireError = Connection.IoError || Session.RequestError || error{ /// The compositor has not said it accepts the format -- in shared memory, /// or as a dma-buf with any modifier -- or it has no single-plane layout /// a shared memory pool can allocate. UnsupportedFormat, /// Zero, or too large for the protocol's 32-bit sizes. InvalidSize, /// memfd_create, ftruncate or mmap failed. SystemResources, /// The dma-buf allocator failed; `allocator_error` says how. AllocationFailed, /// The allocator returned no planes, or more than four. InvalidAllocation, /// The compositor refused the dma-buf. Worth falling back from -- to /// another modifier, or to shared memory. BufferCreationFailed,};
/// A pool drawing its buffers from `shm`. `conn` and `shm` must outlive it.pub fn create(gpa: Allocator, conn: *Connection, shm: *Shm, options: Options) Allocator.Error!*BufferPool { const pool = try gpa.create(BufferPool); pool.* = .{ .gpa = gpa, .conn = conn, .backing = .{ .shm = shm }, .options = options }; return pool;}
/// A pool of dma-bufs from `allocator`, offered the modifiers `dmabuf` says/// the compositor accepts. `conn` and `dmabuf` must outlive it. Needs/// linux-dmabuf in the bindings, and is `void` without it.pub const createDmabuf = if (bindings.has_dmabuf) createDmabufImpl else {};
fn createDmabufImpl( gpa: Allocator, conn: *Connection, dmabuf: *Dmabuf, allocator: Dmabuf.BufferAllocator, options: Options,) Allocator.Error!*BufferPool { const pool = try gpa.create(BufferPool); pool.* = .{ .gpa = gpa, .conn = conn, .backing = .{ .dmabuf = .{ .dmabuf = dmabuf, .allocator = allocator } }, .options = options, }; return pool;}
/// Destroys every buffer, busy or not, and the pool. A buffer on screen stays/// on screen: the compositor keeps what it last showed.pub fn destroy(pool: *BufferPool) void { for (pool.buffers.items) |b| pool.free(b); pool.buffers.deinit(pool.gpa); pool.gpa.destroy(pool);}
/// A buffer of `width` by `height` in `format` that the compositor is not/// using, or null if every buffer is busy and no more may be made. What it/// holds is whatever was last drawn in it, or zeros if it is new.////// The buffer is the caller's until it is presented or given back with/// `cancel`.pub fn acquire(pool: *BufferPool, width: u32, height: u32, format: Format) anyerror!?*Buffer { switch (pool.backing) { .shm => |shm| if (format.bytesPerPixel() == null or !shm.supports(format)) return error.UnsupportedFormat, .dmabuf => |d| if (bindings.has_dmabuf) { if (!d.dmabuf.supports(format)) return error.UnsupportedFormat; } else unreachable, } if (width == 0 or height == 0 or width > std.math.maxInt(i32) or height > std.math.maxInt(i32)) return error.InvalidSize;
// Retire whatever is of another shape. var i: usize = 0; while (i < pool.buffers.items.len) { const b = pool.buffers.items[i]; if (b.width == width and b.height == height and b.format == format) { i += 1; continue; } if (b.state == .free) { pool.free(b); _ = pool.buffers.swapRemove(i); } else { b.stale = true; i += 1; } }
var current: usize = 0; for (pool.buffers.items) |b| { if (b.stale) continue; if (b.state == .free) { b.state = .acquired; return b; } current += 1; } if (current >= pool.options.max_buffers) return null;
try pool.buffers.ensureUnusedCapacity(pool.gpa, 1); const b = switch (pool.backing) { .shm => |shm| try pool.allocateShm(shm, width, height, format), .dmabuf => |d| if (bindings.has_dmabuf) try pool.allocateDmabuf(d, width, height, format) else unreachable, }; // Dispatching for a dma-buf may have run listeners that took buffers out // of the list, but none that put any in, so the room reserved is there. pool.buffers.appendAssumeCapacity(b); return b;}
/// Under explicit sync: the compositor has passed the release point `b` was/// presented with, so it is free again. The pool cannot see that happen/// itself, since it is a DRM syncobj rather than an event.pub fn released(pool: *BufferPool, b: *Buffer) void { std.debug.assert(b.pool == pool); if (b.state != .busy) return; b.state = .free; if (b.stale) pool.remove(b);}
/// Gives back a buffer from `acquire` without presenting it.pub fn cancel(pool: *BufferPool, b: *Buffer) void { std.debug.assert(b.pool == pool and b.state == .acquired); b.state = .free; if (b.stale) pool.remove(b);}
/// How many buffers exist, stale ones included.pub fn count(pool: *const BufferPool) usize { return pool.buffers.items.len;}
fn allocateShm(pool: *BufferPool, shm: *Shm, width: u32, height: u32, format: Format) AcquireError!*Buffer { const bpp = format.bytesPerPixel().?; // Rows start on a word boundary, which pixman -- and so most // compositors' software paths -- insists on for the 24-bit formats. const row = std.math.mul(u32, width, bpp) catch return error.InvalidSize; const stride = std.math.add(u32, row, 3) catch return error.InvalidSize; const aligned = stride & ~@as(u32, 3); const size = std.math.mul(u32, aligned, height) catch return error.InvalidSize; if (size > std.math.maxInt(i32) or width > std.math.maxInt(i32) or height > std.math.maxInt(i32)) return error.InvalidSize;
const rc = linux.memfd_create("wayland-shm", linux.MFD.CLOEXEC | linux.MFD.ALLOW_SEALING); if (std.posix.errno(rc) != .SUCCESS) return error.SystemResources; const fd: linux.fd_t = @intCast(rc); // The descriptor only has to live until the request carrying it is sent. defer _ = linux.close(fd); if (std.posix.errno(linux.ftruncate(fd, size)) != .SUCCESS) return error.SystemResources; // The compositor maps this too; sealing it against shrinking means no // client can make it read past the end. Best effort. _ = linux.fcntl(fd, linux.F.ADD_SEALS, linux.F.SEAL_SHRINK | linux.F.SEAL_SEAL);
const mapped = linux.mmap(null, size, .{ .READ = true, .WRITE = true }, .{ .TYPE = .SHARED }, fd, 0); if (std.posix.errno(mapped) != .SUCCESS) return error.SystemResources; const memory = @as([*]align(std.heap.page_size_min) u8, @ptrFromInt(mapped))[0..size]; errdefer _ = linux.munmap(memory.ptr, memory.len);
const b = try pool.gpa.create(Buffer); errdefer pool.gpa.destroy(b);
const s = &pool.conn.session; const shm_pool = try shm.shm.createPool(s, fd, @intCast(size)); const wl_buffer = try shm_pool.createBuffer(s, 0, @intCast(width), @intCast(height), @intCast(aligned), @enumFromInt(format.toShm())); try shm_pool.destroy(s); try pool.conn.flush();
b.* = .{ .pool = pool, .wl_buffer = wl_buffer, .width = width, .height = height, .stride = aligned, .format = format, .storage = .{ .shm = .{ .memory = memory } }, }; try pool.conn.setListener(wl_buffer, b, onBufferEvent); return b;}
const ParamsResult = union(enum) { pending, created: wl.Buffer, failed };
fn allocateDmabuf(pool: *BufferPool, d: DmabufBacking, width: u32, height: u32, format: Format) anyerror!*Buffer { const modifiers = try d.dmabuf.modifiers(pool.gpa, format); defer pool.gpa.free(modifiers); if (modifiers.len == 0) return error.UnsupportedFormat;
const allocation = d.allocator.allocate(d.allocator.context, width, height, format, modifiers) catch |e| { pool.allocator_error = e; return error.AllocationFailed; }; errdefer d.allocator.free(d.allocator.context, allocation.handle); if (allocation.plane_count == 0 or allocation.plane_count > 4) return error.InvalidAllocation;
const b = try pool.gpa.create(Buffer); errdefer pool.gpa.destroy(b);
const s = &pool.conn.session; const params = try d.dmabuf.object.createParams(s); var result: ParamsResult = .pending; try pool.conn.setListener(params, &result, onParamsEvent); // Whatever happens, the params object is finished with, and `result` // must not be written after this frame is gone. defer { pool.conn.clearListener(params) catch {}; params.destroy(s) catch {}; } const hi: u32 = @truncate(allocation.modifier >> 32); const lo: u32 = @truncate(allocation.modifier); for (allocation.planeSlice(), 0..) |plane, i| { try params.add(s, plane.fd, @intCast(i), plane.offset, plane.stride, hi, lo); } try params.create(s, @intCast(width), @intCast(height), @intFromEnum(format), .{}); try pool.conn.flush(); while (result == .pending) _ = try pool.conn.dispatch();
const wl_buffer = switch (result) { .pending => unreachable, .failed => return error.BufferCreationFailed, .created => |buffer| buffer, }; var planes: [4]Buffer.Plane = undefined; for (allocation.planeSlice(), 0..) |plane, i| planes[i] = .{ .fd = plane.fd, .offset = plane.offset, .stride = plane.stride }; b.* = .{ .pool = pool, .wl_buffer = wl_buffer, .width = width, .height = height, .stride = allocation.planes[0].stride, .format = format, .storage = .{ .dmabuf = .{ .handle = allocation.handle, .modifier = allocation.modifier, .planes = planes, .plane_count = allocation.plane_count, } }, }; try pool.conn.setListener(wl_buffer, b, onBufferEvent); return b;}
fn onParamsEvent( result: *ParamsResult, _: *Connection, _: if (bindings.has_dmabuf) protocols.zwp.LinuxBufferParamsV1 else void, event: if (bindings.has_dmabuf) protocols.zwp.LinuxBufferParamsV1.Event else void,) void { result.* = switch (event) { .created => |c| .{ .created = c.buffer }, .failed => .failed, };}
fn onBufferEvent(b: *Buffer, _: *Connection, _: wl.Buffer, event: wl.Buffer.Event) void { switch (event) { .release => { // Under explicit sync this event means nothing, and the buffer // may already be back in use; `released` is the word that counts. if (b.explicit_release or b.state != .busy) return; b.state = .free; if (b.stale) b.pool.remove(b); }, }}
/// Takes `b` out of the list and frees it.fn remove(pool: *BufferPool, b: *Buffer) void { const i = std.mem.findScalar(*Buffer, pool.buffers.items, b).?; _ = pool.buffers.swapRemove(i); pool.free(b);}
/// Destroys `b`'s `wl_buffer`, unmaps it and frees it, leaving the list alone.fn free(pool: *BufferPool, b: *Buffer) void { // A destructor request cannot fail for want of memory in a way worth // reporting here: the session is gone if it does. b.wl_buffer.destroy(&pool.conn.session) catch {}; switch (b.storage) { .shm => |shm| _ = linux.munmap(shm.memory.ptr, shm.memory.len), .dmabuf => |dmabuf| if (bindings.has_dmabuf) { const allocator = pool.backing.dmabuf.allocator; allocator.free(allocator.context, dmabuf.handle); } else unreachable, } pool.gpa.destroy(b);}