river/river/Root.zig
Isaac Freund 9270a2df08
View: fix unmap/destroy control flow
Currently the view destruction sequence is started as soon as a view
is unmapped. However, this is incorrect as a client may map the view
again instead of destroying it.

Instead, only start the view destruction sequence when the underlying
xdg toplevel or xwayland surface is destroyed.
2021-10-02 14:48:22 +02:00

603 lines
22 KiB
Zig

// This file is part of river, a dynamic tiling wayland compositor.
//
// Copyright 2020 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
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const Self = @This();
const build_options = @import("build_options");
const std = @import("std");
const mem = std.mem;
const assert = std.debug.assert;
const wlr = @import("wlroots");
const wl = @import("wayland").server.wl;
const server = &@import("main.zig").server;
const util = @import("util.zig");
const Output = @import("Output.zig");
const View = @import("View.zig");
const ViewStack = @import("view_stack.zig").ViewStack;
const XwaylandUnmanaged = @import("XwaylandUnmanaged.zig");
const DragIcon = @import("DragIcon.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;
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),
output_manager: *wlr.OutputManagerV1,
manager_apply: wl.Listener(*wlr.OutputConfigurationV1) =
wl.Listener(*wlr.OutputConfigurationV1).init(handleManagerApply),
manager_test: wl.Listener(*wlr.OutputConfigurationV1) =
wl.Listener(*wlr.OutputConfigurationV1).init(handleManagerTest),
power_manager: *wlr.OutputPowerManagerV1,
power_manager_set_mode: wl.Listener(*wlr.OutputPowerManagerV1.event.SetMode) =
wl.Listener(*wlr.OutputPowerManagerV1.event.SetMode).init(handlePowerManagerSetMode),
/// A list of all outputs
all_outputs: std.TailQueue(*Output) = .{},
/// A list of all active outputs. See Output.active
outputs: std.TailQueue(Output) = .{},
/// This output is used internally when no real outputs are available.
/// It is not advertised to clients.
noop_output: Output = undefined,
drag_icons: std.SinglyLinkedList(DragIcon) = .{},
/// This list stores all unmanaged Xwayland windows. This needs to be in root
/// since X is like the wild west and who knows where these things will go.
xwayland_unmanaged_views: if (build_options.xwayland)
std.TailQueue(XwaylandUnmanaged)
else
void = if (build_options.xwayland)
.{},
/// Number of layout demands pending before the transaction may be started.
pending_layout_demands: u32 = 0,
/// Number of pending configures sent in the current transaction.
/// A value of 0 means there is no current transaction.
pending_configures: u32 = 0,
/// Handles timeout of transactions
transaction_timer: *wl.EventSource,
pub fn init(self: *Self) !void {
const output_layout = try wlr.OutputLayout.create();
errdefer output_layout.destroy();
_ = try wlr.XdgOutputManagerV1.create(server.wl_server, output_layout);
const event_loop = server.wl_server.getEventLoop();
const transaction_timer = try event_loop.addTimer(*Self, handleTransactionTimeout, self);
errdefer transaction_timer.remove();
const noop_wlr_output = try server.noop_backend.noopAddOutput();
self.* = .{
.output_layout = output_layout,
.output_manager = try wlr.OutputManagerV1.create(server.wl_server),
.power_manager = try wlr.OutputPowerManagerV1.create(server.wl_server),
.transaction_timer = transaction_timer,
.noop_output = .{
.wlr_output = noop_wlr_output,
// TODO: find a good way to not create a wlr.OutputDamage for the noop output
.damage = try wlr.OutputDamage.create(noop_wlr_output),
.usable_box = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
},
};
noop_wlr_output.data = @ptrToInt(&self.noop_output);
server.backend.events.new_output.add(&self.new_output);
self.output_manager.events.apply.add(&self.manager_apply);
self.output_manager.events.@"test".add(&self.manager_test);
self.output_layout.events.change.add(&self.layout_change);
self.power_manager.events.set_mode.add(&self.power_manager_set_mode);
}
pub fn deinit(self: *Self) void {
self.output_layout.destroy();
self.transaction_timer.remove();
}
fn handleNewOutput(listener: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) void {
const self = @fieldParentPtr(Self, "new_output", listener);
std.log.scoped(.output_manager).debug("new output {s}", .{mem.sliceTo(&wlr_output.name, 0)});
const node = util.gpa.create(std.TailQueue(Output).Node) catch {
wlr_output.destroy();
return;
};
node.data.init(wlr_output) catch {
wlr_output.destroy();
util.gpa.destroy(node);
return;
};
const ptr_node = util.gpa.create(std.TailQueue(*Output).Node) catch {
wlr_output.destroy();
util.gpa.destroy(node);
return;
};
ptr_node.data = &node.data;
self.all_outputs.append(ptr_node);
self.addOutput(&node.data);
}
/// Remove the output from self.outputs and evacuate views if it is a member of
/// the list. The node is not freed
pub fn removeOutput(self: *Self, output: *Output) void {
const node = @fieldParentPtr(std.TailQueue(Output).Node, "data", output);
// If the node has already been removed, do nothing
var output_it = self.outputs.first;
while (output_it) |n| : (output_it = n.next) {
if (n == node) break;
} else return;
self.outputs.remove(node);
// Use the first output in the list as fallback. If the last real output
// is being removed, use the noop output.
const fallback_output = blk: {
if (self.outputs.first) |output_node| {
break :blk &output_node.data;
} else {
// Store the focused output tags if we are hotplugged down to
// 0 real outputs so they can be restored on gaining a new output.
self.noop_output.current.tags = output.current.tags;
break :blk &self.noop_output;
}
};
// Move all views from the destroyed output to the fallback one
while (output.views.last) |view_node| {
const view = &view_node.view;
view.sendToOutput(fallback_output);
}
// Close all layer surfaces on the removed output
for (output.layers) |*layer| {
// Closing the layer surface will cause LayerSurface.handleUnmap()
// to be called synchronously, which will remove it from this list.
while (layer.first) |layer_node| {
const layer_surface = &layer_node.data;
layer_surface.wlr_layer_surface.close();
layer_surface.wlr_layer_surface.output = null;
layer_surface.output = undefined;
}
}
// If any seat has the removed output focused, focus the fallback one
var seat_it = server.input_manager.seats.first;
while (seat_it) |seat_node| : (seat_it = seat_node.next) {
const seat = &seat_node.data;
if (seat.focused_output == output) {
seat.focusOutput(fallback_output);
seat.focus(null);
}
}
// Destroy all layouts of the output
while (output.layouts.first) |layout_node| layout_node.data.destroy();
while (output.status_trackers.first) |status_node| status_node.data.destroy();
// Arrange the root in case evacuated views affect the layout
fallback_output.arrangeViews();
self.startTransaction();
}
/// Add the output to self.outputs and the output layout if it has not
/// already been added.
pub fn addOutput(self: *Self, output: *Output) void {
const node = @fieldParentPtr(std.TailQueue(Output).Node, "data", output);
// If we have already added the output, do nothing and return
var output_it = self.outputs.first;
while (output_it) |n| : (output_it = n.next) if (n == node) return;
self.outputs.append(node);
// This aarranges 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.
self.output_layout.addAuto(node.data.wlr_output);
// If we previously had no real outputs, move focus from the noop output
// to the new one.
if (self.outputs.len == 1) {
// Restore the focused tags of the last output to be removed
output.pending.tags = self.noop_output.current.tags;
output.current.tags = self.noop_output.current.tags;
// Move all views from noop output to the new output
while (self.noop_output.views.last) |n| n.view.sendToOutput(output);
// Focus the new output with all seats
var it = server.input_manager.seats.first;
while (it) |seat_node| : (it = seat_node.next) {
const seat = &seat_node.data;
seat.focusOutput(output);
seat.focus(null);
}
}
}
/// Arrange all views on all outputs
pub fn arrangeAll(self: *Self) void {
var it = self.outputs.first;
while (it) |node| : (it = node.next) node.data.arrangeViews();
}
/// Record the number of currently pending layout demands so that a transaction
/// can be started once all are either complete or have timed out.
pub fn trackLayoutDemands(self: *Self) void {
self.pending_layout_demands = 0;
var it = self.outputs.first;
while (it) |node| : (it = node.next) {
if (node.data.layout_demand != null) self.pending_layout_demands += 1;
}
assert(self.pending_layout_demands > 0);
}
/// This function is used to inform the transaction system that a layout demand
/// has either been completed or timed out. If it was the last pending layout
/// demand in the current sequence, a transaction is started.
pub fn notifyLayoutDemandDone(self: *Self) void {
self.pending_layout_demands -= 1;
if (self.pending_layout_demands == 0) self.startTransaction();
}
/// Initiate an atomic change to the layout. This change will not be
/// applied until all affected clients ack a configure and commit a buffer.
pub fn startTransaction(self: *Self) void {
// If one or more layout demands are currently in progress, postpone
// transactions until they complete. Every frame must be perfect.
if (self.pending_layout_demands > 0) return;
// If a new transaction is started while another is in progress, we need
// to reset the pending count to 0 and clear serials from the views
const preempting = self.pending_configures > 0;
self.pending_configures = 0;
// Iterate over all views of all outputs
var output_it = self.outputs.first;
while (output_it) |output_node| : (output_it = output_node.next) {
var view_it = output_node.data.views.first;
while (view_it) |view_node| : (view_it = view_node.next) {
const view = &view_node.view;
if (view.surface == null) continue;
if (view.shouldTrackConfigure()) {
// Clear the serial in case this transaction is interrupting a prior one.
view.pending_serial = null;
if (view.needsConfigure()) {
view.configure();
self.pending_configures += 1;
// Send a frame done that the client will commit a new frame
// with the dimensions we sent in the configure. Normally this
// event would be sent in the render function.
view.sendFrameDone();
}
// If there are saved buffers present, then this transaction is interrupting
// a previous transaction and we should keep the old buffers.
if (view.saved_buffers.items.len == 0) view.saveBuffers();
} else {
if (view.needsConfigure()) view.configure();
}
}
}
if (self.pending_configures > 0) {
std.log.scoped(.transaction).debug("started transaction with {} pending configure(s)", .{
self.pending_configures,
});
// Timeout the transaction after 200ms. If we are preempting an
// already in progress transaction, don't extend the timeout.
if (!preempting) {
self.transaction_timer.timerUpdate(200) catch {
std.log.scoped(.transaction).err("failed to update timer", .{});
self.commitTransaction();
};
}
} else {
// No views need configures, clear the current timer in case we are
// interrupting another transaction and commit.
self.transaction_timer.timerUpdate(0) catch std.log.scoped(.transaction).err("error disarming timer", .{});
self.commitTransaction();
}
}
fn handleTransactionTimeout(self: *Self) callconv(.C) c_int {
std.log.scoped(.transaction).err("timeout occurred, some imperfect frames may be shown", .{});
self.pending_configures = 0;
self.commitTransaction();
return 0;
}
pub fn notifyConfigured(self: *Self) void {
self.pending_configures -= 1;
if (self.pending_configures == 0) {
// Disarm the timer, as we didn't timeout
self.transaction_timer.timerUpdate(0) catch std.log.scoped(.transaction).err("error disarming timer", .{});
self.commitTransaction();
}
}
/// Apply the pending state and drop stashed buffers. This means that
/// the next frame drawn will be the post-transaction state of the
/// layout. Should only be called after all clients have configured for
/// the new layout. If called early imperfect frames may be drawn.
fn commitTransaction(self: *Self) void {
assert(self.pending_configures == 0);
// Iterate over all views of all outputs
var output_it = self.outputs.first;
while (output_it) |output_node| : (output_it = output_node.next) {
const output = &output_node.data;
// Apply pending state of the output
if (output.pending.tags != output.current.tags) {
std.log.scoped(.output).debug(
"changing current focus: {b:0>10} to {b:0>10}",
.{ output.current.tags, output.pending.tags },
);
var it = output.status_trackers.first;
while (it) |node| : (it = node.next) node.data.sendFocusedTags(output.pending.tags);
}
output.current = output.pending;
var view_tags_changed = false;
var urgent_tags_dirty = false;
var view_it = output.views.first;
while (view_it) |view_node| {
const view = &view_node.view;
view_it = view_node.next;
if (view.surface == null) {
view.dropSavedBuffers();
view.output.views.remove(view_node);
if (view.destroying) view.destroy();
continue;
}
assert(!view.destroying);
if (view.pending_serial != null and !view.shouldTrackConfigure()) continue;
// Apply pending state of the view
view.pending_serial = null;
if (view.pending.tags != view.current.tags) view_tags_changed = true;
if (view.pending.urgent != view.current.urgent) urgent_tags_dirty = true;
if (view.pending.urgent and view_tags_changed) urgent_tags_dirty = true;
view.current = view.pending;
view.dropSavedBuffers();
}
if (view_tags_changed) output.sendViewTags();
if (urgent_tags_dirty) output.sendUrgentTags();
output.damage.addWhole();
}
server.input_manager.updateCursorState();
}
/// Send the new output configuration to all wlr-output-manager clients
fn handleLayoutChange(
listener: *wl.Listener(*wlr.OutputLayout),
output_layout: *wlr.OutputLayout,
) void {
const self = @fieldParentPtr(Self, "layout_change", listener);
const config = self.outputConfigFromCurrent() catch {
std.log.scoped(.output_manager).crit("out of memory", .{});
return;
};
self.output_manager.setConfiguration(config);
}
fn handleManagerApply(
listener: *wl.Listener(*wlr.OutputConfigurationV1),
config: *wlr.OutputConfigurationV1,
) void {
const self = @fieldParentPtr(Self, "manager_apply", listener);
defer config.destroy();
if (self.applyOutputConfig(config)) {
config.sendSucceeded();
} else {
config.sendFailed();
}
// Send the config that was actually applied
const applied_config = self.outputConfigFromCurrent() catch {
std.log.scoped(.output_manager).crit("out of memory", .{});
return;
};
self.output_manager.setConfiguration(applied_config);
}
fn handleManagerTest(
listener: *wl.Listener(*wlr.OutputConfigurationV1),
config: *wlr.OutputConfigurationV1,
) void {
const self = @fieldParentPtr(Self, "manager_test", listener);
defer config.destroy();
if (testOutputConfig(config, true)) {
config.sendSucceeded();
} else {
config.sendFailed();
}
}
/// Apply the given config, return false on faliure
fn applyOutputConfig(self: *Self, config: *wlr.OutputConfigurationV1) bool {
// Ignore layout change events while applying the config
self.layout_change.link.remove();
defer self.output_layout.events.change.add(&self.layout_change);
// Test if the config should apply cleanly
if (!testOutputConfig(config, false)) return false;
var it = config.heads.iterator(.forward);
while (it.next()) |head| {
const output = @intToPtr(*Output, head.state.output.data);
const disable = output.wlr_output.enabled and !head.state.enabled;
// Since we have done a successful test commit, this will only fail
// due to error in the output's backend implementation.
output.wlr_output.commit() catch
std.log.scoped(.output_manager).err("output commit failed for {s}", .{mem.sliceTo(&output.wlr_output.name, 0)});
if (output.wlr_output.enabled) {
// Moves the output if it is already in the layout
self.output_layout.add(output.wlr_output, head.state.x, head.state.y);
}
if (disable) {
self.removeOutput(output);
self.output_layout.remove(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(.mapped);
self.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: *wlr.OutputConfigurationV1, rollback: bool) bool {
var ok = true;
var it = config.heads.iterator(.forward);
while (it.next()) |head| {
const wlr_output = head.state.output;
const width = if (head.state.mode) |m| m.width else head.state.custom_mode.width;
const height = if (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) {
std.log.scoped(.output_manager).info(
"The requested output resolution {}x{} scaled with {} for {s} would be too small.",
.{ width, height, scale, mem.sliceTo(&wlr_output.name, 0) },
);
}
applyHeadToOutput(head, wlr_output);
ok = ok and !too_small and wlr_output.testCommit();
}
if (rollback or !ok) {
// Rollback all changes
it = config.heads.iterator(.forward);
while (it.next()) |head| head.state.output.rollback();
}
return ok;
}
fn applyHeadToOutput(head: *wlr.OutputConfigurationV1.Head, wlr_output: *wlr.Output) void {
wlr_output.enable(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) |mode| {
wlr_output.setMode(mode);
} else {
const custom_mode = &head.state.custom_mode;
wlr_output.setCustomMode(custom_mode.width, custom_mode.height, custom_mode.refresh);
}
wlr_output.setScale(head.state.scale);
wlr_output.setTransform(head.state.transform);
}
}
/// Create the config describing the current configuration
fn outputConfigFromCurrent(self: *Self) !*wlr.OutputConfigurationV1 {
const config = try wlr.OutputConfigurationV1.create();
// this destroys all associated config heads as well
errdefer config.destroy();
var it = self.all_outputs.first;
while (it) |node| : (it = node.next) try self.createHead(node.data, config);
return config;
}
fn createHead(self: *Self, output: *Output, config: *wlr.OutputConfigurationV1) !void {
const head = try wlr.OutputConfigurationV1.Head.create(config, output.wlr_output);
// If the output is not part of the layout (and thus disabled) we dont care
// about the position
if (self.output_layout.getBox(output.wlr_output)) |box| {
head.state.x = box.x;
head.state.y = box.y;
}
}
fn handlePowerManagerSetMode(
listener: *wl.Listener(*wlr.OutputPowerManagerV1.event.SetMode),
event: *wlr.OutputPowerManagerV1.event.SetMode,
) void {
const self = @fieldParentPtr(Self, "power_manager_set_mode", listener);
const enable = event.mode == .on;
const log_text = if (enable) "Enabling" else "Disabling";
std.log.scoped(.output_manager).debug(
"{s} dpms for output {s}",
.{ log_text, mem.sliceTo(&event.output.name, 0) },
);
event.output.enable(enable);
event.output.commit() catch {
std.log.scoped(.server).err("output commit failed for {s}", .{mem.sliceTo(&event.output.name, 0)});
};
}