内存

更新记录
  • 2024/1/2 增加该文档。

  • 2024/2/17 更新该文档。

  • 2024/2/17 增加 句柄对象的内存分配器 章节。

  • 2024/2/17 增加 PFN_vkAllocationFunction 章节。

  • 2024/2/17 增加 PFN_vkReallocationFunction 章节。

  • 2024/2/21 更新 PFN_vkAllocationFunction 章节。

  • 2024/2/21 更新 PFN_vkReallocationFunction 章节。

  • 2024/2/21 增加 PFN_vkInternalAllocationNotification 章节。

  • 2024/2/21 增加 PFN_vkInternalFreeNotification 章节。

  • 2024/2/21 增加 VkSystemAllocationScope 章节。

  • 2024/2/21 增加 VkInternalAllocationType 章节。

  • 2024/2/21 增加 示例 章节。

  • 2024/2/27 增加 设备内存 章节。

  • 2024/2/27 增加 vkGetPhysicalDeviceMemoryProperties 章节。

  • 2024/3/3 更新 vkGetPhysicalDeviceMemoryProperties 章节。

  • 2024/3/3 增加 VkPhysicalDeviceMemoryProperties 章节。

  • 2024/3/3 增加 VkMemoryHeap 章节。

  • 2024/3/3 增加 VkMemoryType 章节。

  • 2024/3/3 更新 设备内存 章节。

  • 2024/3/9 更新 VkMemoryType 章节。

  • 2024/3/9 增加 VkMemoryPropertyFlagBits 章节。

  • 2024/3/10 增加 VkMemoryPropertyFlagBits 章节。

  • 2024/3/10 增加 内存分配 章节。

  • 2024/3/10 增加 vkAllocateMemory 章节。

  • 2024/3/14 更新 vkAllocateMemory 章节。

  • 2024/3/14 增加 VkMemoryAllocateInfo 章节。

  • 2024/3/14 增加 VkMemoryAllocateInfo 章节下增加 示例 章节。

  • 2024/3/14 增加 内存回收 章节。

  • 2024/3/14 增加 vkFreeMemory 章节。

  • 2024/3/14 增加 vkFreeMemory 章节下增加 示例 章节。

  • 2024/3/15 增加 内存映射 章节。

  • 2024/3/16 增加 vkMapMemory 章节。

  • 2024/3/16 增加 vkMapMemory 章节下增加 示例 章节。

  • 2024/3/16 更新 设备内存类型示意图 内容,之前图中有错误。

  • 2024/3/16 更新 内存分配 章节中的 示例 章节。

  • 2024/3/16 增加 内存解映射 章节。

  • 2024/3/16 增加 内存解映射 章节下增加 示例 章节。

  • 2024/3/16 增加 内存同步 章节。

  • 2024/3/17 更新 PFN_vkAllocationFunction 章节,更新示例代码,增加 通用自定义 代码模块。

  • 2024/3/17 更新 PFN_vkReallocationFunction 章节,更新示例代码,增加 通用自定义 代码模块。

  • 2024/3/17 更新 PFN_vkFreeFunction 章节,更新示例代码,增加 通用自定义 代码模块。

  • 2024/3/19 更新 PFN_vkAllocationFunction 章节,增加 通用自定义 代码模块内存分配说明。

  • 2024/3/19 更新 PFN_vkReallocationFunction 章节,增加 通用自定义 代码模块内存分配说明。

  • 2024/3/19 更新 PFN_vkFreeFunction 章节,增加 通用自定义 代码模块内存分配说明。

  • 2024/3/23 更新 vkMapMemory 章节,增加示意图。

  • 2024/3/23 更新 内存同步 章节。

  • 2024/3/24 更新 vkMapMemory 章节。

  • 2024/3/24 更新 内存同步 章节。

  • 2024/3/24 增加 虚拟内存同步到设备内存 章节。

  • 2024/3/24 增加 vkFlushMappedMemoryRanges 章节。

  • 2024/3/24 增加 设备内存同步到虚拟内存 章节。

  • 2024/3/24 设备内存同步到虚拟内存 章节,增加 示例 章节。

  • 2024/3/26 更新 内存映射 章节。

  • 2024/3/26 增加 vkUnmapMemory 章节。

  • 2024/4/13 增加 惰性内存 章节。

  • 2024/4/13 增加 vkGetDeviceMemoryCommitment 章节。

Vulkan 中有两种分配内存的途径:

  1. vkCreate{对象名称}(...)vkDestroy{对象名称}(...) 函数中指定 const VkAllocationCallbacks* pAllocator 内存分配器。比如:

    • vkCreateInstance(...)vkDestroyInstance(...)

    • vkCreateDevice(...)vkDestroyDevice(...)

    该方式是在创建和销毁句柄对象时指定,用于在 内存条 上分配和回收内存。其内部通过 malloc(...)free(...) 之类的函数进行内存分配和销毁。用于 句柄对象 本身的分配和销毁。

    备注

    • 一般 pAllocator 可以直接指定为 nullptr ,用于告诉 Vulkan 使用内置的内存分配器。

    • 如果 不为 nullptr ,则用于指定自定义内存分配器。

    备注

    自定义内存分配器常用于内存统计。

  2. 通过 vkAllocateMemory(...) 函数分配内存。

    该方式主要用于在 Host 端和 Device 端进行内存分配。主要用于存储 GPU 的计算结果。

现在就安顺进行讲解:

句柄对象的内存分配器

在创建句柄(对象)时需要指定 const VkAllocationCallbacks* pAllocator 的内存分配器。其中 VkAllocationCallbacks 定义如下:

// 由 VK_VERSION_1_0 提供
typedef struct VkAllocationCallbacks {
    void*                                   pUserData;
    PFN_vkAllocationFunction                pfnAllocation;
    PFN_vkReallocationFunction              pfnReallocation;
    PFN_vkFreeFunction                      pfnFree;
    PFN_vkInternalAllocationNotification    pfnInternalAllocation;
    PFN_vkInternalFreeNotification          pfnInternalFree;
} VkAllocationCallbacks;
  • pUserData 为用户自定义数据指针。当该分配器中的回调被调用时将会传入 pUserData 作为回调的第一个参数。

  • pfnAllocation 内存分配回调。用于分配内存。

  • pfnReallocation 内存重分配回调。用于重分配内存。

  • pfnFree 内存释放回调。用于释放内存。

  • pfnInternalAllocation 内部内存分配通知回调。该回调由驱动在分配内部内存时调用。仅用于将内部内存分配信息反馈给用户。该回调内部 不应该 分配新内存。

  • pfnInternalFree 内部内存释放通知回调。该回调由驱动在释放内部内存时调用。仅用于将内部内存释放信息反馈给用户。该回调内部 不应该 释放内存。

其中 PFN_vkAllocationFunction 定义如下:

PFN_vkAllocationFunction

// 由 VK_VERSION_1_0 提供
typedef void* (VKAPI_PTR *PFN_vkAllocationFunction)(
   void*                                       pUserData,
   size_t                                      size,
   size_t                                      alignment,
   VkSystemAllocationScope                     allocationScope);
  • pUserData 为用户自定义数据指针。对应 VkAllocationCallbacks::pUserData

  • size 要分配的内存大小。单位为 字节

  • alignment 要分配内存的 内存对齐 大小。单位为 字节必须2 的幂次方。

  • allocationScope 该内存声明周期所属的分配范围。

该函数回调将返回大小为 size 比特,内存对齐为 alignment 分配的新内存。

如果分配失败,该函数 必须 返回 NULL 。如果分配成功,需要返回空间 最少size 字节,并且指针地址为 alignment 的倍数。

内存对齐

重要

此处简单讲解内存对齐,并不完善,只是说明了基本思想,网上有很多详细资料可供参阅。

处理芯片在读取内存时并不是一比特一比特的读,而是 \(n\) 字节 \(n\) 字节的读取(其中 \(n\)2 的幂次方)。如下结构体:

struct Demo
{
   char  a; // 占 1 字节
   int   b; // 占 4 字节
   short c; // 占 2 字节
};

比如当 \(n = 4\) 时,也就是一次读取 4 个字节。判定如下:

  • 由于 a 只占 1 个字节,而处理器一次性读 4 个字节,则 a 成员大小将会扩展到 4 个字节。其中只有第一个字节为 a 成员的有效内存,其他 3 个扩展字节用于占位。

  • 由于 b 的大小为 4 个字节,正好为 4 的倍数。则不需要扩展字节就可以直接读。

  • 由于 c 的大小小于 4 则其处理方式与 a 的一样,扩展到 4 字节,其中前两个字节为 c 成员的有效内存,其他 2 个字节用于占位。

示意图如下:

_images/aligment_struct.png

这样处理器在 4 个字节 4 个字节读的时候就能够读取到正确的数据了。

如上就是按照 4 字节进行的内存对齐。

PFN_vkAllocationFunction 是一个函数指针,需要指向一个返回值为 void* 形参为 (void *pUserData, size_t size, size_t alignment, VkSystemAllocationScope allocationScope) 的函数。比如:

#include <cstdlib>

void *VKAPI_PTR Allocation(void *pUserData, size_t size, size_t alignment, VkSystemAllocationScope allocationScope)
{
   return std::aligned_alloc(alignment, size);
}

PFN_vkAllocationFunction pfn_allocation = &Allocation;

警告

C++ 标准中没有定义如何获取 std::aligned_alloc(...) 分配的内存大小函数。需要自己存储。

具体如何存储,可参考 通用自定义 代码模块,该模块给出了一种解决方案。

#include <malloc.h>

void *VKAPI_PTR Allocation(void *pUserData, size_t size, size_t alignment, VkSystemAllocationScope allocationScope)
{
   return _aligned_malloc(size, alignment);
}

PFN_vkAllocationFunction pfn_allocation = &Allocation;
#include <malloc.h>

void *VKAPI_PTR Allocation(void *pUserData, size_t size, size_t alignment, VkSystemAllocationScope allocationScope)
{
   return memalign(alignment, size);
}

PFN_vkAllocationFunction pfn_allocation = &Allocation;
#include <stdlib.h>

void* AlignedMalloc(size_t size, size_t alignment)
{
   size_t meta_point_size = sizeof(void *);
   size_t aligned_size = sizeof(size_t);
   size_t meta_size = aligned_size + meta_point_size + alignment - 1 + size;

   void *meta = malloc(meta_size);

   uintptr_t start = (uintptr_t)meta + aligned_size + meta_point_size;

   void *aligned_meta = (void *)((start + ((alignment) - 1)) & ~(alignment - 1));

   *(void **)((uintptr_t)aligned_meta - meta_point_size) = meta;
   *(size_t *)((uintptr_t)aligned_meta - (meta_point_size + aligned_size)) = size;

   return aligned_meta;
}

void *VKAPI_PTR Allocation(void *pUserData, size_t size, size_t alignment, VkSystemAllocationScope allocationScope)
{
   return AlignedMalloc(size, alignment);
}

PFN_vkAllocationFunction pfn_allocation = &Allocation;

算法说明

该算法分配的对齐内存结构示意图如下:

_images/aligned_memory_struct.png

AlignedMalloc 对齐内存示意图

其中示意图最上面一行标注为各部分所占字节长度:

  • alignment - 1 用于内存对齐所需的基本占位符长度。该部分数据没用上,仅仅用于占位符。最大为 alignment - 1 ,会随着 (void *)((start + ((alignment) - 1)) & ~(alignment - 1)) 对齐算法中 start 的不同而不同。

  • alignment_size 用于存储需要分配的对齐内存长度。也就是 size 的字面值。

  • meta_point_size 用于存储 malloc(...) 分配的原指针。也就是 meta 的字面值(指针)。

  • size 对齐内存长度。真正会被使用的对齐内存。

最下面一行标注为核心指针位置:

  • meta malloc(...) 分配的原指针。字面值(指针)被存储在 meta_point_size 占有的内存中。

  • aligned_meta 被需要的对齐内存指针。作为结果返回。

其中 aligned_meta 满足 Vulkan 要求的对齐内存地址。并作为目标内存返回给 Vulkan

备注

这里 aligned_meta 前只存储了 sizemeta 基本数据,您可以根据需求自定义扩展这些数据存储,一般会抽象出一个 内存头 用于存储该内存分配信息。

其中 PFN_vkReallocationFunction 定义如下:

PFN_vkReallocationFunction

// 由 VK_VERSION_1_0 提供
typedef void* (VKAPI_PTR *PFN_vkReallocationFunction)(
    void*                                       pUserData,
    void*                                       pOriginal,
    size_t                                      size,
    size_t                                      alignment,
    VkSystemAllocationScope                     allocationScope);
  • pUserData 为用户自定义数据指针。对应 VkAllocationCallbacks::pUserData

  • pOriginal 在该内存的基础上进行重分配。

  • size 要重分配的内存大小。单位为 字节

  • alignment 要分配内存的 内存对齐 大小。单位为 字节必须2 的幂次方。

  • allocationScope 该内存声明周期所属的分配范围。

如果分配成功,需要返回空间 最少size 字节,并且 pOriginal 原始内存内的 \([0, min(原始内存大小, 新分配的内存大小)-1]\) 范围的数据需要原封不动的转移至新分配的内存中。

如果新分配的内存大小大于之前的分配,则多出来的内存数据初始值是未定义的。

如果满足如上要求进行了重新单独分配,则之前的内存需要进行回收。

如果 pOriginal ,则该回调的行为需要与 PFN_vkAllocationFunction 回调一致。

如果 size0 ,则该回调的行为需要与 PFN_vkFreeFunction 回调一致。

如果 pOriginal 非空,该分配 必须 确保 alignmentpOriginal 分配的 alignment 保持一致。

如果重分配失败,并且 pOriginal 非空,则 不能 回收 pOriginal 之前的内存。

PFN_vkReallocationFunction 是一个函数指针,需要指向一个返回值为 void* 形参为 (void *pUserData, void *pOriginal, size_t size, size_t alignment, VkSystemAllocationScope allocationScope) 的函数。比如:

#include <cstdlib>

void *VKAPI_PTR Reallocate(void *pUserData, void *pOriginal, size_t size, size_t alignment, VkSystemAllocationScope allocationScope)
{
   void* new_memory = std::aligned_alloc(alignment, size);
   if(new_memory)
   {
      memcpy(new_memory, pOriginal, size);// 此处 size 不一定对应 pOriginal 的内存大小,存在一定的问题。需要自己存储管理内存大小。
      free(pOriginal);
      return new_memory;
   }

   return nullptr;
}

PFN_vkReallocationFunction pfn_reallocation = &Reallocate;

警告

memcpy(new_memory, pOriginal, size) 中由于标准中没有定义如何获取 memalign(...) 分配的内存大小函数。需要自己存储。所以 size 不一定对应 pOriginal 的内存大小,存在一定的问题。

具体如何存储,可参考 通用自定义 代码模块,该模块给出了一种解决方案。

#include <malloc.h>

void *VKAPI_PTR Reallocate(void *pUserData, void *pOriginal, size_t size, size_t alignment, VkSystemAllocationScope allocationScope)
{
   return _aligned_realloc(pOriginal, size, alignment);
}

PFN_vkReallocationFunction pfn_reallocation = &Reallocate;
#include <malloc.h>
#include <algorithm>

void *VKAPI_PTR Reallocate(void *pUserData, void *pOriginal, size_t size, size_t alignment, VkSystemAllocationScope allocationScope)
{
   void* new_memory = memalign(alignment, size);
   if(new_memory)
   {
      memcpy(new_memory, pOriginal, std::min(malloc_usable_size(pOriginal), size));
      free(pOriginal);
      return new_memory;
   }

   return nullptr;
}

PFN_vkReallocationFunction pfn_reallocation = &Reallocate;
#include <stdlib.h>
#include <algorithm>

void* AlignedRealloc(void* memory, size_t size, size_t alignment)
{
   auto get_aligned_memory_size = [](void *memory) -> size_t
   {
      return *(size_t *)((uintptr_t)memory - sizeof(void *) - sizeof(size_t));
   };

   void *new_meta = AlignedMalloc(size, alignment);
   memcpy(new_meta, memory, std::min(size, get_aligned_memory_size(memory)));
   AlignedFree(memory); // 源码见 PFN_vkFreeFunction 章节中 通用自定义 代码模块
   return new_meta;
}

void *VKAPI_PTR Reallocate(void *pUserData, void *pOriginal, size_t size, size_t alignment, VkSystemAllocationScope allocationScope)
{
   return AlignedRealloc(pOriginal, size, alignment);
}

PFN_vkReallocationFunction pfn_reallocation = &Reallocate;

算法说明

该算法分配的对齐内存结构示意图如下:

_images/aligned_memory_struct.png

AlignedMalloc 对齐内存示意图

其中获取 memory 分配大小,直接获取 aligned_size 字段中的数据即可。

其中 PFN_vkFreeFunction 定义如下:

PFN_vkFreeFunction

// 由 VK_VERSION_1_0 提供
typedef void (VKAPI_PTR *PFN_vkFreeFunction)(
    void*                                       pUserData,
    void*                                       pMemory);
  • pUserData 为用户自定义数据指针。对应 VkAllocationCallbacks::pUserData

  • pMemory 要回收的内存指针。

PFN_vkFreeFunction 是一个函数指针,需要指向一个返回值为 void 形参为 (void *pUserData, void *pMemory) 的函数。比如:

#include <cstdlib>

void VKAPI_PTR Free(void *pUserData, void *pMemory)
{
   std::free(pMemory);
}

PFN_vkFreeFunction pfn_free = &Free;
#include <malloc.h>

void VKAPI_PTR Free(void *pUserData, void *pMemory)
{
   _aligned_free(pMemory);
}

PFN_vkFreeFunction pfn_free = &Free;
#include <malloc.h>

void VKAPI_PTR Free(void *pUserData, void *pMemory)
{
   free(pMemory);
}

PFN_vkFreeFunction pfn_free = &Free;
#include <stdlib.h>

void AlignedFree(void* memory)
{
   auto get_aligned_meta = [](void* memory) -> void*
   {
      return (((void **)pMemory)[-1]);
   };

   free(get_aligned_meta(memory));
}

void VKAPI_PTR Free(void *pUserData, void *pMemory)
{
   AlignedFree(pMemory);
}

PFN_vkFreeFunction pfn_free = &Free;

算法说明

该算法分配的对齐内存结构示意图如下:

_images/aligned_memory_struct.png

AlignedMalloc 对齐内存示意图

其中获取 memory 之前通过 malloc(...) 分配的原指针,直接获取 meta_point_size 字段中的数据即可。

其中 PFN_vkInternalAllocationNotification 定义如下:

PFN_vkInternalAllocationNotification

// 由 VK_VERSION_1_0 提供
typedef void (VKAPI_PTR *PFN_vkInternalAllocationNotification)(
    void*                                       pUserData,
    size_t                                      size,
    VkInternalAllocationType                    allocationType,
    VkSystemAllocationScope                     allocationScope);
  • pUserData 为用户自定义数据指针。对应 VkAllocationCallbacks::pUserData

  • size 分配的内存大小。单位为 字节

  • allocationType 分配的类型。

  • allocationScope 该内存声明周期所属的分配范围。

该函数回调仅仅用于纯信息返回。

其中 PFN_vkInternalFreeNotification 定义如下:

PFN_vkInternalFreeNotification

// 由 VK_VERSION_1_0 提供
typedef void (VKAPI_PTR *PFN_vkInternalFreeNotification)(
    void*                                       pUserData,
    size_t                                      size,
    VkInternalAllocationType                    allocationType,
    VkSystemAllocationScope                     allocationScope);
  • pUserData 为用户自定义数据指针。对应 VkAllocationCallbacks::pUserData

  • size 回收的内存大小。单位为 字节

  • allocationType 分配的类型。

  • allocationScope 该内存声明周期所属的分配范围。

该函数回调仅仅用于纯信息返回。

每一次分配都对应的 allocationScope 分配范围用于定义此次分配与之相关的对象。有效的枚举值被定义在了 VkSystemAllocationScope 中。其定义如下:

VkSystemAllocationScope

// 由 VK_VERSION_1_0 提供
typedef enum VkSystemAllocationScope {
    VK_SYSTEM_ALLOCATION_SCOPE_COMMAND = 0,
    VK_SYSTEM_ALLOCATION_SCOPE_OBJECT = 1,
    VK_SYSTEM_ALLOCATION_SCOPE_CACHE = 2,
    VK_SYSTEM_ALLOCATION_SCOPE_DEVICE = 3,
    VK_SYSTEM_ALLOCATION_SCOPE_INSTANCE = 4,
} VkSystemAllocationScope;
  • VK_SYSTEM_ALLOCATION_SCOPE_COMMAND 表示此次分配作用于 Vulkan 指令。

  • VK_SYSTEM_ALLOCATION_SCOPE_OBJECT 表示此次分配作用于 Vulkan 对象创建或使用。

  • VK_SYSTEM_ALLOCATION_SCOPE_CACHE 表示此次分配作用于 VkPipelineCache 或者 ``VkValidationCacheEXT `` 对象。

  • VK_SYSTEM_ALLOCATION_SCOPE_DEVICE 表示此次分配作用于 Vulkan 的设备。

  • VK_SYSTEM_ALLOCATION_SCOPE_INSTANCE 表示此次分配作用于 Vulkan 的实例。

其中作为 pfnInternalAllocationpfnInternalFree 回调函数形参的 allocationType 有效的枚举值被定义在了 VkInternalAllocationType 中。其定义如下:

VkInternalAllocationType

// 由 VK_VERSION_1_0 提供
typedef enum VkInternalAllocationType {
    VK_INTERNAL_ALLOCATION_TYPE_EXECUTABLE = 0,
} VkInternalAllocationType;
  • VK_INTERNAL_ALLOCATION_TYPE_EXECUTABLE 表示此次分配作用于 Host 端程序。

示例

这里给出 Windows 平台和 通用自定义 代码完整示例, 其他平台以此类推。

#include <malloc.h>

size_t memory_in_use = 0; // 统计内存使用大小

void *VKAPI_PTR Allocation(void *pUserData, size_t size, size_t alignment, VkSystemAllocationScope allocationScope)
{
   memory_in_use += size;
   return _aligned_malloc(size, alignment);
}

void *VKAPI_PTR Reallocate(void *pUserData, void *pOriginal, size_t size, size_t alignment, VkSystemAllocationScope allocationScope)
{
   memory_in_use -= _aligned_msize(pOriginal, alignment, 0);
   memory_in_use += size;
   return _aligned_realloc(pOriginal, size, alignment);
}

void *VKAPI_PTR Free(void *pUserData, void *pMemory)
{
   memory_in_use -= _aligned_msize(pMemory, alignment, 0);
   return _aligned_free(pMemory);
}

void VKAPI_PTR InternalAllocationNotification(void* pUserData, size_t size, VkInternalAllocationType allocationType, VkSystemAllocationScope allocationScope)
{
}

void VKAPI_PTR InternalFreeNotification(void* pUserData, size_t size, VkInternalAllocationType allocationType, VkSystemAllocationScope allocationScope)
{
}

VkAllocationCallbacks GetVkAllocationCallbacks(void* pUserData)
{
   VkAllocationCallbacks vk_allocation_callbacks = {};
   vk_allocation_callbacks.pUserData = pUserData;
   vk_allocation_callbacks.pfnAllocation = &Allocation;
   vk_allocation_callbacks.pfnReallocation = &Reallocate;
   vk_allocation_callbacks.pfnFree = &Free;
   vk_allocation_callbacks.pfnInternalAllocation = &InternalAllocationNotification;
   vk_allocation_callbacks.pfnInternalFree = &InternalFreeNotification;

   return vk_allocation_callbacks;
}

VkInstanceCreateInfo instance_create_info = 之前填写的创建信息;

VkAllocationCallbacks allocation_callbacks = GetVkAllocationCallbacks(nullptr);

VkInstance instance = VK_NULL_HANDLE;

VkResult result = vkCreateInstance(&instance_create_info, &allocation_callbacks, &instance);
if (result != VK_SUCCESS)
{
   throw std::runtime_error("VkInstance 创建失败");
}

// 缤纷绚丽的 Vulkan 程序 ...

vkDestroyInstance(instance, &allocation_callbacks);
void* AlignedMalloc(size_t size, size_t alignment)
{
   size_t meta_point_size = sizeof(void *);
   size_t aligned_size = sizeof(size_t);
   size_t meta_size = aligned_size + meta_point_size + alignment - 1 + size;

   void *meta = malloc(meta_size);

   uintptr_t start = (uintptr_t)meta + aligned_size + meta_point_size;

   void *aligned_meta = (void *)((start + ((alignment) - 1)) & ~(alignment - 1));

   *(void **)((uintptr_t)aligned_meta - meta_point_size) = meta;
   *(size_t *)((uintptr_t)aligned_meta - (meta_point_size + aligned_size)) = size;

   return aligned_meta;
}

void AlignedFree(void* memory)
{
   auto get_aligned_meta = [](void* memory) -> void*
   {
      return (((void **)pMemory)[-1]);
   };

   free(get_aligned_meta(memory));
}

void* AlignedRealloc(void* memory, size_t size, size_t alignment)
{
   auto get_aligned_memory_size = [](void *memory) -> size_t
   {
      return *(size_t *)((uintptr_t)memory - sizeof(void *) - sizeof(size_t));
   };

   void *new_meta = AlignedMalloc(size, alignment);
   memcpy(new_meta, memory, std::min(size, get_aligned_memory_size(memory)));
   AlignedFree(memory);
   return new_meta;
}

size_t GetAlignedMemorySize(void* memory)
{
   return *(size_t *)((uintptr_t)memory - sizeof(void *) - sizeof(size_t));
}

size_t memory_in_use = 0; // 统计内存使用大小

void *VKAPI_PTR Allocation(void *pUserData, size_t size, size_t alignment, VkSystemAllocationScope allocationScope)
{
   memory_in_use += size;
   return AlignedMalloc(size, alignment);
}

void *VKAPI_PTR Reallocate(void *pUserData, void *pOriginal, size_t size, size_t alignment, VkSystemAllocationScope allocationScope)
{
   memory_in_use -= GetAlignedMemorySize(pOriginal);
   memory_in_use += size;
   return AlignedRealloc(pOriginal, size, alignment);
}

void VKAPI_PTR Free(void *pUserData, void *pMemory)
{
   memory_in_use -= GetAlignedMemorySize(pMemory);
   AlignedFree(pMemory);
}

VkAllocationCallbacks GetVkAllocationCallbacks(void* pUserData)
{
   VkAllocationCallbacks vk_allocation_callbacks = {};
   vk_allocation_callbacks.pUserData = pUserData;
   vk_allocation_callbacks.pfnAllocation = &Allocation;
   vk_allocation_callbacks.pfnReallocation = &Reallocate;
   vk_allocation_callbacks.pfnFree = &Free;
   vk_allocation_callbacks.pfnInternalAllocation = &InternalAllocationNotification;
   vk_allocation_callbacks.pfnInternalFree = &InternalFreeNotification;

   return vk_allocation_callbacks;
}

VkInstanceCreateInfo instance_create_info = 之前填写的创建信息;

VkAllocationCallbacks allocation_callbacks = GetVkAllocationCallbacks(nullptr);

VkInstance instance = VK_NULL_HANDLE;

VkResult result = vkCreateInstance(&instance_create_info, &allocation_callbacks, &instance);
if (result != VK_SUCCESS)
{
   throw std::runtime_error("VkInstance 创建失败");
}

// 缤纷绚丽的 Vulkan 程序 ...

vkDestroyInstance(instance, &allocation_callbacks);

设备内存

Vulkan 标准规定了两种设备内存:

  1. Host 端内存 一般表示主板内存条上的内存。

  2. Device 端内存 一般表示 GPU 设备内部使用的内存。

这些设备内存根据不同特性又分为两种类型:

  1. Host 端内存,但可被 Device 端访问 这类内存的前提是在主板的内存条上,并且这部分内存可被 GPU 访问。

  2. Device 端独占内存 GPU 设备自身携带的专有内存。数据在该内存中将会有更高的性能。

其示意图如下:

_images/device_memory_struct.png

Vulkan 设备内存示意图

重要

不管内存是内存条上的还是物理设备上的,只要能被 Vulkan 识别并使用的内存都叫做 设备内存

由于 Vulkan 支持多种类型的内存,所以需要先通过 vkGetPhysicalDeviceMemoryProperties(...) 获取支持的内存信息。其定义如下:

vkGetPhysicalDeviceMemoryProperties

// 由 VK_VERSION_1_0 提供
void vkGetPhysicalDeviceMemoryProperties(
    VkPhysicalDevice                            physicalDevice,
    VkPhysicalDeviceMemoryProperties*           pMemoryProperties);
  • physicalDevice 要获取设备内存所对应的物理设备。

  • pMemoryProperties 返回设备内存信息。

其中 pMemoryProperties 将会写入 physicalDevice 所对应设备的所有可访问内存信息,有关 VkPhysicalDeviceMemoryProperties 定义如下:

VkPhysicalDeviceMemoryProperties

// 由 VK_VERSION_1_0 提供
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 的内存堆信息数组。

VK_MAX_MEMORY_TYPES 和 VK_MAX_MEMORY_HEAPS

#define VK_MAX_MEMORY_TYPES 32U
#define VK_MAX_MEMORY_HEAPS 16U

内存堆

所谓 其实就是一大块连续的容器,当分配内存时,操作系统会尝试从一大块容器中分配连续并且大小合适的小容器返回给用户,之后用户就可以使用这部分容器读写数据了。

Vulkan 中我们知道内存堆可分为两种:

  • Host 端

  • Device 端

其中 memoryHeaps 中就是用于获取具体内存堆是哪一种。其中 VkMemoryHeap 定义如下:

VkMemoryHeap

// 由 VK_VERSION_1_0 提供
typedef struct VkMemoryHeap {
    VkDeviceSize         size;
    VkMemoryHeapFlags    flags;
} VkMemoryHeap;
  • size 该堆大小。单位为字节。

  • flags 该堆类型标志位。

其中 flags 就是用于指示该堆的类型。其有效值定义于 VkMemoryHeapFlagBits 中,如下:

VkMemoryHeapFlagBits

// Provided by VK_VERSION_1_0
typedef enum VkMemoryHeapFlagBits {
    VK_MEMORY_HEAP_DEVICE_LOCAL_BIT = 0x00000001,
} VkMemoryHeapFlagBits;
  • VK_MEMORY_HEAP_DEVICE_LOCAL_BIT 该堆为设备端独占内存。

备注

有时 VkMemoryHeap::flags0 ,该值并没有定义于 VkMemoryHeapFlagBits 中。此时一般认为该内存堆为 Host 端内存。

如下,为一种可能的设备内存堆获取结果:

_images/memory_heaps.png

设备内存堆示意图

其中每个堆自身可以包含一到多个类型的内存,堆上的内存类型信息被定义在 memoryTypes 中,其 VkMemoryType 定义如下:

VkMemoryType

// 由 VK_VERSION_1_0 提供
typedef struct VkMemoryType {
    VkMemoryPropertyFlags    propertyFlags;
    uint32_t                 heapIndex;
} VkMemoryType;
  • propertyFlags 内存类型标志位。

  • heapIndex 对应的 memoryHeaps 堆索引。

其中 propertyFlags 有效值被定义在了 VkMemoryPropertyFlagBits 枚举中,其定义如下:

VkMemoryPropertyFlagBits

// 由 VK_VERSION_1_0 提供
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,
} VkMemoryPropertyFlagBits;
  • VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 表示在此内存类型上分配的内存可被物理设备高效访问。只有对应的堆为 VK_MEMORY_HEAP_DEVICE_LOCAL_BIT 才会有该内存类型。

  • VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT 表示在此内存类型上分配的内存可被 Host 端通过 vkMapMemory(...) 函数进行映射,进而进行访问。

  • VK_MEMORY_PROPERTY_HOST_COHERENT_BIT 表示在此内存类型上分配的内存将会自动进行同步,不需要手动调用 vkFlushMappedMemoryRanges(...)vkInvalidateMappedMemoryRanges(...) 来进行内存同步。

  • VK_MEMORY_PROPERTY_HOST_CACHED_BIT 表示在此内存类型上分配的内存为 缓存 (高速缓存)内存, Host 端访问 非缓存 内存要比访问 缓存 内存慢。但是 非缓存 内存总是 同步内存 ( VK_MEMORY_PROPERTY_HOST_COHERENT_BIT )。

  • VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT 表示在此内存类型上分配的内存只有物理设备可访问。内存类型不能同时为 VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BITVK_MEMORY_PROPERTY_HOST_VISIBLE_BIT 。此外其底层内存将会用于 惰性内存

备注

有时 VkMemoryType::propertyFlags0 ,该值并没有定义于 VkMemoryPropertyFlagBits 中。此时一般认为该内存堆为 Host 端内存(纯内存条上的内存)。

内存同步

所谓内存同步,就是将内存公开给 目标端 ,使得目标端能够看见完整的最新内容并访问。

如果在 VK_MEMORY_PROPERTY_HOST_COHERENT_BIT 类型内存上进行内存分配,则这部分内存将会自动进行内存同步,否则需要手动进行内存同步。

具体如何进行内存同步将会在之后的章节进行讲解。

惰性内存

当使用 VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT 类型分配内存时,表示底层分配为 惰性内存 。所谓惰性内存表示:在该内存分配时,其底层内存占用大小可以为 0 也可以为申请的内存大小。当该内存被需要时,其内存大小会随着需求单调增加。

备注

某些设备 没有 提供支持 VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT 类型的内存。所以该类型内存平时用的不多。

且惰性内存有如下限制:

  • 只有 Device 端能够访问该内存

  • 分配的 VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT 惰性内存类型中不能包含 VK_MEMORY_PROPERTY_HOST_VISIBLE_BITVK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BITVK_MEMORY_PROPERTY_HOST_VISIBLE_BIT 为互斥关系,不能同时存在)。

  • 只能用于 VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT 的图片进行绑定(这意味着 缓存 资源不存在使用惰性内存的情况)。(具体可参阅 资源 文档)。

查询一个惰性内存挡墙占有的内存大小,可通过 vkGetDeviceMemoryCommitment(...) 函数获取到,其定义如下:

vkGetDeviceMemoryCommitment
// 由 VK_VERSION_1_0 提供
void vkGetDeviceMemoryCommitment(
    VkDevice                                    device,
    VkDeviceMemory                              memory,
    VkDeviceSize*                               pCommittedMemoryInBytes);
  • device 对应的逻辑设备。

  • memory 对应的设备内存。其 必须VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT 内存类型进行分配的惰性内存。

  • pCommittedMemoryInBytes 指定的 memory 当前的大小将会写入其中。单位为 字节

备注

驱动会随时更新占用的大小,所以该函数返回的惰性内存大小值将会在不久失效。

如下,为一种可能的 设备内存类型 获取结果:

_images/memory_heap_and_type.png

设备内存类型示意图

从如上示例可看出,不同的 VkMemoryType::propertyFlags 之间可以有重叠的 VkMemoryPropertyFlagBits ,但是两两 VkMemoryType 不会有完全相同的 propertyFlagsVulkan 中是根据不同的 VkMemoryType::propertyFlags 对内存进行分类的。

备注

有些设备的 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 类型内存也会带有 VK_MEMORY_PROPERTY_HOST_VISIBLE_BITVK_MEMORY_PROPERTY_HOST_COHERENT_BIT 属性。这表示该设备专用内存可以被 Host 端直接访问。这种情况多见于移动端,某些 PC 端也可能出现该情况。

重要

VkPhysicalDeviceMemoryProperties::memoryTypes[i] 中的 i 非常重要,内存的分配主要是通过指定该索引进行分配。

内存分配

通过之前 vkGetPhysicalDeviceMemoryProperties(...) 函数我们可以获取到设备的内存信息,现在我们就可以通过这些信息进行内存分配了。为此 Vulkan 为我们提供了 vkAllocateMemory(...) 函数进行内存分配。该函数定义如下:

vkAllocateMemory

// 由 VK_VERSION_1_0 提供
VkResult vkAllocateMemory(
    VkDevice                                    device,
    const VkMemoryAllocateInfo*                 pAllocateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkDeviceMemory*                             pMemory);
  • device 分配内存的目标设备。

  • pAllocateInfo 内存分配信息。

  • pAllocator 句柄内存分配器。

  • pMemory 分配的内存句柄。

其中 pAllocateInfo 用于指定内存的分配信息, pAllocator 用于指定创建 pMemory 内存句柄时的分配器。

其中主要的内存分配信息被定义在了 pAllocateInfo ,对应的 VkMemoryAllocateInfo 定义如下:

VkMemoryAllocateInfo

// 由 VK_VERSION_1_0 提供
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 要分配的内存大小。单位为 字节

  • memoryTypeIndex 分配内存的目标内存类型索引。

其中 memoryTypeIndex 尤为重要,用于指定在 memoryTypes[memoryTypeIndex] 对应的内存类型上进行内存分配,对应分配的堆为 memoryHeaps[memoryTypes[memoryTypeIndex].heapIndex]

由于每个 memoryTypes 都有着不同的属性,所以一般会根据功能需求在某个内存类型上进行分配。

示例

比如在设备专用内存中分配内存(根据 设备内存类型示意图 中的情况):

VkDevice device = 之前创建的逻辑设备;

struct Color
{
   float r;
   float g;
   float b;

   Color(float r, float g, float b)
   {
      this->r = r;
      this->g = g;
      this->b = b;
   }
};

std::vector<Color> colors;
colors.push_back(Color(0, 0, 0));
colors.push_back(Color(0, 0, 1));
colors.push_back(Color(0, 1, 0));
colors.push_back(Color(0, 1, 1));
colors.push_back(Color(1, 0, 0));
colors.push_back(Color(1, 0, 1));
colors.push_back(Color(1, 1, 0));
colors.push_back(Color(1, 1, 1));

VkMemoryAllocateInfo memory_allocate_info = {};
memory_allocate_info.sType = VkStructureType::VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
memory_allocate_info.pNext = nullptr;
memory_allocate_info.allocationSize = sizeof(Color) * colors.size();
memory_allocate_info.memoryTypeIndex = 1; // 对应 VkPhysicalDeviceMemoryProperties::memoryTypes[1]

VkDeviceMemory device_memory = VK_NULL_HANDLE;

VkResult result = vkAllocateMemory(device, &memory_allocate_info, nullptr, &device_memory);
if(result != VkResult::VK_SUCCESS)
{
   throw std::runtime_error("VkDeviceMemory 内存创建失败");
}

内存回收

当内存成功分配之后,一般会对该内存进行一些列写入和读取操作,当该内存不再被需要时,就可以将该内存通过调用 vkFreeMemory(...) 进行回收了。其定义如下:

vkFreeMemory

// 由 VK_VERSION_1_0 提供
void vkFreeMemory(
    VkDevice                                    device,
    VkDeviceMemory                              memory,
    const VkAllocationCallbacks*                pAllocator);
  • device 要回收 memory 在分配时所对应的逻辑设备。

  • memory 要回收的目标内存。

  • pAllocator 要回收 memory 在分配时所对应的句柄分配器。

内存回收相对简单,只要 devicepAllocator 与分配时一致即可。

示例

VkDevice device = 之前创建的逻辑设备;
VkDeviceMemory device_memory = 之前分配的设备内存;

vkFreeMemory(device, device_memory, nullptr);

内存映射

如果内存分配时指定的内存类型支持 VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT 的话,说明该内存 可映射

  • 所谓 可映射 意思是:可以将该设备内存所对应的内存地址返回给 CPU

原则上所有的设备内存对于 CPU 来说并不像 new/malloc 分配出来的内存那样能够直接进行读写。为了 CPU 能够读写设备内存,硬件供应商都会提供一部分带有 VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT 属性的内存用于 CPU 访问。

而在 Vulkan 中分配的内存最终只会对应一个 VkDeviceMemory 句柄,为了能够获得 VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT 内存类型分配的内存句柄底层的内存地址,可以通过 vkMapMemory(...) 函数将分配的设备内存底层的 虚拟 (说明见下文)地址返回给 CPU (也就是 Host 端)。

该函数定义如下:

vkMapMemory

// 由 VK_VERSION_1_0 提供
VkResult vkMapMemory(
    VkDevice                                    device,
    VkDeviceMemory                              memory,
    VkDeviceSize                                offset,
    VkDeviceSize                                size,
    VkMemoryMapFlags                            flags,
    void**                                      ppData);
  • device 内存对应的逻辑设备。

  • memory 要映射的目标内存。

  • offset 内存映射从内存首地址开始的偏移量。从 0 开始。单位为 字节

  • size 要映射的内存大小。单位为 字节 。如果指定为 VK_WHOLE_SIZE ,则表明映射范围为从 offset 开始到 memory 结尾。

  • flags 内存映射的额外标志位参数。

  • ppData 内存映射结果。为 void* 的指针。该指针减去 offset 的对齐大小最小 必须VkPhysicalDeviceLimits::minMemoryMapAlignment

其中 memory 必须VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT 类型的内存上分配。当该函数成功返回后, memory 就被认为在 Host 进行了 内存映射 ,并处于 映射态

VkMemoryMapFlags

Vulkan 1.0 标准中, VkMemoryMapFlags 没有定义有效值,所以相应的 flags 参数赋值为 0 即可。

备注

在已经进行 内存映射 的内存上再次调用 vkMapMemory(...) 是开发错误。开发者应避免该错误。

虚拟地址

vkMapMemory(...) 函数返回的 ppData 内存映射结果确切来说 不是 真正意义上的内存地址,而是一个 虚拟 内存地址,对该地址的操作就 好似 对底层真正的内存进行操作,其本质上是对虚拟内存的操作。

由于返回的是虚拟内存地址,不同平台对于虚拟内存大小有不同的限制,所以当 vkMapMemory() 映射的虚拟地址范围超过平台限制后该函数将会返回 VkResult::VK_ERROR_MEMORY_MAP_FAILED 表示本次映射失败。为此,可通过将内存进行分小块进行映射或对已经映射的内存进行 解映射 (说明见下文)来释放一部分虚拟内存。

_images/memory_map.png

内存映射示意图

虚拟内存

映射出来的 虚拟内存VkDeviceMemory 底层设备内存是两个独立不同的内存。映射的内存有点类似于将 VkDeviceMemory 底层设备内存拷贝到虚拟内存中,并将这部分虚拟内存的首地址返回,作为映射结果。这对理解下文的 内存同步 非常重要。

ppData

对于 vkMapMemory(...) 返回的 ppData 指针进行操作时,其本质上是 对映射的虚拟内存进行操作 ,严格意义上不会影响底层映射的 VkDeviceMemory 内部数据(详情见下文的 内存同步 )。

示例

在有 VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT 内存类型的内存上分配内存,并进行内存映射(根据 设备内存类型示意图 中的情况):

VkDevice device = 之前创建的逻辑设备;

struct Position
{
   float x;
   float y;
   float z;

   Position(float x, float y, float z)
   {
      this->x = x;
      this->y = y;
      this->z = z;
   }
};

std::vector<Position> positions;
positions.push_back(Position(0, 0, 0));
positions.push_back(Position(0, 0, 1));
positions.push_back(Position(0, 1, 0));
positions.push_back(Position(0, 1, 1));
positions.push_back(Position(1, 0, 0));
positions.push_back(Position(1, 0, 1));
positions.push_back(Position(1, 1, 0));
positions.push_back(Position(1, 1, 1));

VkMemoryAllocateInfo memory_allocate_info = {};
memory_allocate_info.sType = VkStructureType::VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
memory_allocate_info.pNext = nullptr;
memory_allocate_info.allocationSize = sizeof(Position) * positions.size();
memory_allocate_info.memoryTypeIndex = 2; // 对应 VkPhysicalDeviceMemoryProperties::memoryTypes[2]

VkDeviceMemory device_memory = VK_NULL_HANDLE;

VkResult result = vkAllocateMemory(device, &memory_allocate_info, nullptr, &device_memory);
if(result != VkResult::VK_SUCCESS)
{
   throw std::runtime_error("VkDeviceMemory 内存创建失败");
}

void* device_memory_ptr = nullptr;

result = vkMapMemory(device, device_memory, 0, VK_WHOLE_SIZE, 0, &device_memory_ptr);
if(result != VkResult::VK_SUCCESS)
{
   throw std::runtime_error("VkDeviceMemory 内存映射失败");
}

memcpy(device_memory_ptr, positions.data(), memory_allocate_info.allocationSize); // 将数据写入 device_memory 内存中

内存解映射

当内存映射并使用结束后,可进行解除映射,进而释放系统的虚拟内存。可通过 vkUnmapMemory(...) 函数将映射过的内存进行 解映射 。该函数定义如下:

vkUnmapMemory

// 由 VK_VERSION_1_0 提供
void vkUnmapMemory(
    VkDevice                                    device,
    VkDeviceMemory                              memory);
  • device 内存对应的逻辑设备。

  • memory 要解映射的目标内存。该内存 必须 处于 映射态

该函数之后 映射态 的状态将解除,回归到 原始状态

示例

对前一个示例中分配的设备内存进行解映射:

VkDevice device = 之前创建的逻辑设备;
VkDeviceMemory device_memory = 之前分配的设备内存; // 分配于 VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT 并处于 映射态

vkUnmapMemory(device, device_memory);

内存同步

所谓内存同步是指:虚拟内存中的数据与对应的 VkDeviceMemory 设备内存底层数据保持一致。

当分配的设备内存所对应的内存类型 包含 VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_COHERENT_BIT 时,内存同步将 会自动 进行。其同步规则如下:

  • 当向映射的虚拟内存中写入时,写入虚拟内存中的数据也会同步到对应的 VkDeviceMemory 底层设备内存中。

  • 如果 GPUVkDeviceMemory 底层设备内存中写入数据时,这部分修改的设备内存也会同步到映射的虚拟内存中。

如果分配的设备内存所对应的内存类型 不包含 VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_COHERENT_BIT 的话,内存同步将 不会自动 进行。需要手动进行内存同步。

换句话说就是,映射的虚拟内存和对应的 VkDeviceMemory 设备内存是两个独立的内存,如果分配的设备内存 包含 VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_COHERENT_BIT 则无论对虚拟内存做修改,还是对设备内存做修改,双方数据将会自动保持一致。否则需要手动进行内存同步。

如此就有两个同步方:

  • 映射的虚拟内存

  • VkDeviceMemory 设备内存

虚拟内存同步到设备内存

当对映射的虚拟内存中的数据修改时,如果设备内存类型 不包含 VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_COHERENT_BIT 的话,则需要通过调用 vkFlushMappedMemoryRanges(...) 函数手动将虚拟内存中的数据同步(拷贝)到设备内存中。也就是将虚拟内存中的内容 冲刷 到设备内存中。其定义如下:

vkFlushMappedMemoryRanges
// 由 VK_VERSION_1_0 提供
VkResult vkFlushMappedMemoryRanges(
    VkDevice                                    device,
    uint32_t                                    memoryRangeCount,
    const VkMappedMemoryRange*                  pMemoryRanges);
  • device 内存对应的逻辑设备。

  • memoryRangeCount 指定 pMemoryRanges 数组长度。

  • pMemoryRanges 指向 VkMappedMemoryRange 数组。用于配置虚拟内存到设备内存的同步。

设备内存同步到虚拟内存

当对设备内存数据修改时,如果设备内存类型 不包含 VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_COHERENT_BIT 的话,则需要通过调用 vkInvalidateMappedMemoryRanges(...) 函数手动将设备内存中的数据同步(拷贝)到虚拟内存中。也就是 放弃 当前虚拟内存中的内容。其定义如下:

设备内存数据修改

对于设备内存数据的修改一般都是通过执行 GPU 的指令将数据写入到设备内存中,详细说明将会在之后的章节进行讲解。

vkInvalidateMappedMemoryRanges
// 由 VK_VERSION_1_0 提供
VkResult vkInvalidateMappedMemoryRanges(
    VkDevice                                    device,
    uint32_t                                    memoryRangeCount,
    const VkMappedMemoryRange*                  pMemoryRanges);
  • device 内存对应的逻辑设备。

  • memoryRangeCount 指定 pMemoryRanges 数组长度。

  • pMemoryRanges 指向 VkMappedMemoryRange 数组。用于配置设备内存到虚拟内存的同步。

其中 VkMappedMemoryRange 定义如下:

VkMappedMemoryRange

// 由 VK_VERSION_1_0 提供
typedef struct VkMappedMemoryRange {
    VkStructureType    sType;
    const void*        pNext;
    VkDeviceMemory     memory;
    VkDeviceSize       offset;
    VkDeviceSize       size;
} VkMappedMemoryRange;
  • sType 是该结构体的类型枚举值, 必须VkStructureType::VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE

  • pNext 要么是 NULL 要么指向其他结构体来扩展该结构体。

  • memory 要同步的目标设备内存。

  • offset 要同步的目标设备内存的偏移。单位为 字节 。有效值为 [0, memory 的大小]

  • size 要同步的目标设备内存的大小。单位为 字节 。如果为 VK_WHOLE_SIZE 则表示同步范围为 [offset, memory 结尾]

其中 VkMappedMemoryRange::memory 在手动同步时 必须 处在 映射态 。也就是 VkMappedMemoryRange::memory 必须已经通过 vkMapMemory(...) 将设备内存进行映射,并且 没有 解映射 。当内存同步结束之后,就可以进行 解映射 了。

示例

虚拟内存同步到设备内存
VkDevice device = 之前创建的逻辑设备;
VkDeviceMemory device_memory = 之前分配的 VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT 设备内存; // 设备内存类型没有 VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_COHERENT_BIT

void* device_memory_ptr = nullptr;

VkResult result = vkMapMemory(device, device_memory, 0, VK_WHOLE_SIZE, 0, &device_memory_ptr);
if(result != VkResult::VK_SUCCESS)
{
   throw std::runtime_error("VkDeviceMemory 内存映射失败");
}

// 对 device_memory_ptr 进行操作,比如:
memcpy(device_memory_ptr, 数据源, 数据大小); // 将数据写入虚拟内存中

VkMappedMemoryRange mapped_memory_range = {};
mapped_memory_range.sType = VkStructureType::VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE;
mapped_memory_range.pNext = nullptr;
mapped_memory_range.memory = device_memory;
mapped_memory_range.offset = 0;
mapped_memory_range.size = VK_WHOLE_SIZE;

// 内存同步
result = vkFlushMappedMemoryRanges(device, 1, &mapped_memory_range);
if(result != VkResult::VK_SUCCESS)
{
   throw std::runtime_error("VkDeviceMemory 同步失败");
}

// 解映射
vkUnmapMemory(device, device_memory);
设备内存同步到虚拟内存
VkDevice device = 之前创建的逻辑设备;
VkDeviceMemory device_memory = 之前分配的 VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT 设备内存; // 设备内存类型没有 VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
                                                                                                                //  此时该设备内存底层内存中已有相应的数据

void* device_memory_ptr = nullptr;

VkResult result = vkMapMemory(device, device_memory, 0, VK_WHOLE_SIZE, 0, &device_memory_ptr);
if(result != VkResult::VK_SUCCESS)
{
   throw std::runtime_error("VkDeviceMemory 内存映射失败");
}

VkMappedMemoryRange mapped_memory_range = {};
mapped_memory_range.sType = VkStructureType::VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE;
mapped_memory_range.pNext = nullptr;
mapped_memory_range.memory = device_memory;
mapped_memory_range.offset = 0;
mapped_memory_range.size = VK_WHOLE_SIZE;

// 内存同步
result = vkInvalidateMappedMemoryRanges(device, 1, &mapped_memory_range);
if(result != VkResult::VK_SUCCESS)
{
   throw std::runtime_error("VkDeviceMemory 同步失败");
}

// 对 device_memory_ptr 进行操作,比如:
目标类型* meta_date = (目标类型*)device_memory_ptr; // 转成目标类型,进行操作
meta_date-> ...;

// 解映射
vkUnmapMemory(device, device_memory);