atagen

monolith

public
Star0
Watch0

Commit 0de2957646aa

tarball

atagen <boss@atagen.co>2026-10-17 02:10 UTC

1 file changed+153-0
design: sketch the State split
Added file: design/state-split.md+153 additions
@@ -0,0 +1,153 @@
1+# Splitting `State`
2+
3+A sketch for review item 1. Nothing here is built yet.
4+
5+## Shape
6+
7+```rust
8+pub struct State {
9+ pub backend: Backend,
10+ pub renderer: VulkanRenderer,
11+ pub wm: Wm,
12+ pub input: Input,
13+ pub services: Services,
14+ pub protocols: Protocols,
15+ pub config: Settings,
16+ pub dirty: Dirty,
17+ pub core: Core,
18+}
19+```
20+
21+smithay requires every handler trait on `State`, and that stays. The `*_state(&mut self)` accessors just reach one level deeper. What changes is that functions take the sub-structs they need instead of `&mut State`, and disjoint borrows stop needing a 15-field destructure.
22+
23+### `Core`
24+
25+`display`, `handle`, `signal`, `socket_name`, `start`. These are the event loop and Wayland display plumbing that everything needs and nothing owns.
26+
27+### `Backend`
28+
29+```rust
30+pub enum Backend {
31+ Drm(Drm),
32+ Nested(Nested),
33+ // Headless(Headless) later, for tests and the smoke harness.
34+}
35+```
36+
37+What shared code needs from it:
38+
39+- `output(OutputId) -> Option<&Output>` and `outputs()`. This replaces `smithay_output`, the nested-only `output_id` field and `drm::output_for`. Each backend keeps its own `OutputId -> Output` map.
40+- `render_node() -> Option<DrmNode>`, replacing the `render_node` field.
41+- `syncobj()`, replacing the `syncobj` field. It exists only on DRM.
42+- `session()` for VT switching and the "is the session active" check. It returns `None` when nested.
43+- `render(state)`. Each backend keeps its own pacing and presentation (deadline scheduler or host frame callbacks) and calls the shared frame path for content.
44+
45+`after_dispatch` matches once instead of calling both backends' render and letting one no-op.
46+
47+### `Wm`, the window model
48+
49+```rust
50+pub struct Wm {
51+ pub layout: Layout,
52+ pub windows: SecondaryMap<WindowId, Managed>,
53+ by_surface: HashMap<ObjectId, WindowId>,
54+ pub unmapped: Vec<Window>,
55+ pub space: Space<Window>,
56+ pub popups: PopupManager,
57+ pub anim: Animator,
58+ pub layer_focus: Option<WlSurface>,
59+}
60+
61+pub struct Managed {
62+ pub window: Window,
63+ /// What the last layout pass configured, to spot tiled, floating and
64+ /// fullscreen switches.
65+ pub placed: Option<(Kind, Rect, OutputId)>,
66+ pub awaiting_buffer: bool,
67+ pub placed_by_rule: bool,
68+ pub list_handle: ForeignToplevelHandle,
69+ pub borders: [SolidColorBuffer; 4],
70+ /// Cached so publishers notice changes on commit instead of polling.
71+ pub app_id: String,
72+ pub title: String,
73+}
74+```
75+
76+- **`SecondaryMap` keyed by the layout's `WindowId`.** Layout already owns that key space, and the window model becomes a typed extension of it, not a parallel `HashMap`.
77+- **`by_surface` gives O(1) lookup by root surface.** It replaces `window_id`, `x11_window_id`, the toplevel key search and `ListHandles::window`. X11 windows join it when xwayland-shell associates their surface.
78+- **One method each for adding and removing.** `Wm::insert` and `Wm::remove(id) -> Option<Managed>` are the only ways in and out. Removal drops the index entry, sends the list handle `closed`, and tells the animator to forget the window. Nothing can be left behind in another map.
79+- **Kept per window:**
80+ - `last_kind`, now `placed`
81+ - `awaiting`
82+ - `placed_by_rule`
83+ - `scene::Borders`
84+ - `ListHandles`
85+ - the polled title and app id
86+- **Not moved into the record:** the animator's per-output tracks are multi-window plans, so they stay in `Animator`. Its per-window glides and spawns could move into the record, but keeping all motion state in one place is easier to reason about. Ghost textures move from `Animator` to the scene, so the animator holds only geometry and time.
87+- `unmapped` stays a plain `Vec<Window>`. These windows have no `WindowId` until their first commit, which is when rules can see their app id.
88+- Methods that are about windows move from `state.rs` to `wm.rs`: `map_window`, `remove_window`, the pure half of `apply_layout` (diffing placements into glides, retargets and configures), `configure`, `root`, `identity` and close.
89+
90+### `Input`
91+
92+`seat`, `seat_state`, `pointer_pos`, `suppressed`, `drag`, `move_inverted`, `cursor` (the xcursor images) and `cursor_status`.
93+
94+### `Services`
95+
96+The subsystems that already have their own types, gathered so their fields stop sprawling:
97+
98+- `lock`
99+- `captures`
100+- `imports`
101+- `xwayland`
102+- `launches`
103+- `ipc`
104+- `idle`, a new struct of `notifier`, `inhibitors` and `inhibited`
105+
106+### `Protocols`
107+
108+The inert globals: `compositor`, `xdg_shell`, `layer_shell`, `xdg_decoration`, `shm`, `dmabuf` plus its global, `output_manager`, the three capture states, `activation`, the three selection states, `session_lock`, `idle_inhibit`, `cursor_shape`, `xwayland_shell`, `data_device`.
109+
110+The publishers go here too: `workspaces`, `foreign_toplevels` and `toplevel_list`. Their sync code moves to a `publish.rs` that reads `Wm` (item 3).
111+
112+### `Settings`
113+
114+`config`, `config_path`, `binds`. Reload swaps this whole struct.
115+
116+### `Dirty`
117+
118+```rust
119+pub struct Dirty {
120+ pub redraw: bool,
121+ pub cursor: bool,
122+ /// Something clients see changed: windows, titles, tags, focus.
123+ pub snapshot: bool,
124+}
125+```
126+
127+`needs_redraw` and `cursor_moved` move here. `snapshot` gates the three publishers (item 3).
128+
129+## The frame path this enables (item 2)
130+
131+```rust
132+pub fn compose(ctx: FrameCtx<'_>, output: &Output, o: OutputId, now: u64) -> Vec<Draw>
133+pub fn send_frame_callbacks(wm: &Wm, services: &Services, output: &Output, elapsed: Duration)
134+```
135+
136+`FrameCtx` borrows `&mut renderer`, `&mut wm` (for the border buffers), `&input` (for the cursor), `&services.lock`, `&config.decoration` and the unmanaged X11 list. DRM, nested and capture all call `compose`. `anim.prune` moves to `after_dispatch`, after every output has drawn.
137+
138+## Migration
139+
140+Every step compiles and passes `nix flake check` on its own.
141+
142+1. `Protocols`, `Core`, `Settings`. This is pure field moves and accessor updates, with no logic changes.
143+2. `Services` and `Idle`.
144+3. `Backend` enum, folding in `output_id`, `render_node` and `syncobj`.
145+4. `Wm` with `Managed` and `by_surface`. This is the one with logic: insert and remove, and lookups through the index.
146+5. `Input` and `Dirty`.
147+6. The frame path (item 2) and publishers (item 3) on top.
148+
149+## Open questions
150+
151+- **Name the record `Managed` or `WindowState`?** smithay already has a `Window`, and layout has `layout::Window`. `Managed` avoids a third `Window`.
152+- **Keep `Space<Window>`?** It is used for hit testing, output enter and leave, and `element_under`. monolith positions everything itself, so Space mostly mirrors layout. Replacing it with our own hit test over the layout arrangement is possible, but not part of this split.
153+- **Does `layer_focus` belong in `Wm` or `Input`?** It is keyboard focus policy, which sits next to `sync_focus` in `Wm`, so this sketch puts it there.