Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
Output: rework for rwm protocol
All state is now applied through the transaction system and modesets are
done backend-wide.
build.zig.zon | 4 +-
protocol/river-window-management-v1.xml | 4 +
river/InputConfig.zig | 8 +-
river/LockManager.zig | 4 +-
river/LockSurface.zig | 2 +-
river/Output.zig | 356 +++++++++++++++----------
river/Root.zig | 445 +++++++++++++++++++-------------
river/Window.zig | 2 +-
river/WindowManager.zig | 67 ++++-
9 files changed, 564 insertions(+), 328 deletions(-)
diff --git a/build.zig.zon b/build.zig.zon
index 892558c..9237e55 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -12,8 +12,8 @@
.hash = "12206015b1750462788de094b8154eab733a36523639124ace437d4631ca01b5ba18",
},
.@"zig-wlroots" = .{
- .url = "https://codeberg.org/ifreund/zig-wlroots/archive/ae6151f22ceb4ccd7efb1291dea573785918a7ec.tar.gz",
- .hash = "12204d99aebfbf88f1ff3ab197362937b3d4bef4f45fde9c4ee0d569e095a2a25889",
+ .url = "https://codeberg.org/ifreund/zig-wlroots/archive/a2372a124d9b8568775849a2ea60da36c4f5b0dd.tar.gz",
+ .hash = "122019ca283db70a0f4932e54b5195e2392b9220c7f97808d0e74c14be30aea4a458",
},
.@"zig-xkbcommon" = .{
.url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/v0.2.0.tar.gz",
diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index b3f0ea6..af697b0 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -165,6 +165,10 @@
<description summary="new logical output">
A new logical output has been created, perhaps due to a new physical
monitor being plugged in or perhaps due to a change in configuration.
+
+ This event is double-buffered state and will be followed by
+ river_output_v1.position and dimensions events as well as a
+ river_window_manager_v1.update event.
</description>
<arg name="id" type="new_id" interface="river_output_v1"/>
</event>
diff --git a/river/InputConfig.zig b/river/InputConfig.zig
index 39fc47d..a0288cf 100644
--- a/river/InputConfig.zig
+++ b/river/InputConfig.zig
@@ -221,10 +221,10 @@ pub const MapToOutput = struct {
fn apply(map_to_output: MapToOutput, device: *InputDevice) void {
const wlr_output = blk: {
if (map_to_output.output_name) |name| {
- var it = server.root.active_outputs.iterator(.forward);
- while (it.next()) |output| {
- if (mem.eql(u8, mem.span(output.wlr_output.name), name)) {
- break :blk output.wlr_output;
+ var it = server.root.output_layout.outputs.iterator(.forward);
+ while (it.next()) |layout_output| {
+ if (mem.eql(u8, mem.span(layout_output.output.name), name)) {
+ break :blk layout_output.output;
}
}
}
diff --git a/river/LockManager.zig b/river/LockManager.zig
index 5c50b10..c52e8b0 100644
--- a/river/LockManager.zig
+++ b/river/LockManager.zig
@@ -147,8 +147,10 @@ pub fn maybeLock(manager: *LockManager) void {
var all_outputs_blanked = true;
var all_outputs_rendered_lock_surface = true;
{
- var it = server.root.active_outputs.iterator(.forward);
+ var it = server.root.outputs.iterator(.forward);
while (it.next()) |output| {
+ if (!output.wlr_output.?.enabled) continue;
+
switch (output.lock_render_state) {
.pending_unlock, .unlocked, .pending_blank, .pending_lock_surface => {
all_outputs_blanked = false;
diff --git a/river/LockSurface.zig b/river/LockSurface.zig
index fb6e14c..af355b3 100644
--- a/river/LockSurface.zig
+++ b/river/LockSurface.zig
@@ -97,7 +97,7 @@ pub fn getOutput(lock_surface: *LockSurface) *Output {
pub fn configure(lock_surface: *LockSurface) void {
var output_width: i32 = undefined;
var output_height: i32 = undefined;
- lock_surface.getOutput().wlr_output.effectiveResolution(&output_width, &output_height);
+ lock_surface.wlr_lock_surface.output.effectiveResolution(&output_width, &output_height);
_ = lock_surface.wlr_lock_surface.configure(@intCast(output_width), @intCast(output_height));
}
diff --git a/river/Output.zig b/river/Output.zig
index a34c1fe..a99eab2 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -1,6 +1,6 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
-// Copyright 2020 The River Developers
+// Copyright 2020-2024 The River Developers
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@@ -26,6 +26,7 @@ const wlr = @import("wlroots");
const wayland = @import("wayland");
const wl = wayland.server.wl;
const zwlr = wayland.server.zwlr;
+const river = wayland.server.river;
const server = &@import("main.zig").server;
const util = @import("util.zig");
@@ -37,19 +38,102 @@ const Config = @import("Config.zig");
const log = std.log.scoped(.output);
-wlr_output: *wlr.Output,
-scene_output: *wlr.SceneOutput,
+pub const State = struct {
+ state: enum {
+ /// Powered on and exposed to the window manager
+ enabled,
+ /// Powered off and exposed to the window manager
+ disabled_soft,
+ /// Powered off and hidden from the window manager
+ disabled_hard,
+ /// Corresponding hardware no longer present
+ destroying,
+ } = .disabled_hard,
+ /// Logical coordinate space
+ x: i32 = 0,
+ /// Logical coordinate space
+ y: i32 = 0,
+ /// The width/height of modes is in physical pixels, not in the
+ /// compositors logical coordinate space.
+ mode: union(enum) {
+ standard: *wlr.Output.Mode,
+ custom: struct {
+ width: i32 = 0,
+ height: i32 = 0,
+ refresh: i32 = 0,
+ },
+ /// Used before the initial modeset and after the wlr_output is destroyed.
+ none,
+ } = .none,
+ scale: f32 = 1,
+ transform: wl.Output.Transform = .normal,
+ adaptive_sync: bool = false,
+ auto_layout: bool = true,
+
+ /// Width in the logical coordinate space
+ pub fn width(state: *const State) i32 {
+ const physical: f32 = blk: {
+ if (@mod(@intFromEnum(state.transform), 2) == 0) {
+ break :blk @floatFromInt(switch (state.mode) {
+ .standard => |mode| mode.width,
+ .custom => |mode| mode.width,
+ .none => 0,
+ });
+ } else {
+ break :blk @floatFromInt(switch (state.mode) {
+ .standard => |mode| mode.height,
+ .custom => |mode| mode.height,
+ .none => 0,
+ });
+ }
+ };
+ return @intFromFloat(physical / state.scale);
+ }
+
+ /// Height in the logical coordinate space
+ pub fn height(state: *const State) i32 {
+ const physical: f32 = blk: {
+ if (@mod(@intFromEnum(state.transform), 2) == 0) {
+ break :blk @floatFromInt(switch (state.mode) {
+ .standard => |mode| mode.height,
+ .custom => |mode| mode.height,
+ .none => 0,
+ });
+ } else {
+ break :blk @floatFromInt(switch (state.mode) {
+ .standard => |mode| mode.width,
+ .custom => |mode| mode.width,
+ .none => 0,
+ });
+ }
+ };
+ return @intFromFloat(physical / state.scale);
+ }
+
+ pub fn apply(state: *const State, wlr_state: *wlr.Output.State) void {
+ wlr_state.setEnabled(state.state == .enabled);
+ switch (state.mode) {
+ .standard => |mode| wlr_state.setMode(mode),
+ .custom => |mode| wlr_state.setCustomMode(mode.width, mode.height, mode.refresh),
+ .none => {},
+ }
+ wlr_state.setScale(state.scale);
+ wlr_state.setTransform(state.transform);
+ wlr_state.setAdaptiveSyncEnabled(state.adaptive_sync);
+ }
+};
-/// For Root.all_outputs
-all_link: wl.list.Link,
+/// Set to null when the wlr_output is destroyed.
+wlr_output: ?*wlr.Output,
+scene_output: ?*wlr.SceneOutput,
-/// For Root.active_outputs
-active_link: wl.list.Link,
+object: ?*river.OutputV1 = null,
/// Tracks the currently presented frame on the output as it pertains to ext-session-lock.
/// The output is initially considered blanked:
/// If using the DRM backend it will be blanked with the initial modeset.
/// If using the Wayland or X11 backend nothing will be visible until the first frame is rendered.
+/// XXX set this to blanked on enabled->disabled transition
lock_render_state: enum {
/// Submitted an unlocked buffer but the buffer has not yet been presented.
pending_unlock,
@@ -71,6 +155,18 @@ lock_render_state: enum {
/// This request is handled while rendering the next frame in handleFrame().
gamma_dirty: bool = false,
+/// Root.outputs
+link: wl.list.Link,
+
+/// Pending state to be sent to the wm in the next update sequence.
+pending: State = .{},
+link_pending: wl.list.Link,
+/// State sent to the wm in the latest update sequence.
+sent: State = .{},
+link_sent: wl.list.Link,
+/// State applied to the wlr_output and rendered.
+current: State = .{},
+
destroy: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleDestroy),
request_state: wl.Listener(*wlr.Output.event.RequestState) = wl.Listener(*wlr.Output.event.RequestState).init(handleRequestState),
frame: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleFrame),
@@ -80,181 +176,189 @@ pub fn create(wlr_output: *wlr.Output) !void {
const output = try util.gpa.create(Output);
errdefer util.gpa.destroy(output);
- if (!wlr_output.initRender(server.allocator, server.renderer)) return error.InitRenderFailed;
-
- // If no standard mode for the output works we can't enable the output automatically.
- // It will stay disabled unless the user configures a custom mode which works.
- //
- // For the Wayland backend, the list of modes will be empty and it is possible to
- // enable the output without setting a mode.
{
- var state = wlr.Output.State.init();
- defer state.finish();
-
- state.setEnabled(true);
-
- if (wlr_output.preferredMode()) |preferred_mode| {
- state.setMode(preferred_mode);
- }
-
- if (!wlr_output.commitState(&state)) {
- log.err("initial output commit with preferred mode failed, trying all modes", .{});
-
- // It is important to try other modes if the preferred mode fails
- // which is reported to be helpful in practice with e.g. multiple
- // high resolution monitors connected through a usb dock.
- var it = wlr_output.modes.iterator(.forward);
- while (it.next()) |mode| {
- state.setMode(mode);
- if (wlr_output.commitState(&state)) {
- log.info("initial output commit succeeded with mode {}x{}@{}mHz", .{
- mode.width,
- mode.height,
- mode.refresh,
- });
- break;
- } else {
- log.err("initial output commit failed with mode {}x{}@{}mHz", .{
- mode.width,
- mode.height,
- mode.refresh,
- });
- }
- }
+ const title = try fmt.allocPrintZ(util.gpa, "river - {s}", .{wlr_output.name});
+ defer util.gpa.free(title);
+ if (wlr_output.isWl()) {
+ wlr_output.wlSetTitle(title);
+ } else if (wlr.config.has_x11_backend and wlr_output.isX11()) {
+ wlr_output.x11SetTitle(title);
}
}
- var width: c_int = undefined;
- var height: c_int = undefined;
- wlr_output.effectiveResolution(&width, &height);
+ if (!wlr_output.initRender(server.allocator, server.renderer)) return error.InitRenderFailed;
const scene_output = try server.root.scene.createSceneOutput(wlr_output);
+ errdefer comptime unreachable;
+
output.* = .{
.wlr_output = wlr_output,
.scene_output = scene_output,
- .all_link = undefined,
- .active_link = undefined,
+ .link = undefined,
+ .link_pending = undefined,
+ .link_sent = undefined,
};
wlr_output.data = @intFromPtr(output);
+ server.root.outputs.append(output);
+ server.wm.pending.outputs.append(output);
+ output.link_sent.init();
+
wlr_output.events.destroy.add(&output.destroy);
wlr_output.events.request_state.add(&output.request_state);
wlr_output.events.frame.add(&output.frame);
wlr_output.events.present.add(&output.present);
- output.setTitle();
-
- output.active_link.init();
- server.root.all_outputs.append(output);
+ output.pending.state = .enabled;
+ if (wlr_output.preferredMode()) |preferred_mode| {
+ output.pending.mode = .{ .standard = preferred_mode };
+ }
- output.handleEnableDisable();
+ server.wm.dirtyPending();
}
-fn handleDestroy(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void {
+fn handleDestroy(listener: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) void {
const output: *Output = @fieldParentPtr("destroy", listener);
- log.debug("output '{s}' destroyed", .{output.wlr_output.name});
-
- // Remove the destroyed output from root if it wasn't already removed
- server.root.deactivateOutput(output);
+ log.debug("output '{s}' destroyed", .{wlr_output.name});
- output.all_link.remove();
+ output.link.remove();
output.destroy.link.remove();
output.request_state.link.remove();
output.frame.link.remove();
output.present.link.remove();
- output.wlr_output.data = 0;
+ wlr_output.data = 0;
- util.gpa.destroy(output);
-
- server.root.handleOutputConfigChange() catch std.log.err("out of memory", .{});
+ output.wlr_output = null;
+ output.scene_output = null;
+ output.pending.state = .destroying;
server.wm.dirtyPending();
}
-fn handleRequestState(listener: *wl.Listener(*wlr.Output.event.RequestState), event: *wlr.Output.event.RequestState) void {
- const output: *Output = @fieldParentPtr("request_state", listener);
+pub fn sendDirty(output: *Output) !void {
+ switch (output.pending.state) {
+ .enabled, .disabled_soft => {
+ const wm_v1 = server.wm.object.?;
+ const new = output.object == null;
+ const output_v1 = output.object orelse blk: {
+ const output_v1 = try river.OutputV1.create(wm_v1.getClient(), wm_v1.getVersion(), 0);
+ output.object = output_v1;
+
+ output_v1.setHandler(*Output, handleRequest, null, output);
+ wm_v1.sendOutput(output_v1);
+ output.link_sent.remove();
+ server.wm.sent.outputs.append(output);
+
+ break :blk output_v1;
+ };
+ errdefer comptime unreachable;
- output.applyState(event.state) catch {
- log.err("failed to commit requested state", .{});
- return;
- };
+ const pending = &output.pending;
+ const sent = &output.sent;
- server.wm.dirtyPending();
-}
+ if (new or pending.width() != sent.width() or pending.height() != sent.height()) {
+ output_v1.sendDimensions(pending.width(), pending.height());
+ }
+ if (new or pending.x != sent.x or pending.y != sent.y) {
+ output_v1.sendPosition(pending.x, pending.y);
+ }
-// TODO double buffer output state changes for frame perfection and cleaner code.
-// Schedule a frame and commit in the frame handler.
-// Get rid of this function.
-pub fn applyState(output: *Output, state: *wlr.Output.State) error{CommitFailed}!void {
+ output.sent = output.pending;
+ },
+ .disabled_hard, .destroying => {
+ if (output.object) |output_v1| {
+ output_v1.sendRemoved();
+ output_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
+ output.object = null;
+ }
- // We need to be precise about this state change to make assertions
- // in updateLockRenderStateOnEnableDisable() possible.
- const enable_state_change = state.committed.enabled and
- (state.enabled != output.wlr_output.enabled);
+ output.link_pending.remove();
+ output.link_sent.remove();
+ output.link_pending.init();
+ output.link_sent.init();
- if (!output.wlr_output.commitState(state)) {
- return error.CommitFailed;
+ if (output.pending.state == .destroying) {
+ util.gpa.destroy(output);
+ }
+ },
}
+}
- if (enable_state_change) {
- output.handleEnableDisable();
- }
+fn handleRequestInert(
+ output_v1: *river.OutputV1,
+ request: river.OutputV1.Request,
+ _: ?*anyopaque,
+) void {
+ if (request == .destroy) output_v1.destroy();
+}
- if (state.committed.mode) {
- if (server.lock_manager.lockSurfaceFromOutput(output)) |s| s.configure();
+fn handleRequest(
+ _: *river.OutputV1,
+ request: river.OutputV1.Request,
+ _: ?*Output,
+) void {
+ switch (request) {
+ .destroy => {}, // XXX send protocol error
}
}
-fn handleEnableDisable(output: *Output) void {
- output.updateLockRenderStateOnEnableDisable();
- output.gamma_dirty = true;
+fn handleRequestState(listener: *wl.Listener(*wlr.Output.event.RequestState), event: *wlr.Output.event.RequestState) void {
+ const output: *Output = @fieldParentPtr("request_state", listener);
- if (output.wlr_output.enabled) {
- // Add the output to root.active_outputs and the output layout if it has not
- // already been added.
- server.root.activateOutput(output);
- } else {
- server.root.deactivateOutput(output);
+ // The only state currently requested by a wlroots backend is a
+ // custom mode as the Wayland/X11 backend window is resized.
+ const committed: u32 = @bitCast(event.state.committed);
+ const supported: u32 = @bitCast(wlr.Output.State.Fields{ .mode = true });
+
+ if (committed != supported) {
+ log.err("backend requested unsupported state {}", .{committed});
+ return;
}
-}
-pub fn updateLockRenderStateOnEnableDisable(output: *Output) void {
- if (output.wlr_output.enabled) {
- assert(output.lock_render_state == .blanked);
+ if (event.state.mode) |mode| {
+ output.pending.mode = .{ .standard = mode };
} else {
- // Disabling and re-enabling an output always blanks it.
- output.lock_render_state = .blanked;
+ output.pending.mode = .{ .custom = .{
+ .width = event.state.custom_mode.width,
+ .height = event.state.custom_mode.height,
+ .refresh = event.state.custom_mode.refresh,
+ } };
}
+
+ server.wm.dirtyPending();
}
-fn handleFrame(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void {
+fn handleFrame(listener: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) void {
const output: *Output = @fieldParentPtr("frame", listener);
- const scene_output = server.root.scene.getSceneOutput(output.wlr_output).?;
// TODO this should probably be retried on failure
- output.renderAndCommit(scene_output) catch |err| switch (err) {
+ output.renderAndCommit() catch |err| switch (err) {
error.OutOfMemory => log.err("out of memory", .{}),
- error.CommitFailed => log.err("output commit failed for {s}", .{output.wlr_output.name}),
+ error.CommitFailed => log.err("output commit failed for {s}", .{wlr_output.name}),
};
var now: posix.timespec = undefined;
posix.clock_gettime(posix.CLOCK.MONOTONIC, &now) catch @panic("CLOCK_MONOTONIC not supported");
- scene_output.sendFrameDone(&now);
+ output.scene_output.?.sendFrameDone(&now);
}
-fn renderAndCommit(output: *Output, scene_output: *wlr.SceneOutput) !void {
- if (output.gamma_dirty) {
- var state = wlr.Output.State.init();
- defer state.finish();
+fn renderAndCommit(output: *Output) !void {
+ const wlr_output = output.wlr_output.?;
+
+ var state = wlr.Output.State.init();
+ defer state.finish();
+
+ output.current.apply(&state);
- const control = server.root.gamma_control_manager.getControl(output.wlr_output);
+ if (output.gamma_dirty) {
+ const control = server.root.gamma_control_manager.getControl(wlr_output);
if (!wlr.GammaControlV1.apply(control, &state)) return error.OutOfMemory;
- if (!output.wlr_output.testState(&state)) {
+ if (!wlr_output.testState(&state)) {
wlr.GammaControlV1.sendFailedAndDestroy(control);
state.clearGammaLut();
// If the backend does not support gamma LUTs it will reject any
@@ -262,15 +366,13 @@ fn renderAndCommit(output: *Output, scene_output: *wlr.SceneOutput) !void {
// has a null LUT. The wayland backend for example has this behavior.
state.committed.gamma_lut = false;
}
+ }
- if (!scene_output.buildState(&state, null)) return error.CommitFailed;
+ if (!output.scene_output.?.buildState(&state, null)) return error.CommitFailed;
- if (!output.wlr_output.commitState(&state)) return error.CommitFailed;
+ if (!wlr_output.commitState(&state)) return error.CommitFailed;
- output.gamma_dirty = false;
- } else {
- if (!scene_output.commit(null)) return error.CommitFailed;
- }
+ output.gamma_dirty = false;
const lock_surface_mapped = blk: {
if (server.lock_manager.lockSurfaceFromOutput(output)) |lock_surface| {
@@ -341,13 +443,3 @@ fn handlePresent(
.blanked, .lock_surface => {},
}
}
-
-fn setTitle(output: Output) void {
- const title = fmt.allocPrintZ(util.gpa, "river - {s}", .{output.wlr_output.name}) catch return;
- defer util.gpa.free(title);
- if (output.wlr_output.isWl()) {
- output.wlr_output.wlSetTitle(title);
- } else if (wlr.config.has_x11_backend and output.wlr_output.isX11()) {
- output.wlr_output.x11SetTitle(title);
- }
-}
diff --git a/river/Root.zig b/river/Root.zig
index c892ece..914a046 100644
--- a/river/Root.zig
+++ b/river/Root.zig
@@ -71,7 +71,6 @@ layers: struct {
new_output: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleNewOutput),
output_layout: *wlr.OutputLayout,
-layout_change: wl.Listener(*wlr.OutputLayout) = wl.Listener(*wlr.OutputLayout).init(handleLayoutChange),
presentation: *wlr.Presentation,
xdg_output_manager: *wlr.XdgOutputManagerV1,
@@ -90,12 +89,8 @@ gamma_control_manager: *wlr.GammaControlManagerV1,
gamma_control_set_gamma: wl.Listener(*wlr.GammaControlManagerV1.event.SetGamma) =
wl.Listener(*wlr.GammaControlManagerV1.event.SetGamma).init(handleSetGamma),
-/// A list of all outputs
-all_outputs: wl.list.Head(Output, .all_link),
-
-/// A list of all active outputs (any one that can be interacted with, even if
-/// it's turned off by dpms)
-active_outputs: wl.list.Head(Output, .active_link),
+/// All Outputs that have a corresponding wlr_output.
+outputs: wl.list.Head(Output, .link),
pub fn init(root: *Root) !void {
const output_layout = try wlr.OutputLayout.create(server.wl_server);
@@ -129,8 +124,7 @@ pub fn init(root: *Root) !void {
.override_redirect = if (build_options.xwayland) try normal_tree.createSceneTree(),
},
.output_layout = output_layout,
- .all_outputs = undefined,
- .active_outputs = undefined,
+ .outputs = undefined,
.presentation = try wlr.Presentation.create(server.wl_server, server.backend),
.xdg_output_manager = try wlr.XdgOutputManagerV1.create(server.wl_server, output_layout),
@@ -139,13 +133,11 @@ pub fn init(root: *Root) !void {
.gamma_control_manager = try wlr.GammaControlManagerV1.create(server.wl_server),
};
- root.all_outputs.init();
- root.active_outputs.init();
+ root.outputs.init();
server.backend.events.new_output.add(&root.new_output);
root.output_manager.events.apply.add(&root.manager_apply);
root.output_manager.events.@"test".add(&root.manager_test);
- root.output_layout.events.change.add(&root.layout_change);
root.power_manager.events.set_mode.add(&root.power_manager_set_mode);
root.gamma_control_manager.events.set_gamma.add(&root.gamma_control_set_gamma);
}
@@ -192,16 +184,6 @@ pub fn at(root: Root, lx: f64, ly: f64) ?AtResult {
}
}
-pub fn layerSurfaceTree(root: Root, layer: zwlr.LayerShellV1.Layer) *wlr.SceneTree {
- const trees = [_]*wlr.SceneTree{
- root.layers.background,
- root.layers.bottom,
- root.layers.top,
- root.layers.overlay,
- };
- return trees[@intCast(@intFromEnum(layer))];
-}
-
fn handleNewOutput(_: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) void {
const log = std.log.scoped(.output_manager);
@@ -215,137 +197,86 @@ fn handleNewOutput(_: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) void {
wlr_output.destroy();
return;
};
-
- server.root.handleOutputConfigChange() catch log.err("out of memory", .{});
-
- server.input_manager.reconfigureDevices();
}
-/// Remove the output from root.active_outputs and the output layout.
-/// Evacuate windows if necessary.
-pub fn deactivateOutput(root: *Root, output: *Output) void {
- {
- // If the output has already been removed, do nothing
- var it = root.active_outputs.iterator(.forward);
- while (it.next()) |o| {
- if (o == output) break;
- } else return;
- }
-
- root.output_layout.remove(output.wlr_output);
-
- output.active_link.remove();
- output.active_link.init();
-
- // XXX Close all layer surfaces on the removed output
-
- // We must call reconfigureDevices here to unmap devices that might be mapped to this output
- // in order to prevent a segfault in wlroots.
- server.input_manager.reconfigureDevices();
-}
+fn handleManagerTest(_: *wl.Listener(*wlr.OutputConfigurationV1), config: *wlr.OutputConfigurationV1) void {
+ defer config.destroy();
-/// Add the output to root.active_outputs and the output layout if it has not
-/// already been added.
-pub fn activateOutput(root: *Root, output: *Output) void {
- {
- // If we have already added the output, do nothing and return
- var it = root.active_outputs.iterator(.forward);
- while (it.next()) |o| if (o == output) return;
+ if (!validateConfigCoordinates(config)) {
+ config.sendFailed();
+ return;
}
- root.active_outputs.append(output);
-
- // This arranges outputs from left-to-right in the order they appear. The
- // wlr-output-management protocol may be used to modify this arrangement.
- // This also creates a wl_output global which is advertised to clients.
- _ = root.output_layout.addAuto(output.wlr_output) catch {
- // This would currently be very awkward to handle well and this output
- // handling code needs to be heavily refactored soon anyways for double
- // buffered state application as part of the transaction system.
- // In any case, wlroots 0.16 would have crashed here, the error is only
- // possible to handle after updating to 0.17.
- @panic("TODO handle allocation failure here");
+ const states = config.buildState() catch {
+ std.log.err("out of memory", .{});
+ config.sendFailed();
+ return;
};
-}
-
-// We need this listener to deal with outputs that have their position auto-configured
-// by the wlr_output_layout.
-fn handleLayoutChange(listener: *wl.Listener(*wlr.OutputLayout), _: *wlr.OutputLayout) void {
- const root: *Root = @fieldParentPtr("layout_change", listener);
-
- root.handleOutputConfigChange() catch std.log.err("out of memory", .{});
-}
+ defer std.c.free(states.ptr);
-/// Sync up the output scene node state with the output_layout and
-/// send the current output configuration to all wlr-output-manager clients.
-pub fn handleOutputConfigChange(root: *Root) !void {
- const config = try wlr.OutputConfigurationV1.create();
- // this destroys all associated config heads as well
- errdefer config.destroy();
+ var swapchain_manager: wlr.OutputSwapchainManager = undefined;
+ swapchain_manager.init(server.backend);
+ defer swapchain_manager.finish();
- var it = root.all_outputs.iterator(.forward);
- while (it.next()) |output| {
- // If the output is not part of the layout (and thus disabled)
- // the box will be zeroed out.
- var box: wlr.Box = undefined;
- root.output_layout.getBox(output.wlr_output, &box);
-
- // XXX
- //output.tree.node.setEnabled(!box.empty());
- //output.tree.node.setPosition(box.x, box.y);
- //output.scene_output.setPosition(box.x, box.y);
-
- const head = try wlr.OutputConfigurationV1.Head.create(config, output.wlr_output);
- head.state.x = box.x;
- head.state.y = box.y;
+ if (swapchain_manager.prepare(states)) {
+ config.sendSucceeded();
+ } else {
+ config.sendFailed();
}
-
- root.output_manager.setConfiguration(config);
}
-fn handleManagerApply(
- listener: *wl.Listener(*wlr.OutputConfigurationV1),
- config: *wlr.OutputConfigurationV1,
-) void {
- const root: *Root = @fieldParentPtr("manager_apply", listener);
- defer config.destroy();
-
+fn handleManagerApply(_: *wl.Listener(*wlr.OutputConfigurationV1), config: *wlr.OutputConfigurationV1) void {
std.log.scoped(.output_manager).info("applying output configuration", .{});
- root.processOutputConfig(config, .apply);
-
- root.handleOutputConfigChange() catch std.log.err("out of memory", .{});
-}
+ if (!validateConfigCoordinates(config)) {
+ config.sendFailed();
+ return;
+ }
-fn handleManagerTest(
- listener: *wl.Listener(*wlr.OutputConfigurationV1),
- config: *wlr.OutputConfigurationV1,
-) void {
- const root: *Root = @fieldParentPtr("manager_test", listener);
- defer config.destroy();
+ var it = config.heads.iterator(.forward);
+ while (it.next()) |head| {
+ const output: *Output = @ptrFromInt(head.state.output.data);
+
+ const prev_state = output.pending.state;
+
+ output.pending = .{
+ .state = if (head.state.enabled) .enabled else .disabled_hard,
+ .mode = blk: {
+ if (head.state.mode) |mode| {
+ break :blk .{ .standard = mode };
+ } else {
+ break :blk .{ .custom = .{
+ .width = head.state.custom_mode.width,
+ .height = head.state.custom_mode.height,
+ .refresh = head.state.custom_mode.refresh,
+ } };
+ }
+ },
+ .x = head.state.x,
+ .y = head.state.y,
+ .transform = head.state.transform,
+ .adaptive_sync = head.state.adaptive_sync_enabled,
+ .auto_layout = false,
+ };
- root.processOutputConfig(config, .test_only);
-}
+ if (output.pending.state == .enabled and prev_state != .enabled) {
+ output.link_pending.remove();
+ server.wm.pending.outputs.append(output);
+ }
+ }
-fn processOutputConfig(
- root: *Root,
- config: *wlr.OutputConfigurationV1,
- action: enum { test_only, apply },
-) void {
- // Ignore layout change events this function generates while applying the config
- root.layout_change.link.remove();
- defer root.output_layout.events.change.add(&root.layout_change);
+ if (server.wm.pending.output_config) |old| {
+ old.sendFailed();
+ old.destroy();
+ }
+ server.wm.pending.output_config = config;
- var success = true;
+ server.wm.dirtyPending();
+}
+fn validateConfigCoordinates(config: *wlr.OutputConfigurationV1) bool {
var it = config.heads.iterator(.forward);
while (it.next()) |head| {
- const wlr_output = head.state.output;
- const output: *Output = @ptrFromInt(wlr_output.data);
-
- var proposed_state = wlr.Output.State.init();
- head.state.apply(&proposed_state);
-
// Negative output coordinates currently cause Xwayland clients to not receive click events.
// See: https://gitlab.freedesktop.org/xorg/xserver/-/issues/899
if (build_options.xwayland and server.xwayland != null and
@@ -354,38 +285,11 @@ fn processOutputConfig(
std.log.scoped(.output_manager).err(
\\Attempted to set negative coordinates for output {s}.
\\Negative output coordinates are disallowed if Xwayland is enabled due to a limitation of Xwayland.
- , .{output.wlr_output.name});
- success = false;
- continue;
- }
-
- switch (action) {
- .test_only => {
- if (!wlr_output.testState(&proposed_state)) success = false;
- },
- .apply => {
- output.applyState(&proposed_state) catch {
- std.log.scoped(.output_manager).err("failed to apply config to output {s}", .{
- output.wlr_output.name,
- });
- success = false;
- };
- if (output.wlr_output.enabled) {
- // applyState() will always add the output to the layout on success, which means
- // that this function cannot fail as it does not need to allocate a new layout output.
- _ = root.output_layout.add(output.wlr_output, head.state.x, head.state.y) catch unreachable;
- }
- },
+ , .{head.state.output.name});
+ return false;
}
}
-
- if (action == .apply) server.wm.dirtyPending();
-
- if (success) {
- config.sendSucceeded();
- } else {
- config.sendFailed();
- }
+ return true;
}
fn handlePowerManagerSetMode(
@@ -400,30 +304,17 @@ fn handlePowerManagerSetMode(
event.output.name,
});
- const requested = event.mode == .on;
-
- if (output.wlr_output.enabled == requested) {
- std.log.debug("output {s} dpms is already {s}, ignoring request", .{
- event.output.name,
- @tagName(event.mode),
- });
- return;
- }
-
- {
- var state = wlr.Output.State.init();
- defer state.finish();
-
- state.setEnabled(requested);
-
- if (!output.wlr_output.commitState(&state)) {
- std.log.scoped(.server).err("output commit failed for {s}", .{output.wlr_output.name});
- return;
- }
+ switch (output.pending.state) {
+ .enabled => {
+ if (event.mode == .off) output.pending.state = .disabled_soft else return;
+ },
+ .disabled_soft => {
+ if (event.mode == .on) output.pending.state = .enabled else return;
+ },
+ .disabled_hard, .destroying => unreachable,
}
- output.updateLockRenderStateOnEnableDisable();
- output.gamma_dirty = true;
+ server.wm.dirtyPending();
}
fn handleSetGamma(
@@ -436,5 +327,187 @@ fn handleSetGamma(
std.log.debug("client requested to set gamma", .{});
output.gamma_dirty = true;
- output.wlr_output.scheduleFrame();
+ event.output.scheduleFrame();
+}
+
+pub fn commitOutputState(root: *Root) void {
+ const wm = &server.wm;
+
+ {
+ var it = wm.sent.outputs.iterator(.forward);
+ while (it.next()) |output| {
+ const wlr_output = output.wlr_output orelse continue;
+ switch (output.sent.state) {
+ .enabled, .disabled_soft => {
+ output.scene_output.?.setPosition(output.sent.x, output.sent.y);
+ _ = root.output_layout.add(wlr_output, output.sent.x, output.sent.y) catch {
+ std.log.err("out of memory", .{});
+ continue; // Try again next time
+ };
+ },
+ .disabled_hard, .destroying => {
+ root.output_layout.remove(wlr_output);
+ },
+ }
+ }
+ }
+
+ server.input_manager.reconfigureDevices();
+
+ const need_modeset = blk: {
+ var it = wm.sent.outputs.iterator(.forward);
+ while (it.next()) |output| {
+ const wlr_output = output.wlr_output orelse continue;
+
+ switch (output.sent.state) {
+ .enabled => if (!wlr_output.enabled) break :blk true,
+ .disabled_soft, .disabled_hard, .destroying => continue,
+ }
+
+ switch (output.sent.mode) {
+ .standard => |mode| {
+ if (mode != wlr_output.current_mode) break :blk true;
+ },
+ .custom => |mode| {
+ if (mode.width != wlr_output.width) break :blk true;
+ if (mode.height != wlr_output.height) break :blk true;
+ if (mode.refresh != wlr_output.refresh) break :blk true;
+ },
+ .none => {},
+ }
+ if (output.sent.adaptive_sync != (wlr_output.adaptive_sync_status == .enabled)) {
+ break :blk true;
+ }
+ }
+
+ break :blk false;
+ };
+
+ if (need_modeset) {
+ var states = std.ArrayList(wlr.Backend.OutputState).init(util.gpa);
+ defer states.deinit();
+ defer for (states.items) |*s| s.base.finish();
+
+ {
+ var it = wm.sent.outputs.iterator(.forward);
+ while (it.next()) |output| {
+ const wlr_output = output.wlr_output orelse continue;
+ const state = states.addOne() catch {
+ std.log.err("out of memory", .{});
+ return;
+ };
+
+ state.output = wlr_output;
+ state.base = wlr.Output.State.init();
+
+ output.sent.apply(&state.base);
+ }
+ }
+
+ var swapchain_manager: wlr.OutputSwapchainManager = undefined;
+ swapchain_manager.init(server.backend);
+ defer swapchain_manager.finish();
+
+ if (!swapchain_manager.prepare(states.items)) {
+ std.log.err("failed to prepare new output configuration", .{});
+ // TODO search for a working fallback
+
+ if (wm.sent.output_config) |config| {
+ config.sendFailed();
+ config.destroy();
+ wm.sent.output_config = null;
+ }
+
+ {
+ // Revert to last working state on failure
+ var it = wm.sent.outputs.iterator(.forward);
+ while (it.next()) |output| {
+ output.pending = output.current;
+ output.sent = output.current;
+ }
+ wm.dirtyPending();
+ }
+ return;
+ }
+
+ for (states.items) |*state| {
+ const output: *Output = @ptrFromInt(state.output.data);
+ if (!output.scene_output.?.buildState(&state.base, &.{
+ .swapchain = swapchain_manager.getSwapchain(state.output),
+ })) {
+ std.log.err("failed to render scene for {s}", .{state.output.name});
+ }
+ }
+
+ if (!server.backend.commit(states.items)) {
+ std.log.err("failed to commit new output configuration", .{});
+
+ if (wm.sent.output_config) |config| {
+ config.sendFailed();
+ config.destroy();
+ wm.sent.output_config = null;
+ }
+
+ {
+ // Revert to last working state on failure
+ var it = wm.sent.outputs.iterator(.forward);
+ while (it.next()) |output| {
+ output.pending = output.current;
+ output.sent = output.current;
+ }
+ wm.dirtyPending();
+ }
+ return;
+ }
+
+ swapchain_manager.apply();
+ }
+
+ if (wm.sent.output_config) |config| {
+ config.sendSucceeded();
+ config.destroy();
+ wm.sent.output_config = null;
+ }
+
+ {
+ var it = wm.sent.outputs.iterator(.forward);
+ while (it.next()) |output| {
+ output.current = output.sent;
+
+ if (output.wlr_output) |wlr_output| {
+ wlr_output.scheduleFrame();
+ }
+ }
+ }
+
+ // XXX sending this every transaction is too noisy
+ root.sendManagerConfig() catch {
+ std.log.err("out of memory", .{});
+ };
+}
+
+/// Send the current output state to all wlr-output-manager clients.
+fn sendManagerConfig(root: *Root) !void {
+ const config = try wlr.OutputConfigurationV1.create();
+ // this destroys all associated config heads as well
+ errdefer config.destroy();
+
+ var it = root.outputs.iterator(.forward);
+ while (it.next()) |output| {
+ const head = try wlr.OutputConfigurationV1.Head.create(config, output.wlr_output.?);
+
+ // It's only necessary to overwrite the state that does not require a modeset.
+ // All state that requires a modeset will have already been committed to the wlr_output.
+ head.state.enabled = switch (output.current.state) {
+ .enabled, .disabled_soft => true,
+ .disabled_hard => false,
+ .destroying => unreachable,
+ };
+ head.state.scale = output.current.scale;
+ head.state.transform = output.current.transform;
+ head.state.x = output.current.x;
+ head.state.y = output.current.y;
+ }
+
+ root.output_manager.setConfiguration(config);
}
diff --git a/river/Window.zig b/river/Window.zig
index 1c10342..c7635b2 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -141,7 +141,7 @@ pending: struct {
/// object.
init,
/// Indicates that the window is ready to be configured.
- /// Create a river_window_v1 object if needed an send events.
+ /// Create a river_window_v1 object if needed and send events.
ready,
/// Indicates that the closed event will be sent in the next update sequence.
closing,
diff --git a/river/WindowManager.zig b/river/WindowManager.zig
index acb2326..8b8dda4 100644
--- a/river/WindowManager.zig
+++ b/river/WindowManager.zig
@@ -19,11 +19,13 @@ const WindowManager = @This();
const std = @import("std");
const assert = std.debug.assert;
const wl = @import("wayland").server.wl;
+const wlr = @import("wlroots");
const river = @import("wayland").server.river;
const server = &@import("main.zig").server;
const util = @import("util.zig");
+const Output = @import("Output.zig");
const Window = @import("Window.zig");
const WmNode = @import("WmNode.zig");
@@ -48,11 +50,21 @@ state: union(enum) {
inflight_configures: u32,
} = .idle,
-/// Pending state from windows to be sent to the wm in the next update sequence.
+/// Pending state to be sent to the wm in the next update sequence.
pending: struct {
/// Pending state has been modified since the last update event sent to the wm.
dirty: bool = false,
+
dirty_windows: wl.list.Head(Window, .link_dirty),
+
+ outputs: wl.list.Head(Output, .link_pending),
+ output_config: ?*wlr.OutputConfigurationV1 = null,
+},
+
+/// State sent to the wm in the latest update sequence.
+sent: struct {
+ outputs: wl.list.Head(Output, .link_sent),
+ output_config: ?*wlr.OutputConfigurationV1 = null,
},
/// State sent by the wm but not yet committed with a commit request.
@@ -86,6 +98,10 @@ pub fn init(wm: *WindowManager) !void {
.windows = undefined,
.pending = .{
.dirty_windows = undefined,
+ .outputs = undefined,
+ },
+ .sent = .{
+ .outputs = undefined,
},
.uncommitted = .{
.render_list = undefined,
@@ -100,6 +116,8 @@ pub fn init(wm: *WindowManager) !void {
};
wm.windows.init();
wm.pending.dirty_windows.init();
+ wm.pending.outputs.init();
+ wm.sent.outputs.init();
wm.uncommitted.render_list.init();
wm.committed.render_list.init();
wm.inflight.render_list.init();
@@ -219,6 +237,22 @@ fn sendUpdate(wm: *WindowManager) void {
// XXX send all dirty pending state
+ wm.autoLayoutOutputs();
+ {
+ var it = wm.pending.outputs.safeIterator(.forward);
+ while (it.next()) |output| {
+ output.sendDirty() catch {
+ log.err("out of memory", .{});
+ continue; // Try again next update
+ };
+ output.link_pending.remove();
+ }
+ }
+
+ assert(wm.sent.output_config == null);
+ wm.sent.output_config = wm.pending.output_config;
+ wm.pending.output_config = null;
+
{
var it = wm.pending.dirty_windows.safeIterator(.forward);
while (it.next()) |window| {
@@ -238,6 +272,35 @@ fn sendUpdate(wm: *WindowManager) void {
wm.startTimeoutTimer();
}
+fn autoLayoutOutputs(wm: *WindowManager) void {
+ // Find the right most edge of any non-autolayout output.
+ var rightmost_edge: i32 = 0;
+ var row_y: i32 = 0;
+ {
+ var it = wm.pending.outputs.iterator(.forward);
+ while (it.next()) |output| {
+ if (output.pending.auto_layout) continue;
+
+ const x = output.pending.x + output.pending.width();
+ if (x > rightmost_edge) {
+ rightmost_edge = x;
+ row_y = output.pending.y;
+ }
+ }
+ }
+ // Place autolayout outputs in a row starting at the rightmost edge.
+ {
+ var it = wm.pending.outputs.iterator(.forward);
+ while (it.next()) |output| {
+ if (!output.pending.auto_layout) continue;
+
+ output.pending.x = rightmost_edge;
+ output.pending.y = row_y;
+ rightmost_edge += output.pending.width();
+ }
+ }
+}
+
fn sendConfigures(wm: *WindowManager) void {
switch (wm.state) {
.idle, .update_acked => {},
@@ -326,6 +389,8 @@ fn commitTransaction(wm: *WindowManager) void {
}
}
+ server.root.commitOutputState();
+
{
var it = server.input_manager.seats.first;
while (it) |node| : (it = node.next) node.data.cursor.updateState();