標籤:

Vulkan中的heap/memory/buffer/image對象

這篇文章主要是在閱讀vulkan spec的第十章,memory allocation,以及第十一章,resource allocation部分的一些小結和個人理解。

Heap

相信大家都知道,vulkan api設計的一個非常重要的特點,就是向app暴露了許多細節,方便app根據自己的情況來優化場景。談到resource的時候,vulkan同樣如此。

在vulkan中,首先有一個叫做heap的概念。其實heap相信大家都已經有所了解,可以認為就是一個有著不同屬性的memory pool. app顯然可以根據它自己對資源的理解,講不同用途的資源申請分配到不同屬性的heap中去。但在之前的API,如ogl, d3d11.3等,資源的屬性更多的時候是作為一個hint,在資源創建的時候傳遞給driver,由driver來最終決定將資源分配到何處。例如,在d3d11.3中,創建資源的時候可以指定用途,從default/dynamic/immutable/staging中選擇一項,然後driver負責根據用途來挑選不同的memory類型。

然而,在vulkan中,資源對象,例如buffer和image,以及資源對象實際使用的memory,這兩個概念已經剝離開來了。也就是說,在vulkan中,創建一個對象,並不會同時為這個對象分配相應的memory。memory需要從heap中分配出來,然後和對象來一次association,對象才真正地有了memory來存放其中的內容。

在具體了解heap之前,我們還需要區分兩個非常重要的概念,即所謂的host memory和device memory。vulkan中所謂的host memory,指的是所有由實現來維護,對設備不可見的存儲部分。host memory的最佳體現,就是在vulkan中,調用各式各樣的api創建對象的時候,可選擇的一個參數VkAllocationCallbacks。這個對象內部包含了一系列的回調函數和所需要創建對象的meta信息,一個vulkan的實現可以使用這個來做自己的host memory維護。所以,host memory對我們而言並無多大用處,它的目的主要是為給driver提供便利。而device memory,就是各種各樣對設備可見的memory。我們在接下來提到memory的時候,指的都是device memory。

回到heap的概念上,heap本身作為一個硬體支撐的對象,屬於physical device的scope,所以,可以用一個vkGetPhysicalDeviceMemoryProperties(3) api來獲取硬體可用的heap資源的信息。這個api返回的數據結構是一個VkPhysicalDeviceMemoryProperties(3)的對象,其摘要如下:

typedef struct VkPhysicalDeviceMemoryProperties {n uint32_t memoryTypeCount;n VkMemoryType memoryTypes[VK_MAX_MEMORY_TYPES];n uint32_t memoryHeapCount;n VkMemoryHeap memoryHeaps[VK_MAX_MEMORY_HEAPS];n} VkPhysicalDeviceMemoryProperties;n

heap的信息以一種非常不直觀的形式,由這種數據結構展示出來。其中,真正的heap存放在memoryHeaps數組中,這個數組本身的大小是固定的,但是其中有效的heap個數由memoryHeapCount指定。一個VkMemoryHeap(3)只包含兩個信息,第一個是heap的大小,第二個則是一個掩碼數,目前只有一個選項可選:是否是device-local的。vulkan規定所有的heap中,至少有一個必須設置為device-local。

VkMemoryHeap(3)對象的信息也太少了,關於heap的其他一些必要的信息,都保存在另一個類似的數組中,即memoryTypes,一個VkMemoryType(3)才真正描述了heap的屬性。然而,memoryTypes數組和memoryHeaps數組之間並不是一一對應的關係,在一個VkMemoryType(3)中,保存了一個索引,這個索引表明這個VkMemoryType(3)所描述的是memoryHepas數組中的哪一個heap。這樣一來,多個不同的VkMemoryType(3)可以指向同一個VkMemoryHeap(3)對象,即一個heap對象可以有著多個不同的類型描述。後續我們會看到,在有些時候,type比heap本身更加重要。

VkMemoryType(3)除了一個索引之外,還保存了這個type的細節,同樣用一個掩碼數來表示,可選的有

  • 是否是device-local的,這個與heap中的device-local是不同的,並且只有heap有device-local設置的時候,type才可以設置為device-local
  • 是否是host-visible的
  • 是否是host-coherent的
  • 是否是host-cached的
  • 是否是lazily-allocated的,如果設置了這個的話,type只能是device-local加上lazily-allocated,不可以有其他多餘掩碼位。

vulkan實現保證至少有一個heap,其對應的type至少設置了host-visible+host-coherent。這裡需要提一下host-coherent,根據vulkan spec中的定義,這個coherent的概念似乎只要求memory在map和unmap之間,不需要使用flush/invalidate相關的API將 cpu寫對gpu可見/gpu寫對cpu可見,並沒有其他的要求。我想這個coherent並不會要求諸如ARM已經實現的CPU和GPU之間的雙向數據一致性吧,否則的話對於其他沒有掌握自己的匯流排技術的GPU廠商而言,做這個也太難了。

這裡有一個叫做lazily-allocated的概念,vulkan中,lazily-allocated的heap中分出來的memory對象只能和有著transient_attachment使用位的image使用。這麼分析下來,大概可以將lazily-allocated分配的memory對象結合renderpass優化出來的臨時對象,在運行的時候動態分配,甚至不為transient對象分配實際的物理空間,從而減少存儲消耗。不過lazily-allocated目前只能算是一個performance-hint,從公開的資料來看,沒有哪家gpu廠商實現了這個功能。

type的這些掩碼位可以有多種可能的組合,vulkan保證這些組合在memoryTypes數組中按照一種偏序的方式排列。關於偏序的定義可以參考vulkan是spec, 10.2章,核心的一點就是讓app可以在最靠前的位置找到滿足自己要求,並且沒有或者盡量少地擁有其他掩碼位的type。例如,app可能只需要一個device-local+host-visible的heap,那麼device-local+host-visible如果存在的話一定會被app從低往高檢索的時候第一個找到,如果不存在device_local+host-visible的heap,那麼可能一個device_local+host_visible+host-coherent的type可以被第一個檢測到,或者這種類型不存在的情況下第一個檢測到的是device_local+host_visible+host_coherent+host-cached,按照兼容性順序,一個一個排列。

Memory

我們一般時候說的memory,都是指存儲空間(不一定是cpu的內存,也可能是gpu的顯存),而在vulkan中,memory也特製memory對象,即從一個heap中分配出來的、可以為image/buffer做back的對象。當我們談到memory對象的時候,我們指的是vulkan中的memory概念,其他時候,我們談memory,指的就是存儲空間。

有了heap之後,我們就可以從中分配memory對象了,分配的方法很簡單,使用vkAllocateMemory(3)即可。分配主要需要的信息都在VkMemoryAllocateInfo(3)數據結構中體現,其摘要如下:

typedef struct VkMemoryAllocateInfo {n VkStructureType sType;n const void* pNext;n VkDeviceSize allocationSize;n uint32_t memoryTypeIndex;n} VkMemoryAllocateInfo;n

前兩項照例不用關心,第三項allocationSize給出了需要分配的memory的大小,第四項則是所需要memory的type索引。注意這裡用的是type的索引,而不是直接指定heap的方式,這就是我們之前提到過的type比heap本身更重要的一個體現。此外,給出type索引而不是指定heap,也允許driver可以在滿足分配要求的前提下,自行調整heap的負載,而不用app特意去關心這些過於細節的東西。不過,在有些uma平台,可能只有一個heap,這個時候做這些負載均衡就沒有什麼意義了。

分配出來的memory對象所滿足的對齊條件由實現自行決定,分配保證memory滿足實現所提出的條件。

一個memory對象在生命周期的盡頭,由vkFreeMemory(3)釋放。當釋放一個memory對象的時候,app應當保證此時device不再引用此memory對象上所關聯的image/buffer。

對於從帶有host-visible掩碼位申請出來的memory對象,app可以使用vkMapMemory(3)來獲取cpu側的指針以訪問其中的內容,然後使用vkUnmapMemory(3)來釋放。如果過程中有cpu寫,則可能需要使用vkFlushMappedMemoryRanges(3)來使得cpu寫對gpu可見,或者相反,如果有gpu寫,則需要使用vkInvalidateMappedMemoryRanges(3)使得其對cpu可見。當然,如果掩碼位同時還有host-coherent,那麼就不需要這兩個api了。在heap保證host-coherent對前提下,調用這兩個API也不是不可以,只不過可能會有performance issue. 這些api以及相關聯的數據結構還是相對比較簡單,可以self-explanation的,所以理解起來應該不會有什麼困難。

Buffer

在討論buffer和image之前,先讓我們看一看vulkan中有哪些類型的buffer以及image。首先是一張來自於vulkan官方spec的圖,

從圖中我們可以看到,vulkan中使用的buffer類型主要有:

  • indirect buffer

  • index buffer
  • vertex buffer
  • uniform texel buffer
  • uniform buffer
  • storage texel buffer
  • storage buffer

當使用vkCreateBuffer(3)創建一個buffer對象的時候,在創建信息VkBufferCreateInfo(3)中,我們就要指定這個buffer對象可能的使用場景。除了上述七種有著一一對應的使用掩碼位之外,還有額外的兩個,分別表示buffer是否可以作為transfer的src或者dst。此外還可以設置是否創建sparse的資源,以及其在queue family之間共享或者獨享等信息。

當我們創建好了一個buffer對象後,我們需要將它和一個memory對象關聯起來,這樣才可以真正讓這個buffer對象擁有memory。關聯是通過vkBindBufferMemory(3)來完成的,然而,直接binding可能會失敗,因為創建的buffer可能會有一些額外的要求,例如對齊等,所以在此之前,我們需要使用vkGetBufferMemoryRequirements(3)來獲取這個buffer對象對於memory等要求。要求被填入一個VkMemoryRequirements(3)的對象中,這個對象的摘要如下:

typedef struct VkMemoryRequirements {n VkDeviceSize size;n VkDeviceSize alignment;n uint32_t memoryTypeBits;n} VkMemoryRequirements;n

關於size和alignment,不再需要多提。需要注意的是最後一項,memoryTypeBits,這個可以看作是一個bool型的數組,從LSB開始,如果第i位置位,則表示我們在查詢出來的heap信息中的memoryTypes數組的第i個type是支持這個資源的。的確是一種非常繞的方式,而且這裡使用的也是memory type,而不是直接的heap。

對於一個texel的buffer,如uniform texel buffer或者storage texel buffer,shader如果需要使用的話,還需要用vkCreateBufferView(3)為其創建一個buffer view,才可以綁定到descriptor set上,創建信息保存在VkBufferViewCreateInfo(3)中,主要制定了view的format,以及view在buffer中的offset和range。

buffer和buffer view分別使用vkDestroyBuffer(3)以及vkDestroyBufferView(3)進行銷毀。

Image

vulkan中的image,大概相對於d3d中的texture, render target等,表示的是具有pixel array和mipmap結構的數據。從vulkan pipeline中,我們可以看出,image主要是作為shader resource的sampled image(即傳統意義上的texture),storage image,以及frame buffer中的input attachment, color attachment和depth/stencil attachment。然而相比於buffer簡單的幾個操作,由於image的一些特性,導致各個不同的實現之間對於image採用不同的存儲策略,例如tile或者不tile,最終導致image對於host的可能的不透明特性,host操作image需要分出種種情況來克服這種困難。image部分比buffer部分的內容要多得多得多。

創建一個image,需要使用API vkCreateImage(3),主要核心在於數據結構VkImageCreateInfo(3),其摘要如下:

typedef struct VkImageCreateInfo {n VkStructureType sType;n const void* pNext;n VkImageCreateFlags flags;n VkImageType imageType;n VkFormat format;n VkExtent3D extent;n uint32_t mipLevels;n uint32_t arrayLayers;n VkSampleCountFlagBits samples;n VkImageTiling tiling;n VkImageUsageFlags usage;n VkSharingMode sharingMode;n uint32_t queueFamilyIndexCount;n const uint32_t* pQueueFamilyIndices;n VkImageLayout initialLayout;n} VkImageCreateInfo;n

首先解釋幾個比較簡答的部分,例如,imageType決定image是1d/2d/3d,注意這裡沒有cube的指定。format決定image的格式,extent決定其大小,mipLevels和arrayLayers則是其mip層數和數組維度,samples給出了採樣個數。如同buffer創建的時候一樣,創建image的時候也需要指定共享模式,以及可用的queue的family索引。

關於image的usage,指定了image潛在可能的使用目的,這同樣是一個掩碼數,可用的掩碼位包括:

  • transfer src,image可以作為一個transfer命令的src
  • transfer dst, image可以作為一個transfer的dst
  • sampled,image可以被採樣
  • storage,image可以作為一個storage image被shader使用
  • color attachment,image可以作為RT或者resolved attachment
  • depth/stencil attachment,image可以作為一個DSV
  • transient attachment,臨時的attachment,必須從lazily-allocated memroy分出來,可以作為一個performance hint
  • input attachment,image可以作為framebuffer中的input attachment

tiling則決定了image本身的tile方法,包括linear和optimal兩個選項。如果是linear的image,host可以預期其layout,從而從host正確訪問其內容。然而,對host友好的linear對於device來說不一定友好。故而有optimal的tiling,這種是對device友好,但是對host而言基本不透明的tiling。更糟糕的是,由於各個vendor有著不同的結構設計,optimal的image layout基本是不兼容的。理解這一點,也是理解對於linear和optimal兩種不同tiling格式image的使用限制的一個非常重要的點。

創建image時候,flags位上除了buffer有的那三種sparse外,還多了額外的兩個掩碼位。一個是muttable_format,即是否可以在image上創建與其兼容卻並不完全一致的format的view. 另一個則是cube_compatible,當image是2d的時候,這指明是否可以創建cube,當然,如果是cube的話,image的extent中,width必須等於height。

接下來,是關於創建一個linear tiling的image的一些限制,只有當一下限制條件都滿足的時候,vulkan才保證創建linear tiling的image是成功的,否則可能會創建失敗,限制條件包括:

  • 必須是2d
  • format不能是depth/stencil,這是由於有些實現可能將depth/stencil放在一起,而有一些則可能分開放,所以如果format是depth/stencil則host是無法預期的
  • miplevel和arrayLayers必須都是1
  • 採樣數只能是1
  • 用途只包括transfer src和/或dst,不能包括其他的掩碼位

這些限制條件基本上確定了linear tiling的image只能用來讓host和device之間交換數據,而不能夠讓device來自己使用。

除此之外,最後一個參數,initialLayout,也是一個非常重要的參數。所謂的image layout大致指的就是image內部可能的擺放方式,可選的layout有:

  • undefined
  • general
  • color attachment
  • depth/stencil attachment
  • depth/stencil read-only
  • shader read-only
  • transfer src
  • transfer dst
  • preinitialized

初始化時候的layout只能是undefined或者preinitialized。當一個image處於某種layout的時候,表明其內部可能為了當前的使用方式做了特殊的優化。故而,當從一種layout轉到另一種layout的時候需要使用memory barrier來進行sync。關於這些layout具體的含義,可以參考VkImageLayout(3)部分。

前面提到過,對於linear tiling的image,host是可以預期其memory布局的。實際上,host可以使用vkGetImageSubresourceLayout(3)來查詢具體的subresource信息。然而,按照之前我們提到的約束,實際上linear tiling的image往往是一個沒有mip-map,不是array的但採樣2d的image,並且其format還不會是depth/stencil的。這個api需要接受一個VkImageSubresource(3)的數據結構來指明子資源,而實際上linear tiling的image值可能有一個子資源。但是因為VkImageSubresource(3)數據結構在其他地方也有用,所以這裡就將其列了出來而已,再者,未來版本可能會放寬對linear tiling的限制(僅僅是說可能),所以api設計對時候還是要留下餘地的。

host拿到linear tiling image的VkSubresourceLayout(3)信息之後,就可以依靠這個來更新image的內容了。如果要將這個image用於除transfer src/dst之外的用途,則還需要在創建一個optimal tiling的image,然後將數據從linear tiling image transfer到optimal tiling的image才可以。這也是host向device upload texture數據的一種比較高效的方式。

如果需要在shader中使用image,必須通過frame buffer或者descriptor set,通過image view的方式才可以。創建image view是通過vkCreateImageView(3)來完成的,這一部分的內容相對而言比較簡單,直接看spec即可。

如同buffer和memory的binding一樣,一個image首先需要使用vkGetImageMemoryRequirements(3)來獲取其對memory的要求,然後才可以用vkBindImageMemory(3)來將memory和image bind起來。


推薦閱讀:

vulkan中的同步和緩存控制
超級瑪麗現已加入 Vulkan 豪華午餐
Dota 2 現已支持 Vulkan API,使用起來究竟怎麼樣?
vulkan中的sparse resource支持

TAG:Vulkan |