DirectX 12 持续整理 ——4.Direct3D 初始化

来源:互联网 发布:mac系统怎么装win7 编辑:程序博客网 时间:2024/05/21 13:22

绝大部分内容来自于《Introduction to 3D Game Programming with DirectX12 Frank D. Luna》

  • Direct3D 初始化 Direct3D Initialization
    • 1 基本概念
      • 11 Direct3D 12 概述
      • 12 COM
      • 13 纹理格式Textures Formats
      • 14 交换链Swap Chain和页面翻转Page Flipping
      • 15 深度缓冲Depth Buffering
      • 16 资源Resources与描述符Descriptors
      • 17 多重采样Multisampling
      • 18 Direct3D 中的多重采样
      • 19 特征级Feature Levels
      • 110 DirectX 图形基础结构DirectX Graphics Infrastructure
      • 111 检查特征支持Checking Feature Support
      • 112 资源驻留Residency
    • 2 CPU 与 GPU 的交互CPUGPU INTERACTION
      • 21 指令列表与队列The Command Queue and Command Lists
      • 22 CPUGPU 的同步CPUGPU Synchronization
      • 23 Resource Transitions

4. Direct3D 初始化 (Direct3D Initialization)

  从此之后,所接触的内容不再是一些枯燥的数学题了,而是和 DirectX 本身切实相关。在本章中,所要了解的内容包括熟悉 Direct3D 中所包含的类型,基本图形概念,以及 Direct3D 初始化的一系列步骤等等。

  所要了解的内容有以下几个:
  1. 需要对 Direct3D 在硬件中的编程所扮演的角色有一个基本的了解;
  2. COM 在Direct3D 中起到的作用;
  3. 了解基本的图形概念。这其中包括 2D 图像的存储,页面翻转,深度缓冲,多重采样,以及 CPU 和 GPU 之间是如何协调工作的;
  4. 要如何进行 Direct3D 初始化;
  5. 在本章中会展示一个通用的框架结构,此框架会贯穿全书,所以要尽可能了解它。

4.1 基本概念

4.1.1 Direct3D 12 概述

  Direct3D是工作在图形处理单元(GPU:Graphics Processing Unit)上的底层应用程序接口(API:Application Programming Interface),所以当我们渲染3D世界图形时,能够使用硬件条件进行加速。每当 Direct3D 应用程序执行函数时,Direct3D 硬件驱动程序会直接将命令转换为GPU所能理解的指令进行执行,因此不必担心 GPU 的工作细节,只需要注意 GPU 所能支持的 Direct3D 版本就可以了。

Direct3D 12 概述:https://msdn.microsoft.com/zh-cn/communitydocs/game-development/directx-12-white-paper/ta15073002

4.1.2 COM

  组件对象模型(COM:Component Object Model)允许 DirectX 独立为一种新的编程语言(理解上可以这么说)并能够向下兼容,换句话说就是面向组件编程。通常来说,我们把 COM 对象称之为接口,而这些接口的细节大多是以 C++ 语言来完成的。使用 COM 组件我们可以通过特殊的方式或者函数来获取 COM 接口的指针,而不必再使用 C++ 的 new 关键字了。此外,COM 对象使用的是引用计数,当我们完成了一个过程需要释放接口对象时,就需要调用 Release 函数(相当于 C++ 的 delete 关键字)来完成释放内存的操作,并将引用计数置为 0 。

  为此,我们使用Windows运行时库(WRL:Windows Runtime Library)的 Microsoft::WRL::ComPtr 类(#include <wrl.h>)来管理COM对象的生命周期。当一个 ComPtr 超出生命周期之外,则Release函数会被自动调用来释放 COM 对象,这时就不需要去手动调用它。

COM智能指针:https://msdn.microsoft.com/magazine/dn904668

  ComPtr主要有3个函数被经常使用:
  1. Get:返回一个COM对象的指针。一般情况下,当一个 COM 对象需要作为参数传递给函数时调用Get。例如:

ComPtr<ID3D12RootSignature> mRootSignature;...mCommandList->SetGraphicsRootSignature(mRootSignature.Get());

  2. GetAddressOf:返回一个 COM 对象地址的指针。可以在某个函数需要 COM 对象的指针时,使用GetAddressOf来获得指针:

ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;...ThrowIfFailed(md3dDevice->CreateCommandAllocator(    D3D12_COMMAND_LIST_TYPE_DIRECT,    mDirectCmdListAlloc.GetAddressOf()    ));

  3. Reset:将一个 ComPtr 置为 nullptr(空指针),并将 COM 引用计数为0。

4.1.3 纹理格式(Textures Formats)

  对于一个 2D 纹理(Textures)来说,它可以看作是一个矩阵,矩阵中的每一个元素都是一个数据点,而每个数据点都是这个 2D 图片的颜色像素。然而在映射中,上述矩阵的每一个元素并非一个颜色像素,而是一个 3D 向量,这样,就可以将一个 2D 的图像映射到 3D 物体表面,这就相当于把一张平面图贴在 3D 物体上。

  当然,纹理不仅仅是一个二维数据组成的数据结构那么简单。它可以被GPU进行特殊处理,让它能够被应用过滤器和多重采样等功能中。

  而且,纹理的数据元素不能是任意类型,它的类型是有限的(以下是DXGI_FORMAT 枚举类型的部分数据成员):
  1. DXGI_FORMAT_R32G32B32_FLOAT:每个数据元素是由3个32位的浮点数组成。
  2. DXGI_FORMAT_R16G16B16A16_UNORM:每个数据元素是由4个16位映射到 [0,1] 范围数字组成。
  3. DXGI_FORMAT_R32G32_UINT:每个数据元素为2个32位的无符号整数。
  4. DXGI_FORMAT_R8G8B8A8_UNORM:每个数据元素映射至 [0,1] 范围,由4个8位无符号数组成。
  5. DXGI_FORMAT_R8G8B8A8_SNORM:每个数据元素映射至 [-1,1] 范围,由4个8位有符号数组成。
  6. DXGI_FORMAT_R8G8B8A8_SINT:4个8位有符号整数、每个元素映射至 [-128,127] 。
  7. DXGI_FORMAT_R8G8B8A8_UINT:4个8位无符号整数、每个元素映射至 [0,255] 。

DXGI_FORMAT enumeration: https://msdn.microsoft.com/en-us/library/windows/desktop/bb173059

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

  为了避免画面闪烁,最好将整个画面帧都写入一个被叫做后缓冲(back buffer)里。当一个画面帧存在于后缓冲中,这个画面就不会在屏幕展示出来,而是作为备用的帧在后端。当前缓冲(front buffer)存储的画面帧在显示器上显示完成,则前后缓冲执行翻转操作,前后缓冲区指针调换,将后缓冲作为前缓冲,存储的画面帧进行显示,此过程被称之为呈现(presenting);前缓冲作为后缓冲,准备下一帧将要显示的画面。

  上述两个缓冲区组成交换链(Swap Chain)。在 Direct3D 中,交换链是由 IDXGISwapChain 接口表示。该接口存储前后缓冲的纹理,并且提供了设置缓冲区大小的函数 IDXGISwapChain::ResizeBuffers 和呈现函数 IDXGISwapChain::Present

  利用两个缓冲区(前后缓冲)组成交换链的方式叫做双重缓冲(double buffering),也就是说,交换链不是必须要用两个缓冲区才可以,也可以用两个数量以上的缓冲,但是通常情况下两个就已经绰绰有余了。

  尽管后缓冲区存储的是一个纹理,但是它却没有存储颜色信息——也就是说,它的每个数据元素并非是一个像素(pixel),而是一个“纹素(texel)”。

4.1.5 深度缓冲(Depth Buffering)

  深度缓冲同样是纹理,而且同样不包含图像信息,它包含像素有关“深度”的信息。深度值的范围在 0.0 与 1.0 之间,其中 0.0 表示是最接近显示器屏幕,1.0 是距离显示器屏幕最远,这样就能达到 3D 物体在 2D 屏幕上显示的遮盖以及近大远小等效果。深度缓冲存储的信息数目与分辨率相同,每一个像素都有属于自己的深度缓冲信息。

  由于深度缓冲属于纹理,所以它必须被特殊的数据格式来定义:

  1. DXGI_FORMAT_D32_FLOAT_S8X24_UINT:指定一个32位浮点数深度缓冲,保留8位(unsigned integer,[0, 255])为模版缓冲(stencil buffer)使用,其余的24位不用于填充。
  2. DXGI_FORMAT_D32_FLOAT:指定一个32位浮点数深度缓冲。
  3. DXGI_FORMAT_D24_UNORM_S8_UINT:指定一个无符号24位([0, 1])深度缓冲,8位(unsigned integer,[0, 255])为模版缓冲(stencil buffer)使用。
  4. DXGI_FORMAT_D16_UNORM:指定一个无符号16位([0, 1])的深度缓冲。

一个应用程序一般不需要模版缓冲(stencil buffer),但如果它一旦存在,它一定是附加到了深度缓冲中。例如 DXGI_FORMAT_D24_UNORM_S8_UINT 使用了24位为深度缓冲使用,而其余8位为模版缓冲使用,所以,深度缓冲的更好说法是“深度/模版缓冲(depth/stencil buffer)”。

4.1.6 资源(Resources)与描述符(Descriptors)

  在渲染的过程中,一系列的资源(例如:后缓冲,深度缓冲等)会从GPU中写出,或是读取资源(例如:描述物体表面外观的纹理,场景中三维几何的位置信息缓冲等等)。在给 GPU 发送绘制命令之前,我们需要绑定(bind)或链接(link)资源至渲染管道,以在将要进行绘制调用中引用。由于一些资源可能在绘制调用中发生变化,所以在必要时候需要更新绑定(update the bindings)。

  GPU 的资源并不是被直接被绑定的,而是通过一个轻量级的结构体来描述资源,此结构体被成为描述符(descriptor)对象。也就是说,给了一个描述符,GPU 就能得到相应的资源数据以及其必要信息。

  描述符的作用不仅仅是识别资源数据,还能告诉 GPU 用什么方式调用这些资源,让这些被使用的资源作为什么样的作用去使用,因为资源本身并没说说明这些信息。或者说明资源的类型(有些资源甚至是无类型的)。

  描述符拥有以下几种类型,它也间接说明了资源的作用:
  1. CBV/SRV/UAV:分别描述为:静态缓冲区、着色器资源、无序视图资源。
  2. Sampler:用于纹理的采样器资源。
  3. RTV:渲染目标资源。
  4. DSV:深度/模版资源。

  描述符堆(descriptor heap)是由若干描述符组成的数组,是应用程序所有使用的描述符的内存备份(所有被使用的描述符的来源)。每种类型的描述符都需要单独的描述符堆,当然也可以创建多个堆来储存相同类型的描述符。

  我们可以使用多个描述符来引用相同的资源。比如,可以使用多个描述符来引用资源的多个分区。还有,资源被绑定在渲染管道(rendering pipeline)的不同阶段,在每个阶段中都需要一个独立的描述符来引用资源。例如,需要 RTV 类型与 SRV 类型的两个描述符来引用作为渲染目标和着色器资源的纹理。同理,如果存在一个无类型的纹理资源,那么它被看作是整型或者浮点型都未尝可知。倘若现在一个场景中需要两个描述符,那么我们就可以将其中一个描述符定义为整型,而另一个定义为浮点型。

  描述符应该在初始化阶段被创建出来,因为创建描述符有一些的类型检查与验证,不适合在运行时进行。

4.1.7 多重采样(Multisampling)

  由于像素在显示器上并不是足够的小,所以并不是所有的线都表现得非常完美。这时为了让直线更像直线(曲线同理),采用了“阶梯”状的锯齿(aliasing)效果。

  通过增大显示器的分辨率来使像素点变小,能减轻这种锯齿情况。但实际上,增大分辨率是不可能的,也远远不够。为了解决这个问题,一种叫做“超级采样(supersampling)(SSAA)”的反锯齿(antialiasing)技术可以让此问题得到解决,它的工作原理是让后缓冲区和深度缓冲区扩大4倍的屏幕分辨率。当后缓冲区交换至前缓冲时,显示的每个像素块就相当于变成了原来的1/4。

  超级采样的代价很高,因为像素点的数量变为原来的4倍,内存的占用和处理时间也相应成倍增加。妥协起见,Direct3D 支持另外一种抗锯齿技术,叫做“多重采样(multisampling)(MSAA)”。其原理是令后缓冲和深度缓冲扩大4倍。但不同的是,多重采样只计算物体边缘的像素块,对它们进行缩放处理,不包括物体中央的像素。

4.1.8 Direct3D 中的多重采样

  DXGI_SAMPLE_DESC 结构体用来描述一个资源多重采样的参数:

typedef struct DXGI_SAMPLE_DESC{    UINT Count;    UINT Quality;} DXGI_SAMPLE_DESC;

  其中,Count 表示每个像素的采样数;Quality 表示图像的质量等级(此质量等级因硬件厂商的不同而不同)。这两个参数的数值越高,则渲染时所造成的代价越高,性能越低。质量等级取决于纹理格式和每个像素的采样数。
  当指定采样数以及纹理格式时,我们可以使用函数 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)    ));

  函数 CheckFeatureSupport 的第二个参数属于输入输出参数。当输入时,则必须指定纹理格式、采样数和想要查询多重采样所支持的标识(Flags),然后函数 CheckFeatureSupport 则会将质量等级 NumQualityLevels 通过结构体 msQualityLevels 输出。

  采样数和质量等级的范围在 0 到 NumQualityLevels -1 之间。

  每个像素的采样数最大值为:

#define D3D11_MAX_MULTISAMPLE_SAMPLE_COUNT ( 32 )

  为了在内存和性能直接权衡,一般来说使用 4 或 8 的采样数比较合理。如果弃用多重采样,则将采样数设置为 1,质量等级设置为 0。只要是支持 Direct3D 11 的设备,所有的渲染目标格式都支持 4 倍多重采样。

交换链缓冲区和深度缓冲区都需要 DXGI_SAMPLE_DESC 结构体,并且必须具有相同的多重采样参数设置。

4.1.9 特征级(Feature Levels)

  特征级(Feature Levels)在 Direct3D 11 中引入, 它是一个定义良好的 GPU 功能集合。在你创建设备时,你会需要指定一个特征级。如果设备开始工作,则当前硬件支持被指定的特征级;相反,则不支持,这时则尝试较为旧的特征级。

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;

  每种特征级都会实现不同的功能。例如,如果指定 9_1 ,则当前应用程序在 Direct3D 9 上实现功能;如果指定 11_0 ,则在 Direct3D 11 上实现功能。

  特征级有以下几个性质:
  1. 允许创建设备的 GPU 满足或超过被指定特征级的功能;
  2. 特征级总是包括以前或较低特征级的功能;
  3. 特征级与性能无关,只与功能有关。性能是由硬件决定的;
  4. 一旦你创建 Direct3D 11(以及以上?)设备,就需要指定特征级。

4.1.10 DirectX 图形基础结构(DirectX Graphics Infrastructure)

  DirectX 图形基础结构(DXGI:DirectX Graphics Infrastructure)是一个与 Direct3D 一同使用的API。一般来说,它的发展要比 DirectX 要慢。它的主要目标是管理可以独立于 DirectX 图形运行时(DirectX graphics runtime)的低级任务。DXGI 为未来的图形组件提供了一个通用框架。例如,为了平滑动画,2D 、3D渲染 API 需要交换链和页面翻转,然而,交换链的接口 IDXGISwapChain 就是属于 DXGI API 的。DXGI 可以操控一些普通的图形功能,这其中包括全屏转换、枚举图形系统信息(比如显示适配器、显示器、被支持的显示模式(分辨率,刷新率等等)),它还定义了各种纹理格式(DXGI_FORMAT)。

DXGI 概述:https://msdn.microsoft.com/en-us/library/windows/apps/bb205075

  其中,DXGI 一个最为重要的接口就是 IDXGIFactory,此接口能够用来创建 IDXGISwapChain 接口和枚举显示适配器(display adapters)。通常来说,显示适配器是一个显卡之类的物理硬件,但是,操作系统也可以有软件的显示适配器,用来仿真硬件。一个操作系统可以拥有多个适配器(例如 4 路泰坦加持)。
  适配器的接口是 DXGIAdapter ,我们可以用类似于以下的代码枚举操作系统中所有的适配器:

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

  如果一个系统中存在多个显示器,那么每个显示器都是一个显示输出(display output)的实例。显示输出用 IDXGIOutput 接口来表示,每个显示适配器连接一系列的显示输出,关系为1 对 N。举个例子,某个系统中存在 2 个适配器和 3 个显示输出,并且其中的 2 个显示输出连接了一个显卡(适配器)上, 而剩下的 1 个适配器和 1 个显示输出相连接。我们可以枚举与适配器相关的所有显示输出,如以下代码:

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

  每个显示器可以支持多种显示模式。显示模式的参数由 DXGI_MODE_DESC 给出:

typedef struct DXGI_MODE_DESC{    UINT Width; // 分辨率的宽    UINT Height; // 分辨率的高    DXGI_RATIONAL RefreshRate; //刷新速率    DXGI_FORMAT Format; // 显示格式    DXGI_MODE_SCANLINE_ORDER ScanlineOrdering; //渲染器的绘图模式    DXGI_MODE_SCALING Scaling; //图像在显示器上的拉伸模式} 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/1000Width = 1920 Height = 1200 Refresh = 59950/1000

  当进入全屏模式时,枚举显示模式尤为重要。为了获得更好的全屏效果,显示模式的参数必须与显示器所匹配。为了保证这点,就需要枚举显示模式了。

4.1.11 检查特征支持(Checking Feature Support)

  我们经常使用 ID3D12Device::CheckFeatureSupport 函数来检查当前显卡驱动对多重采样的支持情况,然而这个函数只能检查一个特性支持:

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

  1. Feature:属于枚举类型 D3D12_FEATURE 的成员:
    a:D3D12_FEATURE_D3D12_OPTIONS:检查各种 Direct3D 12 的特性。
    b:D3D12_FEATURE_ARCHITECTURE:检查各种硬件体系结构的特性。
    c:D3D12_FEATURE_FEATURE_LEVELS:检查支持的特征级。
    d:D3D12_FEATURE_FORMAT_SUPPORT:检查给出的纹理格式的支持情况。
    e:D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS:检查多重采样的特性是否支持。
  2. pFeatureSupportData:获得特征支持的信息,参数为结构体数据的指针,结构体类型由 Feature 参数决定。
  3. FeatureSupportDataSizepFeatureSupportData* 指向结构体的大小。

  ID3D12Device::CheckFeatureSupport 函数可以检查许多特征的支持情况。这里只列举检查特征级的支持:

其它情况查阅:https://msdn.microsoft.com/en-us/library/windows/desktop/dn770363(v=vs.85).aspx

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 资源驻留(Residency)

  一个复杂的游戏常常需要大量的资源,例如纹理或是 3D 网格等等,但是大多数的资源都不是始终在 GPU 中。假设一种情况:现在游戏有存在两个场景,一个是“森林”,另一个是“洞穴”,玩家要从森林进入洞穴中。在进入之前,是不需要有关洞穴的资源的,然而玩家进入了洞穴,有关森林的资源就不在需要了。

  在 Direct3D 12 中,应用程序管理这些内存的调动,在 GPU 需要这些资源的时候将它们驻留,不需要的时候就要清除它们。基本思想是要知道 GPU 需要的最小内存是多少,因为存储整个游戏的资源对内存容量来说可能是力不从心的。应用程序应该避免在一个短时的画面帧之内在 GPU 之外交换相同资源。理想上来说,如果你打算清除一种资源,你应该保证在一段时间内不再需要它。

  默认情况是,当你创建出一种资源,那么它就会驻留;当你清除它,它就会被释放。有些情况我们不需要如此“人性化”的机制。我们也可以手动去控制资源的驻留和清除:

HRESULT ID3D12Device::MakeResident(    UINT NumObjects,    ID3D12Pageable *const *ppObjects);HRESULT ID3D12Device::Evict(    UINT NumObjects,    ID3D12Pageable *const *ppObjects);

  第二个参数是 ID3D12Pageable 资源的数组,第一个参数是数组的元素个数。

有关Residency的更多信息: https://msdn.microsoft.com/enus/library/windows/desktop/mt186622%28v=vs.85%29.aspx

4.2 CPU 与 GPU 的交互(CPU/GPU INTERACTION)

  CPU 与 GPU 之间的交互机制非常重要。基本思想是,为了让应用程序获得更好的性能,需要在 CPU 与 GPU 之间采取一种平衡,尽可能让两者拥有更少的空闲时间,并达到更好的并行性。

4.2.1 指令列表与队列(The Command Queue and Command Lists)

  指令队列(Command Queue)是属于 GPU 的。当 CPU 通过 Direct3D API 的指令列表(Command Lists)向指令队列提交一个命令后,GPU并不会将这些命令立即执行,而是让它们置于队列之中等待处理,因为 GPU 此时可能正在处理队列中其它的命令。就像银行窗口前的排队。

  一旦指令队列为空,则 GPU 不再工作,因为没有指令可以处理。反过来,如果队列已满,则 CPU 不再工作,而是等待 GPU 处理队列中的指令。当然这两种情况哪个都是不可取的,理想情况是存在一种动态的平衡,让指令队列中始终存在可以被处理的指令却不能处于满格状态,这样 GPU 和 CPU 都能够被充分利用。


  在 Direct3D 12 中,指令队列的是由 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 宏用来返回 COM 接口的 ID。

#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)

  另外一个重要的函数是 ExecuteCommandLists,它用来将指令列表中的指令扔进指令队列中:

void ID3D12CommandQueue::ExecuteCommandLists(    // Number of commands lists in the array    UINT Count,    // Pointer to the first element in an array of command lists    ID3D12CommandList *const *ppCommandLists);

  介绍另外一个接口:ID3D12GraphicsCommandList 接口用于图形的指令列表,继承自 ID3D12CommandListID3D12GraphicsCommandList 接口有许多函数用于将指令添加至指令列表中:

// mCommandList pointer to ID3D12CommandList// adds commands that set the viewportmCommandList->RSSetViewports(1, &mScreenViewport);//clear the render target viewmCommandList->ClearRenderTargetView(    mBackBufferView,    Colors::LightSteelBlue,     0,     nullptr);// issue a drawmCommandList->DrawIndexedInstanced(36, 1, 0, 0, 0);

  上述函数只是将指令添加至指令列表中,而不是把指令立即执行。需要再次调用 ExecuteCommandLists 函数才可以让 GPU 执行上述指令。调用 ID3D12GraphicsCommandList::Close 函数表明你已经完成了添加指令至指令列表的过程。但,必须在 ExecuteCommandLists 之前调用。

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

  当添加命令完成,需要调用 ID3D12CommandAllocator 函数为每条指令分配内存,以供指令队列引用:

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

  1. type:与分配器相关的指令列表类型。在这里只列举两种:
    a:D3D12_COMMAND_LIST_TYPE_DIRECT:存储一个指令列表,由 GPU 直接执行。
    b:D3D12_COMMAND_LIST_TYPE_BUNDLE:将指令列表表示成一个“包(bundle)”。由于构造指令列表时会对 GPU 造成一些额外的开销,所以将一个指令序列记录到一个所谓的“包”中,然后在执行渲染时候,驱动程序会对这个包进行预处理。当然,优化是需要一个过程的,因此对指令进行打包应该在初始化的时候完成,而且尽量不要频繁使用。
  2. riidID3D12CommandAllocator 的 COM ID。
  3. ppCommandAllocator:输出参数。输出被创建的 CommandAllocator 指针。

D3D12_COMMAND_LIST_TYPE:https://msdn.microsoft.com/en-us/library/windows/desktop/dn770348(v=vs.85).aspx

  创建一个指令列表:

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

  1. nodeMask:掩码。若设置为 0 则表示单 CPU 操作。有且只有1位可以被设置,来标识用于创建命令列表的节点(设备的物理适配器)。掩码中的每一个位对应一个单独的节点。

Multi-Adapter:https://msdn.microsoft.com/en-us/library/windows/desktop/dn933253(v=vs.85).aspx#multiple_nodes

  2. type:被创建的指令列表的类型。
  3. pCommandAllocator:与被创建的指令列表相关联的指令分配符指针。指令分配符的类型与指令列表的类型必须匹配。
  4. pInitialState:命令列表的初始管线状态。此参数为可选项,可为NULL。若为空,则会在运行时刻创建一个虚拟的管线状态,防止初始管线状态出现未定义的情况。当指令列表的捆绑包可能很小,而且可以经常重时,尝试设置初始状态参数可能更有意义。
  5. riid:将要被创建的 ID3D12CommandList 的 COM ID。
  6. ppCommandList:输出参数,将要被创建的指令列表的指针。

  可以使用相同的指令分配符创建多个指令列表,但是不能同时创建。在创建一个指令列表时,此列表会被标记为“开启”状态,然而如果此时再次用相同的分配符创建新列表,则会得到一个错误:

D3D12 ERROR: ID3D12CommandList::{Create,Reset}CommandList: The command allocator is currently in-use by another command list.

  使用 ID3D12CommandAllocator::Reset 函数可以创建一个新的指令列表,但是仅仅是创建而已,不能取消原来的指令列表,也就是说原来的指令列表依然在和指令队列相互工作中。

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

以上函数的两个参数与 ID3D12Device::CreateCommandList 的参数匹配。

  我们可以调用 HRESULT ID3D12CommandAllocator::Reset(void) 函数将分配符的引用内存释放,准备为下一帧所使用。但是,一定要确保 GPU 执行完所有的指令后才能调用。

4.2.2 CPU/GPU 的同步(CPU/GPU Synchronization)

  既然是并行工作,如果没有特殊的规范和限制,那是一定会出错的。为了解决这种问题,引用了 ID3D12Fence 接口。原理是设置一个隔离(Fence)点, CPU 等待 GPU 处理完所有在隔离点之前的指令,这个过程叫做刷新指令队列(flushing the command queue)。

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

  一个Fence对象维护一个UINT64的值,每到达一个Fence点,则就让它增加:

UINT64 mCurrentFence = 0;void D3DApp::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);    }}

4.2.3 Resource Transitions

  为了完成普通的渲染效果,GPU 会首先向一个资源中写入数据,然后,从资源中读出数据。但在写入和读出的之间,若 GPU 尚未完成写入过程,这就会在读出数据时产生问题。