DirectX 12是微软最新的图形API库,只在Win10系统上提供支持(如果不是Win10必须要升级成Win10才能用)。对这套API,我最大的感触就是特别复杂,因为它暴露了很多底层的方法。即便是我学过DX9,但是面对DX12的时候依旧会是一脸懵逼的状态,不仅是因为DX12暴露了太多方法,还有就是它封装方法的思路。好了,闲话就说到这里,下面我会用我理解的思路来介绍DX12绘制流程,并且给出一套可运行的代码。

DX12的绘制流程大概是这样的:

首先,绘制需要一个绘制的环境。绘制环境是在代码层对硬件环境的一种抽象,所有的绘制都是在这个绘制环境中完成的,可以说绘制环境才是我们的“硬件”。DX12对绘制环境的抽象是ID3D12Device。

然后,绘制自然需要资源。这里的资源就包括用来暂存渲染结果的缓冲区,包括渲染缓冲区和深度缓冲区。

最后,绘制需要绘制指令。必须让GPU知道我们想怎么样绘制才行。

思路就是这么简单,但是DX12如何实现这绘制思路才是我们关心的重点!

 

绘制环境

DX12使用的COM技术,什么是COM技术,简单的理解就是从一个统一的公共基类继承出来的子类,用这种技术最大的好处就是资源的分配和释放可以不用太操心。详细的内容读者可以自行百度。

在创建Device的时候,我们要做两手准备。一是直接创建默认显示适配器的Device,二是如果这个默认显示适配器的Device没法创建,那么就创建一个软件适配器的Device。用代码实现就是下面这样:

/*
 * DirectX 12 
 */
 
ComPtr<IDXGIFactory4> 			g_pDXGIFactory = nullptr; // dxgi工厂
ComPtr<ID3D12Device>			g_pDevice = nullptr; // Device的指针

D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS	g_msQualityLevels;	// 多重采样支持状况

 void CreateDevice() {
	// 创建一个基于硬件的Device
	HRESULT hardwareResult = D3D12CreateDevice(
		nullptr,	// 哪个显示适配器,nullptr表示默认的显示适配器
		D3D_FEATURE_LEVEL_11_0, // 希望支持的功能集
		IID_PPV_ARGS(&g_pDevice));  // 输出的ComPtr<ID3D12Device>对象
	
	// 如果没法创建基于硬件的Device,那就创建一个
        // WARP(Windows Advanced Rasterization Platform)软件适配器的Device
        // DXGI是微软提供的一个基础库,它提供了与图形API有关但是又不适合放入图形API的功能
        // 比如枚举系统中的显示适配器,显示器,支持的显示分辨率刷新率等等。
        // 这里我们用到的一个重要的接口是IDXGIFactory4,因为年代久远的关系,微软都不得不用这种
        // 魔数了。这个接口可以枚举显示适配器,同样,创建交换链也是它的功能。
	ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&g_pDXGIFactory)));
	if (FAILED(hardwareResult)) {
		ComPtr<IDXGIAdapter> pWarpAdapter; // 显示适配器的抽象
		ThrowIfFailed(g_pDXGIFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));  // 枚举显示适配器
		
                // 创建与适配器相关的Device
		ThrowIfFailed(D3D12CreateDevice(
			pWarpAdapter.Get(), // 获取COM的原始指针
			D3D_FEATURE_LEVEL_11_0,
			IID_PPV_ARGS(&g_pDevice)));  // 获取COM的指针的地址
	}

	// 检查支持4X的MSAA的质量等级
	// 所有Direct3D11的兼容设备都支持4X的MSAA,所以我们只需要检查一下支持到什么等级
	g_msQualityLevels.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // 缓冲格式
	g_msQualityLevels.SampleCount = 4; // 采样数
	g_msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE; // 
	g_msQualityLevels.NumQualityLevels = 0;
	ThrowIfFailed(g_pDevice->CheckFeatureSupport(
				D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
				&g_msQualityLevels,
				sizeof (g_msQualityLevels)));
}

为了枚举拥有机器拥有的所以适配器,我们需要用到微软DXGI(DirectX Graphics Infrastructure)库,这个库中包含了图形编程需要用到但是不适合包含在DirextX中的功能,比如枚举显示适配器,显示器,机器支持的分辨率,刷新率等等。g_pDXGIFactory的变量类型是ComPtr<IDXGIFactory4>,创建函数名是CreateDXGIFactory1,很尴尬,在不断发展的过程中,即便是微软也不得不用这种“魔数”来区分版本了。

CheckFeatureSupport用于检查设备支持的多重采样等级,多重采样数最大为32,由D3D11_MAX_MULTISAMPLE_SAMPLE_COUNT宏定义。一般出于性能方面的考虑,会设置成4或者8。之所以要检查这个,是因为在创建渲染缓冲区和深度缓冲区的时候也需要这些数据。

创建命令队列和命令列表

与之前的DX不同,DX12把渲染的过程暴露了很多出来。命令队列(Command Queue)和命令列表(Command List)就是其中之一。命令队列的存在主要是为了充分利用CPU和GPU的算力,让他们可以同时都处于计算之中,减少等待的时间。命令队列主要负责接收CPU生成好的命令(列表),然后通知GPU执行。命令列表主要是让CPU生成命令,将它保存进去,当保存到一定程度时,就可以放到命令队列去让GPU执行了(不一定要把所有命令都生成之后再执行)。

ComPtr<ID3D12CommandQueue>	        g_pCommandQueue;	// 命令队列的指针
ComPtr<ID3D12CommandAllocator>		g_pCommandAllocator;	// 命令分配器的指针(用于分配命令的缓冲区)
ComPtr<ID3D12GraphicsCommandList>	g_pCommandList;		// 保存命令的列表,该命令会被发送到GPU执行

void CreateCommandObjects() {
	// 命令队列创建,需要一个描述结构来定义要什么样的命令队列
	D3D12_COMMAND_QUEUE_DESC queueDesc = {};
	queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
	queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
	ThrowIfFailed(g_pDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&g_pCommandQueue)));
	
	// 命令列表的创建需要一个前置的分配器(Allocator),这个分配器主要是为了资源重用
        // 多个命令列表可以关联相同的分配器,但是一个分配器上同时只能有一个命令列表在操作
	ThrowIfFailed(g_pDevice->CreateCommandAllocator(
		D3D12_COMMAND_LIST_TYPE_DIRECT,
		IID_PPV_ARGS(g_pCommandAllocator.GetAddressOf())));
		
	ThrowIfFailed(g_pDevice->CreateCommandList(
		0,
		D3D12_COMMAND_LIST_TYPE_DIRECT,
		g_pCommandAllocator.Get(),	// 相关的分配器
		nullptr, // 初始管线状态对象
		IID_PPV_ARGS(g_pCommandList.GetAddressOf())));
		
	// 关闭命令列表
        // 在命令列表被创建或者被重置的时候,命令列表的状态是Open,但是Open状态的命令列表
        // 不能被执行,所以,在操作完命令列表之后将它设置成Close是一个好习惯
	g_pCommandList->Close();
}
创建交换链

交换链是一种资源,但它比较特殊,它是当前正在显示的数据和下一帧将显示的数据的保存地。现代的绘制方式通常是这样,一个交换链有2个缓冲区,分别是前缓冲区和后缓冲区,前缓冲区中的数据是当前显示(能看到的)数据,后缓冲区的数据是下一帧要显示的数据,当需要显示后缓冲区中的数据时,将后缓冲区设置为“前”缓冲区就行了,这是一个非常巧妙的设定。

创建交换链的难点在于要描述创建什么样的交换链,这个描述结构的成员很多很繁杂,可以参考官方的[说明文档](DXGI_SWAP_CHAIN_DESC)。创建交换链的流程非常简单,只有三步:

1、释放已有的交换链。

2、填充描述结构,指明要创建什么样的交换链。

3、调用Factory的CreateSwapChain函数创建交换链。

具体的内容请看下面的代码:

ComPtr<IDXGISwapChain> 	g_pSwapChain = nullptr;		// 交换链接口的指针

void CreateSwapChain() {
	// 释放交换链,我们要重新生成。
	g_pSwapChain.Reset();
	
        // 交换链描述结构,用于定义创建什么样的交换链。
	DXGI_SWAP_CHAIN_DESC sd;
	sd.BufferDesc.Width = g_ScreenWidth;
	sd.BufferDesc.Height = g_ScreenHeight;
	sd.BufferDesc.RefreshRate.Numerator = 60; // 刷新率,分子
	sd.BufferDesc.RefreshRate.Denominator = 1; // 刷新率,分母
	sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // DXGI_FORMAT枚举,用于定义显示格式,这里表示一个32位无符号规范化的整数格式,每个通道有8位,包括透明通道
	sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED; // DXGI_MODE_SCANLINE_ORDER枚举,用于定义扫描线绘图格式,这里表示不指定扫描线绘图格式。
	sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED; // DXGI_MODE_SCALING枚举,用于指明图像如何拉伸以适配显示器分辨率。这里同样是不指定。
	sd.SampleDesc.Count = 1; // 多重采样数只能是1,不然无法和DXGI_SWAP_EFFECT_FLIP_DISCARD匹配
	sd.SampleDesc.Quality = g_msQualityLevels.NumQualityLevels - 1; // 采样的质量等级
	sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // 缓存要做什么用
	sd.BufferCount = g_FrameCount; // 缓存数量
	sd.OutputWindow = g_hWnd; // 关联窗口
	sd.Windowed = true; // 是否窗口化
	sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;  // 指明为flip显示模型,调用了IDXGISwapChain1::Present1之后就丢弃缓存数据
	sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; // 交换链的行为标志。允许应用切换窗口模式,显示模式(显示分辨率)会匹配窗口的大小。
	
	// 交换链需要命令队列来刷新
	ThrowIfFailed(g_pDXGIFactory->CreateSwapChain(
					g_pCommandQueue.Get(),
					&sd,
					g_pSwapChain.GetAddressOf()));
}
创建资源描述符

在DX12中,微软将后缓冲区,深度/模板缓冲区统统抽象成资源。当我们提交一个绘制命令的时候,我们需要绑定资源到渲染管线上,这样GPU才能获取到资源。但是,GPU资源不是直接被绑定的,而是通过一种引用的方式来绑定。这个资源描述符(descriptor)就是用来引用资源的工具。我们可以把它当做是一个轻量级的结构体,为GPU描述了资源是做什么用的。

为什么要多出这么一个中间层呢?官方的解释是,GPU资源本来就是一些内存块,用这种原始的保存方式可以在渲染管线的不同阶段都被访问到。从GPU的角度来看,资源就是一块内存中的数据,而只有描述符才知道这块内存到底是做什么用的,是渲染目标,还是深度缓存。

由于历史原因,描述符(descriptor)和视图(view)通常是等价的,所以你经常会看到一些以view为名的函数或者变量。

ComPtr<ID3D12Fence>	g_pFence;	// fence指针
uint32_t g_rtvDescriptorSize = 0;
uint32_t g_dsvDescriptorSize = 0;

void CreateDescriptorHeaps() {
	// fence的作用是控制CPU和GPU的同步性。
        // 当GPU执行到fence命令的时候,CPU就可以知道,避免CPU去操作GPU正在使用的资源。
	ThrowIfFailed(g_pDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&g_pFence)));
	
	// RTV,表示Render Target View
        // 这里就体现了view和descriptor的等价性,也就是说他们是同一个东西。

        // 获取给定描述符堆类型的句柄增量的大小。这个值是用来在获取资源句柄的时候做偏移量用的。
	g_rtvDescriptorSize = g_pDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
	// 描述符堆,可以简单地理解为描述符的一个数组。
        // 创建描述符堆需要先说明这个堆是干嘛用的,我们这里是为了作为渲染目标使用(Render Target View)
        D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
	rtvHeapDesc.NumDescriptors = g_FrameCount; // 堆中描述符的数量
	rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; // 描述符堆的类型(RTV)
	rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; // 描述符堆的标志(NONE表示默认使用方式)
	rtvHeapDesc.NodeMask = 0; // 单适配器设置成0
        // 创建描述符堆
	ThrowIfFailed(g_pDevice->CreateDescriptorHeap(
					&rtvHeapDesc,
					IID_PPV_ARGS(g_pRtvHeap.GetAddressOf())));
					
	// DSV,表示depth-stencil View
	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(g_pDevice->CreateDescriptorHeap(
					&dsvHeapDesc,
					IID_PPV_ARGS(g_pDsvHeap.GetAddressOf())));
}
创建缓冲区

资源描述符只是描述一块内存是什么资源,但是这块内存我们还是需要分配的,这才是真正的资源,缓冲区就是一种资源。我们把创建缓冲区放到单独的函数中。

uint32_t g_currentBackBuffer = 0;
ComPtr<ID3D12Resource> g_SwapChainBuffer[g_FrameCount]; // 渲染缓冲区,渲染缓冲区有2个,一前一后
ComPtr<ID3D12Resource> g_DepthStencilBuffer; // 深度/模板缓冲区

void CreateBuffers() {
	// 释放缓冲区的资源,我们会重新创建
	for (int i = 0; i < g_FrameCount; ++i)
		g_SwapChainBuffer[i].Reset();
	g_DepthStencilBuffer.Reset();

	// 根据窗口大小重新分配缓冲区大小
	ThrowIfFailed(g_pSwapChain->ResizeBuffers(
		g_FrameCount, // 缓冲区数量
		g_ScreenWidth, g_ScreenHeight, // 窗口的宽高
		DXGI_FORMAT_R8G8B8A8_UNORM, // 缓冲区格式
		DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH)); // 交换链的行为指定。允许应用通过ResizeTarget切换模式。当从窗口切换到全屏时
														  // 显示模式(显示器分辨率)会匹配窗口的尺寸。

	g_currentBackBuffer = 0; // 当前后缓冲区重制成0

	/** 创建渲染目标视图(Render Target View) */
		// 为了绑定后缓冲区到管线的输出合并阶段,我们需要创建渲染目标视图给后缓冲区。
		// 获取堆起始位置的描述符句柄,通过句柄来使用描述符堆。
	CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(g_pRtvHeap->GetCPUDescriptorHandleForHeapStart());
	for (int i = 0; i < g_FrameCount; ++i) {
		// 获取缓冲区资源,保存到全局变量中,需要从交换链中获取。
		ThrowIfFailed(g_pSwapChain->GetBuffer(i, IID_PPV_ARGS(&g_SwapChainBuffer[i])));
		// 为缓冲区创建渲染目标视图(Render Target View),这是一种资源,需要跟描述符关联起来
		g_pDevice->CreateRenderTargetView(g_SwapChainBuffer[i].Get(), nullptr, rtvHeapHandle);
		// 堆上下一个资源描述符入口
		rtvHeapHandle.Offset(1, g_rtvDescriptorSize);
	}

	/** 创建深度/模板缓冲区与视图 */
		// 深度/模板缓冲区描述,定义创建的资源格式。
	D3D12_RESOURCE_DESC depthStencilDesc;
	depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; // 缓冲区维数(1维纹理还是2维纹理?)
	depthStencilDesc.Alignment = 0; // 对齐
	depthStencilDesc.Width = g_ScreenWidth; // 缓冲区宽度
	depthStencilDesc.Height = g_ScreenHeight; // 缓冲区高度
	depthStencilDesc.DepthOrArraySize = 1; // 纹理的深度,以纹素为单位;或者是纹理数组尺寸
	depthStencilDesc.MipLevels = 1; // 多级纹理级数
	depthStencilDesc.Format = DXGI_FORMAT_R24G8_TYPELESS; // 纹素格式
	depthStencilDesc.SampleDesc.Count = 1; // 多重采样采样数
	depthStencilDesc.SampleDesc.Quality = 0; // 多重采样采样质量
	depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; // 纹理布局,暂时不用管。
	depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; // 其他资源标志。允许深度/模板视图作为资源创建,
																		  // 并且允许资源转换成D3D12_RESOURCE_STATE_DEPTH_WRITE或D3D12_RESOURCE_STATE_DEPTH_READ状态

	// 清空缓存时候的默认值
	D3D12_CLEAR_VALUE optClear;
	optClear.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; // 缓存数据格式
	optClear.DepthStencil.Depth = 1.0f; // 深度值
	optClear.DepthStencil.Stencil = 0; // 模板值
		// 创建,提交一个资源到特定的堆,这个资源属性是我们指定的。
	ThrowIfFailed(g_pDevice->CreateCommittedResource(
		&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT), // 特定的堆的属性。这里表示堆是被GPU访问的。
		D3D12_HEAP_FLAG_NONE, // 堆选项,指明堆是否包含纹理,资源是否被多个适配器共享。NONE表示不需要额外的功能。
		&depthStencilDesc, // 资源描述符。生成这种资源。
		D3D12_RESOURCE_STATE_COMMON, // 资源的初始状态。要如何使用这个资源?
											 // 这个枚举的官方解释是要跨越图形引擎类型访问资源的话必须转换到这个状态。但是并没有给出详细的说明。
											 // 还指出了,纹理必须是COMMON状态,为了让CPU进行访问。这里是否跟第一个参数冲突?
		&optClear, // 清除值
		IID_PPV_ARGS(g_DepthStencilBuffer.GetAddressOf()))); // 资源的COM地址

	// 创建深度/模板视图,使用与之前创建的资源匹配的格式
	D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc;
	dsvDesc.Flags = D3D12_DSV_FLAG_NONE; // 深度/模板视图的选项,这里表示用默认视图
	dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D; // 如何获取资源?作为2D纹理获取
	dsvDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; // 资源数据格式,包括完整类型和无类型格式。24位深度8位模板。
	dsvDesc.Texture2D.MipSlice = 0; // 多级纹理的第1层
		// 创建深度/模板视图,关联资源和描述符
	g_pDevice->CreateDepthStencilView(g_DepthStencilBuffer.Get(), &dsvDesc, g_pDsvHeap->GetCPUDescriptorHandleForHeapStart());

	// 深度/模板缓冲区资源状态转换成深度写入
	g_pCommandList->ResourceBarrier(
		1,
		&CD3DX12_RESOURCE_BARRIER::Transition(
			g_DepthStencilBuffer.Get(),
			D3D12_RESOURCE_STATE_COMMON,
			D3D12_RESOURCE_STATE_DEPTH_WRITE));
}

让CPU等待直到GPU绘制完毕。这是一个非常低效的操作,但是为了同步,这是必要的操作。

uint32_t g_CurrentFence = 0; // 当前fence值

void FlushCommandQueue() {
	// 增加fence的值,确保执行到这个值。
	g_CurrentFence++;
	
	// 给命令队列中添加一个指令,设置新的fence值。
	// GPU完成了所有指令的处理之后才能到达这个fence。
	ThrowIfFailed(g_pCommandQueue->Signal(g_pFence.Get(), g_CurrentFence));
	
	// 直到GPU到达这个fence点。
	if (g_pFence->GetCompletedValue() < g_CurrentFence) {
                // 创建一个事件
		HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);
		
		// 设置Fence对象到达fence点时激发事件
		ThrowIfFailed(g_pFence->SetEventOnCompletion(g_CurrentFence, eventHandle));
		
		// 等待事件被激发
		WaitForSingleObject(eventHandle, INFINITE);
		CloseHandle(eventHandle);
	}
}
重新分配大小

当窗口大小改变的时候,我们就要重新分配缓冲区的大小了。原有的资源肯定是要释放掉的,然后再创新创建资源,那么有没有方法可以重用之前写过的代码呢?当然有,之前的创建缓冲区的函数就可以重用,这样代码就少了很多。同时,我们还需要把视口和裁剪区的位置保存下来。

void OnResize() {
	// 只有Device、交换链、命令分配器都创建了之后才能重新分配缓冲区大小
	if (g_pDevice == nullptr || g_pSwapChain == nullptr || g_pCommandAllocator == nullptr) {
		return;
	}
	
	// 确保所有的资源都不在使用了。
	FlushCommandQueue();
        
        ThrowIfFailed(g_pCommandList->Reset(g_pCommandAllocator.Get(), nullptr));

	// 创建缓冲区
	CreateBuffers();
	
	// 执行重新创建的命令
	ThrowIfFailed(g_pCommandList->Close());
	ID3D12CommandList* cmdLists[] = {g_pCommandList.Get()};
	g_pCommandQueue->ExecuteCommandLists(_countof(cmdLists), cmdLists);
	
	// 确保创建的指令都已经完成了
	FlushCommandQueue();
	
	// 更新视口的变化
	g_ScreenViewport.TopLeftX = 0;
	g_ScreenViewport.TopLeftY = 0;
	g_ScreenViewport.Width = static_cast<float>(g_ScreenWidth);
	g_ScreenViewport.Height = static_cast<float>(g_ScreenHeight);
	g_ScreenViewport.MinDepth = 0.0f;
	g_ScreenViewport.MaxDepth = 1.0f;
	
	g_ScissorRect = {0, 0, static_cast<LONG>(g_ScreenWidth), static_cast<LONG>(g_ScreenHeight)};
}
绘制

在这里,我们先简单的用一个颜色填充整个窗口。作为一个入门的工程,这种效果绰绰有余了。

void Draw() {
	// 重用记录命令的内存块。
	// 只有GPU执行完绘制命令后,才能调用Allocator的Reset函数。
	ThrowIfFailed(g_pCommandAllocator->Reset());
	
	// 命令列表在提交之后就可以重置了,不需要等到命令执行完。
	// 重用命令列表的内存块。
	ThrowIfFailed(g_pCommandList->Reset(g_pCommandAllocator.Get(), nullptr));

	// 把后缓冲区的资源状态切换成Render Target。
        // ResourceBarrier函数创建了一个通知驱动同步资源访问性的命令,简单地来说就是切换资源的状态。
	g_pCommandList->ResourceBarrier(1,
                // D3D12_RESOURCE_BARRIER是资源栅栏(暂时这么翻译),用来表示对资源的操作。
                // CD3DX12_RESOURCE_BARRIER类是D3D12_RESOURCE_BARRIER结构的辅助类,提供更便利的
                // 使用接口。Transition的作用正如其函数名一样,创建资源状态转换的操作,其返回值
                // 是CD3DX12_RESOURCE_BARRIER 。
		&CD3DX12_RESOURCE_BARRIER::Transition(g_SwapChainBuffer[g_currentBackBuffer].Get(),
			D3D12_RESOURCE_STATE_PRESENT,
			D3D12_RESOURCE_STATE_RENDER_TARGET));
			
	// 设置视口和裁剪区域。视口和裁剪区域要随着命令列表的重置而重新设置。
        g_pCommandList->RSSetViewports(1, &g_ScreenViewport);
	g_pCommandList->RSSetScissorRects(1, &g_ScissorRect);
	
	// 清空后缓存和深度缓存。
	g_pCommandList->ClearRenderTargetView(
                // 同样,CD3DX12_CPU_DESCRIPTOR_HANDLE类也是一个辅助类,它辅助的是D3D12_CPU_DESCRIPTOR_HANDLE结构体
                // 我们要访问资源,必须通过D3D12_CPU_DESCRIPTOR_HANDLE,也就是资源的句柄。
                // 这里我们传入了当前后缓冲区的id以及渲染视图描述符的尺寸从而获取了对应资源的句柄。
		CD3DX12_CPU_DESCRIPTOR_HANDLE(
			g_pRtvHeap->GetCPUDescriptorHandleForHeapStart(),
			g_currentBackBuffer,
			g_rtvDescriptorSize), 
		Colors::LightSteelBlue, // 钢蓝色
		0, // 清空的区域数量
		nullptr);  // 清空的区域矩形数组
	g_pCommandList->ClearDepthStencilView(
		g_pDsvHeap->GetCPUDescriptorHandleForHeapStart(), // 深度/模板描述符的句柄
		D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, // 清理标志,我们要清深度和模板缓存
		1.0f, // 深度值
		0, // 模板值
		0, // 同样,清空的区域数量
		nullptr); // 清空的区域矩形数组
		
	// 设置CPU资源描述符句柄,包括渲染目标和深度/模板缓存
	g_pCommandList->OMSetRenderTargets(1,  // 描述符数
		&CD3DX12_CPU_DESCRIPTOR_HANDLE(
			g_pRtvHeap->GetCPUDescriptorHandleForHeapStart(),
			g_currentBackBuffer,
			g_rtvDescriptorSize), // 获取了渲染目描述符的句柄
		true,
		&g_pDsvHeap->GetCPUDescriptorHandleForHeapStart()); // 模板描述符的句柄
		
	// 把后缓冲区切换成PRESENT状态。
	g_pCommandList->ResourceBarrier(
		1,
		&CD3DX12_RESOURCE_BARRIER::Transition(
			g_SwapChainBuffer[g_currentBackBuffer].Get(),
			D3D12_RESOURCE_STATE_RENDER_TARGET,
			D3D12_RESOURCE_STATE_PRESENT));
			
	// 命令保存完毕,关闭命令列表。
	ThrowIfFailed(g_pCommandList->Close());
	
	// 添加命令列表到队列中并执行。
	ID3D12CommandList* cmdsLists[] = { g_pCommandList.Get()};
	g_pCommandQueue->ExecuteCommandLists(_countof (cmdsLists), cmdsLists);
	
	// 交换前后缓冲区
	ThrowIfFailed(g_pSwapChain->Present(0, 0));
	g_currentBackBuffer = (g_currentBackBuffer + 1) % g_FrameCount;
	
	// 强制刷新,让GPU执行命令。这个操作并不搞笑,但是对一个简单示例来说
	// 就已经足够了。
	FlushCommandQueue();
}

主要功能已经完成了,我们还需要把代码组装起来。这里我不提供Windows上创建窗口的代码,因为这和DX12无关,相关的窗口创建代码可以直接百度,示例很多,不用担心。言归正传,组装代码的过程相当于一个整理DX12绘制流程的过程:

void InitDirect3D12() {
	CreateDevice();
	
	CreateCommandObjects();
	
	CreateSwapChain();
	
	CreateDescriptorHeaps();
        
        OnResize();
}

没错,就是这个流程,最开始是Device,我们代码角度的“硬件”。然后是命令对象,包括命令列表和命令队列,之所以在这里就创建,是因为在创建交换链的时候要用到命令队列。紧接着就是创建交换链,之后是创建描述符堆,包括渲染和深度两个描述符堆,虽然深度只有一个元素。最后是重新分配大小,因为这里面有创建缓冲区的过程,创建缓冲区的时候需要和资源描述符匹配起来。至于绘制在哪里调用,自然是在消息循环中:

int Run() {
    MSG msg = {};

	while(msg.message != WM_QUIT) {
		if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		else {
			Draw(); // 不处理消息的时候绘制界面
		}
	}

    return msg.wParam;
}

相关的代码请参考:

FlashPanda/Panda​github.com

 

参考资料:

龙书:Introduction to 3D Game Programming With DirectX 12

知乎文章:陈文礼:从零开始手敲次世代游戏引擎(十五)

龙书配套的代码:d3dcoder/d3d12book