Austin Shafer

home contact

Image presentation in libvulkan_intel.so

How the intel Vulkan library uses libdrm to display to the screen without X11

VK_KHR_display VK_KHR_swapchain


In Vulkan, rendering to a physical screen involves creating a display surface (VkDisplaySurfaceKHR). Swapchains are then used to present images directly to display surfaces without consulting an X11 server. The Mesa project provides an open source implementation of the Vulkan API for Intel and AMD cards.

The intel implementation of Vulkan is provided by the mesa-dri package on FreeBSD. Internally, mesa's libvulkan_intel.so uses the DRM subsystem to render to the screen.

Debugging Setup

Vulkan is a very complicated API, but debugging its implementation doesn't have to be painful. Stepping through Mesa's code is a great way to learn how Vulkan works even if you've already enabled the validation layers. First we need source code and debug symbols.

We can use FreeBSD's ports collection so we don't have to spend time searching for source repositories and learning their build/install systems. FreeBSD will do all the work for us. By setting /etc/make.conf we can tell the port system to keep debugging symbols.

ashafer:~/ % cat /etc/make.conf 
WITH_DEBUG=YES
CFLAGS += -g

Modifying CFLAGS isn't really required, WITH_DEBUG=YES is the magic we need. Now we can use portmaster(8) to build and install the following ports:

cd /usr/ports/ports-mgmt/portmaster ; make install clean

portmaster -D -K graphics/mesa-dri --packages-build \
	   graphics/mesa-dri/ \
	   devel/vulkan-validation-layers/ \
	   graphics/vulkan-loader/	   

Depending on your machine this may take a few minutes. In FreeBSD's ports, the source code of the package is placed in the /usr/ports/<port_name>/work directory. To view the source tree containing libvulkan_intel.so:

cd /usr/ports/graphics/mesa-dri/work/mesa-18.3.2

We use the -D and -K flags for portmaster to prevent deletion of this work directory. Tracing calls in dtrace or lldb is very useful, but at some point you're going to want to read the code in a real editor.

Vulkan Swapchains

Vulkan uses swapchains to keep track of images and present them to the screen (implemented by the VK_KHR_swapchain) extension. Swapchains are the Vulkan equivalent of eglSwapBuffers or drmModePageFlip.

Presenting an image is done with a call to vkQueuePresentKHR. The Mesa function for this dispatch is anv_QueuePresentKHR (src/vulkan/wsi/wsi_common.c line 917). This is a good place to drop a breakpoint if you are in the debugger.

  • Mesa's Vulkan drivers have prefixes differing between AMD and intel:
    • Radeon: "RADV"
    • Intel: "ANV"

All of the Window System Integration functions in mesa are prefixed with wsi_. These functions implement the display surface and swapchain extensions. anv_QueuePresentKHR will call wsi_common_queue_present to create fences, submit command buffers, and present the queue to the screen:

VkResult wsi_common_queue_present(...) {
 ...
 result = swapchain->queue_present(swapchain,
                           pPresentInfo->pImageIndices[i],
                           region);
}

A swapchain's queue_present function performs the correct action based on the type of surface the swapchain was created with. Wayland or X11 surfaces will talk to the compositor and let it know that the window has been updated. For our VK_KHR_display surface (without X11), queue_present points to the following:

static VkResult
wsi_display_queue_present(struct wsi_swapchain *drv_chain,
                          uint32_t image_index,
                          const VkPresentRegionKHR *damage)
{
   ...
   image->state = WSI_IMAGE_QUEUED;

   result = _wsi_display_queue_next(drv_chain);
   if (result != VK_SUCCESS)
      chain->status = result;
      
   ...
}

Finally we have burrowed down to the good stuff. _wsi_display_queue_next does all of the DRM modesetting that we expect to find in a standalone graphics extension.

Implementing Vulkan image presentation with DRM

DRM requires selecting CRTC's, modes, encoders, and other abstractions before rendering can begin. There's a lot of boilerplate involved so I will just link to the drm-kms(7) manpage. In mesa this is handled in wsi_display_setup_connector (src/vulkan/wsi/wsi_common_display.c line 1351)

At its core, image presentation in Vulkan involves two DRM functions: drmModePageFlip and drmModeSetCrtc. These either flip the image to display or set the CRTC to the next framebuffer. Once a CRTC is set and a connector is active, then _wsi_display_queue_next will flip pages:

static VkResult
_wsi_display_queue_next(struct wsi_swapchain *drv_chain)
{
	...
	if (connector->active) {
         ret = drmModePageFlip(wsi->fd, connector->crtc_id, image->fb_id,
                                   DRM_MODE_PAGE_FLIP_EVENT, image);
	...

If the connector is not active, then nothing is being displayed on the screen. To fix this the CRTC must be set to the next queued image and the connector should be marked as active, signifying that we are already presenting to the physical display.

	...
        ret = drmModeSetCrtc(wsi->fd, connector->crtc_id,
                             image->fb_id, 0, 0,
                             &connector->id, 1,
                             &connector->current_drm_mode);
        if (ret == 0) {
           /* Assume that the mode set is synchronous and that any
            * previous image is now idle.
            */
           image->state = WSI_IMAGE_DISPLAYING;
           wsi_display_idle_old_displaying(image);
           connector->active = true;
        }

At this point the display should have either flipped to the next image or been set to the CRTC selected.

That's essentially the meat of the swapchain presentation bits of Vulkan. Most of the _wsi_display_queue_next method was omitted because it is quite verbose. Parts omitted include finding the next image in a flip sequence, setting image states, and waiting while another VT is active.

  • An interesting note is that _wsi_display_queue_next will poll once a second while waiting for another VT to relinquish the screen. I'm not saying it doesn't work, but I feel like there's a better way to do this.

I'm hardly an expert on these topics, so please let me know if anything is incorrect. I hope this is interesting or useful to others. After all, what is the point of having open source libraries if you don't read the code?