返回资讯中心
外部精选
软件工程
#开发者实时流

Using Vulkan on Wayland Without WSI

In my previous post I covered the process of

Hacker News 最新ibobev2026/10/30 · 昨天30 分钟阅读

以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。

In my previous post I covered the process of putting a software-rendered window on-screen in a Wayland client program without the use of libwayland. While that is a good starting point, for more performance-minded programs we'd typically want to be able to use the GPU for rendering. That is the subject of this post: using the GPU to render our libwayland-free Wayland client program.

In the last post, I described the basics of how the Wayland wire protocol operates and how we can serialize our own messages without use of libwayland. This post is written with the assumption that one has either read my previous post or is otherwise familiar with how the Wayland protocol operates.

Once again, I have provided a demo program to accompany this post, and will be including some (lightly modified) code snippets from that program.

At the end of the last post, I described the following flow to put a window on screen.

While much of this remains unchanged, we do need to modify it a little. After making the appropriate changes, it transforms into something more like the following:

In the interest of not taking on too many tasks at once, I have opted to use the Vulkan graphics API to access the GPU. I do not wish to write a Vulkan tutorial, so I will largely leave the Vulkan-specific implementation details as an exercise for the reader but I will make some small exceptions for details that are directly related to how we will be managing the interactions between Vulkan and Wayland.

I may eventually experiment with invoking the GPU through a lightweight userspace driver using ioctl to access the GPU without involving any graphics API at all, but that is a subject for another time.

Of course, accepting the dependency on libvulkan leads us to the following...

Given the current design of most Linux systems, if we wish to actually load a library such as libvulkan, which does link libc, we will also need to link our program against libc. That isn't to say that it is entirely impossible to use the GPU without linking libc, only that if we wish to load a library that expects libc we must provide everything that libc provides in an ABI-compliant fashion. That is something beyond the scope of this blog post and is not exactly practical for our intentions here.

The demo program associated with this post is written in C, and linked against libc and libdl, as such all code snippets included will be in C.

The typical usage patterns of the Vulkan API involve using the Windowing System Integration (WSI) via the likes of VK_KHR_surface and VK_KHR_swapchain. As we are not using libwayland, the ABI of which is depended upon by the Vulkan WSI for Wayland, we cannot3 use these. This means that management and presentation of rendered frames is up to us. That means we need to create our render image(s) without a swapchain, and write the logic for presenting completed frames when they are ready.

What we can do instead is construct a simple queue of VkImages (and VkImageViews) that we will mark as available or unavailable depending on whether or not the compositor is currently accessing them, and simply draw to the next available buffer in the queue.

For the purposes of this experiment, I have chosen to use Vulkan 1.2 with the following device extensions:

As covered in my previous post, to present a frame, we tell the compositor to attach the buffer associated with a given wl_buffer handle to our wl_surface. This is how frame presentation works regardless of how the frame is produced. So what we need to do is create a wl_buffer handle for each VkImage we have in our queue.

When rendering on the CPU, we create these handles from shared memory descriptors via the wl_shm interface. We could continue doing this, but that would mean copying our produced images from the GPU to CPU for export, and then the compositor would need to copy the image back to the GPU to present it. This approach is obviously very wasteful. There is a better option which does not require any copying of produced images: DMA buffers. On Linux systems, we can use DMA (Direct Memory Access) buffers to share descriptor handles to GPU memory from one process to another.

Under Wayland there is a protocol that allows us to take advantage of this: linux-dmabuf. This protocol provides an interface for us to create a wl_buffer handle from GPU memory and make our GPU memory accessible to the compositor with no copying involved—at least none imposed by us as the client.

We'll need to add zwp_linux_dmabuf_v1 to our list of global interfaces to look for in our registry binding phase. For the rest of this post, I'll be assuming the interface is bound at version 5 as that is what my chosen compositor, Hyprland5, currently supports at the time of writing.

The compositor does not know or care what API we are using to access the GPU. The interface expected by the protocol is that of the Linux Direct Rendering Manager (DRM).

To ensure that the compositor is capable of importing the images we export DMA handles for, we should try to use the same device and allocate the images in a format that the compositor is capable of processing. Of course, this means we need a way to know what device the compositor is using and what formats are supported.

The linux-dmabuf protocol offers us an interface for this. Once we have a wl_surface handle, we can issue a get_surface_feedback request on the global dmabuf interface. This is intended to be used to populate a zwp_linux_dmabuf_feedback_v1 object, so we'll need to provide an object ID for the feedback as per the xml description of the request below.

<request name="get_surface_feedback" since="4"> <arg name="id" type="new_id" interface="zwp_linux_dmabuf_feedback_v1"/> <arg name="surface" type="object" interface="wl_surface"/> </request>

We'll construct the message header with the ID of the global dmabuf interface object, opcode 3, and length 16. The message body will contain two uint32 IDs, the first being that with which we will associate the feedback object, and the second being the ID of our wl_surface handle.

Once we've issued this request, we'll receive a series of events delivering the requested feedback. What we're looking for is:

The compositor may advertise multiple devices in a series of tranches. For our purposes we're going to select one device—the main_device—and collect the list of available format modifiers, for the formats we care about, from the tranche associated with that same device.

The relevant events we'll take are the following:

<event name="format_table"> <arg name="fd" type="fd" summary="table file descriptor"/> <arg name="size" type="uint" summary="table size, in bytes"/> </event> <event name="main_device" deprecated-since="6"> <arg name="device" type="array" summary="device dev_t value"/> </event> <event name="tranche_target_device"> <arg name="device" type="array" summary="device dev_t value"/> </event> <event name="tranche_formats"> <arg name="indices" type="array" summary="array of 16-bit indexes"/> </event>

The compositor will also emit a tranche_done event at the end of each per-device event sequence, and a done event when all the feedback has been communicated. We will use these to decide when to stop pulling events from the socket.

The main_device6 and tranche_formats events will put a series of bytes into the main message data stream. The tranche formats event will provide a series of 16-bit indices into the format table. We will use these to collect a list of available DRM format modifiers for each image format we care about. This list of modifiers will be required when we create the images for our render queue.

We'll take the advertised main_device and will take the formats from the tranche associated with an advertised tranche_target_device equivalent to the main_device.

When pulling the device events we need to remember the wire format's encoding for arrays, coming as a Uint32 length n followed by n bytes of data. In this case we should expect to see a length of 8, followed by the 8 bytes which make up the dev_t device handle.

On a single-GPU machine we could simply assume that, in most cases, whatever Vulkan physical device we select will be the same device that the compositor is using. On multi-GPU machines (e.g. iGPU + dGPU), we will need to be more careful. To ensure we are using the same device, we will need to request some extra information during device selection.

The device advertised to us by the compositor may be one of two kinds of node7: primary or render. To ensure we're selecting the same device as that advertised by the compositor we can access the device major and minor values from each node that is present in the physical device we are evaluating. We will only select the device for which the major and minor values of either the primary or render node matches that of the advertised device.

With Vulkan we can use VkPhysicalDeviceDrmPropertiesEXT in combination with VkPhysicalDeviceProperties2 passed to vkGetPhysicalDeviceProperties2 to provide us the major and minor values of both the primary and render nodes of each device we evaluate. We can then compare these values to the major and minor values of the advertised main_device.

To get the major and minor values from the dev_t handle to the device advertised to us by the compositor, we can use the following:

#define dev_major(dev) (((dev)>>8) & 0xfff) #define dev_minor(dev) (((dev) & 0xff) | (((dev)>>12) & 0xffffff00))

These macros are C rewrites of some functions I had for the same purpose in Zig code—which I believe I hand-translated from the equivalent musl libc macros found in sys/sysmacros.h—as part of a previous iteration of this project.

This will allow us to compare the Vulkan device we are evaluating against the advertised DRM device from the compositor.

// Derive major and minor values from the main_device u64 main_dev_maj = dev_major(wl_main_dev); u64 main_dev_min = dev_minor(wl_main_dev);

VkPhysicalDeviceDrmPropertiesEXT drm_props = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRM_PROPERTIES_EXT }; VkPhysicalDeviceProperties2 pdev_props = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &drm_props, }; vkGetPhysicalDeviceProperties2(vk_physical_device, &pdev_props);

if (drm_props.hasPrimary) { if (drm_props.primaryMajor == main_dev_maj && drm_props.primaryMinor == main_dev_min) { /* Advertised is primary node of this device / } } if (drm_props.hasRender) { if (drm_props.renderMajor == main_dev_maj && drm_props.renderMinor == main_dev_min) { / Advertised is render node of this device */ } }

It is my understanding that we should basically always expect a render node on any modern Linux system—primary nodes being the authenticated kernel modesetting interface—but the spec does allow for compositors to send either node type so we should check for both.

It's worth noting that most DRM fourcc formats are little-endian, so if we take the Vulkan image format VK_FORMAT_R8G8B8A8_UNORM, we'll map this to DRM_FORMAT_ABGR8888.

The format table event will provide us a file descriptor of a shared memory table of format+modifier combinations structured as a 32-bit format and 64-bit modifier with 32 bits of padding in between.

We can model a DRM format entry as the following:

struct DrmFormatTableEntry { u32 format; // DRM Format u32 __pad; // Padding for alignment u64 modifier; // DRM Format Modifier: Vendor-specific blob to convey metadata };

The file descriptor will come in as ancillary data, while the main message data will communicate the size of the file.

We can then mmap the provided file descriptor as private and access the entries at each index provided in the tranche_formats event of our chosen device. The spec does permit duplicate entries, so we should be somewhat selective with regards to which entries we persist outside the table. We may also want to filter the list of format modifiers based on some constraints. I'll touch o

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →