Igor Torrente
October 19, 2022
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. |
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.
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.
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 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 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.
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.
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.
<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).
$ 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.
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,
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); }
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.
08/10/2024
Having multiple developers work on pre-merge testing distributes the process and ensures that every contribution is rigorously tested before…
15/08/2024
After rigorous debugging, a new unit testing framework was added to the backend compiler for NVK. This is a walkthrough of the steps taken…
01/08/2024
We're reflecting on the steps taken as we continually seek to improve Linux kernel integration. This will include more detail about the…
27/06/2024
With each board running a mainline-first Linux software stack and tested in a CI loop with the LAVA test framework, the Farm showcased Collabora's…
26/06/2024
WirePlumber 0.5 arrived recently with many new and essential features including the Smart Filter Policy, enabling audio filters to automatically…
12/06/2024
Part 3 of the cmtp-responder series with a focus on USB gadgets explores several new elements including a unified build environment with…
Comments (6)
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
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
DocMAX:
Feb 13, 2023 at 05:18 AM
Please help me setting up Venus. I am lost thanks!
Reply to this comment
Reply to this comment
Igor Torrente:
Feb 16, 2023 at 02:55 PM
Hi DocMAX
You can read our documentation[1]. It will help you to set up a development environment to test Venus.
1 - https://gitlab.collabora.com/virgl-es/documentation/-/blob/master/dev_qemu_crosvm.md
Reply to this comment
Reply to this comment
Bigfoot29:
May 01, 2023 at 04:27 PM
It would be really really great, if that documentation would be available without the need to have an collabora gitlab account. *hint* *hint* :)
Reply to this comment
Reply to this comment
Igor Torrente:
May 03, 2023 at 03:53 PM
Sorry about that. You can use https://gitlab.freedesktop.org/virgl/virglrenderer/-/wikis/dev_qemu_crosvm
Reply to this comment
Reply to this comment
Add a Comment