Overview
Vulkan
将内存管理的工作交给了开发者自己负责,如何分配内存,如何指定内存策略都是由开发者自己决定的,当然处理问题也是由开发者自己负责的
Vulkan
将内存划分为两大类:主机内存(Host Memory) 和设备内存(Device Memory)
在移动设备上,主机内存就是 CPU
内存,设备内存就是GPU
显存,在此基础上,每种内存类型还可以按照属性进一步划分
Vulkan
提供了一种透明的机制来显示内存的细节和相关属性,这样做在 opengl 中是完全不可能的,后者不允许应用程序显式的控制内存区域和布局
Vulkan
中系统的内存有四种类型(并不是所有设备都支持这个四种类型):
Host Local Memory
,只对 Host 可见的内存,通常称之为普通内存Device Local Memory
,只对 Device 可见的内存,通常称之为显存Host Local Device Memory
,由 Host 管理的,对 Device 可见的内存Device Local Hosy Memory
,由 Device 管理的,对 Host 可见的内存
对比这两种内存类型,主机内存比设备内存更慢,但是 Host Memory 通常容量更大(也就是一般显存速度更快,但是容量更小)
设备内存,它对物理设备是直接可见的,物理设备可以直接读取其中的内存区块,设备内存和物理设备之间的关系十分紧密,因此它的性能比宿主机内存更高
VkImage
,VkBuffer
,以及一致性的缓存对象Uniform Buffer
都是从设备内存端分配的
单一的物理设备可能有多种类型的内存,根据它们的堆类型以及属性的不同还可能进一步细分
枚举内存信息
使用 vkGetPhysicalDeviceMemoryProperties
获取设备的内存信息,其函数定义如下:
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceMemoryProperties(
VkPhysicalDevice physicalDevice,
VkPhysicalDeviceMemoryProperties* pMemoryProperties);
其中 physicalDevice
就是创建的物理设备,VkPhysicalDeviceMemoryProperties
定义如下:
typedef struct VkPhysicalDeviceMemoryProperties {
uint32_t memoryTypeCount;
VkMemoryType memoryTypes[VK_MAX_MEMORY_TYPES];
uint32_t memoryHeapCount;
VkMemoryHeap memoryHeaps[VK_MAX_MEMORY_HEAPS];
} VkPhysicalDeviceMemoryProperties;
memoryTypeCount
表示支持的内存类型数量memoryTypes
有效元素个数为memoryTypeCount
的内存类型信息数组memoryHeapCount
表示支持的内存堆数量memoryHeaps
有效元素是memoryHeapCount
内存堆信息数组
其中 VkMemoryType
类型定义:
typedef struct VkMemoryType {
VkMemoryPropertyFlags propertyFlags;
uint32_t heapIndex;
} VkMemoryType;
propertyFlags
内存类型标志位。heapIndex
对应的memoryHeaps
堆索引。
中 propertyFlags
有效值被定义在了 VkMemoryPropertyFlagBits
枚举中,其定义如下:
typedef enum VkMemoryPropertyFlagBits {
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT = 0x00000001,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT = 0x00000002,
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT = 0x00000004,
VK_MEMORY_PROPERTY_HOST_CACHED_BIT = 0x00000008,
VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT = 0x00000010,
VK_MEMORY_PROPERTY_PROTECTED_BIT = 0x00000020,
VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD = 0x00000040,
VK_MEMORY_PROPERTY_DEVICE_UNCACHED_BIT_AMD = 0x00000080,
VK_MEMORY_PROPERTY_RDMA_CAPABLE_BIT_NV = 0x00000100,
VK_MEMORY_PROPERTY_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkMemoryPropertyFlagBits;
typedef VkFlags VkMemoryPropertyFlags;
-
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
表示该内存设备上分配的内存可以被物理设备高效访问,只有对应的堆为VK_MEMORY_HEAP_DEVICE_LOCAL_BIT
才会有该内存类型 -
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
表示该内存设备上分配的内存将自动进行同步,不需要手动调用vkFlushMappedMemoryRanges()
和vkInvalidateMappedMemoryRanges()
来进行内存同步 -
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
表示在此内存类型上分配的内存可被 Host 端通过vkMapMemory()
函数进行映射,进而进行访问 -
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
表示该内存类型上分配的内存为缓存类型,Host 端访问缓存的内存类型会比较快,但是非缓存内存总是同步内存(VK_MEMORY_PROPERTY_HOST_COHERENT_BIT ) -
VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT
表示在此内存类型上分配的内存只有物理设备可访问
内存类型不能同时为VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT
和VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
另外的 VkMemoryHeap
的类型定义如下图所示:
typedef struct VkMemoryHeap {
VkDeviceSize size;
VkMemoryHeapFlags flags;
} VkMemoryHeap;
- size 该堆大小, 单位为字节
- flags 该堆类型标志位
其中的 VkMemoryHeapFlags
的类型定义如下:
typedef enum VkMemoryHeapFlagBits {
VK_MEMORY_HEAP_DEVICE_LOCAL_BIT = 0x00000001,
VK_MEMORY_HEAP_MULTI_INSTANCE_BIT = 0x00000002,
VK_MEMORY_HEAP_MULTI_INSTANCE_BIT_KHR = VK_MEMORY_HEAP_MULTI_INSTANCE_BIT,
VK_MEMORY_HEAP_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkMemoryHeapFlagBits;
typedef VkFlags VkMemoryHeapFlags;
-
VK_MEMORY_HEAP_DEVICE_LOCAL_BIT
: 表示内存堆是设备本地的,这种内存通常是最快的,因为它和 GPU 紧密集成,适合存储需要频繁访问的数据 -
VK_MEMORY_HEAP_MULTI_INSTANCE_BIT
: 用于多 GPU 配置,表示内存堆在多个物理设备实例中是独立的 -
VK_MEMORY_HEAP_MULTI_INSTANCE_BIT_KHR
和VK_MEMORY_HEAP_MULTI_INSTANCE_BIT
一致,用于兼容
打印内存信息的参考代码如下:
// 获取物理设备内存属性
VkPhysicalDeviceMemoryProperties memoryProperties;
vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memoryProperties);
std::cout << "=============================================================" << std::endl;
#if (DUMP_MEMORY_PROPS == 1)
uint32_t memtypeCount = memoryProperties.memoryTypeCount;
std::cout << "memoryTypeCount: " << memtypeCount << std::endl;
for (uint32_t i = 0; i < memtypeCount; i++) {
std::cout << "propertyFlags:" << memoryProperties.memoryTypes[i].propertyFlags << std::endl;
std::cout << "heap index:" << memoryProperties.memoryTypes[i].heapIndex << std::endl;
}
uint32_t heapCount = memoryProperties.memoryHeapCount;
std::cout << "heapCount: " << heapCount << std::endl;
for (uint32_t i = 0; i < heapCount; i++) {
std::cout << "flags:" << memoryProperties.memoryHeaps[i].flags << std::endl;
std::cout << "size:" << memoryProperties.memoryHeaps[i].size << std::endl;
}
#endif
结果如下:
memoryTypeCount: 3(BIN:011)
propertyFlags:1
heap index:0
propertyFlags:7 (BIN:0111)
heap index:0
propertyFlags:15(BIN:01111)
heap index:0
heapCount: 1
flags:1
size:17008445440
分配内存
分配内存的函数是 vkAllocateMemory
,其原型如下:
VKAPI_ATTR VkResult VKAPI_CALL vkAllocateMemory(
VkDevice device,
const VkMemoryAllocateInfo* pAllocateInfo,
const VkAllocationCallbacks* pAllocator,
VkDeviceMemory* pMemory);
device
是用于分配的逻辑设备pAllocateInfo
分配信息结构体pAllocator
内存分配器,如果传入nullptr
则表示默认的内存分配器pMemory
分配出的内存结构
其中 VkMemoryAllocateInfo
的信息如下:
typedef struct VkMemoryAllocateInfo {
VkStructureType sType;
const void* pNext;
VkDeviceSize allocationSize;
uint32_t memoryTypeIndex;
} VkMemoryAllocateInfo;
- sType 是该结构体的类型枚举值, 必须是
VkStructureType::VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO
- pNext 要么是 NULL 要么指向其他结构体来扩展该结构体
- allocationSize 要分配的内存大小。单位为Bytes
- memoryTypeIndex 分配内存的目标内存类型索引
其中 memoryTypeIndex
其重要,用于指定 memoryTypes[memoryTypeIndex]
对应的内存类型上进行内存分配,对应分配的堆为 memoryHeaps[memoryTypes[memoryTypeIndex].heapIndex]
参考代码如下:
void vulkanBasicDemo::vulkangetMemoryInfo() {
VkDeviceSize size = 1024;
VkDeviceMemory memory;
// 获取物理设备内存属性
VkPhysicalDeviceMemoryProperties memoryProperties;
vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memoryProperties);
std::cout << "=============================================================" << std::endl;
// 查找一个主机可见的内存类型
uint32_t memoryTypeIndex = VK_MAX_MEMORY_TYPES;
for (uint32_t i = 0; i < memoryProperties.memoryTypeCount; i++) {
if ((memoryProperties.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) &&
(memoryProperties.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) {
memoryTypeIndex = i;
break;
}
}
std::cout << "get memoryTypeIndex:" << memoryTypeIndex << std::endl;
if (memoryTypeIndex == VK_MAX_MEMORY_TYPES) {
std::cerr << "failed!! Could not find a suitable memory type!" << std::endl;;
}
// 准备内存分配信息
VkMemoryAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = size;
allocInfo.memoryTypeIndex = memoryTypeIndex;
// 分配内存
VkResult result = vkAllocateMemory(device, &allocInfo, nullptr, &memory);
if(result == VK_SUCCESS) {
std::cout << "Memory allocated successfully!" << std::endl;
}
}
内存映射
使用vkMapMemory
实现宿主机队设备内存的映射访问,这个函数会返回一个虚拟地址的指针,指向映射后的设备内存区域
如果内存分配时指定的内存类型支持 VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
,说明该内存是可以映射的
原则上并不是所有的设备内存都可以从主机端进行读写,为了 CPU 能够读写设备内存,硬件供应商都会提供一部分带有 VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
属性的内存用于CPU访问
Vulkan
中分配的内存会得到一个VkDeviceMemory
对象,通过vkMapMemory()
函数将分配的设备内存底层的虚拟地址返回给CPU(也就是Host端)
vkMapMemory
的函数原型:
VKAPI_ATTR VkResult VKAPI_CALL vkMapMemory(
VkDevice device,
VkDeviceMemory memory,
VkDeviceSize offset,//从内存首地址开始的偏移量,从 0 开始
VkDeviceSize size, // 映射的大小,单位为字节
VkMemoryMapFlags flags,
void** ppData // 产生的虚拟地址为 void* 指针
);
需要注意的是,映射的 memory 必须在有VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
类型的内存上分配
当函数成功返回后,memory
就会在 Host
端进行了内存映射,并处于映射态
当内存映射完成并使用结束后,可以进行接触映射的操作,进而释放系统的虚拟内存,可以通过 vkUnmapMemory()
函数将映射过的内存进行解映射
参考代码如下:
.....
// 分配内存
VkResult result = vkAllocateMemory(device, &allocInfo, nullptr, &memory);
if(result == VK_SUCCESS) {
std::cout << "Memory allocated successfully!" << std::endl;
}
// 映射内存
void* data;
result = vkMapMemory(device, memory, 0, size, 0, &data);
if (result == VK_SUCCESS) {
std::cout << "Memory mapped successfully!" << std::endl;
}
// 写入数据到内存
uint32_t* intData = (uint32_t*)data;
for (uint32_t i = 0; i < size / sizeof(uint32_t); i++) {
intData[i] = i;
}
std::cout << "Data written to memory successfully!" << std::endl;
// 解除内存映射
vkUnmapMemory(device, memory);
std::cout << "Memory unmapped successfully!" << std::endl;
vkFreeMemory(device, memory, nullptr);