cosmix-comp
The cosmix-input-probe hardware gate client redraws on frame callbacks and
uses opaque XRGB buffers by default. --translucent instead uses premultiplied
half-alpha ARGB buffers without an opaque region, including after resize or
maximise. Add --ssd to request server decorations in either mode, allowing the occlusion gate to
compare opaque and translucent maximised windows through the same client path.
cosmix-comp is the Wayland compositor used by the Cosmix desktop. It can run
nested inside an existing Wayland session with cosmix-comp --nested, or use
the KMS backend on a system seat.
Command-line options
--help prints the command usage. --version prints four lines of embedded
build provenance (the package line, full commit, enabled behaviour features,
and the cargo profile) and exits successfully before opening any device,
session, socket or Bus connection. It is recognised anywhere in the argument
vector, including after the kms-live subcommand, and wins when combined with
other options. --version --json prints the same provenance as one JSON object
(component, version, git_sha, git_sha_full, git_dirty, build_time,
features, profile).
KMS target-device changes (0.62.1)
A udev event for the authorised DRM device is no longer treated as proof that the
display was hotplugged. Switching to a text console makes the kernel modeset the
card for fbcon, which emits a udev change, so an ordinary VT round trip used to
manufacture a hotplug and cost the session every client it was hosting.
Only one case is now decided on the session thread: a Removed event while
kms-live is Active revokes authority, because the device being driven is gone
and that needs no query to establish. An Added event while Active is not
authority loss. Every Changed event, and every event of any kind while the
session is not Active — which includes the whole resume transition — is recorded
for diagnosis and left to verification.
That thread does no I/O at all, deliberately: it owns the libseat pause acknowledgement and answers session commands under a three-second deadline, and a synchronous DRM probe cannot be interrupted once it is inside the driver, so a wedged probe there would stall the pause acknowledgement and lose the session harder than the bug being fixed.
The identity invariant is unchanged and is enforced where the code already budgets for a synchronous driver call. The resume path re-verifies the stable device path, the device incarnation, the VT, DRM master state and connector presence before the session can return to the glass, and refuses terminally when the connector is absent — so a device that really went away cannot reach the glass by being deferred. While the session is Active no resume is pending, and there a connector that stops accepting frames is caught by presentation evidence rather than by a connector query.
Occlusion and frame callbacks (0.61.0)
Frame callbacks remain queued while a canonical surface family is provably covered by opaque content on every intersecting output. Popups and subsurfaces keep their family running whenever any member is exposed. This applies to layer-shell backgrounds, ordinary toplevels and XWayland surface trees. Minimised/off-workspace and session-lock gates retain their existing precedence; callbacks before the first buffer are never withheld by occlusion.
Withholding is a throttle, not a stop. A client on Mesa's default FIFO present mode blocks inside present until its frame callback completes, so a covered one could not even answer a configure. Once per second, every surface of an occluded tree that retains a callback has its oldest one completed. It is per surface because a callback releases only its own surface's present, so a root that requests again every second cannot starve a subsurface. This matches the ~1 Hz KWin and Mutter give hidden windows. The trickle is paced by the existing frame opportunities, with no timer of its own, and the window restarts whenever a root becomes occluded again. At most 64 callbacks are retained per surface of an occluded tree, so a root with one subsurface can hold 128. Excess older ones complete at once rather than being dropped.
Coverage uses applied opaque-region transactions and renderer-confirmed installed content. A pending DMA-BUF replacement cannot lend its opaque region to an older texture. Candidate bounds round outwards and occluders round inwards in output physical coordinates, including fractional scales. Each output's actual camera viewport and projection supply the origin and X/Y pixel ratios; missing or generation-mismatched camera evidence permits callbacks. Unknown state, excessive region fragmentation and stale scene revisions permit callbacks. Scene changes invalidate coverage; the next existing frame opportunity drains retained callbacks once with the current monotonic millisecond timestamp. Content-only commits preserve coverage decisions: an occluder's applied and sampled content sequences must each be greater than or equal to the sequence where its current coverage inputs last changed, rather than equal to its latest content sequence. That floor advances at applied opacity-region, format-opacity, buffer-size, generation, transform or viewport changes. A pending import may retain older content from the same coverage interval; content predating the floor cannot occlude. No occlusion polling timer or victim commit is required.
Maximised SSD frames have square opaque chrome bands without outer/inner edge antialiasing. Floating rounded chrome is unchanged. Fullscreen removes SSD; neither maximise nor fullscreen makes translucent client pixels opaque. Reserved work areas or exposed strips of wallpaper correctly prevent withholding.
Read comp.props.get paths surfaces.s<id>.occluded, occlusion_reason
(unknown, exposed, opaque-coverage) and occlusion_revision. These leaves
are mirrored under windows.s<id> where a window row exists. Layer and X11
surfaces remain observable under surfaces. occlusion.counters contains
compositor-wide withheld_opportunities, resumes, recomputes and
conservative_fallbacks; these read-only counters do not emit property changes.
Withheld opportunities count eligible root-tree pulse opportunities, not dropped
callbacks. resumes counts surface trees resumed, not individual callbacks:
one tree delivering several retained callbacks adds one. Resumes match delivered
callback identities against the retained set,
so destroying a child cannot turn an unrelated callback into a resume. They do not count transitions
to unknown visibility. While occluded, each surface retains at most 64 committed
callbacks: excess older callbacks receive done immediately (fail-open), without
discarding protocol objects. This cap does not override session-lock or workspace
gates. comp_occlusion_transition traces surface id, occluded flag and
revision; existing comp_callback_done_queued traces confirm callback delivery.
For single-output presentation reports, covered surfaces receive discarded
feedback only through the content sequence evidenced by that frame. Frame
callbacks remain queued independently. 0.61.0 limit: multi-output callback
coverage is supported, but multi-output presentation retains the existing shared
content-report semantics. Unproven (generation-zero) outputs still count towards
this conservative multi-output gate: missing evidence cannot establish that the
shared report covers a single output. The existing origin-zero canvas predicate in
frame_content remains a presentation-only limit; coverage uses each output's
origin independently; rotated camera projections fail open. Rounded client clips
and partial opaque regions with a crop, transform or non-1:1 buffer-to-destination
ratio (including destination-only viewports and buffer-scale changes) are
conservatively excluded as occluders. Unioned regions covering the entire surface
retain clamped outer edges without artificial internal seams.
Coverage bounds come from layout, not the entity's actual transform. Future
renderer-side offsets or animations must feed occlusion or disable its proof.
Coverage also relies on draw order matching SurfaceStackKey, and chrome bands
remaining tied to the decoration root's visibility; extraction checks that root
before crediting bands. These renderer conventions are not enforced by a shared
geometry/order type. Clients must honour their opaque-region declarations and
pace rendering on callbacks for CPU savings. Native Quoin's continuous-render
setting is unchanged.
KMS input dispatch fairness
Live KMS input routing yields after 16 libinput events or 2 ms, whichever comes first. This keeps a batch well below a 60 Hz frame interval while allowing cheap events to drain promptly. The time bound is cooperative: one callback and libinput's own dispatch cannot be preempted.
A small opt-in vendored Smithay patch retains remaining events in libinput
in their original order. On budget exhaustion, its calloop before_sleep
hook requests synthetic readiness and a nonblocking poll on the next turn.
Real and synthetic readiness share one batch per turn. Each turn returns
through normal frame-command service, client commit dispatch and socket
flush; no input is dropped and no new fd edge is needed to finish a burst.
Live acceptance: with the Boing wallpaper animating, sustain heavy pointer
motion and bucket the frame trace at 200 ms. comp_present,
comp_pulse_sent_busy and comp_callback_done_queued must continue advancing
through the burst, with client buffers and frame callbacks serviced each
vblank. Offline checks cannot establish real-KMS presentation cadence.
KMS hardware cursor
The kms-live build (with the default Bus, frame-capture and XWayland
features) uses an optional DRM cursor plane. native-quoin is not required.
Live KMS admission tries a compatible current CRTC first; other connectors'
CRTC_ID properties cannot evict it from that route. For an unrouted connector,
resume prefers its previous CRTC among free routes, then tries the remaining
free CRTCs in ascending object-ID order. Only after all free routes fail may
it reclaim a route claimed solely by disconnected connectors, again preferring
the previous CRTC and then ascending IDs. Such a selection logs the stale claim
and its former owners. Routes claimed by connected or unknown-status connectors
remain blocked, except for the current route. Rejected admission reports retain typed claim
diagnostics alongside other route and format/modifier failures.
The probe's rejection_matrix rows carry kind (route or format) and
crtc_id; route rows carry a reason, and format rows retain the capability
fields. Probe counters include format attempts on rejected routes.
Resume retries temporary route contention and transient DRM enumeration errors
through the existing three-attempt, 30-second recovery budget. Each failed
attempt returns to paused before retrying. Missing or disconnected connectors,
missing prior modes, unsupported atomic properties and incompatible formats
remain terminal. These retries cover target admission, before presentation;
they do not change atomic commit failure handling.
Connector probe failures use kms-live-connector-scan-failed; the
kms-live-atomic-admission-failed prefix is reserved for admission failures.
The live atomic presenter enumerates ARGB8888 cursor planes compatible with the selected CRTC, checks their atomic properties, and queries the driver's cursor width/height caps (64×64 for older drivers without these caps).
Cursor images use two transparent, pitch-aware ARGB dumb buffers. Image
changes pass an atomic test-only check before a synchronous cursor-plane
commit; hotspots, output scale, SHM source rectangles and transforms are
applied to the uploaded image and placement. For an admitted image, motion
retains only the latest desired position. The next primary atomic submission
appends the cursor plane properties, including its current framebuffer and
CRTC_X/CRTC_Y, to the same request. Both planes share one commit and one
primary pageflip completion. Submitted cursor coordinates advance only when
the ioctl succeeds; every busy retry picks up the latest desired position.
When the presentation pump proves the primary scene idle, it flushes pending
motion with a cursor-only NONBLOCK commit and no pageflip event. EBUSY keeps
the position pending for the next refresh-paced pump update, even if input
has stopped. Motion neither rotates cursor buffers nor advances the primary
scene revision. Image replacement, hide and teardown remain synchronised
with the cursor submission lock and the existing blocking image commits.
If a pending cursor commit makes the next primary commit busy, the presenter
retries at bounded 2 ms intervals within its original deadline; it does not
wait for a cursor pageflip event that was never requested.
Once hardware projection succeeds, the software cursor entity is hidden. The transition renders once to remove its old pixels. Missing planes, allocation/admission/commit errors restore the software path and log the reason. Oversized or GPU-only DMA-BUF cursor images use software projection; a later supported image can use the plane again. Cursor resources are retired with the output and re-enumerated after resume; revoked generations cannot submit cursor updates. Capture retains its separate cursor snapshot.
Offline tests cover cursor requests, pixel packing and scene fallback.
Driver acceptance and motion smoothness still require a live session check:
move the pointer over an animated wallpaper, change cursor shapes, drag a
window, toggle panels, and exercise display sleep/wake. Check the hardware
cursor log and frame trace; cursor motion alone should not create primary
scene revisions or comp_render work after the scene settles.
Optional F9 Bus action
--f9-bus <service> <verb> arms an unmodified F9 press to send a native ABP
request with an empty argument object to a local Bus service. For example,
--f9-bus bg-showcase boing.kick connects the compositor key to the Boing
scene's physical impulse. Both nested and live KMS input use this binding;
the background surface keeps keyboard interactivity disabled. In a nested
session the compositor's host window must have focus.
The binding is absent unless explicitly configured, honours
--no-keybindings, and does not run while the session is locked. A held key
fires once until released. Delivery runs on a bounded worker, so waiting for
the service cannot stall keyboard input. Failed requests are logged and never
automatically retried. --list-bindings --f9-bus <service> <verb> includes the
armed binding. The flag requires a build with Bus support.
For small nested scene previews, COSMIX_COMP_SERIAL_SCHEDULES=1 selects
single-threaded ECS schedule execution to reduce dispatch overhead. This is an
opt-in performance experiment; normal compositor scheduling is unchanged.
Wayland fullscreen
Comp 0.51.1 honours xdg_toplevel fullscreen and unfullscreen requests.
Fullscreen uses the primary logical output's complete rectangle, including
space otherwise reserved for panels. The optional client output hint is not
selected yet. State and geometry change together when the client commits its
acknowledged configure; sending or acknowledging a configure alone does not
move the visible window.
Compositor decorations disappear in fullscreen. While that window has keyboard focus, it is raised into the top band and native Quoin panels and hotspots are hidden. Switching focus restores its previous stacking band and Quoin, allowing other applications to remain usable. Leaving fullscreen restores the previous normal geometry, or the maximised layout if it was maximised first. Restored normal geometry is clamped to the current usable area after output changes.
Bus properties windows.<id>.fullscreen and surfaces.<id>.fullscreen expose
the committed state and emit normal property observation deltas. Application
commands such as media.fullscreen initiate the request through Wayland.
Window switching and X11 placement
Comp 0.51.0 includes an opt-in native-quoin feature. Together with
COSMIX_COMP_HUD_PROBE=1 and COSMIX_COMP_NATIVE_QUOIN=1, this embeds Quoin's
real panels alongside native Boing in the compositor renderer. Application
windows remain Wayland clients; their existing server-side decorations,
caption actions and interactive move/resize handling remain in comp.
Pinned native panels reserve usable space for maximised windows. See
Quoin's compositor host for scope and
remaining acceptance work.
Alt+Tab cycles forward and Alt+Shift+Tab cycles backward through mapped, non-minimised managed windows in stable creation order. This is not an MRU switcher or a visual switcher overlay. Both nested and KMS profiles support these bindings when interception is enabled. Session locking and exclusive keyboard layers retain priority.
Workspace chords (comp 0.59.0) live in both profiles beside
restore-recent-minimized and share one implementation with the
comp.workspace.switch / comp.window.send_to_workspace verbs:
workspace-jump-<n> (Super+<n>, n in 1..9) switches to workspace n;
workspace-move-<n> (Super+Shift+<n>) moves the focused window to n and
follows it; workspace-next / workspace-prev (Super+] / Super+[) step
by one and wrap at the ends. bindings.table lists them with xkb keysym
names: Super+1, Super+Shift+1, Super+bracketright. The matcher reads
the level-0 symbol, so Super+Shift+1 is still the digit, not exclam; on a
layout whose level 0 is not the digit the chord does not fire. A jump above
workspaces.count is a silent no-op (a key press has nobody to reply to; the
verb answers invalid_value). Under a session lock the chords reach the lock
surface, never the compositor. Under an exclusive layer the move chord is
withheld whole (no move, no switch) by the same gate as send_to_workspace {follow:true} — a chord that will activate the window must not re-arrange
the desktop under a layer that owns the screen — while the jump and step
chords, like comp.workspace.switch, still switch.
X11 _NET_ACTIVE_WINDOW requests use the same managed-window admission and
focus path. Local automation is accepted without timestamp-based focus-stealing
prevention; source identifiers are not authentication. Unmapped, minimised and
override-redirect windows cannot be activated through this path.
The root _NET_ACTIVE_WINDOW property follows the compositor seat's managed
X11 focus, including mouse and keyboard switching. Native, lock or absent
focus clears it to zero; delayed X focus events cannot replace it with a root
or ancestor window ID. This keeps xdotool windowactivate --sync and
getactivewindow consistent with compositor focus.
Workspaces reach X11 clients as EWMH virtual desktops (comp 0.59.0). The
root _NET_NUMBER_OF_DESKTOPS and _NET_CURRENT_DESKTOP are rewritten on
every workspace switch and count change (and once when the XWM starts, over
the 1/0 the XWM writes at startup so xprop -root never sees them
absent); EWMH desktops are 0-based, so workspaces.current 3 reads as
_NET_CURRENT_DESKTOP = 2. Every managed X11 window carries
_NET_WM_DESKTOP, written at the moment it maps (the same edge that stamps
its workspace — a window that has only sent its MapRequest has no
workspace and no property yet), rewritten by every move and by a count
shrink that strands it. A client's _NET_WM_DESKTOP message is honoured as
a move — the window goes to that desktop WITHOUT switching, exactly like a
windows.s<id>.workspace write, and gets _NET_WM_STATE_HIDDEN if that
takes it off screen; _NET_ACTIVE_WINDOW remains the request that brings a
window on screen. Ignored, with a debug log: 0xFFFFFFFF (all desktops —
comp has no sticky windows in 0.59.0), a desktop at or above the count, a
request while a session lock is active, and one for an override-redirect
window or a stale identity. A pager's _NET_CURRENT_DESKTOP root message
(wmctrl -s N, xdotool set_desktop N) is honoured as a switch of the
default output, exactly like comp.workspace.switch {index: N + 1}
without wrap; a desktop at or above the count and a request under a
session lock are ignored with a debug log. A pager's
_NET_NUMBER_OF_DESKTOPS root message (wmctrl -n N) is NOT honoured —
the count is workspaces.count, compositor-owned — and is dropped with a
debug log; the atom stays in _NET_SUPPORTED for the property, which is.
A KMS topology change that replaces the default output carries the
retiring output's current workspace to the replacing one (workspaces. current is keyed by the default output, and a replugged monitor must not
change the desktop) and republishes the root pair; if the effective
workspace changed anyway (the only output went away, or came back under a
key that still held an older value) the visibility and suspended state of
every window are re-derived in one settle, so no reader — frame callbacks,
presentation, windows.* — can see a workspace the scene does not.
xprop -root _NET_CURRENT_DESKTOP /
_NET_NUMBER_OF_DESKTOPS and xprop -id <xid> _NET_WM_DESKTOP via
xwayland.display are the live checks (the nested workspace gate's rule
10, which also drives both client messages through xdotool); the offline
suite pins the atoms, the callbacks and the values the compositor asks the
XWM to write, never the X property itself. Note that a property write can
only fail with a dead X connection (the X protocol reports per-request
errors asynchronously): a failed write is a dying Xwayland generation, not
a stale property on a live window.
Initial X11 placement, including size-only configure requests before mapping, respects reserved panel space. Reserved-area changes reflow managed X11 windows; maximised windows use the usable area and fullscreen windows use the full output. Restore geometry is kept until restoration, then clamped to the current usable area. Override-redirect menus retain client-owned coordinates and do not gain priority over top-layer panels or lock surfaces.
Intentional XWayland shutdown is marked before connection teardown. Expected connection closure and exit code 1 are distinguished from runtime crashes; protocol errors, signal deaths and timeout escalation remain errors. The child reaper allows a two-second exit grace before kill/reap. It remains asynchronous: compositor exit does not wait for a positive child-reaped acknowledgement.
Bus control plane
The default bus feature gives the compositor an L2 Bus control plane.
The seat/KMS compositor registers as comp; --nested registers as
comp-nested. --bus-service NAME overrides either name and accepts
^[a-z][a-z0-9-]{1,30}$. A build without the bus feature rejects that flag
instead of silently ignoring it. The broker independently enforces the same
SPEC 10 service-name grammar at registration and rejects an invalid from
with Bus rc 10.
The control plane exposes these verbs:
comp.pingreturns{"pong":true}without taking a compositor snapshot.comp.inforeturns service/build/backend provenance plus output and surface counts and the property event counters.comp.props.get path?returns one leaf or subtree, or the complete tree whenpathis omitted.comp.props.list prefix?returns leaf paths. A prefix is matched by complete path segments, never by string prefix.comp.props.describe pathreturns leaf metadata (type,mutable,sensitive, description, optionalformat/enum/range/persistence, and owner) or an object subtree with its immediate children.comp.props.watchseeds the property-change baseline and returns{topic:"<service>.props.changed",event_seq,lost_count}, whereserviceis the name this compositor instance actually registered. The reply is truthful only for a caller that subscribed to that topic before callingwatchand remains subscribed.comp.props.set {path,value,generation?}mutates the six corner properties,windows.s<id>.band,windows.s<id>.minimized,windows.s<id>.maximized,windows.s<id>.fullscreen,windows.s<id>.workspace,workspaces.count,workspaces.current,workspaces.o_<slug>.current,input.host.passthrough, orxwayland.enabledand returns{path,old,new}; for the file-persistedxwayland.enabledthe reply also carriespersisted—falsemeans the in-memory change and the changed event stand but the write to disk failed and the value will not survive restart. The optionalgenerationfences awindows.s<id>.*write (see Window identity below); it is refused on any other path.comp.pointer.watchrenews a three-second pointer observation lease and returns{version:1,topic:"<service>.pointer.changed",lease_ms:3000}. Subscribe before calling; renew about once per second while observation is wanted. The acknowledgement contains no pointer coordinates.comp.region.select {output?,timeout_ms?}selects a rectangle using native compositor furniture. See Region selection below.comp.window.minimize {id,generation}minimises one window, like its title-bar button. Both fields are required.comp.window.maximize {id,generation},comp.window.unmaximize {id,generation},comp.window.fullscreen {id,generation,output?}andcomp.window.unfullscreen {id,generation}request window state (below).comp.window.restore {id?,generation?}with no arguments restores the most recently minimised window, exactly like theSuper+Shift+Mbinding; with{id,generation}(both required together) it restores that window. If the window is minimised, either form un-minimises it, switches to its workspace if that is not the current one, raises and focuses it (where the switch is not allowed — an exclusive layer, the VT switched away — it un-minimises without switching or focusing); if it is not minimised, nothing happens and the reply sayschanged:false.comp.window.stats {id,generation | source}andcomp.window.stats.reset {id,generation | source | nothing}read and zero presentation statistics (see Presentation statistics below).comp.window.focus,comp.window.raise,comp.window.close,comp.window.placeandcomp.window.waitact on or wait for one window (see Window control below).comp.windows.listlists window rows.comp.workspace.switch {index,output?,wrap?}changes the output's current workspace andcomp.window.send_to_workspace {id,generation,index,follow?}moves one window to a workspace (see Window control below).comp.input.pointer.move,comp.input.pointer.button,comp.input.pointer.scroll,comp.input.key,comp.input.release_allandcomp.input.sequenceinject input through the real seat (see Input injection below).
Region selection
comp.region.select holds one pending reply while the seat selects a region.
Left-button drag selects; reverse drags are normalised and rounded outwards to
integer logical units. A click or zero-area drag keeps selection armed. Esc or
right-button press cancels. Unknown fields are rejected. timeout_ms defaults
to 30000 and accepts 1–55000: three further seconds bound clean-frame removal,
with a four-second responder margin, strictly below the 60-second long-verb cap.
Set the caller's Bus timeout above that total budget.
An explicit output restricts selection to that named output. Otherwise the
first left press chooses its output. Dragging across its edge clips the rectangle;
this does not stitch multiple outputs. Success (rc 0) is:
{"version":1,"status":"selected","output":"Output-1","output_generation":42,"coordinate_space":"output-local-logical","region":{"x":100,"y":80,"width":640,"height":360}}
Coordinates are relative to the displayed output's top-left, before conversion
to physical pixels. Cancellation returns
{"version":1,"status":"cancelled","reason":"escape"} (or right_button);
timeout returns {"version":1,"status":"timeout"}. These are rc 0 outcomes.
Successful completion waits for a submitted frame without the overlay on the
selected output; an unrelated sleeping monitor does not delay that reply.
If the selected output disappears during cleanup, all surviving overlay outputs
must submit clean frames; no surviving output means success cannot be proven.
Cancellation, timeout and refusal restore focus and publish overlay removal, then
reply without waiting for presentation: they do not authorise a capture.
Failure to prove removal within the margin returns rc 10 busy, never success.
A second selector, an existing pointer/popup grab, touch sequence, native panel
drag, input sequence or window manipulation returns rc 10 busy. Output identity,
generation, geometry, scale or transform changing before a result is decided
returns output_changed. A decided result is preserved during cleanup.
A requested output name that does not exist returns unknown_output.
Session lock returns locked. These are lifecycle/correctness rules, not caller
permissions. Temporary seat focus does not deactivate or restack fullscreen windows.
Pointer constraints are released for selection and reconsidered on focus restoration.
VT/focus/device loss and a closed local responder clean up input ownership.
Touchscreen removal aborts even a pointer-driven selection with busy; KMS
session pause reports output_changed before generic input-loss cleanup.
Remote caller disappearance is bounded by the deadline; immediate remote request
cancellation is not currently propagated to the local responder.
The result contains geometry only. Pass output and region to
capture.screenshot; an agent that already knows its rectangle can call capture
directly. output_generation describes selection-time identity; capture's current
Wayland request does not carry that generation, so this is not an atomic
selection-to-capture topology fence.
Compositor log colours follow the stderr log sink's terminal status. Redirecting stdout alone preserves colour; stderr pipes, files and journald receive plain text in both KMS and nested modes.
Minimise and restore
Minimise and restore reply {id,generation,title,app_id,minimized,changed};
changed:false means the window was already in the requested state.
comp.window.restore {} with nothing to restore replies rc 10
{"error":"not_found","minimized_count":N}. While a session lock is active
both verbs, and writes to windows.s<id>.minimized, reply
{"error":"locked"}. An argument other than id or generation is refused
with {"error":"invalid_args","field":"<name>","allowed":["id","generation"]},
so a typo such as gen cannot silently turn a fenced call into an unfenced
one.
No verb checks who the caller is. Any caller that noded delivers, local or from the WireGuard mesh, can read, write and watch; being on the mesh is the whole authorization. What the port still refuses is a malformed or mis-aimed request (unknown path, wrong type, out-of-range value, a window that is not there).
The complete L2 read tree is:
info.{service,version,backend,engine,instance}
outputs.o_<slug>.{name,default,x,y,width,height,scale,refresh_mhz,
usable.{x,y,width,height},
presentation.{clock_id,flags,flags_mask,refresh_us,frames,
interval_p50_us,interval_p99_us,since_us}} (presentation: volatile)
surfaces.s<id>.{id,role,mapped,visible,x,y,width,height,band,sequence,
tree_index,parent,output,title,app_id,focused,activated,
maximized,fullscreen,minimized,workspace,decoration,
layer.{stratum,interactivity,exclusive_zone,binding},foreign_id,
generation}
windows.s<id>.{id,foreign_id,title,app_id,x,y,width,height,focused,
maximized,fullscreen,minimized,output,band,generation,
window_x,window_y,window_width,window_height,visible,pid,workspace,
presentation.{presented,discarded,last_presented_us,
interval_p50_us,interval_p99_us,interval_max_us,
commit_to_present_p50_us,commit_to_present_p99_us,
input_to_present_p50_us,input_to_present_p99_us,
missed,refresh_us,since_us}} (presentation: volatile)
sources.<id>.{output,registered_at_us,revision,registration,
presentation.{<the window leaves>,upload_bytes_total,
damage_px_total,upload_bytes_p50,upload_bytes_p99,
damage_px_p50,damage_px_p99}} (volatile)
workspaces.{count,current,o_<slug>.current,list}
stack
focus.{keyboard,exclusive_latch,pointer,pointer_grab,session_lock,
window.{id,generation}}
decoration.{enabled,style}
bindings.{enabled,profile,table}
input.corners.{holders,enabled,deadzone_px,dwell_ms,velocity_max_px_s,affordance,discovery,
enforced.{top,bottom,left,right},
held.{top,bottom,left,right}} (enforced, held: volatile)
input.host.passthrough (nested backend only)
input.seats.{human,agent}.{name,keyboard_focus:{id,generation}|null,
pointer_focus:{id,generation}|null,
pointer:{output,x,y}|null,last_input_us} (volatile)
input.last_origin (human|agent|null; volatile)
xwayland.{enabled,persist_path,display}
port.{level,event_seq,lost_count,queue_depth,reply_timeouts,publish_timeouts,
slug_collisions,broker}
Surface keys are s plus the decimal session-local surface ID. Output keys are
o_ plus the lower-case output name with each non-alphanumeric character
replaced by _; the raw output name remains in name. If output names collide
after slugging, the first output wins, each omitted output increments
port.slug_collisions, and the compositor logs each collision at debug level.
band includes background, bottom, normal, top, overlay and lock.
stack contains mapped roots from top to bottom. windows is a projection of
mapped XDG toplevels. port.level is L2. port.event_seq is the live global
sequence watermark across every topic, and port.lost_count is cumulative.
port.broker is driven by connection-state edges and is connected or
retrying. port.reply_timeouts and port.publish_timeouts count their
separate bounded lanes; both abandon a sink wait after two seconds.
Window rows add seven read-only leaves. generation is the window's role
generation (below). window_x/window_y are the window-geometry origin and
window_width/window_height its extent, all in logical pixels. The origin
stands its buffer on a whole physical pixel, so window_x/window_y can be
fractional at a fractional scale and are integers at scale 1 (see
Whole-pixel placement). Use all four
for window screenshots that exclude client-side shadow margins. x/y remain
the buffer origin and width/height the buffer extent, including those margins.
Without explicit client geometry, all four window_* fields use the effective
committed surface-tree bounds, including mapped subsurfaces; shadow margins
cannot then be distinguished. If no geometry is cached, the fallback is the
root buffer with zero geometry offset and its full extent. visible is
effective on-screen visibility: use it to ask "is this on screen", and
minimized for the user's minimise state. pid is the process id of the
client's socket peer, or null when the compositor cannot read it. A client
reached through a proxy (waypipe, a sandbox, a PID namespace) reports the
proxy's or namespace's view, which may not be the application's own pid.
focus.window.{id,generation} names the managed window (xdg or X11) that
holds keyboard focus, both null when none does.
Window identity. A wl_surface keeps its s<id> when a client gives it a
new role, so an id alone can name a different window than the one a script
read. Every role assignment (including the role ending) takes a new,
never-reused generation. Unmapping and remapping the same role (a null
buffer, then a new one) keeps it; an X11 window that is associated again
counts as a new role. A client may also destroy its xdg_toplevel and
xdg_surface and later wrap the same wl_surface in a fresh xdg_surface
(Qt's hide→show does this); comp accepts it, and the new toplevel is a new
role. A second xdg_surface is refused with xdg_wm_base.role only while an
earlier one for that wl_surface is still alive, or when the surface carries
a non-xdg role.
Every surface row publishes it as surfaces.s<id>.generation, and window rows
repeat it. X11 windows have no windows.* row yet, so read their generation
from surfaces.s<id>. Treat {id, generation} as the window's identity:
the window verbs require both, and comp.props.set accepts generation on
any windows.s<id>.* path. A mismatch replies rc 10
{"error":"stale_target","id","generation","current"} and changes nothing.
The window verbs also reply unknown_window, not_managed or not_mapped
(each with id) when the target is not a mapped managed window. Moving or
resizing a window never changes its generation.
windows.s<id>.minimized accepts true or false. true minimises the
window; false restores that window (not the most recently minimised one),
takes it out of the restore order, and raises and focuses it. X11 windows are
accepted too and get the EWMH hidden state cleared. A write to a window that
does not exist or is not a mapped managed window replies invalid_value, like
the band leaf.
Workspaces (virtual desktops) are 1-based. workspaces.count (default 4,
1..=16) is the number of workspaces; shrinking it moves every window on a
removed workspace to the last remaining one and clamps every current.
workspaces.current is the default output's current workspace and
workspaces.o_<slug>.current the same value under the output's key (one
output today, so they mirror each other; a key that is not the default
output's — even one that exists under outputs.* — is refused with
invalid_value whose range says only the default output switches).
Writing either switches, exactly like comp.workspace.switch.
workspaces.list is read-only: one {index,windows} row per workspace,
windows counting the mapped managed toplevels on it — X11 windows
included, although they have no windows.* row, so a pager never shows a
workspace empty while an X11 window is on it. windows.s<id>.workspace is
the window's workspace; writing it moves the window there WITHOUT
switching, so a window moved off the current workspace reads
visible:false, minimized:false (use visible for on-screen, minimized
for the user's minimise state). A move never changes the window's
generation. surfaces.s<id>.workspace carries the same value for every
mapped managed toplevel, X11 windows included (they have no windows.*
row), and null for every other surface and for an unmapped one. A window
that unmaps and remaps joins the current workspace again. An
override-redirect X11 window (a menu, tooltip, dropdown, DND icon) is not a
window — no row, no workspace value, never movable — but it hides with the
workspace it mapped on, so an open menu does not outlive the switch that
hid its owner; it is back, still open, when that workspace is. Moving the
OWNER (0.59.1: Super+Shift+N, send_to_workspace, a
windows.s<id>.workspace write) carries its override-redirect children
along, resolved through WM_TRANSIENT_FOR — the menu is relabelled to the
same workspace as its owner, so it stays visible alongside it instead of
being left open and invisible on the workspace it mapped on; it still gets
no _NET_WM_DESKTOP of its own (that property is for managed windows). X11 clients see
the same model through EWMH: the root _NET_NUMBER_OF_DESKTOPS and
_NET_CURRENT_DESKTOP (0-based, so workspace 1 is desktop 0) follow every
switch and count change, every managed X11 window carries _NET_WM_DESKTOP
from the moment it maps, a client's own _NET_WM_DESKTOP message moves
its window exactly like a windows.s<id>.workspace write, and a pager's
_NET_CURRENT_DESKTOP root message switches exactly like
comp.workspace.switch (see Window switching and X11 placement). A refused or
no-op write publishes nothing and attributes nothing: the next unrelated
change keeps its own cause. All of these are watchable; the
changed events of a switch, move or count change carry the cause of the
write (props.set) or the verb. Values outside 1..=count (0 included) and
non-integers are invalid_value; every write is locked while a session
lock is active.
Window band writes accept bottom or normal. They move the complete window
tree, including popups, behind normal windows or back into their normal band.
Assignments last for the current session. Use the canonical ID returned by
comp.props.get; aliases with leading zeroes are rejected. Other bands remain
reserved for layer-shell and session-lock roles. Corner activation takes
priority over client pointer constraints; reactivation waits for physical
pointer motion out of the corner.
Window control
comp.window.maximize {id,generation} and
comp.window.unmaximize {id,generation} use the compositor's native maximize
configure path, including XWayland. comp.window.fullscreen {id,generation,output?}
and comp.window.unfullscreen {id,generation} use its fullscreen path.
output accepts an output key or name; an unknown name is unknown_output.
Without it, fullscreen retains an existing explicit selection or uses the default
output. The selection survives later configures and output reflows. A change to
the selected output reconfigures the window even when the default output is
unchanged; removing the selected output clears the selection and reconfigures
fullscreen on the default output. Leaving fullscreen clears the selection.
Fullscreen geometry takes precedence over maximize in both protocol families;
changing maximize underneath fullscreen preserves the original restore rectangle.
Fixed size constraints on either axis refuse entry with
rc 10 unsupported_state, reason:"fixed_size". Target failures use the existing
unknown_window, not_managed, not_mapped and stale_target replies.
Success is rc 0 {id,generation,title,app_id,minimized,changed,maximized,fullscreen,configure_pending}.
changed reports a change in requested state or fullscreen output selection.
maximized and fullscreen report committed state. Wayland clients must ack
and commit the configure before those fields change; configure_pending reports
that outstanding configure. Use comp.window.wait with until:maximized,
unmaximized, fullscreen or unfullscreen to wait for the committed state
with no outstanding window-state configure, including opposite requests issued
before an earlier configure was acknowledged.
The same transitions publish props.changed through the normal window-row diff.
windows.s<id>.maximized and windows.s<id>.fullscreen are writable booleans
through comp.props.set {path,value,generation?}, fenced like .minimized.
Their {path,old,new} reply describes requested state; reads and changed events
describe committed state. These writes use the same refusal and configure paths
as the verbs. All four verbs and both properties are available to mesh callers.
Every verb here names its window with {id, generation}, refuses a stale or
missing target as described under Window identity, and replies
{"error":"locked"} while a session lock is active. Each is recorded in the
frame trace as comp_window_control (subject the id; detail 1 minimize,
2 restore, 3 focus, 4 raise, 5 close, 6 place, 7 wait, 8 forced close,
9 workspace switch, 10 send to workspace, 11 window state; comp.window.stats and
.stats.reset reuse 7 and 8, a collision kept until 0.60 renumbers them).
Every mapped window is on one workspace and each output has a current one;
a window off its output's current workspace reads visible:false, minimized:false, gets no frame callbacks and is never presented. Every
path that brings a window into view — comp.window.focus,
comp.window.restore, a client's xdg-activation, an X11
_NET_ACTIVE_WINDOW or un-minimise — switches to the window's workspace
first and never pulls the window across, in one settle with the keyboard
landing on that window and on no bystander in between (the same
preference send_to_workspace {follow:true} gives a followed window: the
highest window already on the arriving workspace never sees a
wl_keyboard.enter the activation then reverses); where the switch is not allowed
(a session lock, an exclusive layer, the VT switched away) the window is
not focused either, so the keyboard never lands on a window that is off
screen: focus replies with the reason, an activation request is ignored,
and a restore un-minimises without switching or focusing. A client's
xdg-activation of a MINIMISED window RESTORES it (0.59.1, KWin/GNOME
parity): exactly comp.window.restore for that window — un-minimise,
switch to its workspace where that is allowed, raise, focus, one settle —
and it leaves the minimized_toplevels MRU list truthful, the same as any
other restore. _NET_ACTIVE_WINDOW keeps the 0.59.0 refusal: a MINIMISED
window is not a valid X11 activation target
(window_switch_candidate), un-minimising is the separate request that
restores it. Two verbs drive the workspaces:
-
comp.workspace.switch {index,output?,wrap?}makesindexthe output's current workspace: a 1-based number, or"next"/"prev"relative to the current one, which wrap at the ends unlesswrap:false, when they are refused with{"error":"at_end",output,from,count}(outputthere is theoutputskey, as in the success reply, whichever spelling the request used).outputis anoutputskey or output name and defaults to the default output (in 0.59.0 the only output with a switchable workspace; any other isinvalid_value). A number outside1..=count(0 included) isinvalid_valuenamingindexwith the range. The reply is{output,from,to}; a switch to the current workspace replies withfrom == toand does nothing. Minimise state is untouched: a minimised window on the arriving workspace stays minimised. The verb names no window but changes what is on screen, so a session lock refuses it (locked). -
comp.window.send_to_workspace {id,generation,index,follow?}moves the window toindex(a number, or"next"/"prev"relative to the window's own workspace, always wrapping) without switching; the window becomesvisible:false, minimized:falseif it leaves the current workspace. Withfollow:truecomp also switches to that workspace and activates the window — move and switch settle once, so no other window on either workspace takes the keyboard in between, whatever stacking band it is in — unless the switch is withheld by the same gate every bring-into-view path has: an exclusive layer owns the screen, the window is minimised, or it is not presentable while the VT is switched away. Then the move alone runs and nothing is activated. The reply gainsfollowed, which is simplyworkspaces.current == indexREAD BACK after the attempt, not a claim that a switch or an activation happened: it isfalsewhen the gate held and the window's new workspace is not the current one, and it istruewhenever the new workspace is the current one — including with no switch and no activation, when the window was already there and the gate held (a minimised window sent to the workspace it is on answersfollowed:trueand stays minimised). With no default output there is nothing to switch andcurrentreads 1, so a send to workspace 1 answersfollowed:true. The move has happened either way. The reply is{id,generation,index}(plusfollowedwithfollow:true). A move never changes the window'sgeneration. Out-of-range indices areinvalid_valueand change nothing; the usual{id,generation}fence applies. -
comp.window.focus {id,generation,raise?}gives the window keyboard focus. Withraise(the default) it also raises it and re-targets the pointer, exactly like Alt+Tab. The reply is{id,generation,focused}. Whenfocusedis false,reasonsays why, the first that holds in this order:exclusive_layer(an exclusive layer surface holds the keyboard),minimized,not_presentable(the VT is switched away),not_visible, orrefused. The first three are what keep an off-workspace window's switch from running, so such a window names the gate that held it, not the visibility the switch would have given it; a refusal changes nothing and attributes nothing to the window. -
comp.window.raise {id,generation}raises the window within its band without focusing it. The reply is{id,generation,raised}. Raise is stacking only: it never switches workspace and never un-minimises. An off-workspace or minimised window is restacked in place and stays where it is;raisedreports the stacking change alone.comp.window.focusandcomp.window.restoreare the verbs that bring a window into view. -
comp.window.close {id,generation}asks the client to close (xdgclose, or X11WM_DELETE_WINDOW) and replies{closed:"polite"}at once.- With
force:true(and optionaltimeout_ms, default 3000, at most 60000) the reply waits. The deadline runs from when the port admitted the request. - If the window's role ends first (the client destroyed it, or the id now
names another role), the reply is
{closed:"gone",waited_ms}. - A window that is only unmapped is still alive: an app that hides to a
tray on close is not gone. If the same
{id,generation}is still alive at the deadline, mapped or not, comp disconnects its client and replies{closed:"killed",window:"mapped"|"unmapped",scope:"client",pid,windows,waited_ms}. - The kill ends the whole client connection, so every window in
windowsgoes with it. No kill happens if the caller has stopped waiting, or while a session lock is active (the reply is thenlocked). - An X11 window is never killed this way: its Wayland client is XWayland,
and the window manager has no per-client kill. The polite close is sent
and the verb replies at once with
{"error":"still_open","reason":"x11_kill_unsupported",polite_close_sent:true}.
- With
-
comp.window.place {id,generation,output?,x?,y?,width?,height?}moves and/or resizes the window, with at least one field given.x/yare output-local logical coordinates of the window-geometry origin (window_x/window_y).outputis anoutputskey or output name, and defaults to the window's own output. An absent coordinate keeps the window's offset within its output.width/heightrequest a window-geometry size, clamped to the client's minimum and maximum. An absent one keeps the window's current window-geometry size (what the client committed, not what comp last asked for), so a width-only or height-only place re-sends the current size for the other axis in the configure. For an xdg window this is a configure, so the size changes when the client answers: wait withcomp.window.wait {until:"size"}.- The reply is
{id,generation,output,window_x,window_y,requested:{width,height}|null,configure_pending}. - The window lands on whole physical pixels, so
window_x/window_ycan differ from the request by up to half a physical pixel and need not be integers: at scale 2.5,x: 1answerswindow_x: 1.2. See Whole-pixel placement. To put a neighbour flush against this window, place it at the REPLIED origin plus the window's logical size, not the requested one. - A maximised or fullscreen window is refused with
{"error":"invalid_state",maximized,fullscreen}. An unknown output isunknown_output. A place that would leave the window wholly outside every output is refused with{"error":"off_output",x,y,width,height}.
-
comp.window.wait {match,until,width?,height?,timeout_ms?}replies when a window reaches a state.matchis either{id,generation?}or{app_id?,title?,title_contains?}with at least one field. Name filters are at most 4096 bytes. Withid, the wait is about that window; an id comp never handed out is refused withunknown_window. Without it, the wait is about the lowest-id mapped window whose names match.untilismapped,visible,presented(the renderer showed the window in a frame at or after the current mapping began, judged from the renderer's frame reports thatcomp.window.statsalso reads, so nowp_presentationfeedback is needed. Static content that was hidden at map time counts once exposed. Feedback presented in that span also counts. A late report of an earlier frame does not. The window must also be unminimised and on the current workspace when the wait resolves),size(needswidthandheight, compared with the window-geometry size),focused,maximized,unmaximized,fullscreen,unfullscreen,unmappedorgone. State waits require a mapped window and test committed state. For a match withoutid,unmappedandgonemean no mapped window matches.mappedis workspace-blind;visibleandpresentedneed the window's workspace to be the current one, so a wait on a window that is off its workspace times out rather than resolving, and resolves once a switch (orsend_to_workspace {follow:true}) brings it on screen.timeout_msdefaults to 10000 and is at most 60000, counted from when the port admitted the request.- While a session lock is active a wait learns nothing it could not read
from the (redacted) tree: only
goneandunmappedfor a namedidcan resolve; everything else waits for the unlock or the deadline, so a name-based wait that spans the whole lock ends withtimeout. - A condition that already holds is answered at once. Otherwise comp checks after each dispatch cycle and sets one timer for the deadline; nothing polls.
- The reply is
{window:<row>|null,until,waited_ms}.windowis thewindows.s<id>row, or null forunmappedandgone. On the deadline the reply is{"error":"timeout",until,waited_ms}. - Waits and forced closes use the same eight-permit pool as
comp.input.sequence.
-
comp.windows.list {app_id?,title?,title_contains?,visible?,workspace?}returns{windows:[<row>...]}in id order, filtered by every given field (text filters at most 4096 bytes;workspaceis an index in1..=count— above the count isinvalid_value, not an empty list —"current"for the current workspace, or"all", the default). The rows are thewindows.s<id>rows. Like that tree, the list has no X11 windows and is empty while a session lock is active.
Every argument object is checked for unknown fields
({"error":"invalid_args",field,allowed}).
Whole-pixel placement
At a fractional output scale a logical origin can fall between physical
pixels: x = 1 at 2.5 is physical 2.5. The KMS renderer projects each window
edge to a whole pixel on its own, so a 795-wide window there spans 1987
pixels while its fractional-scale client drew round(795 x 2.5) = 1988, and
the content is resampled and blurs. comp therefore stands every xdg
toplevel's BUFFER origin on a whole physical pixel. The snap moves the buffer
by at most half a physical pixel. It applies in these places:
- the cascade slot a new window opens in;
comp.window.place;- interactive move and resize;
- the clamp after the output shrinks;
- every commit that changes the client's window-geometry inset, whichever buffer path it arrives on (none, SHM or DMA-BUF), and the inset comp derives from subsurface bounds;
- a return from maximised or fullscreen;
- a nested host scale change.
The reported window_x/window_y and the windows.s<id> position leaves are
the snapped values. They can be fractional in logical units at a fractional
scale, and they are integers at scale 1, where the same rule rounds a
fractional pointer-driven origin to a whole pixel.
Re-snaps start from where comp meant the window to be. comp remembers the unsnapped origin it last placed a window at. A later inset change or scale change derives the new whole-pixel origin from that anchor, never from an already snapped value. So a GTK window whose shadow inset changes on every focus change stays put, and a scale change from 2.5 to 1.25 and back returns the window to the same pixel.
Which requests set the anchor:
- A move,
comp.window.place, an interactive resize and the output-shrink clamp each REPLACE the anchor, per axis, with the origin they asked for. - An axis such a request leaves where the window already stands keeps its recorded anchor. Examples are a size-only place, a right or bottom edge drag, and the unmoved axis of a left drag. So interleaving resizes with inset changes cannot walk the window either.
- Anything else that moves the window, such as a return from maximised or a decoration-mode switch, makes its new origin the anchor.
Constraints choose between the two nearest grid points.
comp.window.placevalidates the snapped origin, not the requested one. When the nearest grid point would put the window wholly off every output, it takes the grid point on the other side.- The output-shrink clamp takes the nearest grid point that is still inside the clamp. The window does not slide under a panel or lose the room it was clamped to fit. When the window is wider than the work area, the clamp interval is narrower than a pixel. Then the origin takes the nearest grid point at or past the work area's near edge, and the size clamp that follows shrinks the window. That grid point becomes the anchor, so the next commit keeps it.
- A left or top edge drag whose stationary edge sits exactly on the output's left or top edge may have no grid point that keeps it on its pixel. It then moves one pixel toward the visible side, never behind the output edge.
- A left or top edge drag keeps the STATIONARY edge on the physical pixel it started on. The moving origin takes whichever grid point preserves it, so the far edge does not wobble as the drag crosses odd sizes.
What 1:1 means, and where. On the grid, both edges of the buffer project
exactly. On KMS the drawn width is then the client's round(width x scale)
buffer, sampled 1:1. The nested compositor renders through the host camera's
scale without this edge projection, so it still resamples a window whose
logical width is odd at 2.5. Nested pixel gates cannot prove sharpness at a
fractional scale. A neighbour placed at the REPLIED origin plus the window's
logical size projects its left edge to this window's right edge, with no seam
and no overlap. Placing it at the REQUESTED origin plus the width can overlap
by one pixel. With a client-side-decorated window only the buffer is on the
grid; its visible window-geometry edges sit at the client's integer logical
inset from it and can fall between pixels.
Three kinds of surface are left alone:
- X11 windows keep integer X coordinates.
- Maximised and fullscreen windows start exactly at the work area or output origin.
- Popups and subsurfaces keep their client-chosen offsets from the parent.
A window whose right or bottom edge falls on a negative half pixel, straddling the left or top output edge, can still project one pixel short. The renderer rounds half away from zero on both sides of the origin.
Input injection
0.72.0 (unreleased): Bus input and sequences now default to the agent seat;
callers needing human focus, bindings, chrome or idle activity must pass
seat:"human". Bare release_all {} still clears both seats' injected holds.
cosmix-input-probe --seats binds each advertised
seat's keyboard and pointer and labels their events by the quoted seat name.
--idle-timeout-ms N enables the same mode and adds one ext-idle-notify-v1
notification per seat: PROBE seat "cosmix" idled / resumed (likewise for
cosmix-agent). PROBE seats_ready follows a roundtrip confirming setup;
PROBE seats_done follows the final event-draining roundtrip. With neither
flag the probe retains its existing bindings and output. The flags do not
change the compositor's input default.
The former 0.71.0/0.71.1 development versions were never installed; their seat
delivery and probe changes ship together in 0.72.0. cosmix-agent has its own
keyboard, XKB modifiers, pointer, focus and injected holds. Its global remains
hidden from Xwayland. Every comp.input.* verb accepts seat:"human" or
seat:"agent"; there are no caller authorisation gates on either choice.
Since 0.72.4, native Wayland drag icons are composited above windows and
desktop layers, below the pointer. The source application supplies the artwork
(for example, Thunderbird's floating message icon and label); comp moves its
surface tree with the human pointer, honours logical surface offsets and buffer
scale/viewport sizing, and passes input through to the drop target. Drop or
cancellation removes the artwork immediately, including source/icon destruction
and seat teardown. Session locking hides ordinary drag artwork with the rest of
the desktop scene.
Since 0.72.3, Firefox and Thunderbird also see only the human seat:
Mozilla's GTK legacy pointer lookup and its own Wayland seat selection disagree
when both seats are advertised. This avoids selecting the idle agent pointer
for ordinary mouse interaction and drag requests. Compatibility is fixed per
connection from the peer's executable basename (firefox, firefox-bin,
firefox-esr, thunderbird, or thunderbird-bin), including executables retained
after a package update. App IDs and window titles do not select this policy.
Unknown or inaccessible executables retain both seats. These Mozilla clients
remain available to Bus input with explicit seat:"human"; agent requests return
agent_seat_unbound with the human-seat hint, rather than changing input seats
silently. Reconnecting the application is required after a compositor update.
Clipboard and primary selections now relay between the two seats with source
provenance. Consecutive compatible agent motions coalesce, and agent delivery
runs in bounded batches after ready human input. Full-click menu switching
preserves the next menu's press serial; unmapping a menu's root retires its grabs.
The default is agent (cosmix-agent). Select seat:"human" explicitly for
human input semantics (including X11 targets). The deliberate exception
is release_all: without a seat it releases both seats' injected holds. A sequence's
explicit seat is inherited by its steps; args.seat overrides it for a step.
Without a sequence-level seat, each verb uses its own default.
| Behaviour | seat:"human" | seat:"agent" |
|---|---|---|
| Keyboard | Human keymap/modifiers and compositor bindings | Independent keymap/modifiers; every key forwards, no compositor chords |
| Targeted focus | Existing window focus arbitration | Agent keyboard only; no Activated, restack, restore or workspace switch |
Omitted raise | true for a targeted key/button | false; explicit true returns invalid_argument |
| Idle | Human activity resumes idle notifications on both seats | Neither seat's idle notification is reset; no user-activity observation |
| Session lock | Untargeted input retains the lock-screen path | All verbs refuse with session_lock |
| Pointer | Device path, including chrome, corners and constraints | Client surfaces only; independent position; human cursor image/position unchanged |
| Clipboard and primary | Shared selection, including the XWM bridge | Shared selection, with the source seat and generation tracked explicitly |
Bus injection now records agent origin unless seat:"human" is selected.
Successful agent delivery updates input.seats.agent.last_input_us and
input.last_origin:"agent". Human delivery updates the human leaves and reports
last_origin:"human". The existing focus/pointer leaves retain their human
meaning. Human Bus injection still follows inject → handle_host_input → notify_idle_activity(Human); agent delivery uses the agent Smithay handles.
Clipboard and primary are separate logical selections, each offered on both seats. A client setting either selection replaces the other seat's mirror; clearing it or destroying/disconnecting its current source clears both offers. Destroying a replaced source leaves the newer selection intact. Data-control history clients on either seat see each replacement once. Mirrors use compositor selection setters, which do not invoke the source-change callback, so they cannot echo ownership back and forth.
Each mirror records a source seat (or X11) and a selection generation. Reading
a replaced mirror closes the transfer fd without serving newer clipboard data.
For a current Wayland source, the relay passes the recipient's fd to that
source; clipboard bytes do not pass through comp. X11 offers carry X11
provenance on both seats. An X11 paste of an agent-sourced selection routes
explicitly through the relay to the agent source, never through a presumed
human source. Xwayland generation teardown clears its clipboard and primary
offers on both seats; newer Wayland-owned replacements survive that teardown.
Clipboard sharing is independent of input reachability: agent
key and pointer delivery to X11 still returns x11_unsupported.
| Verb | Arguments, in addition to seat? |
|---|---|
comp.input.pointer.move | {x,y,output?} for output-local absolute coordinates; {dx,dy} for displacement; or {window:{id,generation},x,y,require_hit?}. output is an output key or name, defaulting to the default output. corners? defaults to true on the human path and has no effect on the agent path. |
comp.input.pointer.button | {button?,action?,window?,raise?}. button: left (default), right, middle, or evdev 0x100..=0x2ff. action: press, release or click (default). |
comp.input.pointer.scroll | {dx?,dy?,source?,v120?}. At least one axis; positive dy scrolls down. source: wheel (default), finger or continuous; zero on finger/continuous is an axis stop. v120:{dx?,dy?} supplies wheel detents; otherwise a wheel derives 120 per 15 units. |
comp.input.key | {key,action?,modifiers?,window?,raise?} or {text,window?,raise?}. key is an XKB keysym name or evdev code; action is press, release or tap (default). modifiers accepts shift, ctrl, alt, super, altgr. text is at most 256 characters. |
comp.input.release_all | {seat?}; omitted seat cleans both seats (seat:"both" in the reply). |
comp.input.sequence | {seat?,steps:[{verb,args?,delay_ms?}],interval_ms?}. |
Targeted keys, text and buttons require both identity fields in
window:{id,generation}. Resolve, focus and injection run without yielding to
the event loop. Before each generated press, the compositor rechecks the
identity against the driven seat's delivery target. A change returns
target_changed and releases this operation's new holds; already delivered
events are not rolled back. raise requires a window.
Agent targeting accepts mapped windows on other workspaces, minimised windows
and windows hidden from the human view, including while an exclusive layer has
human focus. It retains mapping, presentation and session-lock checks. An
interfering agent grab returns keyboard_grab or pointer_grab; a matching
popup chain is allowed. Untargeted keys use the agent keyboard's existing focus
or return no_keyboard_target. Agent shortcuts reach the client: use compositor
verbs for window management, or explicitly choose the human seat to test a
compositor binding.
Hidden and off-workspace windows receive no frame callbacks. A FIFO client can receive these input events yet defer processing them until the window becomes visible: protocol delivery does not guarantee application progress.
Agent reachability checks run before either device's focus changes:
x11_unsupported: X11 targets, including coordinate-selected descendants.agent_seat_unbound: the target client is currently unbound on the agent keyboard or pointer needed by the operation. There is no silent fallback.chrome_target: compositor chrome, decoration or a requested root-local point that does not hit that window's client tree.
These errors include hint:{seat:"human"}. Clients that bind only one seat
need that explicit opt-in. Toolkit compatibility still needs the release's
toolkit matrix; successful protocol tests do not establish toolkit support.
For agent {window,x,y}, coordinates are relative to the window-geometry
origin, exactly as on the human seat (CSD shadows do not shift the requested
point when switching seats). Hit-testing still uses wl_surface layout origins and
considers only that root's mapped client tree, including subsurfaces and popups,
in committed stacking order and with committed input regions. Every ancestor
must remain mapped. Workspace, minimisation, visibility and other windows'
occlusion do not exclude it; require_hit adds no human-visibility restriction.
Outside that tree, the move refuses without changing focus.
For agent {x,y,output}, hit-testing considers visible mapped client surfaces;
chrome and embedded Quoin panels refuse before focus changes. An existing implicit
agent button grab may cross chrome while retaining its client target.
Empty space clears agent pointer focus and permits a subsequent
click to dismiss an agent popup. {dx,dy} offsets the known agent position;
it sends ordinary pointer motion, never relative-pointer protocol events. A
targeted agent button uses the current agent position when it belongs to that
window, otherwise the centre of the root buffer, and validates the client hit
before focusing. Untargeted buttons/scroll need an agent pointer target (or a
popup grab handling an outside click), otherwise no_pointer_target is returned.
Agent input does not drive hot corners, pointer constraints, interactive
move/resize, compositor decorations or drag and drop. A client StartDrag on
the agent seat is refused before Smithay assigns an icon role or installs a
grab; its supplied data source is cancelled. A null-source StartDrag is also
refused but has no data source through which to send cancellation feedback.
Popup grabs use the agent seat's
own input serials and survive changes to human focus.
Human window-relative moves retain the window-geometry-origin contract.
require_hit:true additionally requires an on-output hit of that window/frame;
failure is off_output or occluded (with under). Human targeted keys/buttons
retain target_unfocusable with reason, id and generation for unmapped,
minimised, off-workspace, invisible or non-presentable windows, session lock,
exclusive layers, region selection or interfering grabs. They never restore a
window or switch workspace. Targeted releases reconcile holds without refocusing.
Human targeted buttons use the existing pointer route at the human cursor and
then reconcile pointer focus. Untargeted human input retains bindings, modal
owners and lock-screen delivery. Human pointer moves arm corners unless
corners:false; that option still permits leaving an engaged corner.
For human Xwayland targets, Smithay sends SetInputFocus / WM_TAKE_FOCUS
without waiting for X focus confirmation. A successful injection reply does
not guarantee X focus has completed before the first key; use client-side
acknowledgement when that ordering matters.
Each successful single verb replies:
{seat, input_seq, injected_at_us, pointer:{output,x,y}|null,
target:{id,generation}|null, targeted:{id,generation}|null, completed_events}
input_seq increases per verb. pointer is the driven seat's output-local
position; targeted identifies an explicit targeted key/button. target is
payload delivery, not a later focus state; consumed human bindings report null.
completed_events counts generated key events processed before completion or
refusal. Runtime refusals also identify the seat. Unknown/stale window identities
retain the existing target errors; moves retain unknown_output and
out_of_bounds. Later target changes can refuse after partial key delivery.
Timestamps use CLOCK_MONOTONIC: microseconds in replies, wrapping 32-bit
milliseconds on input events, matching the clock advertised by wp_presentation.
Text resolves against the driven keyboard's live keymap, locked modifiers and
layout. It uses the first two shift levels, adding Shift where required; newline
types Return. AltGr-only, dead-key and compose characters are unmappable
(char and index) before anything is typed. Unknown key names return
unknown_key; human text under an input-method grab retains ime_active.
Holds belong to a seat and an operation owner. release_all releases both seats'
injected holds unless explicitly scoped with seat, never a physical-device or
host hold. An explicit release
clears injected ownership of that key/button on that seat. Taps and clicks
release their generated presses; action:"press" can retain a hold.
An agent tap or click retains one recent press serial per device for a later
popup grab on the same root, without yielding inside the operation. A later
press replaces it; relevant focus changes and cleanup invalidate it; a successful
popup grab consumes it. Human clicks outside do not dismiss agent popups.
release_all with no seat or with seat:"agent" dismisses the agent popup chain
and removes its keyboard/pointer grabs; seat:"human" leaves it alone.
The agent half of bare cleanup does not record agent input activity or change
input.last_origin. Naturally ended popup chains retire their handles while
preserving a newer press that may open the next menu. Explicit dismissal clears
both device action slots. A chain whose popup or root has unmapped is also
retired; a newly created popup may still complete its initial configure/map
handshake, provided its root remains mapped.
Keys during that interval refuse with unmapped: validation checks the popup
the keyboard grab would deliver to, rather than its mapped parent.
Explicit agent key/button releases bypass target candidacy and focus mutation,
so a hold can be retired after its old target unmaps; session lock still refuses.
A sequence has at most 256 steps and 4096 generated events in total. Delays
run on compositor timers, before the step, defaulting to interval_ms (default
0), and total at most 60 seconds. It yields after 256 injected events; runs can
interleave at delays and yields. The reply is
{seat,steps:[<each step's reply, including its seat>],elapsed_ms}.
An explicitly seated sequence reports that seat at the top level. An unseated
sequence reports human, agent or mixed from the steps reached, including
the refusing step for step_failed; bare both-seat cleanup counts as mixed.
An empty capability probe is refused with invalid_argument before any seat
applies. Per-step seats are unchanged.
On a failed step, it returns step_failed with index, verb, step,
completed and released:true, and gives up its holds on both seats.
Other owners' holds remain. Caller cancellation also releases the run's holds.
Eight sequence permits are available; admission beyond that returns busy.
Within a dispatch batch, only consecutive agent motions to the same live client surface and with the same coordinate contract coalesce. Window motions must name the same window generation; output motions must use the same output selector; consecutive relative deltas are summed. Grabs, surface transitions, sequence delays and intervening controls (including keys, buttons and scroll) are delivery boundaries. Human motions retain their existing path and cannot merge with agent motions. Separate sequence runs are also boundaries.
Each coalesced request is answered after the final motion is delivered.
Its reply reports that final position, target, timestamp and shared input_seq,
with coalesced:N giving the number of requests represented by that delivery.
Intermediate coordinates are not reported as delivered. Sequence replies keep
one entry per requested step, sharing those delivery fields for a coalesced run.
Ready human HostInput drains before Bus delivery. Each dispatch services at
most eight agent controls (standalone verbs or sequence admissions) and one
resumption burst, plus a separate allowance for one initial sequence burst.
The initial burst counts toward the eight-control budget and runs its own
sequence at its admission position, before later controls. Resumptions cannot
spend that allowance. A second initial sequence and all later controls remain
queued for the next dispatch. Snapshot/props reads admitted after
parked controls wait for those controls to finish; earlier reads can complete.
Sequences yield at the existing 256-event boundary between atomic operations.
Sequences using the agent seat also yield after visiting 256 steps so
coalescing cannot create an unbounded scan. Ready agent sequences rotate in
queue order. Queued agent work wakes the event loop directly: continuously
arriving human input cannot postpone it indefinitely, and no polling timer is
used for agent continuation. Explicitly requested delays still use one-shot
timers. Resolve, focus and each individual input operation remain atomic.
Lock entry, VT switching, session pause and input-authority loss clear agent
holds, grabs and focus and cancel sequences using the agent seat with
input_cleared. They also refuse parked agent controls and pending sequence
admissions with input_cleared, seat:"agent", released:true. An admission
epoch catches requests still in ingress at the boundary, so stale queued work
cannot recreate cleared holds. Other controls retain their order.
Surface loss is reconciled per device without cancelling sequences:
a dead pointer target loses pointer focus and pointer holds; a dead keyboard
target keeps an active popup keyboard grab, which delivers the next key to its
topmost live menu. Without that grab it returns to its nearest live ancestor,
or loses keyboard focus and
key holds if no parent survives. This does not arbitrate human focus. Unlock reconciliation samples human
pressed state only. Agent cleanup does not reset idle notifications.
Human region selection and nested keyboard-focus loss do not clear agent state.
input.host.passthrough remains nested-only (unknown_path on KMS) and controls
the human host path. Setting it false suppresses host keys, motion, buttons
and scroll; resize, scale, pointer leave and touch still pass. Pre-existing host
holds still receive releases. Restore it to true when finished; the value lasts
until changed or comp exits.
The frame trace retains comp_input_injected: subject input_seq, detail
1 move / 2 button / 3 scroll / 4 key / 5 text / 6 release_all, aux target id or 0.
Presentation input marks also retain the originating seat.
The compositor publishes non-retained messages under the registered service
namespace. The seat instance therefore uses comp.*, the default nested
instance uses comp-nested.*, and --bus-service NAME moves the complete
namespace to NAME.*. Every inner command is the unprefixed suffix shown
below, so handlers do not depend on the instance name.
| Topic | Inner command | Exact body |
|---|---|---|
<service>.props.changed | props.changed | {path,old,new,ts,cause,event_seq} |
<service>.surface.mapped | surface.mapped | {id,role,generation,app_id,title,foreign_id?,event_seq} |
<service>.surface.unmapped | surface.unmapped | {id,role,generation,app_id,title,foreign_id?,event_seq} |
<service>.focus.changed | focus.changed | {keyboard,previous,exclusive_latch,event_seq} |
<service>.output.changed | output.changed | {output,geometry:{x,y,width,height},usable:{x,y,width,height},event_seq} |
<service>.corner.entered | corner.entered | {output,corner,dwell_ms,event_seq} |
<service>.corner.left | corner.left | {output,corner,dwell_ms,event_seq} |
<service>.corner.clicked | corner.clicked | {output,corner,dwell_ms,event_seq} |
<service>.corner.clicked.v2 | corner.clicked.v2 | {output,corner,button,kind,modifiers,dwell_ms,event_seq} |
<service>.pointer.changed | pointer.changed | {version:1,instance,output,position,valid,timestamp_ms,event_seq} |
Map edges carry the surface's role generation and its app_id and title
(null when the client set none, and null while a session lock is active). A
map edge reports the values at the end of the cycle; an unmap edge reports the
values from before the unmap. A window unmapped and mapped again under a new
role within one cycle emits both edges (unmap with the old generation, then
map with the new one). An XWayland window whose buffer arrived before
its map request now emits its map edge too.
For a stream of window changes (moves, resizes, state, focus), hold a
comp.props.watch and read the windows.s<id>.* props.changed frames, plus
focus.changed. There is no separate window topic. To wait for one
condition, use comp.window.wait rather than subscribing and hoping: the
topics are not retained, so an edge that happened before the subscription is
never delivered.
Engaged corners consume pointer presses and their matching releases. The v2
click topic reports button: "left"|"right" and kind: "brief".
Both buttons emit on release, with no hold timer or hold action. modifiers
contains the active shift, ctrl, alt and super names captured at press
time in the compositor input path, even if they change before release. The field
is always present, including modifiers: []; Quoin refuses a v2 body without
it. Other buttons are consumed without an action.
Movement further than input.corners.deadzone_px from the press position cancels
the pending action, even within the hotspot. Leaving the corner or resetting
engagement (including output changes, lock, or config changes) also cancels it.
Cancellation retains release ownership; returning to the corner cannot revive
the action.
The original corner.clicked topic retains its exact JSON body and emits only
successful unmodified LMB brief actions, on release. Every modified click,
including Ctrl/Alt/Super+LMB, emits only v2. V2 consumers should subscribe only
to corner.clicked.v2 to avoid handling LMB twice. Consumers that also take
legacy, as a fallback while their v2 subscription settles, must deduplicate:
each unmodified LMB emits legacy at sequence N immediately followed by v2 at
N+1, with the same output, corner and engagement dwell. Quoin uses this pair to
admit the first click once in either delivery order, then ignores legacy after
observing v2. The legacy sibling is required even with modifiers: []: Quoin
canonicalises the v2 sequence to N. Modified clicks keep their own sequence.
Compositor and shell-host must be upgraded together; neither skew is clean. A
host that predates v2 entirely sees legacy LMB and nothing else. A v2-aware host
from before modifiers existed rejects every v2 body, never marks v2 as seen
and acts only on legacy, so RMB and every Shift/Ctrl/Alt/Super+LMB click is lost
silently. The reverse skew — a compositor from before modifiers against the
current Quoin — is louder but no better: Quoin refuses every v2 body from it
(ERROR quoin_corner_old_format_rejected, field=modifiers), so RMB does
nothing and the corner menu is unreachable from the corner. That compositor
also emitted legacy for every LMB, modified or not, so unmodified LMB still
pins and Shift+LMB pins instead of docking — but only on a fresh Quoin
connection. Rolling the compositor back in place while Quoin stays connected
is worse still: Quoin has already seen v2, so it ignores legacy, and the
rewound sequences drop as stale, so no corner click acts until Quoin
reconnects.
Both versions carry engagement dwell, not press duration. Quoin routes LMB brief
to overlay pinning, Shift+LMB to docking, and RMB to the corner menu.
Ctrl/Alt/Super without Shift retain LMB pinning. LMB from docked becomes pinned;
Shift+LMB toggles docked/hidden. No bare button click docks a panel.
For a reliable property bootstrap: subscribe to the instance topic (for
example comp.props.changed on the seat or comp-nested.props.changed when
nested), call comp.props.watch, verify its returned topic, then read the
required tree or subtree. The watcher is itself the subscriber: while that
subscription remains active, noded cannot send topic.idle for its subscriber
generation. An idle delivered in the same control batch as watch therefore
belongs to a previous generation; the next zero-to-one topic.active re-seeds
the baseline. Mix handlers match the suffix: on props.changed,
on surface.mapped, on focus.changed, and so on. Changes are reduced after
each complete protocol dispatch. A leaf therefore appears at most once per
cycle, with its cycle-start old, final new, lexical path order and one of
wayland.map, wayland.unmap, wayland.focus, output.geometry,
layer.arrange, session.lock or props.set as cause. Operational port.*
leaves are readable but are not self-published as property changes.
Keyed row creation and removal are row-granular: an appearing
surfaces.s<id>, windows.s<id> or outputs.o_<slug> emits one frame at the
row path with old:null,new:<full row>, and removal emits the inverse.
Mutations within an existing row remain leaf-granular.
The sequence is process-global, strictly increasing and shared by property,
surface, focus, output, corner and pointer records. If it reaches u64::MAX, that value
is offered once and observation enters a terminal exhausted state rather than
reusing a sequence. The outbox is one bounded 256-entry lane. On overflow the
producer evicts one oldest record in fixed time and carries that record's loss
interval inside the next record it sends; if an evicted record already carries
loss, both intervals coalesce. Once the publisher learns an interval, it emits
a gap on each affected topic before the next record it publishes on that topic,
or during the idle flush when the lane drains empty. Survivors produced before
the carried loss reaches the publisher may therefore be published before its
gap. The gap's Bus header is event_seq=<last lost seq> (the coalesced
interval's last-lost sequence), which locates the hole, and its body is
{gap:true,lost_count,cause:"outbox.overflow"}. lost_count is the same
cumulative process-wide counter as port.lost_count, not a per-interval tally.
Consecutive intervals coalesce while pending, bounding gap traffic to at most
one gap per topic per published record plus the idle flush. A rejected or
timed-out publication discards its uncertain backlog and recovers under the
same ordering rule with cause:"publisher.loss". A failed pending gap retries
immediately on broker connection-state edges and on a single one-shot backoff
timer (1 second, doubling to a 30-second cap); that timer exists only while the
gap remains pending. After either gap, read a fresh property tree.
Pointer observation reads the latest cursor state after protocol dispatch, with at least 34 ms between samples. Input handlers do not serialize or wait for the Bus. Renewing a lease requests a fresh sample even if the pointer is stationary; expiry leaves no publication timer or motion history. Multiple local observers share the bounded lease and publication lane.
Valid samples contain the output's advertised name and position:{x,y} in
output-local logical pixels. timestamp_ms is monotonic elapsed time within
the compositor instance, not wall-clock time. Locking, pausing the KMS session,
leaving the output or an invalid position produces
valid:false,output:null,position:null. Retained client output globals do not
keep a paused session's pointer valid. Pause and resume request fresh samples
within the existing rate limit even when coordinates stay unchanged. No key
or button data is included. These transient events are not retained by comp.
Noded reserves <service>.pointer.changed publication to its registered owner
and stamps broker_service on reserved event deliveries. Local consumers
must check that stamp and broker_origin:local, plus instance and sequence;
a topic header alone is not proof of publication. Keep only the latest
sample, clear stale samples on disconnection/lock, and advance the snapshot
fence when output geometry changes. Watch acknowledgements and samples may
arrive in either order over separate connections; keep a bounded early sample
with its receipt time until acknowledgement. A filtered global sequence need
not be consecutive.
The desktop's calloop 0.14 channel patch prevents unrelated child-source tokens from generating new wakeups. This fixes a session-management busy loop in the deferred libseat notifier; an inactive VT should wait for genuine session/input/control events rather than occupy a CPU core.
Cursor projection compares resolved materials, transforms and visibility before writing them. Unchanged built-in and SHM client cursors therefore avoid repeated render-asset updates; new client content still invalidates its material binding when the image handle is reused. DMA-BUF cursors retain per-update material rebinding while their import owner reports pending render work. Live client-content rendering can skip unchanged frames; active-VT CPU costs still require measurement.
The renderer also retains a protocol/cursor/asset/component scene revision independently of capture subscriptions. KMS binds the extracted revision to an acquired frame and reports it with a successful presentation; later scene changes cannot relabel a retained frame. Idle admission combines this revision with asynchronous asset/pipeline readiness and output lifecycle checks.
Main-world observers run after the standard Last schedule for installed image,
mesh, 2D material, shader and font assets. They track direct resource changes and
read asset events independently of Bevy's renderer. Late edits and their delayed
events each advance the revision; quiet asset maintenance does not. Bare asset
stores are observed without replacing them or requiring an event plugin. This
records demand, not GPU readiness or arbitrary component changes. Event-bearing
turns require extraction before messages expire.
The same post-Main observation schedule tracks the installed 2D scene's transform, visibility, sprite, mesh/material binding, text layout/style and camera components. Additions, changes, removals and despawns advance demand without consuming the renderer’s removal notifications. This catches layout completion using glyphs already present in an atlas. The observers run on one thread because they share the revision writer. Camera and projection observers compare their rendered values, so target-refresh bookkeeping does not perpetually demand another frame. Global clear-colour changes also advance demand. New render features must extend the component inventory.
An idle turn still runs Main, including input, protocol, layout and capture maintenance. It skips extraction and rendering only for a ready output whose known scene revision matches a settled, genuinely presented frame. Pending render commands, acquired frames, capture/security presentations, DMA-BUF work or quiescence prevent idle. Checking queued commands retains them in their owner for normal processing or teardown. First-light animation and enabled diagnostic capture/DMA-BUF probes keep full rendering. Idle services the device-error hook, nonblocking device polling and Bevy's time handoff without acquiring another frame.
Live KMS readiness carries the output key and generation through startup and resume. Both supervisors validate each complete frame-event batch against that identity before watchdog, telemetry, security acknowledgement or callback effects. A stale submission or cancellation therefore cannot renew output health or acknowledge another output's presentation. Terminal render failures retain their existing handling. Update requests also carry a sequence starting at one for each ready generation. The renderer rejects stale or skipped requests; both supervisors and pause reconciliation require the reply to match the exact outstanding request. Reports distinguish a full Main/extract/render pass, with its demand revision sampled after Main, from proven idle and lifecycle maintenance. Neither maintenance nor an empty frame-event batch certifies healthy idle or relaxes the submission watchdog.
Each active renderer update has its own two-second response deadline. The
coordinator waits against the earlier response or submission deadline; silent or
late updates cannot pulse clients or acknowledge security presentation. A timely
empty response does not extend the frame-submission budget. A response timeout
has the distinct kms-live-update-response-timeout diagnostic. Validated idle
suspends the submission requirement while keeping response checks active. New
demand starts a fixed submission deadline that further empty replies cannot
postpone. Idle waits use the interruptible coordinator mailbox at nominal output
refresh cadence; callbacks occur at most once per interval. Idle does not count
as a submitted frame or acknowledge security presentation. Registration, resume
scene draining and transition budgets are unchanged.
GPU asset preparation has a separate internal snapshot. It observes extracted image, mesh and installed 2D material IDs without consuming Bevy's queues, and retains pending replacements/removals until preparation reflects them. Successful KMS frame reports include the post-preparation snapshot. Retry work cannot look complete merely because no new extraction arrived. Pipeline compilation, DMA-BUF ownership and the final idle-admission decision remain separate.
Pipeline settlement is sampled after drawing and capture, before presentation. One additional queue-processing pass exposes newly queued pipelines and completed asynchronous compilation; missing shaders/imports remain pending, while permanent shader errors are reported separately. New entries or pipelines becoming ready after drawing are flagged as requiring a later rendered frame. The snapshot is attached only to successful KMS presentation and is cleared during output teardown. This pass can start compilation and adds preparation cost; synchronous mode can compile within the call. It does not loop until pipelines settle or prove every intended draw was submitted. An independent 30-second settlement deadline prevents fallback presentations from indefinitely hiding pending assets or pipelines. The budget resets for a replacement output generation after resume.
The panel holder plane is two verbs, sent like every comp verb as the literal
command (comp.panel.hold, not <service>.panel.hold) addressed to the
selected service. comp.panel.hold takes output (raw connector name), edge
(top, bottom, left, right), surface (the layer-shell namespace token),
holder (pointer, focus, popup) and boolean acquire.
comp.panel.mode takes the same output/edge/surface address, mode
(hidden, pinned, docked) and an optional generation (the reporter's
Bus connection generation, a non-negative integer). Both run at the stable
observation dispatch boundary and return {"accepted":true,"surface":...}.
Malformed arguments are refused as invalid_args naming the offending
field, with the allowed list (a hold carrying generation is malformed).
Acquisitions require a live layer on that output. Mode reports survive concealed
panel-layer destruction; releases match their recorded token even if the layer
has already gone, and a release for an edge comp holds no state for is a no-op.
Refusals are unknown_output, unknown_panel_surface (acquire with no layer,
or with a layer on an edge no registered holder service has reported yet),
panel_output_mismatch (the token's one layer is on another output),
ambiguous_panel_surface (the token names more than one layer, whatever their
order), panel_owner_mismatch (the token's layer belongs to a different live
Wayland client than the one that owns the edge), stale_generation (a holder
report whose generation is lower than the edge's current one: a delayed
report from an older Bus connection, which changes nothing) and locked
(session lock).
Explicit requests are idempotent; persistent modes clear holders and ignore
acquisitions.
The namespace token is created by Quoin for each layer lifetime. It resolves
to comp's own surface identity without relying on client-local Wayland object
numbers or choosing the topmost layer. Popup holds name the menu's own layer
because the panel can be hidden. The association is re-resolved whenever a layer
maps or unmaps, so a mode report that overtakes its layer binds when the layer
maps; tokens are never reused, and Quoin's carry 128 random bits, so another
client cannot guess one. Namespaces are not authenticated: a client that
copies a token makes it ambiguous, which refuses rather than misdirects. A Bus
peer that can read the panel topics can copy a token and report it; that is
the mesh trust boundary (every mesh caller reaches every verb), not a hole
this plane closes. For the same reason, enforcement that hides a panel or
excludes its input acts on the surface identity comp resolved from the token,
never on a namespace prefix match. Output
removal drops that output's state without a signal; Quoin rebuilds its panels
with fresh tokens when an output goes, so nothing stale is suppressed.
Besides the explicit holds, comp tracks two holders per reported panel itself
(shell design §4.3). The pointer holder is acquired by dwelling in the edge's
hotspot (the corner engaging) or by the pointer entering the panel's layer or a
held popup's; any contact with those, including an undwelled pass through the
hotspot, keeps it; leaving them all starts an 800 ms conceal delay that re-entry
cancels. The focus holder is keyboard focus on the panel's layer or a held
popup's. A held popup's layer being destroyed releases its hold at once, even
before the client's release arrives; the popup hold also records the keyboard
focus the popup displaced when it takes focus and restores it (toplevel or
layer) only when the popup's destruction moved focus and focus is still where
comp's fallback put it; focus moved off a live popup cancels the restoration,
except into another held popup (a nested menu), which restores back to it.
Membership is evaluated at the stable post-dispatch boundary, and again after
the cycle's Bus controls, so a command is
emitted only when an edge's verdict changes: reveal when the first holder
arrives, conceal when the last leaves — at once for focus and popup, after the
delay for the pointer. The delay is a single one-shot calloop timer, armed only
while a lingering pointer is the last holder of a hidden panel. A hidden
comp.panel.mode report always re-states the current verdict, so a client that
has just started following the commands learns it. Commands go out on
<service>.panel.command through the existing bounded observation outbox and
its gap reporting; the version-1 body contains output, edge, surface (the
panel's token when comp has one), action and event_seq.
Each edge belongs to one Quoin incarnation, identified by the Wayland client
of its panel layer — an identity comp attests itself, unlike the token. An
unowned edge is adopted only for a layer whose token a registered holder
service (the broker-stamped sender, never an anonymous caller) named: in a
comp.panel.mode report, or in a hold once that service has reported the
edge (a corner menu can open while its panel has no layer). The first such
service to report an edge is its holder service; a mode report from anyone
else — an anonymous caller or another service — is recorded (its mode applies)
but never replaces the token the holder named and binds nothing, so a foreign
token can never become the one a later mapping adopts. A layer from a
different client that is still connected is refused as panel_owner_mismatch
and never binds, even when it is the only layer the (copied) token names; an
edge whose owner has gone is taken over by the next reported client with
nothing of the old incarnation carried across. When the owning Wayland client
disconnects (Quoin crashed or exited), comp drops every explicit hold it
acquired, its popups and any enforcement, and the edge conceals by the normal
rules: the automatic pointer and focus holders are comp's own and still apply.
Comp also subscribes to noded's registry (noded.props.changed,
services.registered): when the holder service that reported an edge leaves
the Bus, its explicit holds go, while the owner and any enforcement — which
belong to the Wayland client — stay. This is the local node's registry: a
holder reaching comp over the mesh is not in it, so its holds drop on the next
local registry change until its next report. noded rate-limits each path's
diffs, so one can be dropped; every diff carries the full set, so the next
repairs it. The subscription is retried on every reconnect until it succeeds,
after which the client replays it. Generations only move forward: a holder
report with a higher generation is a new Bus incarnation whose
predecessor's holds end there, and a lower one is refused. The liveness probe
below bounds anything a missed registry event could leave behind.
Comp enforces its conceals (shell design §7: a slow or stopped shell must not
keep a panel shown or hold the input). When a conceal ends a reveal comp
itself commanded, comp records the owner's layers showing for that edge — the
panel layer and any popup layer acquired for it. If any of them unmaps or goes,
the shell has applied the conceal and nothing is owed, so a panel the user
shows again at once is never hidden. If they are still mapped 1 s later (the
shell's slide takes 200 ms), comp asks whether the shell is alive: it re-sends
each layer its current configure, unchanged, and waits up to 1 s for the
ack_configure a live client sends and a stopped one cannot. The same probe
runs when an owner layer stays shown for 1 s while the verdict is conceal for
another reason (a shell that stopped during its startup intro or an explicit
show), and at once when the user clicks or types somewhere the shell does not
own while only a popup or focus hold keeps the edge revealed (a stopped menu
or launcher). A probe costs a live shell one configure round-trip and one
re-rendered frame; at most one runs per owner at a time, and an owner that has
just answered is not probed by a press again for a second, however often the
user clicks elsewhere. An answered probe owes nothing and is not repeated
until the next trigger. An unanswered one marks the owner stalled: its popup and focus
holds drop, its keyboard focus stops counting as a holder, its Exclusive
layers lose their keyboard grab (arbitration treats them as on-demand, so the
application gets the keyboard back), the edge conceals, and the showing layers
are hidden and excluded from input at once. Any later acknowledgement or Bus
request from the owner clears the stalled mark.
Hiding goes through the same effective-visibility funnel as minimising, so the layers stop rendering, stop being hit-tested, lose keyboard and pointer focus and can no longer hold an exclusive keyboard grab; their subsurfaces and popups go with them. It acts only on the recorded surface ids, never on a namespace or prefix match, so no other client's surface — a foreign layer or toplevel on the same output — is touched. Enforcement ends when comp reveals the edge again, when the client unmaps or destroys the layer itself, on any mode report for the edge, and with the owner's disconnect. A hidden report that finds a conceal still owed lifts the exclusion but owes it again with a fresh grace, so a shell that resumes and stalls again stays bounded. A stopped shell's panel is therefore hidden about 2 s after the conceal: 1 s of grace and 1 s for the probe to go unanswered. The grace and the probe share the single one-shot timer with the conceal delay; nothing polls and nothing is sent to a shell that is not being checked.
What remains open to a Bus peer: the verbs are mesh-open, so any caller can
send a comp.panel.mode for an edge. Before a holder service has reported an
edge, a registered service's report makes it the holder; after that, anyone
else's report can change the edge's mode and lift an exclusion (owing it
again), but cannot change the token, make another client's layer the panel or
select it for hiding. A registered service name is trusted as the broker
stamps it: the mesh is the trust boundary.
Not covered: a docked panel's reservation from a stalled shell stays in place (shell design §7, "stale reservations"); tracked in TODO-cos.
The read-only input.corners.holders leaf is the switch clients gate on. It
reads true: the verbs, holder tracking, the conceal timer, the liveness
probe and enforcement on a stalled client, disconnect cleanup and
resynchronisation are all live, and Quoin hands reveal/conceal over to comp
when it reads it. Two families of
read-only, volatile leaves (served by comp.props.get/list/describe,
never in props.changed) report the plane per edge, summed over outputs:
input.corners.enforced.{top,bottom,left,right} counts the layers comp is
hiding and excluding right now, and input.corners.held.{top,bottom,left,right}
the explicit holds it records. A stalled-shell check reads them: with the
shell stopped (SIGSTOP) after a pointer reveal, the pointer's departure
conceals after 800 ms, comp probes a second later, and enforced.<edge> reads
1 about a second after that; with a menu open instead, a click on an
application probes at once, and a second later the keyboard returns to the
application and the panel and menu are enforced. After SIGCONT the shell's
own conceal returns enforced to 0, and held.<edge> returns to 0 once its
menus have closed.
The per-seat input.seats.* observations and input.last_origin are also
read-only and volatile: get, list and describe serve them, but
props.changed never reports them. Focus identifies a surface by {id,generation}
or is null; pointer coordinates are output-local logical coordinates or null
when unknown or locked. last_input_us uses CLOCK_MONOTONIC microseconds and
is null before input. An unset last_origin is omitted from the serialised
tree (a direct read returns null). Existing focus and pointer leaves remain human.
Hot-corner detection is compositor-side and uses the current logical output.
It emits one entered, then one left on deadzone exit, output or geometry
change, session lock, disable, or config invalidation. corner is tl, tr,
bl or br; left repeats the dwell measured by entered. Fast transit is
not accepted until a velocity-qualified dwell, while continued slow outward
motion constrained by the output edge can enter early. Defaults and inclusive
ranges are:
| Property | Default | Range |
|---|---|---|
input.corners.enabled | true | boolean |
input.corners.deadzone_px | 10.0 | 1.0..=256.0 logical px |
input.corners.dwell_ms | 200 | 0..=5000 ms |
input.corners.velocity_max_px_s | 1500.0 | 1.0..=20000.0 logical px/s |
input.corners.affordance | true | boolean |
input.corners.discovery | false | boolean |
deadzone_px is the hotspot: a square of that many logical units at each
output corner, so it is the same size on a 2x output as on a 1x one.
The compositor draws the hotspot affordance itself, above every client —
layer-shell panels included — and below the cursor, in the scheme accent. With affordance true it
shows the engaged hotspot while the pointer rests there, flashes it for
180 ms on every recognised release (the brief LMB or RMB that emits
corner.clicked.v2), and, while discovery is true, blinks every hotspot
slowly until the first engagement. That engagement sets discovery back to
false with cause corner.entered; a shell that reveals a panel another way
(keyboard) writes false itself. The compositor keeps no record of a first
run, so it never turns discovery on: a shell does, when its own state says
the user has not yet found the corners — Quoin writes true once, on the
launch that finds no quoin.state.mix, and creates that file when the
compositor accepts the write, so it is never requested again. affordance: false makes the corners
silent without changing detection. Nothing is drawn under a session lock.
The affordance renders only when what it draws changes — once on engage, once
on leave, a few quantised steps for a flash, two frames per 2 s blink — so a
settled corner leaves the renderer idle.
The affordance is ordinary on-screen furniture in the base layer, not a
cursor-plane overlay, so screenshots and screencasts include a hover square
or flash that is showing when they are taken. With the embedded-quoin
build feature, the embedded Quoin draws its panels as UI that composites
above the compositor's scene, so an embedded panel covering a corner hides
that corner's affordance; the production Quoin is a separate layer-shell
client and is unaffected, and the ordering belongs to the embedding work.
Squares are sized and placed in each output's own logical coordinates, but
the renderer currently places every output camera over one shared logical
canvas at one output scale — the same limit client placement has — so
mixed-scale, multi-output correctness depends on the renderer's multi-output
camera model, not on the affordance.
The mutable leaves are the six corner leaves, windows.s<id>.band,
windows.s<id>.minimized, windows.s<id>.maximized, windows.s<id>.fullscreen,
windows.s<id>.workspace, workspaces.count,
workspaces.current, workspaces.o_<slug>.current, input.host.passthrough
(nested only) and xwayland.enabled. The corner, window and workspace
descriptors say mutable:true and
persistence:"none" (numeric leaves also carry the range above) and those
values live for the compositor process only. xwayland.enabled is the one
exception: its descriptor says
persistence:"file" — the value is read once at compositor startup (whether
to spawn XWayland at all; there is no live toggle) and a write persists it
for the NEXT startup into a per-socket file under the COSMIX etc tree, whose
resolved absolute path the read-only xwayland.persist_path leaf reports
and the compositor logs at startup. The COSMIX_COMP_XWAYLAND environment
variable (0/false/off/no or 1/true/on/yes) overrides both the file and
the default at launch — the no-rebuild back-out that works even when the
props surface is unreachable. Unknown and immutable
paths return unknown_path and read_only; type/range failures return
{error:"invalid_value",path,expected,range}. All four path/type/range checks
run on the worker before admission and are repeated on calloop as
defence-in-depth, so invalid writes consume no ingress or responder permit. A
no-op write replies normally without a change record.
focus.session_lock is none, locking, locked, orphaned or unlocking.
While a session lock is active, the read tree applies the same
WaylandState::session_lock_active presentation boundary as the renderer and
foreign-toplevel publication: ordinary surfaces retain ids, roles, bands and
geometry, but report visible=false and null title/app_id; windows is
empty. During the KMS unlock window (normal_scene_restricted()), the read
tree stays redacted with focus.session_lock="unlocking" until the
compositor's own presentation predicate lifts, at the same moment the renderer
resumes. Unlock then restores the ordinary projection.
All application errors use Bus rc 10. Every refusal body carries
error_code with the same value as error (error is kept as an alias for
0.58.x), so a Mix send receives the whole object: $result.error_code,
$result.under and so on. In addition to the write errors above,
read/dispatch errors include unknown_path, busy and unknown_verb, plus
{"error":"too_large","limit_bytes":N,"hint":"read a subtree"} when a
serialised reply would exceed the effective broker-path ceiling. N is
8,384,512 bytes: min(16 MiB Bus message, 8 MiB single WebSocket frame) minus
4 KiB of documented header/framing headroom. Immediately before sending, comp
also measures the actual canonical response headers, correlation id and
framing with the body and refuses any reply whose complete wire size would
exceed that ceiling. Replies are never truncated.
A busy reply to comp.props.set or a window verb means the reply missed its
two-second budget or the queue was full at admission. If the request had
already been queued, it can still be applied when the compositor drains the
queue. A caller that gets busy should read the state again (for example
windows.s<id>.minimized) before retrying, not retry blindly. An absent broker never
delays compositor startup: the port thread reports retrying and reconnects
independently. A registration rejection (collision, invalid SPEC 10 name or
admission) is logged once, ends the port worker without renaming, and leaves
the compositor running.
The broker client lives on the named cosmix-comp-port OS thread with its own
current-thread Tokio runtime. At most 16 accepted controls/reads cross a bounded calloop
channel. The calloop callback only stages requests; after the current protocol
transaction and popup cleanup, one owned snapshot is built and shared by all
requests in that dispatch. Snapshot admission is released before reply I/O.
Replies and publications use separate lanes and two-second deadlines, so a
stalled topic sink does not stop incoming commands. Full-tree JSON is
serialised once per snapshot on the blocking pool, with only one full-tree
serialisation active process-wide; requests share the resulting string. Subtree
reads serialise only the selected value.
The semantic observation reducer carries typed rows and scalar values across
the bounded outbox; only the worker constructs topic JSON. Successful offers
wake the publisher with an event notification, which drains the outbox to
empty. There is no publisher polling timer or idle tick source. topic.idle
drops the property baseline and a later topic.active seeds one at the next
stable service point; both lifecycle directions coalesce latest-wins if the
ingress is temporarily full. These notices, and the noded.props.changed
registry diffs, are honoured only from the local broker: from: noded with
broker_origin absent or local. Absent is the normal case, because noded
does not stamp its topic notices. A message claiming noded with
broker_origin: mesh is ignored and logged at warn level, at most once every
10 seconds. This is defence in depth. Mesh ingress strips from, and responses
never reach this dispatch. The one reachable forgery is a client connected
over WireGuard to a pre-0.18 noded that let it register the name noded.
noded 0.18.0 refuses that name.
The 16,384-surface cap bounds tree cardinality, not reply bytes. A full tree can
still serialise far beyond the wire allowance, so comp measures the cached
full-tree bytes once and returns too_large; callers can read a leaf or subtree
from the same snapshot. Single-flight serialisation prevents same-snapshot
multiplication.
Absent by design after P-1:
comp.surface.*control verbs: focus, raise, close, move and resize arecomp.window.*verbs on managed windows only;- render timings, because they are metrics rather than properties; and
- a Bus screenshot verb, because it is a later control-plane slice; Arc 4's capture service is available through the Wayland protocol described below.
Supported Wayland protocols
The compositor advertises the core compositor, subcompositor, seat, output, shared-memory, DMA-BUF, explicit synchronisation, viewporter, fractional-scale, presentation-time (see below), XDG shell and XDG decoration globals needed by its desktop clients.
| Protocol | Version | Current support |
|---|---|---|
zwlr_layer_shell_v1 | 4 | Layer surfaces and layer popups map, arrange and configure through Smithay's LayerMap; protocol strata, keyboard interactivity, input regions and exclusive usable-area effects are supported. |
ext_idle_notifier_v1 | 2 | Per-seat notifications use Smithay's calloop timers; real pointer, keyboard, touch, pointer-gesture and tablet-tool activity resets the timeout and resumes an idle notification. Device-removal reconciliation does not count as activity. |
ext_foreign_toplevel_list_v1 | 1 | Mapped XDG toplevels expose stable mapping identifiers, title and app ID updates; unmap or destruction closes the handle, and late clients receive the current mapped set. |
ext_session_lock_v1 | 1 | Nested and live KMS modes support immediate output-sized lock-surface configures, secure blank-first presentation acknowledgement, lock-only input, VT pause/resume preservation and the locked/orphaned lifecycle. |
zwlr_screencopy_manager_v1 | 3 | Compatibility output capture into exact-layout wl_shm buffers, plus eligible whole-output v3 DMA-BUF destinations; includes clipped SHM regions, real damage waiting, exact cursor inclusion and presentation-timestamped nested or KMS completion. |
wp_presentation | 2 | Nested mode, and live KMS in client-content mode with kernel page-flip times, vblank sequence and mode refresh (see Presentation feedback below). |
DMA-BUF import observations
The linux-dmabuf format table says what the driver claims to support.
The dmabuf.* properties say what comp actually accepted, which is a
different fact. Every zwp_linux_buffer_params_v1 import that comp
answers is counted. A request that Smithay refuses first with a protocol
error, before comp sees the buffer, is not counted:
dmabuf.acceptedcounts accepted imports.dmabuf.failedcounts refused imports.dmabuf.failureslists the newest 16 refusals, oldest first. Each is{format, modifier, reason, detail, at_us}: the fourcc as four characters, the modifier as0xplus 16 hex digits, a reason, the refusing check's own message, and the CLOCK_MONOTONIC µs it happened.
reason is one of invalid_metadata (comp's own size, plane or format
checks), vulkan_rejected (the Vulkan test import on the renderer's
device said no), descriptor_dup_failed, queue_full, worker_stopped,
probe_panicked or probe_retired (refused because an earlier panic
retired the probe). The leaves are read-only and volatile: comp.props.get dmabuf reads them, but they never appear in props.changed, because a
refusal storm would flood the topic. They live in memory only and start
from zero when comp starts. The advertised format set is not yet demoted
from these observations.
Presentation feedback
wp_presentation reports when a client's commit was actually shown. The
global is advertised only once a backend has wired a frame reporter, so no
client waits on feedback nothing will resolve.
- Clock: CLOCK_MONOTONIC (
clock_id1), the same clockframe_traceuses. - Which commit a frame showed: feedback is taken when the commit is
applied, so a later commit cannot discard it while the earlier one is still
on its way to the screen. Every new buffer the compositor hands to the
renderer gets a content sequence number, and a presented frame reports, per
surface, the sequence it actually sampled. Feedback for exactly that commit
is
presented. Commits older than it were replaced before any frame showed them, so they arediscarded. Newer commits keep waiting. A commit without a new buffer carries the sequence of the content it leaves on screen, so it is presented with the next frame that shows that content. When one transaction applies several commits at once (a synchronised subsurface waiting for its parent, or a commit held by a blocker), each commit keeps its own feedback: a commit whose buffer a later commit in the same transaction replaced isdiscarded, even if the later commit asked for no feedback. - Re-uploads are not refusals: when the compositor re-sends a surface's current content (a relayout or recovery) and that upload fails, the content already on screen stays shown and its feedback is not discarded. Only a failed newer commit is.
- What "shown" means: the surface is mapped and visible, lies at least partly on the output, and the frame samples that commit's texture. Occlusion by other windows is not checked, so a fully covered window still counts as shown.
- Discarded without a frame: unmap, minimise, destroy, a new role, a
buffer the compositor or renderer refused, a surface that can no longer be
drawn, a workspace switch or move that takes the window off the current
workspace, and commits made before the surface was mapped (including an
X11 window's commits before its map). A session lock needs no extra step: while
locked, every frame treats the surfaces the lock hides as not shown. At most
8 commits per surface wait; a faster client loses the oldest as
discarded. - Nested backend: the host compositor gives no presentation timing, so
tvis CLOCK_MONOTONIC when the frame was handed to the host (not first photon),flagsis 0,seqis 0 andrefreshis 0 (unknown). It is reported only after the swapchain image was actually presented. - KMS backend: advertised in client-content mode (not in
--first-light, which draws no clients). A frame is reported only after its atomic commit's page-flip event arrived; a cancelled or failed flip reports nothing, and its commits keep waiting.tvis the kernel's page-flip time. At startup comp asks DRM whether those stamps are CLOCK_MONOTONIC (DRM_CAP_TIMESTAMP_MONOTONIC). If they are not, each stamp is moved from CLOCK_REALTIME by the offset between the two clocks sampled when the event is read. If the capability query fails,tvis the time the event was read.- A MONOTONIC stamp may lie up to one refresh period in the future:
vblank-helper drivers stamp the start of scanout, which the flip event
can precede. Such a stamp is kept. A stamp further ahead is reported at
read time without
hw_clock; a second ahead, or three such flips in a row, and the capability is not trusted for the rest of the run. flagsarevsync(there are no async flips) andhw_completion(the flip-complete event), plushw_clockonly whentvis the kernel's own MONOTONIC stamp. Neverzero_copy: client buffers are composited into scanout buffers, not scanned out directly.- A device without vblank support (virtio-gpu, simpledrm; probed with
DRM_IOCTL_CRTC_GET_SEQUENCE, or seen as a sequence stuck at 0) completes flips on no vblank grid, so its frames carry onlyhw_completionandseq0. seqis the CRTC's vblank counter at the flip, extended per output beyond 32 bits so it only ever increases (across wraps, and across a counter that restarts on resume).refreshis the scanned-out mode's period,1e12 / refresh_mHzns.- Each flip is reported on the client output registered for its connector; a flip on an output that is not a client output resolves nothing. A flip that completed just before a VT switch is still reported.
- With several outputs, each flip carries the frame's content and the first presented one wins (content is not yet tracked per output); content-source costs are counted with the first flip only.
- A buffer the renderer fails to import is discarded as soon as the render world sees it, as on the nested backend.
- SHM clients: a surface counts as sampling its newest buffer once the GPU image is prepared. Bevy drops the old GPU image while a replacement is pending, so a pending upload is reported as not shown rather than stale.
- DMA-BUF clients: the import bridge replaces images in place and keeps
the previous texture while a replacement is pending or after it failed.
A frame therefore counts a commit as shown only when the bridge reports
that commit's import as the installed one. A failed import is
discarded, never presented.
In-process scene content (a Bevy plugin inside comp, not a Wayland client)
can be measured the same way: the plugin puts a ContentSource component on
its root entity and bumps ContentSourceFrame.revision for every content
update. Revisions shown in a presented frame count as presented, skipped ones
as discarded, and upload/damage costs are carried until a presented frame
reports them. Changing the id (re-inserting the component) re-registers the
source; a refused duplicate takes over the id when its holder goes.
ContentSource.output is reported as sources.<id>.output; the source is
measured on the output that reports the frame (nested has one).
Presentation statistics
Every window, output and content source is measured, whether or not the
client asks for feedback. The statistics follow content, not feedback
objects: an update is one buffer handed to the renderer (a window) or one
revision (a content source). A frame that shows a newer update presents it;
the updates it skipped count as discarded.
- Windows:
windows.s<id>.presentation.*. Subsurface updates count for their window; a frame that shows several surfaces of one window counts once. A new role (a newgeneration) starts from zero. - Shown, stalled, hidden. A frame that samples a newer update presents it. A visible window whose newest update is not sampled yet (a texture still uploading) is stalled: its run continues, and the eventual presentation measures the whole gap and counts the vblanks it missed. A frame that hides a window (minimised, off the output, locked away, or not in the frame at all) discards the updates it did not show, as the feedback protocol does; showing the same content again later is not a new presentation.
- Intervals are measured only between two presentations while the window stays shown, so a minimised or hidden stretch is not one long interval. An idle client's gaps are included, so the interval leaves describe cadence only for a client that updates steadily.
commit_to_presentruns from the moment the compositor publishes the buffer to the renderer (inside the client's commit, after it is accepted) to the frame that showed it. For a content source it starts when comp first sees the revision.missedcounts vblanks skipped while an update was waiting: for a fixed refreshR, a gap ofround(interval / R)vblanks counts the skipped ones at or after the moment the oldest waiting update was committed. An idle client that commits late misses nothing. With an unknown or variable refresh (nested)missedis null, never 0: unmeasured is not perfect.input_to_presentis the time from an injected input to the first presented update committed after it, within one second (a comp-side upper bound; a hide or reset drops the wait). The input goes to the window that owns the target surface, through subsurfaces and popups. A content source's update records it when it names the input'sinput_seq. These leaves stay null until thecomp.input.*injection verbs record marks; every injection has its own increasinginput_seq.- Outputs:
outputs.o_<slug>.presentation.*counts presented frames.flagsnames the newest frame's kind flags (vsync,hw_clock,hw_completion,zero_copy) andflags_maskis the same as a number; both are null before the first frame.refresh_usis null when the refresh is unknown (nested) or variable, never 0. - Single output for now: a frame report names one output and a surface is either shown by it or not; per-output accounting of a window shown on two outputs is not split yet.
- Rings keep the newest 512 samples;
_p50/_p99are nearest-rank percentiles over them, null without samples. - All times are CLOCK_MONOTONIC microseconds;
since_usis when counting started (the first update, registration, compositor start, or the last reset).
The presentation leaves, the whole sources subtree, input.seats.* and
input.last_origin are volatile:
get, list and describe serve them (describe says volatile: true),
but props.changed never reports them, so a watched client presenting at
60 Hz does not flood the topic. Row add and remove events carry no
presentation leaves either. Reads compute these leaves only for the paths
they can reach, and property-change diffs never compute them.
Counts saturate. Updates are assumed to be numbered one apart (a surface's buffers, a source's revisions); a jump counts every skipped number as discarded.
comp.window.stats {id, generation, samples?} returns the window's leaves
plus the newest samples (default and maximum 512) of intervals_us,
commit_to_present_us and input_to_present_us. {source, registration?, samples?} does the same for a content source and adds upload_bytes and
damage_px (per reported frame). {id, generation} and {source} are
mutually exclusive (invalid_value). comp.window.stats.reset takes the same
targets without samples, or no target to zero every window, output and
source; it replies {reset, since_us, ...}. Errors: the window errors of
comp.window.*, unknown_source, and stale_target with registration and
current when the source id was registered again. A session lock refuses the
window forms (locked); source reads and the global reset still work.
Each comp.window.* verb emits a comp_window_control trace record
(subject window id or 0, aux generation; detail 1 minimise / 2 restore /
3 focus / 4 raise / 5 close / 6 place / 7 wait and stats / 8 forced close
and stats reset / 9 workspace switch / 10 send to workspace — the 7/8
double use is renumbered in 0.60).
Content-source honesty limits: a source counts as presented when its entity
was visible in a presented frame; comp cannot tell whether the plugin's own
texture upload for that revision had finished. Flags and clock are the
frame's. Re-inserting the same id with another output keeps the original
registration and its output. Only the newest consumed_input is kept
between two reports.
Gate G1s: a build with the test-only content-source-probe feature adds a
nested quad registered as source probe that changes every frame for
COSMIX_CONTENT_SOURCE_PROBE_REVISIONS revisions (default 300), logs
COSMIX_CONTENT_SOURCE_PROBE DONE revisions=… upload_bytes=… damage_px=…,
holds for four seconds and despawns (… DESPAWNED).
XWayland
The xwayland cargo feature is in the default set: every default build
supervises one rootless Xwayland instance and acts as its X11 window
manager. The runtime control is the xwayland.enabled property described
above (startup-read, file-persisted per socket) with the
COSMIX_COMP_XWAYLAND environment variable as the launch-time override —
the cargo feature is no longer the switch. The read-only xwayland.display
leaf reports the X display (:N) of the ready generation — the Bus-side
equivalent of the per-socket DISPLAY descriptor file, published at the
same moment and null while no generation serves X clients (watch it to know
when xprop/xdotool can connect). Normal X11 windows become managed toplevels on the existing
scene, buffer, focus, stacking and server-side-decoration paths: association
(via the xwayland-shell serial handshake) creates the window's surface
record, the map grant makes it eligible, and its first committed buffer
renders through exactly the renderer path a Wayland toplevel uses — the
renderer has no X11 branch. Title/class metadata, focus (including the X
SetInputFocus/WM_TAKE_FOCUS half), interactive and client-requested
move/resize, maximise/minimise/fullscreen, EWMH state mirroring (including
the virtual-desktop trio _NET_NUMBER_OF_DESKTOPS / _NET_CURRENT_DESKTOP
/ _NET_WM_DESKTOP, described under Window switching and X11 placement),
close via WM_DELETE_WINDOW, and cross-protocol stacking in the normal
band are supported.
DISPLAY is never set globally. After the XWM owns WM_S0, the compositor
atomically publishes a mode-0600 per-socket descriptor at
$XDG_RUNTIME_DIR/cosmix-comp/<WAYLAND_DISPLAY>.xwayland.env containing
DISPLAY=:N and the XWayland generation; launchers read it at each launch
(the number can change when XWayland restarts) and pass DISPLAY
explicitly to each X client. The desktop's apps.launch does exactly that
(see the apps citizen in desktop-bus). comp itself spawns
no desktop applications, so setting DISPLAY in its own process would
reach nothing but Xwayland. A missing Xwayland binary or a
failed start degrades to a fully working native-Wayland compositor with a
warning. An unexpected XWayland death destroys that generation's windows,
removes the descriptor and arms a single 60-second one-shot restart backstop;
one retry credit exists, and only a generation that then survives five
minutes restores it. There is no readiness or liveness polling anywhere in
the path.
Override-redirect windows — X11 menus, context menus, tooltips and
combo-box drop-downs — render (X-2a): they get a surface record on the
ordinary renderer path at their own absolute client coordinates (negative
origins included; the compositor never places, clamps, configures, grants
or decorates them — including for _NET_WM_MOVERESIZE, whose interactive
move/resize is refused for override-redirect windows), stack in the
normal band and are raised to its top when mapped (not tethered to a
parent and not kept on top afterwards), and acquire none of the managed
behaviours — no focus candidacy, no minimise/maximise, no
foreign-toplevel export. Keyboard focus deliberately never moves to an
override-redirect window — focus arbitration itself refuses them, so
neither a click nor a touchscreen tap on one changes focus: the X
client's own grab machinery routes keys. In the property tree they appear
under surfaces.* with role:"x11-override-redirect" (managed X11
windows are "x11-toplevel"); neither appears in windows.*, which
remains the xdg-toplevel projection. A window that changes its
override-redirect flag between map cycles transitions by record
destruction and rebirth in both directions. Known scope edges: relative
sibling restacks are ignored, a menu overlapping a layer-shell panel
draws under the panel, and dismissal is client-owned — a click on a
pure-Wayland surface is invisible to the X grab and dismissal then
depends on the client's grab-break handling.
Still not supported, by design:
- Clipboard and primary selection are not bridged in either direction; selection access is refused at the XWM (X-2b).
- Drag-and-drop across the X11/Wayland boundary (X-3).
- HiDPI/fractional scaling for X11 clients: the X11 client scale is held at 1 and RandR primary-output changes are only logged (X-3).
- KMS qualification: X11 rendering is proven on the nested backend; the live-KMS proof is a later slice (X-3).
- Relative X restack requests (
Above/Below/Bottomsiblings) are refused with a log; the compositor scene stays the stacking authority and only raise-to-top is honoured.
Screen capture
SHM publication copies bounded chunks while continuing to dispatch clients. Queued chunks keep the protocol loop runnable: they never wait for unrelated input or surface commits between copies. Once publication finishes or is cancelled, the loop resumes its normal blocking wait. On x86-64 CPUs with SSE4.1, unrotated GPU readback uses aligned streaming loads into cached CPU memory to avoid slow generic copies from write-combining mappings. Other CPUs retain the portable copy path. Capture reservations account for source, staging and converted pixel storage. They are capped at 512 MiB per client and 1 GiB globally, allowing three active full 4K SHM captures plus one retiring request per client. Request count caps remain four per client and eight globally. This bounded memory allowance overlaps readback latency without bypassing reservation accounting.
Arc 4 provides wlr-screencopy-unstable-v1 output capture for existing
clients such as grim. A frame advertises one opaque XRGB8888 shared-memory
layout with an exact width * 4 stride. Whole-output and logically clipped
region requests are converted into displayed physical pixels; invalid or stale
outputs fail rather than falling back to another output. Plain copies are
force-presented, including on an otherwise idle output. Damage copies wait for
relevant base-scene damage, or cursor-only damage for a cursor-inclusive copy,
without forcing a frame. The bounded journal is manager-scoped; its baseline
advances only after ready, and history overflow conservatively reports the
full captured region.
Version 3 frames may additionally advertise a DMA-BUF destination after the
SHM buffer event and before buffer_done. The advertisement is immutable for
that frame and exists only for a whole-output request whose transform is
Normal, whose displayed and storage extents are equal, and whose underlying
render texture has an exact copy-compatible opaque format. Its modifiers are
the intersection of linux-dmabuf feedback and an exact Vulkan external-image
TRANSFER_DST import query at that extent. Versions 1 and 2 never receive the
DMA-BUF event; regions, transformed outputs and unsupported renderer states
remain SHM-only.
A submitted DMA-BUF must have exactly one plane and match the advertised
fourcc and extent. A kind, fourcc, extent or plane-count mismatch posts
invalid_buffer on the frame and never enters renderer admission. A modifier
outside the frame's stored transfer-destination set is instead an operational
miss and produces recoverable failed: linux-dmabuf feedback can legitimately
guide a client to a sampled-image modifier which this exact capture use cannot
import. Other operational misses—fd cloning, import/acquire, capacity,
deadline, cancellation, resize, completion or FOREIGN release—also produce one
failed. The compositor never sends damage or flags before discovering such a
failure.
A submitted wl_buffer does not carry the DRM device which allocated it.
Version 4 linux-dmabuf feedback steers compliant allocators to the renderer's
real main_device, and an import failure fails the frame, but the compositor
also fail-closes advertisement if bridge and feedback renderer identities ever
disagree. It does not pretend that Dmabuf::node() proves allocation identity. A
cross-device import which succeeds is a residual hardware risk and belongs to
the real-GBM gate.
Pixel completion and the matching output presentation form a two-part completion
latch: ready is sent only after both arrive for the same frame. For SHM the
completion half is mapped readback. For DMA-BUF, FOREIGN acquire is encoded in
the same wgpu command buffer as the copy; the worker waits for that exact
SubmissionIndex, submits the release barrier through wgpu's thread-safe queue,
then waits for the exact release submission. No capture ownership barrier uses
a raw queue submit or an infinite fence wait. Sampled-image ownership barriers
also enter wgpu-owned command buffers, so every renderer-queue submission has
one authority: wgpu::Queue. A single bounded completion
authority owns those destination jobs. It retries a transient 250 ms GPU wait
up to four bounded attempts, treats a full/disconnected job queue as terminal,
and never waits on the render or protocol thread. The shared terminal gate
closes the sole sender while excluding concurrent submissions, then drains the
definitively closed channel. Any terminal worker failure fails and safely
strands every live post-import job and clears future
screencopy DMA-BUF advertisements. Nested records
are bound to the exact acquired host window texture-view identity; a missing,
unconsumed or mismatched acquisition fails that capture instead of rebinding it
to a later presentation. Nested mode uses the completed host presentation time;
KMS retains the exact tv_sec/tv_usec from the
matching kernel page-flip event. The compositor bounds live and in-flight jobs, reserves
a byte budget before allocation, performs conversion away from the render and
protocol threads, then copies shared memory in bounded chunks across protocol
loop iterations. Each client may bind at most eight live screencopy managers,
with 64 live managers globally; exceeding either implementation limit is a fatal
protocol error on the new manager object. Cancellation is checked at the
protocol outbox, ECS queue, renderer-owned map and conversion worker. GPU map
errors become the frame's failed event. The byte
reservation remains charged until the last renderer-side request or
packed-result holder drops. Once the renderer submits its COPY_SRC readback,
the lease remains with the cancellable map job. Admission also requests a redraw, so a plain
copy is presented even when the output has no animation or other damage. Every
admitted copy has a five-second absolute request deadline: this is a deadline on
that one client operation, not a periodic compositor timer.
Main-world maintenance retires expired or cancelled admissions before rendering, including PNG requests deferred for output readiness. It preserves live requests and their original deadlines, and leaves submitted jobs with their completion workers. Read-only demand checks match the capture source and KMS generation; waiting for damage or holding a PNG completion slot does not by itself require a render. The nested redirection path uses these checks to avoid preparing a target for stale work; live KMS idle admission uses the same non-consuming checks.
Completion does not depend on a later render tick. The retirement worker sends
its result directly through the calloop-backed protocol command channel, which
wakes an idle protocol loop; the destination can therefore reach ready on a
static desktop without polling or a redraw timer. Teardown first puts every
live job into failed/strand mode and closes the sole job sender. It performs a
non-blocking worker acknowledgement check: an already-finished worker is
joined, while an unacknowledged worker is detached. If a driver call never
returns, that detached worker retains the in-flight job and all queued jobs,
including every import, buffer token and reporter, until it returns or the
process exits. The retained set is bounded by MAX_IN_FLIGHT_CAPTURES; renderer
teardown itself does not wait.
A pre-import failure releases the retained buffer token immediately. After an
acquire/copy submission, only successful copy retirement plus successful
FOREIGN hand-back may release it; an unprovable hand-back strands both import
and token. fail_capture cancels publication but cannot release that
renderer-owned half early. Sending wl_buffer.release after the client has
already destroyed its object relies on Wayland's inert-object send behaviour.
The existing SIGUSR1/evidence PNG path shares the renderer-owned RGBA snapshot, deadline, cancellation sweep and conversion worker with wire capture. The PNG and wire consumers then create their own packed BGRA buffers for their distinct outputs. PNG capture retains its filename, atomic-rename and cadence contract; an unavailable output or an encode/write task which starts after the deadline releases the one-batch-in-flight latch. A genuinely blocked filesystem write cannot be cancelled safely and remains outside this deadline guarantee.
PNG requests which reach KMS before their camera or frame token is ready are retained for a later render frame within the original deadline. This does not extend the deadline or add a timer. Terminal PNG failures emit a structured reason, target and output path so a missing evidence image can be diagnosed; the batch latch is released exactly once on completion or failure. For unrotated output, pixel normalisation bulk-copies each mapped row into CPU memory before any BGRA channel swizzle. This avoids slow scalar reads from mapped GPU memory at high resolutions without extending capture deadlines.
The per-destination fd duplication and Vulkan image creation/bind syscalls run
on the render thread. This is intentionally retained for S-2: admission is
bounded to eight destinations per render batch, and moving import into the
worker would break same-frame copy-out. Hardware-gate runs should continue to
record this bounded syscall cost. Sampled-image cleanup now retains FOREIGN
release batches across frames and checks their exact submission with a zero
timeout. Acquire remains queue-ordered ahead of sampling. The retirement worker
also polls without holding wgpu's fence lock across GPU progress waits. See the
asynchronous ownership contract.
Flag either failed to release DMA-BUF queue ownership or
DMA-BUF release completion unproven: submission failure or an unproved release
after the 250 ms deadline strands the backing and withholds wl_buffer.release.
That cached image cannot be reused; another use requires a fresh import.
Live renderer reconstruction currently rebuilds the renderer, capture bridge,
advertisement registry and retirement worker together, leaving DMA-BUF
advertisement empty until the new render world republishes it. This whole
restart path is structurally pinned by run_live_render_pump, but remains an
explicit untested end-to-end path because the regression would require the
forbidden real-seat live pump.
overlay_cursor=0 selects the cursor-free base. Every non-zero value selects an
inclusive copy using the retained default, chrome or client cursor asset with
its hotspot, clipping and hidden state. Nested mode copies its cursor-free scene
to the host swapchain and composites the capture-only overlay into a separate
temporary target, avoiding a doubled host cursor. The production cursor camera
renders into an independent transparent target, so it cannot clear or swap the
scene camera's base texture. SHM and imported DMA-BUF cursor assets remain
retained and are sampled from that target on the GPU in both nested and KMS
capture. KMS base copies precede the GPU overlay into scan-out and inclusive
copies follow it.
Nested redirect and cursor-composed textures use an unsuffixed BGRA8/RGBA8 base with the matching sRGB view format. Rendering therefore keeps the sRGB view, while DMA-BUF copies compare and copy the exact linear base format used by the destination import.
Capture is deliberately default-open, including while the session is locked. Lock and unlock change the capture epoch: stale work fails, while newly admitted work captures the currently displayed lock surface or compositor-owned black fallback. This is an agentic desktop policy rather than a portal permission prompt.
KMS copies select the exact Ready OutputKey and generation, copy out within
that frame without retaining a slot or storing a destination token in the
scan-out pool, and latch completion against its
acquisition token and kernel page-flip timestamp. Pause, unplug, generation
replacement, cancellation and map failure fail the affected one-shot rather
than returning another output or stale pixels. --first-light keeps the same
capture feed and completion path while ignoring client scene content; every
changed animation frame marks full-output damage, so copy_with_damage wakes.
The wlr protocol is a compatibility surface; the planned
ext-image-copy-capture-v1 implementation will become another consumer of the
same capture service.
Frames are physical pixels, on nested too. A whole-output screencopy frame
is the output's physical buffer: at scale 2.5 a 320x240 logical output
advertises and delivers 800x600, and a logical region is projected to
physical pixels. On nested, the extent is the host window's own swapchain
size, not the truncated logical size times the scale (a 2762-pixel-wide host
at 2.5 is 1104.8 logical, reported as 1104). The nested scene itself is laid
out on that truncated 1104-wide canvas, so the last physical pixel or two at
the right or bottom of such a frame are background that no client can draw
into. The strip is cosmetic and is not a capture error. What a client does with the frame
is its own business. grim composes its image at the output's integer
wl_output.scale, which is 1 on nested and ceil(scale) on KMS, so by
default it downsamples a nested frame to the logical size and upsamples a KMS
one. A pixel gate that must see what the panel shows runs
grim -s <exact scale>, for example grim -s 2.5, which composes at 1:1.
An earlier note held that nested screencopy returned a LOGICAL-size frame and
that nested pixel gates were therefore vacuous. That premise was wrong. The
1105x622 images it cited were grim's default composition. comp itself has no
Bus capture verb: capture.screenshot belongs to the separate
cosmix-capture screencopy client, and a native comp.capture.frame is
still a proposal.
The automated nested acceptance gate uses grim; the
cosmix-screencopy-probe binary is a deadline-bounded manual diagnostic for the
advertised layout, non-zero SHM offset, guard bytes and non-black pixels. Its
--dmabuf --drm-node PATH mode waits for buffer_done, allocates an advertised
modifier with GBM, submits that destination, maps it only after ready, and
prints the presentation timestamp plus a content checksum.
DMA-BUF advertisements are republished from live view targets after every
output registration/re-registration and generation change. Reconstructing a
renderer reconstructs its bridge, completion worker and advertisement registry;
until that replacement has published fresh capabilities, new frames advertise
SHM only.
Automated tests cover readback, transforms, ordering and damage. A real Vulkan
render-attachment gate uses the production GPU cursor-composite pass, reads back
base and inclusive bytes, compares both with byte-exact references, and proves
that channel-swap and shifted-hotspot mutants fail. These gates prefer a Vulkan
fallback adapter. With COSMIX_REQUIRE_FALLBACK_ADAPTER=1, absence of one fails
the test (the CI rule); otherwise the gate runs on an available Vulkan adapter
and prints one line naming the adapter actually used. Physical driver behaviour,
real kernel page-flip clock provenance and end-to-end grim -o on KMS remain
manual-hardware-only checks. The automated Vulkan equivalence gate uses an
ordinary Vulkan COPY_DST texture; it does not prove real GBM allocation,
DMA-BUF import, cross-device behaviour or FOREIGN ownership on a physical
driver. Those are explicitly hardware-gated.
Layer surfaces stack in the protocol order: Background, Bottom, normal XDG toplevels and popups, Top, then Overlay. Raising a surface changes its order only within its stratum. A layer popup stays in its parent's stratum, including when the parent changes layer. Session-lock surfaces use a sixth Lock stratum above Overlay. An opaque compositor-owned black element sits below Lock and above every client stratum while a lock is active.
keyboard_interactivity=None layers receive pointer and touch input but never
take keyboard focus or raise; their popups can use pointer/touch grabs but not a
keyboard grab. OnDemand layers take focus on a pointer press or first touch.
The highest stacked mapped Exclusive layer latches keyboard focus until it
unmaps, is destroyed, or commits a non-Exclusive policy. Normal-window clicks
may still raise their window while that latch is active. Installing or
transferring an Exclusive latch, changing interactivity, or otherwise moving
keyboard focus dismisses any active XDG popup keyboard grab before the arbiter
sets its chosen focus. While an Exclusive latch is held, an unrelated popup
grab request is denied with popup_done, because xdg-shell requires the
topmost grabbing popup to own keyboard focus; a popup belonging to the latched
layer may grab normally. A latched layer that holds the keyboard (with no
popup keyboard grab active) and commits
OnDemand keeps the keyboard: the latch ends, the focus stays, and from then
on a click elsewhere moves it like any OnDemand layer (Quoin's panels ask
for the grab only until it lands). When a focused layer stops being eligible, focus
moves to the next Exclusive layer, otherwise to the highest visible normal
toplevel, or to no surface when neither exists. Keyboard focus inside an
Exclusive layer's own active popup grab satisfies the layer's latch: ordinary
panel redraws neither pull focus back to the layer root nor dismiss its menu.
Committed wl_surface input regions participate in hit testing for every
surface role. An empty region makes a panel click-through, and regions are
clipped to the surface's presented buffer bounds. A committed region change,
map or unmap retargets a stationary pointer after its complete Smithay surface
transaction applies. Committed stack-band, subsurface-order and LayerMap
geometry changes use the same retargeting path. Synchronized descendants
therefore produce one atomic leave/enter transition and one hit test for the
transaction, never intermediate targets from partially applied sibling state.
If rearrangement moves the currently focused surface without changing its
identity, the compositor sends one motion with the corrected surface-local
coordinates. Output resize and KMS topology changes batch layer arrangement,
usable-area derivation and window clamping before that single reconciliation.
Up to 256 region operations are retained exactly; larger regions use their
added rectangles' bounding box so protocol-thread hit testing stays bounded.
Exclusive zones reduce the usable output rectangle. New-window cascade origins, maximised sizes and normal restore clamping all use that rectangle and are recalculated after layer map, unmap or destruction and after output size or KMS topology changes. Layers are arranged before maximised windows are reconfigured, so their wire configure uses the current usable rectangle.
Layer shell honours the requested output, including an explicit
wl_output. A request with no output uses the backend's default output; if no
output exists, the layer surface is closed and is never mapped.
Session locking
An accepted lock enters Locking, immediately removes ordinary client
content from the renderer roster and installs the opaque black security scene.
The compositor sends locked only after the nested renderer acquires a
swapchain image containing that epoch, submits the frame, calls the winit/wgpu
present path, and waits for the submitted GPU work to complete. A Bevy schedule
turn is not presentation evidence: a minimised, occluded, skipped or failed
frame leaves the epoch pending and locked withheld. The blank itself satisfies
the barrier, so a slow or absent client lock surface never delays a frame that
is actually presented.
Live KMS uses the corresponding atomic-display authority boundary. The renderer
captures the security epoch when it acquires the output image, carries the exact
output key and render generation through the submission, and acknowledges it
only when atomic presentation returns Displayed. OutputReady, an enqueued
frame, a frame-clock pulse, cancellation and a failed commit are not security
presentation evidence. Every output in the epoch must report its exact current
generation before locked is sent.
Lock ownership lives on the Wayland protocol thread and is independent of DRM,
seat and VT authority. Losing authority never unlocks: Locking, Locked and
OrphanedLocked survive the pause. Resume creates a fresh epoch, installs the
opaque lock scene before the output becomes usable, and withholds client frame
callbacks and physical input until that epoch is displayed. Before selecting
its resume policy, the coordinator asks the protocol thread whether Locking,
Locked or OrphanedLocked is active. Any active lock disables seamless
resume on every output: the retained pre-pause framebuffer is discarded rather
than page-flipped, and the first submitted frame after authority returns is a
freshly rendered opaque lock surface or black fallback. A lock output with no
current lock surface remains black. Unlock received while paused changes the
protocol state exactly once but keeps the black scene and input gate in place;
after authority returns, the compositor displays an ordinary-scene epoch before
restoring focus, input and normal client delivery.
Keys, pointer buttons, scroll sequences and touches held by the lock scene are
ended before its surfaces are retired. Input arriving behind the display gate
is quarantined until its matching physical release, so no held gesture or
release crosses into the restored ordinary focus.
The live KMS Ctrl-Alt-F1 through Ctrl-Alt-F12 bindings are evaluated before
the presentation gate. They are compositor-only and never reach a Wayland
client; every other key, pointer and touch event remains gated until secure
presentation completes. Gated key presses advance Smithay's private XKB state
so the VT chord can be recognised. If the presentation barrier opens before a
matching release, quarantine still feeds that release through the same
intercepted XKB path while suppressing client delivery; pressed keys and
modifiers therefore cannot leak across the barrier. Physical key releases
matching a synthetic pause release are quarantined by device and keycode, and
pause clears both physical and suppressed touch-slot state.
An output removed or replaced while locked does not change lock ownership. Its
exact Smithay (output, lock-surface, lock) registration is retired, and a new
output starts with the compositor-owned black scene until the lock client maps a
new surface through the normal wl_output lifecycle. The KMS transition logs
stable harness markers: session-lock-kms-resume-blank-first,
session-lock-kms-normal-exposure-held,
session-lock-kms-{initial,resume,unlock}-epoch-displayed, and
session-lock-kms-normal-exposure-restored. The actual display path also emits
exactly one marker for each output's first successful flip after resume:
session-lock-kms-resume-first-flip scene=<lock|blank|retained|client> output=<key> epoch=<presentation-epoch|none> generation=<generation>. A locked
resume is valid only with lock or blank and the epoch announced by that
resume; retained, client, an absent epoch or a mismatched epoch are
fail-closed evidence of exposure.
The private desk_vt_run.mix --arm session-lock-vt arm brackets a real VT run
with the existing recovery timer and snapshots. It launches the release
cosmix-lock-probe, which creates a solid SHM lock surface for each output and
prints COSMIX_LOCK_PROBE checkpoint=... records, then proves initial lock,
VT away/back blank-first resume, delayed normal exposure and displayed-epoch
unlock. It requires the safe per-output first-flip marker and requests a KMS
texture-view PNG through the compositor's SIGUSR1 frame-capture path; the image
must contain only the solid lock colour or black fallback. The arm scopes
resume markers after a journal cursor captured while away and matches
the resume-start epoch as well as the output. It accepts the probe's terminal
unlock markers after either an active observation or an exact successful exit;
mid-run lock markers still require the probe to be alive. The arm is
intentionally a manual hardware gate; ordinary development only builds the
helper and lints the script.
Each physical output accepts one lock surface for the current generation. Its first configure is immediate and exactly matches the logical output; a buffer may map only after acknowledging the current configure and must have the acknowledged dimensions. Every responding commit is checked against its effective buffer: an empty first commit is a null-buffer error, while an empty commit after resize revalidates the retained buffer against the new size. A surface with any earlier attach or commit history cannot become a lock surface. Lock surfaces and their subsurface trees receive input above any Exclusive layer. Ordinary surfaces may continue committing, but receive no renderer publication, frame callbacks, focus or input until unlock. Lock entry also purges ordinary deltas already queued for the renderer. Unlock or a pre-locked abort sends complete upserts for every presentable surface, so unchanged static windows are recreated as well as listed in the roster.
Entering Locking dismisses popups, ends pointer and drag-and-drop grabs,
cancels touch, reconciles pressed keys, hides the client cursor, clears
data-device focus and suppresses ordinary compositor bindings. The KMS VT
switch binding remains the sole exception. Foreign-toplevel handles close at
entry and neither update nor replay while locked; unlock reannounces mapped
toplevels with the same identifiers. Physical input still resets idle
notifications. If the owner dies before locked, locking aborts. If it dies
after locked, the compositor becomes orphan-locked, removes the dead lock
surfaces and retains the opaque blank while swallowing input. Blank-only areas
and holes in a lock surface's input region swallow pointer, keyboard and touch;
the compositor does not scan hidden window chrome there, start move/resize or
caption-button grabs, dispatch close, or reveal a chrome cursor.
The global is deliberately advertised to every connected client: any client
may request the lock. The accepted generation is nevertheless bound to the
exact ext_session_lock_v1 resource, so a rejected object from the same client
cannot create surfaces or unlock it. Destroying the accepted resource during
Locking aborts safely. An orphaned lock never auto-unlocks and persists until
the compositor exits.
Explicit-sync fault policy
A permanently faulted explicit-sync retirement pipeline restarts the
compositor loudly instead of degrading the session (since 0.57.0). Previously
a retirement fault withdrew the linux-drm-syncobj-v1 global and the session
kept running on implicit sync — DMA-BUF clients could then be scanned out
mid-write, which shows as constant flicker, and nothing surfaced the
degradation. Now every fault path (asynchronous worker report, worker channel
closure, and the synchronous request-time fault) withdraws the global for the
brief teardown window, then requests a RuntimeFailure shutdown: under KMS
the process exits non-zero for its supervisor to restart with explicit sync
intact; nested, the scene fails loudly.
Transient stalls get bounded patience before that happens: the retirement worker grants each batch one 3 s wait on its captured submission index (sized to outlast a GPU engine-reset recovery) before a timeout is terminal — a single fixed-target wait, not a retry loop, because re-invoking the wait submits a fresh empty batch and chases a moving target. Structural faults (adapter panic, wait failure) are terminal immediately.
Two read-only props leaves expose the state over the Bus:
info.explicit_sync_advertised (the protocol global is currently offered to
clients) and info.explicit_sync_healthy (the retirement pipeline has not
permanently faulted). Reads are computed from live state per request. They are
not covered by comp.props.watch diffs: after the loud-restart policy a
fault's unhealthy window is at most one dispatch cycle, so the leaves are
process-lifetime constants in practice — poll them, don't watch them.
Vendored changes
Smithay's SeatState::new_wl_seat_with_filter filters seat-global visibility
per client through GlobalDispatch::can_view. The existing new_wl_seat
delegates with an always-true predicate.
The vendored Smithay layer-surface handle has an additive reset_after_unmap
helper so the compositor can clear Smithay's private configure queue while
applying layer-shell's protocol-mandated post-unmap state reset. Smithay's
CompositorHandler also has an additive transaction_applied callback so
pointer hit testing can observe one complete synchronized-surface transaction
instead of each surface's intermediate state. Its pointer handle has one
additive no-focus-restore grab teardown used when a grabbed surface disappears
inside that transaction, preventing stale cached focus from being replayed
before the final hit test.
Smithay's foreign-toplevel list has one additive constructor accepting a
compositor-provided identifier. This lets the identifier include Cosmix's
surface identity, mapping generation and an unpredictable per-compositor
instance nonce while protocol dispatch and replay remain entirely delegated
to Smithay.
Smithay's session-lock surface has an additive serial-returning configure
method. This lets the common Cosmix configure gate record the exact immediate
and resize serials while Smithay's default initial send observes that the
pending state was already consumed and does not duplicate it. Its session-lock
handler also has additive originating-lock, lock-object destruction,
lock-surface destruction and construction-history hooks. These bind surface
creation and Locking lifetime to the accepted resource while letting Cosmix's
attach/commit ledger enforce AlreadyConstructed.
The session-lock registry also exposes a narrow exact-surface retirement helper
for KMS output replacement; it removes only the originating protocol object and
does not alter the accepted lock generation.
Smithay's xdg_wm_base.get_xdg_surface guard is narrowed. It still refuses
a wl_surface with any non-xdg role, and one that a live xdg_surface
still wraps. Once the previous xdg_surface is destroyed, a fresh one for
the same wl_surface is accepted and may take the same xdg role again.
Live wrappers are tracked per wl_surface, so a second xdg_wm_base
binding cannot bypass the guard. The same request also enforces
xdg-shell's rule that the wl_surface has no buffer attached or committed:
either posts xdg_wm_base.invalid_surface_state. A client re-wrapping a
surface must first commit a NULL buffer. XdgShellHandler gained an
additive surface_has_buffer hook for this, because comp consumes
committed buffers out of Smithay's surface state and answers from its own
record instead.
Smithay's X11Surface has one additive test-support setter
(set_wl_surface_offline) that assigns the associated wl_surface directly.
The xwayland-shell serial handshake owns the real association; the setter
exists only so the compositor's deterministic tests can fabricate offline
X11 surfaces whose focus forwarding and metadata lookups still reach a real
wl_surface.
The vendored XWM also carries the EWMH virtual-desktop additions (comp
0.59.0): the _NET_NUMBER_OF_DESKTOPS, _NET_CURRENT_DESKTOP and
_NET_WM_DESKTOP atoms (interned, advertised in _NET_SUPPORTED, the root
pair written as 1/0 at WM start), X11Wm::set_number_of_desktops /
X11Wm::set_current_desktop for the root pair, X11Surface::set_desktop
for the per-window property (with a desktop() read-back of the last value
asked for, so the offline tests can see it through a dead connection), and
an XwmHandler::desktop_request callback dispatched for 32-bit
_NET_WM_DESKTOP client messages and an XwmHandler::current_desktop_request
callback for 32-bit _NET_CURRENT_DESKTOP root messages, both no-ops by
default: the compositor owns the desktop model and decides whether a
request becomes a move or a switch.
The vendored session-lock implementation also carries five marked fixes:
- invalid
unlock_and_destroyreturns after postingInvalidUnlock, so a rejected object cannot fall through to the compositor's unlock handler; - a valid
unlock_and_destroyconsumes its locked state before calling the compositor, so the unlock transition can occur only once even while visible restoration is deferred across a VT pause; AlreadyConstructedvalidation happens before the output is inserted into Smithay's duplicate registry, preventing failed constructions from leaking entries; and- each output registration records its owning lock-surface and lock object, so abort retires only that generation and a stale destructor cannot erase a newer generation's registration; and
- commit validation tracks the effective retained buffer, rejecting null first commits and stale-sized buffers after a newly acknowledged resize.
The associated output resource is retained until lock-surface destruction, valid unlock or generation abort so Smithay's resource duplicate registry and Cosmix's physical-output ownership map are both released while the compositor-owned blank remains.
The vendored wgpu-core carries a hand-backport of upstream wgpu 385520f7
("re-read the fence before the queue-empty assert in Device::maintain").
Without it, a timed-out fence wait racing another thread's queue-drain
panicked the retirement worker on a defensive assert — the trigger for the
explicit-sync fault policy above firing every session. Provenance and the
inherited caveats are in src/desktop/vendor/README.md.