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

commitf6fae48936067193d2b5235c01f79d12c39257ca
parent3f9b2abdff
authorIsaac Freund <[email protected]>
date2026-03-16 17:00
WindowManager: fix possible manage_dirty related hang

Currently if the window manager repeatedly makes the manage_dirty
request during a pointer operation it can prevent river from making
forward progress with input event processing and cause the event queue
to fill up.

The patch fixes this problem by changing river's handling of the
manage_dirty request to not block input event processing.

 river/Seat.zig          | 10 +---------
 river/WindowManager.zig | 31 ++++++++++++++++++++++---------
 2 files changed, 23 insertions(+), 18 deletions(-)

diff --git a/river/Seat.zig b/river/Seat.zig
index 967ecb7..57b7123 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -206,7 +206,6 @@ pointer_bindings: wl.list.Head(PointerBinding, .link),
 cursor: Cursor,
 
 op: ?struct {
-    dirty: bool = false,
     sent_release: bool = false,
     input: enum {
         pointer,
@@ -378,13 +377,6 @@ pub fn processEvents(seat: *Seat) void {
         }
     }
     assert(server.wm.state == .idle);
-
-    if (seat.op) |*op| {
-        if (op.dirty) {
-            op.dirty = false;
-            server.wm.dirtyWindowing();
-        }
-    }
 }
 
 pub fn manageStart(seat: *Seat) void {
@@ -836,7 +828,7 @@ pub fn opUpdate(seat: *Seat, x: i32, y: i32) void {
     const op = &seat.op.?;
     op.x = x;
     op.y = y;
-    op.dirty = true;
+    server.wm.dirtyWindowingLazy();
 }
 
 pub fn opEnd(seat: *Seat) void {
diff --git a/river/WindowManager.zig b/river/WindowManager.zig
index 3d2b552..56ac5bf 100644
--- a/river/WindowManager.zig
+++ b/river/WindowManager.zig
@@ -43,7 +43,11 @@ windows: SlotMap(*Window) = .empty,
 /// State to be sent to the wm in the next manage sequence.
 scheduled: struct {
     /// State has been modified since the last manage sequence.
+    /// Prevents processing further input events until a manage sequence is completed.
     dirty: bool = false,
+    /// A manage sequence should be started when idle, but don't prevent processing
+    /// further input events.
+    dirty_lazy: bool = false,
 
     output_config: ?*wlr.OutputConfigurationV1 = null,
 } = .{},
@@ -164,7 +168,10 @@ fn handleRequest(
             }
             wm.manageFinish();
         },
-        .manage_dirty => wm.dirtyWindowing(),
+        .manage_dirty => {
+            wm.scheduled.dirty_lazy = true;
+            wm.addDirtyIdle();
+        },
         .render_finish => {
             if (wm.state != .render) {
                 wm_v1.postError(.sequence_order,
@@ -223,6 +230,11 @@ pub fn dirtyWindowing(wm: *WindowManager) void {
     wm.addDirtyIdle();
 }
 
+pub fn dirtyWindowingLazy(wm: *WindowManager) void {
+    wm.scheduled.dirty_lazy = true;
+    wm.addDirtyIdle();
+}
+
 pub fn cleanWindowing(wm: *WindowManager) void {
     wm.scheduled.dirty = false;
     wm.removeDirtyIdle();
@@ -239,7 +251,7 @@ pub fn cleanRendering(wm: *WindowManager) void {
 }
 
 fn addDirtyIdle(wm: *WindowManager) void {
-    assert(wm.scheduled.dirty or wm.rendering_scheduled.dirty);
+    assert(wm.scheduled.dirty or wm.scheduled.dirty_lazy or wm.rendering_scheduled.dirty);
     if (wm.dirty_idle == null) {
         const event_loop = server.wl_server.getEventLoop();
         wm.dirty_idle = event_loop.addIdle(*WindowManager, dirtyIdle, wm) catch {
@@ -259,13 +271,16 @@ fn removeDirtyIdle(wm: *WindowManager) void {
 }
 
 fn dirtyIdle(wm: *WindowManager) void {
-    assert(wm.scheduled.dirty or wm.rendering_scheduled.dirty);
+    assert(wm.scheduled.dirty or wm.scheduled.dirty_lazy or wm.rendering_scheduled.dirty);
     wm.dirty_idle = null;
     switch (wm.state) {
         .idle => {
             if (wm.rendering_scheduled.dirty) {
                 wm.renderStart();
             } else {
+                assert(wm.scheduled.dirty or wm.scheduled.dirty_lazy);
+                wm.scheduled.dirty = true;
+                wm.scheduled.dirty_lazy = false;
                 wm.manageStart();
             }
         },
@@ -527,11 +542,9 @@ fn renderFinish(wm: *WindowManager) void {
 
     log.debug("finished committing transaction", .{});
 
-    if (wm.rendering_scheduled.dirty) {
-        wm.dirtyRendering();
-    } else if (wm.scheduled.dirty) {
-        wm.dirtyWindowing();
-    } else {
-        server.input_manager.processEvents();
+    if (wm.scheduled.dirty or wm.scheduled.dirty_lazy or wm.rendering_scheduled.dirty) {
+        wm.addDirtyIdle();
     }
+
+    server.input_manager.processEvents();
 }