Vulkan API — урок 35. Память буфера вершин (+ листинг)

Итак, буфер был создан, но ему еще не присвоена память. Первый шаг – выделение памяти для буфера. Опишем запрос требований к памяти используя функцию vkGetBufferMemoryRequirements.

VkMemoryRequirements memRequirements;
vkGetBufferMemoryRequirements(device, vertexBuffer, &memRequirements);

Структура VkMemoryRequirements имеет три поля:

  • size: Размер требуемого объема памяти в байтах, может отличаться bufferInfo.size.
  • alignment: Смещение в байтах, где начало буфера в выделенном участке памяти, основывается на bufferInfo.usage и bufferInfo.flags.
  • memoryTypeBits: Битовое поле типов памяти, которые подходят для буфера.

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

uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) {

}

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

VkPhysicalDeviceMemoryProperties memProperties;
vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);

Структура VkPhysicalDeviceMemoryProperties имеет два массива memoryTypes and memoryHeaps. Heaps (кучи) памяти представляют собой различные ресурсы памяти, на подобии выделенного VRAM и пространства подкачки в RAM (когда VRAM заканчивается). Различные типы памяти имеют место быть в этих heaps.  Прямо сейчас же мы касаемся только типов памяти, но не heap, откуда они происходят, но следует упомянуть, что это может повлиять на производительность.

Найдем тип памяти, который подходит для самого буфера:

for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
    if (typeFilter & (1 << i)) {
        return i;
    }
}

throw std::runtime_error("failed to find suitable memory type!");

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

Но мы заинтересованы не только в типе памяти, который подойдет для буфера вершин, нам так же нужна возможность записать наши данные вершин в эту память. Массив memoryTypes состоит из структур VkMemoryType, которые задают heap и свойства каждого типа памяти. Свойства определяют особенности памяти, такую как возможность разметить (map) её таким образом, что бы можно произвести запись из CPU. Это свойство обозначается VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, но мы также должны использовать свойство  VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, про него чуть позже

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

for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
    if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
        return i;
    }
}

Может понадобится не одно свойство, потому необходимо проверять, что не только на побитовое И не равно нулю,  но и конструкция в целом соответствует желаемому результату.

Выделение памяти

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

VkMemoryAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);

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

VDeleter<VkBuffer> vertexBuffer{device, vkDestroyBuffer};
VDeleter<VkDeviceMemory> vertexBufferMemory{device, vkFreeMemory};

...

if (vkAllocateMemory(device, &allocInfo, nullptr, vertexBufferMemory.replace()) != VK_SUCCESS) {
    throw std::runtime_error("failed to allocate vertex buffer memory!");
}

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

Если выделение памяти прошло успешно, то её можно связать с буфером, используя vkBindBufferMemory:

vkBindBufferMemory(device, vertexBuffer, vertexBufferMemory, 0);

Последний параметр – смещение в области памяти. Т.к. эта память выделяется специально для буфера вершин, смещение просто 0. Если же оно не будет равно нулю, то значение должно быть кратным memRequirements.alignment.

Заполнение буфера вершин

Теперь скопируем данные вершин в буфер. Это можно сделать разметив (mapping) память буфера (Memory-mapped I/O ≈ Порт ввода-вывода) в памяти доступной CPU при помощи vkMapMemory.

void* data;
vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data);

Эта функция позволяет получить доступ к указанному участку памяти, определенного смещением и размером. Смещение и размер здесь 0 и bufferInfo.size соответственно. Кроме того можно указать специальное значение  VK_WHOLE_SIZE для разметки (map) всей памяти. Предпоследний параметр может быть использован для указания флагов, но они пока не доступны в текущей версии API. Последний параметр определяет указатель на размеченную память.

void* data;
vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data);
    memcpy(data, vertices.data(), (size_t) bufferInfo.size);
vkUnmapMemory(device, vertexBufferMemory);

Ранее уже упоминалась функция memcpy, благодаря которой данные будут перемещены в размеченную память и обратно (unmap) при помощи vkUnmapMemory. К сожалению драйвер не может мгновенно скопировать данные в память буфера, например из-за кеширования. Так же возможно, что записи в буфере еще не видны в размеченной памяти. Для решения этой проблемы есть два способа:

  • Использовать heap с когерентным управлением, обозначается VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
  • Вызвать vkFlushMappedMemoryRanges после записи в размеченой памяти, и вызвать vkInvalidateMappedMemoryRanges пред чтением из той же памяти

Мы пошли первым путем, что гарантирует совпадение содержимого размеченной памяти и выделенной. Имейте в виду, что это может повлиять на производительность в худшую сторону, в сравнении с явным flush’ингом, но это не имеет значения, о чем в одном из следующих двух уроков.

Привязка буферу вершин

А теперь свяжем все это с буфером вершин, во время операций рендеринга, а именно в функции createCommandBuffers:

vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);

VkBuffer vertexBuffers[] = {vertexBuffer};
VkDeviceSize offsets[] = {0};
vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets);

vkCmdDraw(commandBuffers[i], vertices.size(), 1, 0, 0);

Функция vkCmdBindVertexBuffers используется для привязки буферов вершин. Второй и третий параметр указывают смещение и количество привязок, для которых мы определяем буферы вершин. Последние два параметра определяют массив буферов вершин и смещение в байтах до начала чтения данных вершин. Так же нужно отредактировать вызов vkCmdDraw, ведь количество вершин у нас более не жестко задано.

Запустим программу, увидим:

Изменим цвета вершин в массиве (что бы проверить работоспособность, вдруг забыли перекомпилировать шейдер):

const std::vector<Vertex> vertices = {
    {{0.0f, -0.5f}, {1.0f, 1.0f, 1.0f}},
    {{0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}},
    {{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}}
};

Запустим еще раз, увидим:

Ну и листинг.

Main Admin

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

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