【学习DirectX12】渲染——第一节

Introduction

在DirectX 12里面,用于描述资源的唯一接口是ID3D12Resource。ID3D12Heap接口允许不同的内存映射技术,用于优化GPU内存。为了在DX12里更好地状态管理,使用pipeline state object (PSO)来描述不同的渲染和计算管线。也就是PSO结合了渲染管线中的input assembly (IA), vertex shader (VS), hull shader (HS), domain shader (DS), geometry shader (GS), stream output (SO), rasterizer stage (RS), pixel shader (PS), and output merger (OM),但是另外一些特性比如viewport和scissor rectangle需要额外的API。另一个特性是Root signature用于描述向可渲染编程管线传递参数。比如稍后我们将用Constant buffer作为root signature来旋转场景中的cube。

在本章中,我们将学习以下东西

 

  • Uploading Buffer Resources to the GPU
  • Heaps
  • Pipeline State Objects
  • Root signatures

Heaps and Resources

在之前的课程中,我们使用 IDXGIFactory2::CreateSwapChainForHwnd 方法来创建交换链,并且一些纹理资源被自动创建了。但在这节课,我们将手动创建一些缓存资源。使用ID3D12Heap来安排GPU资源有几种方法。

  • Committed Resources
  • Placed Resources
  • Reserved Resources

Committed Resources

使用ID3D12Device::CreateCommittedResource来创建committed resource。这个方法会同时创建资源和implicit heap,后者非常大足以包含整个资源。资源同时也会映射到heap上。Committed resource也是操作最方便的,因为开发者无需担心在heap的哪里放置这个资源。

Committed resource来创建大型资源比如纹理或大小不变的资源时效率很高,同时也可用于在uploda heap理创建大型资源,比如动态顶点或索引缓存,这在渲染UI或上传每次draw call都要改变的constant buffer时非常有用。

Committed Resources

Placed Resource

placed resource位于一个heap中,但它还有另外一个属性叫偏移量。不过在创建resource之前需要先用ID3D12Device::CreateHeap来创建heap。然后用ID3D12Device::CreatePlacedResouce来在heap里创建资源。

Placed-Resource.png

因为这儿的heap不需要被全局GPU分配,所以这个placed resource可以提供更好的性能,使用这个也需要图形程序员遵循这个准则。Placed resouces为增强不同的内存管理提供了更多的选项,但是能力越大,责任也越大。使用placed resouces时,也需要更多的考虑。用于放置placed resouces的heap的大小必须提前知道。如果太大了也没必要,因为重用GPU内存的方法只能是evict或完全摧毁它。由于你销毁heap时只能整个从GPU内存中销毁,此时这个heap中的任何资源都不能被正在GPU上正在执行的指令集所引用。

由于GPU架构,你可以在特定的heap里创建的资源种类也是特定的。比如,缓存资源(顶点缓存,索引缓存,常数缓存,结构缓存等)只能存放在使用ALLOW_ONLY_BUFFERS这个标签所创建的heap里。而Render Target或depth/stencil资源只能使用ALLOW_ONLY_RT_DS_TEXTURES标签,反之只能使用ALLOW_ONLY_NOT_RT_DS_TEXTURES而支持 heap tier 2 的适配器只能使用ALLOW_ALL_BUFFERS_AND_TEXTURES来允许任何资源种类来被放置在这个heap里。但由于heap tier取决于GPU架构,大多数应用程序只能使用heap tier1 支持。

如果多个placed resouces不会同时放在一个aliased heap里,那他们就可以放在一起。Aliasing可以帮助减少重复申请使用的GPU内存,因为 the size of the heap can be limited to the size of the largest resource that will be placed in the heap at any moment in time。Aliased 资源可以说使用resource aliasing barrier来交换。

Aliasing-Placed-Resources.png

Reserved Resources

Reserved资源在创建时并未指定要放置的heap。但是在使用ID3D12Device::CreateReservedResource 创建Reserved 资源前,需要使用ID3D12CommandQueue::UpdateTileMappings 方法来把这个映射到heap。部分reserved资源可以被映射到物理GPU内存中的heap。Reserved resources哪怕比单个heap大,也可以放置进去。

如果使用Reserved Resource,可以使用虚拟内存来创建很大的体积纹理,但是只有这个体积纹理的resident空间可以被映射到物理内存上。这样一来,使用sparse voxel octrees就可以不为GPU内存增添额外负担了。

 Reserved Resources

Pipeline State Object

The Pipeline State Object (PSO) 包含了很多需要的渲染或计算管线。包括如下:

  • Shader bytecode (vertex, pixel, domain, hull, and geometry shaders)
  • Vertex format input layout
  • Primitive topology type (point, line, triangle, or patch)
  • Blend state
  • Rasterizer state
  • Depth-stencil state
  • Number of render targets and render target formats
  • Depth-stencil format
  • Multisample description
  • Stream output buffer description
  • Root signature

 

The pipeline state object 结构包含了信息,如果这些状态需要在draw call之间被改变,比如需要不同的像素着色器或者混合状态,那么则需要一个新的pipeline state object。虽然pipeline state object 包含了很大信息,但其仍然需要几个外部的参数。

  • Vertex and Index buffers
  • Stream output buffer
  • Render targets
  • Descriptor heaps
  • Shader parameters (constant buffers, read-write buffers, and read-write textures)
  • Viewports
  • Scissor rectangles
  • Constant blend factor
  • Stencil reference value
  • Primitive topology and adjacency information

The pipeline state object can optionally be specified for a graphics command list when the command list is reset using the ID3D12GraphicsCommandList::Reset method but it can also be changed for the command list at any time using the ID3D12GraphicsCommandList::SetPipelineState method.

Root Signatures

根签名与C++的函数签名很像,它定义了要传递给着色器管线的参数,然后被绑定至渲染管线上,之后要再改变这个参数就可以不用改变这个根签名。根签名中的根函数不仅定义了希望传递到着色器中的参数类型,他们也定义了shader registers和register spaces,用于作为绑定至着色器的选项。

Shader Register & Register Spaces

着色器参数必须绑定至一个register,例如,常数缓存必须绑定至 b registers (b0 – bN),shader resouce view(纹理和非常数的缓存类型)要绑定至t registers (t0 – tN), unordered access views (writeable textures and buffer types) are bound to u registers (u0 – uN), and texture samplers are bound to s registers (s0 – sN) where N is the maximum number of shader registers。Shader Model 5.1 removes the limit to the number of shader registers that can be used. I think it is limited to \(2^{32}-1\) but I haven’t tested this upper limit.

在之前版本的DX中,不同的资源可以绑定到相同的register slot中,只要它们用在渲染管线的不同着色阶段。例如,一个常数缓存可以绑定至register b0到顶点着色器中,而另一个常数缓存可以绑定至b0到像素着色器中,而不会引起冲突。而DX12的Shader Model 5.1中,甚至连不同的着色管线阶段这个限制条件都没了,只要它们在不同的register spaces中即可。Prior to Shader Model 5.1, resource registers could overlap across shader stages (left). Shader Model 5.1 introduces shader spaces which can be used to overlap register slots (right).It is important for the graphics programmer to understand the shader register and register spaces overlapping rules when porting legacy shaders to DirectX 12.

Registers-and-Spaces.png

 

 

Root Signature Parameters

一个根签名可以包含很多参数,这些参数可以是如下种类:

32-BIT CONSTANTS

如果使用32-bit常数,那么常数缓存就可以直接传递而无需创建常数缓存资源。存储在根签名中的常数数据不支持动态索引。比如,先的常数缓存定义可以被映射成32-bit 常数并存储在根签名中。

1

2

3

4

5

6

cbuffer TransformsCB : register(b0, space0)

{

    matrix Model;

    matrix View;

    matrix Projection;

}

但是数组就不行了,如下。必须要使用inline descriptor或者descriptor heap。每个根签名中的根常数需要一个DWORD,32bit。

1

2

3

4

cbuffer TransformsCB : register(b0, space0)

{

    matrix MVPMatrices[3];

}

INLINE DESCRIPTORS

Descriptors可以被直接放在根签名里而无需descriptor heap[6]。Only constant buffers (CBV), and buffer resources (SRV, UAV) resources containing 32-bit (FLOATUINT, or SINT) components在根签名里可以用inline descriptors触及到。Inline UAV descriptors for buffer resources cannot contain counters (for example, if a RWStructuredBuffers contains a counter resource, it may not be accessed through an inline descriptor in the root signature. Texture resources cannot be referenced using inline descriptors in the root signature and must be placed in a descriptor heap and referenced through a descriptor table.

与根常数不同的是,包含数组的常数缓存可以在根签名里可以使用inline descriptor获取。每个inline descriptor需要消耗两个DWORD也就是64bits。

1

2

3

4

5

6

cbuffer SceneData : register(b0, space0)

{

   uint foo;

   float bar[2];

   int moo;

};

DESCRIPTOR TABLES

Descriptor table定义了一些在GPU可见descriptor heap里连续存储的descriptor range。

Descriptor-Tables.png

上面这张图展示了拥有一个descriptor table参数的根签名A。这个descriptor table包含三个descriptor rangesB,这三个分别是3 Constant Buffer Views (CBV), 4 Shader Resource Views (SRV), and 2 Unordered Access Views (UAV).CBV’s, SRV’s and UAV’s因为其这三种descritptors可以被存储在同种descriptor heap里,所以可以被同一种descriptor引用。The GPU visible descriptors (C)必须在在一段连续的heap上,而类似于D的资源则不用连续,甚至不用在相同的heap上。

Each descriptor table in the root signature costs 1 DWORD (32-bits) [5].

Static Samplers

使用纹理采样来指定如何使用纹理。但是这里可以直接在根签名里使用根签名而无需descriptor heap。这是使用D3D12_STATIC_SAMPLER_DESC结构。静态采样不使用任何根签名中的空间,不占大小。

Root Signature Constraints

根签名最大是64 DWORDs,即2048比特。

Root signatures are limited to 64 DWORDs (2048-bits) [5]

  • 32-bit constants each costs 1 DWORD
  • Inline descriptors each costs 2 DWORDs
  • Descriptor tables each costs 1 DWORD
  • static sampler each costs 0

开发者应当平衡好性能。如果一个根参数经常出现,那么就应该让它出现在根签名中的第一位。否认则尽量靠后。Since 32-bit constants and inline descriptors have better performance in terms of level of indirection, they should be favored over using descriptor tables as long as the size of the root signature does not become to large.

DirectX 12 Demo

The previous lesson showed how to initialize a DirectX 12 application without using any C++ classes. Some of the source code from the first lesson was refactored in order to simplify the source code for this and future lessons. There are three primary classes that are used for this lesson:

  1. Application
  2. Window
  3. CommandQueue
  4. Game

The Application class is responsible for initializing application specific data such as the DirectX 12 device and command queues. The Application class is also responsible for creating the Window instances and it is also the owner of the Window instances (Window instances can only be created and destroyed using the Application class). The Application class also exposes a Run method which is used to run the game and execute the message loop. The Quit method is used to quit the running application.

The Window class creates the swap chain which contains the final rendered image that will be presented to the screen. The Window class also contains functions to resize the window and toggle vsync, and fullscreen state.

The source code for the Application and the Window classes are not discussed in this lesson. You are encouraged to go back to the previous lesson if you are not familiar with the functionality of these classes. You may also refer to the source code for this lesson on GitHub. A link to the source code for this lesson also is provided at the end of the tutorial.

The CommandQueue and the Game class on the other hand may not be immediately clear and therefore will be discussed in greater detail in this lesson.

If you would prefer to skip the discussion on refactoring of the CommandQueue and Game classes then you can continue directly to the section about Shaders. Be aware that you may see code later in the lesson that you are not familiar with if you skip these sections.

The Command Queue Class

CommandQueue类包装了ID3D12CommandQueue接口以及用于让GPU与CPU同步的同步图元。其必须支持如下功能

  • 获取可以用于记录绘制命令的指令集
  • 在指令队列上执行指令集
  • 在指令队列上Signal a Fence
  • 检查Fence是否到达了一个特殊值
  • 如果没有,则继续等待
  • 然后Flush所有在指令队列上的指令

CommandQueue-1.png

/** * Wrapper class for a ID3D12CommandQueue. */
#pragma once#include <d3d12.h>  // For ID3D12CommandQueue, ID3D12Device2, and ID3D12Fence
#include <wrl.h>    // For Microsoft::WRL::ComPtr
#include <cstdint>  // For uint64_t
#include <queue>    // For std::queue

 

COMMANDQUEUE CLASS DEFINITION

class CommandQueue
{
public:
    CommandQueue(Microsoft::WRL::ComPtr<ID3D12Device2> device, D3D12_COMMAND_LIST_TYPE type);
    virtual ~CommandQueue();
    // Get an available command list from the command queue.
    Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList2> GetCommandList();
    // Execute a command list.
    // Returns the fence value to wait for for this command list.
    uint64_t ExecuteCommandList(Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList2> commandList);
    uint64_t Signal();
    bool IsFenceComplete(uint64_t fenceValue);
    void WaitForFenceValue(uint64_t fenceValue);
    void Flush();
    Microsoft::WRL::ComPtr<ID3D12CommandQueue> GetD3D12CommandQueue() const;

GetCommandList方法返回了可以发布绘制指令的指令集。这种方法返回的指令集可以被立即用于发布指令。并且不需要重设指令集或创建指令分配器。在指令被记录到指令集后,然后可以使用ExecuteCommandList方法来在指令队列上执行这些指令。这个方法返回Fence值,用于检查指令集中的指令是否在指令队列中执行完成。而GetD3D12CommandQueue方法用于获取ID3D12CommmandQueue接口。

protected:
    Microsoft::WRL::ComPtr<ID3D12CommandAllocator> CreateCommandAllocator();
    Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList2> CreateCommandList(Microsoft::WRL::ComPtr<ID3D12CommandAllocator> allocator);
private:

    // Keep track of command allocators that are "in-flight"
    struct CommandAllocatorEntry
    {
        uint64_t fenceValue;
        Microsoft::WRL::ComPtr<ID3D12CommandAllocator> commandAllocator;
    };
    using CommandAllocatorQueue = std::queue<CommandAllocatorEntry>;
    using CommandListQueue = std::queue< Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList2> >;
    D3D12_COMMAND_LIST_TYPE                     m_CommandListType;
    Microsoft::WRL::ComPtr<ID3D12Device2>       m_d3d12Device;
    Microsoft::WRL::ComPtr<ID3D12CommandQueue>  m_d3d12CommandQueue;
    Microsoft::WRL::ComPtr<ID3D12Fence>         m_d3d12Fence;
    HANDLE                                      m_FenceEvent;
    uint64_t                                    m_FenceValue;
    CommandAllocatorQueue                       m_CommandAllocatorQueue;
    CommandListQueue                            m_CommandListQueue;

CommandAllocatorFactory结构用于把一个Fence值与一个指令分配器关联起来。在之前一课说过,指令集在指令队列上执行后就可以立即复用,但是指令分配器不能,除非存储在指令分配器中的指令以及在指令队列上执行完成。为了检查后者的指令是否已经执行完成,将给指令队列一个相关联的fence value。而CommandAllocatorQueue则色一个std::queue物体,用于给正在执行的command allocators排序。与CommmandAllocatorEntry类型,CommandListQueue也是一个std::queueu物体,用于给可以复用的指令集排序。

m_d3d12Device成员变量存储了ID3D12Device2接口的指针用于创建指令队列,指令集和指令分配器。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值