river/river/Output.zig
Isaac Freund 3865a7be7c
session-lock: fix assertion failures and clean up
The removed assertions aren't possible to guarantee due to the fact that
the lock render state is updated asynchronously as the output is
rendered.
2023-03-24 15:27:25 +01:00

536 lines
20 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, version 3.
//
// 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 std = @import("std");
const assert = std.debug.assert;
const math = std.math;
const mem = std.mem;
const fmt = std.fmt;
const wlr = @import("wlroots");
const wayland = @import("wayland");
const wl = wayland.server.wl;
const zwlr = wayland.server.zwlr;
const server = &@import("main.zig").server;
const util = @import("util.zig");
const LayerSurface = @import("LayerSurface.zig");
const Layout = @import("Layout.zig");
const LayoutDemand = @import("LayoutDemand.zig");
const LockSurface = @import("LockSurface.zig");
const OutputStatus = @import("OutputStatus.zig");
const SceneNodeData = @import("SceneNodeData.zig");
const View = @import("View.zig");
const log = std.log.scoped(.output);
wlr_output: *wlr.Output,
/// The area left for views and other layer surfaces after applying the
/// exclusive zones of exclusive layer surfaces.
/// TODO: this should be part of the output's State
usable_box: wlr.Box,
/// Scene node representing the entire output.
/// Position must be updated when the output is moved in the layout.
tree: *wlr.SceneTree,
normal_content: *wlr.SceneTree,
locked_content: *wlr.SceneTree,
/// Child nodes of normal_content
layers: struct {
background_color_rect: *wlr.SceneRect,
/// Background layer shell layer
background: *wlr.SceneTree,
/// Bottom layer shell layer
bottom: *wlr.SceneTree,
/// Views in the layout
layout: *wlr.SceneTree,
/// Floating views
float: *wlr.SceneTree,
/// Top layer shell layer
top: *wlr.SceneTree,
/// Fullscreen views
fullscreen: *wlr.SceneTree,
/// Overlay layer shell layer
overlay: *wlr.SceneTree,
/// xdg-popups of views and layer-shell surfaces
popups: *wlr.SceneTree,
},
/// 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.
lock_render_state: enum {
/// Submitted an unlocked buffer but the buffer has not yet been presented.
pending_unlock,
/// Normal, "unlocked" content may be visible.
unlocked,
/// Submitted a blank buffer but the buffer has not yet been presented.
/// Normal, "unlocked" content may be visible.
pending_blank,
/// A blank buffer has been presented.
blanked,
/// Submitted the lock surface buffer but the buffer has not yet been presented.
/// Normal, "unlocked" content may be visible.
pending_lock_surface,
/// The lock surface buffer has been presented.
lock_surface,
} = .blanked,
/// The state of the output that is directly acted upon/modified through user input.
///
/// Pending state will be copied to the inflight state and communicated to clients
/// to be applied as a single atomic transaction across all clients as soon as any
/// in progress transaction has been completed.
///
/// Any time pending state is modified Root.applyPending() must be called
/// before yielding back to the event loop.
pending: struct {
/// A bit field of focused tags
tags: u32 = 1 << 0,
/// The stack of views in focus/rendering order.
///
/// This contains views that aren't currently visible because they do not
/// match the tags of the output.
///
/// This list is used to update the rendering order of nodes in the scene
/// graph when the pending state is committed.
focus_stack: wl.list.Head(View, .pending_focus_stack_link),
/// The stack of views acted upon by window management commands such
/// as focus-view, zoom, etc.
///
/// This contains views that aren't currently visible because they do not
/// match the tags of the output. This means that a filtered version of the
/// list must be used for window management commands.
///
/// This includes both floating/fullscreen views and those arranged in the layout.
wm_stack: wl.list.Head(View, .pending_wm_stack_link),
},
/// The state most recently sent to the layout generator and clients.
/// This state is immutable until all clients have replied and the transaction
/// is completed, at which point this inflight state is copied to current.
inflight: struct {
/// A bit field of focused tags
tags: u32 = 1 << 0,
/// See pending.focus_stack
focus_stack: wl.list.Head(View, .inflight_focus_stack_link),
/// See pending.wm_stack
wm_stack: wl.list.Head(View, .inflight_wm_stack_link),
/// The view to be made fullscreen, if any.
fullscreen: ?*View = null,
layout_demand: ?LayoutDemand = null,
},
/// The current state represented by the scene graph.
/// There is no need to have a current focus_stack/wm_stack copy as this
/// information is transferred from the inflight state to the scene graph
/// as an inflight transaction completes.
current: struct {
/// A bit field of focused tags
tags: u32 = 1 << 0,
/// The currently fullscreen view, if any.
fullscreen: ?*View = null,
} = .{},
/// Remembered version of tags (from last run)
previous_tags: u32 = 1 << 0,
/// List of all layouts
layouts: std.TailQueue(Layout) = .{},
/// The current layout namespace of the output. If null,
/// config.default_layout_namespace should be used instead.
/// Call handleLayoutNamespaceChange() after setting this.
layout_namespace: ?[]const u8 = null,
/// The last set layout name.
layout_name: ?[:0]const u8 = null,
/// Active layout, or null if views are un-arranged.
///
/// If null, views which are manually moved or resized (with the pointer or
/// or command) will not be automatically set to floating. Everything is
/// already floating, so this would be an unexpected change of a views state
/// the user will only notice once a layout affects the views. So instead we
/// "snap back" all manually moved views the next time a layout is active.
/// This is similar to dwms behvaviour. Note that this of course does not
/// affect already floating views.
layout: ?*Layout = null,
status: OutputStatus,
destroy: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleDestroy),
enable: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleEnable),
mode: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleMode),
frame: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleFrame),
present: wl.Listener(*wlr.Output.event.Present) = wl.Listener(*wlr.Output.event.Present).init(handlePresent),
pub fn create(wlr_output: *wlr.Output) !void {
const node = try util.gpa.create(std.TailQueue(Self).Node);
errdefer util.gpa.destroy(node);
const output = &node.data;
if (!wlr_output.initRender(server.allocator, server.renderer)) return error.InitRenderFailed;
if (wlr_output.preferredMode()) |preferred_mode| {
wlr_output.setMode(preferred_mode);
wlr_output.enable(true);
wlr_output.commit() catch {
var it = wlr_output.modes.iterator(.forward);
while (it.next()) |mode| {
if (mode == preferred_mode) continue;
wlr_output.setMode(mode);
wlr_output.commit() catch continue;
// This mode works, use it
break;
}
// If no mode works, then we will just leave the output disabled.
// Perhaps the user will want to set a custom mode using wlr-output-management.
};
}
var width: c_int = undefined;
var height: c_int = undefined;
wlr_output.effectiveResolution(&width, &height);
const tree = try server.root.layers.outputs.createSceneTree();
const normal_content = try tree.createSceneTree();
output.* = .{
.wlr_output = wlr_output,
.tree = tree,
.normal_content = normal_content,
.locked_content = try tree.createSceneTree(),
.layers = .{
.background_color_rect = try normal_content.createSceneRect(
width,
height,
&server.config.background_color,
),
.background = try normal_content.createSceneTree(),
.bottom = try normal_content.createSceneTree(),
.layout = try normal_content.createSceneTree(),
.float = try normal_content.createSceneTree(),
.top = try normal_content.createSceneTree(),
.fullscreen = try normal_content.createSceneTree(),
.overlay = try normal_content.createSceneTree(),
.popups = try normal_content.createSceneTree(),
},
.pending = .{
.focus_stack = undefined,
.wm_stack = undefined,
},
.inflight = .{
.focus_stack = undefined,
.wm_stack = undefined,
},
.usable_box = .{
.x = 0,
.y = 0,
.width = width,
.height = height,
},
.status = undefined,
};
wlr_output.data = @ptrToInt(output);
output.pending.focus_stack.init();
output.pending.wm_stack.init();
output.inflight.focus_stack.init();
output.inflight.wm_stack.init();
output.status.init();
_ = try output.layers.fullscreen.createSceneRect(width, height, &[_]f32{ 0, 0, 0, 1.0 });
output.layers.fullscreen.node.setEnabled(false);
wlr_output.events.destroy.add(&output.destroy);
wlr_output.events.enable.add(&output.enable);
wlr_output.events.mode.add(&output.mode);
wlr_output.events.frame.add(&output.frame);
wlr_output.events.present.add(&output.present);
// Ensure that a cursor image at the output's scale factor is loaded
// for each seat.
var it = server.input_manager.seats.first;
while (it) |seat_node| : (it = seat_node.next) {
const seat = &seat_node.data;
seat.cursor.xcursor_manager.load(wlr_output.scale) catch
log.err("failed to load xcursor theme at scale {}", .{wlr_output.scale});
}
output.setTitle();
const ptr_node = try util.gpa.create(std.TailQueue(*Self).Node);
ptr_node.data = &node.data;
server.root.all_outputs.append(ptr_node);
handleEnable(&output.enable, wlr_output);
}
pub fn layerSurfaceTree(self: Self, layer: zwlr.LayerShellV1.Layer) *wlr.SceneTree {
const trees = [_]*wlr.SceneTree{
self.layers.background,
self.layers.bottom,
self.layers.top,
self.layers.overlay,
};
return trees[@intCast(usize, @enumToInt(layer))];
}
/// Arrange all layer surfaces of this output and adjust the usable area.
/// Will arrange views as well if the usable area changes.
/// Requires a call to Root.applyPending()
pub fn arrangeLayers(output: *Self) void {
var full_box: wlr.Box = .{
.x = 0,
.y = 0,
.width = undefined,
.height = undefined,
};
output.wlr_output.effectiveResolution(&full_box.width, &full_box.height);
// This box is modified as exclusive zones are applied
var usable_box = full_box;
// Ensure all exclusive zones are applied before arranging surfaces
// without exclusive zones.
output.sendLayerConfigures(full_box, &usable_box, .exclusive);
output.sendLayerConfigures(full_box, &usable_box, .non_exclusive);
output.usable_box = usable_box;
}
fn sendLayerConfigures(
output: *Self,
full_box: wlr.Box,
usable_box: *wlr.Box,
mode: enum { exclusive, non_exclusive },
) void {
for ([_]zwlr.LayerShellV1.Layer{ .background, .bottom, .top, .overlay }) |layer| {
const tree = output.layerSurfaceTree(layer);
var it = tree.children.iterator(.forward);
while (it.next()) |node| {
assert(node.type == .tree);
if (@intToPtr(?*SceneNodeData, node.data)) |node_data| {
const layer_surface = node_data.data.layer_surface;
const exclusive = layer_surface.wlr_layer_surface.current.exclusive_zone > 0;
if (exclusive != (mode == .exclusive)) continue;
layer_surface.scene_layer_surface.configure(&full_box, usable_box);
layer_surface.popup_tree.node.setPosition(
layer_surface.scene_layer_surface.tree.node.x,
layer_surface.scene_layer_surface.tree.node.y,
);
}
}
}
}
fn handleDestroy(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void {
const output = @fieldParentPtr(Self, "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.removeOutput(output);
assert(output.pending.focus_stack.empty());
assert(output.pending.wm_stack.empty());
assert(output.inflight.focus_stack.empty());
assert(output.inflight.wm_stack.empty());
assert(output.inflight.layout_demand == null);
assert(output.layouts.len == 0);
var it = server.root.all_outputs.first;
while (it) |all_node| : (it = all_node.next) {
if (all_node.data == output) {
server.root.all_outputs.remove(all_node);
break;
}
}
output.destroy.link.remove();
output.enable.link.remove();
output.frame.link.remove();
output.mode.link.remove();
output.present.link.remove();
output.tree.node.destroy();
if (output.layout_namespace) |namespace| util.gpa.free(namespace);
output.wlr_output.data = 0;
const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", output);
util.gpa.destroy(node);
server.root.applyPending();
}
fn handleEnable(listener: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) void {
const self = @fieldParentPtr(Self, "enable", listener);
// We can't assert the current state of normal_content/locked_content
// here as this output may be newly created.
if (wlr_output.enabled) {
switch (server.lock_manager.state) {
.unlocked => {
assert(self.lock_render_state == .blanked);
self.normal_content.node.setEnabled(true);
self.locked_content.node.setEnabled(false);
},
.waiting_for_lock_surfaces, .waiting_for_blank, .locked => {
assert(self.lock_render_state == .blanked);
self.normal_content.node.setEnabled(false);
self.locked_content.node.setEnabled(true);
},
}
} else {
// Disabling and re-enabling an output always blanks it.
self.lock_render_state = .blanked;
self.normal_content.node.setEnabled(false);
self.locked_content.node.setEnabled(true);
}
// Add the output to root.outputs and the output layout if it has not
// already been added.
if (wlr_output.enabled) server.root.addOutput(self);
}
fn handleMode(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void {
const output = @fieldParentPtr(Self, "mode", listener);
output.updateBackgroundRect();
output.arrangeLayers();
server.root.applyPending();
}
pub fn updateBackgroundRect(output: *Self) void {
var width: c_int = undefined;
var height: c_int = undefined;
output.wlr_output.effectiveResolution(&width, &height);
output.layers.background_color_rect.setSize(width, height);
var it = output.layers.fullscreen.children.iterator(.forward);
const fullscreen_background = @fieldParentPtr(wlr.SceneRect, "node", it.next().?);
fullscreen_background.setSize(width, height);
}
fn handleFrame(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void {
const output = @fieldParentPtr(Self, "frame", listener);
const scene_output = server.root.scene.getSceneOutput(output.wlr_output).?;
if (scene_output.commit()) {
if (server.lock_manager.state == .locked or
(server.lock_manager.state == .waiting_for_lock_surfaces and output.locked_content.node.enabled) or
server.lock_manager.state == .waiting_for_blank)
{
assert(!output.normal_content.node.enabled);
assert(output.locked_content.node.enabled);
switch (server.lock_manager.state) {
.unlocked => unreachable,
.locked => switch (output.lock_render_state) {
.pending_unlock, .unlocked, .pending_blank, .pending_lock_surface => unreachable,
.blanked, .lock_surface => {},
},
.waiting_for_blank => {
if (output.lock_render_state != .blanked) {
output.lock_render_state = .pending_blank;
}
},
.waiting_for_lock_surfaces => {
if (output.lock_render_state != .lock_surface) {
output.lock_render_state = .pending_lock_surface;
}
},
}
} else {
if (output.lock_render_state != .unlocked) {
output.lock_render_state = .pending_unlock;
}
}
} else {
log.err("output commit failed for {s}", .{output.wlr_output.name});
}
var now: std.os.timespec = undefined;
std.os.clock_gettime(std.os.CLOCK.MONOTONIC, &now) catch @panic("CLOCK_MONOTONIC not supported");
scene_output.sendFrameDone(&now);
}
fn handlePresent(
listener: *wl.Listener(*wlr.Output.event.Present),
event: *wlr.Output.event.Present,
) void {
const output = @fieldParentPtr(Self, "present", listener);
if (!event.presented) {
return;
}
switch (output.lock_render_state) {
.pending_unlock => {
assert(server.lock_manager.state != .locked);
output.lock_render_state = .unlocked;
},
.unlocked => assert(server.lock_manager.state != .locked),
.pending_blank, .pending_lock_surface => {
output.lock_render_state = switch (output.lock_render_state) {
.pending_blank => .blanked,
.pending_lock_surface => .lock_surface,
.pending_unlock, .unlocked, .blanked, .lock_surface => unreachable,
};
if (server.lock_manager.state != .locked) {
server.lock_manager.maybeLock();
}
},
.blanked, .lock_surface => {},
}
}
fn setTitle(self: Self) void {
const title = fmt.allocPrintZ(util.gpa, "river - {s}", .{self.wlr_output.name}) catch return;
defer util.gpa.free(title);
if (self.wlr_output.isWl()) {
self.wlr_output.wlSetTitle(title);
} else if (wlr.config.has_x11_backend and self.wlr_output.isX11()) {
self.wlr_output.x11SetTitle(title);
}
}
pub fn handleLayoutNamespaceChange(self: *Self) void {
// The user changed the layout namespace of this output. Try to find a
// matching layout.
var it = self.layouts.first;
self.layout = while (it) |node| : (it = node.next) {
if (mem.eql(u8, self.layoutNamespace(), node.data.namespace)) break &node.data;
} else null;
server.root.applyPending();
}
pub fn layoutNamespace(self: Self) []const u8 {
return self.layout_namespace orelse server.config.default_layout_namespace;
}