Недавно я завершил прохождение видеокурса по Vulkan, и спешу законспектировать информацию, пока она не выветрилась из моей дырявой головы.
Литература
- Vulkan Tutorial
- LunarG Vulkan Samples Tutorial
- API without Secrets: Introduction to Vulkan by Pavel Lapinski
- Understanding Vulkan Objects
- Vulkan Guide
- Writing an efficient Vulkan renderer
- Samsung GameDev Resources
- Vulkan Specification
- Мой репозиторий для поделок на 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).
Первое, что нужно сделать — это загрузить библиотеку и найти адреса некоторых функций:
{
// 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::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.
Создание 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.
// 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-функцию:
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:
// 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
};
}
Очистка
В конце мы должны уничтожить созданные нами объекты в порядке обратном тому, в котором мы их создали:
m_VkInstance.destroyDebugUtilsMessengerEXT(m_DebugMessenger);
m_VkInstance.destroy();
Продолжение следует…