前言
VMA(Vulkan Memory Allocator),是AMD提供的Vulkan内存分配管理器,那么Vulkan的内存分配为何要使用VMA这种内存分配器呢?原因就在于其显存的分配次数是有限的(比如4096次),那么我们就需要分配一整块显存,然后自己使用offset以及size来进行分割使用,这个过程冗长繁杂,而且容易出错,那么VMA就成为了我们管理Vulkan内存的首要选择。
Memory Mapping技术,是指把Vulkan当中生成的任何内存(VkDeviceMemory)映射到CPU端的一个void*指针的过程,这样我们就可以从CPU端读取或者写入这块内存。这种可以映射的内存可能只能适用于拥有VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT标识符的内存块。在Vulkan当中,我们是使用vkMapMemory这个函数进行映射操作,使用vkUnmapMemory这个函数进行映射解除操作。你可以直接使用Vulkan的这两个函数对VMA生成的内存进行操作,但是呢,我们并不推荐这么做,因为:多次对同一块VkDeviceMemory进行Mapping的操作,在Vulkan当中是禁止的,一块内存只能够Mapping一次。所以在Vulkan当中,Mapping这个操作是没有操作计数这一说的,在VMA当中,我们就能够做到操作计数以及多次映射。
一、Mapping Functions
VMA(Vulkan Memory Allocator)提供了如下函数进行映射以及关闭映射操作: vmaMapMemory(), vmaUnmapMemory()。这组函数比Vulkan更加的便捷且安全。你可以同时对同一块内存(VmaAllocation)进行多次Mapping操作——Mapping这个操作是拥有操作计数的(Mapping一次就+1,UnMapping一次就-1)。对于多个VmaAllocation,他们可能来自同一个VkDeviceMemory,那么你仍然可以对他们进行各自的Mapping操作,这是安全的!原因是,VMA对于每一个VmaAllocation的Mapping操作,其实是把整个内存Chunk都进行了Mapping操作,并不是只Mapping了一个区域,我们可以这么写代码:
代码如下(示例):
// Having these objects initialized:
struct ConstantBuffer
{
...
};
ConstantBuffer constantBufferData;
VmaAllocator allocator;
VkBuffer constantBuffer;
VmaAllocation constantBufferAllocation;
// You can map and fill your buffer using following code:
void* mappedData;
vmaMapMemory(allocator, constantBufferAllocation, &mappedData);
memcpy(mappedData, &constantBufferData, sizeof(constantBufferData));
vmaUnmapMemory(allocator, constantBufferAllocation);
当进行Mapping操作的时候,你可能在Vulkan的Validation Layer看到一个警告:
Mapping an image with layout VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL can result in undefined behavior if this memory is used by the device. Only GENERAL or PREINITIALIZED should be used.
这个警告被发出,是因为VMA把整个VkDeviceMemory都给映射了,不同的Image或者Buffer可能会在同一块内存上,特别是在集成显卡上(比如Intel),如果你有完全把握,可以忽略这个警告。
二、Persistently mapped memory(永久Mapping)
在 Vulkan当种,我们可以永久的让一块内存处于Mapping状态。在GPU使用数据前你可以不进行Unmapping操作。VMA定义了一个特殊的特性Flags:使用VMA_ALLOCATION_CREATE_MAPPED_BIT 这个标识符生成的VmaAllocation会永久性的保持Mapping状态,你可以通过VkAllocationInfo这个结构体对象的pMappedData成员对内存进行直接访问,你可以这么写代码:
代码如下(示例):
VkBufferCreateInfo bufCreateInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
bufCreateInfo.size = sizeof(ConstantBuffer);
bufCreateInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
VmaAllocationCreateInfo allocCreateInfo = {};
allocCreateInfo.usage = VMA_MEMORY_USAGE_CPU_ONLY;
allocCreateInfo.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT;
VkBuffer buf;
VmaAllocation alloc;
VmaAllocationInfo allocInfo;
vmaCreateBuffer(allocator, &bufCreateInfo, &allocCreateInfo, &buf, &alloc, &allocInfo);
// Buffer is already mapped. You can access its memory.
memcpy(allocInfo.pMappedData, &constantBufferData, sizeof(constantBufferData));
当然,在某些情况下,你也得考虑只对内存进行短时间的Mapping操作:
- 当系统是Windows7或者8.x(Windows10不会受到影响,因为他使用了WDDM2图形驱动模型),设备是AMD独显,并且分配的内存是符合DEVICE_LOCAL + HOST_VISIBLE 特性的那个特殊的256M内存(也就是选择了VMA_MEMORY_USAGE_CPU_TO_GPU)。如果本内存满足如上所有条件且做了永久Mapping,那么当调用vkQueueSubmit()或者vkQueuePresentKHR()的时候,那么命令当中绑定的或者关联的内存就会被WDDM(Windows图形驱动模型)给移动到RAM当中,就会降低运行效率。这个行为不管本内存是否真正被提交的命令使用到了,都会触发。
- 保持大的内存块Mapping状态,会影响运行效率,并且影响Debug工具的稳定性。
三、Cache flush and invalidate
在Vulkan当中,除非GPU上要使用,你是不需要对Mapped内存进行UnMap操作的。但是呢,如果这块内存不具备VK_MEMORY_PROPERTY_HOST_COHERENT_BIT这个能力的话,CPU与GPU的数据就无法及时更新入内存(存在缓存)。你就得在CPU端读取之前,调用invalidate cache操作;在CPU写入数据后,进行flush cache操作(CPU的Cache到主存RAM都会有一些延迟)。Map/UnMap并不会自动的执行相关操作。所以Vulkan提供了vkFlushMappedMemoryRanges(), vkInvalidateMappedMemoryRanges()作为CPU写入/CPU读取相关的刷新函数。VMA提供了更为方便的接口 vmaFlushAllocation(), vmaInvalidateAllocation(), 并且我们可以同时操作多个内存,使用如下接口: vmaFlushAllocations(), vmaInvalidateAllocations()。
当不使用VK_MEMORY_PROPERTY_HOST_COHERENT_BIT的时候,我们flush/invalidate的内存区域大小需要符合VkPhysicalDeviceLimits::nonCoherentAtomSize的基本对齐。VMA会自动保证这个对齐。在任何的HOST_VISIBLE且非HOST_COHERENT的内存当中,所有的内存分配都是自动符合这个规则的。所以他们的offset值总是nonCoherentAtomSize的整数倍且两个不同的allocation是不会在同一个AtomSize内部重叠的。
请注意使用VMA_MEMORY_USAGE_CPU_ONLY 这种方式分配的VMA内存是保证HOST_COHERENT的。
当然,作为Windows的图形驱动,在如下三个GPU供应商上(AMD,Intel,NVIDIA),只要本内存拥有HOST_VISIBLE属性,就直接打开了HOST_COHERENT,所以在这些平台上你不需要烦恼这个问题哦。
四、检查内存是否可Mapping
在VMA当中,可能在你没有强制要求的情况下,给你分配的内存就拥有了HOST_VISIBLE的属性。比如你在Intel的集成显卡上工作或者在显存上分配失败而给到了你一个RAM的内存作为补偿。
你可以检测这种情况是否发生,如果发生了就可以直接对内存进行Mapping操作,从而直接操作其数据。为了做到这点,你可以调用 vmaGetAllocationMemoryProperties(),然后看下是否拥有VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT这个Flag。你可以这么写代码:
代码如下(示例):
VkBufferCreateInfo bufCreateInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
bufCreateInfo.size = sizeof(ConstantBuffer);
bufCreateInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
VmaAllocationCreateInfo allocCreateInfo = {};
allocCreateInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY;
allocCreateInfo.preferredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT;
VkBuffer buf;
VmaAllocation alloc;
vmaCreateBuffer(allocator, &bufCreateInfo, &allocCreateInfo, &buf, &alloc, nullptr);
VkMemoryPropertyFlags memFlags;
vmaGetAllocationMemoryProperties(allocator, alloc, &memFlags);
if((memFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0)
{
// Allocation ended up in mappable memory. You can map it and access it directly.
void* mappedData;
vmaMapMemory(allocator, alloc, &mappedData);
memcpy(mappedData, &constantBufferData, sizeof(constantBufferData));
vmaUnmapMemory(allocator, alloc);
}
else
{
// Allocation ended up in non-mappable memory.
// You need to create CPU-side buffer in VMA_MEMORY_USAGE_CPU_ONLY and make a transfer.
}
你也可以直接使用VMA_ALLOCATION_CREATE_MAPPED_BIT这个标识符直接创建Allocation(这个不一定是HOST_VISIBLE的要求),如果得到的内存属于HOST_VISIBLE,那么就可以测试下VmaALlocationInfo当中的pMappedData这个成员,看看是否已经被开启了永久Mapping,你可以这么写代码:
代码如下(示例):
VkBufferCreateInfo bufCreateInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
bufCreateInfo.size = sizeof(ConstantBuffer);
bufCreateInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
VmaAllocationCreateInfo allocCreateInfo = {};
allocCreateInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY;
allocCreateInfo.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT;
VkBuffer buf;
VmaAllocation alloc;
VmaAllocationInfo allocInfo;
vmaCreateBuffer(allocator, &bufCreateInfo, &allocCreateInfo, &buf, &alloc, &allocInfo);
if(allocInfo.pMappedData != nullptr)
{
// Allocation ended up in mappable memory.
// It is persistently mapped. You can access it directly.
memcpy(allocInfo.pMappedData, &constantBufferData, sizeof(constantBufferData));
}
else
{
// Allocation ended up in non-mappable memory.
// You need to create CPU-side buffer in VMA_MEMORY_USAGE_CPU_ONLY and make a transfer.
}
总结
以上就是今天的内容,大家对于vulkan的学习,也可以参考我出品的vulkan系列教程,下面给大家贴出链接。