DX12光线追踪(DXR)描述符堆资源管理方案咨询
我非常理解你的困惑——DXR的资源模型和光栅化确实差异很大,尤其是光线追踪要求所有资源在发射光线前全局就绪,很容易在根签名、描述符堆的设计上绕弯路。结合你的三个尝试,我来梳理下正确的实践方向:
首先澄清你的核心误解:根签名可以绑定多个堆
你第二个方案的方向是对的,但你没找对绑定多个堆的方法。在DX12(包括DXR)中,根签名完全可以同时关联多个描述符堆,只需要在定义堆范围时指定**堆索引(heapIndex)**即可。
看你用的nv_helpers_dx12::RootSignatureGenerator,AddHeapRangesParameter接收的每个HeapRange结构体最后一个参数就是堆索引。比如你想绑定两个堆:一个全局堆(存TLAS、相机CBV),一个物体资源堆(存各物体的纹理SRV),可以这么修改代码:
bool PipelineState::CreateHitSignature(Microsoft::WRL::ComPtr<ID3D12RootSignature>& signature) { const auto device = RaytracingModule::GetInstance()->GetDevice(); if (device == nullptr) { return false; } nv_helpers_dx12::RootSignatureGenerator rsc; // 1. 添加根常量:用于传递当前实例的纹理SRV在物体堆中的偏移(可选,也可以用实例自定义数据) rsc.AddRootParameter(D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS, 1, 0); // b0: instanceSrvOffset // 2. 添加描述符表:指向全局堆(heapIndex=0)的TLAS和全局CBV rsc.AddHeapRangesParameter({ {2 /*t2*/, 1, 0, D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 0}, // TLAS在全局堆的第2个槽位 {0 /*b0*/, 1, 0, D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 0}, // 相机CBV在全局堆的第0个槽位 }); // 3. 添加描述符表:指向物体资源堆(heapIndex=1)的纹理SRV范围 rsc.AddHeapRangesParameter({ {3 /*t3*/, 100, 0, D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1}, // 预留100个槽位给物体纹理 }); signature = rsc.Generate(device, true); return signature.Get() != nullptr; }
这里的关键是每个HeapRange的最后一个参数heapIndex,它对应你后续绑定命令列表时的堆顺序(比如SetDescriptorHeaps(2, heaps),heaps[0]是全局堆,heaps[1]是物体资源堆)。
最佳实践:按描述符类型组织全局堆
像Falcor那样,把描述符按类型拆分到少数几个全局堆里,是DXR和DX12通用的高效做法:
- 创建一个CBV/SRV/UAV堆:存放所有全局资源(TLAS、相机CBV)和所有物体的纹理SRV、材质CBV等。
- 创建一个采样器堆:存放所有采样器资源(比如线性采样、各向异性采样)。
这种方式的好处是:
- 堆的数量极少(通常2-3个),避免频繁切换堆的开销。
- 同类型描述符放在一起,便于管理和批量更新。
- 符合DX12的资源绑定规范,减少API调用次数。
根签名的正确设计:复用+实例数据驱动
你第一个方案的问题在于试图为每个物体创建根签名——这完全违背了最佳实践,根签名切换是有性能开销的,应该尽量复用。正确的做法是:
- 设计通用根签名,覆盖所有可能的资源类型(全局资源、实例级资源)。
- 用实例数据(比如TLAS实例的CustomData、根常量)来传递当前物体的资源在堆中的索引/偏移,让命中着色器动态获取对应资源。
比如在命中着色器中,你可以这样获取当前物体的纹理:
[shader("closesthit")] void ClosestHitShader(inout RayPayload payload, in HitAttribs attribs) { // 获取实例自定义数据(比如我们把纹理SRV索引存在这里) uint instanceSrvIndex = GetInstanceCustomData(); // 从物体资源堆的描述符表中索引对应的纹理 Texture2D tex = t3[instanceSrvIndex]; // 采样纹理并计算颜色 payload.color = tex.Sample(samplerLinear, attribs.uv); }
这里的t3就是根签名中定义的物体纹理描述符表,instanceSrvIndex是我们在构建TLAS实例时存入的自定义数据(比如通过D3D12_RAYTRACING_INSTANCE_DESC::InstanceID或者扩展的实例数据)。
完整的资源管理流程示例
结合上面的思路,给你一个清晰的流程:
- 创建全局堆:
- 一个CBV/SRV/UAV堆,大小足够容纳TLAS、相机CBV、所有物体的纹理SRV和材质CBV。
- 一个采样器堆,存放常用的采样器。
- 填充全局堆:
- 把TLAS、相机CBV的描述符写入CBV/SRV/UAV堆的固定位置。
- 依次把每个物体的纹理SRV写入CBV/SRV/UAV堆,记录每个物体对应的SRV索引。
- 创建通用根签名:
- 包含根常量(或实例自定义数据)、全局资源描述符表、实例资源描述符表。
- 构建TLAS:
- 每个实例的
InstanceID或CustomData设置为该物体的纹理SRV索引。
- 每个实例的
- 绑定资源并发射光线:
- 在命令列表中绑定两个全局堆。
- 设置根签名,更新根常量(如果用的话)。
- 调用
DispatchRays发射光线。
对你之前三个方案的复盘
- 第一个方案:每个纹理一个根签名——完全没必要,根签名应尽量复用,用实例数据区分资源即可。
- 第二个方案:拆分全局堆和物体堆——方向正确,只是没掌握绑定多个堆的方法,修改堆范围的
heapIndex即可解决。 - 第三个方案:每个物体一个堆——不符合最佳实践,堆的数量有限制,且会导致大量堆切换开销,应合并同类型资源到全局堆。
内容的提问来源于stack exchange,提问作者Jakub D.

