Internals
SamWM architecture, concurrency model, and memory management.
Architecture
cmd/samwm/main.go Entry point
internal/compositor/ Core compositor loop
internal/config/ Configuration loading
internal/layout/ Layout algorithms
internal/window/ Window types and store
pkg/wlroots/ CGo wlroots bindings
pkg/lua/ Lua scripting VMThreading Model
SamWM runs the wlroots event loop on a single locked OS thread (required by wlroots). All C callbacks execute on this thread.
Main goroutine (locked OS thread):
├── wl_display_run() ← blocks on Wayland events
│ ├── output frame callback
│ ├── keyboard events
│ ├── pointer events
│ ├── XDG surface events
│ └── ...
└── wl_display_destroy()
Signal goroutine:
├── SIGINT/SIGTERM handler
└── wl_display_terminate() ← safe from any threadConcurrency Safety
All mutable state uses sync.RWMutex:
- Window: Each window has its own mutex for geometry, state flags, metadata
- Store: Global window store with read-write lock
- Compositor: RWMutex on workspace state, output state
- Lua VM: RWMutex for window state cache, event handlers
Lock ordering (to prevent deadlocks):
- Compositor.mu
- Store.mu
- Window.mu
Memory Management
Allocation Patterns
- Window slices: Pre-allocated with capacity hints (
make([]*Window, 0, len)) - Window store:
map[int64]*Window— O(1) lookup, no slice scanning - Event handlers: Append-only slices, never shrunk
- Layout queue: BFS queue nodes allocated on stack when possible
GC Pressure Reduction
sync.RWMutexover channels for state access (zero allocation reads)sync.Poolnot used because window objects are long-lived, not transient- Pre-sized slices in hot paths (layout algorithms, window iteration)
- Atomic counters for window IDs (no mutex needed)
CGo Memory
- All wlroots objects are Go structs wrapping C pointers
cgo.Handleused for callback routing (Go GC tracks handles)- ListenerManager cleans up all C signal handlers on shutdown
- No manual
C.malloc/C.free— all C objects owned by wlroots
Layout Engine
Layouts implement the layout.Layout interface:
type Layout interface {
Name() string
Apply(windows []*window.Window, bounds window.Rect, cfg config.Config)
}Layouts receive window pointers and set geometry directly. The compositor then reads geometry and applies scene tree positions.
Layout Algorithm Patterns
| Layout | Algorithm | Time Complexity |
|---|---|---|
| master-stack | Two-pass partition | O(n) |
| dwindle | BFS split queue | O(n) |
| bsp | BFS split queue | O(n) |
| columns | Direct calculation | O(n) |
| floating | No-op | O(1) |
| fullscreen | Direct set | O(n) |
All layouts are O(n) — no nested loops, no sorting.
Workspace System
10 workspaces, each with:
- Independent window list
- Independent layout algorithm
- Scene tree subtree (enabled/disabled for visibility)
Workspace switching:
- Disable old workspace scene tree
- Enable new workspace scene tree
- Re-apply layout
- Focus first window in new workspace
Shutdown Sequence
- Signal received →
wl_display_terminate()(safe from any goroutine) wl_display_run()returns (on main thread)ListenerManager.RemoveAll()— disconnect all C signal handlerswl_display_destroy()— clean up wlroots resources
This ensures no callbacks fire after resources are freed.
Build
nix-shell --run "make build"
# or
nix-shell --run "go build -ldflags '-s -w' -o build/samwm ./cmd/samwm/"Binary size: ~3.2MB (stripped).
Dependencies
- wlroots 0.20 — Wayland compositor library (C, via CGo)
- gopher-lua — Lua 5.1 VM (pure Go)
- Go 1.22+ — Required for built-in
min/max