Vulkan API «Hello Triangle»
Автор: The Player
Статья описывает основные принципы работы с Vulkan API и включает в себя материал по установке SDK, обработке ошибок, работе с различными сущностями API и краткий курс по выводу треугольника, в качестве практической части. Работа позиционирует себя как быстрый экскурс в основы Vulkan API, не претендуя на полноту и оставляя детали на самостоятельное рассмотрение.
Для лучшего понимания материала крайне рекомендуется использовать документацию с официального сайта https://www.khronos.org/registry/vulkan/
Вступление
Подготовка
Установка SDK
Консольный проект
Окно
Обработка ошибок
Результат выполнения
Слои
Фундаментальные объекты
Образец интерфейса
Отчет об ошибках
Физические устройства
Логические устройства
Поверхность вывода и цепочка обмена
Поддержка Win32
Поверхность вывода
Цепочка обмена
Пасс вывода
Буфер кадра и вид изображения
Вид изображения
Буфер кадра
Команды
Очередь команд
Пул команд
Буфер команд
Запись буферов
Ограждение и обновление экрана
Ограждение
Обновление экрана
Буферы и память
Буфер
Память
Передача информации о вершинах на GPU
Шейдеры
Исходный код
Шейдерные модули
Конвейер
Планировка и кэш конвейера
Этапы и состояния графического конвейера
Этапы обработки примитивов
Информация о вершинах
Информация о режиме рисования
Состояние выводимой области
Этап растеризации
Этап множественной выборки
Этап теста глубины и трафарета
Этап смешивания
Создание конвейера
Модификация командных буферов
Вступление
В связи с большим количеством кода присущим Vulkan API, ради облегчения понимания происходящего структура программы не будет разделена на модули или обернута в классы, ровно как и не будет производится освобождение ресурсов и обработка сопутствующих этому процессу ошибок. Статья требует понимания базовых принципов программирования на С/С++ и уровень знакомства с WinAPI достаточный для создания окна и работы с ним. Также необходим опыт в работе со средой разработки Visual Studio 2013. Материалы по этим темам не будут включены в данную работу, т.к. могут быть легко найдены в сети.
Подготовка
Для простейшей демонстрации Vulkan API (вывод треугольника) нам потребуется установить «LunarG Vulkan SDK», создать консоль для вывода ошибок и логов, а также окно WinAPI, в которое бы и осуществлялся вывод.
Установка SDK
Поскольку сам Vulkan API реализован непосредственно на уровне драйвера, доступ к его функционалу может быть получен посредством динамических библиотек (dynamic linked library - dll), специфических для разработчика видеокарты (Intel, NVidia, AMD, etc.). «LunarG Vulkan SDK» - загрузчик, проделывающий эту работу за нас и предоставляющий унифицированный интерфейс. С эго помощью, нам не надо заботиться о том, какую именно dll загружать.
Скачать установщик «LunarG Vulkan SDK» можно на официальном сайте https://vulkan.lunarg.com/. Само собой, для работы с любыми версиями Vulkan API необходим не только SDK, но и соответствующий драйвер, специфический для производителя видеокарты. Также важно то, что имея SDK определенной версии, мы можем ограничить использование драйвера более низкой. При написании статьи использовалась версия SDK 1.0.21.1.
После установки SDK, все необходимые нам файлы могут быть найдены в соответствующем каталоге («C:/VulkanSDK/%версия SDK%» по умолчанию). В первую очередь нас интересуют заголовочные файлы (папка «vulkan» целиком), которые находятся в каталоге «Include» и файлы библиотек (файл «vulkan-1.lib»), в каталоге «Bin»/«Bin32». Имея их на руках, мы можем приступить к созданию консоли и окна.
Консольный проект
Нам потребуется обычный консольный проект для вывода информации обо всем происходящем, а также для оповещения о ошибках. Процесс его создания не будет освещен в статье, т.к. может быть легко найден в сети.
Первым делом, необходимо убедится в работоспособности проекта:
#include <iostream> void main() { system( “pause”); }
Затем, скопировать в папку с проектом (где размещен «*.vcxproj» файл, а не «*.sln») все необходимые нам файлы, т.е. папку заголовочных файлов «vulkan» и библиотечный файл «vulkan-1.lib». Убедимся в работоспособности подключенных файлов:
#include "vulkan/vulkan.h" #pragma comment(lib, "vulkan-1.lib")
На данном этапе окно не является для нас чем-то необходимым, но мы бегло рассмотрим код его создания сейчас, дабы не возвращаться к этой теме во время изучения Vulkan API.
Окно
Для работы с окном нам необходимо подключить WinAPI:
#include <Windows.h>
Также, нам необходимо получить образец (instance) нашей программы (в контексте WinAPI):
auto handleInstance = GetModuleHandleA(nullptr);
окно:
auto handleWindow = [&]() { auto windowClassName = "window class"; { WNDCLASSA windowClassInfo; { windowClassInfo.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; windowClassInfo.lpfnWndProc = DefWindowProcA; windowClassInfo.cbClsExtra = 0; windowClassInfo.cbWndExtra = 0; windowClassInfo.hInstance = handleInstance; windowClassInfo.hIcon = nullptr; windowClassInfo.hCursor = nullptr; windowClassInfo.hbrBackground = nullptr; windowClassInfo.lpszMenuName = nullptr; windowClassInfo.lpszClassName = windowClassName; } if( !RegisterClassA( &windowClassInfo)) { throw std::exception( "failed to register window class"); } } auto handle = CreateWindowA( windowClassName, "window", WS_OVERLAPPEDWINDOW | WS_VISIBLE, 0, 0, 800, 600, nullptr, nullptr, handleInstance, nullptr ); if( !handle) { throw std::exception( "failed to create window"); } return handle; }( );
и обрабатывать сообщения, адресованные этому окну:
while(true) { MSG msg; { while( PeekMessage( &msg, handleWindow, 0, 0, PM_REMOVE)) { TranslateMessage( &msg); DispatchMessage( &msg); } } }
Описание этих функций упускается умышленно, т.к. они не являются предметом данной статьи. Всю необходимую информацию по ним можно легко найти в сети.
Обработка ошибок
В условиях большого количества кода, т.е. в условиях Vulkan API, жизненно необходимо правильно отлавливать ошибки. Важность понимания этого процесса столь велика, что обработка ошибок будет описана в отдельном разделе этой статьи.
Мы не будем рассматривать логические ошибки (вроде передачи в качестве аргумента вектора на 10 элементов, когда в действительности он рассчитан всего лишь на 5), но рассмотрим способы найти их в некоторых ситуациях (в случае передачи несовместимых параметров). Так же мы рассмотрим способы выявления ошибок, которые можно отловить в «инженерной» версии программы, а также те, которые могут различаться для видеокарт разных производителей. В конечном счете, мы получим инструмент для составления детального отчета об ошибках в debug-версии приложения.
Результат выполнения
Простейшим и самым «грубым» способом отлова ошибок в Vulkan API являются возвращаемые значения функций. Они имеют тип VkResult, который позволяет определить успех выполнения (VK_SUCCESS, например, означает отсутствие ошибок). Такой механизм, являясь достаточно простым, способен указать на грубые ошибки или ляпы. Тем не менее, его оказывается недостаточно для детального анализа.
Слои
В большинстве случаев детальный анализ ошибок и большое количество проверок допустимо только в debug-версии приложения. Фокус заключается в том, что release-версия не должна содержать ошибки по определению, а потому и тратить лишние ресурсы на их проверку нет смысла. С другой стороны, debug-версия должна предоставлять не только сам факт наличия ошибки, но и более-менее развернутое описание. К счастью, Vulkan API разрабатывался отталкиваясь именно от такой идеологии.
Слои – набор опциональных модулей, предназначенных для проверки корректности (соответствие стандарту) использования Vulkan API. Их наличие и реализация зависят от версии API и видеокарты, а потому их использование носит отладочный характер – они не включаются в release-версию приложения. Еще одним важным нюансом является то, что для образца и устройства Vulkan API слои различаются.
Доступные для образца слои перечисляются следующим образом:
VkResult vkEnumerateInstanceLayerProperties(uint32_t* pPropertyCount, VkLayerProperties* pProperties);
Если pProperties равно нулю, функция записывает по адресу pPropertyCount количество доступных слоев. В противном случае, функция заполняет данные по адресу pProperties массивом элементов типа VkLayerProperties в количестве, записанном по адресу pPropertyCount.
Доступные для устройства слои перечисляются схожей функцией:
VkResult vkEnumerateDeviceLayerProperties(VkPhysicalDevice physicalDevice, uint32_t* pPropertyCount, VkLayerProperties* pProperties);
Она работает аналогично, с поправкой на то, что нужно указать физическое устройство, о котором мы узнаем позже.
Фундаментальные объекты
Прежде чем приступить к непосредственному выводу графики, нам необходимо создать серию абстрактных объектов, которые являются фундаментом для дальнейшей работы.
В общих чертах наш план выглядит следующим образом:
• Подготовить списки слоев и расширений для образца интерфейса
• Создать образец интерфейса
• Инициализировать получение отчетов об ошибках
• Перечислить все имеющиеся физические устройства
• Подготовить списки слоев и расширений для логического устройства
• На основе любого физического устройства создать логическое устройство
Но для начала, ознакомимся с функционалом.
Образец интерфейса
Первым необходимым нам объектом является образец интерфейса (instance) Vulkan API. Он содержит в себе базовую информацию о нашем приложении, необходимых для его работы слоях и расширениях.
VkResult vkCreateInstance(const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkInstance* pInstance);
Функция принимает информацию о нашем образце, хранящуюся по адресу pCreateInfo, и в случае отсутствия ошибок инициализирует его по адресу pInstance. Параметр pAllocator в пределах этой статьи мы рассматривать не будем, а потому в качестве него всегда будем передавать нулевой указатель.
Структура VkInstanceCreateInfo выглядит следующим образом:
typedef struct VkInstanceCreateInfo { VkStructureType sType; const void* pNext; VkInstanceCreateFlags flags; const VkApplicationInfo* pApplicationInfo; uint32_t enabledLayerCount; const char* const* ppEnabledLayerNames; uint32_t enabledExtensionCount; const char* const* ppEnabledExtensionNames; } VkInstanceCreateInfo;
sType и pNext, ровно как и структуры типа Vk***CreateInfo являются часто встречаемыми объектами Vulkan API. Они служат для минимизации аргументов функций, при передаче параметров. Поле sType говорит о том, к какому типу принадлежит конкретная структура Vk***CreateInfo (для примера, значение sType для структуры типа VkInstanceCreateInfoдолжно быть равно VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO). Поле pNext, в большинстве случаев, является зарезервированным для использования в будущем, потому его следует инициализировать нулевым указателем, кроме исключительных ситуаций. Т.к. подобные конструкции встречаются довольно таки часто, мы больше не будем возвращаться к их описанию.
Поле flags зарезервировано для использования в будущем и должно быть равно нулю. pApplicationInfo – указатель на структуру, содержащую информацию о приложении. enabledLayerCount и ppEnabledLayerNames – количество слоев и указатель на массив их названий. enabledExtensionCount и ppEnabledExtensionNames – количество и названия расширений.
Больше всего здесь нас интересует указатель на структуру с информацией о приложении – pApplicationInfo:
typedef struct VkApplicationInfo { VkStructureType sType; const void* pNext; const char* pApplicationName; uint32_t applicationVersion; const char* pEngineName; uint32_t engineVersion; uint32_t apiVersion; } VkApplicationInfo;
Хотя исходя из спецификации pApplicationInfo при желании можно обнулить, делать этого не стоит (некоторые бессовестные вендоры требуют наличие информации о приложении). Поля pApplicationName и applicationVersion указывают на название приложения и его версию, а pEngineName и engineVersion, соответственно, на название и версию движка. apiVersion содержит информацию о версии Vulkan API используемой в приложении и формируется макросом VK_MAKE_VERSION().
Теперь можно приступать к созданию образца интерфейса. Для начала нужно подготовить списки слоев:
std::vector<const char*> layerNames = { "VK_LAYER_LUNARG_core_validation", };
и расширений:
std::vector<const char*> extensionNames = { VK_EXT_DEBUG_REPORT_EXTENSION_NAME, };
Заполним информацию о приложении:
VkApplicationInfo vk_applicationInfo; { vk_applicationInfo.sType = VkStructureType::VK_STRUCTURE_TYPE_APPLICATION_INFO; vk_applicationInfo.pNext = nullptr; vk_applicationInfo.pApplicationName = "Application name"; vk_applicationInfo.applicationVersion = 1; vk_applicationInfo.pEngineName = "Engine name"; vk_applicationInfo.engineVersion = 1; vk_applicationInfo.apiVersion = VK_MAKE_VERSION(1,0,21); }
И, собственно, информацию для создания образца интерфейса:
VkInstanceCreateInfo vk_instanceCreateInfo; { vk_instanceCreateInfo.sType = VkStructureType::VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; vk_instanceCreateInfo.pNext = nullptr; vk_instanceCreateInfo.flags = 0; vk_instanceCreateInfo.pApplicationInfo = &vk_applicationInfo; vk_instanceCreateInfo.enabledLayerCount = layerNames.size(); vk_instanceCreateInfo.ppEnabledLayerNames = layerNames.data( ); vk_instanceCreateInfo.enabledExtensionCount = extensionNames.size( ); vk_instanceCreateInfo.ppEnabledExtensionNames = extensionNames.data( ); }
Когда все готово, можно создавать образец интерфейса: