DX11与DX12区别

实践总结

1 常量缓冲区的使用

DX11创建常量缓冲区后,用设备上下文将其绑定到管线。刷新时用设备上下文更新常量缓冲区。绘制时用设备上下文下绘制命令。

DX12依次创建根签名(绑定到PSO)、描述符堆、常量缓冲区视图(绑定到描述符堆)。刷新时用资源接口更新常量缓冲区。绘制时用命令列表设置根签名、绑定根参数和描述符堆、下绘制命令。

DX11中的设备上下文(ID3D11DeviceContext)被DX12废弃。DX12用PSO(ID3D12PipelineState)代替其进行管线设置,用命令列表(ID3D12GraphicsCommandList)代替其装配管线和下绘制命令,用资源接口(ID3D12Resource)代替其进行资源的map和unmap。

DX12提出了根签名(ID3D12RootSignature),用以将着色器需要用到的数据绑定到对应的寄存器槽上供着色器访问。根签名是根参数数组,根参数(CD3DX12_ROOT_PARAMETER)可以是描述符表、根描述符、根常量。根签名会绑定到PSO。

设常量缓冲区:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// C++
struct ConstantBuffer
{
DirectX::XMMATRIX world;
DirectX::XMMATRIX view;
DirectX::XMMATRIX proj;
DirectX::XMFLOAT4 color;
uint32_t useCustomColor;
uint32_t pads[3];
};

// HLSL
cbuffer ConstantBuffer : register(b0)
{
matrix g_World;
matrix g_View;
matrix g_Proj;
vector g_Color;
uint g_UseCustomColor;
}

DX11做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1.新建常量缓冲区描述
D3D11_BUFFER_DESC cbd;
ZeroMemory(&cbd, sizeof(cbd));
cbd.Usage = D3D11_USAGE_DYNAMIC;
cbd.ByteWidth = sizeof(ConstantBuffer); // ConstantBuffer是常量缓冲区结构体
cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
// m_pd3dDevice类型是ComPtr<ID3D11Device>
HR(m_pd3dDevice->CreateBuffer(&cbd, nullptr, m_pConstantBuffer.GetAddressOf())); // 不使用初始数据

// 2.将常量缓冲区绑定到顶点着色器和像素着色器供它们使用
// 此常量缓冲区寄存于b0,从b0开始只有一个
// m_pd3dImmediateContext类型为ComPtr<ID3D11DeviceContext>
m_pd3dImmediateContext->VSSetConstantBuffers(0, 1, m_pConstantBuffer.GetAddressOf());
m_pd3dImmediateContext->PSSetConstantBuffers(0, 1, m_pConstantBuffer.GetAddressOf());

// 3.绘制时更新常量缓冲区
D3D11_MAPPED_SUBRESOURCE mappedData;
HR(m_pd3dImmediateContext->Map(m_pConstantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
memcpy_s(mappedData.pData, sizeof(m_CBuffer), &m_CBuffer, sizeof(m_CBuffer)); // m_CBuffer类型为ConstantBuffer
m_pd3dImmediateContext->Unmap(m_pConstantBuffer.Get(), 0);

m_pd3dImmediateContext->(IndexCount, 1, StartIndexLocation, BaseVertexLocation, 0); // 绘制

DX12做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 1.创建根签名
CD3DX12_ROOT_PARAMETER slotRootParameter[1]; // 根参数

CD3DX12_DESCRIPTOR_RANGE cbvTable0; // 创建一个常量缓冲区描述符表,寄存于b0
cbvTable0.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0);
slotRootParameter[0].InitAsDescriptorTable(1, &cbvTable0);

CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1, slotRootParameter, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT); // 根签名是根参数数组

ComPtr<ID3DBlob> serializedRootSig = nullptr;
ComPtr<ID3DBlob> errorBlob = nullptr;
HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc, D3D_ROOT_SIGNATURE_VERSION_1,
serializedRootSig.GetAddressOf(), errorBlob.GetAddressOf()); // 序列化,使GPU理解

ThrowIfFailed(md3dDevice->CreateRootSignature(
0,
serializedRootSig->GetBufferPointer(),
serializedRootSig->GetBufferSize(),
IID_PPV_ARGS(mRootSignature.GetAddressOf()))); // mRootSignature类型为ComPtr<ID3D12RootSignature>

// 2.创建描述符堆
D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
cbvHeapDesc.NumDescriptors = numDescriptors; // numDescriptors为常量缓冲区描述符数量
cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
cbvHeapDesc.NodeMask = 0;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(&cbvHeapDesc,
IID_PPV_ARGS(&mCbvHeap)));

// 3.创建常量缓冲区视图绑定到描述符堆上
// 若用到帧资源,每个帧资源都要创建一个常量缓冲区视图
auto handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(mCbvHeap->GetCPUDescriptorHandleForHeapStart());
handle.Offset(heapIndex, mCbvSrvUavDescriptorSize); // heapIndex是在描述符堆中的偏移

UINT CBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ConstantBuffer)); // CBByteSize为常量缓冲区尺寸
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
cbvDesc.BufferLocation = CBAddress; // 偏移位置
cbvDesc.SizeInBytes = CBByteSize;
md3dDevice->CreateConstantBufferView(&cbvDesc, handle); // md3dDevice类型为ComPtr<ID3D12Device>

// 4.创建PSO时绑定根签名
D3D12_GRAPHICS_PIPELINE_STATE_DESC opaquePsoDesc;
opaquePsoDesc.pRootSignature = mRootSignature.Get();

// 5.绘制时更新缓冲区,在命令列表设置根签名
// 更新缓冲区略,使用ID3D12Resource
ID3D12DescriptorHeap* descriptorHeaps[] = { mCbvHeap.Get() };
// mCommandList类型为ComPtr<ID3D12GraphicsCommandList>
mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps); // 设置描述符堆

mCommandList->SetGraphicsRootSignature(mRootSignature.Get()); // 设置根签名

auto cbvHandle = CD3DX12_GPU_DESCRIPTOR_HANDLE(mCbvHeap->GetGPUDescriptorHandleForHeapStart());
cbvHandle.Offset(cbvIndex, mCbvSrvUavDescriptorSize); // cbvIndex是在描述符堆中的偏移
mCommandList->SetGraphicsRootDescriptorTable(0, cbvHandle); // 绑定根参数和描述符堆,寄存到b0

mCommandList->DrawIndexedInstanced(IndexCount, 1, StartIndexLocation, BaseVertexLocation, 0); // 绘制

2 渲染方式

Direct3D 11支持两种渲染方式:立即渲染(immediate rendering,利用immediate context实现)以及延迟渲染(deferred rendering,利用deferred context实现)。立即渲染将缓冲区中的命令直接借驱动层发往GPU执行,延迟渲染与命令列表模型相似(但执行命令列表时仍然要依赖immediate context)。前者延续了Direct3D 11之前一贯的绘制方式,而后者则为Direct3D 11中新添加的绘制方式。

Direct3D 12取消了立即渲染方式,完全采用“命令列表->命令队列”模型,使多个命令列表同时记录命令,借此充分发挥多核心处理器的性能。

3 资源转换

为了实现常见的渲染效果,经常会通过GPU对某个资源按顺序进行先写后读这两种操作。当GPU的写操作还没有完成抑或甚至还没有开始,却开始读取资源,便会导致资源冒险(resource hazard)。

为此,Direct3D专门针对资源设计了一组相关状态。资源在创建伊始会处于默认状态,该状态将一直持续到应用程序通过Direct3D将其转换(transition)为另一种状态为止。这就使GPU能够针对资源状态转换与防止资源冒险作出适当的行为。例如,如果要对某个资源(比如纹理)执行写操作时,需要将它的状态转换为渲染目标状态;而要对该纹理进行读操作时,再把它的状态变为着色器资源状态。根据Direct3D给出的转换信息,GPU就可以采取适当的措施避免资源冒险的发生。譬如,在读取某个资源之前,它会等待所有与之相关的写操作执行完毕。

Direct3D 11中由驱动管理资源转换。一个自动跟踪状态转换的系统会强行增加程序的额外开销。

Direct3D 12中,通过命令列表设置转换资源屏障(transition resource barrier, D3D12_RESOURCE_BARRIER)数组,即可指定资源的转换。


DX11与DX12区别
https://reddish.fun/posts/Article/difference-between-DX11-and-DX12/
作者
bit704
发布于
2023年1月6日
许可协议