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
- 1 基本概念
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 屏幕上显示的遮盖以及近大远小等效果。深度缓冲存储的信息数目与分辨率相同,每一个像素都有属于自己的深度缓冲信息。
由于深度缓冲属于纹理,所以它必须被特殊的数据格式来定义:
DXGI_FORMAT_D32_FLOAT_S8X24_UINT
:指定一个32位浮点数深度缓冲,保留8位(unsigned integer,[0, 255])为模版缓冲(stencil buffer)使用,其余的24位不用于填充。DXGI_FORMAT_D32_FLOAT
:指定一个32位浮点数深度缓冲。DXGI_FORMAT_D24_UNORM_S8_UINT
:指定一个无符号24位([0, 1])深度缓冲,8位(unsigned integer,[0, 255])为模版缓冲(stencil buffer)使用。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. FeatureSupportDataSize
:pFeatureSupportData
* 指向结构体的大小。
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
接口用于图形的指令列表,继承自 ID3D12CommandList
。ID3D12GraphicsCommandList
接口有许多函数用于将指令添加至指令列表中:
// 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. riid
:ID3D12CommandAllocator
的 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 尚未完成写入过程,这就会在读出数据时产生问题。
- DirectX 12 持续整理 ——4.Direct3D 初始化
- DirectX 12 持续整理 ——1. 向量
- DirectX 12 持续整理 ——2. 矩阵
- DirectX 12 持续整理 ——3. 变换
- 【DirectX游戏开发】Direct3D初始化
- 【DirectX 9.0学习之路(第一话)】——Direct3D初始化(上)
- 【DirectX 9.0学习之路(第二话)】——Direct3D初始化(下)
- DirectX入门之初始化Direct3D及常见问题
- 摸爬滚打DirectX11_day03——Direct3D的初始化
- DirectX游戏编程学习(一)初始化Direct3D
- DirectX学习笔记(二):Direct3D初始化详解
- DirectX 11游戏编程学习笔记之5: 第4章Direct3D Initialization(Direct3D初始化)
- 初始化Direct3D
- 初始化Direct3D
- 初始化Direct3D
- 初始化Direct3D
- Direct3D 初始化
- Direct3D 初始化
- 二叉树的序列化和反序列化
- 静默安装oracle client
- 单例模式
- oracle的left join和inner join的区别
- 安装tensorflow,查看tensorflow版本,在终端退出python的命令
- DirectX 12 持续整理 ——4.Direct3D 初始化
- 开发html5六人九人十人十二人牛牛源码安装搭建
- C语言实现顺序线性表的表示、插入、删除
- 表达式17.11.14
- malloc()动态分配内存
- 向filter过滤器中的request对象添加额外的参数
- Edgware.RC1中ZuulFallbackProvider的改进
- 搭建(增加) jenkins slave 机器
- C++ STL一一迭代器配接器