DirectX12_Windows_GameDevelop_3:Direct3D的初始化

引言

  • 查看龙书时发现,第四章介绍预备知识的代码不太利于学习。因为它不像是LearnOpenGL那样从头开始一步一步教你敲代码,导致你没有一种整体感。
  • 如果你把它当作某一块的代码进行学习,你跟着敲会发现,总有几个变量是没有定义的。这是因为书上的代码都是把框架里的某一部分粘过来,缺少上文中对变量的定义,也根本不利于学习。
  • 学习图形学API就是为了使用GPU进行图形运算,说白了我们学习的DirectX就是一个工具,因此熟练掌握工具、能使用工具生产作品才是最重要的。因此不妨从4.3开始学习,学到那块不会再查了解前面的预备知识就会好很多
  • 由于现在学习的代码都是框架中的一部分,因此我的学习方法是
     1. 看书学习理论,并查看书中代码。
     2. 在VS中Ctrl+F搜索书中代码,对照搜索到的代码框架跟着敲。如果遇到哪个变量是未定义的,直接Ctrl+鼠标左键(点击未定义的变量),跳转到这个变量的定义后,复制定义到我的代码里。
     3. 如果遇到书中未详细介绍的内容,直接在MSDN查询相关信息。
  • 下图展示了我从4.3章节开始,根据龙书源代码框架,学习书本示例代码的过程
  • 跟着此教程,你能学会Direct3D初始化的9个步骤,如果现在你的手上有一本龙书,我建议你从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 渲染目标输出
HWND 窗口句柄
Client 客户
Reset 重置
Format 总体安排
Ordering 订购
Effect 效应
Flush 刷新
Event 事件
Completion 完成
Wait 等待
Single 仅有一个的
Handle 手柄
Current 现在的
Resource 资源
viewport 视口
Scissor 裁剪
RECT 矩形
Rectangle 长方形

一、初始化Direct3D

  • 初始化Direct3D的9个步骤如下:
步骤序号 步骤内容
1 D3D12CreateDevice 函数创建 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
  • 话不多说,直接上代码:
cpp 复制代码
#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技术。分析一下,上文示例代码中有:
cpp 复制代码
// IDXGIFactory: DXGI中最关键的接口之一,可以枚举显示适配器
ComPtr<IDXGIFactory4> mdxgiFactory;

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

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

2.1 完整示例

  • CPU和GPU的同步需要使用到围栏,第一步中我们创建好设备,第二步就可以创建围栏了。
  • 另外,如果使用描述符进行工作,需要获取描述符的大小描述符在不同GPU平台上的大小不同,因此我们需要将获取的描述符大小信息缓存起来,方便需要时直接引用
  • 话不多说,直接上代码:
cpp 复制代码
	// 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 质量级别的图像
  • 话不多说,直接上代码:
  • 核心代码:
cpp 复制代码
// 第三步:
    // 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.");
  • 全部代码:
cpp 复制代码
#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最高版本的代码为:
cpp 复制代码
#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想要提交的命令。过程非常简单,让我们直接来做吧!
  • 代码如下:
  • 核心代码:
cpp 复制代码
// 第四步: 创建命令队列和命令列表
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();
}
  • 全部代码:
cpp 复制代码
#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()中我们完成了命令队列和命令列表的创建 ,当然为了易于理解我将命令队列、命令列表和命令分配器 声明为了局部变量,你应该把它们放到函数外面作为全局变量,如下所示:
cpp 复制代码
// 声明一个命令队列
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)描述并创建交换链

  • 由于创建交换链时必须设定渲染窗口的句柄 ,所以我们必须使用Windows API创建一个窗口,得到窗口的句柄后记录下来,这样在创建交换链时就可以设置了。
  • 关于使用Windows API CreateWindow ,如果你不会使用可以查一下MSDN,也可以参考我这篇文章里的代码。

5.1 完整示例

  • 核心代码:
cpp 复制代码
// 定义缓冲区分辨率的高度和宽度
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()
    ));
}
  • 全部代码:
cpp 复制代码
#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;
}
  • 可以看到代码很简单,最主要的是后台缓冲区有很多选项供我们设置,让我们可以大展拳脚去设计最好的作品!
  • 你可以试着修改下面的代码,将其值改为false,在我电脑上会发生奇怪的现象,好像电脑屏幕变成一个放大镜一样!
cpp 复制代码
sd.Windowed = true;
  • 可以看到我们使用HWND类型的全局变量mhMainWnd记录渲染窗口的句柄,在主函数中我们使用CreateWindow创建了一个窗口,并把它返回的句柄赋值给mhMainWnd,因此创建的窗口就被作为了渲染窗口

5.2 相关知识

  • 这一小节让我们来讲讲CPU和GPU的同步吧!
  • **CPU和GPU必须相互配合,就像两人三足一样往前走!**如果CPU非常顺畅的执行,而GPU非常繁忙的绘制图形,会发生什么呢?
  • 试想CPU提交了一个模型的位置p1,让你GPU去绘制。由于你太繁忙,还没处理到这个模型时,我CPU已经循环好几轮了,我现在又想把模型的位置改为p2。于是GPU读取时模型位置时,模型的位置可能是p1,也可能是p2,甚至可能是不确定的(CPU刚好在修改数据)。
  • 上述情况是我们不希望的,我们希望模型先位于p1,再位于p2。解决此问题的一种办法是:强制CPU等待,直到GPU处理完所有命令的处理,达到某个指定的围栏为止 。我们将这种方法称为刷新命令队列 ,它可以通过围栏来实现
  • 话不多说,都在码里:
cpp 复制代码
/*
    使用围栏实现刷新命令队列
*/

// 记录围栏值
UINT64 mCurrentFence = 0;

void FlushCommandQueue()
{
    // 将围栏值增加1
    mCurrentFence++;

    // 向GPU命令队列末尾添加命令:将mFence的值修改为mCurrentFence
    ThrowIfFailed(mCommandQueue->Signal(
        mFence.Get(), mCurrentFence
    ));

    // 如果mFence < mCurrentFence,则GPU没有执行完刚才添加的命令,则GPU没有处理完所有命令
    if (mFence->GetCompletedValue() < mCurrentFence)
    {
        HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);

        // 如果mFence等于mCurrentFence,则激发事件eventHandle
        ThrowIfFailed((
            mFence->SetEventOnCompletion(mCurrentFence, eventHandle)
            ));

        // CPU等待eventHandle激发,当且仅当激发事件即GPU处理完命令时,CPU继续运行
        WaitForSingleObject(eventHandle, INFINITE);
        CloseHandle(eventHandle);
    }
}
  • 你可以复制这段代码试试,围栏我使用的是之前创建的mFence。
  • 这种方案并不完美,因为CPU会处于空闲状态,后面会有更好的解决方案。
  • 我们几乎可以在任何时间刷新命令队列 ,如在每一帧渲染命令都传入渲染队列后。当然不一定仅要每帧时才能刷新,比如GPU的初始化命令就可以在进行渲染循环之前进行刷新。当我们在执行某项CPU操作时,我们希望GPU把之前的命令都执行完了,这时就应该使用刷新命令队列
  • 记得我们说过,命令列表可以随意重置,而命令分配器不可以命令分配器必须等到GPU将命令队列中的命令都执行完了,才能进行重置因此如果你想要重置命令分配器,可以先刷新命令队列,再重置

(6)创建描述符堆

  • 我们需要通过创建描述符堆来存储程序中要用到的描述符/视图 。对此,Direct3D12以ID3D12DescriptorHeap接口表示描述符堆 ,并用ID3D12Device::CreateDescriptorHeap方法来创建它。
  • 在下面的示例中,我们将为交换链中的SwapChainBufferCount个用于渲染数据的缓冲区资源创建对应的渲染目标视图(Render Target View,RTV) ,并为用于深度测试(depth test)的深度/模板缓冲区资源传教一个深度/模板视图(Depth/Stencil View,DSV)所以我们需要创建两个描述符堆,第一个用来存储SwapChainBufferCount个RTV,第二个用来存储一个DSV

6.1 完整示例

  • 核心代码:
cpp 复制代码
/*
    第六步:创建描述符堆
*/

// ID3D12DescriptorHeap: 描述符堆接口
ComPtr<ID3D12DescriptorHeap> mRtvHeap;
ComPtr<ID3D12DescriptorHeap> mDsvHeap;

// 记录当前后台缓冲区的索引
int mCurrBackBuffer = 0;

void CreateRtvAndDsvDescriptorHeaps()
{
    // 创建描述结构体
    D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
    rtvHeapDesc.NumDescriptors = SwapChainBufferCount;  // 描述符堆中的描述符数量
    rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;  // 描述符的类型为RTV
    rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;// 其他选项设为无
    rtvHeapDesc.NodeMask = 0;
    ThrowIfFailed(md3dDevice->CreateDescriptorHeap(     // 用ID3D12Device接口创建描述符堆
        &rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf())
    ));

    D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;             // 同上
    dsvHeapDesc.NumDescriptors = 1;                     // 数量为1
    dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;  // 类型是DSV
    dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
    dsvHeapDesc.NodeMask = 0;
    ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
        &dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));
}
  • 全部代码:
cpp 复制代码
#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;


/*
    第六步:创建描述符堆
*/

// ID3D12DescriptorHeap: 描述符堆接口
ComPtr<ID3D12DescriptorHeap> mRtvHeap;
ComPtr<ID3D12DescriptorHeap> mDsvHeap;

// 记录当前后台缓冲区的索引
int mCurrBackBuffer = 0;

void CreateRtvAndDsvDescriptorHeaps()
{
    // 创建描述结构体
    D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
    rtvHeapDesc.NumDescriptors = SwapChainBufferCount;  // 描述符堆中的描述符数量
    rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;  // 描述符的类型为RTV
    rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;// 其他选项设为无
    rtvHeapDesc.NodeMask = 0;
    ThrowIfFailed(md3dDevice->CreateDescriptorHeap(     // 用ID3D12Device接口创建描述符堆
        &rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf())
    ));

    D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;             // 同上
    dsvHeapDesc.NumDescriptors = 1;                     // 数量为1
    dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;  // 类型是DSV
    dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
    dsvHeapDesc.NodeMask = 0;
    ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
        &dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));
}

/*
    第五步:描述并创建交换链
*/
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();

    /*
        第六步:创建描述符堆
    */
    CreateRtvAndDsvDescriptorHeaps();
}

#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;
}

6.2 相关知识

  • 在上述程序中我们定义了变量mCurrBackBuffer它的作用是记录当前后台缓冲区的索引 。因为利用页面翻转技术交换前台缓冲区和后台缓冲区,所以需要对其进行记录,以便得知哪个缓冲区是当前正用于渲染数据的后台缓冲区。
  • 创建了描述符堆后,我们还需要知道如何访问其中所存的描述符 。在程序中我们通过句柄来引用描述符 ,并以ID3D12DescriptorHeap::GetCPUDescriptorHandleForHeapStart方法获得描述符堆中第一个描述符的句柄
  • 获取当前后台缓冲区RTV描述符的伪代码如下:
cpp 复制代码
// 获取当前后台缓冲区的RTV描述符
D3D12_CPU_DESCRIPTOR_HANDLE CurrentBackBuffer()
{
    return mRtvHeap->GetCPUDescriptorHandleForHeapStart()
        + mCurrBackBuffer * mRtvDescriptorSize;
}
  • 注意上述代码是伪代码,因为它并不能正常运行,它的逻辑是正确的。即通过堆中首个句柄和描述符的序号,可以计算出相对偏移量,加上后就是该序号描述符的句柄。你可以将这段代码复制到工程中,会发现报错类型无法运算,因为加号左边是D3D12_CPU_DESCRIPTOR_HANDLE,而右边是unsigned int。当然如果你想要获取深度/模板缓冲区描述符,由于堆中就只有一个,直接获取首个句柄即可。
  • 文中所用代码为:
cpp 复制代码
// 获取当前后台缓冲区的RTV描述符
D3D12_CPU_DESCRIPTOR_HANDLE CurrentBackBuffer()
{
    return CD3DX12_CPU_DESCRIPTOR_HANDLE(
        mRtvHeap->GetCPUDescriptorHandleForHeapStart(),
        mCurrBackBuffer,
        mRtvDescriptorSize
    );
}
  • 其中CD3DX12_CPU_DESCRIPTOR_HANDLE是龙书框架中头文件d3dx12.h里的类。

(7)创建渲染目标视图

  • 正如前文所言,资源不能与渲染流水线之间绑定 ,而是需要描述符作为中间人。现在我们已经创建了描述符堆 ,是时候创建描述符 ,并将它绑定到流水线阶段了
  • 我们要将后台缓冲区绑定到渲染流水线的输出合并阶段 ,这样Direct3D才知道将图像的最终结果渲染到后台缓冲区。因此我们需要为后台缓冲区创建渲染目标视图/描述符
  • 交换链中可能有多个缓冲区 ,比如我们的程序是双缓冲所以有两个缓冲区,我们要为交换链中的每一个缓冲区创建渲染目标视图/描述符
  • 创建渲染目标视图很简单,我们要获取交换链中每个缓冲区的资源 ,还要计算每个缓冲区的描述符句柄 。然后就可以调用ID3D12Device::CreateRenderTargetView方法即可。

7.1 完整示例

  • 核心代码:
cpp 复制代码
/*
    第七步:调整后台缓冲区的大小,并为它创建渲染目标视图/描述符
*/

// ID3D12Resource: 表示资源的接口,它将物理内存和堆资源抽象组织为可处理的数据数组与多维数组,从而使CPU和GPU可以读写
ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];

// 描述符句柄
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(
    mRtvHeap->GetCPUDescriptorHandleForHeapStart()
);

void OnResize()
{
    // 对交换链中的每个缓冲区创建渲染目标视图/描述符
    for (UINT i = 0; i < SwapChainBufferCount; ++i)
    {
        // 先获取交换链中每个缓冲区资源,函数中i表示缓冲区索引,将获取到的资源记录到资源数组中
        ThrowIfFailed(
            mSwapChain->GetBuffer(i, IID_PPV_ARGS(&mSwapChainBuffer[i])));

        // ID3D12Device::CreateRenderTargetView方法: 创建渲染目标视图
        md3dDevice->CreateRenderTargetView(
            mSwapChainBuffer[i].Get(),  // 指定用作渲染目标的资源 
            nullptr,                    // 资源中元素的数据类型,如果在资源创建时已指定,则可设为空
            rtvHeapHandle);             // 引用所创建渲染目标视图的描述符句柄

        // 将rtvHeapHandle句柄偏移到描述符对中下一个缓冲区
        rtvHeapHandle.Offset(1, mRtvDescriptorSize);
    }
}
  • 上述代码是无法运行的,因为描述符句柄类型CD3DX12_CPU_DESCRIPTOR_HANDLE来源于框架头文件<d3dx12.h>。当然不必计较,虽然龙书是很生硬的把框架代码一比一拿出来讲,但是我想我们有个印象就行了。
  • 我没有给出完整代码,这个示例代码位于框架的OnResize()函数中,是其中的一小部分,大家感兴趣直接在VS搜索整个项目即可找到。
  • 学会龙书里的框架是我们的必经之路,然后跟随书籍学完Direct3D的其他知识,最后再设计自己的框架!

7.2 相关知识

  • Direct3D 12中的许多结构都有其对应的扩展辅助结构变体(variation)考虑到使用上的方便性,我们更偏爱于运用哪些变体以CD3DX12作为前缀的变体全都定义在d3dx12.h头文件当中这个头文件并不属于DirectX 12 SDK的核心部分,但是可以通过微软官方网站下载获得。为了方面起见,本书框架中包含一份d3dx12.h头文件。
  • 以上是书籍原文,现在让我们把这份神秘的头文件dx3x12.h包含进我们的项目吧,代码如下:
cpp 复制代码
#include"../../Common/d3dx12.h"
  • 要注意上述代码和你项目的文件路径相关 ,上文代码中"..."表示项目文件夹即DirectX12_Windows_GameDevelop。而".../..."表示从项目文件夹回退一级,即到达下图所示的文件夹。如图所示,我将书籍中的基础代码文件夹Common放在下图所示文件夹,即让Common和项目文件夹处于同一目录下。于是".../.../Common/d3dx12.h"就能找到Common文件夹中的d3dx12.h头文件了。
  • 如果你不会没有Common文件或者不会配置,可以查看这这篇文章,它最下面有书籍配套资源,中间展示了搭建框架的步骤。
  • 既然已经有了d3dx12.h头文件,就让我们修改刚才的代码,给出完整创建渲染目标视图的代码把!
  • 代码如下:
cpp 复制代码
#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>
#include"../../Common/d3dx12.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;

// ID3D12DescriptorHeap: 描述符堆接口
ComPtr<ID3D12DescriptorHeap> mRtvHeap;
ComPtr<ID3D12DescriptorHeap> mDsvHeap;

// 记录当前后台缓冲区的索引
int mCurrBackBuffer = 0;


/*
    第七步:调整后台缓冲区的大小,并为它创建渲染目标视图
*/

// ID3D12Resource: 将物理内存和堆资源抽象组织为可处理的数据数组与多维数组,从而使CPU和GPU可以读写
ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];

void OnResize()
{
    // CPU描述符句柄
    CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(
        mRtvHeap->GetCPUDescriptorHandleForHeapStart()
    );

    // 对交换链中的每个缓冲区创建渲染目标视图
    for (UINT i = 0; i < SwapChainBufferCount; ++i)
    {
        // 先获取交换链中每个缓冲区资源,函数中i表示缓冲区索引,将获取到的资源记录到资源数组中
        ThrowIfFailed(
            mSwapChain->GetBuffer(i, IID_PPV_ARGS(&mSwapChainBuffer[i])));

        // ID3D12Device::CreateRenderTargetView方法: 创建渲染目标视图
        md3dDevice->CreateRenderTargetView(
            mSwapChainBuffer[i].Get(),  // 指定用作渲染目标的资源 
            nullptr,                    // 资源中元素的数据类型,如果在资源创建时已指定,则可设为空
            rtvHeapHandle);             // 引用所创建渲染目标视图的描述符句柄

        // 将rtvHeapHandle句柄偏移到描述符对中下一个缓冲区
        rtvHeapHandle.Offset(1, mRtvDescriptorSize);
    }
}

/*
    第六步:创建描述符堆
*/

void CreateRtvAndDsvDescriptorHeaps()
{
    // 创建描述结构体
    D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
    rtvHeapDesc.NumDescriptors = SwapChainBufferCount;  // 描述符堆中的描述符数量
    rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;  // 描述符的类型为RTV
    rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;// 其他选项设为无
    rtvHeapDesc.NodeMask = 0;
    ThrowIfFailed(md3dDevice->CreateDescriptorHeap(     // 用ID3D12Device接口创建描述符堆
        &rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf())
    ));

    D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;             // 同上
    dsvHeapDesc.NumDescriptors = 1;                     // 数量为1
    dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;  // 类型是DSV
    dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
    dsvHeapDesc.NodeMask = 0;
    ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
        &dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));

    OnResize();
}

UINT mCurrentFence = 0;

void FlushCommandQueue()
{
    // Advance the fence value to mark commands up to this fence point.
    mCurrentFence++;

    // Add an instruction to the command queue to set a new fence point.  Because we 
    // are on the GPU timeline, the new fence point won't be set until the GPU finishes
    // processing all the commands prior to this Signal().
    ThrowIfFailed(mCommandQueue->Signal(mFence.Get(), mCurrentFence));

    // Wait until the GPU has completed commands up to this fence point.
    if (mFence->GetCompletedValue() < mCurrentFence)
    {
        HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);

        // Fire event when GPU hits current fence.  
        ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));

        // Wait until the GPU hits current fence event is fired.
        WaitForSingleObject(eventHandle, INFINITE);
        CloseHandle(eventHandle);
    }
}

/*
    第五步:描述并创建交换链
*/
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();

    /*
        第六步:创建描述符堆
    */
    CreateRtvAndDsvDescriptorHeaps();

}

#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;
}
  • 你可以看到我将rtvHeapHandle变量放到了函数内 ,这是因为如果将其声明为全局变量会报错,因为mRtvHeap在第六步创建描述符堆时才被获取,之前为空
  • 框架中初始化Direct3D的代码到第六步就结束了,你看OnResize函数名也可以得知这个函数不是用在此处的。为了验证其效果,我还是在第六步函数的末尾调用了OnResize函数,你也可以调用试试。

(8)创建深度/模板缓冲区及其视图

  • 刚才我们创建了渲染目标视图,现在我们来创建深度/模板缓存区。
  • 深度/模板缓冲区是一种2D纹理,它存储着最近可视对象的深度和模板信息。纹理是一种GPU资源,因此我们需要先填写D3D12_RESOURCE_DESC结构体来描述纹理资源,再用ID3D12Device::CreateCommittedResource方法来创建它。

8.1 完整示例

  • 核心代码:
cpp 复制代码
/*
    第八步:创建深度/模板缓冲区及其视图
*/
ComPtr<ID3D12Resource> mDepthStenilBuffer; // 创建的深度/模板缓冲资源

D3D12_RESOURCE_DESC depthStencilDesc;      // 资源描述结构体
DXGI_FORMAT mDepthStencilFormat;           // 深度/模板存储数据的格式

UINT m4xMsaaState = 1;                     // 是否启用4X MSAA

// 框架里面复制来的,下面会用到
D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView()
{
    return mDsvHeap->GetCPUDescriptorHandleForHeapStart();
}

// 创建深度/模板缓存区资源
void CreateDepthStencilDescriptor()
{
    // 使用结构体描述资源信息
    depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; // 资源的维度:2D纹理
    depthStencilDesc.Alignment = 0;                                  
    depthStencilDesc.Width = mClientWidth;                           // 资源的像素宽度
    depthStencilDesc.Height = mClientHeight;                         // 资源的像素高度
    depthStencilDesc.DepthOrArraySize = 1;                           // 资源的像素深度
    depthStencilDesc.MipLevels = 1;                                  // 资源的mipmap层级数量: 深度/模板缓冲资区只能有一个级别
    depthStencilDesc.Format = mDepthStencilFormat;                   // 像素数据的格式
    depthStencilDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;        // 每个像素采样次数
    depthStencilDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0; // 质量级别
    depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;          // 纹理布局
    depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;// 杂项标志

    D3D12_CLEAR_VALUE optClear;                             // 描述用于清理资源的优化值
    optClear.Format = mDepthStencilFormat;                  // 格式为像素数据的格式
    optClear.DepthStencil.Depth = 1.0f;                     // 深度清理为1.0
    optClear.DepthStencil.Stencil = 0;                      // 模板清理为0.0
    ThrowIfFailed(md3dDevice->CreateCommittedResource(      // 创建深度/模板缓冲区资源
        &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),// 指定堆为默认堆
        D3D12_HEAP_FLAG_NONE,                             // 杂项无选项
        &depthStencilDesc,                                // 描述资源结构体的指针
        D3D12_RESOURCE_STATE_COMMON,                      // 初始通常设为此
        &optClear,                                        // 清除优化对象的指针,若不希望优化则设为空
        IID_PPV_ARGS(mDepthStenilBuffer.GetAddressOf())
    ));

    // 利用资源的格式,为整个资源的第0层 mip层创建描述符
    md3dDevice->CreateDepthStencilView(   
        mDepthStenilBuffer.Get(),
        nullptr,
        DepthStencilView()
    );

    
    mCommandList->ResourceBarrier(             // 转换资源屏障,告知GPU资源状态正在进行转换
        1,
        &CD3DX12_RESOURCE_BARRIER::Transition( // 将资源从初始状态转换为深度缓冲区
            mDepthStenilBuffer.Get(),
            D3D12_RESOURCE_STATE_COMMON,        
            D3D12_RESOURCE_STATE_DEPTH_WRITE
            )
    );
}
  • 完整代码:
cpp 复制代码
#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>
#include"../../Common/d3dx12.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;

// ID3D12DescriptorHeap: 描述符堆接口
ComPtr<ID3D12DescriptorHeap> mRtvHeap;
ComPtr<ID3D12DescriptorHeap> mDsvHeap;

// 记录当前后台缓冲区的索引
int mCurrBackBuffer = 0;

// ID3D12Resource: 将物理内存和堆资源抽象组织为可处理的数据数组与多维数组,从而使CPU和GPU可以读写
ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];

/*
    第八步:创建深度/模板缓冲区及其视图
*/
ComPtr<ID3D12Resource> mDepthStenilBuffer; // 创建的深度/模板缓冲资源

D3D12_RESOURCE_DESC depthStencilDesc;      // 资源描述结构体
DXGI_FORMAT mDepthStencilFormat;           // 深度/模板存储数据的格式

UINT m4xMsaaState = 1;                     // 是否启用4X MSAA

// 框架里面复制来的,下面会用到
D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView()
{
    return mDsvHeap->GetCPUDescriptorHandleForHeapStart();
}

// 创建深度/模板缓存区资源
void CreateDepthStencilDescriptor()
{
    // 使用结构体描述资源信息
    depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; // 资源的维度:2D纹理
    depthStencilDesc.Alignment = 0;                                  
    depthStencilDesc.Width = mClientWidth;                           // 资源的像素宽度
    depthStencilDesc.Height = mClientHeight;                         // 资源的像素高度
    depthStencilDesc.DepthOrArraySize = 1;                           // 资源的像素深度
    depthStencilDesc.MipLevels = 1;                                  // 资源的mipmap层级数量: 深度/模板缓冲资区只能有一个级别
    depthStencilDesc.Format = mDepthStencilFormat;                   // 像素数据的格式
    depthStencilDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;        // 每个像素采样次数
    depthStencilDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0; // 质量级别
    depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;          // 纹理布局
    depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;// 杂项标志

    D3D12_CLEAR_VALUE optClear;                             // 描述用于清理资源的优化值
    optClear.Format = mDepthStencilFormat;                  // 格式为像素数据的格式
    optClear.DepthStencil.Depth = 1.0f;                     // 深度清理为1.0
    optClear.DepthStencil.Stencil = 0;                      // 模板清理为0.0
    ThrowIfFailed(md3dDevice->CreateCommittedResource(      // 创建深度/模板缓冲区资源
        &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),// 指定堆为默认堆
        D3D12_HEAP_FLAG_NONE,                             // 杂项无选项
        &depthStencilDesc,                                // 描述资源结构体的指针
        D3D12_RESOURCE_STATE_COMMON,                      // 初始通常设为此
        &optClear,                                        // 清除优化对象的指针,若不希望优化则设为空
        IID_PPV_ARGS(mDepthStenilBuffer.GetAddressOf())
    ));

    // 利用资源的格式,为整个资源的第0层 mip层创建描述符
    md3dDevice->CreateDepthStencilView(   
        mDepthStenilBuffer.Get(),
        nullptr,
        DepthStencilView()
    );

    
    mCommandList->ResourceBarrier(             // 转换资源屏障,告知GPU资源状态正在进行转换
        1,
        &CD3DX12_RESOURCE_BARRIER::Transition( // 将资源从初始状态转换为深度缓冲区
            mDepthStenilBuffer.Get(),
            D3D12_RESOURCE_STATE_COMMON,        
            D3D12_RESOURCE_STATE_DEPTH_WRITE
            )
    );
}

/*
    第七步:调整后台缓冲区的大小,并为它创建渲染目标视图
*/

void OnResize()
{
    // CPU描述符句柄
    CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(
        mRtvHeap->GetCPUDescriptorHandleForHeapStart()
    );

    // 对交换链中的每个缓冲区创建渲染目标视图
    for (UINT i = 0; i < SwapChainBufferCount; ++i)
    {
        // 先获取交换链中每个缓冲区资源,函数中i表示缓冲区索引,将获取到的资源记录到资源数组中
        ThrowIfFailed(
            mSwapChain->GetBuffer(i, IID_PPV_ARGS(&mSwapChainBuffer[i])));

        // ID3D12Device::CreateRenderTargetView方法: 创建渲染目标视图
        md3dDevice->CreateRenderTargetView(
            mSwapChainBuffer[i].Get(),  // 指定用作渲染目标的资源 
            nullptr,                    // 资源中元素的数据类型,如果在资源创建时已指定,则可设为空
            rtvHeapHandle);             // 引用所创建渲染目标视图的描述符句柄

        // 将rtvHeapHandle句柄偏移到描述符对中下一个缓冲区
        rtvHeapHandle.Offset(1, mRtvDescriptorSize);
    }

    CreateDepthStencilDescriptor();
    1;
}

/*
    第六步:创建描述符堆
*/

void CreateRtvAndDsvDescriptorHeaps()
{
    // 创建描述结构体
    D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
    rtvHeapDesc.NumDescriptors = SwapChainBufferCount;  // 描述符堆中的描述符数量
    rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;  // 描述符的类型为RTV
    rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;// 其他选项设为无
    rtvHeapDesc.NodeMask = 0;
    ThrowIfFailed(md3dDevice->CreateDescriptorHeap(     // 用ID3D12Device接口创建描述符堆
        &rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf())
    ));

    D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;             // 同上
    dsvHeapDesc.NumDescriptors = 1;                     // 数量为1
    dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;  // 类型是DSV
    dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
    dsvHeapDesc.NodeMask = 0;
    ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
        &dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));

    OnResize();
}

UINT mCurrentFence = 0;

void FlushCommandQueue()
{
    // Advance the fence value to mark commands up to this fence point.
    mCurrentFence++;

    // Add an instruction to the command queue to set a new fence point.  Because we 
    // are on the GPU timeline, the new fence point won't be set until the GPU finishes
    // processing all the commands prior to this Signal().
    ThrowIfFailed(mCommandQueue->Signal(mFence.Get(), mCurrentFence));

    // Wait until the GPU has completed commands up to this fence point.
    if (mFence->GetCompletedValue() < mCurrentFence)
    {
        HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);

        // Fire event when GPU hits current fence.  
        ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));

        // Wait until the GPU hits current fence event is fired.
        WaitForSingleObject(eventHandle, INFINITE);
        CloseHandle(eventHandle);
    }
}

/*
    第五步:描述并创建交换链
*/
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();

    /*
        第六步:创建描述符堆
    */
    CreateRtvAndDsvDescriptorHeaps();

}

#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;
}
  • 不必要求太多,因为这些代码示例的片段都是书中框架中的一部分,我们后面总能窥探到她的全貌。

8.2 相关知识

  • 为了使性能达到最佳,通常将资源放置在默认堆中,只有在需要上传堆或回传堆得特性之时,才选用其他类型的堆。

(9)设置视口和裁剪矩形

  • 我们的后台缓冲区通常是和渲染窗口大小相等的,但有时我们希望把3D场景绘制到后台缓冲区的某个矩形子区域中,这样就能在窗口的一部分中绘制图形了。
  • 这种效果是通过视口来实现的,后台缓冲区的矩形子区域叫做视口视口是一个矩形,最左上角为视口坐标系内原点、水平向右为x轴、竖直向下为y轴
  • 在Direct3D中,存储在深度缓冲区中的数据都是范围在0~1的归一化深度值 。而视口的属性中有两个值:MinDepth和MaxDepthDirect3D会将归一化的深度值转换到 [MinDepth,MaxDepth] 范围内借此可以实现许多特性通常情况下会将它们设为0和1,这样深度值就不会改变

9.1 完整示例

  • 核心代码:
cpp 复制代码
/*
    第九步:设置视口及裁剪矩形
*/
void SetViewPortAndScissorRectangle()
{
    // 定义视口
    D3D12_VIEWPORT vp;
    vp.TopLeftX = 0;    // 视口左上角相对于后台缓冲区左上角的坐标
    vp.TopLeftY = 0;
    vp.Width = static_cast<float>(mClientWidth);  // 视口的宽和高 
    vp.Height = static_cast<float>(mClientHeight);
    vp.MinDepth = 0.0f; // 视口的最大最小深度,所有物体的深度会被转换到这个范围内
    vp.MaxDepth = 1.0f;

    // 设置Direct3D中的视口
    mCommandList->RSSetViewports(1, &vp);

    // 定义一个裁剪矩形(左上角坐标,宽度和高度)
    D3D12_RECT mScissorRectangle = { 0,0,mClientWidth / 2,mClientHeight / 2 };
    
    // 使得后台缓冲区此矩阵范围内得像素都被剔除
    mCommandList->RSSetScissorRects(1, &mScissorRectangle);
}
  • 全部代码:
cpp 复制代码
#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>
#include"../../Common/d3dx12.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;

// ID3D12DescriptorHeap: 描述符堆接口
ComPtr<ID3D12DescriptorHeap> mRtvHeap;
ComPtr<ID3D12DescriptorHeap> mDsvHeap;

// 记录当前后台缓冲区的索引
int mCurrBackBuffer = 0;

// ID3D12Resource: 将物理内存和堆资源抽象组织为可处理的数据数组与多维数组,从而使CPU和GPU可以读写
ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];

/*
    第九步:设置视口及裁剪矩形
*/
void SetViewPortAndScissorRectangle()
{
    // 定义视口
    D3D12_VIEWPORT vp;
    vp.TopLeftX = 0;    // 视口左上角相对于后台缓冲区左上角的坐标
    vp.TopLeftY = 0;
    vp.Width = static_cast<float>(mClientWidth);  // 视口的宽和高 
    vp.Height = static_cast<float>(mClientHeight);
    vp.MinDepth = 0.0f; // 视口的最大最小深度,所有物体的深度会被转换到这个范围内
    vp.MaxDepth = 1.0f;

    // 设置Direct3D中的视口
    mCommandList->RSSetViewports(1, &vp);

    // 定义一个裁剪矩形(左上角坐标,宽度和高度)
    D3D12_RECT mScissorRectangle = { 0,0,mClientWidth / 2,mClientHeight / 2 };
    
    // 使得后台缓冲区此矩阵范围内得像素都被剔除
    mCommandList->RSSetScissorRects(1, &mScissorRectangle);
}


/*
    第八步:创建深度/模板缓冲区及其视图
*/
ComPtr<ID3D12Resource> mDepthStenilBuffer; // 创建的深度/模板缓冲资源

D3D12_RESOURCE_DESC depthStencilDesc;      // 资源描述结构体
DXGI_FORMAT mDepthStencilFormat;           // 深度/模板存储数据的格式

UINT m4xMsaaState = 1;                     // 是否启用4X MSAA

// 框架里面复制来的,下面会用到
D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView()
{
    return mDsvHeap->GetCPUDescriptorHandleForHeapStart();
}

// 创建深度/模板缓存区资源
void CreateDepthStencilDescriptor()
{
    // 使用结构体描述资源信息
    depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; // 资源的维度:2D纹理
    depthStencilDesc.Alignment = 0;                                  
    depthStencilDesc.Width = mClientWidth;                           // 资源的像素宽度
    depthStencilDesc.Height = mClientHeight;                         // 资源的像素高度
    depthStencilDesc.DepthOrArraySize = 1;                           // 资源的像素深度
    depthStencilDesc.MipLevels = 1;                                  // 资源的mipmap层级数量: 深度/模板缓冲资区只能有一个级别
    depthStencilDesc.Format = mDepthStencilFormat;                   // 像素数据的格式
    depthStencilDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;        // 每个像素采样次数
    depthStencilDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0; // 质量级别
    depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;          // 纹理布局
    depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;// 杂项标志

    D3D12_CLEAR_VALUE optClear;                             // 描述用于清理资源的优化值
    optClear.Format = mDepthStencilFormat;                  // 格式为像素数据的格式
    optClear.DepthStencil.Depth = 1.0f;                     // 深度清理为1.0
    optClear.DepthStencil.Stencil = 0;                      // 模板清理为0.0
    ThrowIfFailed(md3dDevice->CreateCommittedResource(      // 创建深度/模板缓冲区资源
        &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),// 指定堆为默认堆
        D3D12_HEAP_FLAG_NONE,                             // 杂项无选项
        &depthStencilDesc,                                // 描述资源结构体的指针
        D3D12_RESOURCE_STATE_COMMON,                      // 初始通常设为此
        &optClear,                                        // 清除优化对象的指针,若不希望优化则设为空
        IID_PPV_ARGS(mDepthStenilBuffer.GetAddressOf())
    ));

    // 利用资源的格式,为整个资源的第0层 mip层创建描述符
    md3dDevice->CreateDepthStencilView(   
        mDepthStenilBuffer.Get(),
        nullptr,
        DepthStencilView()
    );

    
    mCommandList->ResourceBarrier(             // 转换资源屏障,告知GPU资源状态正在进行转换
        1,
        &CD3DX12_RESOURCE_BARRIER::Transition( // 将资源从初始状态转换为深度缓冲区
            mDepthStenilBuffer.Get(),
            D3D12_RESOURCE_STATE_COMMON,        
            D3D12_RESOURCE_STATE_DEPTH_WRITE
            )
    );
}

/*
    第七步:调整后台缓冲区的大小,并为它创建渲染目标视图
*/

void OnResize()
{
    // CPU描述符句柄
    CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(
        mRtvHeap->GetCPUDescriptorHandleForHeapStart()
    );

    // 对交换链中的每个缓冲区创建渲染目标视图
    for (UINT i = 0; i < SwapChainBufferCount; ++i)
    {
        // 先获取交换链中每个缓冲区资源,函数中i表示缓冲区索引,将获取到的资源记录到资源数组中
        ThrowIfFailed(
            mSwapChain->GetBuffer(i, IID_PPV_ARGS(&mSwapChainBuffer[i])));

        // ID3D12Device::CreateRenderTargetView方法: 创建渲染目标视图
        md3dDevice->CreateRenderTargetView(
            mSwapChainBuffer[i].Get(),  // 指定用作渲染目标的资源 
            nullptr,                    // 资源中元素的数据类型,如果在资源创建时已指定,则可设为空
            rtvHeapHandle);             // 引用所创建渲染目标视图的描述符句柄

        // 将rtvHeapHandle句柄偏移到描述符对中下一个缓冲区
        rtvHeapHandle.Offset(1, mRtvDescriptorSize);
    }

    //CreateDepthStencilDescriptor();

    SetViewPortAndScissorRectangle();
}

/*
    第六步:创建描述符堆
*/

void CreateRtvAndDsvDescriptorHeaps()
{
    // 创建描述结构体
    D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
    rtvHeapDesc.NumDescriptors = SwapChainBufferCount;  // 描述符堆中的描述符数量
    rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;  // 描述符的类型为RTV
    rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;// 其他选项设为无
    rtvHeapDesc.NodeMask = 0;
    ThrowIfFailed(md3dDevice->CreateDescriptorHeap(     // 用ID3D12Device接口创建描述符堆
        &rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf())
    ));

    D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;             // 同上
    dsvHeapDesc.NumDescriptors = 1;                     // 数量为1
    dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;  // 类型是DSV
    dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
    dsvHeapDesc.NodeMask = 0;
    ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
        &dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));

    OnResize();
}

UINT mCurrentFence = 0;

void FlushCommandQueue()
{
    // Advance the fence value to mark commands up to this fence point.
    mCurrentFence++;

    // Add an instruction to the command queue to set a new fence point.  Because we 
    // are on the GPU timeline, the new fence point won't be set until the GPU finishes
    // processing all the commands prior to this Signal().
    ThrowIfFailed(mCommandQueue->Signal(mFence.Get(), mCurrentFence));

    // Wait until the GPU has completed commands up to this fence point.
    if (mFence->GetCompletedValue() < mCurrentFence)
    {
        HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);

        // Fire event when GPU hits current fence.  
        ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));

        // Wait until the GPU hits current fence event is fired.
        WaitForSingleObject(eventHandle, INFINITE);
        CloseHandle(eventHandle);
    }
}

/*
    第五步:描述并创建交换链
*/
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();

    /*
        第六步:创建描述符堆
    */
    CreateRtvAndDsvDescriptorHeaps();
}

#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;
}

9.2 相关知识

  • 不能为同一个渲染目标指定多个视口
  • 命令列表一旦被重置,视口也就需要随之重置
  • 不能为同一个渲染目标指定多个裁剪矩形
  • 裁剪矩形需要随着命令列表的重置而重置

结语

  • 当你看到这里,你已经熟悉了4.3中所有程序 。你可以查看4.1~4.3 之间的内容进行查漏补缺,了解一些零散的内容。
  • 如果你对零散的内容依旧感到烦恼,敬请期待我的下一篇博客,它会简洁明了的概括4.4节有关计时器等内容 ,等学完4.4后,我们就可以学习完整的Direct3D基本框架了
  • 下篇博客见😊!
相关推荐
立秋678937 分钟前
Python的defaultdict详解
服务器·windows·python
Indigo_code1 小时前
【数据结构】【链表代码】合并有序链表
数据结构·windows·链表
暮雪倾风1 小时前
【WPF开发】超级详细的“文件选择”(附带示例工程)
windows·wpf
何中应3 小时前
如何使用CMD命令启动应用程序(二)
windows·桌面应用·batch命令
sukalot4 小时前
windows C++-使用任务和 XML HTTP 请求进行连接(一)
c++·windows
ぃ扶摇ぅ5 小时前
Windows系统编程(三)进程与线程二
c++·windows
weixin_419349796 小时前
windows上安装python环境
windows
天上掉下来个程小白7 小时前
Stream流的中间方法
java·开发语言·windows
暮雪倾风7 小时前
【WPF开发】控件介绍-Grid(网格布局)
windows·wpf
sukalot8 小时前
windows C++-windows C++-使用任务和 XML HTTP 请求进行连接(二)
c++·windows