Vulkan API架構及詳述
一、API架構
下圖是Vulkan中主要的組件以及它們之間的關係:
1.1 Device
Device很好理解,一個Device就代表着一個你係統中的物理GPU。它的功能除了讓你可以選擇用來渲染(或者計算)的GPU以外,主要功能就是爲你提供其他GPU上的資源,例如所有要用到顯存的資源,以及接下來會提到的Queue和Synchronization等組件。
1.2 Pipeline
第二個主要的組件,比Device複雜很多,就是Pipeline。一個Pipeline包含了傳統API中大部分的狀態和設定。只不過Pipeline是需要事先創建好的,這樣所有的狀態組合的驗證和編譯都可以在初始化的時候完成,運行時不會再因爲這些操作有任何性能上的浪費。但正是因爲這一點,如果你不同的Pass需要不同的狀態,你需要預先創造多個不同的Pipeline。然而我們不能把所有渲染需要的信息全都prebake進pipeline中,一個Pipeline應該是可以通過綁定不同的資源而複用的。接下來介紹的幾個組件就可以被動態的綁定給任何Pipeline。
1.3 Buffer
接下來是Buffer。Buffer是所有我們所熟悉的Vertex Buffer, Index Buffer, Uniform Buffer等等的統稱。而且一個Buffer的用途非常多樣。在Vulkan中需要特別注意Buffer是從什麼類型的內存中分配的,有的類型CPU可以訪問,有的則不行。有的類型會在CPU上被緩存。現在這些內存的類型是重要的功能屬性,不再只是對驅動的一個提示了。
1.4 Image
Image在Vulkan中代表所有具有像素結構的數組,可以用於表示紋理,Render Target等等。和其他組件一樣,Image也需要在創建的時候指定使用它的模式,例如Vulkan裏有參數指定Image的內存Layout,可以是Linear,也可以是Tiled Linear便於紋理Filter。如果把一個Linear layout的Image當做紋理使用,在某些平臺上可能導致嚴重的性能損失。類似傳統的API,紋理本身並不直接綁定給Pipeline。需要讀取和使用Image則要依賴於ImageView。
1.5 Binding Resources
講了幾種不同類型的內存, 但是內存是從什麼地方分配的呢?在Vulkan中,所有內存都分配與一個指定的Heap。一個Device也許支持幾種不同類型的Heap,有些也許可以分配Mappable的內存,有些不行。具體的類型取決於程序運行的平臺。值得注意的是,Vulkan Heap分配的內存和最終的Vulkan組件例如Buffer和Image直接可以不,也不應該是一對一的映射。一段內存可以分配成數段,並且分配給不同的資源使用。某種程度上這樣的資源複用也是Vulkan基本的設計哲學之一。
上面提到,Buffer和Image可以動態的綁定給任意Pipeline。而具體綁定的規則就是由Descriptor指定。和其他組件一樣,Descriptor Set也需要在被創建的時候,就由App指定它的固定的Layout,以減少渲染時候的計算量。Descriptor Set Layout可以指定綁定在指定Descriptor Set上的所有資源的種類和數量,以及在Shader中訪問它們的索引。App可以定義多個不同的Descriptor Set Layout,所以如何爲你的程序或者引擎設計Descriptor Set的Layout將是優化的重要一環。當然,程序也可以擁有多個指定Layout的Descriptor Set。因爲Descriptor Set是預先創建並且無法更改的,所以改變一個綁定的資源需要重新創建整個Descriptor Set,但改變一個資源的Offset可以非常快速的在綁定Descriptor Set的時候完成。一會我會討論如何利用這一點來實現高效的資源更新。
1.6 Command Buffer
介紹了那麼多組件,都是渲染需要的數據。那麼Command Buffer就是渲染本身所需要的行爲。在Vulkan裏,沒有任何API允許你直接的,立即的像GPU發出任何命令。所有的命令,包括渲染的Draw Call,計算的調用,甚至內存的操作例如資源的拷貝,都需要通過App自己創建的Command Buffer。Vulkan對於Command Buffer有特有的Flag,讓程序制定這些Command只會被調用一次(例如某些資源的初始化),亦或者應該被緩存從而重複調用多次(例如渲染循環中的某個Pass)。另一個值得注意的是,爲了讓驅動能更加簡易的優化這些Command的調用,沒有任何渲染狀態會在Command Buffer之間繼承下來。每一個Command Buffer都需要顯式的綁定它所需要的所有渲染狀態,Shader,和Descriptor Set等等。這和傳統API中,只要你不改某個狀態,某個狀態就一直不會變,這一點很不一樣。
1.7 Command Buffer Pool
Command Buffer Pool是一個需要注意的多線程相關的組件。它是Command Buffer的父親組件,負責分配Command Buffer。Command Buffer相關的操作會對其對應的Command Buffer Pool裏造成一定的工作,例如內存分配和釋放等等。因爲多個線程會並行的進行Command Buffer相關的操作,這個時候如果所有的Command Buffer都來自同一個Command Buffer Pool的話,這時Command Buffer Pool內的操作一定要在線程間被同步。所以這裏建議每個線程都有自己的Command Buffer Pool,這樣每個線程纔可以任意的做任何Command Buffer相關的操作。
Command Buffer Pool的另一個性質就是支持非常高效的重置。一旦重置,所有由當前Pool分配的Command Buffer都會被清零,並且不會有任何內存管理上的碎片。所以程序只要爲每一個幀和線程的組合分配一個Command Buffer Pool,就可以利用這一點,在更新Round Robin中的Command Buffer時非常快速的將需要的Buffer清零。
1.8 Descriptor Pool
另一個類似Command Buffer Pool的組件,就是Descriptor Pool。所有Descriptor Set都由Descriptor Pool分配,Descriptor Set操作會導致對應的Descriptor Pool工作而且需要線程間同步,並且Descriptor Pool也支持非常高效的將所有由當前Pool分配的Descriptor Set一次性清零。所以程序應該爲每個線程分配一個Descriptor Pool,可以根據Descriptor Set的更新頻率,創建不同的Descriptor Pool,例如每幀、每場景等等。
1.9 Queue
最後一個關鍵組件, Queue,是Vulkan中唯一給GPU遞交任務的渠道。Vulkan將Queue設計成了完全透明的對象,所以在驅動裏沒有任何其他的隱藏Queue,也不會有任何的Synchronization發生。在Vulkan中,給GPU遞交任務不再依賴於任何所謂的綁定在單一線程上的Context,Queue的API極其簡單,你向它遞交任務(Command Buffer),然後如果有需要的話,你可以等待當前Queue中的任務完成。這些Synchronization操作是由Vulkan提供的各種同步組件完成的。例如Samaphore可以讓你同步Queue內部的任務,程序無法干預。Fence和Event則可以讓程序知道某個Queue中指定的任務已經完成。所有這些組件組合起來,使得基於Command Buffer和Queue遞交任務的Vulkan非常易於編寫多線程程序。後文會簡單討論一些常見的多線程模式。最後,和前面提到的一樣,Queue不光接收圖形渲染的調用,也接受計算調用和內存操作。
二、相關API簡述
2.1 設備初始化
2.1.1 Instance --> GPU --> Device
Instance表示具體的Vulkan應用。在一個應用程序中可以創建多個實例,這些實例之間相互獨立,互不干擾。
當調用API創建Vulkan實例的時候,Vulkan SDK內部會經由驅動裝載器(loader)查找可用的GPU設備。
創建Vulkan實例需要兩個輸入信息:
- 應用程序的信息
- 內存分配回調函數
Vulkan通過用戶輸入的內存分配器來分配內存。
創建好Instance,就可以用Instance枚舉所有可用的Vulkan GPU設備。
有了GPU設備,就可以獲取具體GPU的信息。如果系統中安裝了多個GPU設備,就可用GPU信息比較GPU設備之間的兼容性等。
找到了合適的GPU後,就可以通過GPU創建設備示例。
2.2、繪製
2.2.1 Queues
有了設備,就可以創建命令隊列。命令隊列是與設備綁定的,不能跨設備使用。
隊列封裝了圖形、計算、直接內存訪問功能,獨立調度、異步等調度操作。
2.2.2 Command Buffer
有了設備,就可以創建命令緩衝。命令緩衝是繪製命令的批次集合。
用戶可創建任意多的命令緩衝,支持在多線程中創建。
Command佔用的內存是通過Command Buffer內存池動態分配,不需要指定內存池的大小。
2.2.3 Command
在命令緩衝區中可以創建多個命令。多個命令完成批次即命令緩衝後,可以重複利用。
這裏有點像OpenGL裏面的NameList。
Command Buffer的操作使用pipeline barrier區分。barrier可以等待和觸發事件。
注:
- 這裏可以看出Command被包裝在Command Buffer中,當把Command
Buffer提交給Queue中後,Queue中執行的是Command Buffer中的Command。 - Command Buffer和Queue的類別需要匹配,否則不能正確執行,但一個Command
Buffer並不會跟任何Queue有聯繫。也就意味着,一個Command Buffer可以被提交給多個Queue,只要他們的類別匹配。
2.2.4 Shaders
使用設備創建Shader。
同樣支持多線程。
2.2.5 Pipeline
渲染管線同樣需要設備創建。
渲染管線狀態包括:Shaders,混合、深度、剔除、模版狀態等。
另外Vulkan提供了API保存和加載渲染管線的狀態。
2.2.6 Descriptors
Vulkan資源都用descriptor表示, descriptor分成descriptor set,descriptor set從descriptor pool分配。
每個descriptor set都有個layout佈局,佈局是在pipeline創建的時候確定的。layout在descriptor set 和pipeline之間共享,並且必須匹配。
pipeline可以切換使用相同layout的descriptor set。
多個不同layout的set可以組成鏈在一個pipeline中使用。
2.2.7 Render Pass
Render Pass表示一幀的某個階段,包含了繪製過程中的很多信息,包括:
- Layout和framebuffer attachment的類型
-Render Pass在開始和結束的時候該做什麼
-Render Pass影響framebuffer的區域-分塊渲染和延遲渲染需要的信息
2.2.8 Drawing
Draws位於Render Pass內,在Command Buffer的上下文中執行。支持多種繪製類型:基於索引的和非索引的,直接的和間接的等
3.1、多線程支持
3.1.1 同步
使用事件同步任務,可以設置、重置、查詢和等待事件。
Command Buffer執行完成後可以觸發事件
3.1.2 任務隊列
任務在設備所屬的隊列中執行,準備好的Command buffer送到隊列中執行。
隊列保留內存,驅動不負責管理內存,同樣由程序管理。
隊列可以觸發事件,等待信號量。
4.1、呈現
4.1.1 Presentation
展現就是如何在屏幕上顯示圖片。
可顯示的資源使用與framebuffer綁定的“圖片”展現,由與平臺相關的模塊創建,即所謂的窗口系統接口WSI。
WSI用了列舉系統的顯示設備和視頻模式,全屏,控制顯示垂直同步等。
Presenta跟命令緩衝一起進入隊列。
5.1、資源創建和釋放
資源包括CPU資源和GPU資源。
CPU資源通過vkCreate創建,GPU資源由vkAllocMemory創建,由vkBindObjectMemory與CPU對象綁定。
應用程序負責Vulkan對象的析構,需要保證順序。Vuilkan資源沒有引用計數機制,都需要顯式釋放。