渲染依赖图(3/4)

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 Indices(GraphBuilder, NumIndices);

    // 分配数据
    Indices[0] = // ...;
    Indices[1] = // ...;
    Indices[NumIndices - 1] = // ...;

    // 上传数据
    GraphBuilder.QueueBufferUpload(IndexBuffer, Indices, ERDGInitialDataFlags::NoCopy);
Copy full snippet

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 Lambda中,但图在执行时不能保证该内存仍存在。

为了帮助解决这个问题,RDG包含了自己的线性分配器,具有适当的生命周期保证。API支持不同开销的分配。

对于POD类型,请使用 FRDGBuilder::AllocPOD。

如果C++对象需要析构函数跟踪,请使用 FRDGBuilder::AllocObject。

C++代码示例:

    // 不佳!
    FMyObject Object;
    GraphBuilder.AddPass(..., [&Object] (FRHICommandList&) { /** Object is captured by reference but exists on the stack! 指针无效!*/ });

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

    // 最佳情况是C++对象(调用析构函数,增加一点开销)
    FMyObject* Object = GraphBuilder.AllocObject();
    GraphBuilder.AddPass(..., [Object = MoveTemp(Object)] (FRHICommandList&) { /** Object is valid and was cheap to allocate. */ });

    // 最佳情况是POD结构体(不会调用析构函数)
    FMyObject* Object = GraphBuilder.AllocPOD();
    ...

    // 对于原始内存。
    void* Memory = GraphBuilder.Alloc(SizeInBytes, AlignInBytes);

    // 对于RDG通道参数——可能会执行额外的跟踪。
    FMyPassParameters* PassParameters = GraphBuilder.AllocParameters();
Copy full snippet

// 不佳! FMyObject Object; GraphBuilder.AddPass(..., [&Object] (FRHICommandList&) { /* Object is captured by reference but exists on the stack! 指针无效!/ });

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

// 最佳情况是C++对象(调用析构函数,增加一点开销)
FMyObject* Object = GraphBuilder.AllocObject<FMyObject>();
GraphBuilder.AddPass(..., [Object = MoveTemp(Object)] (FRHICommandList&) { /** Object is valid and was cheap to allocate. */ });

// 最佳情况是POD结构体(不会调用析构函数)
FMyObject* Object = GraphBuilder.AllocPOD<FMyObject>();
...

// 对于原始内存。
void* Memory = GraphBuilder.Alloc(SizeInBytes, AlignInBytes);

// 对于RDG通道参数——可能会执行额外的跟踪。
FMyPassParameters* PassParameters = GraphBuilder.AllocParameters<FMyPassParameters>();

所有分配的内存都将持续存在,直到图构建器实例被销毁。它使用线性分配器并且非常快。

性能分析

RDG支持引擎中各种分析器的范围定义:

  • 使用 RDG_EVENT_SCOPE 在通道周围添加GPU配置文件范围。这些将由RenderDoc等外部分析器以及RDG Insights使用。
  • 使用 RDG_GPU_STAT_SCOPE 为 stat gpu 命令添加新范围。
  • 使用 RDG_CSV_STAT_EXCLUSIVE_SCOPE 为CSV分析器添加新范围。

RDG范围将构建器作为输入,并适当考虑单独的设置和执行时间轴。

约定

以下是使用RDG编写代码时使用的常规编码约定。遵循这些约定可确保整个渲染器的一致性。

  • 采用句点分隔为资源构建命名空间。 例如,TSR.History.ScreenPercentage。这简化了RDG Insights和其他工具中的名称过滤。

命名图生成器实例:GraphBuilder 在着色器实例上内联命名着色器参数:FParameters 使用 RDG_EVENT_SCOPE 作为通道的命名空间。 尽可能使用 RenderGraphUtils.h 或 ScreenPass.h 中的辅助函数。