Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
“Well! I’ve often seen a cat without a grin,” thought Alice;“but a grin without a cat! It’s the most curious thing I ever saw in all my life!”
В этой части мы рассмотрим выделение и освобождение памяти в Vulkan, а также создание буферов в памяти GPU.
В отличии от CPU, где есть просто оперативная память, на GPU есть несколько различных типов памяти. Поэтому когда мы делаем запрос на выделение памяти, то нам нужно указать не только объем выделяемой памяти, но и память какого типа мы хотим получить.
Vulkan предоставляет довольно простой способ получения информации обо всех типах памяти, имеющихся на конкретном физическом устройстве.
При помощи функции vkGetPhysicalDeviceMemoryProperties
мы можем получить структуру типа VkPhysicalDeviceMemoryProperties
,
описывающую все типы памяти, имеющиеся на данном устройстве.
Фактически данная структура состоит из двух массивов - массив типов памяти (memoryTypes
, число элементов в нем задается в memoryTypeCount
) и
массив куч (heap) памяти (memoryHeaps
, число элементов в нем задается в memoryHeapCount
).
Каждая куча (heap) это некоторый объем памяти, причем одна куча может содержать в себе память нескольких различных типов.
Тип памяти описывается всегда двумя величинами - индексом кучи heapIndex
, из которой можно выделять память этого типа, и
флагами propertyFlags
, описывающими свойства данной памяти.
Флаги это массив битов и есть различные биты, задающие те или иные свойства памяти, мы рассмотрим ниже лишь некоторые из них.
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
- память устройства, обычно невидимая CPU, является наиболее быстрой для доступа со стороны GPU;
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
- память, которая видна со стороны CPU и может быть отображена в адресное пространство CPU;
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
- для того, чтобы изменения сделанные на одной стороне, были сразу видны на другой, не нужны специальные команды;
такие как vkFlushMappedMemoryRanges
и vkInvalidateMappedMemoryRanges
.
Обычно для данных, постоянно хранящихся в памяти GPU и используемых только GPU (таких как вершинные и индексные буфера, текстуры и т.п.)
лучше всего подходит тип памяти с только одним флагом - VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
.
Комбинации флагов VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
и VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
обычно соответствует некоторая область памяти (обычно фиксированного размера),
в которую CPU может писать на каждом кадре.
Этот тип лучше всего подходит для постоянно изменяющихся данных, таких как uniform-буфера и динамические вершинные буфера.
Комбинация VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
и VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
обычно обозначает память CPU, которая
непосредственно видна GPU.
Если нет памяти предыдущего типа, то этот тип лучше всего подходит для постоянно изменяющихся данных.
Ниже приводится фрагмент кода, распечатывающий для заданного физического устройства информацию обо всех имеющихся типах и кучах памяти.
// get memory properties for physical device
VkPhysicalDeviceMemoryProperties memoryProperties;
vkGetPhysicalDeviceMemoryProperties ( physicalDevice, &memoryProperties );
// print info about all found types and heaps
std::cout << "Memory properties:" << std::endl;
std::cout << "\tmemoryTypeCount " << memoryProperties.memoryTypeCount << std::endl;
for ( uint32_t i = 0; i < memoryProperties.memoryTypeCount; i++ )
{
std::cout << "\t" << i << ": ";
// most efficient for device access
if ( memoryProperties.memoryTypes [i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT )
std::cout << "device local, ";
// can be mapped for host access
if ( memoryProperties.memoryTypes [i].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT )
std::cout << "host visible, ";
// host cache management commands are not needed to flush host writes to device
// or make device writes visible to host
if ( memoryProperties.memoryTypes [i].propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT )
std::cout << "host coherent, ";
// is cached on the host, uncached memory is slower but are always host coherent
if ( memoryProperties.memoryTypes [i].propertyFlags & VK_MEMORY_PROPERTY_HOST_CACHED_BIT )
std::cout << "host cached, ";
// only allows device access
if ( memoryProperties.memoryTypes [i].propertyFlags & VK_MEMORY_PROPERTY_PROTECTED_BIT )
std::cout << "protected (1.1), ";
std::cout << "Heap index " << memoryProperties.memoryTypes [i].heapIndex << std::endl;
}
std::cout << "memoryHeapCount " << memoryProperties.memoryHeapCount << std::endl;
for ( uint32_t i = 0; i < memoryProperties.memoryHeapCount; i++ )
{
std::cout << "\t Heap " << i << ": ";
std::cout << "\tSize " << memoryProperties.memoryHeaps [i].size;
// heap belongs to device-only memory
if ( memoryProperties.memoryHeaps [i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT )
std::cout << " device local, ";
// for logical device representing more than one physical device
if ( memoryProperties.memoryHeaps [i].flags & VK_MEMORY_HEAP_MULTI_INSTANCE_BIT )
std::cout << " multi instance ";
std::cout << std::endl;
}
Для своего GPU (GeForce 3060 RTX) я получаю следующую информацию:
Memory properties:
memoryTypeCount 5
0: Heap index 1
1: device local, Heap index 0
2: host visible, host coherent, Heap index 1
3: host visible, host coherent, host cached, Heap index 1
4: device local, host visible, host coherent, Heap index 2
memoryHeapCount 3
Heap 0: Size 12726566912 device local,
Heap 1: Size 8558542848
Heap 2: Size 224395264 device local,
При работе с такими объектами как буфера и изображения (image) у нас будет постоянно возникать необходимость выделения памяти с заданными свойствами. При этом в эти свойства входят не только требуемые флаги, но и маска допустимых типов памяти. Поэтому будет удобно сразу написать функцию, которая по этим свойствам буфет сама подбирать подходящий тип памяти.
// return index of memory type with given property bits
// satifying type bitmask
uint32_t findMemoryType ( VkPhysicalDevice physicalDevice, uint32_t typeFilter, VkMemoryPropertyFlags properties )
{
VkPhysicalDeviceMemoryProperties memoryProperties;
// get memory properties for the device
vkGetPhysicalDeviceMemoryProperties ( physicalDevice, &memoryProperties );
// check every nonmasked type to have all required bits
for ( uint32_t i = 0; i < memoryProperties.memoryTypeCount; i++ )
if ( (typeFilter & (1 << i)) && (memoryProperties.memoryTypes[i].propertyFlags & properties) == properties )
return i;
// type was not found
fatal () << "GpuMemory: failed to find suitable memory type! " << properties;
return 0; // we won't get here, but compiler complains about returning no value
}
Для выделения памяти в Vulkan служит функция vkAllocateMemory
.
Она принимает на вход логическое устройство, указатель на структуру типа VkMemoryAllocateInfo
, указывающие требуемые свойства выделяемой памяти,
а также свой аллокатор памяти CPU и адрес переменной типа VkDeviceMemory
, в которую будет записан хэндл выделенной памяти.
VkResult vkAllocateMemory ( VkDevice device, const VkMemoryAllocateInfo * info,
const VkAllocationCallbacks * allocator, VkDeviceMemory * memory );
В структуре VkMemoryAllocateInfo
в полях allocationSize
и memoryTypeIndex
мы передаем требуемый объем памяти в байтах
и номер типа памяти соответственно.
При помощи команды vkFreeMemory
мы можем освободить выделенную на устройстве память.
Если выделялась память, поддерживающая отображение в адресное пространство CPU, то мы можем выполнить это отображение
при помощи функции vkMapMemory
.
В результате ее вызова мы получим указатель в память CPU, по которому можно обращаться к этой памяти со стороны CPU.
// allocate memory with required properties
VkMemoryAllocateInfo allocInfo = {};
VkDeviceMemory memory = VK_NULL_HANDLE;
VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT ;
memRequirements.memoryTypeBits = properties;
memRequirements.size = size;
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = findMemoryType ( physicalDevice, memRequirements.memoryTypeBits, properties );
if ( vkAllocateMemory ( device, &allocInfo, nullptr, &memory ) != VK_SUCCESS )
fatal () << "Cannot allocate memory for buffer" << std::endl;
// map allocated memory into CPU address space and copy data
void * data;
std::vector<float> testData ( 1024 );
for ( int i = 0; i < 1024; i++ )
testData [i] = i;
vkMapMemory ( device, memory, 0, size, 0, &data );
memcpy ( (char *) data, testData.data (), size );
vkUnmapMemory ( device, memory );
Обратите внимание, что операция отображения памяти в адресное пространство CPU довольно дорогая. Поэтому в тех случаях, когда нам нужно часто обновлять данные (например в случае uniform-буферов) имеет смысл сразу после выделения памяти получить ее отображение и держать это отображение и использовать все время работы (то что в OpenGL называется persistent mapping).
Также очень важной особенностью выделения памяти устройства является то, что во-первых это тоже довольно дорогая операция и во-вторых, гарантируется только 4096 успешных выделений.
Поэтому нормальной практикой является памяти сразу большими блоками, после чего память из блоков раздается по частям.
Но при этом есть момент, на который нужно обязательно обратить внимание - у буферов, изображений и других объектов могут быть требования по выравниванию выделенной памяти.
Сама команда vkAllocateMemory
гарантирует выравнивание, достаточное для любых целей.
Но когда вы начинаете раздавать большой блок по частям, то важно учитывать требования на выравнивание данных.
Обычно требования на выравнивание заметно отличается для буферов и изображений и имеет смысл выделять память под них из разных блоков.
Если у вас память без флага VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
, то возможно что какие-то операции записи в эту память находятся в кэше и
поэтому не могут быть сразу видны.
Для гарантии того, что все подобные записи будут видны на стороне GPU, служит функция vkFlushMappedMemoryRanges
:
VkResult vkFlushMappedMemoryRanges(
VkDevice device,
uint32_t memoryRangeCount,
const VkMappedMemoryRange* pMemoryRanges );
После вызова этой функции гарантируется, что все записи, со стороны CPU, в области памяти, заданные в pMemoryRanges
будут видны на GPU.
Структура VkMappedMemoryRange
задается следующим образом:
typedef struct VkMappedMemoryRange {
VkStructureType sType;
const void* pNext;
VkDeviceMemory memory;
VkDeviceSize offset;
VkDeviceSize size;
} VkMappedMemoryRange;
Функция vkInvalidateMappedMemoryRanges
служит для другой цели - сбросить данные в кэше CPU для определенных участков памяти, чтобы записи, сделанные на стороне GPU,
были видны на стороне CPU.
VkResult vkInvalidateMappedMemoryRanges (
VkDevice device,
uint32_t memoryRangeCount,
const VkMappedMemoryRange* pMemoryRanges );
Буфера в Vulkan - это области памяти для хранения данных. Есть много различных типов буферов, таких как вершинных буфера, индексные буфера, uniform-буфера и другие. Важным отличием буферов в Vulkan от буферов в OpenGL является то что они не выделяют сами под себя память - ее необходимо явно выделить и прикрепить к буферу (а также освободить в конце). Каждый буфер в Vulkan идентифицируется при помощи значения типа VkBuffer.
Рассмотрим теперь сам процесс создания буфера с самого начала и до заполнения его данными.
Для создания буфера нам понадобится экземпляр структуры VkBufferCreateInfo
, содержащий в себе
необходимую информацию о нашем буфере.
// now create storage buffer for 1024 floats
VkBuffer buffer = VK_NULL_HANDLE;
VkBufferCreateInfo bufferInfo = {};
uint32_t size = 1024 * sizeof ( float );
VkMemoryRequirements memRequirements;
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = size;
bufferInfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if ( vkCreateBuffer ( device, &bufferInfo, nullptr, &buffer ) != VK_SUCCESS )
fatal () << "Buffer: failed to create buffer!" << std::endl;;
Поле usage
задает допустимое применение буфера и является битовой маской.
Допустимыми битами являются VK_BUFFER_USAGE_TRANSFER_SRC_BIT
, VK_BUFFER_USAGE_TRANSFER_DST_BIT
, VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT
,
VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT
, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT
, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT
,
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT
, VK_BUFFER_USAGE_VETEX_BUFFER_BIT
и VK_BUFFER_USAGE_INDEX_BUFFER_BIT
.
Также есть еще дополнительные биты вводимые расширениями Vulkan.
Поле flags
позволяет задавать дополнительные параметры буфера и также является битовой маской, содержащей такие биты как
VK_BUFFER_CREATE_SPARSE_BINDING_BIT
, VK_BUFFER_CREATE_SPARSE_RESIDENCY_BIT
и VK_BUFFER_CREATE_SPARSE_ALIASED_BIT
.
Могут быть и другие биты, вводимые более поздними версиями Vulkan и расширениями.
Поле size
задает размер буфера в байтах.
Поле sharingMode
возможно ли обращаться к буферу сразу из очередей, принадлежащим различным семействам очередей.
Допустимыми значениями являются VK_SHARING_MODE_EXCLUSIVE
(доступ возможен только из одного семейства очередей) и VK_SHARING_MODE_CONCURRENT
(доступ возможен из нескольких семейств очередей).
Поля queueFamilyIndexCount
и pQueueFamilyIndices
служат для задания массива индексов для семейств очередей, из которых возможен
доступ к этому буферу.
Игнорируется если sharingMode
равно VK_SHARING_MODE_EXCLUSIVE
.
После заполнения этой структуры мы создаем буфер при помощи вызова vCreateBuffer
.
Уничтожить буфер можно при помощи вызова vkDestroyBuffer
.
VkResult vkCreateBuffer ( VkDevice device, const VkBufferCreateInfo * info, const VkAllocationCallbacks *, VkBuffer * );
Следующим шагом после создания буфера будет выделение памяти для него и прикрепление выделенной памяти к буферу.
При помощи функции vkGetBufferMemoryRequirements
можно получить информацию о требуемой для буфера памяти.
Это информация возвращается через поля структуры VkMemoryRequirements
.
Полями этой структуры являются size
(размер в байтах), alignment
(требуемое выравнивание в байтах) и memoryTypeBits
.
Последнее поле является битовой маской , содержащей по одному биту для каждого типа памяти. Бит, равный единице, обозначает, что можно выделять память этого типа под данный буфер.
// now we need to allocate memory for this buffer
// so get memory requirements for it
vkGetBufferMemoryRequirements ( device, buffer, &memRequirements );
// allocate memory with required properties (
VkMemoryAllocateInfo allocInfo = {};
VkDeviceMemory memory = VK_NULL_HANDLE;
VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT ;
memRequirements.memoryTypeBits = properties;
memRequirements.size = size;
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = findMemoryType ( physicalDevice, memRequirements.memoryTypeBits, properties );
if ( vkAllocateMemory ( device, &allocInfo, nullptr, &memory ) != VK_SUCCESS )
fatal () << "Cannot allocate memory for buffer" << std::endl;
// map allocated memory into CPU address space and copy data
void * data;
std::vector<float> testData ( 1024 );
for ( int i = 0; i < 1024; i++ )
testData [i] = i;
vkMapMemory ( device, memory, 0, size, 0, &data );
memcpy ( (char *) data, testData.data (), size );
vkUnmapMemory ( device, memory );
// now we can bind allocated and initialized memory to our buffer
vkBindBufferMemory ( device, buffer, memory, 0 );
Код к этой статье (исходный код на С++ и проект для cmake) можно скачать по этой ссылке.