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