Codebase review
A read of every crate on 2026-10-17, after M5 to M7 and the M6 follow-ups. Findings are grouped by theme and ordered by payback within each group. The proposed order of work is at the end.
1. The monolith crate has no internal boundaries
State(state.rs:55) has 70 public fields, and all 22 modules areimpl State. Nothing stops any function touching anything, and every new protocol adds a field and anew()line. The fields fall into a few groups:- Inert protocol globals that only their own handler touches. These are the 20 or so
*_statefields and_dmabuf_global. - The backend:
host,drm,output_id,render_node,syncobj. - The window model, spread over seven maps (see below).
- Subsystems with their own state:
lock,captures,imports,xwayland,launches,ipc, plusinhibitorsandinhibited. - Input:
suppressed,drag,pointer_pos,move_inverted,layer_focus,cursor_status. - Redraw flags:
needs_redraw,cursor_moved.
- Inert protocol globals that only their own handler touches. These are the 20 or so
The backend is two
Options plus side fields.host: Option<Host>anddrm: Option<Drm>both exist.output_idonly means something nested andrender_nodeonly on bare metal.after_dispatchcalls bothnested::renderanddrm::renderevery time, and each returns early when its option isNone. ABackendenum, or a small trait for render, present and outputs, would make the split explicit.One window’s state lives in seven places:
windowslast_kindawaitingplaced_by_rulescene::Borderstoplevels::ListHandles- the animator’s tracks, glides and spawns
remove_window(state.rs:404) cleans only some of them.awaitingandlast_kindkeep stale ids, andListHandlesis cleaned lazily inpublish_toplevels. A per-window record (Managed { window, kind, awaiting, by_rule, list_handle }) would make removal one operation.apply_layout(state.rs:469, about 110 lines) does six jobs: it collects the shown windows, detects kind switches for glides, retargets animations, configures clients, maps the space, then syncs focus and idle inhibit. It runs after every command, includingSpawn,Quit,ReloadandClose, throughcommands.rs:166.The commit handler (
handlers.rs:63) runs eight steps in sequence:- the buffer handler
- early import
- the window’s own commit
- capture damage
- first-commit mapping
- first-buffer detection
- popups
- layers
It looks up the window by root surface twice, scanning
windowsandunmappedlinearly each time. Routing on the surface’s role first (toplevel, popup, layer, lock, cursor, X11) would make each path short.
2. Drawing an output exists three times
drm::render_surface (drm.rs:585, 140 lines), nested::render (nested/mod.rs:169, 135 lines) and capture::output_elements (capture.rs:265) each assemble the cursor plus lock-or-scene. Each destructures State field by field to get past the borrow checker. On top of that, render_surface handles VRR switching, budget accounting, slow-frame logging, lock confirmation, capture damage and frame callbacks. Frame callbacks are written twice, once in each backend, with the lock and layer variants on top.
- One
frame_elements(world, output) -> Vec<Draw>used by all three. - One
send_frame_callbacks(world, output, elapsed). - The backends keep pacing and presentation only.
scene::buildtakes ten arguments (scene.rs:45,#[expect(clippy::too_many_arguments)]). That disappears once it takes a borrowed view struct.
Correctness smell. anim.prune(now) runs inside each output’s render with that output’s own now (drm.rs:658). With two outputs at different rates, the later one can prune state the earlier one is still drawing. It’s also why animating only re-arms the output that noticed it. Pruning belongs once per dispatch, after every output has drawn.
3. Snapshots for clients are rebuilt three ways, every dispatch
ipc::broadcast, publish_workspaces and publish_toplevels each derive a view of tags, windows and outputs from Layout plus windows. They run after every batch of events, and they lock every toplevel’s role data for titles even when nothing changed.
- A single derived model, computed only when a dirty flag is set by commits, layout and focus changes, would feed all three.
slotmap::Key::data(&id).as_ffi()appears in bothipc.rs:177andtoplevels.rs:26.- Tag names are looked up by id with a linear
tags().findtwice inipc.rs.Layoutlacks atag(id) -> &Tagaccessor.
4. Duplication
CLEARis defined identically indrm.rs:40,nested/mod.rs:32andcapture.rs.- Closing a window (xdg
send_closeor X11close) is written in bothcommands.rs:63andtoplevels.rs:57. - Window lookup is a linear scan in four places:
State::window_id,x11_window_id, the key search intoplevels.rs:42andListHandles::window. - Point-in-rect is written out inline in
drm.rs:787,drm.rs:645,capture.rs:270andstate.rssurface_under. The planner’sRecthas nocontains. - Rect helpers are scattered:
inset,centredandtranslateinlayout/lib.rs:979,pixelsinscene.rs:279,spanandwith_spanintree.rs. Dir,AxisandExpandScopeare defined in bothmonolith-ipcandmonolith-layout, with hand-written mapping functions incommands.rs:42.ModifierandModsfrom config are matched against smithay’sModifiersStateby hand in two places (commands.rs:29,input.rs:182).protocols/src/workspace.rsandtoplevel.rsrepeat the same skeleton: a manager list, a snapshot, a per-manager sync, bind-then-sync, and stop/destroyed pruning. If a third such protocol arrives, factor out the skeleton. With two, they’re tolerable.- Config defaults live in two places: the
Defaultimpls inconfig/src/lib.rsanddefault.toml, 111 lines. The only test (config/lib.rs:415) checks that the file parses, not that it agrees withDefault.Raw(lib.rs:264) mirrorsConfigfield by field as well.
5. Module placement
state.rsfree functions belong elsewhere.root,identityandconfigureare window helpers.gapsandxkb_configare config adapters.now_nsis a clock.handlers.rsmixes core compositor, xdg-shell, seat, selection, DnD, syncobj, decorations and workspace actions. The newer protocols each got their own module (layers,lock,idle,capture,toplevels). The older ones should follow so the file stops being a junk drawer.render-vk/src/renderer.rs(1113 lines) holds:- the smithay trait impls
- submission and the NVIDIA stall logic
- the 160-line
submitthat records all ops - upload and read-back
- the shm cache
DmabufImporter- the image barrier helpers (
barrier,acquire,releaseatrenderer.rs:690)
The barriers belong next to
sync.rs. Stall handling and the shm cache would be self-contained modules.layout/src/lib.rs(1008 lines) covers tags, outputs and parking, windows, focus history, scratchpads and arrangement in oneimpl. Scratchpads (765–853) and output parking (225–339) are the clean cuts.anim.rskeepsVulkanTextureinsideGhost, which ties the animation model to the GPU. The ghost’s texture could live in the scene, keyed by window, with the animator holding only geometry and timing.The DRM frame pacing (
Budget,Surface::next_deadline,drm.rs:86–163) and nested’sHost::predictare pure timing logic stuck inside backend files.
6. Code quality
- render-vk has 84
unsafesites and no// SAFETY:comments. The monolith crate does comment its few.submitkeeps oneunsafeblock across about 120 lines of recording. Narrower blocks with an invariant stated per helper would make the Vulkan code reviewable. - Silent
let _ =sits on X11configureandset_*(state.rs,xwayland.rs),notifier.successful,popup.send_configure,display.flush_clientsand protocol sends. The X11 ones can hide real failures, so log those at debug. commands::spawnstarts one OS thread per child just towait()on it. A SIGCHLD source on the event loop reaps them all without threads.drm::on_inputmaps absolute pointer motion onto the first output only (drm.rs:751). That’s wrong for tablets and multi-monitor.Budget::nsallocates and sorts 32 samples on every call, which is several times per frame. It’s cheap, but a fixed array or a running quantile would do.- A SIGKILLed monolith leaves
/tmp/.X*-lockand its socket behind (xwayland.rs:31cleans up only onDrop).claimrecovers stale locks by pid, but a recycled pid wastes a display number. - Comment density is uneven across modules. Do one pass against the commenting rules.
7. Tests: what earns its keep and what’s missing
What exists:
- planner: 5 named strategic cases plus 3 proptests (random transitions, rewinding, interrupts). High value, low count. The custom
Rnginside proptest is unusual but fine. - layout: a 2000-case proptest of random commands with
check_geometry, plus two named tree cases. One invariant added this week found a real bug at once. - motion: five small curve and velocity tests.
- render-vk: golden image tests, dmabuf round-trips and a transform test.
- config: one parse test.
- monolith: none.
Gaps, by value:
- Layout invariants are cheap and catch real bugs. Add these to
check_geometry:- the focused window is visible
- every visible window’s tag is viewed on its output, or the window is sticky or a shown scratchpad
- no window appears in two trees
- parked outputs restore their windows
- Frame pacing.
Budgetandnext_deadlineare pure, and they have been the source of most of the visible bugs: speed changes, periodic misses, slack decay. A small test that drives a timing sequence through them (a spike, then recovery, then miss and hold) would pin the behaviour that took several bare-metal rounds to find. - Protocol snapshot diffing in
workspace.rsandtoplevel.rsis fiddly and has a history of hidden-state bugs. Pull the diff into a pure function from (sent, snapshot) to events and test it with the cases that matter: removal, output enter and leave, and a property change with no structural change. - Config: assert that
default.tomlparses to the same values asDefault, which closes the drift in section 4. - Nested smoke harness. Every protocol this week was verified with a throwaway shell script that starts nested monolith with clients and greps logs. One
.nuharness (or anxtask) that runs scenarios (layer-shell plus a client, lock, capture, toplevel list, Xwayland) would make those checks repeatable. It could run inflake checkagainst lavapipe later.
Proposed order
- Split
Stateinto protocol globals, the backend enum, the window model with per-window records, subsystems and input. This unlocks most of the rest. - One output frame path: element assembly and frame callbacks shared by both backends and capture, with pruning once per dispatch.
- One derived snapshot with a dirty flag, feeding ipc, ext-workspace and wlr-toplevel.
- Mechanical dedup:
CLEAR, close, window index,Rect::contains, the shared enums between ipc and layout, the modifier mapping, and single-source config defaults. - File moves:
handlers.rsper protocol, the render-vk barriers, stall and shm cache modules, layout scratchpads and parking, frame pacing out ofdrm.rs. - Tests: layout invariants, pacing, snapshot diffs, config defaults, then the smoke harness.
- Hygiene sweep: SAFETY comments,
let _ =triage, the child reaper, absolute pointer mapping, and comments.