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

commite20ce9943d7ff3c753eb3b3d6f672f02bffe9a2b
parent6c38726b78
authorIsaac Freund <[email protected]>
date2024-07-24 15:09
WindowManager: continue fleshing out state machine

 river/Cursor.zig                   | 16 +++----
 river/ForeignToplevelHandle.zig    |  2 +-
 river/LayerSurface.zig             |  8 ++--
 river/LockManager.zig              |  2 +-
 river/Output.zig                   |  6 +--
 river/Root.zig                     |  2 +-
 river/Seat.zig                     |  2 +-
 river/Window.zig                   |  4 +-
 river/WindowManager.zig            | 90 +++++++++++++++++++++-----------------
 river/XdgDecoration.zig            |  2 +-
 river/XdgToplevel.zig              |  6 +--
 river/XwaylandOverrideRedirect.zig |  2 +-
 river/XwaylandWindow.zig           |  6 +--
 13 files changed, 78 insertions(+), 70 deletions(-)

diff --git a/river/Cursor.zig b/river/Cursor.zig
index 85f49fa..26cd3a2 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -338,7 +338,7 @@ fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.P
             cursor.mode = .passthrough;
             cursor.passthrough(event.time_msec);
 
-            server.wm.applyPending();
+            server.wm.dirtyPending();
         } else {
             _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
         }
@@ -375,10 +375,10 @@ fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.P
         }
     }
 
-    server.wm.applyPending();
+    server.wm.dirtyPending();
 }
 
-/// Requires a call to WindowManager.applyPending()
+/// Requires a call to WindowManager.dirtyPending()
 fn updateKeyboardFocus(cursor: Cursor, result: Root.AtResult) void {
     switch (result.data) {
         .window => |window| {
@@ -509,7 +509,7 @@ fn handleTouchDown(
         }
     }
 
-    server.wm.applyPending();
+    server.wm.dirtyPending();
 }
 
 fn handleTouchMotion(
@@ -796,7 +796,7 @@ fn enterMode(cursor: *Cursor, mode: Mode, window: *Window, xcursor_name: [*:0]co
     cursor.seat.wlr_seat.pointerNotifyClearFocus();
     cursor.setXcursor(xcursor_name);
 
-    server.wm.applyPending();
+    server.wm.dirtyPending();
 }
 
 fn processMotion(cursor: *Cursor, device: *wlr.InputDevice, time: u32, delta_x: f64, delta_y: f64, unaccel_dx: f64, unaccel_dy: f64) void {
@@ -853,7 +853,7 @@ fn processMotion(cursor: *Cursor, device: *wlr.InputDevice, time: u32, delta_x:
 
             // XXX move window
 
-            server.wm.applyPending();
+            server.wm.dirtyPending();
         },
         .resize => |*data| {
             dx += data.delta_x;
@@ -908,7 +908,7 @@ fn processMotion(cursor: *Cursor, device: *wlr.InputDevice, time: u32, delta_x:
                 data.y = box.height - data.initial_height;
             }
 
-            server.wm.applyPending();
+            server.wm.dirtyPending();
         },
     }
 }
@@ -950,7 +950,7 @@ pub fn updateState(cursor: *Cursor) void {
                 .passthrough, .down => {},
                 inline .move, .resize => |data, mode| {
 
-                    // These conditions are checked in WindowManager.applyPending()
+                    // These conditions are checked in WindowManager.dirtyPending()
                     assert(!data.window.current.fullscreen);
 
                     // Keep the cursor locked to the original offset from the edges of the window.
diff --git a/river/ForeignToplevelHandle.zig b/river/ForeignToplevelHandle.zig
index fb424d3..beaa607 100644
--- a/river/ForeignToplevelHandle.zig
+++ b/river/ForeignToplevelHandle.zig
@@ -86,7 +86,7 @@ fn handleForeignActivate(
     const seat: *Seat = @ptrFromInt(event.seat.data);
 
     seat.focus(window);
-    server.wm.applyPending();
+    server.wm.dirtyPending();
 }
 
 fn handleForeignFullscreen(
diff --git a/river/LayerSurface.zig b/river/LayerSurface.zig
index 348a397..9fe11a9 100644
--- a/river/LayerSurface.zig
+++ b/river/LayerSurface.zig
@@ -108,7 +108,7 @@ fn handleMap(listener: *wl.Listener(void)) void {
         if (consider) layer_surface else null,
     );
 
-    server.wm.applyPending();
+    server.wm.dirtyPending();
 }
 
 fn handleUnmap(listener: *wl.Listener(void)) void {
@@ -118,7 +118,7 @@ fn handleUnmap(listener: *wl.Listener(void)) void {
 
     layer_surface.output.arrangeLayers();
     handleKeyboardInteractiveExclusive(layer_surface.output, null);
-    server.wm.applyPending();
+    server.wm.dirtyPending();
 }
 
 fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
@@ -138,13 +138,13 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
     {
         layer_surface.output.arrangeLayers();
         handleKeyboardInteractiveExclusive(layer_surface.output, null);
-        server.wm.applyPending();
+        server.wm.dirtyPending();
     }
 }
 
 /// Focus topmost keyboard-interactivity-exclusive layer surface above normal
 /// content, or if none found, focus the surface given as `consider`.
-/// Requires a call to WindowManager.applyPending()
+/// Requires a call to WindowManager.dirtyPending()
 fn handleKeyboardInteractiveExclusive(output: *Output, consider: ?*LayerSurface) void {
     if (server.lock_manager.state != .unlocked) return;
 
diff --git a/river/LockManager.zig b/river/LockManager.zig
index 1a16c66..c40b124 100644
--- a/river/LockManager.zig
+++ b/river/LockManager.zig
@@ -218,7 +218,7 @@ fn handleUnlock(listener: *wl.Listener(void)) void {
 
     handleDestroy(&manager.destroy);
 
-    server.wm.applyPending();
+    server.wm.dirtyPending();
 }
 
 fn handleDestroy(listener: *wl.Listener(void)) void {
diff --git a/river/Output.zig b/river/Output.zig
index e816404..b73cffb 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -237,7 +237,7 @@ pub fn layerSurfaceTree(output: Output, layer: zwlr.LayerShellV1.Layer) *wlr.Sce
 
 /// Arrange all layer surfaces of this output and adjust the usable area.
 /// Will arrange windows as well if the usable area changes.
-/// Requires a call to WindowManager.applyPending()
+/// Requires a call to WindowManager.dirtyPending()
 pub fn arrangeLayers(output: *Output) void {
     var full_box: wlr.Box = .{
         .x = 0,
@@ -329,7 +329,7 @@ fn handleDestroy(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void {
 
     server.root.handleOutputConfigChange() catch std.log.err("out of memory", .{});
 
-    server.wm.applyPending();
+    server.wm.dirtyPending();
 }
 
 fn handleRequestState(listener: *wl.Listener(*wlr.Output.event.RequestState), event: *wlr.Output.event.RequestState) void {
@@ -340,7 +340,7 @@ fn handleRequestState(listener: *wl.Listener(*wlr.Output.event.RequestState), ev
         return;
     };
 
-    server.wm.applyPending();
+    server.wm.dirtyPending();
 }
 
 // TODO double buffer output state changes for frame perfection and cleaner code.
diff --git a/river/Root.zig b/river/Root.zig
index 43ef70f..0f6fb10 100644
--- a/river/Root.zig
+++ b/river/Root.zig
@@ -359,7 +359,7 @@ fn processOutputConfig(
         }
     }
 
-    if (action == .apply) server.wm.applyPending();
+    if (action == .apply) server.wm.dirtyPending();
 
     if (success) {
         config.sendSucceeded();
diff --git a/river/Seat.zig b/river/Seat.zig
index 8426990..48e8f98 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -129,7 +129,7 @@ pub fn deinit(seat: *Seat) void {
 
 /// Set the current focus. If a visible window is passed it will be focused.
 /// If null is passed, the top window in the stack of the focused output will be focused.
-/// Requires a call to WindowManager.applyPending()
+/// Requires a call to WindowManager.dirtyPending()
 pub fn focus(seat: *Seat, target: ?*Window) void {
     // Views may not receive focus while locked.
     if (server.lock_manager.state != .unlocked) return;
diff --git a/river/Window.zig b/river/Window.zig
index 0f67d8f..5f806b5 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -484,7 +484,7 @@ pub fn map(window: *Window) !void {
 
     window.foreign_toplevel_handle.map();
 
-    server.wm.applyPending();
+    server.wm.dirtyPending();
 }
 
 /// Called by the impl when the surface will no longer be displayed
@@ -501,7 +501,7 @@ pub fn unmap(window: *Window) void {
 
     window.foreign_toplevel_handle.unmap();
 
-    server.wm.applyPending();
+    server.wm.dirtyPending();
 }
 
 pub fn notifyTitle(window: *const Window) void {
diff --git a/river/WindowManager.zig b/river/WindowManager.zig
index 91f9e72..d8d1d21 100644
--- a/river/WindowManager.zig
+++ b/river/WindowManager.zig
@@ -36,8 +36,21 @@ object: ?*river.WindowManagerV1 = null,
 
 windows: wl.list.Head(Window, .link),
 
+state: union(enum) {
+    idle,
+    /// An update event was sent to the window manager but has not yet been acked.
+    /// Value is the update serial
+    update_sent: u32,
+    /// An update event was sent to the window manager and has been acked but not yet committed.
+    update_acked,
+    /// The number of configures sent that have not yet been ack'd
+    configures_inflight: u32,
+} = .idle,
+
 /// Pending state from windows to be sent to the wm in the next update sequence.
 pending: struct {
+    /// Pending state has been modified since the last update event sent to the wm.
+    dirty: bool = false,
     new_windows: wl.list.Head(Window, .link_new),
 },
 
@@ -48,6 +61,7 @@ uncommitted: struct {
 
 /// State sent by the wm and committed with a commit request.
 committed: struct {
+    dirty: bool = false,
     render_list: wl.list.Head(Window, .committed_render_list_link),
 },
 
@@ -57,12 +71,11 @@ inflight: struct {
     render_list: wl.list.Head(Window, .inflight_render_list_link),
 },
 
+dirty_idle: ?*wl.EventSource = null,
+
 /// Number of inflight configures sent to windows in the current transaction.
 inflight_configures: u32 = 0,
 transaction_timeout: *wl.EventSource,
-/// Set to true if applyPending() is called while a transaction is inflight.
-/// If true when a transaction completes, causes applyPending() to be called again.
-pending_state_dirty: bool = false,
 
 pub fn init(wm: *WindowManager) !void {
     const event_loop = server.wl_server.getEventLoop();
@@ -149,48 +162,43 @@ fn handleRequest(
     }
 }
 
-/// Trigger asynchronous application of pending state for all outputs and windows.
-/// Changes will not be applied to the scene graph until the layout generator
-/// generates a new layout for all outputs and all affected clients ack a
-/// configure and commit a new buffer.
-pub fn applyPending(wm: *WindowManager) void {
-    {
-        // Changes to the pending state may require a focus update to keep
-        // state consistent. Instead of having focus(null) calls spread all
-        // around the codebase and risk forgetting one, always ensure focus
-        // state is synchronized here.
-        var it = server.input_manager.seats.first;
-        while (it) |node| : (it = node.next) node.data.focus(null);
-    }
+pub fn dirtyPending(wm: *WindowManager) void {
+    wm.pending.dirty = true;
 
-    // If there is already a transaction inflight, wait until it completes.
-    if (wm.inflight_configures > 0) {
-        wm.pending_state_dirty = true;
-        return;
+    if (wm.dirty_idle == null) {
+        const event_loop = server.wl_server.getEventLoop();
+        wm.dirty_idle = event_loop.addIdle(*WindowManager, handleDirty, wm) catch {
+            log.err("out of memory", .{});
+            return;
+        };
     }
-    wm.pending_state_dirty = false;
+}
 
-    {
-        var it = server.input_manager.seats.first;
-        while (it) |node| : (it = node.next) {
-            const cursor = &node.data.cursor;
-
-            switch (cursor.mode) {
-                .passthrough, .down => {},
-                inline .move, .resize => |data| {
-                    if (data.window.inflight.fullscreen) {
-                        cursor.mode = .passthrough;
-                        data.window.pending.resizing = false;
-                        data.window.inflight.resizing = false;
-                    }
-                },
-            }
+fn handleDirty(wm: *WindowManager) void {
+    switch (wm.state) {
+        .idle => {
+            assert(!wm.committed.dirty);
 
-            cursor.inflight_mode = cursor.mode;
-        }
+            if (wm.pending.dirty) {
+                wm.sendUpdate();
+            }
+        },
+        .update_sent, .update_acked, .configures_inflight => {},
     }
+}
+
+fn sendUpdate(wm: *WindowManager) void {
+    assert(wm.state == .idle);
+
+    const wm_v1 = wm.object orelse return;
+
+    // XXX send all dirty pending state
+
+    wm.pending.dirty = false;
 
-    wm.sendConfigures();
+    const serial = server.wl_server.nextSerial();
+    wm_v1.sendUpdate(serial);
+    wm.state = .{ .update_sent = serial };
 }
 
 fn sendConfigures(wm: *WindowManager) void {
@@ -282,7 +290,7 @@ fn commitTransaction(wm: *WindowManager) void {
 
     server.idle_inhibit_manager.checkActive();
 
-    if (wm.pending_state_dirty) {
-        wm.applyPending();
+    if (wm.pending.dirty) {
+        wm.dirtyPending();
     }
 }
diff --git a/river/XdgDecoration.zig b/river/XdgDecoration.zig
index 840b9c2..0cd57c2 100644
--- a/river/XdgDecoration.zig
+++ b/river/XdgDecoration.zig
@@ -81,5 +81,5 @@ fn handleRequestMode(
         .server_side => window.pending.decoration_hint = .prefers_ssd,
     }
 
-    server.wm.applyPending();
+    server.wm.dirtyPending();
 }
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index 427451d..0ca4a56 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -395,7 +395,7 @@ fn handleRequestFullscreen(listener: *wl.Listener(void)) void {
     const toplevel: *XdgToplevel = @fieldParentPtr("request_fullscreen", listener);
     if (toplevel.window.pending.fullscreen_requested != toplevel.wlr_toplevel.requested.fullscreen) {
         toplevel.window.pending.fullscreen_requested = toplevel.wlr_toplevel.requested.fullscreen;
-        server.wm.applyPending();
+        server.wm.dirtyPending();
     }
 }
 
@@ -409,7 +409,7 @@ fn handleRequestMove(
 
     // Moving windows with touch or tablet tool is not yet supported.
     if (seat.wlr_seat.validatePointerGrabSerial(null, event.serial)) {
-        // XXX queue pointer_move_requested, applyPending()
+        // XXX queue pointer_move_requested, dirtyPending()
     }
 }
 
@@ -420,7 +420,7 @@ fn handleRequestResize(listener: *wl.Listener(*wlr.XdgToplevel.event.Resize), ev
 
     // Resizing windows with touch or tablet tool is not yet supported.
     if (seat.wlr_seat.validatePointerGrabSerial(null, event.serial)) {
-        // XXX queue pointer_resize_requested, applyPending()
+        // XXX queue pointer_resize_requested, dirtyPending()
     }
 }
 
diff --git a/river/XwaylandOverrideRedirect.zig b/river/XwaylandOverrideRedirect.zig
index 4f4680e..835ef00 100644
--- a/river/XwaylandOverrideRedirect.zig
+++ b/river/XwaylandOverrideRedirect.zig
@@ -176,7 +176,7 @@ fn handleUnmap(listener: *wl.Listener(void)) void {
         }
     }
 
-    server.wm.applyPending();
+    server.wm.dirtyPending();
 }
 
 fn handleSetGeometry(listener: *wl.Listener(void)) void {
diff --git a/river/XwaylandWindow.zig b/river/XwaylandWindow.zig
index a234534..e51f3f6 100644
--- a/river/XwaylandWindow.zig
+++ b/river/XwaylandWindow.zig
@@ -234,7 +234,7 @@ fn handleRequestConfigure(
     // Allow xwayland windows to set their own dimensions (but not position) if floating
     xwayland_window.window.pending.box.width = event.width;
     xwayland_window.window.pending.box.height = event.height;
-    server.wm.applyPending();
+    server.wm.dirtyPending();
 }
 
 fn handleSetOverrideRedirect(listener: *wl.Listener(void)) void {
@@ -281,7 +281,7 @@ fn handleSetDecorations(listener: *wl.Listener(void)) void {
         window.pending.decoration_hint = .prefers_ssd;
     }
 
-    server.wm.applyPending();
+    server.wm.dirtyPending();
 }
 
 fn handleRequestFullscreen(listener: *wl.Listener(void)) void {
@@ -291,7 +291,7 @@ fn handleRequestFullscreen(listener: *wl.Listener(void)) void {
 
     if (window.pending.fullscreen_requested != xwayland_surface.fullscreen) {
         window.pending.fullscreen_requested = xwayland_surface.fullscreen;
-        server.wm.applyPending();
+        server.wm.dirtyPending();
     }
 }