Vulkan Learning — 2. Поверхность. Физический прибор. Логический прибор. Очереди команд

Поверхность (Surface)

Следующий шаг после создания Instance — создание поверхности (Surface). Vulkan использует абстракцию Surface для представления нативной оконной подсистемы имеющейся операционной системы. В каждой операционной системе имеется своя функция для создания поверхности и, соответственно, свое расширение (extension), частью которого является эта функция. Для Windows существует расширение VK_KHR_win32_surface. Однако поддержка поверхностей как таковых сама является расширением (поскольку не во всех случаях графику нужно выводить на экран — см. offscreen rendering) — VK_KHR_surface.

// THE FOLLOWING IS PSEUDOCODE!

// Field
vk::SurfaceKHR m_Surface;

// Constructor
m_Surface(m_VkInstance.createWin32SurfaceKHR(
    vk::Win32SurfaceCreateInfoKHR
    {
        .hinstance = ::GetModuleHandle(nullptr),
        .hwnd = windowHandle
    }))

// Destructor
m_VkInstance.destroySurfaceKHR(m_Surface);

Мы замечаем здесь, что для каждого окна программы создается своя поверхность.

На что косвенно влияет созданная поверхность. Далее выбирается физический прибор (physical device) и в нем выбирается Presentation Queue Family (см. ниже). И то, и другое проверяется на совместимость с созданной поверхностью. Соответственно, из всех физических приборов (читай — видеопроцессоров), установленных в системе, для работы выбирается тот, который совместим с данной поверхностью. И среди всех поддерживаемых этим пробором Presentation Queue Families выбирается та, которая совместима с поверхностью. В подтверждение приведу цитату из спецификации:

Not all physical devices will include WSI support. Within a physical device, not all queue families will support presentation.

(Vulkan Speicification)
В следующем листинге показано, как примерно выглядит проверка совместимости прибора и presentation queue family с поверхностью (для ясности из кода удалено всё, не имеющее отношения к Surface):

// THE FOLLOWING IS PSEUDOCODE!

// Field
vk::PhysicalDevice m_PhysicalDevice;

// Constructor
m_PhysicalDevice(PickAppropriatePhysicalDevice())

// Function
vk::PhysicalDevice PickAppropriatePhysicalDevice()
{
    for (const auto& pdev : m_VkInstance.enumeratePhysicalDevices())
    {
        auto presentationQueueFamilyIndex = getPresentationQueueFamilyIndex(pdev, m_Surface);
        if (!presentationQueueFamilyIndex.has_value())
            continue;

        if (pdev.getSurfacePresentModesKHR(m_Surface).empty() ||
            pdev.getSurfaceFormatsKHR(m_Surface).empty())
            continue;

        return pdev;
    }
}

static std::optional<uint32_t> getPresentationQueueFamilyIndex(
    const vk::PhysicalDevice& pdev, const vk::SurfaceKHR& surf)
{
    uint32_t i = 0;
    for (const auto& queueFamily : pdev.getQueueFamilyProperties())
    {
        if (pdev.getSurfaceSupportKHR(i, surf)) return i;
        i++;
    }
    return std::optional<uint32_t>{};
}

// Destructor
m_VkInstance.destroySurfaceKHR(m_Surface);

getSurfacePresentModesKHR() возвращает поддерживаемые presentation modes (fifo, mailbox и пр. — имеют непосредственное отношение к Swapchain — см. далее).
getSurfaceFormatsKHR() возвращает поддерживаемые форматы пикселей (R8G8B8A8 и пр.)

Кто ссылается на поверхность. Только Swapchain (см. далее).

Physical Device

Физический прибор — это, как правило, один из видеопроцессоров (GPU), установленных в системе (во многих настольных ПК есть два GPU — интегрированный и дискретный). Но Vulkan также позволяет выбрать для работы и CPU. Для работы нужно выбрать один из физических приборов, и он обычно должен удовлетворять ряду условий:

  • Поддерживать необходимые расширения уровня прибора (device-level extensions)
  • Поддерживать Graphics Queue Family
  • Поддерживать Presentation Queue Family
  • Быть совместимым с заданной поверхностью (см. выше)
  • Поддерживать дополнительные возможности вроде Geometry Shader или анизотропной фильтрации (см. physicalDevice.getFeatures())
  • Быть интегрированным или дискретным GPU (см. physicalDevice.getProperties().deviceType)

Проверка того, поддерживает ли физический прибор то или иное расширение:

bool IsDeviceExtensionSupported(
    const vk::PhysicalDevice& physicalDevice,
    const char* extensionName)
{
    auto extensions = physicalDevice.enumerateDeviceExtensionProperties();
    return std::find_if(extensions.cbegin(), extensions.cend(),
        [extensionName](const vk::ExtensionProperties& prop) { return std::strcmp(prop.extensionName, extensionName) == 0; })
    != extensions.cend();
}

Для отрисовки графики на экране нужно расширение VK_KHR_swapchain.

Очереди (Queues). Работа приложения с видеопроцессором осуществляется путем отправки ему команд (типичная команда — нарисовать заданный объект). Команды буферизуются, т. е. не отправляются немедленно, а сохраняются в буфере (Command Buffer). Буферы же посылаются видеопроцессору через очереди (Command Queues). Буферы и очереди команд тесно связаны между собой. Команды делятся на несколько классов:

  • graphics commands (рисование)
  • transfer commands (копирование и перемещение данных между различными видами памяти)
  • presentation commands (отправка изображения для отображения)
  • compute commands (GPGPU)

Прибор имеет множество очередей. Очереди делятся на семейства (queue families) — наборы очередей с одинаковыми свойствами (возможностями). Каждое семейство способно выполнять один или несколько перечисленных выше классов команд. Причем семейство, которое поддерживает graphics commands, также по-умолчанию поддерживает transfer commands — так говорится в спецификации. Для отрисовки графики на экране требуются graphics, transfer и presentation commands — для этого требуется по крайней мере одна или две очереди (их принято называть GraphicsQueue и PresentationQueue). Очереди могут использоваться для распараллеливания работы по нескольким потокам. При этом отправлять (submit) буферы команд в конкретную очередь в каждый момент времени должен только один поток. Чтобы получить очередь в свое распоряжение, сначала нужно выбрать семейство (оно идентифицируется целочисленным индексом). Список семейств очередей можно получить путем вызова physicalDevice.getQueueFamilyProperties(). Поддержку семейством тех или иных классов команд можно узнать из поля QueueFamilyProperties::queueFlags. Поддержка семейством presentation commands проверяется для конкретной поверхности (см. выше).

std::optional<uint32_t> getGraphicsQueueFamilyIndex(
    const vk::PhysicalDevice& pdev)
{
    uint32_t i = 0;
    for (const auto& queueFamily : pdev.getQueueFamilyProperties())
    {
        // Queue family is something that contains several queues of commands running in parallel
        if (queueFamily.queueCount > 0 && queueFamily.queueFlags & vk::QueueFlagBits::eGraphics) return i;
        i++;
    }
    return std::optional<uint32_t>{};
}

Индексы выбранных нами семейств очередей нужно сохранить в переменных — далее они понадобятся для создания логического прибора (очереди создаются вместе с ним) и получения ссылок на сами очереди.

Где используется физический прибор.

  • Создание Swapchain: свойства физического прибора влияют на выбор параметров Swapchain. Эти свойства опрашиваются при помощи функций: getSurfaceFormatsKHR, getSurfacePresentModesKHR, getSurfaceCapabilitiesKHR.
  • Создание любых буферов памяти, в том числе буфера глубины (depth buffer), буфера цвета (color buffer), буфера для Uniform Variables и пр. Для выделения памяти необходимо найти подходящий тип этой памяти (а типов памяти, доступных для GPU существует несколько). Для перечисления типов памяти используется функция physicalDevice.getMemoryProperties().

Таким образом мы замечаем, что ссылка на физический прибор нужна для получения различных свойств видеопроцессора (типы памяти, форматы пикселей, выравнивание данных в Dynamic Uniform Buffers и пр.)

Кто ссылается на физический прибор. Только логический прибор (Logical Device).

Логический прибор (Logical Device)

Логический прибор — это объект, который представляет интерфейс между программой и физическим прибором со всеми расширениями и свойствами (features), включенными в данный конкретный интерфейс. Он инкапсулирует очереди команд, содержит адреса функций уровня прибора (device-level functions — см. предыдущий пост), служит для создания разнообразных объектов (семафоров, пайплайнов, сэмплеров и всех остальных объектов, которые используются в процессе работы).

// THE FOLLOWING IS PSEUDOCODE!

// Field
vk::Device m_LogicalDevice;

// Constructor
m_LogicalDevice(CreateLogicalDevice())

// Function
vk::Device CreateLogicalDevice()
{
    // Queues are created automatically along with the device.
    // Each element in the following vector describes a different queue family
    // (we cannot create additional queues or use queues from families we didn’t request).
    std::vector<vk::DeviceQueueCreateInfo> queuesCreateInfo;
    // Create graphics queue
    {
        // if several queues are running in parallel Vulkan uses this
        // information to prioritize queues (lowest=0.0 highest=1.0)
        float queuePriorities = 1.0f;
        vk::DeviceQueueCreateInfo queueCreateInfo
        {
            .queueFamilyIndex = m_GraphicsQueueFamilyIndex, // the index to the family to create the queue from
            .queueCount = 1,                                // number of queues to create
            .pQueuePriorities = &queuePriorities
        };
        queuesCreateInfo.push_back(queueCreateInfo);
    }
    // Create presentation queue (taking into account that it may appear to be the same as the graphics queue)
    if (m_GraphicsQueueFamilyIndex != m_PresentationQueueFamilyIndex)
    {
        float queuePriorities = 1.0f;
        vk::DeviceQueueCreateInfo queueCreateInfo
        {
            .queueFamilyIndex = m_PresentationQueueFamilyIndex, // the index to the family to create the queue from
            .queueCount = 1,                                    // number of queues to create
            .pQueuePriorities = &queuePriorities
        };
        queuesCreateInfo.push_back(queueCreateInfo);
    }

    vk::PhysicalDeviceFeatures deviceFeatures{};
    deviceFeatures.setGeometryShader(VK_TRUE);
    deviceFeatures.setSamplerAnisotropy(VK_TRUE);

    vk::DeviceCreateInfo devCreateInfo
    {
        .queueCreateInfoCount = queuesCreateInfo.size(),
        .pQueueCreateInfos = queuesCreateInfo.data(),
        .enabledExtensionCount = DEVICE_EXTENSIONS.size(),
        .ppEnabledExtensionNames = DEVICE_EXTENSIONS.data(),
        .pEnabledFeatures = &deviceFeatures // physical device features logical device will use
    };

    auto device = m_PhysicalDevice.createDevice(devCreateInfo);
    VULKAN_HPP_DEFAULT_DISPATCHER.init(device);
    return device;
}

// Destructor
// Wait on the host for the completion of outstanding queue
// operations for all queues on a given logical device.
m_LogicalDevice.waitIdle();
m_LogicalDevice.destroy();

Очереди (Command Queues)

Про очереди всё было сказано выше. Они создаются вместе с логическим прибором (в примере выше были созданы одна или две очереди). Затем ссылки на очереди можно получить вызовом Device::getQueue:

// THE FOLLOWING IS PSEUDOCODE!

// Fields
vk::Queue m_GraphicsQueue;
vk::Queue m_PresentationQueue;

// Constructor
m_GraphicsQueue(m_LogicalDevice.getQueue(m_GraphicsQueueFamilyIndex, 0)),
m_PresentationQueue(m_LogicalDevice.getQueue(m_PresentationQueueFamilyIndex, 0))

Продолжение следует…

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *