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

commitae7b2952ef6f06c26a51e7e8ecc5850dab3db512
parent9d32e4826b
authorIsaac Freund <[email protected]>
date2026-02-12 16:17
Scene: workaround wlroots API limitations

Leon reports GTK4 clients failing to handle the redundant output
enter/leave events gracefully and dying. While those clients are surely
buggy, river should also not send redundant events.

Unfortunately, wlroots makes it hard for river to do the right thing
here currently, this hack should hopefully improve things enough in the
short term that we don't have clients crashing due to river's redundant
events.

 river/WindowManager.zig | 31 +++++++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)

diff --git a/river/WindowManager.zig b/river/WindowManager.zig
index 9e4c650..553c0b3 100644
--- a/river/WindowManager.zig
+++ b/river/WindowManager.zig
@@ -67,6 +67,7 @@ rendering_scheduled: struct {
 /// The list is in rendering order, the last node in the list is rendered on top.
 rendering_requested: struct {
     list: wl.list.Head(WmNode, .link),
+    order_hash: u64 = 0,
 },
 
 dirty_idle: ?*wl.EventSource = null,
@@ -450,13 +451,42 @@ fn renderFinish(wm: *WindowManager) void {
         }
     }
 
+    // This is a hack to avoid excessive modification of the wlroots scene graph.
+    // There is currently no way to atomically apply multiple changes to the
+    // scene graph, which means that damage and visibility are re-calculated
+    // every API call, resulting in redundant events being sent to clients.
+    //
+    // TODO(wlroots) provide a way to batch changes to the scene graph.
+    const new_order_hash = blk: {
+        var hash = std.crypto.hash.Blake3.init(.{});
+        var it = wm.rendering_requested.list.iterator(.forward);
+        while (it.next()) |node| {
+            switch (node.get()) {
+                .window => |window| {
+                    hash.update(@ptrCast(&window));
+                    hash.update(&.{@intFromBool(window.wm_requested.fullscreen != null)});
+                },
+                .shell_surface => |shell_surface| {
+                    hash.update(@ptrCast(&shell_surface));
+                },
+            }
+        }
+        var final: u64 = undefined;
+        hash.final(@ptrCast(&final));
+        break :blk final;
+    };
+
     {
+        const reorder = wm.rendering_requested.order_hash != new_order_hash;
+        wm.rendering_requested.order_hash = new_order_hash;
+
         var found_fullscreen: bool = false;
         var it = wm.rendering_requested.list.iterator(.forward);
         while (it.next()) |node| {
             switch (node.get()) {
                 .window => |window| {
                     window.renderFinish();
+                    if (!reorder) continue;
                     window.popup_tree.node.reparent(server.scene.layers.popups);
                     if (window.wm_requested.fullscreen != null) {
                         window.tree.node.reparent(server.scene.layers.fullscreen);
@@ -469,6 +499,7 @@ fn renderFinish(wm: *WindowManager) void {
                 },
                 .shell_surface => |shell_surface| {
                     shell_surface.renderFinish();
+                    if (!reorder) continue;
                     shell_surface.popup_tree.node.reparent(server.scene.layers.popups);
                     if (found_fullscreen) {
                         shell_surface.tree.node.reparent(server.scene.layers.fullscreen);