tearing-control-v1: implement

Implement the wp-tearing-control-v1 protocol allowing window to hint
the compositor that they prefer async "tearing" page flips.

Add tearing/no-tearing rules to allow the user to manually
enabled/disable tearing for a window.

Use async "tearing" page flips when a window that should be allowed to
tear is fullscreen.

This still requires several kernel patches to work with the wlroots
atomic DRM backend. For now, either set WLR_DRM_NO_ATOMIC=1 or use a
custom kernel that includes the unmerged patches (such as CachyOS).

Closes: https://codeberg.org/river/river/issues/1094
This commit is contained in:
Violet Purcell 2024-08-07 22:21:23 -04:00 committed by Isaac Freund
parent db7de8151c
commit 066baa5753
No known key found for this signature in database
GPG Key ID: 86DED400DDFD7A11
10 changed files with 120 additions and 16 deletions

View File

@ -94,6 +94,7 @@ pub fn build(b: *Build) !void {
scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml");
scanner.addSystemProtocol("staging/cursor-shape/cursor-shape-v1.xml");
scanner.addSystemProtocol("staging/ext-session-lock/ext-session-lock-v1.xml");
scanner.addSystemProtocol("staging/tearing-control/tearing-control-v1.xml");
scanner.addSystemProtocol("unstable/pointer-constraints/pointer-constraints-unstable-v1.xml");
scanner.addSystemProtocol("unstable/pointer-gestures/pointer-gestures-unstable-v1.xml");
scanner.addSystemProtocol("unstable/tablet/tablet-unstable-v2.xml");
@ -124,6 +125,7 @@ pub fn build(b: *Build) !void {
scanner.generate("zxdg_decoration_manager_v1", 1);
scanner.generate("ext_session_lock_manager_v1", 1);
scanner.generate("wp_cursor_shape_manager_v1", 1);
scanner.generate("wp_tearing_control_manager_v1", 1);
scanner.generate("zriver_control_v1", 1);
scanner.generate("zriver_status_manager_v1", 4);

View File

@ -12,8 +12,8 @@
.hash = "1220687c8c47a48ba285d26a05600f8700d37fc637e223ced3aa8324f3650bf52242",
},
.@"zig-wlroots" = .{
.url = "https://codeberg.org/ifreund/zig-wlroots/archive/ae6151f22ceb4ccd7efb1291dea573785918a7ec.tar.gz",
.hash = "12204d99aebfbf88f1ff3ab197362937b3d4bef4f45fde9c4ee0d569e095a2a25889",
.url = "https://codeberg.org/ifreund/zig-wlroots/archive/e486223799648d27e8b91c5fe0ea4c088b74b707.tar.gz",
.hash = "1220aeb3317e16c38583839961c9d695fa60d23a3d506c8275fb0e8fa9849844f2f7",
},
.@"zig-xkbcommon" = .{
.url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/v0.2.0.tar.gz",

View File

@ -307,11 +307,17 @@ matches everything while _\*\*_ and the empty string are invalid.
- *fullscreen*: Make the view fullscreen. Applies only to new views.
- *no-fullscreen*: Don't make the view fullscreen. Applies only to
new views.
- *tearing*: Enable tearing for view when fullscreen regardless of the
supplied tearing hint. Note that tearing additionally requires setting the
*allow-tearing* server option. Applies to new and existing views.
- *no-tearing*: Disable tearing for view regardless of the supplied
tearing hint. Applies to new and existing views.
Both *float* and *no-float* rules are added to the same list,
which means that adding a *no-float* rule with the same arguments
as a *float* rule will overwrite it. The same holds for *ssd* and
*csd*, *fullscreen* and *no-fullscreen* rules.
*csd*, *fullscreen* and *no-fullscreen*, *tearing* and
*no-tearing* rules.
If multiple rules in a list match a given view the most specific
rule will be applied. For example with the following rules
@ -364,6 +370,9 @@ matches everything while _\*\*_ and the empty string are invalid.
Set the attach mode of the currently focused output, overriding the value of
default-attach-mode if any.
*allow-tearing* *enabled*|*disabled*
Allow windows to tear if requested by either the program or the user.
*background-color* _0xRRGGBB_|_0xRRGGBBAA_
Set the background color.

View File

@ -31,6 +31,11 @@ const Mode = @import("Mode.zig");
const RuleList = @import("rule_list.zig").RuleList;
const View = @import("View.zig");
pub const AllowTearing = enum {
disabled,
enabled,
};
pub const AttachMode = union(enum) {
top,
bottom,
@ -68,6 +73,9 @@ pub const Dimensions = struct {
height: u31,
};
/// Whether to allow tearing page flips if a view requests it.
allow_tearing: AllowTearing = .disabled,
/// Color of background in RGBA with premultiplied alpha (alpha should only affect nested sessions)
background_color: [4]f32 = [_]f32{ 0.0, 0.16862745, 0.21176471, 1.0 }, // Solarized base03
@ -98,6 +106,7 @@ rules: struct {
position: RuleList(Position) = .{},
dimensions: RuleList(Dimensions) = .{},
fullscreen: RuleList(bool) = .{},
tearing: RuleList(bool) = .{},
} = .{},
/// The selected focus_follows_cursor mode

View File

@ -538,10 +538,12 @@ fn handleFrame(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void {
}
fn renderAndCommit(output: *Output, scene_output: *wlr.SceneOutput) !void {
if (output.gamma_dirty) {
var state = wlr.Output.State.init();
defer state.finish();
if (!scene_output.buildState(&state, null)) return error.CommitFailed;
if (output.gamma_dirty) {
const control = server.root.gamma_control_manager.getControl(output.wlr_output);
if (!wlr.GammaControlV1.apply(control, &state)) return error.OutOfMemory;
@ -553,15 +555,20 @@ 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.allowTearing() and server.config.allow_tearing == .enabled) {
state.tearing_page_flip = true;
if (!output.wlr_output.testState(&state)) {
log.debug("tearing page flip test failed for {s}, retrying without tearing", .{output.wlr_output.name});
state.tearing_page_flip = false;
}
}
if (!output.wlr_output.commitState(&state)) return error.CommitFailed;
output.gamma_dirty = false;
} else {
if (!scene_output.commit(null)) return error.CommitFailed;
}
if (output.gamma_dirty) output.gamma_dirty = false;
if (server.lock_manager.state == .locked or
(server.lock_manager.state == .waiting_for_lock_surfaces and output.locked_content.node.enabled) or
@ -635,6 +642,14 @@ fn setTitle(output: Output) void {
}
}
fn allowTearing(output: *Output) bool {
if (output.current.fullscreen) |fullscreen_view| {
return fullscreen_view.allowTearing();
}
return false;
}
pub fn handleLayoutNamespaceChange(output: *Output) void {
// The user changed the layout namespace of this output. Try to find a
// matching layout.

View File

@ -84,6 +84,8 @@ screencopy_manager: *wlr.ScreencopyManagerV1,
foreign_toplevel_manager: *wlr.ForeignToplevelManagerV1,
tearing_control_manager: *wlr.TearingControlManagerV1,
input_manager: InputManager,
root: Root,
config: Config,
@ -159,6 +161,8 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void {
.foreign_toplevel_manager = try wlr.ForeignToplevelManagerV1.create(wl_server),
.tearing_control_manager = try wlr.TearingControlManagerV1.create(wl_server, 1),
.config = try Config.init(),
.root = undefined,

View File

@ -23,6 +23,7 @@ const math = std.math;
const posix = std.posix;
const wlr = @import("wlroots");
const wl = @import("wayland").server.wl;
const wp = @import("wayland").server.wp;
const server = &@import("main.zig").server;
const util = @import("util.zig");
@ -59,6 +60,12 @@ const AttachRelativeMode = enum {
below,
};
const TearingMode = enum {
override_false,
override_true,
window_hint,
};
pub const State = struct {
/// The output the view is currently assigned to.
/// May be null if there are no outputs or for newly created views.
@ -177,6 +184,9 @@ foreign_toplevel_handle: ForeignToplevelHandle = .{},
/// Connector name of the output this view occupied before an evacuation.
output_before_evac: ?[]const u8 = null,
// Current tearing mode of the view. Defaults to using the window tearing hint.
tearing_mode: TearingMode = .window_hint,
pub fn create(impl: Impl) error{OutOfMemory}!*View {
assert(impl != .none);
@ -572,6 +582,17 @@ pub fn getAppId(view: View) ?[*:0]const u8 {
};
}
/// Return true if the view can currently tear.
pub fn allowTearing(view: *View) bool {
switch (view.tearing_mode) {
TearingMode.override_false => return false,
TearingMode.override_true => return true,
TearingMode.window_hint => if (view.rootSurface()) |root_surface| {
return server.tearing_control_manager.hintFromSurface(root_surface) == .@"async";
} else return false,
}
}
/// Clamp the width/height of the box to the constraints of the view
pub fn applyConstraints(view: *View, box: *wlr.Box) void {
box.width = math.clamp(box.width, view.constraints.min_width, view.constraints.max_width);
@ -640,6 +661,10 @@ pub fn map(view: *View) !void {
view.pending.ssd = ssd;
}
if (server.config.rules.tearing.match(view)) |tearing| {
view.tearing_mode = if (tearing) .override_true else .override_false;
}
if (server.config.rules.dimensions.match(view)) |dimensions| {
view.pending.box.width = dimensions.width;
view.pending.box.height = dimensions.height;

View File

@ -41,6 +41,7 @@ const command_impls = std.StaticStringMap(
).initComptime(
.{
// zig fmt: off
.{ "allow-tearing", @import("command/config.zig").allowTearing },
.{ "attach-mode", @import("command/attach_mode.zig").defaultAttachMode },
.{ "background-color", @import("command/config.zig").backgroundColor },
.{ "border-color-focused", @import("command/config.zig").borderColorFocused },

View File

@ -24,6 +24,17 @@ const Error = @import("../command.zig").Error;
const Seat = @import("../Seat.zig");
const Config = @import("../Config.zig");
pub fn allowTearing(
_: *Seat,
args: []const [:0]const u8,
_: *?[]const u8,
) Error!void {
if (args.len < 2) return Error.NotEnoughArguments;
if (args.len > 2) return Error.TooManyArguments;
server.config.allow_tearing = std.meta.stringToEnum(Config.AllowTearing, args[1]) orelse
return Error.UnknownOption;
}
pub fn borderWidth(
_: *Seat,
args: []const [:0]const u8,

View File

@ -38,6 +38,8 @@ const Action = enum {
dimensions,
fullscreen,
@"no-fullscreen",
tearing,
@"no-tearing",
};
pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void {
@ -53,7 +55,7 @@ pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void
const action = std.meta.stringToEnum(Action, result.args[0]) orelse return Error.UnknownOption;
const positional_arguments_count: u8 = switch (action) {
.float, .@"no-float", .ssd, .csd, .fullscreen, .@"no-fullscreen" => 1,
.float, .@"no-float", .ssd, .csd, .fullscreen, .@"no-fullscreen", .tearing, .@"no-tearing" => 1,
.tags, .output => 2,
.position, .dimensions => 3,
};
@ -83,6 +85,14 @@ pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void
apply_ssd_rules();
server.root.applyPending();
},
.tearing, .@"no-tearing" => {
try server.config.rules.tearing.add(.{
.app_id_glob = app_id_glob,
.title_glob = title_glob,
.value = (action == .tearing),
});
apply_tearing_rules();
},
.tags => {
const tags = try fmt.parseInt(u32, result.args[1], 10);
try server.config.rules.tags.add(.{
@ -177,6 +187,10 @@ pub fn ruleDel(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void
.fullscreen, .@"no-fullscreen" => {
_ = server.config.rules.fullscreen.del(rule);
},
.tearing, .@"no-tearing" => {
_ = server.config.rules.tearing.del(rule);
apply_tearing_rules();
},
}
}
@ -191,6 +205,17 @@ fn apply_ssd_rules() void {
}
}
fn apply_tearing_rules() void {
var it = server.root.views.iterator(.forward);
while (it.next()) |view| {
if (view.destroying) continue;
if (server.config.rules.tearing.match(view)) |tearing| {
view.tearing_mode = if (tearing) .override_true else .override_false;
}
}
}
pub fn listRules(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!void {
if (args.len < 2) return error.NotEnoughArguments;
if (args.len > 2) return error.TooManyArguments;
@ -203,6 +228,7 @@ pub fn listRules(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!
position,
dimensions,
fullscreen,
tearing,
}, args[1]) orelse return Error.UnknownOption;
const max_glob_len = switch (rule_list) {
inline else => |list| @field(server.config.rules, @tagName(list)).getMaxGlobLen(),
@ -218,12 +244,13 @@ pub fn listRules(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!
try writer.writeAll("action\n");
switch (rule_list) {
inline .float, .ssd, .output, .fullscreen => |list| {
inline .float, .ssd, .output, .fullscreen, .tearing => |list| {
const rules = switch (list) {
.float => server.config.rules.float.rules.items,
.ssd => server.config.rules.ssd.rules.items,
.output => server.config.rules.output.rules.items,
.fullscreen => server.config.rules.fullscreen.rules.items,
.tearing => server.config.rules.tearing.rules.items,
else => unreachable,
};
for (rules) |rule| {
@ -234,6 +261,7 @@ pub fn listRules(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!
.ssd => if (rule.value) "ssd" else "csd",
.output => rule.value,
.fullscreen => if (rule.value) "fullscreen" else "no-fullscreen",
.tearing => if (rule.value) "tearing" else "no-tearing",
else => unreachable,
}});
}