ios跨线程通知_一种基于Metal、Vulkan多线程渲染能力的渲染架构
隨著3D渲染場景規模越來越復雜,單線程渲染架構在滿足業務性能要求時已經捉襟見肘,因此,多線程渲染顯得愈發重要。本文首先介紹了新一代圖形渲染接口Metal、Vulkan,以及它們的多線程渲染特性,然后描述了一種優雅的渲染架構,可以解決渲染管線開發中的諸多困擾,并且基于此實現了多線程渲染,極大的提升了3D渲染效率。
一、背景
Metal[1]、Vulkan[2]是分別由蘋果(Apple)和科納斯組織(Khronos Group)面向新一代圖形處理器(GPU)設計的圖形API。在移動平臺上,Metal應用于iOS系列產品,Vulkan主要由Android系統提供。Metal、Vulkan作為新一臺的圖形API,相較于OpenGL(ES),設計了更底層、更細粒度的接口,提供了全新的狀態管理,改善了錯誤處理機制,增加了新的特性支持。
使用新一代的圖形渲染接口,可以全方位的增強3D引擎的渲染能力。比如使用狀態緩存機制,可以降低CPU負載;利用ray tracing的三角面求交能力,可以帶來全新的全局光照方案(Global Illumination);自定義顯存管理器,可以提高顯存利用率……本文受限于篇幅,無法從各個角度逐一而述,這里主要討論新一代圖形渲染接口帶來的多線程渲染能力,以及如何圍繞它設計一種渲染架構。
二、多線程渲染
通常一個單線程渲染引擎會面臨著硬件資源浪費的問題。如下圖1所示,一個CPU線程需要先負責邏輯更新,然后反復更新渲染狀態,提交渲染指令。這種架構同時造成了CPU和GPU閑置。對CPU而言,完全可以將這些任務分配給多個CPU線程并行執行;就GPU而言,CPU線程在進行邏輯更新和提交渲染指令時,GPU處于閑置狀態,浪費了算力。
圖1??單線程渲染面臨的問題
作為開發者,在不考慮功耗、散熱、資源分配等現實限制條件下,通常希望CPU、GPU永遠處于滿負荷狀態。為了達到滿負荷的狀態,每個線程通常需要具備三個基本能力:
·?資源創建管理
·?Pipeline創建管理
·?Command?Buffer創建管理(提交Draw?Calls)
如果使用OpenGL(ES),引擎開發者將面臨的最大問題是,OpenGL(ES)API不能跨線程調用,它們的調用只能局限在GL Context創建線程內,這也就意味著OpenGL(ES)無法原生支持多線程渲染。為了達到OpenGL(ES)多線程渲染的目的,開發者需要手動實現Command Buffer,記錄圖形調用指令,考慮數據線程安全問題,解決資源同步可能帶來的性能損耗,不勝其煩。而這些問題,已經在metal、vulkan設計者考慮之內,新一代的圖形API天然支持跨線程調用,讓多線程渲染變得更具有實踐可行性。如下圖2所示,使用Vulkan時,Thread 1、2、3獨自組織更新工作,寫入Command Buffer,最后提交到Thread 4中的Command Queue中。
圖2 ?Vulkan多線程渲染架構
三、小結與轉進
從上面的描述可以看到,新一代的圖形API為了提高硬件資源利用率,進行了全新的設計,似乎多線程渲染唾手可得,渲染效率即將走向巔峰。然而,如果到此為止,急沖沖拿著多線程渲染API試圖提升渲染效率,進行實踐時,才會發現腦子里一團亂,無從下手。畢竟上面只是簡單描述了圖形API的基本能力,并沒有提供任何實際可行的方案。就好比做菜,有一樣的原料,做出來的菜品可能千差萬別。不過下面不準備直接描述一種多線程渲染方案,先轉進到一個相關問題—Render Graph。
四、Render?Graph
Render Graph在有些文獻也稱之為Frame Graph,這里使用Render Graph一詞,主要是考慮到與Shader Graph[3]一詞呼應。附錄對Shader Graph有一個簡單描述。
Render Graph是基于數據驅動的理念,將行為與數據解耦。只不過是Shader Graph處理shading相關問題,Render Graph處理rendering相關問題。下面先了解下,渲染器開發時遇到的一些痛點問題,然后了解下Render Graph如何解決這些痛點。
4.1 ?網狀結構的渲染流程
從圖3可以看到,一個常見的渲染系統,包含了地形、粒子、全局光照、反射、PBR、后處理、天空……各種模塊,而這些模塊還具有相互依賴關系,形成網狀結構,實際開發中會導致一系列令人沮喪的問題,簡單舉例:
·?記憶難度大。即使渲染系統由一個專人負責,過了一段時間之后,也很難對整個渲染流程記得分毫不差。
·?不易了解渲染流程全貌。閱讀線性結構的代碼,來理解圖狀結構的流程,非常困難。
·?日常維護難。為了支持新的渲染特性,經常需要向已有的流程中添加新的模塊,新的模塊意味著需要打斷與重建各模塊之間的聯系,稍有不慎,就會出錯。
·?資源管理困難。比如motion blur和Temporal Anti-Alias都需要使用velocity buffer,一旦渲染流程中各種開關過多,那么確定velocity buffer創建、銷毀時機就變得異常困難。如果采用粗暴的設計方式,就會變成渲染開始階段創建velocity buffer,直到渲染生命周期結束才銷毀velocity buffer,占用了緊缺的GPU資源。更加糟糕的是,即使精妙的設計了velocity buffer創建和銷毀時間,后續渲染流程稍有改動,這個所謂的精妙設計反而變成了錯誤設計,作繭自縛,心智負擔極重。
·?模塊管理困難。比如 shadow map模塊,如果某個場景中不需要使用陰影,那么shadow map模塊就不應該執行。為了達到這種開關效果,往往實際中建立一系列的if分支判斷,決定流程是否執行。但這種帶有“土味”的方法,會使渲染流程更難理解,代碼bug極容易發生。
圖3??Frostbite渲染系統
上面一系列令人頭疼的問題,實踐過的人會明白,痛不欲生,Render Graph應運而生。
4.2 ?Render?Graph應用在延遲渲染上
圖4 ?延遲渲染流程使用Render?Graph描述
要講清楚什么是Render Graph,最好的方式是直接看一個Render Graph例子。如上圖4所示,它描述的就是一個延遲渲染流程的Render Graph,從中可以看到橙色的方框代表的是行為(operation),藍色的方框代表的是資源(resource)。在實踐中,引擎中使用一個基類RenderGraphRenderPass來定義行為,使用一個基類RenderGraphResource來定義資源。這樣在實現RenderGraph時,需要經歷三個階段:
·?構建階段。在此階段中需定義所有RenderGraphRenderPass、并為其定義輸入輸出RenderGraphResource。資源類型分為兩種,permanent和transient。Permanent一般是Render Graph的外界資源,比如back color buffer之類。Transient資源可根據讀寫屬性分為三種,只讀資源,可寫資源、內部創建資源。
·?編譯階段。在這個階段,Render Graph可以自動的根據配置剔除無效行為和數據,并且自動的計算資源生命周期。
·?運行階段。執行代碼,完成整個渲染流程。
有了Render Graph,數據和行為得到解耦,加上清晰的數據流,很好的解決了4.1節提到的諸多混亂。比如,如果需要在上面的延遲渲染流程中添加Screen Space Ambient Occlusion(SSAO),只需要將全部的精力集中在SSAO的算法實現上,實現一個RenderGraphSSAORenderPass,然后定義好輸入輸出,不用關心現有的渲染流程如何復雜。然后在Render Graph中設置好AO相關節點,連接好相應的數據流,整個過程就完成了。
4.3??復用延遲渲染渲染流程
在4.2小節中定義了一個延遲渲染的Render Graph,在本節可以看看如何復用這個Render Graph來實現新的功能。從圖5可以看到,對于延遲渲染的絕大部分流程,不必做改變,只需要把Lighting Buffer拷貝到cube map之中,然后進行卷積,就可以得到反射球。
圖5??復用延遲渲染生成反射球
4.4??剔除無效數據和行為
在4.2小節中提到了, RenderGraph第二階段是編譯階段,會進行無效行為和數據剔除。如下圖6所示,這里本來Gbuffer是作為Lighting render pass的輸入,現在改為Debug View render pass的輸入,然后拷貝到Final target上,就可以顯示渲染中間變量,用來進行調試。此時Lighting render pass及其相關資源和render pass就變成了無效資源,可以直接在編譯階段剔除。
圖6 ?剔除Lighting及其相關資源
4.5 ?異步執行的Compute?Shader
除了常見的渲染單元,現代圖形架構中還存在著不可忽視的計算單元(Compute Shader)。Compute shader相較于渲染單元,將精力集中在計算方面,類似于光柵化這種渲染單元中的標準步驟,在計算單元中不復存在。可以簡單的認為,在執行通用計算時,compute shader的效率優于fragment shader。因此,一個完整的渲染架構不可回避通用計算模塊,可以定義一個基類RenderGraphComputePass來描述這個功能。
既然同時有了RenderPass和ComputePass,就需要考慮RenderPass和ComputePass是否串行執行。考慮到RenderPass和ComputePass都涉及大量IO訪問和圖形API提交,GPU有可能處于閑置狀態,因此將RenderPass和ComputePass進行并行設計。如圖7所示,因為SSAO功能考慮使用compute shader實現,那么SSAO相關功能放到另外一個異步隊列中。因為異步設計,同時會造成相應資源的生命周期延長,增加了整體顯存消耗。
圖7 ?上部和下部分別描述了SSAO的同步和異步設計
4.6??小結
基于Render Graph的渲染管線架構,數據流清晰,維護容易,高度可配置,甚至可以做到為每個渲染場景定義一套獨特的渲染管線,最大化的利用硬件效率。
那么這一切和本文提到的多線程渲染有什么關聯??
五、基于多線程渲染的Render?Graph
站在更高角度抽象的去看Render Graph, Render Graph是一個有向無環圖。如圖8所示,執行C依賴F的完成,但D并不依賴F,也就是C、F必須串行執行,而D、F可以并行運算。因此只要對這個有向無環圖進行拓撲排序,系統便可以自動判定任務并行性,這就是多線程渲染的切入點。如果具象的理解這個問題,可以認為任務D是Gbuffer pass,而任務F是shadow pass,那么D和F不存在相互依賴關系,可以并行計算。
圖8??Render?Graph的抽象形式
這里設計了一種拓撲排序方法:
1. 統計各節點出度和入度。D、F入度為0,具備并行性,出度分別為1、2。E的入度為1。
2. 執行入度為0的節點。執行D、F,F任務結束后,將它指向的節點入度減去1,這樣E的入度變為0。
3. 反復執行第二步,直到整個Graph執行完成。
經過上面的操作,整個Render Graph變成線性執行關系,最后提交給常規的并行系統執行,如圖9所示。
圖9 ?拓撲排序Render?Graph?tasks并執行
六、總結
本文首先介紹了Metal、Vulkan的多線程能力,然后使用Render Graph作為渲染架構,最后利用了一種簡單的拓撲排序方法,將有向無環圖展開為線型執行結構,使Render Graph天然支持并發的特性得到發揮,能夠充分利用新一代圖形API的多線程渲染能力。
本文受限于篇幅,主要探討新一代圖形API的多線程渲染能力,其它方面來不及深究。此外,CPU邏輯的并行能力也沒有來得及闡述,希望未來有機會進一步進行補充。
作者簡介
饒超,2018年加入快手,圖形引擎開發工程師,負責特效圖形引擎設計與開發。
附錄
Shader Graph主要是根據數據驅動的理念,將行為(shading)和數據解耦。下面圖10展示了Unreal Engine 4 Shader Graph的UI界面。UI最右邊面板含有Base color、Metallic、Roughness、Opacity等等slot,其余的部分為這些slot提供數據。這種設計,引擎內部可以不用關心data provider(整個Shader Graph就是一個data provider)具體邏輯,直接拿到這些數據,進行進一步的shading。Shader Graph具體實現本文不做深入探討,它與多線程渲染并沒有太多的關聯。
圖10??Unreal?Engine?4 Shader?Graph
引用
[1] Metal Programming Guide:https://developer.apple.com/library/archive/documentation/Miscellaneous/Conceptual/MetalProgrammingGuide/Introduction/Introduction.html
[2] Vulkan:https://www.khronos.org/vulkan/
[3] Unity3D shader graph:https://unity.com/shader-graph
快手Y-tech介紹Y-tech團隊是快手公司在人工智能領域的探索者和先行者,致力于計算機視覺、計算機圖形學、機器學習、AR/VR等領域的技術創新和業務落地,不斷探索新技術與新用戶體驗的最佳結合點。目前Y-tech在北京、深圳、杭州、Seattle、Palo Alto有研發團隊,大部分成員來自于國際知名公司和大學。如果你對我們做的事情很感興趣,希望一起做酷炫的東西,創造更大的價值,請聯系我們:ytechservice@kuaishou.com。
總結
以上是生活随笔為你收集整理的ios跨线程通知_一种基于Metal、Vulkan多线程渲染能力的渲染架构的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java setter_java – 如
- 下一篇: mysql ppl_浅析pplx库的设计