Architecture
Atrium is niri with the window-management layer replaced. A Wayland compositor is the display server: GPU rendering, buffer management, input routing, and forty-odd protocols. That is most of the work, and niri’s implementation of it on Smithay is proven, including on NVIDIA hybrid graphics. Where windows go is the part Atrium has different opinions about, and that part is cleanly separable.
The split
atrium
├── compositor core (niri, kept; fixes cherry-picked)
│ ├── src/backend/ DRM/KMS, libinput, GPU rendering, winit and headless backends
│ ├── src/handlers/ Wayland protocol trait implementations
│ ├── src/render_helpers/ rendering pipeline, damage tracking, shaders
│ ├── src/protocols/ layer-shell, foreign-toplevel, screencopy, ext-workspace, ...
│ └── src/utils/ spawning, scaling, signals, transactions
├── layout engine (ours)
│ ├── src/layout/mod.rs facade: global workspace pool, bring/send/cycle
│ ├── src/layout/workspace.rs TilingSpace + FloatingSpace per workspace
│ ├── src/layout/tiling_space.rs
│ ├── src/layout/tiling/ TilingLayout trait, five algorithms
│ ├── src/layout/floating.rs
│ └── src/layout/monitor.rs thin Monitor: output + workspace index
├── bridge (adapted)
│ ├── src/atrium.rs central state; calls into the layout
│ ├── src/input/ keybind dispatch to layout actions
│ └── src/ipc/ IPC server with workspace-aware commands
├── atrium-config/ KDL config model
└── atrium-ipc/ IPC types (shared with atrium-portal and the shell)
Core abstractions
Layout is a pure function. layout(windows, rect, params) → geometries. The TilingLayout trait encodes exactly that; all five algorithms are stateless and LayoutParams holds the state per workspace. A new algorithm is one file.
A workspace is a container. It owns a TilingSpace (ordered tiles, active algorithm, params), a FloatingSpace, an optional output binding, and per-workspace config overrides. It exists whether or not a monitor shows it.
A monitor is a viewport. Output hardware plus a workspace index. It owns no windows and no layout state, so disconnecting a monitor detaches a workspace with everything intact.
IPC is state plus events. A full snapshot on connect, then change events. Consumers filter client-side.
Animations are per-tile and clock-driven. A shared clock advances once per frame; when nothing animates, the output idles at zero CPU. animations { off } makes everything instant.
Extension is by trait. New algorithm: implement TilingLayout. New window behavior: implement LayoutElement. New event: add to the Event enum.
Data flow
A client creates an xdg_toplevel; the window sits unmapped until its first buffer; rules resolve on app id and title; it lands in the active workspace’s tiling or floating space; arrange() computes every tile; the window receives a configure with its size; it renders at that position. Close reverses it: remove, transfer focus, re-arrange, play the close animation.
A workspace switch snapshots the screen for a crossfade, repoints the monitor, transfers focus, emits WorkspaceActivated, and renders the new workspace over roughly 200 ms.
Hotplug: on connect, prefer the workspace last shown on that output, else the lowest unbound one, compute the working area minus layer-shell exclusive zones, arrange. On disconnect, remember the mapping and detach.
Resource model
| State | CPU | GPU |
|---|---|---|
| Idle desktop | 0% (epoll sleep) | none |
| Typing in a terminal | minimal | active output, damage-tracked |
| Workspace switch | brief | active output, ~12 frames |
| Ratio adjust | per keystroke | active output, one frame |
Single-monitor actions redraw only the active output.
What Atrium is not
Not a desktop environment (no bar, notifications, lock screen, or launcher inside the compositor). Not a floating window manager (floating is an escape hatch). Not a scrolling window manager. Not tag-based (workspace and window are one-to-one).
GPU recovery
The compositor creates robust GL contexts (LOSE_CONTEXT_ON_RESET), polls reset status each frame, and on a detected reset settles for two seconds, then rebuilds the GPU half in place: EGL context, GBM device, renderer, modeset. Wayland connections and window state stay in memory and the dmabuf global is kept alive through the rebuild, so clients survive. recover-gpu is bindable as a manual trigger. This is why Atrium runs on a private Smithay fork pinned by revision.