Expand description
DRI3 zero-copy X11 capture: the X server blits the screen into GPU buffers this process allocated, and the hardware encoder reads them where they lie.
It serves a server whose screen lives on the GPU (XLibre’s Xvfb with glamor, an Xorg on a
DRM driver), where the XShm path in super would read the screen back into host memory
for the hardware session to upload again. A small pool of dmabufs is allocated through GBM
on the render node the server draws with, each is wrapped in an X pixmap
(PixmapFromBuffers), and every frame is one CopyArea from the root window into the next
pixmap, which glamor runs as a GPU-to-GPU blit. The encoder then imports the dmabuf in place,
through the same FrameEncoder::encode_dmabuf the Wayland zero-copy path uses, so a frame
is never touched by the CPU between the screen and the bitstream.
Completion is waited for with a one-pixel GetImage of the destination pixmap: the server
answers only after the blit it ordered before has landed, so the encoder never reads a
half-copied frame, and no fence object or implicit-sync assumption is involved.
Where the server offers SELKIES-SEMAPHORE (the images’ Xvfb) and the encoder can wait on a GPU semaphore (NVENC), the server signals one the encoder made after each blit instead, and the encode is queued at once behind a wait on it. Under a GPU-bound client each GPU job that has to wait for the CPU to submit it waits a draw of that client for its turn, so the GetImage cost two draws a frame where the semaphore costs one: an uncapped 72 fps client streamed at 36 fps with the GetImage. A checked Signal request is the round trip the wait follows, which answers once the server has queued the signal, not once the GPU ran it: a wait queued before the server’s signal lost its turn as well.
Nothing here reads pixels, so what stands in for the XShm path’s per-stripe content hashing is
the Damage extension on the root window: the server’s report is both what says a frame is worth
encoding and what ends the wait for one, within the pacing crate::pace keeps, so a change is
published as it lands rather than on the next tick. Its region says how much changed, so a
blinking caret stays a small change and a still screen is cleaned up around it. The XFixes
cursor is composited by the server through Render, on the GPU, so the capture_cursor overlay
costs no readback either; the server keeps its own software cursor out of a copy from the
screen, so the cursor is never drawn twice.
Everything that can be checked is checked before a frame is delivered, and the path is declined otherwise: a codec no hardware engine serves, software encoding, a server without DRI3 1.2, Damage, or Render, a server drawing on a GPU other than the encode node’s, a buffer the server will not import, or a first frame the encoder cannot read. A watermark is not among them: it is composited by the server through Render, as the cursor is. The capture then runs the XShm path with nothing half-built.
Functions§
- run_
capture - Run the zero-copy X11 capture until
controls.stopis set, or report that it cannot start.