From c3b89860542a6f9c44374a465c165f3c07353054 Mon Sep 17 00:00:00 2001 From: Marten Ringwelski Date: Mon, 23 Nov 2020 14:58:33 +0100 Subject: [PATCH] Implement wlr_output_management_unstable_v1 --- river/Output.zig | 14 +++ river/OutputManager.zig | 209 +++++++++++++++++++++++++++++++++++++++- river/View.zig | 2 +- river/c.zig | 1 + 4 files changed, 223 insertions(+), 3 deletions(-) diff --git a/river/Output.zig b/river/Output.zig index 7e24008..244c1a5 100644 --- a/river/Output.zig +++ b/river/Output.zig @@ -78,6 +78,7 @@ active: bool = false, // All listeners for this output, in alphabetical order listen_destroy: c.wl_listener = undefined, +listen_enable: c.wl_listener = undefined, listen_frame: c.wl_listener = undefined, listen_mode: c.wl_listener = undefined, @@ -108,6 +109,9 @@ pub fn init(self: *Self, root: *Root, wlr_output: *c.wlr_output) !void { self.listen_destroy.notify = handleDestroy; c.wl_signal_add(&wlr_output.events.destroy, &self.listen_destroy); + self.listen_enable.notify = handleEnable; + c.wl_signal_add(&wlr_output.events.enable, &self.listen_enable); + self.listen_frame.notify = handleFrame; c.wl_signal_add(&wlr_output.events.frame, &self.listen_frame); @@ -523,6 +527,7 @@ fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { // Remove all listeners c.wl_list_remove(&self.listen_destroy.link); + c.wl_list_remove(&self.listen_enable.link); c.wl_list_remove(&self.listen_frame.link); c.wl_list_remove(&self.listen_mode.link); @@ -534,6 +539,15 @@ fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { util.gpa.destroy(node); } +fn handleEnable(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_enable", listener.?); + + if (self.wlr_output.enabled and !self.active) { + const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); + self.root.addOutput(node); + } +} + fn handleFrame(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { // This function is called every time an output is ready to display a frame, // generally at the output's refresh rate (e.g. 60Hz). diff --git a/river/OutputManager.zig b/river/OutputManager.zig index 208a2e6..009ef78 100644 --- a/river/OutputManager.zig +++ b/river/OutputManager.zig @@ -28,15 +28,33 @@ const Output = @import("Output.zig"); const Root = @import("Root.zig"); const Server = @import("Server.zig"); +// Minimum effective width/height for outputs. +// This is needed, to prevent integer overflows caused by the output effective +// resolution beeing too small to fit clients that can't get scaled more and +// thus will be bigger than the output resolution. +// The value is totally arbitrary and low enough, that it should never be +// encountered during normal usage. +const min_size = 50; + root: *Root, listen_new_output: c.wl_listener = undefined, +listen_output_layout_change: c.wl_listener = undefined, + +wlr_output_manager: *c.wlr_output_manager_v1, +listen_output_manager_apply: c.wl_listener = undefined, +listen_output_manager_test: c.wl_listener = undefined, wlr_output_power_manager: *c.wlr_output_power_manager_v1, listen_output_power_manager_set_mode: c.wl_listener = undefined, +/// True if and only if we are currently applying an output config +output_config_pending: bool = false, + pub fn init(self: *Self, server: *Server) !void { self.* = .{ + .wlr_output_manager = c.wlr_output_manager_v1_create(server.wl_display) orelse + return error.OutOfMemory, .wlr_output_power_manager = c.wlr_output_power_manager_v1_create(server.wl_display) orelse return error.OutOfMemory, .root = &server.root, @@ -45,6 +63,16 @@ pub fn init(self: *Self, server: *Server) !void { self.listen_new_output.notify = handleNewOutput; c.wl_signal_add(&server.wlr_backend.events.new_output, &self.listen_new_output); + // Set up wlr_output_management + self.listen_output_manager_apply.notify = handleOutputManagerApply; + c.wl_signal_add(&self.wlr_output_manager.events.apply, &self.listen_output_manager_apply); + self.listen_output_manager_test.notify = handleOutputManagerTest; + c.wl_signal_add(&self.wlr_output_manager.events.@"test", &self.listen_output_manager_test); + + // Listen for changes in the output layout to send them to the clients of wlr_output_manager + self.listen_output_layout_change.notify = handleOutputLayoutChange; + c.wl_signal_add(&self.root.wlr_output_layout.events.change, &self.listen_output_layout_change); + // Set up output power manager self.listen_output_power_manager_set_mode.notify = handleOutputPowerManagementSetMode; c.wl_signal_add(&self.wlr_output_power_manager.events.set_mode, &self.listen_output_power_manager_set_mode); @@ -77,6 +105,50 @@ fn handleNewOutput(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void self.root.addOutput(node); } +/// Sends the new output configuration to all clients of wlr_output_manager +fn handleOutputLayoutChange(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_output_layout_change", listener.?); + // Dont do anything if the layout change is coming from applying a config + if (self.output_config_pending) return; + + const config = self.createOutputConfigurationFromCurrent() catch { + log.err(.output_manager, "Could not create output configuration", .{}); + return; + }; + c.wlr_output_manager_v1_set_configuration(self.wlr_output_manager, config); +} + +fn handleOutputManagerApply(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_output_manager_apply", listener.?); + const config = util.voidCast(c.wlr_output_configuration_v1, data.?); + defer c.wlr_output_configuration_v1_destroy(config); + + if (self.applyOutputConfig(config)) { + c.wlr_output_configuration_v1_send_succeeded(config); + } else { + c.wlr_output_configuration_v1_send_failed(config); + } + + // Now send the config that actually was applied + const actualConfig = self.createOutputConfigurationFromCurrent() catch { + log.err(.output_manager, "Could not create output configuration", .{}); + return; + }; + c.wlr_output_manager_v1_set_configuration(self.wlr_output_manager, actualConfig); +} + +fn handleOutputManagerTest(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_output_manager_test", listener.?); + const config = util.voidCast(c.wlr_output_configuration_v1, data.?); + defer c.wlr_output_configuration_v1_destroy(config); + + if (testOutputConfig(config, true)) { + c.wlr_output_configuration_v1_send_succeeded(config); + } else { + c.wlr_output_configuration_v1_send_failed(config); + } +} + fn handleOutputPowerManagementSetMode(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { const self = @fieldParentPtr(Self, "listen_output_power_manager_set_mode", listener.?); const mode_event = util.voidCast(c.wlr_output_power_v1_set_mode_event, data.?); @@ -88,15 +160,148 @@ fn handleOutputPowerManagementSetMode(listener: ?*c.wl_listener, data: ?*c_void) log.debug( .output_manager, "{} dpms for output {}", - .{log_text, wlr_output.name}, + .{ log_text, wlr_output.name }, ); c.wlr_output_enable(wlr_output, enable); if (!c.wlr_output_commit(wlr_output)) { log.err( - .server, + .output_manager, "wlr_output_commit failed for {}", .{wlr_output.name}, ); } } + +/// Applies an output config +fn applyOutputConfig(self: *Self, config: *c.wlr_output_configuration_v1) bool { + // We need to store whether a config is pending because we listen to wlr_output_layout.change + // and this event can be triggered by applying the config + self.output_config_pending = true; + defer self.output_config_pending = false; + + // Test if the config should apply cleanly + if (!testOutputConfig(config, false)) return false; + + const list_head: *c.wl_list = &config.heads; + var it: *c.wl_list = list_head.next; + while (it != list_head) : (it = it.next) { + const head = @fieldParentPtr(c.wlr_output_configuration_head_v1, "link", it); + const output = util.voidCast(Output, @as(*c.wlr_output, head.state.output).data.?); + const disable = output.wlr_output.enabled and !head.state.enabled; + + // This commit will only fail due to runtime errors. + // We choose to ignore this error + if (!c.wlr_output_commit(output.wlr_output)) { + log.err(.output_manager, "wlr_output_commit failed for {}", .{output.wlr_output.name}); + } + + if (output.wlr_output.enabled) { + // Moves the output if it is already in the layout + c.wlr_output_layout_add(self.root.wlr_output_layout, output.wlr_output, head.state.x, head.state.y); + } + + if (disable) { + const node = @fieldParentPtr(std.TailQueue(Output).Node, "data", output); + self.root.removeOutput(node); + c.wlr_output_layout_remove(self.root.wlr_output_layout, output.wlr_output); + } + // Arrange layers to adjust the usable_box + // We dont need to call arrangeViews() since arrangeLayers() will call + // it for us because the usable_box changed + output.arrangeLayers(); + self.root.startTransaction(); + } + + return true; +} + +/// Tests the output configuration. +/// If rollback is false all changes are applied to the pending state of the affected outputs. +fn testOutputConfig(config: *c.wlr_output_configuration_v1, rollback: bool) bool { + var ok = true; + const list_head: *c.wl_list = &config.heads; + var it: *c.wl_list = list_head.next; + while (it != list_head) : (it = it.next) { + const head = @fieldParentPtr(c.wlr_output_configuration_head_v1, "link", it); + const wlr_output = @as(*c.wlr_output, head.state.output); + + const width = if (@as(?*c.wlr_output_mode, head.state.mode)) |m| m.width else head.state.custom_mode.width; + const height = if (@as(?*c.wlr_output_mode, head.state.mode)) |m| m.height else head.state.custom_mode.height; + const scale = head.state.scale; + + const too_small = (@intToFloat(f32, width) / scale < min_size) or + (@intToFloat(f32, height) / scale < min_size); + + if (too_small) { + log.info( + .output_manager, + "The requested output resolution {}x{} scaled with {} for {} would be too small.", + .{ width, height, scale, wlr_output.name }, + ); + } + + applyHeadToOutput(head, wlr_output); + ok = ok and !too_small and c.wlr_output_test(wlr_output); + } + + if (rollback or !ok) { + // Rollback all changes + it = list_head.next; + while (it != list_head) : (it = it.next) { + const head = @fieldParentPtr(c.wlr_output_configuration_head_v1, "link", it); + const wlr_output = @as(*c.wlr_output, head.state.output); + c.wlr_output_rollback(wlr_output); + } + } + + return ok; +} + +fn applyHeadToOutput(head: *c.wlr_output_configuration_head_v1, wlr_output: *c.wlr_output) void { + c.wlr_output_enable(wlr_output, head.state.enabled); + // The output must be enabled for the following properties to apply + if (head.state.enabled) { + // TODO(wlroots) Somehow on the drm backend setting the mode causes + // the commit in the rendering loop to fail. The commit that + // applies the mode works fine. + // We can just ignore this because nothing bad happens but it + // should be fixed in the future + // See https://github.com/swaywm/wlroots/issues/2492 + if (head.state.mode != null) { + c.wlr_output_set_mode(wlr_output, head.state.mode); + } else { + const custom_mode = &head.state.custom_mode; + c.wlr_output_set_custom_mode(wlr_output, custom_mode.width, custom_mode.height, custom_mode.refresh); + } + // TODO(wlroots) Figure out if this conversion is needed or if that is a bug in wlroots + c.wlr_output_set_scale(wlr_output, @floatCast(f32, head.state.scale)); + c.wlr_output_set_transform(wlr_output, head.state.transform); + } +} + +/// Creates an wlr_output_configuration from the current configuration +fn createOutputConfigurationFromCurrent(self: *Self) !*c.wlr_output_configuration_v1 { + var config = c.wlr_output_configuration_v1_create() orelse return error.OutOfMemory; + errdefer c.wlr_output_configuration_v1_destroy(config); + + var it = self.root.all_outputs.first; + while (it) |node| : (it = node.next) { + try self.createHead(node.data, config); + } + + return config; +} + +fn createHead(self: *Self, output: *Output, config: *c.wlr_output_configuration_v1) !void { + const wlr_output = output.wlr_output; + const head: *c.wlr_output_configuration_head_v1 = c.wlr_output_configuration_head_v1_create(config, wlr_output) orelse + return error.OutOfMemory; + + // If the output is not part of the layout (and thus disabled) we dont care about the position + const box = @as(?*c.wlr_box, c.wlr_output_layout_get_box(self.root.wlr_output_layout, wlr_output)); + if (box) |b| { + head.state.x = b.x; + head.state.y = b.y; + } +} diff --git a/river/View.zig b/river/View.zig index 6992049..f049895 100644 --- a/river/View.zig +++ b/river/View.zig @@ -52,7 +52,7 @@ const Impl = union(enum) { }; const State = struct { - /// The output-relative coordinates and dimensions of the view. The + /// The output-relative effective coordinates and effective dimensions of the view. The /// surface itself may have other dimensions which are stored in the /// surface_box member. box: Box = Box{ .x = 0, .y = 0, .width = 0, .height = 0 }, diff --git a/river/c.zig b/river/c.zig index 26363dc..306ae25 100644 --- a/river/c.zig +++ b/river/c.zig @@ -46,6 +46,7 @@ pub usingnamespace @cImport({ @cInclude("wlr/types/wlr_matrix.h"); @cInclude("wlr/types/wlr_output.h"); @cInclude("wlr/types/wlr_output_layout.h"); + @cInclude("wlr/types/wlr_output_management_v1.h"); @cInclude("wlr/types/wlr_output_power_management_v1.h"); @cInclude("wlr/types/wlr_pointer.h"); @cInclude("wlr/types/wlr_primary_selection.h");