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;