Base object structs

The Vulkan runtime code provides a set of base object structs which must be used if you want your driver to take advantage of any of the runtime code. There are other base structs for various things which are not covered here but those are optional. The ones covered here are the bare minimum set which form the core of the Vulkan runtime code:

As one might expect, vk_instance is the required base struct for implementing VkInstance, vk_physical_device is required for VkPhysicalDevice, and vk_device for VkDevice. Everything else must derive from vk_object_base or from some struct that derives from vk_object_base.

vk_object_base

The root base struct for all Vulkan objects is vk_object_base. Every object exposed to the client through the Vulkan API must inherit from vk_object_base by having a vk_object_base or some struct that inherits from vk_object_base as the driver struct’s first member. Even though we have container_of() and use it liberally, the vk_object_base should be the first member as there are a few places, particularly in the logging framework, where we use void pointers to avoid casting and this only works if the address of the driver struct is the same as the address of the vk_object_base.

The standard pattern for defining a Vulkan object inside a driver looks something like this:

struct drv_sampler {
   struct vk_object_base base;

   /* Driver fields */
};

VK_DEFINE_NONDISP_HANDLE_CASTS(drv_sampler, base, VkSampler,
                               VK_OBJECT_TYPE_SAMPLER);

Then, to the object in a Vulkan entrypoint,

VKAPI_ATTR void VKAPI_CALL drv_DestroySampler(
    VkDevice                                    _device,
    VkSampler                                   _sampler,
    const VkAllocationCallbacks*                pAllocator)
{
   VK_FROM_HANDLE(drv_device, device, _device);
   VK_FROM_HANDLE(drv_sampler, sampler, _sampler);

   if (!sampler)
      return;

   /* Tear down the sampler */

   vk_object_free(&device->vk, pAllocator, sampler);
}

The VK_DEFINE_NONDISP_HANDLE_CASTS() macro defines a set of type-safe cast functions called drv_sampler_from_handle() and drv_sampler_to_handle() which cast a VkSampler to and from a struct drv_sampler *. Because compile-time type checking with Vulkan handle types doesn’t always work in C, the _from_handle() helper uses the provided VkObjectType to assert at runtime that the provided handle is the correct type of object. Both cast helpers properly handle NULL and VK_NULL_HANDLE as inputs. The VK_FROM_HANDLE() macro provides a convenient way to declare a drv_foo pointer and initialize it from a VkFoo handle in one smooth motion.

struct vk_object_base

Base struct for all Vulkan objects

VkObjectType type

Type of this object

This is used for runtime type checking when casting to and from Vulkan handle types since compile-time type checking doesn’t always work.

struct vk_device *device

Pointer to the device in which this object exists, if any

This is NULL for instances and physical devices but should point to a valid vk_device for almost everything else. (There are a few WSI objects that don’t inherit from a device.)

struct vk_instance

Pointer to the instance in which this object exists

This is NULL for device level objects as it’s main purpose is to make the instance allocator reachable for freeing data owned by instance level objects.

void vk_object_base_init(struct vk_device *device, struct vk_object_base *base, VkObjectType obj_type)

Initialize a vk_base_object

Parameters:
  • device[in] The vk_device this object was created from or NULL

  • base[out] The vk_object_base to initialize

  • obj_type[in] The VkObjectType of the object being initialized

void vk_object_base_finish(struct vk_object_base *base)

Tear down a vk_object_base

Parameters:
  • base[out] The vk_object_base being torn down

VK_DEFINE_HANDLE_CASTS(__driver_type, __base, __VkType, __VK_TYPE)

Define handle cast macros for the given dispatchable handle type

For a given driver_struct, this defines driver_struct_to_handle() and driver_struct_from_handle() helpers which provide type-safe (as much as possible with Vulkan handle types) casts to and from the driver_struct type. As an added layer of protection, these casts use the provided VkObjectType to assert that the object is of the correct type when running with a debug build.

Parameters:
  • __driver_type – The name of the driver struct; it is assumed this is the name of a struct type and struct will be prepended automatically

  • __base – The name of the vk_base_object member

  • __VkType – The Vulkan object type such as VkImage

  • __VK_TYPE – The VkObjectType corresponding to __VkType, such as VK_OBJECT_TYPE_IMAGE

VK_DEFINE_NONDISP_HANDLE_CASTS(__driver_type, __base, __VkType, __VK_TYPE)

Define handle cast macros for the given non-dispatchable handle type

For a given driver_struct, this defines driver_struct_to_handle() and driver_struct_from_handle() helpers which provide type-safe (as much as possible with Vulkan handle types) casts to and from the driver_struct type. As an added layer of protection, these casts use the provided VkObjectType to assert that the object is of the correct type when running with a debug build.

Parameters:
  • __driver_type – The name of the driver struct; it is assumed this is the name of a struct type and struct will be prepended automatically

  • __base – The name of the vk_base_object member

  • __VkType – The Vulkan object type such as VkImage

  • __VK_TYPE – The VkObjectType corresponding to __VkType, such as VK_OBJECT_TYPE_IMAGE

VK_FROM_HANDLE(__driver_type, __name, __handle)

Declares a __driver_type pointer which represents __handle

Parameters:
  • __driver_type – The name of the driver struct; it is assumed this is the name of a struct type and struct will be prepended automatically

  • __name – The name of the declared pointer

  • __handle – The Vulkan object handle with which to initialize __name

vk_instance

struct vk_instance

Base struct for all VkInstance implementations

This contains data structures necessary for detecting enabled extensions, handling entrypoint dispatch, and implementing vkGetInstanceProcAddr(). It also contains data copied from the VkInstanceCreateInfo such as the application information.

VkAllocationCallbacks alloc

Allocator used when creating this instance

This is used as a fall-back for when a NULL pAllocator is passed into a device-level create function such as vkCreateImage().

struct vk_app_info app_info

VkInstanceCreateInfo::pApplicationInfo

const struct vk_instance_extension_table *supported_extensions

Table of all supported instance extensions

This is the static const struct passed by the driver as the supported_extensions parameter to vk_instance_init().

struct vk_instance_extension_table enabled_extensions

Table of all enabled instance extensions

This is generated automatically as part of vk_instance_init() from VkInstanceCreateInfo::ppEnabledExtensionNames.

struct vk_instance_dispatch_table dispatch_table

Instance-level dispatch table

struct [anonymous]

List of all physical devices and callbacks

This is used for automatic physical device creation, deletion and enumeration.

VkResult (*enumerate)(struct vk_instance *instance)

Enumerate physical devices for this instance

The driver can implement this callback for custom physical device enumeration. The returned value must be a valid return code of vkEnumeratePhysicalDevices.

Note that the loader calls vkEnumeratePhysicalDevices of all installed ICDs and fails device enumeration when any of the calls fails. The driver should return VK_SUCCESS when it does not find any compatible device.

If this callback is not set, try_create_for_drm will be used for enumeration.

VkResult (*try_create_for_drm)(struct vk_instance *instance, struct _drmDevice *device, struct vk_physical_device **out)

Try to create a physical device for a drm device

The returned value must be a valid return code of vkEnumeratePhysicalDevices, or VK_ERROR_INCOMPATIBLE_DRIVER. When VK_ERROR_INCOMPATIBLE_DRIVER is returned, the error and the drm device are silently ignored.

void (*destroy)(struct vk_physical_device *pdevice)

Handle the destruction of a physical device

This callback has to be implemented when using common physical device management. The device pointer and any resource allocated for the device should be freed here.

uint64_t trace_mode

Enabled tracing modes

VkResult vk_instance_init(struct vk_instance *instance, const struct vk_instance_extension_table *supported_extensions, const struct vk_instance_dispatch_table *dispatch_table, const VkInstanceCreateInfo *pCreateInfo, const VkAllocationCallbacks *alloc)

Initialize a vk_instance

Along with initializing the data structures in vk_instance, this function validates the Vulkan version number provided by the client and checks that every extension specified by VkInstanceCreateInfo::ppEnabledExtensionNames is actually supported by the implementation and returns VK_ERROR_EXTENSION_NOT_PRESENT if an unsupported extension is requested.

Parameters:
  • instance[out] The instance to initialize

  • supported_extensions[in] Table of all instance extensions supported by this instance

  • dispatch_table[in] Instance-level dispatch table

  • pCreateInfo[in] VkInstanceCreateInfo pointer passed to vkCreateInstance()

  • alloc[in] Allocation callbacks used to create this instance; must not be NULL

void vk_instance_finish(struct vk_instance *instance)

Tears down a vk_instance

Parameters:
  • instance[out] The instance to tear down

Once a driver has a vk_instance, implementing all the various instance-level vkGet*ProcAddr() entrypoints is trivial:

VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL
drv_GetInstanceProcAddr(VkInstance _instance,
                        const char *pName)
{
   VK_FROM_HANDLE(vk_instance, instance, _instance);
   return vk_instance_get_proc_addr(instance,
                                    &drv_instance_entrypoints,
                                    pName);
}

PUBLIC VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL
vk_icdGetInstanceProcAddr(VkInstance instance,
                          const char *pName)
{
   return drv_GetInstanceProcAddr(instance, pName);
}
PFN_vkVoidFunction vk_instance_get_proc_addr(const struct vk_instance *instance, const struct vk_instance_entrypoint_table *entrypoints, const char *name)

Implementaiton of vkGetInstanceProcAddr()

PFN_vkVoidFunction vk_instance_get_proc_addr_unchecked(const struct vk_instance *instance, const char *name)

Unchecked version of vk_instance_get_proc_addr

This is identical to vk_instance_get_proc_addr() except that it doesn’t check whether extensions are enabled before returning function pointers. This is useful in window-system code where we may use extensions without the client explicitly enabling them.

PFN_vkVoidFunction vk_instance_get_physical_device_proc_addr(const struct vk_instance *instance, const char *name)

Implementaiton of vk_icdGetPhysicalDeviceProcAddr()

We also provide an implementation of vkEnumerateInstanceExtensionProperties() which can be used similarly:

VKAPI_ATTR VkResult VKAPI_CALL
drv_EnumerateInstanceExtensionProperties(const char *pLayerName,
                                         uint32_t *pPropertyCount,
                                         VkExtensionProperties *pProperties)
{
   if (pLayerName)
      return vk_error(NULL, VK_ERROR_LAYER_NOT_PRESENT);

   return vk_enumerate_instance_extension_properties(
      &instance_extensions, pPropertyCount, pProperties);
}
VkResult vk_enumerate_instance_extension_properties(const struct vk_instance_extension_table *supported_extensions, uint32_t *pPropertyCount, VkExtensionProperties *pProperties)

Implementaiton of vkEnumerateInstanceExtensionProperties()

vk_physical_device

struct vk_physical_device

Base struct for all VkPhysicalDevice implementations

struct vk_instance *instance

Instance which is the parent of this physical device

struct vk_device_extension_table supported_extensions

Table of all supported device extensions

This table is initialized from the supported_extensions parameter passed to vk_physical_device_init() if not NULL. If a NULL extension table is passed, all extensions are initialized to false and it’s the responsibility of the driver to populate the table. This may be useful if the driver’s physical device initialization order is such that extension support cannot be determined until significant physical device setup work has already been done.

struct vk_features supported_features

Table of all supported features

This table is initialized from the supported_features parameter passed to vk_physical_device_init() if not NULL. If a NULL features table is passed, all features are initialized to false and it’s the responsibility of the driver to populate the table. This may be useful if the driver’s physical device initialization order is such that feature support cannot be determined until significant physical device setup work has already been done.

struct vk_properties properties

Table of all physical device properties which is initialized similarly to supported_features

struct vk_physical_device_dispatch_table dispatch_table

Physical-device-level dispatch table

struct disk_cache *disk_cache

Disk cache, or NULL

struct wsi_device *wsi_device

WSI device, or NULL

const struct vk_sync_type *const *supported_sync_types

A null-terminated array of supported sync types, in priority order

The common implementations of VkFence and VkSemaphore use this list to determine what vk_sync_type to use for each scenario. The list is walked and the first vk_sync_type matching their criterion is taken. For instance, VkFence requires that it not be a timeline and support reset and CPU wait. If an external handle type is requested, that is considered just one more criterion.

const struct vk_pipeline_cache_object_ops *const *pipeline_cache_import_ops

A null-terminated array of supported pipeline cache object types

The common implementation of VkPipelineCache uses this to remember the type of objects stored in the cache and deserialize them immediately when importing the cache. If an object type isn’t in this list, then it will be loaded as a raw data object and then deserialized when we first look it up. Deserializing immediately avoids a copy but may be more expensive for objects that aren’t hit.

VkResult vk_physical_device_init(struct vk_physical_device *physical_device, struct vk_instance *instance, const struct vk_device_extension_table *supported_extensions, const struct vk_features *supported_features, const struct vk_properties *properties, const struct vk_physical_device_dispatch_table *dispatch_table)

Initialize a vk_physical_device

Parameters:
  • physical_device[out] The physical device to initialize

  • instance[in] The instance which is the parent of this physical device

  • supported_extensions[in] Table of all device extensions supported by this physical device

  • supported_features[in] Table of all features supported by this physical device

  • dispatch_table[in] Physical-device-level dispatch table

void vk_physical_device_finish(struct vk_physical_device *physical_device)

Tears down a vk_physical_device

Parameters:
  • physical_device[out] The physical device to tear down

vk_device

struct vk_device

Base struct for VkDevice

VkAllocationCallbacks alloc

Allocator used to create this device

This is used as a fall-back for when a NULL pAllocator is passed into a device-level create function such as vkCreateImage().

struct vk_physical_device

Pointer to the physical device

struct vk_device_extension_table enabled_extensions

Table of enabled extensions

struct vk_features enabled_features

Table of enabled features

struct vk_device_dispatch_table dispatch_table

Device-level dispatch table

const struct vk_device_dispatch_table *command_dispatch_table

Command dispatch table

This is used for emulated secondary command buffer support. To use emulated (trace/replay) secondary command buffers:

  1. Provide your “real” command buffer dispatch table here. Because this doesn’t get populated by vk_device_init(), the driver will have to add the vk_common entrypoints to this table itself.

  2. Add vk_enqueue_unless_primary_device_entrypoint_table to your device level dispatch table.

const struct vk_command_buffer_ops *command_buffer_ops

Command buffer vtable when using the common command pool

const struct vk_device_shader_ops *shader_ops

Shader vtable for VK_EXT_shader_object and common pipelines

VkResult (*capture_trace)(VkQueue queue)

Driver provided callback for capturing traces

Triggers for this callback are:
  • Keyboard input (F12)

  • Creation of a trigger file

  • Reaching the trace frame

VkResult (*check_status)(struct vk_device *device)

Checks the status of this device

This is expected to return either VK_SUCCESS or VK_ERROR_DEVICE_LOST. It is called before vk_queue::driver_submit and after every non-trivial wait operation to ensure the device is still around. This gives the driver a hook to ask the kernel if its device is still valid. If the kernel says the device has been lost, it MUST call vk_device_set_lost().

This function may be called from any thread at any time.

VkResult (*create_sync_for_memory)(struct vk_device *device, VkDeviceMemory memory, bool signal_memory, struct vk_sync **sync_out)

Creates a vk_sync that wraps a memory object

This is always a one-shot object so it need not track any additional state. Since it’s intended for synchronizing between processes using implicit synchronization mechanisms, no such tracking would be valid anyway.

If signal_memory is set, the resulting vk_sync will be used to signal the memory object from a queue via vk_queue_submit::signals. The common code guarantees that, by the time vkQueueSubmit() returns, the signal operation has been submitted to the kernel via the driver’s vk_queue::driver_submit hook. This means that any vkQueueSubmit() call which needs implicit synchronization may block.

If signal_memory is not set, it can be assumed that memory object already has a signal operation pending from some other process and we need only wait on it.

struct vk_pipeline_cache

Implicit pipeline cache, or NULL

enum vk_device_timeline_mode

An enum describing how timeline semaphores work

enumerator VK_DEVICE_TIMELINE_MODE_NONE

Timeline semaphores are not supported

enumerator VK_DEVICE_TIMELINE_MODE_EMULATED

Timeline semaphores are emulated with vk_timeline

In this mode, timeline semaphores are emulated using vk_timeline which is a collection of binary semaphores, one per time point. These timeline semaphores cannot be shared because the data structure exists entirely in userspace. These timelines are virtually invisible to the driver; all it sees are the binary vk_syncs, one per time point.

To handle wait-before-signal, we place all vk_queue_submits in the queue’s submit list in vkQueueSubmit() and call vk_device_flush() at key points such as the end of vkQueueSubmit() and vkSemaphoreSignal(). This ensures that, as soon as a given submit’s dependencies are fully resolvable, it gets submitted to the driver.

enumerator VK_DEVICE_TIMELINE_MODE_ASSISTED

Timeline semaphores are a kernel-assisted emulation

In this mode, timeline semaphores are still technically an emulation in the sense that they don’t support wait-before-signal natively. Instead, all GPU-waitable objects support a CPU wait-for-pending operation which lets the userspace driver wait until a given event on the (possibly shared) vk_sync is pending. The event is “pending” if a job has been submitted to the kernel (possibly from a different process) which will signal it. In vkQueueSubit, we use this wait mode to detect waits which are not yet pending and, the first time we do, spawn a thread to manage the queue. That thread waits for each submit’s waits to all be pending before submitting to the driver queue.

We have to be a bit more careful about a few things in this mode. In particular, we can never assume that any given wait operation is pending. For instance, when we go to export a sync file from a binary semaphore, we need to first wait for it to be pending. The spec guarantees that the vast majority of these waits return almost immediately, but we do need to insert them for correctness.

enumerator VK_DEVICE_TIMELINE_MODE_NATIVE

Timeline semaphores are 100% native

In this mode, wait-before-signal is natively supported by the underlying timeline implementation. We can submit-and-forget and assume that dependencies will get resolved for us by the kernel. Currently, this isn’t supported by any Linux primitives.

enum vk_queue_submit_mode submit_mode

Per-device submit mode

This represents the device-wide submit strategy which may be different from the per-queue submit mode. See vk_queue.submit.mode for more details.

VkResult vk_device_init(struct vk_device *device, struct vk_physical_device *physical_device, const struct vk_device_dispatch_table *dispatch_table, const VkDeviceCreateInfo *pCreateInfo, const VkAllocationCallbacks *alloc)

Initialize a vk_device

Along with initializing the data structures in vk_device, this function checks that every extension specified by VkInstanceCreateInfo::ppEnabledExtensionNames is actually supported by the physical device and returns VK_ERROR_EXTENSION_NOT_PRESENT if an unsupported extension is requested. It also checks all the feature struct chained into the pCreateInfo->pNext chain against the features returned by vkGetPhysicalDeviceFeatures2 and returns VK_ERROR_FEATURE_NOT_PRESENT if an unsupported feature is requested.

Parameters:
  • device[out] The device to initialize

  • physical_device[in] The physical device

  • dispatch_table[in] Device-level dispatch table

  • pCreateInfo[in] VkDeviceCreateInfo pointer passed to vkCreateDevice()

  • alloc[in] Allocation callbacks passed to vkCreateDevice()

void vk_device_finish(struct vk_device *device)

Tears down a vk_device

Parameters:
  • device[out] The device to tear down