
1. Overview of Vulkan
1.1 計算機圖形軟件
圖形軟件有兩個大類:專用軟件包(special-purpose packages)和通用編程軟件包(general programming packages)。
專用軟件包通常提供一種UI設計語言,讓用戶直接生成想要的圖形,不用關心內部實現。這類軟件例子是PS、CAD等等。
相反,通用編程軟件包提供一個可使用C、C++或Java等高級語言編程的圖形函數庫。圖形函數庫中提供幾何圖元、矩陣變換等操作,提供了間接操作硬件的軟件接口,所以這組圖形函數又被稱為計算機圖形應用編程接口(computer-graphics application programming interface,CG API)。OpenGL、Vulkan、DirectX、Metal皆在此列。
1.2 Vulkan多線程的設計理念
Vulkan不僅僅是圖形(graphics)API,而是一個面向圖形和計算的編程接口(graphics and compute)。支持Vulkan的設備可以是GPU,也可以是DSP或者固定功能的硬件。
Vulkan中的計算模型主要基于并行計算,因此支持多線程是Vulkan設計的核心理念之一。
為了較少Vulkan內部因為互斥同步等操作造成的卡頓問題,Vulkan內部默認認為對任何資源的訪問不存在多線程競爭,所有的資源同步操作由應用開發者去負責,因為對資源的訪問和使用沒有人比應用開發者自己更加清楚。Vulkan稱之為外部同步(external synchronization)。
因為這個原因,資源管理和線程同步工作成為編寫Vulkan程序的最大難點之一。想要讓Vulkan多線程正常運行,你需要做大量的工作。當然,換來的是Vulkan有了更加干凈的線程模型以及比其它CG API高得多的性能。

1.3. Instances, Devices, and Queues
在正式研究Vulkan多線程之前,有三個重要的基礎概念需要了解—Instances, Devices, and Queues。
Instances可以看做是應用的子系統,從邏輯上把Vulkan與應用程序上下文中的其他邏輯隔開。Instances可以看做是Vulkan的上下文,它會跟蹤所有狀態,從邏輯上把所有支持Vulkan的設備整合在一起。
Devices有兩個概念:Physical devices和Logical device。
Physical devices通常代表一個或者多個支持Vulkan的硬件設備,這些設備具有特定功能,可以提供一系列Queues。圖形顯卡、加速器、DSP等都可以是Vulkan的Physical devices。
Logical device是Physical devices的軟件抽象,用于預訂一些硬件資源。
Queues可以理解為一個“GPU線程”,它是實現Vulkan多線程的關鍵元素之一,用于響應應用的請求,大部分時間,應用都在與其交互。
Vulkan功能的層次結構圖如下:

2. Queues and Command Buffer
2.1 Queues
Queue代表一個GPU線程,Vulkan設備執行的就是提交到Queues中的工作。物理設備中Queue可能不止一個,每一個Queue都被包含在Queue Families中。
Queue Families是一個有相同功能的Queues的集合,它們的性能水平和對系統資源的訪問是相同的,并且在它們之間數據傳輸工作沒有任何成本(同步之外)。
一個物理設備中可以存在多個Queue Families,不同的Queue Families有不同的特性。相同Queue Families中的Queues的功能相同,并且可以并行運行。
按照Queue的能力,可以將其劃分為:
- Graphics(圖形)
- 該系列中的Queues支持圖形操作,例如繪制點,線和三角形。
- Compute(計算)
- 該系列中的Queues支持諸如computer shader之類的計算操作。
- Transfer(傳輸,拷貝)
- 該系列中的Queues支持傳輸操作,例如復制緩沖區和圖像內容。
- Sparse binding(稀疏綁定)
- 該系列中的隊列支持用于更新稀疏資源(sparse resource)的內存綁定操作。

2.2 Command Buffer
2.2.1 單線程的性能瓶頸
傳統CG API是單線程的,性能的提升只能依賴于CPU主頻的提高。能有的優化方案也不外乎主線程和渲染線程分開,或者某些資源的異步加載、離線處理。

但是在實際應用中我們還是經常遇到傳統CG API導致的性能瓶頸。
以手機終端為例,CPU主頻提升有限,各大芯片廠商開始向多核多線程發展,考慮到功耗溫控問題,又不能把CPU頻率升的太高,越來越高的刷新率對實時渲染的速度要求越來越苛刻。

Vulkan為了充分發揮CPU多核多線程的作用,引入了command buffer的概念。多個線程可以同時協作,每個CPU線程都可以往自己的command buffer中提交渲染命令,然后統一提交到對應的Queue中,大大提高了CPU的利用率。

2.2.2 Command Buffer的作用
應用在繪制時會提交一系列繪制命令給GPU驅動,但是這些繪制命令不會立刻被執行,而是被簡單的添加到Command Buffer的末尾。
在其他CG APIs中,驅動程序在應用不感知的情況下,把API調用翻譯成GPU command并儲存在command buffer中,最終提交給GPU處理。command buffer的創建和銷毀都由驅動負責。
在Vulkan中,你需要自己從Command Buffer Pool中申請command buffer,將想要記錄的命令放入command buffer中。
Command Buffer Pool:

2.2.3 Recording command
Command Buffer可以記錄(Record)很多命令,比如:設置狀態、繪制操作、數據拷貝...


理論上,一個線程可以把Command記錄到多個Command Buffer中,多個線程也可以共享同一個Command Buffer,但是一般不鼓勵多個線程共享一個Command Buffer。
Vulkan的關鍵設計原則之一就是做到高效的多線程。想實現這一點,應用程序要注意因為資源競爭導致的多線程彼此阻塞。因此,每個線程最好有一個或者對個Command Buffer,不要嘗試共享一個。另外,Command Buffer由Command Buffer Pool分配,應用可以為每一個線程創建一個Command Buffer Pool,讓各個工作線程從Command Buffer Pool中分配Command Buffer,無需參與競爭。

2.2.4 Submitting Command Buffers
提交過程使用示意圖更加好理解一點。
單線程Command Buffer提交過程:



多線程Command Buffer提交過程:


整體流程如下:

3. Synchronization
3.1 顯示同步操作
Vulkan把同步的操作交給了應用(external synchronization),絕大多數的Vulkan命令根本不提供同步,需要應用自己負責。Vulkan給應用提供了同步原語,幫助應用進行同步操作。
Vulkan中主要有四種同步原語(synchronization primitives):
- Fences
- 最大顆粒度的同步原語,目的是給CPU端提供一種方法,可以知道GPU或者其他Vulkan Device什么時候把提交的工作全部做完。
- 如果你熟悉Android顯示機制的話,acquire fence或者retire fence就是類似的作用
- Semaphores
- 顆粒度比Fences更小一點,通常用于不同Queue之間的數據同步操作
- Events
- 顆粒度更小,可以用于Command Buffer之間的同步工作
- Barriers
- Vulkan流水線(Pipeline)階段內用于內存訪問管理和資源狀態移動的同步機制
下面這張圖取自NVIDIA公司Vulkan 多線程講解的PPT:

3.2 隱藏的執行順序
Vulkan是顯式的API沒錯,號稱是“沒有秘密的API”。但是在多線程同步時,還是存在一些潛規則。
以下面這張圖為例,同一個Queue中,Command Buffer1 和Command Buffer2 誰先執行?Command Buffer中記錄的一堆命令是如何執行的?

Vulkan的執行順序其實是有一定的潛規則的,在沒有同步原語的情況下:
- Command Buffer中的Command,先記錄的先執行
- 先提交的Command Buffer先執行
- 同一個Queue中,一起提交的Command Buffer1 和Command Buffer2 按照下標的順序執行,Command Buffer1 先執行
3.3 Barriers
所有的同步原語中,Barriers使用起來最為困難。Barriers用于顯式的控制buffer或者image的訪問范圍,避免hazards(RaW,WaR,and WaW),保證數據一致性。
Barriers需要開發者了解渲染管線的各個階段,能清晰的把握管線中每個步驟對資源的讀寫順序。
Vulkan中將Pipeline的各個階段定義為:
- TOP_OF_PIPE_BIT
- DRAW_INDIRECT_BIT
- VERTEX_INPUT_BIT
- VERTEX_SHADER_BIT
- TESSELLATION_CONTROL_SHADER_BIT
- TESSELLATION_EVALUATION_SHADER_BIT
- GEOMETRY_SHADER_BIT
- FRAGMENT_SHADER_BIT
- EARLY_FRAGMENT_TESTS_BIT
- LATE_FRAGMENT_TESTS_BIT
- COLOR_ATTACHMENT_OUTPUT_BIT
- TRANSFER_BIT
- COMPUTE_SHADER_BIT
- BOTTOM_OF_PIPE_BIT
對應:

假設我們有個兩個渲染管線P1 和 P2,P1會通過Vertex Shader往buffer寫入頂點數據,P2需要在Compute Shader中使用這些數據。
如果使用fence去同步,你的流程應該是這樣:P1的Command提交后,P2通過fence確保P1的操作已經被全部執行完,再開始工作。

但是這種大顆粒度的同步操作無疑造成了耗時操作:P1的數據在Vertex Shader階段就已經準備好了,我們為什么要等到它所有操作執行完再開始?P2平白多等待了很長時間,而且在這個期間P2的其他階段并沒有使用到P1的數據,也是可以執行的啊。
Barriers的引入完全解決了這個問題,我們只需要告訴Vulkan,我們在P2的Compute Shader階段才會等待P1 Vertex Shader里面的數據,其他階段并不關心,可以同步進行。

使用方法:

參考文檔:
- Vulkan Overview
- Android and Vulkan - GDD China.pdf
- Vulkan Programming Guide
- Vulkan Cookbook
- Learning Vulkan
- Vulkan Multi-Threading
- Vulkan中的同步機制
- Vulkan? 1.1.148 - A Specification
- VULKAN BARRIERS EXPLAINED
- vulkan中的同步和緩存控制之二,barrier和event
- Xinzhao:vulkan中的同步和緩存控制之一,fence和semaphore
本系列文章匯總:
- Vulkan 簡介
- Vulkan 多線程渲染
- Vulkan 內存管理
- Vulkan 繪制與顯示
- Vulkan 資源綁定和狀態管理