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 Flagm_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.2D3D_FEATURE_LEVEL_12_1,		// 12.1D3D_FEATURE_LEVEL_12_0,		// 12.0D3D_FEATURE_LEVEL_11_1,		// 11.1D3D_FEATURE_LEVEL_11_0		// 11.0
};// 用 EnumAdapters1 先遍历电脑上的每一块显卡
// 每次调用 EnumAdapters1 找到显卡会自动创建 DXGIAdapter 接口,并返回 S_OK
// 找不到显卡会返回 ERROR_NOT_FOUNDfor (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 快好几帧),导致跳帧、闪屏、或者画面撕裂


在这里插入图片描述


围栏同步


资源屏障


7.渲染:Render


多线程渲染优化:MsgWaitForMultipleObjects()

第二节全代码

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【MySQL】7.0 入门学习(七)——MySQL基本指令:帮助、清除输入、查询等

1.0 help &#xff1f; 帮助指令&#xff0c;查询某个指令的解释、用法、说明等。详情参考博文&#xff1a; 【数据库】6.0 MySQL入门学习&#xff08;六&#xff09;——MySQL启动与停止、官方手册、文档查询 https://www.cnblogs.com/xiaofu007/p/10301005.html 2.0 在cmd命…

鸿蒙开发:了解帧动画

前言 所谓帧动画&#xff0c;就是类似播放电影一样&#xff0c;一帧一帧的进行播放&#xff0c;相对于属性动画&#xff0c;其每一帧&#xff0c;我们都可以进行设置相关的属性值&#xff0c;并且具有暂停播放&#xff0c;继续播放的优点&#xff0c;而且还具备事件的实时响应&…

发际线不断后移,生发液排行榜第一名,让绒毛碎发爆出来

脱发真的很困扰人&#xff0c;不疼不痒&#xff0c;但看着就老了几岁&#xff0c;形象也大打折扣&#xff0c;男性脱发就更直观了&#xff01;同学严重时&#xff0c;后脑勺几块拳头大的脱发区域&#xff0c;还有的呈“m”型脱发&#xff0c;脑门看着都大了。脱发除了遗传因素以…

Y3地图制作1:水果缤纷乐、密室逃脱

文章目录 一、水果缤纷乐1.1 游戏设计1.1.1 项目解析1.1.2 项目优化1.1.3 功能拆分 1.2 场景制作1.2.1 场景需求1.2.2 创建主镜头、绘制草稿&#xff0c;构思文案和情景1.2.3 构建场景地图1.2.4 光源与氛围设置 1.3 游戏初始化1.3.1 物编、UI预设置1.3.2 游戏初始化1.3.2 玩家初…

48页PPT|2024智慧仓储解决方案解读

本文概述了智慧物流仓储建设方案的行业洞察、业务蓝图及建设方案。首先&#xff0c;从政策层面分析了2012年至2020年间国家发布的促进仓储业、物流业转型升级的政策&#xff0c;这些政策强调了自动化、标准化、信息化水平的提升&#xff0c;以及智能化立体仓库的建设&#xff0…

【Linux】ChatGLM-4-9B模型之All Tools

一、摘要 最近在研究GLM4模型&#xff0c;发现自带的All Tools比较感兴趣&#xff0c;它具有完整工具调用能力的对话模式&#xff0c;原生支持网页浏览、代码执行、图表生成、图片生成&#xff0c;并支持自定义工具。它能够满足大模型私有化部署的个性定制&#xff0c;因此记录…

Vue零基础必学教程(16) 计算属性

往期内容&#xff1a; Vue零基础必学教程&#xff08;5&#xff09;挂载 Vue零基础必学教程&#xff08;6&#xff09;基本选项 Vue零基础必学教程&#xff08;7&#xff09;模板 Vue零基础必学教程&#xff08;8&#xff09;模板语法 Vue零基础必学教程&#xff08;9&…

14:30面试,14:08就出来了,面试问的有点变态呀。。。

从小厂出来&#xff0c;没想到在另一家公司又寄了。 到这家公司开始上班&#xff0c;加班是每天必不可少的&#xff0c;看在钱给的比较多的份上&#xff0c;就不太计较了。没想到一纸通知&#xff0c;所有人不准加班&#xff0c;加班费不仅没有了&#xff0c;薪资还要降40%,这…

【Leetcode】1705. 吃苹果的最大数目

文章目录 题目思路代码复杂度分析时间复杂度空间复杂度 结果总结 题目 题目链接&#x1f517; 有一棵特殊的苹果树&#xff0c;一连 n n n 天&#xff0c;每天都可以长出若干个苹果。在第 i i i 天&#xff0c;树上会长出 a p p l e s [ i ] apples[i] apples[i] 个苹果&a…

kimi搜索AI多线程批量生成txt原创文章软件-不需要账号及key

kimi搜索AI多线程批量生成txt原创文章软件介绍&#xff1a; 软件可以设置三种模型写文章&#xff1a;kimi&#xff1a;默认AI模型&#xff0c;kimi-search&#xff1a;联网检索模型 &#xff0c;kimi-research&#xff1a;探索版搜索聚合模型 1、可以设置写联网搜索文章&#…

游戏引擎学习第58天

发现一个vscode Log 断点的用法 回顾 我们正在继续推进工作&#xff0c;之前做了一些测试和清理工作&#xff0c;但还有一件事没有完成&#xff0c;因此我们还没有完全回到功能平衡的状态。昨天我们已经为实体做了空间划分&#xff0c;所以接下来的目标是继续完成这部分工作&a…

day14-16系统服务管理和ntp和防火墙

一、自有服务概述 服务是一些特定的进程&#xff0c;自有服务就是系统开机后就自动运行的一些进程&#xff0c;一旦客户发出请求&#xff0c;这些进程就自动为他们提供服务&#xff0c;windows系统中&#xff0c;把这些自动运行的进程&#xff0c;称为"服务" window…

Idea导入Springboot项目,无法正确加载yml文件,且不为绿色图标的解决办法

一、出现问题的环境 将项目复制新的环境后&#xff0c;.yml 文件不能显示为绿色&#xff0c;导致无法配置数据库。 二、解决办法。 在网上找了多种办法&#xff0c;并不适用&#xff0c;发现resources的显示也有问题&#xff0c;右击resources->Mark->Directory as -&g…

以太网通信--读取物理层PHY芯片的状态

PHY芯片通过MDIO接口进行读写&#xff0c;框图如下所示&#xff1a; 原理很简单&#xff0c;就是按照时序将PHY芯片的指定寄存器信息读出或者写入。 MDC时钟需要输出到PHY芯片&#xff0c;一般不低于80MHz。 MDIO是双向接口&#xff0c;FPGA读出状态信息时为输入&#xff0c;FP…

Doris Tablet 损坏如何应对?能恢复数据吗?

开门见山&#xff0c;能不能修&#xff1f; Doris 的 Tablet 损坏了&#xff0c;到底能不能修呢&#xff1f;数据会不会丢&#xff1f; 这玩意还真不好说&#xff1f; 哎&#xff0c;怎么又不好说了呢&#xff1f; 这个主要是因为下面的原因&#xff1a; Doris 数据的高可…

【Linux】查询磁盘空间被谁占用了

查询磁盘空间被谁占用了 先说下常见的几种原因&#xff1a; 1、删除的文件未释放空间 2、日志或过期文件未及时清理 3、inode导致 4、隐藏文件夹或者目录 6、磁盘碎片 最后一种单独介绍。 环境&#xff1a;情况是根分区&#xff08;/&#xff09;的总容量为44GB&#xf…

Scala课堂小结

(一)数组&#xff1a; 1.不可变数组 2创建数组

GitPuk安装配置指南

GitPuk是一款开源免费的代码管理工具&#xff0c;上篇文章已经介绍了Gitpuk的功能与优势&#xff0c;这篇文章将为大家讲解如何快速安装和配置GitPuk&#xff0c;助力你快速的启动GitPuk管理代码 1. 安装 支持 Windows、Mac、Linux、docker 等操作系统。 1.1 Windows安装 下载…

大恒相机开发(2)—Python软触发调用采集图像

大恒相机开发&#xff08;2&#xff09;—Python软触发调用采集图像 完整代码详细解读和功能说明扩展学习 这段代码是一个Python程序&#xff0c;用于从大恒相机采集图像&#xff0c;通过软件触发来采集图像。 完整代码 咱们直接上python的完整代码&#xff1a; # version:…

本科阶段最后一次竞赛Vlog——2024年智能车大赛智慧医疗组准备全过程——12使用YOLO-Bin

本科阶段最后一次竞赛Vlog——2024年智能车大赛智慧医疗组准备全过程——12使用YOLO-Bin ​ 根据前面内容&#xff0c;所有的子任务已经基本结束&#xff0c;接下来就是调用转化的bin模型进行最后的逻辑控制了 1 .YOLO的bin使用 ​ 对于yolo其实有个简单的办法&#xff0c;也…