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

commite03c3eaa9f5fa060d277f72d03eb15f73de5c466
parent29bb5beb4b
authorIsaac Freund <[email protected]>
date2025-01-06 11:53
river: mostly implement river_shell_surface_v1

This commit implements creation, positioning, and rendering of shell
surfaces. Input and focus is TODO.

 build.zig                               |   4 +
 build.zig.zon                           |   4 +-
 protocol/river-window-management-v1.xml |  17 ++++
 river/Scene.zig                         |  55 +++++++++++
 river/ShellSurface.zig                  | 165 ++++++++++++++++++++++++++++++++
 river/Window.zig                        |  60 ++----------
 river/WindowManager.zig                 |  26 ++++-
 river/WmNode.zig                        |  14 ++-
 river/XdgToplevel.zig                   |   2 +-
 rivercompat/ShellSurface.zig            |  70 ++++++++++++++
 rivercompat/Window.zig                  |  10 +-
 rivercompat/WindowManager.zig           |  19 +++-
 rivercompat/main.zig                    |  20 +++-
 13 files changed, 401 insertions(+), 65 deletions(-)

diff --git a/build.zig b/build.zig
index 17cee2f..22d9acf 100644
--- a/build.zig
+++ b/build.zig
@@ -92,8 +92,10 @@ pub fn build(b: *Build) !void {
     const scanner = Scanner.create(b, .{});
 
     scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml");
+    scanner.addSystemProtocol("stable/viewporter/viewporter.xml");
     scanner.addSystemProtocol("staging/cursor-shape/cursor-shape-v1.xml");
     scanner.addSystemProtocol("staging/ext-session-lock/ext-session-lock-v1.xml");
+    scanner.addSystemProtocol("staging/single-pixel-buffer/single-pixel-buffer-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/tablet/tablet-unstable-v2.xml");
@@ -123,6 +125,8 @@ 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_viewporter", 1);
+    scanner.generate("wp_single_pixel_buffer_manager_v1", 1);
 
     scanner.generate("river_window_manager_v1", 1);
 
diff --git a/build.zig.zon b/build.zig.zon
index 0b14132..c532bcc 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -12,8 +12,8 @@
             .hash = "1220d41b23ae70e93355bb29dac1c07aa6aeb92427a2dffc4375e94b4de18111248c",
         },
         .@"zig-wlroots" = .{
-            .url = "https://codeberg.org/ifreund/zig-wlroots/archive/70a4de5cd6886003e54f55239d2753b025271bc9.tar.gz",
-            .hash = "122026eea643c568b68cb58d925344033bc143b8ff2f43b0851a4577f68c83b9f96c",
+            .url = "https://codeberg.org/ifreund/zig-wlroots/archive/7fd9f01746e25c5f623384ebc2e45632e4a60965.tar.gz",
+            .hash = "122012d30acca9039fecdf28a0fe410dee5ebc42380b75902f60dbb19c15b8052516",
         },
         .@"zig-xkbcommon" = .{
             .url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/v0.2.0.tar.gz",
diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index cbfa045..40b7f73 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -39,6 +39,11 @@
       compositor should use the unavailable event if necessary to enforce this.
     </description>
 
+    <enum name="error">
+      <entry name="role" value="0"
+        summary="given wl_surface already has a role"/>
+    </enum>
+
     <event name="unavailable">
       <description summary="window management unavailable">
         This event indicates that window management is not available to the
@@ -209,6 +214,11 @@
         - window cropping
     </description>
 
+    <enum name="error">
+      <entry name="node_exists" value="0"
+        summary="window already has a node object"/>
+    </enum>
+
     <request name="destroy" type="destructor">
       <description summary="destroy the window object">
         This request indicates that the client will no longer use the window
@@ -792,6 +802,13 @@
       whatever else it wants.
     </description>
 
+    <enum name="error">
+      <entry name="node_exists" value="0"
+        summary="shell surface already has a node object"/>
+      <entry name="no_commit" value="1"
+        summary="failed to commit the surface before the window manager commit"/>
+    </enum>
+
     <request name="destroy" type="destructor">
       <description summary="destroy the shell surface object">
         This request indicates that the client will no longer use the shell
diff --git a/river/Scene.zig b/river/Scene.zig
index 764d58b..f0d369a 100644
--- a/river/Scene.zig
+++ b/river/Scene.zig
@@ -16,6 +16,8 @@
 
 const Scene = @This();
 
+const std = @import("std");
+const assert = std.debug.assert;
 const build_options = @import("build_options");
 const wlr = @import("wlroots");
 
@@ -123,3 +125,56 @@ pub fn at(scene: *const Scene, lx: f64, ly: f64) ?AtResult {
         return null;
     }
 }
+
+pub const SaveableSurfaces = struct {
+    tree: *wlr.SceneTree,
+    saved: *wlr.SceneTree,
+
+    pub fn init(parent: *wlr.SceneTree) !SaveableSurfaces {
+        const surfaces = .{
+            .tree = try parent.createSceneTree(),
+            .saved = try parent.createSceneTree(),
+        };
+        surfaces.saved.node.setEnabled(false);
+        return surfaces;
+    }
+
+    pub fn save(surfaces: SaveableSurfaces) void {
+        if (surfaces.saved.node.enabled) return;
+        assert(surfaces.tree.node.enabled);
+        assert(surfaces.saved.children.empty());
+
+        surfaces.tree.node.forEachBuffer(*wlr.SceneTree, saveSurfaceTreeIter, surfaces.saved);
+
+        surfaces.tree.node.setEnabled(false);
+        surfaces.saved.node.setEnabled(true);
+    }
+
+    fn saveSurfaceTreeIter(
+        buffer: *wlr.SceneBuffer,
+        sx: c_int,
+        sy: c_int,
+        saved: *wlr.SceneTree,
+    ) void {
+        const scene_buffer = saved.createSceneBuffer(buffer.buffer) catch {
+            std.log.err("out of memory", .{});
+            return;
+        };
+        scene_buffer.node.setPosition(sx, sy);
+        scene_buffer.setDestSize(buffer.dst_width, buffer.dst_height);
+        scene_buffer.setSourceBox(&buffer.src_box);
+        scene_buffer.setTransform(buffer.transform);
+    }
+
+    pub fn dropSaved(surfaces: SaveableSurfaces) void {
+        if (!surfaces.saved.node.enabled) return;
+
+        assert(!surfaces.tree.node.enabled);
+
+        var it = surfaces.saved.children.safeIterator(.forward);
+        while (it.next()) |node| node.destroy();
+
+        surfaces.saved.node.setEnabled(false);
+        surfaces.tree.node.setEnabled(true);
+    }
+};
diff --git a/river/ShellSurface.zig b/river/ShellSurface.zig
new file mode 100644
index 0000000..0336e6e
--- /dev/null
+++ b/river/ShellSurface.zig
@@ -0,0 +1,165 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2024 The River Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+const ShellSurface = @This();
+
+const build_options = @import("build_options");
+const std = @import("std");
+const assert = std.debug.assert;
+const wlr = @import("wlroots");
+const wl = @import("wayland").server.wl;
+const river = @import("wayland").server.river;
+
+const server = &@import("main.zig").server;
+const util = @import("util.zig");
+
+const Scene = @import("Scene.zig");
+const SceneNodeData = @import("SceneNodeData.zig");
+const WmNode = @import("WmNode.zig");
+
+const log = std.log.scoped(.wm);
+
+const WmState = struct {
+    x: i32 = 0,
+    y: i32 = 0,
+    sync_next_commit: bool = false,
+};
+
+const role: wlr.Surface.Role = .{
+    .name = "river_scene_surface_v1",
+    .client_commit = clientCommit,
+    .commit = commit,
+    .unmap = null,
+    .destroy = null,
+};
+
+object: *river.ShellSurfaceV1,
+surface: *wlr.Surface,
+tree: *wlr.SceneTree,
+surfaces: Scene.SaveableSurfaces,
+node: WmNode,
+
+uncommitted: WmState = .{},
+committed: WmState = .{},
+
+pub fn create(
+    client: *wl.Client,
+    version: u32,
+    id: u32,
+    surface: *wlr.Surface,
+) !void {
+    log.debug("new river_shell_surface_v1", .{});
+
+    const shell_surface_v1 = try river.ShellSurfaceV1.create(client, version, id);
+
+    if (!surface.setRole(&role, @ptrCast(shell_surface_v1), @intFromEnum(river.WindowManagerV1.Error.role))) {
+        return;
+    }
+    surface.setRoleObject(@ptrCast(shell_surface_v1));
+
+    const shell_surface = try util.gpa.create(ShellSurface);
+    errdefer util.gpa.destroy(shell_surface);
+
+    const tree = try server.scene.hidden_tree.createSceneTree();
+    errdefer tree.node.destroy();
+
+    const surfaces = try Scene.SaveableSurfaces.init(tree);
+    _ = try surfaces.tree.createSceneSubsurfaceTree(surface);
+
+    shell_surface.* = .{
+        .object = shell_surface_v1,
+        .surface = surface,
+        .tree = tree,
+        .surfaces = surfaces,
+        .node = undefined,
+    };
+
+    shell_surface.node.init(.shell_surface);
+    server.wm.uncommitted.render_list.append(&shell_surface.node);
+
+    shell_surface_v1.setHandler(*ShellSurface, handleRequest, handleDestroy, shell_surface);
+}
+
+fn handleDestroy(_: *river.ShellSurfaceV1, shell_surface: *ShellSurface) void {
+    shell_surface.node.makeInert();
+    shell_surface.node.deinit();
+
+    util.gpa.destroy(shell_surface);
+}
+
+fn handleRequest(
+    shell_surface_v1: *river.ShellSurfaceV1,
+    request: river.ShellSurfaceV1.Request,
+    shell_surface: *ShellSurface,
+) void {
+    assert(shell_surface.object == shell_surface_v1);
+    switch (request) {
+        .destroy => shell_surface_v1.destroy(),
+        .get_node => |args| {
+            if (shell_surface.node.object != null) {
+                shell_surface_v1.postError(.node_exists, "shell surface already has a node object");
+                return;
+            }
+            shell_surface.node.createObject(
+                shell_surface_v1.getClient(),
+                shell_surface_v1.getVersion(),
+                args.id,
+            );
+        },
+        .sync_next_commit => {
+            shell_surface.uncommitted.sync_next_commit = true;
+        },
+    }
+}
+
+fn clientCommit(wlr_surface: *wlr.Surface) callconv(.C) void {
+    if (wlr_surface.role != &role) return;
+    const resource = wlr_surface.role_resource orelse return;
+
+    const shell_surface: *ShellSurface = @ptrCast(@alignCast(resource.getUserData()));
+
+    if (shell_surface.uncommitted.sync_next_commit) {
+        shell_surface.surfaces.save();
+    }
+}
+
+fn commit(wlr_surface: *wlr.Surface) callconv(.C) void {
+    if (wlr_surface.unmap_commit) {
+        wlr_surface.unmap();
+    } else if (wlr_surface.hasBuffer()) {
+        wlr_surface.map();
+    }
+}
+
+pub fn commitWmState(shell_surface: *ShellSurface) void {
+    shell_surface.committed = shell_surface.uncommitted;
+
+    if (shell_surface.uncommitted.sync_next_commit) {
+        shell_surface.uncommitted.sync_next_commit = false;
+
+        if (!shell_surface.surfaces.saved.node.enabled) {
+            shell_surface.object.postError(.no_commit,
+                \\no wl_surface.commit after sync_next_commit and before river_window_manager_v1.commit
+            );
+        }
+    }
+}
+
+pub fn commitTransaction(shell_surface: *ShellSurface) void {
+    shell_surface.surfaces.dropSaved();
+
+    shell_surface.tree.node.setPosition(shell_surface.committed.x, shell_surface.committed.y);
+}
diff --git a/river/Window.zig b/river/Window.zig
index a62cc95..045a96d 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -30,6 +30,7 @@ const server = &@import("main.zig").server;
 const util = @import("util.zig");
 
 const Output = @import("Output.zig");
+const Scene = @import("Scene.zig");
 const SceneNodeData = @import("SceneNodeData.zig");
 const Seat = @import("Seat.zig");
 const WmNode = @import("WmNode.zig");
@@ -137,8 +138,8 @@ node: WmNode,
 impl: Impl,
 
 tree: *wlr.SceneTree,
-surface_tree: *wlr.SceneTree,
-saved_surface_tree: *wlr.SceneTree,
+surfaces: Scene.SaveableSurfaces,
+
 border: struct {
     left: *wlr.SceneRect,
     right: *wlr.SceneRect,
@@ -218,8 +219,7 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
         .node = undefined,
         .impl = impl,
         .tree = tree,
-        .surface_tree = try tree.createSceneTree(),
-        .saved_surface_tree = try tree.createSceneTree(),
+        .surfaces = try Scene.SaveableSurfaces.init(tree),
         .border = .{
             .left = try tree.createSceneRect(0, 0, &.{ 0, 0, 0, 0 }),
             .right = try tree.createSceneRect(0, 0, &.{ 0, 0, 0, 0 }),
@@ -236,7 +236,6 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
 
     window.tree.node.setEnabled(false);
     window.popup_tree.node.setEnabled(false);
-    window.saved_surface_tree.node.setEnabled(false);
 
     try SceneNodeData.attach(&window.tree.node, .{ .window = window });
     try SceneNodeData.attach(&window.popup_tree.node, .{ .window = window });
@@ -260,7 +259,7 @@ pub fn destroy(window: *Window, when: enum { lazy, assert }) void {
     // If there are still saved buffers, then this window needs to be kept
     // around until the current transaction completes. This function will be
     // called again in WindowManager.commitTransaction()
-    if (!window.saved_surface_tree.node.enabled) {
+    if (!window.surfaces.saved.node.enabled) {
         window.tree.node.destroy();
         window.popup_tree.node.destroy();
 
@@ -453,7 +452,8 @@ fn handleRequest(
         .close => uncommitted.close = true,
         .get_node => |args| {
             if (window.node.object != null) {
-                // XXX send protocol error
+                window_v1.postError(.node_exists, "window already has a node object");
+                return;
             }
             window.node.createObject(window_v1.getClient(), window_v1.getVersion(), args.id);
         },
@@ -582,7 +582,7 @@ pub fn configure(window: *Window) bool {
     };
 
     if (track_configure and window.mapped) {
-        window.saveSurfaceTree();
+        window.surfaces.save();
         window.sendFrameDone();
     }
 
@@ -720,42 +720,6 @@ pub fn sendFrameDone(window: Window) void {
     window.rootSurface().?.sendFrameDone(&now);
 }
 
-pub fn dropSavedSurfaceTree(window: *Window) void {
-    if (!window.saved_surface_tree.node.enabled) return;
-
-    var it = window.saved_surface_tree.children.safeIterator(.forward);
-    while (it.next()) |node| node.destroy();
-
-    window.saved_surface_tree.node.setEnabled(false);
-    window.surface_tree.node.setEnabled(true);
-}
-
-pub fn saveSurfaceTree(window: *Window) void {
-    assert(!window.saved_surface_tree.node.enabled);
-    assert(window.saved_surface_tree.children.empty());
-
-    window.surface_tree.node.forEachBuffer(*wlr.SceneTree, saveSurfaceTreeIter, window.saved_surface_tree);
-
-    window.surface_tree.node.setEnabled(false);
-    window.saved_surface_tree.node.setEnabled(true);
-}
-
-fn saveSurfaceTreeIter(
-    buffer: *wlr.SceneBuffer,
-    sx: c_int,
-    sy: c_int,
-    saved_surface_tree: *wlr.SceneTree,
-) void {
-    const saved = saved_surface_tree.createSceneBuffer(buffer.buffer) catch {
-        log.err("out of memory", .{});
-        return;
-    };
-    saved.node.setPosition(sx, sy);
-    saved.setDestSize(buffer.dst_width, buffer.dst_height);
-    saved.setSourceBox(&buffer.src_box);
-    saved.setTransform(buffer.transform);
-}
-
 pub fn close(window: Window) void {
     switch (window.impl) {
         .toplevel => |toplevel| toplevel.wlr_toplevel.sendClose(),
@@ -792,12 +756,6 @@ pub fn getAppId(window: Window) ?[*:0]const u8 {
     };
 }
 
-/// Clamp the width/height of the box to the constraints of the window
-pub fn applyConstraints(window: *Window, box: *wlr.Box) void {
-    box.width = math.clamp(box.width, window.constraints.min_width, window.constraints.max_width);
-    box.height = math.clamp(box.height, window.constraints.min_height, window.constraints.max_height);
-}
-
 /// Called by the impl when the surface is ready to be displayed
 pub fn map(window: *Window) !void {
     log.debug("window '{?s}' mapped", .{window.getTitle()});
@@ -810,7 +768,7 @@ pub fn map(window: *Window) !void {
 pub fn unmap(window: *Window) void {
     log.debug("window '{?s}' unmapped", .{window.getTitle()});
 
-    if (!window.saved_surface_tree.node.enabled) window.saveSurfaceTree();
+    window.surfaces.save();
 
     assert(window.mapped and !window.destroying);
     window.mapped = false;
diff --git a/river/WindowManager.zig b/river/WindowManager.zig
index 7056497..b93b3b6 100644
--- a/river/WindowManager.zig
+++ b/river/WindowManager.zig
@@ -26,7 +26,9 @@ const server = &@import("main.zig").server;
 const util = @import("util.zig");
 
 const Output = @import("Output.zig");
+const Scene = @import("Scene.zig");
 const Seat = @import("Seat.zig");
+const ShellSurface = @import("ShellSurface.zig");
 const Window = @import("Window.zig");
 const WmNode = @import("WmNode.zig");
 
@@ -196,6 +198,7 @@ fn handleRequest(
                     wm.committed.render_list.append(node);
                     switch (node.get()) {
                         .window => |window| window.commitWmState(),
+                        .shell_surface => |shell_surface| shell_surface.commitWmState(),
                     }
                 }
             }
@@ -214,7 +217,19 @@ fn handleRequest(
                 .update_sent, .inflight_configures => {},
             }
         },
-        .get_shell_surface => |_| {},
+        .get_shell_surface => |args| {
+            const surface = wlr.Surface.fromWlSurface(args.surface);
+            ShellSurface.create(
+                wm_v1.getClient(),
+                wm_v1.getVersion(),
+                args.id,
+                surface,
+            ) catch {
+                wm_v1.getClient().postNoMemory();
+                log.err("out of memory", .{});
+                return;
+            };
+        },
     }
 }
 
@@ -308,6 +323,7 @@ pub fn sendConfigures(wm: *WindowManager) void {
                         wm.state.inflight_configures += 1;
                     }
                 },
+                .shell_surface => {},
             }
         }
     }
@@ -364,7 +380,7 @@ fn commitTransaction(wm: *WindowManager) void {
     {
         var it = wm.windows.safeIterator(.forward);
         while (it.next()) |window| {
-            window.dropSavedSurfaceTree();
+            window.surfaces.dropSaved();
             if (window.destroying) window.destroy(.assert);
         }
     }
@@ -383,6 +399,12 @@ fn commitTransaction(wm: *WindowManager) void {
                     window.tree.node.setEnabled(!window.pending.hidden);
                     window.popup_tree.node.setEnabled(!window.pending.hidden);
                 },
+                .shell_surface => |shell_surface| {
+                    shell_surface.commitTransaction();
+
+                    shell_surface.tree.node.reparent(server.scene.layers.wm);
+                    shell_surface.tree.node.raiseToTop();
+                },
             }
         }
     }
diff --git a/river/WmNode.zig b/river/WmNode.zig
index 58dc7f1..7c8de0e 100644
--- a/river/WmNode.zig
+++ b/river/WmNode.zig
@@ -25,9 +25,11 @@ const server = &@import("main.zig").server;
 const util = @import("util.zig");
 
 const Window = @import("Window.zig");
+const ShellSurface = @import("ShellSurface.zig");
 
 const Type = union(enum) {
     window: *Window,
+    shell_surface: *ShellSurface,
 };
 const Tag = @typeInfo(Type).Union.tag_type.?;
 
@@ -64,6 +66,7 @@ pub fn deinit(node: *WmNode) void {
 pub fn get(node: *WmNode) Type {
     return switch (node.tag) {
         .window => .{ .window = @fieldParentPtr("node", node) },
+        .shell_surface => .{ .shell_surface = @fieldParentPtr("node", node) },
     };
 }
 
@@ -74,7 +77,7 @@ pub fn createObject(node: *WmNode, client: *wl.Client, version: u32, id: u32) vo
         client.postNoMemory();
         return;
     };
-    node_v1.setHandler(*WmNode, handleRequest, null, node);
+    node_v1.setHandler(*WmNode, handleRequest, handleDestroy, node);
     node.object = node_v1;
 }
 
@@ -93,6 +96,10 @@ fn handleRequestInert(
     if (request == .destroy) node_v1.destroy();
 }
 
+fn handleDestroy(_: *river.NodeV1, node: *WmNode) void {
+    node.object = null;
+}
+
 fn handleRequest(
     node_v1: *river.NodeV1,
     request: river.NodeV1.Request,
@@ -102,7 +109,6 @@ fn handleRequest(
     switch (request) {
         .destroy => {
             node_v1.destroy();
-            node.object = null;
         },
         .set_position => |args| switch (node.get()) {
             .window => |window| {
@@ -111,6 +117,10 @@ fn handleRequest(
                     .y = args.y,
                 };
             },
+            .shell_surface => |shell_surface| {
+                shell_surface.uncommitted.x = args.x;
+                shell_surface.uncommitted.y = args.y;
+            },
         },
         .place_top => {
             node.link_uncommitted.remove();
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index 7515bbb..ab34c3c 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -96,7 +96,7 @@ pub fn create(wlr_toplevel: *wlr.XdgToplevel) error{OutOfMemory}!void {
     wlr_toplevel.base.surface.events.unmap.add(&toplevel.unmap);
     errdefer toplevel.unmap.link.remove();
 
-    _ = try window.surface_tree.createSceneXdgSurface(wlr_toplevel.base);
+    _ = try window.surfaces.tree.createSceneXdgSurface(wlr_toplevel.base);
 
     toplevel.window = window;
 
diff --git a/rivercompat/ShellSurface.zig b/rivercompat/ShellSurface.zig
new file mode 100644
index 0000000..2e8b425
--- /dev/null
+++ b/rivercompat/ShellSurface.zig
@@ -0,0 +1,70 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2024 The River Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+const ShellSurface = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const math = std.math;
+const wayland = @import("wayland");
+const wl = wayland.client.wl;
+const wp = wayland.client.wp;
+const river = wayland.client.river;
+
+const WindowManager = @import("WindowManager.zig");
+
+const gpa = std.heap.c_allocator;
+
+wm: *WindowManager,
+surface: *wl.Surface,
+viewport: *wp.Viewport,
+shell_surface_v1: *river.ShellSurfaceV1,
+node: *river.NodeV1,
+
+pub fn create(wm: *WindowManager) void {
+    const shell_surface = gpa.create(ShellSurface) catch @panic("OOM");
+    const surface = wm.compositor.createSurface() catch @panic("OOM");
+    const viewport = wm.viewporter.getViewport(surface) catch @panic("OOM");
+    const shell_surface_v1 = wm.wm_v1.getShellSurface(surface) catch @panic("OOM");
+
+    shell_surface.* = .{
+        .wm = wm,
+        .surface = surface,
+        .viewport = viewport,
+        .shell_surface_v1 = shell_surface_v1,
+        .node = shell_surface_v1.getNode() catch @panic("OOM"),
+    };
+
+    shell_surface.node.placeBottom();
+    shell_surface.node.setPosition(0, 0);
+    shell_surface_v1.syncNextCommit();
+
+    const rgb = 0xfdf6e3;
+
+    const buffer = wm.single_pixel.createU32RgbaBuffer(
+        @as(u32, (rgb >> 16) & 0xff) * (0xffff_ffff / 0xff),
+        @as(u32, (rgb >> 8) & 0xff) * (0xffff_ffff / 0xff),
+        @as(u32, (rgb >> 0) & 0xff) * (0xffff_ffff / 0xff),
+        0xffff_ffff,
+    ) catch @panic("OOM");
+    defer buffer.destroy();
+
+    surface.attach(buffer, 0, 0);
+
+    surface.damageBuffer(0, 0, math.maxInt(i32), math.maxInt(i32));
+    viewport.setDestination(math.maxInt(i32) / 2, math.maxInt(i32) / 2);
+    surface.commit();
+}
diff --git a/rivercompat/Window.zig b/rivercompat/Window.zig
index cc87ef4..0d89f70 100644
--- a/rivercompat/Window.zig
+++ b/rivercompat/Window.zig
@@ -43,13 +43,15 @@ pub fn create(window_v1: *river.WindowV1, wm: *WindowManager) void {
     window_v1.setListener(*Window, handleEvent, window);
     window.node_v1.placeTop();
     window_v1.useSsd();
+
+    const rgb = 0x586e75;
     window_v1.setBorders(
         .{ .left = true, .bottom = true, .top = false, .right = true },
         6, // width
-        std.math.maxInt(u32),
-        std.math.maxInt(u32),
-        std.math.maxInt(u32),
-        std.math.maxInt(u32),
+        @as(u32, (rgb >> 16) & 0xff) * (0xffff_ffff / 0xff),
+        @as(u32, (rgb >> 8) & 0xff) * (0xffff_ffff / 0xff),
+        @as(u32, (rgb >> 0) & 0xff) * (0xffff_ffff / 0xff),
+        0xffff_ffff,
     );
 }
 
diff --git a/rivercompat/WindowManager.zig b/rivercompat/WindowManager.zig
index 056d039..6ef5727 100644
--- a/rivercompat/WindowManager.zig
+++ b/rivercompat/WindowManager.zig
@@ -21,18 +21,33 @@ const assert = std.debug.assert;
 const main = @import("main.zig");
 const wayland = @import("wayland");
 const wl = wayland.client.wl;
+const wp = wayland.client.wp;
 const river = wayland.client.river;
 
 const Seat = @import("Seat.zig");
+const ShellSurface = @import("ShellSurface.zig");
 const Window = @import("Window.zig");
 
 wm_v1: *river.WindowManagerV1,
+compositor: *wl.Compositor,
+viewporter: *wp.Viewporter,
+single_pixel: *wp.SinglePixelBufferManagerV1,
+
 windows: wl.list.Head(Window, .link),
 seats: wl.list.Head(Seat, .link),
 
-pub fn init(wm: *WindowManager, wm_v1: *river.WindowManagerV1) void {
+pub fn init(
+    wm: *WindowManager,
+    wm_v1: *river.WindowManagerV1,
+    compositor: *wl.Compositor,
+    viewporter: *wp.Viewporter,
+    single_pixel: *wp.SinglePixelBufferManagerV1,
+) void {
     wm.* = .{
         .wm_v1 = wm_v1,
+        .compositor = compositor,
+        .viewporter = viewporter,
+        .single_pixel = single_pixel,
         .windows = undefined,
         .seats = undefined,
     };
@@ -40,6 +55,8 @@ pub fn init(wm: *WindowManager, wm_v1: *river.WindowManagerV1) void {
     wm.seats.init();
 
     wm_v1.setListener(*WindowManager, handleEvent, wm);
+
+    ShellSurface.create(wm);
 }
 
 fn handleEvent(wm_v1: *river.WindowManagerV1, event: river.WindowManagerV1.Event, wm: *WindowManager) void {
diff --git a/rivercompat/main.zig b/rivercompat/main.zig
index 1d6666a..e8d07e1 100644
--- a/rivercompat/main.zig
+++ b/rivercompat/main.zig
@@ -23,6 +23,7 @@ const assert = std.debug.assert;
 
 const wayland = @import("wayland");
 const wl = wayland.client.wl;
+const wp = wayland.client.wp;
 const river = wayland.client.river;
 const flags = @import("flags");
 
@@ -40,12 +41,21 @@ const usage =
 
 const Globals = struct {
     wm_v1: ?*river.WindowManagerV1 = null,
+    compositor: ?*wl.Compositor = null,
+    viewporter: ?*wp.Viewporter = null,
+    single_pixel: ?*wp.SinglePixelBufferManagerV1 = null,
 
     fn handleEvent(registry: *wl.Registry, event: wl.Registry.Event, globals: *Globals) void {
         switch (event) {
             .global => |global| {
                 if (mem.orderZ(u8, global.interface, river.WindowManagerV1.interface.name) == .eq) {
                     globals.wm_v1 = registry.bind(global.name, river.WindowManagerV1, 1) catch return;
+                } else if (mem.orderZ(u8, global.interface, wl.Compositor.interface.name) == .eq) {
+                    globals.compositor = registry.bind(global.name, wl.Compositor, 4) catch return;
+                } else if (mem.orderZ(u8, global.interface, wp.Viewporter.interface.name) == .eq) {
+                    globals.viewporter = registry.bind(global.name, wp.Viewporter, 1) catch return;
+                } else if (mem.orderZ(u8, global.interface, wp.SinglePixelBufferManagerV1.interface.name) == .eq) {
+                    globals.single_pixel = registry.bind(global.name, wp.SinglePixelBufferManagerV1, 1) catch return;
                 }
             },
             .global_remove => {},
@@ -88,10 +98,16 @@ pub fn main() !void {
     if (display.roundtrip() != .SUCCESS) fatal("initial roundtrip failed", .{});
 
     const wm_v1 = globals.wm_v1 orelse
-        fatal("wayland compositor does not support river-window-management-v1.\n", .{});
+        fatal("wayland compositor does not support river-window-management-v1", .{});
+    const compositor = globals.compositor orelse
+        fatal("wayland compositor does not support wl_compositor", .{});
+    const viewporter = globals.viewporter orelse
+        fatal("wayland compositor does not support viewporter", .{});
+    const single_pixel = globals.single_pixel orelse
+        fatal("wayland compositor does not support wp-single-pixel-buffer-v1", .{});
 
     var wm: WindowManager = undefined;
-    wm.init(wm_v1);
+    wm.init(wm_v1, compositor, viewporter, single_pixel);
 
     while (true) {
         if (display.dispatch() != .SUCCESS) fatal("failed to dispatch wayland events", .{});