ПрограммированиеСтатьиГрафика

Vulkan API «Hello Triangle»

Автор:

Статья описывает основные принципы работы с 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();
}

Когда все готово, можно создавать образец интерфейса:

if(vkCreateInstance(&vk_instanceCreateInfo, nullptr, &vk_instance) != VkResult::VK_SUCCESS)
{
  throw std::exception("failed to create vk instance");
}

Отчет об ошибках

Отчет об ошибках — мощный инструмент, который позволяет получать информацию от слоев, используя функцию обратного вызова (callback).
Как и любой другой объект Vulkan API, отчет рассматривается как объект и имеет соответствующие функции для создания и структуры, для описания. Отличие в том, что функцию для создания отчета мы будем получать с помощью vkGetInstanceProcAddr:

auto vk_vkCreateDebugReportCallbackEXT = 
  (PFN_vkCreateDebugReportCallbackEXT)vkGetInstanceProcAddr(vk_instance,"vkCreateDebugReportCallbackEXT");

Полученная нами функция имеет следующий вид:

VkResult vkCreateDebugReportCallbackEXT (
  VkInstance instance,
  const VkDebugReportCallbackCreateInfoEXT* pCreateInfo,
  const VkAllocationCallbacks* pAllocator,
  VkDebugReportCallbackEXT* pCallback);

Она, не нарушая традиций, инициализирует объект, опираясь на информацию из описания и сигнализирует о любых ошибках посредством возвращаемого значения. Единственное отличие – теперь нужно указывать образец интерфейса, к которому будет привязан наш отчет.

typedef struct VkDebugReportCallbackCreateInfoEXT {
    VkStructureType      sType;
    const void*        pNext;
    VkDebugReportFlagsEXT    flags;
    PFN_vkDebugReportCallbackEXT  pfnCallback;
    void*          pUserData;
} VkDebugReportCallbackCreateInfoEXT;

flags состоит из битовых операций над значениями enum-а VkDebugReportFlagBitsEXT. Эго значение говорит о том, при какого рода отчетах будет вызываться наша функция. pfnCallback, соответственно, указывает на то, какую функцию следует вызывать, а с помощью pUserData мы можем передавать в отчет какую-нибудь информацию извне.

Итак, преступим к созданию отчета:

VkDebugReportCallbackEXT vk_debugReportCallbackEXT;
{
  VkDebugReportCallbackCreateInfoEXT vk_debugReportCallbackCreateInfoEXT;
  {
    vk_debugReportCallbackCreateInfoEXT.sType = VkStructureType::VK_STRUCTURE_TYPE_DEBUG_REPORT_CALLBACK_CREATE_INFO_EXT;
    vk_debugReportCallbackCreateInfoEXT.pNext = nullptr;
    vk_debugReportCallbackCreateInfoEXT.flags =
VkDebugReportFlagBitsEXT::VK_DEBUG_REPORT_INFORMATION_BIT_EXT |
      VkDebugReportFlagBitsEXT::VK_DEBUG_REPORT_WARNING_BIT_EXT |
VkDebugReportFlagBitsEXT::VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT |
      VkDebugReportFlagBitsEXT::VK_DEBUG_REPORT_ERROR_BIT_EXT |
      VkDebugReportFlagBitsEXT::VK_DEBUG_REPORT_DEBUG_BIT_EXT;
    vk_debugReportCallbackCreateInfoEXT.pfnCallback = [](
      VkDebugReportFlagsEXT    flags,
      VkDebugReportObjectTypeEXT    objectType,
      uint64_t        object,
      size_t          location,
      int32_t          messageCode,
      const char*        pLayerPrefix,
      const char*        pMessage,
      void*          pUserData) -> VkBool32
    {
      std::cout << "(";
      if((flags & VkDebugReportFlagBitsEXT::VK_DEBUG_REPORT_INFORMATION_BIT_EXT) != 0) std::cout << "INFO";
      if((flags & VkDebugReportFlagBitsEXT::VK_DEBUG_REPORT_WARNING_BIT_EXT) != 0) std::cout << "WARNING";
      if((flags & VkDebugReportFlagBitsEXT::VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT) != 0) std::cout << "PERFORMANCE";
      if((flags & VkDebugReportFlagBitsEXT::VK_DEBUG_REPORT_DEBUG_BIT_EXT) != 0) std::cout << "DEBUG";
      if((flags & VkDebugReportFlagBitsEXT::VK_DEBUG_REPORT_ERROR_BIT_EXT) != 0) std::cout << "ERROR";
      std::cout << ")";
      std::cout << "{" << pLayerPrefix << "} " << pMessage << std::endl;
      return VK_FALSE;
    };
    vk_debugReportCallbackCreateInfoEXT.pUserData = nullptr;
  }
  if(vk_vkCreateDebugReportCallbackEXT(
vk_instance,
&vk_debugReportCallbackCreateInfoEXT,
nullptr,
&vk_debugReportCallbackEXT) != VkResult::VK_SUCCESS)
  {
    throw std::exception("failed to create debug collback");
  }
}

Последним и наиболее важным моментом является вид функции-обработчика отчетов – pfnCallback. Обработчик вызывается каждый раз, когда один из слоев информирует или предупреждает пользователя о наличии ошибки. Через параметры передается разного рода информация. Пока что нас будет интересовать только имя слоя, который вызвал который отправил отчет - pLayerPrefix и текст отчета - pMessage. Как видно, наша лямбда-функция попросту выводит сообщения отчета на консоль.

Физические устройства

Имея образец интерфейса, мы можем перечислить все физические устройства (видеокарты, если вам угодно), с Vulkan API на борту. Подобное перечисление производит следующая функция:

VkResult vkEnumeratePhysicalDevices(
    VkInstance    instance,
    uint32_t*    pPhysicalDeviceCount,
    VkPhysicalDevice*  pPhysicalDevices);

Как и в предыдущих случаях, она принимает в качестве аргументов образец интерфейсаinstance, указатель на количество физических устройств к перечислению – pPhysicalDeviceCount и указатель на массив, для записи информации об устройствахpPhysicalDevices. Если последние параметр равен нулевому указателю – функция запишет по адресу pPhysicalDeviceCount количество доступных физических устройств. В противном случае – функция заполнит массив информацией о соответствующих устройствах.

Как не трудно догадаться, физических устройств может быть несколько (например, интегрированная и дискретная видеокарты на ноутбуках). Поскольку нам нужно лишь одно, мы всегда будем выбирать первой попавшееся.

VkPhysicalDevice vk_physicalDevice;
{
  uint32_t vk_physicalDevicesCount;
  if(vkEnumeratePhysicalDevices(vk_instance, &vk_physicalDevicesCount, nullptr) != VkResult::VK_SUCCESS)
  {
    throw std::exception("failed to get physical devices count");
  }
  std::vector<VkPhysicalDevice> vk_physicalDevices(vk_physicalDevicesCount);
  if(vkEnumeratePhysicalDevices(vk_instance, &vk_physicalDevicesCount, vk_physicalDevices.data()) != VkResult::VK_SUCCESS)
  {
    throw std::exception("failed to get physical devices");
  }
  if(vk_physicalDevices.empty())
  {
    throw std::exception("no physical devices");
  }
  vk_physicalDevice = vk_physicalDevices[0];
}

А теперь интересный момент: некоторые слои могут ругаться, если при дальнейшей работе мы не перечислим свойства семейств очередей физического устройства. Вообще говоря, наше приложение должно опираться на эту информацию, но т.к. материал всего лишь обучающий, мы умышленно упустим это из виду и просто перечислим свойства:

std::vector<VkQueueFamilyProperties> vk_physicalDeviceQueueFamilyProperties;
{
  uint32_t vk_physicalDeviceQueueFamilyPropertiesCount;
  vkGetPhysicalDeviceQueueFamilyProperties(vk_physicalDevice, &vk_physicalDeviceQueueFamilyPropertiesCount, nullptr);

  vk_physicalDeviceQueueFamilyProperties.resize(vk_physicalDeviceQueueFamilyPropertiesCount);
  vkGetPhysicalDeviceQueueFamilyProperties(vk_physicalDevice, &vk_physicalDeviceQueueFamilyPropertiesCount, vk_physicalDeviceQueueFamilyProperties.data());
}
Страницы: 1 2 3 4 5 6 7 Следующая »

#Vulkan, #основы

15 сентября 2016

Комментарии [69]