用于光线追踪的 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 个对象)。您的命中组索引,由着色器中的系统值给出,将是您的纹理索引。
在观看了有关 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 个对象)。您的命中组索引,由着色器中的系统值给出,将是您的纹理索引。