[Перевод] Vulkan. Руководство разработчика. Window surface

Я из IT-компании CGTribe и здесь я перевожу руководство к Vulkan API. Ссылка на оригинал — vulkan-tutorial.com.

Моя следующая публикация посвящена переводу главы Window surface из раздела Drawing a triangle, подраздела Presentation.

Содержание

1. Вступление

2. Краткий обзор

3. Настройка окружения

4. Рисуем треугольник

  1. Подготовка к работе
  2. Отображение на экране
    • Window surface
    • Swap chain
    • Image views
  3. Основы графического конвейера (pipeline)
  4. Отрисовка
  5. Повторное создание цепочки показа

5. Буферы вершин

  1. Описание
  2. Создание буфера вершин
  3. Staging буфер
  4. Буфер индексов

6. Uniform-буферы

  1. Дескриптор layout и буфера
  2. Дескриптор пула и sets

7. Текстурирование

  1. Изображения
  2. Image view и image sampler
  3. Комбинированный image sampler

8. Буфер глубины

9. Загрузка моделей

10. Создание мип-карт

11. Multisampling

FAQ
Политика конфиденциальности

Window surface

Поскольку Vulkan API полностью независим от платформы, он не может напрямую взаимодействовать с оконной системой. Чтобы Vulkan мог выводить результат на экран, необходимо использовать стандартизованные расширения WSI (Window System Integration). В этой главе мы расскажем про одно из них — VK_KHR_surface. Расширение предоставляет объект VkSurfaceKHR — абстрактный тип поверхности для показа отрендеренных изображений. Эта поверхность будет создана при поддержке окна GLFW, полученного нами ранее.

VK_KHR_surface – это расширение Vulkan уровня экземпляра. У нас оно уже подключено, поскольку находится в списке расширений, возвращаемых функцией glfwGetRequiredInstanceExtensions. В списке есть и другие расширения WSI, которые мы будем использовать в следующих главах.

Window surface нужно создать сразу после VkInstance, поскольку это может повлиять на выбор физического устройства. Нужно помнить, что window surfaces — полностью опциональный компонент Vulkan. Вы можете обойтись без него, если вам нужен offscreen рендеринг. Это позволяет избежать таких хаков, как, например, создание невидимого окна для OpenGL.

Создание window surface

Начнем с того, что добавим новый член класса surface сразу после вызова debugMessenger.

VkSurfaceKHR surface;

Процесс создания объекта VkSurfaceKHR зависит от платформы. Так, например, для создания в Windows нужны дескрипторы HWND и HMODULE. Для разных платформ у Vulkan есть платформенно-зависимое дополнение к расширению, которое в Windows называется VK_KHR_win32_surface. Оно также автоматически включено в список расширений, возвращаемых функцией glfwGetRequiredInstanceExtensions.

Мы покажем, как можно использовать это расширение для создания surface в Windows, однако в руководстве оно нам не понадобится. В библиотеке GLFW, которую мы используем, есть функция-обертка glfwCreateWindowSurface, содержащая специфичный для платформы код. Но было бы не плохо увидеть, что происходит за кулисами.

Window surface – это объект Vulkan, поэтому мы должны заполнить структуру VkWin32SurfaceCreateInfoKHR, чтобы создать его. В ней есть два важных параметра: hwnd и hinstance — дескрипторы окна и текущего процесса.

VkWin32SurfaceCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
createInfo.hwnd = glfwGetWin32Window(window);
createInfo.hinstance = GetModuleHandle(nullptr);

Здесь для получения сырого HWND используется функция glfwGetWin32Window, а для получения HINSTANCE – функция GetModuleHandle.

После этого мы можем создать surface с помощью функции vkCreateWin32SurfaceKHR, в которую передаются следующие параметры: экземпляр Vulkan, информация о surface, кастомный аллокатор и указатель для записи результата. Технически это функция расширения WSI, но она используется так часто, что была включена в стандартный загрузчик Vulkan, поэтому вам не нужно загружать ее явно.

if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) {
    throw std::runtime_error("failed to create window surface!");
}

Для других платформ подход аналогичен. Для Linux, например, используется функция vkCreateXcbSurfaceKHR.

Функция glfwCreateWindowSurface делает именно эту работу, но имеет свою реализацию для каждой платформы. Интегрируем ее в нашу программу. Для этого добавим функцию createSurface, которая вызывается из initVulkan сразу после createInstance и setupDebugMessenger.

void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
}

void createSurface() {

}

Вместо структуры для вызова GLFW нужны простые параметры, что упрощает реализацию функции:

void createSurface() {
    if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) {
        throw std::runtime_error("failed to create window surface!");
    }
}

В функцию передаются следующие параметры: VkInstance, указатель на окно GLFW, кастомный аллокатор и указатель для записи результата. Функция возвращает VkResult.

У GLFW нет специальной функции для уничтожения surface, но это легко можно сделать непосредственно с помощью Vulkan:

void cleanup() {
        ...
        vkDestroySurfaceKHR(instance, surface, nullptr);
        vkDestroyInstance(instance, nullptr);
        ...
    }

Не забудьте уничтожить surface до VkInstance.

Проверка поддержки отображения

Хотя конкретная реализация Vulkan может поддерживать интеграцию с оконной системой, это не значит что каждое из устройств в системе это тоже поддерживает. Поэтому нам нужно расширить isDeviceSuitable, чтобы быть уверенными, что устройство может отображать изображения на surface, которую мы создали. Поскольку отображение — это процесс, происходящий через очереди команд, то задача заключается в том, чтобы найти семейство очередей, которое поддерживает отображение в созданную surface.

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

struct QueueFamilyIndices {
    std::optional<uint32_t> graphicsFamily;
    std::optional<uint32_t> presentFamily;

    bool isComplete() {
        return graphicsFamily.has_value() && presentFamily.has_value();
    }
};

Изменим функцию findQueueFamilies, чтобы найти семейство очередей с поддержкой отображения на surface нашего окна. Для проверки используем функцию vkGetPhysicalDeviceSurfaceSupportKHR, которая принимает следующие параметры: физическое устройство, индекс семейства очередей и surface. Добавим вызов функции в тот же цикл, в котором находится проверка VK_QUEUE_GRAPHICS_BIT:

VkBool32 presentSupport = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);

Затем проверим значение типа VkBool32 и сохраним индекс нужного нам семейства:

if (presentSupport) {
    indices.presentFamily = i;
}

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

Создание очереди отображения

Осталось изменить процесс создания логического устройства, чтобы создать очередь с поддержкой отображения и получить дескриптор VkQueue. Добавим переменную класса:

VkQueue presentQueue; 

Нам нужно несколько VkDeviceQueueCreateInfo, чтобы создать очередь для каждого из семейств. Элегантный способ это сделать — использовать std::set, чтобы выделить уникальные семейства из найденных:

#include <set>

...

QueueFamilyIndices indices = findQueueFamilies(physicalDevice);

std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
std::set<uint32_t> uniqueQueueFamilies = {indices.graphicsFamily.value(), indices.presentFamily.value()};

float queuePriority = 1.0f;
for (uint32_t queueFamily : uniqueQueueFamilies) {
    VkDeviceQueueCreateInfo queueCreateInfo{};
    queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
    queueCreateInfo.queueFamilyIndex = queueFamily;
    queueCreateInfo.queueCount = 1;
    queueCreateInfo.pQueuePriorities = &queuePriority;
    queueCreateInfos.push_back(queueCreateInfo);
}

Изменим структуру VkDeviceCreateInfo, чтобы она указывала на наш вектор:

createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
createInfo.pQueueCreateInfos = queueCreateInfos.data();

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

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

vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);

В следующей главе мы рассмотрим swap chains и расскажем, как они помогают выводить изображения на экран.

Код C++

Let’s block ads! (Why?)

Read More

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

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