diff --git a/build.zig b/build.zig index 93dafd3..7b97593 100644 --- a/build.zig +++ b/build.zig @@ -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); diff --git a/build.zig.zon b/build.zig.zon index c70ba55..b35b27e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -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", diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd index a40eb90..68008fa 100644 --- a/doc/riverctl.1.scd +++ b/doc/riverctl.1.scd @@ -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. diff --git a/river/Config.zig b/river/Config.zig index 0b54f77..ed34617 100644 --- a/river/Config.zig +++ b/river/Config.zig @@ -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 diff --git a/river/Output.zig b/river/Output.zig index 77f0eb8..d7f087f 100644 --- a/river/Output.zig +++ b/river/Output.zig @@ -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. diff --git a/river/Server.zig b/river/Server.zig index 6b5d12f..2fbc97c 100644 --- a/river/Server.zig +++ b/river/Server.zig @@ -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, diff --git a/river/View.zig b/river/View.zig index bb02da0..7ecceb4 100644 --- a/river/View.zig +++ b/river/View.zig @@ -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; diff --git a/river/command.zig b/river/command.zig index 6ea5b96..85a5247 100644 --- a/river/command.zig +++ b/river/command.zig @@ -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 }, diff --git a/river/command/config.zig b/river/command/config.zig index a97e233..3db604c 100644 --- a/river/command/config.zig +++ b/river/command/config.zig @@ -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, diff --git a/river/command/rule.zig b/river/command/rule.zig index 7703955..a8076fa 100644 --- a/river/command/rule.zig +++ b/river/command/rule.zig @@ -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, }}); }