渲染器緩存
Qt 3D的運(yùn)行是基于兩種現(xiàn)有的數(shù)據(jù)結(jié)構(gòu):
Scene Graph-描述場景的內(nèi)容;
Frame Graph-描述渲染Scene Graph的方式。
每渲染一幀畫面,我們必須做大量的工作,才能把scene graph和frame graph中的抽象描述轉(zhuǎn)換為底層的draw調(diào)用并傳送至GPU。簡而言之,步驟如下:
遍歷frame graph并識別各個(gè)渲染階段。每個(gè)階段包括渲染target(屏幕或FBO);要使用哪個(gè)攝影機(jī);要使用哪個(gè)窗口;應(yīng)該繪制scene graph的哪些部分;設(shè)置GPU的特定狀態(tài)(例如,禁用深度測試或?qū)懭耄騿⒂媚0鍦y試)。
步驟1中的每個(gè)渲染階段都需要從scene graph中篩選出我們關(guān)心的實(shí)體。
為每個(gè)實(shí)體以及當(dāng)前渲染階段選擇相應(yīng)的著色器。實(shí)體可以在不同階段使用不同的著色器,例如,使用一個(gè)簡單的片段著色器執(zhí)行early Z填充或生成陰影貼圖,而使用完全光照著色器實(shí)現(xiàn)屏幕上最終效果。
合并uniform變量(用于自定義著色器中的變量)。
將所有這些信息綁定到RenderCommands中。
一旦所有階段都完成了,我們將通過一個(gè)獨(dú)立線程向OpenGL提交RenderCommands,由于OpenGL歷史悠久,它對線程非常挑剔。
OpenGL提交線程迭代各個(gè)渲染階段和其中包含的命令,將它們從我們的中間描述翻譯成OpenGL格式,并分派給原始的OpenGL函數(shù)調(diào)用。
這一切都使Qt 3D變得非常靈活,但代價(jià)是運(yùn)行時(shí)的性能。通常能夠大幅提升性能的方法無非就是通過緩存避免無謂的繪制開銷。理論上,我們可以通過緩存一些中間結(jié)果來取得提升。而實(shí)際上,要考慮很多內(nèi)容比如怎樣結(jié)合動(dòng)態(tài)渲染模式等,渲染器緩存確實(shí)很難做到。
這其中有太多可以影響渲染場景外觀的東西需要跟蹤,還要弄清楚當(dāng)不同畫面之間某些屬性更新之后,必須重新繪制的最小任務(wù)集是什么。我們已經(jīng)在Qt 5版本中添加了一些跟蹤功能,但要完全做到這一點(diǎn)需要更大的重構(gòu)。
在詳細(xì)描述我們在這方面所做的工作之前,我先討論另一個(gè)問題:
現(xiàn)代圖形API
到目前為止,Qt Quick(基本上)已經(jīng)完全架構(gòu)在OpenGL(或OpenGL ES)之上,Qt 3D大抵如此。雖然OpenGL長期以來為圖形工程師提供了很好的服務(wù),但它是一個(gè)非常古老的API,有一些根生蒂固的結(jié)構(gòu)性問題,以至于在不引入新API的情況下無法解決。此外,OpenGL經(jīng)過多年的擴(kuò)展和“改造”,試圖跟上現(xiàn)代GPU的實(shí)際工作方式,并處理藝術(shù)家們所要求的、不斷增長的數(shù)據(jù)量。盡管這促使OpenGL做了大量令人印象深刻的改進(jìn),但它仍然受到限制,特別是其多線程模型和驅(qū)動(dòng)實(shí)現(xiàn)中的啟發(fā)式模式,即驅(qū)動(dòng)試圖預(yù)測應(yīng)用程序開發(fā)者的行為模式。
如上一節(jié)所述,在驅(qū)動(dòng)程序內(nèi)部,OpenGL的操作方式與Qt 3D非常相似。當(dāng)您發(fā)出一堆OpenGL函數(shù)調(diào)用時(shí),這些調(diào)用會被轉(zhuǎn)換成命令并存儲在命令緩沖區(qū)中,然后在某個(gè)時(shí)間點(diǎn)(由驅(qū)動(dòng)程序的最佳預(yù)估決定)被提交給硬件進(jìn)行處理。
一旦命令緩沖區(qū)中的命令被硬件處理掉,下一幀我們必須再次發(fā)出OpenGL函數(shù)調(diào)用。同樣的流程會一幀接一幀地發(fā)生,這可非常浪費(fèi)。
在驅(qū)動(dòng)中,創(chuàng)建命令是一項(xiàng)非常耗資源的操作,而且在OpenGL中,這一切都被限制在一個(gè)線程內(nèi)。所以,清空命令緩沖區(qū)有點(diǎn)浪費(fèi)。編寫驅(qū)動(dòng)的GPU廠商添加了各種啟發(fā)算法,試圖預(yù)測庫和應(yīng)用程序開發(fā)者實(shí)際的意圖,藉此盡可能緩存數(shù)據(jù)并優(yōu)化操作。這使得驅(qū)動(dòng)變得更大、更復(fù)雜、更難維護(hù),并在某些情況下導(dǎo)致GPU廠商之間的巨大性能差異。
OpenGL的線程模型本質(zhì)上是單線程的。是的,可以通過共享context等一些方式支持多線程,但在驅(qū)動(dòng)內(nèi)部調(diào)用仍然會被序列化。考慮到OpenGL已有20多年的歷史,這并不奇怪。
OpenGL標(biāo)準(zhǔn)陳舊是另一個(gè)問題。蘋果公司已宣布棄用OpenGL,將只專注于將Metal作為其圖形API。在未來的某個(gè)時(shí)候,我們可能會發(fā)現(xiàn)OpenGL從MacOS和iOS中消失。即使在那之前,這些平臺上的OpenGL庫也不會看到任何新的功能了(事實(shí)上,它們已經(jīng)很多年沒有更新了)。
對于這些問題我們能做什么?好吧,在過去幾年中,現(xiàn)代圖形API的出現(xiàn)就是用于解決這些和其他問題的。Vulkan、Metal和DirectX 12都是非常流行的API,與OpenGL相比,它們提供了更直接控制GPU的接口。
您可能會說這太好了,但其實(shí)存在妥協(xié)之處。OpenGL驅(qū)動(dòng)程序所做的大部分工作現(xiàn)在由庫或應(yīng)用程序開發(fā)者負(fù)責(zé)。乍聽上去很嚇人,然而在某種程度上的確如此。但是,畢竟我們可以利用自己對應(yīng)用程序工作模式的宏觀理解從GPU中榨取性能。另一方面我們可以選擇在更短的時(shí)間內(nèi)完成類似的工作,從而讓CPU/GPU進(jìn)入休眠或省電模式,最終提高續(xù)航表現(xiàn)。這對于移動(dòng)設(shè)備和臺式機(jī)而言都是巨大的提升。
OpenGL驅(qū)動(dòng)程序?qū)G棄命令緩沖區(qū),而且它在每一幀上的創(chuàng)建消耗都很高,但是當(dāng)我們作為應(yīng)用程序開發(fā)者使用Vulkan或類似工具時(shí),我們可以知道何時(shí)保留這些命令緩沖區(qū)并在下一幀重新提交它們是安全的。
您可能想知道那有什么好處。提交相同的命令緩沖區(qū)只能讓我們在屏幕上看到與前一幀完全相同的內(nèi)容,難道不是嗎?如果是的話,那這么做有什么意義呢?
這是好問題。其實(shí)即使我們一次又一次地向GPU提交相同的命令緩沖區(qū),它們引用的資源卻可以包含不同的數(shù)據(jù)。不僅是頂點(diǎn)緩沖區(qū)和紋理,還可以包括通常用于保存材質(zhì)屬性和相機(jī)變換矩陣的uniform緩沖區(qū)對象。如果我們能夠跟蹤場景中哪些東西發(fā)生變化,就能確定是否可以將相同的命令重新提交給GPU,從而節(jié)省大量工作,這就非常棒。
還有一個(gè)錦上添花的情況!Vulkan使用了主命令緩沖區(qū)和輔命令緩沖區(qū)的概念。主命令緩沖區(qū)是我們提交給GPU的內(nèi)容,可能包含對輔命令緩沖區(qū)的調(diào)用。一種常見的使用方式是預(yù)先記錄某些實(shí)體的繪制命令并保存到輔命令緩沖區(qū)。
當(dāng)我們想要繪制整個(gè)場景時(shí),我們的渲染器可以創(chuàng)建一個(gè)主命令緩沖區(qū),調(diào)用那些可見實(shí)體的命令緩沖區(qū)。當(dāng)可見性改變時(shí)(例如,如果相機(jī)移動(dòng)或某些實(shí)體移動(dòng)),我們可以重新記錄主命令緩沖區(qū)。那也很好。
更多的錦上添花!使用Vulkan,我們還可以在不同線程上讀寫命令緩沖區(qū)!我們來負(fù)責(zé)向GPU隊(duì)列提交命令緩沖區(qū),并在不同的GPU隊(duì)列(圖形/計(jì)算/傳輸?shù)龋┲g以及GPU和CPU之間同步任務(wù)。
如您所見,我們可以在所涉及的操作和硬件上獲得更多的控制,但我們必須做更多的工作。總的來說,這是一個(gè)巨大的性能提升機(jī)會。
繼續(xù)聊Qt 6中的Qt 3D
以Qt 6開發(fā)時(shí)間節(jié)點(diǎn)來看,我們正積極研究這兩個(gè)大方向。從以上描述可以看出,這兩項(xiàng)任務(wù)都涉及大量工作,關(guān)于如何跟蹤用戶在scene graph和frame graph上狀態(tài)的修改以及接下來Qt 3D必須完成的剩余工作。這包括我們?nèi)绾巫罱K緩存命令緩沖區(qū)以及幀之間的其他中間狀態(tài),以避免不必要的重復(fù)工作。
正如您可能已經(jīng)了解,Qt Quick和Qt Quick 3D會在QRhi層基礎(chǔ)上重新構(gòu)建,QRhi層提供對Vulkan、Metal、DirectX 11和OpenGL的支持。我們?nèi)栽谘芯克欠窨梢院侠頂U(kuò)展此功能以滿足Qt 3D在功能和多線程方面的需求,或者是否需要使用其他方式集成圖形API,這樣Qt 3D仍然可以很好地與Qt Quick和Qt Widgets模塊配合使用。
這方面還有很多工作要做,但初步結(jié)果看起來非常有希望。我們測試了包含大約1000個(gè)實(shí)體的場景,在一個(gè)中檔桌面平臺上當(dāng)我們試圖最大化利用GPU時(shí),可以實(shí)現(xiàn)每秒600幀(畫面無撕裂)的渲染速度,或者當(dāng)我們限制到60fps時(shí),可以實(shí)現(xiàn)1%的CPU負(fù)載占用!現(xiàn)在這還只使用了單個(gè)內(nèi)核!為了進(jìn)一步改進(jìn)多線程架構(gòu),超越Qt 5系列的極限,我們正在驗(yàn)證一些想法。
這項(xiàng)工作有一個(gè)副產(chǎn)品,我們還開發(fā)出了frame graph的下一個(gè)迭代版本,它的更新非常自然平滑,因此更加容易理解,也更易于Qt 3D用戶的修改。
總結(jié)
如您所見,在Qt 5.x周期及以后的時(shí)間里,我們會在幕后做許多工作改進(jìn)Qt 3D。我們還將尋找改進(jìn)public API的方法,但我們預(yù)計(jì)在這方面不會有太大的變化,而是對一些不太理想的函數(shù)名和屬性名進(jìn)行一些清理。
所有這些改進(jìn)也將有利于基于Qt 3D的Kuesa和其他任何使用Qt 3D開發(fā)的3D應(yīng)用程序。這些變化將幫助打造一個(gè)堅(jiān)實(shí)的基礎(chǔ),使我們可以在Qt 6時(shí)加入更多令人興奮的擴(kuò)展。