上回書《Part 0 引擎基礎》說到,我們粗略地知道UE4是以哪些類來管理一個遊戲場景裡的資料的,但這僅僅是我們開始探索UE4渲染體系的一小步。
本回主要介紹UE4渲染體系中比較巨集觀頂層的一部分——多執行緒渲染,具體的多執行緒中,又分為:
- 遊戲執行緒(GameThread)
- 渲染執行緒(RenderThread)
- RHI執行緒(Render Hardware Interface Thread)
為什麼是多執行緒?
用來描述“渲染”的最基礎的理論就是像圖上的那樣,CPU呼叫圖形API提供的DrawCall命令(也叫繪製指令),在命令中說明需要渲染的資料、屬性等等,然後CPU等待GPU返回渲染結果,完成渲染。對於那些渲染頻率不高的場景,這種方式並沒有什麼問題,但在遊戲這種需要實時性渲染的高頻率場景下,問題就顯現出來了。
遊戲引擎完成渲染不只有提交DrawCall這一個任務,除了這個以外,CPU要花費非常多的時間在處理遊戲邏輯運算和準備渲染資料上,比如處理使用者的輸入、執行遊戲指令碼、更新物理和動畫、可見性剔除等等等等。
假如引擎把所有的事都交由GameThread來完成,當GameThread把當前這一幀該做的事都做完了,準備好要渲染的資料,提交到GPU後,GameThread就只能等待渲染結果,但GameThread接受到當前這一幀的使用者輸入後,完全可以去執行下一幀的各種任務,但單執行緒的機制並不允許這樣的事情。
多核心的CPU和多執行緒併發並行的作業系統在今天已經不是什麼稀罕事了,將與渲染相關的任務從GameThread中剝離出來,讓GameThread專注處理遊戲邏輯上的的各種計算任務,讓RenderThread專門和GPU來完成渲染任務,就成了自然而然的事情。
加入RenderThread後,每次GameThread處理完各種任務,準備好渲染資料,把資料傳送給RenderThread,然後就繼續處理下一幀的任務了,RenderThread收到資料,進行一些資料處理後(比如可見性剔除),向GPU提交DrawCall,等待渲染結果,完成渲染。
那RHIThread是什麼呢?UE4中RHI的提出可能有很多原因:
- 支援跨平臺多種圖形API
- 並行提交DrawCall
- 其他各種各樣的效能優化
首先是針對跨平臺多種圖形API,由於不同平臺支援的圖形API不同,Windows限定的Direct3D、MacOS限定的Metal以及跨平臺(包括移動端)的OpenGL和Vulkan,在有RHIThread之前,RenderThread會根據不同的圖形API來選擇DrawCall,這肯定會增加不少工作量,維護也更加複雜。
"All problems in computer science can be solved by another level of indirection." —— Jay Black
如果把這件事交由單獨的一個執行緒來做,豈不美哉?這不,RHIThread就來了。
RenderThread準備好渲染資料後,向RHIThread提交一個與圖形API無關的RHIDrawCommand,RHIThread掏出來一個表,查詢當前平臺的圖形API裡哪一句是對應的DrawCall,然後再向GPU提交DrawCall,等待渲染結果,完成渲染。這樣一來,RenderThread就可以在自己的任務上專注(方便優化),在RHIThread上完成對各個平臺的圖形API版本迭代維護。
當然這是從工程優化角度上RHIThread存在的理由,當然RHI還有一些更加直接的存在理由,那就是為了支援並行化提交DrawCall。在一些比較舊的圖形API裡,DrawCall都是阻塞的,即一個執行緒提交DrawCall時,不允許其他執行緒提交。圖形API呼叫GPU計算後,GPU本身計算渲染是需要時間的,而在這時間裡,圖形API如果能準備好下一次DrawCall,那必然是更好的。
隨著技術更新,一些新的圖形API開始提供一些並行化提交DrawCall的方式,在沒有RHI的時候,難道讓UE4跑多個RenderThread嗎?好像也不太合理,RenderThread裡面除了提交DrawCall的其他部分也不需要多個執行緒來完成,那需要單獨提出來多執行緒化的任務就順理成章地變成了RHIThread了。
總結
可以看到UE4渲染體系中多執行緒渲染的設計並不是一開始就是這樣,而是跟隨著技術的需求在不斷髮展進步的(新的UE5裡面估計又改了不少了)。
本回並沒有著重討論各種執行緒內部細節的任務,也沒有非常深入的講解各個執行緒之間是如何傳遞具體的命令和資料的,因為講起來那篇幅真的就太長了,之後再慢慢地整理吧,網路上的資料也很多,大家可以自行擴充閱讀。