RPS 教程第一部分 – Hello Triangle

首次发布时间:
最后更新:
Noah Cabral's avatar
Noah Cabral

引言

本教程通过将现有的 Hello Triangle 示例转换为 RPS 示例,引导读者了解 RPS API 的基本用法。在最终的教程源代码中,可以通过将 m_useRps=false 设置为 false 来切换回初始示例。源代码可以在这里找到:hello_triangle.cpp。另外,请忽略 hello_triangle.cpp 中标记为仅用于第三部分教程的内容。

如简介中所述,所有相关的教程示例都利用了预先提供的 app framework。它(除其他功能外)提供了一个已创建的 d3d12 设备;交换链、命令队列和栅极的管理;以及 AcquireCmdListRecycleCmdList 等实用函数。

概述

RPS 的表面积很小,可以实现一个基本示例

初始化时:

每一帧:

清理时:

  • 销毁图。

  • 销毁运行时设备。

总而言之,RPS SDK 的使用归结为构建渲染图;向 RPS 提供命令缓冲区;让 RPS 记录到该命令缓冲区;最后,提交此命令缓冲区以及适当的信号。

RPS 初始化

告别了前言,我们准备将此示例转换为 RPS 示例!

创建运行时设备

我们将从创建运行时设备开始,

virtual void OnInit(ID3D12GraphicsCommandList* pInitCmdList,
std::vector<ComPtr<ID3D12Object>>& tempResources) override
{
// ...
// Create RPS runtime device.
RpsD3D12RuntimeDeviceCreateInfo runtimeDeviceCreateInfo = {};
runtimeDeviceCreateInfo.pD3D12Device = m_device.Get();
AssertIfRpsFailed(rpsD3D12RuntimeDeviceCreate(&runtimeDeviceCreateInfo, &m_rpsDevice));
// ...
}

我们调用 rpsD3D12RuntimeDeviceCreate 创建了两个对象:RPS 设备和 RPS 运行时。该设备由 RPS 用于管理主机应用程序的内存分配和诊断日志记录,以及其他功能。运行时设备在很大程度上实现了每个后端图形 API 的运行时调用,例如资源创建和销毁。

调用 rpsD3D12RuntimeDeviceCreate 通过指向 RpsDevice 类型变量的指针返回创建的设备。相反,运行时设备句柄不会返回。运行时设备句柄存储在设备内部,因为主机应用程序不需要引用它。

RpsDevice 以及 RpsRenderGraph 等其他句柄类型是此 SDK 中的常见模式。这些类型在头文件中通过 RPS_DEFINE_HANDLE 定义,并在许多 API 调用中用作 SDK 对象实例的引用。头文件还定义了辅助句柄类型,这些类型使用 RPS_DEFINE_OPAQUE_HANDLE 定义。这里没有意外;这些句柄类型与其他 API 中的类型相似。第一个句柄类型是对 SDK 内部类型的引用,其数据表示在 API 中未公开。第二个句柄类型与常规句柄相反,它引用一个数据表示可以在运行时动态修改的类型。

设置渲染图

接下来,我们将设置渲染图。有两种方法可以做到这一点。第一种方法在存储库 README 中提到——通过 RPSL 着色器隐式定义它。第二种方法是使用 RPS C API。为了简单起见并介绍 RPSL,我们将使用前者。

RPSL 简介

有关 RPSL 的更深入文档,请参阅 rpsl.h

首先,让我们回顾一下我们对 RPSL 的了解

  • RPSL 是一种 HLSL 派生语言。

  • RPSL “着色器”在 CPU 上执行,并由 RPS 运行时调用。

  • 执行 RPSL 着色器会构建渲染图。

  • 执行渲染图会调用其图节点实现,其中每个节点都构成一个组件化渲染器的部分。

对于本教程,我们的应用程序将使用以下 .rpsl 文件,

graphics node Triangle([readwrite(rendertarget)] texture renderTarget : SV_Target0);
export void main([readonly(present)] texture backbuffer)
{
// clear and then render geometry to backbuffer
clear(backbuffer, float4(0.0, 0.2, 0.4, 1.0));
Triangle(backbuffer);
}

上述 RPSL 源定义的组件渲染器非常简单;它清除后缓冲区,然后执行 Triangle 节点,作为应用程序程序员,我们理解该节点会将三角形渲染到后缓冲区。

虽然上面的示例仅演示了一个 RPSL 程序,但一个 RPSL 着色器可以包含许多不同的程序,每个程序如果执行,都会构建一个单独的图。这些程序由它们的入口节点标识,这些节点使用 export 关键字声明。对于上面的代码片段,入口点是 main

但是,一旦上面的程序执行完毕,它会构建什么渲染图?一般来说,渲染图的结构是什么?

RPSL 的执行模型类似于在 CPU 上执行程序——在执行过程中,它会“调用”节点。这些不是实际的“调用”,而是“触摸”——节点被注意到将被执行,并且传递给它们的执行数据被保存。一旦 RPSL 程序完成执行,所有被触摸的节点就定义了构成渲染图的节点,渲染图的形式是线性的、有序的节点调用序列。

因此,当调用上面的 RPSL 程序中的 main 入口点时,它最终会触摸两个节点:clearTriangle。因此,渲染图由这两个节点组成。clear 节点是一个内置节点——它的实现已由 RPS 提供。至于 Triangle 节点——这是一个用户定义的节点,或者更准确地说,是一个节点声明。在执行渲染图之前,我们需要将一个实现回调挂钩到这个声明。

不要忘记 RPS SDK 的主要功能之一——优化 GPU 上的工作执行。为此,在构建完渲染图并在图执行之前,图中的节点由 RPS 运行时调度程序重新排序。这发生在调用 rpsRenderGraphUpdate 时,这是用于构建渲染图的阶段之一。因此,RPSL 程序作者在编写其 RPSL 着色器时应牢记这一点。只要它能保持管道的正确性,就可以任意重新排序。

您可能会问,那么,调度时使用什么规则?开发人员如何利用这些规则来推断渲染图的编写?

这一切都与资源视图和访问属性有关。资源视图是 RPS 的工作流。资源视图包含对底层资源的引用以及几个视图属性,例如资源被查看的子资源范围。在 RPSL 中,texturebuffer 类型都是资源视图。至于访问属性,它们是顶部的装饰。它们指定节点如何通过其资源视图参数访问资源。正是通过此规范定义了隐式排序依赖项,RPS 调度程序必须保证这些依赖项。

可以合理地假设 clear 节点在 Triangle 节点之前调度,事实上,这是正确的。但为什么是这样?

让我们考虑节点定义。我们将从 clear 节点开始(有许多重载,所以这只是一个)

node clear( [writeonly(rendertarget, clear)] texture dst, float4 clearValue );

从这个定义中,我们可以说后缓冲区在帧中的第一个访问具有 writeonly 的访问属性。这意味着第一次访问只能写入后缓冲区,不能从中读取,并且可以丢弃之前的数据。随后的 Triangle 节点以 readwrite 的访问属性访问后缓冲区。这意味着该节点可以同时读取和写入后缓冲区。因此,程序顺序访问序列为:writeonly (WO) -> readwrite (RW)。此类序列不得重新排序。将 WO 放在 RW 之后会丢弃 Triangle 节点生成的所有内容,从而在功能上改变指定的管道。由于重新排序无效,调度程序必须保留程序顺序。

好的,但是 Triangle 节点中的 rendertarget 部分在做什么?这是一个传递给访问属性的参数,并且在管道规范中也起着特殊作用。rendertarget 参数意味着它装饰的资源视图节点参数可以作为渲染目标视图访问。这取决于所使用的运行时后端,具有不同的含义。例如,DX12 运行时后端满足的要求之一是确保在调用节点回调之前,资源处于 D3D12_RESOURCE_STATE_RENDER_TARGET 状态。渲染目标视图非常常见,各种其他访问也一样。因此,RPS 定义了一组方便的宏;例如,rtv 是 [readwrite(rendertarget)] texture 的别名。

上面 .rpsl 中要分解的最后一部分是入口节点的访问属性,因为它比常规节点声明的访问具有不同的语义。对于入口节点,访问属性不适用于节点范围内部,而是适用于节点范围外部。这为入口节点访问的资源建立了进入/退出状态。传递给 main 的输入参数以 [readonly(present)] 访问。这就是我们想要的——在渲染完帧的渲染图后,我们将后缓冲区呈现给交换链。

为了将所有内容整合在一起,我们将反思一下我们刚刚构建的内容。我们构建了一个组件渲染器,它通过内置的 clear 节点清除后缓冲区,然后执行 Triangle 节点实现(用户定义的),最终使用当前使用的任何图形 API 进行绘制调用。最后,在帧结束时,后缓冲区将过渡到 D3D12_RESOURCE_STATE_PRESENT 状态。

编译和链接 RPSL 模块

再次回顾我们简短的 RPS 介绍,.rpsl 着色器文档必须编译。这是通过捆绑的 RPSL 编译器 rps-hlslc 实现的。

要编译 RPSL 文件,只需执行,

终端窗口
rps-hlslc.exe rps_tutorial_p1.rpsl -O0

正如之前所说,此编译结果是一个 .c 文件。但是,也可以将我们的模块编译成 DLL,甚至通过 RPSL-JIT 编译器加载它。现在,我们将做最简单的事情——静态链接一个 .c 文件。

为此,它以调用类似函数的宏开始——不需要 .h 文件,RPSL 编译器也不会生成 .h 文件。在我们的例子中,它看起来像这样

// ...
RPS_DECLARE_RPSL_ENTRY(rps_tutorial_p1, main);
// ...

通过这个宏,我们可以前向声明 .c RPSL 模块中的某个入口,然后稍后在 RpsRenderGraphCreateInfo 中指定此函数是主入口点。该宏是为了处理 RPSL .c 后端输出的实现细节以及 RPS 运行时与该输出的链接,这可能与“链接函数”的概念模型不同。确保您的宏中包含正确的模块和入口名称很重要,因为不正确的信息可能导致复杂的调试过程。

好的,我们终于准备好调用 rpsRenderGraphCreate 了,

virtual void OnInit(ID3D12GraphicsCommandList* pInitCmdList,
std::vector<ComPtr<ID3D12Object>>& tempResources) override
{
// ...
// Create RPS render graph.
RpsRenderGraphCreateInfo renderGraphInfo = {};
RpsQueueFlags queueFlags[] = {RPS_QUEUE_FLAG_GRAPHICS, RPS_QUEUE_FLAG_COMPUTE, RPS_QUEUE_FLAG_COPY};
renderGraphInfo.scheduleInfo.numQueues = 3;
renderGraphInfo.scheduleInfo.pQueueInfos = queueFlags;
renderGraphInfo.mainEntryCreateInfo.hRpslEntryPoint = RPS_ENTRY_REF(hello_triangle, main);
AssertIfRpsFailed(rpsRenderGraphCreate(m_rpsDevice, &renderGraphInfo, &m_rpsRenderGraph));
// ...
}

调用 rpsRenderGraphCreate 并不会执行主 RPSL 模块;它只是设置 RenderGraph 对象。

如果此调用返回 RPS_OK,则表示一切正常!否则,关于此调用需要注意的一点是 scheduleInfo 结构成员。

在这里,我们向 RPS 指定了可用的队列及其索引布局。这些索引与队列的映射关系在稍后变得相关,当时 RPS 将沟通要被调度到某个队列索引的工作。在上面的代码片段中,我们将队列索引 0、1 和 2 关联到应用程序管理的图形、计算和复制队列。这些托管队列及其索引顺序是我们 app framework 特有的。队列索引在我们的 hello triangle 示例中用于索引存储队列指针的数组。通常,应用程序应向 RPS 传达正确的队列索引,以适应应用程序定义的可用队列索引布局。

绑定渲染图节点回调

创建渲染图后,我们可以将回调绑定到 RPSL 模块中声明的节点。在此简单示例中,我们只有一个节点 Triangle

virtual void OnInit(ID3D12GraphicsCommandList* pInitCmdList,
std::vector<ComPtr<ID3D12Object>>& tempResources) override
{
// ...
// Bind nodes.
AssertIfRpsFailed(
rpsProgramBindNode(rpsRenderGraphGetMainEntry(m_rpsRenderGraph), "Triangle", &DrawTriangleCb, this));
// ...
}
// ...
static void DrawTriangleCb(const RpsCmdCallbackContext* pContext)
{
HelloTriangle* pThis = static_cast<HelloTriangle*>(pContext->pCmdCallbackContext);
pThis->DrawTriangle(rpsD3D12CommandListFromHandle(pContext->hCommandBuffer));
}
// ...
void DrawTriangle(ID3D12GraphicsCommandList* pCmdList)
{
pCmdList->SetGraphicsRootSignature(m_rootSignature.Get());
pCmdList->SetPipelineState(m_pipelineState.Get());
pCmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
pCmdList->DrawInstanced(3, 1, 0, 0);
}

实际上,节点未绑定是可以的。此类节点,如果在图记录时被调度然后迭代,在调用时什么都不做。话虽如此,如果渲染图有默认回调,那么对于任何未绑定的节点都会调用它。要设置默认回调,请调用 rpsProgramBindNode 并将名称参数设置为 null 指针。

rps_runtime.h 中,实际上有多个 rpsProgramBindNode 重载;这些是用于绑定自由函数和成员函数的。现在,我们调用的是唯一的非模板版本。

rpsProgramBindNode 的第一个参数是类型为 RpsSubprogram 的句柄。RpsSubprogram 正如其名;它代表一个 RPSL 程序(一个入口点)。我们之前调用 rpsRenderGraphGetMainEntry 返回与我们静态链接的 RPSL main 入口点关联的子程序。然后,调用 rpsProgramBindNode 获取此子程序句柄作为包含要绑定回调的节点的程序。有关子程序强大功能的更多详细信息,请参阅 Modular RPSL

最后一个参数是传递给回调的用户定义上下文。this 指针被传递,因为我们需要解析类实例来调用我们的成员函数 DrawTriangle。用户上下文可以通过 RpsCmdCallbackContextpCmdCallbackContext 成员在回调中访问。

虽然本节在“初始化时”部分,但绑定节点回调不是一次性操作。只要在更新渲染图之前完成,动态绑定回调就没有问题。节点回调绑定在本节中讨论,仅仅是因为我们期望典型的应用程序使用是为每个节点只绑定一次回调。

RPS 逐帧逻辑

由于我们还没有更改 OnRender 回调函数,DrawTriangleCb 将永远不会被调用。这就引出了下一步——修改我们的 OnRender 回调以将 RPS 生成的命令提交给我们的 D3D12 命令列表。这是一个多步骤的过程,因为我们必须首先更新渲染图。

更新渲染图

更新渲染图意味着执行渲染图阶段,从而重建(或首次创建)图。此时将执行 RPSL 模块。因此,在更新渲染图时,我们有责任传入主入口点所消耗的参数。

在我们的特定 .rpsl 模块的情况下,主入口点接收单个纹理,即后缓冲区。

让我们退一步来回顾一下我们正在做什么,从高层次来看;我们的最终目标是为每一帧渲染一个三角形到相应的交换链图像。由于渲染图只接收一个纹理,我们可能会推断需要一些主机应用程序端逻辑来推断要传入哪个交换链图像视图。

但是,这不需要,因为 RPS 有一个有用的机制叫做时间资源,可以帮助解决这个问题。

时间资源

时间资源由切片组成。对于任何给定帧,时间资源将解析为其切片之一(它本身也是一个资源)。要解析的选定切片是通过 RpsRenderGraphUpdateInfo 中的 frameIndex 成员以及资源视图的 TemporalLayer 成员计算的。

解析时间切片的计算如下:

uint32_t temporalSliceIdx = (frameIndex - std::min(TemporalLayer, frameIndex)) % numTemporalLayers;

默认情况下,资源视图的 TemporalLayer 设置为零,这会导致解析的切片是当前帧的切片。

虽然时间资源结构对于解析当前帧的正确渲染目标这一特定问题很有用,但该结构的强大之处当然远不止于此——更普遍地说,时间资源允许轻松访问历史数据。设置 TemporalLayer 可用于访问历史时间切片。要在资源视图上设置时间层,必须创建一个派生视图。在 RPSL 中,这可以通过类似 resA.temporal(1) 的方式实现。这是一个视图,它将访问 resA 的某个切片,该切片是 resA 在上一帧中默认视图所访问的。

向渲染图提供输入参数

在下面的代码片段中,我们更新渲染图,并通过 ppArgResources 将交换链图像/时间切片作为 ID3D12Resource 句柄数组提供给 RPSL 程序。

virtual void OnUpdate(uint32_t frameIndex) override
{
if (m_rpsRenderGraph != RPS_NULL_HANDLE)
{
RpsRuntimeResource backBufferResources[DXGI_MAX_SWAP_CHAIN_BUFFERS];
for (uint32_t i = 0; i < m_backBuffers.size(); i++)
{
// RpsRuntimeResource is a RPS_OPAQUE_HANDLE.
// this type has a single elem "void* ptr".
backBufferResources[i].ptr = m_backBuffers[i].Get();
}
RpsResourceDesc backBufferDesc = {};
backBufferDesc.type = RPS_RESOURCE_TYPE_IMAGE_2D;
backBufferDesc.temporalLayers = uint32_t(m_backBuffers.size());
backBufferDesc.image.width = m_width;
backBufferDesc.image.height = m_height;
backBufferDesc.image.arrayLayers = 1;
backBufferDesc.image.mipLevels = 1;
backBufferDesc.image.format = RPS_FORMAT_R8G8B8A8_UNORM;
backBufferDesc.image.sampleCount = 1;
RpsConstant argData[] = {&backBufferDesc};
const RpsRuntimeResource* argResources[] = {backBufferResources};
uint32_t argDataCount = 1;
// ...
// RpsAfx always waits for presentation before rendering to a swapchain image again,
// therefore the guaranteed last completed frame by the GPU is m_backBufferCount frames ago.
//
// RPS_GPU_COMPLETED_FRAME_INDEX_NONE means no frames are known to have completed yet;
// we use this during the initial frames.
const uint64_t completedFrameIndex =
(frameIndex > m_backBufferCount) ? frameIndex - m_backBufferCount : RPS_GPU_COMPLETED_FRAME_INDEX_NONE;
RpsRenderGraphUpdateInfo updateInfo = {};
updateInfo.frameIndex = frameIndex;
updateInfo.gpuCompletedFrameIndex = completedFrameIndex;
updateInfo.numArgs = argDataCount;
updateInfo.ppArgs = argData;
updateInfo.ppArgResources = argResources;
assert(_countof(argData) == _countof(argResources));
AssertIfRpsFailed(rpsRenderGraphUpdate(m_rpsRenderGraph, &updateInfo));
}
}

通常,通过设置 RpsRenderGraphUpdateInfoppArgResourcesppArgs 成员来向 RPSL 程序的入口点提供参数。这些成员都是指针数组。对于 ppArgs 中的每个指针,都有一个对应的指向资源句柄(类型均为 const RpsRuntimeResource *)的指针在 ppArgResources 中。对于时间资源,指向资源句柄的相应指针指向一个资源句柄的整个数组,事实上,这就是上面 OnUpdate 函数中所做的。

同样,ppArgs 中的指针可以仅仅指向一个内置类型,例如 intfloat,而 ppArgsResources 中的相应指针为 nullptr。通常,任何 C 结构/POD C++ 类都可以传递,前提是内存大小和布局在每个索引处的参数在主机应用程序和 HLSL 之间匹配。请注意,由于 HLSL 布尔值为 32 位,因此 ppArgs 中类型为 bool 的元素是未定义行为。为此,我们提供了一个类型 typedef int32_t RpsBool;。

我们进一步注意到 gpuCompletedFrameIndex 成员。主机应用程序应将其填充为一个值,该值至少传达一个框架索引,该索引的关联命令缓冲区保证已在 GPU 上完成。RPS 使用此值来管理框架资源。如果应用程序持续提供 RPS_GPU_COMPLETED_FRAME_INDEX_NONE 的值,RPS 可能会遇到内部内存错误,因为框架资源有一个缓冲限制。

最后,我们注意到,虽然图可能构建正确,但它可能在语义上无效。目前,RPS SDK 执行的至少一些语义验证是在线进行的。在这种情况下,SDK 会以 RPS_ERROR_INVALID_PROGRAM 错误退出。

记录渲染图命令

既然我们已经更新了渲染图,我们就可以研究更新的 OnRender 回调来记录图命令。

但是,“图命令”到底是什么?

它不是 API 命令,例如 DrawInstanced;相反,它是一个更抽象的工作分组,将被记录到 API 命令缓冲区中。最常见的图命令类型是节点回调的调用。事实上,这样的图命令是调度程序阶段的结果——构成渲染图的重新排序的“触摸”节点序列。

命令记录从调用 rpsRenderGraphGetBatchLayout 开始,

virtual void OnRender(uint32_t frameIndex) override
{
if (m_useRps)
{
RpsRenderGraphBatchLayout batchLayout = {};
AssertIfRpsFailed(rpsRenderGraphGetBatchLayout(m_rpsRenderGraph, &batchLayout));
// ...
}
// ...
}

这将从渲染图中返回批次布局。RPS 指定要提交的命令按批次提交,其中每个批次是提交到某个队列的工作块。在记录每个批次之前,我们可能需要排队一组 GPU 端等待,以在某些队列上发生一些信号值。

virtual void OnRender(uint32_t frameIndex) override
{
// ...
m_fenceSignalInfos.resize(batchLayout.numFenceSignals);
for (uint32_t ib = 0; ib < batchLayout.numCmdBatches; ib++)
{
const RpsCommandBatch& batch = batchLayout.pCmdBatches[ib];
for (uint32_t iw = batch.waitFencesBegin; iw < batch.waitFencesBegin + batch.numWaitFences; ++iw)
{
const FenceSignalInfo& sInfo = m_fenceSignalInfos[batchLayout.pWaitFenceIndices[iw]];
AssertIfFailed(m_queues[batch.queueIndex]->Wait(m_fences[sInfo.queueIndex].Get(), sInfo.value));
}
// ...
}
// ...
}

在排队等待之后,我们可以记录图并提交生成的 API 命令。RpsCommandBatch 结构中的 queueIndex 成员将批次直接与要提交的某个队列相关联。

virtual void OnRender(uint32_t frameIndex) override
{
// ...
ID3D12CommandQueue* pQueue = GetCmdQueue(RpsAfxQueueIndices(batch.queueIndex));
ActiveCommandList cmdList = AcquireCmdList(RpsAfxQueueIndices(batch.queueIndex));
RpsRenderGraphRecordCommandInfo recordInfo = {};
recordInfo.hCmdBuffer = rpsD3D12CommandListToHandle(cmdList.cmdList.Get());
recordInfo.pUserContext = this;
recordInfo.frameIndex = frameIndex;
recordInfo.cmdBeginIndex = batch.cmdBegin;
recordInfo.numCmds = batch.numCmds;
AssertIfRpsFailed(rpsRenderGraphRecordCommands(m_rpsRenderGraph, &recordInfo));
CloseCmdList(cmdList);
ID3D12CommandList* pCmdLists[] = {cmdList.cmdList.Get()};
pQueue->ExecuteCommandLists(1, pCmdLists);
RecycleCmdList(cmdList);
// ...
}

除了节点回调明确记录的内容之外,RPS 还生成哪些 API 命令?

如您可能还记得,它主要生成资源状态管理命令(障碍物插入);然而,RPS 也有权插入诸如绑定 RT、清除 RT 以及设置视口和剪刀矩形之类的命令。通常,我们可以称之为“渲染通道设置”命令。这些命令发生在 RPS 将控制权交给主机应用程序回调之前。

这些设置命令通过节点声明的语义进行控制。例如,要绑定的 RT 由用 SV_Target[n] 语义注解的参数指定。

回想一下我们 .rpsl 中的 Triangle 节点,

graphics node Triangle([readwrite(rendertarget)] texture renderTarget : SV_Target0);

由于 Triangle 节点是用渲染目标语义声明的,RPS 将自动将传递的资源参数绑定为渲染目标——因此,在回调中没有这样的绑定。我们还看到没有设置视口/剪刀矩形。RPS 将设置默认的视口和剪刀矩形,其宽度和高度范围设置为一组绑定 RT 的最小范围。默认视口 Z 范围设置为 [0.0, 1.0]。

可以通过具有 SV_Viewport[n]SV_ScissorRect[n] HLSL 语义的节点参数覆盖默认的视口/剪刀矩形。

总的来说,HLSL 语义作为控制图形状态设置的方法将在下一教程部分中进行更详细的解释。

RT 的自动绑定以及视口和剪刀矩形可以通过 RPS_CMD_CALLBACK_CUSTOM_VIEWPORT_SCISSOR_BIT 和/或 RPS_CMD_CALLBACK_CUSTOM_RENDER_TARGETS_BIT 标志选择性地禁用。这些标志在调用 rpsProgramBindNode 时传递。

记录后逻辑

即使渲染图命令已被记录,我们的 OnRender 函数仍有一些工作要做!批处理可能还指定,在提交后我们必须在队列上发出某个信号,

virtual void OnRender(uint32_t frameIndex) override
{
// ...
if (batch.signalFenceIndex != RPS_INDEX_NONE_U32)
{
m_fenceValue++;
FenceSignalInfo& sInfo = m_fenceSignalInfos[batch.signalFenceIndex];
sInfo.queueIndex = batch.queueIndex;
sInfo.value = m_fenceValue;
AssertIfFailed(m_queues[batch.queueIndex]->Signal(m_fences[sInfo.queueIndex].Get(), sInfo.value));
}
// ...
}

事实上,正是这个确切的信号调度时刻会产生在批次提交之前进行的栅栏信号等待调用。虽然这可能已经很明显了,但我们要提到,RPS 选择(以批次形式)调度命令,是为了多队列和异步计算场景。

RPS 清理

就是这样!RPS 正在施展它的魔法,我们的三角形又在渲染了。最后一步是确保 RPS 运行时得到妥善清理。

virtual void OnCleanUp() override
{
rpsRenderGraphDestroy(m_rpsRenderGraph);
rpsDeviceDestroy(m_rpsDevice);
// ...
}

… 瞧!这是我们刚刚渲染的三角形的截图。

a screenshot of the triangle that we just rendered

恭喜您完成了本教程!您现在应该对如何使用 RPS 设置一个简单的渲染管线有了基本的了解。

随着您继续使用 RPS,您可能会有一些进一步的问题。也许您正在思考

  • 如何在回调中从 RPSL 节点检索参数?

  • RPS 如何帮助管理瞬态内存?

  • 渲染图的结构是什么?

  • 调度程序是如何工作的?

  • 如何从多个线程记录命令?

别担心!在教程的接下来的几部分中,我们将回答所有这些问题以及更多问题。在本教程系列结束时,您将对使用 RPS 创建优化管线有一个坚实的基础。

Noah Cabral's avatar

Noah Cabral

Noah Cabral 是 AMD 核心技术组的图形研发实习生。他对计算机科学和计算机图形学的一切都怀有长久的热情,并希望为最先进的实时图形的发展做出贡献。
© . This site is unofficial and not affiliated with AMD.