写点什么

Vulkan 内存模型 + 管理

作者:江湖修行
  • 2023-09-08
    北京
  • 本文字数:3349 字

    阅读完需:约 11 分钟

Vulkan内存模型+管理

前言

最近在研究 Vulkan,在 Vulkan 中使用内存是个麻烦的过程,而且容易用错,今天就给大家分享下 Vulkan 的内存模型。


内存,在任何时候都是个稀缺的资源,内存管理更是个让人望而却步的事情。在这个崇尚用户体验的今天,不管是底层系统还是上层应用都在追求极致的性能优化,内存优化也是重中之重。从系统层的分页,池化,脏页回收,连续大内存到上层的优化的数据结构,内存共享,压缩等都在不停地压榨系统内存以获得更高性能的应用程序。

Vulkan 内存初心

我认为简单地理解 Vulkan 区别于 OpenGL 的最大特点就是 Vulkan 可以让应用开发者细粒度的控制,在内存方面也是如此。因为 Vulkan 认为应用自身对于内存占用是最清楚的,内存的创建释放,生命周期都是应用自身的行为导致的。面对复杂的应用场景,很难有通用的优化策略解决所有问题。不管是通过虚拟机还是驱动程序,帮助应用进行内存管理永远是低效的。


基于上述原因,Vulkan 将内存管理的工作交给了开发者负责,如何分配释放内存,怎样制定内存策略都由开发者自己决定,这无疑是返璞归真的至理。但话说回来这样的机制对开发者来说却不是友好的,所以我们更需要知道 Vulkan 的内存模型才能更高效地管理。

Vulkan 内存管理

Vulkan 中的内存分为两种:宿主内存和设备内存。


这两种内存的特点是宿主内存比设备内存慢。但是宿主内存的容量通常更大。另一方面来说,设备内存是直接对物理设备可见的,因此它更有效率也更为快速。

宿主内存

Vulkan 使用宿主内存来存储 API 的内部数据结构。Vulkan 提供了内存分配器机制,允许应用程序控制宿主机端的内存分配。如果应用程序不使用分配器机制,那么 Vulkan 将使用一个默认的分配器来管理内存和数据结构。


主机内存管理通过以下数据结构来完成:


typedef struct VkAllocationCallbacks {    Void*                                 pUserData;    PFN_vkAllocationFunction              pfnAllocation;    PFN_vkReallocationFunction            pfnReallocation;    PFN_vkFreeFunction                    pfnFree;    PFN_vkInternalAllocationNotification  pfnInternalAlloc;    PFN_vkInternalFreeNotification        pfnInternalFree;} VkAllocationCallback
复制代码


  • pUserData 是由用户自定义的值,因为每次回调的时候这个值可能会变。

  • PFN_vkAllocationFunction 是一个指向应用程序定义的内存分配函数的指针,用来管理 Vulkan API 创建的数据结构产生的内存。

  • PFN_vkReallocationFunction 是一个指向应用程序定义的内存重分配函数的指针,用来重新管理 Vulkan API 创建的数据结构产生的内存。

  • PFN_vkFreeFunction 是一个指向应用程序定义的内存释放函数。

  • PFN_vkInternalAllocationNotification 是一个指向应用程序定义的函数的指针,当被 Vulkan 实现调用时,就会给应用程序发内存分配的通知

  • PFN_vkInternalFreeNotification 是一个指向应用程序定义的函数的指针,当被 Vulkan 实现调用时,就会给应用程序发释放内存的通知


Vulkan 对宿主内存的要求就是内存地址是对齐的,这是因为某些高性能 CPU 指令在对齐的内存地址上效果最佳。通过假定存储 CPU 端数据结构的分配是对齐的,Vulkan 可以无条件使用这些高性能指令,从而提供显著的性能优势。

设备内存


设备内存, 即 GPU 内存,它对于物理设备是直接可见的, 物理设备可以直接读取其中的内存区块。图像对象,缓存对象以及 UBO(uniform buffer objec)都是在设备内存端分配的。


用 vkGetPhysicalDeviceMemoryProperties 函数查询后可以得到一个 VkPhysicalDeviceMemoryProperties 结构体中记载了物理设备上的内存属性。


void vkGetPhysicalDeviceMemoryProperties(    VkPhysicalDevice                       physicalDevice,    VkPhysicalDeviceMemoryProperties*      pMemoryProperties)typedef struct VkPhysicalDeviceMemoryProperties {     Uint32_t         memoryTypeCount;    VkMemoryType     memoryTypes[VK_MAX_MEMORY_TYPES];    Uint32_t         memoryHeapCount;    VkMemoryHeap     memoryHeaps[VK_MAX_MEMORY_HEAPS];} VkPhysicalDeviceMemoryProperties
复制代码

内存分配

VkResult vkAllocateMemory (    VkDevice                       device;    const VkMemoryAllocateInfo*    allocateInfo,    const VkAllocationCallbacks*   allocator,    VkDeviceMemory*                memory)typedef struct VkMemoryAllocateInfo {    VkStructureType  type;    Const void*      pNext;    VkDeviceSize     allocationSize;    uint32_t         momoryTypeIndex;        // 内存类型索引} VkMemoryAllocateInfo
复制代码


  • 用 vkAllocateMemory 函数来分配 VkDeviceMemory 类型的设备内存对象。

  • type 代表类型,类型必须是 VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO.

  • allocationSize 指定了分配的内存大小(字节)

  • memoryTypeIndex 的值就可以用刚刚的辅助函数获取。

  • pNext 扩展指针可以填入一个 VkMemoryDedicatedAllocateInfo 结构体,这样就可以在分配内存的时候指定一个专用 Buffer 或 Image 对象.

释放内存

Void VkFreeMemory (  VkDevice          device,  VkDeviceMemory    memory,  const VkAllocationCallbacks* allocator)
复制代码


  • VKDevice 设备句柄

  • VKDeviceMemory 准备释放的内存对象

  • allocator 控制内存释放的分配器对象

内存和资源

解决了内存分配的问题,但 GPU 绘制过程中需要各种资源,而资源通常是存储在 CPU 内存中的,和 GPU 内存并不互通,无法被 GPU 直接访问,因此我们需要一个方法把资源放到 GPU 内存中而且能被 GPU 按照一定的规矩访问。

Buffer and Image



Buffer 是最简单的资源类型,可以用来储存线性的结构化的数据,也可以储存内存中原始字节。它可以通过调用命令缓冲区来绑定,交由 GPU 硬件操作。Vulkan 中用 VkBuffer 句柄来指示 Buffer 对象,并且用以下方法进行创建:


typedef struct VkBufferCreateInfo {    VkStructureType sType;    const void* pNext;    VkBufferCreateFlags flags;    VkDeviceSize size;    VkBufferUsageFlags usage;    VkSharingMode sharingMode;    uint32_t queueFamilyIndexCount;    const uint32_t* pQueueFamilyIndices;} VkBufferCreateInfo;

VkResult vkCreateBuffer( VkDevice device, const VkBufferCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkBuffer* pBuffer )
复制代码


  • flags 是一个 VkBufferCreateFlagBits 类型的枚举

  • size 是 VkBuffer 映射的一段区域的内存大小,即数据大小。

  • usage 是 Buffer 的具体功用,例如用作顶点缓存,索引缓存,转移缓存

  • sharingMode 明确了 Buffer 被多个队列共享访问的模式,一般选 VK_SHARING_MODE_EXCLUSIVE。

  • queueFamilyIndexCount 和 pQueueFamilyIndices 代表访问这个 Buffer 的队列族

  • pNext 在 Vulkan1.1 版本后,允许我们使用 VkExternalMemoryBufferCreateInfo 结构体来创建一个用于存储的外部缓冲,在 Vulkan1.2 版本后,允许我们使用 VkBufferOpaqueCaptureAddressCreateInfo 结构体来为 Buffer 要求具体的设备地址。


Image 相对复杂,其具有特殊的布局和格式。Image 的布局(layout)对内存有特殊需求,主要有两种主要的平铺模式:


  • linear - 其中的图像(Image)数据线性排列在内存中。

  • optimal - 其中的图像(Image)数据以高度优化的模式进行布局,可以有效利用设备的内存子系统。

  • 线性布局(linear layout)适合连续的单行的读写,但是大多数图形操作都涉及到跨行读写纹理元素,如果图像自身的宽度非常宽,相邻行的访问在线性布局中会有非常大的跳转。这可能会导致性能问题。


优化布局(optimal layout)的好处是内存数据根据不同内存子系统进行优化,比如将所有的纹理像素都优化到一块连续的内存区域中,加快内存处理速度。下图很形象地说明了两种布局的优劣势:



GPU 通常倾向于使用优化布局以实现更有效的渲染。但优化部分因不同品牌有差异且是内部逻辑,所以 CPU 想要读取图像信息还需要多一层转换。

总结

本文讲述了 Vulkan 的内存布局和管理方式,希望能对大家后续实现 Vulkan 程序有所帮助,在此建议大家尽量做到内存复用,因内存分配和释放都需要昂贵的开销。连续内存对象可以享受更好的缓存利用率,内存对齐的数据性能更优。


微信公众号首发,欢迎关注:江湖修行,欢迎关注,转发,评论交流

发布于: 刚刚阅读数: 4
用户头像

江湖修行

关注

还未添加个人签名 2021-12-05 加入

还未添加个人简介

评论

发布
暂无评论
Vulkan内存模型+管理_移动端_江湖修行_InfoQ写作社区