GPU 驅動的渲染一直是許多游戲應用程序的主要目標。它能夠提高處理大型虛擬場景的可擴展性,并減少 CPU 對游戲性能的瓶頸。
除了在 GPU 上運行游戲邏輯之外,我認為 GPU 驅動渲染的巔峰時刻就是 CPU 只發送新幀的攝像頭信息,而 GPU 則負責其余工作,直到屏幕上顯示的最終像素。
NVIDIA Omniverse 平臺已經對 Direct3D 12 (D3D12) API 進行了改進,包括支持 ExecuteIndirect
、不受限制的資源數組 和 ResourceDescriptorHeap
。這些功能增強了平臺的性能和靈活性,為開發者提供了更多的創作空間。
工作圖形是我很期待討論的另一個功能。工作圖形提供了一種編程范式,允許 GPU 隨時生成自己的工作。這為解決一些知名游戲引擎問題提供了解決方案,并開辟了新的創意思路。
本文介紹了工作圖的高級概念:結構、啟動模式和數據流。我將介紹如何使用 HLSL 編寫工作圖,以及從 CPU 啟動工作圖的步驟。如要充分利用本文,您應該熟悉以下內容:
- D3D12 API
- 編寫和編譯計算著色器
ExecuteIndirect
和光線追蹤 API
請注意,工作圖形在 NVIDIA Ampere 架構 和 NVIDIA Ada Lovelace 架構 上需要 NVIDIA 顯示驅動程序版本 551.76 或更高版本,可以通過 NVIDIA 驅動程序下載 獲取。
工作圖形概述
適用于 D3D12 的 Shader Model 6.8 以及許多其他功能,標志著工作圖形的正式發布。名稱中的“圖形”一詞很好地適應了其定義:一個由邊連接的節點集合。在工作圖形中,節點執行任務 (“工作”) 并通過圖形邊傳遞數據給其他節點。
但是節點執行的工作是什么?節點執行的命令是什么?Dispatch
呼叫?一個線程運行某種著色器?或者可能是一組線程運行相同的著色器?
答案是,上述所有內容。每個節點都有一個在程序員選擇的特定配置中啟動的著色器。這種配置或啟動模式可以是一個完整的分配網格 (廣播啟動) 或計算線程 (線程啟動)。請注意,線程啟動工作可以匯集到一個可能的浪潮中運行,但每個線程仍然獨立于其他線程。
通過選擇目標節點并將數據傳遞給它來實現與其他節點的連接,這類似于通常所說的繼續圖形術語。目標節點接收數據并在其調用者的范圍之外運行。此系統沒有堆棧,只有從圖形頂部到底部的數據流。
數據單元,即記錄,驅動整個工作圖的執行。要啟動一個節點,必須為其寫入記錄。然后在選定的啟動模式中啟動節點,并使用該記錄作為輸入。記錄是由生產者填充的數據包結構。生產者可以是 CPU 的命令 DispatchGraph
或工作圖中的任何節點。節點消耗記錄可以視為子生產者節點。

工作圖形新功能
如前所述,D3D12 已公開功能,以幫助實現 GPU 驅動的渲染。本節重點介紹了工作圖形引入的新功能與現有功能的比較。
動態著色器選擇
工作圖形中的每個節點都可以選擇哪些子節點要運行。該決定由生成者著色器代碼本身驅動。因此,可以根據前一個節點或工作負載中由 GPU 生成的信息做出決定。
另一方面,ExecuteIndirect
只能按照啟動時的狀態工作,尤其是管線狀態對象指定的著色器。需要根據 GPU 端數據啟動不同著色器的應用只能發出一系列SetPipelineState
和ExecuteIndirect
或依賴效率低下的 Uber 著色器來覆蓋某些潛在可能性。
隱式微型依賴關系模型
渲染一幀包括執行幾個主要通道,例如深度、幾何圖形或照明通道。在每個通道中,數據通過并行處理,每個數據單元通過多個順序操作。通常會在操作之間放置資源屏障,以確保前一個操作完成數據處理后再進行下一個操作。
工作圖表通過生產節點將記錄傳遞給子節點來表示這種依賴關系。子節點著色器僅在生產節點完成寫入記錄后才會運行,這表示數據已經準備就緒,可供子節點使用。請注意,工作圖表生產節點-消費節點依賴關系的范圍在于數據記錄范圍,而資源屏障則在于資源的所有訪問。
與屏障相比,工作圖依賴模型更精細。這可以翻譯為更高的 GPU 占用率,因為依賴工作可以提前啟動,而不是等待屏障完成。記錄可以立即從生產者節點傳輸到消費者節點,而無需在算法步驟中完全清除Dispatch-ResourceBarrier
序列。
圖 2 展示了每種情況下的工作負載執行方式。在左側,兩個Dispatch
分隔符,ResourceBarrier
。每一行代表一個生產者線程組 (綠色) 和其消費者線程組 (藍色)。在右側,同樣的工作負載使用工作圖進行運行。

在 HLSL 中編寫工作圖
與光線追蹤著色器類似,工作圖形以計算著色器庫的形式編寫。一個 HLSL 文件可能包含圖形中所有節點的代碼,但也可以從不同來源構建圖形,并在運行期間逐步連接這些節點。
以下 HLSL 代碼片段演示了由兩個節點 (生產者節點和消費者節點) 組成的非常簡單的工作圖。
struct RecordData { int myData; }; [Shader( "node" )] [NodeLaunch( "thread" )] [NodeIsProgramEntry] void MyGraphRoot( [MaxRecords(1)] NodeOutput MyChildNode) { ThreadNodeOutputRecords childNodeRecord = MyChildNode.GetThreadNodeOutputRecords(1); childNodeRecord.Get().myData = 123456; childNodeRecord.OutputComplete(); } [Shader( "node" )] [NodeLaunch( "broadcasting" )] [NodeDispatchGrid(1, 1, 1)] [numthreads(8, 8, 1)] void MyChildNode( DispatchNodeInputRecord inputData, uint2 dispatchThreadId : SV_DispatchThreadID) { int myData = inputData.Get().myData; } |
此代碼片段展示了節點著色器基本上是帶有一些附加聲明的計算著色器。值得注意的是NodeLaunch
屬性,用于指定該節點的啟動模式。圖形中的根節點 (表示為NodeIsProgramEntry
屬性) 是線程啟動節點。因此,對于每個輸入記錄,都會有一個計算著色器線程來處理它。根輸入記錄來自DispatchGraph
調用命令列表。
著色器函數簽名包含一個參數MyChildNode
類型NodeOutput
。此參數可用于生成MyChildNode
節點的名稱。因此,參數的名稱必須與圖形中的另一個節點的名稱相匹配。
MyChildNode
是一個廣播發布節點。這意味著,向此節點推送一條記錄會產生類似于Dispatch
尺寸,該尺寸由此處的屬性NodeDispatchGrid
與您的 Omniverse 帳戶關聯的MyChildNode
函數簽名。
有關新語法和聲明的詳細信息,請參閱 HLSL 語法參考 中的工作圖形部分。
CPU 端設置
啟動工作圖形需要 D3D12 中其他類型工作所需的相同步驟,具體如下:
- 執行特征支持檢查
- 在離線或運行時編譯著色器
- 加載著色器庫并構建工作圖形狀態對象
- 分配背景內存
- 啟動圖形
功能支持檢查
為了確保在目標設備上支持工作圖形,必須調用 CheckFeaturesSupport
并檢查 D3D12_FEATURE_DATA_D3D12_OPTIONS_21
結構中的 WorkGraphsTier
預訓練模型。
編譯著色器
工作圖形需要將 HLSL 源代碼編譯為lib_6_8
著色器目標。除了其他著色器類型使用的標準開關之外,無需向編譯器傳遞任何特定要求。
加載著色器庫
編譯器的二進制文件必須加載并D3D12_STATE_OBJECT_DESC
必須準備具備工作圖形所需的所有部件。必須提供大量信息,包括:
- 要使用的 DXIL 庫 (即預編譯的著色器二進制文件)。
- 工作圖形使用的根簽名。
- 哪些節點組成圖形 (所有節點或某個特定子集)。
- 覆蓋已在著色器庫中設置的某些屬性 (如果需要)。這些覆蓋可以使用運行時確定的值驅動工作圖中的靜態值。
分配背景內存
此外,圖形還需要在執行期間使用輔助內存。在創建狀態對象后,必須查詢圖形所需的輔助內存,并在啟動圖形之前進行分配。如果報告的輔助內存要求的最小大小不同于最大大小,則建議遵循最大大小,以實現圖形執行的最佳性能。
啟動圖形
此步驟與啟動計算著色器非常相似。命令列表必須處于良好狀態,其中包括描述符緩沖區、根簽名、根參數和描述符表。
啟動圖形需要調用SetProgram
在命令列表中首先執行此調用。此調用指定要啟動的圖形、它的后盾內存,以及如果需要的其他啟動標志。
一個重要的標志是D3D12_SET_WORK_GRAPH_FLAG_INITIALIZE
.這個標志必須在工作圖形首次使用其后備內存時傳遞。如果后備內存未被圖形以外的其他內容使用,則后續啟動可以省略此標志。
最后,調用DispatchGraph
此時,可以為圖形的根指定輸入記錄,這些記錄可以從 CPU 或 GPU 顯存提供。
參考示例代碼的LoadWorkGraphPipelines
和PopulateDeferredShadingWorkGraph
函數顯示和說明整個 CPU 端設置過程中的每個步驟。
使用工作圖表表達算法
了解工作圖如何運作以及它們的功能和局限性至關重要。借此了解,您可以選擇任何算法,并決定如何以更高效的方式將其表示為工作圖。
工作圖形推動數據流和轉換操作,即必須通過一系列步驟流動的獨立數據,并且可能在流程中擴展,最終達到最終結果。
使用此版本的工作圖形時需要考慮以下事項:
- 除 root 輸入記錄、具體數字或上限外,圖形中的工作大小和潛在擴展必須指定。例如,廣播啟動節點必須使用固定的分配網格大小,或者為其提供上限。作為開發者,您必須能夠從算法和其輸入的潛在大小中獲得這些數字。
- 節點只能使用一種輸入記錄類型。不允許使用多種輸入記錄類型。這意味著單個節點不能作為不同生產者的“join”目標。雖然可以通過手動實現此類“join”來繞過此問題,但我建議避免這種情況。此類“join”意味著數據記錄在圖形執行內存中停留,直到所有輸入記錄準備就緒,并且“join”目標可以啟動。請注意,融合節點不適用于解決此問題,因為它們的啟動條件不保證輸入記錄的數量,并且可能啟動的數量較低。
- 圖形中不能包含任何循環,唯一例外是節點可以循環到自身。
- 圖形的深度不得超過 32 個節點。每個節點的最大自環數也會計入這個數字。
- 節點目前還不能生成繪圖調用,但請記住,可以使用
TraceRayInline
。 - 資源在執行工作圖形期間無法轉換到不同的狀態。
總結
Direct3D 12 API 中的工作圖形幫助解決了一些知名問題,并啟發了新的創意想法。在本文中,我介紹了不同的啟動節點,以及如何通過記錄傳遞數據。我還討論了工作圖形的 HLSL 代碼,以及從 CPU 啟動工作圖的步驟。最后,我介紹了此功能背后的心智模型,以及如何最好地將 GPU 算法映射到此版本的工作圖。如需了解構建和運行工作圖所需的所有詳細信息,請訪問 GitHub 的?NVIDIAGameWorks/donut_examples。
如需詳細了解工作圖形 (包括高級主題和案例研究),請參閱 Direct3D 12 中的工作圖:延遲著色案例研究。
?