DirectX12_Windows_GameDevelop_3:Direct3D的初始化

news2024/12/22 20:29:49

引言

  • 查看龙书时发现,第四章介绍预备知识的代码不太利于学习。因为它不像是LearnOpenGL那样从头开始一步一步教你敲代码,导致你没有一种整体感。
  • 如果你把它当作某一块的代码进行学习,你跟着敲会发现,总有几个变量是没有定义的。这是因为书上的代码都是把框架里的某一部分粘过来,缺少上文中对变量的定义,也根本不利于学习。
  • 学习图形学API就是为了使用GPU进行图形运算,说白了我们学习的DirectX就是一个工具,因此熟练掌握工具、能使用工具生产作品才是最重要的。因此不妨从4.3开始学习,学到那块不会再查了解前面的预备知识就会好很多
  • 由于现在学习的代码都是框架中的一部分,因此我的学习方法是
     1. 看书学习理论,并查看书中代码。
     2. 在VS中Ctrl+F搜索书中代码,对照搜索到的代码框架跟着敲。如果遇到哪个变量是未定义的,直接Ctrl+鼠标左键(点击未定义的变量),跳转到这个变量的定义后,复制定义到我的代码里。
     3. 如果遇到书中未详细介绍的内容,直接在MSDN查询相关信息。
  • 下图展示了我从4.3章节开始,根据龙书源代码框架,学习书本示例代码的过程:
    在这里插入图片描述
  • 本文包含的核心英文单词和释义如下:
英文单词中文含义
Create创建
Fence围栏
Factory工厂
Controller控制器
Failed失败
FEATURE特性
Level级别
Adapter适配器
Enable启用
Enum枚举
Descriptor描述符
Handle句柄
Increment增量
HEAP
TYPE LESS无类型
Support支持
Quality质量
Sample Count采样数
NORM规范
MULTI
Flags标志
command命令
Queue队列
DESC描述
Allocator分配器
DIRECT直接的
Address住址
Refresh Rate刷新率
numerator分子
Denominator分母
Scanline Ordering扫描线排序
MODE模式
SCANLINE扫描线
UNSPECIFIED未指明
Scaling缩放
RENDER TARGET OUTPUT渲染目标输出

一、初始化Direct3D

  • 初始化Direct3D的9个步骤如下:
步骤序号步骤内容
1D3D12CreateDevice 函数创建 ID3D12Device 接口实例
2创建一个 ID3D12Fence 对象,并查询描述符的大小
3检测用户设备对 4X MSAA 质量级别的支持
4依次创建命令队列、命令列表分配器和主命令列表
5描述并创建交换链
6创建应用程序所需的描述符堆
7调整后台缓冲区的大小,并为它创建渲染目标视图
8创建深度/模板缓冲区及与之关联的深度/模板视图
9设置视口和裁剪矩形
  • 书中的代码没有上下文和完整框架,不妨跟着我一起学
  • 先看书中4.3章对每一个步骤的描述再查看我提供的完整代码二者对照岂不美哉!
  • 本文第一章分为九个小节,对应初始化Direct3d的九个步骤,每个小节第一部分提供了完整可运行代码,第二部分提供代码涉及的知识点。

(1)创建设备

1.1 完整示例

  • 初始Direct3D,必须先创建Direct3D设备(ID3D12Device)
  • Direce3D设备代表着一个显示适配器显示适配器一般指3D图形硬件即显卡。但是显示适配器也可以用软件来代替,通过模拟硬件显卡的计算处理过程,软件也可以作为适配器(如WARP适配器)
  • Direct3D设备既可以检测系统环境对功能的支持情况,又能创建所有其他的Direct3D接口对象(如资源、视图和命令列表)。
  • 创建Direct3D设备使用函数D3D12CreateDevice
  • 话不多说,直接上代码:
#include<windows.h>     // windows API编程所需
#include<wrl.h>         // 提供了ComPtr类,它是COM对象的智能指针,使我们无需手动Release
#include<d3d12.h>       // Direct3D12头文件,ID3D12开头类型始于此
#include<dxgi1_4.h>     // DirectX图形基础设施头文件,IDXGI开头类型始于此
#include<string>        // 提供wsring类,在Windows平台上应该使用wstring和wchar_t

using namespace Microsoft::WRL; // 方便使用Microsoft::WRL::ComPtr类       

// AnsiToWString函数(将字符串映射到 UTF-16 (宽字符) 字符串)
inline std::wstring AnsiToWString(const std::string& str)
{
    WCHAR buffer[512];
    MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, buffer, 512);
    return std::wstring(buffer);
}

// 定义异常类
class DxException
{
public:
    DxException() = default;
    // 显示:异常函数的返回值、函数名、代码所处文件名,所处代码行数
    DxException(HRESULT hr, const std::wstring& functionName, const std::wstring& filename, int lineNumber);

    std::wstring ToString()const;

    HRESULT ErrorCode = S_OK;
    std::wstring FunctionName;
    std::wstring Filename;
    int LineNumber = -1;
};

// 如果发生异常,抛出一个异常实例
#ifndef ThrowIfFailed
#define ThrowIfFailed(x)                                              \
{                                                                     \
    HRESULT hr__ = (x);                                               \
    std::wstring wfn = AnsiToWString(__FILE__);                       \
    if(FAILED(hr__)) { throw DxException(hr__, L#x, wfn, __LINE__); } \
}
#endif

// IDXGIFactory: DXGI中最关键的接口之一,可以枚举显示适配器
ComPtr<IDXGIFactory4> mdxgiFactory;

// ID3D12Device: 代表一个显示适配器(显卡)
ComPtr<ID3D12Device> md3dDevice;

// 初始化Direct3D
bool InitDirect3D()
{
#if defined(DEBUG) || defined(_DEBUG) 
    // 如果在Debug模式,启用D3D12的调试层,
    // 启用调试后,D3D会在错误发生时向VC++的输出窗口发送调试信息
    {
        ComPtr<ID3D12Debug> debugController;
        ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
        debugController->EnableDebugLayer();
    }
#endif

    // 创建可用于生成其他 DXGI 对象的 DXGI 1.0 Factory
    ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));

    // 尝试创建一个D3D硬件适配器
    // D3D12CreateDevice参数为:(适配器指针,应用程序所需最低功能级别,
    // 所建ID3D12Device接口的COM ID,返回所创建的D3D12设备
    HRESULT hardwareResult = D3D12CreateDevice(
        nullptr,
        D3D_FEATURE_LEVEL_12_0,
        IID_PPV_ARGS(&md3dDevice));
    // 适配器指针传入空代表使用主显示适配器,
    // 可以分析出需要COM指针即riid的地方,通过使用IID_PPV_ARGS即可

    // 如果创建失败,应用程序回退至WARP软件适配器
    if (FAILED(hardwareResult))
    {
        ComPtr<IDXGIAdapter> pWarpAdapter;
        ThrowIfFailed(
            mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));

        // 不同windows版本的WARP最高支持的功能级别也不同
        // 再次创建D3D设备时,仅适配器指针参数不同,传入WARP适配器指针
        ThrowIfFailed(D3D12CreateDevice(
            pWarpAdapter.Get(),
            D3D_FEATURE_LEVEL_12_0,
            IID_PPV_ARGS(&md3dDevice)
        ));
    }
}

int WINAPI WinMain(
    _In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPSTR lpCmdLine,
    _In_ int nShowCmd)
{
	// 在主函数中调用Direct3D的初始化函数
    InitDirect3D();
}
  • 代码如上,大致过一遍有个记忆就行。如果对其中哪些地方有疑问或者好奇,可以在MSDN上搜索相关类型或函数查看相关信息。

1.2 相关知识

  • 组件对象模型(Component Object Model,COM)是一种令DirectX不受编程语言束缚,并且使之向后兼容的技术。我们通常将COM对象视为一种接口,但由于我们的编程目的,我们可以把它当作一个C++类来使用。当我们使用C++编写DirectX程序时,COM帮我们隐藏了大量的底层细节。我们只需要知道,要获取指向某COM接口的指针,需借助特定函数或另一COM接口的方法
  • Windows运行时库(Windows Runtime Library,WRL)为COM对象提供了Microsoft::WRL::ComPtr类,它位于<wrl.h>头文件中,它是COM对象的智能指针。当一个COM对象超出作用域范围时,智能指针会调用相应COM对象的Release方法,省去了我们手动调用的麻烦。
  • 书中的COM接口、对象和指针,让我晕头转向,因为我并没有接触过COM技术。分析一下,上文示例代码中有:
// IDXGIFactory: DXGI中最关键的接口之一,可以枚举显示适配器
ComPtr<IDXGIFactory4> mdxgiFactory;

// ID3D12Device: 代表一个显示适配器(显卡)
ComPtr<ID3D12Device> md3dDevice;
  • 无论是IDXGIFactory4还是ID3D12Device,这两种Direct3D中的对象都是通过COM指针来表示的。因此我们现在可以知道的是:
     使用C++开发Direct3D应用程序时,每个Direct3D对象都是一种COM对象或者说COM接口,我们要记录对象就要使用COM指针,我们要获取这种对象就要使用特定函数或另一COM对象的方法
  • 不必迷茫,只需要知道Direct3D对象的记录和获取方法如下即可:
// Direct3D对象的记录方法
ComPtr<Direct3D对象类型> 对象名。

// Direct3d对象的获取方法
对象名 = 特定函数();
对象名 = 其他Direct3D对象.函数();
  • ComPtr类的三个常用函数如下:
函数名描述
Get返回指向此底层COM对象的指针
GetAddressOf返回指向此底层COM对象的指针的地址
Rest将ComPtr实例设置为nullptr释放与之相关的所有引用
  • 注意:
    Rest函数的作用和将ComPtr对象赋值为nullptr的效果相同
    COM对象都以大写字母 “I” 开头,例如表示设备的COM对象为ID3D12Device。
     COM对象、COM接口、COM实例,都是一个意思。
  • 读到这里就清楚了吧,为了使DirectX不受语言束缚,DirectX中的对象都被定义为一种以 “I” 开头的COM对象。而使用COM对象最方便的方法就是使用COM指针即ComPtr类,因此我们使用ComPtr类记录每个DirectX对象即可
  • 如果读者对Windows平台为什么要使用wstring等问题感兴趣,可以查找MSDN或其他搜索。如果对IID_PPV_ARGS宏感兴趣,可以查看书籍4.2.1即95页中间对其描述:这个宏会展开为两项,第一项根据__uuidof获取了COM对象的ID(全局唯一标识符,GUID)IID_PPV_ARGS辅助函数本质是把指针强制转换为void类型
#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)

(2)创建围栏并获取描述符的大小

2.1 完整示例

  • CPU和GPU的同步需要使用到围栏,第一步中我们创建好设备,第二步就可以创建围栏了。
  • 另外,如果使用描述符进行工作,需要获取描述符的大小描述符在不同GPU平台上的大小不同,因此我们需要将获取的描述符大小信息缓存起来,方便需要时直接引用
  • 话不多说,直接上代码:
	// ID3D12Fence: 表示围栏
    ComPtr<ID3D12Fence> mFence;
    // 创建围栏
    ThrowIfFailed(md3dDevice->CreateFence(
        0, 
        D3D12_FENCE_FLAG_NONE, 
        IID_PPV_ARGS(&mFence)
        ));


    // RTV描述符大小,RTV描述符: 渲染目标视图资源
    UINT mRtvDescriptorSize = 0;
    // DSV描述符大小,DSV描述符: 深度/模板视图资源
    UINT mDsvDescriptorSize = 0;
    // CbvSrvUav描述符大小,CBV描述符: 常量缓冲区视图资源...
    UINT mCbvSrvUavDescriptorSize = 0;

    // 获取描述符大小
    mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
    mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
    mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
  • 上述代码直接放在第一步 InitDirect3D 函数末尾即可,需要注意的是mFence、mRtvDescriptorSize和mDsvDescriptorSize等变量应该定义为全局变量,上文为易于理解才写在函数内部。
  • 可以看到新出现的四个变量都是通过 md3dDevice 对象获取到的,印证了:
     1. “Direct3D12 设备对象能创建所有其他的Direct3D接口对象”。
     2. “COM对象需要使用特定方法或通过其他COM对象的方法获取”。

2.2 相关知识

  • 资源与描述符,详见4.1.6节。在渲染过程中,GPU需要对资源进行读和写。但是GPU和资源并不是直接绑定的,而是通过一个媒婆 “描述符” ,这个中间人进行绑定的。
  • 描述符是一种对送往GPU的资源进行描述的轻量级结构,它是一个中间层。当GPU需要对资源进行读或写时,GPU就会问媒婆:“资源在哪里?我应该按照哪种数据格式进行读写?”。
  • 可见描述符的作用有两点:
     1. 指定资源数据
     2. 为GPU解释资源信息
  • 创建资源时可用无类型格式,如DXGI_FORMAT_R8G8B8A8_TYPELESS类型,我们知道它是4个分量组成,每个分量占8个位,但是我们不知道每个分量的8个位应该解析为整数、浮点数还是无符号整数?
  • 如果某个资源在创建时采用了无类型格式,那么在为它创建描述符时必须指明其具体类型
  • 注意:视图和描述符的含义是等价的。
  • 每个描述符都有一种具体类型,此类型指明了资源的具体作用。常用的描述符如下:
描述符含义
CBV常量缓冲区视图
SRV着色器资源视图
UAV无序访问视图
sampler采样器资源描述符
RTV渲染目标视图资源
DSV深度/模板视图资源
  • 描述符堆中存有一系列描述符,本质上是存放用户程序中某特定类型描述符的一块内存我们需要为每一种类型的描述符创建出单独的描述符,当然同一种描述符也可以创建多个描述符堆
  • 我们可以用不同的描述符来描述同一个资源,达到以不同的数据格式或内容部分去读写资源的目的。
  • 由于创建描述符的过程中需要执行一些类型的检测和验证工作,索引最好不要在运行时才创建描述符,创建描述符的最佳时机为初始化期间
  • 当然有时确实需要使用无类型资源所带来的灵活性,此时在一定限度内,可以考虑在运行时创建描述符。

(3)检测对4X MSAA 质量级别的支持

3.1 完整示例

  • 本书中我们要使用4X MSAA,之所以选择4X,是因为借助此采样数量就可以获取开销不高但性能非凡的效果。而使用4X MSAA之前,我们要先检查设备是否支持4X MSAA 质量级别的图像
  • 话不多说,直接上代码:
#include<windows.h>     // windows API编程所需
#include<wrl.h>         // 提供了ComPtr类,它是COM对象的智能指针,使我们无需手动Release
#include<d3d12.h>       // Direct3D12头文件,ID3D12开头类型始于此
#include<dxgi1_4.h>     // DirectX图形基础设施头文件,IDXGI开头类型始于此
#include<string>        // 提供wsring类,在Windows平台上应该使用wstring和wchar_t
#include<assert.h>

using namespace Microsoft::WRL; // 方便使用Microsoft::WRL::ComPtr类       

// AnsiToWString函数(将字符串映射到 UTF-16 (宽字符) 字符串)
inline std::wstring AnsiToWString(const std::string& str)
{
    WCHAR buffer[512];
    MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, buffer, 512);
    return std::wstring(buffer);
}

// 定义异常类
class DxException
{
public:
    DxException() = default;
    // 显示:异常函数的返回值、函数名、代码所处文件名,所处代码行数
    DxException(HRESULT hr, const std::wstring& functionName, const std::wstring& filename, int lineNumber);

    std::wstring ToString()const;

    HRESULT ErrorCode = S_OK;
    std::wstring FunctionName;
    std::wstring Filename;
    int LineNumber = -1;
};

// 如果发生异常,抛出一个异常实例
#ifndef ThrowIfFailed
#define ThrowIfFailed(x)                                              \
{                                                                     \
    HRESULT hr__ = (x);                                               \
    std::wstring wfn = AnsiToWString(__FILE__);                       \
    if(FAILED(hr__)) { throw DxException(hr__, L#x, wfn, __LINE__); } \
}
#endif

// IDXGIFactory: DXGI中最关键的接口之一,可以枚举显示适配器
ComPtr<IDXGIFactory4> mdxgiFactory;

// ID3D12Device: 代表一个显示适配器(显卡)
ComPtr<ID3D12Device> md3dDevice;

// 初始化Direct3D
bool InitDirect3D()
{
#if defined(DEBUG) || defined(_DEBUG) 
    // 如果在Debug模式,启用D3D12的调试层,
    // 启用调试后,D3D会在错误发生时向VC++的输出窗口发送调试信息
    {
        ComPtr<ID3D12Debug> debugController;
        ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
        debugController->EnableDebugLayer();
    }
#endif
    
    // 创建可用于生成其他 DXGI 对象的 DXGI 1.0 Factory
    ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));

	// 第一步:
    // 创建一个D3D设备
    // D3D12CreateDevice参数为:(适配器指针,应用程序所需最低功能级别,
    // 所建ID3D12Device接口的COM ID,返回所创建的D3D12设备
    HRESULT hardwareResult = D3D12CreateDevice(
        nullptr,
        D3D_FEATURE_LEVEL_12_0,
        IID_PPV_ARGS(&md3dDevice));
    // 适配器指针传入空代表使用主显示适配器,
    // 可以分析出需要COM指针即riid的地方,通过使用IID_PPV_ARGS即可

    // 如果创建失败,应用程序回退至WARP软件适配器
    if (FAILED(hardwareResult))
    {
        ComPtr<IDXGIAdapter> pWarpAdapter;
        ThrowIfFailed(
            mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));

        // 不同windows版本的WARP最高支持的功能级别也不同
        // 再次创建D3D设备时,仅适配器指针参数不同,传入WARP适配器指针
        ThrowIfFailed(D3D12CreateDevice(
            pWarpAdapter.Get(),
            D3D_FEATURE_LEVEL_12_0,
            IID_PPV_ARGS(&md3dDevice)
        ));
    }

	// 第二步:
    // ID3D12Fence: 表示围栏
    ComPtr<ID3D12Fence> mFence;
    // 创建围栏
    ThrowIfFailed(md3dDevice->CreateFence(
        0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence)));

    // RTV描述符大小,RTV描述符: 渲染目标视图资源
    UINT mRtvDescriptorSize = 0;
    // DSV描述符大小,DSV描述符: 深度/模板视图资源
    UINT mDsvDescriptorSize = 0;
    // CbvSrvUav描述符大小,CBV描述符: 常量缓冲区视图资源...
    UINT mCbvSrvUavDescriptorSize = 0;

    // 获取描述符大小
    mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
    mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
    mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

	// 第三步:
    // DXGI_FORMAT: 资源数据的格式,一种枚举类型 
    DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
    
    // 检测对4X MASS质量级别的支持
    D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
    msQualityLevels.Format = mBackBufferFormat;                         // 纹理格式
    msQualityLevels.SampleCount = 4;                                    // 采样次数
    msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE; // 默认选项
    msQualityLevels.NumQualityLevels = 0;                               // 质量级别

    // 使用ID3D12Device::CheckFeatureSupport函数,查询我们设置的这种图像质量的级别
    ThrowIfFailed(md3dDevice->CheckFeatureSupport(
        D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
        &msQualityLevels,
        sizeof(msQualityLevels)
    ));

    // 使用无符号整数存储:我们查询的图像质量所对应的质量级别(不为零则说明支持)
    UINT m4xMsaaQuality = 0;
    // 我们查询的采样数为4,因此返回的质量级别,即为达到4X MSAA的质量级别
    m4xMsaaQuality = msQualityLevels.NumQualityLevels;

    // 只要返回的质量级别不为零,则表示支持此种图像质量,在该处即支持4X MSAA
    assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");
}

int WINAPI WinMain(
    _In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPSTR lpCmdLine,
    _In_ int nShowCmd)
{
    InitDirect3D();
}
  • 在此给出完整代码,若读者对第二步的完整代码不清楚,也可以在此查看。
  • 可以看到我们先定义了一种资源类型的格式,那就是要查询的纹理格式。你可以把代码复制到你的项目里运行试试,当然你要记得使用Windows API开发项目,如果只会创建控制台项目,你可以看看我之前文章中的第六步。
  • 详细的讲解都在代码里了,我就不多说了。我的显卡不是很好,但运行后依旧不会报错,你可以试试运行,如果报错就说明你的显卡不支持这种4X MSAA 的图像质量级别了。
  • 你可以将纹理资源的格式改为:DXGI_FORMAT_R16G16B16A16_UNORM,这样的纹理显然更精细点,然后再运行试试。
  • 多次运行,你发现都不会报错,你觉得要么是程序错了要么是你的显卡太棒了!那不妨试试修改采样数量,即msQualityLevels.SampleCount,将它改为8、16和32呢?

3.2 相关知识

  • 多重采样技术的原理位于4.1.7小节,在书籍85页,我就不做过多阐述了。需要注意的是ID3DDevice->CheckFeatureSupport方法的第二个参数兼具输入和输出的属性。
  • 如果不希望使用多重采样,可以将采用数量设置为1,并将质量级别设为0
  • 在创建交换链缓冲区和深度缓冲区时都需要填写DXGI_SAMPLE_DESC结构体。当创建后台缓冲区和深度缓冲区时,多重采样的有关设置必须相同
  • 功能支持的检测位于4.1.11小节。函数ID3D12Device::CheckFeatureSupport可以对许多功能进行检测,其第一个参数的类型是一个枚举类:D3D12_FEATURE,第二个参数是枚举类对应的数据结构指针,第三个参数是传入的数据结构变量所占字节数
  • 使用ID3D12Device::CheckFeatureSupport检测系统支持Direct3D最高版本的代码为:
#include<windows.h>     // windows API编程所需
#include<wrl.h>         // 提供了ComPtr类,它是COM对象的智能指针,使我们无需手动Release
#include<d3d12.h>       // Direct3D12头文件,ID3D12开头类型始于此
#include<dxgi1_4.h>     // DirectX图形基础设施头文件,IDXGI开头类型始于此
#include<string>        // 提供wsring类,在Windows平台上应该使用wstring和wchar_t
#include<assert.h>

using namespace Microsoft::WRL; // 方便使用Microsoft::WRL::ComPtr类       

// AnsiToWString函数(将字符串映射到 UTF-16 (宽字符) 字符串)
inline std::wstring AnsiToWString(const std::string& str)
{
    WCHAR buffer[512];
    MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, buffer, 512);
    return std::wstring(buffer);
}

// 定义异常类
class DxException
{
public:
    DxException() = default;
    // 显示:异常函数的返回值、函数名、代码所处文件名,所处代码行数
    DxException(HRESULT hr, const std::wstring& functionName, const std::wstring& filename, int lineNumber);

    std::wstring ToString()const;

    HRESULT ErrorCode = S_OK;
    std::wstring FunctionName;
    std::wstring Filename;
    int LineNumber = -1;
};

// 如果发生异常,抛出一个异常实例
#ifndef ThrowIfFailed
#define ThrowIfFailed(x)                                              \
{                                                                     \
    HRESULT hr__ = (x);                                               \
    std::wstring wfn = AnsiToWString(__FILE__);                       \
    if(FAILED(hr__)) { throw DxException(hr__, L#x, wfn, __LINE__); } \
}
#endif

// IDXGIFactory: DXGI中最关键的接口之一,可以枚举显示适配器
ComPtr<IDXGIFactory4> mdxgiFactory;

// ID3D12Device: 代表一个显示适配器(显卡)
ComPtr<ID3D12Device> md3dDevice;

// 初始化Direct3D
bool InitDirect3D()
{
#if defined(DEBUG) || defined(_DEBUG) 
    // 如果在Debug模式,启用D3D12的调试层,
    // 启用调试后,D3D会在错误发生时向VC++的输出窗口发送调试信息
    {
        ComPtr<ID3D12Debug> debugController;
        ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
        debugController->EnableDebugLayer();
    }
#endif
    
    // 创建可用于生成其他 DXGI 对象的 DXGI 1.0 Factory
    ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));

    // 创建一个D3D设备
    // D3D12CreateDevice参数为:(适配器指针,应用程序所需最低功能级别,
    // 所建ID3D12Device接口的COM ID,返回所创建的D3D12设备
    HRESULT hardwareResult = D3D12CreateDevice(
        nullptr,
        D3D_FEATURE_LEVEL_12_0,
        IID_PPV_ARGS(&md3dDevice));
    // 适配器指针传入空代表使用主显示适配器,
    // 可以分析出需要COM指针即riid的地方,通过使用IID_PPV_ARGS即可

    // 如果创建失败,应用程序回退至WARP软件适配器
    if (FAILED(hardwareResult))
    {
        ComPtr<IDXGIAdapter> pWarpAdapter;
        ThrowIfFailed(
            mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));

        // 不同windows版本的WARP最高支持的功能级别也不同
        // 再次创建D3D设备时,仅适配器指针参数不同,传入WARP适配器指针
        ThrowIfFailed(D3D12CreateDevice(
            pWarpAdapter.Get(),
            D3D_FEATURE_LEVEL_12_0,
            IID_PPV_ARGS(&md3dDevice)
        ));
    }

    // ID3D12Fence: 表示围栏
    ComPtr<ID3D12Fence> mFence;
    // 创建围栏
    ThrowIfFailed(md3dDevice->CreateFence(
        0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence)));


    // RTV描述符大小,RTV描述符: 渲染目标视图资源
    UINT mRtvDescriptorSize = 0;
    // DSV描述符大小,DSV描述符: 深度/模板视图资源
    UINT mDsvDescriptorSize = 0;
    // CbvSrvUav描述符大小,CBV描述符: 常量缓冲区视图资源...
    UINT mCbvSrvUavDescriptorSize = 0;

    // 获取描述符大小
    mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
    mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
    mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

    // DXGI_FORMAT: 资源数据的格式,一种枚举类型 
    DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
    
    // 检测对4X MASS质量级别的支持
    D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
    msQualityLevels.Format = mBackBufferFormat;                         // 纹理格式
    msQualityLevels.SampleCount = 4;                                    // 采样次数
    msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE; // 默认选项
    msQualityLevels.NumQualityLevels = 0;                               // 质量级别

    // 使用ID3D12Device::CheckFeatureSupport函数,查询我们设置的这种图像质量的级别
    ThrowIfFailed(md3dDevice->CheckFeatureSupport(
        D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
        &msQualityLevels,
        sizeof(msQualityLevels)
    ));

    // 使用无符号整数存储:我们查询的图像质量所对应的质量级别(不为零则说明支持)
    UINT m4xMsaaQuality = 0;
    // 我们查询的采样数为4,因此返回的质量级别,即为达到4X MSAA的质量级别
    m4xMsaaQuality = msQualityLevels.NumQualityLevels;

    // 只要返回的质量级别不为零,则表示支持此种图像质量,在该处即支持4X MSAA
    assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");

    // 检测支持的Direct3D最高版本

    // 需要检测的版本
    D3D_FEATURE_LEVEL feature[3] =
    {
        D3D_FEATURE_LEVEL_12_0,
        D3D_FEATURE_LEVEL_12_1,
        D3D_FEATURE_LEVEL_12_2,
    };

    // 枚举类型对应的数据结构
    D3D12_FEATURE_DATA_FEATURE_LEVELS featureLevelsInfo;
    featureLevelsInfo.NumFeatureLevels = 3;                 // 检测三种版本
    featureLevelsInfo.pFeatureLevelsRequested = feature;    // 检测的版本

    // 执行检测函数
    md3dDevice->CheckFeatureSupport(
        D3D12_FEATURE_FEATURE_LEVELS,
        &featureLevelsInfo,
        sizeof(featureLevelsInfo)
    );
    
    // 返回支持的最高版本(可以打个断点查看一下)
    featureLevelsInfo.MaxSupportedFeatureLevel;
}

int WINAPI WinMain(
    _In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPSTR lpCmdLine,
    _In_ int nShowCmd)
{
    InitDirect3D();
}
  • 我最高支持的版本是:D3D_FEATURE_LEVEL_12_1。真是太捞了,等工作有钱了去买一台intel i9 + 4090,那时候估计就不贵了,释放性能!

(4)创建命令队列和列表

4.1 完整示例

  • 我们需要使用命令队列来存储GPU需要执行的命令,用命令列表来存储CPU想要提交的命令。过程非常简单,让我们直接来做吧!
  • 代码如下:
#include<windows.h>     // windows API编程所需
#include<wrl.h>         // 提供了ComPtr类,它是COM对象的智能指针,使我们无需手动Release
#include<d3d12.h>       // Direct3D12头文件,ID3D12开头类型始于此
#include<dxgi1_4.h>     // DirectX图形基础设施头文件,IDXGI开头类型始于此
#include<string>        // 提供wsring类,在Windows平台上应该使用wstring和wchar_t
#include<assert.h>

using namespace Microsoft::WRL; // 方便使用Microsoft::WRL::ComPtr类       

// AnsiToWString函数(将字符串映射到 UTF-16 (宽字符) 字符串)
inline std::wstring AnsiToWString(const std::string& str)
{
    WCHAR buffer[512];
    MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, buffer, 512);
    return std::wstring(buffer);
}

// 定义异常类
class DxException
{
public:
    DxException() = default;
    // 显示:异常函数的返回值、函数名、代码所处文件名,所处代码行数
    DxException(HRESULT hr, const std::wstring& functionName, const std::wstring& filename, int lineNumber);

    std::wstring ToString()const;

    HRESULT ErrorCode = S_OK;
    std::wstring FunctionName;
    std::wstring Filename;
    int LineNumber = -1;
};

// 如果发生异常,抛出一个异常实例
#ifndef ThrowIfFailed
#define ThrowIfFailed(x)                                              \
{                                                                     \
    HRESULT hr__ = (x);                                               \
    std::wstring wfn = AnsiToWString(__FILE__);                       \
    if(FAILED(hr__)) { throw DxException(hr__, L#x, wfn, __LINE__); } \
}
#endif

// IDXGIFactory: DXGI中最关键的接口之一,可以枚举显示适配器
ComPtr<IDXGIFactory4> mdxgiFactory;

// ID3D12Device: 代表一个显示适配器(显卡)
ComPtr<ID3D12Device> md3dDevice;

// ID3D12Fence: 表示围栏
ComPtr<ID3D12Fence> mFence;


// RTV描述符大小,RTV描述符: 渲染目标视图资源
UINT mRtvDescriptorSize = 0;
// DSV描述符大小,DSV描述符: 深度/模板视图资源
UINT mDsvDescriptorSize = 0;
// CbvSrvUav描述符大小,CBV描述符: 常量缓冲区视图资源...
UINT mCbvSrvUavDescriptorSize = 0;

// DXGI_FORMAT: 资源数据的格式,一种枚举类型 
DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;

// 使用无符号整数存储:我们查询的图像质量所对应的质量级别(不为零则说明支持)
UINT m4xMsaaQuality = 0;

// 第四步: 创建命令队列和命令列表
void CreateCommandObjects()
{
    // 声明一个命令队列
    ComPtr<ID3D12CommandQueue> mCommandQueue;
    
    // 调用ID3D12Device::CreateCommandQueue方法创建队列前,
    // 需要填写D3D12_COMMAND_QUEUE_DESC结构体来描述队列
    D3D12_COMMAND_QUEUE_DESC queueDesc = {};

    // 定义队列中的命令类型
    queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
    // 设置其他选项为空
    queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;

    // 调用ID3D12Device::CreateCommandQueue方法创建命令队列
    ThrowIfFailed(md3dDevice->CreateCommandQueue(
        &queueDesc, IID_PPV_ARGS(&mCommandQueue)
    ));

    // 声明ID3D12CommandAllocator命令内存管理对象
    ComPtr<ID3D12CommandAllocator> mCommandAllocator;

    // 记录在命令列表内的命令,实际上是存储在与之关联的命令分配器上
    // 通过ID3D12Device创建命令分配器ID3D12CommandAllocator
    ThrowIfFailed(md3dDevice->CreateCommandAllocator(
        D3D12_COMMAND_LIST_TYPE_DIRECT,                 // 与此命令分配器关联的命令列表类型
        IID_PPV_ARGS(mCommandAllocator.GetAddressOf())   
    ));

    // 通过ID3D12Device创建命令列表ID3D12GraphicsCommandList
    ComPtr<ID3D12GraphicsCommandList> mCommandList;
    ThrowIfFailed(md3dDevice->CreateCommandList(
        0,                                       // 指定与所建命令列表相关联的物理GPU,对于仅有一个GPU的系统而言设为0
        D3D12_COMMAND_LIST_TYPE_DIRECT,          // 命令列表的类型
        mCommandAllocator.Get(),                 // 关联的命令分配器
        nullptr,                                 // 打包和初始化无绘制命令时传nullptr
        IID_PPV_ARGS(mCommandList.GetAddressOf())// 输出指向所建列表的指针
    ));

    // 在关闭状态下启动
    // 这是因为当我们第一次引用命令列表时,我们会重置它,并且在调用Reset之前需要关闭它。
    mCommandList->Close();
}

// 初始化Direct3D
bool InitDirect3D()
{
#if defined(DEBUG) || defined(_DEBUG) 
    // 如果在Debug模式,启用D3D12的调试层,
    // 启用调试后,D3D会在错误发生时向VC++的输出窗口发送调试信息
    {
        ComPtr<ID3D12Debug> debugController;
        ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
        debugController->EnableDebugLayer();
    }
#endif
    
    /*
        第一步:创建Direct3D设备
    */ 

    // 创建可用于生成其他 DXGI 对象的 DXGI 1.0 Factory
    ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));

    // 创建一个D3D设备
    // D3D12CreateDevice参数为:(适配器指针,应用程序所需最低功能级别,
    // 所建ID3D12Device接口的COM ID,返回所创建的D3D12设备
    HRESULT hardwareResult = D3D12CreateDevice(
        nullptr,
        D3D_FEATURE_LEVEL_12_0,
        IID_PPV_ARGS(&md3dDevice));
    // 适配器指针传入空代表使用主显示适配器,
    // 可以分析出需要COM指针即riid的地方,通过使用IID_PPV_ARGS即可

    // 如果创建失败,应用程序回退至WARP软件适配器
    if (FAILED(hardwareResult))
    {
        ComPtr<IDXGIAdapter> pWarpAdapter;
        ThrowIfFailed(
            mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));

        // 不同windows版本的WARP最高支持的功能级别也不同
        // 再次创建D3D设备时,仅适配器指针参数不同,传入WARP适配器指针
        ThrowIfFailed(D3D12CreateDevice(
            pWarpAdapter.Get(),
            D3D_FEATURE_LEVEL_12_0,
            IID_PPV_ARGS(&md3dDevice)
        ));
    }

    /*
        第二步:创建围栏并计算描述符大小
    */

    // 创建围栏
    ThrowIfFailed(md3dDevice->CreateFence(
        0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence)));

    // 获取描述符大小
    mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
    mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
    mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

    /*
        第三步:检测系统对4X MSAA的支持
    */
    
    // 检测对4X MSAA质量级别的支持
    D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
    msQualityLevels.Format = mBackBufferFormat;                         // 纹理格式
    msQualityLevels.SampleCount = 4;                                    // 采样次数
    msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE; // 默认选项
    msQualityLevels.NumQualityLevels = 0;                               // 质量级别

    // 使用ID3D12Device::CheckFeatureSupport函数,查询我们设置的这种图像质量的级别
    ThrowIfFailed(md3dDevice->CheckFeatureSupport(
        D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
        &msQualityLevels,
        sizeof(msQualityLevels)
    ));

    // 我们查询的采样数为4,因此返回的质量级别,即为达到4X MSAA的质量级别
    m4xMsaaQuality = msQualityLevels.NumQualityLevels;
    // 只要返回的质量级别不为零,则表示支持此种图像质量,在该处即支持4X MSAA
    assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");

    CreateCommandObjects();
}

int WINAPI WinMain(
    _In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPSTR lpCmdLine,
    _In_ int nShowCmd)
{
    InitDirect3D();
}
  • 可以看到在函数CreateCommandObjects()中我们完成了命令队列和命令列表的创建,当然为了易于理解我将命令队列、命令列表和命令分配器声明为了局部变量,你应该把它们放到函数外面作为全局变量,如下所示:
// 声明一个命令队列
ComPtr<ID3D12CommandQueue> mCommandQueue;

// 声明ID3D12CommandAllocator命令内存管理对象
ComPtr<ID3D12CommandAllocator> mCommandAllocator;

// 通过ID3D12Device创建命令列表ID3D12GraphicsCommandList
ComPtr<ID3D12GraphicsCommandList> mCommandList;

void CreateCommandObjects()
{
    // 调用ID3D12Device::CreateCommandQueue方法创建队列前,
    // 需要填写D3D12_COMMAND_QUEUE_DESC结构体来描述队列
    D3D12_COMMAND_QUEUE_DESC queueDesc = {};

    // 定义队列中的命令类型
    queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
    // 设置其他选项为空
    queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;

    // 调用ID3D12Device::CreateCommandQueue方法创建命令队列
    ThrowIfFailed(md3dDevice->CreateCommandQueue(
        &queueDesc, IID_PPV_ARGS(&mCommandQueue)
    ));

    // 记录在命令列表内的命令,实际上是存储在与之关联的命令分配器上
    // 通过ID3D12Device创建命令分配器ID3D12CommandAllocator
    ThrowIfFailed(md3dDevice->CreateCommandAllocator(
        D3D12_COMMAND_LIST_TYPE_DIRECT,                 // 与此命令分配器关联的命令列表类型
        IID_PPV_ARGS(mCommandAllocator.GetAddressOf())   
    ));

    ThrowIfFailed(md3dDevice->CreateCommandList(
        0,                                       // 指定与所建命令列表相关联的物理GPU,对于仅有一个GPU的系统而言设为0
        D3D12_COMMAND_LIST_TYPE_DIRECT,          // 命令列表的类型
        mCommandAllocator.Get(),                 // 关联的命令分配器
        nullptr,                                 // 打包和初始化无绘制命令时传nullptr
        IID_PPV_ARGS(mCommandList.GetAddressOf())// 输出指向所建列表的指针
    ));

    // 在关闭状态下启动
    // 这是因为当我们第一次引用命令列表时,我们会重置它,并且在调用Reset之前需要关闭它。
    mCommandList->Close();
}
  • 现在你就不会感觉好像什么都没有得到了吧!从这里我们也可以看出初始化Direct3D工作的一大重点就是:声明一系列核心的Direct3D变量,并且在初始化时获取它们!

4.2 相关知识

  • 在进行图形学编程的时候,有两种处理器在参与工作:CPU和GPU两者并行运行,但有时也需要同步。为了获得最佳性能,理想的情况下是不进行同步,但许多时候同步是必须要进行的
  • 每个GPU都至少维护着一个命令队列(本质上是环形缓冲区,即ring buffer)。使用Direct3D API,CPU可利用命令列表将命令提交到这个队列中去
  • 一系列命令被提交至命令队列之时,它们并不会被GPU立即执行。由于GPU可能正在处理先前插入命令队列,因此新到达的命令常常会先等待再执行。
  • 如果命令队列全空,会浪费GPU大量的运算能力。如果命令队列全满,会导致CPU不可避免的等待,因此会浪费CPU大量的运算能力。对于游戏这样的高性能应用程序来说,应该充分利用硬件资源,保持CPU和GPU的同时忙碌
  • ExecuteCommandLists是一种常用的ID3D12CommandQueue接口(对象)方法利用它可将命令列表里的命令添加到命令队列中。它包含两个参数,第一个参数是UINT型变量,代表命令列表中命令的数量,第二个参数是命令列表数组的首指针。GPU会从数组中的第一个命令开始顺序执行。
  • ID3D12GraphicsCommandList接口封装了一系列图形渲染命令,它实际上继承于ID3D12CommandList接口。调用其方法即可向命令列表添加命令,但需要注意的是命令并不会立即执行,其方法仅仅是将命令加入了命令列表而已调用ExecuteCommandLists方法才会将命名真正地送入命令队列,供GPU在合适的时机处理
  • 随着内容不断深入,我们将逐步掌握D3D12GraphicsCommandList所支持的各种命令。当命令被加入命令列表后,我们必须调用D3D12GraphicsCommandList::Close()方法来结束命令的记录。在调用ExecuteCommandLists方法将命令提交给命令列表之前,一定要先将其关闭,即调用Close方法
  • 我们可以通过ID3D12Device->GetNodeCount()方法查询系统中GPU适配器节点(物理GPU)的数量
  • 很明显每个命令列表对应一个命令分配器,那么一个命令分配器是否可以对应多个命令列表呢?答案是可以的,我们可以让一个命令分配器关联多个命令列表,但必须关闭同一命令分配器的其他命令列表,即任何时候只能有一个命令列表处于打开状态。这保证了命令列表中的所有命令都会按顺序连续地添加到命令分配器内,即只有当正在使用的命令列表使用完已关闭时,其他命令列表才可以使用。
  • 注意,当创建或重置一个命令列表时,它会处于 “打开” 的状态,因此你如果为同一个命令分配器连续创建或重置两个命令列表时,就会得到报错信息
  • 在调用ExecuteCommandLists方法将命令列表中的命令提交给命令队列后,我们就可以通过D3D12GraphicsCommandList::Reset方法将命令列表恢复为刚创建时的初态。此方法功能类似于std::vector::clear方法,仅使得存储元素的数量归零,但是仍保持其当前的容量。借助此方法,我们就可以继续复用其底层内存,避免释放列表再新建的繁琐操作
  • 命令队列可能会引用命令分配器中的数据,但是重置命令列表却不会影响命令队列,这是因为相关的命令分配器仍在维护着其内存中被命令队列引用的一系列命令因此当调用ExecuteCommandLists方法后就可以重置命名列表。但是注意,在没有确定GPU执行完命令分配器中的所有命令之前,千万不要重置命令分配器!
  • 因此差不多会在每一帧重置命令列表,但命令分配器却不一定。

(5)描述并创建交换链

5.1 完整示例

#include<windows.h>     // windows API编程所需
#include<wrl.h>         // 提供了ComPtr类,它是COM对象的智能指针,使我们无需手动Release
#include<d3d12.h>       // Direct3D12头文件,ID3D12开头类型始于此
#include<dxgi1_4.h>     // DirectX图形基础设施头文件,IDXGI开头类型始于此
#include<string>        // 提供wsring类,在Windows平台上应该使用wstring和wchar_t
#include<assert.h>

using namespace Microsoft::WRL; // 方便使用Microsoft::WRL::ComPtr类       

// AnsiToWString函数(将字符串映射到 UTF-16 (宽字符) 字符串)
inline std::wstring AnsiToWString(const std::string& str)
{
    WCHAR buffer[512];
    MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, buffer, 512);
    return std::wstring(buffer);
}

// 定义异常类
class DxException
{
public:
    DxException() = default;
    // 显示:异常函数的返回值、函数名、代码所处文件名,所处代码行数
    DxException(HRESULT hr, const std::wstring& functionName, const std::wstring& filename, int lineNumber);

    std::wstring ToString()const;

    HRESULT ErrorCode = S_OK;
    std::wstring FunctionName;
    std::wstring Filename;
    int LineNumber = -1;
};

// 如果发生异常,抛出一个异常实例
#ifndef ThrowIfFailed
#define ThrowIfFailed(x)                                              \
{                                                                     \
    HRESULT hr__ = (x);                                               \
    std::wstring wfn = AnsiToWString(__FILE__);                       \
    if(FAILED(hr__)) { throw DxException(hr__, L#x, wfn, __LINE__); } \
}
#endif

// IDXGIFactory: DXGI中最关键的接口之一,可以枚举显示适配器
ComPtr<IDXGIFactory4> mdxgiFactory;

// ID3D12Device: 代表一个显示适配器(显卡)
ComPtr<ID3D12Device> md3dDevice;

// ID3D12Fence: 表示围栏
ComPtr<ID3D12Fence> mFence;

// RTV描述符大小,RTV描述符: 渲染目标视图资源
UINT mRtvDescriptorSize = 0;
// DSV描述符大小,DSV描述符: 深度/模板视图资源
UINT mDsvDescriptorSize = 0;
// CbvSrvUav描述符大小,CBV描述符: 常量缓冲区视图资源...
UINT mCbvSrvUavDescriptorSize = 0;

// DXGI_FORMAT: 资源数据的格式,一种枚举类型 
DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;

// 使用无符号整数存储:我们查询的图像质量所对应的质量级别(不为零则说明支持)
UINT m4xMsaaQuality = 0;

// 声明一个命令队列
ComPtr<ID3D12CommandQueue> mCommandQueue;

// 声明ID3D12CommandAllocator命令内存管理对象
ComPtr<ID3D12CommandAllocator> mCommandAllocator;

// 通过ID3D12Device创建命令列表ID3D12GraphicsCommandList
ComPtr<ID3D12GraphicsCommandList> mCommandList;

// 定义缓冲区分辨率的高度和宽度
int mClientWidth = 800;
int mClientHeight = 600;

// 定义交换链中的缓冲区数目,这里使用双缓冲
const int SwapChainBufferCount = 2;

// 存储窗口句柄
HWND mhMainWnd = nullptr;

// 声明交换链接口(对象)IDXGISwapChain
ComPtr<IDXGISwapChain> mSwapChain;

/*
    第五步:描述并创建交换链
*/
void CreateSwapChain()
{
    // 释放我们将要重新创建的上一个交换链。
    mSwapChain.Reset();

    // 使用DXGI_SWAP_CHAIN_DESC结构体描述交换链的特性
    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 = 1;                         // 设置采样数目为1
    sd.SampleDesc.Quality = 0;                       // 设置质量级别为0
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;// 设置后台缓冲区为渲染目标输出
    sd.BufferCount = SwapChainBufferCount;           // 设置交换链中的缓冲区数量
    sd.OutputWindow = mhMainWnd;                     // 设置渲染窗口的句柄
    sd.Windowed = true;                              // 设置为true程序将在窗口模式运行,否则为全屏
    sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;   
    sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
    // 可选标志,设为此项表示程序切换为全屏时,选择最适合于当前应用程序窗口尺寸的显示模式
    // 若无该标志,则采用当前桌面的显示模式。

    // 使用IDXGIFactory::CreateSwapChain方法,创建交换链接口
    ThrowIfFailed(mdxgiFactory->CreateSwapChain(
        mCommandQueue.Get(),
        &sd,
        mSwapChain.GetAddressOf()
    ));
}

/*
    第四步:创建命令队列和命令列表
*/
void CreateCommandObjects()
{
    // 调用ID3D12Device::CreateCommandQueue方法创建队列前,
    // 需要填写D3D12_COMMAND_QUEUE_DESC结构体来描述队列
    D3D12_COMMAND_QUEUE_DESC queueDesc = {};

    // 定义队列中的命令类型
    queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
    // 设置其他选项为空
    queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;

    // 调用ID3D12Device::CreateCommandQueue方法创建命令队列
    ThrowIfFailed(md3dDevice->CreateCommandQueue(
        &queueDesc, IID_PPV_ARGS(&mCommandQueue)
    ));

    // 记录在命令列表内的命令,实际上是存储在与之关联的命令分配器上
    // 通过ID3D12Device创建命令分配器ID3D12CommandAllocator
    ThrowIfFailed(md3dDevice->CreateCommandAllocator(
        D3D12_COMMAND_LIST_TYPE_DIRECT,                 // 与此命令分配器关联的命令列表类型
        IID_PPV_ARGS(mCommandAllocator.GetAddressOf())   
    ));

    ThrowIfFailed(md3dDevice->CreateCommandList(
        0,                                       // 指定与所建命令列表相关联的物理GPU,对于仅有一个GPU的系统而言设为0
        D3D12_COMMAND_LIST_TYPE_DIRECT,          // 命令列表的类型
        mCommandAllocator.Get(),                 // 关联的命令分配器
        nullptr,                                 // 打包和初始化无绘制命令时传nullptr
        IID_PPV_ARGS(mCommandList.GetAddressOf())// 输出指向所建列表的指针
    ));

    // 在关闭状态下启动
    // 这是因为当我们第一次引用命令列表时,我们会重置它,并且在调用Reset之前需要关闭它。
    mCommandList->Close();
    md3dDevice->GetNodeCount();
}

// 初始化Direct3D
bool InitDirect3D()
{
#if defined(DEBUG) || defined(_DEBUG) 
    // 如果在Debug模式,启用D3D12的调试层,
    // 启用调试后,D3D会在错误发生时向VC++的输出窗口发送调试信息
    {
        ComPtr<ID3D12Debug> debugController;
        ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
        debugController->EnableDebugLayer();
    }
#endif
    
    /*
        第一步:创建Direct3D设备
    */ 

    // 创建可用于生成其他 DXGI 对象的 DXGI 1.0 Factory
    ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));

    // 创建一个D3D设备
    // D3D12CreateDevice参数为:(适配器指针,应用程序所需最低功能级别,
    // 所建ID3D12Device接口的COM ID,返回所创建的D3D12设备
    HRESULT hardwareResult = D3D12CreateDevice(
        nullptr,
        D3D_FEATURE_LEVEL_12_0,
        IID_PPV_ARGS(&md3dDevice));
    // 适配器指针传入空代表使用主显示适配器,
    // 可以分析出需要COM指针即riid的地方,通过使用IID_PPV_ARGS即可

    // 如果创建失败,应用程序回退至WARP软件适配器
    if (FAILED(hardwareResult))
    {
        ComPtr<IDXGIAdapter> pWarpAdapter;
        ThrowIfFailed(
            mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));

        // 不同windows版本的WARP最高支持的功能级别也不同
        // 再次创建D3D设备时,仅适配器指针参数不同,传入WARP适配器指针
        ThrowIfFailed(D3D12CreateDevice(
            pWarpAdapter.Get(),
            D3D_FEATURE_LEVEL_12_0,
            IID_PPV_ARGS(&md3dDevice)
        ));
    }

    /*
        第二步:创建围栏并计算描述符大小
    */

    // 创建围栏
    ThrowIfFailed(md3dDevice->CreateFence(
        0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence)));

    // 获取描述符大小
    mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
    mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
    mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

    /*
        第三步:检测系统对4X MSAA的支持
    */
    
    // 检测对4X MSAA质量级别的支持
    D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
    msQualityLevels.Format = mBackBufferFormat;                         // 纹理格式
    msQualityLevels.SampleCount = 4;                                    // 采样次数
    msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE; // 默认选项
    msQualityLevels.NumQualityLevels = 0;                               // 质量级别

    // 使用ID3D12Device::CheckFeatureSupport函数,查询我们设置的这种图像质量的级别
    ThrowIfFailed(md3dDevice->CheckFeatureSupport(
        D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
        &msQualityLevels,
        sizeof(msQualityLevels)
    ));

    // 我们查询的采样数为4,因此返回的质量级别,即为达到4X MSAA的质量级别
    m4xMsaaQuality = msQualityLevels.NumQualityLevels;
    // 只要返回的质量级别不为零,则表示支持此种图像质量,在该处即支持4X MSAA
    assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");


    /*
        第四步:创建命令队列和命令列表
    */
    CreateCommandObjects();

    /*
        第五步:描述并创建交换链
    */
    CreateSwapChain();
}

#include<Windows.h>

#define WINDOW_WIDTH 800
#define WINDOW_HEIGHT 600
#define WINDOW_TITLE L"GameEngine"

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(
    _In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPSTR lpCmdLine,
    _In_ int nShowCmd)
{
    SetProcessDPIAware();
    int cx = GetSystemMetrics(SM_CXSCREEN);
    int cy = GetSystemMetrics(SM_CYMAXTRACK);

    WNDCLASSEX wndClass = { 0 };
    wndClass.cbSize = sizeof(WNDCLASSEX);
    wndClass.style = CS_DBLCLKS | CS_NOCLOSE | CS_VREDRAW | CS_HREDRAW;
    wndClass.lpfnWndProc = WndProc;
    wndClass.cbClsExtra = 0;
    wndClass.cbWndExtra = 0;
    wndClass.hInstance = hInstance;
    wndClass.hIcon = (HICON)::LoadImage(NULL, L"Image.ico", IMAGE_ICON, 0, 0,
        LR_DEFAULTSIZE | LR_LOADFROMFILE);
    wndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndClass.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH);
    wndClass.lpszMenuName = NULL;
    wndClass.lpszClassName = L"ForTheDreamOfGameDevelop";

    if (!RegisterClassEx(&wndClass))
        return -1;

    mhMainWnd = CreateWindow(L"ForTheDreamOfGameDevelop",
        WINDOW_TITLE,
        WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, WINDOW_WIDTH,
        WINDOW_HEIGHT, NULL, NULL, hInstance, NULL);

    InitDirect3D();

    MoveWindow(mhMainWnd, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, true);

    ShowWindow(mhMainWnd, nShowCmd);
    UpdateWindow(mhMainWnd);

    MSG msg = { 0 };
    while (msg.message != WM_QUIT)
    {
        if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    UnregisterClass(L"ForTheDreamOfGameDevelop", wndClass.hInstance);
    return 0;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_PAINT:
        ValidateRect(hwnd, NULL);
        break;
    case WM_KEYDOWN:
        if (wParam == VK_ESCAPE)
            DestroyWindow(hwnd);
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hwnd, message, wParam, lParam);
    }

    return 0;
}

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

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

相关文章

【C++设计模式之策略模式】分析及示例

描述 策略模式&#xff08;Strategy Pattern&#xff09;是一种行为型设计模式&#xff0c;它允许在运行时根据不同的情况选择算法的行为。该模式将算法的定义封装成一组易于切换和替换的类&#xff0c;使得算法可以独立于其使用者进行变化。 原理 策略模式通过将具体的算法…

FastThreadLocal 快在哪里 ?

FastThreadLocal 快在哪里 &#xff1f; 引言FastThreadLocalset如何获取当前线程私有的InternalThreadLocalMap &#xff1f;如何知道当前线程使用到了哪些FastThreadLocal实例 ? get垃圾回收 小结 引言 FastThreadLocal 是 Netty 中造的一个轮子&#xff0c;那么为什么放着…

前端到底有多卷?可以转行吗?

我前几天招人&#xff0c;前后端各招一个人。 后端一天大概60多个投简历的。 前端岗位发出去&#xff0c;我吃了个饭&#xff0c;1小时回来 收到300多份简历…… 是一位HR回复的前端卷到什么程度的回答&#xff01; 下面我们来看两组官方纰漏的数据&#xff1a; 2023届全国高…

Git 学习笔记 | Git 的简介与历史

Git 学习笔记 | Git 的简介与历史 Git 学习笔记 | Git 的简介与历史Git 简介Git 历史 Git 学习笔记 | Git 的简介与历史 Git 简介 Git是分布式版本控制系统&#xff08;Distributed Version Control System&#xff0c;简称 DVCS&#xff09;&#xff0c;分为两种类型的仓库&…

100M跨境电商服务器能同时容纳多少人访问?

​  随着“出国”“出海”需求的业务量增多&#xff0c;网络的不断发展&#xff0c;服务商开始在带宽资源配备上作出各种改进。无论是纯国际带宽还是优化回国带宽租用&#xff0c;我们都可以独享&#xff0c;并且享受到大带宽。一般&#xff0c;做跨境电商业务的群体&#xf…

黑客都是土豪吗?真实情况是什么?

黑客的利益链条真的这么大这么好么,连最外围的都可以靠信息不对称赚普通人大学毕业上班族想都不敢想的金钱数目,黑客们是不是基本都是土豪 网络技术可以称为黑客程度的技术是不是真的很吃香&#xff1f;如果大部分大学生的智力资源都用在学习网络技术&#xff0c;会不会出现僧…

如何杜绝聊天泄密事件的发生呢(企业如何管理通讯工具,防止员工聊天泄密)

在现代企业中&#xff0c;员工之间的沟通是必不可少的。然而&#xff0c;随着科技的发展&#xff0c;员工聊天泄密的风险也日益增加。企业需要采取一系列措施来防止员工聊天泄密&#xff0c;以保护企业的核心竞争力和商业机密。本文将介绍一些有效的防止员工聊天泄密的方法。 1…

PHP8的匿名类-PHP8知识详解

PHP8支持通过new class 来实例化一个匿名类。所谓匿名类&#xff0c;就是指没有名称的类&#xff0c;只能在创建时使用new语句来声明它们。 匿名类是一种没有命名的即时类&#xff0c;可以用于简单的对象封装和实现接口。 以下是PHP 8中匿名类的基本语法示例&#xff1a; $ob…

Springboot使用Aop保存接口请求日志到mysql(及解决Interceptor拦截器中引用mapper和service为null)

一、Springboot使用Aop保存接口请求日志到mysql 1、添加aop依赖 <!-- aop日志 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency> 2、新建接口保存数据…

VsCode 常见的配置、常用好用插件

1、自动保存&#xff1a;不用装插件&#xff0c;在VsCode中设置一下就行 2、设置ctr滚轮改变字体大小 3、设置选项卡多行展示 这样打开了很多个文件&#xff0c;就不会导致有的打开的文件被隐藏 4、实时刷新网页的插件&#xff1a;LiveServer 5、open in browser 支持快捷键…

FM100/FM101协议系列-快速充电接口芯片

产品描述&#xff1a; FM100/FM101是一款支持Quick Charge 2.0&#xff08;QC 2.0&#xff09;快速充电协议的充电接口控制器 IC&#xff0c;可自动识别快速充电设备类型&#xff0c;并通过QC2.0协议与设备握手&#xff0c;使之获得设备允许的安全最高充电电压&#xff0c;在保…

为什么程序员必须坚持写技术博客?

当你申请一份工作的时候&#xff0c;你的简历通常大概只有两页的篇幅。当你接受面试的时候&#xff0c;你通常会跟面试官聊上一两个小时。以如此简短的简历和如此短暂的面试来评估一名软件开发人员的技能非常困难&#xff0c;所以雇主以此判定某个人是否适合某个工作岗位也颇具…

VB.NET vs. VB6.0:现代化编程语言 VS 经典老旧语言

目录 ​.NET背景&#xff1a; 特点: VB6.0背景&#xff1a; 特点: 两者之间的不同: 总结: 升华: .NET背景&#xff1a; VB.NET一种简单&#xff0c;现代&#xff0c;面向对象计算机编程语言&#xff0c;有微软开发&#xff0c;VB.NET是一种基于.NET Framework的面向对象…

基于Dockerfile搭建LNMP

目录 一、基础环境准备 1、环境前期准备 二、部署nginx&#xff08;容器IP 为 172.18.0.10&#xff09; 1、配置Dockerfile文件 2、配置nginx.conf文件 3、构建镜像、启动镜像 三、部署mysql 1、配置Dockerfile文件 2、配置my.conf文件 3、构建镜像、启动镜像 5、验…

Linux 基本指令(上)

文章内容&#xff1a; 1. ls 指令 语法&#xff1a; ls [选项][目录或文件] 功能&#xff1a;对于目录&#xff0c;该命令列出该目录下的所有子目录与文件。对于文件&#xff0c;将列出文件名以及其他信息。 单个ls显示当前目录下的文件和目录 常用选项&#…

5MW风电永磁直驱发电机-1200V直流并网Simulink仿真模型

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【Redis学习笔记二】三种特殊数据类型、事务的基本操作、锁、持久化、发布订阅、主从复制、哨兵模式

文章目录 三种特殊数据类型geospatial 地理位置Hyperloglog 基数统计Bitmaps 事务基本操作悲观锁乐观锁 持久化RDB&#xff08;Redis Database&#xff09;AOF&#xff08;Append Only File&#xff09;拓展 Redis发布订阅命令原理缺点应用 redis主从复制概念作用为什么使用集群…

JVM222

文章目录 JVM222运行时数据区的内部结构线程程序计数器&#xff08;PC寄存器&#xff09;虚拟机栈 JVM222 运行时数据区的内部结构 概述 本节主要讲的是运行时数据区&#xff0c;也就是下图这部分&#xff0c;它是在类加载器加载完成后的阶段&#xff0c;如下图&#xff1a; …

MySql学习笔记:MySql性能优化

本文是自己的学习笔记&#xff0c;主要参考以下资料 - 大话设计模式&#xff0c;程杰著&#xff0c;清华大学出版社出版 - 马士兵教育 1、MySql调优金字塔2、MySql调优2.1、查询性能2.1.1、慢查询2.1.1.1、总结 1、MySql调优金字塔 Mysql 调优时设计三个层面&#xff0c;分别是…

华为云云耀云服务器L实例评测|云耀云服务器L实例部署JumpServer开源堡垒机

华为云云耀云服务器L实例评测&#xff5c;云耀云服务器L实例部署JumpServer开源堡垒机 一、前言二、JumpServer 介绍2.1 JumpServer 简介2.2 JumpServer特点2.3 JumpServer支持的资产类型 三、本次实践介绍3.1 本次实践简介3.2 本次环境规划3.3 操作系统及配置要求3.4 数据库环…