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 快好几帧),导致跳帧、闪屏、或者画面撕裂


在这里插入图片描述

在这里插入图片描述


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


在这里插入图片描述

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

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


围栏


资源屏障


7.渲染:Render


多线程渲染优化:MsgWaitForMultipleObjects()


第二节全代码


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

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

相关文章

【合作原创】使用Termux搭建可以使用的生产力环境(八)

前言 在上一篇【合作原创】使用Termux搭建可以使用的生产力环境&#xff08;七&#xff09;-CSDN博客中我们讲到了安装百度网盘、VS Code还有java&#xff0c;这篇我打算讲一下最后的编程&#xff0c;还有输入法相关问题解决。众所周知我的本职工作是Java程序猿&#xff0c;因…

VLMs之Gemma 2:PaliGemma 2的简介、安装和使用方法、案例应用之详细攻略

VLMs之Gemma 2&#xff1a;PaliGemma 2的简介、安装和使用方法、案例应用之详细攻略 导读&#xff1a;2024年12月4日&#xff0c;PaliGemma 2是一个基于Gemma 2系列语言模型的开源视觉语言模型 (VLM) 家族。PaliGemma 2 通过提供一个规模化、多功能且开源的VLM家族&#xff0c;…

24.12.26 SpringMVCDay01

SpringMVC 也被称为SpringWeb Spring提供的Web框架,是在Servlet基础上,构建的框架 SpringMVC看成是一个特殊的Servlet,由Spring来编写的Servlet 搭建 引入依赖 <dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc<…

国产 HighGo 数据库企业版安装与配置指南

国产 HighGo 数据库企业版安装与配置指南 1. 下载安装包 访问 HighGo 官方网站&#xff08;https://www.highgo.com/&#xff09;&#xff0c;选择并下载企业版安装包。 2. 上传安装包到服务器 将下载的安装包上传至服务器&#xff0c;并执行以下命令&#xff1a; [rootmas…

Java程序设计,使用属性的选项库,轻松实现商品检索的复杂查询(上)

一、背景 本文我们以某商城的商品检索为例,说一说如何使用属性及选项,实现复杂的逻辑表达式的查询。 先贴图,总结出业务需求。 可以通过一系列属性及选项的组合,过滤出用户想要的商品列表。 1、属性 上文中的品牌、分类、屏幕尺寸、CPU型号、运行内存、机身内存、屏幕材…

机器学习(二)-简单线性回归

文章目录 1. 简单线性回归理论2. python通过简单线性回归预测房价2.1 预测数据2.2导入标准库2.3 导入数据2.4 划分数据集2.5 导入线性回归模块2.6 对测试集进行预测2.7 计算均方误差 J2.8 计算参数 w0、w12.9 可视化训练集拟合结果2.10 可视化测试集拟合结果2.11 保存模型2.12 …

WHAT KAN I SAY?Kolmogorov-Arnold Network (KAN)网络结构介绍及实战(文末送书)

一、KAN网络介绍 1.1 Kolmogorov-Arnold Network (KAN)网络结构的提出 2024年4月&#xff0c;来自MIT、加州理工学院、东北大学等团队的研究&#xff0c;引爆了一整个科技圈&#xff1a;Yes We KAN&#xff01; 这种创新方法挑战了多层感知器(Multilayer Perceptron&#xff…

YOLO11改进-模块-引入星型运算Star Blocks

当前网络设计中&#xff0c;“星型运算”&#xff08;逐元素乘法&#xff09;的应用原理未被充分探究&#xff0c;潜力有待挖掘。为解决此问题&#xff0c;我们引入 Star Blocks&#xff0c;其内部由 DW - Conv、BN、ReLU 等模块经星型运算连接&#xff0c;各模块有特定参数。同…

3.银河麒麟V10 离线安装Nginx

1. 下载nginx离线安装包 前往官网下载离线压缩包 2. 下载3个依赖 openssl依赖&#xff0c;前往 官网下载 pcre2依赖下载&#xff0c;前往Git下载 zlib依赖下载&#xff0c;前往Git下载 下载完成后完整的包如下&#xff1a; 如果网速下载不到请使用网盘下载 通过网盘分享的文件…

【理解机器学习中的过拟合与欠拟合】

在机器学习中&#xff0c;模型的表现很大程度上取决于我们如何平衡“过拟合”和“欠拟合”。本文通过理论介绍和代码演示&#xff0c;详细解析过拟合与欠拟合现象&#xff0c;并提出应对策略。主要内容如下&#xff1a; 什么是过拟合和欠拟合&#xff1f; 如何防止过拟合和欠拟…

【婚庆摄影小程序设计与实现】

摘 要 社会发展日新月异&#xff0c;用计算机应用实现数据管理功能已经算是很完善的了&#xff0c;但是随着移动互联网的到来&#xff0c;处理信息不再受制于地理位置的限制&#xff0c;处理信息及时高效&#xff0c;备受人们的喜爱。所以各大互联网厂商都瞄准移动互联网这个潮…

12.26 学习卷积神经网路(CNN)

完全是基于下面这个博客来进行学习的&#xff0c;感谢&#xff01; ​​【深度学习基础】详解Pytorch搭建CNN卷积神经网络LeNet-5实现手写数字识别_pytorch cnn-CSDN博客 基于深度神经网络DNN实现的手写数字识别&#xff0c;将灰度图像转换后的二维数组展平到一维&#xff0c;…

Unity URP多光源支持,多光源阴影投射,多光源阴影接收(优化版)

目录 前言&#xff1a; 一、属性 二、SubShader 三、ForwardLitPass 定义Tags 声明变体 声明变量 定义结构体 顶点Shader 片元Shader 四、全代码 四、添加官方的LitShader代码 五、全代码 六、效果图 七、结语 前言&#xff1a; 哈喽啊&#xff0c;我又来啦。这…

如何使用React,透传各类组件能力/属性?

在23年的时候&#xff0c;我主要使用的框架还是Vue&#xff0c;当时写了一篇“如何二次封装一个Vue3组件库&#xff1f;”的文章&#xff0c;里面涉及了一些如何使用Vue透传组件能力的方法。在我24年接触React之后&#xff0c;我发现这种扩展组件能力的方式有一个专门的术语&am…

109.【C语言】数据结构之求二叉树的高度

目录 1.知识回顾&#xff1a;高度&#xff08;也称深度&#xff09; 2.分析 设计代码框架 返回左右子树高度较大的那个的写法一:if语句 返回左右子树高度较大的那个的写法二:三目操作符 3.代码 4.反思 问题 出问题的代码 改进后的代码 执行结果 1.知识回顾&#xf…

分析排名靠前的一些自媒体平台,如何运用这些平台?

众所周知&#xff0c;现在做网站越来越难了&#xff0c;主要的原因还是因为流量红利时代过去了。并且搜索引擎都在给自己的平台做闭环改造。搜索引擎的流量扶持太低了。如百度投资知乎&#xff0c;给知乎带来很多流量扶持&#xff0c;也为自身内容不足做一个填补。 而我们站长…

2024大模型在软件开发中的具体应用有哪些?(附实践资料合集)

大模型在软件开发中的具体应用非常广泛&#xff0c;以下是一些主要的应用领域&#xff1a; 自动化代码生成与智能编程助手&#xff1a; AI大模型能够根据开发者的自然语言描述自动生成代码&#xff0c;减少手动编写代码的工作量。例如&#xff0c;GitHub Copilot工具就是利用AI…

Ubuntu网络配置(桥接模式, nat模式, host主机模式)

windows上安装了vmware虚拟机&#xff0c; vmware虚拟机上运行着ubuntu系统。windows与虚拟机可以通过三种方式进行通信。分别是桥接模式&#xff1b;nat模式&#xff1b;host模式 一、桥接模式 所谓桥接模式&#xff0c;也就是虚拟机与宿主机处于同一个网段&#xff0c; 宿主机…

3.系统学习-熵与决策树

熵与决策树 前言1.从数学开始信息量(Information Content / Shannon information)信息熵(Information Entropy)条件熵信息增益 决策树认识2.基于信息增益的ID3决策树3.C4.5决策树算法C4.5决策树算法的介绍决策树C4.5算法的不足与思考 4. CART 树基尼指数&#xff08;基尼不纯度…

SpringBoot + HttpSession 自定义生成sessionId

SpringBoot HttpSession 自定义生成sessionId 业务场景实现方案 业务场景 最近在做用户登录过程中&#xff0c;由于默认ID是通过UUID创建的&#xff0c;缺乏足够的安全性&#xff0c;决定要自定义生成 sessionId。 实现方案 正常的获取session方法如下&#xff1a; HttpSe…