Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
"You useed to be much more muchierYou've lost your muchness".
The Mad Hatter
Как вы уже могли ранее заметить выделение памяти GPU в Vulkan дело очень непростое. Есть разные типы памяти и разные требования к ней (для буферов и изображений). И очень важно выбрать правильный тип памяти, иначе вы получите ошибку на этапе выполнения или просто неэффективную работу на GPU.
Кроме этого есть еще одно хитрое место - по стандарту Vulkan вы можете гарантированно выделить только 4096 выделенных участков памяти. Возможно у вас GPU/драйвер поддерживает большее число максимальных выделений памяти, но всеже не на столько много, как этого бы хотелось (или требовалось). И, что также справедливо, выделение и освобождение памяти GPU это дорогая операция, поэтому лучше минимизировать число этих операций.
Это все приводит к тому, что обычно память GPU выделяется сразу большими блоками. После этого из блока подходящего типа выделяется часть, соответствующая требованиям. Таким образом уменьшается число дорогих операций выделения памяти Vulkan и обходится ограничение на максимальное число выделений. Однако это не так просто как может показаться - есть разные типы памяти и разные объекты обычно требуют память с надлежащим выравниванием, что нужно обязательно соблюдать.
К счастью есть довольно удобная и простая библиотека VMA (Vulkan Memory Allocator), которая
может всю эту работу взять на себя.
Подобно библиотеке STB она вся содержится в одном заголовочном файле - vk_mem_alloc.h
.
В этом файле содержатся и описания всех функций и их реализации.
Поэтому один раз перед включением этого файла нужно включить определить макрос VMA_IMPLEMENTATION
.
#define VMA_IMPLEMENTATION
#include <vk_mem_alloc.h>
Обратите внимание, что данный файл включает файл vulkan/vulkan.h
, а он (под Windows) включает файл windows.h
.
У этой библиотеки С-интерфейс, хотя внутри она написана на самом деле на С++.
Библиотеку VMA необходимо проинициализировать перед использованием и деиницилизировыать перед завершением работы с Vulkan (иначе вы получите предупреждение о не освобожденной памяти в слоях валидации).
Инициализация состоит в создании аллокатора, который будет использоваться для выделения памяти:
vmaDestroyAllocator ( allocator );
Перед завершением работы созданный аллокатор необходимо уничтожить при помощи вызова функции vmaDestroyAllocator
:
VmaAllocator allocator;
VmaAllocatorCreateInfo allocatorCreateInfo = {};
allocatorCreateInfo.vulkanApiVersion = VK_API_VERSION_1_0;
allocatorCreateInfo.physicalDevice = physicalDevice;
allocatorCreateInfo.device = device;
allocatorCreateInfo.instance = instance;
if ( vmaCreateAllocator ( &allocatorCreateInfo, &allocator ) != VK_SUCCESS )
fatal () << "vmasCreateAllocator failure" << std::endl;
Ранее мы отдельно создавали буфер/изображение, запрашивали свойства требуемой памяти, выделяли ее и подключали к буферу/изображению.
Библиотека VMA предлагает для создания буферов и изображений использовать специальные функции vmaCreateBuffer
и vmaCreateImage
.
Каждая из этих функций не просто создает требуемый объект (буфер или изображение), но и выделяет под него память требуемого типа и прикрепляет ее нему. Рассмотрим в качестве примера создание изображения.
VkImageCreateInfo imageInfo = {};
VkImage image = VK_NULL_HANDLE;
VmaAllocationCreateInfo allocCreateInfo = {};
VmaAllocation allocation = VK_NULL_HANDLE;
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.extent.width = 512;
imageInfo.extent.height = 512;
imageInfo.extent.depth = 1;
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;
imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
allocCreateInfo.usage = VMA_MEMORY_USAGE_AUTO;
allocCreateInfo.flags = 0;
allocCreateInfo.priority = 1.0f;
if ( vmaCreateImage ( allocator, &imageInfo, &allocCreateInfo, &image, &allocation, nullptr ) != VK_SUCCESS )
fatal () << "vmaImage: Cannot create image" << std::endl;
Для уничтожения созданных таким образом буферов и изображений также следует использовать функции из библиотеки VMA - vmaDestroyBuffer
и vmaDestroyImage
.
vmaDestroyImage ( allocator, image, allocation );
При создании буферов и изображений мы через поля структуры VmaAllocatorCreateInfo
передаем наши пожелания по тому,
какую именно память следует выделить.
Через поле flags
мы передаем свои требования к выделяемому боку памяти в виде битовых флагов.
Я не буду перечислять все допустимые флаги, так как их очень много, а укажу лишь наиболее полезные и распространенные.
VMA_ALLOCATION_CREATE_MAPPED_BIT
- обозначает, что мы хотим, чтобы эта память была все время отображена в память CPU.
VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT
- память может быть отображена в адресное пространство CPU и пригодна для последовательной записи.
VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT
- отображение в адресное пространство CPU должно поддерживать произвольный доступ на чтение и запись.
Поле usage
позволяет задать планируемое использование выделяемой памяти.
Основными возможными значениями являются:
VMA_MEMORY_USAGE_AUTO
- выбрать наиболее подходящий тип памяти автоматически. Скорее всего вам нужно именно это значение.
Если вы хотите, чтобы выделенную память можно было отобразить в адресное пространство CPU, то надо задать флаги *_CREATE_MAPPED_BIT
и
один из флагов *_HOST_ACCESS_RANDOM_BIT
или *_SEQUENTIAL*
.
VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE
- по возможности выбрать память GPU
VMA_MEMORY_USAGE_AUTO_PREFER_HOST
- по возможности выбрать память CPU
С каждым выделенным участком памяти можно связать указатель и имя при помощи следующий функций:
void vmaSetAllocationUserData ( VmaAllocator allocator, VmaAllocation allocation, void * pUserData );
void vmaSetAllocationName ( VmaAllocator allocator, VmaAllocation allocation, const char * name );
В любой момент можно получить информацию о выделенном участке памяти при помощи функций vmaGetAllocationInfo
и vmaGetAllocationMemoryProperties
.
void vmaGetAllocationInfo ( VmaAllocator allocator, VmaAllocation allocation, VmaAllocationInfo * pAllocationInfo );
void vmaGetAllocationMemoryProperties ( VmaAllocator allocator, VmaAllocation allocation, VkMemoryPropertyFlags * flags );
Информацию о выделенном участке памяти возвращается в полях структуры VmaAllocationInfo
:
memoryType
- индекс типа памяти
deviceMemory
- хэндл бока памяти
offset
- смещение внутри выделенного блока памяти
size
- размер выделенного ока памяти в байтах
pMappedData
- указатель на начало отображенной памяти
pUserData
- заданный пользовательский указатель
pName
- заданное пользователем имя
Для отображения выделенной памяти в адресное пространство CPU служат функции:
VkResult vmaMapMemory ( VmaAllocator allocator, VmaAllocation allocation, void **ppData );
void vmaUnmapMemory ( VmaAllocator allocator, VmaAllocation allocation );
При использовании библиотеки VMA сама библиотека выделяет большие блоки памяти и раздает их по частям. Сама библиотека отвечает за тип памяти, выравнивание и т.п. По этой ссылке можно скачать пример с треугольником из более ранней статьи, переписанный с использованием библиотеки VMA.
Код к этой статье (исходный код на С++ и проект для cmake) можно скачать по этой ссылке.