Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
protocol: add river_output_v1.set_presentation_mode
build.zig | 2 ++
protocol/river-window-management-v1.xml | 44 +++++++++++++++++++++++++++++++++
river/Output.zig | 31 +++++++++++++++++++++++
river/OutputManager.zig | 1 +
river/Server.zig | 5 ++++
river/Window.zig | 20 +++++++++++++++
6 files changed, 103 insertions(+)
diff --git a/build.zig b/build.zig
index 48ea3d5..a03318a 100644
--- a/build.zig
+++ b/build.zig
@@ -78,6 +78,7 @@ pub fn build(b: *Build) !void {
scanner.addSystemProtocol("stable/tablet/tablet-v2.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/xdg-decoration/xdg-decoration-unstable-v1.xml");
@@ -111,6 +112,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("river_window_manager_v1", 4);
scanner.generate("river_xkb_bindings_v1", 2);
diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index da1fd14..87af214 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -1045,6 +1045,17 @@
<arg name="width" type="int" summary="clip box width"/>
<arg name="height" type="int" summary="clip box height"/>
</request>
+
+ <event name="presentation_hint" since="4">
+ <description summary="presentation hint set by the window">
+ This event communicates the window's preferred presentation mode.
+
+ This event will be followed by a render_start event after all other new
+ state has been sent by the server.
+ </description>
+ <arg name="hint" type="uint" enum="river_output_v1.presentation_mode"
+ summary="presentation hint"/>
+ </event>
</interface>
<interface name="river_decoration_v1" version="4">
@@ -1337,6 +1348,39 @@
<arg name="width" type="int" summary="output width"/>
<arg name="height" type="int" summary="output height"/>
</event>
+
+ <enum name="error" since="4">
+ <entry name="invalid_presentation_mode" value="0" since="4"
+ summary="invalid presentation mode enum value"/>
+ </enum>
+
+ <enum name="presentation_mode" since="4">
+ <entry name="vsync" value="0">
+ <description summary="tearing-free presentation">
+ Output page-flips should be synchronized to the vertical blanking
+ period, eliminating tearing. This is the default presentation mode.
+ </description>
+ </entry>
+ <entry name="async" value="1">
+ <description summary="asynchronous presentation">
+ Output page-flips should not be synchronized to the vertical blanking
+ period, visual screen tearing may occur.
+ </description>
+ </entry>
+ </enum>
+
+ <request name="set_presentation_mode" since="4">
+ <description summary="set the preferred presentation mode">
+ Set the preferred presentation mode of the output. The compositor should
+ always respect the preference of the window manager if possible. If this
+ request is never made, the preferred presentation mode is vsync.
+
+ This request modifies rendering state and may only be made as part of a
+ render sequence, see the river_window_manager_v1 description.
+ </description>
+ <arg name="mode" type="uint" enum="presentation_mode"
+ summary="preferred presentation mode"/>
+ </request>
</interface>
<interface name="river_seat_v1" version="4">
diff --git a/river/Output.zig b/river/Output.zig
index 7ede781..75bb410 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -123,6 +123,13 @@ pub const State = struct {
}
};
+const RenderingState = struct {
+ tearing: bool,
+ const init: RenderingState = .{
+ .tearing = false,
+ };
+};
+
/// Set to null when the wlr_output is destroyed.
wlr_output: ?*wlr.Output,
scene_output: ?*wlr.SceneOutput,
@@ -160,8 +167,11 @@ scheduled: State,
sent: State,
link_sent: wl.list.Link,
sent_wl_output: bool = false,
+/// Rendering state requested by the window manager.
+rendering_requested: RenderingState = .init,
/// State applied to the wlr_output and rendered.
current: State,
+rendering_current: RenderingState = .init,
destroy: wl.Listener(*wlr.Output) = .init(handleDestroy),
request_state: wl.Listener(*wlr.Output.event.RequestState) = .init(handleRequestState),
@@ -385,6 +395,17 @@ fn handleRequest(
assert(output.object == output_v1);
switch (request) {
.destroy => output_v1.destroy(),
+ .set_presentation_mode => |args| {
+ if (!server.wm.ensureRendering()) return;
+ output.rendering_requested.tearing = switch (args.mode) {
+ .vsync => false,
+ .async => true,
+ _ => {
+ output_v1.postError(.invalid_presentation_mode, "invalid presentation mode enum value");
+ return;
+ },
+ };
+ },
}
}
@@ -440,6 +461,16 @@ fn renderAndCommit(output: *Output) !void {
if (!output.scene_output.?.buildState(&state, null)) return error.CommitFailed;
+ if (output.rendering_current.tearing) {
+ state.tearing_page_flip = true;
+ // TODO don't try this every frame if it consistently fails. Stop trying if it fails
+ // for 10 frames in a row or something.
+ if (!wlr_output.testState(&state)) {
+ log.info("tearing page flip test failed for {s}, retrying without tearing", .{wlr_output.name});
+ state.tearing_page_flip = false;
+ }
+ }
+
if (!wlr_output.commitState(&state)) return error.CommitFailed;
switch (server.lock_manager.state) {
diff --git a/river/OutputManager.zig b/river/OutputManager.zig
index 8174429..003d955 100644
--- a/river/OutputManager.zig
+++ b/river/OutputManager.zig
@@ -253,6 +253,7 @@ pub fn commitOutputState(om: *OutputManager) void {
var it = wm.sent.outputs.iterator(.forward);
while (it.next()) |output| {
assert(output.sent.state != .destroying);
+ output.rendering_current = output.rendering_requested;
// This may be null even when the state is not .destroying if the
// output is destroyed between manage start and render finish.
const wlr_output = output.wlr_output orelse continue;
diff --git a/river/Server.zig b/river/Server.zig
index 0b9a252..2053e6e 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -78,6 +78,8 @@ output_image_capture_source_manager: *wlr.ExtOutputImageCaptureSourceManagerV1,
foreign_toplevel_list: *wlr.ExtForeignToplevelListV1,
+tearing_control_manager: *wlr.TearingControlManagerV1,
+
scene: Scene,
input_manager: InputManager,
libinput_config: LibinputConfig,
@@ -152,6 +154,8 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void {
.foreign_toplevel_list = try wlr.ExtForeignToplevelListV1.create(wl_server, 1),
+ .tearing_control_manager = try wlr.TearingControlManagerV1.create(wl_server, 1),
+
.scene = undefined,
.om = undefined,
.input_manager = undefined,
@@ -315,6 +319,7 @@ fn allowlist(server: *Server, global: *const wl.Global) bool {
global == server.xdg_activation.global or
global == server.data_device_manager.global or
global == server.primary_selection_manager.global or
+ global == server.tearing_control_manager.global or
global == server.om.presentation.global or
global == server.om.xdg_output_manager.global or
global == server.input_manager.relative_pointer_manager.global or
diff --git a/river/Window.zig b/river/Window.zig
index 340952e..f06b6c6 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -233,6 +233,7 @@ rendering_scheduled: struct {
rendering_sent: struct {
width: u31 = 0,
height: u31 = 0,
+ presentation_hint: river.OutputV1.PresentationMode = .vsync,
} = .{},
/// Rendering state requested by the wm.
@@ -815,6 +816,25 @@ pub fn renderStart(window: *Window) void {
}
sent.width = scheduled.width;
sent.height = scheduled.height;
+
+ const presentation_hint = window.presentationHint();
+ if (sent.presentation_hint != presentation_hint) {
+ if (window.object) |window_v1| {
+ if (window_v1.getVersion() >= 4) {
+ window_v1.sendPresentationHint(presentation_hint);
+ }
+ }
+ sent.presentation_hint = presentation_hint;
+ }
+}
+
+fn presentationHint(window: *Window) river.OutputV1.PresentationMode {
+ const root_surface = window.rootSurface() orelse return .vsync;
+ return switch (server.tearing_control_manager.hintFromSurface(root_surface)) {
+ .async => .async,
+ .vsync => .vsync,
+ _ => unreachable,
+ };
}
pub fn renderFinish(window: *Window) void {