前言
VMA(Vulkan Memory Allocator),是AMD提供的Vulkan内存分配管理器,那么Vulkan的内存分配为何要使用VMA这种内存分配器呢?原因就在于其显存的分配次数是有限的(比如4096次),那么我们就需要分配一整块显存,然后自己使用offset以及size来进行分割使用,这个过程冗长繁杂,而且容易出错,那么VMA就成为了我们管理Vulkan内存的首要选择。
- 程序可能不会出任何问题。
- 程序可能会变慢,因为某些GPU的内存(VRAM)会被分配到CPU端的内存种(RAM),那么GPU想访问这块内存就得通过PCI总线。
- 一个新的内存分配,可能会花费长达几秒钟,甚至可能冻结整个系统。
- 新的内存分配可能会返回VK_ERROR_OUT_OF_DEVICE_MEMORY。
- 可能会引发GPU的崩溃(crash),称为TDR(Timeout Detection & Recovery),返回的错误是VK_ERROR_DEVICE_LOST。
一、Querying for budget(检查内存预算)
为了获得当前系统内存使用情况以及可用的内存预算,使用 vmaGetHeapBudgets()这个VMA函数,返回一个 VmaBudget结构提,里面包含了一些数值变量(都是以bytes)为单位,描述了Vulkan的堆内存的情况。
先明确几个概念:
- 堆内存(Heap Memory)属于操作系统管辖范围,然后各种程序都有可能再这一块堆内存上拿到显存。
- 你自己的Vulkan程序的管辖范围,在这里面 ,有的内存分配是你自己管理,有的你可能会托付给VMA。
- VMA内存范围,是在Vulkan之上来管理,是指一切通过Vma进行Allocate分配的内存。VMA会在堆内存上先分配各种Chunk,然后再每个Chunk上自己再分配Allocation给到我们使用者
//函数定义,这里传入的pBudgets是一个已经分配好的内存数组
VMA_CALL_PRE void VMA_CALL_POST vmaGetHeapBudgets(
VmaAllocator allocator,
VmaBudget* pBudgets)
//返回结果结构
typedef struct VmaBudget
{
/** 本堆内存当中,已经被VMA分配出去的内存数量(这里是Chunk分配的总数bytes,有的被allocation用了,有的还没被用
*/
VkDeviceSize blockBytes;
/** 对于本堆内存来说,已经被VMA分配到Allocation当中的内存有多少,那么
`blockBytes - allocationBytes`这个减法结果,就是当前剩余的没被Allocation用掉的内存池,当然,由于内存碎片的存在,也有可能代表了一些没法被使用且浪费的内存。
*/
VkDeviceSize allocationBytes;
/**
这个数值,必须通过开启 `VK_EXT_memory_budget`的Vulkan特性才能获取
对于整个程序而言(Vulkan)层面上,这块Vulkan的堆内存被使用了多少
这个数值往往会大于blockBytes,因为这块堆内存不只是VMA再使用,可能还会被
SwapChain,Pipeline,DescriptorSet,CommandBuffer或者其他不是从VMA当中分配的VkDeviceMemory使用!
*/
VkDeviceSize usage;
/**
这个数值,必须通过开启 `VK_EXT_memory_budget`的Vulkan特性才能获取
描述了这块堆内存给到你自己的Vulkan程序的可见大小,有可能比 `VkMemoryHeap::size[heapIndex]`要小,因为
其他应用程序可能也会占用这块内存。
`budget - usage` 这个减法值就代表了可以被使用的剩余内存量。超过这个数值再去分配就各种问题了。
*/
VkDeviceSize budget;
} VmaBudget;
注意:
- 这里传入的pBudgets是一个已经分配好的内存数组,里面元素的数量必须大于等于当前显卡的堆内存的数量。
- allocationBytes这个值可能会大于blockBytes,因为存在内存换页,比如GPU想分配一个内存,结果不够用了,就把没用到的资源放到RAM,然后给新的腾地方,像这种也会被计算到allocationBytes当中。
请注意vmaGetHeapBudgets返回的数值信息不同于 vmaCalculateStats(). vmaGetHeapBudgets(),并且比他们更快。vmaGetHeapBudgets可以每一帧都调用甚至是每一次内存Allocation的分配之前都可以调用,vmaCalculateStats就用的比较少,只是为了得到静态信息,比如你要Debug。
我们应该使用VK_EXT_memory_budget 这个Device扩展去获得Vulkan的预算信息,VMA可以自动的应用这个扩展。当这个扩展没有被开启的时候,分配器的行为不会发生改变,但是当它估计当前的使用以及可用情况的时候,在精确度方面就出现了一些偏差。为了使用这个扩展:
- 确保VK_EXT_memory_budget (Device扩展)以及VK_KHR_get_physical_device_properties2 (Instance扩展)已经被开启了。
- 在创建VmaAllocator的时候,开启 VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT标识符。
- 在每一帧都要调用vmaSetCurrentFrameIndex这个函数,传入当前帧号,这个函数可以每一帧都更新当前的Budget预算。这种更新,是从Vulkan内部获取一次数据,然后本帧的各种Allocation都会依赖于这个数据进行检查,你不需要每一次Allocation都调用Vulkan内部的东西了。
二、Controlling memory usage(控制内存使用)
如果你想限定所有的Allocation操作都能够在Budget预算范围内搞定(比如不能换页,比如不能VRAM转RAM),不超出边界,还是有一些方法的:
首先,当你要本次的分配,需要分配一块全新的内存Chunk的时候,VMA会努力在Budget预算范围内进行分配。如果超出了这个预算范围,可能会分配一个比原定数额小的内存Chunk。即便你是需要分配一个Dedicated(专用)内存,也会被压制在预算范围内,给出一个小的内存块。
第二,如果你要分配的内存大小加上当前已经使用的大小,超出了内存Budget,默认来讲,VMA会把这件事丢给Vulkan去执行,让Vulkan来决定失败还是成功。
你可以通过启用VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT 这个Flags来避免丢给Vulkan决策,这个是VmaAllocationCreateInfo当中的Flags。
使用这个Flag后,当内存分配超出Budget就不会成功。当Lost Allocations这个VMA特性被启用的时候,如果超出边界就会做LOST换页行为(这个我们后面的章节会详细介绍,说白了就是不用的资源先换到RAM)。如果换页都不行了,那么就会丢出VK_ERROR_OUT_OF_DEVICE_MEMORY。这种机制一般都是用于系统不必须的资源上面,比如Texture贴图;当然这种不必须的资源,不能是创建关键资源的时候必须的资源。
最后,你也可以使用 VMA_ALLOCATION_CREATE_NEVER_ALLOCATE_BIT这个flag去保证本次分配必须在当前VMA已经创建的Chunk范围内。如果要分配新的Chunk池子, 那就会以VK_ERROR_OUT_OF_DEVICE_MEMORY的返回失败告终。这个主要是保证了分配速度,因为永远不需要深入Vulkan内部分配新的Chunk内存块了。
注意一点,如果使用了用户**自主内存池( Custom memory pools )**并且把 VmaPoolCreateInfo::minBlockCount设置为大于0的数值,那么就会在不检查Budget的情况下去分配内存Chunk块了。
总结
以上就是今天的内容,大家对于vulkan的学习,也可以参考我出品的vulkan系列教程,下面给大家贴出链接。