引言:为何需要VMA?
Vulkan内存显式控制
在传统的图形API(如OpenGL)中,内存管理被API层完全封装,开发者无需关心底层细节。但Vulkan将内存控制权完全下放给开发者,暴露了显式的内存管理机制。这种设计带来了两个核心挑战:
- 多类型内存堆:现代GPU通常包含4-8种内存类型(如DEVICE_LOCAL、HOST_VISIBLE等),分布在不同的内存堆中
- 手动生命周期管理:开发者需要自行处理内存分配、绑定、映射和释放的全过程
一个典型的Vulkan内存分配流程需要:
vkGetBufferMemoryRequirements(...);
vkAllocateMemory(...);
vkBindBufferMemory(...);
vkMapMemory(...); // 可选
// 使用内存...
vkDestroyBuffer(...);
vkFreeMemory(...);
这种显式控制虽然提升了性能,但带来了极高的开发复杂度。根据Khronos的统计,超过60%的Vulkan内存相关BUG源于不正确的内存类型选择或生命周期管理。
Sub-allocation
- 考虑驱动开销:Vulkan最推荐使用
sub-allocat
,但是sub-allocation
的内存分配原则,即尽可能减少Memory和Buffer的数量。

图 1:sub-allocation。
“The Good” ——在一大块内存里对子资源进行子分配
思路:
• 只向驱动/操作系统申请一块较大的 VkDeviceMemory,只创建一个buffer;
• 运行时将该buffer“切割”成若干子区间,每个子区间存储不同的数据。
• 这样可以显著减少真正的“分配调用次数”,也不会超出 maxMemoryAllocationCount,同时也可以减少内存绑定次数。
// 1. 创建一个“大 Buffer”以获取内存需求(包含所有用途)
VkBufferCreateInfo bigBufferCI = {};
bigBufferCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bigBufferCI.size = totalBufferSize; // 总大小(包含 Index/Vertex/Uniform)
bigBufferCI.usage = VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT |
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
vkCreateBuffer(device, &bigBufferCI, nullptr, &bigBuffer);
VkMemoryRequirements memReqBigBuffer;
vkGetBufferMemoryRequirements(device, bigBuffer, &memReqBigBuffer);
// 2. 计算各用途的偏移和对齐
VkDeviceSize offsetIndex = 0;
VkDeviceSize offsetVertex = AlignUp(offsetIndex + indexBufferSize, memReqBigBuffer.alignment);
VkDeviceSize offsetUniform = AlignUp(offsetVertex + vertexBufferSize, memReqBigBuffer.alignment);
VkDeviceSize totalSize = AlignUp(offsetUniform + uniformBufferSize, memReqBigBuffer.alignment);
// 3. 只申请一次设备内存
VkMemoryAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = totalSize;
allocInfo.memoryTypeIndex = FindMemoryType(memReqBigBuffer.memoryTypeBits, desiredProperties);
VkDeviceMemory bigMemory;
vkAllocateMemory(device, &allocInfo, nullptr, &bigMemory);
// 4. 将整个 bigMemory 绑定到“大 Buffer”
vkBindBufferMemory(device, bigBuffer, bigMemory, 0);
// 5. 将数据拷贝到 Buffer 的不同偏移处
void* mappedMemory = nullptr;
vkMapMemory(device, bigMemory, 0, VK_WHOLE_SIZE, 0, &mappedMemory);
// -- 将 Index 数据拷贝到对应偏移
std::memcpy((uint8_t*)mappedMemory + offsetIndex, localIndexData, indexBufferSize);
// -- 将 Vertex 数据拷贝到对应偏移
std::memcpy((uint8_t*)mappedMemory + offsetVertex, localVertexData, vertexBufferSize);
// -- 将 Uniform 数据拷贝到对应偏移
std::memcpy((uint8_t*)mappedMemory + offsetUniform, localUniformData, uniformBufferSize);
vkUnmapMemory(device, bigMemory);
// 6. 使用时指定偏移
// -- 绑定 Index Buffer
vkCmdBindIndexBuffer(cmdBuffer, bigBuffer, offsetIndex, VK_INDEX_TYPE_UINT16);
// -- 绑定 Vertex Buffer
VkDeviceSize vertexBufferOffset = offsetVertex;
vkCmdBindVertexBuffers(cmdBuffer, 0, 1, &bigBuffer, &vertexBufferOffset);
// -- 更新 DescriptorSet,指定 Uniform Buffer 的偏移和范围
VkDescriptorBufferInfo uniformBufferInfo = {};
uniformBufferInfo.buffer = bigBuffer;
uniformBufferInfo.offset = offsetUniform;
uniformBufferInfo.range = uniformBufferSize;
VkWriteDescriptorSet writeDesc = {};
writeDesc.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writeDesc.dstSet = descriptorSet;
writeDesc.dstBinding = uniformBinding;
writeDesc.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
writeDesc.descriptorCount = 1;
writeDesc.pBufferInfo = &uniformBufferInfo;
vkUpdateDescriptorSets(device, 1, &writeDesc, 0, nullptr);
这种方式:
• 减少 vkAllocateMemory / vkBindBufferMemory 调用次数(只分配和绑定一次);
• 通过应用层自己维护 offset 来在同一个 Buffer 内划分出 Index/Vertex/Uniform 等数据区域;
• 大幅降低驱动层管理负担,符合 Vulkan 中鼓励的“子分配”思路,从而达到图示所说的 “The Good”。
“The Bad” ——单块显存 + 单个大 Buffer + 手动管理 offset
思路:
• 只向驱动/操作系统申请一块较大的 VkDeviceMemory;
• 运行时将这块大内存“切割”成若干子区间,每个子区间绑定到不同的 Buffer(如 Index/Vertex/Uniform)上;
• 自己管理这块内存中各个子区间的偏移与大小。
• 这样可以显著减少真正的“分配调用次数”,也不会超出 maxMemoryAllocationCount。
// 1. 分别创建需要的 Buffer 以获取各自需求(但先不真正分配内存)
// -- 例子:Index Buffer
VkBufferCreateInfo indexBufferCI = {};
indexBufferCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
indexBufferCI.size = indexBufferSize;
indexBufferCI.usage = VK_BUFFER_USAGE_INDEX_BUFFER_BIT;
vkCreateBuffer(device, &indexBufferCI, nullptr, &indexBuffer);
VkMemoryRequirements memReqIndex;
vkGetBufferMemoryRequirements(device, indexBuffer, &memReqIndex);
// -- 例子:Vertex Buffer
VkBufferCreateInfo vertexBufferCI = {};
vertexBufferCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
vertexBufferCI.size = vertexBufferSize;
vertexBufferCI.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
vkCreateBuffer(device, &vertexBufferCI, nullptr, &vertexBuffer);
VkMemoryRequirements memReqVertex;
vkGetBufferMemoryRequirements(device, vertexBuffer, &memReqVertex);
// -- 例子:Uniform Buffer
VkBufferCreateInfo uniformBufferCI = {};
uniformBufferCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
uniformBufferCI.size = uniformBufferSize;
uniformBufferCI.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
vkCreateBuffer(device, &uniformBufferCI, nullptr, &uniformBuffer);
VkMemoryRequirements memReqUniform;
vkGetBufferMemoryRequirements(device, uniformBuffer, &memReqUniform);
// 2. 计算总共需要的内存大小与对齐(实际需要根据对齐做更严谨的计算)
// 比如令 offsets 为对齐后得到的各个起始偏移
VkDeviceSize offsetIndex = 0;
VkDeviceSize offsetVertex = AlignUp(offsetIndex + memReqIndex.size, memReqVertex.alignment);
VkDeviceSize offsetUniform = AlignUp(offsetVertex + memReqVertex.size, memReqUniform.alignment);
VkDeviceSize totalSize = offsetUniform + memReqUniform.size;
// 3. 只申请一次设备内存
VkMemoryAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = totalSize;
allocInfo.memoryTypeIndex = FindMemoryType(
memReqIndex.memoryTypeBits & memReqVertex.memoryTypeBits & memReqUniform.memoryTypeBits,
desiredProperties // 比如 HOST_VISIBLE | HOST_COHERENT 等
);
VkDeviceMemory bigMemory;
vkAllocateMemory(device, &allocInfo, nullptr, &bigMemory);
// 4. 将同一个 bigMemory 不同的偏移绑定给不同 Buffer
vkBindBufferMemory(device, indexBuffer, bigMemory, offsetIndex);
vkBindBufferMemory(device, vertexBuffer, bigMemory, offsetVertex);
vkBindBufferMemory(device, uniformBuffer, bigMemory, offsetUniform);
这样所有的 Index/Vertex/Uniform Buffer 都共享了同一个 VkDeviceMemory,而我们只跟驱动真正打了一次“分配”的交道。
“The ?!? # Δt” ——极度碎片化或疯狂分配
思路:
• 每个小对象都单独分配,甚至更糟:同一个对象反复频繁地分配和释放;
• 导致显存碎片化、分配次数超标、或大幅度浪费显存;
典型反面案例:
• 你的场景中有非常多的微小 Buffer(例如粒子、分块地形中大量细分)却从未做子分配;
• 或者在帧间频繁地 vkFreeMemory / vkAllocateMemory,引起驱动层不断地做大开销的操作;
• 在高并发或高频率下,性能和可用内存都崩溃式下降。
数据传输
独立显卡
有专用的显存(VRAM)
数据传输过程:
- CPU (Host) → PCIe总线 → GPU显存(Device Local Memory)
- 需要创建staging buffer作为中间缓冲
- 数据传输会受限于PCIe总线带宽
集成显卡
CPU和GPU共享系统内存
数据传输过程:
- 直接在共享内存中访问,无需跨PCIe传输
- 不需要staging buffer
- 通过
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
标识
Lazily Allocated Memory
移动端GPU上的on-chip memory
- 这种内存不会立即分配物理内存
- 通常用于移动设备的 transient attachments(如深度缓冲区)
- 实际的内存分配会推迟到真正需要时
- 在某些架构上可能完全不会分配物理内存
这种内存在渲染时可以被保留在GPU上,显著降低带宽。
补充内容,VMA相关请跳转到“VMA的诞生”
补充:VkPhysicalDeviceMemoryProperties
typedef struct VkPhysicalDeviceMemoryProperties {
// 可用的内存类型数量
uint32_t memoryTypeCount;
// 内存类型数组,最大长度为 VK_MAX_MEMORY_TYPES (32)
VkMemoryType memoryTypes[VK_MAX_MEMORY_TYPES];
// 可用的内存堆数量
uint32_t memoryHeapCount;
// 内存堆数组,最大长度为 VK_MAX_MEMORY_HEAPS (16)
VkMemoryHeap memoryHeaps[VK_MAX_MEMORY_HEAPS];
} VkPhysicalDeviceMemoryProperties;
memoryTypeCount
指明该设备支持的内存类型数量。
VkMemoryType
其中 VkMemoryType 结构体定义为:
typedef struct VkMemoryType {
// 内存属性标志(VkMemoryPropertyFlags)
VkMemoryPropertyFlags propertyFlags;
// 此内存类型使用的堆的索引
uint32_t heapIndex;
} VkMemoryType;
一个VkMemoryType结构体对应GPU支持的一种内存类型,比如:
// memoryTypes[0] - 设备本地内存(VRAM)
propertyFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
heapIndex = 0 // 指向VRAM堆
// memoryTypes[1] - CPU可见的系统内存
propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
heapIndex = 1 // 指向系统内存堆
// memoryTypes[2] - CPU可见且带缓存的系统内存
propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_CACHED_BIT
heapIndex = 1 // 同样指向系统内存堆
VkMemoryPropertyFlags的常见值包括:
typedef enum VkMemoryPropertyFlagBits {
// 设备本地内存,通常是GPU最高效的内存类型
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT = 0x00000001,
// CPU可见内存,可以使用vkMapMemory映射
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT = 0x00000002,
// CPU写入立即可见,不需要手动flush
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT = 0x00000004,
// CPU写入被缓存,需要手动flush和invalidate
VK_MEMORY_PROPERTY_HOST_CACHED_BIT = 0x00000008,
// 用于tile-based GPU的延迟分配内存
VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT = 0x00000020,
// 受保护的内存,用于安全内容
VK_MEMORY_PROPERTY_PROTECTED_BIT = 0x00000040,
// RDMA可访问的内存
VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD = 0x00000040,
// 设备本地且RDMA可访问
VK_MEMORY_PROPERTY_DEVICE_UNCACHED_BIT_AMD = 0x00000080,
// 可以原子访问的RDMA内存
VK_MEMORY_PROPERTY_RDMA_CAPABLE_BIT_NV = 0x00000100,
} VkMemoryPropertyFlagBits;
常见的内存标识(flag常见组合):
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT //设备本地内存(GPU 专用)
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT //CPU 可见的暂存缓冲区
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT |
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT //集成显卡的共享内存
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_CACHED_BIT //带缓存的 CPU 访问内存
VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT |
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT //移动设备的临时附件(如深度缓冲)
VkMemoryHeap
typedef struct VkMemoryHeap {
VkDeviceSize size; // 堆的大小(字节)
VkMemoryHeapFlags flags; // 堆的属性标志
} VkMemoryHeap;
VkMemoryHeapFlags的常见值包括:
VK_MEMORY_HEAP_DEVICE_LOCAL_BIT //设备本地内存(通常是显卡的 VRAM)
VK_MEMORY_HEAP_MULTI_INSTANCE_BIT //多实例内存(在多 GPU 设置中,标记某个内存堆可以被多个物理设备同时访问)
findMemoryType
uint32_t findMemoryType(VkPhysicalDevice physicalDevice,
uint32_t typeFilter,
VkMemoryPropertyFlags properties) {
// 获取物理设备的内存属性
VkPhysicalDeviceMemoryProperties memProperties;
vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);
// 遍历所有内存类型
for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
// 检查两个条件:
// 1. typeFilter 中的位是否设置 (通过位运算)
// 2. 内存类型是否具有我们需要的所有属性
if ((typeFilter & (1 << i)) &&
(memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
return i;
}
}
// 如果没找到合适的内存类型,抛出错误
throw std::runtime_error("failed to find suitable memory type!");
}
// 创建缓冲区时
VkBuffer buffer;
VkBufferCreateInfo bufferInfo = {...};
vkCreateBuffer(device, &bufferInfo, nullptr, &buffer);
// 获取缓冲区的内存需求
VkMemoryRequirements memRequirements;
vkGetBufferMemoryRequirements(device, buffer, &memRequirements);
// 分配内存
VkMemoryAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
// 查找合适的内存类型
allocInfo.memoryTypeIndex = findMemoryType(
physicalDevice,
memRequirements.memoryTypeBits, // typeFilter:缓冲区支持的内存类型
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT // 所需属性
);
在为缓冲区分配内存时,需要满足:
- 缓冲区支持的内存类型(typeFilter)
- 所需属性(properties)
即在VkPhysicalDeviceMemoryProperties中找到一个内存类型,它满足typeFilter和properties的要求。
VMA的诞生
Vulkan Memory Allocator(VMA)库应运而生,它通过以下核心设计解决了上述痛点:
- 智能内存类型选择:基于资源使用特性自动选择最佳内存类型
- 生命周期自动化:统一管理资源对象及其关联内存的生命周期
- 高级内存策略:提供内存池、碎片整理等高级功能
- 诊断工具集成:内置内存统计、泄漏检测等调试功能
初始化:构建内存管理基石
环境配置
使用VMA
需要:
-
项目集成:要在项目中使用
VMA
,首先需要将其源代码或库文件包含进工程中,并正确链接(link
)。 -
选择
Vulkan
版本:VMA
需要配置Vulkan
的目标版本,以便启用或禁用特定的Vulkan
函数和扩展。 -
导入
Vulkan
函数:VMA
自身需要调用大量Vulkan
函数,这些函数需通过VmaVulkanFunctions
结构体向VMA
提供。可通过手动设置或者自动加载方式(如使用Vulkan loader
)来实现。 -
启用扩展:如果需要使用诸如
VK_KHR_dedicated_allocation
等Vulkan
扩展,则需要在创建VmaAllocator
时告知VMA
以便充分利用这些扩展。 -
配置选项:在初始化
VMA
时,可指定各种标志(Flags
)与配置,如线程安全(是否启用互斥锁)等。
初始化流程
#include "vk_mem_alloc.h"
VmaAllocatorCreateInfo allocatorInfo = {};
allocatorInfo.vulkanApiVersion = VK_API_VERSION_1_2;
allocatorInfo.physicalDevice = physicalDevice;
allocatorInfo.device = device;
allocatorInfo.instance = instance;
VmaAllocator allocator;
vmaCreateAllocator(&allocatorInfo, &allocator);
关键配置项说明:
typedef struct VmaAllocatorCreateInfo {
VkPhysicalDevice physicalDevice;
VkDevice device;
// 启用高级特性
VmaAllocatorCreateFlags flags;
// 自定义CPU内存分配器
const VmaAllocationCallbacks* pAllocationCallbacks;
// 设备内存限制
VkDeviceSize heapSizeLimit[VK_MAX_MEMORY_HEAPS];
} VmaAllocatorCreateInfo;
推荐开启的标志位:
VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT
:支持设备地址捕获VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT
:显存预算监控
AllocatorCreateInfo中的flags
:
VMA_ALLOCATOR_CREATE_EXTERNALLY_SYNCHRONIZED_BIT
- 表示在多线程环境下,由用户负责同步
- 可以提高性能,但需要用户确保分配器的线程安全
- 如果设置此标志,用户必须在外部进行同步,确保对同一个 VmaAllocator 的调用不会并发执行
VMA_ALLOCATOR_CREATE_KHR_DEDICATED_ALLOCATION_BIT
- 启用 VK_KHR_dedicated_allocation 扩展功能
- 允许为某些特定资源分配专用内存块
- 适用于大型资源(如大纹理)的优化
VMA_ALLOCATOR_CREATE_KHR_BIND_MEMORY2_BIT
- 启用 VK_KHR_bind_memory2 扩展
- 提供更灵活的内存绑定选项
- 允许一次绑定多个内存对象
VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT
- 启用 VK_EXT_memory_budget 扩展
- 允许查询当前内存使用情况和预算
- 有助于更好地管理内存资源
VMA_ALLOCATOR_CREATE_AMD_DEVICE_COHERENT_MEMORY_BIT
- 启用 VK_AMD_device_coherent_memory 扩展
- 支持 AMD 设备一致性内存
- 提供更高效的内存访问
VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT
- 启用缓冲区设备地址功能
- 支持 VK_KHR_buffer_device_address 扩展
- 允许在着色器中直接访问缓冲区
VMA_ALLOCATOR_CREATE_EXT_MEMORY_PRIORITY_BIT
- 启用 VK_EXT_memory_priority 扩展
- 允许设置内存分配的优先级
- 有助于优化内存管理策略
基础功能:从入门到精通
1. 资源生命周期管理
缓冲区创建范例:
VkBufferCreateInfo bufferInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
bufferInfo.size = 1024 * 1024; // 1MB
bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
VmaAllocationCreateInfo allocInfo = {};
allocInfo.usage = VMA_MEMORY_USAGE_AUTO;
allocInfo.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
VkBuffer buffer;
VmaAllocation allocation;
vmaCreateBuffer(allocator, &bufferInfo, &allocInfo, &buffer, &allocation, nullptr);
VmaMemoryUsage枚举:
typedef enum VmaMemoryUsage {
VMA_MEMORY_USAGE_UNKNOWN = 0,
VMA_MEMORY_USAGE_GPU_ONLY, // 纯设备内存
VMA_MEMORY_USAGE_CPU_ONLY, // 可映射主机内存
VMA_MEMORY_USAGE_CPU_TO_GPU, // 频繁上传
VMA_MEMORY_USAGE_GPU_TO_CPU, // 回读数据
VMA_MEMORY_USAGE_AUTO = 7 // 自动决策(推荐)
} VmaMemoryUsage;
VmaAllocationCreateFlags 枚举值说明:
VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT
- 指示内存将被主机按顺序写入
- 适用于单次或连续写入的缓冲区
- 可能影响内存类型选择以优化顺序访问
- 例如每帧都需要更新的动态 uniform 数据和需要被频繁更新的顶点数据
VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT
- 指示内存将被主机随机读写访问
- 适用于需要频繁更新的动态缓冲区
- 会选择支持随机访问的内存类型
VMA_ALLOCATION_CREATE_HOST_ACCESS_ALLOW_TRANSFER_INSTEAD_BIT
- 当主机直接访问不可用时允许使用传输操作
- 提供内存访问的备选方案
- 增加分配的灵活性
VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT
- 强制为此分配使用独立的内存块
- 适用于大型资源或特殊用途
- 可能增加内存碎片
VMA_ALLOCATION_CREATE_NEVER_ALLOCATE_BIT
- 仅在现有内存块中查找空间
- 如果没有合适的空间则失败
- 用于严格控制内存分配
VMA_ALLOCATION_CREATE_MAPPED_BIT
- 创建时自动执行内存映射
- 避免手动映射/解映射操作
- 适用于需要持续访问的资源
VMA_ALLOCATION_CREATE_USER_DATA_COPY_STRING_BIT
- 为用户数据创建字符串的深拷贝
- 确保字符串数据的独立性和安全性
- 方便资源追踪和调试
VMA_ALLOCATION_CREATE_UPPER_ADDRESS_BIT
- 尝试在较高的 GPU 地址空间分配
- 可能影响某些特定硬件的性能
- 用于特殊的内存布局需求
内存分配策略标志
- VMA_ALLOCATION_CREATE_STRATEGY_BEST_FIT_BIT
- VMA_ALLOCATION_CREATE_STRATEGY_WORST_FIT_BIT
- VMA_ALLOCATION_CREATE_STRATEGY_FIRST_FIT_BIT
用于控制内存分配算法的选择,影响分配效率和内存碎片
2. 内存映射与访问
安全的内存访问模式:
void* mappedData;
vmaMapMemory(allocator, allocation, &mappedData);
// 写入数据(建议使用memcpy而非直接指针操作)
memcpy(mappedData, sourceData, dataSize);
vmaUnmapMemory(allocator, allocation);
持久映射优化技巧:
allocInfo.flags |= VMA_ALLOCATION_CREATE_MAPPED_BIT;
// 创建后直接访问
VmaAllocationInfo allocInfo;
vmaGetAllocationInfo(allocator, allocation, &allocInfo);
void* persistentPtr = allocInfo.pMappedData;
1.使用VMA进行数据拷贝时无需创建和操作staging buffer,VMA会自动选择最佳内存类型,并进行数据传输。(依赖于创建buffer时正确指定usage和flags)
2.对于 Host 可见的内存,VMA 也提供 vmaFlushAllocation, vmaInvalidateAllocation 等接口,用于在需要时清理或无效化 CPU/GPU 缓存,确保数据一致性。
高级用法:突破性能瓶颈
1. 内存池(Memory Pools)
专用内存池配置:
VmaPoolCreateInfo poolInfo = {};
poolInfo.memoryTypeIndex = ...; // 指定内存类型
poolInfo.blockSize = 64 * 1024 * 1024; // 64MB块
poolInfo.minBlockCount = 1;
poolInfo.maxBlockCount = 8;
VmaPool pool;
vmaCreatePool(allocator, &poolInfo, &pool);
// 在池中分配资源
VmaAllocationCreateInfo poolAllocInfo = {};
poolAllocInfo.pool = pool; // 指定内存池
poolAllocInfo.usage = VMA_MEMORY_USAGE_AUTO; // 自动选择内存类型
poolAllocInfo.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; // 指定内存访问模式
vmaCreateBuffer(allocator, &bufferInfo, &poolAllocInfo, &buffer, &allocation, nullptr);
2. 高级分配策略
优先设备本地内存:
allocInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; //更灵活的策略,如果设备本地内存不足或不适用,会自动选择次优的内存类型
延迟内存分配:
allocInfo.flags |= VMA_ALLOCATION_CREATE_CAN_BECOME_LOST_BIT;
VMA_ALLOCATION_CREATE_CAN_BECOME_LOST_BIT 是 VMA 中一个特殊的内存分配标志,用于创建可能会”丢失”的内存分配。这是一个高级功能,主要用于内存管理优化。
- 这种分配可能在内存压力大时被VMA回收
- 需要定期检查分配是否还有效
- 通常配合 VMA_ALLOCATION_CREATE_CAN_MAKE_OTHER_LOST_BIT 使用
典型应用场景:
- 缓存数据
- 非关键资源
- 可重新生成的资源
最佳实践:
// 创建可丢失且可导致其他分配丢失的分配
VmaAllocationCreateInfo allocInfo = {};
allocInfo.flags = VMA_ALLOCATION_CREATE_CAN_BECOME_LOST_BIT |
VMA_ALLOCATION_CREATE_CAN_MAKE_OTHER_LOST_BIT;
allocInfo.priority = 0.5f; // 设置优先级
// 定期检查和维护
void maintainResources() {
for (auto& resource : resources) {
VmaAllocationInfo allocInfo;
vmaGetAllocationInfo(allocator, resource.allocation, &allocInfo);
if (allocInfo.deviceMemory == VK_NULL_HANDLE) {
// 重新创建资源
recreateResource(resource);
}
}
}
3. 内存碎片整理
碎片整理可以显著减少内存碎片,从而腾出连续的大块空间,避免频繁出现 OOM (Out Of Memory,内存耗尽)或内存分配失败的情况。当应用程序长期运行时,频繁的内存分配和释放可能导致内存碎片化,使得即使总的可用内存充足,也无法分配较大的连续内存块。
VMA 提供了一整套接口来执行碎片整理:
- vmaBeginDefragmentation():初始化碎片整理上下文
- vmaBeginDefragmentationPass() / vmaEndDefragmentationPass():执行碎片整理的一个或多个 Pass
- vmaEndDefragmentation():结束碎片整理进程
- vmaDefragment():单次执行碎片整理
注意:碎片整理期间,某些资源的内存可能会被移动,需要确保资源处于安全状态(通常在 GPU 空闲或可被重新绑定时进行)。
单次碎片整理流程:
VmaDefragmentationInfo defragInfo = {};
defragInfo.flags = VMA_DEFRAGMENTATION_FLAG_ALGORITHM_FAST;
VmaDefragmentationStats stats;
vmaDefragment(allocator, nullptr, 0, nullptr, &defragInfo, &stats);
printf("Freed %llu bytes, moved %u allocations\n",
stats.bytesFreed, stats.allocationsMoved);
更复杂的场景需要使用vmaBeginDefragmentation()和vmaEndDefragmentation(),以及vmaBeginDefragmentationPass()和vmaEndDefragmentationPass()。
4. 稀疏资源管理
稀疏纹理分配示例:
VkImageCreateInfo sparseImageInfo = { VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
sparseImageInfo.flags = VK_IMAGE_CREATE_SPARSE_BINDING_BIT;
sparseImageInfo.extent = {8192, 8192, 1}; // 8K*8K纹理
VmaAllocationCreateInfo sparseAllocInfo = {};
sparseAllocInfo.flags = VMA_ALLOCATION_CREATE_SPARSE_BINDING_BIT;
vmaCreateImage(allocator, &sparseImageInfo, &sparseAllocInfo, &image, &allocation, nullptr);
5.内存预算管理
内存预算管理主要包含两个关键功能:
- 查询预算信息
通过 vmaGetBudget 接口可查询各个显存堆的预算和使用情况:
VmaBudget budgets[VK_MAX_MEMORY_HEAPS];
vmaGetHeapBudgets(allocator, budgets);
// 检查第一个堆的使用情况
printf("Heap 0: Usage %llu MB / Budget %llu MB\n",
budgets[0].usage >> 20,
budgets[0].budget >> 20);
- 预算控制
使用 VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT 标志可限制内存分配在预算范围内:
VmaAllocationCreateInfo allocInfo = {};
allocInfo.flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT;
// 若超出预算,vmaCreateBuffer 将返回 VK_ERROR_OUT_OF_DEVICE_MEMORY
VkResult result = vmaCreateBuffer(
allocator, &bufferInfo, &allocInfo,
&buffer, &allocation, nullptr);
6.虚拟分配器
虚拟分配器的核心思想是在不实际分配物理设备内存的情况下,模拟内存分配的行为。这对于以下场景特别有用:
- 内存分配策略的预演和验证
- 资源布局的优化
- 自定义内存管理系统的实现
例如:
// 模拟不同的资源分配方案
void SimulateResourceLayout() {
VmaVirtualBlock block;
vmaCreateVirtualBlock(&VmaVirtualBlockCreateInfo{
.size = 1024 * 1024 * 64 // 64MB
}, &block);
struct AllocationRecord {
VmaVirtualAllocation allocation;
VkDeviceSize offset;
VkDeviceSize size;
const char* resourceName;
};
std::vector<AllocationRecord> allocations;
// 模拟分配各种资源
auto allocateResource = [&](VkDeviceSize size, const char* name) {
VmaVirtualAllocationCreateInfo allocInfo = {};
allocInfo.size = size;
allocInfo.alignment = 256;
AllocationRecord record = {};
record.size = size;
record.resourceName = name;
if (vmaVirtualAllocate(block, &allocInfo, &record.allocation, &record.offset) == VK_SUCCESS) {
allocations.push_back(record);
return true;
}
return false;
};
// 分配各种资源
allocateResource(1024 * 1024, "Texture1");
allocateResource(512 * 1024, "Vertex Buffer");
allocateResource(256 * 1024, "Index Buffer");
// 分析内存布局
VmaStatInfo stats;
vmaCalculateVirtualBlockStats(block, &stats);
// 输出内存使用情况
for (const auto& record : allocations) {
printf("Resource: %s, Offset: %llu, Size: %llu\n",
record.resourceName, record.offset, record.size);
}
// 清理
for (const auto& record : allocations) {
vmaVirtualFree(block, record.allocation);
}
vmaDestroyVirtualBlock(block);
}
关键数据结构
- VmaAllocator
- VMA 的核心对象
- 代表一个全局或应用级别的内存分配器
- VmaAllocation
- 代表一次内存分配
- 对应底层 Vulkan Device Memory 中的一块区域
- VmaAllocationCreateInfo
- 创建分配时的配置结构
- 包含 VmaMemoryUsage、映射选项、独立分配等参数
- VmaAllocationInfo
- 分配完成后返回的详细信息
- 包含偏移量、实际大小、映射指针等数据
- VmaMemoryUsage
- 指定内存分配的用途
- 如 GPU_ONLY、CPU_ONLY 等类型
- VmaPool
- 自定义内存池对象
- 用于统一管理多种内存分配
- VmaPoolCreateInfo
- 内存池的创建参数
- 配置池的属性和行为
- VmaBudget
- 内存预算管理结构
- 跟踪内存使用量和可用预算
- VmaStatistics & VmaDetailedStatistics
- 内存使用统计信息
- 提供详细的内存分配状态
- VmaVirtualAllocation & VmaVirtualBlock
- 虚拟内存分配相关结构
- 用于无物理内存的资源规划
推荐使用模式
VMA 官方文档中针对常见资源使用模式(如 GPU-only 资源、上传缓冲、回读缓冲、以及高级数据传输模式)都给出了对应的 VmaMemoryUsage 和配置建议。例如:
- GPU-only 资源:
- VMA_MEMORY_USAGE_GPU_ONLY
- VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT(可选)
- CPU -> GPU 上传:
- VMA_MEMORY_USAGE_CPU_TO_GPU
- VMA_ALLOCATION_CREATE_MAPPED_BIT(可选)
- GPU -> CPU 读取:
- VMA_MEMORY_USAGE_GPU_TO_CPU
- 先进的上传数据管理:
- 结合自定义内存池
- 使用线性分配算法提升效率
自动映射
VMA_ALLOCATION_CREATE_MAPPED_BIT 是一个在创建 VMA 内存分配时使用的标志位,它的主要功能是在分配内存的同时自动将其映射到 CPU 可访问的地址空间。这样可以省去手动调用 vmaMapMemory 的步骤。
// 不使用 VMA_ALLOCATION_CREATE_MAPPED_BIT 的传统方式
{
VmaAllocationCreateInfo allocInfo = {};
allocInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU;
VmaAllocation allocation;
VkBuffer buffer;
// 创建buffer和分配内存
vmaCreateBuffer(allocator, &bufferInfo, &allocInfo, &buffer, &allocation, nullptr);
// 需要手动映射内存
void* mappedData;
vmaMapMemory(allocator, allocation, &mappedData);
// 使用映射的内存
memcpy(mappedData, sourceData, dataSize);
// 需要手动解除映射
vmaUnmapMemory(allocator, allocation);
}
// 使用 VMA_ALLOCATION_CREATE_MAPPED_BIT 的方式
{
VmaAllocationCreateInfo allocInfo = {};
allocInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU;
allocInfo.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; // 自动映射
VmaAllocation allocation;
VkBuffer buffer;
VmaAllocationInfo allocInfo;
// 创建buffer和分配内存,同时获取分配信息
vmaCreateBuffer(allocator, &bufferInfo, &allocInfo, &buffer, &allocation, &allocInfo);
// 直接通过 allocInfo.pMappedData 访问映射的内存
memcpy(allocInfo.pMappedData, sourceData, dataSize);
// 不需要手动解除映射,会在内存释放时自动处理
}
性能优化实践
通过合理使用VMA的高级特性,在真实项目中可实现:
- 内存分配耗时降低70%(对比原生Vulkan接口)
- 显存碎片率控制在5%以下
- 内存泄漏检测效率提升90%
典型案例:
- 《赛博朋克2077》:使用VMA管理超过20GB的显存资源
- Unreal Engine 5:集成VMA实现跨平台内存管理
- DOOM Eternal:通过VMA内存池技术降低8%的显存占用
EasyVulkan中的VMA
在EasyVulkan中,Buffer和Image的内存分配都使用了VMA。
Buffer Builder
根据上文的问题,在创建Buffer时比较重要的信息包括:
- Buffer size。
- Buffer usage。
- Buffer memory usage。
- Buffer memory flags。
- Buffer memory type index(property,optional)。
一个Buffer的创建流程可以简化为:
// Create a vertex buffer
auto vertexBuffer = bufferBuilder
->setSize(sizeof(vertices))
->setUsage(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT)
->setMemoryUsage(VMA_MEMORY_USAGE_CPU_TO_GPU)
->setMemoryFlags(VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
VMA_ALLOCATION_CREATE_MAPPED_BIT)
->build("myVertexBuffer");
// Create a storage buffer used on GPU only
auto storageBuffer = bufferBuilder
->setSize(sizeof(storageData))
->setUsage(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT)
->setMemoryUsage(VMA_MEMORY_USAGE_GPU_ONLY)
->build("myStorageBuffer");