DXR 流程

以 Nvidia 的 DxrTutorials 为例 NVIDIAGameWorks/DxrTutorials

​ DXR 与 DX12 正常流程并存 ,主要为了 DX12 流程补充光追效果,所以主要初始化流程与原DX12无异,但 DXR 中引入了用于光线追踪的新 interface ,资源管理与 DX12 无异,都需要手动管理;

​ 接下来通过 DxrTurtorials 例程来简述 DXR 的新流程;本文旨在描述构建 DXR 的管线流程,而并非 DXR 高级效果教程

先来速览一下微软给出的 DXR 所用的大致数据结构

rayGeometryIntersection

Acceleration Structures

​ 简称AS ,为用于加速光线追踪所用的特殊数据结构,其构成可为 BVH Kd-Tree 等常见加速结构,在 DXR 中实现了数种不同特点的 BVH 构造方式,在使用 API 构造 AS 时可指定 flag 使用由硬件提供的 BVH 构造方法

  • 当指定 PREFER_FAST_BUILD flag 时,硬件会使用构造速度尽可能快的算法;
  • 当指定 PREFER_FAST_TRACE flag 时,硬件使用尽可能高质量的 BVH 构造算法是光线求交更为高效,但同时也带来更长的构造开销;
  • 当不指定 flag 时采用默认构造算法,在构造开销与求交质量中折中;

PREFER_FAST_BUILD 可能对应 LBVH 算法 ;

PREFER_FAST_TRACE 可能对应 Spatial-split SAH ;

折中算法可能对应 Binning SAH

​ 在 DxrTurtorials 中使用如下结构体表示 AS

struct AccelerationStructureBuffers
{
    ID3D12ResourcePtr pScratch;
    ID3D12ResourcePtr pResult;
    ID3D12ResourcePtr pInstanceDesc;    // Used only for top-level AS
};

在 DXR 中,我们需要生成两种 AS 用于加速光线求交 :

NVIDIA RTX

Bottom-Level Acceleration Structure

​ 简称 BLAS ,该数据用于表示 local-space mesh ,并不包括对于世界空间的变换矩阵和 Instancing Data ;

​ 为创建 BLAS,我们需要如下几个组件:

  • Scratch Buffer (Resource) 用于表示中间计算所需的暂存缓冲区
  • Result Buffer (Resource) 用于存储加速结构数据
  • Structure Inputs (D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS) 场景物体的结构表示

​ 我们先从对场景数据的表示开始 :如 DX12 创建 Resource 相同 ,我们需要一个对应的 Desc 结构体用于描述所创建的资源类型与格式等数据。而对于 DXR 我们使用 D3D12_RAYTRACING_GEOMETRY_DESC 用于描述物体的数据:

D3D12_RAYTRACING_GEOMETRY_DESC geomDesc = {};
geomDesc.Type = D3D12_RAYTRACING_GEOMETRY_TYPE_TRIANGLES;
geomDesc.Triangles.VertexBuffer.StartAddress = pVB->GetGPUVirtualAddress();
geomDesc.Triangles.VertexBuffer.StrideInBytes = sizeof(vec3);
geomDesc.Triangles.VertexFormat = DXGI_FORMAT_R32G32B32_FLOAT;
geomDesc.Triangles.VertexCount = 3;
geomDesc.Flags = D3D12_RAYTRACING_GEOMETRY_FLAG_OPAQUE;

​ 在这段代码中,我们创建了一个包含 "一个三角形" 的 Desc ,使用 D3D12_RAYTRACING_GEOMETRY_TYPE_TRAIANGLES 的 type 意味着我们将使用 DXR 内置的 triangle intersection shader 进行光线与图元的求交 ;

​ 然后是对 Vertex Buffer 数据的提交与描述

​ 最后使用 D3D12_RAYTRACING_GEOMETRY_FLAG_OPAQUE flag 表示我们所提交的是非透明图元,DXR规范中建议尽可能的使用该 flag 描述 (为什么呢?因为可以直接跳过 intersection shader !后面讲)

​ 在构建好一组 D3D12_RAYTRACING_GEOMETRY_DESC 的场景不同物体的表示后,就可以将这些数据进行组合,调用 GetRaytracingAccelerationStructurePrebuildInfo 形成场景图元的结构表示 D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS

D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS inputs = {};
inputs.DescsLayout = D3D12_ELEMENTS_LAYOUT_ARRAY;
inputs.Flags = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_NONE;
inputs.NumDescs = 1;
inputs.pGeometryDescs = &geomDesc;
inputs.Type = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL;

D3D12_RAYTRACING_ACCELERATION_STRUCTURE_PREBUILD_INFO info;
pDevice->GetRaytracingAccelerationStructurePrebuildInfo(&inputs, &info);

D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS 结构体如下:

typedef struct D3D12_RAYTRACING_ACCELERATION_STRUCTURE_PREBUILD_INFO
{
    UINT64 ResultDataMaxSizeInBytes;
    UINT64 ScratchDataSizeInBytes;
    UINT64 UpdateScratchDataSizeInBytes;
} 	D3D12_RAYTRACING_ACCELERATION_STRUCTURE_PREBUILD_INFO;

其中就包含了我们所需的创建各个 Buffer 的长度,所以接下来我们就可以创建 BLAS 的所使用的 Buffer Resource

buffers.pScratch = createBuffer(pDevice, info.ScratchDataSizeInBytes, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, kDefaultHeapProps);
buffers.pResult = createBuffer(pDevice, info.ResultDataMaxSizeInBytes, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_RAYTRACING_ACCELERATION_STRUCTURE, kDefaultHeapProps);

createBuffer 为对DX12创建 Buffer 的封装

​ 这里需要注意的是 Scratch BufferResult Buffer 都必须以 D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS 描述,因为我们需要对这两个 Buffer 进行读写操作,且我们需要使用 UAV barriers 来进行同步操作。

​ 对于初始状态也有如下规定:

  • Scratch Buffer 初始状态需为 D3D12_RESOURCE_STATE_UNORDERED_ACCESS
  • Result Buffer 初始状态需为 D3D12_RESOURCE_STATE_RAYTRACING_ACCELERATION_STRUCTURE

有了所有构成 BLAS 所需要的组件,我们就可以生成指令让 GPU 进行 AS 的构建

D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC asDesc = {};
asDesc.Inputs = inputs;
asDesc.DestAccelerationStructureData = buffers.pResult->GetGPUVirtualAddress();
asDesc.ScratchAccelerationStructureData = buffers.pScratch->GetGPUVirtualAddress();

pCmdList->BuildRaytracingAccelerationStructure(&asDesc);

为保证对 Result Buffer 的读取发生在 AS 的生成完成之后,我们需要使用一个 Resource Barrier 来进行同步

D3D12_RESOURCE_BARRIER uavBarrier = {};
uavBarrier.Type = D3D12_RESOURCE_BARRIER_TYPE_UAV;
uavBarrier.UAV.pResource = buffers.pResult;
pCmdList->ResourceBarrier(1, &uavBarrier);

Top-Level Acceleration Structure

​ 简称为 TLAS ,TLAS 是用于表示场景数据在世界空间中的表示 ,而先前的 BLAS 则是场景数据在局部空间的表示,则我们需要使用的 local-to-world 矩阵与 Instancing 数据来对 BLAS 进一步修饰。

对于 TLAS 生成,我们需要如下几个组件:

  • Scratch Buffer (Resource) 用于表示中间计算所需的暂存缓冲区
  • Result Buffer (Resource) 用于存储加速结构数据
  • Structure Inputs (D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS) 场景物体的结构表示
  • ScratchAccelerationStructureData (Resource) 用于存储场景物体的 Instancing 数据,包含InstanceID和 shader-binding-table 中的偏移量 (后面讲)

​ TLAS 的生成步骤与 BLAS 的生成方式非常相似 ,但对于 TLAS而言, 并不需要获取实际场景数据在local space的描述,所以对于 D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS 有所不同:

D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS inputs = {};
inputs.DescsLayout = D3D12_ELEMENTS_LAYOUT_ARRAY;
inputs.Flags = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_NONE;
inputs.NumDescs = 1;
inputs.Type = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL;

此时我们不需要提交描述每个物体数据的 desc ,只需要给出 desc 的数量并使用 D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL type 描述指明我们正在创建 Top-Level AS;

AccelerationStructureBuffers buffers;
buffers.pScratch = createBuffer(pDevice, info.ScratchDataSizeInBytes, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, kDefaultHeapProps);
buffers.pResult = createBuffer(pDevice, info.ResultDataMaxSizeInBytes, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_RAYTRACING_ACCELERATION_STRUCTURE, kDefaultHeapProps);

创建 TLAS 所需要的 Resource Buffer 和 Scratch Buffer 与先前无异;

而接下来我们需要创建 TLAS 所需要的、用于存储 Instancing 数据的 InstanceDesc Buffer

buffers.pInstanceDesc = createBuffer(pDevice, sizeof(D3D12_RAYTRACING_INSTANCE_DESC), D3D12_RESOURCE_FLAG_NONE, D3D12_RESOURCE_STATE_GENERIC_READ, kUploadHeapProps);

D3D12_RAYTRACING_INSTANCE_DESC 描述如下

typedef struct D3D12_RAYTRACING_INSTANCE_DESC
{
    FLOAT Transform[ 3 ][ 4 ];
    UINT InstanceID	: 24;
    UINT InstanceMask	: 8;
    UINT InstanceContributionToHitGroupIndex	: 24;
    UINT Flags	: 8;
    D3D12_GPU_VIRTUAL_ADDRESS AccelerationStructure;
} 	D3D12_RAYTRACING_INSTANCE_DESC;

​ 而这个 buffer 中的内容需要我们自己进行填充;接下来展示对先前 BLAS 中存储的一个三角形进行 Instancing 数据的填充:

D3D12_RAYTRACING_INSTANCE_DESC* pInstanceDesc;
buffers.pInstanceDesc->Map(0, nullptr, (void**)&pInstanceDesc);

// Initialize the instance desc. We only have a single instance
pInstanceDesc->InstanceID = 0;                            // This value will be exposed to the shader via InstanceID()
pInstanceDesc->InstanceContributionToHitGroupIndex = 0;   // This is the offset inside the shader-table. We only have a single geometry, so the offset 0
pInstanceDesc->Flags = D3D12_RAYTRACING_INSTANCE_FLAG_NONE;
mat4 m; // Identity matrix
memcpy(pInstanceDesc->Transform, &m, sizeof(pInstanceDesc->Transform));
pInstanceDesc->AccelerationStructure = pBottomLevelAS->GetGPUVirtualAddress();
pInstanceDesc->InstanceMask = 0xFF;

// Unmap
buffers.pInstanceDesc->Unmap(0, nullptr);

InstanceID 只会影响 shader 中 InstanceID() 的返回值 ,对 raytrace 没有实际影响

组件齐全!接下来我们就可以生成命令生成 TLAS 了

D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC asDesc = {};
asDesc.Inputs = inputs;
asDesc.Inputs.InstanceDescs = buffers.pInstanceDesc->GetGPUVirtualAddress();
asDesc.DestAccelerationStructureData = buffers.pResult->GetGPUVirtualAddress();
asDesc.ScratchAccelerationStructureData = buffers.pScratch->GetGPUVirtualAddress();

别忘了同步

D3D12_RESOURCE_BARRIER uavBarrier = {};
uavBarrier.Type = D3D12_RESOURCE_BARRIER_TYPE_UAV;
uavBarrier.UAV.pResource = buffers.pResult;
pCmdList->ResourceBarrier(1, &uavBarrier);

在两个 Acceleration Structure 都生成完毕后,我们已经拥有了对场景的数据描述,接下来就是真正的Ray Tracing 🕶(?

Raytracing Pipeline

​ 让我们回顾在光线追踪中的一个简化流程 :

  • 由相机发出光线、通过镜头变换后进入场景
  • => 与场景加速结构求交 (包括 alpha test)、找到 closest hit point
  • => 通过相交片元的材质、法线方向、重心坐标等属性获取光线折射、反射、能量吸收等进一步情况
  • => 递归至设定情况后回溯计算相机发出点像素颜色

DXR 流程与上述类似,为此提供了五种新 Shader 类型 :ray-generationmissclosest-hitany-hitintersection

NVIDIA RTX

该图为简化流程、详细流程如下

traceRayControlFlow

Ray-Generation Shader

​ Ray-Generation shader 是 DXR 管线的第一个阶段,将会为每个工作项 (后面讲) 调用一次,用户将在这里生成 primary-rays 和 发起光线求交查询;

这里是一个简单的 Ray-Generation shader 样例

RaytracingAccelerationStructure gRtScene : register(t0);
RWTexture2D<float4> gOutput : register(u0);

[shader("raygeneration")]
void rayGen()
{  
    uint3 launchIndex = DispatchRaysIndex();
    float3 col = linearToSrgb(float3(0.4, 0.6, 0.2));
    gOutput[launchIndex.xy] = float4(col, 1);
}

​ 留意到使用了新的 HLSL 对象 RaytracingAccelerationStructure ,这就是我们先前生成的 Top-level AS ,使用 resource view进行绑定 ,而 gOutput 则是我们用于输出结果的一张 texture;

DispatchRaysIndex() 则是返回当前工作项的 3D-index ,可以类比 GroupThreadID

Miss Shader

​ 当光线求交查询并没有和 TLAS 中任何片元相交时,将会调用该 Shader ;

这里是一个简单的 Miss-Shader 样例

struct Payload
{
    bool hit;
}

[shader("miss")]
void miss(inout Payload payload)
{  
    payload.hit = false;
}

​ Miss Shader 接受一个 inout 的 payload 参数,该参数是用户自定义用于 shader 间传递数据的一个结构体、此处我们只是将其设定为由一个 bool 判断是否有交点组成的结构体。

Hit-Group

​ 我们将 Closest-HitAny-Hit 、**Intersection ** 三种 shader 统称为 Hit-Group ;由这样的一组 Hit-Group 共同决定了当光线与 TLAS 获取交点后的行为。

Any Hit Shader

​ 该 shader 将在找到任意光线与 TLAS 的交点时进行调用,其主要的应用是对本次交点结果进行取舍,如对当前点进行 alpha-test 结果为失败时 ,对本次相交结果进行舍弃。

​ 这里需要留意,先前生成 BLAS 时 ,如果我们使用了 D3D12_RAYTRACING_GEOMETRY_FLAG_OPAQUE flag 进行修饰,默认该片元不存在 alpha-test ,DXR 管线则会跳过 Any Hit Shader

​ 由于光线与 TLAS 的求交并不保证找到多个交点时调用 Any Hit Shader 的顺序,也就是第一个调用该shader 的并不意味着该点就是 Closest Hit Point;

Closest Hit Shader

​ 在获取所有交点后,将会对光线传输路径上的最近交点调用该 Shader

Intersection Shader

​ 该 shader 将在光线与TLAS求交时、与我们声明 BLAS 时指定为 axis-aligned bounding-box 种类的片元发生相交判断时调用;如果是与三角形片元进行相交判断,则无论是否用户自定义了 Intersection Shader 都将调用内置的 triangle-intersection shader

快速回顾

D3D12_RAYTRACING_GEOMETRY_DESC geomDesc = {};
geomDesc.Type = D3D12_RAYTRACING_GEOMETRY_TYPE_TRIANGLES; 
//为三角形,将调用DXR内置的三角形 Intersection Shader
——————————————————————————————————————————————————————————
geomDesc.Type = D3D12_RAYTRACING_GEOMETRY_TYPE_PROCEDURAL_PRIMITIVE_AABBS
//为AABB,将调用用户自定义的 Intersection Shader

RT Pipeline State Object

​ 在了解了 DXR 所使用的 Shader 与流程后,我们现在来关注一些基础设施,如DX12中的光栅化管线需要我们设置PSO、RootSignature、绑定资源一样,DXR管线同样需要这些步骤;

​ 但创建 RT PSO 的流程与创建 PSOs 有所不同,我们将使用类似于 D3D12_GRAPHICS_PIPELINE_STATE_DESC 的一组 D3D12_STATE_SUBOBJECT 来组成 D3D12_STATE_OBJECT_DESC 进行 RT PSO 构建。

std::array<D3D12_STATE_SUBOBJECT, 10> subobjects;
......//subobject 的填充
D3D12_STATE_OBJECT_DESC desc;
desc.NumSubobjects = index; // 10
desc.pSubobjects = subobjects.data();
desc.Type = D3D12_STATE_OBJECT_TYPE_RAYTRACING_PIPELINE;
//合成吧!
d3d_call(mpDevice->CreateStateObject(&desc, IID_PPV_ARGS(&mpPipelineState)));

ok 问题就在 subobject 的填充上,先来看看 D3D12_STATE_SUBOBJECT_TYPE 有哪些

typedef 
enum D3D12_STATE_SUBOBJECT_TYPE
    {
        D3D12_STATE_SUBOBJECT_TYPE_STATE_OBJECT_CONFIG	= 0,
        D3D12_STATE_SUBOBJECT_TYPE_GLOBAL_ROOT_SIGNATURE	= 1,
        D3D12_STATE_SUBOBJECT_TYPE_LOCAL_ROOT_SIGNATURE	= 2,
        D3D12_STATE_SUBOBJECT_TYPE_NODE_MASK	= 3,
        D3D12_STATE_SUBOBJECT_TYPE_DXIL_LIBRARY	= 5,
        D3D12_STATE_SUBOBJECT_TYPE_EXISTING_COLLECTION	= 6,
        D3D12_STATE_SUBOBJECT_TYPE_SUBOBJECT_TO_EXPORTS_ASSOCIATION	= 7,
        D3D12_STATE_SUBOBJECT_TYPE_DXIL_SUBOBJECT_TO_EXPORTS_ASSOCIATION	= 8,
        D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_SHADER_CONFIG	= 9,
        D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_PIPELINE_CONFIG	= 10,
        D3D12_STATE_SUBOBJECT_TYPE_HIT_GROUP	= 11,
        D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_PIPELINE_CONFIG1	= 12,
        D3D12_STATE_SUBOBJECT_TYPE_MAX_VALID	= ( D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_PIPELINE_CONFIG1 + 1 ) 
    } 	D3D12_STATE_SUBOBJECT_TYPE;

我们将通过 DxrTurtorials 样例,来展示一个 StateObject 如何构建 :

DXIL_LIBRARY

uint32_t index = 0;

// Create the DXIL library
DxilLibrary dxilLib = createDxilLibrary();
subobjects[index++] = dxilLib.stateSubobject; // 0 Library
......

​ 此处 DxilLibrary 是对 D3D12_STATE_SUBOBJECT_TYPE_DXIL_LIBRARY 的构建封装

struct DxilLibrary
{
    DxilLibrary(ID3DBlobPtr pBlob, const WCHAR* entryPoint[], uint32_t entryPointCount) : pShaderBlob(pBlob)
    {
        stateSubobject.Type = D3D12_STATE_SUBOBJECT_TYPE_DXIL_LIBRARY;
        stateSubobject.pDesc = &dxilLibDesc;

        dxilLibDesc = {};
        exportDesc.resize(entryPointCount);
        exportName.resize(entryPointCount);
        if (pBlob)
        {
            dxilLibDesc.DXILLibrary.pShaderBytecode = pBlob->GetBufferPointer();
            dxilLibDesc.DXILLibrary.BytecodeLength = pBlob->GetBufferSize();
            dxilLibDesc.NumExports = entryPointCount;
            dxilLibDesc.pExports = exportDesc.data();

            for (uint32_t i = 0; i < entryPointCount; i++)
            {
                exportName[i] = entryPoint[i];
                exportDesc[i].Name = exportName[i].c_str();
                exportDesc[i].Flags = D3D12_EXPORT_FLAG_NONE;
                exportDesc[i].ExportToRename = nullptr;
            }
        }
    };

    DxilLibrary() : DxilLibrary(nullptr, nullptr, 0) {}

    D3D12_DXIL_LIBRARY_DESC dxilLibDesc = {};
    D3D12_STATE_SUBOBJECT stateSubobject{};
    ID3DBlobPtr pShaderBlob;
    std::vector<D3D12_EXPORT_DESC> exportDesc;
    std::vector<std::wstring> exportName;
};

​ dxcompiler (SM6.x) 提供了一种名为 DxilLibrary 的新概念,支持在无需指定 shader 程序入口的方式编译含有多个 shader 的文件,这里如何进行编译可自行查阅源代码;

​ 通过填写 D3D12_STATE_SUBOBJECT 中的 type 与 D3D12_DXIL_LIBRARY_DESC 我们就得到了所需要的第一个 State Sub Object ,该 State Sub Object 存储了所有使用到的 shader 的 Dxil blob 数据 ;

​ 此处需要留意以下两点 :

  • 我们将 shader 入口点的名称预先缓存了起来
  • 在此例子中我们将 ExportToRename 设为 nullptr ,实际上在后续我们需要通过 shader 入口点来识别 shader 文件以传递给其他对象时,可能会出现多个 shader 使用的入口点名称相同,从而使标识不明确,所以我们可以使用 ExportToRename 来指定唯一名称,样例中入口点名称本就不同所以并不需要使用该标识。

Hit_Group

......
HitProgram hitProgram(nullptr, kClosestHitShader, kHitGroup);
subobjects[index++] = hitProgram.subObject; // 1 Hit Group
......

​ 此处 HitProgram 是对 D3D12_STATE_SUBOBJECT_TYPE_HIT_GROUP 的构建封装; 前文也描述过 Hit_Group 是一系列 intersection , any-hit 和 closest-hit shader 的组合(每种至少一个),在样例中并不使用自定义的 Intersection shader 所以该 HitProgram 封装只接受 AHS 和 CHS 的入口名称(简写自己猜)

struct HitProgram
{
    HitProgram(LPCWSTR ahsExport, LPCWSTR chsExport, const std::wstring& name) : exportName(name)
    {
        desc = {};
        desc.AnyHitShaderImport = ahsExport;
        desc.ClosestHitShaderImport = chsExport;
        desc.HitGroupExport = exportName.c_str();

        subObject.Type = D3D12_STATE_SUBOBJECT_TYPE_HIT_GROUP;
        subObject.pDesc = &desc;
    }

    std::wstring exportName;
    D3D12_HIT_GROUP_DESC desc;
    D3D12_STATE_SUBOBJECT subObject;
};

与先前一样填充 D3D12_STATE_SUBOBJECT 的 type 和 D3D12_HIT_GROUP_DESC 数据 ,AnyHitShaderImportClosestHitShaderImport 是我们在创建 Dxil Library 时对应 Shader 声明的导出名 (没有rename的话就是原入口名),HitGroupExport 则是后续在引用该 hit-group 时的唯一名称

LocalRootSignature

......
// Create the ray-gen root-signature 
LocalRootSignature rgsRootSignature(mpDevice, createRayGenRootDesc().desc);
subobjects[index] = rgsRootSignature.subobject; // 2 RayGen Root Sig
......

​ 这是在 DXR 中出现的新概念,在 graphics 和 compute 管线下,我们通常使用一个全局唯一的 RootSignature ;而对于 Raytracing 管线 ,我们可以建立 Local RootSignature 并将它绑定到不同的 shader 上 ,该Local RootSignature 的尺寸将影响 Shader Binding Table (后面讲、简写SBT) ,并允许我们去进一步优化 SBT

此处 Local RootSignature 是对 D3D12_STATE_SUBOBJECT_TYPE_LOCAL_ROOT_SIGNATURE 的构建封装

struct LocalRootSignature
{
    LocalRootSignature(ID3D12Device5Ptr pDevice, const D3D12_ROOT_SIGNATURE_DESC& desc)
    {
        pRootSig = createRootSignature(pDevice, desc);
        pInterface = pRootSig.GetInterfacePtr();
        subobject.pDesc = &pInterface;
        subobject.Type = D3D12_STATE_SUBOBJECT_TYPE_LOCAL_ROOT_SIGNATURE;
    }
    ID3D12RootSignaturePtr pRootSig;
    ID3D12RootSignature* pInterface = nullptr;
    D3D12_STATE_SUBOBJECT subobject = {};
};

ID3D12RootSignaturePtr createRootSignature(ID3D12Device5Ptr pDevice, const D3D12_ROOT_SIGNATURE_DESC& desc)
{
    ID3DBlobPtr pSigBlob;
    ID3DBlobPtr pErrorBlob;
    HRESULT hr = D3D12SerializeRootSignature(&desc, D3D_ROOT_SIGNATURE_VERSION_1, &pSigBlob, &pErrorBlob);
    if (FAILED(hr))
    {
        std::string msg = convertBlobToString(pErrorBlob.GetInterfacePtr());
        msgBox(msg);
        return nullptr;
    }
    ID3D12RootSignaturePtr pRootSig;
    d3d_call(pDevice->CreateRootSignature(0, pSigBlob->GetBufferPointer(), pSigBlob->GetBufferSize(), IID_PPV_ARGS(&pRootSig)));
    return pRootSig;
}

​ 可见 Local RootSignature 的构建与 Global RootSignature 一模一样,但我们需要的是 D3D12_STATE_SUBOBJECT 所以指定 type 为 D3D12_STATE_SUBOBJECT_TYPE_LOCAL_ROOT_SIGNATURE 和 填充ID3D12RootSignature 与先前一样 ;

​ 但这里并没有指定我们这个 Local RootSignature 会用在什么 shader 上! 接下来就又要引入一个新的概念,将该 Local RootSignature 和我们在 Dxil Library 中的 shader 数据进行关联;

ExportAssociation

......
uint32_t rgsRootIndex = index++; // 2
ExportAssociation rgsRootAssociation(&kRayGenShader, 1, &(subobjects[rgsRootIndex]));
subobjects[index++] = rgsRootAssociation.subobject; // 3 Associate Root Sig to RGS
......

​ ExportAssociation 是 D3D12_SUBOBJECT_TO_EXPORTS_ASSOCIATION 的构建封装 ,将 Local RootSignature 和 shader 绑定起来的 ,居然还是一个 sub-object ...... ;

ExportAssociation 将一个 sub-object 与 shaders 和 hit-groups 进行绑定 ,其封装定义如下:

struct ExportAssociation
{
    ExportAssociation(const WCHAR* exportNames[], uint32_t exportCount, const D3D12_STATE_SUBOBJECT* pSubobjectToAssociate)
    {
        association.NumExports = exportCount;
        association.pExports = exportNames;
        association.pSubobjectToAssociate = pSubobjectToAssociate;

        subobject.Type = D3D12_STATE_SUBOBJECT_TYPE_SUBOBJECT_TO_EXPORTS_ASSOCIATION;
        subobject.pDesc = &association;
    }

    D3D12_STATE_SUBOBJECT subobject = {};
    D3D12_SUBOBJECT_TO_EXPORTS_ASSOCIATION association = {};
};

​ 我们可以将同一 sub-object 与多个 shader 和 hit-groups 进行绑定,只需要指定其入口名称即可;

​ 这里的关键在于,这个 sub-object 必须在我们最后组合 D3D12_STATE_OBJECT_DESC 时使用的 D3D12_STATE_SUBOBJECT 列表上;并且传入的参数为该 sub-object 在 subobject array 上的地址!&(subobjects[rgsRootIndex]) 而不是 sub-object 自身的地址 &(rgsRootSignature.subobject)

​ 接下来就是填充好 D3D12_STATE_SUBOBJECT 的 type 和 D3D12_SUBOBJECT_TO_EXPORTS_ASSOCIATION

ShaderConfig

......
// Bind the payload size to the programs
ShaderConfig shaderConfig(sizeof(float) * 2, sizeof(float) * 1);
subobjects[index] = shaderConfig.subobject; // 6 Shader Config
......

​ ShaderConfig 是对 D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_SHADER_CONFIG 的构造封装,该 sub-object 用于指定以下两个数值:

  • 在 shader 间传递数据的 payload 大小

  • attribute 的数据大小,该数据为 intersection shader 传递给 hit-shader 的数据大小 ,对于内置的 triangle intersection shader 该值为两个 float

struct ShaderConfig
{
    ShaderConfig(uint32_t maxAttributeSizeInBytes, uint32_t maxPayloadSizeInBytes)
    {
        shaderConfig.MaxAttributeSizeInBytes = maxAttributeSizeInBytes;
        shaderConfig.MaxPayloadSizeInBytes = maxPayloadSizeInBytes;

        subobject.Type = D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_SHADER_CONFIG;
        subobject.pDesc = &shaderConfig;
    }

    D3D12_RAYTRACING_SHADER_CONFIG shaderConfig = {};
    D3D12_STATE_SUBOBJECT subobject = {};
};

当我们创建了 ShaderConfig 后,我们需要将他和我们使用的 shader联系起来,就是使用先前提到的ExportAssociation 进行关联

uint32_t shaderConfigIndex = index++; // 6
const WCHAR* shaderExports[] = { kMissShader, kClosestHitShader, kRayGenShader };
ExportAssociation configAssociation(shaderExports, arraysize(shaderExports), &(subobjects[shaderConfigIndex]));
subobjects[index++] = configAssociation.subobject; // 7 Associate Shader Config to Miss, CHS, RGS

PipelineConfig

// Create the pipeline config
PipelineConfig config(0); //这里并不发出光线
subobjects[index++] = config.subobject; // 8

有了对于对于单个 shader 的 config ,那自然也会有全局性的 config,PipelineConfig 是对 D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_PIPELINE_CONFIG 的构造封装;而该 config 只有唯一一个变量需要设置 :MaxTraceRecursionDepth

struct PipelineConfig
{
    PipelineConfig(uint32_t maxTraceRecursionDepth)
    {
        config.MaxTraceRecursionDepth = maxTraceRecursionDepth;

        subobject.Type = D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_PIPELINE_CONFIG;
        subobject.pDesc = &config;
    }

    D3D12_RAYTRACING_PIPELINE_CONFIG config = {};
    D3D12_STATE_SUBOBJECT subobject = {};
};

该数值直接表明了我们发出的光线的递归调用次数,当我们不需要发出 raytracing call 时,直接将他设为 0

GlobalRootSignautre

​ 最后我们需要一个全局性的 RootSignature 就像 graphics 管线那样,最终 shader 中起作用的 rootSignature 将是 global 和 local 共同结合的 rootSignature ;

GlobalRootSignature root(mpDevice, {}); 
mpEmptyRootSig = root.pRootSig;
subobjects[index++] = root.subobject; // 9

GlobalRootSignature 是对 D3D12_STATE_SUBOBJECT_TYPE_GLOBAL_ROOT_SIGNATURE 的构造封装,具体实现也相当直接:

struct GlobalRootSignature
{
    GlobalRootSignature(ID3D12Device5Ptr pDevice, const D3D12_ROOT_SIGNATURE_DESC& desc)
    {
        pRootSig = createRootSignature(pDevice, desc);
        pInterface = pRootSig.GetInterfacePtr();
        subobject.pDesc = &pInterface;
        subobject.Type = D3D12_STATE_SUBOBJECT_TYPE_GLOBAL_ROOT_SIGNATURE;
    }
    ID3D12RootSignaturePtr pRootSig;
    ID3D12RootSignature* pInterface = nullptr;
    D3D12_STATE_SUBOBJECT subobject = {};
};

终于终于终于,攒齐了七颗龙珠之后该召唤神龙 RT PSO 了

D3D12_STATE_OBJECT_DESC desc;
desc.NumSubobjects = index; // 10
desc.pSubobjects = subobjects.data();
desc.Type = D3D12_STATE_OBJECT_TYPE_RAYTRACING_PIPELINE;

d3d_call(mpDevice->CreateStateObject(&desc, IID_PPV_ARGS(&mpPipelineState)));

接下来,是不是终于能画出我最爱的三角形了...还不是...😭

回顾一下先前对 DXR 所用数据结构的概述

rayGeometryIntersection

还有一个占了相当大篇幅的部分 Shader Table

Shader Table

​ 这里并不会展开对 shader table 细节描述,因为这本身就覆盖了相当大的范围,先回归本篇的初心,跑通DXR 的流程先。

​ 前面我们已经创建了加速结构 TLAS 和 BLAS 与 RT PSO ,而接下来我们需要补上最后一块极其重要的部分 Shader Table ;Shader Table 是一块 GPU-visible 的 Buffer ,由用户负责创建与资源管理;它由一系列 Shader Table Records 组成且有两个重要职责:

  • 描述 场景片元 与 shader program 之间的的联系
  • 绑定资源到 Pipeline 中

​ 首先第一个职责很容易理解,我们可以绑定多个 hit 和 miss shader programe 到 RT PSO 中,而具体当光线与片元相交时(或 miss 无相交)我们需要调用哪个 shader program 则通过查询 Shader Table 实现

​ 而为什么会有第二个职责呢,是因为我们可以为每个 shader program 使用不同的 local RootSignature ,而每个片元可能有不同的资源需求 (vertex buffer,texture 等等),为了实现如上需求的资源绑定,就需要使用 Shader Table实现。

​ 目前 API 允许我们同时绑定多个 shader table 到一次 DispatchRays 的调用上。

Shader Table Records

​ Shader Table Records 由两部分组成,一个是 shader program identifier (通过调用ID3D12StateObjectPropertiesPtr::GetShaderIdentifier() 获取) ,另一部分则是 Root Table ,包含了 shader resource 的绑定。

​ 该 Root Table 的资源绑定与 Graphics 管线中绑定资源类似,但是我们需要直接通过 memcpy 的方式设置而不是调用 setter 方法。但在设置时需要注意,Root table 设置时 “条目”(Root Constants , Root Descriptors , Descriptor Tables) 的大小与常规管线的中不同:

  • Root Constants are 4 bytes.

  • Root Descriptors are 8 bytes.

  • Descriptor Tables are 8 bytes. This is different than the size required by the regular root-signature.

对于根常量和根描述符,我们设置的数据与传递给 setter 函数的数据相同。但描述符表则需要填写 D3D12_GPU_DESCRIPTOR_HANDLE::ptr 。另外需要注意的是根描述符需要以 8-byte aligned

Shader Table Layout

​ Shader Table 是由 shader table records 组成的数组,我们接下来创建一个与我们上文建立的 shader program 相匹配的 Shader Table

​ 我们需要做第一件事就是为 Shader Table 创建 buffer ,为此我们需要弄清楚他的 size;这里需要注意,所有的 shader table records 将占用同样的 size ,所以我们将根据所需的最大条目来选择该 size。假设我们这时最大的条目使用了一个 描述符表 占用 8 bytes ; 则当前 shader table records 将占用

mShaderTableEntrySize = D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES + 8

(shader table records 由 shader program identifierRoot Table 组成)

​ 接着我们需要对该 size 进行对齐处理 :则占用为

mShaderTableEntrySize = align_to(D3D12_RAYTRACING_SHADER_RECORD_BYTE_ALIGNMENT, mShaderTableEntrySize)

​ 我们将使用 3 个 programes 和 1 个 geometry ,所以我们需要 3 份当前size

 uint32_t shaderTableSize = mShaderTableEntrySize * 3;

和 geometry 有什么关系? 后面讲

在计算出占用 size 后,我们就可以在 Upload heap 创建 Shader Table buffer

mpShaderTable = createBuffer(mpDevice, shaderTableSize, D3D12_RESOURCE_FLAG_NONE,   D3D12_RESOURCE_STATE_GENERIC_READ, kUploadHeapProps);

当然也可以在 Default Heap 创建,初始状态需要设置为 D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE

接下来就是通过 map 的方式来填充我们的根描述符与 shader program identifier ,为获取shader program identifier,我们需要使用 ID3D12StateObjectProperties 的接口,可以通过以下创建该接口

ID3D12StateObjectPropertiesPtr pRtsoProps;
mpPipelineState->QueryInterface(IID_PPV_ARGS(&pRtsoProps));

接下来就是对 Shader Table 填充的样例:

// Entry 0 - ray-gen program ID and descriptor data
memcpy(pData, pRtsoProps->GetShaderIdentifier(kRayGenShader), D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES);
// This is where we need to set the descriptor data for the ray-gen shader

// Entry 1 - miss program
memcpy(pData + mShaderTableEntrySize, pRtsoProps->GetShaderIdentifier(kMissShader),                             D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES);

// Entry 2 - hit program. Program ID and one constant-buffer as root descriptor    
uint8_t* pHitEntry = pData + mShaderTableEntrySize * 2; // +2 skips the ray-gen and miss entries
memcpy(pHitEntry, pRtsoProps->GetShaderIdentifier(kHitGroup), D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES);

program identifier 必须放在 record 的开头,而此处并无对 RayGenShader 的root table 进行设置

​ ok 现在我们距离光线追踪就剩最后一步了,我们已经拥有发起 DispatchRays 的底气,接下来将尝试进行光线追踪。

Raytrace()

​ 首先我们需要创建一些 shader 中绑定使用的资源与描述符,如:

  • 一个用于存储输出结果的 output texture
  • 一个用于存储两个对象的 CBV / UAV / SRV heap
  • 一个用于 output texture 的 UAV
  • 一个用于 TLAS 的 SRV

前三个就是常规 DX12 创建流程,这里我们看最后用于 TLAS 的 SRV 创建流程,该 SRV 的创建流程与常规 SRV 非常相似,但我们需要填充 D3D12_SHADER_RESOURCE_VIEW_DESC 的一个新条目 RaytracingAccelerationStructure

D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_RAYTRACING_ACCELERATION_STRUCTURE;
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.RaytracingAccelerationStructure.Location = mpTopLevelAS->GetGPUVirtualAddress();
D3D12_CPU_DESCRIPTOR_HANDLE srvHandle = mpSrvUavHeap->GetCPUDescriptorHandleForHeapStart();
srvHandle.ptr += mpDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
mpDevice->CreateShaderResourceView(nullptr, &srvDesc, srvHandle);

​ 回顾前文,在创建 Shader Table 时,我们还没有将 descriptor table 填入,在创建好 ray-generation Shader 所需的 descriptor table 后,我们就可以通过 memcpy 的方式填入数据;

// Entry 0 - ray-gen program ID and descriptor data
memcpy(pData, pRtsoProps->GetShaderIdentifier(kRayGenShader), D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES);
uint64_t heapStart = mpSrvUavHeap->GetGPUDescriptorHandleForHeapStart().ptr;
*(uint64_t*)(pData + D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES) = heapStart;

​ 接下来,为了实际发起 raytrace ,我们需要填写 D3D12_DISPATCH_RAYS_DESC 作为 DispatchRays 所用参数:

  • 首先我们需要填充的是类似 compute 管线下指定线程组数量的数据 、指定 ray-generation 的 grid
D3D12_DISPATCH_RAYS_DESC raytraceDesc = {};
raytraceDesc.Width = mSwapChainSize.x;
raytraceDesc.Height = mSwapChainSize.y;
raytraceDesc.Depth = 1;
————————————————————————————————————
//ray-generation shader 
    [shader("raygeneration")]
    void rayGen()
{
    uint3 launchIndex = DispatchRaysIndex(); //此处获取当前管线在 grid 中的index
    float3 col = linearToSrgb(float3(0.4, 0.6, 0.2));
    gOutput[launchIndex.xy] = float4(col, 1);
}
————————————————————————————————————

  • 接下来我们需要填写 ray-generation shader 的 shader table record 位置与大小
// RayGen is the first entry in the shader-table
raytraceDesc.RayGenerationShaderRecord.StartAddress = mpShaderTable->GetGPUVirtualAddress() + 0 * mShaderTableEntrySize;
raytraceDesc.RayGenerationShaderRecord.SizeInBytes = mShaderTableEntrySize;
  • Miss Shader 的 shader table records 我们可以指定多个 miss shader record ,但必须使用同一 buffer,且指定每个 miss shader record 间 的 stride
size_t missOffset = 1 * mShaderTableEntrySize;
raytraceDesc.MissShaderTable.StartAddress = mpShaderTable->GetGPUVirtualAddress() + missOffset;
raytraceDesc.MissShaderTable.StrideInBytes = mShaderTableEntrySize;
raytraceDesc.MissShaderTable.SizeInBytes = mShaderTableEntrySize;   // Only a s single miss-entry
  • hit-group shader records 同上
// Hit is the third entry in the shader-table
size_t hitOffset = 2 * mShaderTableEntrySize;
raytraceDesc.HitGroupTable.StartAddress = mpShaderTable->GetGPUVirtualAddress() + hitOffset;
raytraceDesc.HitGroupTable.StrideInBytes = mShaderTableEntrySize;
raytraceDesc.HitGroupTable.SizeInBytes = mShaderTableEntrySize;

​ 我们需要指定一个全局使用的 rootsignature ,使用 SetComputeRootSignature 设置

// Bind the empty root signature
mpCmdList->SetComputeRootSignature(mpEmptyRootSig);

​ 最后,我们指定 RT pipeline 所使用 RT PSO ,通过 ID3D12GraphicsCommandList4::SetPipelineState1() 实现:

mpCmdList->SetPipelineState1(mpPipelineState.GetInterfacePtr());

​ 最后最后最后,就到激动人心的 DispatchRays 了:

mpCmdList->DispatchRays(&raytraceDesc); 

image-20241220210450095

至此,对 DXR 的流程已经简单的过了一遍,具体实现请看源代码,这里给出几个 DXR 的入门学习资料:

DirectX Raytracing (DXR) Functional Spec | DirectX-Specs

DX12 Raytracing tutorial - Part 1 | NVIDIA Developer

DirectX-Graphics-Samples/Samples/Desktop/D3D12Raytracing at master · microsoft/DirectX-Graphics-Samples

posted on 2025-03-03 22:51  glssa  阅读(395)  评论(0)    收藏  举报