剖析虛幻渲染體系(11)- RDG

胡安尼塔 2024-06-12 19:24 11次浏览 0 条评论 taohigo.com

11.1 本篇概述

RDG全稱是Rendering Dependency Graph,意為渲染依賴性圖表,是UE4.22開始引進的全新的渲染子系統,基於有向無環圖(Directed Acyclic Graph,DAG)的調度系統,用於執行渲染管線的整幀優化。

它利用現代的圖形API(DirectX 12、Vulkan和Metal 2),實現自動異步計算調度以及更高效的內存管理和屏障管理來提升性能。

傳統的圖形API(DirectX 11、OpenGL)要求驅動器調用復雜的啟發法,以確定何時以及如何在GPU上執行關鍵的調度操作。例如清空緩存,管理和再使用內存,執行佈局轉換等等。由於接口存在即時模式特性,因此需要復雜的記錄和狀態跟蹤才能處理各種極端情況。這些情況最終會對性能產生負面影響,並阻礙並行。

現代的圖形API(DirectX 12、Vulkan和Metal 2)與傳統圖形API不同,將低級GPU管理的負擔轉移到應用程序。這使得應用程序可以利用渲染管線的高級情境來驅動調度,從而提高性能並且簡化渲染堆棧。

RDG的理念不在GPU上立即執行Pass,而是先收集所有需要渲染的Pass,然後按照依賴的順序對圖表進行編譯和執行,期間會執行各類裁剪和優化。

依賴性圖表數據結構的整幀認知與現代圖形API的能力相結合,使RDG能夠在後臺執行復雜的調度任務:

  • 執行異步計算通道的自動調度和隔離。
  • 在幀的不相交間隔期間,使資源之間的別名內存保持活躍狀態。
  • 盡早啟動屏障和佈局轉換,避免管線延遲。

此外,RDG利用依賴性圖表在通道設置期間提供豐富的驗證,對影響功能和性能的問題進行自動捕捉,從而改進開發流程。

RDG並非UE獨創的概念和技術,早在2017年的GDC中,寒霜就已經實現並應用瞭Frame Graph(幀圖)的技術。Frame Graph旨在將引擎的各類渲染功能(Feature)和上層渲染邏輯(Renderer)和下層資源(Shader、RenderContext、圖形API等)隔離開來,以便做進一步的解耦、優化,其中最重要的就是多線程和並行渲染。

FrameGraph是高層級的Render Pass和資源的代表,包含瞭一幀中所用到的所有信息。Pass之間可以指定順序和依賴關系,下圖是其中的一個示例:

寒霜引擎采用幀圖方式實現的延遲渲染的順序和依賴圖。

可以毫不誇張地說,UE的RDG正是基於Frame Graph之上定制和實現而成的。到瞭UE4.26,RDG已經被大量普及,包含場景渲染、後處理、光追等等模塊都使用瞭RDG代替原本直接調用RHI命令的方式。

本篇主要闡述UE RDG的以下內容:

  • RDG的基本概念和類型。
  • RDG的使用方法。
  • RDG的內部機制和原理。

11.2 RDG基礎

本章先闡述RDG涉及的主要類型、概念、接口等。

11.2.1 RDG基礎類型

RDG基礎類型和接口主要集中於RenderGraphUtils.h和RenderGraphDefinitions.h之中。部分解析如下:

// EngineSourceRuntimeRenderCorePublicRenderGraphDefinitions.h

// RDG Pass類型.
enum class ERDGPassFlags : uint8
{
None = 0, // 用於無參數的AddPass函數.
Raster = 1 << 0, // Pass在圖形管道上使用光柵化.
Compute = 1 << 1, // Pass在圖形管道上使用compute.
AsyncCompute = 1 << 2, // Pass在異步計算管道上使用compute
Copy = 1 << 3, // Pass在圖形管道上使用復制命令.
NeverCull = 1 << 4, // 不被裁剪優化, 用於特殊pass.
SkipRenderPass = 1 << 5, // 忽略BeginRenderPass/EndRenderPass, 留給用戶去調用. 隻在Raster綁定時有效. 將禁用Pass合並.
UntrackedAccess = 1 << 6, // Pass訪問原始的RHI資源,這些資源可能被註冊到RDG中,但所有資源都保持在當前狀態. 此標志阻止圖形調度跨越通道的分割障礙。任何拆分都被延遲到pass執行之後。資源可能不會在pass執行過程中改變狀態。影響性能的屏障。不能與AsyncCompute組合。
Readback = Copy | NeverCull, // Pass使用復制命令,但寫入暫存資源(staging resource).

CommandMask = Raster | Compute | AsyncCompute | Copy, // 標志掩碼,表示提交給pass的RHI命令的類型.
ScopeMask = NeverCull | UntrackedAccess // 可由傳遞標志作用域使用的標志掩碼
};

// Buffer標記.
enum class ERDGBufferFlags : uint8
{
None = 0, // 無標記.
MultiFrame = 1 << 0 // 存續於多幀.
};

// 紋理標記.
enum class ERDGTextureFlags : uint8
{
None = 0,
MultiFrame = 1 << 0, // 存續於多幀.
MaintainCompression = 1 << 1, // 防止在此紋理上解壓元數據.
};

// UAV標記.
enum class ERDGUnorderedAccessViewFlags : uint8
{
None = 0,
SkipBarrier = 1 << 0 // 忽略屏障.
};

// 父資源類型.
enum class ERDGParentResourceType : uint8
{
Texture,
Buffer,
MAX
};

// 視圖類型.
enum class ERDGViewType : uint8
{
TextureUAV, // 紋理UAV(用於寫入數據)
TextureSRV, // 紋理SRV(用於讀取數據)
BufferUAV, // 緩沖UAV(用於寫入數據)
BufferSRV, // 緩沖SRV(用於讀取數據)
MAX
};

// 用於在創建視圖時指定紋理元數據平面
enum class ERDGTextureMetaDataAccess : uint8
{
None = 0, // 主平面默認壓縮使用.
CompressedSurface, // 主平面不壓縮使用.
Depth, // 深度平面默認壓縮使用.
Stencil, // 模板平面默認壓縮使用.
HTile, // HTile平面.
FMask, // FMask平面.
CMask // CMask平面.
};

// 簡單的C++對象分配器, 用MemStack分配器追蹤和銷毀物體.
class FRDGAllocator final
{
public:
FRDGAllocator();
~FRDGAllocator();

// 分配原始內存.
FORCEINLINE void* Alloc(uint32 SizeInBytes, uint32 AlignInBytes)
{
return MemStack.Alloc(SizeInBytes, AlignInBytes);
}
// 分配POD內存而不跟蹤析構函數.
template <typename PODType>
FORCEINLINE PODType* AllocPOD()
{
return reinterpret_cast<PODType*>(Alloc(sizeof(PODType), alignof(PODType)));
}
// 帶析構追蹤的C++對象分配.
template <typename ObjectType, typename... TArgs>
FORCEINLINE ObjectType* AllocObject(TArgs&&... Args)
{
TTrackedAlloc<ObjectType>* TrackedAlloc = new(MemStack) TTrackedAlloc<ObjectType>(Forward<TArgs&&>(Args)...);
check(TrackedAlloc);
TrackedAllocs.Add(TrackedAlloc);
return TrackedAlloc->Get();
}
// 不帶析構追蹤的C++對象分配. (危險, 慎用)
template <typename ObjectType, typename... TArgs>
FORCEINLINE ObjectType* AllocNoDestruct(TArgs&&... Args)
{
return new (MemStack) ObjectType(Forward<TArgs&&>(Args)...);
}

// 釋放全部已分配的內存.
void ReleaseAll();

private:
class FTrackedAlloc
{
public:
virtual ~FTrackedAlloc() = default;
};

template <typename ObjectType>
class TTrackedAlloc : public FTrackedAlloc
{
public:
template <typename... TArgs>
FORCEINLINE TTrackedAlloc(TArgs&&... Args) : Object(Forward<TArgs&&>(Args)...) {}

FORCEINLINE ObjectType* Get() { return &Object; }

private:
ObjectType Object;
};

// 分配器.
FMemStackBase MemStack;
// 所有已分配的對象.
TArray<FTrackedAlloc*, SceneRenderingAllocator> TrackedAllocs;
};

// EngineSourceRuntimeRenderCorePublicRenderGraphUtils.h

// 清理未使用的資源.
extern RENDERCORE_API void ClearUnusedGraphResourcesImpl(const FShaderParameterBindings& ShaderBindings, ...);
(......)

// 註冊外部紋理, 可附帶備用實例.
FRDGTextureRef RegisterExternalTextureWithFallback(FRDGBuilder& GraphBuilder, ...);
inline FRDGTextureRef TryRegisterExternalTexture(FRDGBuilder& GraphBuilder, ...);
inline FRDGBufferRef TryRegisterExternalBuffer(FRDGBuilder& GraphBuilder, ...);

// 計算著色器的工具類.
struct RENDERCORE_API FComputeShaderUtils
{
// 理想的組大小為8x8,在GCN上至少占據一個wave,在Nvidia上占據兩個warp.
static constexpr int32 kGolden2DGroupSize = 8;
static FIntVector GetGroupCount(const int32 ThreadCount, const int32 GroupSize);

// 派發計算著色器到RHI命令列表, 攜帶其參數.
template<typename TShaderClass>
static void Dispatch(FRHIComputeCommandList& RHICmdList, const TShaderRef<TShaderClass>& ComputeShader, const typename TShaderClass::FParameters& Parameters, FIntVector GroupCount);
// 派發非直接的計算著色器到RHI命令列表, 攜帶其參數.
template<typename TShaderClass>
static void DispatchIndirect(FRHIComputeCommandList& RHICmdList, const TShaderRef<TShaderClass>& ComputeShader, const typename TShaderClass::FParameters& Parameters, FRHIVertexBuffer* IndirectArgsBuffer, uint32 IndirectArgOffset);
// 派發計算著色器到render graph builder, 攜帶其參數.
template<typename TShaderClass>
static void AddPass(FRDGBuilder& GraphBuilder,FRDGEventName&& PassName,ERDGPassFlags PassFlags,const TShaderRef<TShaderClass>& ComputeShader,typename TShaderClass::FParameters* Parameters,FIntVector GroupCount);

(......)

// 清理UAV.
static void ClearUAV(FRDGBuilder& GraphBuilder, FGlobalShaderMap* ShaderMap, FRDGBufferUAVRef UAV, uint32 ClearValue);
static void ClearUAV(FRDGBuilder& GraphBuilder, FGlobalShaderMap* ShaderMap, FRDGBufferUAVRef UAV, FVector4 ClearValue);
};

// 增加拷貝紋理Pass.
void AddCopyTexturePass(FRDGBuilder& GraphBuilder, FRDGTextureRef InputTexture, FRDGTextureRef OutputTexture, const FRHICopyTextureInfo& CopyInfo);
(......)

// 增加拷貝到解析目標的Pass.
void AddCopyToResolveTargetPass(FRDGBuilder& GraphBuilder, FRDGTextureRef InputTexture, FRDGTextureRef OutputTexture, const FResolveParams& ResolveParams);

// 清理各類資源的Pass.
void AddClearUAVPass(FRDGBuilder& GraphBuilder, FRDGBufferUAVRef BufferUAV, uint32 Value);
void AddClearUAVFloatPass(FRDGBuilder& GraphBuilder, FRDGBufferUAVRef BufferUAV, float Value);
void AddClearUAVPass(FRDGBuilder& GraphBuilder, FRDGTextureUAVRef TextureUAV, const FUintVector4& ClearValues);
void AddClearRenderTargetPass(FRDGBuilder& GraphBuilder, FRDGTextureRef Texture);
void AddClearDepthStencilPass(FRDGBuilder& GraphBuilder,FRDGTextureRef Texture,bool bClearDepth,float Depth,bool bClearStencil,uint8 Stencil);
void AddClearStencilPass(FRDGBuilder& GraphBuilder, FRDGTextureRef Texture);
(......)

// 增加回讀紋理的Pass.
void AddEnqueueCopyPass(FRDGBuilder& GraphBuilder, FRHIGPUTextureReadback* Readback, FRDGTextureRef SourceTexture, FResolveRect Rect = FResolveRect());
// 增加回讀緩沖區的Pass.
void AddEnqueueCopyPass(FRDGBuilder& GraphBuilder, FRHIGPUBufferReadback* Readback, FRDGBufferRef SourceBuffer, uint32 NumBytes);

// 創建資源.
FRDGBufferRef CreateStructuredBuffer(FRDGBuilder& GraphBuilder, ...);
FRDGBufferRef CreateVertexBuffer(FRDGBuilder& GraphBuilder, ...);

// 無參數的Pass增加.
template <typename ExecuteLambdaType>
void AddPass(FRDGBuilder& GraphBuilder, FRDGEventName&& Name, ExecuteLambdaType&& ExecuteLambda);
template <typename ExecuteLambdaType>
void AddPass(FRDGBuilder& GraphBuilder, ExecuteLambdaType&& ExecuteLambda);

// 其它特殊Pass
void AddBeginUAVOverlapPass(FRDGBuilder& GraphBuilder);
void AddEndUAVOverlapPass(FRDGBuilder& GraphBuilder);

(......)