Expand description
Host-capture mode: capture/inject as a client of an external compositor.
Host-capture mode: pixelflux as a CLIENT of an external Wayland compositor
(e.g. labwc running WLR_BACKENDS=headless), inverting the nested topology.
The host compositor owns the session — one seat, one selection, one screen
model — and pixelflux captures and injects as a privileged-protocol client:
frames via ext_image_copy_capture_v1 where the host offers it (wlroots
0.19+, KWin 6.2+, cosmic) and zwlr_screencopy_v1 (v3 copy_with_damage)
everywhere else — both damage-gated, so idle screens cost nothing
(PIXELFLUX_HOST_CAPTURE=zwlr forces the fallback); input over libei where
the backend grants an EIS socket, else a persistent zwp_virtual_keyboard_v1
device carrying selkies’ own keymap text with zwlr_virtual_pointer_v1, else
kernel uinput devices where /dev/uinput is writable. Zero-copy is preserved by
allocating the capture buffers from pixelflux’s OWN GBM device (render node —
no privileges): the compositor blits straight into the dmabufs the encoder
imports, so no CPU ever touches a frame. A host that cannot import our
dmabufs (other GPU, software renderer) and any run without a GPU degrade to
wl_shm buffers feeding the CPU encode pool.
Every host wl_output maps to one selkies display id, in registry order: a
control thread on the primary connection owns wlr-output-management and
applies the whole layout (per-output custom modes plus row positions) as one
atomic configuration, while each output gets its own capture connection and
thread. Input proxies are called from the calloop thread directly
(wayland-client proxies are thread-safe); pointer coordinates arrive in the
same union-layout space selkies uses, so the virtual-pointer extent is the
layout’s bounding box. All capture-side waits poll a wake pipe next to the
Wayland socket, so a resize or teardown lands immediately even while the
thread is parked in a copy_with_damage wait on a static screen.
A host without those protocols (GNOME, KDE before ext-image-copy-capture) is captured
through xdg-desktop-portal instead: one RemoteDesktop session hands out a PipeWire stream per
monitor and takes the input the seat lacks a virtual device for (portal, pwcapture).
Each rung is chosen per capability, ordered by measured delivery cost: a socket write to the
compositor (libei, then the virtual-input protocols) runs about 4 us, a uinput write reaching
evdev about 12 us, and a portal Notify* D-Bus call about 2 ms, so the kernel serves as the
universal fallback rather than the first choice, and the portal comes last. A uinput pointer
declares a normalized absolute axis, since the layout extent changes while a device’s range
cannot. Frames take the Wayland protocol where the host offers it and the portal where it does
not — so a
KWin that serves ext-image-copy-capture but no virtual pointer still gets its pointer from
the portal. Portal keys go by keysym, resolved from selkies’ own keymap, so the host applies
its layout to the symbol rather than to a keycode from another layout. The compositor owns
the portal buffers: a dmabuf frame is imported where it lies and a memfd frame is read in
place, and the portal stream is asked for at most the capture’s rate so no frame is
produced only to be dropped.
Macros§
Structs§
- Capture
State 🔒 - Ctrl
State 🔒 - Frame
Sink 🔒 - The capture thread’s end of one output’s frame queue: the frames, an exact count of those queued and not yet taken (so a wake that the timer has already served is recognized as such), and the calloop wake that follows every frame.
- Host
CpuFrame - Borrow-by-Arc view of a software frame still sitting in its shm mapping.
- Host
Frame - A frame the compositor finished blitting, ready for the encoder.
- Host
Keyboard 🔒State - Host
Session - The calloop-side session: input proxies plus one capture handle per output.
- Layout
Ledger 🔒 - Outcome ledger for layout requests: every Apply carries an epoch, and the
control thread records whether the newest one it processed was realized by
the host. A request is answered once
decidedhas caught up to its epoch; a newer request subsumes an older one (the layout map is cumulative), so the newest verdict stands for every epoch at or below it. Polled, never waited on: the session side reads it from its own loop. - Layout
Slot 🔒 - One display’s slice of the union layout, as selkies configured it. Slots are keyed by selkies display id (‘display2’ is id 2 — ids are sparse); active ids map onto host outputs by ascending rank.
- Output
Geom 🔒 - One host output’s placement, for pairing portal streams with outputs.
- Output
Handle 🔒 - One host output’s calloop-side handle: control channel, wake pipe, frames.
- Portal
Ctl 🔒 - Session-wide portal state: the session thread opens (and, when the cursor mode changes, reopens) the portal session; the outputs and the input methods read the live one.
- Portal
Inner 🔒 - Portal
Live 🔒 - A live portal session: the D-Bus session, and the PipeWire connection its streams live on when frames come through it.
- Portal
Point 🔒 - Where a union-layout point lands on the portal stream under it (
HostSession::portal_target). - Serialized
Mods 🔒 - Want 🔒
- What a capture thread is currently aiming at: the encoder’s dimensions, the buffer type its consumer can take, and whether the host paints the cursor in. A size or buffer-type change forces a slot renegotiation; a cursor change reopens the ext session, whose option is fixed at creation.
Enums§
- Ctl 🔒
- What a control-channel drain decided while a capture wait was in progress.
- CtrlMsg 🔒
- ExtOutcome 🔒
- How the ext capture loop ended:
Finishedis a normal teardown (the thread is done);Unavailablemeans it could not get going, so wlr-screencopy should run. - Pump 🔒
- Slot
Buffer 🔒 - ToHost 🔒
Constants§
- DETACHED 🔒
- The generation of a retained frame the session copied out of its buffer
(
HostSession::detach_retained): no capture’s, so releasing it reaches no slot. - KEYMAP_
FORMAT_ 🔒XKB_ V1 - LAYOUT_
DEADLINE - Bound for one layout application on the control thread: a host that has not answered by then is settled without it, and the session follows whatever mode the host announces.
- SCROLL_
PX_ 🔒PER_ NOTCH - Pixels of a finger’s scroll per wheel notch where only notches exist (the uinput wheels): the size Selkies’ page takes for a touchpad’s notch.
- SLOTS 🔒
- Frames in flight between a capture thread and the encode tick. Two slots: one being blitted by the compositor while the previous one is encoded.
Functions§
- alloc_
cpu_ 🔒slot - An shm-backed capture slot in a memfd pool the CPU consumer reads in place.
- alloc_
gpu_ 🔒slot - A dmabuf-backed capture slot: a GBM buffer object from pixelflux’s render node,
exported and wrapped in a host
wl_buffer.modifiersnarrows the allocation to what the host advertised; empty leaves the driver’s choice, which the host learns through the params anyway.Ok(None)means the host refused the import (wrong device, unsupported modifier): the caller degrades to shm slots instead of dying to the protocol errorcreate_immedwould raise. - apply_
layout 🔒 - Ask the host (wlr-output-management) to give every active output its wanted mode and
layout position in one atomic configuration. Retries across a
canceled(stale serial). Returns whether the host said it applied the layout:falsefor a refusal, no answer byLAYOUT_DEADLINE, or no manager at all (KWin offers only its own kde_output_management protocol). Either answer settles the request, and the session owner then re-sizes its capture to the mode the host announces, so capture follows the host instead of gating forever on a size it will never produce. - assign_
streams 🔒 - Which portal stream backs each output rank: the stream announcing the output’s position, else the one announcing its logical size when that is unambiguous, else the stream at the same index (a backend that sends no geometry lists monitors in its output order).
- capture_
loop 🔒 - capture_
loop_ 🔒ext - ext-image-copy-capture capture loop: one persistent session per output. The
compositor holds each capture until the content actually changes, so a static
screen costs nothing — the property
copy_with_damageprovides on the wlr path. Frames land in the same slot machinery andHostFrameshape as that path. - control_
loop 🔒 - convert_
shm_ 🔒row - Convert one source row to a BGRA destination row per
(src_bpp, swap_rb)fromshm_src_layout: a 4-byte source either copies straight or swaps R/B keeping alpha, a 3-byte source fills opaque alpha and swaps R/B only when the layout calls for it. - drain_
ctl 🔒 - Drain the control channel from inside a capture wait: releases are applied (generation-checked), and the newest Start/Idle decides the wait’s fate.
- fourcc_
to_ 🔒gbm - fps_
milli 🔒 - A capture rate as the millihertz a portal stream is asked to hold.
- nearest_
slot 🔒 - The active slot holding
(x, y), else the one nearest to it, by rank. - now_ns 🔒
- CLOCK_MONOTONIC in nanoseconds, the clock PipeWire stamps frames with.
- open_
eis 🔒 - Ask the portal for an EIS socket and bring an injector up on it, or
Nonewhen the backend refusesConnectToEIS(leaving theNotify*path live) or the EIS handshake binds nothing. - open_
ext_ 🔒session - Open an ext capture session on
output, with or without the host painting its cursor into the frames, and wait for its first buffer constraints: the point that decides whether this protocol works here at all. A session’s cursor option is fixed at creation, so a toggle closes the session and opens another through here. - output_
order_ 🔒key - Natural sort key for an output name: text prefix plus trailing number, so HEADLESS-2 orders before HEADLESS-10. Unnamed outputs keep registry order after every named one.
- portal_
capture_ 🔒loop - The capture thread of a portal-fed output: it holds the PipeWire stream of the output’s portal stream for as long as a Start aims at it, re-offering the stream’s configuration on every change, returning released buffers, and following the portal session across reopenings. Frames themselves are published by the stream’s own thread.
- portal_
thread 🔒 - The portal thread: opens the session once the first Start has decided the cursor mode, reopens it (restoring with the token the backend gave) whenever that mode changes, and ends with the host session.
- pump_
ctrl 🔒 - One bounded dispatch step for the control connection (false = connection died).
- pump_
until 🔒 - Dispatch events until
done(state)holds, waiting on the Wayland socket and the wake pipe together so control messages interrupt any capture wait. The predicate is re-checked after every dispatch, so a wait whose condition was satisfied by already-queued events returns without ever blocking. - scroll_
notches 🔒 - The whole notches in
carry + deltapixels, the fraction left incarry. - shm_
src_ 🔒layout - Source bytes-per-pixel and whether R and B swap to reach BGRA, for one announced wl_shm format. The consumers downstream all read BGRA (byte order B,G,R,A), so a format is mapped by where its red and blue land in memory under the little-endian byte order wayland.xml specifies:
- stream_
point 🔒 - A union-layout point in the logical space of the stream behind
slot: the slot captures its output at physical sizewant, the portal addresses it at logical sizelogical. - trace 🔒
PIXELFLUX_HOST_TRACEset: log every host frame’s age on arrival and once encoded.- wait_
gpu_ done - Wait, bounded, until the GPU work the compositor queued on a host dmabuf has finished: the CUDA import reads the buffer without honoring its implicit fences, so a frame encoded the instant it was announced could otherwise carry a half-blitted image. Drivers that attach no fence return at once.