atagen

monolith

public
Star0
Watch0
b03e3b32603ac851a66da1909687f43d6fa81c80
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git

Blame: design/state-split.md

0de2957646aaatagen2026-10-17 01:57 UTC1
# Splitting `State`
0de2957646aaatagen2026-10-17 01:57 UTC2
0de2957646aaatagen2026-10-17 01:57 UTC3
A sketch for review item 1. Nothing here is built yet.
0de2957646aaatagen2026-10-17 01:57 UTC4
0de2957646aaatagen2026-10-17 01:57 UTC5
## Shape
0de2957646aaatagen2026-10-17 01:57 UTC6
0de2957646aaatagen2026-10-17 01:57 UTC7
```rust
0de2957646aaatagen2026-10-17 01:57 UTC8
pub struct State {
0de2957646aaatagen2026-10-17 01:57 UTC9
    pub backend: Backend,
0de2957646aaatagen2026-10-17 01:57 UTC10
    pub renderer: VulkanRenderer,
0de2957646aaatagen2026-10-17 01:57 UTC11
    pub wm: Wm,
0de2957646aaatagen2026-10-17 01:57 UTC12
    pub input: Input,
0de2957646aaatagen2026-10-17 01:57 UTC13
    pub services: Services,
0de2957646aaatagen2026-10-17 01:57 UTC14
    pub protocols: Protocols,
0de2957646aaatagen2026-10-17 01:57 UTC15
    pub config: Settings,
0de2957646aaatagen2026-10-17 01:57 UTC16
    pub dirty: Dirty,
0de2957646aaatagen2026-10-17 01:57 UTC17
    pub core: Core,
0de2957646aaatagen2026-10-17 01:57 UTC18
}
0de2957646aaatagen2026-10-17 01:57 UTC19
```
0de2957646aaatagen2026-10-17 01:57 UTC20
0de2957646aaatagen2026-10-17 01:57 UTC21
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.
0de2957646aaatagen2026-10-17 01:57 UTC22
0de2957646aaatagen2026-10-17 01:57 UTC23
### `Core`
0de2957646aaatagen2026-10-17 01:57 UTC24
0de2957646aaatagen2026-10-17 01:57 UTC25
`display`, `handle`, `signal`, `socket_name`, `start`. These are the event loop and Wayland display plumbing that everything needs and nothing owns.
0de2957646aaatagen2026-10-17 01:57 UTC26
0de2957646aaatagen2026-10-17 01:57 UTC27
### `Backend`
0de2957646aaatagen2026-10-17 01:57 UTC28
0de2957646aaatagen2026-10-17 01:57 UTC29
```rust
0de2957646aaatagen2026-10-17 01:57 UTC30
pub enum Backend {
0de2957646aaatagen2026-10-17 01:57 UTC31
    Drm(Drm),
0de2957646aaatagen2026-10-17 01:57 UTC32
    Nested(Nested),
0de2957646aaatagen2026-10-17 01:57 UTC33
    // Headless(Headless) later, for tests and the smoke harness.
0de2957646aaatagen2026-10-17 01:57 UTC34
}
0de2957646aaatagen2026-10-17 01:57 UTC35
```
0de2957646aaatagen2026-10-17 01:57 UTC36
0de2957646aaatagen2026-10-17 01:57 UTC37
What shared code needs from it:
0de2957646aaatagen2026-10-17 01:57 UTC38
0de2957646aaatagen2026-10-17 01:57 UTC39
- `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.
0de2957646aaatagen2026-10-17 01:57 UTC40
- `render_node() -> Option<DrmNode>`, replacing the `render_node` field.
0de2957646aaatagen2026-10-17 01:57 UTC41
- `syncobj()`, replacing the `syncobj` field. It exists only on DRM.
0de2957646aaatagen2026-10-17 01:57 UTC42
- `session()` for VT switching and the "is the session active" check. It returns `None` when nested.
0de2957646aaatagen2026-10-17 01:57 UTC43
- `render(state)`. Each backend keeps its own pacing and presentation (deadline scheduler or host frame callbacks) and calls the shared frame path for content.
0de2957646aaatagen2026-10-17 01:57 UTC44
0de2957646aaatagen2026-10-17 01:57 UTC45
`after_dispatch` matches once instead of calling both backends' render and letting one no-op.
0de2957646aaatagen2026-10-17 01:57 UTC46
0de2957646aaatagen2026-10-17 01:57 UTC47
### `Wm`, the window model
0de2957646aaatagen2026-10-17 01:57 UTC48
0de2957646aaatagen2026-10-17 01:57 UTC49
```rust
0de2957646aaatagen2026-10-17 01:57 UTC50
pub struct Wm {
0de2957646aaatagen2026-10-17 01:57 UTC51
    pub layout: Layout,
0de2957646aaatagen2026-10-17 01:57 UTC52
    pub windows: SecondaryMap<WindowId, Managed>,
0de2957646aaatagen2026-10-17 01:57 UTC53
    by_surface: HashMap<ObjectId, WindowId>,
0de2957646aaatagen2026-10-17 01:57 UTC54
    pub unmapped: Vec<Window>,
0de2957646aaatagen2026-10-17 01:57 UTC55
    pub space: Space<Window>,
0de2957646aaatagen2026-10-17 01:57 UTC56
    pub popups: PopupManager,
0de2957646aaatagen2026-10-17 01:57 UTC57
    pub anim: Animator,
0de2957646aaatagen2026-10-17 01:57 UTC58
    pub layer_focus: Option<WlSurface>,
0de2957646aaatagen2026-10-17 01:57 UTC59
}
0de2957646aaatagen2026-10-17 01:57 UTC60
0de2957646aaatagen2026-10-17 01:57 UTC61
pub struct Managed {
0de2957646aaatagen2026-10-17 01:57 UTC62
    pub window: Window,
0de2957646aaatagen2026-10-17 01:57 UTC63
    /// What the last layout pass configured, to spot tiled, floating and
0de2957646aaatagen2026-10-17 01:57 UTC64
    /// fullscreen switches.
0de2957646aaatagen2026-10-17 01:57 UTC65
    pub placed: Option<(Kind, Rect, OutputId)>,
0de2957646aaatagen2026-10-17 01:57 UTC66
    pub awaiting_buffer: bool,
0de2957646aaatagen2026-10-17 01:57 UTC67
    pub placed_by_rule: bool,
0de2957646aaatagen2026-10-17 01:57 UTC68
    pub list_handle: ForeignToplevelHandle,
0de2957646aaatagen2026-10-17 01:57 UTC69
    pub borders: [SolidColorBuffer; 4],
0de2957646aaatagen2026-10-17 01:57 UTC70
    /// Cached so publishers notice changes on commit instead of polling.
0de2957646aaatagen2026-10-17 01:57 UTC71
    pub app_id: String,
0de2957646aaatagen2026-10-17 01:57 UTC72
    pub title: String,
0de2957646aaatagen2026-10-17 01:57 UTC73
}
0de2957646aaatagen2026-10-17 01:57 UTC74
```
0de2957646aaatagen2026-10-17 01:57 UTC75
0de2957646aaatagen2026-10-17 01:57 UTC76
- **`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`.
0de2957646aaatagen2026-10-17 01:57 UTC77
- **`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.
0de2957646aaatagen2026-10-17 01:57 UTC78
- **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.
0de2957646aaatagen2026-10-17 01:57 UTC79
- **Kept per window:**
0de2957646aaatagen2026-10-17 01:57 UTC80
  - `last_kind`, now `placed`
0de2957646aaatagen2026-10-17 01:57 UTC81
  - `awaiting`
0de2957646aaatagen2026-10-17 01:57 UTC82
  - `placed_by_rule`
0de2957646aaatagen2026-10-17 01:57 UTC83
  - `scene::Borders`
0de2957646aaatagen2026-10-17 01:57 UTC84
  - `ListHandles`
0de2957646aaatagen2026-10-17 01:57 UTC85
  - the polled title and app id
0de2957646aaatagen2026-10-17 01:57 UTC86
- **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.
0de2957646aaatagen2026-10-17 01:57 UTC87
- `unmapped` stays a plain `Vec<Window>`. These windows have no `WindowId` until their first commit, which is when rules can see their app id.
0de2957646aaatagen2026-10-17 01:57 UTC88
- 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.
0de2957646aaatagen2026-10-17 01:57 UTC89
0de2957646aaatagen2026-10-17 01:57 UTC90
### `Input`
0de2957646aaatagen2026-10-17 01:57 UTC91
0de2957646aaatagen2026-10-17 01:57 UTC92
`seat`, `seat_state`, `pointer_pos`, `suppressed`, `drag`, `move_inverted`, `cursor` (the xcursor images) and `cursor_status`.
0de2957646aaatagen2026-10-17 01:57 UTC93
0de2957646aaatagen2026-10-17 01:57 UTC94
### `Services`
0de2957646aaatagen2026-10-17 01:57 UTC95
0de2957646aaatagen2026-10-17 01:57 UTC96
The subsystems that already have their own types, gathered so their fields stop sprawling:
0de2957646aaatagen2026-10-17 01:57 UTC97
0de2957646aaatagen2026-10-17 01:57 UTC98
- `lock`
0de2957646aaatagen2026-10-17 01:57 UTC99
- `captures`
0de2957646aaatagen2026-10-17 01:57 UTC100
- `imports`
0de2957646aaatagen2026-10-17 01:57 UTC101
- `xwayland`
0de2957646aaatagen2026-10-17 01:57 UTC102
- `launches`
0de2957646aaatagen2026-10-17 01:57 UTC103
- `ipc`
0de2957646aaatagen2026-10-17 01:57 UTC104
- `idle`, a new struct of `notifier`, `inhibitors` and `inhibited`
0de2957646aaatagen2026-10-17 01:57 UTC105
0de2957646aaatagen2026-10-17 01:57 UTC106
### `Protocols`
0de2957646aaatagen2026-10-17 01:57 UTC107
0de2957646aaatagen2026-10-17 01:57 UTC108
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`.
0de2957646aaatagen2026-10-17 01:57 UTC109
0de2957646aaatagen2026-10-17 01:57 UTC110
The publishers go here too: `workspaces`, `foreign_toplevels` and `toplevel_list`. Their sync code moves to a `publish.rs` that reads `Wm` (item 3).
0de2957646aaatagen2026-10-17 01:57 UTC111
0de2957646aaatagen2026-10-17 01:57 UTC112
### `Settings`
0de2957646aaatagen2026-10-17 01:57 UTC113
0de2957646aaatagen2026-10-17 01:57 UTC114
`config`, `config_path`, `binds`. Reload swaps this whole struct.
0de2957646aaatagen2026-10-17 01:57 UTC115
0de2957646aaatagen2026-10-17 01:57 UTC116
### `Dirty`
0de2957646aaatagen2026-10-17 01:57 UTC117
0de2957646aaatagen2026-10-17 01:57 UTC118
```rust
0de2957646aaatagen2026-10-17 01:57 UTC119
pub struct Dirty {
0de2957646aaatagen2026-10-17 01:57 UTC120
    pub redraw: bool,
0de2957646aaatagen2026-10-17 01:57 UTC121
    pub cursor: bool,
0de2957646aaatagen2026-10-17 01:57 UTC122
    /// Something clients see changed: windows, titles, tags, focus.
0de2957646aaatagen2026-10-17 01:57 UTC123
    pub snapshot: bool,
0de2957646aaatagen2026-10-17 01:57 UTC124
}
0de2957646aaatagen2026-10-17 01:57 UTC125
```
0de2957646aaatagen2026-10-17 01:57 UTC126
0de2957646aaatagen2026-10-17 01:57 UTC127
`needs_redraw` and `cursor_moved` move here. `snapshot` gates the three publishers (item 3).
0de2957646aaatagen2026-10-17 01:57 UTC128
0de2957646aaatagen2026-10-17 01:57 UTC129
## The frame path this enables (item 2)
0de2957646aaatagen2026-10-17 01:57 UTC130
0de2957646aaatagen2026-10-17 01:57 UTC131
```rust
0de2957646aaatagen2026-10-17 01:57 UTC132
pub fn compose(ctx: FrameCtx<'_>, output: &Output, o: OutputId, now: u64) -> Vec<Draw>
0de2957646aaatagen2026-10-17 01:57 UTC133
pub fn send_frame_callbacks(wm: &Wm, services: &Services, output: &Output, elapsed: Duration)
0de2957646aaatagen2026-10-17 01:57 UTC134
```
0de2957646aaatagen2026-10-17 01:57 UTC135
0de2957646aaatagen2026-10-17 01:57 UTC136
`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.
0de2957646aaatagen2026-10-17 01:57 UTC137
0de2957646aaatagen2026-10-17 01:57 UTC138
## Migration
0de2957646aaatagen2026-10-17 01:57 UTC139
0de2957646aaatagen2026-10-17 01:57 UTC140
Every step compiles and passes `nix flake check` on its own.
0de2957646aaatagen2026-10-17 01:57 UTC141
0de2957646aaatagen2026-10-17 01:57 UTC142
1. `Protocols`, `Core`, `Settings`. This is pure field moves and accessor updates, with no logic changes.
0de2957646aaatagen2026-10-17 01:57 UTC143
2. `Services` and `Idle`.
0de2957646aaatagen2026-10-17 01:57 UTC144
3. `Backend` enum, folding in `output_id`, `render_node` and `syncobj`.
0de2957646aaatagen2026-10-17 01:57 UTC145
4. `Wm` with `Managed` and `by_surface`. This is the one with logic: insert and remove, and lookups through the index.
0de2957646aaatagen2026-10-17 01:57 UTC146
5. `Input` and `Dirty`.
0de2957646aaatagen2026-10-17 01:57 UTC147
6. The frame path (item 2) and publishers (item 3) on top.
0de2957646aaatagen2026-10-17 01:57 UTC148
0de2957646aaatagen2026-10-17 01:57 UTC149
## Open questions
0de2957646aaatagen2026-10-17 01:57 UTC150
0de2957646aaatagen2026-10-17 01:57 UTC151
- **Name the record `Managed` or `WindowState`?** smithay already has a `Window`, and layout has `layout::Window`. `Managed` avoids a third `Window`.
0de2957646aaatagen2026-10-17 01:57 UTC152
- **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.
0de2957646aaatagen2026-10-17 01:57 UTC153
- **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.