The problem we are solving in this post is how to efficiently share textures across processes on Linux. We want to do this as fast as possible without any texture data transfer between GPU and CPU memory.
For our example we have two processes named server and client, with both rendering a texture to a window. We want to make the server create a texture and share it to the client so that both can render the same texture.
We will be using DMA-BUF buffer sharing subsystem to share this texture. To interface with this subsystem we will use only EGL and GL/GLES with appropriate extensions. Another options is to use GBM and/or DRM directly but it is out of the scope of this post.
The link and instructions for the full working example are located at the end of the post. What follows now is the explanation of the main part, texture sharing.
Prerequisites
To be able to do texture sharing as described here check that your runtime environment has:
- Linux OS (for dma-buf and unix domain sockets)
- EGL extensions:
- GLES extensions:
You can easily find the supported extensions on your system with eglinfo
and glxinfo
commands.
Server
On the server we will:
- Create a GL texture.
- Create an EGL image out of the GL texture.
- Get a dma-buf file descriptor and texture storage metadata from the EGL image.
- Use a unix domain socket to send the file descriptor and metadata to the client.
The first thing we do is create a GL texture.
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
Nothing fancy here, just standard GL API.
From the new GL texture we then create an EGL image.
EGLImage image = eglCreateImage(egl_display,
egl_context,
EGL_GL_TEXTURE_2D,
(EGLClientBuffer)(uint64_t)texture,
NULL);
Again, just standard EGL API.
Next we retrieve the data that we will be sending to the client.
int texture_dmabuf_fd;
struct texture_storage_metadata_t
{
int fourcc;
EGLint offset;
EGLint stride;
} texture_storage_metadata;
PFNEGLEXPORTDMABUFIMAGEQUERYMESAPROC eglExportDMABUFImageQueryMESA =
(PFNEGLEXPORTDMABUFIMAGEQUERYMESAPROC)eglGetProcAddress("eglExportDMABUFImageQueryMESA");
PFNEGLEXPORTDMABUFIMAGEMESAPROC eglExportDMABUFImageMESA =
(PFNEGLEXPORTDMABUFIMAGEMESAPROC)eglGetProcAddress("eglExportDMABUFImageMESA");
eglExportDMABUFImageQueryMESA(egl_display,
image,
&texture_storage_metadata.fourcc,
NULL,
NULL);
eglExportDMABUFImageMESA(egl_display,
image,
&texture_dmabuf_fd,
&texture_storage_metadata.stride,
&texture_storage_metadata.offset);
We are using the EGL extension EGL_MESA_image_dma_buf_export
here. From the EGL image we get a dma-buf file
descriptor and texture storage metadata. Because the functions to do this are not exported from libEGL.so
we
need to retrieve them with eglGetProcAddress
.
Finally we send the file descriptor and metadata to the client using a unix domain socket.
int sock = create_socket(SERVER_FILE);
while (connect_socket(sock, CLIENT_FILE) != 0)
;
write_fd(sock, texture_dmabuf_fd, &texture_storage_metadata, sizeof(texture_storage_metadata));
close(sock);
close(texture_dmabuf_fd);
We use helper functions from our example to create a socket, connect to the client and send the data. After this is done we can safely close both the socket and the dma-buf file descriptor.
Unix domain sockets need to be used because OS needs to properly transfer file descriptor from one process to the other. Its value will most probably be different in each process since it is a process-local handle.
The server can now continue to updating and rendering the texture.
IMPORTANT: When sharing a texture we must not change the texture storage layout (size, format,…), we can only update the image data. Otherwise the texture will stop being shared.
Client
On the client we will basically be doing the same transformations as on the server but in reverse:
- Use a unix domain socket to receive the dma-buf file descriptor and texture metadata from the server.
- Create an EGL image out of the file descriptor and metadata.
- Create a GL texture out of the EGL image.
First we get the dma-buf file descriptor and texture storage metadata from the server by reading from the unix domain socket.
int texture_dmabuf_fd;
struct texture_storage_metadata_t
{
int fourcc;
EGLint offset;
EGLint stride;
} texture_storage_metadata;
int sock = create_socket(CLIENT_FILE);
read_fd(sock, &texture_dmabuf_fd, &texture_storage_metadata, sizeof(texture_storage_metadata));
close(sock);
We create a socket and block until we get the data from the server. We also close the socket after we get the data.
From the retrieved file descriptor and metadata we create an EGL image.
EGLAttrib const attribute_list[] = {
EGL_WIDTH, 2,
EGL_HEIGHT, 2,
EGL_LINUX_DRM_FOURCC_EXT, texture_storage_metadata.fourcc,
EGL_DMA_BUF_PLANE0_FD_EXT, texture_dmabuf_fd,
EGL_DMA_BUF_PLANE0_OFFSET_EXT, texture_storage_metadata.offset,
EGL_DMA_BUF_PLANE0_PITCH_EXT, texture_storage_metadata.stride,
EGL_NONE};
EGLImage image = eglCreateImage(egl_display,
NULL,
EGL_LINUX_DMA_BUF_EXT,
(EGLClientBuffer)NULL,
attribute_list);
close(texture_dmabuf_fd);
We are using the EGL extension EGL_EXT_image_dma_buf_import
here. Notice how this call is different from the
regular eglCreateImage
usage, we pass all the arguments through the attribute list while the buffer argument itself
is NULL
. We also close the file descriptor since we won’t be needing it any more.
Finally we create a GL texture from the EGL image.
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image);
We are using the GLES extension GL_OES_EGL_image_external
here. Even though it is a GLES extension it is normally
implemented on GL also.
The GL texture can now be used for rendering on the client, it is a handle to the same texture as the one on the server.
Example program
A working example to play with is in this repo.
Aside from texture sharing, both the server and the client are executing the same code, rendering a texture on a quad. The only difference is that the server is periodically updating its texture data. As you can see both the server and the client create the same output.
To compile the example run make
. The generated executable dmabufshare
can act as a server or a client.
$ make
# Terminal 1
$ ./dmabufshare server
# Terminal 2
$ ./dmabufshare client
Debugging
While working on this the debug messages from the radeonsi MESA driver really helped. You can enable them with environment variables which are listed here.
For example, I used
$ AMD_DEBUG=tex,vm ./dmabufshare server
to see when and what the driver allocated.
I was able to use this info to make sure I was properly transfering the texture between the processes (some memory adresses were the same in both processes).
Unresolved questions
Do we need explicit synchronization between processes?
Because the client could be using the texture while the server is updating it. My guess is that this is transparently
taken care of by the dma-buf subsystem (or the driver) since I haven’t experienced any rendering issues or crashes.
Can the texture data be updated from the client?
I tried it and the server successfully renders the updated texture. But I am not sure if this is supported by
definition or just implemented this way. It seems to me that this kind of usage is valid because the extensions
documentation doesn’t define any limits on who can read or write to the shared texture.