Vulkan API — урок 37. Использование staging buffer (+листинг)

Теперь изменим createVertexBuffer таким образом, что бы использовать видимый CPU буфер, только как временный буфер и использовать локальный как фактический буфер устройства вершин.

void createVertexBuffer() {
    VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size();

    VDeleter<VkBuffer> stagingBuffer{device, vkDestroyBuffer};
    VDeleter<VkDeviceMemory> stagingBufferMemory{device, vkFreeMemory};
    createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory);

    void* data;
    vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data);
        memcpy(data, vertices.data(), (size_t) bufferSize);
    vkUnmapMemory(device, stagingBufferMemory);

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

В коде выше используем новые stagingBuffer с stagingBufferMemory для маппинга и копирования данных вершин.

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

  • VK_BUFFER_USAGE_TRANSFER_SRC_BIT: Буфер может быть использован как источник в операциях копирования (memory transfer operation).
  • VK_BUFFER_USAGE_TRANSFER_DST_BIT: Буфер может быть использован как получатель в операциях копирования (memory transfer operation).

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

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

void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {

}

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

void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {
    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(device, &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);

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

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

VkBufferCopy copyRegion = {};
copyRegion.srcOffset = 0; // Optional
copyRegion.dstOffset = 0; // Optional
copyRegion.size = size;
vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, &copyRegion);

Этот буфер команд создержит только команды vkCmdCopyBuffer копирования, так что мы можем закончить его работу сразу после этого

vkEndCommandBuffer(commandBuffer);

Теперь выполним буфер команд для завершения передачи. В отличии от команд рисования, на этот раз никаких особых операций ожидания не понадобится. Мы просто выполняем непосредственно передачу в буфер. Есть два возможных способа ожидать выполнения передачи. Мы можем использовать барьер и  vkWaitForFences, либо просто подождать пока очередь передачи будет простаивать – vkQueueWaitIdle.

VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;

vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
vkQueueWaitIdle(graphicsQueue);

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

vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

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

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

copyBuffer(stagingBuffer, vertexBuffer, bufferSize);

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

Так же стоит принять во внимание, что в реальных приложениях на самом деле вызывать vkAllocateMemory для каждого отдельного буфера не стоит. Максимальное количество выделений памяти ограничено и достаточно мало в физическом устройстве в maxMemoryAllocationCount, что может быть равно 4096 даже на новых видеокартах, например NVIDIA GTX 1080. Правильным путем будет выделить память для большого количества объектов и написать свой аллокатор, который при помощи смещения (offset, которое есть в большинстве функций) и будет определять что есть что в этой области.

И листинг.

Main Admin

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

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