標籤:

vulkan中的sparse resource支持

之前一篇談到了vulkan中的buffer和image對象,以及兩者是如何綁定到一個memory對象上的。一般而言,這種綁定具有以下幾個約束:

  • 綁定必須是完全綁定,即一個buffer/image對象必須完全綁定到memory對象上。反之則不需要,即memory對象上不必只綁定一個buffer/image對象
  • 綁定是連續的,這點很容易理解,buffer/image實際需要的存儲空間在memory中以一個offset加上size來指定,這樣的話實際上只能是一個連續的range才可以
  • buffer/image對象只能綁定到一個memory對象上
  • 綁定關係是不可變的,不存在所謂的「解除綁定」和「再次綁定」的概念

這樣的設計雖然保證了硬體和driver實現的簡單,卻犧牲了app的靈活性。因此,vulkan同時提供了一個可選的sparse的概念,來一一破除上述限制條件。vulkan中的sparse特性包括如下三點:

  1. sparse binding,除了必須遵守「完全綁定」這一條之外,放寬了其他三個限制條件,因此,與其說這是一個「稀疏綁定」,不如說是「離散綁定」(discrete binding)更加合適
  2. sparse residency,同時放寬了所有的四項約束條件,這才是真正意義上的「稀疏綁定「
  3. sparse aliased,稀疏綁定時候可能存在的memory混疊

sparse binding是後邊兩項的基礎,指定sparse residency或者sparse aliased的時候,必須同時指定sparse binding。

需要特別註明一點的是,sparse特性是以(可能)犧牲性能為代價來(潛在地)提高靈活性的,使用的時候需要認真評估一下,是否有必要。

Sparse Binding

sparse binding,如前所說,允許將一個buffer/image對象的區域綁定到多個memory對象上,並且綁定可以被修改。只是sparse binding的資源在使用之前必須被完全綁定,故而實在談不上什麼「稀疏」,更準確的描述應該是「離散」吧。

buffer和image都可以使用sparse binding,並且所有格式的image,不管是linear tiling的,還是optimal tiling的,也都支持sparse binding。從這一點來看,sparse binding的資源和普通的資源除了在和memory對象上的映射關係之外,使用上基本沒什麼差別。

當綁定一個buffer或者image對象的時候,必須滿足對齊以及粒度條件。對齊以及粒度,都是通過VkMemoryRequirements(3)中的alignment參數給出的。一般來說,對於sparse的資源,對齊和粒度都是頁面本身的大小,典型情況下,就是4kB。

我們都知道,buffer對象的線性存儲方式是高度地host可預測的。故而,當一個buffer對象終端一段,例如,開始的4kB,被綁定到一個memory對象mem0上的4kB,而接下來的4-8kB,被綁定到另一個memory對象mem1上的4kB的時候,host可以預測到,當host訪問buffer對象前4kB的內容的時候,一定會落在mem0上綁定的4kB,而不會落在mem1上。當然,規範中指明至於前4kB和mem0中的內存空間的映射關係是由實現自行決定的,但最普遍的做法,其實還是一一映射。

上述一段話看起來似乎是理所應當的。對於buffer而言的確如此,但是對於image而言,這種對象空間和存儲空間的映射關係是無法保證的。整個image對象,都作為一整塊opaque的東西,映射到綁定到memory(s)上,host是無法預知它訪問memory哪一部分的時候會落到實際的哪個memory上。對於image而言,無論是linear tiling,還是optimal tiling,都是如此。

引入sparse binding的概念,一定程度上允許資源在創建的時候就預留好所需要的虛擬地址空間,而不用等到實際綁定到memory對象的時候才能夠獲取虛擬地址空間。

是否支持sparse binding可以通過VkPhysicalDeviceFeatures(3)中的sparseBinding成員來查詢到。

Sparse Residency

sparse residency特性需要sparse binding的支持,即創建buffer/image資源的時候應當同時指定sparse residency和sparse binding。sparse residency相比於sparse binding的提升點,就是不再要求資源在使用之前必須全部綁定(即全部擁有back memory空間)。那麼,一個很顯然的問題就出現了:如果device恰好訪問到沒有綁定memory部分的資源內容怎麼辦?

物理設備的限制,被保存在一個叫做VkPhysicalDeviceProperties(3)的數據結構中,有一個成員類型是VkPhysicalDeviceSparseProperties(3),在這個數據結構中,有一個bool值residencyNonResidentStrict指明了當device訪問到沒有綁定memory,即術語non-residency部分資源的行為。如果這個值為true,則讀返回全零的數據,寫被丟棄。如果這個值為false,則讀返回未定義的數據,寫同樣被丟棄。我們可以看到,這個值主要控制讀返回的數據究竟是定義好的全零,還是未定義的數據。由於cache本身的存在,嚴格地返回全零很可能是不現實的。例如,如果向non-residency寫入一筆數據,然而這筆數據還沒有真正抵達地址翻譯部分,從而沒有真正地被發現應當被丟棄,那麼隨後的讀很可能直接命中寫入數據的cache,從而提前返回。這一點在報feature的時候需要額外考慮到。

說回sparse residency,sparse residency的buffer除了不需要全部駐留外,與僅僅sparse binding的buffer別無二致。綁定時候的對齊和粒度條件也一模一樣,由alignment決定。buffer是否支持sparse residency可以通過VkPhysicalDeviceFeatures(3)中的sparseResidencyBuffer來查詢到。

但是,sparse residency的image就和僅僅sparse binding的image大不一樣了。首先是格式上的支持,sparse residency僅支持optimal-tiling,並且format是color的那些非壓縮的格式。format是depth/stencil,或者壓縮格式的那些,是否支持sparse residency可以由實現自行決定,並且通過vkGetPhysicalDeviceSparseImageFormatProperties(3)來查詢。其次,僅僅是sparse binding的image因為使用之前必須全部綁定,所以我們不必去區分image本身哪一塊區域需要綁定哪一塊不需要。然而,sparse residency的image,要求必須提供一種機制,來指明image的layout和memory的footprint之間的對應關係。因此,vulkan中設計了一套比較複雜的機制來實現這一點,並且在其中引入了諸如mip-tail region,meta-data aspect等新的概念,我們隨後會講到。

sparse residency的image的支持是通過VkPhysicalDeviceFeatures(3)中的sparseResidencyImage2D/sparseResidencyImage3D/sparseResidency2Samples/sparseResidency4Samples/sparseResidency8Samples/sparseResidency16Samples 來體現的,其中需要特彆強調一點的是,sparseResidencyImage2D僅僅指明對於單採樣的2D image的支持,多採樣(2/4/8/16)由隨後的一系列值指定。

sparse residency的image做綁定的時候同樣要滿足對齊和粒度的條件,只不過這次決定對齊和粒度的並非都是alignment,alignment只決定對其對齊條件,一個叫做image sparse block的新的概念決定了粒度。所謂的image sparse block,可以認為是一個sparse residency的image的一個最小的綁定單位,不僅有形狀,還有對應的內存空間的要求。事實上,此時的alignment的值就是image sparse block的size大小。

注意,image sparse block內部的對象和內存映射關係由實現自行決定。

vulkan中建議的image sparse block的內存空間是64kB,並且在不同的情況下,這麼大空間能夠放得下的image子區域的形狀有:

並且,對於多採樣的情況下,有:

如果一個實現決定遵從vulkan的建議,將其所支持的sparse residency的image sparse block的大小和形狀設置為上述值,則我們認為這個實現遵從「標準」的sparse residency。這裡的「標準」並非強制要求,只是建議的標準。在VkPhysicalDeviceSparseProperties(3)中,使用residencyStandard2DBlockShape/residencyStandard2DMultisampleBlockShape/residencyStandard3DBlockShape分別表示不同類型的image對「標準」image sparse block的支持。

對於特定條件的image,我們可以通過查詢VkSparseImageFormatProperties(3)中,flags位是否包含掩碼位K_SPARSE_IMAGE_FORMAT_NONSTANDARD_BLOCK_SIZE_BIT來獲取是否支持「標準」 image sparse block設定。如果實現支持標準image sparse block的話,VkSparseImageFormatProperties(3)中的imageGranularity必須和上述的形狀匹配。當然,如果實現不支持標準image sparse block的話,imageGranularity將給出這個時候的image sparse block的形狀。

對於同一個image,實現不能同時支持「標準」和「非標準」image sparse block,並且,如果支持「標準」的話,必須優先使用「標準」image sparse block。

現在,我們已經有了一個image中最小的綁定單位,不僅有大小,還有形狀。例如,如果我們有一個512x512的2d image,bpe為8-bit,並且我們手上的顯卡恰好支持標準的image sparse block。我們就可以將其拆成4個256x256的image sparse block,然後分別將這4個image sparse block任意地綁定到memory對象上,全部綁定,或者只綁定一部分,按順序或者亂序,綁定到一個memory對象,或者分別綁定到多個上。只要我們滿足綁定到最小粒度是一個image sparse block,我們就可以隨心所欲。

當然,事情到這裡還沒有結束。還是以上邊的數據為例,假如我們的2d image很不幸,是一個mip-map的texture,lod 0的image大小是512x512,這個沒問題,4個image sparse block就可以搞定了。lod 1的是256x256,一個image sparse block剛好放得下。lod 2的image大小變成了128x128,放到一個image sparse block中似乎有點兒浪費資源,不過還好,也不算浪費太多。接下來是lod 3的64x64,lod 4的32x32,lod 5的16x16,一直到最後的1x1。從這裡可以看出,假如為每個mip level都至少分配一個image sparse block的話,內存空間的浪費將是不可避免的,這與本來就致力於解決存儲空間不足的sparse特性相矛盾。故而,這裡又提出了一個mip-tail region的概念。那麼,什麼叫做mip-tail region呢?所謂的mip-tail region,就是將一個完整的mip-map資源中,從某些mip-level開始,往下所有的mip層都搜集到一個區域,叫做mip-tail區域。這個mip-tail區域的大小是image sparse block大小的整數倍,但是顯然不應該有形狀的約束,畢竟是多個mip-level的集合。並且,memory的foot print在mip-tail區域內部是由實現自行決定的。當綁定mip-map的image時,mip-tail區域必須被全部綁定。

在mip-tail的定義中,我提到了「某些mip-level開始」,那麼究竟從哪一個mip-level開始呢?對於上述的例子,顯然從lod 2開始是最合適的。然而,如果image sparse本身是768*768的呢?這樣lod 1開始就是384*384,一個image sparse block放不下,四個image sparse block又有點兒多餘。故而,vulkan在這裡提到了一個aligned-mip-size的概念。如果設定為aligned-mip-size的話,那麼mip-tail region就從第一個不對齊於image sparse block開始。如果沒有設定aligned-mip-size的話,那麼mip-tail region就從第一個小於image sparse block的mip-level開始。

aligned-mip-size體現在哪裡呢?第一個體現就是在VkPhysicalDeviceSparseProperties(3)最後一個我們還沒有提到的residencyAlignedMipSize部分,它決定了是否可能是aligned-mip-size的。如果這個值是false,則所有的mip-tail region都從第一個小於image sparse block的mip-level開始,沒得商量。如果這個值是true,則要具體到每一個資源,來看看是不是從不對齊的那一個開始,這個類似於總控開關。具體到每一個資源的時候,資源查出來的VkSparseImageFormatProperties(3)中的flags位不僅僅有表示是否採用「標準」image sparse block的掩碼位,還有一個叫做VK_SPARSE_IMAGE_FORMAT_ALIGNED_MIP_SIZE_BIT的掩碼位,表示這個資源採用哪一種策略來組織mip-tail區域。

VkSparseImageFormatProperties(3)中的flags總共有三個掩碼位,目前我們已經講過其中的兩個了,還有最後一個,VK_SPARSE_IMAGE_FORMAT_SINGLE_MIPTAIL_BIT,這個又是幹什麼的呢?這個實際上是為了array的resource做的優化。在array的resource中,如果每一個array都有自己單獨的mip-tail region,很可能同樣造成資源的浪費,我們可以將所有array slice的mip-tail region組織在一起,形成一個單獨的mip-tail region,這就是名字里的single-miptail的含義了。

之前我們提過,sparse residency的image只要求支持color format。如果實現同時決定支持depth/stencil的format的話,還會帶來一點兒麻煩的地方。這個麻煩就在於,depth/stencil的layout可以是seperate的,也可以是interleaved的。我們都知道,depth和stencil很多時候都是作為整體出現,例如D24S8格式,depth用24位,剩下的8位當作stencil,剛剛好32位。然而,具體實現的時候,可能由於某些原因,有些實現寧願浪費一些資源,也要將depth和stencil分開放,depth用32位保存,只用到其中的24位,剩下的8位不要了。此外,stencil單獨保存。如果大家都這麼做的話也還好,麻煩就麻煩在某些實現還是將depth和stencil交織地擺放在一起。在支持sparse residency的時候,vulkan同時也要考慮到對這些支持的可能性,因此提出了aspect的概念。

aspect,就是指format中的一個方面。這個並不是說一個R8G8B8A8就分別有RGBA四個aspect,而是這個作為一個color的format,只有一個aspect,叫做color aspect。D24S8顯然擁有兩個aspect,叫做depth aspect和stencil aspect。此外,image可能有一些meta信息要保留在memory中,這些在作partial residency的時候也要全部綁定的,所以還有一個叫做metadata的aspect。在我們使用vkGetImageSparseMemoryRequirements(3)來查詢一個具體的image對於memory layout的要求的時候,這些aspect可能會被分別報出來,故而app需要認真對待這些細節,做到正確地綁定。

Sparse Aliased

sparse aliased就是多個buffer/image,或者一個buffer/image內部的不同區域,映射到同一個物理存儲區域上。sparse aliased限制了aliasing只能發生在buffer和buffer之間,image和image之間。並且,如果image和image之間alias,則要保證format兼容,並且要使用同樣的image sparse block形狀。此外,由於mip-tail region和metadata aspect是opaque的,這兩個部分是不能alias的,否則會出現為定義的行為。

API

到目前為止,我們已經講完了所有sparse resource相關的概念了。然而如何使用sparse resource,如何做binding,我們還沒有提及。在這裡,我們將會接觸到這一部分,真正地把一個sparse resource和memory(s)綁定到一起。

不同於普通的vkBindBufferMemory(3)和vkBindImageMemory(3),將sparse的buffer/image綁定到memory(s)上是通過queue來執行的。而且這個queue於普通的 graphic/compute/transfer queue不一樣,必須支持sparse。此外,往sparse queue上提交任務也並不是像graphic queue一樣,錄製命令到command buffer再提交,而是通過一個vkQueueBindSparse(3) 的命令。這個命令的核心是一個VkBindSparseInfo(3)的數組,其摘要如下:

typedef struct VkBindSparseInfo {n VkStructureType sType;n const void* pNext;n uint32_t waitSemaphoreCount;n const VkSemaphore* pWaitSemaphores;n uint32_t bufferBindCount;n const VkSparseBufferMemoryBindInfo* pBufferBinds;n uint32_t imageOpaqueBindCount;n const VkSparseImageOpaqueMemoryBindInfo* pImageOpaqueBinds;n uint32_t imageBindCount;n const VkSparseImageMemoryBindInfo* pImageBinds;n uint32_t signalSemaphoreCount;n const VkSemaphore* pSignalSemaphores;n} VkBindSparseInfo;n

除了分別定義等待的semaphore,以及完成後的semaphore外,核心部分是三個數組,數組類型分別是VkSparseBufferMemoryBindInfo(3),VkSparseImageOpaqueMemoryBindInfo(3)和VkSparseImageMemoryBindInfo(3)。

VkSparseBufferMemoryBindInfo(3)用於綁定sparse的buffer,無論是fully-resident還是partial-resident的都是用這個數據結構來實現的。數據結構本身來說也很簡單,不再多說。

VkSparseImageOpaqueMemoryBindInfo(3)用於綁定opaque的image,主要用於綁定fully-resident的image,或者partial-resident的image中的mip-tail region和meta-data aspect。其特點是所需要綁定的部分由於沒有一個well-defined的mapping關係,故而需要全部綁定。這也是名字中的opaque的含義所在。除此之外,如果一個partial-residency的image,某一個mip-level的image需要全部綁定,也可以用這個來實現。

VkSparseImageMemoryBindInfo(3)用於綁定partial-resident的image的子區域,因此這裡定義了比較多的參數用於指定綁定的子區域位置以及其他信息。

在vkQueueBindSparse(3) 中,實際提交了一系列的綁定操作。這些綁定操作按照數組中的順序開始執行,但是可能會亂序結束。如果要保證順序,需要使用同步原語。


推薦閱讀:

Dota 2 現已支持 Vulkan API,使用起來究竟怎麼樣?
超級瑪麗現已加入 Vulkan 豪華午餐
vulkan中的同步和緩存控制
為什麼部分 PC 遊戲開發者更喜歡 DirectX 而非 OpenGL ?

TAG:Vulkan |