Vulkan Learning — 1. Установка. Инициализация. Instance. Отладка

Недавно я завершил прохождение видеокурса по Vulkan, и спешу законспектировать информацию, пока она не выветрилась из моей дырявой головы.

Литература

Установка Vulkan SDK

Скачивается с сайта LunarG. Кроссплатформенный SDK, переносим на разные платформы на уровне исходного кода (в коде присутствуют директивы условной компиляции). Включает в частности следующее:

  • vulkan.hpp — поставляется в виде исходного кода на C++. Находится в папке C:\VulkanSDK\1.2.___._\Include\vulkan
  • glslc.exe — компилятор языка GLSL в промежуточный код SPIR-V. Находится в папке C:\VulkanSDK\1.2.___._\Bin

Библиотека функций Vulkan API vulkan-1.dll не входит в Vulkan SDK, а поставляется вместе с операционной системой. Находится в папке C:\Windows\System32

Инициализация Vulkan

Библиотека функций Vulkan API (vulkan-1.dll) загружается динамически, затем в ней ищутся адреса функций Vulkan API. За загрузку отвечает класс vk::DynamicLoader (все упоминаемые здесь и далее классы из Vulkan SDK находятся в файле vulkan.hpp). За поиск и хранение адресов функций отвечает класс DispatchLoaderDynamic (точнее, его экземпляр — глобальная переменная под названием VULKAN_HPP_DEFAULT_DISPATCHER).

Первое, что нужно сделать — это загрузить библиотеку и найти адреса некоторых функций:

void InitializeVulkan()
{
    // Vulkan API library is loaded dynamically in the constructor of the
    // DynamicLoader class and a handle to the library is holded in it.
    static vk::DynamicLoader dl;

    static bool uninitialized = true;
    if (uninitialized)
    {
        // vkGetInstanceProcAddr is the only function that is guarateed to be available
        // by calling an OS-specific GetProcAddress() function in Windows or dlsym() in Linux.
        PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = dl.getProcAddress<PFN_vkGetInstanceProcAddr>("vkGetInstanceProcAddr");

        // This is to initialize defaultDispatchLoaderDynamic - a global object that
        // loads Vulkan API functions and holds the pointers to those functions.
        VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr);

        uninitialized = false;
    }
}

VULKAN_HPP_DEFAULT_DISPATCHER.init() загрузит следующие функции:

  • vkCreateInstance
  • vkEnumerateInstanceExtensionProperties
  • vkEnumerateInstanceLayerProperties
  • vkEnumerateInstanceVersion

Однако это еще не всё. Функции Vulkan API разделяются на три слоя: глобальные (которые мы загрузили выше), Instance-level functions, Device-level functions (сущности Instance и Device будут рассмотрены ниже). Поэтому загружать адреса соответствующих функций придется в последствии — после создания объектов Instance и Device:

vk::Instance CreateInstance()
{
    vk::InstanceCreateInfo instInfo { ... };
    vk::Instance instance = vk::createInstance(instInfo);

    // Load instance-level functions' entry points
    VULKAN_HPP_DEFAULT_DISPATCHER.init(instance);

    return instance;
}

vk::Device CreateLogicalDevice()
{
    vk::DeviceCreateInfo devCreateInfo { ... };
    auto device = m_PhysicalDevice.createDevice(devCreateInfo);

    // Loads device-level function's entry points. A good thing to do if you use just one logical device.
    // When we acquire device-level procedures using this function we in fact acquire addresses of a
    // simple "jump" functions. These functions take the handle of a logical device and jump to a
    // proper implementation (function implemented for a specific device). The overhead of this jump
    // can be avoided. The recommended behavior is to load procedures for each device separately using
    // another function (vkGetDeviceProcAddr).
    // See https://software.intel.com/content/www/us/en/develop/articles/api-without-secrets-introduction-to-vulkan-part-1.html
    VULKAN_HPP_DEFAULT_DISPATCHER.init(device);

    return device;
}

Создание vkInstance

Насколько я понимаю, объект Instance — это некое олицетворение драйвера Vulkan, который будет загружен в память при создании данного объекта.

Creating an instance initializes the loader. The loader also loads and initializes the low-level graphics driver, usually provided by the vendor of the GPU hardware.

(Create a Vulkan Instance)

Создание Instance включает следующие компоненты:

  • ApplicationInfo — структура описывает минимально необходимую версию Vulkan
  • Extensions — коллекция расширений Vulkan, которые должен поддерживать драйвер
  • Layers — коллекция слоев, которые будут включены

Расширение — набор «суперспособностей» драйвера, которые не входят в базовую спецификацию Vulkan, но которые нужны для работы программы. Суперспособности включают в себя поддержку драйвером некоторых нестандартных функций API и структур данных. Например, расширение VK_KHR_win32_surface добавляет в API функцию vkCreateWin32SurfaceKHR, которая необходима для отображения графики в Windows.

Слой. Если расширения добавляют новые функции в API, то слои — изменяют поведение существующих функций API путем hook’ания исходных точек входа этих функций (извините, не знаю как перевести это). Например, слой VK_LAYER_KHRONOS_validation добавляет в функции API проверку валидности параметров функций и состояния объектов, т. е. служит эдаким оператором assert внутри функций. Хорошее описание того, как работают слои: Brief guide to Vulkan layers. Зачем нужна концепция слоев: некоторую функциональность (читай, некоторые слои) следует включать только в отладочной версии программы, в релизной же версии эта функциональность лишь создает излишний overhead.

// THE FOLLOWING IS PSEUDOCODE!

// Class field
vk::Instance m_VkInstance;

// Constructor
m_VkInstance(CreateInstance())

// Function
vk::Instance CreateInstance()
{
    vk::ApplicationInfo appInfo
    {
        .pApplicationName = "VulkanLearning",           // custom name of the application
        .applicationVersion = VK_MAKE_VERSION(1, 0, 0), // custom version of the application
        .pEngineName = "OpenRenderingEngine",           // custom engine name
        .engineVersion = VK_MAKE_VERSION(1, 0, 0),      // custom engine version
        .apiVersion = VK_API_VERSION_1_2                // minimal required Vulkan version
    };

    std::vector<const char*> extensions
    {
        VK_EXT_DEBUG_UTILS_EXTENSION_NAME, // For Debug version only
        VK_KHR_SURFACE_EXTENSION_NAME,
        VK_KHR_WIN32_SURFACE_EXTENSION_NAME
    };
    // Check that the extensions above are supported. Use vkEnumerateInstanceExtensionProperties() for that.

    std::vector<const char*> layers
    {
        "VK_LAYER_KHRONOS_validation"      // For Debug version only
    };
    // Check that the layers above are supported. Use vkEnumerateInstanceLayerProperties() for that.

    vk::InstanceCreateInfo instInfo
    {
        .pApplicationInfo = &appInfo,
        .enabledLayerCount = layers.size(),
        .ppEnabledLayerNames = layers.data(),
        .enabledExtensionCount = extensions.size(),
        .ppEnabledExtensionNames = extensions.data()
    };
    auto instance = vk::createInstance(instInfo);

    VULKAN_HPP_DEFAULT_DISPATCHER.init(instance);
    return instance;
}

Где используется Instance. При создании поверхности (Surface), перечислении физических приборов (GPU) и при настройке отладочных сообщений.

Отладочные сообщения

Расширение VK_EXT_debug_utils и слой VK_LAYER_KHRONOS_validation позволяют нам принимать отладочные сообщения. Для этого нужно создать callback-функцию:

static VKAPI_ATTR VkBool32 VKAPI_CALL DebugCallback(
    VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
    VkDebugUtilsMessageTypeFlagsEXT messageType,
    const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
    void* pUserData)
{
    // VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT - Diagnostic message
    // VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT    - Informational message like the creation of a resource
    // VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT - Message about behavior that is not necessarily an error, but very likely a bug in your application
    // VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT   - Message about behavior that is invalid and may cause crashes
    // Source: https://vulkan-tutorial.com/en/Drawing_a_triangle/Setup/Validation_layers
    // if the message is severe enough...
    if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT)
        std::cerr << pCallbackData->pMessage << std::endl << std::endl;

    return VK_FALSE;
}

Чтобы подцепить данную функцию к Vulkan, надо создать объект DebugUtilsMessengerEXT:

// THE FOLLOWING IS PSEUDOCODE!

// Class field
vk::DebugUtilsMessengerEXT m_DebugMessenger;

// Constructor
m_DebugMessenger(m_VkInstance.createDebugUtilsMessengerEXT(DebugMessengerCreateInfo()))

// Function
vk::DebugUtilsMessengerCreateInfoEXT DebugMessengerCreateInfo()
{
    return vk::DebugUtilsMessengerCreateInfoEXT
    {
        .messageSeverity = vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose
                         | vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning
                         | vk::DebugUtilsMessageSeverityFlagBitsEXT::eError,

        .messageType = vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral
                     | vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation
                     | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance,

        .pfnUserCallback = DebugCallback,
        .pUserData = this
    };
}

Очистка

В конце мы должны уничтожить созданные нами объекты в порядке обратном тому, в котором мы их создали:

// Destructor
m_VkInstance.destroyDebugUtilsMessengerEXT(m_DebugMessenger);
m_VkInstance.destroy();

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

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

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