Initialising a Physical Device#

A physical device is an object in Vulkan that represents the GPU that the application uses. The application must be aware of the rendering hardware it will use. This object can then be queried by the application for properties and features in the GPU, including:

  • Whether the device is discrete (external) or integrated.

  • Device limits such as the maximum allowed image dimensions.

  • Support for features such as geometry shaders.

  • Image and formats support.

  • Types and numbers of memory heaps.

  • Types and numbers of queues and queue families.

  • Memory alignment requirements.

In the case where multiple GPUs are present, the properties and features of the device can be used to filter the available GPUs, based on the application requirements. Commonly, in more complex applications, this could be used to populate a list so the user could select among the available GPUs.

In the example code, initPhysicalDevice(), the application retrieves a list of all available GPUs to the instance, then iterates through them to retrieve their properties. For this application, there is no real requirement, so any GPU can be selected. For more complex applications, this is a good way of ensuring the hardware is capable of performing the tasks required. View in context.

void VulkanHelloAPI::initPhysicalDevice()
{
    // This will hold the number of GPUs available.
    uint32_t gpuCount;

    // Query for the number of GPUs available.
    debugAssertFunctionResult(vk::EnumeratePhysicalDevices(appManager.instance, &gpuCount, nullptr), "GPUS Enumeration - Get Count");

    // Resize the GPUs vector to match the number of GPUs available.
    appManager.gpus.resize(gpuCount);

    // Populate the vector with a list of GPUs available on the platform.
    debugAssertFunctionResult(vk::EnumeratePhysicalDevices(appManager.instance, &gpuCount, appManager.gpus.data()), "GPUS Enumeration - Allocate Data");

    // Log some properties for each of the available physical devices.
    Log(false, "%s", "------------Properties for Physical Devices--------------");
    for (const auto& device : appManager.gpus)
    {
        // General device properties like vendor and driver version.
        VkPhysicalDeviceProperties deviceProperties;
        vk::GetPhysicalDeviceProperties(device, &deviceProperties);

        Log(false, "Device Name: %s", deviceProperties.deviceName);
        Log(false, "Device ID: 0x%X", deviceProperties.deviceID);
        Log(false, "Device Driver Version: 0x%X", deviceProperties.driverVersion);
        Log(false, "%s", "--------------------------------------");

        // Features are more in-depth information that is not needed right now so these are not outputted.
        VkPhysicalDeviceFeatures deviceFeatures;
        vk::GetPhysicalDeviceFeatures(device, &deviceFeatures);
    }

    // Get the device compatible with the needs of the application using a custom helper function.
    // The physical device is also queried for its memory properties which will be used later when allocating memory for buffers.
    appManager.physicalDevice = getCompatibleDevice();
    vk::GetPhysicalDeviceMemoryProperties(appManager.physicalDevice, &appManager.deviceMemoryProperties);

    // Get the compatible device's properties.
    // These properties will be used later when creating the surface and swapchain objects.
    vk::GetPhysicalDeviceProperties(appManager.physicalDevice, &appManager.deviceProperties);
}

getCompatibleDevice() is a helper method that finds and selects a physical device from the list of available GPUs. View in context.

VkPhysicalDevice VulkanHelloAPI::getCompatibleDevice()
{
    for (const auto& device : appManager.gpus)
    {
        VkPhysicalDeviceProperties deviceProperties;
        VkPhysicalDeviceFeatures deviceFeatures;
        vk::GetPhysicalDeviceProperties(device, &deviceProperties);
        vk::GetPhysicalDeviceFeatures(device, &deviceFeatures);

        // Return the first device which is either a discrete GPU or an integrated GPU.
        if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU || deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU)
        {
            Log(false, "Active Device is -- %s", deviceProperties.deviceName);
            return device;
        }
    }

    // If there is only one device, then return that one.
    if (appManager.gpus.size() == 1) { return appManager.gpus[0]; }

    // Return null if nothing is found.
    return nullptr;
}

Identifying Queue Families#

As part of the initialisation of the physical device, the application also needs to outline its queue families. The concept of queues and command buffering has been outlined already in Vulkan Concepts. These queue families are directly tied to the physical device capabilities, so it is best to define them now so that they can be accessed later on during queue initialisation.

The code example uses initQueuesFamilies() to do this. There is also another helper method, getCompatibleQueueFamilies to filter the list of available queue families.

View initQueuesFamilies() in context.

View getCompatibleQueueFamilies in context.

void VulkanHelloAPI::initQueuesFamilies(
{
    // This will hold the number of queue families available.
    uint32_t queueFamiliesCount;

    // Get the number of queue families the physical device supports.
    vk::GetPhysicalDeviceQueueFamilyProperties(appManager.physicalDevice, &queueFamiliesCount, nullptr);

    // Resize the vector to fit the number of supported queue families.
    appManager.queueFamilyProperties.resize(queueFamiliesCount);

    // Load the queue families data from the physical device to the list.
    vk::GetPhysicalDeviceQueueFamilyProperties(appManager.physicalDevice, &queueFamiliesCount, &appManager.queueFamilyProperties[0]);

    // Get the indices of compatible queue families.
    getCompatibleQueueFamilies(appManager.graphicsQueueFamilyIndex, appManager.presentQueueFamilyIndex);
}

void VulkanHelloAPI::getCompatibleQueueFamilies(uint32_t& graphicsfamilyindex, uint32_t& presentfamilyindex)
{
    int i = 0;
    VkBool32 compatible = VK_FALSE;

    // Check if the family has queues, and that they are graphical and not compute queues.
    for (const auto& queuefamily : appManager.queueFamilyProperties)
    {
        // Look for a graphics queue.
        if (queuefamily.queueCount > 0 && queuefamily.queueFlags & VK_QUEUE_GRAPHICS_BIT)
        {
            graphicsfamilyindex = i;
            break;
        }
        i++;
    }
    i = 0;

    // Check if the family has queues, and that they are graphical and not compute queues.
    for (const auto& queuefamily : appManager.queueFamilyProperties)
    {
        if (queuefamily.queueCount > 0 && queuefamily.queueFlags & VK_QUEUE_GRAPHICS_BIT)
        {
            // Check if the queue family supports presenting.
            debugAssertFunctionResult(vk::GetPhysicalDeviceSurfaceSupportKHR(appManager.physicalDevice, i, appManager.surface, &compatible), "Querying Physical Device Surface Support");
            if (compatible)
            {
                presentfamilyindex = i;
                break;
            }
        }
        i++;
    }
}