用于光线追踪的 DXR 描述符堆管理

DXR Descriptor Heap management for raytracing

在观看了有关 DXR 和 DX12 的视频和文档后,我仍然不确定如何管理 DX12 光线追踪 (DXR) 的资源。

光栅化和光线追踪在资源管理方面有很大区别,主要区别在于光栅化有很多可以动态绑定的时间资源,而光线追踪需要所有资源 在投射光线时准备就绪。原因很明显,一条光线可以击中整个场景中的任何东西,所以我们需要在投射一条光线之前让每个着色器、每个纹理、每个堆都准备好并填充数据。

到目前为止一切顺利。

我的第一个测试是将所有资源添加到一个堆中 - 基于一些 DXR 教程。这种方法的问题出现在具有相同着色器但不同纹理的对象上。我为我的单一命中组定义了 1 个着色器根签名,我必须在光线追踪之前准备好。但是在创建根签名的时候,我们要准确的告诉纹理所在的SRV在堆中的哪个位置对应。由于堆中有许多位置不同的纹理,我需要为每个具有不同纹理的对象创建 1 个根签名。这当然不是首选,因为根据文档和常识,我们应该使根签名数量尽可能小。 因此,我放弃了这个测试。

我的第二种方法是为每个对象创建一个描述符堆,其中包含该特定对象(纹理、常量等)的所有本地描述符。全局资源 = TLAS(顶级加速结构),输出和相机常量缓冲区全局保存在一个单独的堆中。在这种方法中,我认为我认为我可以将多个堆添加到根签名,从而误解了文档。在我写这篇 post 时,我找不到将 2 个单独的堆添加到单个根签名的方法。如果这是可能的,我很想知道如何,所以任何帮助表示赞赏。

这里是我为根签名使用的代码(使用 dx12 助手):

bool PipelineState::CreateHitSignature(Microsoft::WRL::ComPtr<ID3D12RootSignature>& signature)
{
    const auto device = RaytracingModule::GetInstance()->GetDevice();
    if (device == nullptr)
    {
        return false;
    }

    nv_helpers_dx12::RootSignatureGenerator rsc;

    rsc.AddRootParameter(D3D12_ROOT_PARAMETER_TYPE_SRV,0);  // "t0" vertices and colors

    // Add a single range pointing to the TLAS in the heap
    rsc.AddHeapRangesParameter({
        {2 /*t2*/, 1, 0, D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1},   /* 2nd slot of the first heap */
        {3 /*t3*/, 1, 0, D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 3},   /* 4nd slot of the first heap. Per-instance data */
    });

    signature = rsc.Generate(device, true);

    return signature.Get() != nullptr;
}

现在我最后的方法是创建一个包含所有必要资源的堆 -> 每个对象的 TLAS、CBV、SRV(纹理)等 = 每个对象有效 1x 堆。同样,当我阅读文档时,不建议这样做,并且文档说明我们应该将资源分组到全局堆中。在这一点上,我感觉我正在混合 DX12 和 DXR 文档和最佳实践,通过在 DXR 域中使用 DX12 的建议,这可能是错误的。

我还部分阅读了 Nvidia Falcor 源代码,他们似乎每个描述符类型有 1 个资源堆,有效地将描述符堆的数量限制在最低限度(完全有意义),但我没有找到根签名使用多个单独的堆创建。

我觉得在一切都到位并创造出美丽的形象之前,我好像错过了这个谜团的最后一个拼图部分。因此,如果有人可以解释在 DXR 中应该如何处理资源管理(堆、描述符等),如果我们想要拥有许多不同资源的对象,那将对我有很大帮助。

提前致谢! 雅库布

HLSL 5.1 的动态索引可能是这个问题的解决方案。

https://docs.microsoft.com/en-us/windows/win32/direct3d12/dynamic-indexing-using-hlsl-5-1

  • 使用动态索引,我们可以创建一个包含所有 material 的堆,并为每个对象使用一个索引,该索引将在着色器中使用以在 运行 处获取正确的 material时间
  • 因此,我们不需要相同类型的多个堆,因为无论如何这是不可能的。每种堆类型只能同时使用 1 个堆

对于 DXR,您需要从着色器模型 6.2 开始,其中动态索引开始获得更多官方支持,而不仅仅是“最后一个描述符在看似超限的索引中自由泄漏”,这是“秘密”方法在 5.1

现在您使用 type var[] : register(t4, 1); 声明性语法实现了完全“无绑定”,您可以自由索引 var[1] 将访问寄存器 (t5,1)
您可以在描述符 table 中设置寄存器范围,因此如果您有 100 个纹理,则可以跨越 100 个。
只要记得跳转所有寄存器,你甚至可以在数组变量之后声明其他资源。但是使用不同的虚拟 spaces:

更容易
float4 ambiance : register(b0, 0);
Texture2D all_albedos[] : register(t0, 1);
matrix4x4 world : register(b1, 0);

现在您可以转到 t100 而不会干扰以下 space0 项声明。
SM6 中取消了对寄存器值的限制。这是

up to max supported heap allocation

所以 all_albedos[3400].Sample(..) 是一个完美的 acceptable 调用(前提是你的堆已经绑定了视图)。

不幸的是,在 DX12 中,它们给您的感觉是您可以使用 CommandList::SetDescriptorHeaps 函数绑定多个堆,但如果您尝试,您将遇到运行时错误:

D3D12 ERROR: ID3D12CommandList::SetDescriptorHeaps: pDescriptorHeaps[1] sets a descriptor heap type that appears earlier in the pDescriptorHeaps array.
Only one of any given descriptor heap type can be set at a time. [ EXECUTION ERROR #554: SET_DESCRIPTOR_HEAP_INVALID]

这是一种误导,所以不要相信方法名称中的复数 s
真的,如果我们有多个堆,那只会是因为三重缓冲循环 update/usage 情况,或者我想是 upload/shader-visible。只需将所有内容都放在一个堆中,然后根据需要让描述符 table 索引在其中。

一个描述符table是一个非常轻量级的元素,它只有3个整数。一个描述符开始,一个跨度和一个虚拟 space。只需使用它,如果场景中有 1000 个纹理,则可以跨越 1000 个纹理。如果将 material ID 嵌入到具有独特 UV(如光照贴图)的间接纹理中,则可以获得 material ID。或者在顶点数据中,或者只是整个命中组(如果您设置 1 个命中组 = 1 个对象)。您的命中组索引,由着色器中的系统值给出,将是您的纹理索引。