DirectX12 绘制流程
一、Dx12绘制流程
上一章说完了Dx12的初始化,接下来就是核心了,开始绘制画面。
这一阶段,就是CPU向GPU送任务,处理,如此往复即可,具体可以理解为如下几个步骤:
1、重置上传;
2、提交命令;
3、页面翻转;
4、等待同步;
一、重置上传
在每次渲染绘制之前,都要把上传相关的进行重置,当初用快递举例,但快递是接收到,快递壳子就丢掉了,但这里不是,只是重置而已,其目的就是为了避免删除后再创建这样的开销;
void cDx12Rendering::ResetCMDListAlloctor()
{
//重置上传
m_spCommandAllocator->Reset();
m_spGraphicsCommandList->Reset(m_spCommandAllocator.Get(), NULL);
}
二、提交命令
提交绘制任务是绘制的绝对核心,也是开放了很复杂的参数,用来“本地化”Dx12的通用方案,以期达到想要的渲染效果。
也因为任务的可定制化极高,这里将会涉及到动辄上千的代码,但暂时先采用最简单的绘制让我们有一个总体的印象,细节将会在后面的章节不断补充。
先,来绘制一个纯色背景吧。
void cDx12Rendering::SubmitDrawingTask()
{
//把后缓冲区的资源状态切换成Render Target
CD3DX12_RESOURCE_BARRIER ResourceBarrierPresent = CD3DX12_RESOURCE_BARRIER::Transition(
vecRTVBuffer[m_iCurrentSwapBuffIndex].Get(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET);
m_spGraphicsCommandList->ResourceBarrier(1, &ResourceBarrierPresent);
//设置视口和裁剪区域
m_spGraphicsCommandList->RSSetViewports(1, &m_dvViewprotInfo);
m_spGraphicsCommandList->RSSetScissorRects(1, &m_drViewprotRect);
//清空后缓存和深度缓存
m_spGraphicsCommandList->ClearRenderTargetView(
CD3DX12_CPU_DESCRIPTOR_HANDLE(
m_spRTVHeap->GetCPUDescriptorHandleForHeapStart(),
m_iCurrentSwapBuffIndex,
m_iRTVDescriptorSize),
DirectX::Colors::Blue,
0, nullptr);
m_spGraphicsCommandList->ClearDepthStencilView(
m_spDSVHeap->GetCPUDescriptorHandleForHeapStart(),
D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL,
1.f, 0, 0, NULL);
//输出的合并阶段
D3D12_CPU_DESCRIPTOR_HANDLE SwapBufferView = CD3DX12_CPU_DESCRIPTOR_HANDLE(
m_spRTVHeap->GetCPUDescriptorHandleForHeapStart(),
m_iCurrentSwapBuffIndex,
m_iRTVDescriptorSize);
D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView = m_spDSVHeap->GetCPUDescriptorHandleForHeapStart();
m_spGraphicsCommandList->OMSetRenderTargets(
1,
&SwapBufferView,
true,
&DepthStencilView);
//把后缓冲区切换成PRESENT状态
CD3DX12_RESOURCE_BARRIER ResourceBarrierPresentRenderTarget = CD3DX12_RESOURCE_BARRIER::Transition(
vecRTVBuffer[m_iCurrentSwapBuffIndex].Get(),
D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_PRESENT);
m_spGraphicsCommandList->ResourceBarrier(1, &ResourceBarrierPresentRenderTarget);
//录入完成,提交命令
m_spGraphicsCommandList->Close();
ID3D12CommandList* pCommandList[] = { m_spGraphicsCommandList.Get() };
m_spCommandQueue->ExecuteCommandLists(_countof(pCommandList), pCommandList);
}
1、前言
在这一部分,主要就是CPU向GPU送任务的核心操作,包括了六个步骤,逐步的打包好必要资源,然后贴上任务列表,提交给GPU。
在此要注意,打包的必要资源中,绘制模型资源的步骤这里没有提及,毕竟这是最基础的任务提交,在之后,我们会把这个步骤展开详细的进行解释,包括上传资源堆放在GPU哪里,怎么控制GPU渲染的效果等等。
2、把后缓冲区的资源状态切换成Render Target
此步骤就是堆资源的状态进行转换,将资源从呈现状态转换为渲染目标状态(Render Target)。
我们可以将此转换看作是一条告知GPU某资源状态正在进行转换的命令,让它在执行后续的命令时防范一下读写的资源冒险,而这种防范是dx12设定好的,只需要用好资源屏障就可以。
CD3DX12_RESOURCE_BARRIER ResourceBarrierPresent = CD3DX12_RESOURCE_BARRIER::Transition(
vecRTVBuffer[m_iCurrentSwapBuffIndex].Get(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET);
主要用到的 API 为 ResourceBarrier:
void ResourceBarrier(
[in] UINT NumBarriers,
[in] const D3D12_RESOURCE_BARRIER *pBarriers
);
a、NumBarriers:UINT类型,提交的屏障说明数。
b、pBarriers:指向屏障说明数组的指针。
3、设置视口和裁剪区域
明确两个概念,即视口和裁剪区域。
视口:是窗口还可以分为若干个区域,称为视口,窗口中用来绘图的区域。一般设置视口大小等于窗口大小。
裁剪区域:是在视口中让你看到的图形,即显示出来的那部分。
m_spGraphicsCommandList->RSSetViewports(1, &m_dvViewprotInfo);
m_spGraphicsCommandList->RSSetScissorRects(1, &m_drViewprotRect);
这里主要的API是 RSSetViewports 和 RSSetScissorRects:
void RSSetViewports(
[in] UINT NumViewports,
[in, optional] const D3D11_VIEWPORT *pViewports
);
a、NumBarriers:UINT类型,要绑定的视区数。
b、pViewports:const D3D11_VIEWPORT*类型,指向要绑定到设备的 D3D11_VIEWPORT 结构的数组的指针。
void RSSetScissorRects(
[in] UINT NumRects,
[in] const D3D10_RECT *pRects
);
a、NumBarriers:UINT类型,要绑定的剪刀矩形数。
b、pRects :const D3D10_RECT*类型,指向剪刀矩形数组的数组的指针。
4、清空后缓存和深度缓存
在此步骤,应该是为GPU处理做前置的,先把原资源进行初始化,主要关注两个堆,即RTV堆 和 DSV堆 。
m_spGraphicsCommandList->ClearRenderTargetView(
CD3DX12_CPU_DESCRIPTOR_HANDLE(
m_spRTVHeap->GetCPUDescriptorHandleForHeapStart(),
m_iCurrentSwapBuffIndex,
m_iRTVDescriptorSize),
DirectX::Colors::Blue,
0, nullptr);
m_spGraphicsCommandList->ClearDepthStencilView(
m_spDSVHeap->GetCPUDescriptorHandleForHeapStart(),
D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL,
1.f, 0, 0, NULL);
这里主要的API是 ClearRenderTargetView和 ClearDepthStencilView:
void ClearRenderTargetView(
[in] D3D12_CPU_DESCRIPTOR_HANDLE RenderTargetView,
[in] const FLOAT [4] ColorRGBA,
[in] UINT NumRects,
[in] const D3D12_RECT *pRects
);
a、RenderTargetView: D3D12_CPU_DESCRIPTOR_HANDLE类型,指定D3D12_CPU_DESCRIPTOR_HANDLE结构,该结构描述 CPU 描述符句柄,该句柄表示要清除的呈现器目标的堆的开头。
b、ColorRGBA:一个由 4 分量构成的数组,表示要填充呈现器目标时使用的颜色。
c、NumRects:UINT类型,参数指定的数组中的矩形数。
d、pRects:D3D12_RECT类型,要清除的资源视图中矩形的 D3D12_RECT 结构的数组。 如果 为NULL,ClearRenderTargetView 将清除整个资源视图。
void ClearDepthStencilView(
[in] ID3D11DepthStencilView *pDepthStencilView,
[in] UINT ClearFlags,
[in] FLOAT Depth,
[in] UINT8 Stencil
);
a、pDepthStencilView:ID3D11DepthStencilView类型,指向要清除的深度模具的指针。
b、ClearFlags:const D3D10_RECT类型,确定要清除的数据类型 。
c、Depth:FLOAT类型,使用此值清除深度缓冲区。 此值将固定在 0 和 1 之间。
d、Stencil:UINT8类型,使用此值清除模具缓冲区。
5、输出的合并阶段
在此步骤,应该是为GPU处理的后续,在开始提过,GPU处理是没有做的,将在后续文章中补充,但假定GPU处理已经设定好了,这些改好的数据就需要合并到对应结果中,方便后续CPU拿走结果送给显示器进行显示。
D3D12_CPU_DESCRIPTOR_HANDLE SwapBufferView = CD3DX12_CPU_DESCRIPTOR_HANDLE(
m_spRTVHeap->GetCPUDescriptorHandleForHeapStart(),
m_iCurrentSwapBuffIndex,
m_iRTVDescriptorSize);
D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView = m_spDSVHeap->GetCPUDescriptorHandleForHeapStart();
m_spGraphicsCommandList->OMSetRenderTargets(
1,
&SwapBufferView,
true,
&DepthStencilView);
主要用到的 API 为 OMSetRenderTargets:
void OMSetRenderTargets(
[in] UINT NumRenderTargetDescriptors,
[in, optional] const D3D12_CPU_DESCRIPTOR_HANDLE *pRenderTargetDescriptors,
[in] BOOL RTsSingleHandleToDescriptorRange,
[in, optional] const D3D12_CPU_DESCRIPTOR_HANDLE *pDepthStencilDescriptor
);
a、NumRenderTargetDescriptors:UINT类型,pRenderTargetDescriptors 阵列中的项目数, (介于 0 到D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT) 之间。 如果此参数为非零,pRenderTargetDescriptors 指向的数字数组中的项目数必须等于此参数中的数字。
b、pRenderTargetDescriptors:D3D12_CPU_DESCRIPTOR_HANDLE*类型,指定描述 CPU 描述项句柄 的D3D12_CPU_DESCRIPTOR_HANDLE 结构阵列,此句柄代表转译目标描述项堆积的开头。 如果此参数为 NULL 且 NumRenderTargetDescriptors 为 0,则不会绑定转译目标。
c、RTsSingleHandleToDescriptorRange:BOOL类型,True 表示传入的句柄是 连续范围 NumRenderTargetDescriptors 描述元的指标,False 表示句柄是 NumRenderTargetDescriptors 句柄的第一个阵列。
d、pDepthStencilDescriptor:D3D12_CPU_DESCRIPTOR_HANDLE*类型,D3D12_CPU_DESCRIPTOR_HANDLE结构的指标,描述CPU描述项句柄,代表保存深度样板描述项之堆积的开头。 如果此参数为NULL,则不会系结深度样板描述项。。
6、把后缓冲区切换成PRESENT状态
此步骤与第一个阶段基本相同,只是需要把 RENDER_TARGET状态 转换(Transition)为 PRESENT状态。
CD3DX12_RESOURCE_BARRIER ResourceBarrierPresentRenderTarget = CD3DX12_RESOURCE_BARRIER::Transition(
vecRTVBuffer[m_iCurrentSwapBuffIndex].Get(),
D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_PRESENT);
m_spGraphicsCommandList->ResourceBarrier(1, &ResourceBarrierPresentRenderTarget);
7、录入完成,提交命令
最后一个步骤,当然就是炮弹入膛后关闭膛们和点火了,也就是关闭提交列表,把让队列执行任务列表。
m_spGraphicsCommandList->Close();
ID3D12CommandList* pCommandList[] = { m_spGraphicsCommandList.Get() };
m_spCommandQueue->ExecuteCommandLists(_countof(pCommandList), pCommandList);
void ExecuteCommandLists(
[in] UINT NumCommandLists,
[in] ID3D12CommandList * const *ppCommandLists
);
a、NumCommandLists:UINT类型,要执行的命令清单数目。
b、ppCommandLists:ID3D12CommandList* 类型,指定要执行的 ID3D12CommandList 命令列表阵列。
三、页面翻转
上次提到的交换链,我们是写入后台,显示前台的,因此在写入后台后,不要忘了告诉GPU,后台转前台。
//交换两个buff缓冲区
void cDx12Rendering::PageTurn()
{
//交换两个buff缓冲区
m_spSwapChain->Present(0, 0);
m_iCurrentSwapBuffIndex = !(bool)m_iCurrentSwapBuffIndex;
}
四、执行同步
任务提交了,也通知GPU页面反转了,最后就是等待处理结果,这里就是调用初始化的时候准备的同步函数WaitGPUComplete(),因在初始化已经解释过了,这里就不多说了。
void cDx12Rendering::WaitGPUComplete()
{
m_iCurFenceIndex++;
//向GUP设置新的隔离点 等待GPU处理玩信号
m_spCommandQueue->Signal(m_spFence.Get(), m_iCurFenceIndex);
if (m_spFence->GetCompletedValue() < m_iCurFenceIndex)
{
HANDLE hEventEX = CreateEventEx(NULL, NULL, 0, EVENT_ALL_ACCESS);
//GPU完成后会通知我们的Handle
m_spFence->SetEventOnCompletion(m_iCurFenceIndex, hEventEX);
//等待GPU,阻塞主线程
WaitForSingleObject(hEventEX, INFINITE);
CloseHandle(hEventEX);
}
}
五、全流程代码
在Dx12TestMian.cpp的主函数中添加:
while (true)
{
cDx12Render.Draw(0.03);
}
在Dx12Render.h中添加如下成员函数:
public:
void Draw(float DeltaTime);
private:
void ResetCMDListAlloctor();
void SubmitDrawingTask();
void PageTurn();
void WaitGPUComplete(); //初始化阶段已经做好了,这里只是搬到这里,方便理解
在Dx12Render.cpp中添加如下代码:
void cDx12Rendering::Draw(float DeltaTime)
{
ResetCMDListAlloctor();
//核心点
SubmitDrawingTask();
PageTurn();
//CPU等GPU
WaitGPUComplete();
}
void cDx12Rendering::ResetCMDListAlloctor()
{
//重置录制相关的内存,为下一帧做准备
m_spCommandAllocator->Reset();
m_spGraphicsCommandList->Reset(m_spCommandAllocator.Get(), NULL);
}
void cDx12Rendering::SubmitDrawingTask()
{
//把后缓冲区的资源状态切换成Render Target
CD3DX12_RESOURCE_BARRIER ResourceBarrierPresent = CD3DX12_RESOURCE_BARRIER::Transition(
vecRTVBuffer[m_iCurrentSwapBuffIndex].Get(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET);
m_spGraphicsCommandList->ResourceBarrier(1, &ResourceBarrierPresent);
//设置视口和裁剪区域
m_spGraphicsCommandList->RSSetViewports(1, &m_dvViewprotInfo);
m_spGraphicsCommandList->RSSetScissorRects(1, &m_drViewprotRect);
//清空后缓存和深度缓存
m_spGraphicsCommandList->ClearRenderTargetView(
CD3DX12_CPU_DESCRIPTOR_HANDLE(
m_spRTVHeap->GetCPUDescriptorHandleForHeapStart(),
m_iCurrentSwapBuffIndex,
m_iRTVDescriptorSize),
DirectX::Colors::Blue,
0, nullptr);
m_spGraphicsCommandList->ClearDepthStencilView(
m_spDSVHeap->GetCPUDescriptorHandleForHeapStart(),
D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL,
1.f, 0, 0, NULL);
//输出的合并阶段
D3D12_CPU_DESCRIPTOR_HANDLE SwapBufferView = CD3DX12_CPU_DESCRIPTOR_HANDLE(
m_spRTVHeap->GetCPUDescriptorHandleForHeapStart(),
m_iCurrentSwapBuffIndex,
m_iRTVDescriptorSize);
D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView = m_spDSVHeap->GetCPUDescriptorHandleForHeapStart();
m_spGraphicsCommandList->OMSetRenderTargets(
1,
&SwapBufferView,
true,
&DepthStencilView);
//把后缓冲区切换成PRESENT状态
CD3DX12_RESOURCE_BARRIER ResourceBarrierPresentRenderTarget = CD3DX12_RESOURCE_BARRIER::Transition(
vecRTVBuffer[m_iCurrentSwapBuffIndex].Get(),
D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_PRESENT);
m_spGraphicsCommandList->ResourceBarrier(1, &ResourceBarrierPresentRenderTarget);
//录入完成,提交命令
m_spGraphicsCommandList->Close();
ID3D12CommandList* pCommandList[] = { m_spGraphicsCommandList.Get() };
m_spCommandQueue->ExecuteCommandLists(_countof(pCommandList), pCommandList);
}
void cDx12Rendering::PageTurn()
{
//交换两个buff缓冲区
m_spSwapChain->Present(0, 0);
m_iCurrentSwapBuffIndex = !(bool)m_iCurrentSwapBuffIndex;
}
void cDx12Rendering::WaitGPUComplete()
{
m_iCurFenceIndex++;
//向GUP设置新的隔离点 等待GPU处理玩信号
m_spCommandQueue->Signal(m_spFence.Get(), m_iCurFenceIndex);
if (m_spFence->GetCompletedValue() < m_iCurFenceIndex)
{
HANDLE hEventEX = CreateEventEx(NULL, NULL, 0, EVENT_ALL_ACCESS);
//GPU完成后会通知我们的Handle
m_spFence->SetEventOnCompletion(m_iCurFenceIndex, hEventEX);
//等待GPU,阻塞主线程
WaitForSingleObject(hEventEX, INFINITE);
CloseHandle(hEventEX);
}
}
六、效果展示
代码项目在gitcode上,会随着章节更新,点击跳转;