レンダリング依存関係グラフ

レンダリング コマンドがコンパイルおよび実行されるようにグラフ データ構造に記録する即時モード API。

レンダリング依存関係グラフ はレンダリング グラフまたは RDG とも呼ばれ、レンダリング コマンドがコンパイルおよび実行されるようにグラフ データ構造に記録する即時モードのアプリケーション プログラミング インターフェース (API) です。RDG は、エラーが発生しやすい処理を自動化することで高レベルのレンダリング コードを単純化し、グラフをトラバースしてメモリ使用量を最適化し、CPU および GPU でのレンダリング パスを並列化します。

レンダリング依存関係グラフには以下の機能があります。

  • 非同期コンピュート フェンスのスケジューリング

  • 最適な存続期間およびメモリ エイリアシングでの非常駐リソースの割り当て

  • レイテンシーを隠し、GPU でのオーバーラップを改善する分割バリアを使用したサブリソースの遷移

  • 並列コマンド リスト記録

  • グラフで使用されていないリソースおよびパスのカリング

  • API の使用方法およびリソース依存関係の検証

  • RDG Insights でのグラフ構造およびメモリ存続期間のビジュアライゼーション

レンダリング グラフ API は、ディファード レンダラとモバイル レンダラの両方、および関連するプラグインに合わせて変換されています。高レベルのすべてのレンダリング コードは RDG を使用して記述します。特に上記の高度な機能が必要とされる場合は必須です。

RDG プログラミング ガイド

このガイドでは、シェーダーおよびレンダリング ハードウェア インターフェース (RHI) についてある程度の知識があって、低レベルのレンダリング機能を C++ で記述するプログラマー向けに、例を通して API の使用方法の実例を示し、その根底にある概念についても説明しています。

シェーダー パラメータ構造

RDG はシェーダー パラメータ構造システムへの拡張を使用して、グラフの依存関係を表現します。

HLSL ソース ファイルで宣言されている以下のシェーダー パラメータを考慮します。

HLSL ソース ファイルにあるシェーダー入力:

float2 ViewportSize;
float4 Hello;
float World;
float3 FooBarArray[16];

Texture2D BlueNoiseTexture;
SamplerState BlueNoiseSampler;

Texture2D SceneColorTexture;
SamplerState SceneColorSampler;

RWTexture2D<float4> SceneColorOutput;

上記のシェーダー パラメータはフラットな C++ データ構造体として表現することもできます。

架空の等価な C++ コード:

struct FMyShaderParameters
{
    FVector2D ViewportSize;
    FVector4 Hello;
    float World;
    FVector FooBarArray[16];

    FRHITexture*        BlueNoiseTexture = nullptr;
    FRHISamplerState*   BlueNoiseSampler = nullptr;

    FRHITexture*        SceneColorTexture = nullptr;
    FRHISamplerState*   SceneColorSampler = nullptr;

    FRHIUnorderedAccessView* SceneColorOutput = nullptr;
};

シェーダー パラメータ構造システムでは、これを実現するために一連の宣言マクロが使用されています。

シェーダー パラメータ構造:

BEGIN_SHADER_PARAMETER_STRUCT(FMyShaderParameters, /** MODULE_API_TAG */)
    SHADER_PARAMETER(FVector2D, ViewportSize)
    SHADER_PARAMETER(FVector4, Hello)
    SHADER_PARAMETER(float, World)
    SHADER_PARAMETER_ARRAY(FVector, FooBarArray, [16])

    SHADER_PARAMETER_TEXTURE(Texture2D, BlueNoiseTexture)
    SHADER_PARAMETER_SAMPLER(SamplerState, BlueNoiseSampler)

    SHADER_PARAMETER_TEXTURE(Texture2D, SceneColorTexture)
    SHADER_PARAMETER_SAMPLER(SamplerState, SceneColorSampler)

    SHADER_PARAMETER_UAV(RWTexture2D, SceneColorOutput)
END_SHADER_PARAMETER_STRUCT()

これらのマクロは、フラットで等価な C++ データ構造体、およびその構造体のスタティック メンバとしてアクセスできるコンパイル時リフレクション メタデータを生成します。

コンパイル時リフレクション メタデータ:

const FShaderParametersMetadata* ParameterMetadata = FMyShaderParameters::FTypeInfo::GetStructMetadata();

このメタデータによって、パラメータを RHI に動的にバインドするために必要となる、構造体のランタイム トラバーサルが可能になります。各メンバについて利用可能な情報には、名前C++ の型HLSL の型バイト オフセット などがあります。

詳細については FShaderParametersMetadata::FMember を参照してください。RDG はこのメタデータを当てにして パス パラメータ (このページで後述) をトラバースします。

シェーダー バインディング

シェーダー パラメータ構造体は FShader と組み合わされて、RHI コマンド リストにサブミットするために必要なバインディングが生成されます。

そうするために、パラメータ構造体を FShader 派生クラスで FParameters 型として宣言します。

それを行うには、インライン定義または typedef ディレクティブを使用します。そして、SHADER_USE_PARAMETER_STRUCT マクロを使用して、バインディングを登録するクラスのコンストラクタを生成します。

最初のシェーダー クラス:

class FMyShaderCS : public FGlobalShader
{
    DECLARE_GLOBAL_SHADER(FMyShaderCS);

    // Generates a constructor which will register FParameter bindings with this FShader instance.
    SHADER_USE_PARAMETER_STRUCT(FMyShaderCS, FGlobalShader);

    // Assign an FParameters type to the shader--either with an inline definition or using directive.
    using FParameters = FMyShaderParameters;
};

シェーダー パラメータを RHI コマンド リストにバインドするには、その構造体をインスタンス化し、そこにデータを入れ、SetShaderParameters ユーティリティ関数を呼び出します。

パラメータの割り当て:

TShaderMapRef<FMyShaderCS> ComputeShader(View.ShaderMap);
RHICmdList.SetComputeShader(ComputeShader.GetComputeShader());

FMyShaderCS::FParameters ShaderParameters;

// Assign the parameters.
ShaderParameters.ViewportSize = View.ViewRect.Size();
ShaderParameters.World = 1.0f;
ShaderParameters.FooBarArray[4] = FVector(1.0f, 0.5f, 0.5f);

// Submit the parameters.
SetShaderParameters(RHICmdList, ComputeShader, ComputeShader.GetComputeShader(), Parameters);

RHICmdList.DispatchComputeShader(GroupCount.X, GroupCount.Y, GroupCount.Z);

ユニフォーム バッファ

ユニフォーム バッファ はシェーダー パラメータを、それ自体がシェーダー パラメータとしてバインドされている 1 つの RHI リソースにグループ化します。各ユニフォーム バッファでは HLSL のグローバル名前空間が定義されています。ユニフォーム バッファは BEGIN_UNIFORM_BUFFER_STRUCT マクロと END_UNIFORM_BUFFER_STRUCT マクロを使用して宣言されています。

ユニフォーム バッファの定義:

BEGIN_UNIFORM_BUFFER_STRUCT(FSceneTextureUniformParameters, RENDERER_API)
    SHADER_PARAMETER_TEXTURE(Texture2D, SceneColorTexture)
    SHADER_PARAMETER_SAMPLER(SamplerState, SceneColorTextureSampler)
    SHADER_PARAMETER_TEXTURE(Texture2D, SceneDepthTexture)
    SHADER_PARAMETER_SAMPLER(SamplerState, SceneDepthTextureSampler)

    // ...
END_UNIFORM_BUFFER_STRUCT()

C++ ソース ファイルで IMPLEMENT_UNIFORM_BUFFER_STRUCT を使用して、ユニフォーム バッファ定義をシェーダー システムに登録し、その HLSL 定義を生成します。

ユニフォーム バッファの実装:

IMPLEMENT_UNIFORM_BUFFER_STRUCT(FSceneTextureUniformParameters, "SceneTexturesStruct")

ユニフォーム バッファのパラメータは、UniformBuffer.Member 構文を使用してコンパイルおよびアクセスされるシェーダーによって自動的に生成されます。

HLSL でのユニフォーム バッファ:

// Generated file containing uniform buffer declarations.Automatically included by Common.ush.
#include "/Engine/Generated/GeneratedUniformBuffers.ush"

// Reference uniform buffer members like a struct.
Texture2DSample(SceneTexturesStruct.SceneColorTexture, SceneTexturesStruct.SceneColorTextureSampler);

これで、SHADER_PARAMTER_STRUCT_REF マクロを使用して、親シェーダー パラメータ構造体でユニフォーム バッファをパラメータとしてインクルードできます。

SHADER_PARAMETER_STRUCT_REF:

BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
    // ...

    // Defines a refcounted TUniformBufferRef<FSceneTextureUniformParameters> instance.
    SHADER_PARAMETER_STRUCT_REF(FSceneTextureUniformParameters, SceneTextures)
END_SHADER_PARAMETER_STRUCT()

静的バインディング

シェーダー パラメータは各シェーダーに対して一意にバインドされているので、各シェーダー ステージ (頂点、ピクセル、など) に固有のシェーダーが必要です。シェーダーは、Set{Graphics, Compute}PipelineState を使用して、Pipeline State Object (PSO) としてまとめて 1 つの RHI コマンド リストにバインドされます。

パイプライン状態をコマンド リストにバインドすると、すべてのシェーダー バインディングが無効になります。

そのため、PSO を設定した後に、すべてのシェーダー パラメータをバインドする必要があります。たとえば、1 つの PSO を共有するドロー コールのセットに対するコマンドのフローについて考えてみます。

  • PSO A を設定する

  • 各ドロー コールに対して次のことを行う

    • 頂点シェーダー パラメータを設定する

    • ピクセル シェーダー パラメータを設定する

    • 描画する

  • PSO B を設定する

  • 各ドロー コールに対して次のことを行う

    • 頂点シェーダー パラメータを設定する

    • ピクセル シェーダー パラメータを設定する

    • 描画する

このやり方での問題点は、レンダラのメッシュ描画コマンドはキャッシュされて、複数のパスとビューで共有されることです。フレームごとにパス/ビューの各組み合わせに対して描画コマンドの独自のセットを生成するのは非常に非効率的です。しかし、メッシュ描画コマンドは、パス/ビューのユニフォーム バッファ リソースを適切にバインドするために、それを知っている必要もあります。この問題を解決するために、ユニフォーム バッファに 静的 バインディング モデルが使用されています。

静的バインディングで宣言すると、ユニフォーム バッファは、個々のシェーダーそれぞれの 固有スロット ではなく、RHI コマンド リストに直接つながっている 静的スロット にバインドされています。コマンド リストは、ユニフォーム バッファがシェーダーによってリクエストされたときに、その静的スロットからバインディングを直接取り出します。これで、バインディングは PSO の頻度ではなく パス の頻度で発生することになります。

前述の例について考えてみますが、今度はシェーダー入力が静的ユニフォーム バッファから供給されます。

  • 静的ユニフォーム バッファを設定する

  • PSO A を設定する

  • 各ドロー コールに対して次のことを行う

    • 描画する

  • PSO B を設定する

  • 各ドロー コールに対して次のことを行う

    • 描画する

このモデルでは、各ドロー コールがコマンド リストからシェーダー バインディングを継承することができます。

静的ユニフォーム バッファを割り当てる

静的バインディングを持つユニフォーム バッファを定義するには IMPLEMENT_STATIC_UNIFORM_BUFFER_STRUCT マクロを使用します。追加のスロット宣言が必要であり、IMPLEMENT_STATIC_UNIFORM_BUFFER_SLOT マクロを使用して指定されています。

複数の静的ユニフォーム バッファ定義で同じ静的スロットを参照できますが、一度にバインドできるのはその中の 1 つのみです。可能であればスロットを再利用して、エンジン内でのスロットの総数を減らすのが最適です。

静的ユニフォーム バッファ:

// Defines a unique static slot by name.
IMPLEMENT_STATIC_UNIFORM_BUFFER_SLOT(SceneTextures);

// Defines the SceneTexturesStruct uniform buffer with a static binding to the SceneTextures slot.
IMPLEMENT_STATIC_UNIFORM_BUFFER_STRUCT(FSceneTextureUniformParameters, "SceneTexturesStruct", SceneTextures);

// Defines the MobileSceneTextures uniform buffer with the same static slot.Only one is bound at a time.
IMPLEMENT_STATIC_UNIFORM_BUFFER_STRUCT(FMobileSceneTextureUniformParameters, "MobileSceneTextures", SceneTextures);

RHICmdList.SetStaticUniformBuffers メソッドを使用して静的ユニフォーム バッファをバインドします。RDG は各パスを実行する前に静的ユニフォーム バッファをコマンド リストに自動的にバインドします。どの静的ユニフォーム バッファもパス パラメータ構造体に含める必要があります。

レンダリング グラフ ビルダー

レンダリング依存関係グラフは使いやすいように設計されています。

  • FRDGBuilder インスタンスをインスタンス化し、リソースを作成し、パスを追加してグラフを設定します。その後に、FRDGBuilder::Execute を呼び出してそのグラフをコンパイルおよび実行します。

  • FRDGBuilder::CreateTexture を使用してテクスチャを作成するか、FRDGBuilder::CreateBuffer を使用してバッファを作成します。

    • これらのメソッドは記述子のみを作成します。下層の RHI リソースは後で実行時に割り当てられます。

  • FRDGBuilder::AddPass 関数を使用し、パス パラメータ構造体および実行ラムダ式を引数として指定して、パスを追加します。

    • パス パラメータ構造体は、シェーダー パラメータ構造体に、RDG リソースが入っているパラメータが追加されて拡張されています。

      • RDG はこれらのパラメータを使用して、グラフ内のパスと非常駐リソースの存続期間の間の依存関係を導出します。

      • GraphBuilder::AllocParameters を使用してパス パラメータを割り当て、実行ラムダ式で使用されるすべての関連 RDG リソースを割り当てます。

    • パス実行ラムダ式は、グラフの実行時に RHI コマンド リストにサブミットするための操作を記録します。

      • コンピュート パス (非同期コンピュートとグラフィックス コンピュートの間の共有インターフェース) には FRHIComputeCommandList を使用し、ラスタ パスには FRHICommandList を使用します。

      • FRHICommandListImmediate を使用するとパスが並列に実行されなくなるため、厳密に必要である場合以外に使用することは避けます。

      • すべてのパス ラムダ式がスレッド セーフであることが理想的ですが、実際にはいくつかのパスでは実行時に RHI リソースの作成またはロックを必要としていて、それらはレンダリング スレッドで行われる必要があります。これらのケースでは即時コマンド リストを使用します。

次のサンプル コード スニペットは出発点となるサンプルです。

グラフ ビルダー:

{
    FRDGBuilder GraphBuilder(RHICmdList);

    FMyShaderCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FMyShaderCS::FParameters>();
    //...
    PassParameters->SceneColorTexture = SceneColor;
    PassParameters->SceneColorSampler = TStaticSamplerState<SF_Point, AM_Clamp, AM_Clamp>::GetRHI();
    PassParameters->SceneColorOutput = GraphBuilder.CreateUAV(NewSceneColor);

    GraphBuilder.AddPass(
        // Friendly name of the pass for profilers using printf semantics.
        RDG_EVENT_NAME("MyShader %d%d", View.ViewRect.Width(), View.ViewRect.Height()),
        // Parameters provided to RDG.
        PassParameters,
        // Issues compute commands.
        ERDGPassFlags::Compute,
        // This is deferred until Execute.May execute in parallel with other passes.
        [PassParameters, ComputeShader, GroupCount] (FRHIComputeCommandList& RHICmdList)
    {
        FComputeShaderUtils::Dispatch(RHICmdList, ComputeShader, PassParameters, GroupCount);
    });

    // Execute the graph.
    GraphBuilder.Execute();
}

グラフ ビルダーの API はシングルスレッドであり、一度にインスタンス化できるのは 1 つのインスタンスのみであるため、階層グラフや横並びグラフには対応していません。ディファード レンダラとモバイル レンダラの両方で、シーン レンダーの呼び出しごとに 1 つのビルダー インスタンスが使用されます。

タイムラインのセットアップと実行

RDG はレンダリング パイプラインを セットアップ実行 の 2 つのタイムラインに分割します。

グラフはセットアップのタイムラインでビルドされます。セットアップのタイムラインでは、リソース作成およびレンダリング パイプライン構成の分岐処理が行われます。すべての RHI コマンドは、実行のタイムラインで呼び出されるパスのラムダ式に先送りされます。

パスの実行は並列化される可能性があるので、パスのラムダ式で指定されているコードは、サイド エフェクトがなく、コマンドをコマンド リストに記録するだけである必要があります。

Setup and execute timelines with and without RDG.

上図は RDG がある場合とない場合のレンダリング パイプラインのタイムラインを表しています。

RDG がない場合の図では、レンダリング機能は、セットアップと実行の両方が行われる単一のタイムラインで記述されています。RHI コマンドが記録され、パイプラインの分岐処理およびリソース割り当てに合わせて直接サブミットされます。

RDG がある場合は、ユーザー提供のパス実行ラムダ式によって、セットアップのコードが実行から切り離されます。RDG はパス実行ラムダ式を呼び出す前に追加のコンパイル手順を行い、実行は複数のスレッドにわたって行われ、ラムダ式を呼び出してレンダリング コマンドを RHI コマンド リストに記録します。

RDG のユーティリティ関数

レンダリング依存関係グラフには、よく使用されるパスを追加するためのいくつかの便利なユーティリティ関数が付属していて、RenderGraphUtils.h で定義されています。エンジン全体でのボイラープレートを減らすために、可能であればこれらのユーティリティ関数を使用します。

たとえば、コンピュート シェーダー パスには FComputeShaderUtils::AddPass を使用し、フルスクリーンのピクセル シェーダー パスには FPixelShaderUtils::AddFullscreenPass を使用します。

この後の例は、教育目的で少し冗長に記述されています。可能であればユーティリティ関数を使用します。

RDG のリソースとビュー

RDG リソースには、最初は RHI リソース記述子が入っています。関連付けられている RHI リソースにアクセスできるのは、そのリソースがパス パラメータとして宣言されているパスのラムダ式内のみです。すべての RDG リソースは、指定されているサブクラス タイプに対して FRDGResource::GetRHI オーバーロードを提供します。このメソッドへのアクセスは、パスのラムダ式に限定されていて、このメソッドが不適切に呼び出されると検証レイヤーがアサートします。

以下のプロパティは、バッファ リソースおよび テクスチャ リソースに特有です。

  • リソースは 非常駐 であることがあります。その場合、存続期間はそのグラフに限定され、メモリでは存続期間がバラバラである他の非常駐リソースのエイリアスになっていることがあります。

  • または、リソースは 外部 であることがあります。その場合、存続期間はそのグラフの外部で延長されます。これは、ユーザーが既存の RHI リソースをグラフに登録した場合や、実行が完了した後にグラフからリソースを抽出した場合に発生します。

RDG のバッファとテクスチャは、RDG の順序付けされていないアクセス用またはシェーダー リソース ビュー用に特化されていることがあります。他の RDG リソースと同様に、下層の RHI リソースは実行時にオンデマンドで割り当てられ、そのリソースがパラメータとして宣言されているパスに対してのみアクセスが許可されます。 次のサンプル コードは、テクスチャ、バッファ、ビューといったリソースを作成する方法の実例を示しています。

リソース作成の例:

// Create a new transient texture instance.No GPU memory is allocated at this point, just the descriptor.
FRDGTexture* Texture = GraphBuilder.CreateTexture(FRDGTextureDesc::Create2D(...), TEXT("MyTexture"));

// Invalid!Will trigger an assert.This is only allowed in a pass lambda if declared on the pass!
FRHITexture* TextureRHI = Texture->GetRHI();

// Create a new UAV referencing the texture at a specific mip level.
FRDGTextureUAV* TextureUAV = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(Texture, MipLevel));

// Invalid!
FRHIUnorderedAccessView* UAVRHI = TextureUAV->GetRHI();

// Create a new transient structured buffer instance.
FRDGBuffer* Buffer = GraphBuilder.CreateBuffer(FRDGBufferDesc::CreateBufferDesc(...), TEXT("MyBuffer"));

// Invalid!
FRHIBuffer* BufferRHI= Buffer->GetRHI();

// Create a new SRV referencing the buffer with an R32 float format.
FRDGBufferSRV* BufferSRV = GraphBuilder.CreateSRV(Buffer, PF_R32_FLOAT);

// Invalid!
FRHIShaderResourceView* SRVRHI = TextureSRV->GetRHI();

RDG リソースのポインタはグラフ ビルダーが所有しているため、グラフ ビルダーが破棄されると、それらのポインタは無効になります。グラフの実行後は、すべてのポインタが null になっています。

パスとパラメータ

パス パラメータは FRDGBuilder::AllocParameters 関数を使用して割り当てられるため、正しいメモリ存続期間が保証されています。RDG は独自のマクロでシェーダー パラメータ構造体を拡張します。FRDGBuilder:AddPass はシェーダー パラメータのマクロを無視しながら、カスタム RDG マクロを消費します。

パス パラメータはシェーダー パラメータと意図的に組み合わされています。Unreal Engine 全体でのパス パラメータの大多数はシェーダー パラメータでもあります。それら両方を同じ API を使用して宣言すると、ボイラープレートが減ります。

以下の具体的な例は、パスとパラメータの使用方法を適切に示しています。

シェーダー パラメータの例

このシェーダー パラメータの例では、架空の単純なコンピュート シェーダーを宣言し、いくつかのシェーダー パラメータをそのシェーダーにバインドしていて、RDG はまったく関与していません。

これによって、RDG がシェーダー パラメータ構造体システムのどのような拡張であるかを示すのに役立つ基準が定められます。

コメント付きのコードを読んで、この例で何が起きているかをすべて理解してください。

C++ コードの例:

Texture2D MyTexture;
Texture2D MySRV;
RWTexture2D MyUAV;
float MyFloat;

C++ コードの例:

class FMyComputeShader: public FGlobalShader
{
public:
    DECLARE_GLOBAL_SHADER(FMyComputeShader);

    // Generates shader bindings for FParameters.
    SHADER_USE_PARAMETER_STRUCT(FMyComputeShader, FGlobalShader);

    // Inline shader parameter definition.Use FParameters name by convention.
    BEGIN_SHADER_PARAMETER_STRUCT(FParameters, /** MODULE_API */)

        // An FRHITexture* which maps to 'MyTexture' in HLSL code.
        SHADER_PARAMETER_TEXTURE(Texture2D, MyTexture)

        // An FRHIShaderResourceView* which maps to 'MySRV' in HLSL code.
        SHADER_PARAMETER_SRV(Texture2D, MySRV)

        // An FRHIUnorderedAccessView* which maps to 'MyUAV' in HLSL code.
        SHADER_PARAMETER_UAV(RWTexture2D, MyUAV)

        // A float shader parameter, which maps to 'MyFloat' in HLSL code.
        SHADER_PARAMETER(float, MyFloat)

    END_SHADER_PARAMETER_STRUCT()
};

IMPLEMENT_GLOBAL_SHADER(FMyComputeShader, "Path/To/Shader.usf", "MainCS", SF_Compute);

void Render(FRHICommandList& RHICmdList, TShaderMapRef<FMyComputeShader> ComputeShader)
{
    RHICmdList.SetComputeShader(ComputeShader.GetComputeShader());

    // Instantiate just like any C++ struct.
    FMyComputeShader::FParameters Parameters;

    FRHITexture* MyTexture = ...;
    Parameters.MyTexture = MyTexture;

    FRHIUnorderedAccessView* MyUAV = ...;
    Parameters.MyUAV = MyUAV;

    FRHIShaderResourceView* MySRV = ...;
    Parameters.MySRV = MySRV;

    Parameters.MyFloat = 1.0f;

    // Set the entire shader parameter struct on the RHI command list using bindings from the shader.
    SetShaderParameters(RHICmdList, ComputeShader, ComputeShader.GetComputeShader(), Parameters);

    // ...
}

C++ コードの例:

この例では、コードが RDG に変換されています。コンピュート シェーダーは、FParameters 構造体に RDG リソースを入れ、新しい RDG パスが追加されて、コンピュート シェーダー パラメータをバインドしています。

C++ コードの例:

class FMyComputeShader: public FGlobalShader
{
public:
    DECLARE_GLOBAL_SHADER(FMyComputeShader);
    SHADER_USE_PARAMETER_STRUCT(FMyComputeShader, FGlobalShader);

    BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )

        // Declares read access to an FRDGTexture* which maps to 'MyTexture' in HLSL code.
        SHADER_PARAMETER_RDG_TEXTURE(Texture2D, MyTexture)

        // Declares read access to an FRDGTextureSRV* which maps to 'MySRV' in HLSL code.
        SHADER_PARAMETER_RDG_TEXTURE_SRV(Texture2D, MySRV)

        // Declares write access to an FRDGTextureUAV* which maps to 'MyUAV' in HLSL code.
        SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D, MyUAV)

        // A float shader parameter, which maps to 'MyFloat' in HLSL code.Ignored by RDG.
        SHADER_PARAMETER(float, MyFloat)

    END_SHADER_PARAMETER_STRUCT()
};

IMPLEMENT_GLOBAL_SHADER(FMyCS, "Path/To/Shader.usf", "MainCS", SF_Compute);

void AddPass(FRDGBuilder& GraphBuilder, TShaderMapRef<FMyCS> ComputeShader)
{
    FMyComputeShader::FParameters* PassParameters = GraphBuilder.AllocParameters<FMyCS::FParameters>();

    // Consumed by FRDGBuilder::AddPass and SetShaderParameters
    FRDGTexture* MyTexture = ...;
    PassParameters->MyTexture = MyTexture;

    FRDGTextureUAV* MyUAV = ...;
    PassParameters->MyUAV = MyUAV;

    // NOTE:You can also assign null pointers just like with shader parameters.RDG will ignore null parameters.
    FRDGTextureSRV* MySRV = nullptr;
    PassParameters->MySRV = MySRV;

    // Ignored by FRDGBuilder::AddPass, consumed by SetShaderParameters
    PassParameters->MyFloat = 1.0f;

    // Adds a single compute pass to be executed later during GraphBuilder.Execute().The user provides the PassParameter struct
    // as well as a C++ lambda to call later during graph execution.ERDGPassFlags::Compute tells RDG that this pass will only
    // issue Compute commands (e.g. no Raster commands would be allowed).

    GraphBuilder.AddPass(
        RDG_EVENT_NAME("MyComputeShader"),
        PassParameters, // <- RDG will consume the pass parameters here.
        ERDGPassFlags::Compute,
        [ComputeShader, PassParameters /** <- PassParameters is marshaled into the lambda here */](FRHIComputeCommandList& RHICmdList)
    {
        RHICmdList.SetComputeShader(ComputeShader.GetComputeShader());

        // PassParameters are set here.All non-null RDG parameters are dereferenced to their respective RHI resources.
        SetShaderParameters(RHICmdList, ComputeShader, ComputeShader.GetComputeShader(), *PassParameters);

        // ...
    });
}

シェーダー パラメータがないパス パラメータの例

この例を引き続き使用して、RDG で単純な CopyTexture ユーティリティを実装することによって、一部のパス パラメータがどのようにシェーダーのセマンティクスを持たなくなるかを、次のコードで示しています。

これは、パスがシェーダーと 1 対 1 ではない場合や、シェーダーがまったく関与していない場合に便利です。

C++ コードの例:

BEGIN_SHADER_PARAMETER_STRUCT(FCopyTextureParameters, )

    // Declares CopySrc access to an FRDGTexture*
    RDG_TEXTURE_ACCESS(Input,  ERHIAccess::CopySrc)

    // Declares CopyDest access to an FRDGTexture*
    RDG_TEXTURE_ACCESS(Output, ERHIAccess::CopyDest)

END_SHADER_PARAMETER_STRUCT()

void AddCopyTexturePass(
    FRDGBuilder& GraphBuilder,
    FRDGTextureRef InputTexture,
    FRDGTextureRef OutputTexture,
    const FRHICopyTextureInfo& CopyInfo)
{
    FCopyTextureParameters* Parameters = GraphBuilder.AllocParameters<FCopyTextureParameters>();
    Parameters->Input = InputTexture;
    Parameters->Output = OutputTexture;

    GraphBuilder.AddPass(
        RDG_EVENT_NAME("CopyTexture(%s -> %s)", InputTexture->Name, OutputTexture->Name),
        Parameters,
        ERDGPassFlags::Copy,
        [InputTexture, OutputTexture, CopyInfo](FRHICommandList& RHICmdList)
    {
        RHICmdList.CopyTexture(InputTexture->GetRHI(), OutputTexture->GetRHI(), CopyInfo);
    });
}

ラスタ パス

RDG は、RENDER_TARGET_BINDING_SLOTS パラメータを通じて、ラスタ パス用の固定機能のレンダー ターゲットを公開しています。RHI はレンダリング パスを利用して、レンダー ターゲットをコマンド リストにバインドします。RDG はその処理すべてを、レンダリング パスの開始点と終了点を判断することで、ユーザーに代わって自動的に行うので、ユーザーはそれを行うためのバインディング提供するだけで済みます。

ロード アクション

カラー ターゲットまたは深度/ステンシル ターゲットをバインドするには、1 つまたは複数の ロード アクション を指定する必要があります。これらのアクションは、各ターゲットの初期ピクセル値を制御するために使用されます。タイル レンダリングのハードウェアでは、パフォーマンスを最大限に引き出すために的確なアクションが必要とされます。

有効なロード アクションを以下に示します。

  • Load: テクスチャの既存のコンテンツを維持します。

  • Clear: テクスチャをクリアして、その最適化されたクリア値に設定します。

  • NoAction: コンテンツは維持されない可能性があります。このオプションを使用すると、有効なすべてのピクセルに書き込まれる場合に、一部のハードウェアではより高速になります。

深度およびステンシルに対しては独自のロード アクションが個別に指定されます。深度/ステンシル ターゲットでは、各プレーンに読み取りまたは書き込みのアクセス権があるかどうかを制御するために FExclusivieDepthStencil 構造体も必要です。

次の例は、RDG を使用してレンダー ターゲットをクリアする 2 つの異なる方法を示しています。カラー ターゲットは手動でクリアされていますが、深度/ステンシル ターゲットに対してはハードウェアのクリア アクションが使用されています。

C++ コードの例:

BEGIN_SHADER_PARAMETER_STRUCT(FRenderTargetParameters, RENDERCORE_API)

    // These binding slots contain color and depth stencil targets.
    RENDER_TARGET_BINDING_SLOTS()

END_SHADER_PARAMETER_STRUCT()

void AddClearRenderTargetPass(FRDGBuilder& GraphBuilder, FRDGTexture* Texture, const FLinearColor& ClearColor, FIntRect Viewport)
{
    FRenderTargetParameters* Parameters = GraphBuilder.AllocParameters<FRenderTargetParameters>();

    Parameters->RenderTargets[0] = FRenderTargetBinding(
        Texture,
        ERenderTargetLoadAction::ENoAction // <- We do not want to load prior contents of the render target, since we are manually clearing.
    );

    GraphBuilder.AddPass(
        RDG_EVENT_NAME("ClearRenderTarget(%s) [(%d, %d), (%d, %d)] ClearQuad", Texture->Name, Viewport.Min.X, Viewport.Min.Y, Viewport.Max.X, Viewport.Max.Y),
        Parameters,
        ERDGPassFlags::Raster,
        [Parameters, ClearColor, Viewport](FRHICommandList& RHICmdList)
    {
        RHICmdList.SetViewport(Viewport.Min.X, Viewport.Min.Y, 0.0f, Viewport.Max.X, Viewport.Max.Y, 1.0f);
        DrawClearQuad(RHICmdList, ClearColor);
    });
}

void AddClearDepthStencilPass(FRDGBuilder& GraphBuilder, FRDGTextureRef Texture)
{
    auto* PassParameters = GraphBuilder.AllocParameters<FRenderTargetParameters>();

    PassParameters->RenderTargets.DepthStencil = FDepthStencilBinding(
        Texture,
        ERenderTargetLoadAction::EClear, // <- Clear depth to its optimized clear value.
        ERenderTargetLoadAction::EClear, // <- Clear stencil to its optimized clear value.
        FExclusiveDepthStencil::DepthWrite_StencilWrite // <- Allow writes to both depth and stencil.
    );

    GraphBuilder.AddPass(
        RDG_EVENT_NAME("ClearDepthStencil (%s)", Texture->Name),
        PassParameters,
        ERDGPassFlags::Raster,
        [](FRHICommandList&)
    {
        // No actual work to do in the lambda!RDG handles the render pass for us!The clear is done via the clear action.
    });
}
UAV ラスタ パス

REG ではラスタ パスがサポートされています。ラスタ パスは、固定機能のレンダー ターゲットではなく、順序付けされていないアクセス ビュー (UAV) に出力します。最もわかりやすい方法は、FPixelShaderUtils::AddUAVPass ユーティリティ関数を使用することです。このユーティリティ関数は、レンダー ターゲットなしでカスタム レンダリング パスを作成し、自動的に RHI ビューポートをバインドします。

C++ コードの例:

BEGIN_SHADER_PARAMETER_STRUCT(FUAVRasterPassParameters, RENDERCORE_API)
    SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D, Output)
END_SHADER_PARAMETER_STRUCT()

auto* PassParameters = GraphBuilder.AllocParameters<FUAVRasterPassParameters>();
PassParameters.Output = GraphBuilder.CreateUAV(OutputTexture);

// Specify the viewport rect.
FIntRect ViewportRect = ...;

FPixelShaderUtils::AddUAVPass(
    GraphBuilder,
    RDG_EVENT_NAME("Raster UAV Pass"),
    PassParameters,
    ViewportRect,
    [](FRHICommandList& RHICmdList)
{
    // Bind parameters and draw.
});
リソース依存関係管理

FRDGBuilder::AddPass に提供されているパス パラメータ構造体に RDG リソースが存在する場合、その関連付けによって、リソースの存続期間が延長されることや、先行パスとの依存関係が作成されることがあります。理想的には、必要な場合にのみリソースが宣言されると、グラフの複雑さが軽減され、使用されていないリソース パラメータが null としてマークされていると、それらのパラメータがそのパスから取り除かれます。

問題は、シェーダーの並べ替えによってリソースがコンパイル時に除外されるか新しいリソースが導入された後に、リソースが使用されているかどうかを、シェーダーが決定することです。この問題を解決するために、そのシェーダーで使用されていないリソースを null に設定する、ClearUnusedGraphResources ユーティリティ関数が RDG に付属しています。

ClearUnusedGraphResources Utility Example:

ClearUnusedGraphresources(*ComputeShader, PassParameters); GraphBuilder.AddPass(

RDG_EVENT_NAME("..."),
PassParameters,
ERDGPassFlags::Compute,
[PassParameters, ComputeShader, GroupCount] (FRHIComputeCommandList& RHICmdList)
FComputeShaderUtils::Dispatch(RHICmdList, ComputeShader, *PassParameters, GroupCount);

ClearUnusedGraphResources ユーティリティの、複数シェーダー対応バージョンもあります。このバリアントは、どのシェーダーでも使用されていないリソースのみをクリアします。

ミップマップ生成の例

主要な部分はすべて記述されているので、このセクションでのコード例では、ラスタコンピュート の両方のパスを使用してミップマップ チェーンを生成する方法の実例を示します。サブリソースを表現するために、テクスチャの順序付けされていないアクセス ビュー (UAV) とシェーダー リソース ビュー (SRV) を使用して、複数のパスが 1 つにつながれています。

次のコードは、Unreal Engine のコードベースから取り出した単純化された例であり、単純なフルスクリーン用のボイラープレートを減らすユーティリティ関数や、コンピュートのディスパッチ用のユーティリティ関数の使用方法を示しています。

ラスタ ミップマップ生成の例:

class FGenerateMipsVS : public FGlobalShader
{
public:
    DECLARE_GLOBAL_SHADER(FGenerateMipsVS);
};

IMPLEMENT_GLOBAL_SHADER(FGenerateMipsVS, "/Engine/Private/ComputeGenerateMips.usf", "MainVS", SF_Vertex);

class FGenerateMipsPS : public FGlobalShader
{
public:
    DECLARE_GLOBAL_SHADER(FGenerateMipsPS);
    SHADER_USE_PARAMETER_STRUCT(FGenerateMipsPS, FGlobalShader);

    BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
        SHADER_PARAMETER(FVector2D, HalfTexelSize)
        SHADER_PARAMETER(float, Level)
        SHADER_PARAMETER_RDG_TEXTURE_SRV(Texture2D, MipInSRV)
        SHADER_PARAMETER_SAMPLER(SamplerState, MipSampler)
        RENDER_TARGET_BINDING_SLOTS()
    END_SHADER_PARAMETER_STRUCT()
};

IMPLEMENT_GLOBAL_SHADER(FGenerateMipsPS, "/Engine/Private/ComputeGenerateMips.usf", "MainPS", SF_Pixel);

void FGenerateMips::ExecuteRaster(FRDGBuilder& GraphBuilder, FRDGTexture* Texture, FRHISamplerState* Sampler)
{
    auto ShaderMap = GetGlobalShaderMap(GMaxRHIFeatureLevel);
    TShaderMapRef<FGenerateMipsVS> VertexShader(ShaderMap);
    TShaderMapRef<FGenerateMipsPS> PixelShader(ShaderMap);

    for (uint32 MipLevel = 1, MipCount = Texture->Desc.NumMips; MipLevel < MipCount; ++MipLevel)
    {
        const uint32 InputMipLevel = MipLevel - 1;

        const FIntPoint DestTextureSize(
            FMath::Max(Texture->Desc.Extent.X >> MipLevel, 1),
            FMath::Max(Texture->Desc.Extent.Y >> MipLevel, 1));

        FGenerateMipsPS::FParameters* PassParameters = GraphBuilder.AllocParameters<FGenerateMipsPS::FParameters>();
        PassParameters->HalfTexelSize = FVector2D(0.5f / DestTextureSize.X, 0.5f / DestTextureSize.Y);
        PassParameters->Level = InputMipLevel;
        PassParameters->MipInSRV = GraphBuilder.CreateSRV(FRDGTextureSRVDesc::CreateForMipLevel(Texture, InputMipLevel));
        PassParameters->MipSampler = Sampler;
        PassParameters->RenderTargets[0] = FRenderTargetBinding(Texture, ERenderTargetLoadAction::ELoad, MipLevel);

        FPixelShaderUtils::AddFullscreenPass(
            GraphBuilder,
            ShaderMap,
            RDG_EVENT_NAME("GenerateMips DestMipLevel=%d", MipLevel),
            PixelShader,
            PassParameters,
            FIntRect(FIntPoint::ZeroValue, DestTextureSize));
    }
}

コンピュート ミップマップ生成の例:

class FGenerateMipsCS : public FGlobalShader
{
public:
    DECLARE_GLOBAL_SHADER(FGenerateMipsCS)
    SHADER_USE_PARAMETER_STRUCT(FGenerateMipsCS, FGlobalShader)

    BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
        SHADER_PARAMETER(FVector2D, TexelSize)
        SHADER_PARAMETER_RDG_TEXTURE_SRV(Texture2D, MipInSRV)
        SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D, MipOutUAV)
        SHADER_PARAMETER_SAMPLER(SamplerState, MipSampler)
    END_SHADER_PARAMETER_STRUCT()
};

IMPLEMENT_GLOBAL_SHADER(FGenerateMipsCS, "/Engine/Private/ComputeGenerateMips.usf", "MainCS", SF_Compute);

void FGenerateMips::ExecuteCompute(FRDGBuilder& GraphBuilder, FRDGTexture* Texture, FRHISamplerState* Sampler)
{
    TShaderMapRef<FGenerateMipsCS> ComputeShader(GetGlobalShaderMap(GMaxRHIFeatureLevel));

    // Loop through each level of the mips that require creation and add a dispatch pass per level.
    for (uint32 MipLevel = 1, MipCount = TextureDesc.NumMips; MipLevel < MipCount; ++MipLevel)
    {
        const FIntPoint DestTextureSize(
            FMath::Max(TextureDesc.Extent.X >> MipLevel, 1),
            FMath::Max(TextureDesc.Extent.Y >> MipLevel, 1));

        FGenerateMipsCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FGenerateMipsCS::FParameters>();
        PassParameters->TexelSize  = FVector2D(1.0f / DestTextureSize.X, 1.0f / DestTextureSize.Y);
        PassParameters->MipInSRV   = GraphBuilder.CreateSRV(FRDGTextureSRVDesc::CreateForMipLevel(Texture, MipLevel - 1));
        PassParameters->MipOutUAV  = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(Texture, MipLevel));
        PassParameters->MipSampler = Sampler;

        FComputeShaderUtils::AddPass(
            GraphBuilder,
            RDG_EVENT_NAME("GenerateMips DestMipLevel=%d", MipLevel),
            ComputeShader,
            PassParameters,
            FComputeShaderUtils::GetGroupCount(DestTextureSize, FComputeShaderUtils::kGolden2DGroupSize));
    }
}
非同期コンピュート

RDG は、グラフ依存関係を調べ、同期ポイントにフェンスを挿入することによって、非同期コンピュートのスケジューリングをサポートしています。

パスに ERDGPassFlags::AsyncCompute フラグを付け、FRHIComputeCommandList 型をパス ラムダ式の引数として使用することによって、非同期コンピュートが可能になっています。

非同期コンピュートは、プラットフォームと RDG の両方で有効になっている必要があります。非同期コンピュートがサポートされていないプラットフォームでは、グラフィックス パイプにフォールバックされます。RDG でのサポートは、r.RDG.AsyncCompute コンソール コマンドを使用して明示的に無効にすることができます (デフォルトでは有効になっている)。

C++ コードの例:

GraphBuilder.AddPass(
    RDG_EVENT_NAME("MyAsyncComputePass"),
    ERDGPassFlags::AsyncCompute, // <- Specify the AsyncCompute flag here.
    PassParameters,
    [] (FRHIComputeCommandList& RHICmdList) // <- Specify the FRHIComputeCommandList here.
{
    // Execute.
});

単純なコンピュート シェーダーでは、FComputeShaderUtils::AddPass の非同期コンピュート バリアントを使用します。

非同期コンピュートの作業は依存関係グラフを使用してスケジューリングされています。1 つまたは複数のパスがタグ付けされている場合、RDG はグラフをトラバースしてグラフィックス パイプラインにある最後のプロデューサーを見つけて、フェンスを挿入します。同様に、非同期コンピュートでは、そのグラフィックス パイプラインで最初に作業が消費されるときに、グラフィックスにつなぎ直されます。

下図は、前述のシナリオを表しています。

Asynchronous compute scheduling

この図は、グラフィックスおよび非同期コンピュートのキューを表しています (横軸が時間)。

上図では、Pass APass C のプロデューサーであるため、Pasu A が実行された直後にフェンスが導入されて、それが作業を開始するように Pass C に合図を送ります。非同期コンピュート パイプは Pass D が完了するまで実行され、その時点でグラフィックス パイプと同期を取り直して、コンシューマーである Pass E で正しい結果が確認されるようにします。

RDG Insights ツールを使用すると、グラフでの非同期コンピュートのイベントを視覚化できます。次のスクリーンショットは、上図と類似した RDG Insights ツールでのビューを示していますが、これはエンジンでの実際のワークロードからキャプチャしたものです。このツールの詳細については、このページの「RDG Insights」セクションを参照してください。

RDG Insights Timeline Views

外部リソース

リソースの存続期間がグラフの外で延長されていれば、そのリソースは 外部 とみなされ、リソースがグラフに登録されているか、またはグラフから抽出されたという 2 つの状況で起こることがあります。

グラフへの登録は FRDGBuilder::RegisterExternal{Texture, Buffer} メソッドを使用して行われます。この場合は、事前割り当て済みで参照カウントを持つプールの RHI リソース ポインタを使用して新規 RDG リソースが作成されます。そのポインタは、テクスチャでは IPooledRenderTarget、バッファでは FRDGPooledTexture です。

グラフからの抽出では、実行が完了した後に、プールされているリソース ポインタに値が入ります。リソースを登録すると、RDG リソースの存続期間がそのグラフの先頭まで延長されます。グラフのセットアップ時に割り当てが行われることを考慮する必要があります。抽出では、ユーザーが参照を保持しているのでリソースの存続期間がグラフの最後まで延長されるという逆のことが行われます。

ユーザーがグラフの外部で参照を保持していなければ、登録または抽出されたリソースは、厳密に言えば、プールされているメモリを、そのフレームの後方または前方で他の RDG リソースと引き続き共有できます。

次のコード抜粋は、グラフ ビルダーを使用してテクスチャを登録または抽出するさまざまな方法を示しています。登録および抽出で、プールされている同じテクスチャ タイプがどのように使用されて、グラフ間での移動が可能になっているかに注目してください。

C++ コードの例:

// Extract pooled render target.The pointer is filled AFTER Execute() is called.
TRefCountPtr<IPooledRenderTarget> ExtractedTexture;

// First graph produces the texture and extracts it.
{
    FRDGBuilder GraphBuilder(RHICmdList);

    FRDGTexture* Texture = GraphBuilder.CreateTexture(...);

    // ...

    GraphBuilder.QueueTextureExtraction(Texture, &ExtractedTexture);
    GraphBuilder.Execute();

    check(ExtractedTexture); // Valid
}

// Second graph registers the pooled texture.
{
    FRDGBuilder GraphBuilder(RHICmdList);

    // Register pooled render target to get RDG texture.
    FRDGTexture* Texture = GraphBuilder.RegisterExternalTexture(ExtractedTexture);

    // ...

    GraphBuilder.Execute();
}

このコードは、代わりに FRDGPooledBuffer クラスを使用すること以外は、バッファに対しても同じように使用できます。

別のやり方:外部に変換する

外部リソースにする別のやり方として、FRDGBuilder:ConvertToExternal{Texture, Buffer} があります。これは、プールされている下層のリソースの即時割り当てを行って、そのリソースを返します。

このメソッドは、リソースを抽出するのをグラフの最後まで待つことができない状況で便利です。変換と抽出の最も大きな違いは、存続期間の延長です。抽出では存続期間はグラフの最後まで延長されますが、変換ではグラフの先頭まで延長されるため、そのリソースでは下層の割り当てをそのフレームにある他のリソースと共有できません。

非常駐リソース

レンダリング依存関係グラフでは、グラフのコンパイル時に非常駐リソース アロケータを使用して、実行タイムライン全体での割り当てを計画します。存続期間がバラバラであるリソースがメモリ内でオーバーラップすることがあります。

非常駐アロケータが実装されているプラットフォームでは、デフォルトのリソース プール アプローチと比べて、GPU メモリ使用量の大幅な低下を達成できます。これは、メモリのエイリアシングの柔軟性が向上することが原因です。リソース プールでは、RHI 記述子を比較および照合してその再利用を判断することに重点が置かれています。一方、非常駐アロケータでは下層のメモリを共有することができます。

非常駐アロケータが有効であるかどうかは、ユーザーがコンソール変数 r.RDG.TransientAllocator を使用して制御できます。

この変数は、エイリアシング特有の問題を探すときに切り替えるのに便利です。特に、リソースの前回のコンテンツが明確に定義されていることを当てにしないことが重要です。リソース プールでは通常、同一か類似したリソースが再利用されることが多いので、そのような問題は見えなくなっていますが、非常駐アロケータではそのような問題はありません。前回のコンテンツは破棄されるからです。

RDG ユニフォーム バッファ

RDG ユニフォーム バッファには RDG リソースが入っていることがあります。期待どおりに、RDG はグラフのセットアップ時に記述子を初期化し、下層の RHI ユニフォーム バッファの作成を、実行まで先送りにします。リソースが使用されていないと判断されると、そのリソースはカリングされ、初期化されることはありません。

RDG ユニフォーム バッファの作成は、ユニフォーム パラメータ構造体 を入力とする FRDGBuilder::CreateUniformBuffer を使用して行われます。ユニフォーム パラメータ構造体はパス パラメータの拡張であり、RDG リソースが入っていることがあります。FRDGBuilder::AddPass はルート パス パラメータだけでなく子のユニフォーム パラメータもトラバースします。

RDG ユニフォーム バッファの現時点での主な欠点は、リソース パラメータが null に設定されないことがあることと、使用されていないパラメータをシェーダーが反映およびカリングできないことです。現時点では、各パラメータ セットに対して独自のユニフォーム バッファを作成することによって、リソースを手動で取り除く必要があります。

実用的な例については、「ディファード シェーディング レンダラ」の「シーン テクスチャ ユニフォーム バッファ」を参照してください。

RDG ユニフォーム バッファは、ユニフォーム バッファがパスのラムダ式で逆参照可能になるように、FRDGBuilder:AddPass に渡されるパス パラメータ構造体に対して SHADER_PARAMETER_RDG_UNIFORM_BUFFER を使用して宣言する 必要があります

C++ コードの例:

// Simple uniform buffer containing a single RDG texture.
BEGIN_UNIFORM_BUFFER_STRUCT(FMyUniformParameters, )
    SHADER_PARAMETER_RDG_TEXTURE(Texture2D, Texture)
END_UNIFORM_BUFFER_STRUCT()

//  Define pass parameters with a single RDG uniform buffer parameter.
BEGIN_SHADER_PARAMETER_STRUCT(FMyPassParameters, )
    SHADER_PARAMETER_RDG_UNIFORM_BUFFER(FMyUniformParameters, UniformBuffer)
    RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()

void AddPass(FRDGBuilder& GraphBuilder, TShaderMapRef<FShader> PixelShader, FRDGTexture* InputTexture, FRDGTexture* OutputTexture)
{
    // Create the uniform buffer first.
    FMyUniformParameters* UniformParameters = GraphBuilder.AllocParameters<FMyUniformParameters>();
    UniformParameters->Texture = InputTexture;

    TRDGUniformBuffer<FMyUniformParameters>* UniformBuffer = GraphBuilder.CreateUniformBuffer(UniformParameters);

    // Now construct the pass.
    FMyPassParameters* PassParameters = GraphBuilder.AllocParameters<FMyPassParameters>();
    PassParameters->UniformBuffer = UniformBuffer;
    PassParameters->RenderTargets[0] = FRenderTargetBinding(OutputTexture, ERenderTargetLoadAction::ELoad);

    GraphBuilder.AddPass(
        RDG_EVENT_NAME("MyPass"),
        PassParameters,
        ERDGPassFlags::Raster,
        [PassParameters, UniformBuffer, PixelShader](FRHICommandList& RHICmdList)
    {   
        // ... bind shaders, etc.

        // You can access the RHI uniform buffer here!
        FRHIUniformBuffer* UniformBufferRHI = UniformBuffer->GetRHI();

        // You can also access the RDG texture and it is RHI texture!
        FRHITexture* TextureRHI = (*UniformBuffer)->Texture->GetRHI();

        // You can also call the same SetShaderParameters helper method to bind the RDG uniform buffer.
        SetShaderParameters(RHICmdList, PixelShader, PixelShader.GetComputeShader(), *PassParameters);
    });
}

バッファをアップロードする

RDG リソースで、グラフを実行する前に CPU からの初期データが必要であれば、それを日程する最も効率的な手段は FRDGBuilder::QueueBufferUpload メソッドです。RDG は、グラフのコンパイル時に複数のアップロードをまとめてバッチ処理するので、その処理を他のコンパイル タスクとオーバーラップできることがあります。

次のコード例は、CPU の配列を RDG バッファにアップロードする方法を示しています。

バッファアップロードの例:

FRDGBuffer IndexBuffer = GraphBuilder.CreateBuffer(
    FRDGBufferDesc::CreateUploadDesc(sizeof(uint32), NumIndices),
    TEXT("MyIndexBuffer"));

// Allocates an array of data using the internal RDG allocator for deferral.
FRDGUploadData<int32> Indices(GraphBuilder, NumIndices);

// Assign Data
Indices[0] = // ...;
Indices[1] = // ...;
Indices[NumIndices - 1] = // ...;

// Upload Data
GraphBuilder.QueueBufferUpload(IndexBuffer, Indices, ERDGInitialDataFlags::NoCopy);

RDG でアップロード バッファを使用する際に以下のことを考慮します。

  • RDG を使用してアップロードを行わない。

    • パス内で即時コマンド リストを使用して手動ロック/ロック解除を行うと、同期ポイントが導入されて、そのパスが並列実行されなくなる。

    • アップロード バッファは非常駐ではないと自動的にマークされる。非常駐リソースでは CPU アップロードはサポートされていない。

  • 最も正確な ERDGInitialDataFlags は使用しない。

    • データの存続期間がグラフの遅延より十分に長ければ NoCopy を使用する。そうしない場合、RDG によってコピーが作成される。

メモリ存続期間

セットアップと実行のタイムライン分割では、メモリ存続期間を扱う際に注意する必要があります。よくある間違いは、後でグラフが実行される時点で存在することが保証されていない RDG ラムダ式にメモリを渡すことです。

これを扱いやすくするために、RDG には、適切な存続期間が保証されている独自のリニア アロケータが付属しています。その API では、コストが変動する割り当てがサポートされています。

POD タイプに対しては FRDGBuilder::AllocPOD を使用します。

C++ オブジェクトに対してデストラクタ追跡が必要であれば、FRDGBuilder::AllocObject を使用します。

C++ コードの例:

// Bad!
FMyObject Object;
GraphBuilder.AddPass(..., [&Object] (FRHICommandList&) { /** Object is captured by reference but exists on the stack!Invalid pointer! */ });

// Good
TUniquePtr<FMyObject> Object(new FMyObject());
GraphBuilder.AddPass(..., [Object = MoveTemp(Object)] (FRHICommandList&) { /** Object is valid but was expensive to allocate. */ });

// Best if C++ object (calls destructor, adds a bit of cost)
FMyObject* Object = GraphBuilder.AllocObject<FMyObject>();
GraphBuilder.AddPass(..., [Object = MoveTemp(Object)] (FRHICommandList&) { /** Object is valid and was cheap to allocate. */ });

// Best if POD struct (will not call destructor)
FMyObject* Object = GraphBuilder.AllocPOD<FMyObject>();
...

// For raw memory.
void* Memory = GraphBuilder.Alloc(SizeInBytes, AlignInBytes);

// For RDG pass parameters -- may perform additional tracking.
FMyPassParameters* PassParameters = GraphBuilder.AllocParameters<FMyPassParameters>();

割り当てられたメモリは、そのグラフ ビルダー インスタンス破棄されるまで存続します。リニア アロケータが使用されるので非常に高速です。

パフォーマンスのプロファイリング

RDG では、エンジンの各種プロファイラに対するスコープ定義がサポートされています。

  • 複数のパスに関する GPU プロファイル スコープを追加するには RDG_EVENT_SCOPE を使用します。これらは RenderDoc などの外部プロファイラ、および RDG Insights によって消費されます。

  • stat gpu コマンドに新しいスコープを追加するには RDG_GPU_STAT_SCOPE を使用します。

  • CSV プロファイラに新しいスコープを追加するには RDG_CSV_STAT_EXCLUSIVE_SCOPE を使用します。

RDG のスコープでは、ビルダーを入力として受け取り、分離されたセットアップと実行のタイムラインが適切に考慮されます。

コーディング規約

RDG を使用してコードを記述する際の一般的なコーディング規約を以下に示します。これらの規約に従うと、レンダラ全体での一貫性が確保されます。

  • リソースの名前空間はドットで区切って構築する。

    • 例:TSR.History.ScreenPercentage。こうすることで、RDG Insights や他のツールでの名前のフィルタリングが単純化される。

  • グラフ ビルダーのインスタンスを次の名前にする。GraphBuilder

  • シェーダー インスタンスでのシェーダー パラメータをインラインで次の名前にする。FParameters

  • パスの名前空間として RDG_EVENT_SCOPE を使用する。

  • 可能であれば、RenderGraphUtils.h または ScreenPass.h にあるユーティリティを使用する。

デバッグと検証

レンダリング グラフ システムではディファード モードのデータ構造が導入されているので、いっそう複雑になっています。実行時に失敗した場合に、実行ラムダ式と関連付けられているパスのセットアップ場所を見つけるのが難しいことがあります。RHI スレッドが有効になっている場合は、実行している RHI コマンドがそのセットアップ場所から 2 段階で取り除かれるようになったので、さらに複雑です。

たとえば、プラットフォーム RHI の内部で RHI シェーダー パラメータを設定しているときにクラッシュが発生すると、どこで失敗したかをコール スタックの位置だけから推定するのは不可能です。RDG と RHI のどちらにも、そのような問題への対処に役立つツールがあります。RDG の 即時モード は、AddPass 呼び出しでパスが直接実行されるようにすることで、グラフのコンパイルをバイパスするデバッグ機能です。

デバッグまたは開発ビルドを使用している場合に、以下の手法を有効にします。

手法

変数

コンソール変数

r.RDG.ImmediateMode

コマンドライン引数

-rdgimmediate

もう一つの例では、パス パラメータ構造体にある null ポインタが原因で RDG パスのラムダ式内でクラッシュが発生した場合、デバッガはそのラムダ式の内部で一時停止しますが、セットアップ コード (本当の問題が持ち込まれた個所) を調べるにはその時点では遅すぎます。即時モードが有効であれば、ラムダ式はセットアップのタイムラインで実行されるので、セットアップ コードを直接調べることができます。

コンソール コマンド r.RHICmdBypass を使用すると、並列レンダリングとソフトウェア コマンド リストが無効になります。RDG の即時モードと併用すると、すべての遅延メカニズムが取り除かれて、デバッグ用の単一のコール スタックが得られます。

RHI のスレッド動作を制御するその他のコンソール コマンドが存在し、それらについては 「並列レンダリングの概要」ドキュメントを参照してください。

即時モードでは、非常駐割り当て、グラフのカリング、レンダリング パスのマージなど、すべてのグラフ最適化が無効になっています。この手法では、それらが無効になっていることによる副作用が生じます。

また、問題が発生した場合に即時モードで呼び出して機能を除外することなく、RDG の個々の機能を無効にするには以下の CVar を使用します。

変数

説明

r.RDG.CullPasses

無効にすると、パスがカリング対象から除外されます。

r.RDG.MergeRenderPasses

無効にすると、RDG のラスタ パスごとに独自のレンダリング パスが必要とされます。

r.RDG.ParallelExecute

無効にすると、レンダリング スレッドですべてのパスが順次実行されます。

r.RDG.TransientAllocator

無効にすると、リソース プールの使用にフォールバックされます。

検証レイヤー

RDG には検証レイヤーがあり、デバッグまたは開発ビルドの使用時にデフォルトで有効になっています。このレイヤーは、RDG の使い方が間違っている場合に、明確にマークされているリソースおよびパスの名前を使用して、できるだけ早く致命的問題のチェックを発行します。このレイヤーによって CPU オーバーヘッドが増加し、このレイヤーはテストおよびシッピングのビルドではコンパイル時に除外されます。

リソース遷移のデバッグ

RHI のリソース遷移の API では、ERHIAccessERHIPipeline のマスクが各サブリソースに割り当てられます。RDG は、リソースが RDG パス パラメータを使用して適切に宣言されていると想定して、グラフ全体にわたって状態間で遷移する個々のサブリソースに対処します。RHI の検証によって、リソースが不適切に遷移されたときにログ記録されますが、コール スタックの位置が常に同じように見えるため、RDG 内で起きる遷移をデバッグするのが難しいことがあります。

RDG 自体は、正しい遷移を確実に生成するように徹底的にテストされています。ただし、もし必要になった場合には、RDG の遷移ログ記録を RHI の遷移ログ記録と突き合わせると、不一致に気づくことがあります。

RDG の遷移ログ:

  • -rdgtransitionlog または r.rdg.transitionlog X (X はログ記録するフレームの数) を使用すると、RDG 内で起きたすべての遷移がログ記録されます。

  • r.RDG.Debug.ResourceFilter [ResourceName] を使用すると、ログがリソース名でフィルタリングされます。

  • r.RDG.Debug.PassFilter PassName を使用すると、ログがパス名でフィルタリングされます。

RHI の遷移ログ:

-rhivalidation-rhivalidationlog=ResourceName を使用すると、特定のリソースがログ記録されます。

RDG はデフォルトでは、レンダリング スレッド からの遷移を出力しますが、RHI は RHI スレッド からの遷移ログを出力します。それらを揃えるには、-norhithread -forcerhibypass または -onethread を指定する必要があります。残念ながら、RHI スレッドを無効にすると、特定のパイプライン間の遷移エラーが見えなくなることがありますが、ほとんどの場合、そのような問題は引き続き再現されます。

たとえば、SceneDepthZ に対する RDG と RHI のすべてのアクティビティをログ記録するには、次のコマンドライン引数を指定します。

-rhivalidation -rhivalidationlog=SceneDepthZ -rdgtransitionlog -rdgdebugresourcefilter=SceneDepthZ -onethread

テクスチャを視覚化する

RDG は、開発ビルドではすべてのテクスチャの UAV または RTV の vis への書き込みを公開します。このコマンドを使用すると、リソースが画面上に視覚化されます。コマンドラインに「vis」と入力すると、利用可能なリソースの一覧およびコマンドの形式を確認できます。

非常駐アロケータのデバッグ

非常駐アロケータがアーティファクトの潜在的な発生源になっていることがあります。非常駐アロケータを有効または無効にするには r.RDG.TransientAllocator を使用します。

これを無効にするとアーティファクトが解消される場合は、以下の追加テストを検討します。

  • .RDG.ClobberResources を使用すると、すべてのリソースが強制初期化されて既知の値になります。非常駐アロケータを有効にせずにこれを使用しても同様のアーティファクトが生じる場合は、そのリソースが読み取られる前に適切に初期化されていなかった可能性があります。

  • r.RDG.Debug.ExtendResourceLifetimes を使用すると、そのグラフ内のすべてのエイリアシングが無効になります。これは、欠落しているエイリアシング バリアやリソースの間違った存続期間を除外するのに便利です。

  • r.RDG.Debug.DisableTransientResources を使用すると、非常駐アロケータからのリソースが無効になります。

上記のコマンドのいずれかで r.RDG.Debug.ResourceFilter を使用すると、影響を受けるリソースがフィルタリングされます。これは、問題のあるリソースを絞り込むのに役立つことがあります。

RDG Insights プラグイン

Unreal Insights ツールへの拡張として、レンダリング依存関係グラフには、RDG グラフ構造のリアルタイム ビジュアライゼーションのための RDG Insights と呼ばれる独自のプラグインがあります。トレースがキャプチャされて、[Timing Insights] ビューに他の CPU トラックと一緒にトラックとして表示されます。

RDG Insights プラグインを有効にするには、メイン メニューで [Edit (編集)] > [Plugins (プラグイン)] > [Insights] の順に選択します。

RDG Insights Timeline Views

The RDG Insights プラグインを使用すると、グラフに関する以下のプロパティを調べることができます。

  • リソースの存続期間、パスの関連付け、リソース プール割り当てのオーバーラップ

  • 非同期コンピュートのフェンスおよびオーバーラップ

  • グラフのカリングおよびレンダリング パスのマージ

  • 並列実行パスの範囲

  • 非常駐メモリのレイアウト

また、RDG Insights プラグインをデバッグ ツールおよび診断ツールとして使用して、次のような質問に答えることもできます。

  • 非同期コンピュート パスがグラフィックス パスとオーバーラップしていないのはなぜか?

  • フレーム全体でリソースがどのように使用されているか?

  • リソース割り当てが他のリソースとオーバーラップしているか?

  • ポストプロセスでどのリソースが使用されているか?

  • どのパスがカリングされたか?

トレースをキャプチャする

トレースをキャプチャするために必要なのは、Unreal Insights で RDG チャンネルを有効にすることだけであり、クライアント アプリケーションを起動するときに -trace=rdg,defaults 引数を指定するだけです。

Unreal Insights のライブ トレースに接続していれば、RDG チャンネルを有効にするだけです。

RDG トレースでは膨大なデータが生成されます。

スライド資料

このツールのもっと詳細なウォークスルーについては、以下の「スライド資料」を参照してください。

Unreal Engine のドキュメントを改善するために協力をお願いします!どのような改善を望んでいるかご意見をお聞かせください。
調査に参加する
キャンセル