Vulkan 多線程渲染

1. Overview of Vulkan

1.1 計(jì)算機(jī)圖形軟件

圖形軟件有兩個(gè)大類:專用軟件包(special-purpose packages)和通用編程軟件包(general program-
ming packages)。

專用軟件包通常提供一種UI設(shè)計(jì)語(yǔ)言,讓用戶直接生成想要的圖形,不用關(guān)心內(nèi)部實(shí)現(xiàn)。這類軟件例子是PS、CAD等等。

相反,通用編程軟件包提供一個(gè)可使用C、C++或Java等高級(jí)語(yǔ)言編程的圖形函數(shù)庫(kù)。圖形函數(shù)庫(kù)中提供幾何圖元、矩陣變換等操作,提供了間接操作硬件的軟件接口,所以這組圖形函數(shù)又被稱為計(jì)算機(jī)圖形應(yīng)用編程接口(computer-graphics application programming interface,CG API)。OpenGL、Vulkan、DirectX、Metal皆在此列。

1.2 Vulkan多線程的設(shè)計(jì)理念

Vulkan不僅僅是圖形(graphics)API,而是一個(gè)面向圖形和計(jì)算的編程接口(graphics and compute)。支持Vulkan的設(shè)備可以是GPU,也可以是DSP或者固定功能的硬件。

Vulkan中的計(jì)算模型主要基于并行計(jì)算,因此支持多線程Vulkan設(shè)計(jì)的核心理念之一。

為了較少Vulkan內(nèi)部因?yàn)榛コ馔降炔僮髟斐傻目D問(wèn)題,Vulkan內(nèi)部默認(rèn)認(rèn)為對(duì)任何資源的訪問(wèn)不存在多線程競(jìng)爭(zhēng),所有的資源同步操作由應(yīng)用開(kāi)發(fā)者去負(fù)責(zé),因?yàn)閷?duì)資源的訪問(wèn)和使用沒(méi)有人比應(yīng)用開(kāi)發(fā)者自己更加清楚。Vulkan稱之為外部同步(external synchronization)。

因?yàn)檫@個(gè)原因,資源管理和線程同步工作成為編寫(xiě)Vulkan程序的最大難點(diǎn)之一。想要讓Vulkan多線程正常運(yùn)行,你需要做大量的工作。當(dāng)然,換來(lái)的是Vulkan有了更加干凈的線程模型以及比其它CG API高得多的性能。

image-20200730105359314.png

1.3. Instances, Devices, and Queues

在正式研究Vulkan多線程之前,有三個(gè)重要的基礎(chǔ)概念需要了解—Instances, Devices, and Queues。

Instances可以看做是應(yīng)用的子系統(tǒng),從邏輯上把Vulkan與應(yīng)用程序上下文中的其他邏輯隔開(kāi)。Instances可以看做是Vulkan的上下文,它會(huì)跟蹤所有狀態(tài),從邏輯上把所有支持Vulkan的設(shè)備整合在一起。

Devices有兩個(gè)概念:Physical devices和Logical device。

Physical devices通常代表一個(gè)或者多個(gè)支持Vulkan的硬件設(shè)備,這些設(shè)備具有特定功能,可以提供一系列Queues。圖形顯卡、加速器、DSP等都可以是Vulkan的Physical devices。

Logical device是Physical devices的軟件抽象,用于預(yù)訂一些硬件資源。

Queues可以理解為一個(gè)“GPU線程”,它是實(shí)現(xiàn)Vulkan多線程的關(guān)鍵元素之一,用于響應(yīng)應(yīng)用的請(qǐng)求,大部分時(shí)間,應(yīng)用都在與其交互。

Vulkan功能的層次結(jié)構(gòu)圖如下:

image-20200730105801733.png

2. Queues and Command Buffer

2.1 Queues

Queue代表一個(gè)GPU線程,Vulkan設(shè)備執(zhí)行的就是提交到Queues中的工作。物理設(shè)備中Queue可能不止一個(gè),每一個(gè)Queue都被包含在Queue Families中。

Queue Families是一個(gè)有相同功能的Queues的集合,它們的性能水平和對(duì)系統(tǒng)資源的訪問(wèn)是相同的,并且在它們之間數(shù)據(jù)傳輸工作沒(méi)有任何成本(同步之外)。

一個(gè)物理設(shè)備中可以存在多個(gè)Queue Families,不同的Queue Families有不同的特性。相同Queue Families中的Queues的功能相同,并且可以并行運(yùn)行。

按照Queue的能力,可以將其劃分為:

  • Graphics(圖形)
    • 該系列中的Queues支持圖形操作,例如繪制點(diǎn),線和三角形。
  • Compute(計(jì)算)
    • 該系列中的Queues支持諸如computer shader之類的計(jì)算操作。
  • Transfer(傳輸,拷貝)
    • 該系列中的Queues支持傳輸操作,例如復(fù)制緩沖區(qū)和圖像內(nèi)容。
  • Sparse binding(稀疏綁定)
    • 該系列中的隊(duì)列支持用于更新稀疏資源(sparse resource)的內(nèi)存綁定操作。
image-20200730112126405.png

2.2 Command Buffer

2.2.1 單線程的性能瓶頸

傳統(tǒng)CG API是單線程的,性能的提升只能依賴于CPU主頻的提高。能有的優(yōu)化方案也不外乎主線程和渲染線程分開(kāi),或者某些資源的異步加載、離線處理。

image-20200730114946379.png

但是在實(shí)際應(yīng)用中我們還是經(jīng)常遇到傳統(tǒng)CG API導(dǎo)致的性能瓶頸。

以手機(jī)終端為例,CPU主頻提升有限,各大芯片廠商開(kāi)始向多核多線程發(fā)展,考慮到功耗溫控問(wèn)題,又不能把CPU頻率升的太高,越來(lái)越高的刷新率對(duì)實(shí)時(shí)渲染的速度要求越來(lái)越苛刻。

image-20200730115516772.png

Vulkan為了充分發(fā)揮CPU多核多線程的作用,引入了command buffer的概念。多個(gè)線程可以同時(shí)協(xié)作,每個(gè)CPU線程都可以往自己的command buffer中提交渲染命令,然后統(tǒng)一提交到對(duì)應(yīng)的Queue中,大大提高了CPU的利用率。

image-20200730115609709.png

2.2.2 Command Buffer的作用

應(yīng)用在繪制時(shí)會(huì)提交一系列繪制命令給GPU驅(qū)動(dòng),但是這些繪制命令不會(huì)立刻被執(zhí)行,而是被簡(jiǎn)單的添加到Command Buffer的末尾。

在其他CG APIs中,驅(qū)動(dòng)程序在應(yīng)用不感知的情況下,把API調(diào)用翻譯成GPU command并儲(chǔ)存在command buffer中,最終提交給GPU處理。command buffer的創(chuàng)建和銷毀都由驅(qū)動(dòng)負(fù)責(zé)。

在Vulkan中,你需要自己從Command Buffer Pool中申請(qǐng)command buffer,將想要記錄的命令放入command buffer中。

Command Buffer Pool:

image-20200730142932026.png

2.2.3 Recording command

Command Buffer可以記錄(Record)很多命令,比如:設(shè)置狀態(tài)、繪制操作、數(shù)據(jù)拷貝...

image-20200730142958943.png
image-20200730142302468.png

理論上,一個(gè)線程可以把Command記錄到多個(gè)Command Buffer中,多個(gè)線程也可以共享同一個(gè)Command Buffer,但是一般不鼓勵(lì)多個(gè)線程共享一個(gè)Command Buffer。

Vulkan的關(guān)鍵設(shè)計(jì)原則之一就是做到高效的多線程。想實(shí)現(xiàn)這一點(diǎn),應(yīng)用程序要注意因?yàn)橘Y源競(jìng)爭(zhēng)導(dǎo)致的多線程彼此阻塞。因此,每個(gè)線程最好有一個(gè)或者對(duì)個(gè)Command Buffer,不要嘗試共享一個(gè)。另外,Command Buffer由Command Buffer Pool分配,應(yīng)用可以為每一個(gè)線程創(chuàng)建一個(gè)Command Buffer Pool,讓各個(gè)工作線程從Command Buffer Pool中分配Command Buffer,無(wú)需參與競(jìng)爭(zhēng)。

image-20200730144149412.png

2.2.4 Submitting Command Buffers

提交過(guò)程使用示意圖更加好理解一點(diǎn)。

單線程Command Buffer提交過(guò)程
submit cb1.PNG
submit cb5.PNG
submit cb2.PNG
多線程Command Buffer提交過(guò)程
submit cb3.PNG
submit cb4.PNG
整體流程如下
image-20200730144906368.png

3. Synchronization

3.1 顯示同步操作

Vulkan把同步的操作交給了應(yīng)用(external synchronization),絕大多數(shù)的Vulkan命令根本不提供同步,需要應(yīng)用自己負(fù)責(zé)。Vulkan給應(yīng)用提供了同步原語(yǔ),幫助應(yīng)用進(jìn)行同步操作。

Vulkan中主要有四種同步原語(yǔ)(synchronization primitives):

  • Fences
    • 最大顆粒度的同步原語(yǔ),目的是給CPU端提供一種方法,可以知道GPU或者其他Vulkan Device什么時(shí)候把提交的工作全部做完。
    • 如果你熟悉Android顯示機(jī)制的話,acquire fence或者retire fence就是類似的作用
  • Semaphores
    • 顆粒度比Fences更小一點(diǎn),通常用于不同Queue之間的數(shù)據(jù)同步操作
  • Events
    • 顆粒度更小,可以用于Command Buffer之間的同步工作
  • Barriers
    • Vulkan流水線(Pipeline)階段內(nèi)用于內(nèi)存訪問(wèn)管理和資源狀態(tài)移動(dòng)的同步機(jī)制

下面這張圖取自NVIDIA公司Vulkan 多線程講解的PPT:

image-20200730145823767.png

3.2 隱藏的執(zhí)行順序

Vulkan是顯式的API沒(méi)錯(cuò),號(hào)稱是“沒(méi)有秘密的API”。但是在多線程同步時(shí),還是存在一些潛規(guī)則。

以下面這張圖為例,同一個(gè)Queue中,Command Buffer1 和Command Buffer2 誰(shuí)先執(zhí)行?Command Buffer中記錄的一堆命令是如何執(zhí)行的?

image-20200730144906368.png

Vulkan的執(zhí)行順序其實(shí)是有一定的潛規(guī)則的,在沒(méi)有同步原語(yǔ)的情況下:

  • Command Buffer中的Command,先記錄的先執(zhí)行
  • 先提交的Command Buffer先執(zhí)行
  • 同一個(gè)Queue中,一起提交的Command Buffer1 和Command Buffer2 按照下標(biāo)的順序執(zhí)行,Command Buffer1 先執(zhí)行

3.3 Barriers

所有的同步原語(yǔ)中,Barriers使用起來(lái)最為困難。Barriers用于顯式的控制buffer或者image的訪問(wèn)范圍,避免hazards(RaW,WaR,and WaW),保證數(shù)據(jù)一致性。

Barriers需要開(kāi)發(fā)者了解渲染管線的各個(gè)階段,能清晰的把握管線中每個(gè)步驟對(duì)資源的讀寫(xiě)順序。

Vulkan中將Pipeline的各個(gè)階段定義為:

  • 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

對(duì)應(yīng):

image-20200730152820973.png

假設(shè)我們有個(gè)兩個(gè)渲染管線P1 和 P2,P1會(huì)通過(guò)Vertex Shader往buffer寫(xiě)入頂點(diǎn)數(shù)據(jù),P2需要在Compute Shader中使用這些數(shù)據(jù)。

如果使用fence去同步,你的流程應(yīng)該是這樣:P1的Command提交后,P2通過(guò)fence確保P1的操作已經(jīng)被全部執(zhí)行完,再開(kāi)始工作。

image-20200730153109052.png

但是這種大顆粒度的同步操作無(wú)疑造成了耗時(shí)操作:P1的數(shù)據(jù)在Vertex Shader階段就已經(jīng)準(zhǔn)備好了,我們?yōu)槭裁匆鹊剿胁僮鲌?zhí)行完再開(kāi)始?P2平白多等待了很長(zhǎng)時(shí)間,而且在這個(gè)期間P2的其他階段并沒(méi)有使用到P1的數(shù)據(jù),也是可以執(zhí)行的啊。

Barriers的引入完全解決了這個(gè)問(wèn)題,我們只需要告訴Vulkan,我們?cè)赑2的Compute Shader階段才會(huì)等待P1 Vertex Shader里面的數(shù)據(jù),其他階段并不關(guān)心,可以同步進(jìn)行。

image-20200730152521195.png

使用方法:

image-20200730153743130.png

參考文檔:

  1. Vulkan Overview

  2. Android and Vulkan - GDD China.pdf

  3. Vulkan Programming Guide

  4. Vulkan Cookbook

  5. Learning Vulkan

  6. Vulkan Multi-Threading

  7. Vulkan中的同步機(jī)制

  8. Vulkan? 1.1.148 - A Specification

  9. vulkan中的同步和緩存控制之二,barrier和event

  10. vulkan中的同步和緩存控制之一,fence和semaphore

  11. VULKAN BARRIERS EXPLAINED

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容