compd

The MixOS Wayland compositor.

compd owns KMS scanout, the GLES renderer and one shared wgpu device, its own egui surfaces (capture overlays, notifications, the Mix Scenes shell), the Xwayland window manager, frame pacing, fractional scale, input devices and capture. On top of that sits the MixOS policy layer: the agent seat, the comp Bus service (verbs, topics and the props tree), the frame and presentation ledgers, panel holders and hot corners, and strict idle discipline (no frames on a static screen).

Bevy, when enabled, may render effects on the shared wgpu device. It never owns the frame loop, scheduling, compositing or protocol handling.

Settings at startup

Quoin prepares cached or embedded settings while its existing Bus supervisor registers. Its persistent cache is service-owned under the configured variable data directory at compd/cache/settings; all validation and filesystem work runs on the scene host's Bus worker (scene-host lane). shell.props.get exposes settings and settings_cache evidence. A cache persistence receipt must match the current applied identity before it describes that presentation. Transport outages and terminal registration refusal preserve usable appearance. Only explicit initial refusal permits the configured scene service override; an established shell identity never switches names. Shutdown drains the newest activated capture and closes the worker under one two-second budget with a bounded completion margin. Timeouts report failure without claiming persistence.

The settings come from settingsd through the settings library.

Toast scenes

A scene with window: {"kind":"toast","w":360,"h":120} uses a separate, pointer-interactive toast seat. It never takes keyboard focus, including on click or with an authored autofocus field. It draws above windows and edge panels, below the dialog and session lock.

The draft intent verbs are shell.toast.show {scene, output?, h?} and shell.toast.hide {scene}. Both reply {scene, visible, applied:true}. Scenes load hidden; show reserves the one toast seat and hide releases it. A different scene's show returns rc 10 TOAST_BUSY with holder:{scene, owner}. Unknown scenes return NOT_FOUND, other mount kinds NOT_TOAST, and a nonnumeric height INVALID_HEIGHT.

The output defaults to the selected output, as for dialogs. Width clamps to 240–2048 logical px and height to 60–2048 (defaults 360×120). A show can override height; showing the same scene again resizes it, and omitting height preserves its current override. Reload updates the authored geometry and keeps visibility; changing kind requires an unload first.

Placement is top right, 12 logical px inside the usable area left by layer-shell exclusive zones and docked scene panels. The drawn size fits inside that area on small outputs. Chrome defaults to false; chrome:true adds a title frame and a close button that hides the toast.

Layout

PathWhat
src/the compd binary: CLI, event loop, Bus and scene wiring
crates/compd's private crates (engine, protocols, policy, backends)
layers.conf.mixthe private crates' dependency order, checked by tools/layering_gate.mix
tests/integration tests

The source is services/compd. The desktop-tier gates that run compd nested or on hardware live in tests/desktop/ at the repository root.

Build

cargo build --profile release-fast -p compd                     # nested backend
cargo build --profile release-fast -p compd --no-default-features \
    --features backend-native                                   # KMS backend
cargo build --profile release-fast -p compd --features backend-all   # all three
cargo build --profile release-fast -p compd --no-default-features \
    --features backend-headless                                 # headless

capture-ffmpeg adds hardware video capture through libav; the default build links no FFmpeg.

desktop-dbus explicitly enables the optional desktop compatibility adapters. The default native session has no D-Bus client or portal dependency. It passes environment directly to children; delegated cgroup placement before exec and cgroup.kill provide complete cleanup when MIXOS_CONTAIN_CHILDREN=1.

Run

Nested, inside another Wayland session:

compd --nested --socket wayland-compd --config-file=settings.json

On a VT, through the seat (libseat/seatd):

compd kms-live --device /dev/dri/card1 --connector HDMI-A-1 --scale 2.5

compd waits for its own VT to become active and never switches VT itself. --bus-service NAME and --scene-service NAME override the Bus names (default comp); compd --help lists every flag. Settings are read from --config-file, else ~/.config/compd/settings.json. Preferences are read from preferences.json beside it.

Native fixed-refresh outputs use late-latch composition by default. After a flip with pending redraws, each CRTC waits until the next kernel-timestamped vblank minus its render budget. Two 50-commit buckets retain the maximum time from the armed deadline (or clamped plan instant) to commit return, including dispatch delay and sibling rendering. Exportable render fences also contribute their kernel signal timestamp through a non-blocking calloop source. GPU callback dispatch latency is excluded; unavailable or invalid timestamps leave GPU tails to the learned floor. Modesets and the first commit and flip after reset do not train steady-state cost or phase.

Deadline anchoring accepts DRM scanout timestamps up to 2 ms ahead of the handler's monotonic clock, covering an interrupt delivered during blanking before active scanout begins. Larger future offsets and dispatch delays over 200 ms are rejected. The tearing phase keeps its existing arrival fallback for future timestamps.

The budget is the larger of that measured maximum and a learned floor, plus late_latch_margin_us (default 1500). A miss on a deferred frame raises the floor to the submitted budget minus margin plus one eighth of a refresh, capped at one refresh interval. The floor shrinks by only 50 µs per 100 target hits; idle gaps preserve that run. A miss, or unknown feedback on a deferred frame, breaks the run. The initial estimate is 2 ms. The 1.5 ms margin covers residual jitter and GPU observation error; hardware timing still needs measurement under load.

Set late_latch:false in the settings file to disable it, or set the emergency environment switch COMPD_DISABLE_LATE_LATCH (any value). Both take effect at startup without rebuilding; the margin uses the same settings file. Schema 7 adds these fields to older files in memory. Startup, the first redraw after idle, untrustworthy timestamps, over-budget frames, tearing, governed pacing, active VRR on that CRTC and active capture retain immediate composition. The VRR preference alone does not disable late latch: unsupported connectors, refused requests and VRR-off pipes still use fixed-refresh deadlines. The gate reads cached committed DRM state, independently for each output. Nested and headless backends keep their existing pacing. Frame callbacks and presentation feedback use their existing paths, including empty-frame callback timers.

With COMPD_FRAME_TRACE=1, kms_latch_deferred and kms_latch_deadline describe the wait and render; other kms_latch_* decision stages name the immediate-path reason. kms_latch_hit, kms_latch_miss or kms_latch_unknown on completion records the result. These ledger trace events use the same output ID as kms_queue_frame, a target CLOCK_MONOTONIC timestamp in detail (microseconds, zero if unavailable), and the budget in aux (microseconds). The result uses the submitted frame's budget. A hit allows half an interval of timestamp tolerance; a full missed vblank is a miss. Tracing off performs no extra ID hashing or trace clock reads.

With the backend-headless feature, a render node can drive a virtual output without a VT, seat, input device or parent compositor:

compd --headless --socket wayland-headless --size 1920x1080 --scale 1 \
    --render-node /dev/dri/renderD128 --config-file settings.json

The output is headless-0 at 60 Hz; --size sets physical pixels and --scale sets the logical-to-physical scale (defaults 1920x1080 and 1). --render-node defaults to the render_node setting. EGL must report the selected node. The default Bus name is comp-headless, overridden with --bus-service; the binding profile is headless, without VT or host interception chords. XWayland is disabled. Startup preserves inherited scheduling and nice values and does not publish desktop session variables.

Frames repaint into alternating offscreen textures only when the scene changes or a capture is due, capped at 60 Hz. COMPD_HEADLESS_REFRESH_HZ selects 30–60 Hz (default 60) for both the virtual mode and timer. Completion is waited on before screencopy, frame callbacks and software presentation feedback. Both zwlr_screencopy and comp.capture.frame work on idle outputs. Captures choose cursor inclusion individually: the normal frame excludes cursor elements, and a second pass runs only for captures requesting them. Callback-only commits and unchanged captures do not increase the window's presentation.presented counter.

The ignored headless GPU tests need a built compd with this feature beside the test binaries, or selected by COMPD_TEST_BINARY. The capture tests also need built noded and testkit-screenshot binaries beside them; they start an owned private broker, without using a running desktop's Bus. The plain EGL client probe needs weston-simple-egl (override its path with COMPD_TEST_EGL_CLIENT); it records the first buffer's actual creation request and checks captured pixels.

The Phase 5 app test also needs editd, the testkit binaries (including testkit-ssim) and cargo build -p ced --example ced-headless-probe. This test-only launcher adds wgpu adapter logging to ced's normal entry point; production apps and backends keep their behaviour. The test runs ced against its own Bus and compositor, checks an idle pair at SSIM ≥ 0.99, types through the agent seat, requires SSIM < 0.999 and reads the exact text through ced.state. PNGs and the client trace remain in headless-app-artifacts beside the built binaries. Supply COMPD_TEST_APP_REFERENCE=/absolute/ref.png to additionally compare the initial headless window with a console reference at the fixed 0.99 threshold; absence is explicitly reported as unmeasured.

Capture that reference in the console session environment with a running editd, no existing ced window and a scale-1 output:

/opt/mixos/bin/mix services/compd/crates/testkit/scripts/app_reference.mix /tmp/ced-reference.png --comp comp --output OUTPUT

The same helper launches the headless test window. It gives ced fresh private app state, places its geometry at (80,60), waits for 800×560, creates a blank document and captures its window without the cursor via comp.capture.frame. Its last stdout line is JSON containing the PNG path, requested geometry, observed window row, scale and capture metadata. It closes its own child after capture; the GPU test uses --keep to take ownership for input checks. Use the same theme, fonts and application build for both references. Session artefacts are left beside the PNG in <path>.session, which must not already exist. testkit-ssim A.png B.png --threshold 0.99 compares the pair; it also accepts --crop X,Y,WxH, or tightly packed raw RGBA with --size WxH. Its grayscale metric uses uniform sliding 8×8 valid windows, population covariance, rounded Rec.601 luma, K1=0.01, K2=0.03 and L=255; alpha is ignored. It returns 1 below threshold and 2 for invalid arguments or images, including mismatched sizes.

compd-headless@.service runs inside a headless desk with its own noded, session bus, PipeWire and portald. %i is the session slot (for example rec), with settings in /etc/mixos/session/%i.env and Bus service comp inside that desk. It uses the render group, no seatd or tty, nice 10 and mixos-headless.slice (CPUQuota=200%). Provisioning places the desk's capture services and encoder in the same slice. Both units are owned by services/compd/units/; they do not create the desk.

testkit-presentation-probe --rate 60 --hold-s 60 measures independent commit pacing and presentation misses. Add --stall-every 30 --stall-ms 50 for a positive control. Its header documents the pixel frame ID marker; services/compd/crates/testkit/scripts/frame_ids.mix FILE.rgba --size WxH decodes unscaled raw RGBA client content (PNG is not supported). Run timing assertions with an optimised compositor, built with --profile release-fast and selected through COMPD_TEST_BINARY; the test harness and probe can remain debug builds. Report the binary profile with the measured rates and misses. Debug compositor overhead is part of its measured presentation latency and can dominate a one-interval deadline.

cursor_sensitivity in preferences.json is a speed multiplier on top of the output scale: compd multiplies each relative pointer delta by both, so 1.0 moves the cursor at the same logical speed at every scale. Clients asking for relative motion get the raw deltas.

Session activity

comp.session.get {} is a draft query returning {"version":1,"active":true,"vt":3,"since_ms":1792108800123}. active describes this compositor's libseat session, including while paused after a console VT switch. since_ms is Unix milliseconds at the last transition, or at compd's Bus startup before the first transition.

Topic <service>.session.changed (normally comp.session.changed) publishes {"version":1,"active":false,"vt":3,"since_ms":1792108800456,"event_seq":42} once at startup and once on each pause or resume. It uses the shared observation sequence, outbox and gap reporting. The publisher runs on its own Bus worker, so pause events do not wait for rendering or resumption. Subscribe before querying to establish the current state without polling; on an observation gap, query again. Across desks, the most recent active:true transition identifies the desk the human is viewing.

The session unit supplies MIXOS_VT=%i. compd reads it once when starting its Bus service; vt is a u32 or null when the variable is absent or cannot be parsed. A nested compositor with no supplied VT reports null. The VT is never inferred from a runtime path or display name.

Workspaces

Every output has a current workspace; windows belong to one. Verbs on the comp service:

VerbArgumentsReply
comp.workspaces.listnone{count, current, o_<key>: {current}, list: [{index, windows, name}]}, the same object as comp.props.get {path: "workspaces"}; name is null when unnamed
comp.workspace.switch{index, output?, wrap?}{output, from, to}
comp.window.send_to_workspace{id, generation, index, follow?}the window
comp.workspaces.set_count{count: 1..=16, persist?}{from, count, persisted?, persist_error?}
comp.workspace.rename{index, name, persist?}{index, name, previous, persisted?}

index is 1-based; switch and send also take "next" and "prev". Shrinking the count moves every window above it to the last workspace and drops the removed workspaces' names. A name is trimmed, at most 64 characters, with no control characters; "" clears it. persist defaults to true: the count and the names are written to preferences.json (workspaces_count, workspace_names) and compd starts with them. If the file cannot be written, the change still applies and the reply has persisted: false and persist_error. A session lock refuses every workspace change and hides the names (and window counts) from readers.

Topic comp.workspace.changed (<service>.workspace.changed) carries {version: 1, count, current, outputs: {o_<key>: current}, names, event_seq} whenever the count, a current workspace or a name changes. names has one entry per workspace, null when unnamed. Window counts are not in it; comp.props.changed on workspaces.list carries them. The two topics are sequenced separately, so a subscriber may see one before the other.

Test

cargo test --workspace
mix tests/desktop/nested_smoke.mix --help
mix tests/desktop/parity_suite.mix --help

This page as Markdown · This page describes the current main branch of MixOS.