RPS 教程第三部分 – RPS 与主机之间的互操作

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

引言

本教程将指导读者深入使用 RPS API,实现主机应用程序和 RPS 运行时之间的数据互操作。它将对第一部分教程中构建的 hello_triangle.cpp 示例进行修改。为了启用这些修改,请将 c_bBreathing 设置为 true。修改内容包括创建一个新的、次级渲染图,以渲染一个“呼吸”的三角形。

概述

将成员回调函数绑定到节点

我们在第一部分教程中看到,如果我们的节点回调函数是一个成员函数,但我们绑定的是一个非成员函数,那么就需要一个必要的步骤,将 this 指针存储在 RpsCmdCallbackContext 的用户数据中(即 pCmdCallbackContext 字段)。

通过切换到不同重载的 rpsProgramBindNode,我们可以避免这种样板代码,直接绑定到成员回调函数。

我们将在此方法中绑定新渲染图所调用的节点,

class HelloTriangle : public RpsAfxD3D12Renderer
{
virtual void OnInit(ID3D12GraphicsCommandList* pInitCmdList,
std::vector<ComPtr<ID3D12Object>>& tempResources) override
{
// ...
AssertIfRpsFailed(rpsProgramBindNode(rpsRenderGraphGetMainEntry(m_rpsRenderGraph),
"TriangleBreathing",
&HelloTriangle::DrawTriangleBreathingCb,
this));
}
// ...
void DrawTriangleBreathingCb(const RpsCmdCallbackContext* pContext)
{
// ...
}
// ...
};

对于此重载,我们必须传递 this 指针,而不是传递任何用户上下文。这确保了 RPS 可以通过指向成员的指针访问我们类实例的成员回调函数。

在回调函数中检索通用参数和节点信息

在对第一部分教程的扩展中,我们将渲染三角形而不受应用程序窗口大小调整的影响。我们将使用 RPS 来计算一个纠正性的纵横比参数,并将其传递给主机应用程序以提供给着色器。

这是我们新渲染图的 .rpsl 文件,

// ...
graphics node TriangleBreathing([readwrite(rendertarget)] texture renderTarget
: SV_Target0, float oneOverAspectRatio);
export void mainBreathing([readonly(present)] texture backbuffer)
{
ResourceDesc backbufferDesc = backbuffer.desc();
uint32_t width = (uint32_t)backbufferDesc.Width;
uint32_t height = backbufferDesc.Height;
float oneOverAspectRatio = height / (float)width;
// clear and then render geometry to backbuffer
clear(backbuffer, float4(0.0, 0.2, 0.4, 1.0));
TriangleBreathing(backbuffer, oneOverAspectRatio);
}

接下来,我们将更新 TriangleBreathing 回调函数以检索纵横比参数,

void DrawTriangleBreathingCb(const RpsCmdCallbackContext* pContext)
{ // ...
float oneOverAspectRatio = *rpsCmdGetArg<float, 1>(pContext);
pCmdList->SetGraphicsRoot32BitConstant(0, *(const UINT*)&oneOverAspectRatio, 0);
// ...
}

在这里,我们使用 rpsCmdGetArg 模板重载之一来检索节点参数。该函数不检查类型转换是否有效。如果需要,您可以使用 rpsCmdGetParamDesc 在调用此函数之前查询参数的类型信息和数组大小。

就是这样!

为了使这一切正常工作,DX12 端还需要一些其他更改(例如,修改 HLSL 着色器),但我们将省略它们。目前,请欣赏下图,

the aspect correct triangle

现在,将任意参数从节点调用传递到回调函数并不是最常见的情况。可能且有用的做法是,例如检索与某个资源视图关联的描述符。

这是如何实现的?

在 DX12 端,这是通过调用 rpsD3D12GetCmdArgDescriptor 来实现的。如果需要,还可以通过 rpsD3D12GetCmdArgResource 检索资源句柄。通常,可以通过调用 rpsCmdGet* 系列函数来查询有关节点参数或节点本身的任何信息。例如,可以使用 rpsCmdGetArgResourceDesc 来获取视图下资源的描述。在 VK 端,也有类似的 rpsVKGetCmdArg* 系列函数用于查询图像/缓冲区视图等。

如果节点声明参数是资源视图数组,则可以使用 rpsCmdGetArg*ArrayrpsD3D12*ArrayrpsVK*Array 系列函数来检索节点参数的信息。

让我们通过一个用程序生成的纹理渲染三角形的假设示例来探讨通过节点参数检索描述符的想法。

考虑以下节点声明,

compute node PerlinNoise([readwrite(cs)] texture t);

这声明了一个节点,我们打算用它通过计算着色器生成 Perlin 噪声。在渲染三角形时,我们将对该纹理进行采样。

这是其余的 RPSL,

graphics node DrawTriangle(rtv renderTarget : SV_Target0, srv noise);
export void main([readonly(present)] texture backBuffer)
{
clear(backBuffer, float4(0.0, 0.0, 0.0, 1.0));
uint noiseDim = 512;
texture noise = create_tex2d(RPS_FORMAT_R8G8B8A8_UNORM, noiseDim, noiseDim);
PerlinNoise(noise);
DrawTriangle(backBuffer, noise);
}

最后,在主机应用程序端,我们将修改回调函数以检索 SRV 描述符,

void DrawTriangle(const RpsCmdCallbackContext* pContext)
{
D3D12_CPU_DESCRIPTOR_HANDLE srv;
rpsD3D12GetCmdArgDescriptor(pContext, 1, &srv);
// ...
}

还有最后一点需要注意,您可能自己已经明白了,与资源视图节点参数关联的此类描述符存储在 RPS 管理的图形 API 堆中。在 DX12 端,这些是不可着色器可见的堆。

在回调函数中访问图形上下文绑定

首先,图形上下文绑定到底是什么?

这是在第一部分和第二部分教程中提到过的概念。这里有一个回顾

graphics node DrawTriangle(rtv backbuffer : SV_Target0);

具有 backbuffer 纹理视图绑定到语义 SV_Target0 的此类节点。这意味着在进入节点回调函数之前,RPS 的默认行为是将命令插入到您的命令缓冲区中,将此渲染目标绑定到管线。在后续的教程部分中,我们将通常将此行为称为节点前置处理。任何关闭行为,如 MSAA 解析(如果您使用了 SV_ResolveTarget[n] 装饰节点参数),发生在节点后置处理

回到最初的问题,我们可以说图形上下文绑定是通过节点前置处理设置状态,并在节点后置处理时拆除状态。

如前一节所述,rpsCmdGet* 系列函数可用于查询节点的通用信息。这些信息包括由前置处理设置的状态。

例如,查询此信息对于 VK 端设置 PSO 非常有用。主机应用程序可以查询由前置处理设置的渲染通道,用于创建合适的 PSO。

考虑以下代码片段,

// DrawGeo is the function bound as the node callback.
void DrawGeo(const RpsCmdCallbackContext* pContext)
{
CreateDefaultPso(pContext);
// record commands ...
}
void CreateDefaultPso(const RpsCmdCallbackContext* pContext)
{
if (!m_pso)
{
VkRenderPass rp;
rpsVKGetCmdRenderPass(pContext, &rp);
CreateGraphicsPipeline(rp, &m_pso);
}
}
void CreateGraphicsPipeline(VkRenderPass renderPass, VkPipeline* pPso)
{
// ...
VkGraphicsPipelineCreateInfo psoCI = {};
// ...
psoCI.renderPass = renderPass;
// ...
}

提前创建 Vulkan PSO

编译 PSO 可能会很昂贵,因此应用程序通常希望提前创建它们。如果使用 Vulkan 运行时后端,提前创建 PSO,并且不使用自定义渲染通道,主机应用程序必须使用与节点回调前置处理自动设置的渲染通道对象兼容的渲染通道对象。

没有办法查询 RPS 创建的 RP 的结构;但是,它很简单,我们将在这里描述其结构

  • 否则使用默认值。

  • 它包含一个子通道。

  • 它没有依赖项。

采用这种方法时,请确保根据节点的访问属性和语义,以及传递给节点的资源视图,正确设置附件和描述。

第二部分教程所述,节点语义控制着 RPS 创建的 RP 所使用的附件。

自动解包参数到节点回调

如果您的主机应用程序是用 C++ 编写的,您可以利用节点回调中的参数自动解包。这仅支持 rpsProgramBindNode 的模板版本。

继续修改 hello_triangle.cpp,让我们为 TriangleBreathing 回调函数添加一个时间参数。

从 RPSL 修改开始,

graphics node TriangleBreathing([readwrite(rendertarget)] texture renderTarget
: SV_Target0, float oneOverAspectRatio, float timeInSeconds);
export void mainBreathing([readonly(present)] texture backbuffer, float timeInSeconds)
{
// ...
TriangleBreathing(backbuffer, oneOverAspectRatio, timeInSeconds);
}

然后,我们可以轻松地做到以下几点,

void DrawTriangleBreathingCb(const RpsCmdCallbackContext* pContext,
rps::UnusedArg u0,
float oneOverAspectRatio,
float timeInSeconds)
{
// ...
oneOverAspectRatio *= abs(sin(timeInSeconds));
pCmdList->SetGraphicsRoot32BitConstant(0, *(const UINT*)&oneOverAspectRatio, 0);
// ...
}

rps::UnusedArg u0 旨在与 Triangle 节点的 backbuffer 参数对齐。RPS 为这种情况提供此特殊结构类型,以便解包,当您不使用特定参数但仍要求其他参数对齐时。

最后一步是将 timeInSeconds 参数传入 main 条目。我们将更新对 rpsRenderGraphUpdate 的调用,

virtual void OnUpdate(uint32_t frameIndex) override
{
// ...
float time = float(RpsAfxCpuTimer::SecondsSinceEpoch().count());
// ...
RpsConstant argData[] = {&backBufferDesc, &time};
// ...
AssertIfRpsFailed(rpsRenderGraphUpdate(m_rpsRenderGraph, &updateInfo));
// ...
}

就这样,我们的三角形现在看起来在呼吸了!

.GIF of a breathing triangle

同样,感兴趣的情况是当节点参数是资源视图时。在解包这些参数时,它们可以解包为例如 D3D12_CPU_DESCRIPTOR_HANDLEID3D12Resource*VkImageVkImageView 等。

继续上面的假设示例两节之前,我们的 DrawTriangle 函数可以这样转换,

void DrawTriangle(const RpsCmdCallbackContext* pContext, rps::UnusedArg u0, D3D12_CPU_DESCRIPTOR_HANDLE srv)
{
// ...
}

更一般地说,我们可以考虑一种自动解包情况,其中节点参数是资源视图数组,而不是上面的示例,它只演示了一个视图。但是,在这种情况下,目前不支持自动解包。可以使用 rpsCmdGetArgRuntimeResourceArray 代替。

编写自定义解包器

各种数据类型,如 D3D12_CPU_DESCRIPTOR_HANDLEID3D12Resource* 等,这些节点资源视图参数可以解包成,只是这种便利解包的冰山一角。主机应用程序可以轻松地扩展解包以支持解包到任意数据类型。

在深入研究如何做到这一点之前,我们将提供一些关于解包如何工作的细节。支持此功能的大部分代码都包含在 rps_cmd_callback_wrapper.hpp 中。通过一些 C++ 模板魔法,rps::details::CommandArgUnwrapper 的相应模板特化函数将为每个节点参数(不包括命令回调上下文)调用,以推断要传递给节点回调的内容。所选模板基于绑定回调中参数的类型。

再次考虑上面的 DrawTriangle 函数,

void DrawTriangle(const RpsCmdCallbackContext* pContext, rps::UnusedArg u0, D3D12_CPU_DESCRIPTOR_HANDLE srv)
{
// ...
}

对于第二个函数参数,将调用 rps::details::CommandArgUnwrapper<0, rps::UnusedArg>。对于第三个参数,将调用 rps::details::CommandArgUnwrapper<1, D3D12_CPU_DESCRIPTOR_HANDLE>

因此,扩展解包功能就像定义一个新的 rps::details::CommandArgUnwrapper 模板特化一样简单。

例如,让我们考虑您想创建一个解包器,该解包器将资源注册到您的无绑定资源系统,并返回该资源在描述符堆中的偏移量。这不成问题!

template <int32_t Index>
struct CommandArgUnwrapper<Index, MyBindlessOffset>
{
MyBindlessOffset operator()(const RpsCmdCallbackContext* pContext)
{
RpsVkImageViewInfo imageViewInfo;
RpsResult result = rpsVKGetCmdArgImageViewInfo(pContext, Index, &imageViewInfo);
if (RPS_FAILED(result))
{
rpsCmdCallbackReportError(pContext, result);
}
return MyVkRenderer->registerTransientTexture(imageViewInfo.hImageView, imageViewInfo.layout);
}
};
Noah Cabral's avatar

Noah Cabral

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