git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit6a2ad802b6fdd9efea930ad525df57e424e66dc8
parentb813d1a058
authorIsaac Freund <[email protected]>
date2026-02-05 13:06
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 {