Chapter 4

早期的,基本乱翻,随便看

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 )

它可以被认为是一个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;

我们使用 mCurrBackBuffer 保持对当前后台缓冲索引的跟踪.

创建好堆以后,我们需要能够访问堆内存储的说明符.我们的应用通过句柄来引用说明符.堆内第一个说明符的句柄 是通过 ID3D12DescriptorHeap::GetCPUDescriptorHandleForHeapStart 方法来获得的.下面的函数可分别获得当前后台缓冲 RTV 和 DSV:

D3D12_CPU_DESCRIPTOR_HANDLE
CurrentBackBufferView()const
{
// CD3DX12 constructor to offset to the RTV of the current back buffer.
    return CD3DX12_CPU_DESCRIPTOR_HANDLE(
        mRtvHeap->GetCPUDescriptorHandleForHeapStart(),// handle start
        mCurrBackBuffer, // index to offset
        mRtvDescriptorSize); // byte size of descriptor
}
D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView()const
{
    return mDsvHeap->GetCPUDescriptorHandleForHeapStart();
}

我们现在看到了需要说明符大小的栗子.为了偏移(offset to)到当前后台缓冲 RTV 说明符,我们需要知道 RTV 说明符字节大小.


4.3.7创建渲染目标视图

正如 §4.1.6 所说的,我们间接地绑定,我们必须创建一个资源视图(说明符)到资源和绑定该视图到渲染管线阶段.特别地,为了绑定后台缓冲到渲染管线的输出合并阶段(所以 Direct3D 才能渲染到这里去),我们需要创建一个渲染目标视图到后台缓冲.第一步是获得存储在交换链中的缓冲资源:

HRESULT IDXGISwapChain::GetBuffer(
    UINT Buffer,
    REFIID riid,
    void **ppSurface);
  1. Buffer: 一个确认我们想要获得的特定的(防止多个)后台缓冲的索引.
  2. riid: 我们想要获得的一个指针指向的 ID3D12Resource 接口的 COM ID.
  3. ppSurface: 返回一个指向 ID3D12Resource 的指针(代表后台缓冲).

IDXGISwapChain::GetBuffer 接口的调用增加了到后台缓冲的 COM 引用计数,所以当我们完成时我们必须释放它.这可以是自动完成的,如果你用的是 ComPtr 的话.

为了创建渲染目标视图,我们使用 ID3D12Device::CreateRenderTargetView 方法:

void ID3D12Device::CreateRenderTargetView(
    ID3D12Resource *pResource,
    const D3D12_RENDER_TARGET_VIEW_DESC *pDesc,
    D3D12_CPU_DESCRIPTOR_HANDLE DestDescriptor);
  1. pResource: 指定将会用作渲染目标的资源,在上文指的是后台缓冲 (也就是说,我们正要创建一个渲染目标视图到后台缓冲).
  2. pDesc: 一个指向 D3D12_RENDER_TARGET_VIEW_DESC 的指针.此外,该结构体说明了在资源里的元素的数据类型(格式).如果这个资源存在类型格式(不是无类型的),该参数可以为 null,这指明要创建一个带有相应格式的视图到该资源的第一层 mipmap level(后台缓冲只有一层).因为我们指定了我们的后台缓冲的类型,我们指定为null.(注:原文可能曲解)
  3. DestDescriptor: 存储着创建的渲染目标视图的说明符句柄.

下面又是调用这两个我们为交换链的每个缓冲创建一个 RTV 的方法的栗子:

ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(
    mRtvHeap->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < SwapChainBufferCount; i++)
{
// Get the ith buffer in the swap chain.
    ThrowIfFailed(mSwapChain->GetBuffer(
        i, IID_PPV_ARGS(&mSwapChainBuffer[i])));
// Create an RTV to it.
    md3dDevice->CreateRenderTargetView(
        mSwapChainBuffer[i].Get(), 
        nullptr,
        rtvHeapHandle);
// Next entry in heap.
    rtvHeapHandle.Offset(1, mRtvDescriptorSize);
}

4.3.8创建深度/模板缓冲和视图

纹理是一类 GPU 资源,所以我们通过填写一个说明纹理资源的 D3D12_RESOURCE_DESC 结构体来创建它,然后调用 ID3D12Device::CreateCommittedResource 方法,结构体定义如下:

typedef struct D3D12_RESOURCE_DESC
{
    D3D12_RESOURCE_DIMENSION Dimension;
    UINT64 Alignment;
    UINT64 Width;
    UINT Height;
    UINT16 DepthOrArraySize;
    UINT16 MipLevels;
    DXGI_FORMAT Format;
    DXGI_SAMPLE_DESC SampleDesc;
    D3D12_TEXTURE_LAYOUT Layout;
    D3D12_RESOURCE_MISC_FLAG MiscFlags;
} D3D12_RESOURCE_DESC;
  1. Dimension: 资源的尺寸大小, 它是下面枚举类型的其中之一:

    enum D3D12_RESOURCE_DIMENSION
    {
        D3D12_RESOURCE_DIMENSION_UNKNOWN = 0,
        D3D12_RESOURCE_DIMENSION_BUFFER = 1,
        D3D12_RESOURCE_DIMENSION_TEXTURE1D = 2,
        D3D12_RESOURCE_DIMENSION_TEXTURE2D = 3,
        D3D12_RESOURCE_DIMENSION_TEXTURE3D = 4
    } D3D12_RESOURCE_DIMENSION;
  2. Width: 纹理单元内纹理的宽度.对于缓冲资源,这是在缓冲内的字节的数量.

  3. Height: 纹理单元内纹理的高度.

  4. DepthOrArraySize: 纹理单元内纹理的深度,或者纹理数组大小(1D 和 2D 纹理),不可以是3D.

  5. MipLevels: 多级渐远纹理等级的数目,我们会在第九章纹理生成讨论它,对于创建深度/模板缓冲,我们的纹理只需要一层多级渐远纹理等级.

  6. Format: 指定纹理单元格式的 DXGI_FORMAT 枚举类型的一个成员.对于深度/模板缓冲,它需要 §4.1.5 的其中一种格式.

  7. SampleDesc: 多重采样和质量等级的数目,从前面章节我们知道 4X MSAA 使用一个4倍大于屏幕分辨率的后台缓冲和深度缓冲,是为了存储每个子像素的颜色和深度/模板信息.因此,用于深度/模板缓冲的多重采样的设置必须与用于渲染目标的设置一致.

  8. Layout: 指定纹理布局的 D3D12_TEXTURE_LAYOUT 枚举类型的一个成员.现在我们不必担心布局这回事,我们可以指定为 D3D12TEXTURE_LAYOUT_UNKNOWN.

  9. MiscFlags: 其他资源的flags.对于一个深度/模板缓冲资源,指定为 D3D12_RESOURCE_MISC_DEPTH_STENCIL.

GPU资源存在于堆中,它们是一块带有特定性质的显存. ID3D12Device::CreateCommittedResource 方法创建和移交(commit to)一份资源到特定的带有我们指定特性的堆中.

HRESULT ID3D12Device::CreateCommittedResource(
    const D3D12_HEAP_PROPERTIES *pHeapProperties,
    D3D12_HEAP_MISC_FLAG HeapMiscFlags,
    const D3D12_RESOURCE_DESC *pResourceDesc,
    D3D12_RESOURCE_USAGE InitialResourceState,
    const D3D12_CLEAR_VALUE *pOptimizedClearValue,
    REFIID riidResource,
    void **ppvResource);
typedef struct D3D12_HEAP_PROPERTIES {
    D3D12_HEAP_TYPE Type;
    D3D12_CPU_PAGE_PROPERTIES CPUPageProperties;
    D3D12_MEMORY_POOL MemoryPoolPreference;
    UINT CreationNodeMask;
    UINT VisibleNodeMask;
} D3D12_HEAP_PROPERTIES;

1.pHeapProperties:我们想要移交资源到堆中的性质.目前来说,主要的性质我们需要关心的是 D3D12_HEAP_TYPE,它能是 D3D12_HEAP_PROPERTIES 枚举类型中的一个成员:

  1. D3D12_HEAP_TYPE_DEFAULT: 默认堆.这个我们移交资源的堆仅会被 GPU 访问.把 深度/模板缓冲作为一个栗子:GPU从深度/模板缓冲中读写.CPU却不需要访问它,所以深度/模板缓冲放置到默认堆就好了.
  2. D3D12_HEAP_TYPE_UPLOAD: 上传堆.这个我们移交资源的堆需要我们从 CPU 到 GPU 资源上传数据.
  3. D3D12_HEAP_TYPE_READBACK: 回读(read-back)堆,这个我们移交资源的堆需要被CPU读取.
  4. D3D12_HEAP_TYPE_CUSTOM: 高级应用,请移步 MSDN.

2.HeapMiscFlags: 关于我们要移交资源的堆的额外 flags.一般是 D3D12_HEAP_MISC_NONE.

3.pResourceDesc: 指向说明我们要创建的资源的 D3D12_RESOURCE_DESC 实例的指针.

4.InitialResourceState: §4.2.3有介绍,使用此参数来设置资源的初始阶段(创建时).对于深度/模板缓冲,出书阶段应该是 D3D12_RESOURCE_USAGE_INITIAL ,然后我们将会过渡它到 D3D12_RESOURCE_USAGE_DEPTH 所以它能绑定到渲染管线作为深度/模板缓冲.

5.pOptimizedClearValue: 指向一个说明清空资源最优值的 D3D12_CLEAR_VALUE 对象的指针.清空调用中匹配最佳清空值(optimized clear value)可以潜在地加速清空(相比没有匹配的).指定null既不匹配最佳清空值.

struct D3D12_CLEAR_VALUE
{
    DXGI_FORMAT Format;
    union
    {
        FLOAT Color[ 4 ];
        D3D12_DEPTH_STENCIL_VALUE DepthStencil;
    };
} D3D12_CLEAR_VALUE;

6.riidResource: 我们想要获得一个指针指向的 ID3D12Resource 接口的 COM ID.

7.ppvResource: 返回一个指向代表最新创建资源的 ID3D12Resource 的指针.

除此以外,在使用深度/模板缓冲之前,我们必须创建一个关联的将要绑定到管线的深度/模板视图.

下面展示我们如何创建深度/模板纹理和其相对应的深度/模板视图:

// Create the depth/stencil buffer and view.
D3D12_RESOURCE_DESC depthStencilDesc;
depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
depthStencilDesc.Alignment = 0;
depthStencilDesc.Width = mClientWidth;
depthStencilDesc.Height = mClientHeight;
depthStencilDesc.DepthOrArraySize = 1;
depthStencilDesc.MipLevels = 1;
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;
optClear.DepthStencil.Stencil = 0;
ThrowIfFailed(md3dDevice->CreateCommittedResource(
    &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
    D3D12_HEAP_FLAG_NONE,
    &depthStencilDesc,
    D3D12_RESOURCE_STATE_COMMON,
    &optClear,
    IID_PPV_ARGS(mDepthStencilBuffer.GetAddressOf())));
// Create descriptor to mip level 0 of entire resource using the
// format of the resource.
md3dDevice->CreateDepthStencilView(
    mDepthStencilBuffer.Get(),
    nullptr, // 指向 D3D12_DEPTH_STENCIL_VIEW_DESC 的指针
    DepthStencilView());
// Transition the resource from its initial state to be used as a depth buffer.
mCommandList->ResourceBarrier(1,&CD3DX12_RESOURCE_BARRIER::Transition(
    mDepthStencilBuffer.Get(),
    D3D12_RESOURCE_STATE_COMMON,
    D3D12_RESOURCE_STATE_DEPTH_WRITE));

注意我们使用 CD3DX12_HEAP_PROPERTIES 辅助构造函数来创建堆特性结构体,实现如下:

explicit CD3DX12_HEAP_PROPERTIES(
    D3D12_HEAP_TYPE type,
    UINT creationNodeMask = 1,
    UINT nodeMask = 1 )
{
    Type = type;
    CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
    MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
    CreationNodeMask = creationNodeMask;
    VisibleNodeMask = nodeMask;
}

D3D12_DEPTH_STENCIL_VIEW_DESC 结构体说明了资源的元素的数据类型(格式).


4.3.9设置视区

有时候我们只想描绘3D场景到后台缓冲的一小部分,这一部分称为视区(viewport),它通过一下结构体说明:

typedef struct D3D12_VIEWPORT {
    FLOAT TopLeftX;
    FLOAT TopLeftY;
    FLOAT Width;
    FLOAT Height;
    FLOAT MinDepth;
    FLOAT MaxDepth;
} D3D12_VIEWPORT;

MinDepth 和 MaxDepth 成员用于转换深度区间[0,1]到区间[MinDepth,MaxDepth].这个转换可以完成特定功能,比如设 MinDepth = MaxDepth = 0,所有在视区里被描绘的对象将会深度值归0,出现在场景的所有其他对象的前面.然而,一般我们设 MinDepth = 0 和 MaxDepth = 1 因此深度值并没有被修改.

一旦我们填写好 D3D12_VIEWPORT 结构体,我们通过 ID3D12CommandList::RSSetViewports 来设置好视区.下面示范创建和设置描绘道整个后台缓冲的视区:

D3D12_VIEWPORT vp;
vp.TopLeftX = 0.0f;
vp.TopLeftY = 0.0f;
vp.Width = static_cast<float>(mClientWidth);
vp.Height = static_cast<float>(mClientHeight);
vp.MinDepth = 0.0f;
vp.MaxDepth = 1.0f;

mCommandList->RSSetViewports(1, &vp);
//第一个参数是要绑定的视区的数目,第二个参数是指向视区数组的指针.

你不能制定多个视区到同样的喧嚷目标.多个视区是用于同时渲染多个渲染目标的高级应用.

只要命令列表重设,视区也需要重设.

用途:比如说分屏多人游戏.


4.3.10设置裁剪矩阵

我们可以定义一个相对于后台缓冲的裁剪矩阵因此越出该矩阵的像素都会被剔除(没有对后台缓冲点阵化rasterized).这可以用于优化,比如如果我们知道由一个屏幕上的区域会包含一个在所有之上的矩形 UI 元素,我们不需要处理这些被遮住的像素.

裁剪矩阵由 D3D12_RECT 结构体定义,此结构体 是这样 typedefed 的:

typedef struct tagRECT
{
    LONG left;
    LONG top;
    LONG right;
    LONG bottom;
} RECT;

我们通过 Direct3D 用 ID3D12CommandList::RSSetScissorRects 方法来设置裁剪矩阵.

如下示例,创建和设置一个覆盖左上后台缓冲1/4的裁剪矩阵:

mScissorRect = { 0, 0, mClientWidth/2, mClientHeight/2 };
mCommandList->RSSetScissorRects(1, &mScissorRect);

与 RSSetViewports 相似.

第一个参数是要绑定的裁剪矩阵的数目(大于1时是高级应用),第二个参数是指向矩阵数组的指针.

不能在同一渲染目标上指定多个裁剪矩阵,多个裁剪矩阵同时用于多个渲染目标.

每当命令列表重设时,裁剪矩阵也需要重设.


4.4计时(timing)和动画

为了高精度测量时间,我们使用性能计时器(timer或counter).为了使用 Win32 函数来询问性能计时器,我们必须

#include <windows.h>

性能计时器测量时间的单位叫 counts.我们获取当前时间值,用counts单位来测量,通过 QueryPerformanceCounter 函数 比如:

__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
//通过参数返回当前时间值

为了得到性能计时器的频率(counts 每秒),我们使用 QueryPerformanceFrequency函数:

__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);

那么秒每 counts 的值就是上面的倒数:

mSecondsPerCount = 1.0 / (double)countsPerSec;

因此,要转换一个限时器(time reading) valueInCounts 到秒,只需要乘以转换因子 mSecondsPerCount:

valueInSecs = valueInCounts * mSecondsPerCount;

时间流逝的差值:

__int64 A = 0;
QueryPerformanceCounter((LARGE_INTEGER*)&A);
/* Do work */
__int64 B = 0;
QueryPerformanceCounter((LARGE_INTEGER*)&B);
//过了 (B–A)*mSecondsPerCount 秒.

自行查阅 SetThreadAffinityMask ,可防止平台差距带来不同结果.


4.4.2游戏计时器类(Game Timer Class)

GameTimer的实现:

class GameTimer
{
public:
    GameTimer();
    float GameTime()const; // in seconds
    float DeltaTime()const; // in seconds
    void Reset(); // Call before message loop.
    void Start(); // Call when unpaused.
    void Stop(); // Call when paused.
    void Tick(); // Call every frame.
private:
    double mSecondsPerCount;
    double mDeltaTime;
    __int64 mBaseTime;
    __int64 mPausedTime;
    __int64 mStopTime;
    __int64 mPrevTime;
    __int64 mCurrTime;
    bool mStopped;
};

特别地,该构造函数询问性能计时器的频率.

GameTimer::GameTimer()
    : mSecondsPerCount(0.0), mDeltaTime(-1.0),
    mBaseTime(0),
    mPausedTime(0), mPrevTime(0), mCurrTime(0),
    mStopped(false)
    {
        __int64 countsPerSec;
        QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
        mSecondsPerCount = 1.0 / (double)countsPerSec;
    }

GameTimer 类和实现在 GameTimer.hGameTimer.cpp 文件内.Common 目录里面也有.


4.4.3每帧的时间流逝

当我们在渲染动画帧时,我们需要知道每一帧间经过了多长时间因此我们可以基于时间的流逝来更新我们的游戏对象.Δt计算如下:

void GameTimer::Tick()
{
    if( mStopped )
    {
        mDeltaTime = 0.0;
        return;
    }
// Get the time this frame.
    __int64 currTime;
    QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
    mCurrTime = currTime;
// Time difference between this frame and the previous.
    mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount;
// Prepare for next frame.
    mPrevTime = mCurrTime;
// Force nonnegative. The DXSDK’s CDXUTTimer mentions that if the
// processor goes into a power save mode or we get shuffled to
// another processor, then mDeltaTime can be negative.
    if(mDeltaTime < 0.0)
    {
        mDeltaTime = 0.0;
    }
}
float GameTimer::DeltaTime()const
{
    return (float)mDeltaTime;
}

Tick 在消息循环中如此调用:

int D3DApp::Run()
{
    MSG msg = {0};
    mTimer.Reset();
    while(msg.message != WM_QUIT)
    {
// If there are Window messages then process them.
        if(PeekMessage( &msg, 0, 0, 0, PM_REMOVE ))
        {
            TranslateMessage( &msg );
            DispatchMessage( &msg );
        }
// Otherwise, do animation/game stuff.
        else
        {
            mTimer.Tick();
            if( !mAppPaused )
            {
                CalculateFrameStats();
                Update(mTimer);
                Draw(mTimer);
            }
            else
            {
                Sleep(100);
            }
        }
    }
    return (int)msg.wParam;
}

Reset 方法的实现如下:

void GameTimer::Reset()
{
    __int64 currTime;
    QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
    mBaseTime = currTime;
    mPrevTime = currTime;
    mStopTime = 0;
    mStopped = false;
}

4.4.4总时间

实现使用到的变量如下:

__int64 mBaseTime;
__int64 mPausedTime;
__int64 mStopTime;

当调用 Reset 时,mBaseTime 被初始化为当前的时间.我们可以把它认为时应用开始时的事件.大多数情况下,我们只需在消息循环前调用一次 Reset,所以 mBaseTime 在应用的生命周期内保持为常量.而 mPausedTime 积累所有的暂停时间,为了不记录暂停的时间,我们从总时间减去它(mPausedTime).mStopTime 在暂停时会给出时间,这帮助我们对暂停的时间保持跟踪.

GameTimer类中两大重要的方法为 Stop 和 Start.它们应该在应用暂停和停止暂停时分别调用,因此 GameTimer 可以对暂停的时间保持跟踪.

void GameTimer::Stop()
{
// If we are already stopped, then don’t do anything.
    if( !mStopped )
    {
        __int64 currTime;
        QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
// Otherwise, save the time we stopped at, and set
// the Boolean flag indicating the timer is stopped.
        mStopTime = currTime;
        mStopped = true;
    }
}
void GameTimer::Start()
{
    __int64 startTime;
    QueryPerformanceCounter((LARGE_INTEGER*)&startTime);
// Accumulate the time elapsed between stop and start pairs.
//
// |<––-d––->|
// –––––*–––––—*––––> time
// mStopTime startTime
// If we are resuming the timer from a stopped state…
    if( mStopped )
    {
// then accumulate the paused time.
        mPausedTime += (startTime - mStopTime);
// since we are starting the timer back up, the current
// previous time is not valid, as it occurred while paused.
// So reset it to the current time.
        mPrevTime = startTime;
// no longer stopped…
        mStopTime = 0;
        mStopped = false;
    }
}

最后,返回自从 Reset 被调用的时间的 TotalTime 成员函数定义如下:

float GameTimer::TotalTime()const
{
// If we are stopped, do not count the time that has passed
// since we stopped. Moreover, if we previously already had
// a pause, the distance mStopTime - mBaseTime includes paused
// time,which we do not want to count. To correct this, we can
// subtract the paused time from mStopTime:
//
// previous paused time
// |<–––—>|
// –*––––*––––-*––-*–––—*––> time
// mBaseTime mStopTime mCurrTime
    if( mStopped )
    {
        return (float)(((mStopTime - mPausedTime)-
            mBaseTime)*mSecondsPerCount);
    }
// The distance mCurrTime - mBaseTime includes paused time,
// which we do not want to count. To correct this, we can subtract
// the paused time from mCurrTime:
//
// (mCurrTime - mPausedTime) - mBaseTime
//
// |<—paused time—>|
// –-*–––––*–––––—*––––*––> time
// mBaseTime mStopTime startTime mCurrTime
    else
    {
        return (float)(((mCurrTime-mPausedTime)-
            mBaseTime)*mSecondsPerCount);
    }
}

4.5 DEMO 应用框架

书中的 demo 使用来自 d3dUtil.h, d3dUtil.cpp, d3dApp.h,d3dApp.cpp 的代码.

d3dUtil.h 和 d3dUtil.cpp 包含有用的多功能代码.

d3dApp.h 和 d3dApp.cpp 包含用于封装一个 Direct3D样本应用的核心的 Direct3D 应用类代码.

框架的目的是为了隐藏 windows 创建代码和 Direct3D 初始化代码.


4.5.1 D3DApp

D3DApp类是Direct3D应用程序基类,提供函数用于创建主应用程序窗口,运行应用程序消息循环,处理窗口消息和初始化Direct3D.此外,类为示例应用程序定义了框架的功能.用户只需从D3DApp派生,覆盖框架虚函数,并实例化一个D3DApp派生类的实例.D3DApp类定义如下:

#include “d3dUtil.h”
#include “GameTimer.h”
// Link necessary d3d12 libraries.
#pragma comment(lib,“d3dcompiler.lib”)
#pragma comment(lib, “D3D12.lib”)
#pragma comment(lib, “dxgi.lib”)
class D3DApp
{
protected:
    D3DApp(HINSTANCE hInstance);
    D3DApp(const D3DApp& rhs) = delete;
    D3DApp& operator=(const D3DApp& rhs) = delete;
    virtual ˜D3DApp();
public:
    static D3DApp* GetApp();
    HINSTANCE AppInst()const;
    HWND MainWnd()const;
    float AspectRatio()const;
    bool Get4xMsaaState()const;
    void Set4xMsaaState(bool value);
    int Run();
    virtual bool Initialize();
    virtual LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM
        wParam, LPARAM lParam);
protected:
    virtual void CreateRtvAndDsvDescriptorHeaps();
    virtual void OnResize();
    virtual void Update(const GameTimer& gt)=0;
    virtual void Draw(const GameTimer& gt)=0;
// Convenience overrides for handling mouse input.
    virtual void OnMouseDown(WPARAM btnState, int x, int y){ }
    virtual void OnMouseUp(WPARAM btnState, int x, int y) { }
    virtual void OnMouseMove(WPARAM btnState, int x, int y){ }
protected:
    bool InitMainWindow();
    bool InitDirect3D();
    void CreateCommandObjects();
    void CreateSwapChain();
    void FlushCommandQueue();
    ID3D12Resource* CurrentBackBuffer()const
    {
        return mSwapChainBuffer[mCurrBackBuffer].Get();
    }
    D3D12_CPU_DESCRIPTOR_HANDLE CurrentBackBufferView()const
    {
        return CD3DX12_CPU_DESCRIPTOR_HANDLE(
            mRtvHeap->GetCPUDescriptorHandleForHeapStart(),
            mCurrBackBuffer,
            mRtvDescriptorSize);
    }
    D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView()const
    {
        return mDsvHeap-
            >GetCPUDescriptorHandleForHeapStart();
    }
    void CalculateFrameStats();
    void LogAdapters();
    void LogAdapterOutputs(IDXGIAdapter* adapter);
    void LogOutputDisplayModes(IDXGIOutput* output,DXGI_FORMAT format);
protected:
    static D3DApp* mApp;
    HINSTANCE mhAppInst = nullptr; // application
    instance handle HWND mhMainWnd = nullptr; // main window handle
    bool mAppPaused = false; // is the application paused?
    bool mMinimized = false; // is the application minimized?
    bool mMaximized = false; // is the application maximized?
    bool mResizing = false; // are the resize bars being dragged?
    bool mFullscreenState = false;// fullscreen enabled
// Set true to use 4X MSAA (§4.1.8). The default is false.
    bool m4xMsaaState = false; // 4X MSAA enabled
    UINT m4xMsaaQuality = 0; // quality level of 4X MSAA
// Used to keep track of the “delta-time” and game time (§4.4).
    GameTimer mTimer;
    Microsoft::WRL::ComPtr<IDXGIFactory4> mdxgiFactory;
    Microsoft::WRL::ComPtr<IDXGISwapChain> mSwapChain;
    Microsoft::WRL::ComPtr<ID3D12Device> md3dDevice;
    Microsoft::WRL::ComPtr<ID3D12Fence> mFence;
    UINT64 mCurrentFence = 0;
    Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
    Microsoft::WRL::ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
    Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> mCommandList;
    static const int SwapChainBufferCount = 2;
    int mCurrBackBuffer = 0;
    Microsoft::WRL::ComPtr<ID3D12Resource>
        mSwapChainBuffer[SwapChainBufferCount];
    Microsoft::WRL::ComPtr<ID3D12Resource> mDepthStencilBuffer;
    Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mRtvHeap;
    Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mDsvHeap;
    D3D12_VIEWPORT mScreenViewport;
    D3D12_RECT mScissorRect;
    UINT mRtvDescriptorSize = 0;
    UINT mDsvDescriptorSize = 0;
    UINT mCbvSrvDescriptorSize = 0;
// Derived class should set these in derived constructor to customize
// starting values.
    std::wstring mMainWndCaption = L”d3d App”;
    D3D_DRIVER_TYPE md3dDriverType = D3D_DRIVER_TYPE_HARDWARE;
    DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
    DXGI_FORMAT mDepthStencilFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;
    int mClientWidth = 800;
    int mClientHeight = 600;
};

4.5.2非框架方法

1.D3DApp:构造函数,把数据成员初始化为默认值.

2.~D3DApp:析构函数释放 D3DApp 请求的 COM 接口,以及 flushes the command queue.析构函数需要后者的理由是我们需要等到直到在我们销毁任何 GPU 仍在引用的资源前 GPU 完成处理队列里的命令.否则,当应用退出时 GPU 可能会崩溃.

D3DApp::˜D3DApp()
{
    if(md3dDevice != nullptr)
        FlushCommandQueue();
}

3.AppInst: 访问函数返回一个应用示例句柄的副本.

4.MainWnd: 访问汗啊书返回一个主窗口句柄的副本.

5.AspectRatio: 长宽比由后台缓冲的宽比它的高定义.长宽比将会用于下一章节.一般的实现为:

float D3DApp::AspectRatio()const
{
    return static_cast<float>(mClientWidth) / mClientHeight;
}

6.Get4xMsaaState: 如果开启 4X MSAA 则为 true ,否则 false.

7.Set4xMsaaState: 开启/关闭 4X MSAA.

8.Run: 该方法封装了应用程序的消息循环.它使用 Win32 PeekMessage 函数因此它可以在没有消息呈现的时候处理我们的游戏逻辑.该实现见 §4.4.3.

9.InitMainWindow: 初始化主应用程序窗口.

10.InitDirect3D: 通过实现 §4.3 的各步骤来初始化 Direct3D.

11.CreateSwapChian: 创建交换链.

12.CreateCommandObjects: 创建命令队列,一个命令列表分配器,和一个命令列表.

13.FlushCommandQueue: 强制 CPU 等待直到 GPU 完成处理所有的队列中的命令.

14.CurrentBackBuffer: 返回一个交换链里当前的后台缓冲的 ID3D12Resource.

15.CurrentBackBufferView: 返回当前的后台缓冲的 RTV .

16.DepthStencilView: 返回一个主深度/模板缓冲的 DSV.

17.CalculateFrameStats: 计算每秒的平均帧数和每帧的平均毫秒数.

18.LogAdapters: 列举所有系统上的适配器.

19.LogAdapterOutputs: 列举所有与一个适配器相关联的输出设备.

20.LogOutputDisplayModes: 列举所有一个对于给定格式输出设备支持的显示模式.


4.5.3框架方法

我们不断重写 D3DApp 里的6个虚函数用于实现特定功能.这样设置的好处是初始化代码,消息处理等在 D3DApp 类中实现,因此派生类仅需要集中于 demo 应用的特定的代码:

1.Initialize:使用这种方法讲应用程序初始化,比如分配之源,初始化对象和设置3D场景. 该方法的 D3DApp 的实现调用 InitMainWindow 和 InitDirect3D.因此.你应该像这样首先在你的派生实现调用 D3DApp 函数版本.(?)

bool TestApp::Init()
{
    if(!D3DApp::Init())
        return false;
/* Rest of initialization code goes here */
}
//所以你的初始化代码可以访问 D3DApp 的初始化成员

2.MsgProc: 该方法为主应用程序窗口实现了窗口过程函数(window procedure function).一般地,如果存在一条你需要处理的但 D3DApp::MsgProc 并不处理(或处理得不够好)的消息你只需重写该方法.该方法的实现在 §4.5.5 探究.如果你重写这个方法,任何你没有处理的消息应该转发到 D3DApp::MsgProc.

3.CreateRtvAndDsvDescriptorHeaps: 创建所需 RTV 和 DSV 说明符堆的虚函数.略

4.OnResize: 当D3DApp:MsgProcWM_SIZE消息接收时,调用此方法.当窗口的大小改变,一些Direct3D属性需要改变,因为他们依赖于客户区尺寸.特别是,后置缓冲区和背面深度/模板缓冲区需要重新匹配新窗口的客户区域.通过调用IDXGISwapChain::ResizeBuffers方法可以调整后置缓冲区大小.根据新的尺寸,深度/模板缓冲区需要被摧毁,然后重建。此外,渲染目标和深度/模板视图也需要被重新创建.在缓冲区之外,还有其他属性取决于客户区的大小(如,投影矩阵).

5.Update: 这种抽象方法在每一帧调用一次,应该用于更新 3D 应用程序(如,执行动画,移动相机,碰撞检测,检查用户输入,等等).

6.Draw: 这个抽象方法每一帧被调用,根据我们的渲染命令绘制当前帧到后置缓冲.当我们完成帧的绘制,我们调用IDXGISwapChain::Present方法呈现后置缓冲区到屏幕上.

除了前面的五个方法,我们提供其他三个虚函数处理事件的鼠标按键被按下时,释放,鼠标移动的事件:

  virtual void OnMouseDown(WPARAM btnState, int x, int y){ }
  virtual void OnMouseUp(WPARAM btnState, int x, int y) { }
  virtual void OnMouseMove(WPARAM btnState, int x, int y){ }

通过这种方式,如果你想处理鼠标消息,您可以重写这些方法而不是覆盖 MsgProc 方法.第一个参数是WPARAM,用于存储鼠标按钮的状态(即鼠标按钮被按下时,事件响应)。第二个和第三个参数是客户区(x,y)鼠标光标的坐标.


4.5.4帧数统计

游戏和图形应用程序测量每秒被渲染帧数(FPS)是常见的。要做到这一点,我们简单地在一些指定的时间t计算帧的数量n。然后,平均FPS:fpsavg = n / t。如果我们设置t = 1,那么fpsavg = n / 1 = n。在我们的代码中,我们使用t = 1(第二个),因为它避免了除法,此外,一秒钟给了一个很好的平均-它不太长也不太短。计算FPS的代码是D3DApp:CalculateFrameStats函数:

void D3DApp::CalculateFrameStats()
{
// Code computes the average frames per second, and also the
// average time it takes to render one frame. These stats
// are appended to the window caption bar.
    static int frameCnt = 0;
    static float timeElapsed = 0.0f;
    frameCnt++;
// Compute averages over one second period.
    if( (mTimer.TotalTime() - timeElapsed) >= 1.0f )
    {
        float fps = (float)frameCnt; // fps = frameCnt / 1
        float mspf = 1000.0f / fps;
        wstring fpsStr = to_wstring(fps);
        wstring mspfStr = to_wstring(mspf);
        wstring windowText = mMainWndCaption +
        L” fps: ” + fpsStr +
        L” mspf: ” + mspfStr;
        SetWindowText(mhMainWnd, windowText.c_str());
// Reset for next average.
        frameCnt = 0;
        timeElapsed += 1.0f;
    }
}

为了计算帧数该方法会在每一帧都会被调用.

除了计算 FPS,以上代码还可以计算毫秒数,一般地:

float mspf = 1000.0f / fps;

秒每帧只是 FPS 的倒数,但我们乘以 000 ms /1 s 来实现从秒转换成毫秒.

(废话略)


4.5.5消息处理程序(The Message Handler)

我们从最底层次为我们的应用程序框架实现了窗口过程.一般地,我们不会用 Win32 消息处理做到很多事情.事实上,我们的应用程序的代码的核心在空闲时就得以执行(也就是说,当没有窗口消息呈现时).确实,我们还有很多重要的消息需要处理.然而,因为窗口过程的长度,我们并没有涉及(embed)所有的代码.我们只是说明我们处理的每个消息背后的动机.

我们处理的第一个消息是 WM_ACTIVATE 消息.当应用激活或休眠时该消息就被发送(sent).我们实现如下:

case WM_ACTIVATE:
    if( LOWORD(wParam) == WA_INACTIVE )
    {
        mAppPaused = true;
        mTimer.Stop();
    }
    else
    {
        mAppPaused = false;
        mTimer.Start();
    }
    return 0;

正如你所看到的,当我们的应用程序暂停时,我们设置了数据成员mAppPaused为true,当我们 应用程序被激活,我们设置了数据成员mAppPaused为false。此外,当应用程序暂停时,我们停止了计时器,然后重新开始计时,一旦应用程序再次激活。如果我们回头看D3DApp::Run的实现 (§4.3.3),我们发现,如果我们的应用程序被暂停,我们不更新我们的应用程序代码,而是释放一些CPU周期到操作系统;以这种方式,我们的应用程序不占用CPU周期。

接下来我们处理的是WM_SIZE消息。回想一下,当窗口大小改变时此消息被调用。处理此消息主要的原因是我们想要的后台缓冲和深度/模板尺寸与客户端区域的矩形的尺寸相匹配(所以没有发生拉伸)。因此,当一个窗口大小的时候,我们要调整缓冲区大小。调整缓冲区的代码在D3DApp:: onResize实现。如前所述,后台缓冲区可以通过IDXGISwapChain :: ResizeBuffers函数的调用被调整大小。深度/模板缓冲区需要销毁然后重新创建。此外,渲染目标和深度/模板缓冲需要重新创建。如果用户拖动调整大小,我们必须要小心,因为拖动调整大小酒吧将连WM_SIZE消息,我们不希望继续调整缓冲区。因此,如果我们确定该用户是通过拖动调整,我们实际上什么都不做(除了暂停应用程序),直到用户完成拖动调整大小。我们可以通过处理WM_EXITSIZEMOVE信息做到这一点。此消息被发送时,用户完成了调整大小。

// WM_ENTERSIZEMOVE is sent when the user grabs the resize bars.
case WM_ENTERSIZEMOVE:
    mAppPaused = true;
    mResizing = true;
    mTimer.Stop();
    return 0;
// WM_EXITSIZEMOVE is sent when the user releases the resize bars.
// Here we reset everything based on the new window dimensions.
case WM_EXITSIZEMOVE:
    mAppPaused = false;
    mResizing = false;
    mTimer.Start();
    OnResize();
    return 0;

接下来的三个消息的代码如下:

// WM_DESTROY is sent when the window is being destroyed.
case WM_DESTROY:
    PostQuitMessage(0);
    return 0;
// The WM_MENUCHAR message is sent when a menu is active and the user presses
// a key that does not correspond to any mnemonic or accelerator key.
case WM_MENUCHAR:
    // Don't beep when we alt-enter.
    return MAKELRESULT(0, MNC_CLOSE);
// Catch this message to prevent the window from becoming too small.
case WM_GETMINMAXINFO:
    ((MINMAXINFO*)lParam)->ptMinTrackSize.x = 200;
    ((MINMAXINFO*)lParam)->ptMinTrackSize.y = 200;
    return 0;

最后,为了支持我们的鼠标输入的虚函数,我们处理下面的鼠标消息:

case WM_LBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_RBUTTONDOWN:
    OnMouseDown(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
    return 0;
case WM_LBUTTONUP:
case WM_MBUTTONUP:
case WM_RBUTTONUP:
    OnMouseUp(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
    return 0;
case WM_MOUSEMOVE:
    OnMouseMove(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
    return 0;

我们必须#include,当使用GET_X_LPARAMGET_Y_LPARAM宏。


4.5.6 "Init Direct3D" Demo

#include “../../Common/d3dApp.h”
#include <DirectXColors.h>
using namespace DirectX;
class InitDirect3DApp : public D3DApp
{
public:
    InitDirect3DApp(HINSTANCE hInstance);
    ˜InitDirect3DApp();
    virtual bool Initialize()override;
private:
    virtual void OnResize()override;
    virtual void Update(const GameTimer& gt)override;
    virtual void Draw(const GameTimer& gt)override;
};
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance, 
                   PSTR cmdLine, int showCmd)
{
    // 开启即时内容检查for debug builds.
    #if defined(DEBUG) | defined(_DEBUG)
    _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
    #endif
    try
    {
        InitDirect3DApp theApp(hInstance);
        if(!theApp.Initialize())
            return 0;
        return theApp.Run();
    }
    catch(DxException& e)
    {
        MessageBox(nullptr, e.ToString().c_str(), L”HR Failed”, MB_OK);
        return 0;
    }
}
InitDirect3DApp::InitDirect3DApp(HINSTANCE hInstance): D3DApp(hInstance){}
InitDirect3DApp::˜InitDirect3DApp(){}
bool InitDirect3DApp::Initialize()
{
    if(!D3DApp::Initialize())
        return false;
    return true;
}
void InitDirect3DApp::OnResize()
{
    D3DApp::OnResize();
}
void InitDirect3DApp::Update(const GameTimer& gt){}
void InitDirect3DApp::Draw(const GameTimer& gt)
{
    // Reuse the memory associated with command recording.
    // We can only reset when the associated command lists have finished
    // execution on the GPU.
    ThrowIfFailed(mDirectCmdListAlloc->Reset());
    // A command list can be reset after it has been added to the
    // command queue via ExecuteCommandList. 
    //Reusing the command list reuses memory.
    ThrowIfFailed(mCommandList->Reset(
    mDirectCmdListAlloc.Get(), nullptr));
    // Indicate a state transition on the resource usage.
    mCommandList->ResourceBarrier(
        1, &CD3DX12_RESOURCE_BARRIER::Transition(
            CurrentBackBuffer(),
            D3D12_RESOURCE_STATE_PRESENT,
            D3D12_RESOURCE_STATE_RENDER_TARGET));
    // Set the viewport and scissor rect. This needs to be reset
    // whenever the command list is reset.
    mCommandList->RSSetViewports(1, &mScreenViewport);
    mCommandList->RSSetScissorRects(1, &mScissorRect);
    // Clear the back buffer and depth buffer.
    mCommandList->ClearRenderTargetView(
        CurrentBackBufferView(),
        Colors::LightSteelBlue, 0, nullptr);
    mCommandList->ClearDepthStencilView(
        DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH |
        D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
    // Specify the buffers we are going to render to.
    mCommandList->OMSetRenderTargets(1,
        &CurrentBackBufferView(),
        true, &DepthStencilView());
    // Indicate a state transition on the resource usage.
    mCommandList->ResourceBarrier(
        1, &CD3DX12_RESOURCE_BARRIER::Transition(
        CurrentBackBuffer(),
        D3D12_RESOURCE_STATE_RENDER_TARGET,
        D3D12_RESOURCE_STATE_PRESENT));
    // Done recording commands.
    ThrowIfFailed(mCommandList->Close());
    // Add the command list to the queue for execution.
    ID3D12CommandList* cmdsLists[] = {mCommandList.Get()};
    mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
    // swap the back and front buffers
    ThrowIfFailed(mSwapChain->Present(0, 0));
    mCurrBackBuffer = (mCurrBackBuffer + 1) % SwapChainBufferCount;
    // Wait until frame commands are complete. This waiting is
    // inefficient and is done for simplicity. Later we will show how to
    // organize our rendering code so we do not have to wait per frame.
    FlushCommandQueue();
}

ClearRenderTargetView 方法清空指定的渲染目标到一个给定的颜色.

ClearDepthStencilView 方法清空指定的深度/模板缓冲.

我们总是在描绘前每帧地清空后台缓冲渲染目标和深度/模板缓冲到开始新的图像.

void ID3D12GraphicsCommandList::ClearRenderTargetView(
    D3D12_CPU_DESCRIPTOR_HANDLE RenderTargetView,
    const FLOAT ColorRGBA[ 4 ],
    UINT NumRects,
    const D3D12_RECT *pRects);
  1. RenderTargetView: RTV to the resource we want to clear.
  2. ColorRGBA: Defines the color to clear the render target to.
  3. NumRects: The number of elements in the pRects array. This can be 0.
  4. pRects: An array of D3D12_RECTs that identify rectangle regions on the render target to clear. This can be a nullptr to indicate to clear the entire render target.
void ID3D12GraphicsCommandList::ClearDepthStencilView(
    D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView,
    D3D12_CLEAR_FLAGS ClearFlags,
    FLOAT Depth,
    UINT8 Stencil,
    UINT NumRects,
    const D3D12_RECT *pRects);
  1. DepthStencilView: DSV to the depth/stencil buffer to clear.
  2. ClearFlags: Flags indicating which part of the depth/stencil buffer to clear. This can be either D3D12_CLEAR_FLAG_DEPTH, D3D12_CLEAR_FLAG_STENCIL, or both bitwised ORed together.
  3. Depth: Defines the value to clear the depth values to.
  4. Stencil: Defines the value to clear the stencil values to.
  5. NumRects: The number of elements in the pRects array. This can be 0.
  6. pRects: An array of D3D12_RECTs that identify rectangle regions on the render target to clear. This can be a nullptr to indicate to clear the entire render target.

ID3D12GraphicsCommandList::OMSetRenderTargets 方法设置我们想要使用到管线的的渲染目标和深度/模板缓冲.现在,我们想要使用当前的后台缓冲作为一个渲染管线和我们的主深度/模板缓冲.原型如下:

void ID3D12GraphicsCommandList::OMSetRenderTargets(
    UINT NumRenderTargetDescriptors,
    const D3D12_CPU_DESCRIPTOR_HANDLE *pRenderTargetDescriptors,
    BOOL RTsSingleHandleToDescriptorRange,
    const D3D12_CPU_DESCRIPTOR_HANDLE *pDepthStencilDescriptor);
  1. NumRenderTargetDescriptors: Specifies the number of RTVs we are going to bind. Using multiple render targets simultaneously is used for some advanced techniques. For now, we always use one RTV.
  2. pRenderTargetDescriptors: Pointer to an array of RTVs that specify the render targets we want to bind to the pipeline.
  3. RTsSingleHandleToDescriptorRange: Specify true if all the RTVs in the previous array are contiguous in the descriptor heap. Otherwise, specify false.
  4. pDepthStencilDescriptor: Pointer to a DSV that specifies the depth/stencil buffer we want to bind to the pipeline.

最后,IDXGISwapChain::Present 方法交换前后台缓冲.当我们呈现交换链来交换前后台缓冲时,我们必须更新当前后台缓冲的索引因此我们在接下来的帧渲染到新的后台缓冲:

ThrowIfFailed(mSwapChain->Present(0, 0));
mCurrBackBuffer = (mCurrBackBuffer + 1) % SwapChainBufferCount;

~~大功告成!

发表评论

电子邮件地址不会被公开。 必填项已用*标注