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.
9.4 kB · 233 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! What the compositor says about the dma-bufs it would like: which device//! it composites on, and which format and modifier pairs it accepts, in//! order of preference.//!//! This is `zwp_linux_dmabuf_feedback_v1` from version 4 of linux-dmabuf,//! for the default feedback or one surface's. The compositor sends a format//! table as a file descriptor -- sixteen bytes an entry, `u32` format, four//! bytes of padding, `u64` modifier -- then the main device, then one or more//! *tranches*, each a target device, some flags, and indices into the table.//! `done` ends a batch. Nothing here changes until `done`: a batch is//! assembled on the side and swapped in whole, so the fields always describe//! one consistent answer.//!//! The compositor may send a new batch at any time -- a window moved to//! another output, direct scanout became possible. Each one sets `changed`//! and calls `on_change`, and the consumer reallocates if it cares.//!//! For a compositor older than version 4 there is no feedback object, and//! `Dmabuf` fills one of these from the `format` and `modifier` events//! instead: a single tranche with no device, and `main_device` null, because//! that compositor never says which device it uses.
const std = @import("std");const linux = std.os.linux;const Allocator = std.mem.Allocator;const Connection = @import("client").Connection;const format_ = @import("format.zig");const Format = format_.Format;const bindings = @import("bindings.zig");const protocols = @import("wayland-protocols");
const Feedback = @This();
gpa: Allocator,conn: *Connection,/// Null when filled from a version 3 compositor's events.object: ?FeedbackObject,
/// The device the compositor composites on, as a `dev_t`. Null when the/// compositor has not said -- which a version 3 compositor never does.main_device: ?u64 = null,/// In the compositor's order of preference: the first tranche is the one it/// would most like buffers to match.tranches: std.ArrayList(Tranche) = .empty,/// How many batches have arrived. Zero until the first `done`.generation: u32 = 0,/// Set by each batch, for the consumer to clear once it has acted on it.changed: bool = false,/// Called after each batch.on_change: ?Hook = null,
// The batch being assembled.table: []FormatModifier = &.{},pending_main_device: ?u64 = null,pending_tranches: std.ArrayList(Tranche) = .empty,building: Building = .{},
const FeedbackObject = if (bindings.has_dmabuf) protocols.zwp.LinuxDmabufFeedbackV1 else void;
pub const FormatModifier = struct { format: Format, modifier: u64,};
pub const Tranche = struct { /// The device buffers matching this tranche should be allocated on, as /// a `dev_t`; null for a version 3 compositor's single tranche. target_device: ?u64, /// Buffers matching this tranche may be scanned out directly, without /// the compositor copying them. scanout: bool, /// In the compositor's order. pairs: []const FormatModifier,};
pub const Hook = struct { context: ?*anyopaque, call: *const fn (context: ?*anyopaque, feedback: *Feedback) void,};
const Building = struct { target_device: ?u64 = null, scanout: bool = false, indices: std.ArrayList(u16) = .empty,};
/// An empty feedback, listening to `object` if one is given.pub fn create(gpa: Allocator, conn: *Connection, object: ?FeedbackObject) !*Feedback { const f = try gpa.create(Feedback); errdefer gpa.destroy(f); f.* = .{ .gpa = gpa, .conn = conn, .object = object }; if (bindings.has_dmabuf) { if (object) |o| try conn.setListener(o, f, onEvent); } return f;}
/// Destroys the feedback object, if there is one, and frees everything.pub fn destroy(f: *Feedback) void { if (bindings.has_dmabuf) { if (f.object) |o| o.destroy(&f.conn.session) catch {}; } freeTranches(f.gpa, &f.tranches); freeTranches(f.gpa, &f.pending_tranches); f.building.indices.deinit(f.gpa); f.gpa.free(f.table); f.gpa.destroy(f);}
/// The modifiers the compositor accepts with `format`, in its order of/// preference and without repeats. Owned by the caller.pub fn modifiers(f: *const Feedback, gpa: Allocator, format: Format) Allocator.Error![]u64 { var list: std.ArrayList(u64) = .empty; errdefer list.deinit(gpa); for (f.tranches.items) |tranche| { for (tranche.pairs) |pair| { if (pair.format != format) continue; if (std.mem.findScalar(u64, list.items, pair.modifier) != null) continue; try list.append(gpa, pair.modifier); } } return list.toOwnedSlice(gpa);}
/// Whether the compositor accepts `format` with any modifier at all.pub fn supports(f: *const Feedback, format: Format) bool { for (f.tranches.items) |tranche| { for (tranche.pairs) |pair| if (pair.format == format) return true; } return false;}
/// Adds a pair to the single tranche of a version 3 compositor, which says/// what it accepts one event at a time with no batches.pub fn addLegacy(f: *Feedback, pair: FormatModifier) Allocator.Error!void { if (f.tranches.items.len == 0) try f.tranches.append(f.gpa, .{ .target_device = null, .scanout = false, .pairs = &.{} }); const tranche = &f.tranches.items[0]; for (tranche.pairs) |p| if (p.format == pair.format and p.modifier == pair.modifier) return; const pairs = try f.gpa.alloc(FormatModifier, tranche.pairs.len + 1); @memcpy(pairs[0..tranche.pairs.len], tranche.pairs); pairs[tranche.pairs.len] = pair; f.gpa.free(tranche.pairs); tranche.pairs = pairs; f.changed = true;}
fn freeTranches(gpa: Allocator, list: *std.ArrayList(Tranche)) void { for (list.items) |t| gpa.free(t.pairs); list.deinit(gpa);}
/// A `dev_t` from the array the protocol carries it in, which is exactly/// `sizeof(dev_t)` bytes -- eight on Linux. Anything else is not one.fn devT(bytes: []const u8) ?u64 { if (bytes.len != @sizeOf(u64)) return null; return std.mem.readInt(u64, bytes[0..8], @import("builtin").cpu.arch.endian());}
const EventUnion = if (bindings.has_dmabuf) protocols.zwp.LinuxDmabufFeedbackV1.Event else void;
fn onEvent(f: *Feedback, _: *Connection, _: FeedbackObject, event: EventUnion) !void { switch (event) { .format_table => |t| { defer _ = linux.close(t.fd); try f.readTable(t.fd, t.size); }, .main_device => |d| f.pending_main_device = devT(d.device), .tranche_target_device => |d| f.building.target_device = devT(d.device), .tranche_flags => |flags| f.building.scanout = flags.flags.scanout, .tranche_formats => |formats| { const count = formats.indices.len / 2; try f.building.indices.ensureUnusedCapacity(f.gpa, count); for (0..count) |i| { f.building.indices.appendAssumeCapacity(std.mem.readInt(u16, formats.indices[i * 2 ..][0..2], @import("builtin").cpu.arch.endian())); } }, .tranche_done => { // Indices past the end of the table are the compositor's mistake, // and are dropped rather than trusted. var pairs: std.ArrayList(FormatModifier) = .empty; errdefer pairs.deinit(f.gpa); for (f.building.indices.items) |i| { if (i < f.table.len) try pairs.append(f.gpa, f.table[i]); } try f.pending_tranches.ensureUnusedCapacity(f.gpa, 1); f.pending_tranches.appendAssumeCapacity(.{ .target_device = f.building.target_device, .scanout = f.building.scanout, .pairs = try pairs.toOwnedSlice(f.gpa), }); f.building.indices.clearRetainingCapacity(); f.building.target_device = null; f.building.scanout = false; }, .done => { freeTranches(f.gpa, &f.tranches); f.tranches = f.pending_tranches; f.pending_tranches = .empty; f.main_device = f.pending_main_device; f.generation += 1; f.changed = true; if (f.on_change) |hook| hook.call(hook.context, f); }, }}
/// Copies the format table out of the file the compositor sent. A table/// that is not a whole number of entries is read as far as it goes.fn readTable(f: *Feedback, fd: linux.fd_t, size: u32) !void { const entry_size = 16; const count = size / entry_size; const table = try f.gpa.alloc(FormatModifier, count); errdefer f.gpa.free(table); if (count > 0) { const mapped = linux.mmap(null, size, .{ .READ = true }, .{ .TYPE = .PRIVATE }, fd, 0); if (std.posix.errno(mapped) != .SUCCESS) return error.FormatTableUnreadable; const bytes = @as([*]const u8, @ptrFromInt(mapped))[0..size]; defer _ = linux.munmap(bytes.ptr, bytes.len); const endian = @import("builtin").cpu.arch.endian(); for (table, 0..) |*entry, i| { const raw = bytes[i * entry_size ..][0..entry_size]; entry.* = .{ .format = @enumFromInt(std.mem.readInt(u32, raw[0..4], endian)), .modifier = std.mem.readInt(u64, raw[8..16], endian), }; } } f.gpa.free(f.table); f.table = table;}