Implement wallpaper rendering with multi-output support

This actually renders a wallpaper for each output using the newly added
Buffer and BufferPool for shared-memory surfaces and creates a
wlr-layer-shell surface per output. Right now, each wallpaper
shares the same wallpaper (though scaled to each).

wl_output globals get added to a HashMap that is used by Output when it
gets an output event.

Fix null-safety in WindowManager when no seats/outputs exist and route
Window dimensions through pending_manage.
This commit is contained in:
Ben Buhse 2026-02-07 17:27:24 -06:00
commit e186a2d017
No known key found for this signature in database
GPG key ID: 7916ACFCD38FD0B4
9 changed files with 568 additions and 35 deletions

View file

@ -10,6 +10,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
These are in rough order of my priority, though no promises I do them in this order. These are in rough order of my priority, though no promises I do them in this order.
- [ ] Switch all structs to idiomatic Zig init/deinit pattern (init returns value, caller decides stack/heap)
- [ ] Support per-host config using properties
- [ ] Support wallpapers - [ ] Support wallpapers
- [ ] Support a bar - [ ] Support a bar
- [ ] Support starting programs at WM launch - [ ] Support starting programs at WM launch

View file

@ -111,6 +111,6 @@ pub fn build(b: *std.Build) void {
exe_check.linkSystemLibrary("pixman-1"); exe_check.linkSystemLibrary("pixman-1");
exe_check.linkSystemLibrary("xkbcommon"); exe_check.linkSystemLibrary("xkbcommon");
const check = b.step("check", "Check if beanbag compiles"); const check = b.step("check", "Check if beansprout compiles");
check.dependOn(&exe_check.step); check.dependOn(&exe_check.step);
} }

128
src/Buffer.zig Normal file
View file

@ -0,0 +1,128 @@
// SPDX-FileCopyrightText: 2026 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: GPL-3.0-or-later
const Buffer = @This();
width: u31,
height: u31,
stride: u31,
busy: bool,
size: u31,
data: ?[]align(std.heap.page_size_min) u8,
wl_buffer: *wl.Buffer,
pixman_image: *pixman.Image,
/// Used to add Buffers to a BufferPool
node: std.DoublyLinkedList.Node = .{},
pub fn init(shm: *wl.Shm, width: u31, height: u31) !Buffer {
// We use argb8888
const stride = width * 4;
const size: u31 = height * stride;
log.debug("initializing a new buffer with size {d}", .{size});
// Open a memory-backed file with sealing enabled
const fd = switch (builtin.target.os.tag) {
.linux => try posix.memfd_createZ("beansprout-shm-buffer", os.linux.MFD.CLOEXEC | os.linux.MFD.ALLOW_SEALING),
.freebsd => try posix.memfd_createZ("beansprout-shm-buffer", std.c.MFD.CLOEXEC | std.c.MFD.ALLOW_SEALING),
else => @compileError("target OS not supported"),
};
defer posix.close(fd);
// Try to allocate it to the desired size
try posix.ftruncate(fd, size);
// mmap the memory file for the pixman image
const data = mem.bytesAsSlice(
u8,
try posix.mmap(null, size, posix.PROT.READ | posix.PROT.WRITE, .{ .TYPE = .SHARED }, fd, 0),
);
errdefer posix.munmap(data);
// Seal the fd to prevent size changes. The compositor maps the same fd,
// so without sealing it could access invalid memory if the client resized it.
_ = try posix.fcntl(fd, seal.F_ADD_SEALS, seal.SEAL_GROW | seal.SEAL_SHRINK | seal.SEAL_SEAL);
// Create a Wayland shm buffer for the same memory file.
const pool = try shm.createPool(fd, size);
defer pool.destroy();
const wl_buffer = try pool.createBuffer(0, width, height, stride, .argb8888);
errdefer wl_buffer.destroy();
// Create the pixman image.
const pixman_image = pixman.Image.createBitsNoClear(
.a8r8g8b8,
@as(c_int, @intCast(width)),
@as(c_int, @intCast(height)),
@as([*c]u32, @ptrCast(data)),
@as(c_int, @intCast(stride)),
) orelse return error.NoPixmanImage;
// The pixman image and the Wayland buffer now share the same memory.
return .{
.width = width,
.height = height,
.stride = stride,
.busy = true,
.size = size,
.wl_buffer = wl_buffer,
.data = data,
.pixman_image = pixman_image,
};
}
pub fn deinit(buffer: *Buffer) void {
_ = buffer.pixman_image.unref();
buffer.wl_buffer.destroy();
if (buffer.data) |data| posix.munmap(data);
}
// We have to do this later because of the way init() works
pub fn setListener(buffer: *Buffer) void {
buffer.wl_buffer.setListener(*Buffer, buffer_listener, buffer);
}
fn buffer_listener(_: *wl.Buffer, event: wl.Buffer.Event, buffer: *Buffer) void {
switch (event) {
.release => buffer.busy = false,
}
}
const std = @import("std");
const builtin = @import("builtin");
const mem = std.mem;
const os = std.os;
const posix = std.posix;
const wayland = @import("wayland");
const wl = wayland.client.wl;
const pixman = @import("pixman");
const utils = @import("utils.zig");
/// Sealing constants for memfd. Prevents the compositor from accessing
/// invalid memory by locking the fd's size after setup.
const seal = switch (builtin.target.os.tag) {
.linux => struct {
// Linux values are missing from stdlib right now,
// just take the values from fcntl.h
const F_ADD_SEALS: i32 = 1033;
const SEAL_SEAL: usize = 0x0001;
const SEAL_SHRINK: usize = 0x0002;
const SEAL_GROW: usize = 0x0004;
},
.freebsd => struct {
const F_ADD_SEALS = std.c.F.ADD_SEALS;
const SEAL_SEAL = std.c.F.SEAL_SEAL;
const SEAL_SHRINK = std.c.F.SEAL_SHRINK;
const SEAL_GROW = std.c.F.SEAL_GROW;
},
else => @compileError("target OS not supported"),
};
const log = std.log.scoped(.Buffer);

122
src/BufferPool.zig Normal file
View file

@ -0,0 +1,122 @@
// SPDX-FileCopyrightText: 2026 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: GPL-3.0-or-later
// Borrowed and adapted from https://git.sr.ht/~leon_plickat/wayprompt
const BufferPool = @This();
/// The amount of buffers per surface we consider the reasonable upper limit.
/// Some compositors sometimes tripple-buffer, so three seems to be ok.
/// Note that we can absolutely work with higher buffer numbers if needed,
/// however we consider that to be an anomaly and therefore do not want to
/// keep all those extra buffers around if we can avoid it, as to not have
/// unecessary memory overhead.
const max_buffer_multiplicity = 3;
/// The buffers. This is a linked list and not an array list, because we
/// need stable pointers for the listener of the wl_buffer object.
buffers: DoublyLinkedList = .{},
len: usize = 0,
/// Number of surfaces sharing this pool, used to determine when to cull extra buffers.
/// Each surface is allowed up to max_buffer_multiplicity buffers.
surface_count: usize = 0,
/// Deinit the buffer pool, destroying all buffers and freeing all memory.
pub fn deinit(buffer_pool: *BufferPool) void {
var it = buffer_pool.buffers.first;
while (it) |node| {
// Advance before destroying, since node is embedded in buffer
it = node.next;
const buffer: *Buffer = @fieldParentPtr("node", node);
buffer.deinit();
utils.allocator.destroy(buffer);
}
}
/// Get a buffer with the specified dimensions. If possible, an idle buffer is
/// reused, otherwise a new one is created.
pub fn nextBuffer(buffer_pool: *BufferPool, wl_shm: *wl.Shm, width: u31, height: u31) !*Buffer {
log.debug("looking for buffer with dimensions {}x{}, total existing buffers: {}", .{ width, height, buffer_pool.len });
defer {
// Clear up extra buffers
if (buffer_pool.len > max_buffer_multiplicity * buffer_pool.surface_count) {
buffer_pool.cullBuffers();
}
}
if (try buffer_pool.findSuitableBuffer(wl_shm, width, height)) |buffer| {
return buffer;
} else {
return try buffer_pool.newBuffer(wl_shm, width, height);
}
}
/// Get the first free buffer with the specified dimensions.
/// If there are no free buffers with the right dimensions, re-init a free buffer that
/// has other dimensions. If no free buffer exists at all, return null.
fn findSuitableBuffer(buffer_pool: *BufferPool, wl_shm: *wl.Shm, width: u31, height: u31) !?*Buffer {
var it = buffer_pool.buffers.first;
var first_unbusy_buffer: ?*Buffer = null;
while (it) |node| : (it = node.next) {
const buffer: *Buffer = @fieldParentPtr("node", node);
if (buffer.busy) continue;
if (buffer.width == width and buffer.height == height) {
return buffer;
} else {
first_unbusy_buffer = buffer;
}
}
// No buffer has matching dimensions, however we do have an unbusy
// buffer which we can just re-init.
if (first_unbusy_buffer) |buffer| {
buffer.deinit();
buffer.* = try Buffer.init(wl_shm, width, height);
return buffer;
}
return null;
}
fn newBuffer(buffer_pool: *BufferPool, wl_shm: *wl.Shm, width: u31, height: u31) !*Buffer {
log.debug("creating new buffer {}x{}", .{ width, height });
const buffer = try utils.allocator.create(Buffer);
errdefer utils.allocator.destroy(buffer);
buffer.* = try Buffer.init(wl_shm, width, height);
buffer.setListener();
buffer_pool.buffers.append(&buffer.node);
buffer_pool.len += 1;
return buffer;
}
fn cullBuffers(buffer_pool: *BufferPool) void {
log.debug("culling extra buffers", .{});
var overhead = buffer_pool.len - max_buffer_multiplicity * buffer_pool.surface_count;
var it = buffer_pool.buffers.first;
while (it) |node| {
if (overhead == 0) break;
// Advance before destroying, since node is embedded in buffer
it = node.next;
const buffer: *Buffer = @fieldParentPtr("node", node);
if (!buffer.busy) {
buffer.deinit();
buffer_pool.buffers.remove(node);
utils.allocator.destroy(buffer);
buffer_pool.len -= 1;
overhead -= 1;
}
}
log.debug(" -> new buffer count: {}", .{buffer_pool.len});
}
const std = @import("std");
const DoublyLinkedList = std.DoublyLinkedList;
const wayland = @import("wayland");
const wl = wayland.client.wl;
const Buffer = @import("Buffer.zig");
const utils = @import("utils.zig");
const log = std.log.scoped(.BufferPool);

View file

@ -12,15 +12,23 @@ initialized: bool,
// Wayland globals // Wayland globals
wl_compositor: *wl.Compositor, wl_compositor: *wl.Compositor,
wl_display: *wl.Display, wl_display: *wl.Display,
wl_output: *wl.Output,
wl_registry: *wl.Registry, wl_registry: *wl.Registry,
wl_shm: *wl.Shm, wl_shm: *wl.Shm,
wl_outputs: *std.AutoHashMapUnmanaged(u32, *wl.Output),
zwlr_layer_shell_v1: *zwlr.LayerShellV1,
// Wayland globals that we have special structs for // Wayland globals that we have special structs for
wallpaper_image: *WallpaperImage,
wm: *WindowManager, wm: *WindowManager,
xkb_bindings: *XkbBindings, xkb_bindings: *XkbBindings,
/// Pool of Buffers used for rendering wallpapers
buffer_pool: BufferPool = .{},
/// Holds a pixman.Image (and its raw pixels) for the wallpaper
/// (same image on all outputs, but scaled separately)
wallpaper_image: *WallpaperImage,
// WM Configuration // WM Configuration
config: *Config, config: *Config,
@ -36,12 +44,14 @@ pub const PendingManage = struct {
pub const Options = struct { pub const Options = struct {
wl_compositor: *wl.Compositor, wl_compositor: *wl.Compositor,
wl_display: *wl.Display, wl_display: *wl.Display,
wl_output: *wl.Output,
wl_registry: *wl.Registry, wl_registry: *wl.Registry,
wl_shm: *wl.Shm, wl_shm: *wl.Shm,
river_layer_shell_v1: *river.LayerShellV1, wl_outputs: *std.AutoHashMapUnmanaged(u32, *wl.Output),
river_layer_shell_v1: *river.LayerShellV1, // TODO
river_window_manager_v1: *river.WindowManagerV1, river_window_manager_v1: *river.WindowManagerV1,
river_xkb_bindings_v1: *river.XkbBindingsV1, river_xkb_bindings_v1: *river.XkbBindingsV1,
zwlr_layer_shell_v1: *zwlr.LayerShellV1, zwlr_layer_shell_v1: *zwlr.LayerShellV1,
config: *Config, config: *Config,
}; };
@ -54,9 +64,10 @@ pub fn create(options: Options) !*Context {
.initialized = false, .initialized = false,
.wl_compositor = options.wl_compositor, .wl_compositor = options.wl_compositor,
.wl_display = options.wl_display, .wl_display = options.wl_display,
.wl_output = options.wl_output,
.wl_registry = options.wl_registry, .wl_registry = options.wl_registry,
.wl_shm = options.wl_shm, .wl_shm = options.wl_shm,
.wl_outputs = options.wl_outputs,
.zwlr_layer_shell_v1 = options.zwlr_layer_shell_v1,
.wallpaper_image = try WallpaperImage.create("FIXME"), // FIXME: TODO: Get this from Config .wallpaper_image = try WallpaperImage.create("FIXME"), // FIXME: TODO: Get this from Config
.wm = try WindowManager.create(context, options.river_window_manager_v1), .wm = try WindowManager.create(context, options.river_window_manager_v1),
.xkb_bindings = try XkbBindings.create(context, options.river_xkb_bindings_v1), .xkb_bindings = try XkbBindings.create(context, options.river_xkb_bindings_v1),
@ -70,6 +81,9 @@ pub fn destroy(context: *Context) void {
context.xkb_bindings.destroy(); context.xkb_bindings.destroy();
context.wm.destroy(); context.wm.destroy();
context.wallpaper_image.destroy();
context.buffer_pool.deinit();
utils.allocator.destroy(context); utils.allocator.destroy(context);
} }
@ -98,6 +112,7 @@ const zwlr = wayland.client.zwlr;
const utils = @import("utils.zig"); const utils = @import("utils.zig");
const Config = @import("Config.zig"); const Config = @import("Config.zig");
const BufferPool = @import("BufferPool.zig");
const WallpaperImage = @import("WallpaperImage.zig"); const WallpaperImage = @import("WallpaperImage.zig");
const WindowManager = @import("WindowManager.zig"); const WindowManager = @import("WindowManager.zig");
const XkbBindings = @import("XkbBindings.zig"); const XkbBindings = @import("XkbBindings.zig");

View file

@ -8,11 +8,22 @@ context: *Context,
river_output_v1: *river.OutputV1, river_output_v1: *river.OutputV1,
width: i32 = 0, // We have to wait for the rwm.wl_output event to get this
height: i32 = 0, wl_output: ?*wl.Output = null,
// Output geometry
scale: u31 = 0,
width: u31 = 0,
height: u31 = 0,
x: i32 = 0, x: i32 = 0,
y: i32 = 0, y: i32 = 0,
// Information for this Output's wallpaper
render_width: u31 = 0,
render_height: u31 = 0,
wl_surface: ?*wl.Surface = null,
layer_surface: ?*zwlr.LayerSurfaceV1 = null,
/// Proportion of output width taken by the primary stack /// Proportion of output width taken by the primary stack
primary_ratio: f32 = 0.55, primary_ratio: f32 = 0.55,
@ -25,13 +36,19 @@ tags: u32 = 0x0001,
/// State consumed in manage() phase, reset at end of manage(). /// State consumed in manage() phase, reset at end of manage().
pending_manage: PendingManage = .{}, pending_manage: PendingManage = .{},
// Friendly name of this output
name: ?[]const u8 = null,
/// Used for wallpaper rendering management
configured: bool = false,
windows: wl.list.Head(Window, .link), windows: wl.list.Head(Window, .link),
link: wl.list.Link, link: wl.list.Link,
pub const PendingManage = struct { pub const PendingManage = struct {
width: ?i32 = null, width: ?u31 = null,
height: ?i32 = null, height: ?u31 = null,
x: ?i32 = null, x: ?i32 = null,
y: ?i32 = null, y: ?i32 = null,
@ -53,7 +70,7 @@ pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output {
output.windows.init(); output.windows.init();
output.river_output_v1.setListener(*Output, outputListener, output); output.river_output_v1.setListener(*Output, riverOutputListener, output);
return output; return output;
} }
@ -65,6 +82,7 @@ pub fn destroy(output: *Output) void {
window.destroy(); window.destroy();
} }
output.deinitWallpaperLayerSurface();
output.river_output_v1.destroy(); output.river_output_v1.destroy();
utils.allocator.destroy(output); utils.allocator.destroy(output);
} }
@ -85,7 +103,8 @@ pub fn prevWindow(output: *Output, current: *Window) ?*Window {
return @fieldParentPtr("link", prev_link); return @fieldParentPtr("link", prev_link);
} }
fn outputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.Event, output: *Output) void { // Used for the river_output_v1 interface
fn riverOutputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.Event, output: *Output) void {
assert(output.river_output_v1 == river_output_v1); assert(output.river_output_v1 == river_output_v1);
switch (event) { switch (event) {
.removed => { .removed => {
@ -136,21 +155,220 @@ fn outputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.Event,
output.destroy(); output.destroy();
}, },
.wl_output => |ev| { .wl_output => |ev| {
log.debug("initializing new river_output_v1 corresponding to wl_output: {d}", .{ev.name}); // It's guaranteed for the wl_output global to advertised before this event happens
output.wl_output = output.context.wl_outputs.get(ev.name) orelse unreachable;
output.wl_output.?.setListener(*Output, wlOutputListener, output);
}, },
.dimensions => |ev| { .dimensions => |ev| {
output.pending_manage.width = ev.width; // Protocol guarantees that width and height are strictly greater than zero
output.pending_manage.height = ev.height; assert(ev.width > 0 and ev.height > 0);
output.context.wm.river_window_manager_v1.manageDirty(); output.pending_manage.width = @intCast(ev.width);
output.pending_manage.height = @intCast(ev.height);
}, },
.position => |ev| { .position => |ev| {
output.pending_manage.x = ev.x; output.pending_manage.x = ev.x;
output.pending_manage.y = ev.y; output.pending_manage.y = ev.y;
output.context.wm.river_window_manager_v1.manageDirty();
}, },
} }
} }
// Used for the wl_output global interface that corresponds to the river_output_v1
fn wlOutputListener(_: *wl.Output, event: wl.Output.Event, output: *Output) void {
switch (event) {
.mode => |ev| {
if (ev.width < 0 or ev.height < 0) {
// I'm not actually sure if this is possible, but just to be safe
log.warn("Received wl_output.mode event with a negative width or height ({d}x{d})", .{ ev.width, ev.height });
return;
}
output.width = @intCast(ev.width);
output.height = @intCast(ev.height);
},
.done => {
output.initWallpaperLayerSurface() catch |err| {
const output_name = output.name orelse "some output";
log.err("failed to add a surface to {s}: {}", .{ output_name, err });
return;
};
},
.scale => |ev| {
if (ev.factor < 0) {
// I'm not actually sure if this is possible, but just to be safe
log.warn("Received wl_output.scale event with a negative factor ({d})", .{ev.factor});
return;
}
output.scale = @intCast(ev.factor);
},
.name => |ev| {
output.name = utils.allocator.dupe(u8, mem.span(ev.name)) catch @panic("Out of memory");
},
else => {},
}
}
fn initWallpaperLayerSurface(output: *Output) !void {
if (output.wl_surface) |_| {
log.warn("Skipping adding a second wallpaper surface to {s}", .{output.name orelse "some output"});
return;
}
const context = output.context;
const wl_surface: *wl.Surface = try context.wl_compositor.createSurface();
// We don't want our surface to have any input region (default is infinite)
const empty_region: *wl.Region = try context.wl_compositor.createRegion();
defer empty_region.destroy();
wl_surface.setInputRegion(empty_region);
// Full surface should be opaque
const opaque_region: *wl.Region = try context.wl_compositor.createRegion();
defer opaque_region.destroy();
wl_surface.setOpaqueRegion(opaque_region);
const layer_surface: *zwlr.LayerSurfaceV1 = try context.zwlr_layer_shell_v1.getLayerSurface(wl_surface, output.wl_output, .background, "beansprout");
layer_surface.setExclusiveZone(-1);
layer_surface.setAnchor(.{ .top = true, .right = true, .bottom = true, .left = true });
output.wl_surface = wl_surface;
output.layer_surface = layer_surface;
context.buffer_pool.surface_count += 1;
layer_surface.setListener(*Output, wallpaperLayerSurfaceListener, output);
wl_surface.commit();
}
fn deinitWallpaperLayerSurface(output: *Output) void {
if (output.layer_surface) |layer_surface| {
layer_surface.destroy();
}
if (output.wl_surface) |wl_surface| {
wl_surface.destroy();
}
output.layer_surface = null;
output.wl_surface = null;
output.configured = false;
output.context.buffer_pool.surface_count -= 1;
}
fn wallpaperLayerSurfaceListener(layer_surface: *zwlr.LayerSurfaceV1, event: zwlr.LayerSurfaceV1.Event, output: *Output) void {
switch (event) {
.configure => |ev| {
layer_surface.ackConfigure(ev.serial);
if (ev.width < 0 or ev.height < 0) {
// I'm not actually sure if this is possible, but just to be safe
log.warn("Received zwlr_layer_surface_v1.configure event with a negative width or height ({d}x{d})", .{ ev.width, ev.height });
return;
}
const width: u31 = @intCast(ev.width);
const height: u31 = @intCast(ev.height);
if (output.configured and output.render_width == width and output.render_height == height) {
if (output.wl_surface) |wl_surface| {
wl_surface.commit();
} else {
log.warn("Output is marked as configured but is missing a layer_surface for the wallpaper", .{});
}
return;
}
log.debug("configuring wallpaper surface with width {} and height {}", .{ width, height });
output.render_width = width;
output.render_height = height;
output.configured = true;
output.renderWallpaper() catch |err| {
fatal("Wallpaper render failed: E{}", .{err});
};
},
.closed => {
output.deinitWallpaperLayerSurface();
},
}
}
/// Calculates image_dimension / (output_dimension * scale)
fn calculate_scale(image_dimension: c_int, output_dimension: u31, scale: u31) f64 {
const numerator: f64 = @floatFromInt(image_dimension);
const denominator: f64 = @floatFromInt(output_dimension * scale);
return numerator / denominator;
}
/// Calculates (image_dimension / dimension_scale - output_dimension) / 2 / dimension_scale;
fn calculate_transform(image_dimension: c_int, output_dimension: u31, dimension_scale: f64) f64 {
const numerator1: f64 = @floatFromInt(image_dimension);
const denominator1: f64 = dimension_scale;
const subtruend: f64 = @floatFromInt(output_dimension);
const numerator2: f64 = numerator1 / denominator1 - subtruend;
return numerator2 / 2 / dimension_scale;
}
/// Render the wallpaper image onto the layer surface
fn renderWallpaper(output: *Output) !void {
const context = output.context;
const width = output.render_width;
const height = output.render_height;
const scale = output.scale;
// Don't have anything to render
if (width == 0 or height == 0 or scale == 0) {
return;
}
const buffer: *Buffer = try context.buffer_pool.nextBuffer(context.wl_shm, width * scale, height * scale);
// Scale our loaded image and then copy it into the Buffer's pixman.Image
const image = context.wallpaper_image.image;
const image_data = image.getData();
const image_width = image.getWidth();
const image_height = image.getHeight();
const image_stride = image.getStride();
const image_format = image.getFormat();
const pix = pixman.Image.createBitsNoClear(image_format, image_width, image_height, image_data, image_stride);
if (pix == null) {
log.err("failed to copy the background image for rendering", .{});
return error.ImageCopyError;
}
defer _ = pix.?.unref();
// Get scale for our image compared to the monitor's scale
// XXX: This sucks in Zig but also I'm sure there's a better way to write it
var sx: f64 = @as(f64, @floatFromInt(image_width)) / @as(f64, @floatFromInt(width * scale));
var sy: f64 = calculate_scale(image_height, height, scale);
const s = if (sx > sy) sy else sx;
sx = s;
sy = s;
const tx: f64 = calculate_transform(image_width, width, sx);
const ty: f64 = calculate_transform(image_height, height, sy);
var t: pixman.FTransform = undefined;
var t2: pixman.Transform = undefined;
pixman.FTransform.initTranslate(&t, tx, ty);
pixman.FTransform.initScale(&t, sx, sy);
_ = pixman.Transform.fromFTransform(&t2, &t);
_ = pix.?.setTransform(&t2);
_ = pix.?.setFilter(.best, &[_]pixman.Fixed{}, 0);
pixman.Image.composite32(.src, pix.?, null, buffer.pixman_image, 0, 0, 0, 0, 0, 0, width * scale, height * scale);
log.info("render: {}x{} (scaled from {}x{})", .{ width * scale, height * scale, image_width, image_height });
// Attach the buffer to the surface
const wl_surface = output.wl_surface.?;
wl_surface.setBufferScale(scale);
wl_surface.attach(buffer.wl_buffer, 0, 0);
wl_surface.damageBuffer(0, 0, width * scale, height * scale);
wl_surface.commit();
}
pub fn manage(output: *Output) void { pub fn manage(output: *Output) void {
defer output.pending_manage = .{}; defer output.pending_manage = .{};
@ -316,13 +534,18 @@ fn calculatePrimaryStackLayout(output: *Output) void {
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const fatal = std.process.fatal;
const mem = std.mem;
const DoublyLinkedList = std.DoublyLinkedList; const DoublyLinkedList = std.DoublyLinkedList;
const wayland = @import("wayland"); const wayland = @import("wayland");
const wl = wayland.client.wl; const wl = wayland.client.wl;
const river = wayland.client.river; const river = wayland.client.river;
const zwlr = wayland.client.zwlr;
const pixman = @import("pixman");
const utils = @import("utils.zig"); const utils = @import("utils.zig");
const Buffer = @import("Buffer.zig");
const Context = @import("Context.zig"); const Context = @import("Context.zig");
const Window = @import("Window.zig"); const Window = @import("Window.zig");

View file

@ -35,8 +35,7 @@ pending_manage: PendingManage = .{},
/// State consumed in render() phase, reset at end of render(). /// State consumed in render() phase, reset at end of render().
pending_render: PendingRender = .{}, pending_render: PendingRender = .{},
/// Used to put Windows into a list in /// Used to put Windows into a list in calculatePrimaryStackLayout()
/// WindowManager.calculatePrimaryStackLayout()
active_list_node: DoublyLinkedList.Node = .{}, active_list_node: DoublyLinkedList.Node = .{},
link: wl.list.Link, link: wl.list.Link,
@ -141,10 +140,10 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event,
window.destroy(); window.destroy();
}, },
.dimensions => |ev| { .dimensions => |ev| {
// The protocol requires these are strictly greater than zero. // Protocol guarantees that width and height are strictly greater than zero
assert(ev.width > 0 and ev.height > 0); assert(ev.width > 0 and ev.height > 0);
window.width = @intCast(ev.width); window.pending_manage.width = @intCast(ev.width);
window.height = @intCast(ev.height); window.pending_manage.height = @intCast(ev.height);
}, },
.dimensions_hint => { .dimensions_hint => {
// TODO: Maybe could use this for floating windows // TODO: Maybe could use this for floating windows

View file

@ -179,10 +179,12 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv
const output = Output.create(context, ev.id) catch @panic("Out of memory"); const output = Output.create(context, ev.id) catch @panic("Out of memory");
wm.outputs.append(output); wm.outputs.append(output);
// If there was already a seat, but no outputs, set this new output as focused // If there was already a seat, but no outputs, set this new output as focused
const seat = wm.seats.first() orelse return; const first_seat = wm.seats.first();
if (first_seat) |seat| {
if (seat.focused_output == null and seat.pending_manage.output == null) { if (seat.focused_output == null and seat.pending_manage.output == null) {
seat.pending_manage.output = .{ .output = output }; seat.pending_manage.output = .{ .output = output };
} }
}
// If there are orphan windows, send them to the new output // If there are orphan windows, send them to the new output
var it = wm.orphan_windows.iterator(.forward); var it = wm.orphan_windows.iterator(.forward);
@ -210,7 +212,9 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv
wm.seats.append(seat); wm.seats.append(seat);
// If there was already an output, but no seats, set the first output as focused // If there was already an output, but no seats, set the first output as focused
seat.pending_manage.output = .{ .output = wm.outputs.first() orelse return }; if (wm.outputs.first()) |output| {
seat.pending_manage.output = .{ .output = output };
}
}, },
.window => |ev| { .window => |ev| {
// TODO: Support multiple seats // TODO: Support multiple seats

View file

@ -7,10 +7,20 @@ const Globals = struct {
river_layer_shell_v1: ?*river.LayerShellV1 = null, river_layer_shell_v1: ?*river.LayerShellV1 = null,
river_window_manager_v1: ?*river.WindowManagerV1 = null, river_window_manager_v1: ?*river.WindowManagerV1 = null,
river_xkb_bindings_v1: ?*river.XkbBindingsV1 = null, river_xkb_bindings_v1: ?*river.XkbBindingsV1 = null,
wl_compositor: ?*wl.Compositor = null, wl_compositor: ?*wl.Compositor = null,
wl_output: ?*wl.Output = null,
wl_shm: ?*wl.Shm = null, wl_shm: ?*wl.Shm = null,
wl_outputs: std.AutoHashMapUnmanaged(u32, *wl.Output) = .empty,
zwlr_layer_shell_v1: ?*zwlr.LayerShellV1 = null, zwlr_layer_shell_v1: ?*zwlr.LayerShellV1 = null,
fn deinit(globals: *Globals) void {
var it = globals.wl_outputs.valueIterator();
while (it.next()) |output| {
output.*.release();
}
globals.wl_outputs.deinit(utils.allocator);
}
}; };
pub fn main() !void { pub fn main() !void {
@ -27,6 +37,7 @@ pub fn main() !void {
const wl_registry = try wl_display.getRegistry(); const wl_registry = try wl_display.getRegistry();
var globals: Globals = .{}; var globals: Globals = .{};
defer globals.deinit();
wl_registry.setListener(*Globals, registryListener, &globals); wl_registry.setListener(*Globals, registryListener, &globals);
const errno = wl_display.roundtrip(); const errno = wl_display.roundtrip();
@ -34,12 +45,15 @@ pub fn main() !void {
fatal("Initial roundtrip failed: E{s}", .{@tagName(errno)}); fatal("Initial roundtrip failed: E{s}", .{@tagName(errno)});
} }
const wl_compositor = globals.wl_compositor orelse utils.interfaceNotAdvertised(wl.Compositor);
const wl_shm = globals.wl_shm orelse utils.interfaceNotAdvertised(wl.Shm);
// We can theoretically start with zero wl_outputs; don't panic if it's empty.
const wl_outputs = &globals.wl_outputs;
const river_layer_shell_v1 = globals.river_layer_shell_v1 orelse utils.interfaceNotAdvertised(river.LayerShellV1); const river_layer_shell_v1 = globals.river_layer_shell_v1 orelse utils.interfaceNotAdvertised(river.LayerShellV1);
const river_window_manager_v1 = globals.river_window_manager_v1 orelse utils.interfaceNotAdvertised(river.WindowManagerV1); const river_window_manager_v1 = globals.river_window_manager_v1 orelse utils.interfaceNotAdvertised(river.WindowManagerV1);
const river_xkb_bindings_v1 = globals.river_xkb_bindings_v1 orelse utils.interfaceNotAdvertised(river.XkbBindingsV1); const river_xkb_bindings_v1 = globals.river_xkb_bindings_v1 orelse utils.interfaceNotAdvertised(river.XkbBindingsV1);
const wl_compositor = globals.wl_compositor orelse utils.interfaceNotAdvertised(wl.Compositor);
const wl_output = globals.wl_output orelse utils.interfaceNotAdvertised(wl.Output);
const wl_shm = globals.wl_shm orelse utils.interfaceNotAdvertised(wl.Shm);
const zwlr_layer_shell_v1 = globals.zwlr_layer_shell_v1 orelse utils.interfaceNotAdvertised(zwlr.LayerShellV1); const zwlr_layer_shell_v1 = globals.zwlr_layer_shell_v1 orelse utils.interfaceNotAdvertised(zwlr.LayerShellV1);
const config = try Config.create(); const config = try Config.create();
@ -47,7 +61,7 @@ pub fn main() !void {
const context = try Context.create(.{ const context = try Context.create(.{
.wl_compositor = wl_compositor, .wl_compositor = wl_compositor,
.wl_display = wl_display, .wl_display = wl_display,
.wl_output = wl_output, .wl_outputs = wl_outputs,
.wl_registry = wl_registry, .wl_registry = wl_registry,
.wl_shm = wl_shm, .wl_shm = wl_shm,
.river_layer_shell_v1 = river_layer_shell_v1, .river_layer_shell_v1 = river_layer_shell_v1,
@ -73,20 +87,46 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: *
if (mem.orderZ(u8, ev.interface, wl.Compositor.interface.name) == .eq) { if (mem.orderZ(u8, ev.interface, wl.Compositor.interface.name) == .eq) {
if (ev.version < 4) utils.versionNotSupported(wl.Compositor, ev.version, 4); if (ev.version < 4) utils.versionNotSupported(wl.Compositor, ev.version, 4);
globals.wl_compositor = registry.bind(ev.name, wl.Compositor, 4) catch |e| { globals.wl_compositor = registry.bind(ev.name, wl.Compositor, 4) catch |e| {
fatal("Failed to bind to compositor: {any}", .{@errorName(e)}); fatal("Failed to bind to wl_compositor: {any}", .{@errorName(e)});
};
} else if (mem.orderZ(u8, ev.interface, wl.Output.interface.name) == .eq) {
if (ev.version < 4) utils.versionNotSupported(wl.Output, ev.version, 4);
const wl_output = registry.bind(ev.name, wl.Output, 4) catch |e| {
fatal("Failed to bind to wl_output: {any}", .{@errorName(e)});
};
// We can get multiple wl_outputs, so we have to try add them to our HashMap
// instead of just keeping the one
globals.wl_outputs.put(utils.allocator, ev.name, wl_output) catch |e| {
fatal("Failed to add wl_output to hashmap: {any}", .{@errorName(e)});
};
} else if (mem.orderZ(u8, ev.interface, wl.Shm.interface.name) == .eq) {
globals.wl_shm = registry.bind(ev.name, wl.Shm, 1) catch |e| {
fatal("Failed to bind to wl_shm: {any}", .{@errorName(e)});
}; };
} else if (mem.orderZ(u8, ev.interface, river.WindowManagerV1.interface.name) == .eq) { } else if (mem.orderZ(u8, ev.interface, river.WindowManagerV1.interface.name) == .eq) {
globals.river_window_manager_v1 = registry.bind(ev.name, river.WindowManagerV1, 3) catch |e| { globals.river_window_manager_v1 = registry.bind(ev.name, river.WindowManagerV1, 3) catch |e| {
fatal("Failed to bind to window_manager_v1: {any}", .{@errorName(e)}); fatal("Failed to bind to river_window_manager_v1: {any}", .{@errorName(e)});
}; };
} else if (mem.orderZ(u8, ev.interface, river.XkbBindingsV1.interface.name) == .eq) { } else if (mem.orderZ(u8, ev.interface, river.XkbBindingsV1.interface.name) == .eq) {
globals.river_xkb_bindings_v1 = registry.bind(ev.name, river.XkbBindingsV1, 2) catch |e| { globals.river_xkb_bindings_v1 = registry.bind(ev.name, river.XkbBindingsV1, 2) catch |e| {
fatal("Failed to bind to xkb_bindings_v1: {any}", .{@errorName(e)}); fatal("Failed to bind to river_xkb_bindings_v1: {any}", .{@errorName(e)});
};
} else if (mem.orderZ(u8, ev.interface, zwlr.LayerShellV1.interface.name) == .eq) {
if (ev.version < 3) utils.versionNotSupported(zwlr.LayerShellV1, ev.version, 3);
globals.zwlr_layer_shell_v1 = registry.bind(ev.name, zwlr.LayerShellV1, 3) catch |e| {
fatal("Failed to bind to zwlr_layer_shell_v1: {any}", .{@errorName(e)});
}; };
} }
}, },
// We don't need .global_remove // We don't need .global_remove
.global_remove => {}, .global_remove => |ev| {
// The only remove we care about is for wl_outputs
if (!globals.wl_outputs.remove(ev.name)) {
log.debug("Received a global_remove event for something other than a wl_output", .{});
}
},
} }
} }