GPU surfaces (zero-copy)¶
A local client that renders on the GPU, a video player or a drawing canvas, can
share its output with the terminal without a single CPU copy by transmitting a
GPU surface, t=g. The payload is the name of the surface: an IOSurface
on macOS and a dma-buf on Linux, as below. Other kinds of surface are refused
with EINVAL, as is t=g on platforms without an implementation.
Chunking (m) and compression (o) are not allowed with t=g.
On macOS the payload is iosurface:<id> where <id> is the integer
returned by IOSurfaceGetID(). The surface must be created with
kIOSurfaceIsGlobal set, otherwise IOSurfaceLookup() in the terminal
cannot find it. The surface must be 32-bit BGRA or RGBA with straight
(not pre-multiplied) alpha, as for f=32. The f, s and v keys
are ignored, the dimensions and pixel format come from the surface itself.
On Linux it is a dma-buf, the kind of buffer that GPU drivers, Vulkan, GBM and udmabuf share between processes. A dma-buf is a file descriptor and has no name that another process could look up, so the payload names the process that holds it and its descriptors there:
dmabuf:<pid>:<fourcc>:<modifier>:<fd>,<offset>,<stride>[:<fd>,<offset>,<stride>...]
<pid>The id of the process that holds the buffer, as the terminal sees it, so a client in a container with process ids of its own cannot use this.
<fourcc>The DRM format, as the four characters of its code.
AB24(DRM_FORMAT_ABGR8888) has the bytes R, G, B, A in memory, the order off=32, andAR24(DRM_FORMAT_ARGB8888) has B, G, R, A.XB24andXR24are the same with the fourth byte ignored. Alpha is straight, as forf=32.<modifier>The DRM format modifier of the buffer, in decimal or in hexadecimal after
0x:0for a linear buffer,0x00ffffffffffffff(DRM_FORMAT_MOD_INVALID) to leave the layout to the driver, or the one the allocator chose.<fd>,<offset>,<stride>One to four planes, each the number of the descriptor in the process that holds the buffer, the offset of the plane in it and its stride in bytes, as the allocator gives them. The formats above have one plane, a modifier can add more.
A dma-buf does not know its size, so s and v are required and give the
width and height in pixels; f is ignored.
The terminal takes duplicates of the descriptors with pidfd_getfd(2), as a
debugger could, and only from a process in the session of the program the
window runs: the shell of the window and everything started from it. Any other
process is refused, so that a program cannot have the terminal show the
buffers of another window or of a program that is not its own. The kernel
adds a condition of its own, that the terminal be allowed to trace the
process. With Yama’s ptrace_scope at 0 or 1, the default of most
distributions, that holds for the processes the terminal started; at 2 or
3 it holds for none, and every dma-buf is refused. A descriptor that is
not a dma-buf is refused too. The terminal closes its duplicates once the
buffer is imported, the driver keeping the buffer for as long as the image
exists, and the client may close its own descriptors whenever it likes after
the reply.
The buffer is imported with EGL_EXT_image_dma_buf_import, and
EGL_EXT_image_dma_buf_import_modifiers for a modifier other than 0
and 0x00ffffffffffffff, so it works where kitty draws with EGL: under
Wayland, and not under X11, where it draws with GLX. Every refusal is an
EINVAL with a message that says why: under X11, a payload that is not as
above, no s or v, a process outside the session, a descriptor that
cannot be taken or is not a dma-buf, or a driver that cannot import the
buffer. A query, a=q, does all of it but the import, so its answer says
whether the terminal could take the buffer.
When it copies a dma-buf, the terminal relies on the synchronization that the drivers of dma-bufs do implicitly for the rendering the client submitted before it sent the command. A client whose driver does not, or that writes the buffer with the CPU, finishes its writes before it sends the command.
This was tried on Ubuntu 26.04 with Mesa’s software renderer, under a headless
sway, with buffers made from memory with udmabuf in the formats AB24 and
XR24 and the modifiers 0 and 0x00ffffffffffffff: the content of
the buffer showed, and after a refresh its new content. It has not been tried
with buffers of a GPU driver or with other modifiers.
The terminal keeps the surface for as long as the image exists, and copies it into the image on the GPU when it is transmitted and again on every refresh, so the image shows what the surface held at the last of those. After updating the surface, the client sends the refresh action:
<ESC>_Ga=r,i=31<ESC>\
which copies the surface again and marks the image as changed so that the
next frame shows the new content. The terminal responds OK unless responses are suppressed with
q. Sending a=t or a=T again with t=g and the same surface
for an existing image id is equivalent to a refresh followed by the usual
placement. Deleting the image releases the surface. Animation frames
(a=f) and composition (a=c) are not supported for surface backed
images, the terminal responds with EINVAL. a=q with t=g can be
used to check for support, a terminal that does not know the medium
responds with an error.
Relays such as multiplexers or Goblin Skiff that forward a terminal session
to another machine cannot pass the surface along, but they can read it. On
macOS any process of the same user can call IOSurfaceLookup() with the
id, then IOSurfaceLock() and IOSurfaceGetBaseAddress() to read the
pixels. On Linux a relay that started the client can take the descriptors
with pidfd_getfd(2) as the terminal does, and read the pixels of a linear
buffer with mmap(2); the terminal itself refuses a dma-buf named through a
multiplexer, whose clients are not in the session of the window. A relay can
poll the surface, for instance on every refresh action, and forward the
pixels to the far side with an ordinary t=d transmission.
The base transmission, placement, deletion, and reply rules are specified by the kitty graphics protocol.