atagen

monolith

public
Star0
Watch0
4908715e181486e3430106463f206920112de59f
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
design/state-split.mdMarkdown153 lines6.6 KB

Splitting State

A sketch for review item 1. Nothing here is built yet.

Shape

pub struct State {
    pub backend: Backend,
    pub renderer: VulkanRenderer,
    pub wm: Wm,
    pub input: Input,
    pub services: Services,
    pub protocols: Protocols,
    pub config: Settings,
    pub dirty: Dirty,
    pub core: Core,
}

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.

Core

display, handle, signal, socket_name, start. These are the event loop and Wayland display plumbing that everything needs and nothing owns.

Backend

pub enum Backend {
    Drm(Drm),
    Nested(Nested),
    // Headless(Headless) later, for tests and the smoke harness.
}

What shared code needs from it:

  • 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.
  • render_node() -> Option<DrmNode>, replacing the render_node field.
  • syncobj(), replacing the syncobj field. It exists only on DRM.
  • session() for VT switching and the “is the session active” check. It returns None when nested.
  • render(state). Each backend keeps its own pacing and presentation (deadline scheduler or host frame callbacks) and calls the shared frame path for content.

after_dispatch matches once instead of calling both backends’ render and letting one no-op.

Wm, the window model

pub struct Wm {
    pub layout: Layout,
    pub windows: SecondaryMap<WindowId, Managed>,
    by_surface: HashMap<ObjectId, WindowId>,
    pub unmapped: Vec<Window>,
    pub space: Space<Window>,
    pub popups: PopupManager,
    pub anim: Animator,
    pub layer_focus: Option<WlSurface>,
}

pub struct Managed {
    pub window: Window,
    /// What the last layout pass configured, to spot tiled, floating and
    /// fullscreen switches.
    pub placed: Option<(Kind, Rect, OutputId)>,
    pub awaiting_buffer: bool,
    pub placed_by_rule: bool,
    pub list_handle: ForeignToplevelHandle,
    pub borders: [SolidColorBuffer; 4],
    /// Cached so publishers notice changes on commit instead of polling.
    pub app_id: String,
    pub title: String,
}
  • 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.
  • 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.
  • 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.
  • Kept per window:
    • last_kind, now placed
    • awaiting
    • placed_by_rule
    • scene::Borders
    • ListHandles
    • the polled title and app id
  • 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.
  • unmapped stays a plain Vec<Window>. These windows have no WindowId until their first commit, which is when rules can see their app id.
  • 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.

Input

seat, seat_state, pointer_pos, suppressed, drag, move_inverted, cursor (the xcursor images) and cursor_status.

Services

The subsystems that already have their own types, gathered so their fields stop sprawling:

  • lock
  • captures
  • imports
  • xwayland
  • launches
  • ipc
  • idle, a new struct of notifier, inhibitors and inhibited

Protocols

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.

The publishers go here too: workspaces, foreign_toplevels and toplevel_list. Their sync code moves to a publish.rs that reads Wm (item 3).

Settings

config, config_path, binds. Reload swaps this whole struct.

Dirty

pub struct Dirty {
    pub redraw: bool,
    pub cursor: bool,
    /// Something clients see changed: windows, titles, tags, focus.
    pub snapshot: bool,
}

needs_redraw and cursor_moved move here. snapshot gates the three publishers (item 3).

The frame path this enables (item 2)

pub fn compose(ctx: FrameCtx<'_>, output: &Output, o: OutputId, now: u64) -> Vec<Draw>
pub fn send_frame_callbacks(wm: &Wm, services: &Services, output: &Output, elapsed: Duration)

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.

Migration

Every step compiles and passes nix flake check on its own.

  1. Protocols, Core, Settings. This is pure field moves and accessor updates, with no logic changes.
  2. Services and Idle.
  3. Backend enum, folding in output_id, render_node and syncobj.
  4. Wm with Managed and by_surface. This is the one with logic: insert and remove, and lookups through the index.
  5. Input and Dirty.
  6. The frame path (item 2) and publishers (item 3) on top.

Open questions

  • Name the record Managed or WindowState? smithay already has a Window, and layout has layout::Window. Managed avoids a third Window.
  • 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.
  • 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.