You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DX12光线追踪(DXR)描述符堆资源管理方案咨询

解决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等。
  • 创建一个采样器堆:存放所有采样器资源(比如线性采样、各向异性采样)。

这种方式的好处是:

  1. 堆的数量极少(通常2-3个),避免频繁切换堆的开销。
  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或者扩展的实例数据)。

完整的资源管理流程示例

结合上面的思路,给你一个清晰的流程:

  1. 创建全局堆:
    • 一个CBV/SRV/UAV堆,大小足够容纳TLAS、相机CBV、所有物体的纹理SRV和材质CBV。
    • 一个采样器堆,存放常用的采样器。
  2. 填充全局堆:
    • 把TLAS、相机CBV的描述符写入CBV/SRV/UAV堆的固定位置。
    • 依次把每个物体的纹理SRV写入CBV/SRV/UAV堆,记录每个物体对应的SRV索引。
  3. 创建通用根签名:
    • 包含根常量(或实例自定义数据)、全局资源描述符表、实例资源描述符表。
  4. 构建TLAS:
    • 每个实例的InstanceID或CustomData设置为该物体的纹理SRV索引。
  5. 绑定资源并发射光线:
    • 在命令列表中绑定两个全局堆。
    • 设置根签名,更新根常量(如果用的话)。
    • 调用DispatchRays发射光线。

对你之前三个方案的复盘

  • 第一个方案:每个纹理一个根签名——完全没必要,根签名应尽量复用,用实例数据区分资源即可。
  • 第二个方案:拆分全局堆和物体堆——方向正确,只是没掌握绑定多个堆的方法,修改堆范围的heapIndex即可解决。
  • 第三个方案:每个物体一个堆——不符合最佳实践,堆的数量有限制,且会导致大量堆切换开销,应合并同类型资源到全局堆。

内容的提问来源于stack exchange,提问作者Jakub D.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 04:33:37