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("stable/xdg-shell/xdg-shell.xml");
scanner.addSystemProtocol("staging/cursor-shape/cursor-shape-v1.xml"); scanner.addSystemProtocol("staging/cursor-shape/cursor-shape-v1.xml");
scanner.addSystemProtocol("staging/ext-session-lock/ext-session-lock-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-constraints/pointer-constraints-unstable-v1.xml");
scanner.addSystemProtocol("unstable/pointer-gestures/pointer-gestures-unstable-v1.xml"); scanner.addSystemProtocol("unstable/pointer-gestures/pointer-gestures-unstable-v1.xml");
scanner.addSystemProtocol("unstable/tablet/tablet-unstable-v2.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("zxdg_decoration_manager_v1", 1);
scanner.generate("ext_session_lock_manager_v1", 1); scanner.generate("ext_session_lock_manager_v1", 1);
scanner.generate("wp_cursor_shape_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_control_v1", 1);
scanner.generate("zriver_status_manager_v1", 4); scanner.generate("zriver_status_manager_v1", 4);

View File

@ -12,8 +12,8 @@
.hash = "1220687c8c47a48ba285d26a05600f8700d37fc637e223ced3aa8324f3650bf52242", .hash = "1220687c8c47a48ba285d26a05600f8700d37fc637e223ced3aa8324f3650bf52242",
}, },
.@"zig-wlroots" = .{ .@"zig-wlroots" = .{
.url = "https://codeberg.org/ifreund/zig-wlroots/archive/ae6151f22ceb4ccd7efb1291dea573785918a7ec.tar.gz", .url = "https://codeberg.org/ifreund/zig-wlroots/archive/e486223799648d27e8b91c5fe0ea4c088b74b707.tar.gz",
.hash = "12204d99aebfbf88f1ff3ab197362937b3d4bef4f45fde9c4ee0d569e095a2a25889", .hash = "1220aeb3317e16c38583839961c9d695fa60d23a3d506c8275fb0e8fa9849844f2f7",
}, },
.@"zig-xkbcommon" = .{ .@"zig-xkbcommon" = .{
.url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/v0.2.0.tar.gz", .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. - *fullscreen*: Make the view fullscreen. Applies only to new views.
- *no-fullscreen*: Don't make the view fullscreen. Applies only to - *no-fullscreen*: Don't make the view fullscreen. Applies only to
new views. 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, Both *float* and *no-float* rules are added to the same list,
which means that adding a *no-float* rule with the same arguments 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 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 If multiple rules in a list match a given view the most specific
rule will be applied. For example with the following rules 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 Set the attach mode of the currently focused output, overriding the value of
default-attach-mode if any. 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_ *background-color* _0xRRGGBB_|_0xRRGGBBAA_
Set the background color. Set the background color.

View File

@ -31,6 +31,11 @@ const Mode = @import("Mode.zig");
const RuleList = @import("rule_list.zig").RuleList; const RuleList = @import("rule_list.zig").RuleList;
const View = @import("View.zig"); const View = @import("View.zig");
pub const AllowTearing = enum {
disabled,
enabled,
};
pub const AttachMode = union(enum) { pub const AttachMode = union(enum) {
top, top,
bottom, bottom,
@ -68,6 +73,9 @@ pub const Dimensions = struct {
height: u31, 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) /// 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 background_color: [4]f32 = [_]f32{ 0.0, 0.16862745, 0.21176471, 1.0 }, // Solarized base03
@ -98,6 +106,7 @@ rules: struct {
position: RuleList(Position) = .{}, position: RuleList(Position) = .{},
dimensions: RuleList(Dimensions) = .{}, dimensions: RuleList(Dimensions) = .{},
fullscreen: RuleList(bool) = .{}, fullscreen: RuleList(bool) = .{},
tearing: RuleList(bool) = .{},
} = .{}, } = .{},
/// The selected focus_follows_cursor mode /// 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 { fn renderAndCommit(output: *Output, scene_output: *wlr.SceneOutput) !void {
if (output.gamma_dirty) {
var state = wlr.Output.State.init(); var state = wlr.Output.State.init();
defer state.finish(); 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); const control = server.root.gamma_control_manager.getControl(output.wlr_output);
if (!wlr.GammaControlV1.apply(control, &state)) return error.OutOfMemory; 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. // has a null LUT. The wayland backend for example has this behavior.
state.committed.gamma_lut = false; 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; if (!output.wlr_output.commitState(&state)) return error.CommitFailed;
output.gamma_dirty = false; if (output.gamma_dirty) output.gamma_dirty = false;
} else {
if (!scene_output.commit(null)) return error.CommitFailed;
}
if (server.lock_manager.state == .locked or 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_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 { pub fn handleLayoutNamespaceChange(output: *Output) void {
// The user changed the layout namespace of this output. Try to find a // The user changed the layout namespace of this output. Try to find a
// matching layout. // matching layout.

View File

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

View File

@ -23,6 +23,7 @@ const math = std.math;
const posix = std.posix; const posix = std.posix;
const wlr = @import("wlroots"); const wlr = @import("wlroots");
const wl = @import("wayland").server.wl; const wl = @import("wayland").server.wl;
const wp = @import("wayland").server.wp;
const server = &@import("main.zig").server; const server = &@import("main.zig").server;
const util = @import("util.zig"); const util = @import("util.zig");
@ -59,6 +60,12 @@ const AttachRelativeMode = enum {
below, below,
}; };
const TearingMode = enum {
override_false,
override_true,
window_hint,
};
pub const State = struct { pub const State = struct {
/// The output the view is currently assigned to. /// The output the view is currently assigned to.
/// May be null if there are no outputs or for newly created views. /// 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. /// Connector name of the output this view occupied before an evacuation.
output_before_evac: ?[]const u8 = null, 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 { pub fn create(impl: Impl) error{OutOfMemory}!*View {
assert(impl != .none); 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 /// Clamp the width/height of the box to the constraints of the view
pub fn applyConstraints(view: *View, box: *wlr.Box) void { pub fn applyConstraints(view: *View, box: *wlr.Box) void {
box.width = math.clamp(box.width, view.constraints.min_width, view.constraints.max_width); 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; 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| { if (server.config.rules.dimensions.match(view)) |dimensions| {
view.pending.box.width = dimensions.width; view.pending.box.width = dimensions.width;
view.pending.box.height = dimensions.height; view.pending.box.height = dimensions.height;

View File

@ -41,6 +41,7 @@ const command_impls = std.StaticStringMap(
).initComptime( ).initComptime(
.{ .{
// zig fmt: off // zig fmt: off
.{ "allow-tearing", @import("command/config.zig").allowTearing },
.{ "attach-mode", @import("command/attach_mode.zig").defaultAttachMode }, .{ "attach-mode", @import("command/attach_mode.zig").defaultAttachMode },
.{ "background-color", @import("command/config.zig").backgroundColor }, .{ "background-color", @import("command/config.zig").backgroundColor },
.{ "border-color-focused", @import("command/config.zig").borderColorFocused }, .{ "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 Seat = @import("../Seat.zig");
const Config = @import("../Config.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( pub fn borderWidth(
_: *Seat, _: *Seat,
args: []const [:0]const u8, args: []const [:0]const u8,

View File

@ -38,6 +38,8 @@ const Action = enum {
dimensions, dimensions,
fullscreen, fullscreen,
@"no-fullscreen", @"no-fullscreen",
tearing,
@"no-tearing",
}; };
pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void { 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 action = std.meta.stringToEnum(Action, result.args[0]) orelse return Error.UnknownOption;
const positional_arguments_count: u8 = switch (action) { 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, .tags, .output => 2,
.position, .dimensions => 3, .position, .dimensions => 3,
}; };
@ -83,6 +85,14 @@ pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void
apply_ssd_rules(); apply_ssd_rules();
server.root.applyPending(); 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 => { .tags => {
const tags = try fmt.parseInt(u32, result.args[1], 10); const tags = try fmt.parseInt(u32, result.args[1], 10);
try server.config.rules.tags.add(.{ 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" => { .fullscreen, .@"no-fullscreen" => {
_ = server.config.rules.fullscreen.del(rule); _ = 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 { 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.NotEnoughArguments;
if (args.len > 2) return error.TooManyArguments; 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, position,
dimensions, dimensions,
fullscreen, fullscreen,
tearing,
}, args[1]) orelse return Error.UnknownOption; }, args[1]) orelse return Error.UnknownOption;
const max_glob_len = switch (rule_list) { const max_glob_len = switch (rule_list) {
inline else => |list| @field(server.config.rules, @tagName(list)).getMaxGlobLen(), 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"); try writer.writeAll("action\n");
switch (rule_list) { switch (rule_list) {
inline .float, .ssd, .output, .fullscreen => |list| { inline .float, .ssd, .output, .fullscreen, .tearing => |list| {
const rules = switch (list) { const rules = switch (list) {
.float => server.config.rules.float.rules.items, .float => server.config.rules.float.rules.items,
.ssd => server.config.rules.ssd.rules.items, .ssd => server.config.rules.ssd.rules.items,
.output => server.config.rules.output.rules.items, .output => server.config.rules.output.rules.items,
.fullscreen => server.config.rules.fullscreen.rules.items, .fullscreen => server.config.rules.fullscreen.rules.items,
.tearing => server.config.rules.tearing.rules.items,
else => unreachable, else => unreachable,
}; };
for (rules) |rule| { 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", .ssd => if (rule.value) "ssd" else "csd",
.output => rule.value, .output => rule.value,
.fullscreen => if (rule.value) "fullscreen" else "no-fullscreen", .fullscreen => if (rule.value) "fullscreen" else "no-fullscreen",
.tearing => if (rule.value) "tearing" else "no-tearing",
else => unreachable, else => unreachable,
}}); }});
} }