目录

4.1 PRELIMINARIES

4.1.1 Direct3D

4.1.2 COM(Component Object Model)

4.1.3 Textures Formats

4.1.4 The Swap Chain and Page Flipping

4.1.5 Depth Buffering

4.1.6 Resources and Descriptors

4.1.7 Multisampling Theory

4.1.8 Multisampling in Direct3D

4.1.9 Feature Levels

4.1.10 DirectX Graphics Infrastructure

4.1.11 Checking Feature Support

4.1.12 Residency

4.2 CPU/GPU INTERACTION

4.2.1 The Command Queue and Command Lists

4.2.2 CPU/GPU Synchronization

4.2.3 Resource Transitions

4.2.4 Multithreading with Commands

4.3 INITIALIZING DIRECT3D

4.3.1 Create the Device

4.3.2 Create the Fence and Descriptor Sizes

4.3.3 Check 4X MSAA Quality Support

4.3.4 Create Command Queue and Command List

4.3.5 Describe and Create the Swap Chain

4.3.6 Create the Descriptor Heaps

4.3.7 Create the Render Target View

4.3.8 Create the Depth/Stencil Buffer and View

4.3.9 Set the Viewport

4.3.10 Set the Scissor Rectangles

4.4 TIMING AND ANIMATION

4.4.1 The Performance Timer

4.4.2 Game Timer Class

4.4.3 Time Elapsed Between Frames

4.4.4 Total Time

4.5 THE DEMO APPLICATION FRAMEWORK

4.6 DEBUGGING DIRECT3D APPLICATIONS


4.1 PRELIMINARIES

4.1.1 Direct3D

是一种底层图形API,经应用程序来控制和对GPU编程进行硬件加速的3D渲染。Direct3D 12添加了一些新的特性,主要的改进是显著减少了CPU开销并提高了对多线程的支持。

4.1.2 COM(Component Object Model)

为了让DirectX成为独立于编程语言并向下兼容。一般用接口的形式来使用COM。我们不使用new来直接创建COM,而是用特定的函数或其它COM的接口获取指向COM接口的指针。COM对象是引用计数,调用接口之后要再调用Release方法而不是delete——COM对象在他们的引用计数为0后会释放自己的内存。

为了管理COM对象的生命周期,Windows Runtime Library(WRL)提供了Microsoft::WRL::ComPtr类,可以看作是COM的智能指针。当一个ComPtr实例超出范围时它会自动调用包含的COM对象的Release方法。

书里会用到3个主要的ComPtr方法:

  1. Get:返回一个指向COM接口的指针。一般用于传递参数给接受原生COM接口指针的函数。
  2. GetAddressOf:返回指向COM接口的指针的地址。
  3. Rest:设置ComPtr实例为nullptr并减少包含的COM接口的引用计数。也可以直接赋值ComPtr对象为nullptr。

4.1.3 Textures Formats

纹理不仅可以作为图片的数据,也可以作为1-3维的数据数组。纹理不能保存任意的数据格式,在DXGI_FORMAT描述了它可以用的枚举类型(DXGI_FORMAT也描述了顶点数据类型和索引数据类型)。其中也有无类型的格式,我们只申请了内存,在纹理绑定到管线时才会解释数据,如4个个16bit通道的DXGI_FORMAT_R16G16B16A16_TYPELESS。

4.1.4 The Swap Chain and Page Flipping

为了避免动画的闪烁问题,最好的解决办法是在一张叫做back buffer的离屏缓冲上绘制完整的一帧内容,绘制完成后才在屏幕上显示这完整的一帧。为了实现这个方案我们需要两张缓冲,在屏幕显示的叫front buffer。

front和back buffer来自一个交换链。在Direct3D中,交换链通过IDXGISwapChain来表示。这个接口储存了front和back buffer纹理,并提供IDXGISwapChain::ResizeBuffers和IDXGISwapChain::Present来重置尺寸和在屏幕上呈现。

用两个缓冲称为双缓冲,类似也可以用三缓冲。

4.1.5 Depth Buffering

用来储存每个像素的深度信息。0.0代表视锥体最近的物体,1.0代表最远的物体。depth buffering和back buffer每一个元素是一一对应的,所以两者需要相同的分辨率(MSAA除外)。

depth buffering通过深度测试来工作,小于depth buffer的值的像素才能被写入back buffer,当然我们也可以改变这个规则。depth buffer是一张纹理,所以也需要特定的格式来创建:

  1. XGI_FORMAT_D32_FLOAT_S8X24_UINT:32位浮点数的depth buffer,8位uint[0-255]留给stencil buffer,以及填充未使用的24位。
  2. DXGI_FORMAT_D32_FLOAT:32位浮点数的depth buffer。
  3. DXGI_FORMAT_D24_UNORM_S8_UINT:24位浮点数的depth buffer,8位uint[0-255]stencil buffer。
  4. DXGI_FORMAT_D16_UNORM:16位被映射到[0,255]的depth buffer。

我们使用第三种24位深度和8位模板,也因此称他为深度/模板缓存depth/stencil buffer。

4.1.6 Resources and Descriptors

在渲染过程中,GPU会写入和读取资源。在我们发出Draw命令之前,我们需要绑定资源到将调用draw call的管线。Gpu不直接绑定资源,而是用过descriptor对象来引用。descriptor可以堪为是一种描述GPU资源的轻量结构体,本质上它是一种间接的层级,给一个resource descriptor,GPU就可以得到实际的资源数据,并知道它的必要信息。我们通过将在draw call引用的descriptor把资源绑定到渲染管线。

为什么要这样额外的层级descriptor?因为GPU资源本质上是通用的内存块,所以它们可以在渲染管线不同的阶段被使用。比如一张纹理被用来做rander target,然后又被作为shader的资源。一个资源它本身不能被说明它到底是render target还是depth/stencil buffer或者shader resource,第二我们可能只想把资源的一个子区域绑定到渲染管线上,第三资源可以是typeless类型,GPU无法知道它的格式。

view是descriptor的同义词,在以前版本的Direct3D使用,在某些Direct3D 12 API也仍然使用。本书中这两个词可以相互转换使用:如constant buffer view和constant buffer descriptor是同一个事物。

Descriptors拥有类型,定义它将如何被使用。这本书会用到:

  1. CBV/SRV/UAV constant buffers/shader resources/unordered access view resources.
  2. Sampler descriptors 采样器资源。
  3. RTV render target资源
  4. DSV depth/stencil资源

一个descriptor heap是descriptors的数组。它是我们应用程序使用的每一种特定类型的所有descriptors的内存支持,我们需要一个单独的descriptor heap来对应每种类型的descriptor。当然也可以创建相同descriptor的多个heap。

也可以有descriptors引用同一个资源,比如用多个descriptors来引用一个资源的不用子区域,可以如之前说的用在渲染不同阶段。如果创建了一个typeless的资源,它可以被视为浮点或者整型,这就需要两个descriptors。

descriptors应该在初始化的时候创建,这是因为需要做一些类型检查和验证。并且descriptors在初始化创建比在运行时创建更好。

2009年8月的SDK文档说:“创建一个全类型的资源限制资源的格式。这使运行时能够优化访问。“只有在真正需要它们提供的灵活性时,才应该创建typeless的资源,否则应该创建一个全类型的资源。

4.1.7 Multisampling Theory

抗锯齿,介绍了SSAA(4倍back buffer,4倍depth/stencil buffer)和MSAA(1倍back buffer,4倍depth/stencil buffer)。

4.1.8 Multisampling in Direct3D

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

Count指定每个像素多少次采样,Quality指定预期的质量级别(“质量水平”的含义可能因硬件制造商而异)。质量级别的范围取决于纹理格式和每个像素的采样数量。

我们可以对给定纹理格式和采样数量来查询质量级别

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

第二个参数即是输入又是输出,输入时我们必须定义它的纹理格式,采样数量和flag,输出时函数会填上它的最大质量级别。我们就可以知道有效的质量级别范围是0到NumQualityLevels–1。

每个像素采样数量最大值定义为:

    #define D3D11_MAX_MULTISAMPLE_SAMPLE_COUNT (32)

如果不想用msaa,可以设置采样数量为1,质量等级为0。所有Direct3D 11(不是12么?)设备对所有render target formats支持4xmsaa。

交换链缓冲区和深度缓冲区都需要填充DXGI_SAMPLE_DESC,back buffer和depth buffer都必须使用相同的msaa设置。

4.1.9 Feature Levels

Direct3D 11 介绍了feature levels的概念(在代码中表现为D3D_FEATURE_LEVEL枚举类型),它大致对应Direct3D 9-11的各种版本:

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;

feature levels规定了一系列严格的功能,如如果一个GPU支持Direct3D 11,那么它必须支持整个Direct3D 11的功能集,很少有例外(如msaa采样数量仍需要查询,因为它们可以在不用的Direct3D 11硬件上变化)。特性集使开发更加容易——一旦我们了解支持的特性集,我们就知道了可以使用的Direct3D功能。

如果用户的硬件不支持某一个feature level,应用程序可以退回到旧的feature level。比如,为了支持更广泛的受众,应用程序你支持Direct3D 11、10和9.3的硬件。应用程序将检查最新到最旧的feature level的支持。在本书中,我们总是要求支持D3D_FEATURE_LEVEL_11_0的feature level,但现实的应用程序还必须考虑支持旧的硬件来最大化他们的用户。

4.1.10 DirectX Graphics Infrastructure

DirectX Graphics Infrastructure (DXGI)是和Direct3D一起用的API。它的基本思想是一些图形相关的任务对多个图形APIs是相同的,如2D和3D的API都需要交换链,因此交换链接口IDXGISwapChain实际上是DXGI的API。DXGI处理其它相同的图形功能,如全屏模式转换,枚举图形系统信息如显示适配器、显示器和支持的现实模式(分辨率,刷新率等),定义了各种支持的surface模式(DXGI_FORMAT)。

简短描述下初始化Direct3D时用到的DXGI概念和接口。IDXGIFactory主要用来创建IDXGISwapChain和枚举显示适配器。显示适配器实行图形功能,通常他是一个物理硬件(图形显卡),但系统也可以有一个软件显示适配器来模拟图形功能。一个系统可以有多个适配器,适配器用IDXGIAdapter接口表示。我们可以这样枚举系统的所有适配器

void D3DApp::LogAdapters()
{UINT i = 0;IDXGIAdapter* adapter = nullptr;std::vector<IDXGIAdapter*> adapterList;// 用mdxgiFactory枚举每个适配器到adapterwhile(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){// 枚举适配器关联的outputLogAdapterOutputs(adapterList[i]);ReleaseCom(adapterList[i]);}
}

一个系统可以有多个显示器,一个显示器是一个display output的示例。一个output由IDXGIOutput接口表示。每个适配器都和一个列表的output相关联。如有两个显卡和三个显示器的系统,两个显示器连到了第一个显卡,第三个显示器连到第二个显卡。我们可以这样枚举一个适配器关联的所有output

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());// 输出DXGI_FORMAT_B8G8R8A8_UNORM下支持的显示模式LogOutputDisplayModes(output, DXGI_FORMAT_B8G8R8A8_UNORM);ReleaseCom(output);++i;}
}

根据文档,“Microsoft Basic Render Driver”表示没有display output。

每一个显示器支持一系列的显示模式,由DXGI_MODE_DESC中一下数据表示:

typedef struct DXGI_MODE_DESC
{UINT Width; // Resolution widthUINT Height; // Resolution heightDXGI_RATIONAL RefreshRate;DXGI_FORMAT Format; // Display formatDXGI_MODE_SCANLINE_ORDER ScanlineOrdering; //Progressive vs. interlacedDXGI_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;

固定显示模式(DXGI_FORMAT,如上面代码中的DXGI_FORMAT_B8G8R8A8_UNORM)的格式,我们可以得到一个在这个格式下output支持的所有显示模式的列表

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

输出如下:

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

进入全屏模式时,枚举显示模式变得尤为重要,为了获得全屏的最佳性能,制定的显示模式(包括刷新率)必须与显示器支持的显示模式完全匹配。

4.1.11 Checking Feature Support

我们已经用了ID3D12Device::CheckFeatureSupport方法来检查当前图形驱动对msaa的支持,这只是我们能用这个函数检查的其中一个feature。它的参数如下:

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

1.Feature:D3D12_FEATURE枚举类型的一个成员,识别我们想要检查支持的features类型:

  1. D3D12_FEATURE_D3D12_OPTIONS:各种DirectX 12 features的支持。
  2. D3D12_FEATURE_ARCHITECTURE:硬件架构的支持。
  3. D3D12_FEATURE_FEATURE_LEVELS:feature level的支持。
  4. D3D12_FEATURE_FORMAT_SUPPORT:给定纹理类型的feature的支持。
  5. D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS:mass的支持。

2.pFeatureSupportData:指向数据结构的指针,用于检索feature支持信息。它的类型取决于你指定的Feature,指定什么类型就传递那个类型的实例。

3.FeatureSupportDataSize:传递数据类型的大小。

ID3D12Device::CheckFeatureSupport函数检查大量feature的支持,很多是本书没有使用的高级特性。以feature levels举例:

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 supportD3D_FEATURE_LEVEL_10_0, // Next, check D3D 10 supportD3D_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));

注意第二个参数是输入也是输出,输入是我们指定数组的元素数量(NumFeatureLevels)和我们想要检查硬件是否支持的feature level数组的指针(pFeatureLevelsRequested)。这个函数输出则是能支持最高的feature level(MaxSupportedFeatureLevel)。

4.1.12 Residency

在Direct3D 12中,应用程序通过从Gpu内存中回收资源,然后根据需要将它们重新驻留在GPU上来管理资源的Residency(也就是驻留,本质上看一个资源是否在GPU中)。基本思想是最小化应用程序使用的内存。作为一个性能的注意点,应用程序应该避免在短时间往GPU移入和移出相同的资源的情况。理想情况是,如果你要驱逐一个资源,那个资源应该有一段时间会不被需要。

默认情况下,一个资源被创建时它是常驻的,被销毁时它会被驱逐。然而一个应用程序可以用下面的方法控制Residency

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

第二个参数都是ID3D12Pageable类型的资源数组,本书出于简单并且demo较小,将不会管理residency。

更多参考:https://msdn.microsoft.com/enus/library/windows/desktop/mt186622(v=vs.85).aspx

4.2 CPU/GPU INTERACTION

CPU/GPU是并行工作并在某些时候同步,为了优化性能,应让他们尽可能长的保持busy并减少同步。

4.2.1 The Command Queue and Command Lists

GPU有一个命令队列,CPU会通过Direct3DAPI把命令提交给这个队列来使用它,这些提交的命令会一直在队列里直到GPU准备处理他们,因为GPU可能忙着处理之前插入的命令。如果命令队列空了,GPU就会闲置,如果太满则CPU不得不停下等GPU赶上。

在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宏定义为:

#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(// Number of commands lists in the arrayUINT Count,// Pointer to the first element in an array of command listsID3D12CommandList *const *ppCommandLists);

这个命令列表会从第一个命令顺序执行。

一个图形命令列表用ID3D12GraphicsCommandList来表示,它继承自ID3D12CommandList接口。这个ID3D12GraphicsCommandList接口有很多方法把命令加入到命令列表。如设置视口,清理渲染render target view,发出draw call:

// mCommandList pointer to ID3D12CommandList
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的内存支持类,当命令被记录到命令列表时,它们将实际存储在相关的命令分配器中。当命令列表通过ID3D12CommandQueue::ExecuteCommandLists执行时,命令队列将引用分配器中的命令。命令分配器从ID3D12Device创建:

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

1.type:可与此分配器关联的命令列表类型。我们在本书中常用的两种类型是:

  1. D3D12_COMMAND_LIST_TYPE_DIRECT:存储一个由GPU直接执行的命令列表(我们到目前为止描述的命令列表类型)。
  2. D3D12_COMMAND_LIST_TYPE_BUNDLE:指定命令列表表示为一个包(bundle)。在构建命令列表时会有一些CPU的开销,因此Direct3D 12提供了一个优化,允许我们将一系列命令记录到一个所谓的bundle中。在记录了一个bundle之后,驱动程序将对命令进行预处理,以优化它们在渲染过程中的执行。因此bundles需要在初始化的时候被记录。如果分析工具显示构建特定的命令列表占用非常多的时间,那么可以考虑使用bundles。Direct3D 12绘制API时非常高效的,所以不应该经常使用bundles,而应该只要在通过它们获得性能收益的情况下使用它们。也就是说默认情况下不要使用他们。

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:0表示单个GPU系统,否则节点掩码将标识与此命令列表相关联的物理GPU。
  2. type:命令列表类型:_COMMAND_LIST_TYPE_DIRECT或D3D12_COMMAND_LIST_TYPE_BUNDLE。
  3. pCommandAllocator:命令分配器的指针,将会关联命令列表,分配器要和命令列表的类型一致。
  4. pInitialState:指定命令列表的初始管线状态,对bendle可以指定为null。特殊情况下,执行命令列表是为了初始化目的,并且不包含任何draw命令,这时候也可以是null。
  5. riid:我们想要创建的ID3D12CommandList接口的COM ID。
  6. ppCommandList:输出一个创建出来的命令列表的指针。

可以使用ID3D12Device::GetNodeCount方法查询系统上的GPU适配器节点数量。

可以创建与同一分配器关联的多个命令列表,但不能同时记录。也就是说,除了我们要记录命令的列表其它所有的命令列表都必须关闭。因此,来自给定命令列表的所有命令都将被连续添加到分配器中,而不会是分散的。注意,当创建或重置命令列表时,它处于“打开”状态。因此,如果我们试图用相同的分配器在一行中创建两个命令列表,我们会得到一个错误:

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

我们调用ID3D12CommandQueue::ExecuteCommandList(C)后,通过调用ID3D12CommandList::Reset方法,重用C的内部内存来记录一组新的命令是安全的:

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

此方法将命令列表重置到和刚创建时相同的状态,但允许重用内部内存,避免释放旧的命令列表并分配新命令列表。注意:重置命令列表不会影响命令队列中的命令,因为相关的命令分配器仍然在命令队列引用的内存中有命令。(就是说只是相当于建了新的命令列表关联原来的命令分配器,命令分配器是不受影响的)。

当我们向GPU提交完整帧的渲染命令后,我们希望在下一帧中重用命令分配器中的内存:

HRESULT ID3D12CommandAllocator::Reset(void);

这和调用std::vector::clear相似,只是把vector的大小重置为0,但保持相同的容量。然而,因为命令队列可能引用分配器中的数据,所以在我们确定GPU已经执行完分配器中的所有命令之前,命令分配器不能被重置,如何做到这一点将在下一节中介绍。

4.2.2 CPU/GPU Synchronization

由于有两个处理器并行运行,出现了许多同步问题。

假设我们有一些资源R来存储我们想要绘制的几何图形的位置。此外,假设CPU更新R的数据来存储位置p1,然后添加一个绘制命令C,该命令C将R引用到命令队列中,目的是在位置p1处绘制几何图形。向命令队列添加命令不会阻塞CPU,因此CPU将继续运行。在GPU执行draw命令C之前,CPU继续覆盖R的数据来存储新的位置p2,这样就会出现错误。

一种解决方案是强制CPU等待,直到GPU完成队列中所有命令的处理直到指定的fence point。我们称之为flushing the command queue冲刷命令队列。我们可以用一个fence来做这件事。一个fence由ID3D12Fence接口表示,用于同步GPU和CPU。一个fence对象可以通过以下方法创建:

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

一个fence对象维护一个UINT64值,该值只是一个整数,用于及时标识一个fence点。我们从0开始,每次我们需要标记一个新的栅栏点,我们就增加这个整数。现在,下面的代码/注释展示了如何使用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().// 给命令队列添加一个指令来设置一个新的fence point。ThrowIfFailed(mCommandQueue->Signal(mFence.Get(), mCurrentFence));// Wait until the GPU has completed commands up to this fence point.// mFence->GetCompletedValue()在GPU执行到Signal(mFence.Get(), mCurrentFence)命令前一直是0.if(mFence->GetCompletedValue() < mCurrentFence){HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);// 当GPU hit到当前的fence触发事件// Fire event when GPU hits current fence.ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));// 等待直到hit到当前fence的事件被触发// Wait until the GPU hits current fence event is fired.WaitForSingleObject(eventHandle, INFINITE);CloseHandle(eventHandle);     }
}

下图解释了这个代码:

GPU已经处理到了,CPU也调用了ID3D12CommandQueue::Signal(fence, n+1)方法。这实际上是在队列末尾添加一条指令,将fence值更改为n + 1。然而,mFence->GetCompletedValue()将继续返回n,直到GPU处理队列中在信号(fence, n+1)指令之前添加的所有命令。

但这个解决方案并不理想,因为它意味着CPU在等待GPU完成时处于空闲状态,但是它提供了一个简单的解决方案,我们将在第7章之前使用它。我们可以在几乎任何点刷新命令队列(不一定每帧只刷新一次)。比如:如果我们有一些初始化GPU命令,可以在进入主渲染循环之前刷新命令队列来执行初始化。正如上一小节最后所说的,我们也可以使用fence(flushing the command queue)以确保在重置命令分配器之前所有的GPU命令都已执行。

4.2.3 Resource Transitions

为了实现常见的渲染效果,GPU写入资源R,然后在后续步骤中读取资源R非常常见。然而,如果GPU没有完成对一个资源的写入甚至还没开始写之前就要读取它,这就会是一个资源风险。为了解决这个问题,Direct3D将一个状态关联到资源。创建资源时,它们处于默认状态,由应用程序告诉Direct3D它们任何状态的转换。这使得GPU可以做任何它需要做的工作来进行转换和防止资源风险。例如,如果我们写入一个资源如一个纹理,我们将纹理状态设置为渲染目标状态。当我们需要读取纹理时,我们会将其状态更改为着色器资源状态。通过通知Direct3D一个转换,GPU可以采取措施来避免这个危险,例如,在从资源读取之前等待所有的写入操作完成。由于性能原因,资源转换的责任落在了应用程序开发人员的身上。应用程序开发人员知道这些转换何时发生,自动转换跟踪系统会增加额外的开销。

通过在命令列表上设置transition resource barriers数组来指定一个资源转换。一个resource barrier由D3D12_RESOURCE_BARRIER_DESC结构来表示。下面的helper函数(在d3dx12.h中定义)返回给定资源的transition resource barriers描述,并指定之前和之后的状态

struct CD3DX12_RESOURCE_BARRIER : public D3D12_RESOURCE_BARRIER
{// [...] convenience methodsstatic 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
};

大多数Direct3D 12结构都有扩展的辅助变量,为了方便起见我们更喜欢使用那些变量。CD3DX12的变换都定义在d3dx12.h。这个文件不是DirectX 12 SDK核心的一部分,但可以从微软下载。本书源代码的Common目录中包含了一个副本。

这是一个资源转换的例子:

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

这个代码我们把显示在屏幕上的图像纹理从presentation状态转换为render target状态。我们可以把资源barrier看作是指示GPU资源的状态正在被转换的命令,以便在执行后续命令时可以采取必要的措施来防止资源风险。

后续会介绍除了转换的其它resource barriers类型。

4.2.4 Multithreading with Commands

Direct3D 12是为高效的多线程设计的。命令列表的设计是利用多线程的一种方式。对于由很多对象的大型场景,构建命令列表来绘制整个场景可能会占用CPU时间。因此我们的想法是并行构建命令列表,比如可以用四个线程,每个线程构建一个绘制25%场景对象的命令列表。

关于多线程命令列表的一些注意点:

  1. 命令列表不是线程自由的,多个线程不能并发使用同一个命令列表和调用它的方法。每个线程必须拥有自己的命令列表。
  2. 命令分配器不是线程自由的,有个线程不能并发使用同一个命令分配器和调用它的方法。每个线程必须拥有自己的命令分配器。
  3. 命令队列是线程自由的,所以多个线程可以使用同一个命令队列和调用它的方法。特别每个线程可以并发将它们生成的命令列表提交给线程队列。
  4. 处于性能考虑,应用程序必须在初始化时指定将并发记录的命令列表的最大数量。

简单起见本书不适用多线程。看完书之后建议研究一下多线程12 SDK示例,看看如何并行生成命令列表。想要最大化系统资源的应用程序一定要利用多个CPU核心的优势使用多线程。

4.3 INITIALIZING DIRECT3D

我们初始化Direct3D的步骤可以被分为下列的几个步骤:

  1. 用D3D12CreateDevice函数创建ID3D12Device。
  2. 创建一个ID3D12Fence对象并查询descriptor大小。
  3. 检查4X MSAA quality level支持。
  4. 创建命令队列、命令分配器和主要的命令列表。
  5. 述并创建交换链。
  6. 创建应用程序需要的descriptor heaps。
  7. 调整back buffer的大小,并为back buffer创建一个render target view。
  8. 创建depth/stencil buffer及其关联的depth/stencil view。
  9. 设置视口和scissor矩形。

4.3.1 Create the Device

初始化Direct3D首先创建Direct3D 12设备(ID3D12Device)。该设备表示一个显示适配器。通常显示适配器是一个物理的3D硬件件(例如,显卡);然而,一个系统也可以有一个软件显示适配器来模拟3D硬件功能(如WARP适配器)。Direct3D 12设备用于检查特性支持,并创建所有其他Direct3D接口对象,如资源、视图和命令列表。设备可以通过以下功能来创建:

HRESULT WINAPI D3D12CreateDevice(IUnknown* pAdapter,D3D_FEATURE_LEVEL MinimumFeatureLevel,REFIID riid, // Expected: ID3D12Devicevoid** ppDevice );
  1. pAdapter:指定要创建的想要表示设备的显示适配器。该参数指定为null将使用主显示适配器。在本书的示例程序中,我们总是使用主适配器。DirectX Graphics Infrastructure演示了如何枚举系统的所有显示适配器。
  2. MinimumFeatureLevel:我们的应用程序需要支持的最小feature level。如果适配器不支持这个feature level,则设备创建将失败。在我们的框架里我们指定为D3D_FEATURE_LEVEL_11_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();
}
#endifThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));// Try to create hardware device.
HRESULT hardwareResult = D3D12CreateDevice(nullptr, // default adapterD3D_FEATURE_LEVEL_11_0,IID_PPV_ARGS(&md3dDevice));// Fallback to WARP device.
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)));
}

一开始的代码是为调试模式启用调试层。当调试层启用后,Direct3D会启动额外的调试并发送调试信息打匹vc++输出窗口,如下:

D3D12 ERROR: ID3D12CommandList::Reset: Reset fails because the command list was not closed.

如果我们调用D3D12CreateDevice失败了,我们会退回一个软件设配器WARP设备。WARP代表Windows的高级光栅化平台。为了创建一个WARP适配器,我们需要创建一个IDXGIFactory4对象来枚举warp适配器(已包含在上述代码):

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

mdxgiFactory对象也将用于创建交换链,因为它是DXGI的一部分。

4.3.2 Create the Fence and Descriptor Sizes

创建设备之后,我们可以创建用于CPU/GPU同步的fence对象。此外,一旦我们开始使用描述符,我们需要知道它们的大小。描述符的大小在不同的gpu之间是不同的,因此我们需要查询这些信息。缓存描述符大小,以便当我们需要不同descriptor类型时可以使用它:

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 Check 4X MSAA Quality Support

在这本书中,我们检查对4X MSAA的支持。选择4X是因为它提供了很好的改进而不过于昂贵,并且因为所有具有direct3d11功能的设备支持4X MSAA和所有渲染目标格式。因此它可以保证在Direct3D 11硬件上可用,我们不必验证对它的支持。无论如何我们都需要检查所支持的quality level,这可以通过以下方法来实现:

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.”);

因为总是支持4X MSAA,所以返回的质量应该总是大于0,因此我们验证情况是不是确实如此,不是则退出程序。

4.3.4 Create Command Queue and Command List

命令队列由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 allocatornullptr, // Initial PipelineStateObjectIID_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();
}

注意,对于CreateCommandList,我们为管道状态对象参数指定null。在本章的示例程序中,我们不发出任何draw命令,因此我们不需要有效的管道状态对象。

4.3.5 Describe and Create the Swap Chain

下一步是创建交换链,通过首先填写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 widthUINT Height; // Buffer resolution heightDXGI_RATIONAL RefreshRate;DXGI_FORMAT Format; // Buffer display formatDXGI_MODE_SCANLINE_ORDER ScanlineOrdering; //Progressive vs. interlacedDXGI_MODE_SCALING Scaling; // How the image is stretched over the monitor.
} DXGI_MODE_DESC;

在下面的数据成员描述中,先只讨论对初学者来说最重要的通用标志和选项:

  1. BufferDesc:这个结构描述了我们想要创建的back buffer的属性。我们主要关心宽度,高度和像素格式。其他请参阅SDK文档。
  2. SampleDesc:msaa的采样数量和quality level,参考4.1.8。
  3. BufferUsage:指定DXGI_USAGE_RENDER_TARGET_OUTPUT,因为我们将渲染到back buffer(即作为render target)。
  4. BufferCount:交换链中使用的buffer数量,指定两个为双缓冲。
  5. OutputWindow:我们要渲染的窗口句柄。
  6. Windowed:true为窗口模式,false为全屏模式。
  7. SwapEffect:指定DXGI_SWAP_EFFECT_FLIP_DISCARD。
  8. Flags:可选的flag。如果指定DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH,那么当应用程序切换到全屏模式时,它将选择与当前应用程序窗口大小最匹配的显示模式。如果未指定此标志,则当应用程序切换到全屏模式时,它将使用当前桌面显示模式。

在我们描述了交换链之后,我们可以用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 Create the Descriptor Heaps

我们需要创建descriptor heaps来存储应用程序所需的descriptor/view(4.1.6)。descriptor heaps由ID3D12DescriptorHeap接口表示。使用ID3D12Device::CreateDescriptorHeap方法创建heap。在本章的示例程序中,我们需要SwapChainBufferCount许多呈现目标视图(RTVs)来描述将要呈现到的交换链中的缓冲区资源,以及一个深度/模板视图(DSV)来描述用于深度测试的深度/模板缓冲资源。因此,我们需要一个heap来存储SwapChainBufferCount RTVs,以及一个heap来存储一个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跟踪当前的回缓冲区索引。

创建堆之后,我们需要能够访问它们存储的descriptor。我们的应用程序通过句柄引用descriptor。使用ID3D12DescriptorHeap::GetCPUDescriptorHandleForHeapStart方法获得堆中第一个descriptor的句柄。下面的函数分别得到当前的back buffer的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 startmCurrBackBuffer, // index to offsetmRtvDescriptorSize); // byte size of descriptor
}D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView()const
{return mDsvHeap->GetCPUDescriptorHandleForHeapStart();
}

现在我们看到了需要descriptor大小的例子:为了偏移到当前back buffer RTV descriptor,我们需要知道RTV descriptor的大小。

4.3.7 Create the Render Target View

如4.1.6所述,我们不直接将资源绑定到管道阶段。相反,我们必须为资源创建一个resource view (descriptor),并将该视图绑定到pipeline stage。如为了将back buffer绑定到流水线的输出合并阶段(这样Direct3D就可以渲染到上面),我们需要在back buffer中创建一个render target view。第一步是获取存储在交换链中的buffer资源

HRESULT IDXGISwapChain::GetBuffer(UINT Buffer,REFIID riid,void **ppSurface);
  1. Buffer:标识我们想要获取的back buffer的索引。
  2. riid:我们需要获得ID3D12Resource接口的COM ID的指针。
  3. ppSurface:返回一个指向ID3D12Resource的指针,该资源表示back buffer。

对IDXGISwapChain::GetBuffer的调用增加了返回缓冲区的COM引用计数,所以我们必须在完成后释放它。如果使用ComPtr,这是自动完成的。

创建render target view,我们使用ID3D12Device::CreateRenderTargetView方法:

void ID3D12Device::CreateRenderTargetView(ID3D12Resource *pResource,const D3D12_RENDER_TARGET_VIEW_DESC *pDesc,D3D12_CPU_DESCRIPTOR_HANDLE DestDescriptor);
  1. pResource:指定将用作render target的资源,在上面的示例中,该资源是back buffer。
  2. pDesc:指向D3D12_RENDER_TARGET_VIEW_DESC的指针。该结构描述了资源中元素的数据类型(格式)。如果资源是用有类型的格式创建的(不是typeless),那么这个参数可以是null,这表示用资源的格式创建该资源的第一个mipmap级别的view(back buffer只有一个mipmap级别)。因为我们指定了back buffer的类型,所以为这个参数指定null。
  3. DestDescriptor:将存储创建的render target view的descriptor的句柄。

下面是一个调用这两个方法的例子,我们创建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 Create the Depth/Stencil Buffer and View

现在我们需要创建depth/stencil buffer(4.1.5),深度缓冲区只是一个2D纹理,用于存储最近可见对象们的深度信息(如果使用模板,则存储模板信息)。纹理是一种GPU资源,所以我们通过填充一个描述纹理的D3D12_RESOURCE_DESC结构,然后调用ID3D12Device::CreateCommittedResource方法来创建纹理。D3D12_RESOURCE_DESC结构定义如下:

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;
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;
  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:纹理的宽度。对于缓冲区资源,这是buffer中的字节数。
  3. Height:纹理的高度。
  4. DepthOrArraySize:纹理的深度,或纹理数组的大小(对于1D和2D纹理)。注意,你不能有一个3D纹理的纹理数组。
  5. MipLevels:mipmap level的数量。我们的纹理只需要一个mipmap level。
  6. Format:指定文本格式的DXGI_FORMAT枚举类型的成员。depth/stencil buffer的格式可以在4.1.5中查看。
  7. SampleDesc:msaa采样数量和quality level(4.1.7/4.1.8)。4X MSAA使用back buffer和比屏幕分辨率大4倍的depth buffer。用于depth/stencil buffer的多采样设置必须与render target的设置匹配(不应该是交换链吗?)。
  8. Layout:指定纹理布局的D3D12_TEXTURE_LAYOUT枚举类型。目前我们先不管布局,指定为D3D12_TEXTURE_LAYOUT_UNKNOWN。
  9. MiscFlags:其他资源的旗帜。对于depth/stencil buffer资源指定为D3D12_RESOURCE_MISC_DEPTH_STENCIL。

GPU资源以堆的形式存在,堆本质上是带有某些属性的GPU内存块。CreateCommittedResource方法使用我们用指定的属性创建资源并将其提交到特定的堆

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;
  • pHeapProperties:要提交资源的堆的属性。其中一些属性用于高级用途。现在,我们需要担心的主要属性是D3D12_HEAP_TYPE,它可以是D3D12_HEAP_PROPERTIES枚举类型的以下成员之一:
  1. D3D12_HEAP_TYPE_DEFAULT:默认堆。这就是我们提交资源的地方,这些资源只会被GPU访问。GPU会读写depth/stencil buffer,而CPU从来不需要访问它,因此depth/stencil buffer将被放置在默认堆中。
  2. D3D12_HEAP_TYPE_UPLOAD:上传堆。这是我们提交资源的地方,我们需要把数据从CPU上传到GPU资源。
  3. D3D12_HEAP_TYPE_READBACK:回读堆。这是我们提交的资源需要由CPU读取的地方。
  4. D3D12_HEAP_TYPE_CUSTOM:和高级使用场景有关,请参阅MSDN文档了解更多信息。
  • HeapMiscFlags:关于我们提交资源的堆的附加flag。通常是D3D12_HEAP_MISC_NONE。
  • pResourceDesc:指向描述资源的D3D12_RESOURCE_DESC实例的指针。
  • InitialResourceState:资源具有当前使用状态(4.2.3)。使用此参数设置资源创建时的初始状态。对于depth/stencil buffer,初始状态将是D3D12_RESOURCE_USAGE_INITIAL,然后我们希望将其转换为D3D12_RESOURCE_USAGE_DEPTH,以便将其作为depth/stencil buffer绑定到管线。
  • pOptimizedClearValue:描述一个清除资源的优化值的D3D12_CLEAR_VALUE对象的指针。匹配清除优化值的清除调用可能比不匹配的清除优化值的清除调用更快。也可以为指定为Null,即不指定清除优化值。
    struct D3D12_CLEAR_VALUE
    {DXGI_FORMAT Format;union{FLOAT Color[ 4 ];D3D12_DEPTH_STENCIL_VALUE DepthStencil;};
    } D3D12_CLEAR_VALUE;
  • riidResource:ID3D12Resource接口的COM ID的指针。
  • ppvResource:代表刚创建资源的ID3D12Resource的指针。

为了获得最佳性能,应该将资源放在默认堆中。只有在需要这些特性时才使用上传或回读堆。

此外,在使用depth/stencil buffer之前,我们必须创建一个关联的depth/stencil buffer view来绑定到管线上。这类似于创建render target view。下面的代码示例显示了我们如何创建depth/stencil buffer(类似创建交换链)及其相关联的depth/stencil buffer view:

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

CreateDepthStencilView的第二个参数是指向D3D12_DEPTH_STENCIL_VIEW_DESC的指针。该结构描述了资源中元素的数据类型(格式)。如果资源是用有类型的格式创建的(不是typeless),则此参数可以为null,表示创建该资源的第一个mipmap level的view(depth/stencil buffer仅使用一个mipmap level创建),并使用创建资源的格式。因为我们指定了depth/stencil buffer的类型,所以我们为这个参数指定null(和D3D12_RENDER_TARGET_VIEW_DESC一样)。

现在我们回顾上面两小节的内容再理一下:

  • 设置描述
  1. 创建交换链,设置了交换链的描述结构,这一步其实也设置了back buffer的属性。
  2. depth/stencil buffer要由D3D12_RESOURCE_DESC来描述,并且可以设置清除优化值。
  • 创建descriptor heap
  1. 创建了back buffer和depth/stencil buffer的descriptor heap。
  2. 我们可以从descriptor heap里获取到back buffer和depth/stencil buffer的descriptor句柄。
  • 创建ID3D12Resource资源,提交到特定堆,此时是根据描述设置了初始的属性
  1. 用交换链的GetBuffer函数获取back buffer的ID3D12Resource类型资源。
  2. 用CreateCommittedResource函数来创建depth/stencil buffer的ID3D12Resource类型资源,并提交到特定堆。
  • 用CreateRenderTargetView函数和CreateDepthStencilView函数来创建对应的view,此时就要用到上述的ID3D12Resource类型资源和descriptor句柄。

4.3.9 Set the Viewport

通常我们喜欢将3D场景绘制到整个back buffer,其中后台缓冲区的大小对应于整个屏幕(全屏模式)或窗口的整个客户区。然而,有时我们只想把3D场景绘制到一个back buffer的子矩形中,如下图。

我们绘制的back buffer的子矩形称为viewport,它由以下结构描述:

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

前四个数据成员定义了相对于back buffer的viewport矩形(注意数据成员的类型是float)。在Direct3D中,深度值以0到1的规格化范围存储在深度缓冲区中。使用MinDepth和MaxDepth成员将深度间隔[0,1]转换为深度间隔[MinDepth, MaxDepth]。能够变换深度范围可以达到一定的效果:如可以设置MinDepth=0和MaxDepth=0,这样用这个viewport绘制的所有对象的深度值都将为0,并出现在场景中所有其他对象的前面。通常MinDepth设置为0,MaxDepth设置为1,这样深度值就不会被修改。

填完D3D12_VIEWPORT结构后,我们使用ID3D12CommandList::RSSetViewports方法用Direct3D设置viewport。下面的例子创建并设置了一个viewport,它可以绘制到整个back buffer

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

第一个参数是要绑定的viewports数量(对于高级效果使用多个viewports),第二个参数是指向viewports数组的指针。

不能为同一个render target指定多个viewports。多个viewports用于同时渲染多个render target的高级技术。

每当命令列表重置时,viewpor也需要重置。

可以使用viewport来实现双人游戏模式的屏幕分割。

4.3.10 Set the Scissor Rectangles

我们可以定义一个与back buffer相关的scissor rectangle,渲染时使在这个矩形外的像素被剔除(不对这些像素进行光栅化)。例如,如果我们知道屏幕的某个区域将包含一个矩形UI元素,那么我们就不需要处理UI元素将会遮盖的3D世界的像素。

scissor rectangles是由D3D12_RECT结构定义的,其结构类型如下:

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

使用ID3D12CommandList::RSSetScissorRects方法用Direct3D设置scissor rectangle。下面的例子创建并设置了一个scissor rectangle,它覆盖了back buffer的左上角象限:

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

与RSSetViewports类似,第一个参数是要绑定的scissor rectangle数量(使用多个用于高级效果),第二个参数是指向scissor rectangle数组的指针。

不能在同一个render target上指定多个scissor rectangles。多个scissor rectangles用于同时渲染多个render target的高级技术。

每当重置命令列表时,都需要重置scissor rectangle。

4.4 TIMING AND ANIMATION

为了正确地播放动画,我们需要记录时间,并且需要一个具有高精确度的计时器。

4.4.1 The Performance Timer

为了精确地测量时间,我们使用performance timer(或performance counter)。要使用Win32函数来查询performance timer,必须使用#include <windows.h>.。

performance timer以计数为单位度量时间。我们使用QueryPerformanceCounter函数获得performance timer的当前时间值(以计数为单位),如下所示(根据频率的计数):

__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

注意,该函数通过其参数返回当前时间值,该参数是一个64位整数值。

为了得到performance counter的频率(每秒计数),我们使用QueryPerformanceFrequency函数(一秒的频率):

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

那么每个计数的秒数就是countsPerSec的倒数(一个频率的秒数):

mSecondsPerCount = 1.0 / (double)countsPerSec;

因此,要将时间读数valueInCounts转换为秒,只需将其乘以转换因子mSecondsPerCount:

valueInSecs = valueInCounts * mSecondsPerCount;

QueryPerformanceCounter函数返回的值本身并不特别有趣。我们所做的是使用QueryPerformanceCounter获取当前时间值(频率计数),然后稍后再使用QueryPerformanceCounter获取当前时间值。那么这两个时间调用之间经过的时间就是差值。也就是说,我们总是查看两个时间戳之间的相对差异来度量时间,而不是performance counter返回的实际值。下面更好地说明了这个想法:

__int64 A = 0;
QueryPerformanceCounter((LARGE_INTEGER*)&A);/* Do work */__int64 B = 0;
QueryPerformanceCounter((LARGE_INTEGER*)&B);

所以它需要用(B - A)计数来做这个工作,或者(B - A)*mSecondsPerCount seconds来做这个工作。

MSDN对QueryPerformanceCounter有如下评论:“在多处理器计算机上,调用哪个处理器都不重要。但是,由于基本输入/输出系统(BIOS)或硬件抽象层(HAL)中的错误,您可以在不同的处理器上得到不同的结果。你可以使用SetThreadAffinityMask函数,这样主应用程序线程就不会切换到另一个处理器。

4.4.2 Game Timer Class

在接下来的两小节中,我们将讨论以下GameTimer类的实现。

class GameTimer
{
public:GameTimer();float GameTime()const; // in secondsfloat DeltaTime()const; // in secondsvoid 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;
};

构造函数查询了performance counter的频率:

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

4.4.3 Time Elapsed Between Frames

当我们渲染我们的动画帧时,我们需要知道帧与帧之间经过了多少时间,这样我们需要根据时间来更新游戏对象,下面的代码显示了如何计算Δ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;
}

这样Δt计算每一帧并送入UpdateScene方法,以便场景可以更新动画。复位方法的实现为:

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

所示的一些没有用到的变量在下节讨论。

4.4.4 Total Time

另一个有用的时间是自应用程序启动以来经过的时间,不包括暂停时间,我们称之为总时间。为了实现总时间,我们使用以下变量:

__int64 mBaseTime;
__int64 mPausedTime;
__int64 mStopTime;

我们可以把mBaseTime看作是应用程序启动的时间。mPausedTime是累积所有经过的暂停时间,我们需要积累这个时间,这样我们就可以从总运行时间中减去它,这样就不会计算暂停时间,从而得到总时间。mStopTime变量给出了时间停止(暂停)的时间。

GameTimer类的两个重要方法是Stop和Start。它们应该分别在应用程序暂停和未暂停时被调用:

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

最后,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 mCurrTimeif( 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 mCurrTimeelse{return (float)(((mCurrTime - mPausedTime) - mBaseTime) * mSecondsPerCount);}
}

龙书的演示框架创建了一个GameTimer实例,用于测量自应用程序启动以来的总时间以及帧与帧之间的时间间隔。

4.5 THE DEMO APPLICATION FRAMEWORK

本书中的演示使用了来自d3dUtil的代码。d3dUtil.h,d3dUtil.cpp,d3dApp.h和d3dApp.cpp文件都可从本书网站下载。d3dUtil.h和d3dUtil.cpp文件包含有用的实用程序代码,d3dApp.h和d3dApp.cpp文件包含用于封装Direct3D示例应用程序的核心Direct3D应用程序类代码。前几节没有涉及这些文件中的每一行代码(例如没有展示如何创建一个窗口,因为基本的Win32编程是这本书的先决条件)。该框架的目标是隐藏窗口创建代码和Direct3D初始化代码。通过隐藏这些代码可以减少演示的干扰,只关注示例代码试图说明的特定细节。

具体代码部分会在https://blog.csdn.net/Calette/article/details/103723633中分析。

4.6 DEBUGGING DIRECT3D APPLICATIONS

许多Direct3D函数会返回HRESULT错误代码,在我们的例子里,我们用一个简单的错误处理系统来检查返回的HRESULT,如果它失败了,我们将抛出一个异常来存储出错调用的错误代码、函数名、文件名和行号:

class DxException
{
public:DxException() = default;DxException(HRESULT hr, const std::wstring& functionName, const std::wstring& filename, int lineNumber);std::wstring ToString()const;HRESULT ErrorCode = S_OK;std::wstring FunctionName;std::wstring Filename;int LineNumber = -1;
};#ifndef ThrowIfFailed
#define ThrowIfFailed(x)                                              \
{                                                                     \HRESULT hr__ = (x);                                               \std::wstring wfn = AnsiToWString(__FILE__);                       \if(FAILED(hr__)) { throw DxException(hr__, L#x, wfn, __LINE__); } \
}
#endif

ThrowIfFailed必须是宏而不是函数,否则,_ file__和_ line__将引用函数实现的文件和行,而不是写入ThrowIfFailed的文件和行。L#x将ThrowIfFailed宏的参数转换为Unicode字符串,这样我们就可以将引起错误的函数调用输出到消息框中。

我们的整个应用程序存在于一个try/catch块::

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

如果一个HRESULT失败了,一个异常将会抛出,我们通过MessageBox函数输出信息,然后退出应用程序。举个CreateCommittedResource的例子如下:

DirectX12学习笔记(四)Direct3D Initialization相关推荐

  1. Directx12学习笔记(一)

    Directx12学习笔记(一) 数学基础: 第一部分: 1.坐标系: 左手坐标系:如Directx使用的坐标系就是左手坐标系 右手坐标系:OpenGl和3dsmax使用的就是右手坐标系 第二部分:向 ...

  2. C#可扩展编程之MEF学习笔记(四):见证奇迹的时刻

    前面三篇讲了MEF的基础和基本到导入导出方法,下面就是见证MEF真正魅力所在的时刻.如果没有看过前面的文章,请到我的博客首页查看. 前面我们都是在一个项目中写了一个类来测试的,但实际开发中,我们往往要 ...

  3. IOS学习笔记(四)之UITextField和UITextView控件学习

    IOS学习笔记(四)之UITextField和UITextView控件学习(博客地址:http://blog.csdn.net/developer_jiangqq) Author:hmjiangqq ...

  4. RabbitMQ学习笔记四:RabbitMQ命令(附疑难问题解决)

    RabbitMQ学习笔记四:RabbitMQ命令(附疑难问题解决) 参考文章: (1)RabbitMQ学习笔记四:RabbitMQ命令(附疑难问题解决) (2)https://www.cnblogs. ...

  5. JSP学习笔记(四十九):抛弃POI,使用iText生成Word文档

    POI操作excel的确很优秀,操作word的功能却不敢令人恭维.我们可以利用iText生成rtf文档,扩展名使用doc即可. 使用iText生成rtf,除了iText的包外,还需要额外的一个支持rt ...

  6. Ethernet/IP 学习笔记四

    Ethernet/IP 学习笔记四 EtherNet/IP Quick Start for Vendors Handbook (PUB213R0): https://www.odva.org/Port ...

  7. OpenCV学习笔记四-image的一些整体操作

    title: OpenCV学习笔记四-image的一些整体操作 categories: 编程 date: 2019-08-08 12:50:47 tags: OpenCV image的一些操作 sP4 ...

  8. 吴恩达《机器学习》学习笔记四——单变量线性回归(梯度下降法)代码

    吴恩达<机器学习>学习笔记四--单变量线性回归(梯度下降法)代码 一.问题介绍 二.解决过程及代码讲解 三.函数解释 1. pandas.read_csv()函数 2. DataFrame ...

  9. esp8266舵机驱动_arduino开发ESP8266学习笔记四—–舵机

    arduino开发ESP8266学习笔记四-–舵机 使用时发现会有ESP8266掉电的情况,应该是板上的稳压芯片的限流导致的,观测波形,发现当舵机运转时,电源线3.3V不再是稳定的3.3V,大概是在3 ...

  10. mysql新增表字段回滚_MySql学习笔记四

    MySql学习笔记四 5.3.数据类型 数值型 整型 小数 定点数 浮点数 字符型 较短的文本:char, varchar 较长的文本:text, blob(较长的二进制数据) 日期型 原则:所选择类 ...

最新文章

  1. 光子筛matlab,一种振幅调制器件产生椭圆涡旋光的方法与流程
  2. 非极大值抑制_【计算机视觉——RCNN目标检测系列】三、IoU与非极大抑制
  3. Least-Squares Fitting of Two 3-D Point Sets
  4. 在如今的Web前端环境下,如何提升自己的竞争力?
  5. 小D课堂-SpringBoot 2.x微信支付在线教育网站项目实战_5-9.使用JWT生成用户Token回写客户端...
  6. Arduino资源下载
  7. mujoco_py中文文档
  8. Matlab插值与拟合
  9. 小颗粒积木步骤图纸_loz小颗粒钻石积木拼图图纸谁有
  10. 计算机菜单专业英语,InDesign中英文菜单对照表 -电脑资料
  11. 一位苦逼程序员的找工作经历
  12. 科研论文阅读与写作实战技巧
  13. Direct3D11学习经历分享
  14. unity零基础开始学习做游戏(四)biu~biu~biu发射子弹打飞机
  15. python算积分蒙特卡罗_蒙特卡罗计算积分
  16. Fluent非稳态工况模拟中固定时间步数据输出
  17. 华云数据打造企业社会责任践行范本
  18. JS绑定事件三种方式
  19. idea强大功能_强大的打印功能
  20. Java编写一个桌球_java练习题——简易的桌球游戏

热门文章

  1. 【算法无用系列】AC自动机敏感词过滤
  2. Python 资源大全中文版【2018-11-21】
  3. 直播泡沫?3.5万亿红人经济的未来在这几个字里
  4. 【钱包·RPC搭建】以太坊主网节点搭建
  5. 怎么像“一朵云”一样管理“多个云”?
  6. Java 操作 word 文档 (三)段落Paragraphs,文本加粗、斜体、字体、字体大小、复杂文本
  7. 使用LZMA算法(转载)
  8. Office Word和Excel的工具栏显示不全,只显示文件、绘图和帮助的解决办法
  9. 工欲善其事必先利其器之番茄土豆
  10. Python 队列之传土豆(《Python数据结构与算法分析》第二版)