DX12 快速教程(2) —— 渲染天蓝色窗口

快速导航

  • 新建项目 "002-DrawSkyblueWindow"
  • DirectX 12 入门
    • 1. COM 技术:DirectX 的中流砥柱
      • 什么是 COM 技术
      • COM 智能指针
    • 2.创建 D3D12 调试层设备:CreateDebugDevice
      • 什么是调试层
      • 如何创建并使用调试层
    • 3.创建 D3D12 设备:CreateDevice
      • 认识 CPU 和 GPU
      • 认识显卡
      • DXGI:软件与硬件之间的桥梁
      • 创建 D3D12 核心设备
    • 4.创建命令三件套:CreateCommandComponents
      • 认识命令三件套
      • 创建并使用命令三件套
    • 5.创建渲染目标:CreateRenderTarget
      • 资源管理
      • 创建 RTV 描述符堆
      • 创建交换链
      • 通过交换链创建渲染目标资源,并创建 RTV 描述符
    • 6.创建围栏和资源屏障:CreateFenceAndBarrier
      • GPU 与 CPU 的同步
      • 围栏
      • 资源屏障
    • 7.渲染:Render
  • 多线程渲染优化:MsgWaitForMultipleObjects()
  • 第二节全代码


新建项目 “002-DrawSkyblueWindow”


  • 打开原来的解决方案 “DX12”,右键 “解决方案” -> “添加” -> “新建项目”

在这里插入图片描述

  • 选"空项目" -> 项目名称为 “002-DrawSkyblueWindow” -> “创建”

在这里插入图片描述
在这里插入图片描述

  • 右键刚刚创建的 “002-DrawSkyblueWindow” -> “设为启动项目”

在这里插入图片描述


  • 右键项目 -> “链接器” -> “系统” -> “子系统” -> 选择"窗口" -> 按"确定"

在这里插入图片描述
在这里插入图片描述

  • 右键项目 -> “添加” -> “新建项” -> 命名为"main.cpp" -> “添加”

在这里插入图片描述
在这里插入图片描述

  • 将上一节的代码复制到本项目的 main.cpp 上

在这里插入图片描述

之后新建项目都是重复这里的操作即可。



DirectX 12 入门


后文提到的 DX12、D3D12 都是对 Direct 3D 12 (DirectX 12) 的简称,表示它是 微软 DirectX 3D 的第12代技术。


1. COM 技术:DirectX 的中流砥柱


什么是 COM 技术


COM (Component Object Model,组件对象模型) 技术是微软推出的一种组件编程模型,它支持可重用的组件开发,并具有跨语言和跨平台的特性。



什么是COM组件技术?插件技术就是COM技术,COM技术,其实是程序员想偷懒才产生的,因为它不仅可重用,方便更新和维护;而且一旦编写出来,可以被各种编程语言所使用

以C++为例,COM组件实际上就是一些实现了特定接口的类,而接口都是抽象类组件从接口派生而来。我们可以简单的用C++的语法形式来描述COM是个什么东西:

class IObject		// 接口,这个类是抽象类,不能实例化
{
  public:
    virtual Function1() = 0;
    virtual Function2() = 0;
};
  
class MyObject : public IObject		// 组件,继承并实现接口
{
  public:		// 实现接口的纯虚函数
    virtual Function1(){}
    virtual Function2(){}
}; 

看清楚了吗?IObject 就是我们常说的接口,MyObject 就是所谓的COM组件。切记切记接口都是纯虚类,它所包含的函数都是纯虚函数,而且它没有成员变量。而COM组件就是从这些纯虚类继承下来的派生类,它实现了这些虚函数,仅此而已。从上面也可以看出,COM组件是以 C++为基础的,特别重要的是虚函数多态性的概念,COM中所有函数都是虚函数,都必须通过虚函数表VTable来调用,这一点是无比重要的,必需时刻牢记在心。

COM组件由以 Win 32动态链接库(DLL)可执行文件(EXE)形式发布的可执行代码所组成。DirectX 家族都是基于 COM技术的,这就是我们包含了头文件还要链接 DLL 的原因:

#pragma comment(lib,"d3d12.lib")	// 链接 DX12 核心 DLL
#pragma comment(lib,"dxgi.lib")		// 链接 DXGI DLL
#pragma comment(lib,"dxguid.lib")	// 链接 DXGI 必要的设备 GUID

COM 技术涉及的原理非常复杂,本文不再详细展开,感兴趣可以百度查阅一下相关资料


COM 智能指针


在这里插入图片描述


COM技术不仅规定了组件“如何写”,还规定了这些组件“如何用”。COM组件有严格的生命周期管理,注册和卸载服务稍有不慎就会写错,造成程序错误甚至崩溃。

为了解决这个问题,实现 COM 组件生命周期的自动管理,微软在 WRL库 (Windows Runtime C++ Template Library,Windows 运行时 C++ 模板库) 提供了一套 C++ 风格的 COM 智能指针模板

ComPtr<ID3D12Device4> m_D3D12Device; 

COM接口都以大写字母 “I” 开头。使用 ComPtr<COM接口类型> 变量名 可以轻松创建一个 COM 组件,这是下文 DirectX 12 的基础。


在这里插入图片描述

COM接口后面的数字表示它的版本,高版本接口和低版本接口共用一套 DLL (COM组件的基础是 DLL,DLL 方便远程维护和更新)
有时候常规方法是不能直接创建高版本接口的,需要通过继承低版本接口对象的数据来创建。


被这个模板包裹的组件主要有以下成员方法:

方法名说明示例 (以 ComPtr<T> comp 为例)
ComPtr<T> comp;COM 智能指针对象相当于 T* comp;
.Get()返回指向此底层COM接口的一级指针,常常用于函数的输入参数comp.Get() 等价于 comp
.GetAddressOf()返回指向此底层COM接口指针的地址 (二级指针),常常用于函数的输出参数comp.GetAddressOf() 等价于 &comp
.Reset()重置对象,等价于将 ComPtr 对象赋值为 nullptrcomp.Reset() 等价于 comp = nullptr
&返回重置后的对象指针,相当于调用了 .ReleaseAndGetAddressOf() 方法
常用于创建一个新的COM组件对象,此方法会重置原来的对象,慎用
&comp 等价于
comp.Reset() + comp.GetAddressOf()
->调用底层COM接口指针的具体成员方法调用 comp 里面具体的成员方法 comp->func()
.As(&Interface)查询对应接口 Interface 的实现,相当于 QueryInterface()
常常用于数据继承到高版本接口,或使用原有接口创建相关设备
T3继承于T,是T的高版本接口
创建 T3 对象 comp.As(&T3_comp)

2.创建 D3D12 调试层设备:CreateDebugDevice


进入正篇之前,我们还需要初始化所有的 COM 接口指针:

::CoInitialize(nullptr);	// 注意这里!DX12 的所有设备接口都是基于 COM 接口的,我们需要先全部初始化为 nullptr,否则会抛出组件引用错误!

什么是调试层


为了方便调试查找错误,从 DirectX 10 开始,设计者把渲染和调试分离成两层:用于 3D 图形渲染的叫核心层,用于调试的叫调试层



调试层对于做 3D 程序非常重要,它能在程序调试运行时输出调试信息,在下面的输出窗口提供优化建议与报错提示,帮助我们更快定位和纠正错误

发生了可能导致程序 Crash (崩溃) 的重大错误就会输出 D3D12 ERROR 错误,有时会强制移除核心层设备,防止程序继续运行导致系统崩溃:


在这里插入图片描述


发生了可能影响程序后续运行的行为就会输出 D3D12 WARNING 警告,如果不正确处理,警告也可能会变成错误:


在这里插入图片描述


输出窗口字体太难看?推荐看这篇教程: VS2022 自定义字体大小 - Sky-stars 的博客


如何创建并使用调试层


使用调试层接口 ID3D12Debug 创建设备,然后使用里面的成员方法 EnableDebugLayer() 开启调试层:

	ComPtr<ID3D12Debug> m_D3D12DebugDevice;				// D3D12 调试层设备
	UINT m_DXGICreateFactoryFlag = NULL;				// 创建 DXGI 工厂时需要用到的标志
	
	::CoInitialize(nullptr);	// 注意这里!DX12 的所有设备接口都是基于 COM 接口的,我们需要先全部初始化为 nullptr

#if defined(_DEBUG)		// 如果是 Debug 模式下编译,就执行下面的代码

	// 获取调试层设备接口
	D3D12GetDebugInterface(IID_PPV_ARGS(&m_D3D12DebugDevice));
	// 开启调试层
	m_D3D12DebugDevice->EnableDebugLayer();
	// 开启调试层后,创建 DXGI 工厂也需要 Debug Flag
	m_DXGICreateFactoryFlag = DXGI_CREATE_FACTORY_DEBUG;

#endif

如果创建调试层时抛出访问到 NULL 指针的错误,输出窗口出现“找不到 d3d12sdklayer.dll”,请回看教程第一节的“安装必要组件”:DX12 快速教程(1) —— 做窗口


3.创建 D3D12 设备:CreateDevice


认识 CPU 和 GPU




CPU 是英文“Central Processing Unit”的缩写,翻译成中文是“中央处理单元”它是电脑(计算机)的控制核心,是计算机的"大脑"。从用户按下电脑的开机键那一刻起,电脑进行的每一步操作,都离不开 CPU 的参与,它是电脑的核心部件,主要负责电脑系统的运算、控制、处理、执行,无论用户使用计算机干什么,哪怕是打一个字母或一个汉字,都必须通过 CPU 来完成。

相比 CPU,GPU(Graphics Processing Unit,GPU,图像处理单元)更多的是专注于图像计算。GPU 与 CPU 的架构不同,它不能像 CPU 那样可以执行复杂的命令。相反,它可以批量处理简单命令(例如矩阵运算,向量张量运算等等)并行运算是 GPU 最大的特点。GPU 更多的是一个优秀的助手,可以弥补 CPU 对于海量数据计算 (尤其是 3D 渲染科学计算) 的天生缺陷,从而让 CPU 专注于关键命令的执行。


举个不恰当的例子,把 CPU 比作一台摩托车,GPU 比作一辆公交车


现在路上塞车,一个胖子和一个瘦子想要搭车从A地到B地,摩托车和公交车谁快?答案是摩托车,一次就能将两人送到目的地,而且比公交车更快。


但是,现在有一群人想要搭车,摩托车还是一个好的选择吗?摩托车搭一群人可费劲多了,要往返很多次。但公交车搭他们,只需要一次,花的时间还更少。


把搭载的乘客类比于命令,CPU 和 GPU 擅长的领域是不同的,CPU 更适合串行处理各种复杂的命令,在处理日常办公、编程、数据库管理等任务时游刃有余;而 GPU 更适合并行处理大量、重复的数据运算,尤其是图形渲染深度学习等需要大规模并行计算的任务。


认识显卡


NVIDIA RTX 3080

如图所示,这就是显卡。显卡主要承担输出显示图形的任务,性能好的显卡还支持并行运算,可以用于科学计算和 AI 深度学习

显卡分为两种:集成显卡、独立显卡。



  • 集成显卡


集成显卡,简单来说,就是直接集成在主板或者 CPU 处理器里的显卡兼有 CPU 和 GPU 的功能(不过 GPU 性能很差)。它就像是电脑里的一个“兼职员工”,除了干好自己的本职工作——显示图像外,还得帮 CPU 处理器分担一些其他任务。

集显的性能一般都很低,专用显示内存(显存)一般只有 128 MB,支持的 DirectX 版本也只是达到"刚好能够兼容"的级别(DX12 最低支持到 11.0 版本,11.011.1 版本支持的特性不多)。如果你喜欢玩一些大型3D游戏,或者需要进行一些专业的图形设计、视频剪辑等工作,集成显卡可能就有点力不从心了。它可能会让你的游戏画面卡顿、延迟,或者让你的设计作品看起来不够细腻、流畅。


在这里插入图片描述


常见的集显有 Intel 芯片自带的 Intel HD 系列 和 集成于 AMD 处理器的 AMD Radeon Graphics 系列


在这里插入图片描述

Intel HD Graphics 集显

AMD Radeon Graphics 集显

  • 独立显卡


接下来再看看独立显卡。独立显卡,顾名思义,就是一块独立的显卡,它有自己的处理器(GPU)、内存(显存)和散热系统。它就像是电脑里的一个“专业团队”,专门负责处理图像显示的任务。因为独立显卡是独立的,所以它的性能通常比集成显卡要强得多,专用显存更大。它可以轻松应对那些对图形处理要求很高的游戏和应用程序,让你的游戏体验更加流畅,设计作品更加精美。

DX12 设计的目的就是在软件层面上发掘独显的最大潜能,逼近独显性能的极限。所以相对于集显,独显能支持的 DX12 版本更多(多了 12.112.0),支持的功能也随之增加。


在这里插入图片描述

常见的独显有 NVIDIA 的 NVIDIA RTX 系列(俗称N卡)和 AMD 的 AMD Radeon RX 系列(俗称A卡):


NVIDIA RTX 4080 SUPER

在这里插入图片描述

AMD Radeon RX 6950

要查看你设备的 Direct 3D 配置,可以按 WIN + R,然后输入 dxdiag,就可以查看当前你的显卡对 Direct 3D 的支持程度了。


DXGI:软件与硬件之间的桥梁


回到 DX12 部分,DX12 的渲染需要 GPU 硬件。那么软件层面的 Direct 3D 接口,是如何与硬件层面的显卡和图形驱动联系起来呢?

DirectX 是一个很庞大的家族,包含了 3D 图形渲染 (Direct 3D),2D 图形渲染 (Direct 2D),音频合成 (Xaudio2),文本字体处理 (DirectWrite),手柄管理 (XInput) 等等,这些组件依赖的底层硬件和驱动在每台电脑中各不相同,DirectX 的设计者们为了能统一管理这些硬件和驱动,写出了一套规范化的 API 接口:DXGI (DirectX Graphics Infrastructure,DirectX 图形基础结构)




DX12 需要依赖 DXGI 提供的接口,找到对应的显卡(也叫显示适配器,Display Adapter),用这个显卡来创建核心层设备并渲染:


在这里插入图片描述

右键"此电脑" -> 点击"管理" -> "设备管理器" -> "显示适配器" 可以见到电脑上的所有显卡

首先,我们需要使用 CreateDXGIFactory2() 来创建一个 IDXGIFactory5 对象,这个 DXGI 工厂是 DXGI 的核心设备:

ComPtr<IDXGIFactory5> m_DXGIFactory;				// DXGI 工厂

// 创建 DXGI 工厂
CreateDXGIFactory2(m_DXGICreateFactoryFlag, IID_PPV_ARGS(&m_DXGIFactory));

m_DXGICreateFactoryFlag用来创建 DXGI 工厂的标志,变量定义在 如何创建并使用调试层
如果开启了 DX12 的调试层,这个 Flag 必须指定为 DXGI_CREATE_FACTORY_DEBUG,否则会创建失败。
后两个参数 riidppFactory 分别表示创建工厂需要用到的 GUID值 指向对象的二级指针
这个 GUID (Globally Unique Identifier,全局唯一标识符) 相当于 COM 接口的身份证,它标识了这个接口的身份。这个 GUID 值是不可能重复的,从而保证不可能有第二个重复的接口,这个接口的身份是唯一的。
后续创建接口都会有 riidppFactory,我们可以使用 IID_PPV_ARGS(&comp) 来简化接口的创建,让编译器来帮我们进行等价替换,提高开发效率。


创建 D3D12 核心设备


在这里插入图片描述


我们需要创建 DX12 的核心设备:ID3D12Device4

  1. 通过 IDXGIFacory->EnumAdapters1 枚举合适的显卡,获得 IDXGIAdapter1 显卡 (显示适配器) 对象
  2. 用这个 IDXGIAdapter1 通过 D3D12CreateDevice 创建 ID3D12Device4 核心设备
ComPtr<IDXGIAdapter1> m_DXGIAdapter;				// 显示适配器 (显卡)
ComPtr<ID3D12Device4> m_D3D12Device;				// D3D12 核心设备

// 枚举 0 号显卡 (一般都是性能最高的显卡),创建 Adapter 显卡对象
m_DXGIFactory->EnumAdapters1(0, &m_DXGIAdapter);
// 创建 D3D12 设备
D3D12CreateDevice(m_DXGIAdapter.Get(), D3D_FEATURE_LEVEL_12_1, IID_PPV_ARGS(&m_D3D12Device));

D3D12CreateDevice 创建 D3D12 核心设备对象
第一个参数 pAdapter 经过枚举后的显卡对象
第二个参数 MinimumFeatureLevel D3D12设备要支持的最低版本,如果超过了 pAdapter 支持的最高版本,就会创建失败
返回值是 HRESULT 一个表示状态的整数值,创建成功会返回 S_OK,否则会返回其他值(具体错误可看微软文档)


问题来了,我们用的时候一般都不会在意显卡最高支持 DX12 到什么版本,上文的 D3D_FEATURE_LEVEL_12_1 要求最低版本需要支持到 12.1。问题是我并不知道 0 号显卡是啥,我电脑没有独显,支持不了这么高版本,这些都会导致设备创建失败,怎么办?

我们可以对每一个显卡,从高版本到低版本循环遍历,如果创建成功就直接返回:

ComPtr<IDXGIFactory5> m_DXGIFactory;				// DXGI 工厂
ComPtr<IDXGIAdapter1> m_DXGIAdapter;				// 显示适配器 (显卡)
ComPtr<ID3D12Device4> m_D3D12Device;				// D3D12 核心设备
bool isSucceed = false;								// 是否成功创建设备

// 创建 DXGI 工厂
CreateDXGIFactory2(m_DXGICreateFactoryFlag, IID_PPV_ARGS(&m_DXGIFactory));

// DX12 支持的所有功能版本,你的显卡最低需要支持 11.0
const D3D_FEATURE_LEVEL dx12SupportLevel[] =
{
	D3D_FEATURE_LEVEL_12_2,		// 12.2
	D3D_FEATURE_LEVEL_12_1,		// 12.1
	D3D_FEATURE_LEVEL_12_0,		// 12.0
	D3D_FEATURE_LEVEL_11_1,		// 11.1
	D3D_FEATURE_LEVEL_11_0		// 11.0
};


// 用 EnumAdapters1 先遍历电脑上的每一块显卡
// 每次调用 EnumAdapters1 找到显卡会自动创建 DXGIAdapter 接口,并返回 S_OK
// 找不到显卡会返回 ERROR_NOT_FOUND

for (UINT i = 0; m_DXGIFactory->EnumAdapters1(i, &m_DXGIAdapter) != ERROR_NOT_FOUND; i++)
{
	// 找到显卡,就创建 D3D12 设备,从高到低遍历所有功能版本,创建成功就跳出
	for (const auto& level : dx12SupportLevel)
	{
		// 创建 D3D12 核心层设备,创建成功就跳出循环
		if (SUCCEEDED(D3D12CreateDevice(m_DXGIAdapter.Get(), level, IID_PPV_ARGS(&m_D3D12Device))))
		{
			isSucceed = true;
			break;			// 跳出小循环
		}
	}
	if(isSucceed) break;	// 跳出大循环
}

// 如果找不到任何能支持 DX12 的显卡,就退出程序
if (m_D3D12Device == nullptr)
{
	MessageBox(NULL, L"找不到任何能支持 DX12 的显卡,请升级电脑上的硬件!", L"错误", MB_OK | MB_ICONERROR);
	exit(0);
}

4.创建命令三件套:CreateCommandComponents


认识命令三件套


前文我们提到,GPU 是专用于图像计算的,负责执行图形渲染命令,画东西。但我们写代码是在 CPU 上写的,C++ 代码在 CPU 端上运行,我们需要通知 GPU ,让它执行渲染命令,画东西:


在这里插入图片描述


问题来了,CPUGPU 是两个相互独立的单元如何记录渲染命令?如何将渲染命令发送给 GPU?如何通知 GPU 让它执行渲染命令?

为了解决上述问题,DX12 设计了三个东西:命令列表 (CommandList),命令分配器 (CommandAllocator),命令队列 (CommandQueue)。

  • 命令列表 (CommandList):它是命令的记录者,用来在 CPU 记录需要执行的命令,记录的命令会存储在命令分配器中。命令列表有两种状态,一种叫 Record 录制状态,用于录制命令;另一种叫 Close 关闭状态,用于关闭录制,等待提交到命令队列。命令列表有很多种类型,包括 用于3D渲染的 图形命令列表 (GraphicsCommandList)、用于复制命令的 复制命令列表 (CopyCommandList)、用于音视频解码的 视频命令列表 (VideoCommandList) 等等。
  • 命令分配器 (CommandAllocator):它是命令的存储容器,负责绑定命令列表,存储命令列表中的命令。它位于 CPU 端的共享内存 上,所以可以被 GPU 端读取、引用里面的命令。一个命令分配器可以绑定多个命令列表
  • 命令队列 (CommandQueue):它位于 GPU 端,是命令的执行者,负责读取并执行 命令分配器 中的命令。它会从头到尾一条一条地执行命令,像数据结构中的 队列。所以叫 命令队列

在这里插入图片描述

一个命令分配器可以绑定多个命令列表,但只能有一个处于 Record 录制状态

创建并使用命令三件套


我们需要利用上文的核心设备 ID3D12Device 对象,依照 “命令队列” -> “命令分配器” -> “命令列表” 的顺序来逐个创建:

ComPtr<ID3D12CommandQueue> m_CommandQueue;			// 命令队列
ComPtr<ID3D12CommandAllocator> m_CommandAllocator;	// 命令分配器
ComPtr<ID3D12GraphicsCommandList> m_CommandList;	// 命令列表

// 队列信息结构体,这里只需要填队列的类型 type 就行了
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
// D3D12_COMMAND_LIST_TYPE_DIRECT 表示将命令都直接放进队列里,不做其他处理
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
// 创建命令队列
m_D3D12Device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_CommandQueue));

// 创建命令分配器,它的作用是开辟内存,存储命令列表上的命令,注意命令类型要一致
m_D3D12Device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&m_CommandAllocator));

// 创建图形命令列表,注意命令类型要一致
m_D3D12Device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_CommandAllocator.Get(),
			nullptr, IID_PPV_ARGS(&m_CommandList));

// 命令列表创建时处于 Record 录制状态,我们需要关闭它,这样下文的 Reset 才能成功
m_CommandList->Close();

CreateCommandList 创建命令列表
第一个参数 nodeMask 要使用的显卡编号DX12 支持多显卡渲染,我们这里填 0 就行。
第二个参数 type 命令列表里面的命令类型,有 DIRECT 直接命令BUNDLE 捆绑包 等等,我们这里直接选 DIRECT 直接命令 类型,表示命令直接添加到分配器,不需要打包
第三个参数 pInitialState 初始渲染管线状态,这个渲染管线状态是下一节教程的内容,这里填 nullptr 就行。

注意这里!
m_CommandList->Close(); 命令列表创建时,初始状态是 Record 状态,Record 状态下是不能重置命令列表和命令分配器的,所以要先关闭。


5.创建渲染目标:CreateRenderTarget


资源管理


接下来我们来探讨 DX12 资源 (Resource) 的问题。 资源 就是渲染要用到的东西:缓冲 (Buffer)纹理 (Texture),资源统一用 ID3D12Resource 表示。


在这里插入图片描述


但是"资源"只是一块数据它本身只是写明了存储的格式和大小,没有写明它的作用和用法。

那如何告诉 CPU 和 GPU 这些资源应该怎么用、有什么用 呢?描述符 (View/Descriptor,两者都是"描述符"的意思) 用于标识一个 Resource 资源,是资源作用和用法的说明


在这里插入图片描述


描述符说明
RTV (Render Target View 渲染目标描述符)标识资源为渲染目标程序将要渲染到的目标对象,例如窗口纹理贴图
CBV (Constant Buffer View 常量缓冲描述符)标识资源为常量缓冲常量缓冲是一段预先分配的高速显存,用于存储 Shader (着色器) 中的常量数据
例如矩阵、向量骨骼板
SRV (Shader Resource View 着色器资源描述符)标识资源为着色器资源Shader (着色器) 是 GPU 上的可编辑程序
着色器资源会预先放在 GPU 的寄存器上,GPU 读写会非常快,例如纹理贴图
Sampler 采样器描述符标识资源为采样器,用于纹理采样与纹理过滤,决定纹理图像的清晰度
DSV (Depth Stencil View 深度模板描述符)标识资源为深度模板缓冲,表示这是一块用于深度测试模板测试的缓冲
例如物体遮挡渲染、环境光遮蔽、平面镜效果物体阴影
UAV (Unordered Access View 无序访问描述符)标识资源为无序访问资源,表示这是 CPU 可读写的 GPU 资源,用于计算着色器 (Compute Shader)动态纹理贴图
VBV (Vertex Buffer View 顶点缓冲描述符)标识资源为顶点缓冲,表示这块缓冲存储了一堆顶点
IBV (Index Buffer View 索引缓冲描述符)标识资源为索引缓冲,表示这块缓冲存储了一堆顶点索引,顶点索引决定了顶点绘制的顺序

View 视图Descriptor 描述符 其实是两个一样的东西,都用来描述一块资源,只不过前者是老版本的叫法,后者是 DX12 新增的写法。这里为了防止混淆,统一叫 Descriptor 描述符


那么这些资源和描述符该放哪里呢?DirectX 为了在资源管理上更好支持多线程渲染,设计了叫 堆 (Heap) 的东西来 存储资源和描述符

堆有两大种:一种是专门用来存储 Resource 资源资源堆 (Heap) ,另一种是专门用来存储 Descriptor 描述符描述符堆 (DescriptorHeap)


在这里插入图片描述


本节教程我们只碰 RTV (Render Target View) 渲染目标描述符

创建 RTV 描述符堆


描述符堆本质上是一个数组,里面的元素是描述符。我们要指定渲染目标为 窗口 (窗口缓冲),就要创建一个长度为 3 的 RTV 描述符堆,并将每个 RTV 描述符分别绑定到对应的窗口缓冲上


在这里插入图片描述

为什么要创建 3 个窗口缓冲呢?详情请看:游戏中的“垂直同步”与“三重缓冲”究竟是个啥? - 萧戈 的博客


首先我们先创建 RTV 描述符堆,描述符堆用 ID3D12DescriptorHeap 表示:

ComPtr<ID3D12DescriptorHeap> m_RTVHeap;					// RTV 描述符堆

// 创建 RTV 描述符堆 (Render Target View,渲染目标描述符)
D3D12_DESCRIPTOR_HEAP_DESC RTVHeapDesc = {};
RTVHeapDesc.NumDescriptors = 3;							// 渲染目标的数量
RTVHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;		// 描述符堆的类型:RTV

// 创建一个 RTV 描述符堆,创建成功后,会自动开辟三个描述符的内存
m_D3D12Device->CreateDescriptorHeap(&RTVHeapDesc, IID_PPV_ARGS(&m_RTVHeap));

创建交换链


描述符堆创建好了,现在我们要处理渲染目标,也就是如何创建并获得窗口缓冲呢?DXGI提供了一个叫 IDXGISwapChain 交换链 的东西,用于绑定窗口,并创建、获取、交换窗口缓冲


在这里插入图片描述

// 创建 DXGI 交换链,用于将窗口缓冲区和渲染目标绑定
DXGI_SWAP_CHAIN_DESC1 swapchainDesc = {};
swapchainDesc.BufferCount = 3;								// 缓冲区数量
swapchainDesc.Width = WindowWidth;							// 缓冲区 (窗口) 宽度
swapchainDesc.Height = WindowHeight;						// 缓冲区 (窗口) 高度
swapchainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;			// 缓冲区格式,指定缓冲区每个像素的大小
swapchainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;	// 交换链类型,有 FILP 和 BITBLT 两种类型
swapchainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;// 缓冲区的用途,这里表示把缓冲区用作渲染目标的输出
swapchainDesc.SampleDesc.Count = 1;							// 缓冲区像素采样次数

// 临时低版本交换链接口,用于创建高版本交换链,因为下文的 CreateSwapChainForHwnd 不能直接用于创建高版本接口
ComPtr<IDXGISwapChain1> _temp_swapchain;

// 创建交换链,将窗口与渲染目标绑定
// 注意:交换链需要绑定到命令队列来刷新,所以第一个参数要填命令队列,不填会创建失败!
m_DXGIFactory->CreateSwapChainForHwnd(m_CommandQueue.Get(), m_hwnd,
			&swapchainDesc, nullptr, nullptr, &_temp_swapchain);

ComPtr<IDXGISwapChain3> m_DXGISwapChain;					// DXGI 高版本交换链

// 通过 As 方法,将低版本接口的信息传递给高版本接口
_temp_swapchain.As(&m_DXGISwapChain);

CreateSwapChainForHwnd 创建交换链,并将窗口绑定到交换链上
第一个参数 pDevice 要关联的设备,对于 DX12 而言,每次渲染完成后命令队列都要发送"交换缓冲"的指令,告诉交换链交换窗口缓冲,让图像呈现到窗口上,所以这里必须要填命令队列 CommandQueue,否则会创建失败!
第二个参数 hWnd 要绑定的窗口句柄,交换链创建成功后,会自动创建几个与绑定窗口大小一致的窗口缓冲
第三个参数 pDesc 交换链信息结构体
第四个参数 pFullScreenDesc 全屏交换链信息结构体,游戏一般都会有 “全屏模式” 和 “窗口模式” 两种显示模式,游戏全屏就要用到全屏交换链,我们暂时不需要游戏全屏,所以填 nullptr
第五个参数 pRestrictToOutput 输出目标限制结构体,我们这里不管它,直接填 nullptr
第六个参数 ppSwapChain 要输出到的 DXGISwapChain1 的二级指针,交换链创建完后,会输出到此二级指针上

注意!我们需要调用 IDXGISwapChain3GetCurrentBufferIndex 方法来获取当前正在渲染的后台缓冲区CreateSwapChainForHwnd 只能创建 IDXGISwapChain1 接口的对象,我们需要调用 As() 方法查询接口,使用低版本接口的数据创建高版本接口。


通过交换链创建渲染目标资源,并创建 RTV 描述符


最后一步,就是将 RTV 描述符和窗口缓冲逐一绑定了:


在这里插入图片描述


// 创建完交换链后,我们还需要令 RTV 描述符 指向 渲染目标
// 因为 ID3D12Resource 本质上只是一块数据,它本身没有对数据用法的说明
// 我们要让程序知道这块数据是一个渲染目标,就得创建并使用 RTV 描述符

ComPtr<ID3D12Resource> m_RenderTarget[3];			// 渲染目标数组,每一副渲染目标对应一个窗口缓冲区
D3D12_CPU_DESCRIPTOR_HANDLE RTVHandle;				// RTV 描述符句柄
UINT RTVDescriptorSize = 0;							// RTV 描述符的大小

// 获取 RTV 堆指向首描述符的句柄
RTVHandle = m_RTVHeap->GetCPUDescriptorHandleForHeapStart();
// 获取 RTV 描述符的大小
RTVDescriptorSize = m_D3D12Device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

for (UINT i = 0; i < 3; i++)
{
	// 从交换链中获取第 i 个窗口缓冲,创建第 i 个 RenderTarget 渲染目标
	m_DXGISwapChain->GetBuffer(i, IID_PPV_ARGS(&m_RenderTarget[i]));

	// 创建 RTV 描述符,将渲染目标绑定到描述符上
	m_D3D12Device->CreateRenderTargetView(m_RenderTarget[i].Get(), nullptr, RTVHandle);

	// 偏移到下一个 RTV 句柄
	RTVHandle.ptr += RTVDescriptorSize;
}

6.创建围栏和资源屏障:CreateFenceAndBarrier


GPU 与 CPU 的同步


在此之前,我们先要了解两个名词:同步 (synchronous) 异步 (asynchronous)这两个名词对现代 3D 图形 API 相当重要


在这里插入图片描述

原视频出处:程序中的同步和异步到底是什么?- 码农的荒岛求生

DX12 完全是基于异步渲染的,也就是说,CPUGPU 发送完渲染指令后立即返回,然后 CPUGPU 分别在两个相互独立的子任务上运行,这也是 DX12 相比之前版本的最明显的不同:

在这里插入图片描述


异步渲染 本质上是 多线程渲染,都是为了最终目标 解放 CPU 和 GPU 的生产力,提高渲染效率 而生的!


为什么我们要在异步渲染中引入同步机制呢?

这就不得不提命令分配器了,DX12 有好多像命令分配器这种放在共享内存上的东西是既可以被 CPU 访问,也可以被 GPU 访问的。但是 CPUGPU 各自的访问速度不同,有可能会出现 CPUGPU 同时访问造成资源冲突,或者是 CPU 错序访问了 (比 GPU 快好几帧),导致跳帧、闪屏、或者画面撕裂


在这里插入图片描述

在这里插入图片描述


情况1:GPU 和 CPU 同时访问同一资源导致资源冲突


在这里插入图片描述

情况2:CPU 错序访问(正常情况下最多快一帧),导致画面下半部分渲染比上半部分快,出现了严重的画面撕裂

如何解决同步问题呢?这就要 围栏 (Fence) 资源屏障 (ResourceBarrier) 发挥作用了。


围栏


资源屏障


7.渲染:Render


多线程渲染优化:MsgWaitForMultipleObjects()


第二节全代码


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/943100.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《计算机组成及汇编语言原理》阅读笔记:p116-p120

《计算机组成及汇编语言原理》学习第 7 天&#xff0c;p116-p120 总结&#xff0c;总计 5 页。 一、技术总结 1.CPU优化 (1)increase overall performance number 例如&#xff1a;16位电脑提升到32位电脑。 (2)multiprocessing One way to make computers more useful i…

【蓝桥杯每日一题】12.18

&#x1f3dd;️专栏&#xff1a; 【蓝桥杯备篇】 &#x1f305;主页&#xff1a; f狐o狸x 从今天开始&#xff0c;笨狐狸&#xff0c;啊呸&#xff0c;本狐狸要开始漫长的蓝桥杯备战啦&#xff0c;将会长期更新每日一题这个专栏&#xff0c;直到蓝桥杯结束&#xff0c;各位一起…

Python 写的 智慧记 进销存 辅助 程序 导入导出 excel 可打印

图样&#xff1a; 就可以导入了 上代码 import tkinter as tk from tkinter import ttk import sqlite3 from datetime import datetime from tkinter import messagebox, filedialog import pandas as pd import reclass OrderSystem:def __init__(self, root):self.root r…

【机器学习案列】车牌自动识别系统:基于YOLO11的高效实现

&#x1f9d1; 博主简介&#xff1a;曾任某智慧城市类企业算法总监&#xff0c;目前在美国市场的物流公司从事高级算法工程师一职&#xff0c;深耕人工智能领域&#xff0c;精通python数据挖掘、可视化、机器学习等&#xff0c;发表过AI相关的专利并多次在AI类比赛中获奖。CSDN…

SpringBoot(二)—— yaml配置文件

接上篇&#xff0c;我们对SpringBoot有了基本的了解&#xff0c;接下来探究配置文件。 目录 二、配置文件 1. SpringBoot热部署 2. 配置文件 2.1 配置文件的作用 2.2 YAML 配置文件 2.3 YAML 与 XML 比较 3. YAML语法 3.1 键值对 3.2 值的写法 3.3 对象/Map&#x…

NFV架构

通信&#xff08;CT&#xff09;的NFV技术是借鉴了IT行业的云计算概念&#xff0c;实际大规模应用在4G时代。 区别是增加了以下几点 1、NFVI是openstack的电信增强版本&#xff0c;除了nova cinder nuetru等增加了电信专用组件。 2、设计增加了mano&#xff0c;包括了VIM、NFVO…

关于Edge浏览器的设置

这里记录几条个人比较习惯的使用浏览器方式的设置&#xff0c;主要是edge浏览器 1. 黑背景色 修改整个浏览器的背景色为黑色&#xff0c;而不是主题&#xff0c;只有边框颜色改变地址栏输入edge://flags/#enable-force-dark&#xff0c;将Default 改为 Enabled&#xff1b;如…

Elasticsearch:什么是查询语言?

查询语言定义 查询语言包括数据库查询语言 (database query language - DQL)&#xff0c;是一种用于查询和从数据库检索信息的专用计算机语言。它充当用户和数据库之间的接口&#xff0c;使用户能够管理来自数据库管理系统 (database management system - DBMS) 的数据。 最广…

Android使用PorterDuffXfermode模式PorterDuff.Mode.SRC_OUT橡皮擦实现马赛克效果,Kotlin(3)

Android使用PorterDuffXfermode模式PorterDuff.Mode.SRC_OUT橡皮擦实现马赛克效果&#xff0c;Kotlin&#xff08;3&#xff09; import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Canvas impor…

《信管通低代码信息管理系统开发平台》Linux环境安装说明

1 简介 信管通低代码信息管理系统应用平台提供多环境软件产品开发服务&#xff0c;包括单机、局域网和互联网。我们专注于适用国产硬件和操作系统应用软件开发应用。为事业单位和企业提供行业软件定制开发&#xff0c;满足其独特需求。无论是简单的应用还是复杂的系统&#xff…

jetson Orin nx + yolov8 TensorRT 加速量化 环境配置

参考【Jetson】Jetson Orin NX纯系统配置环境-CSDN博客 一 系统环境配置&#xff1a; 1.更换源&#xff1a; sudo vi /etc/apt/sources.list.d/nvidia-l4t-apt-source.list2.更新源&#xff1a; sudo apt upgradesudo apt updatesudo apt dist-upgrade sudo apt-get updat…

Burp炮台实现(动态ip发包)

基本步骤 1.使用 zmap 爬取大量代理ip 2.使用py1脚本初步筛选可用ip 3.利用py2脚本再次筛选对目标网站可用ip&#xff08;不带payload安全检测&#xff09; 4.配置 burp 插件并加载收集到的代理池 5.加载payload&#xff0c;开始爆破 Zmap kali安装 sudo apt update apt …

springboot495基于java的物资综合管理系统的设计与实现(论文+源码)_kaic

摘 要 如今社会上各行各业&#xff0c;都喜欢用自己行业的专属软件工作&#xff0c;互联网发展到这个时候&#xff0c;人们已经发现离不开了互联网。新技术的产生&#xff0c;往往能解决一些老技术的弊端问题。因为传统物资综合管理系统信息管理难度大&#xff0c;容错率低&am…

STM32-笔记16-定时器中断点灯

一、实验目的 使用定时器 2 进行中断点灯&#xff0c;500ms LED 灯翻转一次。 二&#xff0c;定时器溢出时间计算 Tout&#xff1a;定时器溢出时间 Ft&#xff1a;定时器的时钟源频率 ARR&#xff1a;自动重装载寄存器的值 PSC&#xff1a;预分频器寄存器的值 例如&#xff0c…

【MySQL】 SQL优化讲解

一、优化前的思考 在定位到慢查询后&#xff0c;面试官常问如何优化或分析慢查询的SQL语句。若存在聚合查询、多表查询&#xff0c;可尝试优化SQL语句结构&#xff0c;如多表查询可新增临时表&#xff1b;若表数据量过大&#xff0c;可添加索引&#xff0c;但添加索引后仍慢则…

C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码

一、介绍 栈和队列是限定插入和删除只能在表的“端点”进行的线性表&#xff0c;是线性表的子集&#xff0c;是插入和删除位置受限的线性表。 &#xff08;操作受限的线性表&#xff09; 二、栈 1&#xff09;概念&#xff1a; 栈(stack)是一个特殊的线性表&#xff0c;是限…

【HarmonyOS应用开发——ArkTS语言】购物商城的实现【合集】

目录 &#x1f60b;环境配置&#xff1a;华为HarmonyOS开发者 &#x1f4fa;演示效果&#xff1a; &#x1f4d6;实验步骤及方法&#xff1a; 1. 在src/main/ets文件中创建components文件夹并在其中创建Home.ets和HomeProduct.ets文件。​ 2. 在Home.ets文件中定义 Home 组…

连锁餐饮行业数据可视化分析方案

引言 随着连锁餐饮行业的迅速发展&#xff0c;市场竞争日益激烈。企业需要更加精准地把握运营状况、消费者需求和市场趋势&#xff0c;以制定科学合理的决策&#xff0c;提升竞争力和盈利能力。可视化数据分析可以帮助连锁餐饮企业整合多源数据&#xff0c;通过直观、动态的可…

学习threejs,THREE.RingGeometry 二维平面圆环几何体

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️THREE.RingGeometry 圆环几…

计算机网络实验室建设方案

一、计算机网络实验室拓扑结构 计算机网络综合实验室解决方案&#xff0c;是面向高校网络相关专业开展教学实训的综合实训基地解决方案。教学实训系统采用 B&#xff0f;S架构&#xff0c;通过公有云教学实训平台在线学习模式&#xff0c;轻松实现网络系统建设与运维技术的教学…