Creating a logical device
The logical device is one the most important objects created in our application. It represents real hardware, along with all the extensions and features enabled for it and all the queues requested from it:

The logical device allows us to perform almost all the work typically done in rendering applications, such as creating images and buffers, setting the pipeline state, or loading shaders. The most important ability it gives us is recording commands (such as issuing draw calls or dispatching computational work) and submitting them to queues, where they are executed and processed by the given hardware. After such execution, we acquire the results of the submitted operations. These can be a set of values calculated by compute shaders, or other data (not necessarily an image) generated by draw calls. All this is performed on a logical device, so now we will look at how to create one.
Getting ready
In this recipe, we will use a variable of a custom structure type. The type is called QueueInfo
and is defined as follows:
struct QueueInfo { uint32_t FamilyIndex; std::vector<float> Priorities; };
In a variable of this type, we will store information about the queues we want to request for a given logical device. The data contains an index of a family from which we want the queues to be created, the total number of queues requested from this family, and the list of priorities assigned to each queue. As the number of priorities must be equal to the number of queues requested from a given family, the total number of queues we request from a given family is equal to the number of elements in the Priorities
vector.
How to do it...
- Based on the features, limits, available extensions and supported types of operations, choose one of the physical devices acquired using the
vkEnumeratePhysicalDevices()
function call (refer to Enumerating available physical devices recipe). Take its handle and store it in a variable of typeVkPhysicalDevice
calledphysical_device
. - Prepare a list of device extensions you want to enable. Store the names of the desired extensions in a variable of type
std::vector<char const *>
nameddesired_extensions
.
- Create a variable of type
std::vector<VkExtensionProperties>
namedavailable_extensions
. Acquire the list of all available extensions and store it in theavailable_extensions
variable (refer to Checking available device extensions recipe). - Make sure that the name of each extension from the
desired_extensions
variable is also present in theavailable_extensions
variable. - Create a variable of type
VkPhysicalDeviceFeatures
nameddesired_features
. - Acquire a set of features supported by a physical device represented by the
physical_device
handle and store it in thedesired_features
variable (refer to Getting features and properties of a physical device recipe). - Make sure that all the required features are supported by a given physical device represented by the
physical_device
variable. Do that by checking if the corresponding members of the acquireddesired_features
structure are set to one. Clear the rest of thedesired_features
structure members (set them to zero). - Based on the properties (supported types of operations), prepare a list of queue families, from which queues should be requested. Prepare a number of queues that should be requested from each selected queue family. Assign a priority for each queue in a given family: A floating point value from
0.0f
to1.0f
(multiple queues may have the same priority value). Create astd::vector
variable namedqueue_infos
with elements of a custom typeQueueInfo
. Store the indices of queue families and a list of priorities in thequeue_infos
vector, the size ofPriorities
vector should be equal to the number of queues from each family. - Create a variable of type
std::vector<VkDeviceQueueCreateInfo>
namedqueue_create_infos
. For each queue family stored in thequeue_infos
variable, add a new element to thequeue_create_infos
vector. Assign the following values for members of a new element:VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO
value forsType
.nullptr
value forpNext
.0
value forflags
.- Index of a queue family for
queueFamilyIndex
. - Number of queues requested from a given family for
queueCount
. - Pointer to the first element of a list of priorities of queues from a given family for
pQueuePriorities
.
- Create a variable of type
VkDeviceCreateInfo
nameddevice_create_info
. Assign the following values for members of adevice_create_info
variable:VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO
value forsType
.nullptr
value forpNext
.0
value forflags
.- Number of elements of the
queue_create_infos
vector variable forqueueCreateInfoCount
. - Pointer to the first element of the
queue_create_infos
vector variable inpQueueCreateInfos
. 0
value forenabledLayerCount
.nullptr
value forppEnabledLayerNames
.- Number of elements of the
desired_extensions
vector variable inenabledExtensionCount
. - Pointer to the first element of the
desired_extensions
vector variable (ornullptr
if it is empty) inppEnabledExtensionNames
. - Pointer to the
desired_features
variable inpEnabledFeatures
.
- Create a variable of type
VkDevice
namedlogical_device
. - Call
vkCreateDevice( physical_device, &device_create_info, nullptr, &logical_device )
. Provide a handle of the physical device in the first argument, a pointer to thedevice_create_info
variable in the second argument, anullptr
value in the third argument, and a pointer to thelogical_device
variable in the final argument. - Make sure the operation succeeded by checking that the value returned by the
vkCreateDevice()
function call is equal toVK_SUCCESS
.
How it works...
To create a logical device, we need to prepare a considerable amount of data. First we need to acquire the list of extensions that are supported by a given physical device, and then we need check that all the extensions we want to enable can be found in the list of supported extensions. Similar to Instance creation, we can't create a logical device with extensions that are not supported. Such an operation will fail:
std::vector<VkExtensionProperties> available_extensions; if( !CheckAvailableDeviceExtensions( physical_device, available_extensions ) ) { return false; } for( auto & extension : desired_extensions ) { if( !IsExtensionSupported( available_extensions, extension ) ) { std::cout << "Extension named '" << extension << "' is not supported by a physical device." << std::endl; return false; } }
Next we prepare a vector variable named queue_create_infos
that will contain information about queues and queue families we want to request for a logical device. Each element of this vector is of type VkDeviceQueueCreateInfo
. The most important information it contains is an index of the queue family and the number of queues requested for that family. We can't have two elements in the vector that refer to the same queue family.
In the queue_create_infos
vector variable, we also provide information about queue priorities. Each queue in a given family may have a different priority: A floating-point value between 0.0f
and 1.0f
, with higher values indicating higher priority. This means that hardware will try to schedule operations performed on multiple queues based on this priority, and may assign more processing time to queues with higher priorities. However, this is only a hint and it is not guaranteed. It also doesn't influence queues from other devices:
std::vector<VkDeviceQueueCreateInfo> queue_create_infos; for( auto & info : queue_infos ) { queue_create_infos.push_back( { VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, nullptr, 0, info.FamilyIndex, static_cast<uint32_t>(info.Priorities.size()), info.Priorities.size() > 0 ? &info.Priorities[0] : nullptr } ); };
The queue_create_infos
vector variable is provided to another variable of type VkDeviceCreateInfo
. In this variable, we store information about the number of different queue families from which we request queues for a logical device, number and names of enabled layers, and extensions we want to enable for a device, and also features we want to use.
Layers and extensions are not required for the device to work properly, but there are quite useful extensions, which must be enabled if we want to display Vulkan-generated images on screen.
Features are also not necessary, as the core Vulkan API gives us plenty of features to be able to generate beautiful images or perform complicated calculations. If we don't want to enable any feature, we can provide a nullptr
value for the pEnabledFeatures
member, or provide a variable filled with zeros. However, if we want to use more advanced features, such as geometry or tessellation shaders, we need to enable them by providing a pointer to a proper variable, previously acquiring the list of supported features, and making sure the ones we need are available. Unnecessary features can (and even should) be disabled, because there are some features that may impact performance. This situation is very rare, but it's good to bear this in mind. In Vulkan, we should do and use only those things that need to be done and used:
VkDeviceCreateInfo device_create_info = { VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, nullptr, 0, static_cast<uint32_t>(queue_create_infos.size()), queue_create_infos.size() > 0 ? &queue_create_infos[0] : nullptr, 0, nullptr, static_cast<uint32_t>(desired_extensions.size()), desired_extensions.size() > 0 ? &desired_extensions[0] : nullptr, desired_features };
The device_create_info
variable is provided to the vkCreateDevice()
function, which creates a logical device. To be sure that the operation succeeded, we need to check that the value returned by the vkCreateDevice()
function call is equal to VK_SUCCESS
. If it is, the handle of a created logical device is stored in the variable pointed to by the final argument of the function call:
VkResult result = vkCreateDevice( physical_device, &device_create_info, nullptr, &logical_device ); if( (result != VK_SUCCESS) || (logical_device == VK_NULL_HANDLE) ) { std::cout << "Could not create logical device." << std::endl; return false; } return true;
See also
The following recipes in this chapter:
- Enumerating available physical devices
- Checking available device extensions
- Getting features and properties of a physical device
- Checking available queue families and their properties
- Selecting the index of a queue family with the desired capabilities
- Destroying a logical device