d3d12龙书阅读----Direct3D的初始化
  Ly5WKgqR6znz 2024年03月12日 42 0

d3d12龙书阅读----Direct3D的初始化

使用d3d我们可以对gpu进行控制与编程,以硬件加速的方式来完成3d场景的渲染,d3d层与硬件驱动会将相应的代码转换成gpu可以执行的机器指令,与之前的版本相比,d3d12大大减少了cpu的开销,同时也改进了对多线程的支持,但是使用的api也更加复杂。
接下来,我们将先介绍在d3d初始化中一些重要的概念,之后通过具体的代码进行介绍。

组件对象模型(com)

COM 在 D3D 编程中提供了一种结构化和标准化的方式来处理对象和接口,有助于简化图形编程的复杂性,并提高代码的兼容性和可维护性
在使用com对象时,com对象会统计其引用次数,因此,在使用完com接口之后,我们需要使用它的release方法,当com对象的引用次数为0时,它将自己释放它所占的内存。
为了辅助管理com对象的生存周期,我们可以使用Microsoft::WRL::ComPtr类,我们可以把它当做是com对象的智能指针,当一个ComPtr超出了作用域的范围,它便会自动调用相应Com对象的release方法,免去了我们手动调用的麻烦。

定义举例:

//DXGI接口
Microsoft::WRL::ComPtr<IDXGIFactory4> mdxgiFactory;
Microsoft::WRL::ComPtr<IDXGISwapChain> mSwapChain;
//D3D接口
Microsoft::WRL::ComPtr<ID3D12Device> md3dDevice;

Microsoft::WRL::ComPtr<ID3D12Fence> mFence;
UINT64 mCurrentFence = 0;

Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
Microsoft::WRL::ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> mCommandList;

static const int SwapChainBufferCount = 2;
int mCurrBackBuffer = 0;
Microsoft::WRL::ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];
Microsoft::WRL::ComPtr<ID3D12Resource> mDepthStencilBuffer;

Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mRtvHeap;
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mDsvHeap;

comptr的常用方法:

1.Get方法:返回一个指向此底层com接口的指针 一般将原始的接口指针作为参数传递给函数
Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
ThrowIfFailed(mdxgiFactory->CreateSwapChain(
	mCommandQueue.Get(),
	&sd, 
	mSwapChain.GetAddressOf()));
2.GetAddress方法:返回指向此底层com接口指针的地址 凭借此方法可利用函数参数返回接口指针
Microsoft::WRL::ComPtr<IDXGISwapChain> mSwapChain;
ThrowIfFailed(mdxgiFactory->CreateSwapChain(
	mCommandQueue.Get(),
	&sd, 
	mSwapChain.GetAddressOf()));
3.Reset方法:将comptr设置为nullptr并且释放与之相关的所有引用
Microsoft::WRL::ComPtr<ID3D12Resource> mDepthStencilBuffer;
mDepthStencilBuffer.Reset();

纹理

在本书中,纹理涉及的范围较广,可以把它看成是由数据元素构成的矩阵(1D/2D/3D),可以存储贴图信息与缓冲区信息等等。

缓冲区包括前台缓冲区,后台缓冲区,深度缓冲区,模版缓冲区等等。
其中前台与后台缓冲区,前台缓冲区存储的是当前显示在屏幕上的图像数据,而下一帧的图像数据绘制在后台缓冲区中,当后台缓冲区绘制完成之后,两种缓冲区的角色互换,只需交换两个缓冲区的指针即可,如下图所示:
img

这种方法又被称为双缓冲,而还有一种方法被叫做三缓冲,是为了解决gpu渲染速度与显示器的刷新率之间的矛盾:
在三重缓冲中,有一个正在显示的缓冲区,一个等待显示的缓冲区,和一个正在由 GPU 渲染的缓冲区。当 GPU 完成渲染时,它会将渲染好的帧移到等待显示的缓冲区。当显示器准备好刷新时,它会显示等待中的帧,并将之前显示的帧移动到渲染队列。这样,GPU 可以继续渲染下一帧,而不必等待显示器的刷新。

对于纹理而言,其中存储的数据格式并不是固定的,而是受到一定的限制,常用的设置数据格式有:
img

描述符

描述符是d3d中的又一重要概念,在发出绘制命令之前,我们需要将本次draw call的相应资源绑定到渲染流水线上,但是这一过程并不是直接将资源绑定,而是通过描述符来完成。
通过中间层描述符,有几大好处:

  1. 不同的描述符可以指定不同的资源
  2. 通过描述符可以为GPU解释资源 将资源使用到渲染流水线的不同阶段 告知资源如何使用(我们可以为一个资源创建两个不同的描述符)
  3. 可以使用描述符来绑定资源的局部数据
  4. 资源在创建时采用了无类型格式,描述符可以为其指明具体的类型
    常用的描述符可分为以下几类:
    img
    描述符堆中存有一系列描述符,本质上是存放某种特定类型描述符的一块内存:
//描述符堆的描述符的定义
 Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mRtvHeap;
 Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mDsvHeap;
 D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
 rtvHeapDesc.NumDescriptors = SwapChainBufferCount;
 rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
 rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
 rtvHeapDesc.NodeMask = 0;
 ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
    &rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf())));
//描述符的定义
 Microsoft::WRL::ComPtr<ID3D12Resource> mDepthStencilBuffer;
 D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc;
 dsvDesc.Flags = D3D12_DSV_FLAG_NONE;
 dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
 dsvDesc.Format = mDepthStencilFormat;
 dsvDesc.Texture2D.MipSlice = 0;
 md3dDevice->CreateDepthStencilView(mDepthStencilBuffer.Get(), &dsvDesc, DepthStencilView());

CPU与GPU的交互

命令列表,命令队列与命令分配器

每个gpu都会至少维护一个命令队列(本质上是一个ring buffer,环形缓冲区),cpu可利用命令列表将其中的命令提交给gpu执行,同时命令列表里面的命令存储于命令分配器上,命令队列是从命令分配器中来提取命令。
总结一下:

 在头文件中加入相应com接口的定义
 //命令队列
 Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
 //命令分配器
 Microsoft::WRL::ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
 //命令列表
 Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> mCommandList;
之后进行初始化
void D3DApp::CreateCommandObjects()
{
	//填写命令队列结构体
	D3D12_COMMAND_QUEUE_DESC queueDesc = {};
	queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
	queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
	//创建命令队列
	//IID_PPV_ARGS 将COM ID类型转换为void**类型 作为函数参数
	ThrowIfFailed(md3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue)));
    
	//创建命令分配器
	ThrowIfFailed(md3dDevice->CreateCommandAllocator(
		D3D12_COMMAND_LIST_TYPE_DIRECT,
		IID_PPV_ARGS(mDirectCmdListAlloc.GetAddressOf())));
    
	//创建命令列表
	ThrowIfFailed(md3dDevice->CreateCommandList(
		0,
		D3D12_COMMAND_LIST_TYPE_DIRECT,
		mDirectCmdListAlloc.Get(), // 关联命令分配器
		nullptr,                   
		IID_PPV_ARGS(mCommandList.GetAddressOf())));

	//起始时让命令列表处于关闭状态 因为我们在使用命令列表前需要对其进行reset操作(安全地复用旧列表所占用的底层内存)而在reset之前需要关闭命令列表
	mCommandList->Close();
}
向命令列表中加入命令
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
	D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->RSSetScissorRects(1, &mScissorRect);
将命令列表提交给命令队列 然后执行
ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);

CPU与GPU之间的同步

为了实现cpu与gpu之间的同步,我们需要强制cpu等待 直到gpu完成所有命令的处理,d3d通过围栏实现这一点:

//定义围栏com接口 以及相应的围栏点
 Microsoft::WRL::ComPtr<ID3D12Fence> mFence;
 UINT64 mCurrentFence = 0;
//创建围栏对象
ThrowIfFailed(md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE,
	IID_PPV_ARGS(&mFence)));
//使cpu与gpu同步
void D3DApp::FlushCommandQueue()
{
	//增加围栏点的值
    mCurrentFence++;

    //设置新的围栏点
    ThrowIfFailed(mCommandQueue->Signal(mFence.Get(), mCurrentFence));

	// 直到gpu处理完围栏点之前的命令 围栏点的值才会增加 循环才会结束
    if(mFence->GetCompletedValue() < mCurrentFence)
	{
		HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);
        ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));
		WaitForSingleObject(eventHandle, INFINITE);
        CloseHandle(eventHandle);
	}
}

资源转换

有时候我们可能需要对一个资源先进行写操作,然后再进行读操作进行显示,为了防止在进行写操作的时候读,d3d设置了一组资源状态属性,防止类似上述这种资源冒险的情况发生,例如:

mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
	D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
上述代码将资源从渲染目标状态转换为呈现状态

D3D的初始化

接下来的部分只是大致介绍一下流程 以及重要函数
至于每个函数 每个描述子结构体的参数的详细介绍 可自行查阅

创建设备

进行d3d初始化首先要创建d3d设备

启动调试层
#if defined(DEBUG) || defined(_DEBUG) 
{
	ComPtr<ID3D12Debug> debugController;
	ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
	debugController->EnableDebugLayer();
}
#endif

	ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));

	尝试创建显示适配器(显卡)
	HRESULT hardwareResult = D3D12CreateDevice(
		nullptr,             // default adapter
		D3D_FEATURE_LEVEL_11_0,
		IID_PPV_ARGS(&md3dDevice));

	// 创建失败回退到warp设备
	if(FAILED(hardwareResult))
	{
		ComPtr<IDXGIAdapter> pWarpAdapter;
		ThrowIfFailed(mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));

		ThrowIfFailed(D3D12CreateDevice(
			pWarpAdapter.Get(),
			D3D_FEATURE_LEVEL_11_0,
			IID_PPV_ARGS(&md3dDevice)));
	}

创建围栏

这一点前面已经说明

检测对4x msaa的支持

    首先填写质量等级的结构体 设置为4x 然后使用checkfeaturesupport来检测硬件是否支持
	D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
	msQualityLevels.Format = mBackBufferFormat;
	msQualityLevels.SampleCount = 4;
	msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;
	msQualityLevels.NumQualityLevels = 0;
	ThrowIfFailed(md3dDevice->CheckFeatureSupport(
		D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
		&msQualityLevels,
		sizeof(msQualityLevels)));

    m4xMsaaQuality = msQualityLevels.NumQualityLevels;
	assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");

创建命令队列与列表

描述并创建交换链

void D3DApp::CreateSwapChain()
{
    释放之前创建的交换链
    mSwapChain.Reset();
    填写对应的描述子
    DXGI_SWAP_CHAIN_DESC sd;
    sd.BufferDesc.Width = mClientWidth;
    sd.BufferDesc.Height = mClientHeight;
    sd.BufferDesc.RefreshRate.Numerator = 60;
    sd.BufferDesc.RefreshRate.Denominator = 1;
    sd.BufferDesc.Format = mBackBufferFormat;
    sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
    sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
    sd.SampleDesc.Count = m4xMsaaState ? 4 : 1;
    sd.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    sd.BufferCount = SwapChainBufferCount;
    sd.OutputWindow = mhMainWnd;
    sd.Windowed = true;
	sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
    sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;

    ThrowIfFailed(mdxgiFactory->CreateSwapChain(
		mCommandQueue.Get(),
		&sd, 
		mSwapChain.GetAddressOf()));
}

创建描述符堆

我们需要创建描述符堆来存储相应的描述符

一个堆用于存储rtv 即交换链的两个缓冲区
 D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
 rtvHeapDesc.NumDescriptors = SwapChainBufferCount;
 rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
 rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
	rtvHeapDesc.NodeMask = 0;
 ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
     &rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf())));

一个腿用于存储dsv 即深度缓冲区
 D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;
 dsvHeapDesc.NumDescriptors = 1;
 dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
 dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
	dsvHeapDesc.NodeMask = 0;
 ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
     &dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));

创建渲染目标视图 rtv

在之前创建了交换链的缓冲区之后 我们还需要创建相应的描述子/视图 才能将其绑定到渲染流水线进行输出

CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(mRtvHeap->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < SwapChainBufferCount; i++)
{
	ThrowIfFailed(mSwapChain->GetBuffer(i, IID_PPV_ARGS(&mSwapChainBuffer[i])));
	md3dDevice->CreateRenderTargetView(mSwapChainBuffer[i].Get(), nullptr, rtvHeapHandle);
	rtvHeapHandle.Offset(1, mRtvDescriptorSize);
}

创建深度缓冲区及其视图 dsv

填写深度缓冲区描述符然后使用CreateCommittedResource创建
 D3D12_RESOURCE_DESC depthStencilDesc;
 depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
 depthStencilDesc.Alignment = 0;
 depthStencilDesc.Width = mClientWidth;
 depthStencilDesc.Height = mClientHeight;
 depthStencilDesc.DepthOrArraySize = 1;
 depthStencilDesc.MipLevels = 1;
 depthStencilDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
 depthStencilDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
 depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
 depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;

 D3D12_CLEAR_VALUE optClear;
 optClear.Format = mDepthStencilFormat;
 optClear.DepthStencil.Depth = 1.0f;
 optClear.DepthStencil.Stencil = 0;
 ThrowIfFailed(md3dDevice->CreateCommittedResource(
     &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
		D3D12_HEAP_FLAG_NONE,
     &depthStencilDesc,
		D3D12_RESOURCE_STATE_COMMON,
     &optClear,
     IID_PPV_ARGS(mDepthStencilBuffer.GetAddressOf())));

 //创建深度视图 用于绑定资源
	D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc;
	dsvDesc.Flags = D3D12_DSV_FLAG_NONE;
	dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
	dsvDesc.Format = mDepthStencilFormat;
	dsvDesc.Texture2D.MipSlice = 0;
 md3dDevice->CreateDepthStencilView(mDepthStencilBuffer.Get(), &dsvDesc, DepthStencilView());

 // 将深度缓冲区资源设置为depth buffer 涉及到之前提到的资源的转换
	mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(mDepthStencilBuffer.Get(),
		D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_DEPTH_WRITE));

设置视口与裁剪矩形

可以先设置视口与裁剪矩形的范围:

mScreenViewport.TopLeftX = 0;
mScreenViewport.TopLeftY = 0;
mScreenViewport.Width    = static_cast<float>(mClientWidth);
mScreenViewport.Height   = static_cast<float>(mClientHeight);
mScreenViewport.MinDepth = 0.0f;
mScreenViewport.MaxDepth = 1.0f;

mScissorRect = { 0, 0, mClientWidth, mClientHeight };

之后我们可以在实际渲染过程中设置视口与裁剪矩形:

mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->RSSetScissorRects(1, &mScissorRect);

本结示例代码

void InitDirect3DApp::Draw(const GameTimer& gt)
{
    //reset命令分配器 注意这里要保证里面的命令已经全部被gpu执行完毕
	ThrowIfFailed(mDirectCmdListAlloc->Reset());
   //reset命令列表
    ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(), nullptr));

	//将资源从呈现状态转换到渲染目标状态 读到写
	mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
		D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));

    //reset视口与裁剪矩形 每次reset命令列表都要reset
    mCommandList->RSSetViewports(1, &mScreenViewport);
    mCommandList->RSSetScissorRects(1, &mScissorRect);

    //清空后台缓冲区与深度缓冲区
	mCommandList->ClearRenderTargetView(CurrentBackBufferView(), Colors::LightSteelBlue, 0, nullptr);
	mCommandList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
	
    // 指明我们要写入的缓冲区
	mCommandList->OMSetRenderTargets(1, &CurrentBackBufferView(), true, &DepthStencilView());
	
    // 将后台缓冲区从渲染目标状态转换到呈现状态
	mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
		D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));

    // 关闭命令列表
	ThrowIfFailed(mCommandList->Close());
 
    // 执行命令
	ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
	mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
	
	// 交换前后缓冲区
	ThrowIfFailed(mSwapChain->Present(0, 0));
	mCurrBackBuffer = (mCurrBackBuffer + 1) % SwapChainBufferCount;

	// 等待gpu执行完所有命令 保证同步
	FlushCommandQueue();
}
【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2024年03月12日 0

暂无评论

推荐阅读
  oXKBKZoQY2lx   2024年03月19日   35   0   0 游戏开发
  oXKBKZoQY2lx   2024年03月23日   41   0   0 游戏开发
  1wMBnbixARwC   2024年03月01日   41   0   0 游戏开发
  1wMBnbixARwC   2024年03月09日   59   0   0 游戏开发
  1wMBnbixARwC   2024年03月06日   25   0   0 游戏开发
  398vzLiyyaJn   2024年03月13日   24   0   0 游戏开发
Ly5WKgqR6znz