<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Vulkan - 高性能渲染

          共 7447字,需瀏覽 15分鐘

           ·

          2022-06-19 22:01

          原文鏈接: https://zhuanlan.zhihu.com/p/20712354

          作為下一代圖形API以及OpenGL的繼承者,Vulkan也保留了GL跨平臺(tái)和開(kāi)發(fā)等特性。

          然而Vulkan誕生的最重要的理由是性能,更具體的說(shuō),是優(yōu)化CPU上圖形驅(qū)動(dòng)相關(guān)的性能。下面首先大概談?wù)剛鹘y(tǒng)圖形API,例如OpenGL和D3D11,在設(shè)計(jì)上面有哪些潛在的不夠高效的地方。

          傳統(tǒng)圖形API的局限

          上圖是一段比較典型的圖形應(yīng)用程序中的主循環(huán)偽代碼。循環(huán)的最外層,通常每一幀都會(huì)有好幾個(gè)render pass,例如shadow map和gbuffer的渲染,光照以及各種后處理等。每個(gè)pass都有需要設(shè)定特定的管線狀態(tài),例如blending,depth,raster的狀態(tài)等等。

          在下面幾層循環(huán)中通暢需要遍歷所有的shader和著色系統(tǒng)需要的材質(zhì)參數(shù),如紋理,常量等等。在最內(nèi)層的循環(huán)中,則是需要遍歷共享材質(zhì)的幾何體,在這里同場(chǎng)需要綁定vertex buffer和index buffer,以及針對(duì)物體的常量參數(shù)例如矩陣等。

          這里潛在的問(wèn)題是,當(dāng)渲染的場(chǎng)景非常復(fù)雜的時(shí)候(集合數(shù)量多,材質(zhì)系統(tǒng)復(fù)雜,紋理,參數(shù)多,渲染管線復(fù)雜等),所有這些每一幀大量的狀態(tài)更新,資源綁定操作所花費(fèi)的計(jì)算時(shí)間就不能被簡(jiǎn)單的忽略了。而在現(xiàn)在的圖形程序中,復(fù)雜場(chǎng)景,大量的多邊形,材質(zhì)、shader的組合以及復(fù)雜的渲染管線卻正是所有高質(zhì)量渲染所需要的。

          在處理這些修改管線狀態(tài)的操作時(shí),驅(qū)動(dòng)會(huì)在后臺(tái)運(yùn)行許多工作,包括下載紋理,Mipmap Downsample,資源訪問(wèn)的同步,渲染狀態(tài)組合正確性驗(yàn)證,以及錯(cuò)誤檢查等等等等。對(duì)于3D App開(kāi)發(fā)者來(lái)說(shuō),這些工作什么時(shí)候發(fā)生,是否發(fā)生,都是在API層面無(wú)法確定的。所以這樣的結(jié)果就是在CPU端造成卡頓。

          卡頓也許是你第一次給管線綁定特定的Shader,VBO或者Blend Mode,Render Target的時(shí)候。由于不同的硬件廠商的驅(qū)動(dòng)處理這些工作的方式都不一樣,所以App在不同顯卡上運(yùn)行的癥狀以及優(yōu)化的方式都會(huì)有差別,優(yōu)化也是無(wú)從下手。

          傳統(tǒng)圖形API的另一個(gè)問(wèn)題就是,并不多線程友好?,F(xiàn)在多核的系統(tǒng)以及不能再更加普及,然而大多數(shù)的圖形應(yīng)用和游戲在CPU端并沒(méi)有將這些放在手邊的計(jì)算資源利用起來(lái)。當(dāng)在驅(qū)動(dòng)的工作非常費(fèi)時(shí)的情況下,利用CPU端的多線程非常可能有效的提高整個(gè)程序的性能。

          無(wú)論OpenGL還是Direct3D,都包含一個(gè)Context的概念。Context包括當(dāng)前渲染管線中的所有狀態(tài),綁定的Shader,Render Target等。在OpenGL中Context和單一線程是綁定的,所以所有需要作用于Context的操作,例如改變渲染狀態(tài),綁定Shader,調(diào)用Draw Call,都只能在單一線程上進(jìn)行。

          NV_CommandList拓展可以讓App支持多線程的任務(wù)生成,但是所有渲染狀態(tài)的操作還是只能在主線程進(jìn)行,此處不展開(kāi)。在D3D中,多個(gè)線程訪問(wèn)Context時(shí)需要App顯示的做Synchronization,程序?qū)懫饋?lái)比較麻煩,而且也會(huì)有一定性能的影響。

          Vulkan的設(shè)計(jì)哲學(xué)及架構(gòu)

          Vulkan,亦或者Direct3D12的誕生都是為了擺脫以上提到的局限。Vulkan的API在設(shè)計(jì)上很明顯的可以看到以下幾個(gè)思路:

          • 更依賴于程序自身的認(rèn)知,讓程序有更多的權(quán)限和責(zé)任自主的處理調(diào)度和優(yōu)化,而不依賴于驅(qū)動(dòng)嘗試在后臺(tái)的優(yōu)化。程序開(kāi)發(fā)者應(yīng)該程序的最優(yōu)化行為最為了解,傳統(tǒng)圖形API則靠驅(qū)動(dòng)分析程序中調(diào)用API模式來(lái)揣測(cè)并且推斷所有操作的優(yōu)化方法。

          • 多線程友好。讓程序盡可能的利用所有CPU計(jì)算資源從而提高性能。Vulkan中不再需要依賴于綁定在某個(gè)線程上的Context,而是用全新的基于Queue的方式向GPU遞交任務(wù),并且提供多種Synchronization的組件讓多線程編程更加親民。

          • 強(qiáng)調(diào)復(fù)用,從而減少開(kāi)銷。大多數(shù)Vulkan API的組件都可以高效的被復(fù)用。

          下面通過(guò)簡(jiǎn)單介紹Vulkan的API架構(gòu)來(lái)討論下Vulkan是如何實(shí)踐這些哲學(xué)的。下圖是Vulkan中主要的組件以及它們之間的關(guān)系。

          首先是Device:

          Device很好理解,一個(gè)Device就代表著一個(gè)你系統(tǒng)中的物理GPU。它的功能除了讓你可以選擇用來(lái)渲染(或者計(jì)算)的GPU以外,主要功能就是為你提供其他GPU上的資源,例如所有要用到顯存的資源,以及接下來(lái)會(huì)提到的Queue和Synchronization等組件。

          第二個(gè)主要的組件,比Device復(fù)雜很多,就是Pipeline。一個(gè)Pipeline包含了傳統(tǒng)API中大部分的狀態(tài)和設(shè)定。只不過(guò)Pipeline是需要事先創(chuàng)建好的,這樣所有的狀態(tài)組合的驗(yàn)證和編譯都可以在初始化的時(shí)候完成,運(yùn)行時(shí)不會(huì)再因?yàn)檫@些操作有任何性能上的浪費(fèi)。但正是因?yàn)檫@一點(diǎn),如果你不同的Pass需要不同的狀態(tài),你需要預(yù)先創(chuàng)造多個(gè)不同的Pipeline。然而我們不能把所有渲染需要的信息全都prebake進(jìn)pipeline中,一個(gè)Pipeline應(yīng)該是可以通過(guò)綁定不同的資源而復(fù)用的。接下來(lái)介紹的幾個(gè)組件就可以被動(dòng)態(tài)的綁定給任何Pipeline。

          接下來(lái)是Buffer。Buffer是所有我們所熟悉的Vertex Buffer, Index Buffer, Uniform Buffer等等的統(tǒng)稱。而且一個(gè)Buffer的用途非常多樣。在Vulkan中需要特別注意Buffer是從什么類型的內(nèi)存中分配的,有的類型CPU可以訪問(wèn),有的則不行。有的類型會(huì)在CPU上被緩存?,F(xiàn)在這些內(nèi)存的類型是重要的功能屬性,不再只是對(duì)驅(qū)動(dòng)的一個(gè)提示了。

          Image在Vulkan中代表所有具有像素結(jié)構(gòu)的數(shù)組,可以用于表示文理,Render Target等等。和其他組件一樣,Image也需要在創(chuàng)建的時(shí)候指定使用它的模式,例如Vulkan里有參數(shù)指定Image的內(nèi)存Layout,可以是Linear,也可以是Tiled Linear便于紋理Filter。

          如果把一個(gè)Linear layout的Image當(dāng)做紋理使用,在某些平臺(tái)上可能導(dǎo)致嚴(yán)重的性能損失。類似傳統(tǒng)的API,紋理本身并不直接綁定給Pipeline。需要讀取和使用Image則要依賴于ImageView。

          講了幾種不同類型的內(nèi)存, 但是內(nèi)存是從什么地方分配的呢?在Vulkan中,所有內(nèi)存都分配與一個(gè)指定的Heap。一個(gè)Device也許支持幾種不同類型的Heap,有些也許可以分配Mappable的內(nèi)存,有些不行。

          具體的類型取決于程序運(yùn)行的平臺(tái)。值得注意的是,Vulkan Heap分配的內(nèi)存和最終的Vulkan組件例如Buffer和Image直接可以不,也不應(yīng)該是一對(duì)一的映射。一段內(nèi)存可以分配成數(shù)段,并且分配給不同的資源使用。某種程度上這樣的資源復(fù)用也是Vulkan基本的設(shè)計(jì)哲學(xué)之一。

          上面提到,Buffer和Image可以動(dòng)態(tài)的綁定給任意Pipeline。而具體綁定的規(guī)則就是由Descriptor指定。和其他組件一樣,Descriptor Set也需要在被創(chuàng)建的時(shí)候,就由App指定它的固定的Layout,以減少渲染時(shí)候的計(jì)算量。

          Descriptor Set Layout可以指定綁定在指定Descriptor Set上的所有資源的種類和數(shù)量,以及在Shader中訪問(wèn)它們的索引。App可以定義多個(gè)不同的Descriptor Set Layout,所以如何為你的程序或者引擎設(shè)計(jì)Descriptor Set的Layout將是優(yōu)化的重要一環(huán)。

          當(dāng)然,程序也可以擁有多個(gè)指定Layout的Descriptor Set。因?yàn)镈escriptor Set是預(yù)先創(chuàng)建并且無(wú)法更改的,所以改變一個(gè)綁定的資源需要重新創(chuàng)建整個(gè)Descriptor Set,但改變一個(gè)資源的Offset可以非??焖俚脑诮壎―escriptor Set的時(shí)候完成。一會(huì)我會(huì)討論如何利用這一點(diǎn)來(lái)實(shí)現(xiàn)高效的資源更新。

          介紹了那么多組件,都是渲染需要的數(shù)據(jù)。那么Command Buffer就是渲染本身所需要的行為。在Vulkan里,沒(méi)有任何API允許你直接的,立即的像GPU發(fā)出任何命令。所有的命令,包括渲染的Draw Call,計(jì)算的調(diào)用,甚至內(nèi)存的操作例如資源的拷貝,都需要通過(guò)App自己創(chuàng)建的Command Buffer。

          Vulkan對(duì)于Command Buffer有特有的Flag,讓程序制定這些Command只會(huì)被調(diào)用一次(例如某些資源的初始化),亦或者應(yīng)該被緩存從而重復(fù)調(diào)用多次(例如渲染循環(huán)中的某個(gè)Pass)。另一個(gè)值得注意的是,為了讓驅(qū)動(dòng)能更加簡(jiǎn)易的優(yōu)化這些Command的調(diào)用,沒(méi)有任何渲染狀態(tài)會(huì)在Command Buffer之間繼承下來(lái)。

          每一個(gè)Command Buffer都需要顯式的綁定它所需要的所有渲染狀態(tài),Shader,和Descriptor Set等等。這和傳統(tǒng)API中,只要你不改某個(gè)狀態(tài),某個(gè)狀態(tài)就一直不會(huì)變,這一點(diǎn)很不一樣。

          最后一個(gè)關(guān)鍵組件, Queue,是Vulkan中唯一給GPU遞交任務(wù)的渠道。Vulkan將Queue設(shè)計(jì)成了完全透明的對(duì)象,所以在驅(qū)動(dòng)里沒(méi)有任何其他的隱藏Queue,也不會(huì)有任何的Synchronization發(fā)生。

          在Vulkan中,給GPU遞交任務(wù)不再依賴于任何所謂的綁定在單一線程上的Context,Queue的API極其簡(jiǎn)單,你向它遞交任務(wù)(Command Buffer),然后如果有需要的話,你可以等待當(dāng)前Queue中的任務(wù)完成。

          這些Synchronization操作是由Vulkan提供的各種同步組件完成的。例如Samaphore可以讓你同步Queue內(nèi)部的任務(wù),程序無(wú)法干預(yù)。Fence和Event則可以讓程序知道某個(gè)Queue中指定的任務(wù)已經(jīng)完成。所有這些組件組合起來(lái),使得基于Command Buffer和Queue遞交任務(wù)的Vulkan非常易于編寫多線程程序。

          后文會(huì)簡(jiǎn)單討論一些常見(jiàn)的多線程模式。最后,和前面提到的一樣,Queue不光接收?qǐng)D形渲染的調(diào)用,也接受計(jì)算調(diào)用和內(nèi)存操作。

          Vulkan編程模式

          下面討論一些使用Vulkan時(shí)候比較常見(jiàn)的編程模式,這些模式也都各自彰顯了前面提到的Vulkan的設(shè)計(jì)哲學(xué)。例如對(duì)于內(nèi)存的管理,Vulkan更加依賴于程序本身對(duì)自己資源壽命范圍的理解來(lái)達(dá)到更優(yōu)化的內(nèi)存分配和釋放。這里提到的許多事情,在傳統(tǒng)的API中驅(qū)動(dòng)可能會(huì)嘗試幫你做一部分,但是在Vulkan中,所有的控制權(quán)和責(zé)任都在程序本身上。

          傳統(tǒng)API中,內(nèi)存的分配,資源的創(chuàng)建以及資源的使用都是一對(duì)一的映射,很明顯這不是最佳的資源管理模式。在Vulkan中,一次來(lái)自Heap的資源分配可以同時(shí)創(chuàng)建多個(gè)Buffer,每個(gè)Buffer又可以用于不同的格式以及用途。

          這樣相對(duì)傳統(tǒng)的情況已經(jīng)有不少的優(yōu)化。Vulkan甚至允許講一個(gè)Buffer對(duì)象的不同子區(qū)間劃分給格式以及用途不同的子Buffer,例如索引和頂點(diǎn)Buffer可以共享同一個(gè)Buffer,只要在綁定的時(shí)候指定不同的偏移量即可。這也是最優(yōu)的做法,它既減少了內(nèi)存分配的頻率,也減少了Buffer綁定的頻率。

          正是因?yàn)閂ulkan在Descriptor Set中綁定資源的時(shí)候,不僅需要指定Buffer,也需要指定Buffer的中資源的偏移量,所以我們可以利用這個(gè)特性達(dá)到高效的更新以及綁定的資源。因?yàn)槲覀兛梢酝瑫r(shí)綁定多個(gè)不同的資源到同一個(gè)大Buffer的不同子區(qū)間,然后在需要綁定不同的資源的時(shí)候可以重復(fù)使用同一個(gè)Descriptor Set,指定不同的偏移量即可。

          至于如何組合這些資源和Buffer的組合以及Layout,Vulkan需要程序開(kāi)發(fā)本身找到最佳的資源的分配,綁定以及更新模式。不再依賴于任何驅(qū)動(dòng)的優(yōu)化。。因?yàn)榕抠Y源分配,更新的最佳頻率就是資源本身的更新頻率。有些資源每次只需要每次運(yùn)行更新一次,有些則是每個(gè)場(chǎng)景更新一次,也有的動(dòng)態(tài)資源每幀都需要更新。然而程序本身這些更新的頻率是最清楚明了的,永遠(yuǎn)都能比驅(qū)動(dòng)分析的結(jié)果更加準(zhǔn)確。所以將這個(gè)任務(wù)交給程序本身其實(shí)也是非常合理的。

          下面說(shuō)說(shuō)另一個(gè)非常重要的話題,就是多線程渲染。如前面所說(shuō),Vulkan基于Queue的API設(shè)計(jì)對(duì)多線程非常友好,同時(shí)也提供了多種Synchronization的方法。常見(jiàn)的并行方法有兩種,第一種是在CPU端并行的更新一些Buffer中的數(shù)據(jù)。這里要注意的是,多線程的情況下更新資源要保證安全。

          如果你的程序渲染的非常高效,通常在CPU端會(huì)同時(shí)有好幾幀的數(shù)據(jù)要處理。所以程序會(huì)可以Round Robin的方法更新并且使用這些資源。這個(gè)時(shí)候要是別的線程寫的前面某一幀還沒(méi)有被讀取完的數(shù)據(jù)則會(huì)造成錯(cuò)誤。Vulkan的Event可以被插入在Command Buffer中,在使用指定資源的調(diào)用后面。這樣App回一直等到SetEvent被調(diào)用之后才會(huì)更新指定的資源。

          當(dāng)然在最理想的情況,程序不用真正的等這些Event,因?yàn)樗缫呀?jīng)被Set過(guò)了。當(dāng)然具體情況要取決于整個(gè)系統(tǒng)的性能,以及你的Round robin環(huán)有多長(zhǎng)。

          另一種并行的方式帶來(lái)的性能提升更加顯著,尤其是在渲染非常復(fù)雜的場(chǎng)景的時(shí)候。這也是Vulkan相比傳統(tǒng)API最能體現(xiàn)提高的情況。那就是并行的在不同線程上生成場(chǎng)景不同部分的渲染任務(wù),并且生成自己的Command Buffer,不用任何線程間的Synchronization。最后,不同的線程可以將Command Buffer的Handle傳給主線程然后由主線程將它們寫入Queue中,也可以直接寫入子線程中的per-thread Queue遞交給GPU。不過(guò)Queue的任務(wù)遞交時(shí)間并不是完全可以忽略的,所以這里還是建議將Command傳給主線程一起遞交。這樣的模式達(dá)到了計(jì)算資源利用的最大化,多個(gè)CPU核都參與了場(chǎng)景的渲染,并且有大量的渲染任務(wù)同時(shí)遞交給GPU最大化了GPU的吞吐量。下圖說(shuō)明了這種模式。

          和Buffer更新時(shí)候的線程安全一樣,Command Buffer的更新也需要注意不能直接復(fù)蓋還未被使用的Command Buffer。Vulkan的Queue寫入API接收一個(gè)Fence參數(shù),這個(gè)Fence會(huì)在這個(gè)Queue中的任務(wù)都被GPU處理完畢后會(huì)被Signal。

          所以程序?qū)ommand Buffer遞交給Queue后,可以馬上接著并行的更新和遞交新的任務(wù)。直到Fence之前的Fence被Signal之后,才可以安全的覆蓋那個(gè)Fence所對(duì)應(yīng)Queue中的Command Buffer。

          另一個(gè)需要主意的多線程相關(guān)的組件是Command Buffer Pool。Command Buffer Pool是Command Buffer的父親組件,負(fù)責(zé)分配Command Buffer。Command Buffer相關(guān)的操作會(huì)對(duì)其對(duì)應(yīng)的Command Buffer Pool里造成一定的工作,例如內(nèi)存分配和釋放等等。因?yàn)槎鄠€(gè)線程會(huì)并行的進(jìn)行Command Buffer相關(guān)的操作,這個(gè)時(shí)候如果所有的Command Buffer都來(lái)自同一個(gè)Command Buffer Pool的話,這時(shí)Command Buffer Pool內(nèi)的操作一定要在線程間被同步。所以這里建議每個(gè)線程都有自己的Command Buffer Pool,這樣每個(gè)線程才可以任意的做任何Command Buffer相關(guān)的操作。

          Command Buffer Pool的另一個(gè)性質(zhì)就是支持非常高效的重置。一旦重置,所有由當(dāng)前Pool分配的Command Buffer都會(huì)被清零,并且不會(huì)有任何內(nèi)存管理上的碎片。

          所以程序只要為每一個(gè)幀和線程的組合分配一個(gè)Command Buffer Pool,就可以利用這一點(diǎn),在更新Round Robin中的Command Buffer時(shí)非常快速的將需要的Buffer清零。

          另一個(gè)類似Command Buffer Pool的組件,就是Descriptor Pool。所有Descriptor Set都由Descriptor Pool分配,Descriptor Set操作會(huì)導(dǎo)致對(duì)應(yīng)的Descriptor Pool工作而且需要線程間同步,并且Descriptor Pool也支持非常高效的將所有由當(dāng)前Pool分配的Descriptor Set一次性清零。

          所以程序應(yīng)該為每個(gè)線程分配一個(gè)Descriptor Pool,可以根據(jù)Descriptor Set的更新頻率,創(chuàng)建不同的Descriptor Pool,例如每幀、每場(chǎng)景等等。

          快寫完了,說(shuō)一說(shuō)Vulkan到底適用于哪些人。如果程序性能的瓶頸在于CPU上和圖形相關(guān)的部分,并且這部分任務(wù)能相對(duì)容易的并行化,那么Vulkan很有可能有機(jī)會(huì)提升它的性能。亦或者對(duì)于想要榨干某個(gè)計(jì)算資源相對(duì)有限的平臺(tái)上的性能,那么Vulkan中允許程序?qū)λ匈Y源直接的分配和管理也可能對(duì)性能有一定的幫助。

          再者,對(duì)于非常執(zhí)著于盡可能的減少程序中的延遲和卡頓,因?yàn)閂ulkan的驅(qū)動(dòng)不會(huì)在背后做太多復(fù)雜的工作,那么也許也會(huì)有幫助。

          但是,如果程序本身的瓶頸是GPU,Vulkan不見(jiàn)得有任何幫助。如果立即需要支持許多平臺(tái),并且想要有許多第三方的庫(kù),那Vulkan畢竟還非常新。如果程序在CPU端非常難以多線程并行化,那么Vulkan帶來(lái)的提升也會(huì)比較有限。

          最后,本文只提供了一些非常概念性的介紹,并不期望讀完后會(huì)用Vulkan畫一個(gè)三角形。但是卻介紹了Vulkan背后的理念以及一些使用時(shí)候的一些模式和注意事項(xiàng)。相信這些東西比Hello World更加有價(jià)值。關(guān)于Vulkan編程的教程以及資源,請(qǐng)參考下面的鏈接:

          1. NVIDIA Developer,包括幾個(gè)多線程的Sample:https://developer.nvidia.com/Vulkan

          2. Vulkan Spec:Vulkan API Reference Pages

          3. Vulkan SDK:Home Page

          4. Render Doc的教程:Vulkan in 30 minutes

          5. Cinder引擎的整合:Vulkan Notes :: Cinder


          瀏覽 53
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  无码大骚逼 | 九九九亚洲视频 | 日本在线观看a视频 | 爱爱视频无码 | 亚洲无圣光豆花 |