We're hiring!
*

A look at Vulkan extensions in Venus

Igor Torrente avatar

Igor Torrente
October 19, 2022

Share this post:

Reading time:

When it comes to Vulkan applications and libraries, you'll be hard-pressed to write them without leveraging the ever-expanding list of Vulkan extensions made available to take advantage of the quirks and features graphics hardware has to offer. In brief, Vulkan extensions are addendums to the Vulkan specification that drivers are not required to support. They allow vendors to expand upon the existing API to allow the use of features that may be unique to a particular vendor or generation of devices without having to retrofit the core API.

Even in virtualized environments, the use of extended functionality for hardware-accelerated applications can be paramount for performance. Indeed, it is something that virtual graphics drivers, like Venus, must take into account.

Venus is a virtual Vulkan driver based on the Virtio-GPU protocol. Effectively a protocol on top of another protocol, it defines the serialization of Vulkan commands between guest and host. This post will cover details of the Venus driver, its components, and their relations in the context of extensions.

Counter-Strike Global offensive with DXVK-native backend running on top of Venus inside Crosvm.

Venus components

Many elements comprise the pipeline path of a Vulkan API call in Venus. Starting from an application in the guest, the API call is dispatched to the proper entrypoint in the Venus Mesa driver.

The Mesa driver prepares a command defined by the Venus Protocol to be sent over to the host via the guest kernel's Virtio-GPU driver and the VM. The command is received and decoded in the host by Virglrenderer, whereby it is further dispatched to the host Vulkan driver which performs the actual work defined by the original API call. If a return value is expected, then the same process is repeated, but in reverse.

For the purpose of adding extensions, we need only concern ourselves with three components from this pipeline: the Mesa driver, the protocol, and Virglrenderer.

Mesa driver

This part resides in the guest and is the library that sets up our Vulkan API entrypoints in tandem with the loader.

It's a relatively thin layer whose central purpose is to serialize Vulkan commands and parameters. Much of the serialization and communication with Virglrenderer is handled by generated C code. We will be taking a look at this generated code later in this post. Currently, one can find the code generated from the Venus Protocol under the src/virtio/venus-protocol directory in Mesa.

For performance and bandwidth reasons, the Venus Mesa driver tries to avoid communicating with the host as much as possible by caching certain results and by batching commands when feasible. For example, when calling vkGetPhysicalDeviceFeatures2(), the features from all available devices would have already been fetched and cached from a prior call to either vkEnumeratePhysicalDevices() or vkEnumeratePhysicalDeviceGroups(). In this instance, vkGetPhysicalDeviceFeatures2() would just return the cached values instead of traveling to the host.

Virglrenderer

This component sits on the host and manages several possible client contexts by decoding their commands, dispatching them to the proper driver/device, and sending back the results if necessary.

The Virglrenderer itself is pretty complex, and most of the complexity resides in the parts that handle things like context, memory, etc.

The good news is that we don't need to deal with context to add a new extension. The code that handles the commands is generally fairly straightforward because most of the heavy lifting is done by the code generated by Venus-Protocol.

Venus-protocol

Venus-protocol is the repository with a set of XMLs files that together make up the specification and tools to generate code from the specification.

The specification defines how commands, shaders, buffers, events, and replies will be serialized and deserialized by both guest and host.

In the Venus-protocol, we generate (mentioned in the previous section) code for Mesa and VirglRenderer to handle all these operations. Files generated starting with vn_protocol_driver_* are for the Mesa and vn_protocol_renderer_* for the Virglrenderer project. These files contain the respective encoder and decoder for each Vulkan function supported.

All commands specified by the Venus protocol contain in their headers an identifier that specifies what kind of payload they carry and which generated handle will decode them on the other side.

All the structs, enums, functions, extensions, API versions, and more are defined in XMLs. These XMLs are parsed by python scripts to generate the respective .h files.

Adding a Vulkan extension to Venus

Adding Vulkan extension supports to Venus is slightly different from what happens in a regular Vulkan driver.

To illustrate how we can give support to a Vulkan extension on Venus, we're going to use VK_EXT_extended_dynamic_state2 as an example. This extension adds new functions and also extends existing ones.

Venus Protocol

Let's see what we need to define in the protocol to generate the necessary code. The protocol is defined in three files under the venus-protocol/xmls folder.

  • vk.xml

  • VK_EXT_command_serialization.xml

  • VK_MESA_venus_protocol.xml

We will not change the VK_MESA_venus_protocol.xml as this defines some internal Venus commands which do not need to be modified for our extension.

Starting with vk.xml: this file defines all the components of Vulkan. Including functions, structs, enums, and unions for each Vulkan core version and each extension.

It is a machine-readable file maintained by the Khronos Group in their documentation repository. We copy/update when we want to update to a particular version of the spec.

  1. Add the extension in the VK_XML_EXTENSION_LIST at vn_protocol.py.
    VK_XML_EXTENSION_LIST = [
         [...]
         'VK_EXT_extended_dynamic_state2',
         [...]
    ]
    

And since this extension adds new functions, we need to add them to xmls/VK_EXT_command_serialization.xml.

We can use the utils/print_vk_command_types.py to generate the entry for us. In this example, vkCmdSetDepthBiasEnable and vkCmdSetDepthBiasEnableEXT will be generated.

  1. Copy the entry and paste it somewhere under <enums name="VkCommandTypeEXT" type="enum"> block.
    <enum value="228"   name="VK_COMMAND_TYPE_vkCmdSetDepthBiasEnable_EXT"/>
    <enum               name="VK_COMMAND_TYPE_vkCmdSetDepthBiasEnableEXT_EXT" alias="VK_COMMAND_TYPE_vkCmdSetDepthBiasEnable_EXT"/>
    

In the last part we have to generate the code from XMLs using scripts in this repository. For that we will need meson, ninja, python 3, mako, and a C compiler (to verify the output).

  1. Use the following commands to generate the files:
    $ meson build
    $ ninja -C build
    

In the end, we will have two kinds of files. vn_protocol_renderer* for Virglrender and vn_protocol_driver* for Mesa. These files contain all the code to handle [de]serialization for all commands. Now we just copy these sets of files to the folders in their respective repositories.

And that's it for the Venus-protocol. Next, we will learn how to change the Mesa driver.

Mesa driver

Currently, Venus code stays in src/virtio/vulkan/. Any changes should be made in this folder.

As we can see in the Vulkan specification, this extension adds four new functions. These functions write commands to command buffers. A good place to put them is vn_command_buffer.c.

I will use vkCmdSetRasterizerDiscardEnable as an example and break it down into parts.

Your implementation should follow almost the exact signature of the functions defined in the Vulkan official header, except by the name, where you should replace the vk by vn_.

Next, use vn_ as a prefix instead of vk because we use the vk_entrypoints_gen.py script to generate the entrypoints, and it's receiving vn_ as a prefix from src/virtio/vulkan/meson.build.

void
vn_CmdSetRasterizerDiscardEnable(VkCommandBuffer commandBuffer,
                                 VkBool32 rasterizerDiscardEnable)

And now we implement the function. This includes any specific logic, serialization, and submission (if necessary) of any data to the Venus host.

Luckily the venus-protocol generates much of the repetitive code for us. So, functions like vn_sizeof_vkCmdSetRasterizerDiscardEnable and vn_encode_vkCmdSetRasterizerDiscardEnable are generated automatically by itself.

Here we are only using these two helpers, but many others are generated that can also be used.

Besides these Venus-Protocol functions, Venus provides a lot of helpers like vn_cs_encoder_reserve below.

In this example, we will allocate some space in the command stream, serialize (encode) the parameters, and put them in the space allocated earlier.

Notice that we are not sending any data to the Venus host in this function. Usually, Vulkan clients record several commands into the command buffer. So we wait for it to finish the recording and send them at once to the Venus host. We wait until the client call vkEndCommandBuffer (vn_EndCommandBuffer) to send all commands.

Notice that we are not preparing the encoder. This happens in the vkBeginCommandBuffer (vn_BeginCommandBuffer).

{
   struct vn_command_buffer *cmd =
      vn_command_buffer_from_handle(commandBuffer);
   size_t cmd_size;

   cmd_size = vn_sizeof_vkCmdSetRasterizerDiscardEnable(
      commandBuffer, rasterizerDiscardEnable);
   if (!vn_cs_encoder_reserve(&cmd->cs, cmd_size))
      return;

   vn_encode_vkCmdSetRasterizerDiscardEnable(&cmd->cs, 0, commandBuffer,
                                             rasterizerDiscardEnable);
}

Following the specification, it also adds one new struct VkPhysicalDeviceExtendedDynamicState2FeaturesEXT that informs the capabilities of the physical device (GPU) in terms of this extension. This such struct extends the vkGetPhysicalDeviceFeatures2 and, therefore, we need to modify at least the vn_GetPhysicalDeviceFeatures2 function.

Unfortunately, this will not be as straightforward as the functions due to some optimizations done in Venus guest.

The way things are today, Venus receives vkEnumeratePhysicalDevices (or vn_EnumeratePhysicalDeviceGroups) and caches all the information possible from all physical devices available. One cached information is the Device features.

We will have to change the function that caches all the features. Currently, this function is the vn_physical_device_init_features.

And the features cache resides in the vn_physical_device_features struct at vn_physical_device.h. We will need to add our extension feature in there.

--- a/src/virtio/vulkan/vn_physical_device.h
+++ b/src/virtio/vulkan/vn_physical_device.h
@ -25,6 +25,7 @@ struct vn_physical_device_features {
    /* Vulkan 1.3 */
    VkPhysicalDevice4444FormatsFeaturesEXT argb_4444_formats;
    VkPhysicalDeviceExtendedDynamicStateFeaturesEXT extended_dynamic_state;
+   VkPhysicalDeviceExtendedDynamicState2FeaturesEXT extended_dynamic_state2;
    VkPhysicalDeviceImageRobustnessFeaturesEXT image_robustness;
    VkPhysicalDeviceShaderDemoteToHelperInvocationFeatures
       shader_demote_to_helper_invocation;

In the vn_physical_device_init_features, we need to add our extension feature to the list of the extensions that will be cached. To do it we use the VN_ADD_EXT_TO_PNEXT macro. After passing the feature struct, a flag will indicate if the feature is enabled along with the sType of the struct and the list head.

VN_ADD_EXT_TO_PNEXT(exts->EXT_extended_dynamic_state2,
                    feats->extended_dynamic_state2,
                    EXTENDED_DYNAMIC_STATE_2_FEATURES_EXT, features2);

Now in the vn_GetPhysicalDeviceFeatures2, we add our struct to the giant switch inside this function. This function goes through all the structs passed by the Vulkan client and fills them with the information stored in the cache.

We just need to add the information to identify the struct of our extension and copy it.

@@ -1850,6 +1856,9 @@ vn_GetPhysicalDeviceFeatures2(VkPhysicalDevice physicalDevice,
       case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT:
          *u.extended_dynamic_state = feats->extended_dynamic_state;
          break;
+      case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_2_FEATURES_EXT:
+         *u.extended_dynamic_state2 = feats->extended_dynamic_state2;
+         break;
       case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_ROBUSTNESS_FEATURES_EXT:
          *u.image_robustness = feats->image_robustness;
          break;

And last but not least, we need to enable and passthrough our new extension. Being part of the list of passthrough extensions means you are allowing the extension to be advertised to the guest. If the host does not support the extension, then it is not advertised at all, even if it's in the list of passthrough extensions.

So we will only export if the host driver supports it and if it is in the list of passthrough extensions.

In order to do this, just change the boolean in

@@ -940,6 +943,7 @@ vn_physical_device_get_passthrough_extensions(
       /* promoted to VK_VERSION_1_3 */
       .EXT_4444_formats = true,
       .EXT_extended_dynamic_state = true,
+      .EXT_extended_dynamic_state2 = true,
       .EXT_image_robustness = true,
       .EXT_shader_demote_to_helper_invocation = true,

Virglrenderer

As mentioned before, we need to enable the extension. This is done in the same way as Mesa, just in a file with a different name.

--- a/src/venus/vkr_common.c
+++ b/src/venus/vkr_common.c
@@ -78,7 +78,7 @@ static const struct vn_info_extension_table vkr_extension_table = {
    .KHR_zero_initialize_workgroup_memory = false,
    .EXT_4444_formats = true,
    .EXT_extended_dynamic_state = true,
-   .EXT_extended_dynamic_state2 = false,
+   .EXT_extended_dynamic_state2 = true,
    .EXT_image_robustness = true,
    .EXT_inline_uniform_block = false,
    .EXT_pipeline_creation_cache_control = false,

Similarly, we need to add a specific function defined in the extension. This function will call the respective function of the underlying driver. Let's use the same vkCmdSetRasterizerDiscardEnable as an example.

Different from before, you can - technically - name your function whatever you want. But is highly recommended to follow the current vn_dispatch_ and vkr_dispatch_ conventions.

The parameters need to always be struct vn_dispatch_context and vn_command_<function name>.

In the case of this function, we just decode (deserialize) the parameters and call the underlying driver function that implements this feature.

As with Mesa, the Venus protocol generates all the repetitive code for us, so vn_command_vkCmdSetRasterizerDiscardEnable and vn_replace_vkCmdSetRasterizerDiscardEnable_args_handle are implemented by it. They are responsible for handling all the parameters and a (possible) reply.

vk->CmdSetRasterizerDiscardEnable is a function pointer to the driver function vkCmdSetRasterizerDiscardEnable. Almost all vk* functions are mapped by the vn_device_proc_table (src/venus/venus-protocol/vn_protocol_renderer_util.h) to bypass the loader whenever possible.

static void
vkr_dispatch_vkCmdSetRasterizerDiscardEnable(struct vn_dispatch_context *dispatch,
                                             struct vn_command_vkCmdSetRasterizerDiscardEnable *args)
{
   struct vkr_command_buffer *cmd = vkr_command_buffer_from_handle(args->commandBuffer);
   struct vn_device_proc_table *vk = &cmd->device->proc_table;

   vn_replace_vkCmdSetRasterizerDiscardEnable_args_handle(args);
   vk->CmdSetRasterizerDiscardEnable(args->commandBuffer, args->rasterizerDiscardEnable);
}

Conclusion

As we can see, the Venus driver is very small (as it should be), and the heavy lifting is done by the host driver (which really implements and runs the things). In general, adding a new extension on Venus is relatively easy with a lot of code provided by the Venus-protocol.

This enables us to implement new extensions in the same way we added this one. So, we can augment libraries like DXVK, ANGLE, and Zink to be able to leverage any 3D API in virtualized environments (as long as it can run Vulkan).

On a parting note, below is a video of Dota2 running inside crosvm, with a Vulkan backend on top of Venus. Enjoy!

Note: The above video was recorded using the GNOME screenshot utility which uses software encode and is very taxing on CPU. Combined with Venus, which taxes the CPU more than a plain Vulkan driver, this results in overall lower FPS.

Comments (2)

  1. Dantali0n:
    Oct 20, 2022 at 07:02 AM

    I am wondering how does this tie into the recently presented zink driver (part of gallium I believe) why do we need a bunch of these translation tools in mesa and what role does each part play. Such an overview of moving parts would be highly beneficial for the community to understand the progress and development within mesa better. Thanks

    Reply to this comment

    Reply to this comment

    1. Igor Torrente:
      Oct 20, 2022 at 03:29 PM

      Hi DantaliOn,

      Zink is conceptually way closer to DXVK and Dozen. Both try to translate one API to another. OpenGL/OpenGLES -> Vulkan, DirectX -> Vulkan, and Vulkan -> DirectX respectively.

      One nice thing that Zink makes possible, is the possibility of a vendor providing only Vulkan implementation and getting OpenGL/OpenGLES "for free".
      If I'm not mistaken, this is the plan of Imagination to support OpenGL/OpenGLES with their mesa driver.

      On the other hand, Venus is way closer to Virgl. Both try to expose GPU-backed Graphics API inside a Virtualized environment.

      We can get OpenGL/Vulkan in a VM using LLVMpipe/Lavapipe. But the performance will not be ideal.

      With that said, you can, for example, run Zink on top of Venus on top of ANV. It looks like something like OpenGL -> Vulkan (Guest) -> Vulkan (Host).

      Reply to this comment

      Reply to this comment


Add a Comment






Allowed tags: <b><i><br>Add a new comment:


Search the newsroom

Latest Blog Posts

Tracing stateless video hardware decoding in V4L2

02/12/2022

Earlier this year, I joined Collabora for a six-month internship to learn how V4L2 (Video4Linux2) supports stateless video hardware decoding,…

From Lua to JSON: refactoring WirePlumber's configuration system

27/10/2022

With the upcoming 0.5 release, WirePlumber's configuration system will be moving to a JSON syntax to define settings, bringing a more unified…

A look at Vulkan extensions in Venus

19/10/2022

Venus is a virtual Vulkan driver based on the Virtio-GPU protocol, which defines the serialization of Vulkan commands between guest and…

Carlafox, an open-source web-based CARLA visualizer

11/10/2022

Taking one step towards democratizing the daunting task of dataset generation by making image synthesis and automatic ground truth data…

Open source machine learning for video compression

14/09/2022

Using open source software, Collabora has developed an efficient compression pipeline that enables a face video broadcasting system that…

Improving Vulkan graphics state tracking in Mesa

07/09/2022

Introducing new common code for Mesa Vulkan drivers to support a new Vulkan extension, making it easier for app and game authors to manage…

Open Since 2005 logo

We use cookies on this website to ensure that you get the best experience. By continuing to use this website you are consenting to the use of these cookies. To find out more please follow this link.

Collabora Ltd © 2005-2022. All rights reserved. Privacy Notice. Sitemap.