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

commitdcbc272c96ed5fa4dcf4f6e40d079bea926990fd
parenta4aca7c291
authorIsaac Freund <[email protected]>
date2020-08-20 14:35
view: implement frame-perfect destroy

river's View objects may now outlive their wlroots counterparts so that
we can continue to render a destroyed view until the transaction is
completed.

 river/Output.zig       |  4 ++--
 river/Root.zig         | 17 +++++++++++++++--
 river/View.zig         | 33 ++++++++++++++++++---------------
 river/VoidView.zig     |  4 ++++
 river/XdgToplevel.zig  | 19 +++++++++++--------
 river/XwaylandView.zig | 18 +++++++++++-------
 6 files changed, 61 insertions(+), 34 deletions(-)

diff --git a/river/Output.zig b/river/Output.zig
index 777d831..a05518b 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -288,7 +288,7 @@ fn layoutExternal(self: *Self, visible_count: u32) !void {
     var view_it = ViewStack(View).pendingIterator(self.views.first, self.pending.tags);
     while (view_it.next()) |node| {
         const view = &node.view;
-        if (!view.pending.float and !view.pending.fullscreen) {
+        if (!view.pending.float and !view.pending.fullscreen and !view.destroying) {
             view.pending.box = view_boxen.items[i];
             view.applyConstraints();
             i += 1;
@@ -309,7 +309,7 @@ pub fn arrangeViews(self: *Self) void {
     var it = ViewStack(View).pendingIterator(self.views.first, self.pending.tags);
     while (it.next()) |node| {
         const view = &node.view;
-        if (!view.pending.float and !view.pending.fullscreen) layout_count += 1;
+        if (!view.pending.float and !view.pending.fullscreen and !view.destroying) layout_count += 1;
     }
 
     // If the usable area has a zero dimension, trying to arrange the layout
diff --git a/river/Root.zig b/river/Root.zig
index 03ab9b3..6e7e99e 100644
--- a/river/Root.zig
+++ b/river/Root.zig
@@ -133,6 +133,11 @@ pub fn startTransaction(self: *Self) void {
         while (view_it.next()) |view_node| {
             const view = &view_node.view;
 
+            if (view.destroying) {
+                if (view.saved_buffers.items.len == 0) view.saveBuffers();
+                continue;
+            }
+
             if (view.shouldTrackConfigure()) {
                 // Clear the serial in case this transaction is interrupting a prior one.
                 view.pending_serial = null;
@@ -226,9 +231,17 @@ fn commitTransaction(self: *Self) void {
 
         var view_tags_changed = false;
 
-        var view_it = ViewStack(View).iterator(output.views.first, std.math.maxInt(u32));
-        while (view_it.next()) |view_node| {
+        var view_it = output.views.first;
+        while (view_it) |view_node| {
             const view = &view_node.view;
+
+            if (view.destroying) {
+                view_it = view_node.next;
+                view.destroy();
+                continue;
+            }
+            defer view_it = view_node.next;
+
             if (!view.shouldTrackConfigure() and view.pending_serial != null) continue;
 
             // Apply pending state of the view
diff --git a/river/View.zig b/river/View.zig
index 2686b2e..1d1208c 100644
--- a/river/View.zig
+++ b/river/View.zig
@@ -79,9 +79,14 @@ impl: Impl,
 /// The output this view is currently associated with
 output: *Output,
 
-/// This is non-null exactly when the view is mapped
+/// This is from the point where the view is mapped until the surface
+/// is destroyed by wlroots.
 wlr_surface: ?*c.wlr_surface,
 
+/// This View struct outlasts the wlroots object it wraps. This bool is set to
+/// true when the backing wlr_xdg_toplevel or equivalent has been destroyed.
+destroying: bool,
+
 /// The double-buffered state of the view
 current: State,
 pending: State,
@@ -110,6 +115,7 @@ pub fn init(self: *Self, output: *Output, tags: u32, surface: var) void {
     self.output = output;
 
     self.wlr_surface = null;
+    self.destroying = false;
 
     self.current = .{
         .box = .{
@@ -140,9 +146,17 @@ pub fn init(self: *Self, output: *Output, tags: u32, surface: var) void {
     } else unreachable;
 }
 
-pub fn deinit(self: Self) void {
+/// Deinit the view, remove it from the view stack and free the memory.
+pub fn destroy(self: *Self) void {
     for (self.saved_buffers.items) |buffer| c.wlr_buffer_unlock(&buffer.wlr_client_buffer.*.base);
     self.saved_buffers.deinit();
+    switch (self.impl) {
+        .xdg_toplevel => |*xdg_toplevel| xdg_toplevel.deinit(),
+        .xwayland_view => |*xwayland_view| xwayland_view.deinit(),
+    }
+    const node = @fieldParentPtr(ViewStack(Self).Node, "view", self);
+    self.output.views.remove(node);
+    util.gpa.destroy(node);
 }
 
 /// Handle changes to pending state and start a transaction to apply them
@@ -362,6 +376,8 @@ pub fn unmap(self: *Self) void {
 
     log.debug(.server, "view '{}' unmapped", .{self.getTitle()});
 
+    self.destroying = true;
+
     // Inform all seats that the view has been unmapped so they can handle focus
     var it = root.server.input_manager.seats.first;
     while (it) |node| : (it = node.next) {
@@ -369,12 +385,6 @@ pub fn unmap(self: *Self) void {
         seat.handleViewUnmap(self);
     }
 
-    self.wlr_surface = null;
-
-    // Remove the view from its output's stack
-    const node = @fieldParentPtr(ViewStack(Self).Node, "view", self);
-    self.output.views.remove(node);
-
     self.output.sendViewTags();
 
     // Still need to arrange if fullscreened from the layout
@@ -382,10 +392,3 @@ pub fn unmap(self: *Self) void {
 
     root.startTransaction();
 }
-
-/// Destory the view and free the ViewStack node holding it.
-pub fn destroy(self: *const Self) void {
-    self.deinit();
-    const node = @fieldParentPtr(ViewStack(Self).Node, "view", self);
-    util.gpa.destroy(node);
-}
diff --git a/river/VoidView.zig b/river/VoidView.zig
index f1f1c7a..5b1175c 100644
--- a/river/VoidView.zig
+++ b/river/VoidView.zig
@@ -24,6 +24,10 @@ const c = @import("c.zig");
 const Box = @import("Box.zig");
 const View = @import("View.zig");
 
+pub fn deinit(self: *Self) void {
+    unreachable;
+}
+
 pub fn needsConfigure(self: Self) bool {
     unreachable;
 }
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index 8d4c7b3..69f9ff7 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -60,6 +60,15 @@ pub fn init(self: *Self, view: *View, wlr_xdg_surface: *c.wlr_xdg_surface) void
     c.wl_signal_add(&self.wlr_xdg_surface.events.unmap, &self.listen_unmap);
 }
 
+pub fn deinit(self: *Self) void {
+    if (self.view.wlr_surface != null) {
+        // Remove listeners that are active for the entire lifetime of the view
+        c.wl_list_remove(&self.listen_destroy.link);
+        c.wl_list_remove(&self.listen_map.link);
+        c.wl_list_remove(&self.listen_unmap.link);
+    }
+}
+
 /// Returns true if a configure must be sent to ensure the dimensions of the
 /// pending_box are applied.
 pub fn needsConfigure(self: Self) bool {
@@ -139,14 +148,8 @@ pub fn getConstraints(self: Self) View.Constraints {
 /// Called when the xdg surface is destroyed
 fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
     const self = @fieldParentPtr(Self, "listen_destroy", listener.?);
-    const output = self.view.output;
-
-    // Remove listeners that are active for the entire lifetime of the view
-    c.wl_list_remove(&self.listen_destroy.link);
-    c.wl_list_remove(&self.listen_map.link);
-    c.wl_list_remove(&self.listen_unmap.link);
-
-    self.view.destroy();
+    self.deinit();
+    self.view.wlr_surface = null;
 }
 
 /// Called when the xdg surface is mapped, or ready to display on-screen.
diff --git a/river/XwaylandView.zig b/river/XwaylandView.zig
index d8292a1..0be1c21 100644
--- a/river/XwaylandView.zig
+++ b/river/XwaylandView.zig
@@ -56,6 +56,15 @@ pub fn init(self: *Self, view: *View, wlr_xwayland_surface: *c.wlr_xwayland_surf
     c.wl_signal_add(&self.wlr_xwayland_surface.events.unmap, &self.listen_unmap);
 }
 
+pub fn deinit(self: *Self) void {
+    if (self.view.wlr_surface != null) {
+        // Remove listeners that are active for the entire lifetime of the view
+        c.wl_list_remove(&self.listen_destroy.link);
+        c.wl_list_remove(&self.listen_map.link);
+        c.wl_list_remove(&self.listen_unmap.link);
+    }
+}
+
 pub fn needsConfigure(self: Self) bool {
     return self.wlr_xwayland_surface.x != self.view.pending.box.x or
         self.wlr_xwayland_surface.y != self.view.pending.box.y or
@@ -132,13 +141,8 @@ pub fn getConstraints(self: Self) View.Constraints {
 /// Called when the xwayland surface is destroyed
 fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
     const self = @fieldParentPtr(Self, "listen_destroy", listener.?);
-
-    // Remove listeners that are active for the entire lifetime of the view
-    c.wl_list_remove(&self.listen_destroy.link);
-    c.wl_list_remove(&self.listen_map.link);
-    c.wl_list_remove(&self.listen_unmap.link);
-
-    self.view.destroy();
+    self.deinit();
+    self.view.wlr_surface = null;
 }
 
 /// Called when the xwayland surface is mapped, or ready to display on-screen.