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:
		
				
					committed by
					
						 Isaac Freund
						Isaac Freund
					
				
			
			
				
	
			
			
			
						parent
						
							db7de8151c
						
					
				
				
					commit
					066baa5753
				
			| @ -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); | ||||
|  | ||||
| @ -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", | ||||
|  | ||||
| @ -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. | ||||
|  | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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(); | ||||
|     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,16 +555,21 @@ 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.wlr_output.commitState(&state)) return error.CommitFailed; | ||||
|  | ||||
|         output.gamma_dirty = false; | ||||
|     } else { | ||||
|         if (!scene_output.commit(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.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 | ||||
|         server.lock_manager.state == .waiting_for_blank) | ||||
| @ -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. | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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 }, | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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, | ||||
|                 }}); | ||||
|             } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user