GPU图形驱动工程师,这个DX12 Spec系列会持续更新,任何GPU相关的问题都可以互动:
// 2023年8月5日更新,翻看之前写的有点挫,重新修改一下
目录
1. descriptor
1.1 基本概念
- 不管是做上层的游戏开发,还是做底层的驱动开发,都绕不开descriptor这个概念。
- app想做一些定制化的运算,比如说实现某个特效等,都要游戏开发者去手搓hlsl语句(opengl这边叫glsl,其实都差不多),hlsl如何访问需要的resource呢?就是通过descriptor
- 如字面意思,descriptor就是一个描述符,描述了resource的gpu va(virtual address),format,dimension等信息
1.2 举个例子
- 常见的SRV(shader resource view,shader就是上面说的hlsl语句,那shader resource view就是描述了一个hlsl里面,能直接访问的resource),在CreateShaderResourceView()接口中,开发者要提供以下参数:
HRESULT CreateShaderResourceView(
[in] ID3D11Resource *pResource,
[in, optional] const D3D11_SHADER_RESOURCE_VIEW_DESC *pDesc,
[out, optional] ID3D11ShaderResourceView **ppSRView
);
- pResource指向的就是app自己创建的resource指针,当然在app侧直接解引用是无法使用的,因为这个指针是directx runtime在管理
- pDesc的结构如下:
typedef struct D3D11_SHADER_RESOURCE_VIEW_DESC {
DXGI_FORMAT Format;
D3D11_SRV_DIMENSION ViewDimension;
union {
D3D11_BUFFER_SRV Buffer;
D3D11_TEX1D_SRV Texture1D;
D3D11_TEX1D_ARRAY_SRV Texture1DArray;
D3D11_TEX2D_SRV Texture2D;
D3D11_TEX2D_ARRAY_SRV Texture2DArray;
D3D11_TEX2DMS_SRV Texture2DMS;
D3D11_TEX2DMS_ARRAY_SRV Texture2DMSArray;
D3D11_TEX3D_SRV Texture3D;
D3D11_TEXCUBE_SRV TextureCube;
D3D11_TEXCUBE_ARRAY_SRV TextureCubeArray;
D3D11_BUFFEREX_SRV BufferEx;
};
} D3D11_SHADER_RESOURCE_VIEW_DESC;
- 这里面的参数就很丰富了,描述了resource的维度信息,细分为buffer、1d、2d、3d,然后再去看buffer、1d、2d、3d里面该去提供哪些信息,无非就是ElementWidth,LOD这些
- ppSRView返回创建成功的SRV指针,同样在app侧直接解引用是无法使用的,这个指针是directx runtime在管理
- 如果只是为了了解descriptor的基本概念,看到这里就应该结束了,但是很不幸,因为resource资源的多样性,和directx的灵活性,在简单的descriptor基础概念上,微软又整了很多花活,想深入了解还得继续往下看
1.2 descriptor type
- descriptor按照所访问resource的类型,可以分为:
- Constant buffer view (CBV):Constant buffers contain shader constant data and can be accessed by any GPU shader,注意这里的constant不同于C/C++中的常量,这里的constant是在shader运算过程种保持数据不变,而其他状态可以更新数据
- Unordered access view (UAV):An unordered access view enables the reading and writing to the texture (or other resource) in any order,这里是指为shader无序访问同一块Resource提供读写安全机制,类似于读写锁
- Shader resource view (SRV):Shader resource views typically wrap textures in a format that the shaders can access them,这里SRV为Shader访问Resource最普通的一种形式,无特殊功能
- Samplers:Shader访问采样资源,详细可以查一下细节,Sample采样是提高图形质量的一种手段,在抗锯齿方面有MSAA(多重采样抗锯齿)、SSAA(超级采样抗锯齿)等各种算法
- Render Target View (RTV):Render Target就是dx12整个渲染出来的图形数据,会缓存到buffer中,系统再输出到显示器等,一般调用CreateSwapChainForHwnd()创建一块缓冲区,然后作为缓冲对象输出图形数据
- Depth Stencil View (DSV):做Depth Stencil测试用的缓冲区,3D显示中有图形遮挡的概念,可以用Depth Stencil Buffer缓存像素点的Z坐标,然后根据Z深度判定pixel是否输出和进一步计算
- Vertex Buffer View (VBV):Vertex用来保存所有输入的顶点信息(像素点用Vertex来表示,可以包括点的空间坐标、颜色、法向量等等,Vertex也可以是用户自定义的各种数据结构),是整个DX12 Pipeline的输入数据
- Index Buffer View (IBV):Index是配合Vertex一起使用的,简而言之就是对于Vertex建立索引,这样对于Vertex大量使用的情况,可以直接用索引代替,不用重复保存Vertex数据
- Stream Output View (SOV):在DX12 Pipeline中,我们可以选择在GS(Geometry Shader)之后,直接将数据通过Stream Output的方式,直接输出出来,SOV定义输出对象
2. descriptor heap & descriptor range & descriptor table
2.1 descriptor heap
- 有了descriptor的概念之后,descriptor heap也就好理解了,descriptor heap是分配出来的gpu memory,是存储descriptor的地方
2.2 descriptor range
- 如上图所示,descriptor range就是从descriptor heap中拎出来的subrange,一段连续的descriptor。这个概念就像c语言中的数组和子数组
2.3 Descriptor Table
- Descriptor Table = Descriptor Range 1 + Descriptor Range 2 + … + Descriptor Range n,我把n个Descriptor Range放在一起,就形成了一个Descriptor Table。
- 需要指出来的是:Descriptor Table是由Descriptor Range构成的,每个Descriptor Range只能存放CBV/SRV/UAV/Sampler中的一种,不同的CBV/SRV/UAV Range可以混合存放于一个Table中,Sampler需要单独放在一个Descriptor Table中,不可与CBV/SRV/UAV混合存放。
3. Root Signature
- The root signature is the definition of an arranged collection of descriptor table, root constant and root descriptor,这里牵扯的概念有点多,官方文档也没仔细展开讲,我一个个说明:
3.1 Signature
- 签名的概念应该很多编程语言都有,比如C/C++里面的函数签名,我在.h文件中做了一个函数申明,其中包括了函数形参,规定了传入实参的数据个数、类型等,这就是一个函数签名。
- 对应于DX12里面的Signature,你可以把GPU的Shader(前面提到的GPU流水线的各个运算模块)想象成一个函数,接受Descriptor(参数)并进行运算。开发者必须要在APP层申明一个Root Signature,规定好Shader需要用到哪些Descriptor,这样APP传下去的Descriptor(包含各种Resource信息)和Shader里面用到的(Resource)就会保持一致。
3.2 Root Argument、Root Parament
- Root Signature类似于函数签名,Root Argument就是函数签名中的各个形参,所有Root Argument组合在一起形成了Root Signature。
- Root Parament是啥?它其实就是开发者填写好的函数实参,是Root Argument的填好值的样子。
3.3 Descriptor Table、Root Constant、Root Descriptor
- Descriptor Table:前文阐述过了,不再赘述。还是强调一点,Descriptor Table中只能用来存放CBV/SRV/UAV/Sampler中的1种或者组合。
- Root Descriptor:对于Descriptor Table,GPU会内部分配一块Memory专门存放Descriptor Table,而Root Descriptor则是直接将Descriptor信息放入GPU寄存器,这是硬件做好的。但是也会产生各种问题,比如硬件寄存器的数量是有限的,API申请的Root Descriptor数量可能会超,这些问题都会在Driver层做处理。Root Descriptor只能用来存放CBV/SRV/UAV。
- Root Constant:这个类似于Root Descriptor,不过Root Constant不会存储Resource的地址等信息让GPU解析访问,而是直接保存开发者给的Value,这些数值也是存在硬件寄存器中的,粒度是以32-bit为一个单位。
4. Limits
4.1 Tier Level
- FL就是Feature Level的意思,可以看到Directx 9/10/11等不同Feature Level条件下,对于硬件需要支持的CBV/SRV/UAV/Sampler的最大数量是有要求的,达到对应的数量,硬件才能上报相应的Feature Level:
Tier 1, FL 9.4, 11.0+ | Tier 2, FL 11.0+ | Tier 3, FL 11.1+ | |
---|---|---|---|
Max # descriptors in a shader visible CBV/SRV/UAV heap | 1000000 | 1000000 | 1000000+ |
Max CBVs in all descriptor tables per shader stage | 14 | 14 | full heap |
Max SRVs in all descriptor tables per shader stage | 128 | full heap | full heap |
Max UAVs in all descriptor tables across all stages | 8 (64 for FL 11.1+) | 64 | full heap |
Max Samplers in all descriptor tables per shader stage | 16 | full heap | full heap |
4.2 Root Signature Size
- Root Argument:最大长度是64 DWORDS,这点会在Create Root Signature的时候由DX12 Runtime检查,如果长度超过64 DW并且开启了调试层,Runtime会直接报错并打印详细信息(DX12官方做好的这层Debug Layer很好用,错误信息打印非常全)。
- Descriptor Table:占用1 DW。
- Root Descriptor:占用2 DW。
- Root Constant:占用1 DW * NumOfConstant(1个constant占用32-bit数据,具体占用多少取决于用户申明的NumOfConstant)。
5. API
5.1 CheckFeatureSupport()
- 上面提到了,DirectX12根据硬件支持的Descriptor数量,将硬件的Feature Level数量划分为Tier1/2/3这3个Level,当DX12的Rumtime调用CheckFeatureSupport()向GPU Driver查询时,硬件可以根据自身情况返回3个Tier Level:
typedef enum D3D12_RESOURCE_BINDING_TIER {
D3D12_RESOURCE_BINDING_TIER_1 = 1,
D3D12_RESOURCE_BINDING_TIER_2 = 2,
D3D12_RESOURCE_BINDING_TIER_3 = 3
} ;
5.2 Create Descriptor Heap
5.2.1 Descriptor Heap Type
typedef enum D3D12_DESCRIPTOR_HEAP_TYPE
{
D3D12_CBV_SRV_UAV_DESCRIPTOR_HEAP,
D3D12_SAMPLER_DESCRIPTOR_HEAP,
D3D12_RTV_DESCRIPTOR_HEAP,
D3D12_DSV_DESCRIPTOR_HEAP,
D3D12_NUM_DESCRIPTOR_HEAP_TYPES
} D3D12_DESCRIPTOR_HEAP_TYPE;
5.2.2 CreateDescriptorHeap()
HRESULT CreateDescriptorHeap(
[in] const D3D12_DESCRIPTOR_HEAP_DESC *pDescriptorHeapDesc,
REFIID riid,
[out] void **ppvHeap
);
- 其中需要填充好
D3D12_DESCRIPTOR_HEAP_DESC
结构体,其中规定了Heap存放Descriptor的类型和数量:
typedef struct D3D12_DESCRIPTOR_HEAP_DESC {
D3D12_DESCRIPTOR_HEAP_TYPE Type;
UINT NumDescriptors;
D3D12_DESCRIPTOR_HEAP_FLAGS Flags;
UINT NodeMask;
} D3D12_DESCRIPTOR_HEAP_DESC;
5.2.3 如何访问Descriptor Heap中的某个Descriptor?
- 答案是和C/C++中的数组一样,采用
Start+Offset
的形式,GetCPUHandleForHeapStart()
获取Descriptor Heap的Start
(不可直接解引用,需要给到DX12 Runtime),GetDescriptorHandleIncrementSize()
获取Descriptor的Size
,Start + Size * Index
即可定位到某个Descriptor。
5.2.4 创建Root Descriptor?
- Root Descriptor无外乎CBV/SRV/UAV/Sampler,那么我们对应调用
CreateShaderResourceView() / CreateConstantBufferView() / CreateUnorderedAccessView()/CreateSampler()
即可,注意填充对应的ResourceView
参数。
5.2.5 特殊的SOV、RTV、DSV
- 这3种Descriptor类型在
1.1 Descriptor Type
有提到过,在GPU Driver层一般有对应的register存储该Descriptor信息,我们只需要将对应的Resource创建好,然后将Resource信息存到Descriptor中,调用CreateStreamOutputView() / CreateRenderTargetView() / CreateDepthStencilView()
即可,在Driver中会将Descriptor信息写到对应register。
6. Utils
6.1 Register Space
- The purpose of the register space field is to expand the namespace for register bindings。举个例子,开发者对于同一个SRV寄存器t#0(t#0就是指t寄存器中的第0个,t#对应SRV,u#对应UAV,s#对应Sampler,c#对应CBV),既想要绑定到Resource0上,又想绑定到Resource1上面,那可以这样声明:在绑定到Resource0时,声明为(t#0,space0),绑定到Resource1时,声明为(t#0,space1)。这里的space0/1就是Register Space,类似于C++中的namespace,我在不同的namespace可以申明同一个名称的变量。
6. 参考文档
- DirectX-Specs
- DirectX 12 3D 游戏开发实战(龙书)