Рубрики
3D мир Программирование

Vulkan API 3: связь с окном

В данной заметке рассматривается связь VulkanAPI и GLFW

Введение

В первой заметке было создано окно GLFW, предназначенное для работы с Vulkan API, а во второй была проинициализирована библиотека вулкана. Следующим шагом является настройка процесса передачи изображения, так как показ результата рендеринга — не часть базового API.

Данная заметка разделена на две части:

В конце каждого этапа в репозитории заметки создается релиз. Рассмотрим перечисленные этапы подробнее.

Создание поверхности окна

Поверхность (surface) — объект, в который производится рендеринг Vulkan API.

Для осуществления возможности использовать поверхности необходимо осуществить подключение расширения VK_KHR_surface на уровне экземпляра Vulkan. Данное расширение уже включено в список требуемых для GLFW, получаемый при создании экземпляра.

В зависимости от платформы требуются дополнительные расширения:

  • VK_USE_PLATFORM_ANDROID_KHR — Android;
  • VK_USE_PLATFORM_WAYLAND_KHR — Wayland;
  • VK_USE_PLATFORM_WIN32_KHR — Windows;
  • VK_USE_PLATFORM_XCB_KHR — X Window System, с использованием библиотеки XCB;
  • VK_USE_PLATFORM_XLIB_KHR — X Window System, с использованием библиотеки Xlib.

GLFW в зависимости от платформы подключает нужное дополнительное расширение.

Добавим поле класса Vulkan для хранения созданной поверхности:

VkSurfaceKHR surface;

Добавим к классу Vulkan метод createWindowSurface для создания поверхности окна.

Дальнейшие действия являются платформозависимыми. Рассмотрим платформу Microsoft Windows.

Для создания поверхности необходимо заполнить структуру VkWin32SurfaceCreateInfoKHR. Рассмотрим поля структуры:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
  • pNext — указатель на расширение данной структуры (не используется в заметке);
  • flags — для данной структуры не существует флагов (= 0);
  • hinstance — дескриптор приложения winapi;
  • hwnd — дескриптор окна winapi.

После заполнения необходимо вызвать функцию vkCreateWin32SurfaceKHR.

В рамках библиотеки GLFW существует более простое решение в виде функции glfwCreateWindowSurface, которая не требует заполнения вышеупомянутой структуры и является кроссплатформенным решением. Данная функция принимает в качестве параметров: экземпляр Vulkan, адрес объекта структуры окна GLFW (GLFWwindow*), адрес аллокатора и адрес объекта структуры VkSurfaceKHR . Для корректного вызова передадим адрес GLFWwindow* в качестве параметра метода Vulkan::init и Vulkan::createWindowSurface.

В результате метод создания поверхности имеет следующий вид:

void Vulkan::createWindowSurface(GLFWwindow* window)
{
    if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) 
    {
        throw std::runtime_error("Unable to create window surface");
    }
}

Необходимо дополнить метод Vulkan::destroy вызовом функции vkDestroySurfaceKHR для уничтожения поверхности окна. Данная функция принимает в качестве аргументов экземпляр Vulkan, поверхность окна и адрес алокатора.

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

Текущий вариант доступен на теге v0.1 репозитория 03.

Создание списка показа

Для того чтобы отобразить что-либо на созданной поверхности окна необходимо создать список (цепочку) показа (swap chain). Список показа является кольцевой очередью из одного или нескольких изображений для вывода на экран.

Для использования этих списков требуется подключить расширение VK_KHR_swapchain. Данное расширение подключается на уровне устройства. Добавим данное расширение в список необходимых расширений для устройств в метод Vulkan::init.

Цепочки очередей определяются следующими зависимыми от устройства характеристиками:

  • информация о поверхности — VkSurfaceCapabilitiesKHR;
  • формат поверхности — VkSurfaceFormatKHR;
  • режимы показа — VkPresentModeKHR.

Структура VkSurfaceCapabilitiesKHR содержит общую информацию о поверхности:

  • minImageCount/maxImageCount — минимальное/максимальное количество изображений;
  • minImageExtent/currentExtent/maxImageExtent — минимальные/текущие/максимальные размеры поверхности;
  • maxImageArrayLayers — максимальное число слоев в массиве изображений (если поверхность поддерживает показ из массивов изображений);
  • supportedTransforms — множество доступных преобразований;
  • currentTransform — активные преобразования;
  • supportedCompositeAlpha — поддержка композитинга (совмещения);
  • supportedUsageFlags — предполагаемое использование.

Структура VkSurfaceFormatKHR содержит информацию о представлении поверхности в памяти (формате):

  • format — формат цвета пикселя VkFormat, который является перечислением (enum);
  • colorSpace — цветовое пространство VkColorSpaceKHR, которое является перечислением (enum).

VkPresentModeKHR является перечислением (enum) и содержит информацию о поддерживаемых режимах показа поверхности.

Эти данные пригодятся при создании списка показа. Для повторного использования создадим структуру Surface, которая будет содержать поверхность и данные о ней.

typedef struct _Surface
{
    VkSurfaceKHR surface; // Поверхность окна
    VkSurfaceCapabilitiesKHR capabilities; // общая информация
    std::vector<VkSurfaceFormatKHR> formats; // формат поверхности
    std::vector<VkPresentModeKHR> presentModes; // режим показа
} Surface;

Заменим тип VkSurfaceKHR поля класса Vulkan::surface на Surface и дополним все упоминания surface.

Данные структуры необходимо проанализировать на этапе выбора физического устройства. Для этого требуется передать поверхность в функцию selectPhysicalDeviceByProperties.

Вызовем функцию vkGetPhysicalDeviceSurfaceCapabilitiesKHR, которая заполняет информацию о поверхности. Данная функция в качестве аргументов принимает физическое устройство, созданную ранее поверхность и адрес объекта структуры для заполнения.

Для заполнения массивов формата и режимов показа выполняется два вызова для получения размера необходимого массива и последующего заполнения.

Функция vkGetPhysicalDeviceSurfaceFormatsKHR служит для заполнения массива поддерживаемых форматов поверхности и принимает в качестве параметров физическое устройство, созданную ранее поверхность и адрес переменной под количество форматов и указатель на массив структуры VkSurfaceFormatKHR.

Для заполнения массива режимов показа служит функция vkGetPhysicalDeviceSurfacePresentModesKHR, которая принимает в качестве параметров физическое устройство, созданную ранее поверхность и адрес переменной под количество режимов и указатель на массив перечислений VkPresentModeKHR.

Ниже приведен пример вызовов функций для заполнения данных:

// Получение информации о поверхности
VkSurfaceCapabilitiesKHR capabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &capabilities);

// Получение форматов поверхности
uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);
std::vector<VkSurfaceFormatKHR> formats(formatCount);
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, formats.data());

// Получение данных о поддерживаемых режимах показа
uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr);
std::vector<VkPresentModeKHR> presentModes(presentModeCount);
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, presentModes.data());

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

bool swapchainSupport = formatCount && presentModeCount;

Перед возвратом устройства заполним данные о поверхности:

surface.capabilities = capabilities;
surface.formats = formats;
surface.presentModes = presentModes;

Создадим метод createSwapchain в приватной области видимости класса Vulkan.

Создание списка показа требует выбрать:

  • формат поверхности (VkSurfaceFormatKHR) — цветовое пространство и формат пикселя;
  • режим показа (VkPresentModeKHR);
  • разрешение (VkExtent2D);
  • количество изображений.

Данные параметры могут пригодится в меню настроек — сохраним их в структуре с данными о поверхности:

typedef struct _Surface
{
    VkSurfaceKHR surface; // Поверхность окна
    VkSurfaceCapabilitiesKHR capabilities; // общая информация
    std::vector<VkSurfaceFormatKHR> formats; // формат поверхности
    std::vector<VkPresentModeKHR> presentModes; // режим показа
    // Данные о списке показа
    VkSurfaceFormatKHR selectedFormat; // выбранный формат поверхности
    VkPresentModeKHR selectedPresentMode; // выбранный режим показа
    VkExtent2D selectedExtent; // выбранное разрешение
    uint32_t imageCount; // количество изображений
} Surface;

В качестве формата поверхности необходимо выбрать предпочтительный или первый доступный формат поддерживаемый устройством. В качестве предпочитаемого будет выступать цветовое пространство SRGB (VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) с восемью битами на цвет(VK_COLORSPACE_SRGB_NONLINEAR_KHR):

surface.selectedFormat = surface.formats[0];
for (auto& format : surface.formats) 
{
    if (format.format == VK_FORMAT_B8G8R8A8_SRGB 
    &&  format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) 
    {
        surface.selectedFormat = format;
        break;
    }
}

В качестве режимов показа доступны четыре основных режима и два вспомогательных, подключаемые расширениями. Рассмотрим режимы подробнее:

  • VK_PRESENT_MODE_IMMEDIATE_KHR — немедленный показ изображений по готовности;
  • VK_PRESENT_MODE_FIFO_KHR — режим очереди FIFO (первый зашел — первый вышел), изображения показываются с начала очереди, готовые помещаются в конец (если очередь заполнена — программа будет ждать) — по сути вертикальная синхронизация;
  • VK_PRESENT_MODE_FIFO_RELAXED_KHR — аналогично с предыдущим режимом, но в случае опустошения очереди при появлении нового изображения вывод производится без ожидания обновления экрана;
  • VK_PRESENT_MODE_MAILBOX_KHR — аналогично со вторым режимом в случае если очередь заполнена происходит перезапись кадров (реализация двойной и тройной буферизации).

Графические устройства с выводом под монитор поддерживают по умолчанию режим VK_PRESENT_MODE_FIFO_KHR. По аналогии с форматом показа выберем предпочтительный этому режиму формат.

surface.selectedPresentMode = VK_PRESENT_MODE_FIFO_KHR;
for (auto& presentMode : surface.presentModes) 
{
    if (presentMode == VK_PRESENT_MODE_MAILBOX_KHR)
    {
        surface.selectedPresentMode = presentMode;
        break;
    }
}

Выбор разрешения производится в диапазоне minImageExtent и maxImageExtent из полученной информации о поверхности VkSurfaceCapabilitiesKHR. Выбор будет производится на основании двух этапов: минимум(максимально возможное и текущее разрешение) и максимум(минимальное возможное и минимум от прошлого этапа). Аналогичный выбор осуществляется с помощью std::clamp, который требует C++17. Для понижения уровня стандарта языка заменим его следующим макросом:

#define CLAMP(min, value, max) (value > min) ? min : (max < value) ? max : value;

Который поместим в новый заголовочный файл macroses.h.

Выбор разрешения будет основываться на разрешении окна, которое можно получить функцией glfwGetFramebufferSize, для которой потребуется передать указатель на окно GLFW в метод создания списка показа. Пример выбора разрешения окна:

// Разрешение окна 
int width, height;
glfwGetFramebufferSize(window, &width, &height);
// Выберем разрешение исходя из ограничений физического устройства
surface.selectedExtent.width = CLAMP(  surface.capabilities.minImageExtent.width
                                , width
                                , surface.capabilities.maxImageExtent.width
                            );
surface.selectedExtent.width = CLAMP(  surface.capabilities.minImageExtent.height
                                , height
                                , surface.capabilities.maxImageExtent.height
                            );

Количество изображений (размер буфера) в списке показа определяется диапазоном minImageCount и maxImageCount из полученной информации о поверхности VkSurfaceCapabilitiesKHR. Определим размер буфера как минимальное количество +1, но важно не превышать максимальное ограничение. Если оно не задано, то maxImageCount равно 0. Ниже приведен пример выбора количества изображений в цепочке показа:

surface.imageCount = surface.capabilities.minImageCount + 1;
// Если есть ограничение по максимуму изображений - применим его
if (surface.capabilities.maxImageCount)
    surface.imageCount %= surface.capabilities.maxImageCount;

Для создания цепочки показа используется функция vkCreateSwapchainKHR которая принимает в качестве аргументов дескриптор логического устройства, адрес объекта структуры с данными о цепочке показа (VkSwapchainCreateInfoKHR), адрес аллокатора и адрес дескритора VkSwapchainKHR.

Добавим в приватной области видимости объект swapChain структуры VkSwapchainKHR.

Рассмотрим структуру с данными о цепочке показа подробнее:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
  • flags — битовая маска параметров создания цепочки показа, определяется перечислением (enum) VkSwapchainCreateFlagBitsKHR (в заметке не используется);
  • surface — поверхность окна;
  • minImageCount — выбранное минимальное необходимое количество изображений для приложения;
  • imageFormat — выбранный формат пикселя;
  • imageColorSpace — выбранное цветовое пространство;
  • imageExtent — выбранный размер изображения;
  • imageArrayLayers — число слоев изображения (в рамках курса заметок будет 1);
  • imageUsage — предназначение изображения (VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT — для рендера в изображение, VK_IMAGE_USAGE_TRANSFER_DST_BIT — для переноса из другого буфера);
  • imageSharingMode — режим обработки изображений (зависит от семейств очередей, рассмотрим подробнее далее);
  • queueFamilyIndexCount — количество семейств очередей;
  • pQueueFamilyIndices — адрес массива с семейством очередей;
  • preTransform — преобразования изображения (поворот, отражение), будет использоваться преобразования от поверхности окна;
  • compositeAlpha — использование альфа канала смешивания (не используется в курсе заметок — VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR);
  • presentMode — выбранный режим показа;
  • clipped — исключение скрытых другими окнами пикселей;
  • oldSwapchain — адрес старого списка показа (в случае пересоздания).

Рассмотрим возможные режимы обработки изображений:

  • VK_SHARING_MODE_CONCURRENT — используется в случае если созданы различные очереди для рисования и вывода, требуется заполнить поля queueFamilyIndexCount и pQueueFamilyIndices;
  • VK_SHARING_MODE_EXCLUSIVE — используется в случае если для рисования и представления используется одна и та же очередь, поля queueFamilyIndexCount и pQueueFamilyIndices не требуют заполнения (по умолчанию 0).

Пример заполнения структуры с данными о создаваемом списке показа:

VkSwapchainCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
createInfo.surface = surface.surface;
createInfo.minImageCount = surface.imageCount;
createInfo.imageFormat = surface.selectedFormat.format;
createInfo.imageColorSpace = surface.selectedFormat.colorSpace;
createInfo.imageExtent = surface.selectedExtent;
createInfo.imageArrayLayers = 1;
createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
createInfo.preTransform = surface.capabilities.currentTransform;
createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
createInfo.presentMode = surface.selectedPresentMode;
createInfo.clipped = VK_TRUE;
createInfo.oldSwapchain = VK_NULL_HANDLE;

После заполнения вызовем функцию создания списка показа и передадим адрес заполненного объекта структуры VkSwapchainCreateInfoKHR.

Помимо этого необходимо добавить уничтожение очереди показа в метод Vulkan::destroy.

Добавим к приватной области видимости класса Vulkan std::vector изображений (VkImage), принадлежащих списку показа:

std::vector<VkImage> swapChainImages;

Теперь с помощью функции vkGetSwapchainImagesKHR можно получить изображения список показа. Данная функция принимает в качестве аргументов дескриптор логического устройства, список показа, адрес переменной под количество изображений и указатель на массив структуры VkImage. Пример заполнения массива изображений:

vkGetSwapchainImagesKHR(logicalDevice, swapChain, &surface.imageCount, nullptr);
swapChainImages.resize(surface.imageCount);
vkGetSwapchainImagesKHR(logicalDevice, swapChain, &surface.imageCount, swapChainImages.data());

На данный момент программа выдаст ошибку создания цепочки показа, так как выбранная очередь не проверяется на возможность вывода изображений. Рассмотрим дополнение.

Дополнение

Работа над списком показа показала необходимость переделать функцию выбора очередей.

Удалим метод PhysicalDevice::pickQueueFamily и другой Vulkan::pickQueues.

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

Функция выбора очередей имеет следующий вид:

void Vulkan::pickQueues()
{
    queue.index = -1;
    
    for (int i = 0; i < physicalDevice.queueFamilyProperties.size(); i++)
    {
        // Проверка возможности вывода
        VkBool32 presentSupport = false;
        vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevice.device, i, surface.surface, &presentSupport);
        // Проверка поддержки очередью графических операций
        if (physicalDevice.queueFamilyProperties[i].queueFlags & VK_QUEUE_GRAPHICS_BIT
        &&  presentSupport) 
        {
            queue.index = i;
            queue.properties = physicalDevice.queueFamilyProperties[i];
            break;
        }
    }
}

Если необходимо разделить процесс показа и рисования изображения на разные семейства очередей — необходимо разделить условный оператор на два и добавить в первый continue, после этого дополнить объект структуры с данными о первой очереди адресом второго объекта структуры с данными о второй очереди.

Текущий вариант доступен на теге v0.2 репозитория 03.

Для дальнейшей работы в будущих заметках с буферами кадра (framebuffer) необходимо создать объекты структуры VkImageView из имеющихся изображений списка показа. По сути VkImageView является оберткой для VkImage, которая содержит в себе информацию о том, как необходимо интерпретировать данные, хранящиеся в изображении.

Примечание: VkImageView в заметках будет называться «вид изображения»

Добавим к приватной области класса Vulkan std::vector для хранения информации об изображениях (vkImageView):

std::vector<VkImageView> swapChainImageViews;

Дополним метод класса Vulkan::createSwapchain для создания VkImageView. Для этого в первую очередь изменим размер добавленного вектора в соответствии с количеством изображений из цепочки показа и запустим цикл, перебирающий их индексы:

// Зададим размер массива в соответствии с количеством изображений
swapChainImageViews.resize(swapChainImages.size());
// Для каждого изображения из списка показа
for (int i = 0; i < swapChainImages.size(); i++) 
{
...
}

Для каждого индекса изображения необходимо заполнить объект структуры VkImageViewCreateInfo и вызвать функцию vkCreateImageView, которая принимает в качестве аргументов дескриптор логического устройства, адрес объекта структуры VkImageViewCreateInfo, адрес аллокатора, и адрес дескриптора VkImageView.

Рассмотрим структуру VkImageViewCreateInfo подробнее:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
  • pNext — указатель на расширение структуры (не используется);
  • flags — битовая маска флагов, определяющих дополнительные параметры;
  • image — изображение, для которого создается объект с информацией;
  • viewType — определяет как необходимо воспринимать изображение, является перечислением (enum) VkImageViewType.
  • format — формат пикселя;
  • components — структура, позволяющая манипулировать цветовыми каналами (в заметке используется значение по умолчанию для каждого канала — VK_COMPONENT_SWIZZLE_IDENTITY);
  • subresourceRange — структура VkImageSubresourceRange, описывающая какая часть изображения будет использоваться.

Рассмотрим структуру VkImageSubresourceRange подробнее:

  • aspectMask — битовая маска флагов, определяющих предназначение используемого изображения;
  • baseMipLevel — первый уровень детализации (не используется);
  • levelCount — количество уровней детализации (не используется);
  • baseArrayLayer — первый слой (не используется);
  • layerCount — количество слоев (1).

В заметках используются изображения цепочки показа в качестве буфера цвета (VK_IMAGE_ASPECT_COLOR_BIT) без уровней детализации и состоящие из одного слоя.

Пример заполнения по i-тому индексу и создания ImageView:

VkImageViewCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
createInfo.image = swapChainImages[i];
createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
createInfo.format = surface.selectedFormat.format;  
createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
createInfo.subresourceRange.baseMipLevel = 0;
createInfo.subresourceRange.levelCount = 1;
createInfo.subresourceRange.baseArrayLayer = 0;
createInfo.subresourceRange.layerCount = 1;

if (vkCreateImageView(logicalDevice, &createInfo, nullptr, &swapChainImageViews[i]) != VK_SUCCESS) 
{
    throw std::runtime_error("Unable to create image views");
}

Помимо этого необходимо удалить созданные объекты, добавим в начало метода Vulkan::destroy цикл, вызывающий функцию удаления:

for (auto & imageView : swapChainImageViews) 
{
    vkDestroyImageView(logicalDevice, imageView, nullptr);
}

Текущий вариант доступен на теге v0.3 репозитория 03.

Вывод

В рамках данной заметки была произведена организация обмена с окном: созданы поверхность окна и список показа.

Проект доступен в публичном репозитории: 03
Библиотеки: dependencies

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

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

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.