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

Vulkan API 2: инициализация

В данной заметке производится инициализация библиотеки и работа с устройствами

Введение

В прошлой заметке был настроен проект для работы с библиотекой Vulkan с использованием GLFW3 и VS Code.

Данная заметка весьма объемна и для упрощения взаимодействия разделена на этапы в соответствии с этапами инициализации. Результаты каждого этапа в конце закреплены тегом в соответствующем заметке репозитории.

Для упрощения работы с библиотекой будет написан класс-обертка на основании инициализации. Для этого создадим файлы «vk.h» и «vk.cpp» и заменим подключение заголовочных файлов <vulkan/vulkan.h> и <GLFW/glfw3.h> на <vk.h> — произведем подключение внутри нашего заголовочного файла. Назовем новый класс «Vulkan».

В ходе работы с библиотекой нам понадобится хранить множество состояний и условий влияющих на поведение нашего приложения, для решения данной задачи будет создана структура с bool/char/int переменными, определяющими поведение приложения. Некоторые поля, которые в процессе работы приложения изменять нельзя, будут созданы с ключевым словом const. Структура требуется в одном экземпляре — оставим анонимной, и назовем экземпляр «appStates».

Инициализация библиотеки происходит в виде этапов:

Создадим общий метод, который будет выполнять роль точки входа в процесс инициализации:

void init();

Из этого метода последовательно будут вызываться этапы инициализации.

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

Создание экземпляра

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

Важное замечание! В английской версии используется слово «instance», но автор на протяжении курса заметок будет использовать слово «экземпляр» заместо англицизма.

Создадим экземпляр структуры в приватной области видимости нашего класса:

VkInstance instance;

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

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

Структура с данными по созданию экземпляра (VkInstanceCreateInfo) указывает драйверу общие данные о приложении, а так же какие слои и расширения необходимы приложению.

Создадим объект структуры VkApplicationInfo и заполним его таким образом:

VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; 
appInfo.pApplicationName = "Vulkan Notes";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_0;

Рассмотрим поля подробнее:

  • sType — предназначение данной структуры, должно быть равно VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
  • pApplicationName — имя приложения;
  • applicationVersion — версию приложения;
  • pEngineName — название движка;
  • engineVersion — версия движка;
  • apiVersion — минимальная допустимая версия Vulkan API.

Теперь можно заполнить объект структуры, которая будет передана адресом в функцию создания экземпляра Vulkan:

// Структура с данными 
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;

// Расширения для glfw
uint32_t glfwExtensionCount = 0;
const char** glfwExtensions;
glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
// Инициализируем вектор расширений тем, что требуется для glfw
std::vector<const char*> extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);

// Запишем данные об используемых расширениях в структуру
createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
createInfo.ppEnabledExtensionNames = extensions.data();

Для удобства хранения расширений используется std::vector. Вектор инициализируется расширениями, необходимыми для GLFW. Данные расширения можно получить функцией glfwGetRequiredInstanceExtensions, которая вернет массив си-строк, содержащих имена расширений, и их количество в переменную адрес которой был передан в качестве аргумента.
В дальнейшем расширения можно добавлять с помощью метода push.

Важное замечание! Функция glfwGetRequiredInstanceExtensions возвращает указатель на массив си-строк, который не нуждается в ручном освобождении памяти — это происходит автоматически с вызовом glfwTerminate.

Рассмотрим поля структуры:

  • pApplicationInfo — данные о приложении, которые были заполнены ранее;
  • enabledExtensionCount — количество подключенных расширений;
  • ppEnabledExtensionNames — указатель на массив си-строк с именами расширений;
  • enabledLayerCount — количество необходимых слоев;
  • ppEnabledLayerNames — указатель на массив си-строк с именами слоев;
  • pNext — указатель на расширение данной структуры (не используется в заметке).

Слои будут затронуты в следующей теме.

Важное замечание! Си-строки с именами расширений должны быть в кодировке UTF-8.

После заполнения данных можно вызвать функцию для создания экземпляра:

VkResult result = vkCreateInstance(&createInfo, nullptr, &instance);

Данная функция возвращает VkResult, который является перечислением (enum) и принимает значения:

  • VK_SUCCESS — в случае успешного создания;
  • VK_ERROR_OUT_OF_HOST_MEMORY — невозможно выделить память или же возникает когда устройство не поддерживает Vulkan;
  • VK_ERROR_OUT_OF_DEVICE_MEMORY — невозможно выделить память на графическом устройстве;
  • VK_ERROR_INITIALIZATION_FAILED — ошибка инициализации;
  • VK_ERROR_LAYER_NOT_PRESENT — ошибка в имени слоя проверки;
  • VK_ERROR_EXTENSION_NOT_PRESENT — ошибка в имени расширения;
  • VK_ERROR_INCOMPATIBLE_DRIVER — несовместимый драйвер.

В случае, если значение не соответствует VK_SUCCESS отправим исключение:

if (result != VK_SUCCESS) {
    throw std::runtime_error("Instance create error");
}

Создадим публичный метод destroy и вызовем функцию vkDestroyInstance для уничтожения созданного классом экземпляра Vulkan:

void Vulkan::destroy()
{
    vkDestroyInstance(instance, nullptr);
}

Теперь в main-функции можно создать объект класса Vulkan и вызвать методы init (после создания окна) и destroy (перед уничтожением окна).

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

Подключение слоев

Слои — это способ, при помощи которого поведение Vulkan может быть изменено. Слои используются для:

  • логирование;
  • отслеживание;
  • профилирование;
  • диагностика.

Слои могут подключатся как на уровне устройства, так и на уровне всего экземпляра Vulkan.

Рассмотрим некоторые из них:

  • VK_LAYER_LUNARG_api_dump – выводит в консоль вызовы и передаваемые параметры Vulkan;
  • VK_LAYER_LUNARG_device_limits – проверяет, что значения, передаваемые командам Vulkan лежат в поддерживаемых устройством пределах;
  • VK_LAYER_LUNARG_parameter_validation – проверяет корректность всех параметров функций Vulkan;
  • VK_LAYER_LUNARG_object_tracker – отслеживает объекты Vulkan для обнаружение утечек памяти и обращений к уничтоженным объектам.

В книге Грехема Селлерса упоминается об объединение слоев в VK_LAYER_LUNARG_stardard_validation, который используется для упрощения работы с отладкой, но на SDK версии 1.2.189.2 данный слой оказался недоступен, заместо него будет использоваться VK_LAYER_KHRONOS_validation.

Для упрощения работы со слоями создадим флаг в составе структуры states внутри класса Vulkan:

const bool VALIDATION = true;

Подключим слои на уровне экземпляра Vulkan. Дополним функцию Vulkan::createInstance перед вызовом функции vkCreateInstance:

std::vector<const char*> validationLayers = {
    "VK_LAYER_KHRONOS_validation"
};

if (states.VALIDATION) 
{
    createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
    createInfo.ppEnabledLayerNames = validationLayers.data();
}

Организация хранения слоев аналогична с расширениями.

В случае, если один из слоев может оказаться не доступен — функция, создающая экземпляр вернет ошибку: VK_ERROR_LAYER_NOT_PRESENT (код ошибки -6).

Для обеспечения контроля за доступностью слоев напишем функцию checkValidationLayerSupport:

#include <cstring>
// Проверка слоев на доступность. Возвращает true, если все слои доступны
// по ссылке заполняет вектор недоступных слоев
bool checkValidationLayerSupport(std::vector <const char*> requestedLayers, std::vector <const char*> & unavailableLayers)
{
    bool layerAvailable; // флаг доступности слоя для цикла

    // Первым вызовом определим кол-во доступных слоев
    uint32_t layerCount;
    vkEnumerateInstanceLayerProperties(&layerCount, nullptr);

    // Вторым вызовом запишем в вектор доступные слои
    std::vector<VkLayerProperties> availableLayers(layerCount);
    vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());

    // Цикл по запрошенным слоям
    for (const char* layerName : requestedLayers) 
    {
       layerAvailable = false;

        // Цикл по доступным слоям
        for (const auto& layerProperties : availableLayers) 
        {
            // Сравнение строк
            if (strcmp(layerName, layerProperties.layerName) == 0) 
            {
                layerAvailable = true;
                break;
            }
        }

        // Если слой не найден то заносим в массив недоступных
        if (!layerAvailable) {
            unavailableLayers.push_back(layerName);
        }
    }

    return unavailableLayers.size() == 0;
}

Данная функция запрашивает доступные от системы слои с помощью двух вызовов функции vkEnumerateInstanceLayerProperties, первый возвращает количество доступных слоев, которое используется для выделения памяти под вектор, второй — заполняет выделенную память данными о слоях.
Вызовем эту функцию после подключения слоев:

// Проверим доступность слоев
std::vector<const char*> unavailableLayers;
if (!checkValidationLayerSupport(validationLayers, unavailableLayers))
{
    std::cout << "Запрошены недоступные слои:\n";
    // Цикл по недоступным слоям
    for (const char* layer : unavailableLayers) 
        std::cout << layer << "\n";  
    // Отправим исключение об отсутствующем слое 
    throw std::runtime_error("Requested layer unavailable");
}

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

Выбор физического устройства

Vulkan API позволяет выбирать одно или несколько физических устройств и анализировать существующие на основании их возможностей. Использоваться будет более подходящее устройство — работа с несколькими устройствами это задача для отдельной заметки. Добавим поле к нашему классу:

VkPhysicalDevice physicalDevice;

Приступим к выбору устройства. Для этого создадим метод selectPhysicalDevice и вызовем его из метода отвечающего за инициализацию.

Функция vkEnumeratePhysicalDevices возвращает количество доступных физических устройств и заполняет массив доступных устройств, для этого она принимает в качестве параметров экземпляр Vulkan, указатель переменную с результатом, указатель на массив указателей на VkPhysicalDevice.

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

uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);

Если в системе не обнаружено доступных устройств — выдадим исключение:

if (deviceCount == 0) 
{
    throw std::runtime_error("Unable to find physical devices");
}

Далее выделим память в векторе и заполним созданный массив:

std::vector<VkPhysicalDevice> devices(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());

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

В цикле по устройствам вызовем функции vkGetPhysicalDeviceProperties и vkGetPhysicalDeviceFeatures. Первая возвращает параметры по устройству и принимает в качестве аргументов физическое устройство и указатель на объект структуры VkPhysicalDeviceProperties. Вторая возвращает функции поддерживаемые устройством и принимает в качестве параметров физическое устройство и указатель на объект структуры VkPhysicalDeviceFeatures.

Рассмотрим структуру VkPhysicalDeviceProperties:

  • apiVersion — версия Vulkan, поддерживаемая устройством;
  • driverVersion — версия драйвера устройства;
  • vendorID — идентификатор производителя устройства;
  • deviceID — идентификатор устройства;
  • deviceType — тип устройства;
  • deviceName — имя устройства в кодировке UTF-8;
  • pipelineCacheUUID — массив с восьмибитным представлением универсального идентификатора устройства;
  • limits — структура VkPhysicalDeviceLimits, определяющая ограничения устройства;
  • sparseProperties — структура VkPhysicalDeviceSparseProperties, определяющая свойства физического устройства.

Тип устройства (deviceType) является перечислением (enum) и принимает следующие значения:

  1. VK_PHYSICAL_DEVICE_TYPE_OTHER — устройство не совпадает ни с одним из доступных типов;
  2. VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU — устройство является встроенным в CPU графическим устройством (интегрированная графика);
  3. VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU — устройство является отдельным графическим устройством (дискретная графика);
  4. VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU — виртуальное устройство для виртуального окружения;
  5. VK_PHYSICAL_DEVICE_TYPE_CPU — устройство работает на том же процессоре что и хост (является CPU).

Примечание: номера списка соответствуют значению, заданному в перечислении.

Структура VkPhysicalDeviceFeatures содержит логические флаги и является весьма объемной, ознакомиться с ней можно на официальной документации, тут будет рассмотрены некоторые интересные параметры:

  • geometryShader — поддержка геометрических шейдеров;
  • tessellationShader — поддержка шейдеров управления и оценки тесселяции;
  • largePoints — поддержка точек с размером больше 1,0.

Для получения параметров и функций устройства используется следующий код:

VkPhysicalDeviceProperties properties; // Параметры конкретного устройства
VkPhysicalDeviceFeatures features;   // Функции конкретного устройства
for (const auto& device : devices) 
{
    // Получаем данные
    vkGetPhysicalDeviceProperties(device, &properties);
    vkGetPhysicalDeviceFeatures(device,   &features);

    // Производим оценку
        
}

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

Данные по памяти устройства можно получить с помощью функции vkGetPhysicalDeviceMemoryProperties. Дополнив вызовом данной функции выбор физического устройства можно производить фильтрацию по объему памяти. Например если существует необходимость в устройстве с объемом памяти более 4Гб можно использовать следующее условие:

4000 < memory.memoryHeaps[0].size / 1000 / 1000

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

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

Поле queueFlags определяет общие возможности очереди и принимает соответствует следующей комбинации битов:

  • VK_QUEUE_GRAPHICS_BIT — очереди поддерживает графические операции;
  • VK_QUEUE_COMPUTE_BIT — очереди поддерживает вычислительные операции;
  • VK_QUEUE_TRANSFER_BIT — очереди поддерживают операции копирования.

Помимо флагов можно узнать количество очередей в семействе с помощью поля queueCount.

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

typedef struct _PhysicalDevice
{
    VkPhysicalDevice device; // устройство
    VkPhysicalDeviceProperties properties; // параметры 
    VkPhysicalDeviceFeatures features; // функции 
    VkPhysicalDeviceMemoryProperties memory; // память
    std::vector<VkQueueFamilyProperties> queueFamilyProperties; // семейства очередей
} PhysicalDevice;

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

// Выбор физического устройства на основании требований
PhysicalDevice selectPhysicalDeviceByProperties(std::vector<VkPhysicalDevice> & devices)
{
    int i;
    PhysicalDevice result; // физическое устройство (PhysicalDevice.h)
    for (const auto& device : devices) 
    {
        // Запомним устройство
        result.device = device;
        // Получаем данные
        vkGetPhysicalDeviceProperties(device, &result.properties);
        vkGetPhysicalDeviceFeatures(device, &result.features);
        vkGetPhysicalDeviceMemoryProperties(device, &result.memory);
        
        // Данные по семействам очередей    
        uint32_t queueFamilyPropertiesCount = 0;
        vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyPropertiesCount, nullptr);
        result.queueFamilyProperties.resize(queueFamilyPropertiesCount);
        vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyPropertiesCount, result.queueFamilyProperties.data());

        // Производим оценку
        if (result.features.geometryShader
        &&  4000 < result.memory.memoryHeaps[0].size / 1000 / 1000)
            return result;
    }
    // Если устройство не найдено - вернем пустую структуру
    return PhysicalDevice();
}

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

Создание логического устройства

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

Алгоритм создания логического устройства сопоставим с алгоритмом создания экземпляра Vulkan.

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

  • VkDeviceCreateInfo — общие данные о создаваемом устройстве;
  • VkDeviceQueueCreateInfo — данные о требуемых семействах очередей.

Первым делом создадим и заполним объект структуры с данными о требуемых семействах очередей. Рассмотрим структуру подробнее:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
  • pNext — указатель на расширение данной структуры (не используется в заметке);
  • flags — для данной структуры не существует флагов (= 0);
  • queueFamilyIndex — индекс семейства очередей для создания;
  • queueCount — количество необходимых очередей;
  • pQueuePriorities — обязательный к заполнению указатель на массив приоритетов очередей значения которого нормализованы (0.0 — 1.0).

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

uint32_t PhysicalDevice::pickQueueFamily(VkQueueFlags flags)
{
    // Цикл по параметрам семейств очередей
    for (uint32_t index = 0; index < queueFamilyProperties.size(); index++) 
    {
        // Если очередь соответствует требованиям по возможностям очереди
        if (queueFamilyProperties[index].queueFlags & flags) 
        {
            // возвращаем её индекс
            return index;
        }
    }
}

Рассмотрим пример для создания очередей логического устройства на примере одной очереди:

float priority[1] = {1};
VkDeviceQueueCreateInfo queueCreateInfo{};
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = physicalDevice.pickQueueFamily(VK_QUEUE_GRAPHICS_BIT);
queueCreateInfo.queueCount = 1;
queueCreateInfo.pQueuePriorities = priority; 

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

Важное замечание! Если требуемое количество очередей превышает допустимый лимит на устройстве необходимо заполнить несколько экземпляров этой структуры, которые будут переданы в следующую (VkDeviceCreateInfo).

Теперь рассмотрим структуру с общими данными о создаваемом устройстве:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
  • pNext — указатель на расширение данной структуры (не используется в заметке);
  • flags — для данной структуры не существует флагов (= 0);
  • queueCreateInfoCount — количество заполненных структур с данными об очередях устройства;
  • pQueueCreateInfos — указатель на массив структур с данными об очередях устройства;
  • enabledLayerCount — количество включенных слоев;
  • ppEnabledLayerNames — указатель на массив с именами слоев;
  • enabledExtensionCount — количество включенных расширений;
  • ppEnabledExtensionNames — указатель на массив с именами расширений;
  • pEnabledFeatures — указатель на объект структуры VkPhysicalDeviceFeatures, который задает какие необязательные возможности необходимо предоставить логическому устройству.

pEnabledFeatures — может быть равным либо nullptr — все отключено, либо адресу physicalDevice.features — будут включены все необязательные возможности, либо адресу измененной копии physicalDevice.features с отключенными ненужными возможностями.

Рассмотрим пример для создания логического устройства:

VkDeviceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
createInfo.pQueueCreateInfos = &queueCreateInfo;
createInfo.queueCreateInfoCount = 1;
createInfo.enabledExtensionCount = 0;
createInfo.enabledLayerCount = 0;
createInfo.pEnabledFeatures = nullptr;//&physicalDevice.features

В данной заметке расширения и слои отключены. Подготовим возможности их использования. Для слоев и расширений по аналогии с экземпляром создадим std::vector. Вектор для расширений создадим в методе Vulkan::Init и передадим ссылку на него в функции выбора физического устройства и создания логического, так как не все физические устройства поддерживают специфичные расширения — потребуется учесть их при выборе физического устройства.

Создадим копию функции checkValidationLayerSupport с названием checkDeviceLayerSupport. Для проверки поддерживаемых слоев на уровне устройства используется функция vkEnumerateDeviceLayerProperties, которая работает аналогично vkEnumerateInstanceLayerProperties и принимает дополнительно один аргумент — VkPhysicalDevice, который необходимо передать в качестве параметра нашей новой функции.

Ссылку на массив расширений, переданный в функцию выбора физического устройства необходимо дополнительно передать в функцию selectPhysicalDeviceByProperties. Внутри цикла этой функции необходимо вызвать функцию vkEnumerateDeviceExtensionProperties для заполнения вектора доступных расширений (по аналогии со слоями). Данная функция в качестве аргументов принимает физическое устройство, имя слоя (или nullptr, в случае если необходимо получить все расширения), адрес переменной под количество расширений и указатель на массив структуры VkExtensionProperties.

Пример получения расширений устройства:

uint32_t extensionsCount = 0;
vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionsCount, nullptr);
std::vector<VkExtensionProperties> extesions(extensionsCount);
vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionsCount, extesions.data());

Структура VkExtensionProperties содержит следующие поля:

  • extensionName — массив названий расширений;
  • specVersion — версия расширения.

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

int availableExtensionsCount = 0;
for (auto extension1 : requestedExtensions)
    for (auto extension2 : extensions)
        if (strcmp(extension1, extension2.extensionName) == 0)
        {
            availableExtensionsCount++;
            break;
        }

После подсчета можно дополнить условие проверки устройства (оценки):

availableExtensionsCount == requestedExtensions.size()

После заполнения структур VkDeviceCreateInfo и VkDeviceQueueCreateInfo можно вызвать функцию vkCreateDevice, которая принимает адрес данных по созданию логического устройства, адрес аллокатора и адрес объекта структуры VkDevice.

Необходимо дополнить метод Vulkan::destroy вызовом функции vkDestroyDevice для уничтожения логического устройства. Если этого не сделать приложение выдаст ошибку валидации в stdout от подключенного слоя проверок (валидации).

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

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

Вывод

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

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

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

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

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