Chapter4

这一章信息量和概念相当多,为加深印象才重复码了一遍
PS.专有名词的汉化除部分引用 游戏架构引擎 外,都是瞎编的

4.1.2 COM

我们经常引用一个 COM 对象作为接口,因此可以当作一个C++类来使用.

不能通过new来创建一个新的COM接口.

除此以外,COM对象是引用计数的,当我们被一个称为Release方法的接口(所有COM接口继承自IUnknown COM接口,它提供该方法)而不是delete处理时,COM接口会在引用计数归0时释放他们的内存.

为了辅助管理COM对象的生命周期,Windows Runtime Library (WRL)提供一个

Microsoft::WRL::ComPtr 类(#include <wrl.h>)

它可以被认为是一个COM类的智能指针

当一个ComPtr实例超出范围时,它会自动在基层COM类调用Release,从而避免我们手动撸Release


下面给出3个主要的ComPtr方法

1.Get:返回一个指向基层COM接口的指针.

常用于传参到一个接收COM接口裸指针(raw COM interface pointer)的函数(argument形参)

ComPtr<ID3D12RootSignature> mRootSignature;
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
//SetGraphicsRootSignature期望一个ID3D12RootSignature*的参数

2.GetAdressOf:返回指向基层COM接口的指针的地址

常用于通过一个函数参数返回一个COM接口指针(parameter实参)

ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
ThrowIfFailed(md3dDevice->CreateCommandAllocator(
    D3D12_COMMAND_LIST_TYPE_DIRECT,//暂时保留
    mDirectCmdListAlloc.GetAddressOf()));

3.Reset:把ComPtr实例设置成(set to)nullptr以及减少基层COM接口的引用计数,等价地,你也可以把nullptr赋值给一个ComPtr实例


4.1.3纹理格式(Textures Formats)

一个2D纹理是一个数据元素的矩阵.其中一种2D纹理的用途是存储2D图像数据,它的每一个纹理里面的元素都存储着一个像素的颜色.然而,这种用途并非唯一.举个栗子,法线映射(normal mapping)的技术里头,纹理里的每个元素都存储着一个3D向量(而不是颜色).因此,即使纹理一般会被认为存储图像数据,但它可厉害多了.一个1D纹理像1D的数据元素的数组,2D,3D以此类推

纹理还有mipmap level(多级渐远纹理等级),GPU也可对它们进行特殊操作,比如滤波(filters)和多重采样(multi-sampling)


除此以外,纹理不能储存任意数据元素,它只能存储特定格式的数据元素,这些类型在 DXGI_FORMAT 枚举类型中被阐释.比方说

  1. DXGI_FORMAT_R32G32B32_FLOAT: 每个元素有3个32-bit浮点的元素组成.
  2. DXGI_FORMAT_R16G16B16A16_UNORM: 每个元素有4个16-bit被映射到[0,1]的元素组成
  3. DXGI_FORMAT_R32G32_UINT: 每个元素有2个32-bit unsigned int元素组成.
  4. DXGI_FORMAT_R8G8B8A8_UNORM: 每个元素有4个8-bit 无符号元素组成被映射到[0, 1].
  5. DXGI_FORMAT_R8G8B8A8_SNORM: 每个元素有4个8-bit 符号元素组成被映射到[-1, 1].
  6. DXGI_FORMAT_R8G8B8A8_SINT: 每个元素有4个8-bit 符号整型元素组成被映射到[-128,127].
  7. DXGI_FORMAT_R8G8B8A8_UINT: 每个元素有4个8-bit 无符号整型元素组成被映射到[0,255].

值得说明的是,纹理不一定存储颜色信息(即使命名是这样暗示的),比如DXGI_FORMAT_R32G32B32_FLOAT有3个浮点元素组成,能存储任意with浮点坐标3D向量,同时它们有无类型格式,我们只需存到内存中然后过会当这个纹理被绑定到管线时指定它们如何重新诠释这些数据(类似C++标准转换运算).比如这个DXGI_FORMAT_R16G16B16A16_TYPELESS,它保留4个元素组成但没有指定数据类型


4.1.4交换链和页面翻转(The Swap Chain and Page Flipping)

为了避免动画闪烁,最好在屏幕外的纹理(称为后台缓冲 back buffer)描绘好动画的整一帧.一旦后台缓存为给出的帧描绘完成,它就在屏幕作为一个完整的帧呈现出来.这样的话看屏幕的人就不会看着这一帧一点点地描绘出来.为了实现这一功能,两个纹理缓存是要通过硬件来保持的,一个是前台缓冲另一个是后台缓冲.前台缓冲存储正在显示的图像数据,同时另一帧在后台缓冲描绘,完成这一过程后就交换.(略)

前后台缓冲组成一个交换链,在Direct3D中,一个交换链用IDXGISwapChain接口来表示,该接口存储前后台缓存贴图,还提供为缓冲调整大小IDXGISwapChain::ResizeBuffers呈现IDXGISwapChain::Present的方法.

使用这两个缓冲称为双缓冲,三个则称为三缓冲(triple buffering).


4.1.5深度缓冲(Depth Buffering)

(介绍略)

深度缓冲是一个贴图,所以它必须有特定数据格式才被创建,用于深度缓冲的格式如下:

  1. DXGI_FORMAT_D32_FLOAT_S8X24_UINT: 指定一个32-bit 浮点深度缓冲, 有8-bits (无符号整型) 保留给模板缓冲被映射到 [0, 255] 和24-bits不用于填充.
  2. DXGI_FORMAT_D32_FLOAT: 指定一个32-bit 浮点深度缓冲.
  3. DXGI_FORMAT_D24_UNORM_S8_UINT: 指定一个无符号 24-bit 深度缓冲映射到 [0, 1] ,有8-bits (无符号整型) 保留给模板缓冲被映射到[0, 255].
  4. DXGI_FORMAT_D16_UNORM: 指定一个无符号16-bit depth深度缓冲被映射到[0,1].

4.1.6资源和说明符(Resources and Descriptors)

在渲染处理期间,GPU会在资源写入(比如各种buffer)也会从资源读取(比如说明表面外观的纹理,存储该场景的几何的3D坐标的缓冲).

在我们下达描绘指令前,我们需要绑定(bind)或连接(link)资源到在描绘的调用中正准备被引用的渲染管线.一些资源会在每次描绘调用中有所变化,所以我们需要每一次描绘更新绑定(必要时).

然而,GPU资源并非直接绑定,一份资源通过一个说明符的对象被引用,它可以被认为是一个轻量级的可以向GPU说明这个资源的结构体.重要的是,这是一级间接,给出一个资源说明符,GPU便能得到这个真实的资源的数据并且知道了关于它的必要的信息.我们通过指定说明符来绑定资源到将要在描绘调用中被引用的渲染管线.

为什么要走多一步间接的说明?原因是GPU资源是很大块的内存.资源一直很大块所以它们能被用于渲染管线的不同阶段,一个常见的栗子就是用一个纹理用于渲染目标(比如Direct3D描绘到纹理里面),过一会用作着色器资源(比如这个纹理会被采样以及当作为着色器的输入数据).一个资源本身是不会说它是否要被用作什么,同样,我们可能只要要连接一个资源的子区域到渲染管线,那给予了整块资源的我们该做什么?更不用说一个资源可以无类型格式地创建,所以GPU懂个JB格式.

所以我们引入说明符,除了确认资源数据,还说明这个资源给GPU听.

NOTE:view和说明符descriptor是同义词.


说明符有类型,该类型暗示这份资源将如何使用.

  1. CBV/SRV/UAV 说明符说明常量缓冲,着色器资源and
    无序访问view资源.
  2. Sampler 说明符说明采样器资源 (用于纹理生成).
  3. RTV 说明符说明渲染目标资源.
  4. DSV 说明符说明深度/模板资源.

说明符堆是一个说明符的数组,it is the memory backing for all the descriptors of a particular type your application uses.你需要一个为每种类型的说明符的单独的说明符堆.当然也可以创建同种类型的多个堆.

我们可以有多个说明符引用同一样的资源(不同子区域).像前面提到的,资源绑定到渲染管线,每一阶段,我们需要一个单独的说明符.比如,用纹理当作渲染目标和着色器资源时,需要生成两个说明符(RTV和SRV),类似地,如果创建一个无类型格式的资源,里面的元素可以看作是浮点或整型,这就需要两种说明符.

说明符需要在初始化时间内创建.


4.1.7多重采样原理

(介绍略)

在下一个section,我们要求填写一个 DXGI_SAMPLE_DESC 结构体,定义如下:

typedef struct DXGI_SAMPLE_DESC
{
    UINT Count;//指定每一个像素要采样的次数
    UINT Quality;//指定质量等级(与硬件相关)
} DXGI_SAMPLE_DESC;

我们可以使用 ID3D12Device::CheckFeatureSupport 来为一个给定的纹理格式询问质量等级和采样次数的数量:

typedef struct D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS {
    DXGI_FORMAT Format;
    UINT SampleCount;
    D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG Flags;
    UINT NumQualityLevels;
} D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS;


D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
msQualityLevels.Format = mBackBufferFormat;
msQualityLevels.SampleCount = 4;
msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;
msQualityLevels.NumQualityLevels = 0;
ThrowIfFailed(md3dDevice->CheckFeatureSupport(
    D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
    &msQualityLevels,//既是输入参数也是输出参数
    sizeof(msQualityLevels)));

NOTE: 交换链缓冲和深度缓冲需要填写DXGI_SAMPLE_DESC.后台缓冲和深度缓存一定要生成时有相同的多重采样设置.


4.1.9特性等级

Direct3D 11介绍了特性等级的概念(通过 D3D_FEATURE_LEVEL 枚举类型来表示):

enum D3D_FEATURE_LEVEL
{
    D3D_FEATURE_LEVEL_9_1 = 0x9100,
    D3D_FEATURE_LEVEL_9_2 = 0x9200,
    D3D_FEATURE_LEVEL_9_3 = 0x9300,
    D3D_FEATURE_LEVEL_10_0 = 0xa000,
    D3D_FEATURE_LEVEL_10_1 = 0xa100,
    D3D_FEATURE_LEVEL_11_0 = 0xb000,
    D3D_FEATURE_LEVEL_11_1 = 0xb100
}D3D_FEATURE_LEVEL;

4.1.10DirectX Graphics Infrastructure

DXGI是一种与 Direct3D 一起使用的 API.DXGI 是用于一些图形通用相关任务的.

打个比方,2D渲染API需要交换链和页面翻转以达到和3D渲染API一样的平滑动画,因此交换链接口IDXGISwapChain确实是DXGI API的一部分.DXGI处理其他通用图形功能像全屏模式切换,列出设备信息等,甚至定义了多种支持的图面格式(surface formats).

其中一个重要的 DXGI 接口是IDXGIFactory,主要用于创建 IDXGISwapChain 接口 和列出显示适配器.显示适配器实现图形功能,经常是硬件(比如说显卡啦),然而也可能是软件显示适配器(系统给出,如Microsoft Basic Render Driver).

一个适配器用 IDXGIAdapter 接口表示,通过以下代码可以罗列出一个系统上的所有适配器:

void D3DApp::LogAdapters()
{
    UINT i = 0;
    IDXGIAdapter* adapter = nullptr;
    std::vector<IDXGIAdapter*> adapterList;
    while(mdxgiFactory->EnumAdapters(i, &adapter) != DXGI_ERROR_NOT_FOUND)
    {
        DXGI_ADAPTER_DESC desc;
        adapter->GetDesc(&desc);
        std::wstring text = L”***Adapter: “;
        text += desc.Description;
        text += L”\n”;
        OutputDebugString(text.c_str());
        adapterList.push_back(adapter);
        ++i;
    }
    for(size_t i = 0; i < adapterList.size(); ++i)
     {
        LogAdapterOutputs(adapterList[i]);
        ReleaseCom(adapterList[i]);
    }
}

输出的示例:

*Adapter: NVIDIA GeForce GTX 760
*Adapter: Microsoft Basic Render Driver

一个系统能有多个显示器.显示器是一种显示输出设备的栗子.一个输出设备由 IDXGIOutput 接口表示.每一适配器与一单子的输出设备相关联.我们可以通过下面代码来列出一个适配器相关联的所有输出设备:

void D3DApp::LogAdapterOutputs(IDXGIAdapter* adapter)
{
    UINT i = 0;
    IDXGIOutput* output = nullptr;
    while(adapter->EnumOutputs(i, &output) != DXGI_ERROR_NOT_FOUND)
    {
        DXGI_OUTPUT_DESC desc;
        output->GetDesc(&desc);
        std::wstring text = L”***Output: “;
        text += desc.DeviceName;
        text += L”\n”;
        OutputDebugString(text.c_str());
        LogOutputDisplayModes(output,
        DXGI_FORMAT_B8G8R8A8_UNORM);
        ReleaseCom(output);
        ++i;
    }
}

注意 Microsoft Basic Render Driver 没有显示输出设备

每个显示器由一大堆支持的显示模式,一种显示模式可参考 DXGI_MODE_DESC 里的数据

typedef struct DXGI_MODE_DESC
{
    UINT Width; // Resolution width
    UINT Height; // Resolution height
    DXGI_RATIONAL RefreshRate;
    DXGI_FORMAT Format; // Display format
    DXGI_MODE_SCANLINE_ORDER ScanlineOrdering;
    //Progressive vs. interlaced
    DXGI_MODE_SCALING Scaling; 
    // How the image is stretched over the monitor.
} DXGI_MODE_DESC;
typedef struct DXGI_RATIONAL
{
    UINT Numerator;
    UINT Denominator;
} DXGI_RATIONAL;
typedef enum DXGI_MODE_SCANLINE_ORDER
{
    DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED = 0,
    DXGI_MODE_SCANLINE_ORDER_PROGRESSIVE = 1,
    DXGI_MODE_SCANLINE_ORDER_UPPER_FIELD_FIRST = 2,
    DXGI_MODE_SCANLINE_ORDER_LOWER_FIELD_FIRST = 3
} DXGI_MODE_SCANLINE_ORDER;
typedef enum DXGI_MODE_SCALING
{
    DXGI_MODE_SCALING_UNSPECIFIED = 0,
    DXGI_MODE_SCALING_CENTERED = 1,
    DXGI_MODE_SCALING_STRETCHED = 2
} DXGI_MODE_SCALING;

固定一种显示模式格式,我们可以等到又一单子所有支持该模式的输出设备:

void D3DApp::LogOutputDisplayModes(IDXGIOutput* output, DXGI_FORMAT format)
{
    UINT count = 0;
    UINT flags = 0;
    // Call with nullptr to get list count.
    output->GetDisplayModeList(format, flags, &count, nullptr);
    std::vector<DXGI_MODE_DESC> modeList(count);
    output->GetDisplayModeList(format, flags, &count, &modeList[0]);
    for(auto& x : modeList)
    {
        UINT n = x.RefreshRate.Numerator;
        UINT d = x.RefreshRate.Denominator;
        std::wstring text =
        L”Width = ” + std::to_wstring(x.Width) + L” ” +
        L”Height = ” + std::to_wstring(x.Height) + L” ” +
        L”Refresh = ” + std::to_wstring(n) + L”/” +
        std::to_wstring(d) +
        L”\n”;
        ::OutputDebugString(text.c_str());//
    }
}

*Output: .DISPLAY2

Width = 1920 Height = 1080 Refresh = 59950/1000
Width = 1920 Height = 1200 Refresh = 59950/1000


4.1.11检查特性支持

之前学过的 ID3D12Device::CheckFeatureSupport 原型如下

HRESULT ID3D12Device::CheckFeatureSupport(
    D3D12_FEATURE Feature,
    void *pFeatureSupportData,
    UINT FeatureSupportDataSize);

1.Feature: D3D12_FEATURE 枚举类型的成员之一,确认我们想要检查的特性类型:

  1. D3D12_FEATURE_D3D12_OPTIONS: 检查多种DX12特性的支持
  2. D3D12_FEATURE_ARCHITECTURE: 检查硬件架构特性的支持
  3. D3D12_FEATURE_FEATURE_LEVELS: 检查特性等级的支持
  4. D3D12_FEATURE_FORMAT_SUPPORT: 检查对于给出的纹理格式的支持 (比方说,这个格式可不可以用作渲染目标混合什么的).
  5. D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS: 检查多重采样特性的支持

2.pFeatureSupportData: 指向一个数据结构的指针以找回特性支持信息.你是用的数据结构类型依赖于你为 Feature 参数指定的是什么:

  1. 如果你指定 D3D12_FEATURE_D3D12_OPTIONS, 然后传入一个D3D12_FEATURE_DATA_D3D12_OPTIONS 的实例.
  2. 如果你指定 D3D12_FEATURE_ARCHITECTURE, 然后传入一个

    D3D12_FEATURE_DATA_ARCHITECTURE 的实例.
  3. 如果你指定 D3D12_FEATURE_FEATURE_LEVELS, 然后传入一个

     D3D12_FEATURE_DATA_FEATURE_LEVELS 的实例.
  4. 如果你指定 D3D12_FEATURE_FORMAT_SUPPORT, 然后传入一个

     D3D12_FEATURE_DATA_FORMAT_SUPPORT 的实例.
  5. 如果你指定 D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,

    然后传入一个
    D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS 的实例.

3.FeatureSupportDataSize: 传入 pFeatureSupportData 的数据结构的大小

作为一个示范,看下如何检查一种支持特性:

typedef struct D3D12_FEATURE_DATA_FEATURE_LEVELS {
    UINT NumFeatureLevels;
    const D3D_FEATURE_LEVEL *pFeatureLevelsRequested;
    D3D_FEATURE_LEVEL MaxSupportedFeatureLevel;
} D3D12_FEATURE_DATA_FEATURE_LEVELS;
D3D_FEATURE_LEVEL featureLevels[3] =
{
    D3D_FEATURE_LEVEL_11_0, // First check D3D 11 support
    D3D_FEATURE_LEVEL_10_0, // Next, check D3D 10 support
    D3D_FEATURE_LEVEL_9_3 // Finally, check D3D 9.3 support
};
D3D12_FEATURE_DATA_FEATURE_LEVELS featureLevelsInfo;
featureLevelsInfo.NumFeatureLevels = 3;
featureLevelsInfo.pFeatureLevelsRequested =
featureLevels;
md3dDevice->CheckFeatureSupport(
    D3D12_FEATURE_FEATURE_LEVELS,
    &featureLevelsInfo,
    sizeof(featureLevelsInfo));

4.1.12驻留

(介绍略)

因为是自动管理,手动撸的自行翻书


4.2CPU/GPU 互动

在DX12,命令队列(command queue)通过 ID3D12CommandQueue 接口表示(通过填写一个说明该队列的 D3D12_COMMAND_QUEUE_DESC 结构体然后调用ID3D12Device::CreateCommandQueue)

创建方法如下:

Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
ThrowIfFailed(md3dDevice->CreateCommandQueue(
    &queueDesc, IID_PPV_ARGS(&mCommandQueue)));

IID_PPV_ARGS辅助宏定义如下

#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)
//其中 __uuidof(**(ppType))识别(?)(**(ppType))的COM ID,在上面的代码就是指 ID3D12CommandQueue,而IID_PPV_ARGS_Helper函数则把ppType转换成void**.我们在这本书使用这个宏因为很多Direct3D 12 API 调用由一个需要我们正在创建的实例的COM ID和接收void**的参数

这个接口的其中一个主要方法是把在命令列表的命令加入到命令队列里的 ExecuteCommandLists:

void ID3D12CommandQueue::ExecuteCommandLists(
    UINT Count,// 数组里的命令列表的数目
    ID3D12CommandList *const *ppCommandLists);    
    // 指向命令列表的队列的第一个元素的指针

该命令列表会从第一个数组元素开始依序执行

一个对于图形的命令列表由 ID3D12GraphicsCommandList 表示,它继承自 ID3D12CommandList 接口.ID3D12GraphicsCommandList 接口由多个把命令加入命令列表的方法.比如下面的示例会加入 设置视区(viewport) 清除渲染目标view 和 下达一个描绘调用:

// 指向 ID3D12CommandList 的 mCommandList 指针
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->ClearRenderTargetView(mBackBufferView,Colors::LightSteelBlue, 0, nullptr);
mCommandList->DrawIndexedInstanced(36, 1, 0, 0, 0);

这些方法的名字暗示这些命令会立即执行,然而并不是这样.上面的代码仅仅是把命令加入到命令列表中.而ExecuteCommandLists 方法则把命令加入到命令队列中,GPU从队列中处理命令.

当我们完成添加(把命令加入到命令列表中)后,我们必须指出通过 ID3D12GraphicsCommandList::Close 的方法调用我们完成记录命令

// Done recording commands.
mCommandList->Close();

在把列表传入 ID3D12CommandQueue::ExecuteCommandLists 前,命令列表必须关闭(?).

与一个命令列表相关联的是一个称为 ID3D12CommandAllocator 的内存辅助类(?).当命令被记录到命令列表中,它们会被存储到一个关联的命令分配器(allocator).当一个命令通过 ID3D12CommandQueue::ExecuteCommandLists 被执行,命令队列将会引用在分配器里的命令.一个命令分配器从 ID3D12Device 里创建:

HRESULT ID3D12Device::CreateCommandAllocator(
    D3D12_COMMAND_LIST_TYPE type,
    REFIID riid,
    void **ppCommandAllocator);

1.type: 命令列表的类型与它的分配器相关联,书里我们用2种命令类型.

1.D3D12_COMMAND_LIST_TYPE_DIRECT: 存储一单子将要直接被GPU执行的命令.

2.D3D12_COMMAND_LIST_TYPE_BUNDLE:指定一个命令列表表示为一个bundle.(BLABLA略)

2.riid: 我们想要创建的 ID3D12CommandAllocator 接口的 COM ID.

3.ppCommandAllocator:输出一个指向已创建的命令分配器的指针.

命令列表同样由 ID3D12Device 创建:

HRESULT ID3D12Device::CreateCommandList(
    UINT nodeMask,
    D3D12_COMMAND_LIST_TYPE type,
    ID3D12CommandAllocator *pCommandAllocator,
    ID3D12PipelineState *pInitialState,
    REFIID riid,
    void **ppCommandList);

1.nodeMask: 对于单GPU系统,设置成0.否则的话,节点掩码确认与该命令列表相关联的物理GPU.

2.type: 要么 COMMAND_LIST_TYPE_DIRECT,要么 D3D12COMMAND_LIST_TYPE_BUNDLE.

3.pCommandAllocator: 将要与被创建列表相关联的分配器.该分配器类型一定要和列表相匹配.

4.pInitialState:指定一个命令列表的初始化管线.对于bundles可以是null,在特殊情况下.....

5.riid: 我们想要创建的 ID3D12CommandList 接口的 COM ID.

6.ppCommandList: 输出一个被创建的命令列表的指针.

你可以用 ID3D12Device::GetNodeCount 方法来询问这个系统里 GPU 适配器节点的数目.

你可以创建多个关联相同分配器的命令列表,但你不能同一时间内进行记录.也就是说,所有的命令列表必须关闭除了某个命令我们正在进行记录.因此,来自一个给出的命令列表的所有命令将会被连续地添加到分配器中.注意当一个命令列表被创建或者重设时,它处在"open"状态.所以如果我们尝试在一行中创建有同一分配器两个命令列表时,我们会尝到错误的酸爽.

在我们已经调用了 ID3D12CommandQueue::ExecuteCommandList(C) 之后,重新回收使用C的内部内存去通过调用 ID3D12CommandList::Reset 方法来记录一个新的命令集合是一种安全的方法.此方法的参数和 ID3D12Device::CreateCommandList 里相对应的参数是一样的:

HRESULT ID3D12CommandList::Reset(
    ID3D12CommandAllocator *pAllocator,
    ID3D12PipelineState *pInitialState);

此方法安置命令列表入相同的状态仿佛它就是刚创建那样,但允许我们重新利用内部的内存以及回避收回之前的命令列表和分配一个新的内存.注意重新设置命令列表并不影响在命令队列中的命令,因为关联的命令分配器在内存里头仍然有命令队列中引用的命令.

在我们提交了渲染命令作为整一帧到 GPU 后,我们要为下一帧重新利用内存分配器里头的内存. ID3D12CommandAllocator::Reset 方法可以这样用:

HRESULT ID3D12CommandAllocator::Reset(void);

这种做法和调用 std::vector::clear 是相似的(调整一个向量的大小 size 变回0 但保持现有的规模 capacity),但是,因为命令列表可能正在引用分配器里的数据,一个命令分配器一定不能重设直到我们确定 GPU 已经完成执行所有的分配器里的命令,这要怎么做老子现在不想说给你听.


4.2.2CPU/GPU同步

(介绍略)

一个解决方法(塞车)是强制CPU等到GPU完成处理所有队列中的命令直到一个指定的fence点,此举我们江湖人称 flushing the command queue.我们可以通过使用 fence 来实现这一点.fence 由 ID3D12Fence 接口表示,它用于GPU和CPU间的同步.


fence 对象可以这样创建:

HRESULT ID3D12Device::CreateFence(
    UINT64 InitialValue,
    D3D12_FENCE_FLAGS Flags,
    REFIID riid,
    void **ppFence);
// 栗子
ThrowIfFailed(md3dDevice->CreateFence(
    0,
    D3D12_FENCE_FLAG_NONE,
    IID_PPV_ARGS(&mFence)));

fence 对象保持为一个 UINT64 的值,这仅仅是一个用于确认时间的 fence 点的整型.我们从0开始,每时每刻我们需要mark一个新的 fence 点时, 只需增加这个整型.现在,由以下代码来展示如何使用一个fence来 flush the command queue:

UINT64 mCurrentFence = 0;
void D3DApp::FlushCommandQueue()
{
// 提前该 fence 值来标记命令直到此 fence 点.
    mCurrentFence++;
// 增加一个命令队列的指示来设置一个新的 fence 点.
// 因为我们正处于 GPU 的时间线上, 新的 fence 点不会设置
 //直到 GPU 完成处理所有比 Signal() 优先的命令.
    ThrowIfFailed(mCommandQueue->Signal(mFence.Get(),
        mCurrentFence));
// 等到GPU完成所有直到该 fence 点的命令.
    if(mFence->GetCompletedValue() < mCurrentFence)
    {
        HANDLE eventHandle = CreateEventEx(
            nullptr, false,
            false, EVENT_ALL_ACCESS);
// 当 GPU 碰到此刻的 fence 点就触发事件.
        ThrowIfFailed(mFence->SetEventOnCompletion(
            mCurrentFence, eventHandle));
// 等待直到GPU碰到此刻的事件触发的 fence.
    WaitForSingleObject(eventHandle, INFINITE);
    CloseHandle(eventHandle);
    }
}

Figure 4.8可参考.


4.2.3资源过渡(Resource Transitions)

资源存在状态(防止某种错误操作的产生),一般当资源被创建时状态为default,直到应用告诉Direct任意资源过渡.

资源过渡是通过在命令列表设置一个过渡资源栅(transition resource barriers)的数组来指定的.这个数组防止你想要用一个API调用来过渡多个资源.一个资源栅通过 D3D12_RESOURCE_BARRIER_DESC 结构体来表示.下面的辅助函数(定义在d3dx12.h)返回一个对给定资源的过渡资源栅说明,以及指定前后状态.

struct CD3DX12_RESOURCE_BARRIER : public D3D12_RESOURCE_BARRIER
{
    // […] convenience methods
    static inline CD3DX12_RESOURCE_BARRIER Transition(
        _In_ ID3D12Resource* pResource,
        D3D12_RESOURCE_STATES stateBefore,
        D3D12_RESOURCE_STATES stateAfter,
        UINT subresource =
        D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,
        D3D12_RESOURCE_BARRIER_FLAGS flags =
        D3D12_RESOURCE_BARRIER_FLAG_NONE)
    {
        CD3DX12_RESOURCE_BARRIER result;
        ZeroMemory(&result, sizeof(result));
        D3D12_RESOURCE_BARRIER &barrier = result;
        result.Type =
        D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
        result.Flags = flags;
        barrier.Transition.pResource = pResource;
        barrier.Transition.StateBefore = stateBefore;
        barrier.Transition.StateAfter = stateAfter;
        barrier.Transition.Subresource = subresource;
        return result;
    }
    // […] more convenience methods
};

CD3DX12RESOURCE_BARRIER 扩展了 D3D12RESOURCE_BARRIER_DESC 和添加了方便的方法.大多数 Direct3D 结构体有扩展辅助变量.CD3DX12 的变量全都定义在 d3dx12.h .该文件并非核心的 DirectX 12 SDK 的一部分,但是可在 MSDN 下载,方便起见,这本书源码的 Common 目录里也有.

又是一个栗子:

mCommandList->ResourceBarrier(1,&CD3DX12_RESOURCE_BARRIER::Transition(
    CurrentBackBuffer(),
    D3D12_RESOURCE_STATE_PRESENT,
    D3D12_RESOURCE_STATE_RENDER_TARGET));

这份代码过渡一个纹理从呈现状态到一个渲染目标状态来呈现我们看到的屏幕上的图像.观察到资源栅被添加到命令列表.你可以把它认为资源栅过渡作为一个命令本身指示给GPU听这个资源正在被过渡,因此它才能采取必要的措施来阻止资源危机(当要执行接下来的命令时).


4.2.4多线程下的命令

(略)


4.3初始化DIRECT3D

接下来要为我们的demo的框架进行初始化Direct3D,这是一个很J8长的过程,但只需要一次就能搞定.步骤如下:

  1. 使用 D3D12CreateDevice 函数来创建 ID3D12Device.
  2. 创建一个 ID3D12Fence 对象和询问说明符的大小.
  3. 检查4X MSAA 质量等级的支持.
  4. 创建命令队列,命令列表分配器和主要的命令列表.
  5. 说明和创建交换链.
  6. 创建应用请求的说明符堆.
  7. 调整后台缓冲的大小和创建一个渲染目标视图到后台缓冲.
  8. 创建深度/模板缓冲和与之相关联的深度/模板视图.
  9. 设置视区和裁剪矩阵(scissor rectangles).

4.3.1创建设备

Direct3D 12设备(ID3D12Device)创建如下:

HRESULT WINAPI D3D12CreateDevice(
    IUnknown* pAdapter,
    D3D_FEATURE_LEVEL MinimumFeatureLevel,
    REFIID riid, // Expected: ID3D12Device
    void** ppDevice );
  1. pAdapter: 指定我们想要创建的要设备要呈现的显示适配器,指定为null来使用主(primary)显示适配器.
  2. MinimumFeatureLevel:最小特性等级支持.如果低于此等级则无法创建设备.在我们的框架中,我们指定 D3D_FEATURE_LEVEL11_0.
  3. riid: 我们想要创建的 ID3D12Device 接口的 COM ID.
  4. ppDevice: 返回创建好的设备.

栗子:

#if defined(DEBUG) || defined(_DEBUG)
// Enable the D3D12 debug layer.
{
    ComPtr<ID3D12Debug> debugController;
    ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
    debugController->EnableDebugLayer();
}
#endif
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));
// Try to create hardware device.
HRESULT hardwareResult = D3D12CreateDevice(
    nullptr, // default adapter
    D3D_FEATURE_LEVEL_11_0,
    IID_PPV_ARGS(&md3dDevice));
// Fallback to WARP device.(软件,win7最高支持DX10.1,win8及以上至少11.1)
if(FAILED(hardwareResult))
{
    ComPtr<IDXGIAdapter> pWarpAdapter;
    ThrowIfFailed(mdxgiFactory-
        >EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));
    ThrowIfFailed(D3D12CreateDevice(
        pWarpAdapter.Get(),
        D3D_FEATURE_LEVEL_11_0,
        IID_PPV_ARGS(&md3dDevice)));
}

为了创建 WARP 适配器,我们需要创建一个 IDXGIFactory4 对象因此我们可以枚举 warp 适配器:

ComPtr<IDXGIFactory4> mdxgiFactory;
CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory));
mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter));

mdxgiFactory 对象同样可用于创建我们的交换链因为它是 DXGI 的一部分.


4.3.2创建fence和说明符大小

不同的GPU有不同的说明符大小,因此我们需要询问它们的信息.我们贮存说明符大小因此当我们因多种说明符类型时需要它时就可以获得:

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);
mCbvSrvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(
    D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

4.3.3检查 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;
ThrowIfFailed(md3dDevice->CheckFeatureSupport(
    D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
    &msQualityLevels,
    sizeof(msQualityLevels)));
m4xMsaaQuality = msQualityLevels.NumQualityLevels;
assert(m4xMsaaQuality > 0 && “Unexpected MSAA quality level.”);

4.3.4创建命令队列和命令列表

回忆之前的内容,命令队列用 ID3D12CommandQueue 接口表示,命令分配器用 ID3D12CommandAllocator 接口表示,以及命令列表用 ID3D12GraphicsCommandList 接口表示.

接下来我们一并创建这三个东西:

ComPtr<ID3D12CommandQueue> mCommandQueue;
ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
ComPtr<ID3D12GraphicsCommandList> mCommandList;
void D3DApp::CreateCommandObjects()
{
    D3D12_COMMAND_QUEUE_DESC queueDesc = {};
    queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
    queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
    ThrowIfFailed(md3dDevice->CreateCommandQueue(
        &queueDesc, IID_PPV_ARGS(&mCommandQueue)));
    ThrowIfFailed(md3dDevice->CreateCommandAllocator(
        D3D12_COMMAND_LIST_TYPE_DIRECT,
        IID_PPV_ARGS(mDirectCmdListAlloc.GetAddressOf())));
    ThrowIfFailed(md3dDevice->CreateCommandList(
        0,
        D3D12_COMMAND_LIST_TYPE_DIRECT,
        mDirectCmdListAlloc.Get(), // Associated command allocator
        nullptr, // Initial PipelineStateObject创建时null
        IID_PPV_ARGS(mCommandList.GetAddressOf())));
    // Start off in a closed state. This is because the first time we
    // refer to the command list we will Reset it, and it needs to be
    // closed before calling Reset.
    mCommandList->Close();
}

4.3.5说明和创建交换链

交换链的创建需要填写一个 DXGI_SWAP_CHAIN_DESC 结构体的示例,它说明了该交换链的特性.定义如下:

typedef struct DXGI_SWAP_CHAIN_DESC
{
    DXGI_MODE_DESC BufferDesc;
    DXGI_SAMPLE_DESC SampleDesc;
    DXGI_USAGE BufferUsage;
    UINT BufferCount;
    HWND OutputWindow;
    BOOL Windowed;
    DXGI_SWAP_EFFECT SwapEffect;
    UINT Flags;
} DXGI_SWAP_CHAIN_DESC;

DXGI_MODE_DESC 是另一个结构体,定义如下:

typedef struct DXGI_MODE_DESC
{
    UINT Width; // Buffer resolution width
    UINT Height; // Buffer resolution height
    DXGI_RATIONAL RefreshRate;
    DXGI_FORMAT Format; // Buffer display format
    DXGI_MODE_SCANLINE_ORDER ScanlineOrdering;
    //Progressive vs. interlaced
    DXGI_MODE_SCALING Scaling; // How the image is stretched
    // over the monitor.
} DXGI_MODE_DESC;
  1. BufferDesc: 该结构体解释我们想要创建的后台缓冲的特性. 我们关心的其主要特性是宽和高,以及像素格式.
  2. SampleDesc: 多重采样的次数和质量等级(§4.1.8).对于单次采样, 指定采样次数为1以及质量等级为0.
  3. BufferUsage: 指定 DXGI_USAGE_RENDER_TARGET_OUTPUT 因为我们正要被要被渲染到后台缓冲(比如当作一个渲染目标).
  4. BufferCount: 用于交换链的后台缓存的数量;为双缓冲指定2.
  5. OutputWindow: 我们正要渲染到的窗口的句柄.
  6. Windowed: 指定 true来运行窗口模式或者 false 来进行全屏模式.
  7. SwapEffect: 指定 DXGI_SWAP_EFFECT_FLIP_DISCARD.
  8. Flags: 可选择的 flags. 如果你指定
    DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH, 然后当应用切换到全屏模式时, 它将选择一个对于当前应用窗口尺寸的最佳显示模式.如果flag没有指定, 然后当应用切换到全屏模式时, 它会使用当前的桌面显示模式.

当我们说明交换链以后,我们就可以通过 IDXGIFactory::CreateSwapChain 方法来创建它:

HRESULT IDXGIFactory::CreateSwapChain(
    IUnknown *pDevice, // Pointer to ID3D12CommandQueue.
    DXGI_SWAP_CHAIN_DESC *pDesc, // Pointer to swap chain description.
    IDXGISwapChain **ppSwapChain);// Returns created swap chain interface
DXGI_FORMAT mBackBufferFormat =
DXGI_FORMAT_R8G8B8A8_UNORM;
void D3DApp::CreateSwapChain()
{
// Release the previous swapchain we will be
recreating.
mSwapChain.Reset();
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 = m4xMsaaState ? 4 : 1;
sd.SampleDesc.Quality = m4xMsaaState ?
(m4xMsaaQuality - 1) : 0;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.BufferCount = SwapChainBufferCount;
sd.OutputWindow = mhMainWnd;
sd.Windowed = true;
sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
// Note: Swap chain uses queue to perform flush.
ThrowIfFailed(mdxgiFactory->CreateSwapChain(
mCommandQueue.Get(),
&sd,
 mSwapChain.GetAddressOf()));
}

4.3.6创建说明符堆

说明符堆由 ID3D12DescriptorHeap 接口表示.一个堆由 ID3D12Device::CreateDescriptorHeap 方法创建.在书中我们需要 SwapChainBufferCount 许多渲染目标视图(RTVs)来说明在交换链里的我们将要渲染的缓冲资源,以及一个深度/模板视图(DSV)来为深度测试说明深度/模板缓冲资源.因此,我们需要一个堆来储存 SwapChainBufferCount RTVs,以及需要一个堆来储存一个 DSV:

ComPtr<ID3D12DescriptorHeap> mRtvHeap;
ComPtr<ID3D12DescriptorHeap> mDsvHeap;
void D3DApp::CreateRtvAndDsvDescriptorHeaps()
{
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
rtvHeapDesc.NumDescriptors = SwapChainBufferCount;
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
rtvHeapDesc.NodeMask = 0;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
&rtvHeapDesc,
IID_PPV_ARGS(mRtvHeap.GetAddressOf())));
D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;
dsvHeapDesc.NumDescriptors = 1;
dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
dsvHeapDesc.NodeMask = 0;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
&dsvHeapDesc,
IID_PPV_ARGS(mDsvHeap.GetAddressOf())));
//在我们的应用框架中,定义
static const int SwapChainBufferCount = 2;
int mCurrBackBuffer = 0;

标签: none

添加新评论