Vulkan编程指南翻译 第二章 第三节 GPU设备内存管理

2017-02-20

 

当Vulkan操纵数据,数据必须存储在设备内存。这是GPU设备可以访问的内存。Vulkan系统有四个级别的内存。某些系统或许只有其中的一个或几个。给定一个CPU(应用程序运行的处理器)设备和GPU设备(执行Vulkan命令的处理器),他们都有各自的物理存储器。另外,一个处理器附带的物理存储器的某部分区域可以被另外一个处理器访问到。

某些情况下,共享内存的可见区域可能会相当的小,其他情况下,也许只有一块儿物理存储器,被host和GPU共享。Figure2.5示例了CPU和GPU各自的物理存储器的内存映射。

 

Figure 2.5: Host and Device Memory

可以被GPU访问的内存被称为device memory,即使这些内存是物理连接在CPU端。在这种情况下,它是主机端的设备内存。这与主机端内存有区别,主机内存又被称为系统内存,是可以通过malloc和new操作获取到的普通内存。设备内粗也可以通过映射被CPU访问到。

一个典型的单个GPU通常是插在PCI-Express插槽的插卡中的,它有一定容量的专用内存,是通过电路直接附着在电路板上的。这个存储器的一部分只可被GPU访问到,一部分可以被CPU访问到。另外,GPU可以访问到一些甚至全部的主机系统内存。这些内存池对CPU来说就是堆,。。。

另外一方面,一个典型的嵌入式GPU--比如说那些嵌入式系统,手机,甚至笔记本电脑中可以找到--会与CPU共享存储器控制器和子系统。这种情况下,很有可能对主机内存的访问时一致性的,且GPU会暴露少一些--甚至一个内存堆。这就是通常所谓的“统一内存架构”。

分配GPU内存

GPU上分配的任何内存都通过VkDeviceMemory类型的数据表示,它通过vkAllocateMemory()产生,原型如下:

VkResult vkAllocateMemory (

VkDevice device,

const VkMemoryAllocateInfo* pAllocateInfo,

const VkAllocationCallbacks* pAllocator,

VkDeviceMemory* pMemory);

device参数指定了从哪个GPU分配内存。pAllocateInfo描述了新分配的内存对象,如分配成功,pMemory.将指向新分配的内存。pAllocateInfo指向一个VkMemoryAllocateInfo类型的数据结构,其原型如下:

typedef struct VkMemoryAllocateInfo {

VkStructureType  sType;

const void*  pNext;

VkDeviceSize  allocationSize;

uint32_t  memoryTypeIndex;

} VkMemoryAllocateInfo;

这个数据结构很简单,进包含内存的大小,类型。sType应当被设置为VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,除非是用了需要获知更多内存分配信息的拓展,pNext应当被设置为nullptr。allocationSize指定了需要分配的内存的大小,以byte为单位。内存的类型通过memoryTypeIndex 指定,可调用vkGetPhysicalDeviceMemoryProperties()用memoryTypeIndex,从内存类型的数组查询,可以得到内存类型,我们在第一章中讲述过。

一旦你完成了GPU内存分配,它就可以用来存储buffer、image等资源。Vulkan也许会把内存用作其他用途,比如其他类型的设备对象,内部分配或者数据结构,片段存储,诸如此类。这些分配活动由Vulkan驱动管理,不同Vulkan实现之间的差别可能会较大。

当你不再使用这些内存时,你需要释放它们。可以调用vkFreeMemory(),原型如下:

void vkFreeMemory (

VkDevice device,

VkDeviceMemory memory,

const VkAllocationCallbacks* pAllocator);

vkFreeMemory()需要直接传入memory对象。你需要保证在释放之前,在设备上没有queue正在使用该内存。Vulkan将不会跟踪memory对象的使用情况。如果设备试图访问已经被释放的内存,结果是不可知的,这也许会轻易的导致应用程序崩溃。

深入来说,对内存的访问必须要严格保持同步。当一块内存被其他线程的command访问时,尝试释放它将产生不可知的结果切易导致程序崩溃。

在某些平台上,也许有单线程能够内存分配的次数的上线。如果你尝试分配更多,将导致分配失败。这个上限可以通过调用vkGetPhysicalDeviceProperties()函数并检查返回的maxMemoryAllocationCount类型对象的maxMemoryAllocationCount成员便可获知。Vulkan标准保证的最小值是4096,一些平台或许高得多。即使这个值看起来很小,它被设定为如此的意图就是让你单次尽量分配大的内存块,然后,从这个大的内存块再分配小的内存块,在单次分配的内存中,尽量多放入资源。只要内存允许,资源创建的数量是没有上限的。

平常情况下,当你从堆中分配内存时,分配到的内存被 赋值给被返回的VkDeviceMemory类型的一个对象,它需要调用vkFreeMemory() 被销毁。在一些情况下,你(或者Vulkan实现)并不知道对于某些操作来说需要多少内存,或者是究竟是否需要内存。

特别是,在渲染时被需要用到的中间临时数据来自于图像。当图像被创建时,如果VkImageCreateInfo类型的数据包含VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT,Vulkan就知道这个图像的数据生存周期很短,一次,它很大可能从来被会被写入设备内存。

在这种情况下,你可要求Vulkan内存分配时导游lazy参数,并把分配活动推后到Vulkan决定真的需要使用物理存储空间的时候。若有这种需求,需要报内存类型设置为VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT。选择其他的类型也是可以让程序正常工作的,但是,它总是事先就分配好了内存,不管你使用与否。

如果你想要知道一次内存分配究竟有多少已经真的在物理内存中分配了,和多少备用内存已经为一个内存对象分配了,你可以调用vkGetDeviceMemoryCommitment(),原型如下:

void vkGetDeviceMemoryCommitment (

VkDevice device,

VkDeviceMemory memory,

VkDeviceSize* pCommittedMemoryInBytes);

内存所在的设备通过device参数传入,需要查询的内存块通过memory参数传入。pCommittedMemoryInBytes是一个指向了有多少内存真正被分配了的变量的指针,指向的值被该函数修改。这个数值的大小就是从堆上计算得来的,带有内存类型信息。

对于那些不包含K_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT内存类型的内存对象,或者内存被完全提交的内存对象,vkGetDeviceMemoryCommitment()将只返回整个内存对象的大小。vkGetDeviceMemoryCommitment()返回的值的大小只能做参考用。很多时候,这个信息是过时的,而且你对这个值也无能为力。

 

CPU访问设备内存

如前面小节所述,设备内存被分为几个区域。纯设备内存只能被设备访问。然而,有几个区域是可以被host和device都能访问的。Host就是主应用程序所运行的处理器,且我们可以让Vulkan返回一个指向CPU可访问区域内存的指针。这叫做内存映射。

为了把设备内存映射为主机端内存地址,需要被映射的内存对象必须要从VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT类型的堆中分配获得。假设我们需要这么做,映射内存来获取一个CPU可用的指针是通过调用vkMapMemory() 来实现的,原型如下:

VkResult vkMapMemory (

VkDevice  device,

VkDeviceMemory  memory,

VkDeviceSize  offset,

VkDeviceSize  size,

VkMemoryMapFlags  flags,

void**  ppData);

被映射的内存所在的设备是通过device参数传递的,内存对象的handle是通过memory参数传递。一定要在外部同步的访问这个内存对象。若需要映射一个内存对象的一部分,需要通过offset参数确定起始位置和size参数来制定区域的大小。如果你想要映射整个内存对象,直接设置offset为0,size为VK_WHOLE_SIZE即可。设置offset为非零值且size为VK_WHOLE_SIZE将会从内存对象的offset位置到结束做映射。offset、size的单位都是byte。你不应当尝试把一个内存对象映射到一个较小的区域。flags参数是为以后保留的,当前应当设置为0。

如果vkMapMemory()调用成功,一个指向映射区域的指针就被写入ppData。在应用程序中,这个指针可以被转换为任意的类型,并且被dereference后可以直接读写。Vulkan保证,当指针减去offset时,vkMapMemory()返回的指针是对齐为设备内存对齐最小值的倍数。

这个值是通过调用vkGetPhysicalDeviceProperties()函数返回的VkPhysicalDeviceLimits类型的数据的minMemoryMapAlignment成员获取的。它肯定至少是64byte的,但是,但可能是高于此的任何2的幂的值。在一些CPU架构中,可以通过让数据对齐和指令对齐得到更高的运行效率。为达到此目的,minMemoryMapAlignment经常和cache line size匹配,或者和机器的最大寄存器自然对齐。如果被传入未对齐的指针,一些主机CPU指令会出错。一次,你可以检查minMemoryMapAlignment,决定使用优化过的函数或者使用可处理未对齐指针的默认函数,只是这样就要承受性能损失。

当你用映射指针做完了工作,它需要被解除映射,需调用vkUnmapMemory(),原型如下:

void vkUnmapMemory (

VkDevice  device,

VkDeviceMemory  memory);

内存所在的设备通过device参数传入,需要被解除映射的内存对象通过meory参数传入。和vkMapMemory()一样,对内存对象的访问需要在外部同步访问。

对一个相同的内存对象,不能做多次映射。也就是,你不能对一个内存对象使用不同的参数多次调用vkMapMemory()来做内存映射,不管这些内存是否有重叠部分。在取消映射的时候,范围是不需要的,因为Vulkan知道映射的范围有多大。

一旦内存对象被取消了内存映射,任何指向以前通过调用vkMapMemory()获取的指针是无效的,不应当被使用。即使你把内存对象以相同的参数范围映射,你也不能假设会得到相同的指针。

当设备内存被映射到主机内存地址空间时,就有两个地方可以修改该内存了。对于该映射内存,在CPU和GPU端都很有可能有cache层次的存在。这两个地方的cache也许不能保持一致性。为了保证CPU和GPU能看到一致性的数据视图,即使被另外一边写入了也无所谓。那么就有很有必要强制Vulkan把cache写入内存。

设备支持的每一种内存类型都有一些属性,其中的一个可能是VK_MEMORY_PROPERTY_HOST_COHERENT_BIT。如果有一个映射的内存被设置为这种属性,Vulkan就会保证cache之间的一致性。在某些情况下,cache之间会自动一致,因为它们是被CPU和GPU共享的,或者是有一种形式的一致性协议来保持他们之间的同步。在其他情况下,Vulkan 驱动可能会推断出什么时候cache需要被刷新或者被失效,进而在幕后进行上述操作。

如果一块映射内存区域的属性之一没有被设置为VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,那么你就需要显式地刷新cache或者使cache无效。为了刷新带有pending状态写操作的cache,需要调用vkFlushMappedMemoryRanges(),原型如下:

VkResult vkFlushMappedMemoryRanges (

VkDevice  device,

uint32_t  memoryRangeCount,

const VkMappedMemoryRange*  pMemoryRanges);

拥有内存对象的设备是通过device参数指定的。需要刷新的区域大小是通过memoryRangeCount 参数指定的,每一个范围的信息都是封装在VkMappedMemoryRange类型的数据被传入的。一个指向memoryRangeCount的数组的指针是通过pMemoryRanges参数传入的。VkMappedMemoryRange的定义如下:

typedef struct VkMappedMemoryRange {

VkStructureType  sType;

const void*  pNext;

VkDeviceMemory  memory;

VkDeviceSize  offset;

VkDeviceSize  size;

} VkMappedMemoryRange;

VkMappedMemoryRange 的sType域应当被设置为VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE,pNext应当被设置为nullptr。每一个内存范围都指向了一个内映射的内存对象,是通过memory域指定的,范围是通过offset和size计算得来的。你不需要刷新那个对象的整个映射区域,所以offset和size不需要和vkMapMemory()的参数匹配。如果内存对象没有被映射,或者offset和size确定的一个对象的区域没有被映射,那么刷新操作就不会有任何作用。为了刷新一个内存对象的任何映射区,只要把offset设置为0,size设置为VK_WHOLE_SIZE即可。

如果CPU向映射内存区域写入了数据而且需要设备看到写人的效果,刷新是必须的,然而,如果设备写入内存映射区域且你需要CPU能够看到写入的信息,你需要在CPU端主动的使任何cache无效,因为这些信息可能是没有更新的。你需要调用vkInvalidateMappedMemoryRanges(),原型如下:

VkResult vkInvalidateMappedMemoryRanges (

VkDevice  device,

uint32_t  memoryRangeCount,

const VkMappedMemoryRange*  pMemoryRanges);

对vkFlushMappedMemoryRanges()来说,device参数就是拥有内存对象并

如果有任何意见,欢迎留言讨论。


[ 主页 ]
COMMENTS
POST A COMMENT

(optional)



(optional)