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_vk_objet_base or from some struct that derives from vk_vk_objet_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

Public Members

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.)

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)

static inline struct __driver_type *                                    \

__driver_type ## _from_handle(__VkType _handle)                         \

{                                                                       \

struct

vk_object_base *base = (struct vk_object_base

*)_handle;      \

vk_object_base_assert_valid(base, __VK_TYPE);                        \

STATIC_ASSERT(offsetof(struct __driver_type, __base) == 0);          \

return (struct __driver_type *) base;                                \

}                                                                       \

\

static inline __VkType                                                  \

__driver_type ## _to_handle(struct __driver_type *_obj)                 \

{                                                                       \

vk_object_base_assert_valid(&_obj->__base, __VK_TYPE);               \

if (_obj != NULL)                                                    \

_obj->__base.client_visible = true;                               \

return (__VkType) _obj;                                              \

}


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)

static inline struct __driver_type *                                    \

__driver_type ## _from_handle(__VkType _handle)                         \

{                                                                       \

struct

vk_object_base

*base =                                        \

(struct

vk_object_base

*)(uintptr_t)_handle;                      \

vk_object_base_assert_valid(base, __VK_TYPE);                        \

STATIC_ASSERT(offsetof(struct __driver_type, __base) == 0);          \

return (struct __driver_type *)base;                                 \

}                                                                       \

\

static inline __VkType                                                  \

__driver_type ## _to_handle(struct __driver_type *_obj)                 \

{                                                                       \

vk_object_base_assert_valid(&_obj->__base, __VK_TYPE);               \

if (_obj != NULL)                                                    \

_obj->__base.client_visible = true;                               \

return (__VkType)(uintptr_t) _obj;                                   \

}


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)    struct __driver_type *__name = __driver_type ## _from_handle(__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.

Public Members

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

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

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);

PUBLIC VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL
vk_icdGetInstanceProcAddr(VkInstance instance,
                          const char *pName)
{
   return drv_GetInstanceProcAddr(instance, pName);
}

PUBLIC VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL
vk_icdGetPhysicalDeviceProcAddr(VkInstance  _instance,
                                const char* pName);

PUBLIC VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL
vk_icdGetPhysicalDeviceProcAddr(VkInstance  _instance,
                                const char* pName)
{
   VK_FROM_HANDLE(vk_instance, instance, _instance);
   return vk_instance_get_physical_device_proc_addr(instance, pName);
}

The prototypes for the vk_icd* versions are needed because those are not actually defined in the Vulkan headers and you need the prototype somewhere to get the C compiler to not complain. These are all implemented by wrapping one of the provided vk_instance_get*_proc_addr() functions.

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

Public Members

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_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_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

  • 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

Public Types

enum vk_device_timeline_mode

An enum describing how timeline semaphores work

Values:

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.

Public Members

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 *physical

Pointer to the physical device

struct vk_device_extension_table enabled_extensions

Table of enabled extensions

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.

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.

void (*ref_pipeline_layout)(struct vk_device *device, VkPipelineLayout layout)

Increments the reference count on a pipeline layout

This is required for vk_enqueue_CmdBindDescriptorSets() to avoid use-after-free problems with pipeline layouts. If you’re not using the command queue, you can ignore this.

void (*unref_pipeline_layout)(struct vk_device *device, VkPipelineLayout layout)

Decrements the reference count on a pipeline layout

See ref_pipeline_layout above.

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