准备工作
检测系统显卡信息使用命令行运行: dxdiag
显卡-功能级别:12_x 表明硬件支持 d3d12功能。
推荐使用VS2019开发工具,依赖10.0.19041.0 sdk ,如使用vs2017则单独下载10.0.19041.0 sdk并安装。10.0.19041.0 sdk下载Windows SDK和模拟器存档 | Microsoft Developer
工程sdk设置如下:
创建win32程序并配置DirectX12
本程序使用vs2017+10.0.19041.0 sdk实现,新建win32工程:
2.设置sdk:
3. 运行:
接下来配置DirectX12,下载DirectX-Headers :GitHub - microsoft/DirectX-Headers: Official DirectX headers available under an open source license
把DirectX-Headers-main\include\directx目录加到工程
引入DirectX12头文件和lib文件
到这 配置d3d12环境工作都完成。
调用D3D12接口创建设备对象
这个一样复杂的过程,因此对d3d12接口操作做封装成三角形类对外有三个接口:
1.OnInit()放在win32窗口初始化时调用
2. 把OnRender()放在win32窗口绘画时调用,把OnDestroy()放在win32窗口关闭时调用:
接下来我们使用d3d12接口绘画一个三角形最终效果如下:
程序结构流程图如下:
我们将流程分为两个部份1.初始化和渲染
初始化:
1. 创建D3D设备对象.
查找系统显示适配器(显卡)设备创建D3D设备接口(gpu device), 使用CreateDXGIFactory2创建IDXGIFactory对象,然后IDXGIFactory对象枚举系统显卡设备,最后使用D3D12CreateDevice创建我们D3D设备对象.对应流程图1、2步,代码如下:
void CD3D12Triangle::CreateDevice(UINT dxgiFactoryFlags, ComPtr<IDXGIFactory6>& factory, ComPtr<ID3D12Device>& device)
{
ComPtr<IDXGIAdapter1> adapter;
HRESULT hr = S_OK;
ThrowIfFailed(CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&factory)));
for (int i = 0; SUCCEEDED(factory->EnumAdapterByGpuPreference(i, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(&adapter))); i++)
{
DXGI_ADAPTER_DESC1 desc;
adapter->GetDesc1(&desc);
if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
{
continue;
}
if (SUCCEEDED(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_12_0, _uuidof(ID3D12Device), nullptr)))
{
SetWindowText(m_hWnd, desc.Description);
ThrowIfFailed(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&device)));
break;
}
}
///ThrowIfFailed(factory->MakeWindowAssociation(m_hWnd, DXGI_MWA_NO_ALT_ENTER));
}
2. 创建D3D12命令队列接口:
使用D3D设备对象创建,加上ThrowIfFailed()作用是:如出错直接退出程序
void CD3D12Triangle::CreateCommandQueue(ComPtr<ID3D12CommandQueue>& commandQueue)
{
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
ThrowIfFailed(m_device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&commandQueue)));
}
D3D12_COMMAND_QUEUE_DESC 是一个结构体 在D3D12被称为描述符,在D3D12以结尾xxx_DESC的结构体被称为xxx描述符,作用简化函数参数调(严格说是不对的,暂时先这样理解描述符)。再来看看什么是描述符堆是以xxx_HEAP_DESC以结尾的结构体. 位于gpu显存上。
3. 创建交换链(Swap Chain)
交换链的概念:指的是一系列的表面组成的一个合集,这些表面中有一个是前台表面(显示在屏幕上)只要两个就可以了一个后台缓冲表面,一个前台表面。交换即前台表面变成后台缓冲表面,后台缓冲表面变成前台表面。在后台缓冲表面先绘图操作,当下一次需要显示画面的时候,这两个表面再次交换,这也叫做双缓冲。使用IDXGIFactory6对象创建:
void CD3D12Triangle::CreateSwapChain(ID3D12CommandQueue* pQueue, int w, int h, ComPtr<IDXGISwapChain3>& swapChain3)
{
m_viewport = CD3DX12_VIEWPORT(0.0f, 0.0f, static_cast<float>(w), static_cast<float>(h));
m_scissorRect = CD3DX12_RECT(0, 0, static_cast<LONG>(w), static_cast<LONG>(h));
DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
swapChainDesc.BufferCount = m_nFrameCount;
swapChainDesc.Width = w;
swapChainDesc.Height = h;
swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
swapChainDesc.SampleDesc.Count = 1;
ComPtr<IDXGISwapChain1> swapChain;
ThrowIfFailed(m_factory->CreateSwapChainForHwnd(
pQueue,
m_hWnd,
&swapChainDesc,
nullptr,
nullptr,
&swapChain
));
ThrowIfFailed(swapChain.As(&swapChain3));
m_frameIndex = swapChain3->GetCurrentBackBufferIndex();
}
交换链的创建依赖1.窗口句柄 2.命令队列 3.交换链描述符(交换链结构体)填值如下:
DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {}; ///交换链描述符
swapChainDesc.BufferCount = m_nFrameCount; //数值为2 双缓冲
swapChainDesc.Width = w; //窗口的宽
swapChainDesc.Height = h; //窗口的高
swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
swapChainDesc.SampleDesc.Count = 1;
ThrowIfFailed(swapChain.As(&swapChain3)); 目的是将IDXGISwapChain1转换成IDXGISwapChain3 为使用 IDXGISwapChain3的GetCurrentBackBufferIndex()接口。
3. 创建RTV(Render Target View)
Render Target View(简称RTV)可以作为渲染目标.它的创建依赖于1 RTV描述符堆 2.交换链表面关连 可见程序结构流程图编号5。
1.RTV描述符堆的创建 填充D3D12_DESCRIPTOR_HEAP_DESC 结构体,然后调用CreateDescriptorHeap创建ID3D12DescriptorHeap(RTV描述符堆)代码:
void CD3D12Triangle::CreateRenderTargetViewHeap(ComPtr<ID3D12DescriptorHeap>& heap)
{
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
rtvHeapDesc.NumDescriptors = m_nFrameCount;
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
ThrowIfFailed(m_device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&heap)));
}
2.创建RTV描述符:
void CD3D12Triangle::CreateRenderTargetView(ComPtr<ID3D12Resource> rtv[])
{
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart());
m_rtvDescriptorSize = m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
// Create a RTV for each frame.
for (int i = 0; i < m_nFrameCount; i++)
{
ThrowIfFailed(m_swapChain->GetBuffer(i, IID_PPV_ARGS(&rtv[i])));
m_device->CreateRenderTargetView(rtv[i].Get(), nullptr, rtvHandle);
rtvHandle.Offset(1, m_rtvDescriptorSize);
}
}
通过 RTV描述符堆对象取到RTV句柄,和RTV描述符的大小,可以这样理解 数组: A a[2]={0} a数组是全局的,我们要知道a[0]的大小通过GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV)得到(像sizeof(a[0])),访问数组a变量通过GetCPUDescriptorHandleForHeapStart()得到。
ThrowIfFailed(m_swapChain->GetBuffer(i, IID_PPV_ARGS(&rtv[i]))) RTV描述符它其实是交换链表面,m_device->CreateRenderTargetView(rtv[i].Get(), nullptr, rtvHandle)创建RTV描述符与交换链表面关连起来。rtvHandle.Offset(1, m_rtvDescriptorSize)维护RTV描述符堆数组。
4.创建根签名对象接口
根签名在d3d是一个重要的概念,目前我们只要了解如何使用就好,像使用函数一样先声明:
void CD3D12Triangle::CreateRootSignature(ComPtr<ID3D12RootSignature>& rootSignature)
{
CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc;
rootSignatureDesc.Init(0, nullptr, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
ComPtr<ID3DBlob> signature;
ComPtr<ID3DBlob> error;
ThrowIfFailed(D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error));
ThrowIfFailed(m_device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&rootSignature)));
}
5. 编译Shader程序
void CD3D12Triangle::ShadersCompileFromFile(LPCWSTR pFileName, LPCSTR pEntrypoint, LPCSTR pTarget, ComPtr<ID3DBlob>& shader)
{
UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
ThrowIfFailed(D3DCompileFromFile(pFileName, nullptr, nullptr, pEntrypoint, pTarget, compileFlags, 0, &shader, nullptr));
}
调用 D3DCompileFromFile 函数这里简单的封装一下,d3d中最少要有两个Shader程序 顶点着色器(Vertex Shader)和片段着色器(Fragment Shader) ,用结构体声明然后绑定到Pipeline State Object渲染管线状态对象
D3D12_INPUT_ELEMENT_DESC inputElementDescs[] =
{
//顶点着色器 声明
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
//片段着色器 声明
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};
6. 创建Pipeline State Object(PSO 渲染管线状态对象)
PSO 渲染管线状态对象是复杂而重要的概念,这里也不作说明,先了解如何使用,可以理解成使用gpu绘画时,根签名是函数的声明,PSO 渲染管线状态对象则是函数的定义。填充D3D12_GRAPHICS_PIPELINE_STATE_DESC结构体然后用CreateGraphicsPipelineState创建,创建PSO 渲染管线状态对象, 主要依赖
1输入数据布局
2顶点着色器Shader程序
3片段着色器Shader程序
4根签名上步已创建
5其他项填默认值 代码如下:
void CD3D12Triangle::CreateGPUPipelineState(ComPtr<ID3D12PipelineState>& pipelineState, ComPtr<ID3D12CommandAllocator>& commandAllocator, ComPtr<ID3D12GraphicsCommandList>& commandList)
{
ComPtr<ID3DBlob> vertexShader;
ComPtr<ID3DBlob> pixelShader;
/// 编译 顶点着色器
ShadersCompileFromFile(GetShaderFilePath(L"Shader.hlsl").c_str(), "VSMain", "vs_5_0", vertexShader);
/// 编译 片段着色器
ShadersCompileFromFile(GetShaderFilePath(L"Shader.hlsl").c_str(), "PSMain", "ps_5_0", pixelShader);
/// 声明 顶点着色器 片段着色器 输入数据布局 结构
D3D12_INPUT_ELEMENT_DESC inputElementDescs[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
psoDesc.InputLayout = { inputElementDescs, _countof(inputElementDescs) }; /// 顶点着色器 片段着色器 数据布局 结构
psoDesc.pRootSignature = m_rootSignature.Get(); /// 根签名
psoDesc.VS = CD3DX12_SHADER_BYTECODE(vertexShader.Get()); /// 顶点着色器 Shader程序
psoDesc.PS = CD3DX12_SHADER_BYTECODE(pixelShader.Get()); /// 片段着色器 Shader程序
psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
psoDesc.DepthStencilState.DepthEnable = FALSE;
psoDesc.DepthStencilState.StencilEnable = FALSE;
psoDesc.SampleMask = UINT_MAX;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
psoDesc.SampleDesc.Count = 1;
ThrowIfFailed(m_device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pipelineState)));
ThrowIfFailed(m_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)));
ThrowIfFailed(m_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_commandAllocator.Get(), m_pipelineState.Get(), IID_PPV_ARGS(&commandList)));
ThrowIfFailed(commandList->Close());
}
7.初始化顶点着色器和片段着色器数据
也就是把cpu内存数据 提交到gpu显卡内存上,使用CreateCommittedResource创建资源,D3D12_HEAP_TYPE_UPLOAD说明是 cpu数据到gpu内存,Map->memcpy->Unmap三步进行提交数据,代码:
void CD3D12Triangle::InitializeVertexBuffer(ComPtr<ID3D12Resource>& vertexBuffer,D3D12_VERTEX_BUFFER_VIEW& vertexBufferView)
{
Vertex triangleVertices[] =
{
{ { -0.5, -0.5, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } },
{ { -0.5, 0.5, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } },
{ { 0.5, -0.5, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } },
///{ { 0.5, 0.5, 0.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } },
};
const UINT vertexBufferSize = sizeof(triangleVertices);
ThrowIfFailed(m_device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&vertexBuffer)));
UINT8* pVertexDataBegin = NULL;
CD3DX12_RANGE readRange(0, 0);
ThrowIfFailed(vertexBuffer->Map(0, &readRange, reinterpret_cast<void**>(&pVertexDataBegin)));
memcpy(pVertexDataBegin, triangleVertices, sizeof(triangleVertices));
vertexBuffer->Unmap(0, nullptr);
vertexBufferView.BufferLocation = vertexBuffer->GetGPUVirtualAddress();
vertexBufferView.StrideInBytes = sizeof(Vertex);
vertexBufferView.SizeInBytes = vertexBufferSize;
}
Vertex triangleVertices[]={...} 数组中float值是啥意思?它们分别代表了 三角形顶点坐标,三角形顶点背景色RGBA (1f,1f,1f,1f)表示白色,(0f,0f,0f,1f)表示黑色。
d3d12坐标系统 取值范围 -1到1,以中心为原点如图:
8. 创建命令列表接口
void CD3D12Triangle::CreateGPUPipelineState(ComPtr<ID3D12PipelineState>& pipelineState, ComPtr<ID3D12CommandAllocator>& commandAllocator, ComPtr<ID3D12GraphicsCommandList>& commandList)
{
/ 创建命令列表分配器
ThrowIfFailed(m_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)));
创命令列表
ThrowIfFailed(m_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_commandAllocator.Get(), m_pipelineState.Get(), IID_PPV_ARGS(&commandList)));
ThrowIfFailed(commandList->Close());
}
命令列表依赖 1令列表分配器 2PSO对象
9. 创建围栏接口
创建围栏目的是为CPU与GPU渲染间的同步,简单的说窗口刷新(CPU)要等GPU渲染线程完成,再进入下次窗口刷新,用事件做为同步信号,代码:
void CD3D12Triangle::CreateCpuAndGpuSynchronization()
{
ThrowIfFailed(m_device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_fence)));
m_fenceValue = 1;
m_fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (m_fenceEvent == nullptr)
{
ThrowIfFailed(HRESULT_FROM_WIN32(GetLastError()));
}
}
等GPU渲染线程完成代码:
void CD3D12Triangle::WaitForPreviousFrame(void)
{
const UINT64 fence = m_fenceValue;
ThrowIfFailed(m_commandQueue->Signal(m_fence.Get(), fence));
m_fenceValue++;
// Wait until the previous frame is finished.
if (m_fence->GetCompletedValue() < fence)
{
ThrowIfFailed(m_fence->SetEventOnCompletion(fence, m_fenceEvent));
WaitForSingleObject(m_fenceEvent, INFINITE);
}
m_frameIndex = m_swapChain->GetCurrentBackBufferIndex();
}
到这里所有的创建对象工作都完了,接下来是渲染。
调用D3D12接口渲染
复杂的对象创建完成后,渲染相对来说就简单很多了
第一步使用命令列表接口设置预定操作和设置资源屏障(还没有执行,只是预定操作)
第二步使用命令队列接口 ExecuteCommandLists 执行操作
第三步使用交换链接口刷新Present(1, 0)
第四步等GPU渲染完成后到第一步操作
预定操作和设置资源屏障代码:
void CD3D12Triangle::PopulateCommandList(void)
{
ThrowIfFailed(m_commandAllocator->Reset());
ThrowIfFailed(m_commandList->Reset(m_commandAllocator.Get(), m_pipelineState.Get()));
m_commandList->SetGraphicsRootSignature(m_rootSignature.Get());
m_commandList->RSSetViewports(1, &m_viewport);
m_commandList->RSSetScissorRects(1, &m_scissorRect);
m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart(), m_frameIndex, m_rtvDescriptorSize);
m_commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);
const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
m_commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
m_commandList->IASetVertexBuffers(0, 1, &m_vertexBufferView);
m_commandList->DrawInstanced(3, 1, 0, 0);
m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
ThrowIfFailed(m_commandList->Close());
}
窗口刷新代码:
void CD3D12Triangle::OnRender(void)
{
PopulateCommandList();
ID3D12CommandList* ppCommandLists[] = { m_commandList.Get() };
m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
ThrowIfFailed(m_swapChain->Present(1, 0));
WaitForPreviousFrame();
}