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

Vulkan API 5: буферы данных и командные буферы

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

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

Важное замечание: так как в терминологии Vulkan API присутствуют буферы (buffers) как буферы данных, а так же командные буферы (command buffers). Во избежание путаницы автор будет называть обычные буферы — буферами данных.

Создание буферов данных

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

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

В рамках заметки необходимо создать два буфера: вершин и индексов.

Для создания буфера данных добавим приватный метод к классу Vulkan::createDataBuffer. Необходимо чтобы данный буфер принимал в качестве аргументов размер в байтах, предназначение буфера (битовая маска), параметры памяти, а так же ссылки на дескриптор буфера и дескриптор памяти буфера.

Чтобы создать буфер для хранения произвольных данных необходимо заполнить объект структуры VkBufferCreateInfo, содержащий информацию о создаваемом буфере. Рассмотрим структуру подробнее:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
  • pNext — указатель на расширение структуры (не используется);
  • flags — битовая маска флагов, заданная перечислением (enum) VkBufferCreateFlagBits и определяющая параметры буфера (например работа с разряженным размещением в памяти);
  • size — размер буфера в байтах;
  • usage — битовая маска флагов, заданная перечислением (enum) VkBufferUsageFlagBits и определяющая предназначение буфера, например:
    • VK_BUFFER_USAGE_TRANSFER_SRC_BIT — источник данных для операций переноса (копирования данных),
    • VK_BUFFER_USAGE_TRANSFER_DST_BIT — цель (назначение) для операций переноса (копирования данных),
    • VK_BUFFER_USAGE_VERTEX_BUFFER_BIT — буфер вершин,
    • VK_BUFFER_USAGE_INDEX_BUFFER_BIT — буфер индесов,
    • VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT — используется для хранения uniform переменных;
  • sharingMode — битовая маска флагов, заданная перечислением (enum) VkSharingMode и определяющая режимы обмена:
    • VK_SHARING_MODE_EXCLUSIVE — доступ эксклюзивный для одного семейства очередей за раз,
    • VK_SHARING_MODE_CONCURRENT — поддерживается одновременный доступ из нескольких семейств очередей;
  • queueFamilyIndexCount — количество семейств очередей, которым доступен данный буфер данных;
  • pQueueFamilyIndices — адрес массива индексов семейств очередей, которым доступен данный буфер данных.

Функция vkCreateBuffer служит для создания буфера и принимает в качестве аргументов дескриптор логического устройства, адрес объекта структуры VkBufferCreateInfo, адрес аллокатора и адрес дескриптора создаваемого буфера.

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

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

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
  • pNext — указатель на расширение структуры (не используется);
  • allocationSize — размер выделяемой памяти в байтах;
  • memoryTypeIndex — индекс массива типов памяти (PhysicalDevice::memory.memoryTypes).

Размер выделяемой памяти (allocationSize) можно получить из поля структуры VkMemoryRequirements::size.

Индекс массива типов памяти необходимо выбрать, перебрав типы памяти в цикле. Пример выбора:

// Требования к памяти
VkMemoryRequirements memRequirements;
vkGetBufferMemoryRequirements(logicalDevice, buffer, &memRequirements);

// Поиск индекса типа подходящей памяти
uint32_t index_memory;
for (index_memory = 0; index_memory < physicalDevice.memory.memoryTypeCount; index_memory++) 
{
    if ((memRequirements.memoryTypeBits & (1 << index_memory)) && (physicalDevice.memory.memoryTypes[index_memory].propertyFlags & properties) == properties) 
    {
        break;
    }
}

// Если индекс равен размеру массива - поиск не удался и нужно выдать исключение
if (index_memory == physicalDevice.memory.memoryTypeCount)
    throw std::runtime_error("Unable to find suitable memory type");

После выделения памяти её необходимо привязать к созданному буферу. Для привязки используется функция vkBindBufferMemory, принимающая в качестве аргументов дескрипторы логического устройства, буфера, выделенной памяти и сдвиг в памяти (в заметках не используется = 0).

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

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

Создание командных буферов

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

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

Чтобы создать пул команд необходимо заполнить объект структуры VkCommandPoolCreateInfo, содержащий данные о создаваемом командном пуле. Рассмотрим структуру подробнее:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
  • pNext — указатель на расширение структуры (не используется);
  • flags — битовая маска флагов, заданная перечислением (enum) VkCommandPoolCreateFlags и определяющих поведение пула и командных буферов, выделяемых из него:
    • VK_COMMAND_POOL_CREATE_TRANSIENT_BIT — командные буферы имеют небольшую продолжительность жизни и будут возвращены в пул команд вскоре после использования,
    • VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT — позволяет перезаписывать или сбрасывать существующие буферы;
  • queueFamilyIndex — индекс семейства очередей для которого создается пул команд (в заметке используется индекс семейства, выбранного для графики).

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

В заметке используется только один пул команд, для его создания добавим в приватную область класса Vulkan поле:

VkCommandPool commandPool;

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

// Информация о создаваемом командном пуле
VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
poolInfo.queueFamilyIndex = queue.index;

// Создание командного пула
if (vkCreateCommandPool(logicalDevice, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) 
{
    throw std::runtime_error("Unable to create graphics command pool");
}

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

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

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
  • pNext — указатель на расширение структуры (не используется);
  • commandPool — дескриптор командного пула;
  • level — уровень командных буферов (первичные могут вызывать вторичные):
    • VK_COMMAND_BUFFER_LEVEL_PRIMARY — первичные,
    • VK_COMMAND_BUFFER_LEVEL_SECONDARY — вторичные;
  • commandBufferCount — количество командных буферов, которые необходимо выделить (количество буферов соответствует количеству изображений цепочки показа).

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

Для хранения дескрипторов буферов команд добавим в приватную область класса Vulkan поле:

std::vector<VkCommandBuffer> commandBuffers;

Дополним метод Vulkan::createCommandPool, выделением командных буферов:

// Выделим память под буферы команд
commandBuffers.resize(swapChainImages.size());

// Информация о выделяемых буферах
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = (uint32_t) commandBuffers.size();

// Выделение буферов команд из пула команд
if (vkAllocateCommandBuffers(logicalDevice, &allocInfo, commandBuffers.data()) != VK_SUCCESS) 
{
    throw std::runtime_error("Unable to allocate command buffers");
}

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

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

Командный буфер копирования

Как говорилось ранее: для произведения операций копирования необходимо создать командный буфер, выполняющий данные операции. Необходимо добавить приватный метод Vulkan::copyBuffer, который будет выполнять действие копирования данных на устройство.

Запись команд в командный буфер производится с помощью функций vkBeginCommandBuffer и vkEndCommandBuffer, обе принимают дескриптор командного буфера, но для функции vkBeginCommandBuffer требуется дополнительно передать адрес объекта структуры VkCommandBufferBeginInfo, определяющий операцию начала буфера команд. Рассмотрим эту структуру подробнее:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
  • pNext — указатель на расширение структуры (не используется);
  • flags — битовая маска флагов, заданная перечислением (enum) VkCommandBufferUsageFlagBits и определяющих поведение буфера команд:
    • VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT — командный буфер записывается и после первого использования будет удален,
    • VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT — командный буфер будет использован внутри прохода рендера,
    • VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT — командный буфер выполняется несколько раз;
  • pInheritanceInfo — адрес объекта структуры VkCommandBufferInheritanceInfo, который используется для вторичных командных буферов содержащих информацию о наследуемых состояниях.

Для описания операции копирования необходимо вызвать функцию vkCmdCopyBuffer, которая принимает в качестве аргументов дескрипторы командного буфера, исходного буфера данных, целевого буфера данных, а так же размер и адрес массива объектов структуры VkBufferCopy, описывающих регион копирования. Рассмотрим структуру VkBufferCopy подробнее:

  • srcOffset — сдвиг в исходном буфере данных;
  • dstOffset — сдвиг в целевом буфере данных;
  • size — размер копируемой области.

Запустить созданный буфер команд можно с помощью асинхронной функции vkQueueSubmit, которая принимает в качестве аргументов дескриптор очереди, размер и адрес массива объектов структур VkSubmitInfo, а так же дескриптор барьера (инструмент синхронизации, рассматриваемый в дальнейших заметках). Рассмотрим структуру VkSubmitInfo:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_SUBMIT_INFO;
  • pNext — указатель на расширение структуры (не используется);
  • waitSemaphoreCount — количество ожидаемых семафоров;
  • pWaitSemaphores — адрес массива ожидаемых семафоров;
  • pWaitDstStageMask — адрес массива этапов конвейера, на которых будет происходить ожидание каждого соответствующего семафора;
  • commandBufferCount — количество выполняемых командных буферов;
  • pCommandBuffers — адрес массива выполняемых командных буферов;
  • signalSemaphoreCount — количество семафоров, сигнализирующих после буфера команд;
  • pSignalSemaphores — адрес массива семафоров, сигнализирующих после буфера команд.

Важное примечание: семафоры рассматриваются в заметке про синхронизацию.

Функция vkQueueWaitIdle ожидает выполнения очереди и принимает в качестве аргументов дескриптор очереди.

Пример создания, заполнения и запуска буфера команд:

// Информация о выделяемом буфере команд
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandPool = commandPool;
allocInfo.commandBufferCount = 1;

// Дескриптор и выделение буфера команд
VkCommandBuffer commandBuffer;
vkAllocateCommandBuffers(logicalDevice, &allocInfo, &commandBuffer);

// Начало записи команд
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
vkBeginCommandBuffer(commandBuffer, &beginInfo);

// Операция копирования
VkBufferCopy copyRegion{};
copyRegion.size = size;
vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, &copyRegion);

// Конец записи команд
vkEndCommandBuffer(commandBuffer);

// Информация о запускаемых буферах команд
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;

// Запуск командного буфера копирования и ожидание завершения
vkQueueSubmit(queue.descriptor, 1, &submitInfo, VK_NULL_HANDLE);
vkQueueWaitIdle(queue.descriptor);

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

Теперь, когда весь необходимый инструментарий создан — можно приступить к созданию буферов вершин и индексов.

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

Создание буфера вершин

Добавим приватный метод к классу Vulkan::createVertexBuffer, а так же дескрипторы буфера данных и его памяти.

Запишем в массив вершин (Vertex) данные о четырех вершинах и их цветах:

Vertex vertices[] = {
                    {{-0.5f, -0.5f},{1.0f, 0.0f, 0.0f}},
                    {{ 0.5f, -0.5f},{0.0f, 1.0f, 0.0f}},
                    {{ 0.5f,  0.5f},{0.0f, 0.0f, 1.0f}},
                    {{-0.5f,  0.5f},{1.0f, 1.0f, 1.0f}}
                    };

Важное замечание: в ходе написания заметки 4 была допущена ошибка — шейдер ожидает входные данные положения вершин в виде вектора двух вещественных чисел, а структура Vertex хранит вектор трех. Необходимо заменить вектор трех, на вектор двух вещественных чисел в составе структуры Vertex.

Так как размер буфера требуется указывать в нескольких местах — произведем его расчет и запишем в переменную bufferSize:

VkDeviceSize bufferSize = sizeof(Vertex) * 4;

Создадим промежуточный буфер данных:

VkBuffer stagingBuffer;
VkDeviceMemory stagingBufferMemory;
createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory);

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

  • VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT — возможность отображения в память хоста;
  • VK_MEMORY_PROPERTY_HOST_COHERENT_BIT — согласование памяти хоста и графического устройства.

Функция vkMapMemory включает отображение буфера данных в памяти хоста. В качестве аргументов эта функция принимает дескрипторы логического устройства и памяти промежуточного буфера, сдвиг в памяти, её размер, флаги отображения памяти и адрес указателя без типа (void*) на данные отображения.

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

Теперь можно создать буфер данных, предназначенный для хранения вершин:

createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory);

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

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

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

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

Пример освобождения памяти и удаления промежуточного буфера:

vkDestroyBuffer(logicalDevice, stagingBuffer, nullptr);
vkFreeMemory(logicalDevice, stagingBufferMemory, nullptr);

Необходимо дополнить метод Vulkan::destroy для освобождения памяти и удаления буфера вершин по аналогии с промежуточным.

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

Создание буфера индексов

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

Добавим приватный метод к классу Vulkan::createIndexBuffer, а так же дескрипторы буфера данных и его памяти.

Запишем в массив индексов данные о используемых для примитивов вершинах:

uint16_t indices[] = {0, 1, 2, 2, 3, 0};

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

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

Вывод

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

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

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

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

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