렌더 종속성 그래프

렌더 명령을 컴파일하고 실행할 수 있도록 그래프 데이터 구조체로 기록하는 즉시 모드 API입니다.

Choose your operating system:

Windows

macOS

Linux

렌더 그래프 또는 RDG 라고도 하는 렌더 종속성 그래프(Render Dependency Graph) 는 렌더 명령을 컴파일하고 실행할 수 있도록 그래프 데이터 구조체로 기록하는 즉시 모드 애플리케이션 프로그래밍 인터페이스(API)입니다. RDG는 오류에 취약한 연산을 자동화하여 고수준 렌더링 코드를 단순화하며, 그래프를 탐색해 메모리 사용을 최적화하고 CPU와 GPU의 렌더 패스를 병렬화합니다.

렌더 종속성 그래프의 대표적인 기능은 아래와 같습니다.

  • 비동기 컴퓨트 펜스 예약

  • 최적의 수명과 메모리 에일리어싱을 통한 일시적 리소스 할당

  • 분할 배리어를 사용한 서브리소스 전환으로 레이턴시를 숨기고 GPU 오버랩 향상

  • 병렬 명령 목록 기록

  • 그래프의 미사용 리소스 및 패스 컬링

  • API 사용 및 리소스 의존성 유효성 검사

  • RDG 인사이트에서 그래프 구조체 및 메모리 수명 시각화

렌더 그래프 API는 디퍼드 렌더러와 모바일 렌더러 및 관련 플러그인에 맞춰 전환되었습니다. 특히 위에서 설명한 고급 기능이 필요한 경우 모든 고수준 렌더링 코드는 RDG로 작성해야 합니다.

RDG 프로그래밍 가이드

이 가이드는 C++로 저수준 렌더링 기능을 개발하는 사용자 중 셰이더와 렌더 하드웨어 인터페이스(RHI)에 어느 정도 익숙한 사람들을 대상으로 예시와 함께 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);

    // 이 FShader 인스턴스와 함께 FParameter 바인딩을 등록할 생성자를 만듭니다.
    SHADER_USE_PARAMETER_STRUCT(FMyShaderCS, FGlobalShader);

    // 인라인 정의나 디렉티브로 셰이더에 FParameters 유형을 할당합니다.
    using FParameters = FMyShaderParameters;
};

RHI 명령 목록에 셰이더 파라미터를 바인딩하려면 구조체를 인스턴스화하고 데이터를 채운 다음 SetShaderParameters 유틸리티 함수를 호출합니다.

파라미터 할당하기:

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

FMyShaderCS::FParameters ShaderParameters;

// 파라미터를 할당합니다.
ShaderParameters.ViewportSize = View.ViewRect.Size();
ShaderParameters.World = 1.0f;
ShaderParameters.FooBarArray[4] = FVector(1.0f, 0.5f, 0.5f);

// 파라미터를 제출합니다.
SetShaderParameters(RHICmdList, ComputeShader, ComputeShader.GetComputeShader(), Parameters);

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

유니폼 버퍼

유니폼 버퍼(Uniform Buffers) 는 파라미터를 RHI 리소스로 그룹화합니다. 이 리소스는 그 자체로 셰이더 파라미터로서 바인딩됩니다. 각 유니폼 버퍼는 HLSL에서 글로벌 네임스페이스를 정의합니다. 유니폼 버퍼는 BEGIN_UNIFORM_BUFFER_STRUCTEND_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의 유니폼 버퍼:

// 유니폼 버퍼 선언을 포함한 파일 생성. Common.ush에 의해 자동으로 포함됩니다.
#include "/Engine/Generated/GeneratedUniformBuffers.ush"

// 유니폼 버퍼 멤버를 구조체처럼 참조합니다.
Texture2DSample(SceneTexturesStruct.SceneColorTexture, SceneTexturesStruct.SceneColorTextureSampler);

이제 SHADER_PARAMTER_STRUCT_REF 매크로를 사용해 부모 셰이더 파라미터 구조체에서 유니폼 버퍼를 파라미터로 포함시킬 수 있습니다.

SHADER_PARAMETER_STRUCT_REF

BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
    // ...

    // 레퍼런스 카운트된 TUniformBufferRef<FSceneTextureUniformParameters> 인스턴스를 정의합니다.
    SHADER_PARAMETER_STRUCT_REF(FSceneTextureUniformParameters, SceneTextures)
END_SHADER_PARAMETER_STRUCT()

스태틱 바인딩

셰이더 파라미터는 각 셰이더에 고유하게 바인딩됩니다. 버텍스, 픽셀 등의 각 셰이더 단계에는 별도의 셰이더가 필요합니다. 셰이더는 RHI 명령 목록에서 Set{Graphics, Compute}PipelineState 를 사용해 파이프라인 스테이트 오브젝트(PSO)로 함께 바인딩됩니다.

명령 목록에서 파이프라인 스테이트를 바인딩하면 모든 셰이더 바인딩이 무효화됩니다.

PSO를 설정한 다음에 모든 셰이더 파라미터를 바인딩해야 합니다. 예를 들어, PSO를 공유하는 일반적인 드로 콜의 명령 플로를 살펴보겠습니다.

  • PSO A 설정

  • 각 드로 콜에 대해 아래 작업 수행

    • 버텍스 셰이더 파라미터 설정

    • 픽셀 셰이더 파라미터 설정

    • 드로

  • PSO B 설정

  • 각 드로 콜에 대해 아래 작업 수행

    • 버텍스 셰이더 파라미터 설정

    • 픽셀 셰이더 파라미터 설정

    • 드로

이 접근법의 문제는 렌더러의 메시 드로 명령이 여러 패스와 뷰 간에 캐시 및 공유된다는 점입니다. 프레임마다 각 패스/뷰 조합별로 각기 다른 드로 명령 세트를 생성해야 한다면 매우 비효율적일 것입니다. 하지만 메시 드로 명령은 패스/뷰 유니폼 버퍼 리소스를 알아야 올바르게 바인딩을 수행할 수 있습니다. 이 문제를 해결하기 위해 유니폼 버퍼에는 스태틱 바인딩 모델이 사용됩니다.

스태틱 바인딩으로 선언한 유니폼 버퍼는 개별 셰이더의 고유 슬롯 이 아니라 RHI 명령 목록과 직접 연결된 스태틱 슬롯 에 바인딩됩니다. 셰이더가 유니폼 버퍼를 요청하면 명령 목록은 스태틱 슬롯에서 직접 바인딩을 가져옵니다. 이렇게 하면 바인딩이 PSO 빈도가 아닌 패스 빈도로 이루어집니다.

아래는 위의 예시와 동일하지만 셰이더 입력을 스태틱 유니폼 버퍼에서 가져온다는 차이가 있습니다.

  • 스태틱 유니폼 버퍼 설정

  • PSO A 설정

  • 각 드로 콜에 대해 아래 작업 수행

    • 드로

  • PSO B 설정

  • 각 드로 콜에 대해 아래 작업 수행

    • 드로

이 모델을 사용하면 각 드로 콜이 명령 목록에서 셰이더 바인딩을 상속할 수 있습니다.

스태틱 유니폼 버퍼 정의하기

스태틱 바인딩으로 유니폼 버퍼를 정의하려면 IMPLEMENT_STATIC_UNIFORM_BUFFER_STRUCT 매크로를 사용합니다. 추가 슬롯을 선언해야 합니다. 이는 IMPLEMENT_STATIC_UNIFORM_BUFFER_SLOT 매크로에 지정됩니다.

여러 스태틱 유니폼 버퍼 정의가 동일한 스태틱 슬롯을 참조할 수도 있지만, 바인딩할 수 있는 정의는 한 번에 하나뿐입니다. 가급적 엔진 내 총 슬롯 수를 줄일 수 있도록 슬롯을 재사용하는 것이 좋습니다.

스태틱 유니폼 버퍼:

// 이름으로 고유 스태틱 슬롯을 정의합니다.
IMPLEMENT_STATIC_UNIFORM_BUFFER_SLOT(SceneTextures);

// SceneTextures 슬롯에 스태틱 바인딩하여 SceneTexturesStruct 유니폼 버퍼를 정의합니다.
IMPLEMENT_STATIC_UNIFORM_BUFFER_STRUCT(FSceneTextureUniformParameters, "SceneTexturesStruct", SceneTextures);

// 동일한 스태틱 슬롯으로 MobileSceneTextures 유니폼 버퍼를 정의합니다. 한 번에 하나만 바인딩됩니다.
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(
        // printf 시맨틱을 사용해 프로파일러의 패스에 친근한 이름을 붙입니다.
        RDG_EVENT_NAME("MyShader %d%d", View.ViewRect.Width(), View.ViewRect.Height()),
        // RDG에 파라미터가 제공됩니다.
        PassParameters,
        // 컴퓨트 명령이 발생합니다.
        ERDGPassFlags::Compute,
        // 실행 시까지 연기됩니다. 다른 패스와 병렬 실행될 수 있습니다.
        [PassParameters, ComputeShader, GroupCount] (FRHIComputeCommandList& RHICmdList)
    {
        FComputeShaderUtils::Dispatch(RHICmdList, ComputeShader, PassParameters, GroupCount);
    });

    // 그래프를 실행합니다.
    GraphBuilder.Execute();
}

그래프 빌더 API는 단일 스레드 방식이며 한 번에 하나의 인스턴스만 인스턴스화 가능합니다. 계층 그래프나 사이드 바이 사이드(side-by-side) 그래프는 제외됩니다. 디퍼드 렌더러와 모바일 렌더러는 모두 각 씬 렌더 발동에 단일 빌더 인스턴스를 사용합니다.

타임라인 구성 및 실행

RDG는 렌더 파이프라인을 구성실행 의 두 가지 타임라인으로 나눕니다.

그래프는 구성 타임라인에서 빌드됩니다. 여기서 리소스 생성과 렌더 파이프라인 환경설정 분기가 이루어집니다. 모든 RHI 명령은 패스 람다로 연기되고, 람다는 실행 타임라인에서 호출됩니다.

패스 실행이 병렬화될 수 있으므로 패스 람다에서 지정된 코드는 부작용이 없어야 하며 명령을 명령 목록에 기록하기만 해야 합니다.

RDG가 있는 경우와 없는 경우의 타임라인 구성 및 실행.

위 그림은 RDG가 있는 경우와 없는 경우의 렌더 파이프라인 타임라인을 나타냅니다.

RDG가 없는 경우에는 렌더링 기능이 하나의 타임라인에 작성됩니다. 이 타임라인에서 구성과 실행이 모두 이루어집니다. RHI 명령은 파이프라인 분기 및 리소스 할당에 따라 직접 기록되고 제출됩니다.

RGD가 있으면 사용자 제공 패스 실행 람다를 통해 구성 코드가 실행과 분리됩니다. RDG는 패스 실행 람다를 호출하기 전에 추가 컴파일 단계를 수행합니다. 실행은 여러 스레드에 걸쳐 수행되며, 람다를 호출해 렌더 명령을 RHI 명령 목록에 기록합니다.

RDG 유틸리티 함수

렌더 종속성 그래프의 RenderGraphUtils.h 에는 일반적인 패스를 추가할 수 있는 유용한 유틸리티 함수가 있습니다. 엔진의 보일러플레이트를 줄일 수 있도록 가능하면 유틸리티 함수를 사용하는 것이 좋습니다.

예를 들어, 컴퓨트 셰이더 패스에는 FComputeShaderUtils::AddPass 를, 전체화면 픽셀 셰이더 패스에는 FPixelShaderUtils::AddFullscreenPass 를 사용합니다.

아래 예시는 교육 목적으로 상세히 작성되었습니다. 가능하면 유틸리티 함수를 사용하세요.

RDG 리소스 및 뷰

RDG 리소스는 초기에 RHI 리소스 디스크립터를 포함합니다. 관련 RHI 리소스는 리소스를 패스 파라미터로 선언하는 패스의 실행 람다 내에서만 액세스할 수 있습니다. 모든 RDG 리소스는 특정 서브클래스 유형에 대해 FRDGResource::GetRHI 오버로드를 제공합니다. 이 메서드의 액세스는 패스 람다로 제한되며, 메서드를 부적절하게 호출하면 유효성 검사 레이어에 의해 어설트가 발생합니다.

아래 프로퍼티는 버퍼텍스처 리소스를 대상으로 합니다.

  • 어떤 리소스는 일시적 입니다. 이 경우 리소스 수명이 그래프의 제약을 받고, 메모리가 다른 일시적 리소스와 에일리어싱되어 수명이 해체될 수 있습니다.

  • 어떤 리소스는 외부 리소스입니다. 이 경우 수명이 그래프 외부로 연장됩니다. 이 현상은 사용자가 기존 RHI 리소스를 그래프에 등록하거나 실행이 완료된 이후에 그래프에서 리소스를 추출할 때 나타납니다.

RDG 버퍼와 텍스처는 RDG 언오더드 액세스 뷰(Unordered Access View) 또는 셰이더 리소스 뷰(Shader Resource View)로 특수화할 수 있습니다. 다른 RDG 리소스와 마찬가지로 기본 RHI 리소스는 실행 중 온디맨드 방식으로 할당되며, 액세스는 리소스를 파라미터로 선언한 패스에 대해서만 허용됩니다. 아래의 코드 샘플은 텍스처, 버퍼, 뷰 리소스 생성 방법을 나타낸 것입니다.

리소스 생성 예시:

// 새 일시적 텍스처 인스턴트를 생성합니다. 아직은 GPU 메모리가 할당되지 않습니다. 디스크립터만 지정됩니다.
FRDGTexture* Texture = GraphBuilder.CreateTexture(FRDGTextureDesc::Create2D(...), TEXT("MyTexture"));

// 유효하지 않음! 어설트가 트리거됩니다. 패스에서 선언된 경우에만 패스 람다에서 허용됩니다!
FRHITexture* TextureRHI = Texture->GetRHI();

// 특정 밉 레벨에서 텍스처를 참조하는 새 UAV를 생성합니다.
FRDGTextureUAV* TextureUAV = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(Texture, MipLevel));

// 유효하지 않음!
FRHIUnorderedAccessView* UAVRHI = TextureUAV->GetRHI();

// 새 일시적 구조체 버퍼 인스턴스를 생성합니다.
FRDGBuffer* Buffer = GraphBuilder.CreateBuffer(FRDGBufferDesc::CreateBufferDesc(...), TEXT("MyBuffer"));

// 유효하지 않음!
FRHIBuffer* BufferRHI= Buffer->GetRHI();

// R32 플로트 포맷으로 버퍼를 참조하는 새 SRV를 생성합니다.
FRDGBufferSRV* BufferSRV = GraphBuilder.CreateSRV(Buffer, PF_R32_FLOAT);

// 유효하지 않음!
FRHIShaderResourceView* SRVRHI = TextureSRV->GetRHI();

RDG 리소스 포인터는 그래프 빌더가 소유합니다. 파괴된 이후에는 무효화됩니다. 그래프 실행 후 모든 포인터는 null이어야 합니다.

패스와 파라미터

패스 파라미터는 정확한 메모리 수명을 보장하는 FRDGBuilder::AllocParameters 함수를 사용해 할당합니다. RDG는 자체 매크로를 사용해 셰이더 파라미터 구조체 시스템을 확장합니다. FRDGBuilder:AddPass 는 셰이더 파라미터 매크로를 무시하고 커스텀 RDG 매크로를 사용합니다.

패스 파라미터와 셰이더 파라미터가 연결되어 있는 이유가 따로 있습니다. 언리얼 엔진의 패스 파라미터 대부분은 셰이더 파라미터이기도 합니다. 따라서 두 파라미터를 동일한 API로 선언하면 보일러플레이트가 줄어듭니다.

아래 예시에서는 패스와 파라미터의 사용 방법을 효과적으로 보여줍니다.

셰이더 파라미터 예시

이 셰이더 파라미터 예시에서는 RDG의 개입 없이 단순한 가상 컴퓨트 셰이더를 선언하고 일부 셰이더 파라미터를 바인딩합니다.

이렇게 하면 RDG가 셰이더 파라미터 구조체 시스템의 익스텐션임을 보여주는 데 도움이 되는 베이스라인을 잡아줍니다.

예시의 내용 전체를 이해할 수 있도록 코드에 포함된 코멘트를 참고하세요.

HLSL 코드 예시:

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

C++ 코드 예시:

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

    // FParameters를 위한 셰이더 바인딩을 생성합니다.
    SHADER_USE_PARAMETER_STRUCT(FMyComputeShader, FGlobalShader);

    // 인라인 셰이더 파라미터 정의. 명명 규칙을 따르는 FParameters 이름을 사용합니다.
    BEGIN_SHADER_PARAMETER_STRUCT(FParameters, /** MODULE_API */)

        // FRHITexture* 는 HLSL 코드의 'MyTexture'에 해당합니다.
        SHADER_PARAMETER_TEXTURE(Texture2D, MyTexture)

        // FRHIShaderResourceView* 는 HLSL 코드의 'MySRV'에 해당합니다.
        SHADER_PARAMETER_SRV(Texture2D, MySRV)

        // FRHIUnorderedAccessView* 는 HLSL 코드의 'MyUAV'에 해당합니다.
        SHADER_PARAMETER_UAV(RWTexture2D, MyUAV)

        // 플로트 셰이더 파라미터는 HLSL 코드의 'MyFloat'에 해당합니다.
        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());

    // 일반적인 C++ 구조체와 같이 인스턴스화됩니다.
    FMyComputeShader::FParameters Parameters;

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

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

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

    Parameters.MyFloat = 1.0f;

    // 셰이더의 바인딩을 사용해 RHI 명령 목록에 셰이더 파라미터 구조체 전체를 설정합니다.
    SetShaderParameters(RHICmdList, ComputeShader, ComputeShader.GetComputeShader(), Parameters);

    // ...
}

셰이더 및 패스 파라미터 예시

이 예시에서는 코드를 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, )

        // HLSL 코드의 'MyTexture'에 해당하는 FRDGTexture* 에 읽기 액세스를 선언합니다.
        SHADER_PARAMETER_RDG_TEXTURE(Texture2D, MyTexture)

        // HLSL 코드의 'MySRV'에 해당하는 FRDGTextureSRV* 에 읽기 액세스를 선언합니다.
        SHADER_PARAMETER_RDG_TEXTURE_SRV(Texture2D, MySRV)

        // HLSL 코드의 'MyUAV'에 해당하는 FRDGTextureUAV* 에 쓰기 액세스를 선언합니다.
        SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D, MyUAV)

        // 플로트 셰이더 파라미터는 HLSL 코드의 'MyFloat'에 해당합니다. 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>();

    // FRDGBuilder::AddPass와 SetShaderParameters가 이를 소모합니다.
    FRDGTexture* MyTexture = ...;
    PassParameters->MyTexture = MyTexture;

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

    // 참고: 셰이더 파라미터의 경우처럼 null 포인터를 지정할 수도 있습니다. RDG는 null 파라미터를 무시합니다.
    FRDGTextureSRV* MySRV = nullptr;
    PassParameters->MySRV = MySRV;

    // FRDGBuilder::AddPass는 이를 무시하고 SetShaderParameters는 소모합니다.
    PassParameters->MyFloat = 1.0f;

    // 이후에 GraphBuilder.Execute() 과정에서 실행될 단일 컴퓨트 패스를 추가합니다. 사용자는 이후 그래프 실행 과정에서 호출할
    // C++ 람다와 PassParameter 구조체를 제공합니다. ERDGPassFlags::Compute는 RDG에게 이 패스가
    // Compute 명령만 발생시킨다는 것을 전달합니다. 예를 들어, 래스터 명령은 허용되지 않습니다.

    GraphBuilder.AddPass(
        RDG_EVENT_NAME("MyComputeShader"),
        PassParameters, // <- RDG가 여기서 패스 파라미터를 소모합니다.
        ERDGPassFlags::Compute,
        [ComputeShader, PassParameters /** <- PassParameters가 여기서 람다로 정리됩니다. */](FRHIComputeCommandList& RHICmdList)
    {
        RHICmdList.SetComputeShader(ComputeShader.GetComputeShader());

        // PassParameters가 여기서 설정됩니다. null이 아닌 모든 RDG 파라미터에서 각 RHI 리소스에 대한 레퍼런스가 해제됩니다.
        SetShaderParameters(RHICmdList, ComputeShader, ComputeShader.GetComputeShader(), *PassParameters);

        // ...
    });
}

셰이더 파라미터가 없는 패스 파라미터 예시

예시를 계속 보겠습니다. 아래는 RDG에서 간단한 CopyTexture 유틸리티 함수를 구현해 셰이더 시맨틱이 없는 패스 파라미터를 보여주는 코드입니다.

패스가 셰이더와 일대일로 대응하지 않거나 셰이더가 전혀 없을 때 사용하기 좋습니다.

C++ 코드 예시:

BEGIN_SHADER_PARAMETER_STRUCT(FCopyTextureParameters, )

    // FRDGTexture* 에 대한 CopySrc 액세스를 선언합니다.
    RDG_TEXTURE_ACCESS(Input,  ERHIAccess::CopySrc)

    // FRDGTexture* 에 대한 CopyDest 액세스를 선언합니다.
    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는 렌더 패스의 시작 및 끝 시점을 결정해 이 모든 과정을 자동으로 처리해 줍니다. 사용자는 이 과정에서 필요한 바인딩만 제공하면 됩니다.

로드 액션

컬러나 뎁스/스텐실 타깃을 바인딩하려면 하나 이상의 로드 액션(Load Actions) 을 지정해야 합니다. 로드 액션은 각 타깃의 최초 픽셀 값을 컨트롤하는 데 사용됩니다. 타일 렌더링 하드웨어는 정확한 액션이 있어야 최상의 퍼포먼스를 낼 수 있습니다.

유효한 로드 액션은 아래와 같습니다.

  • 로드(Load): 텍스처의 기존 콘텐츠를 보존합니다.

  • 클리어(Clear): 최적화된 클리어 값에 따라 텍스처를 클리어합니다.

  • 액션 없음(NoAction): 콘텐츠가 보존되지 않습니다. 모든 유효 픽셀이 작성 중인 경우, 이 옵션은 일부 하드웨어에서 더 빠를 수 있습니다.

뎁스와 스텐실에는 고유의 로드 액션이 별도 지정됩니다. 뎁스 스텐실 타깃에는 각 평면의 읽기나 쓰기 액세스 권한을 컨트롤할 수 있도록 FExclusivieDepthStencil 구조체도 필요합니다.

아래 예시에서는 RDG로 렌더 타깃을 클리어하는 몇 가지 방법을 보여줍니다. 컬러 타깃은 수동으로 클리어되고, 뎁스와 스텐실 타깃은 하드웨어 클리어 액션을 사용합니다.

C++ 코드 예시:

BEGIN_SHADER_PARAMETER_STRUCT(FRenderTargetParameters, RENDERCORE_API)

    // 이 바인딩 슬롯들은 컬러 및 뎁스 스텐실 타깃을 포함합니다.
    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 // <- 수동으로 클리어하므로 렌더 타깃의 이전 콘텐츠는 로드하지 않습니다.
    );

    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, // <- 최적화된 클리어 값에 따라 뎁스를 클리어합니다.
        ERenderTargetLoadAction::EClear, // <- 최적화된 클리어 값에 따라 스텐실을 클리어합니다.
        FExclusiveDepthStencil::DepthWrite_StencilWrite // <- 뎁스와 스텐실의 쓰기를 모두 허용합니다.
    );

    GraphBuilder.AddPass(
        RDG_EVENT_NAME("ClearDepthStencil (%s)", Texture->Name),
        PassParameters,
        ERDGPassFlags::Raster,
        [](FRHICommandList&)
    {
        // 람다에서는 실제 작업을 하지 않습니다! RDG가 렌더 패스를 처리해 줍니다! 클리어 액션을 통해 클리어됩니다.
    });
}
UAV 래스터 패스

REG는 고정 함수 렌더 타깃 대신에 언오더드 액세스 뷰(UAV)로 출력되는 래스터 패스를 지원합니다. 가장 간단한 방법은 렌더 타깃 없이 커스텀 렌더 패스를 생성하고 RHI 뷰포트를 바인딩해 주는 FPixelShaderUtils::AddUAVPass 유틸리티를 사용하는 것입니다.

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);

// 뷰포트 렉트를 지정합니다.
FIntRect ViewportRect = ...;

FPixelShaderUtils::AddUAVPass(
    GraphBuilder,
    RDG_EVENT_NAME("Raster UAV Pass"),
    PassParameters,
    ViewportRect,
    [](FRHICommandList& RHICmdList)
{
    // 파라미터를 바인딩하고 드로합니다.
});
리소스 종속성 관리

FRDGBuilder::AddPass 에 제공된 패스 파라미터 구조체에 RDG 리소스가 존재할 경우 연결로 인해 리소스의 수명이 늘어나거나 이전 패스와의 종속성이 만들어질 수 있습니다. 그래프 복잡도를 줄여야 하는 경우에만 리소스를 선언하고, 미사용 리소스 파라미터는 패스에서 삭제할 수 있도록 null로 표시하는 것이 가장 이상적입니다.

문제는 셰이더의 순열이 리소스에서 컴파일되거나 새 리소스를 도입하기 때문에 리소스의 사용 여부가 셰이더에 따라 달라진다는 것입니다. 이 문제를 해결하기 위해 RDG에는 셰이더가 사용하지 않는 리소스를 null 처리하는 ClearUnusedGraphResources 유틸리티 함수가 존재합니다.

ClearUnusedGraphResources 유틸리티 예시:

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)를 사용해 여러 패스를 체인으로 연결하여 서브리소스를 표현할 수 있습니다.

아래는 언리얼 엔진 코드베이스에서 가져와 단순화한 예시로, 유틸리티 함수를 사용해 간단한 전체화면 드로에서 보일러플레이트를 줄이거나 디스패치를 계산하는 방법을 보여줍니다.

래스터 밉맵 생성 예시:

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));

    // 생성이 필요한 각 밉 레벨을 순환하며 레벨당 하나의 디스패치 패스를 추가합니다.
    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, // <- 여기에서 AsyncCompute 플래그를 지정합니다.
    PassParameters,
    [] (FRHIComputeCommandList& RHICmdList) // <- 여기에서 FRHIComputeCommandList를 지정합니다.
{
    // 실행합니다.
});

간단한 컴퓨트 셰이더의 경우 비동기 컴퓨트 버전 FComputeShaderUtils::AddPass 를 사용합니다.

비동기 컴퓨트 작업은 종속성 그래프를 사용해 예약합니다. 하나 이상의 패스가 태그되면 RDG가 그래프를 탐색해 그래프 파이프라인의 마지막 생산자를 찾아 펜스를 삽입합니다. 마찬가지로 그래픽 파이프라인에서 작업이 처음 소모되면 비동기 컴퓨트는 다시 그래픽에 합류합니다.

아래의 그림은 이 시나리오를 나타냅니다.

비동기 컴퓨트 예약

이 그림은 그래픽과 비동기 컴퓨트 큐를 보여줍니다. 가로 축은 시간을 나타냅니다.

위 그림에서는 패스 A패스 C 의 생산자입니다. 따라서 패스 A가 실행된 직후에 펜스가 삽입되고, 이를 신호로 패스 C 작업이 시작됩니다. 비동기 컴퓨트 파이프는 패스 D 가 완료될 때까지 실행되고, 패스 D가 완료되면 컨슈머, 즉 패스 E 가 올바른 결과를 볼 수 있도록 그래픽 파이프와 다시 동기화됩니다.

RDG 인사이트 툴을 사용하면 그래프의 비동기 컴퓨트 이벤트를 시각화할 수 있습니다. 이 스크린샷은 위 그림과 비슷한 뷰를 RDG 인사이트 툴로 나타낸 것입니다. 차이점은 엔진의 실제 작업에서 캡처했다는 것입니다. 툴 사용에 대한 자세한 정보는 이 페이지의 RDG 인사이트 섹션을 참고하세요.

RDG 인사이트 타임라인 뷰

외부 리소스

외부 리소스는 수명이 그래프 바깥으로 뻗어 나가는 리소스입니다. 이 현상은 리소스가 그래프에 등록되거나 그래프에서 추출될 때 발생할 수 있습니다.

리소스를 그래프에 등록할 때는 FRDGBuilder::RegisterExternal{Texture, Buffer} 메서드를 사용합니다. 이렇게 하면 사전 할당된 레퍼런스 카운트 풀 RHI 리소스 포인터, 즉 텍스처의 경우 IPooledRenderTarget, 버퍼의 경우 FRDGPooledTexture 로 새 RDG 리소스가 만들어집니다.

그래프에서 리소스를 추출하면 실행이 완료된 후 풀링된 리소스 포인터가 채워집니다. 리소스를 등록하면 RDG 리소스의 수명이 그래프의 앞쪽으로 연장됩니다. 그래프 구성 과정에서 할당이 이루어진다는 점을 유념해야 합니다. 반대로 추출의 경우, 이제 사용자가 레퍼런스를 보유하므로 수명이 그래프 끝으로 연장됩니다.

사용자가 그래프 외부의 레퍼런스를 보유하지 않아도 등록되거나 추출된 리소스는 이론적으로 프레임 내에서 이전이나 이후에 다른 RDG 리소스와 풀링된 메모리를 공유할 수 있습니다.

아래 코드 예시는 그래프 빌더로 텍스처를 등록하거나 추출하는 다양한 방법을 보여줍니다. 등록과 추출 과정에서 사용하는 풀링된 텍스처 타입이 동일하기 때문에 그래프 간 반복작업이 가능하다는 점을 눈여겨보시기 바랍니다.

C++ 코드 예시:

// 풀링된 렌더 타깃 추출. Execute()가 호출된 후 포인터가 채워집니다.
TRefCountPtr<IPooledRenderTarget> ExtractedTexture;

// 첫 번째 그래프가 텍스처를 생산하고 추출합니다.
{
    FRDGBuilder GraphBuilder(RHICmdList);

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

    // ...

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

    check(ExtractedTexture); // 유효합니다.
}

// 두 번째 그래프가 풀링된 텍스처를 등록합니다.
{
    FRDGBuilder GraphBuilder(RHICmdList);

    // 풀링된 렌더 타깃을 등록해 RDG 텍스처를 얻습니다.
    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 처리되지 않을 수 있으며 셰이더가 미사용 파라미터를 반영하고 컬링할 수 없다는 점입니다. 현재로서는 각 파라미터 세트에 대해 고유의 유니폼 버퍼를 생성해 리소스를 직접 정리해야 합니다.

실제 예시는 디퍼드 셰이딩 렌더러(Deferred Shading Renderer)의 씬 텍스처 유니폼 버퍼(Scene Textures Uniform Buffers)를 참고하시기 바랍니다.

RDG 유니폼 버퍼는 반드시 패스 람다에서 유니폼 버퍼의 레퍼런스 해제가 가능하도록 FRDGBuilder:AddPass 에 제공된 패스 파라미터 구조체에서 SHADER_PARAMETER_RDG_UNIFORM_BUFFER 를 사용해 선언해야 합니다.

C++ 코드 예시:

// 단일 RDG 텍스처를 포함한 단순한 유니폼 버퍼입니다.
BEGIN_UNIFORM_BUFFER_STRUCT(FMyUniformParameters, )
    SHADER_PARAMETER_RDG_TEXTURE(Texture2D, Texture)
END_UNIFORM_BUFFER_STRUCT()

//  단일 RDG 유니폼 버퍼 파라미터로 패스 파라미터를 정의합니다.
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)
{
    // 먼저 유니폼 버퍼를 생성합니다.
    FMyUniformParameters* UniformParameters = GraphBuilder.AllocParameters<FMyUniformParameters>();
    UniformParameters->Texture = InputTexture;

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

    // 이제 패스를 만듭니다.
    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)
    {   
        // ... 셰이더 바인딩 등

        // 여기서 RHI 유니폼 버퍼에 액세스할 수 있습니다!
        FRHIUniformBuffer* UniformBufferRHI = UniformBuffer->GetRHI();

        // RDG 텍스처에 액세스해 RHI 텍스처를 구할 수도 있습니다!
        FRHITexture* TextureRHI = (*UniformBuffer)->Texture->GetRHI();

        // 같은 SetShaderParameters 헬퍼 메서드를 호출해 RDG 유니폼 버퍼를 바인딩할 수도 있습니다.
        SetShaderParameters(RHICmdList, PixelShader, PixelShader.GetComputeShader(), *PassParameters);
    });
}

버퍼 업로드

RDG 리소스가 그래프 실행 이전에 CPU의 초기 데이터를 필요로 하는 경우, 가장 효율적인 스케줄 방법은 FRDGBuilder::QueueBufferUpload 메서드입니다. 그래프 컴파일 과정에서 RDG 배치가 함께 업로드되며, 다른 컴파일 작업과 병행도 가능할 수 있습니다.

아래의 코드 예시는 CPU 데이터 배열을 RDG 버퍼에 업로드하는 방법입니다.

버퍼 업로드 예시:

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

// 연기를 위해 내부 RDG 할당자를 사용해 데이터 배열을 할당합니다.
FRDGUploadData<int32> Indices(GraphBuilder, NumIndices);

// 데이터를 할당합니다.
Indices[0] = // ...;
Indices[1] = // ...;
Indices[NumIndices - 1] = // ...;

// 데이터를 업로드합니다.
GraphBuilder.QueueBufferUpload(IndexBuffer, Indices, ERDGInitialDataFlags::NoCopy);

RDG에서 업로드 버퍼를 사용할 때에는 아래의 사항을 고려하시기 바랍니다.

  • RDG를 사용해 업로드하세요.

    • 즉각 명령 목록을 사용한 패스 내 수동 잠금/해제는 동기화 포인트를 만들어내며 병렬 실행을 방해합니다.

    • 업로드 버퍼는 비일시적으로 자동 표시됩니다. 일시적 리소스는 CPU 업로드를 지원하지 않습니다.

  • 가장 정확한 ERDGInitialDataFlags 를 사용하세요.

    • 데이터 수명이 그래프 연기 이후에도 남을 있을 만큼 길다면 NoCopy 를 사용하세요. 그렇지 않다면 RDG로 사본을 만드세요.

메모리 수명

메모리 수명을 다루는 경우 구성 타임라인과 실행 타임라인을 분할할 때 주의가 필요합니다. 대표적인 실수는 그래프 실행 이후에도 존재할지 확실하지 않은 RDG 람다로 메모리를 전송하는 것입니다.

이를 방지하기 위해 RDG에는 적절한 길이의 수명을 보장하는 자체 선형 할당자가 있습니다. API는 다양한 비용의 할당을 지원합니다.

POD 유형의 경우 FRDGBuilder::AllocPOD 를 사용하세요.

C++ 오브젝트에 소멸자 트래킹이 필요하면 FRDGBuilder::AllocObject 를 사용하세요.

C++ 코드 예시:

// 나쁜 예시!
FMyObject Object;
GraphBuilder.AddPass(..., [&Object] (FRHICommandList&) { /** 오브젝트가 레퍼런스에 의해 캡처되지만 스택에 존재합니다! 포인터가 유효하지 않습니다! */ });

// 좋은 예시
TUniquePtr<FMyObject> Object(new FMyObject());
GraphBuilder.AddPass(..., [Object = MoveTemp(Object)] (FRHICommandList&) { /** 오브젝트가 유효하지만 할당 비용이 너무 높습니다. */ });

// C++ 오브젝트의 경우 가장 좋은 예시(소멸자 호출, 비용 소폭 증가)
FMyObject* Object = GraphBuilder.AllocObject<FMyObject>();
GraphBuilder.AddPass(..., [Object = MoveTemp(Object)] (FRHICommandList&) { /** 오브젝트가 유효하며 비용이 저렴해 할당이 가능합니다. */ });

// POD 구조체의 경우 가장 좋은 예시(소멸자를 호출하지 않음)
FMyObject* Object = GraphBuilder.AllocPOD<FMyObject>();
...

// 미가공 메모리의 경우
void* Memory = GraphBuilder.Alloc(SizeInBytes, AlignInBytes);

// RDG 패스 파라미터의 경우(추가 트래킹이 수행될 수 있음)
FMyPassParameters* PassParameters = GraphBuilder.AllocParameters<FMyPassParameters>();

할당된 메모리는 그래프 빌더 인스턴스가 소멸될 때까지 지속됩니다. 선형 할당자가 사용되며 속도가 매우 빠릅니다.

퍼포먼스 프로파일링

RDG는 엔진의 다양한 프로파일러에 대해 영역 정의를 지원합니다.

  • RDG_EVENT_SCOPE 를 사용해 패스에 대해 GPU 프로파일 영역을 추가합니다. RDG Insights나 RenderDoc과 같은 외부 프로파일러에서 소모합니다.

  • RDG_GPU_STAT_SCOPE 를 사용해 stat gpu 명령에 대해 새 영역을 추가합니다.

  • RDG_CSV_STAT_EXCLUSIVE_SCOPE 를 사용해 CSV 프로파일러에 대해 새 영역을 추가합니다.

RDG 영역은 빌더를 입력으로 사용하며, 분리된 구성 및 실행 타임라인을 올바르게 반영합니다.

규칙

아래는 RDG를 사용해 코드를 작성할 때 사용하는 일반적인 코딩 규칙입니다. 이 규칙을 따르면 렌더러 전체에 걸쳐 일관성을 유지할 수 있습니다.

  • 점으로 분리된 리소스 네임스페이스를 구성합니다.

    • 예를 들어, TSR.History.ScreenPercentage 와 같은 이름을 사용합니다. 이렇게 하면 RDG 인사이트 등의 툴에서 이름 필터링이 간편해집니다.

  • 그래프 빌더 인스턴스의 이름은 GraphBuilder 로 합니다.

  • 셰이더 인스턴스의 셰이더 파라미터 인라인의 이름은 FParameters 로 합니다.

  • 패스의 네임스페이스로는 RDG_EVENT_SCOPE 를 사용합니다.

  • 가능하면 RenderGraphUtils.h 또는 ScreenPass.h 유틸리티를 사용합니다.

디버깅 및 유효성 검사

렌더 그래프 시스템에서는 디퍼드 모드 데이터 구조체를 사용하기 때문에 복잡도가 증가합니다. 실행 중 실패 시 실행 람다와 관련된 패스 구성 위치를 찾기 어려울 수 있습니다. 게다가 RHI 스레드가 활성화된 경우 실행 RHI 명령이 구성 위치에서 두 단계 더 멀어지므로 문제가 악화됩니다.

예를 들어, 플랫폼 RHI 안의 RHI 셰이더 파라미터를 설정할 때 충돌이 발생하면 콜 스택 위치만으로는 실패가 발생한 지점을 알아낼 수 없습니다. RDG와 RHI에 이 문제를 해결하는 데 유용한 툴이 있습니다. RDG의 즉시 모드(Immediate Mode)AddPass 호출에서 직접 패스를 실행할 수 있도록 그래프 컴파일을 우회하는 디버그 기능입니다.

디버그(Debug)나 개발(Development) 빌드를 사용하는 경우 아래 메서드를 활성화하세요.

메서드

변수

콘솔 변수

r.RDG.ImmediateMode

명령줄 아규먼트

-rdgimmediate

다른 예시를 살펴보겠습니다. 패스 파라미터 구조체의 null 포인터로 인해 RDG 패스 람다 내에서 충돌이 발생하면 람다 내에서 디버거가 중단됩니다. 이때는 실제 문제가 발생한 곳의 구성 코드를 조사하기에는 이미 너무 늦은 시점입니다. 즉시 모드를 활성화하면 람다가 구성 타임라인에서 실행되므로 구성 코드를 직접 조사할 수 있습니다.

콘솔 명령어 r.RHICmdBypass 를 사용하면 병렬 렌더링과 소프트웨어 명령 목록이 비활성화됩니다. RDG 즉시 모드와 함께 사용하면 모든 연기 메커니즘을 제거해 단일 콜 스택으로 디버깅이 가능합니다.

병렬 렌더링 개요 문서에서 RHI 스레딩 비헤이비어를 컨트롤하는 다른 콘솔 변수를 확인할 수 있습니다.

즉시 모드를 사용하면 일시적 할당, 그래프 컬링, 렌더 패스 병합 등 모든 그래프 최적화가 비활성화됩니다. 따라서 비활성화로 인한 부작용이 발생합니다.

또한, 아래의 CVar를 사용해 RDG에서 즉시 모드를 발동시키지 않고 각 기능을 비활성화하면 문제가 발생했을 때 해당 기능이 제외됩니다.

변수

설명

r.RDG.CullPasses

비활성화하면 패스의 컬링 여부를 제외하지 않습니다.

r.RDG.MergeRenderPasses

비활성화하면 RDG 래스터 패스당 고유의 렌더 패스를 사용해야 합니다.

r.RDG.ParallelExecute

비활성화하면 렌더 스레드의 모든 패스를 연속 실행합니다.

r.RDG.TransientAllocator

비활성화하면 리소스 풀링으로 예비 전환합니다.

유효성 검사 레이어

RDG는 디버그나 개발 빌드를 사용하는 경우 디폴트로 활성화되는 유효성 검사 레이어를 포함하고 있습니다. 이 레이어는 RDG가 올바르게 사용되고 있지 않을 경우, 조기에 리소스/패스 이름을 정확히 표시하여 치명적 오류를 확인해 줍니다. 테스트(Test) 및 출시(Shipping) 빌드에서 사용 시 CPU 오버헤드가 추가되며 컴파일됩니다.

리소스 트랜지션 디버깅

RHI의 리소스 트랜지션 API는 ERHIAccessERHIPipeline 마스크를 각 서브리소스에 할당합니다. RDG는 RDG 패스 파라미터를 사용해 리소스가 적절히 선언된다고 가정한 상태로 그래프 전체에 걸쳐 스테이트 간 개별 서브리소스의 트랜지션을 관리합니다. 리소스가 올바르지 않게 트랜지션되는 경우 RHI 유효성 검사 로그에 기록이 남지만 RDG 안에서 발생한 트랜지션의 경우 콜 스택 위치가 항상 동일하게 표시되므로 디버그가 어려울 수 있습니다.

RDG는 올바른 트랜지션을 생산할 수 있도록 엄격한 테스트를 거쳤습니다. 그러나 필요한 경우 RDG 트랜지션 로그를 RHI 트랜지션 로그와 혼합해 동일하지 않은 부분을 찾아낼 수 있습니다.

RDG 트랜지션 로그:

  • -rdgtransitionlog 또는 r.rdg.transitionlog X(X는 기록할 프레임 수)를 사용해 RDG 내에서 발생하는 모든 트랜지션을 기록합니다.

  • r.RDG.Debug.ResourceFilter [리소스 이름] 을 사용해 리소스 이름으로 로그를 필터링합니다.

  • 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 인사이트 플러그인

언리얼 인사이트 툴의 익스텐션인 렌더 종속성 그래프에는 RDG 그래프 구조체의 실시간 시각화를 위한 RDG 인사이트 라는 자체 플러그인이 있습니다. 타이밍 인사이트(Timing Insights) 뷰에는 다른 CPU 트랙과 함께 트레이스가 캡처되어 트랙으로 표시됩니다.

RDG 인사이트 플러그인을 활성화하려면 메인 메뉴에서 편집(Edit) > 플러그인(Plugins) > 인사이트(Insights) 를 선택합니다.

RDG 인사이트 타임라인 뷰

RDG 인사이트 플러그인은 다음과 같은 그래프 프로퍼티를 조사하는 데 사용할 수 있습니다.

  • 리소스 수명, 패스 연결, 리소스 풀 할당 오버랩

  • 비동기 컴퓨트 펜스 및 오버랩

  • 그래프 컬링 및 렌더 패스 병합

  • 병렬 실행 패스 범위

  • 일시적 메모리 레이아웃

RDG 인사이트 플러그인은 아래와 같은 질문에 답하기 위한 디버깅 및 진단 툴로도 사용할 수 있습니다.

  • 왜 비동기 컴퓨트 패스가 그래프 패스와 오버랩되지 않는가?

  • 프레임에서 리소스가 어떻게 사용되고 있는가?

  • 리소스 할당이 다른 리소스와 오버랩되고 있는가?

  • 포스트 프로세싱에서 어떤 리소스를 사용하는가?

  • 어떤 패스가 컬링되었는가?

트레이스 캡처하기

트레이스를 캡처하려는 경우 언리얼 인사이트에서 RDG 채널만 활성화하면 됩니다. 클라이언트 애플리케이션을 실행할 때에는 -trace=rdg,defaults 아규먼트를 지정하는 것으로 충분합니다.

언리얼 인사이트의 라이브 트레이스(Live Trace)와 연결된 경우 RDG 채널만 활성화하면 됩니다.

RDG 트레이스는 많은 데이터를 생성합니다.

슬라이드 덱

자세한 툴 사용 방법은 슬라이드 덱을 참조하세요.