DirectX12初始化综合篇包含了四篇文章(​​Direct3D 12简介​​,​​图形管道概述​​,​​Direct3D 12工作原理概述​​,​​Direct3D 12的应用程序流控制概述​​)。

DirectX12里面包含了很多难以理解的概念,这篇文章力求把这些概念都讲解清楚,对于概念的学习可能会比较枯燥,所以希望大家能够坚持下去,加油!!!(暂时不能理解的可以先放一放,以后慢慢的就能理解了)

Direct3D 12简介

DirectX 12是Microsoft最新的DirectX API版本。 Direct3D随DirectX 12一起提供,Direct3D 12是DirectX API集合中的图形API(其他API包括DirectSound,DirectInput,DirectDraw等)。

Direct3D 12的性能比Direct3D的任何以前的迭代要好得多。 Direct3D提供了对图形硬件的较低级别控制,从而可以更有效地使用线程。我们能够使用多个线程来填充命令列表。拥有更多控制权的另一方面意味着我们现在要承担更多责任,例如CPU / GPU同步和内存管理。

Direct3D还通过使用预编译的管道状态对象和命令列表(bundles)来最大程度地减少CPU开销。在应用程序的初始化阶段,我们将创建许多管线状态对象,这些对象由着色器(顶点,像素等)和其他管线状态(混合,光栅化器,基本拓扑等)组成。然后,在运行时,驱动程序不必像在Direct3D 11中那样更改管道的状态,就可以创建管道状态。相反,我们提供了一个管道状态对象,当我们调用draw时,它将使用管道,而且我们没有动态创建管道状态的开销。我们还可以在初始化期间创建一组命令,这些命令可以反复使用称为Bundles的命令。

关于Direct3D的另一件很酷的事情是,它的API调用少得多,根据MSDN,它的调用数约为200(而其中大约三分之一完成了所有艰苦的工作)。

我们将在本教程中学习以下内容:

图形管道概述


  • The Compute Shader
  • Input Assembler (IA) Stage
  • Vertex Shader (VS) Stage
  • Hull Shader (HS) Stage
  • Tessellator (TS) Stage
  • Domain Shader (DS) Stage
  • Geometry Shader (GS) Stage
  • Stream Output (SO) Stage
  • Rasterizer Stage (RS)
  • Pixel Shader (PS) Stage
  • Output Merger (OM) Stage


Direct3D 12工作原理概述


  • The Device
  • Pipeline State Objects
  • Command Lists - Bundles
  • Command Queues
  • Command Allocators
  • Resources
  • Descriptors (Resource Views)
  • Descriptor Tables
  • Descriptor Heaps
  • Root Signatures
  • Resource Barriers
  • Fences and Fence Events
  • Overview of Application Flow Control for Direct3D 12
  • Multithreading in Direct3D 12


初始化Direct3D 12


  •  Creating a device 
  • Creating a command queue
  • Creating a swap chain
  • Creating a descriptor heap
  • Creating a command allocator
  • Creating a root signature
  • Compiling and Creating shader bytecode
  • Creating a pipeline state object
  • Creating a command list
  • Creating a fence and fence event

图形管道概述

图形管道是在图形硬件上运行的一系列过程,被分为许多阶段。 我们将数据推送到流水线中,通过这些阶段处理数据以获得代表3D场景的最终2D图像。 我们还能够使用图形管道从“Stream Output”阶段中流出处理过的几何图形。一些阶段 可以配置(固定功能),而还有一些阶段可以编程(可编程)。 可以编程的阶段称为着色器,它们使用高级着色语言(HLSL)进行编程。

DirectX12初始化综合篇_3d

图形管线里面主要包含五大Shader


  1. ​Vertex Shader​
  2. ​Hull Shader​
  3. ​Domain Shader​
  4. ​Geometry Shader​
  5. ​Pixel Shader​​ 

The Compute Shader(​​MSDN Compute Shader​​)

通过使用GPU作为一种并行处理器来扩展CPU的处理能力,可将计算着色器(也称为Dispatch Pipeline)用于进行极快的计算。 这与图形无关。 例如,您可以使用计算着色器管道在GPU上执行性能非常昂贵的操作,例如精确的碰撞检测。 本课程将不讨论计算着色器。

Input Assembler (IA) Stage(​​MSDN Input Assembler Stage​​)

图形流水线的第一阶段称为输入装配(IA)阶段。 这是一个固定的功能阶段,这意味着我们不能进行编程来实现它。 取而代之的是,我们指示设备配置它IA,以便它知道如何从我们以包含顶点和索引数据的缓冲区形式提供的数据中创建诸如三角形,直线或点之类的几何图元。 我们向IA提供输入布局,以便它知道如何读取顶点数据。 在将数据组合成几何图元之后,它将这些几何图元传送到流水线的其余阶段。

IA阶段还具有另一个功能。 将几何图元放在一起时,它将以字符串形式的系统生成的值附加到几何图元(几何图元ID,实例ID,顶点ID等)。 这些值称为语义。

我们可能提供给IA的输入布局示例如下所示:

D3D12_INPUT_ELEMENT_DESC layout[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_PER_VERTEX_DATA, 0 },
};

此输入布局告诉IA,顶点缓冲区中的每个顶点都有一个元素,应将其绑定到顶点着色器中的“ POSITION”参数。 它还表示此元素从顶点的第一个字节开始(第二个参数为0),并包含3个浮点数,每个浮点数均为32位或4个字节(第三个参数DXGI_FORMAT_R32G32B32_FLOAT)。 在后面的教程中,我们将更多地讨论输入布局。

Vertex Shader (VS) Stage(​​MSDN Vertex Shader Stage​​)

VS是第一个着色器(可编程)阶段,这意味着我们必须自己对其进行编程。 VS阶段是将图元在AI中组装后所有顶点都要经过的阶段。 绘制的每个顶点都将通过VS。 使用VS,您可以执行诸如变换,缩放,照明,对纹理进行位移贴图之类的操作。 即使不需要修改程序中的顶点,也必须始终实现顶点着色器以使管道正常工作。 最简单的顶点着色器将简单地将顶点位置传递到下一个阶段:

float4 main(float4 pos : POSITION) : SV_POSITION
{
return pos;
}

该顶点着色器仅返回输入顶点的位置。 注意VS参数中Pos之后的POSITION。 这是一个语义的例子。 创建顶点(输入)布局时,我们为顶点的位置值指定POSITION,因此它们将被发送到VS中的此参数。 您可以根据需要修改POSITION,保持一致就可以啦。

Hull Shader (HS) Stage(​​MSDN Tesselation Stages​​)

HS阶段是称为Tessellation阶段的三个可选阶段中的第一个。曲面细分(Tessellation)阶段包括“壳(Hull)着色器”,“曲面细分”和“域着色器”阶段。他们共同实现称为Tessellation的东西。Tessellation的作用是将一个基本对象(例如三角形或直线)分割成许多较小的部分,从而以极快的速度增加模型的细节。它会在将所有这些新基础图元放入屏幕之前在GPU上创建它们,并且将它们不保存到内存中,因此,与在需要将它们存储在内存中的CPU上创建它们相比,这节省了很多时间。您可以采用低精度模型,然后使用​​Tesselation ​​来将其转变为高精度的模型。

因此,回到Hull 着色器。这是另一个可编程阶段。这个还用不到,我将不做详细介绍,但此阶段要做的是计算向基本图元对象添加新顶点的方式和位置,以使其增加更多的细节。然后,它将数据发送到Tessellator阶段和Domain Shader阶段。

Tessellator (TS) Stage(​​MSDN Tesselation Stages​​)

细分阶段是细分过程的第二阶段。 这是固定功能阶段。 该阶段要做的是从Hull着色器获得输入,然后实际进行图元的分割。 然后,它将数据传递到“域着色器”。

Domain Shader (DS) Stage(​​MSDN Tesselation Stages​​)

这是细分过程中三个阶段的第三个阶段。 这是一个可编程功能阶段。 这个阶段要做的是从Hull Shader阶段获取新顶点的位置,并转换从tessallator阶段接收到的顶点以创建更多细节,因为仅在三角形或直线的中心添加更多顶点不会增加 任何细节。 然后将顶点传递到几何着色器阶段。

Geometry Shader (GS) Stage(​​MSDN Geometry Shader Stage​​)

这是另一个可选的着色器阶段。 这也是另一个可编程功能阶段。 它接受图元作为输入,例如三角形的3个顶点,直线的2个顶点和一个点的顶点。 它还可以从边缘相邻图元中获取数据作为输入,例如一条线的附加2个顶点,或三角形的附加3个顶点。 GS的一个优点是它可以创建或销毁图元,而VS无法做到(VS可以获取一个顶点,然后输出一个顶点)。 在这一阶段,我们可以将一个点变成四边形或三角形,这使其非常适合在粒子引擎中使用。 我们能够将数据从GS传递到光栅化器阶段,或通过流输出传递到内存中的顶点缓冲区。 我们将在以后的教程中了解有关此着色器阶段的更多信息。

Stream Output (SO) Stage(​​MSDN Stream Output Stage​​ )

该阶段用于从管线中获取顶点数据,特别是在没有GS的情况下,尤其是几何着色器阶段或顶点着色器阶段。 从SO发送到内存的顶点数据被放入一个或多个顶点缓冲区。 从SO输出的顶点数据始终以列表形式发送,例如线列表或三角形列表。 永远不会发出不完整的图元,就像在顶点和几何阶段一样,它们会被无声地分解。 不完整的图元是诸如只有两个顶点的三角形或只有一个顶点的线之类的图元。

Rasterizer Stage (RS)(​​MSDN Rasterizer Stage​​)

RS阶段获取发送给它的矢量信息(形状和图元),并通过在每个图元上插值每个顶点值将它们转换为像素。 它还处理剪切,这基本上是剪切屏幕视图之外的图元。 这取决于我们所谓的视口,可以在代码中进行设置。

Pixel Shader (PS) Stage(​​MSDN Pixel Shader Stage​​)

该阶段进行计算并修改将在屏幕上看到的每个像素,例如基于每个像素的照明。它是另一个可编程着色器,并且是一个可选阶段。 RS为图元中的每个像素调用一次像素着色器。就像我们之前说过的那样,图元中每个顶点的值和属性将插值到RS中的整个图元元素中。基本上就像顶点着色器,其中顶点着色器具有1:1映射(它接受一个顶点并输出一个顶点),而像素着色器也具有1:1映射(它接受一个像素并输出一个像素) 。

像素着色器的工作是计算每个像素片段的最终颜色。像素片段是将被绘制到屏幕上的每个潜在像素。例如,在实心圆后面有一个实心正方形。正方形中的像素是像素片段,圆形中的像素是像素片段。每个都有写入屏幕的机会,但是一旦进入输出合并阶段,即决定要绘制到屏幕的最终像素,它将看到圆的深度值小于圆的深度值。正方形,因此将只绘制圆形中的像素。 PS输出4D颜色值。

一个简单的Pixel Shader的示例可能如下所示:

float4 main() : SV_TARGET
{
return float4(1.0f, 1.0f, 1.0f, 1.0f);
}

该像素着色器将绘制到屏幕上的所有像素的几何图形设置为白色。 基本上,使用此像素着色器,绘制的任何几何图形都将是完全白色的。

Output Merger (OM) Stage(​​MSDN Output Merger Stage​​)

管道中的最后阶段是输出合并阶段。 基本上,此阶段获取像素片段和深度/模板缓冲区,并确定实际将哪些像素写入渲染目标。 它还基于我们设置的混合模型和混合因子应用混合。 渲染目标是Texture2D资源,我们使用设备接口将其绑定到OM。 场景完成渲染到渲染目标上后,我们可以在swapchain上调用present来显示结果!

Direct3D 12工作原理概述

这只是Direct3d 12的概述。以后的教程将更深入。

DirectX12初始化综合篇_d3_02

Pipeline State Objects (PSO)(​​MSDN Pipeline States​​)

管道状态对象由​​ID3D12PipelineState​​接口表示,并由设备接口通过​​CreateGraphicsPipelineState()​​方法创建。若要设置管道状态对象,可以调用命令列表的​​SetPipelineState()​​ 方法。

该接口是使Direct3D 12表现出色的一部分。在初始化期间,您将创建许多此类管道状态对象,然后使用命令列表进行设置会占用很少的CPU开销,因为管道状态对象在设置时已经创建,并且将其设置在GPU上就像传递指针一样简单。 您可以创建多个没有数量限制。

创建管道状态对象时,必须填写​​D3D12_GRAPHICS_PIPELINE_STATE_DESC​​结构。设置管道状态对象时,此结构将确定管道的状态。

可以在“管道状态对象”中设置大多数管道状态,但是有一些不能在“管道状态对象”中设置,而是由“命令列表”设置。

可以在管道状态对象中设置的状态

在命令列表里面设置的状态

管道状态对象设置的管道状态不会被命令列表继承(当命令队列一次执行多个命令列表时,管道状态对象从上一个命令列表设置的管道状态不会被下一个命令继承队列中的列表)或BundlesBundles 不会继承由调用命令列表中的管道状态对象设置的管道状态)。在“命令列表”或“Bundles ”创建时设置“命令列表”和“Bundles ”的初始图形管线状态。

未由“管道状态对象”设置的管道状态也不会被“命令列表”继承。另一方面,Bundles 继承未使用“管道状态对象”设置的所有图形管道状态。当Bundle通过方法调用更改管道状态时,在Bundle完成执行后,该状态将继续返回到“命令列表”。

管道状态对象未为命令列表和Bundles 设置的默认图形管道状态为:


  • 通过​​D3D_PRIMITIVE_TOPOLOGY_UNDEFINED​​设置图元拓扑
  • 视口设置为零
  • 裁剪矩形设置为零
  • 混合因子设置为零
  • 深度/模板参考值设置为零
  • 断言被禁用(Predication is disabled)

您可以通过调用​​ClearState​​ 方法将“命令列表”的管道状态设置回默认值。 如果在Bundle上调用此方法,则对命令列表“ close()”函数的调用将返回E_FAIL。

由命令列表设置的资源绑定由命令列表执行的Bundles 继承。 当Bundles 完成执行时,Bundles 设置的资源绑定也将为调用命令列表保持设置。

The Device

该设备由​​ID3D12Device​​ 接口表示。 该设备是一个虚拟适配器,可用于创建命令列表,管道状态对象,根签名,命令分配器,命令队列,fence,资源,描述符和描述符堆。 计算机可能具有多个GPU,因此我们可以使用DXGI工厂来枚举设备,并找到功能级别为11(与Direct3d 12兼容)的第一台设备,而不是软件设备。 Direct3D的最大功能之一是与多线程应用程序更加兼容。 在本教程中,我们将只创建一个设备,这是我们发现的第一个与Direct3d 12兼容的设备,但实际上我们可以找到所有兼容的设备并全部使用它们。 找到要使用的适配器索引后,就可以通过调用​​D3D12CreateDevice()​​.创建设备。

Command Lists (CL)(​​MSDN Command Lists and Bundles​​)

DirectX12初始化综合篇_3d_03

命令列表由​​ID3D12CommandList​​接口表示,并由设备接口通过​​CreateCommandList()​​方法创建。我们使用命令列表来分配要在GPU上执行的命令。 命令可能包括设置管道状态,设置资源,转换资源状态(资源屏障),设置顶点/索引缓冲区,绘制,清除渲染目标,设置渲染目标视图,执行包(命令组)等。

命令列表与命令分配器相关联,该命令分配器将命令存储在GPU上。

首次创建命令列表时,我们需要使用​​D3D12_COMMAND_LIST_TYPE​​ 标志指定该命令列表的类型,并提供与该列表关联的命令分配器。 有四种类型的命令列表: 直接,捆绑,计算和复制。 我们在本教程中讨论直接命令和捆绑包命令列表。

直接命令列表是GPU可以执行的命令列表。直接命令列表需要与直接命令分配器关联(使用D3D12_COMMAND_LIST_TYPE_DIRECT标志创建的命令分配器)。要将命令列表设置为记录状态,我们调用命令列表的Reset()]方法,提供命令分配器和管道状态对象。将NULL作为管道状态对象的参数传递是有效的,并且将设置默认管道状态。

当我们完成命令列表的填充时,我们必须调用close()方法以将命令列表设置为非记录状态。调用close之后,我们可以使用Command Queue执行命令列表。

一旦执行了命令列表,即使GPU还没有完成,我们也可以将其重置(一旦调用execute,则由Command Allocator存储在GPU上运行的命令)。这样,我们就可以重用分配给命令列表的内存(在CPU端,而不是在GPU端,由Command Allocator将命令存储在内存中)。我们将在本教程中进行此操作,在多线程部分中,我将对此进行更好的解释。

捆绑(Bundles)(​​MSDN Command Lists and Bundles​​)

捆绑包由ID3D12CommandList接口表示,与直接命令列表相同,唯一的区别是在创建捆绑包时(通过调用CreateCommandList()方法),可以使用D3D12_COMMAND_LIST_TYPE_BUNDLE标志而不是D3D12_COMMAND_LIST_TYPE_DIRECT标志来创建捆绑包。
捆绑包是一组经常重复使用的命令。它们很有用,因为与命令组有关的大多数CPU工作都是在捆绑创建时完成的。在大多数情况下,捆绑包与命令列表相同,只是捆绑包只能由直接命令列表执行,而直接命令列表只能由命令队列执行。可以重复使用命令列表,但是在再次调用该命令列表上的execute之前,GPU必须先完成该命令列表的执行。在实践中,几乎不可能重复使用命令列表,因为场景会在帧与帧之间变化,这意味着命令列表将在帧与帧之间变化。

Nvidia上有一篇关于Direct3D最佳实践(DX12做和不做)的不错的文章,并传达他们的建议,一个捆绑软件最多只能包含约12条命令,否则,如果添加太多命令,该捆绑软件的可重用性会受到打击,这意味着您将无法经常重复使用它。最好创建许多可以经常重用的小捆绑包,而不是创建几个您不能经常重用的大捆绑包,因为捆绑包的全部是可重用的命令组。

捆绑包不能直接从命令队列中执行。您可以通过从直接命令列表中调用ExecuteBundle()在命令列表上执行捆绑软件。

捆绑包不继承调用直接命令列表设置的管道状态。

命令队列(CQ)(​​MSDN Command Queues​​)

DirectX12初始化综合篇_描述符_04

命令队列由​​ID3D12CommandQueue​​接口表示,并使用设备接口的​​CreateCommandQueue()​​方法创建。 我们使用命令队列来提交要由GPU执行的命令列表。 命令队列还用于更新资源图块映射。

命令分配器(CA)(​​MSDN Command Allocators​​)

命令分配器由​​ID3D12CommandAllocator​​ 接口表示,并使用设备接口的​​CreateCommandAllocator ()​​ 方法创建。

命令分配器表示用于存储命令列表和捆绑包中的命令的GPU内存。

一旦命令列表执行完毕,您可以在命令分配器上调用​​reset()​​释放内存。尽管可以在执行命令队列调用后立即在命令列表上调用reset(),但在调用reset()之前,与命令分配器关联的命令列表必须完全完成在GPU上的执行,否则调用将失败。这是因为GPU可能正在执行命令分配器所代表的内存中存储的命令。这是我们的应用程序必须使用Fences同步CPU和GPU的地方。在对命令分配器调用reset()之前,必须检查围栏以确保与命令分配器关联的命令列表已完成执行。

任何时候与命令分配器关联的命令列表都只能处于记录状态。这意味着对于每个填充命令列表的线程,您将需要至少一个命令分配器和至少一个命令列表。

资源(​​MSDN Resource Binding​​)

资源包含用于构建场景的数据。 它们是存储几何,纹理和着色器数据的内存块,图形管道可以在其中访问它们。 资源类型是资源包含的数据类型。

资源类型

资源引用/视图


  • Constant buffer view (CBV)
  •  Unordered access view (UAV)
  •  Shader resource view (SRV)
  •  Samplers
  •  Render Target View (RTV)
  •  Depth Stencil View (DSV)
  •  Index Buffer View (IBV)
  •  Vertex Buffer View (VBV)
  •  Stream Output View (SOV)

描述符(资源视图)(​​MSDN Descriptors​​)

DirectX12初始化综合篇_3d_05

描述符是一种结构,它告诉着色器在哪里可以找到资源以及如何解释资源中的数据。在D3D11中查看资源视图时,您可以在D3D12中查看描述符。您可能会为同一资源创建多个描述符,因为管道的不同阶段可能会不同地使用它。例如,我们创建一个Texture2D资源。我们创建一个“渲染目标视图”(RTV),以便可以将该资源用作管道的输出缓冲区(将该资源作为RTV绑定到“输出合并”(OM)阶段)。我们还可以为同一资源创建一个无序访问视图(UAV),我们可以将其用作着色器资源并使用其纹理化几何体(例如,如果场景中某处有安全摄像机,则可以执行此操作。摄像机看到的资源(RTV)上的场景,然后我们将该资源(UAV)渲染到安全室中的电视上。

描述符只能放在描述符堆中。没有其他方法可以将描述符存储在内存中(某些根描述符(只能是CBV的根描述符)以及原始或结构的UAV或SRV缓冲区除外。诸如Texture2D SRV之类的复杂类型不能用作根描述符)。

描述符表(DT)(​​MSDN Descriptor Tables​​)

DirectX12初始化综合篇_d3_06

描述符表是描述符堆中的描述符数组。 所有描述符表都是描述符堆的偏移量和长度。

着色器可以通过根签名的描述符表按索引访问描述符堆中的描述符。 因此,要访问着色器中的描述符,您将索引到根签名描述符表中。

CBV,UAV,SRV和Samplers 存储在描述符堆中,并且可由着色器的描述符引用。

RTV,DSV,IBV,VBV和SOV并非通过描述符表进行引用,而是直接绑定到管道。 MSDN文档在这部分内容上有些令人困惑,因此说实话,我对此并不完全确定,但是MSDN说这些没有存储在描述符堆中,但是对于RTV而言,并不是完全正确的, DSV和SOV,因为您需要为其创建堆和描述符。 据我了解,没有其他方法可以创建它们。

描述符堆(DH)(​​MSDN Descriptor Heaps​​)

描述符堆由接口ID3D12DescriptorHeap表示,并使用方法ID3D12Device :: CreateDescriptorHeap()创建。描述符堆是描述符的列表。它们是存储描述符的一块内存。

采样器不能与资源进入相同的描述符堆。描述符堆也可以是“着色器可见”或“非着色器可见”。着色器可见描述符堆是包含着色器可以访问的描述符的堆。这些类型的堆可能包括CBV,UAV,SRV和Sampler描述符。非着色器可见描述符堆是着色器无法引用的堆。这些类型的堆包括RTV,DSV,IBV,VBV和SOV资源类型。法线贴图可能具有三个描述符堆,一个用于采样器,一个用于着色器可见资源,一个用于非着色器可见资源。本教程将只有一个描述符堆,该堆存储渲染目标视图的描述符。下一个教程将有两个,一个用于渲染目标视图,一个用于顶点缓冲区视图(我们将在下一个教程中绘制一个三角形)。

在任何给定时间,只能将一个着色器可见堆和一个采样器堆绑定到管道。您希望描述符堆在最长时间内具有正确的描述符(根据MSDN,“理想情况下是整个渲染帧或更多”)。描述符堆必须有足够的空间来为每个所需状态集动态定义描述符表。为此,您可以在管道状态改变时重用描述符空间(例如,正在渲染一棵树,描述符堆中有一个描述符,当您绘制树的底部时,该描述符指向树皮的UAV。树,当您需要绘制树的叶子时,管道会发生变化,因此您可以通过将树皮纹理的UAV替换为叶子纹理的UAV来重用树皮纹理的UAV。

D3D12允许您在命令列表中多次更改描述符堆。这非常有用,因为较旧的低功耗GPU仅具有65k的描述符堆存储空间。更改描述符堆会导致GPU“刷新”当前的描述符堆,这是一项昂贵的操作,因此您希望尽可能少地执行此操作,并且在GPU不需要大量工作的情况下(例如在开始时)命令列表。

Bundles 仅允许调用SetDescriptorHeaps一次,并且此命令设置的描述符堆必须与调用Bundles 的命令列表已设置的描述符堆完全匹配。

有两种管理描述符堆的方法,这里有两种(在MSDN文档中提到了这些方法):

基本方法(效率不高,但很容易实现)
第一种方法(也是最基本的方法)就在绘制调用之前,将绘制所需的所有描述符添加到描述符堆中,然后在根签名中设置一个描述符表以指向新的描述符。这种方法很好,因为不需要跟踪描述符堆中的所有描述符。但是,由于我们将对绘制调用的所有描述符都添加到了堆中,因此在堆中将有很多重复的描述符,这使得该方法非常无用,尤其是在渲染相似的对象或场景时。

我们必须在描述符堆中的可用空间中添加新的描述符,而不是覆盖先前绘制中描述符堆中已经存在的描述符的原因是,因为GPU实际上可以同时执行多个绘制调用,这意味着描述符已经当我们开始为当前绘制覆盖它们时,描述符堆中的可能会被使用。

使用此方法,由于以下一个或两个原因,您可能需要一个额外的描述符堆:场景又大又复杂,描述符空间用完了,或者可能存在同步问题,因此您有一个描述符堆, GPU读取,然后在GPU执行命令列表时填充另一个CPU,然后在每一帧交换这两个堆。

第二种方法(效率更高,实施起来更困难)
另一种方法是跟踪描述符堆中每个描述符的索引。这种方法非常有效,因为您可以为相似的对象和场景重用描述符。这种方法是有效的,因为在描述符堆中几乎没有或没有重复的描述符。这种方法的缺点是实施起来有点复杂。

如果您的场景足够小,并且没有在整个场景中更改的资源,则实际上可以创建一个巨型描述符表,并在场景结束时刷新该对象,然后需要重新加载新资源。如果在整个场景中唯一改变的是根常量和根描述符,那么这将起作用。描述符表(在根签名中定义)在整个场景中保持不变。

进行一些优化的其他几种方法是在描述符堆中有两个描述符表。一个描述符表的资源在整个场景中不会改变,而另一个描述符表的资源则经常变化。

您将要做的另一件事是确保根常量和根描述符包含最频繁更改的常量和描述符。

根签名(RS)(​​MSDN Root Signatures​​)

DirectX12初始化综合篇_d3_07

根签名定义了着色器访问的数据(资源)。根签名就像一个函数的参数列表,其中函数是着色器,而参数列表是着色器访问的数据类型。根签名包含根常量,根描述符和描述符表。根参数是根签名中的一项,可以是根常量,根描述符或描述符表。应用程序可以更改的根参数的实际数据称为“根参数”。

根签名的最大大小始终为64个DWORDS。

根常量
根常量是内联32位值(它们占用1 DWORD)。这些值直接存储在根签名中。由于根签名的内存是有限的,因此您只想在此处存储着色器访问最常更改的常量值。这些值显示为着色器的恒定缓冲区。从着色器访问这些变量没有成本(无需重定向),因此访问它们非常快。

根描述符
根描述符是着色器最常访问的内联描述符。这些是64位虚拟地址(2个DWORD)。这些描述符仅限于CBV,原始或结构化SRV和UAV。不能使用诸如Texture2D SRV之类的复杂类型。从着色器引用根描述符时,需要进行一次重定向。关于根描述符的另一件事要注意的是,它们只是指向资源的指针,它们不包含数据的大小,这意味着从根描述符访问资源时不能进行越界检查,这与存储在根描述符中的描述符不同。描述符堆,它确实包括大小,并且可以在其中进行边界检查。

描述符表
上面讨论过,描述符表是描述符堆的偏移量和长度。描述符表只有32位(1个DWORD)。描述符表中有多少个描述符没有限制(间接允许最大描述符堆大小的描述符数目除外)。从描述符表访问资源时,会有两个间接开销。第一个间接方法是从描述符表指针到存储在堆中的描述符,然后从描述符堆到实际资源。

资源屏障(​​MSDN Resource Barriers​​)

资源屏障用于更改资源或子资源的状态或使用情况。 Direct3D 12引入了资源屏障作为其多线程友好API的一部分。资源屏障用于帮助在多个线程之间同步资源的使用。

资源屏障分为三种:过渡屏障(Transition Barrier), 别名屏障(Aliasing Barrier), 和无序访问视图(UAV)屏障( Unordered Access View (UAV) Barrier)

过渡屏障
当您要将资源或子资源的状态从一种状态转换到另一种状态时,可以使用过渡屏障。何时更改资源状态的一个示例是在翻转交换链之前将资源从渲染目标状态更改为当前状态。

别名屏障
别名障碍与平铺资源一起使用。这些障碍用于更改具有映射到同一个图块池(来自msdn)的两个不同资源的用法。目前,我还没有对切片资源的透彻了解,因此在这里我将不做解释。

无序访问视图(UAV)屏障
UAV屏障用于确保在调用此屏障之前完成所有读/写操作。这样一来,例如,如果正在写入UAV,则有一个绘图调用,在执行绘图调用之前完成对UAV的写入。无需在仅从UAV读取的两个抽签或调度调用之间创建UAV屏障。如果同一UAV由两个不同的绘制或调度调用写入,也不需要,只要应用程序确定一个在另一个开始之前就已经完全完成即可。如果要绘制纹理,则可以在UAV上使用UAV屏障,然后使用该纹理在模型上绘制。在将UAV用作模型上的纹理之前,UAV屏障将确保绘制到UAV的调用已完成。

围栏和围栏事件

DirectX 12的一部分“和metal相似”,这是事实,我们可以将命令队列发送到GPU以开始执行,然后可以立即在CPU上再次开始工作。为了确保我们不修改或删除GPU当前正在使用的内容,我们使用围栏。 Fences和Fence Events将让我们知道GPU在执行命令队列时的位置。在此应用中,我们要做的是告诉GPU执行命令队列,更新游戏逻辑,检查/等待GPU完成执行命令队列,通过将更多命令查询到命令列表中来更新管道,然后再次执行执行命令队列。通过首先用命令填充命令列表,执行命令队列,发信号通知命令列表以将围栅值设置为指定值,然后检查围栅值是否是我们告诉命令列表将其设置为的值,该方法起作用。如果是这样,我们知道命令列表已完成其命令列表,并且可以重置命令列表和队列,然后重新填充命令列表。如果围栅值仍然不是我们所指示的值,则创建围栅事件,然后等待GPU发出该事件的信号。围栏由ID3D12Fence接口表示,而围栏事件是句柄HANDLE。设备使用方法ID3D12Device :: CreateFence()创建围栅,并使用CreateEvent()方法创建围栅事件。

Direct3D 12的应用程序流控制概述

DirectX12初始化综合篇_3d_08

当然,有很多方法可以完成工作,这是Direct3d应用程序的典型大纲:


  1. 初始化应用程序
  2. 启动主循环
  3. 设置场景(如果是新场景)
  4. 更新游戏逻辑
  5. 如果需要,加载/释放资源
  6. 等待GPU
  7. 重置命令分配器和命令列表
  8. 填充命令列表
  9. 如果有多线程,请等待命令列表线程
  10. 执行命令列表
  11. 转到3

现在来详细解释每个阶段

1.初始化应用程序
此阶段可能包括以下内容:


  • 从文件或数据库加载设置
  • 确保这是应用程序的唯一实例
  •  查询更新
  • 检查内存需求
  • 检查许可(例如试用版,演示版,甚至是盗版)
  • 创建一个窗口
  • 初始化脚本引擎
  • 设置资源管理器
  • 设置音频系统
  • 设置网络
  • 设置控制器
  • 初始化Direct3D,其中包括以下内容:
  • 设置描述符堆(如果有的话,描述符堆管理器)
  • 设置命令列表
  • 设置RTV
  • 设置命令分配器
  • 设置所有管道状态对象(您将拥有许多)
  • 设置所有根签名(通常只需要一个)

2.启动主循环
您几乎可以在这里开始主循环,检查Windows消息,如果没有,请继续更新游戏。

3.设置场景
当然,这是在主循环内,因为您的游戏中可能有很多场景。您当然可以做任何想做的事,但是通常情况下,场景变化时不必退出主循环

此阶段包括以下内容:


  • 加载整个场景所需的资源(在场景改变或玩家退出之前不会释放的东西)。这包括纹理,几何形状,文本等。
  • 加载初始资源(这些资源是您在场景中立即需要的,例如,如果您在房间内开始,则将加载房间纹理和该房间内的任何模型。离开房间后,这些资源可能会释放)。如果您的场景足够小,可以加载整个场景所需的每个资源,则这些内容可能与上面的项目相同。
  • 设置摄像机,初始视口以及视图和投影矩阵。
  • 设置整个场景可能需要的命令包

4.更新游戏逻辑
这确实是游戏的核心。在这里,您将执行诸如更新A.I,检查来自网络或用户的输入,更新场景中的对象(例如位置和动画)之类的操作。您知道,只需更新游戏逻辑即可。 (我现在暂时不更新其他系统,例如音频,人工智能,网络,控制器,动画等)

5.如果需要,加载/释放资源
这是您的资源管理器进入的位置。如果对象进入的场景具有尚未加载的纹理,则可以在此处加载该对象。如果某个物体离开了场景,您也可以在此处将其释放。如果愿意,可以将资源管理器放在单独的线程上。一种执行此操作的方法是,如果有对象进入场景,则游戏逻辑会通知资源管理器。当主循环继续时,资源管理器将开始加载所需的纹理。如果在绘制对象时尚未加载纹理,则资源管理器将提供临时或默认纹理。这对于具有开放世界的游戏很有用。与对象离开场景时相同,而不是在主循环等待资源管理器释放资源再继续之前,它让资源管理器(在单独的线程上)知道不需要哪些资源(通常是引用计数)达到零,并在资源管理器释放资源时继续执行。

6.等待GPU
您很可能将使用双倍或三倍缓冲,这意味着您将至少拥有2-3个命令分配器。这样做的原因是因为在执行与其关联的命令列表时无法重置命令分配器(另一方面,使用命令队列执行命令后可以立即重置命令列表)。这意味着对于每个帧,此时,就在重置命令分配器之前,您将检查GPU是否已完成与该命令分配器关联的命令列表。您将为此使用围栏和围栏事件。您还需要为每个框架设置围栏,并为每个线程需要一个围栏事件。

当您在帧“ f”上执行命令列表时,下一步是等待GPU在帧“ f + 1”结束。对于三重缓冲,它将如下所示:


  • 等待GPU完成第1帧
  • 渲染框架1
  • 等待GPU完成第2帧
  • 渲染框架2
  • 等待GPU完成第3帧
  • 渲染框架3
  • 等待GPU完成第1帧
  • 渲染框架1

7.重置命令分配器和命令列表
在等待GPU完成您将要使用的命令分配器之后,您将其重置以及重置命令列表。如果与前一帧相比绝对没有任何变化,则不必总是重置命令列表,但是几乎从来没有这样。如果您知道经常重复执行某些命令序列,请将它们放入捆绑包中,然后在命令列表中执行捆绑包。

8.填充命令列表
这包括您希望GPU执行的大多数操作,例如绑定诸如顶点和索引缓冲区之类的资源,纹理,创建描述符,设置管道状态,使用资源屏障,设置围栏值等。

9.如果有多线程,请等待命令列表线程
如果您有多线程应用程序,则可能需要在单独的线程上填充命令列表。一次只能有一个线程访问命令列表,因此每个线程将需要自己的命令列表,以及自己的围栏,围栏事件和命令分配器。您使用命令队列调用执行命令列表数组,因此主线程将等待,直到命令列表线程完成了其命令列表的填充。它将把命令列表(如果尚未放置)放入数组中,并使用命令队列执行它们。下面讨论多线程。

Direct3D 12中的多线程

我觉得我需要对利用多线程的应用程序的结构快速说一句话,因为这才是Direct3D 12真正强大的地方。

实际上很简单。它看起来像这样:

1.初始化应用程序(包括d3d和其他所有内容)
2.启动主循环
3.更新游戏逻辑
4.产生多个线程
5.每个线程等待GPU完成执行上一帧的命令列表
6.每个线程重置先前的帧命令分配器
7.每个线程重置其命令列表
8.每个线程填写其命令列表
9.主线程等待命令列表线程完成其命令列表的填写
10.使用完成的命令列表数组执行命令队列
11.转到3。

每个线程都有自己的命令列表
这是多线程应用程序变得有趣的地方。您需要以某种方式在逻辑上分割场景,以便每个线程可以填充部分场景的命令列表。有两种方法可以执行此操作,但是请记住,在执行命令列表时,将按照您在数组中提供命令的顺序来执行它们。为了使应用程序获得最大性能,您一直想做的一件事就是按管道状态对实体进行分组。因此,如果场景中的两个对象使用特定的PSO,则需要尝试将它们放在一起,这样您只需要为它们更改一次PSO。如果未将它们放在一起,则可能必须更改PSO两次,每个对象一次。但是,这并非始终是对命令进行分组的最佳方法。由于诸如透明度之类的某些因素,您几乎总是需要从远处绘制场景以接近相机。如果共享同一PSO的那两个对象是窗口,一个窗口很远,并且一个在摄像机前面,如果您将这两个对象共享在一起,因为它们共享了相同的PSO,则场景将被渲染为错误。如果您先画出它们,则前窗后将什么也不会出现。

您要分组的第一件事很可能是距相机的距离。您可能有一个绘制远距离对象的命令列表,一个绘制近距离对象的命令列表,一个绘制远处背景风景和天空的命令列表,一个绘制用户界面(例如生命值)的命令列表状态以及用于帧后处理的另一个命令列表。

多线程中的命令分配器
命令分配器在任何时候都只能记录一个命令列表。这意味着对于每个线程,您必须有一个单独的命令列表。您无法在执行命令列表时重置命令分配器,这意味着您将需要每个帧缓冲区的命令分配器(需要2个的双缓冲,需要3个的三重缓冲)。

由于上述原因,程序中的命令分配器的数量必须为:NumberOfThreads * NumberOfFrameBuffers

如果您的应用程序有2个线程来填充命令分配器,并且您正在使用三重缓冲,则将需要2 * 3 = 6个命令分配器。

描述符堆管理

初始化Direct3D 12

在本教程中,我们仅设置Direct3D 12,这样我们就能看到一些东西,我们使用命令列表来清除渲染目标。因此,除了默认管道状态外,我们不需要任何其他内容,因此本教程中不使用管道状态对象(PSO)和根签名。在下一个教程中,我们将绘制一个三角形,在那里我们需要设置PSO和Root Signature。

声明
我们在上一教程中获得的第一批新内容是一堆声明。其中的第一部分是我们需要与GPU交互的接口和变量,而第二部分是新功能。当我们开始使用它们时,我们将更多地讨论其中的大多数。

我们将使用三重缓冲(3帧缓冲),这就是我给您的建议,可在您的应用程序中使用。甚至没有必要让玩家在双重缓冲和三重缓冲之间进行选择,您也可以自动为他们提供三重缓冲。

其中一些对象的数量取决于您拥有多少个帧缓冲区和线程。这些对象包括:

Render Targets:     Number of frame buffers
Command Allocators: Number of frame buffers * number of threads
Fences: Number of threads
Fence Values: Number of threads
Fence Events: Number of threads
Command Lists: Number of threads
// direct3d stuff
const int frameBufferCount = 3; // number of buffers we want, 2 for double buffering, 3 for tripple buffering

ID3D12Device* device; // direct3d device

IDXGISwapChain3* swapChain; // swapchain used to switch between render targets

ID3D12CommandQueue* commandQueue; // container for command lists

ID3D12DescriptorHeap* rtvDescriptorHeap; // a descriptor heap to hold resources like the render targets

ID3D12Resource* renderTargets[frameBufferCount]; // number of render targets equal to buffer count

ID3D12CommandAllocator* commandAllocator[frameBufferCount]; // we want enough allocators for each buffer * number of threads (we only have one thread)

ID3D12GraphicsCommandList* commandList; // a command list we can record commands into, then execute them to render the frame

ID3D12Fence* fence[frameBufferCount]; // an object that is locked while our command list is being executed by the gpu. We need as many
//as we have allocators (more if we want to know when the gpu is finished with an asset)

HANDLE fenceEvent; // a handle to an event when our fence is unlocked by the gpu

UINT64 fenceValue[frameBufferCount]; // this value is incremented each frame. each fence will have its own value

int frameIndex; // current rtv we are on

int rtvDescriptorSize; // size of the rtv descriptor on the device (all front and back buffers will be the same size)

// function declarations
bool InitD3D(); // initializes direct3d 12

void Update(); // update the game logic

void UpdatePipeline(); // update the direct3d pipeline (update command lists)

void Render(); // execute the command list

void Cleanup(); // release com ojects and clean up memory

void WaitForPreviousFrame(); // wait until gpu is finished with command list

WinMain()

在我们的主要功能中,我们需要初始化Direct3D。 如果初始化失败,我们将显示一条消息并关闭我们的应用程序。

在退出主循环(Running为false)后,我们需要等待GPU完成其所有工作(WaitForPreviousFrame()),然后释放资源和COM对象。 我们还需要关闭我们的围栏事件句柄。

...
// initialize direct3d
if (!InitD3D())
{
MessageBox(0, L"Failed to initialize direct3d 12",
L"Error", MB_OK);
Cleanup();
return 1;
}

...

// we want to wait for the gpu to finish executing the command list before we start releasing everything
WaitForPreviousFrame();

// close the fence event
CloseHandle(fenceEvent);

...

InitD3D()

这是本教程的重要部分。 这是我们初始化Direct3D 12的地方。

bool InitD3D()
{
HRESULT hr;

创建Direct3D设备

初始化Direct3D 12所需做的第一件事是创建设备。 我们可能有多个兼容设备,因此我们只选择与功能级别11(directx 12)兼容的第一台设备,而不是软件设备。 找到适配器(实际设备)后,我们通过调用方法D3D12CreateDevice()创建了Direct3d 12设备。

HRESULT WINAPI D3D12CreateDevice(
_In_opt_ IUnknown *pAdapter,
D3D_FEATURE_LEVEL MinimumFeatureLevel,
_In_ REFIID riid,
_Out_opt_ void **ppDevice
);

D3D12CreateDevice()具有3个参数:


  • pAdapter-第一个参数是我们要使用Direct3D 12设备的适配器(GPU)的指针
  • MinimumFeatureLevel-第二个参数是我们希望设备使用的功能级别
  • riid-第三个参数是我们要在其中存储设备的接口的类型ID
  • ppDevice-这是指向设备接口的指针。 通过在此处提供我们的设备(将我们的设备的引用强制转换为指向指针的void指针),一旦此函数完成,它将把我们的设备接口指向一个内存块(就我们而言)

您会注意到,要找到兼容的适配器,我们使用第四个参数为NULL的D3D12CreateDevice()进行调用。 这样一来,我们还不会创建设备,因为在创建设备之前,我们要确保此方法成功。 如果成功,我们知道我们有一个支持功能级别11的适配器(GPU)。
如果找不到适配器,则Direct3D初始化失败,因此我们返回false,以使我们的主函数知道关闭应用程序。

// -- Create the Device -- //

IDXGIFactory4* dxgiFactory;
hr = CreateDXGIFactory1(IID_PPV_ARGS(&dxgiFactory));
if (FAILED(hr))
{
return false;
}

IDXGIAdapter1* adapter; // adapters are the graphics card (this includes the embedded graphics on the motherboard)

int adapterIndex = 0; // we'll start looking for directx 12 compatible graphics devices starting at index 0

bool adapterFound = false; // set this to true when a good one was found

// find first hardware gpu that supports d3d 12
while (dxgiFactory->EnumAdapters1(adapterIndex, &adapter) != DXGI_ERROR_NOT_FOUND)
{
DXGI_ADAPTER_DESC1 desc;
adapter->GetDesc1(&desc);

if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
{
// we dont want a software device
adapterIndex++; // add this line here. Its not currently in the downloadable project
continue;
}

// we want a device that is compatible with direct3d 12 (feature level 11 or higher)
hr = D3D12CreateDevice(adapter, D3D_FEATURE_LEVEL_11_0, _uuidof(ID3D12Device), nullptr);
if (SUCCEEDED(hr))
{
adapterFound = true;
break;
}

adapterIndex++;
}

if (!adapterFound)
{
return false;
}

找到与功能级别11.0(Direct3D 12)兼容的硬件适配器后,就可以创建设备。 您可能想知道为什么这里只有3个参数,而第三个参数引用了我们的设备接口,但是当您查看D3D12CreateDevice()参数时,您会看到第三个参数应该是REFIID或我们的接口类型。 这里实际发生的是使用了一个宏,该宏中提供了两个参数IID_PPV_ARGS。 您将在整个教程代码和整个MSDN代码中看到该用法。 这样做只是为了使其更容易,但是面对现实,我们可以没有它。 首先,我将向您展示该宏:

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

您可以看到第一个是我们设备的uuid,即ID3D12Device。 第二个实际上是模板方法,其定义为:

template<typename T> _Post_equal_to_(pp) _Post_satisfies_(return == pp) void** IID_PPV_ARGS_Helper(T** pp) 
{
#pragma prefast(suppress: 6269, "Tool issue with unused static_cast")
static_cast<IUnknown*>(*pp); // make sure everyone derives from IUnknown
return reinterpret_cast<void** >(pp);
}

基本上,此方法所做的是确保我们提供的接口是从IUnknown派生的。除了使用IID_PPV_ARGS宏,我们可以做的是:

D3D12CreateDevice(
adapter,
D3D_FEATURE_LEVEL_11_0,
_uuidof(ID3D12Device),
reinterpret_cast<void** >(&device)
);

但是本教程将使用IID_PPV_ARGS宏来保持一致性,并减少代码量(上面的代码不像IID_PPV_ARGS提供的那样具有额外的类型安全性)。这是我们将如何创建设备的方法:

// Create the device
hr = D3D12CreateDevice(
adapter,
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&device)
);
if (FAILED(hr))
{
return false;
}

创建RTV命令队列

这是我们为设备创建命令队列的地方。 我们将使用此命令队列执行命令列表,其中包含告诉GPU怎么做的命令。要创建命令队列,我们调用设备接口的CreateCommandQueue()方法。 该方法如下所示:

HRESULT CreateCommandQueue(
[in] const D3D12_COMMAND_QUEUE_DESC *pDesc,
REFIID riid,
[out] void **ppCommandQueue
);

  • pDesc-这是指向已填充的D3D12_COMMAND_QUEUE_DESC结构的指针,该结构描述了命令队列的类型
  • riid-命令队列接口的类型ID
  • ppCommandQueue-指向我们的命令队列接口的指针。

如果我们的GPU内存不足,则此函数将返回E_OUTOFMEMORY。我们必须填写一个D3D12_COMMAND_QUEUE_DESC,我们可以使用它提供CreateCommandQueue()方法,该结构如下所示:

typedef struct D3D12_COMMAND_QUEUE_DESC {
D3D12_COMMAND_LIST_TYPE Type;
INT Priority;
D3D12_COMMAND_QUEUE_FLAGS Flags;
UINT NodeMask;
} D3D12_COMMAND_QUEUE_DESC;



  • Type -这是D3D12_COMMAND_LIST_TYPE枚举。有3种类型的命令队列,我将在下面讨论。默认值为D3D12_COMMAND_LIST_TYPE_DIRECT
  • Priority -这是D3D12_COMMAND_QUEUE_PRIORITY枚举。默认值为D3D12_COMMAND_QUEUE_PRIORITY_NORMAL。如果您有多个命令队列,如果一个队列需要优先级,则可以将其更改为D3D12_COMMAND_QUEUE_PRIORITY_HIGH。
  • Flags -另一个枚举,但来自D3D12_COMMAND_QUEUE_FLAGS枚举。默认值为D3D12_COMMAND_QUEUE_FLAG_NONE,但是如果您不希望GPU在执行命令队列时超时,则可以更改为D3D12_COMMAND_QUEUE_FLAG_DISABLE_GPU_TIMEOUT。除非您知道操作将花费很长时间,否则我不建议您使用D3D12_COMMAND_QUEUE_FLAG_DISABLE_GPU_TIMEOUT。如果GPU出现问题导致其挂起,它将超时并停止执行命令队列。使用D3D12_COMMAND_QUEUE_FLAG_DISABLE_GPU_TIMEOUT并具有导致GPU挂起的队列可能会导致计算机冻结并需要重新启动。
  • NodeMask-这是一个位字段,指示该命令队列应在哪个GPU节点上执行。默认情况下,此值设置为0(零),如果仅使用一个GPU,则应设置为0。如果您有多个GPU,请参阅多功能适配器(​Multi-Adapter​)。


有3种类型的命令队列,由提供给D3D12_COMMAND_QUEUE_DESC结构的D3D12_COMMAND_LIST_TYPE定义。 他们是:


  • 直接命令队列-由D3D12_COMMAND_LIST_TYPE_DIRECT枚举定义。 这是默认命令队列。 直接命令队列是接受所有命令的队列。 这就是我们将要使用的类型
  • 计算命令队列-由D3D12_COMMAND_LIST_TYPE_COMPUTE枚举定义。 计算命令队列仅接受计算和复制命令
  • 复制命令队列-由D3D12_COMMAND_LIST_TYPE_COPY枚举定义。 复制命令队列仅接受复制命令

  注意再次使用IID_PPV_ARGS宏。 我在创建设备时已在上面进行了解释。

// -- Create the Command Queue -- //

D3D12_COMMAND_QUEUE_DESC cqDesc = {}; // we will be using all the default values

hr = device->CreateCommandQueue(&cqDesc, IID_PPV_ARGS(&commandQueue)); // create the command queue
if (FAILED(hr))
{
return false;
}

创建交换链

这是我们创建交换链的地方。 交换链将用于呈现完成的渲染目标。 我们将使用三重缓冲,因此我们还必须跟踪应该渲染到的渲染目标。 dxgi工厂将创建一个IDXGISwapChain,但我们需要一个IDXGISwapChain3来获取当前的后缓冲索引。 我们可以安全地将IDXGISwapChain强制转换为IDXGISwapChain3。

我们首先填充一个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;

  • BufferDesc-这是DXGI_MODE_DESC,用于描述显示模式,例如宽度,高度和格式
  • SampleDesc-这是一个DXGI_SAMPLE_DESC,它描述了我们的多重采样。
  • BufferUsage-这是DXGI_USAGE枚举,它告诉swapchain这是渲染目标还是着色器输入。我不确定将其用作着色器输入有什么用,所以您必须自己找出。我们将其用作渲染目标,因此我们将使用DXGI_USAGE_RENDER_TARGET_OUTPUT。此参数的默认值为DXGI_CPU_ACCESS_NONE。
  • BufferCount-这是我们想要的后备缓冲区数。在本教程中,我们将使用三重缓冲,因此我们将其设置为3(或frameBufferCount)。默认值为0。
  • OutputWindow-这是我们将在其上显示后缓冲区的窗口的句柄。默认值为空指针。
  • Windowed -表示我们应该以全屏模式显示还是以窗口模式显示。两者之间实际上有很大的不同。如果您想了解解锁的FPS,请观看此​​视频​​。本方法实际上将在等待刷新速率时阻塞。在DirectX 12中,如果您想解锁FPS,则必须在应用程序中使用非常非常特定的设置组合。呈现渲染目标时,未锁定的FPS可能会导致撕裂,因此,这不是在发行版本中要执行的操作,仅用于基准测试。当前模式将在显示后备缓冲区之前等待显示器刷新,这意味着您被锁定为显示器刷新率的倍数。使用双缓冲,如果刷新率为60赫兹,则可以获得60FPS。我们使用三重缓冲,这意味着我们能够获得120 FPS。尽管我在某处阅读过使用3个以上的缓冲区,但可能会出现问题,并且当前将锁定或发生某些情况。如果您想了解更多,就必须对此进行研究和实验。另外,取自MSDN:

“我们建议您创建一个窗口交换链,并允许最终用户通过IDXGISwapChain :: SetFullscreenState将交换链更改为全屏;即,不要将此成员设置为FALSE来强制交换链为全屏。 如果您将交换链创建为全屏显示,则还应通过BufferDesc成员向最终用户提供支持的显示模式列表,因为使用不受支持的显示模式创建的交换链可能会导致显示黑屏并阻止最终显示。 用户看不到任何东西。”

SwapEffect-这是​DXGI_SWAP_EFFECT​枚举,它描述了缓冲区在呈现后如何处理。 默认值为DXGI_SWAP_EFFECT_DISCARD。 我们将使用DXGI_SWAP_EFFECT_FLIP_DISCARD

Flags -这是​DXGI_SWAP_CHAIN_FLAG​枚举,您可以| 一起。 默认值为0,在本教程中我们将保持这种方式。

现在我们来看看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;

Width -这是我们的后缓冲区的宽度分辨率。默认值为0。如果指定为0,则从设备界面调用CreatSwapChain()方法时,会将此值设置为窗口的宽度。然后,您可以在swapchain接口上调用GetDesc()以获取后缓冲区的宽度。我们将其手动设置为窗口的宽度,但是在我们的教程代码中,我们可以将其保留为默认值0并获得相同的结果。

Height-与Width相同,但是我们的后缓冲区的高度代替。

RefreshRate-​DXGI_RATIONAL​结构,用于定义交换链的刷新率(以赫兹为单位)。默认值为0分子和0分母。此结构表示一个有理数。 0/0(或分母/分子)有效,结果为0/1。 0 / number的结果为0(意味着默认值0/0被0/1代替,然后为0)。整数用1 /数字表示。

Format -这是交换链的显示格式,由​DXGI_FORMAT​枚举描述。默认格式为DXGI_FORMAT_UNKNOWN,如果尝试保持这种格式,将导致错误。在我们的教程中,我们将其设置为32位无符号普通整数rgba格式,其中rgba每个都有8位。

ScanlineOrdering-这是​DXGI_MODE_SCANLINE_ORDER​结构,用于描述扫描线绘制模式。默认值为DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED,这表示未指定扫描线模式。我们将以这种方式离开它。

Scaling -​DXGI_MODE_SCALING​枚举。此结构定义是否指定缩放比例,缓冲区是否居中或是否应拉伸缓冲区图像。默认值为DXGI_MODE_SCALING_UNSPECIFIED。通过使用未指定的缩放比例,调整窗口大小时不会触发模式更改,这与居中或拉伸的其他两个枚举不同。

这是DXGI_SAMPLE_DESC结构,用于描述多重采样。 我们没有使用多重采样,因此我们将采样数设置为1。之所以需要将采样数设置为1,是因为我们需要从后缓冲中获取至少一个采样。

使用多重采样,因此当图像距离相机更远或更近时,我们会获得更少的伪像和更平滑的外观。

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

  • Count -我们将为每个像素采样的样本数(以不同的分辨率)。 默认值为0,如果我们尝试显示它,则会导致错误,因为我们不会从后缓冲区中提取样本来显示。 将该值设置为1将获取一个采样,而更高的采样称为多重采样。
  • Quality -所采集样品的质量。 默认值为0,我们将保留默认采样值。

最后,为了创建交换链,我们调用了DXGI工厂的CreateSwapChain()方法。 函数签名如下所示:

HRESULT CreateSwapChain(
[in] IUnknown *pDevice,
[in] DXGI_SWAP_CHAIN_DESC *pDesc,
[out] IDXGISwapChain **ppSwapChain
);

  • pDevice-这是指向Direct3d设备的指针,该设备会将图像写入交换链返回缓冲区。
  • pDesc-这是对我们上面讨论的交换链描述的引用,它定义了交换链及其后缓冲区。
  • ppSwapChain-这是指向IDXGISwapChain接口的指针。 在我们的应用程序中,我们使用IDXGISwapChain3,以便可以从交换链中获取当前的后缓冲区,但是由于此函数返回指向IDXGISwapChain的指针,因此我们创建了一个临时IDXGISwapChain接口,该接口将传递给此函数,然后将其static_cast传递给派生的IDXGISwapChain3 并将我们的swapChain接口设置为指向创建的交换链内存。

创建交换链后,通过调用GetCurrentBackBufferIndex()获得交换链中的当前后台缓冲区。 这就是为什么我们需要使用IDXGISwapChain3的原因,因为它提供了此方法。 IDXGISwapChain3是从IDXGISwapChain派生的,但是唯一的区别是派生的IDXGISwapChain3提供了更多的方法,其中之一是GetCurrentBackBufferIndex()方法,我们使用它来获取当前的后台缓冲区。

// -- Create the Swap Chain (double/tripple buffering) -- //

DXGI_MODE_DESC backBufferDesc = {}; // this is to describe our display mode
backBufferDesc.Width = Width; // buffer width
backBufferDesc.Height = Height; // buffer height
backBufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // format of the buffer (rgba 32 bits, 8 bits for each chanel)

// describe our multi-sampling. We are not multi-sampling, so we set the count to 1 (we need at least one sample of course)
DXGI_SAMPLE_DESC sampleDesc = {};
sampleDesc.Count = 1; // multisample count (no multisampling, so we just put 1, since we still need 1 sample)

// Describe and create the swap chain.
DXGI_SWAP_CHAIN_DESC swapChainDesc = {};
swapChainDesc.BufferCount = frameBufferCount; // number of buffers we have
swapChainDesc.BufferDesc = backBufferDesc; // our back buffer description
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // this says the pipeline will render to this swap chain
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; // dxgi will discard the buffer (data) after we call present
swapChainDesc.OutputWindow = hwnd; // handle to our window
swapChainDesc.SampleDesc = sampleDesc; // our multi-sampling description
swapChainDesc.Windowed = !FullScreen; // set to true, then if in fullscreen must call SetFullScreenState with true for full screen to get uncapped fps

IDXGISwapChain* tempSwapChain;

dxgiFactory->CreateSwapChain(
commandQueue, // the queue will be flushed once the swap chain is created
&swapChainDesc, // give it the swap chain description we created above
&tempSwapChain // store the created swap chain in a temp IDXGISwapChain interface
);

swapChain = static_cast<IDXGISwapChain3*>(tempSwapChain);

frameIndex = swapChain->GetCurrentBackBufferIndex();

创建渲染目标描述符堆

这是我们创建描述符堆以保存渲染目标的地方。
我们首先填充一个D3D12_DESCRIPTOR_HEAP_DESC结构来描述我们要创建的描述符堆:

typedef struct D3D12_DESCRIPTOR_HEAP_DESC {
D3D12_DESCRIPTOR_HEAP_TYPE Type;
UINT NumDescriptors;
D3D12_DESCRIPTOR_HEAP_FLAGS Flags;
UINT NodeMask;
} D3D12_DESCRIPTOR_HEAP_DESC;

  • Type -​D3D12_DESCRIPTOR_HEAP_TYPE​枚举。 描述符堆有三种类型,CBV / SRV / UAV,Sampler,RTV和DSV。 我们正在创建RTV堆,因此将其设置为D3D12_DESCRIPTOR_HEAP_TYPE_RTV
  • NumDescriptors -这是我们将存储在此描述符堆中的描述符的数量。 我们正在执行三重缓冲,因此我们需要3个后备缓冲区,这意味着我们有3个描述符
  • Flags -​D3D12_DESCRIPTOR_HEAP_FLAGS​枚举。 flags属性定义此堆是否为着色器可见。 着色器不访问RTV,因此我们不需要此堆即可使着色器可见。 为此,我们将此属性设置为D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE。 非着色器可见堆没有存储在GPU上,因此它们的大小不受着色器可见描述符堆的限制。 ​Shader Visible Descriptor Heaps​​, ​​Non-Shader Visible Descriptor Heaps​​。
  • NodeMask -这是一个位字段,用于确定此堆存储在哪个GPU上。 默认值为0。

着色器只能访问CBV / SRV / UAV或Sampler堆中的描述符。 命令列表只能填充这两种类型的描述符堆。要创建描述符堆,我们调用设备接口的CreateDescriptorHeap()方法:

HRESULT CreateDescriptorHeap(
[in] const D3D12_DESCRIPTOR_HEAP_DESC *pDescriptorHeapDesc,
REFIID riid,
[out] void **ppvHeap
);

  • pDescriptorHeapDesc-这是指向我们填写的D3D12_DESCRIPTOR_HEAP_DESC结构的指针,描述了我们要创建的堆。
  • riid-这是我们将创建的描述符堆接口的类型ID。
  • ppvHeap-这是指向我们RTV描述符堆接口的指针的空指针。

创建RTV描述符堆后,我们需要获取GPU上RTV描述符类型大小的大小。 没有保证一个GPU上的描述符类型与另一个GPU上的描述符大小相同,这就是为什么我们需要向设备询问描述符类型大小的大小的原因。 我们需要描述符类型的大小,以便可以迭代描述符堆中的描述符。 我们通过调用设备的GetDescriptorHandleIncrementSize()方法来做到这一点:

UINT GetDescriptorHandleIncrementSize(
[in] D3D12_DESCRIPTOR_HEAP_TYPE DescriptorHeapType
);

一旦我们有了RTV类型的描述符类型大小,就希望获得堆中描述符的句柄。描述符句柄有两种类型,GPU和CPU。我们的描述符堆着色器不可见,这意味着它存储在CPU端,这也意味着我们需要CPU描述符的句柄。描述符句柄基本上是一个指针,但是我们不能像C ++中的传统指针那样使用它们。这些指针供Direct3D驱动程序用来定位描述符。
我们可以通过调用描述符堆接口的GetCPUDescriptorHandleForHeapStart()方法来获取描述符堆中第一个描述符的句柄。我们在DirectX 12的第一篇教程中添加的d3dx12.h帮助程序文件提供了一些帮助程序结构,其中包括我们将用于RTV描述符句柄的CD3DX12_CPU_DESCRIPTOR_HANDLE结构。我们可以通过从GetDescriptorHandleIncrementSize()函数获得的描述符堆大小偏移当前的句柄,从而遍历堆中的RTV描述符。
当我们拥有对堆中第一个RTV描述符的描述符句柄时,将每个RTV描述符指向交换链中的后缓冲区。通过调用交换链接口的GetBuffer()方法,我们可以获得指向交换链中缓冲区的指针。使用该方法,我们可以将渲染目标资源(ID3D12Resource)设置为交换链缓冲区。

HRESULT GetBuffer(
UINT Buffer,
[in] REFIID riid,
[out] void **ppSurface
);

  • Buffer -这是我们要获取的缓冲区的索引
  • riid-这是接口的类型ID,我们将在其中存储指针
  • ppSurface-这是一个空指针,指向我们要指向缓冲区的接口的指针

现在,我们有3个指向交换链缓冲区的资源,我们可以使用设备接口CreateRenderTargetView()方法“创建” RTV。 此方法将创建一个指向资源的描述符,并将其存储在描述符句柄中。

void CreateRenderTargetView(
[in, optional] ID3D12Resource *pResource,
[in, optional] const D3D12_RENDER_TARGET_VIEW_DESC *pDesc,
[in] D3D12_CPU_DESCRIPTOR_HANDLE DestDescriptor
);

  • pResource-指向作为渲染目标缓冲区的资源的指针。
  • pDesc-指向D3D12_RENDER_TARGET_VIEW_DESC结构的指针。 如果我们使用子资源,则使用它。 我们可以在此处传递一个空指针。
  • DestDescriptor-这是描述符堆中cpu描述符的句柄,它将指向渲染目标资源。

为了到达下一个描述符,我们可以通过调用辅助结构CD3DX12_CPU_DESCRIPTOR_HANDLE的Offset()方法使当前描述符偏移描述符类型的大小。 第一个参数是我们要偏移的描述符数量(我们要转到下一个,所以我们使用1),第二个参数是描述符类型的大小。

// -- Create the Back Buffers (render target views) Descriptor Heap -- //

// describe an rtv descriptor heap and create
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
rtvHeapDesc.NumDescriptors = frameBufferCount; // number of descriptors for this heap.
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; // this heap is a render target view heap

// This heap will not be directly referenced by the shaders (not shader visible), as this will store the output from the pipeline
// otherwise we would set the heap's flag to D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
hr = device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&rtvDescriptorHeap));
if (FAILED(hr))
{
return false;
}

// get the size of a descriptor in this heap (this is a rtv heap, so only rtv descriptors should be stored in it.
// descriptor sizes may vary from device to device, which is why there is no set size and we must ask the
// device to give us the size. we will use this size to increment a descriptor handle offset
rtvDescriptorSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

// get a handle to the first descriptor in the descriptor heap. a handle is basically a pointer,
// but we cannot literally use it like a c++ pointer.
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvDescriptorHeap->GetCPUDescriptorHandleForHeapStart());

// Create a RTV for each buffer (double buffering is two buffers, tripple buffering is 3).
for (int i = 0; i < frameBufferCount; i++)
{
// first we get the n'th buffer in the swap chain and store it in the n'th
// position of our ID3D12Resource array
hr = swapChain->GetBuffer(i, IID_PPV_ARGS(&renderTargets[i]));
if (FAILED(hr))
{
return false;
}

// the we "create" a render target view which binds the swap chain buffer (ID3D12Resource[n]) to the rtv handle
device->CreateRenderTargetView(renderTargets[i], nullptr, rtvHandle);

// we increment the rtv handle by the rtv descriptor size we got above
rtvHandle.Offset(1, rtvDescriptorSize);
}

创建命令分配器

命令分配器用于在GPU上为要执行的命令分配内存,方法是在命令队列上调用execute并提供包含要执行的命令的命令列表。
我们正在使用三重缓冲,因此我们需要创建3个直接命令分配器。 我们需要三个,因为我们无法在GPU执行与其关联的命令列表时重置命令分配器。
要创建命令分配器,我们可以调用设备接口的CreateCommandAllocator()方法,提供命令分配器的类型,命令分配器的接口的类型ID,最后是指向命令分配器接口的指针,以便我们可以 用它。

HRESULT CreateCommandAllocator(
[in] D3D12_COMMAND_LIST_TYPE type,
REFIID riid,
[out] void **ppCommandAllocator
);

type -D3D12_COMMAND_LIST_TYPE类型枚举。 我们可以有一个直接的命令分配器,也可以有一个捆绑命令分配器。 可以将直接命令分配器与直接命令列表相关联,通过在命令队列上使用命令列表调用execute来在GPU上执行直接命令列表。 捆绑软件命令分配器存储捆绑软件的命令。 捆绑用于许多帧多次,因此我们不希望捆绑与直接命令列表位于同一命令分配器上,因为直接命令分配器通常在每一帧都被重置。 我们不想重置捆绑包,否则它们将无用。
riid-我们将使用的接口的类型ID
ppCommandAllocator-指向命令分配器接口的指针的指针

// -- Create the Command Allocators -- //

for (int i = 0; i < frameBufferCount; i++)
{
hr = device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator[i]));
if (FAILED(hr))
{
return false;
}
}

创建命令列表

您将需要与线程记录命令一样多的命令列表。 我们不是在制作多线程应用程序,因此我们只需要一个命令列表。
尽管在GPU执行与该分配器关联的命令列表时无法重置命令分配器,但是在我们使用该命令列表在命令队列上调用execute之后,可以立即重置命令列表。 这就是为什么我们只需要一个命令列表,但需要3个命令分配器的原因(对于三重缓冲单线程应用程序)。
要创建命令列表,我们可以调用设备接口的CreateCommandList()方法:

HRESULT CreateCommandList(
[in] UINT nodeMask,
[in] D3D12_COMMAND_LIST_TYPE type,
[in] ID3D12CommandAllocator *pCommandAllocator,
[in, optional] ID3D12PipelineState *pInitialState,
REFIID riid,
[out] void **ppCommandList
);

  • nodeMask-这是一个位字段,用于指定要使用的GPU。 默认GPU为0。
  • type-这是D3D12_COMMAND_LIST_TYPE,表示我们要创建的命令列表类型。
  • pCommandAllocator-创建命令列表时,必须指定命令分配器,该命令分配器会将命令存储在由命令列表构成的GPU上。
  • pInitialState-这是命令列表的默认或启动管道状态对象。 它是指向ID3D12PipelineState接口的指针。 指定NULL将使管道状态保持其默认值(如果在屏幕上绘制任何内容,则需要至少指定一个顶点着色器,但是在本教程中,我们仅清除渲染目标,不需要管道状态对象 ,这将在下一篇教程中介绍)
  • riid-我们正在创建的命令列表界面的类型ID
  • ppCommandList-指向命令列表接口的指针

使用D3D12_COMMAND_LIST_TYPE枚举指定了4种不同类型的命令列表:

typedef enum D3D12_COMMAND_LIST_TYPE { 
D3D12_COMMAND_LIST_TYPE_DIRECT = 0,
D3D12_COMMAND_LIST_TYPE_BUNDLE = 1,
D3D12_COMMAND_LIST_TYPE_COMPUTE = 2,
D3D12_COMMAND_LIST_TYPE_COPY = 3
} D3D12_COMMAND_LIST_TYPE;

  • D3D12_COMMAND_LIST_TYPE_DIRECT-直接命令列表是一个命令列表,可以在其中执行GPU的命令。 这是我们要创建的命令列表。
  • D3D12_COMMAND_LIST_TYPE_BUNDLE-捆绑包是一个命令列表,其中包含一组经常使用的命令。 这种类型的命令列表不能由命令队列直接执行,而是直接命令列表必须执行bundles。 除当前设置的PSO和原始拓扑外,捆绑包会继承所有管道状态。
  • D3D12_COMMAND_LIST_TYPE_COMPUTE-计算命令列表用于计算着色器。
  • D3D12_COMMAND_LIST_TYPE_COPY-复制命令列表

我们需要创建一个直接命令列表,以便我们可以执行我们的clear render target命令。 为此,我们为第二个参数指定D3D12_COMMAND_LIST_TYPE_DIRECT。
  因为我们只需要一个命令列表,并且在指定命令分配器的每一帧都将重置该命令列表,所以我们只需使用第一个命令分配器创建此命令列表即可。
  创建命令列表后,将以“recording”状态创建命令列表。 我们还不想将其记录到命令列表中,因此我们在创建命令列表后将其Close()。

// create the command list with the first allocator
hr = device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, commandAllocator[0], NULL, IID_PPV_ARGS(&commandList));
if (FAILED(hr))
{
return false;
}
// command lists are created in the recording state. our main loop will set it up for recording again so close it now
commandList->Close();

创建围栏和围栏事件

初始化direct3d函数的最后一部分是创建围栏和围栏事件。 我们只使用一个线程,所以我们只需要一个围栏事件,但是由于我们是三重缓冲,所以我们有三个围栏,每个帧缓冲区一个。 我们还有3个当前的fence值,由fenceValue数组表示,以便我们可以跟踪实际的fence值。
我们在这里要做的第一件事是通过调用设备接口的CreateFence()函数(对于每个围栏)来创建3个围栏:

HRESULT CreateFence(
UINT64 InitialValue,
D3D12_FENCE_FLAGS Flags,
REFIID riid,
[out] void **ppFence
);

  • InitialValue-这是我们希望围栏开始的初始值
  • Flags-D3D12_FENCE_FLAG_NONE类型的枚举。 该标志用于共享围栏。 我们没有与其他GPU共享该围栏,因此我们将其设置为D3D12_FENCE_FLAG_NONE
  • riid-我们想要的fence接口的类型ID
  • ppFence-指向围栏接口的指针

创建所有三个围栏并初始化围栏值数组后,我们将使用Windows CreateEvent()函数创建围栅事件:

HANDLE WINAPI CreateEvent(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
_In_ BOOL bManualReset,
_In_ BOOL bInitialState,
_In_opt_ LPCTSTR lpName
);

  • lpEventAttributes-这是指向SECURITY_ATTRIBUTES结构的指针。 将其设置为空指针将使用默认的安全性结构。
  • bManualReset-如果将其设置为true,则在等待GPU设置事件后,必须将事件自动重置为NOT TRIGGERED(通过使用ResetEvent()函数)。 将其设置为false(我们这样做)将导致我们在等待fence事件之后将此事件自动重置为不触发。
  • bInitialState-将其设置为true将导致该事件的初始状态被信号通知。 我们还不希望收到信号,所以我们说错了。
  • lpName-将其设置为空指针将导致创建事件时不使用名称。

// -- Create a Fence & Fence Event -- //

// create the fences
for (int i = 0; i < frameBufferCount; i++)
{
hr = device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence[i]));
if (FAILED(hr))
{
return false;
}
fenceValue[i] = 0; // set the initial fence value to 0
}

// create a handle to a fence event
fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (fenceEvent == nullptr)
{
return false;
}

return true;
}

Update()函数

update函数现在不执行任何操作,但是稍后我们将向该函数添加逻辑,这些逻辑可以在gpu执行命令队列时运行。 如果我们希望每一帧更改目标,我们可以在此处更改渲染目标的透明颜色。

void Update()
{
// update app logic, such as moving the camera or figuring out what objects are in view
}

UpdatePipeline()函数

此功能是我们将命令添加到命令列表的地方,其中包括更改渲染目标的状态,设置根签名和清除渲染目标。 稍后,我们将设置顶点缓冲区并在此函数中调用draw。

void UpdatePipeline()
{
HRESULT hr;

重置命令分配器和命令列表

如前所述,在GPU执行与其关联的命令列表中的命令时,无法重置命令分配器。这就是为什么我们有围栏和围栏事件。在重置此帧命令分配器之前,我们要做的第一件事是确保GPU已完成执行与此命令分配器关联的命令列表。
您将在render函数中看到,在命令队列上调用execute之后,在命令队列上调用Signal()。基本上,这将在我们刚刚执行的命令列表之后插入一个命令,该命令将增加此帧的围栏值。我们调用WaitForPreviousFrame(),它将检查围栅的值并查看其是否已增加。如果有的话,我们知道命令已列出该帧已执行,因此可以安全地重置命令分配器。
重置此帧命令分配器后,我们想重置命令列表。与命令分配器不同,一旦在命令队列上调用execute,我们就可以立即重置该命令列表并重新使用它。因此,我们在此处重置命令列表,并为其赋予该框架命令分配器和一个空PSO(我们尚未绘制任何内容,因此不需要设置任何种类的管道状态)。
重置命令列表将使其处于记录状态。

// We have to wait for the gpu to finish with the command allocator before we reset it
WaitForPreviousFrame();

// we can only reset an allocator once the gpu is done with it
// resetting an allocator frees the memory that the command list was stored in
hr = commandAllocator[frameIndex]->Reset();
if (FAILED(hr))
{
Running = false;
}

// reset the command list. by resetting the command list we are putting it into
// a recording state so we can start recording commands into the command allocator.
// the command allocator that we reference here may have multiple command lists
// associated with it, but only one can be recording at any time. Make sure
// that any other command lists associated to this command allocator are in
// the closed state (not recording).
// Here you will pass an initial pipeline state object as the second parameter,
// but in this tutorial we are only clearing the rtv, and do not actually need
// anything but an initial default pipeline, which is what we get by setting
// the second parameter to NULL
hr = commandList->Reset(commandAllocator[frameIndex], NULL);
if (FAILED(hr))
{
Running = false;
}

使用命令列表记录命令

现在,我们进入Direct3D 12有趣的部分,记录命令。 对于本教程,我们记录的唯一命令是更改先前和当前渲染目标资源的状态,并将渲染目标清除为某种颜色。
渲染目标资源必须处于渲染目标状态,才能输出Output Merger。 我们可以通过使用资源屏障来更改资源的状态。 这是通过命令列表界面的ResourceBarrier()命令完成的。 我们需要一个过渡屏障,因为我们正在将呈现目标的状态从当前状态(交换链呈现该状态所需的状态)转换为呈现状态(否则您将获得调试错误),从而将其转换为所需的呈现目标状态 在输出合并中继续输出。

void ResourceBarrier(
[in] UINT NumBarriers,
[in] const D3D12_RESOURCE_BARRIER *pBarriers
);

  • NumBarriers-这是我们正在提交的障碍说明的数量(我们仅在此处提交一个)
  • pBarriers-这是指向D3D12_RESOURCE_BARRIER(资源屏障描述)数组的指针。

这是我们再次使用d3dx12.h帮助程序库的地方。我们使用CD3DX12_RESOURCE_BARRIER :: Transition创建过渡资源屏障描述。我们传入渲染目标资源以及当前状态和我们要转换到的状态。在这里,我们将当前渲染目标从当前状态转换为渲染目标状态,以便可以将其清除为一种颜色。在完成此渲染目标的命令后,我们希望再次将其状态转换,但是这次从渲染目标状态转换为当前状态,以便交换链可以呈现它。
我们想要清除渲染目标,所以我们要做的是获取渲染目标的句柄。我们使用CD3DX12_CPU_DESCRIPTOR_HANDLE结构,并为它提供RTV描述符堆中的第一个描述符,当前帧的索引以及每个RTV描述符的大小(基本上,我们获得了一个指向描述符堆开头的指针,然后对该指针进行递增指针frameIndex乘以rtvDescriptorSize)
一旦有了当前渲染目标的描述符句柄,就需要将渲染目标设置为Output Merger的输出。我们使用命令OMSetRenderTargets()进行此操作:

void OMSetRenderTargets(
[in] UINT NumRenderTargetDescriptors,
[in, optional] const D3D12_CPU_DESCRIPTOR_HANDLE *pRenderTargetDescriptors,
[in] BOOL RTsSingleHandleToDescriptorRange,
[in, optional] const D3D12_CPU_DESCRIPTOR_HANDLE *pDepthStencilDescriptor
);

  • NumRenderTargetDescriptors-渲染目标描述符句柄的数量
  • pRenderTargetDescriptors-指向渲染目标描述符句柄数组的指针
  • RTsSingleHandleToDescriptorRange-如果为true,则pRenderTargetDescriptors是指向描述符堆中相邻描述符块开头的指针。获取下一个描述符时,D3D将当前描述符句柄偏移描述符类型的大小。将其设置为false时,pRenderTargetDescriptors是指向渲染目标描述符句柄数组的指针。这比将其设置为true时效率低,因为要获取下一个描述符,D3D需要取消引用数组中的句柄才能到达渲染目标。因为我们只有一个渲染目标,所以我们将其设置为false,因为我们将对句柄的引用传递给我们正在使用的唯一描述符句柄。
  • pDepthStencilDescriptor-指向深度/模板描述符句柄的指针。在本教程中,我们将其设置为null,因为我们还没有深度/模板缓冲区

最后,为了清除渲染目标,我们使用ClearRenderTargetView()命令:

void ClearRenderTargetView(
[in] D3D12_CPU_DESCRIPTOR_HANDLE RenderTargetView,
[in] const FLOAT ColorRGBA[4],
[in] UINT NumRects,
[in] const D3D12_RECT *pRects
);

  • RenderTargetView-我们要清除的渲染目标的描述符句柄
  • ColorRGBA [4]-4个浮点值的数组,分别代表红色,绿色,蓝色和Alpha
  • NumRects-要清除的渲染目标上的矩形数。 将此设置为0以清除整个渲染目标
  • pRects-这是指向D3D12_RECT结构数组的指针,该数组表示我们要清除的渲染目标上的矩形。 当您不想清除整个渲染目标,而只希望清除一个或多个矩形时,这非常有用。 如果将NumRects设置为0,则在此处传递空指针

记录完命令后,我们需要关闭命令列表。 如果在尝试使用命令队列执行命令之前未关闭命令列表,则应用程序将中断。
  关于关闭命令列表的另一条注释。 在direct3d 12中,如果在记录命令列表期间进行了非法操作,则程序将继续运行,直到您调用close,否则close将失败。 您必须启用调试层才能查看调用close时到底发生了什么。

// here we start recording commands into the commandList (which all the commands will be stored in the commandAllocator)

// transition the "frameIndex" render target from the present state to the render target state so the command list draws to it starting from here
commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[frameIndex], D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));

// here we again get the handle to our current render target view so we can set it as the render target in the output merger stage of the pipeline
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvDescriptorHeap->GetCPUDescriptorHandleForHeapStart(), frameIndex, rtvDescriptorSize);

// set the render target for the output merger stage (the output of the pipeline)
commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);

// Clear the render target by using the ClearRenderTargetView command
const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);

// transition the "frameIndex" render target from the render target state to the present state. If the debug layer is enabled, you will receive a
// warning if present is called on the render target when it's not in the present state
commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[frameIndex], D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));

hr = commandList->Close();
if (FAILED(hr))
{
Running = false;
}
}

Render()函数

我们在这里要做的第一件事是通过调用UpdatePipeline()函数来更新管道(记录命令列表)。
记录完命令列表后,我们将创建一个命令列表数组。 我们只有一个命令列表,但是如果我们有多个线程,则每个线程都有一个命令列表。 在这里,我们将按照要执行命令的顺序在数组中组织命令列表。
我们可以通过在commandQueue上调用ExecuteCommandLists()来执行命令列表,并提供要执行的命令列表的数量,以及指向命令列表数组的指针。

void ExecuteCommandLists(
[in] UINT NumCommandLists,
[in] ID3D12CommandList *const *ppCommandLists
);

  • NumCommandLists-要执行的命令列表的数量
  • ppCommandLists-要执行的命令列表数组。 命令列表将按照它们放入数组的顺序执行。

指示GPU执行命令列表后,我们想在命令队列中插入命令以设置该框架的防护范围。 Signal()方法基本上会插入另一个命令,该命令将围栏设置为特定值并发出围栏事件信号。 我们这样做是为了当我们回到该帧缓冲区时,可以检查GPU是否已完成执行命令列表。 我们将知道它什么时候完成,因为信号命令将已经执行,并且围栏将被设置为我们告诉它设置的值。
最后,我们通过调用swapchain的Present()方法来呈现下一个后台缓冲区。

void Render()
{
HRESULT hr;

UpdatePipeline(); // update the pipeline by sending commands to the commandqueue

// create an array of command lists (only one command list here)
ID3D12CommandList* ppCommandLists[] = { commandList };

// execute the array of command lists
commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);

// this command goes in at the end of our command queue. we will know when our command queue
// has finished because the fence value will be set to "fenceValue" from the GPU since the command
// queue is being executed on the GPU
hr = commandQueue->Signal(fence[frameIndex], fenceValue[frameIndex]);
if (FAILED(hr))
{
Running = false;
}

// present the current backbuffer
hr = swapChain->Present(0, 0);
if (FAILED(hr))
{
Running = false;
}
}

Cleanup()函数

此函数仅释放我们创建的接口对象。 在发布任何内容之前,我们要确保GPU在开始发布之前完成所有操作。

void Cleanup()
{
// wait for the gpu to finish all frames
for (int i = 0; i < frameBufferCount; ++i)
{
frameIndex = i;
WaitForPreviousFrame();
}

// get swapchain out of full screen before exiting
BOOL fs = false;
if (swapChain->GetFullscreenState(&fs, NULL))
swapChain->SetFullscreenState(false, NULL);

SAFE_RELEASE(device);
SAFE_RELEASE(swapChain);
SAFE_RELEASE(commandQueue);
SAFE_RELEASE(rtvDescriptorHeap);
SAFE_RELEASE(commandList);

for (int i = 0; i < frameBufferCount; ++i)
{
SAFE_RELEASE(renderTargets[i]);
SAFE_RELEASE(commandAllocator[i]);
SAFE_RELEASE(fence[i]);
};
}

WaitForPreviousFrame()函数

最后,我们等待上一帧功能。 此功能是我们需要fence 和fence event的地方。 我们要做的第一件事是检查当前框架围栏的当前值。 如果当前值小于我们想要的值,则我们知道GPU仍在为此帧执行命令,并且必须输入if块,在此处设置fence事件,一旦fence值等于,就会触发该事件 我们希望它等于什么。 我们使用fence接口的SetEventOnCompletion()方法执行此操作。

HRESULT SetEventOnCompletion(
UINT64 Value,
HANDLE hEvent
);

  • Value-这是我们希望围栏等于的值
  • hEvent-这是我们希望在围栏等于Value时触发的事件

设置事件后,我们等待它被触发。 我们使用Windows WaitForSingleObject()函数执行此操作。

DWORD WINAPI WaitForSingleObject(
_In_ HANDLE hHandle,
_In_ DWORD dwMilliseconds
);

  • hHandle-这是我们要等待被触发的围栏事件。 (如果围栏事件在此函数调用与我们设置围栏事件之间的时间间隔很小的时间内触发,则此函数将立即返回
  • dwMilliseconds-这是我们要等待围栏事件触发的毫秒数。 我们可以使用INFINITE宏,这意味着该方法将永远阻塞,或者直到触发围栏事件为止。

一旦我们看到GPU已完成执行此帧命令列表,就可以增加该帧的围栅值,在交换链中设置当前的后缓冲区,然后继续。

void WaitForPreviousFrame()
{
HRESULT hr;

// swap the current rtv buffer index so we draw on the correct buffer
frameIndex = swapChain->GetCurrentBackBufferIndex();

// if the current fence value is still less than "fenceValue", then we know the GPU has not finished executing
// the command queue since it has not reached the "commandQueue->Signal(fence, fenceValue)" command
if (fence[frameIndex]->GetCompletedValue() < fenceValue[frameIndex])
{
// we have the fence create an event which is signaled once the fence's current value is "fenceValue"
hr = fence[frameIndex]->SetEventOnCompletion(fenceValue[frameIndex], fenceEvent);
if (FAILED(hr))
{
Running = false;
}

// We will wait until the fence has triggered the event that it's current value has reached "fenceValue". once it's value
// has reached "fenceValue", we know the command queue has finished executing
WaitForSingleObject(fenceEvent, INFINITE);
}

// increment fenceValue for next frame
fenceValue[frameIndex]++;
}

这是本教程的最终代码:

stdafx.h

#pragma once

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers.
#endif

#include <windows.h>
#include <d3d12.h>
#include <dxgi1_4.h>
#include <D3Dcompiler.h>
#include <DirectXMath.h>
#include "d3dx12.h"
#include <string>

// this will only call release if an object exists (prevents exceptions calling release on non existant objects)
#define SAFE_RELEASE(p) { if ( (p) ) { (p)->Release(); (p) = 0; } }

// Handle to the window
HWND hwnd = NULL;

// name of the window (not the title)
LPCTSTR WindowName = L"BzTutsApp";

// title of the window
LPCTSTR WindowTitle = L"Bz Window";

// width and height of the window
int Width = 800;
int Height = 600;

// is window full screen?
bool FullScreen = false;

// we will exit the program when this becomes false
bool Running = true;

// create a window
bool InitializeWindow(HINSTANCE hInstance,
int ShowWnd,
bool fullscreen);

// main application loop
void mainloop();

// callback function for windows messages
LRESULT CALLBACK WndProc(HWND hWnd,
UINT msg,
WPARAM wParam,
LPARAM lParam);

// direct3d stuff
const int frameBufferCount = 3; // number of buffers we want, 2 for double buffering, 3 for tripple buffering

ID3D12Device* device; // direct3d device

IDXGISwapChain3* swapChain; // swapchain used to switch between render targets

ID3D12CommandQueue* commandQueue; // container for command lists

ID3D12DescriptorHeap* rtvDescriptorHeap; // a descriptor heap to hold resources like the render targets

ID3D12Resource* renderTargets[frameBufferCount]; // number of render targets equal to buffer count

ID3D12CommandAllocator* commandAllocator[frameBufferCount]; // we want enough allocators for each buffer * number of threads (we only have one thread)

ID3D12GraphicsCommandList* commandList; // a command list we can record commands into, then execute them to render the frame

ID3D12Fence* fence[frameBufferCount]; // an object that is locked while our command list is being executed by the gpu. We need as many
//as we have allocators (more if we want to know when the gpu is finished with an asset)

HANDLE fenceEvent; // a handle to an event when our fence is unlocked by the gpu

UINT64 fenceValue[frameBufferCount]; // this value is incremented each frame. each fence will have its own value

int frameIndex; // current rtv we are on

int rtvDescriptorSize; // size of the rtv descriptor on the device (all front and back buffers will be the same size)

// function declarations
bool InitD3D(); // initializes direct3d 12

void Update(); // update the game logic

void UpdatePipeline(); // update the direct3d pipeline (update command lists)

void Render(); // execute the command list

void Cleanup(); // release com ojects and clean up memory

void WaitForPreviousFrame(); // wait until gpu is finished with command list

main.cpp

#include "stdafx.h"

using namespace DirectX; // we will be using the directxmath library

int WINAPI WinMain(HINSTANCE hInstance, //Main windows function
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd)
{
// create the window
if (!InitializeWindow(hInstance, nShowCmd, FullScreen))
{
MessageBox(0, L"Window Initialization - Failed",
L"Error", MB_OK);
return 1;
}

// initialize direct3d
if (!InitD3D())
{
MessageBox(0, L"Failed to initialize direct3d 12",
L"Error", MB_OK);
Cleanup();
return 1;
}

// start the main loop
mainloop();

// we want to wait for the gpu to finish executing the command list before we start releasing everything
WaitForPreviousFrame();

// close the fence event
CloseHandle(fenceEvent);

// clean up everything
Cleanup();

return 0;
}

// create and show the window
bool InitializeWindow(HINSTANCE hInstance,
int ShowWnd,
bool fullscreen)
{
if (fullscreen)
{
HMONITOR hmon = MonitorFromWindow(hwnd,
MONITOR_DEFAULTTONEAREST);
MONITORINFO mi = { sizeof(mi) };
GetMonitorInfo(hmon, &mi);

Width = mi.rcMonitor.right - mi.rcMonitor.left;
Height = mi.rcMonitor.bottom - mi.rcMonitor.top;
}

WNDCLASSEX wc;

wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = NULL;
wc.cbWndExtra = NULL;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 2);
wc.lpszMenuName = NULL;
wc.lpszClassName = WindowName;
wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);

if (!RegisterClassEx(&wc))
{
MessageBox(NULL, L"Error registering class",
L"Error", MB_OK | MB_ICONERROR);
return false;
}

hwnd = CreateWindowEx(NULL,
WindowName,
WindowTitle,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
Width, Height,
NULL,
NULL,
hInstance,
NULL);

if (!hwnd)
{
MessageBox(NULL, L"Error creating window",
L"Error", MB_OK | MB_ICONERROR);
return false;
}

if (fullscreen)
{
SetWindowLong(hwnd, GWL_STYLE, 0);
}

ShowWindow(hwnd, ShowWnd);
UpdateWindow(hwnd);

return true;
}

void mainloop() {
MSG msg;
ZeroMemory(&msg, sizeof(MSG));

while (Running)
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT)
break;

TranslateMessage(&msg);
DispatchMessage(&msg);
}
else {
// run game code
Update(); // update the game logic
Render(); // execute the command queue (rendering the scene is the result of the gpu executing the command lists)
}
}
}

LRESULT CALLBACK WndProc(HWND hwnd,
UINT msg,
WPARAM wParam,
LPARAM lParam)
{
switch (msg)
{
case WM_KEYDOWN:
if (wParam == VK_ESCAPE) {
if (MessageBox(0, L"Are you sure you want to exit?",
L"Really?", MB_YESNO | MB_ICONQUESTION) == IDYES)
{
Running = false;
DestroyWindow(hwnd);
}
}
return 0;

case WM_DESTROY: // x button on top right corner of window was pressed
Running = false;
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd,
msg,
wParam,
lParam);
}

bool InitD3D()
{
HRESULT hr;

// -- Create the Device -- //

IDXGIFactory4* dxgiFactory;
hr = CreateDXGIFactory1(IID_PPV_ARGS(&dxgiFactory));
if (FAILED(hr))
{
return false;
}

IDXGIAdapter1* adapter; // adapters are the graphics card (this includes the embedded graphics on the motherboard)

int adapterIndex = 0; // we'll start looking for directx 12 compatible graphics devices starting at index 0

bool adapterFound = false; // set this to true when a good one was found

// find first hardware gpu that supports d3d 12
while (dxgiFactory->EnumAdapters1(adapterIndex, &adapter) != DXGI_ERROR_NOT_FOUND)
{
DXGI_ADAPTER_DESC1 desc;
adapter->GetDesc1(&desc);

if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
{
// we dont want a software device
adapterIndex++;
continue;
}

// we want a device that is compatible with direct3d 12 (feature level 11 or higher)
hr = D3D12CreateDevice(adapter, D3D_FEATURE_LEVEL_11_0, _uuidof(ID3D12Device), nullptr);
if (SUCCEEDED(hr))
{
adapterFound = true;
break;
}

adapterIndex++;
}

if (!adapterFound)
{
return false;
}

// Create the device
hr = D3D12CreateDevice(
adapter,
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&device)
);
if (FAILED(hr))
{
return false;
}

// -- Create a direct command queue -- //

D3D12_COMMAND_QUEUE_DESC cqDesc = {};
cqDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
cqDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; // direct means the gpu can directly execute this command queue

hr = device->CreateCommandQueue(&cqDesc, IID_PPV_ARGS(&commandQueue)); // create the command queue
if (FAILED(hr))
{
return false;
}

// -- Create the Swap Chain (double/tripple buffering) -- //

DXGI_MODE_DESC backBufferDesc = {}; // this is to describe our display mode
backBufferDesc.Width = Width; // buffer width
backBufferDesc.Height = Height; // buffer height
backBufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // format of the buffer (rgba 32 bits, 8 bits for each chanel)

// describe our multi-sampling. We are not multi-sampling, so we set the count to 1 (we need at least one sample of course)
DXGI_SAMPLE_DESC sampleDesc = {};
sampleDesc.Count = 1; // multisample count (no multisampling, so we just put 1, since we still need 1 sample)

// Describe and create the swap chain.
DXGI_SWAP_CHAIN_DESC swapChainDesc = {};
swapChainDesc.BufferCount = frameBufferCount; // number of buffers we have
swapChainDesc.BufferDesc = backBufferDesc; // our back buffer description
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // this says the pipeline will render to this swap chain
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; // dxgi will discard the buffer (data) after we call present
swapChainDesc.OutputWindow = hwnd; // handle to our window
swapChainDesc.SampleDesc = sampleDesc; // our multi-sampling description
swapChainDesc.Windowed = !FullScreen; // set to true, then if in fullscreen must call SetFullScreenState with true for full screen to get uncapped fps

IDXGISwapChain* tempSwapChain;

dxgiFactory->CreateSwapChain(
commandQueue, // the queue will be flushed once the swap chain is created
&swapChainDesc, // give it the swap chain description we created above
&tempSwapChain // store the created swap chain in a temp IDXGISwapChain interface
);

swapChain = static_cast<IDXGISwapChain3*>(tempSwapChain);

frameIndex = swapChain->GetCurrentBackBufferIndex();

// -- Create the Back Buffers (render target views) Descriptor Heap -- //

// describe an rtv descriptor heap and create
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
rtvHeapDesc.NumDescriptors = frameBufferCount; // number of descriptors for this heap.
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; // this heap is a render target view heap

// This heap will not be directly referenced by the shaders (not shader visible), as this will store the output from the pipeline
// otherwise we would set the heap's flag to D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
hr = device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&rtvDescriptorHeap));
if (FAILED(hr))
{
return false;
}

// get the size of a descriptor in this heap (this is a rtv heap, so only rtv descriptors should be stored in it.
// descriptor sizes may vary from device to device, which is why there is no set size and we must ask the
// device to give us the size. we will use this size to increment a descriptor handle offset
rtvDescriptorSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

// get a handle to the first descriptor in the descriptor heap. a handle is basically a pointer,
// but we cannot literally use it like a c++ pointer.
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvDescriptorHeap->GetCPUDescriptorHandleForHeapStart());

// Create a RTV for each buffer (double buffering is two buffers, tripple buffering is 3).
for (int i = 0; i < frameBufferCount; i++)
{
// first we get the n'th buffer in the swap chain and store it in the n'th
// position of our ID3D12Resource array
hr = swapChain->GetBuffer(i, IID_PPV_ARGS(&renderTargets[i]));
if (FAILED(hr))
{
return false;
}

// the we "create" a render target view which binds the swap chain buffer (ID3D12Resource[n]) to the rtv handle
device->CreateRenderTargetView(renderTargets[i], nullptr, rtvHandle);

// we increment the rtv handle by the rtv descriptor size we got above
rtvHandle.Offset(1, rtvDescriptorSize);
}

// -- Create the Command Allocators -- //

for (int i = 0; i < frameBufferCount; i++)
{
hr = device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator[i]));
if (FAILED(hr))
{
return false;
}
}

// -- Create a Command List -- //

// create the command list with the first allocator
hr = device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, commandAllocator[0], NULL, IID_PPV_ARGS(&commandList));
if (FAILED(hr))
{
return false;
}

// command lists are created in the recording state. our main loop will set it up for recording again so close it now
commandList->Close();

// -- Create a Fence & Fence Event -- //

// create the fences
for (int i = 0; i < frameBufferCount; i++)
{
hr = device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence[i]));
if (FAILED(hr))
{
return false;
}
fenceValue[i] = 0; // set the initial fence value to 0
}

// create a handle to a fence event
fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (fenceEvent == nullptr)
{
return false;
}

return true;
}

void Update()
{
// update app logic, such as moving the camera or figuring out what objects are in view
}

void UpdatePipeline()
{
HRESULT hr;

// We have to wait for the gpu to finish with the command allocator before we reset it
WaitForPreviousFrame();

// we can only reset an allocator once the gpu is done with it
// resetting an allocator frees the memory that the command list was stored in
hr = commandAllocator[frameIndex]->Reset();
if (FAILED(hr))
{
Running = false;
}

// reset the command list. by resetting the command list we are putting it into
// a recording state so we can start recording commands into the command allocator.
// the command allocator that we reference here may have multiple command lists
// associated with it, but only one can be recording at any time. Make sure
// that any other command lists associated to this command allocator are in
// the closed state (not recording).
// Here you will pass an initial pipeline state object as the second parameter,
// but in this tutorial we are only clearing the rtv, and do not actually need
// anything but an initial default pipeline, which is what we get by setting
// the second parameter to NULL
hr = commandList->Reset(commandAllocator[frameIndex], NULL);
if (FAILED(hr))
{
Running = false;
}

// here we start recording commands into the commandList (which all the commands will be stored in the commandAllocator)

// transition the "frameIndex" render target from the present state to the render target state so the command list draws to it starting from here
commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[frameIndex], D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));

// here we again get the handle to our current render target view so we can set it as the render target in the output merger stage of the pipeline
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvDescriptorHeap->GetCPUDescriptorHandleForHeapStart(), frameIndex, rtvDescriptorSize);

// set the render target for the output merger stage (the output of the pipeline)
commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);

// Clear the render target by using the ClearRenderTargetView command
const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);

// transition the "frameIndex" render target from the render target state to the present state. If the debug layer is enabled, you will receive a
// warning if present is called on the render target when it's not in the present state
commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[frameIndex], D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));

hr = commandList->Close();
if (FAILED(hr))
{
Running = false;
}
}

void Render()
{
HRESULT hr;

UpdatePipeline(); // update the pipeline by sending commands to the commandqueue

// create an array of command lists (only one command list here)
ID3D12CommandList* ppCommandLists[] = { commandList };

// execute the array of command lists
commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);

// this command goes in at the end of our command queue. we will know when our command queue
// has finished because the fence value will be set to "fenceValue" from the GPU since the command
// queue is being executed on the GPU
hr = commandQueue->Signal(fence[frameIndex], fenceValue[frameIndex]);
if (FAILED(hr))
{
Running = false;
}

// present the current backbuffer
hr = swapChain->Present(0, 0);
if (FAILED(hr))
{
Running = false;
}
}

void Cleanup()
{
// wait for the gpu to finish all frames
for (int i = 0; i < frameBufferCount; ++i)
{
frameIndex = i;
WaitForPreviousFrame();
}

// get swapchain out of full screen before exiting
BOOL fs = false;
if (swapChain->GetFullscreenState(&fs, NULL))
swapChain->SetFullscreenState(false, NULL);

SAFE_RELEASE(device);
SAFE_RELEASE(swapChain);
SAFE_RELEASE(commandQueue);
SAFE_RELEASE(rtvDescriptorHeap);
SAFE_RELEASE(commandList);

for (int i = 0; i < frameBufferCount; ++i)
{
SAFE_RELEASE(renderTargets[i]);
SAFE_RELEASE(commandAllocator[i]);
SAFE_RELEASE(fence[i]);
};
}

void WaitForPreviousFrame()
{
HRESULT hr;

// swap the current rtv buffer index so we draw on the correct buffer
frameIndex = swapChain->GetCurrentBackBufferIndex();

// if the current fence value is still less than "fenceValue", then we know the GPU has not finished executing
// the command queue since it has not reached the "commandQueue->Signal(fence, fenceValue)" command
if (fence[frameIndex]->GetCompletedValue() < fenceValue[frameIndex])
{
// we have the fence create an event which is signaled once the fence's current value is "fenceValue"
hr = fence[frameIndex]->SetEventOnCompletion(fenceValue[frameIndex], fenceEvent);
if (FAILED(hr))
{
Running = false;
}

// We will wait until the fence has triggered the event that it's current value has reached "fenceValue". once it's value
// has reached "fenceValue", we know the command queue has finished executing
WaitForSingleObject(fenceEvent, INFINITE);
}

// increment fenceValue for next frame
fenceValue[frameIndex]++;
}

参考链接:


  1. ​https://docs.microsoft.com/en-us/windows/win32/direct3d12/directx-12-programming-guide​​​
  2. ​http://www.d3dcoder.net/​​​
  3. ​https://www.braynzarsoft.net/viewtutorial/q16390-04-directx-12-braynzar-soft-tutorials​​​
  4. ​https://developer.nvidia.com/dx12-dos-and-donts​​​
  5. ​https://www.3dgep.com/learning-directx-12-1/​​​
  6. ​https://gpuopen.com/learn/lets-learn-directx12/​​​
  7. ​https://alain.xyz/blog/raw-directx12​​​
  8. ​https://www.rastertek.com/tutdx12.html​​​
  9. ​https://digitalerr0r.net/2015/08/19/quickstart-directx-12-programming/​​​
  10. ​https://walbourn.github.io/getting-started-with-direct3d-12/​​​
  11. ​https://docs.aws.amazon.com/lumberyard/latest/userguide/graphics-rendering-directx.html​​​
  12. ​http://diligentgraphics.com/diligent-engine/samples/​​​
  13. ​https://www.programmersought.com/article/2904113865/​​​
  14. ​https://www.tutorialspoint.com/directx/directx_first_hlsl.htm​​​
  15. ​http://rbwhitaker.wikidot.com/hlsl-tutorials​​​
  16. ​https://digitalerr0r.net/2015/08/19/quickstart-directx-12-programming/​​​
  17. ​https://www.ronja-tutorials.com/post/002-hlsl/​​​

DirectX12初始化综合篇_d3_09DirectX12初始化综合篇_描述符_10DirectX12初始化综合篇_3d_11