DXR 流程
以 Nvidia 的 DxrTutorials 为例 NVIDIAGameWorks/DxrTutorials
DXR 与 DX12 正常流程并存 ,主要为了 DX12 流程补充光追效果,所以主要初始化流程与原DX12无异,但 DXR 中引入了用于光线追踪的新 interface ,资源管理与 DX12 无异,都需要手动管理;
接下来通过 DxrTurtorials 例程来简述 DXR 的新流程;本文旨在描述构建 DXR 的管线流程,而并非 DXR 高级效果教程
先来速览一下微软给出的 DXR 所用的大致数据结构

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 用于加速光线求交 :

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_OPAQUEflag 表示我们所提交的是非透明图元,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 Buffer 和 Result 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-generation、miss、closest-hit、any-hit 和 intersection

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

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-Hit 、Any-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 数据 ,AnyHitShaderImport 和 ClosestHitShaderImport 是我们在创建 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 所用数据结构的概述

还有一个占了相当大篇幅的部分 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 identifier 和 Root 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);

至此,对 DXR 的流程已经简单的过了一遍,具体实现请看源代码,这里给出几个 DXR 的入门学习资料:
DirectX Raytracing (DXR) Functional Spec | DirectX-Specs
浙公网安备 33010602011771号