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