Physical Devices and Logical Devices
Once we have created a Vulkan instance, we can use two objects to interact with our implementation. These objects are queues and devices. This chapter is going to focus on the two types of devices: physical and logical. A physical device is a single component in the system. It can also be multiple components working in conjunction to function like a single device. A logical device is basically our interface with the physical device.
Physical Devices
A VkPhysicalDevice is a data type that we will use to represent each piece of hardware. There's not much to say here other than we will pass a pointer to an array to the implementation. The implementation will then write handles for each physical device in the system to said array. You can find more information on physical devices here.
Enumerating Physical Devices
To get the handles of all the physical devices in the system, we can call use vkEnumeratePhysicalDevices. We will call it twice. First, we'll pass in NULL as the last parameter. This will allow us to get pPhysicalDeviceCount out by passing in the address to a variable for the second argument. After that, we can allocate the memory necessary to store the pPhysicalDevices and call it with that variable as the last argument.
Definition for vkEnumeratePhysicalDevices:
VkResult vkEnumeratePhysicalDevices(
VkInstance instance,
uint32_t* pPhysicalDeviceCount,
VkPhysicalDevice* pPhysicalDevices);
Documentation for vkEnumeratePhysicalDevices:
instanceis a handle to a Vulkan instance previously created withvkCreateInstance.pPhysicalDeviceCountis a pointer to an integer related to the number of physical devices available or queried.pPhysicalDevicesis eitherNULLor a pointer to an array ofVkPhysicalDevicestructures.
Before we create allocate memory to store the physical devices, we need to figure out how many there are. We can do this by calling vkEnumeratePhysicalDevices with a value of NULL for pPhysicalDevices.
Usage for vkEnumeratePhysicalDevices:
uint32_t deviceCount = 0;
VkResult result = vkEnumeratePhysicalDevices(instance, &deviceCount, NULL);
We should make two assertions.
- The function call was successful
assert(result == VK_SUCCESS);
- We found one or more Vulkan compatible device:
assert(deviceCount >= 1);
Following the usage guidelines outlined in the specification, a second call to vkEnumeratePhysicalDevices with error checking would look like this:
std::vector<VkPhysicalDevice> physicalDevices(deviceCount);
result = vkEnumeratePhysicalDevices(instance, &deviceCount, physicalDevices.data());
assert(result == VK_SUCCESS);
For now, we will simply choose the first device (at index 0) in the array of physicalDevices.
Physical Device Properties
VkPhysicalDeviceProperties is a data type that we will use to represent properties of each physical device. There's not much to say here other than we will pass a pointer of this type to the implementation. The implementation will then write properties for the specified VkPhysicalDevice.
Definition for VkPhysicalDeviceProperties:
typedef struct VkPhysicalDeviceProperties {
uint32_t apiVersion;
uint32_t driverVersion;
uint32_t vendorID;
uint32_t deviceID;
VkPhysicalDeviceType deviceType;
char deviceName[VK_MAX_PHYSICAL_DEVICE_NAME_SIZE];
uint8_t pipelineCacheUUID[VK_UUID_SIZE];
VkPhysicalDeviceLimits limits;
VkPhysicalDeviceSparseProperties sparseProperties;
} VkPhysicalDeviceProperties;
Documentation for `VkPhysicalDeviceProperties:
apiVersionis the version of Vulkan supported by the device (encoded).driverVersionis the vendor-specified version of the driver.vendorIDis a unique identifier for the vendor of the physical device.deviceIDis a unique identifier for the physical device among devices available from the vendor.deviceTypeis aVkPhysicalDeviceTypespecifying the type of device.deviceNameis a null-terminated UTF-8 string containing the name of the device.pipelineCacheUUIDis an array of sizeVK_UUID_SIZE, containing 8-bit values that represent a universally unique identifier for the device.limitsis theVkPhysicalDeviceLimitsstructure which specifies device-specific limits of the physical device.sparsePropertiesis theVkPhysicalDeviceSparsePropertiesstructure which specifies various sparse related properties of the physical device.
And, just for reference, the definition for VkPhysicalDeviceType looks like this:
typedef enum VkPhysicalDeviceType {
VK_PHYSICAL_DEVICE_TYPE_OTHER = 0,
VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU = 1,
VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU = 2,
VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU = 3,
VK_PHYSICAL_DEVICE_TYPE_CPU = 4,
} VkPhysicalDeviceType;
This may be useful if you are trying to detect (for example) if you have an integrated GPU versus a discrete GPU.
Getting Physical Device Properties
A call to vkGetPhysicalDeviceProperties can be useful if you are interested in retrieving information about the physical devices in the system. It will tell you API version, driver version, limitations, sparse properties, etc.
Definition for vkGetPhysicalDeviceProperties:
void vkGetPhysicalDeviceProperties(
VkPhysicalDevice physicalDevice,
VkPhysicalDeviceProperties* pProperties);
Documentation for vkGetPhysicalDeviceProperties:
instanceis a handle to a Vulkan instance previously created withvkCreateInstance.pPhysicalDeviceCountis a pointer to an integer related to the number of physical devices available or queried.pPhysicalDevicesis eitherNULLor a pointer to an array ofVkPhysicalDevicestructures.
Usage for vkGetPhysicalDeviceProperties:
VkPhysicalDeviceProperties physicalProperties = {};
for (uint32_t i = 0; i < deviceCount; i++)
vkGetPhysicalDeviceProperties(physicalDevices[i], &physicalProperties);
We can output some useful parts of the information using this piece of code in the loop above:
fprintf(stdout, "Device Name: %s\n", physicalProperties.deviceName);
fprintf(stdout, "Device Type: %d\n", physicalProperties.deviceType);
fprintf(stdout, "Driver Version: %d\n", physicalProperties.driverVersion);
As I mentioned before, the API version is encoded. So if we want, we can use three macros that will help make it human readable:
VK_VERSION_MAJOR(version)VK_VERSION_MINOR(version)VK_VERSION_PATCH(version)
So, to output the API version, you can use this:
fprintf(stdout, "API Version: %d.%d.%d\n",
VK_VERSION_MAJOR(physicalProperties.apiVersion),
VK_VERSION_MINOR(physicalProperties.apiVersion),
VK_VERSION_PATCH(physicalProperties.apiVersion));
Device Queue Create Information
The next step is to create a device using vkCreateDevice. However, in order to do that, we must have a VkDeviceCreateInfo object. And, as you may have guessed having seen the specification, we need a VkDeviceQueueCreateInfo object.
Definition for VkDeviceQueueCreateInfo:
typedef struct VkDeviceQueueCreateInfo {
VkStructureType sType;
const void* pNext;
VkDeviceQueueCreateFlags flags;
uint32_t queueFamilyIndex;
uint32_t queueCount;
const float* pQueuePriorities;
} VkDeviceQueueCreateInfo;
Documentation for VkDeviceQueueCreateInfo:
sTypeis the type of this structure.pNextis NULL or a pointer to an extension-specific structure.flagsis reserved for future use.queueFamilyIndexis an unsigned integer indicating the index of the queue family to create on this device. This index corresponds to the index of an element of thepQueueFamilyPropertiesarray that was returned byvkGetPhysicalDeviceQueueFamilyProperties.queueCountis an unsigned integer specifying the number of queues to create in the queue family indicated byqueueFamilyIndex.pQueuePrioritiesis an array ofqueueCountnormalized floating point values, specifying priorities of work that will be submitted to each created queue.
Usage for VkDeviceQueueCreateInfo:
float priorities[] = { 1.0f };
VkDeviceQueueCreateInfo queueInfo{};
queueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueInfo.pNext = NULL;
queueInfo.flags = 0;
queueInfo.queueFamilyIndex = 0;
queueInfo.queueCount = 1;
queueInfo.pQueuePriorities = &priorities[0];
You'll note that we create a float array with a single value. Each value in that array will tell the implementation the priority of the queue. Values must be between 0.0 and 1.0. Certain implementations will give higher-priority queues more processing time. However, this is not necessarily true because the specification doesn't require this behavior.
Device Create Info
The parent of VkDeviceQueueCreateInfo is VkDeviceCreateInfo.
Definition for VkDeviceCreateInfo:
typedef struct VkDeviceCreateInfo {
VkStructureType sType;
const void* pNext;
VkDeviceCreateFlags flags;
uint32_t queueCreateInfoCount;
const VkDeviceQueueCreateInfo* pQueueCreateInfos;
uint32_t enabledLayerCount;
const char* const* ppEnabledLayerNames;
uint32_t enabledExtensionCount;
const char* const* ppEnabledExtensionNames;
const VkPhysicalDeviceFeatures* pEnabledFeatures;
} VkDeviceCreateInfo;
Documentation for VkDeviceCreateInfo:
sTypeis the type of this structure.pNextisNULLor a pointer to an extension-specific structure.flagsis reserved for future use.queueCreateInfoCountis the unsigned integer size of thepQueueCreateInfosarray.pQueueCreateInfosis a pointer to an array ofVkDeviceQueueCreateInfostructures describing the queues that are requested to be created along with the logical device.enabledLayerCountis the number of device layers to enable.ppEnabledLayerNamesis a pointer to an array ofenabledLayerCountnull-terminated UTF-8 strings containing the names of layers to enable for the created device.enabledExtensionCountis the number of device extensions to enable.ppEnabledExtensionNamesis a pointer to an array ofenabledExtensionCountnull-terminated UTF-8 strings containing the names of extensions to enable for the created device.pEnabledFeaturesis NULL or a pointer to aVkPhysicalDeviceFeaturesstructure that contains boolean indicators of all the features to be enabled.
Usage for VkDeviceCreateInfo:
std::vector<const char *> enabledExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
VkDeviceCreateInfo deviceInfo{};
deviceInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceInfo.pNext = NULL;
deviceInfo.flags = 0;
deviceInfo.queueCreateInfoCount = 1;
deviceInfo.pQueueCreateInfos = &queueInfo;
deviceInfo.enabledExtensionCount = enabledExtensions.size();
deviceInfo.ppEnabledExtensionNames = enabledExtensions.data();
deviceInfo.pEnabledFeatures = NULL;
Creating a Device
Finally, to wrap up this section, we need to create a logical device. We'll use the vkCreateDevice.
Definition for vkCreateDevice:
VkResult vkCreateDevice(
VkPhysicalDevice physicalDevice,
const VkDeviceCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkDevice* pDevice);
Documentation for vkCreateDevice:
physicalDevicemust be one of the device handles returned from a call tovkEnumeratePhysicalDevices.pCreateInfois a pointer to aVkDeviceCreateInfostructure containing information about how to create the device.pAllocatorcontrols host memory allocation.pDevicepoints to a handle in which the createdVkDeviceis returned.
Usage for vkCreateDevice:
VkResult result = vkCreateDevice(physicalDevice, &deviceInfo,
NULL, &logicalDevice);
assert(result != VK_SUCCESS);