---
title: compd
description: The MixOS Wayland compositor, which owns scanout, rendering, input and capture, and serves the comp Bus service with its workspaces, verbs, topics and props tree.
---

# 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](../settingsd/README.md) through the
[settings](../settings/README.md) 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

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

The source is
[`services/compd`](https://github.com/markc/mixos/tree/main/services/compd).
The desktop-tier gates that run compd nested or on hardware live in
[`tests/desktop/`](https://github.com/markc/mixos/tree/main/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:

```text
/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:

| Verb | Arguments | Reply |
|---|---|---|
| `comp.workspaces.list` | none | `{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
```
