Skip to content

Sway & wlroots Deep Dive

Sway is a drop-in i3 replacement built on wlroots, the compositor library that implements Wayland protocols so you don't have to. This article covers Sway's IPC, wlroots internals, WayVNC integration, and the xdg-shell configure dance -- all the moving parts Warmwind patches daily.

Sway Architecture

graph LR
    IPC["IPC"] --> Tree["Tree Layout"]
    Config["Config"] --> Tree
    Tree --> SG["Scene Graph"]
    Protos["Protocols"] --> SG
    SG --> Renderer["Renderer"] --> Backend["Backend"]

Sway delegates all hardware interaction to wlroots. It focuses on window management (tree layout, workspaces, scratchpad) and configuration.

IPC Protocol

Sway exposes an i3-compatible IPC over $SWAYSOCK (Unix stream socket). Every message is a 14-byte header + JSON payload:

"i3-ipc" (6 bytes magic) | payload_length (u32) | message_type (u32) | JSON payload
# List all outputs
swaymsg -t get_outputs | jq '.[].name'

# Subscribe to window events (streaming JSON)
swaymsg -t subscribe '["window"]'

# Move focused container -- used by automation scripts
swaymsg move container to workspace 2

Warmwind uses IPC to programmatically launch Chromium on the correct output, set fullscreen, and monitor session state.

wlroots Backend / Renderer / Scene Graph

Backend (wlr_backend): Abstracts input/output hardware.

Backend Use case
DRM Real hardware (GPU + displays)
Wayland Compositor-in-compositor (testing)
Headless No display -- WayVNC uses this
X11 Development on X11 host

Renderer (wlr_renderer): GPU abstraction for compositing.

  • GLES2 (default), Vulkan (newer), Pixman (software fallback).
  • Sway never calls GL directly; it goes through wlr_renderer.

Scene graph (wlr_scene): Declarative tree of what to display.

  • wlr_scene_output_commit() renders the tree to an output.
  • Enables direct scanout: if one surface covers the entire output, skip compositing and scan out the client buffer directly.

WayVNC Integration

sequenceDiagram
    participant Client as Chromium
    participant Sway as Sway (wlroots)
    participant VNC as WayVNC
    participant Remote as VNC Viewer

    Client->>Sway: wl_surface.attach(buffer)
    Sway->>Sway: wlr_scene renders frame
    VNC->>Sway: wlr-screencopy-v1: copy_output
    Sway->>VNC: buffer with rendered frame
    VNC->>VNC: Encode (Tight, ZRLE, raw)
    VNC->>Remote: RFB protocol frame
    Remote->>VNC: Key/pointer events
    VNC->>Sway: wlr_virtual_keyboard / pointer

WayVNC uses wlr-screencopy-unstable-v1 to capture frames and wlr-virtual-pointer / virtual-keyboard to inject input. It runs as a regular Wayland client with protocol privileges.

WayVNC now also supports ext-image-copy-capture-v1, the standardized successor to the deprecated wlr-screencopy protocol. New deployments should prefer ext-image-copy-capture-v1; WayVNC falls back to wlr-screencopy on older compositors.

Wayland objects ↔ Rust ownership

Smithay (Rust compositor library) solved the object-lifecycle problem that killed wlroots-rs. Wayland's create/destroy semantics map naturally to Rust's ownership model -- when the Rust struct drops, the protocol object is destroyed. No dangling references, no double-free, no manual ref-counting.

Expert insight

"wlroots is an open standards platform -- it implements the protocols and gets out of your way." -- Drew DeVault, wlroots creator

Minimal wlroots Compositor Skeleton

#include <wlr/backend.h>
#include <wlr/render/allocator.h>
#include <wlr/render/wlr_renderer.h>
#include <wlr/types/wlr_output.h>
#include <wlr/types/wlr_scene.h>
#include <wlr/types/wlr_xdg_shell.h>

struct server {
    struct wl_display *display;
    struct wlr_backend *backend;
    struct wlr_renderer *renderer;
    struct wlr_allocator *allocator;
    struct wlr_scene *scene;
    struct wlr_xdg_shell *xdg_shell;
    struct wl_listener new_output;
    struct wl_listener new_xdg_toplevel;
};

static void handle_new_output(struct wl_listener *listener, void *data) {
    struct wlr_output *output = data;
    wlr_output_init_render(output, server->allocator, server->renderer);
    struct wlr_output_state state;
    wlr_output_state_init(&state);
    wlr_output_state_set_enabled(&state, true);
    wlr_output_commit_state(output, &state);
    wlr_output_state_finish(&state);
    wlr_scene_output_create(server->scene, output);
}

int main(void) {
    struct server server = {0};
    server.display = wl_display_create();
    server.backend = wlr_backend_autocreate(
        wl_display_get_event_loop(server.display), NULL);
    server.renderer = wlr_renderer_autocreate(server.backend);
    server.allocator = wlr_allocator_autocreate(
        server.backend, server.renderer);
    server.scene = wlr_scene_create();

    server.xdg_shell = wlr_xdg_shell_create(server.display, 6);
    /* wire up listeners ... */

    const char *socket = wl_display_add_socket_auto(server.display);
    wlr_backend_start(server.backend);
    setenv("WAYLAND_DISPLAY", socket, true);
    wl_display_run(server.display);  /* event loop */
    return 0;
}

Build with Meson: meson setup build && ninja -C build.

xdg-shell Configure / Ack Dance

sequenceDiagram
    participant C as Client
    participant S as Compositor

    S->>C: xdg_toplevel.configure(width, height, states[])
    S->>C: xdg_surface.configure(serial)
    C->>C: Resize buffer, redraw
    C->>S: xdg_surface.ack_configure(serial)
    C->>S: wl_surface.commit(new buffer)
  1. Compositor sends a configure event with suggested size and states (maximized, fullscreen, activated, tiled).
  2. Client must ack_configure with the serial before committing.
  3. Until acked, the compositor uses the previous buffer geometry.
  4. Client can send its own preferred size via set_min_size / set_max_size.

For kiosk mode, Sway sends configure(fullscreen) immediately. The client must ack and provide a buffer matching the output resolution.

What's new (2025–2026)
  • Sway 1.10 / 1.11 released with incremental protocol and scene-graph improvements. No Sway 2.0 is planned -- the project stays on evolutionary releases atop wlroots 0.18+.
  • COSMIC desktop (System76, Smithay-based) shipped stable in Dec 2025 -- the first major Rust compositor to reach production.
  • niri (Rust scrolling tiler) is daily-drivable, attracting users who want a column-based workflow instead of i3-style tiling.

Glossary

wlroots
Modular C library implementing Wayland compositor plumbing. Used by Sway, Wayfire, Cage.
IPC (Inter-Process Communication)
Sway's JSON-over-Unix-socket control protocol, compatible with i3's IPC.
Scene graph
wlroots tree structure of renderable nodes. Enables damage tracking and direct scanout.
Direct scanout
Bypassing compositing by presenting a client buffer directly to the display plane.
wlr-screencopy
Unstable Wayland protocol for capturing compositor output into a shared buffer.
xdg-shell
Stable Wayland protocol for desktop window management (toplevel, popup, positioner).
configure serial
Monotonic ID linking a compositor's configure event to the client's acknowledgment.