Objectives
1、基本理解Direct3D在3D硬件编程中的作用
2、理解COM组件在Direct3D
3、学习基本图形概念,例如2D图像的储存,交换页,深度缓冲,多重采样,CPU\GPU的相互影响
4、学习怎么去使用性能计数器函数来获取高分辨率的时间读数
5、学习如何初始化Direct3D
6、熟悉D3D常用的数据结构和DX12龙书的demo框架
纹理
纹理是一个矩阵,用来储存一组元素,GPU可以对他们进行一些特殊的操作。
DX纹理支持的格式被定义为DXGI_FORMAT
。其中,例如R16G16B16A16
并非特指固定的颜色通道,而是表示有4个16-bit组成。
交换链和交换页
以下为翻译Dx12龙书章节:
为了避免画面闪烁,最好的方式是画完整的动画帧到一个离屏纹理上(被称为back buffer)。然后一次性将整个帧绘制到屏幕上。为了实现整个方式,硬件中保留了两个纹理buffer,一个被称为front buffer,另一个叫做back buffer。当下一帧在被绘制在back buffer的时候,front buffer储存了当前正在被显示器显示的视频帧。当绘制完成后,front buffer 和 back buffer的任务交换:back buffer变成了front buffer,front buffer变成了back buffer准备下一帧的绘制。交换(Swapping)他们的任务的过程被叫做呈现(presenting)。Presenting是个高效的操作,只需要交换指向buffer的指针就可以了。
front and back buffer来自交换链(swap chain)。在D3D中,一个swap chain是利用接口IDXGISwapChain
来实现的。这个接口储存了一个front buffer和一个back buffer,还提供了接口去resize buffer(IDXGISwapChain::ResizeBuffers
)、Presenting(IDXGISwapChain::Present)。
使用两个buffer叫做双重缓冲double buffering,三个叫三重缓冲triple buffering。
Note:虽然back buffer是一个纹理(所以他的元素应该被称为texel),但我们经常把他的元素称做像素。
Depth Buffering
深度缓冲是一个纹理的样例但不包含图像信息,他包含了深度信息在一些 像素附近。可能的深度信息范围是从0.0到1.0f。0.0f表示了这个物体在视锥体中离的最近,而1.0表示离得最远。这里是一个一一对应关系在depth buffer中的每一个元素和back buffer中的每一个像素。(i.e.,back buffer中的第 ij个元素,对应depth buffer中的ij个元素,所以他们是同大小的)。
当绘制时会比较当前像素深度与depth buffer中对应元素深度的大小,如果新像素的深度值比buffer中的小,则新的像素将在back buffer中代替原有的像素。这样绘制出来的最终效果将呈现正确的物理遮盖。这样的机制使得我们无需关心渲染物体的先后顺序。
depth buffer是一个纹理,所以他必须是某几个数据格式:
1、DXGI_FORMAT_D32_FLOAT_S8X24_UINT
:指定一个32位浮点深度缓冲区,为映射到[0,255]范围的模板缓冲区保留8位(无符号整数),而不用于填充的24位浮点深度缓冲区。
2、DXGI_FORMAT_D32_FLOAT
指定一个32-bit浮点型深度缓冲区。
3、DXGI_FORMAT_D24_UNORM_S8_UINT
:指定映射到[0,1]范围的无符号24位深度缓冲区,为映射到[0,255]范围的模板缓冲区保留8位(无符号整数)。
4、DXGI_FORMAT_D16_UNORM
:指定映射到[0,1]范围的无符号16位深度缓冲区。
Note:一个应用程序不是必须拥有模板缓冲,但如果他有的话,一般依附于深度缓冲。所以一般来说,深度缓冲可以更恰当的叫做:depth/stencil buffer 深度模板缓冲。(以后再解释模板缓冲stencil buffer)
Resources and Descriptors
在我们渲染期间,GPU需要读写资源。我们需要先绑定(bind)或链接(link)我们的资源到渲染管道在我们draw call之前。一些资源在每一次draw call都会被改变,所以我们需要刷新他们在每一次draw call如果必要的话。
但是,GPU资源并不直接绑定。 相反,资源通过描述符对象引用,描述符对象可以被认为是描述GPU的资源的轻量级结构。
为什么要用描述符去这个额外的间接级别? 原因是GPU资源本质上是通用的内存块。 资源保持通用,因此可以在渲染管道的不同阶段使用资源; 一个常见的例子是使用纹理作为渲染目标(即Direct3D绘制到纹理),然后作为着色器资源(即纹理将被采样并用作着色器的输入数据)。 资源本身并不表示它是否被用作渲染目标,深度/模板缓冲区或着色器资源。 此外,也许我们只想将资源数据的一个子区域绑定到渲染管道 - 我们如何才能为整个资源提供资源? 此外,可以使用无类型的格式创建资源,因此GPU甚至不会知道资源的格式。
Descriptors是有类型的,类型意味着我们如何使用资源。
1.CBV/SRV/UAV 描述符描述constant buffers,shader resources,unordered access view resources.
2. Sampler descriptors describe sampler resources (used in texturing).
3. RTV descriptors describe render target resources.
4. DSV descriptors describe depth/stencil resources.
多重采样原理
因为显示器上的像素是离散的,不是可以无限细分的,任意的一条直线没办法被完美的画出来。类似的,三角形边缘也会出现aliasing effects。
在反锯齿技术中,其中有一个叫做超级采样(supersampling),通过放大back buffer 和 depth buffer 4倍。3D场景渲染到这个更大的buffer中。当我们需要present时,我们将back buffer 进行downsample,4个像素取平均值来获得一个平均的像素颜色。实际上,超级采样是通过增加软件的分辨率来实现的。
但是超级采样的代价是昂贵的,因为需要以大的多的分辨率去渲染。D3D支持一种比较妥协的技术叫做多重采样。他在次像素之间共享了一些计算数据使他变得更小的开销相比超级采样。假设我们使用X4多重采样,同样的我们使用4倍大小的depth buffer和back buffer。但是,相比超级采样计算每一个次像素的颜色,我们只计算一次在像素中心,然后共享这个颜色数据到每一个次像素基于他是否可见的(depth/stencil test对于每一个次像素都计算),然后判断次像素是否被多边形覆盖(在多边形内或外)。然后对这些像素求均值,取得更加平滑的结果。
Note:因为计算像素的颜色值是整个渲染中最昂贵的开销,相比于超级采样,多重采样在这个开销上要小的多。换句话来说,超级采样要更加精确在颜色上。
Direct3D的多重采样
在后面我们需要填写DXGI_SAMPLE_DESC
结构体。定义如下:
typedef struct DXGI_SAMPLE_DESC
{
UINT Count;
UINT Quality;
} DXGI_SAMPLE_DESC;
Count
属性指定了对于每一个像素采样的次数。Quality
属性用来指定我们期望的品质等级(实际意义取决于硬件厂商)。Quality levels也取决于纹理的类型和每一个像素的采样数量。我们可以查询他对于一个给定的纹理类型和采样数量,使用ID3D12Device::CheckFeatureSupport
方法。例如:
typedef struct D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS {
DXGI_FORMAT Format;
UINT SampleCount;
D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG Flags;
UINT NumQualityLevels;
} D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS;
D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
msQualityLevels.Format = mBackBufferFormat;
msQualityLevels.SampleCount = 4;
msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;
msQualityLevels.NumQualityLevels = 0;
ThrowIfFailed(md3dDevice->CheckFeatureSupport(
D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,&msQualityLevels,sizeof(msQualityLevels)));
第二个参数既是输入参数又是输出参数。对于输入,我们需要制定纹理类型,采样次数和flag(我们想查询的多重采样支持等级)。函数将填写quality level作为输出。有效的等级是从0到NumQualityLevels–1。
最大的采样数量被定义为:#define D3D11_MAX_MULTISAMPLE_SAMPLE_COUNT ( 32 )
,通常我们使用4或者8是兼顾性能和内存开销比较合理的选择。所有的D3D11支持硬件都支持4x多重采样在所有的渲染格式上。
Note:对于swap chain buffer和depth buffer都需要填写DXGI_SAMPLE_DESC
结构体,而且需要使用同样的多重采样设置。
DirectX Graphics Infrastructure
引用:
DXGI(DirectX Graphics Infrastructure,DirectX 图形基础架构) 提供了对图形硬件进行底层管理的功能。这些功能不涉及显卡的高级特性(比如不需要区别D3D9级别显卡和D3D11级别显卡),因此是与D3D的图形功能是分开的。 DXGI是提供了一个底层的通用框架来支持未来的硬件。DXGI最初是在D3D10中引入的。现在它支持D3D11。即DXGI和D3D可以分开进行进化,而且DXGI的进化应该比D3D慢。
DXGI的主要功能有:枚举显示硬件设备,将渲染好的帧呈现到输出设备,调整Gamma,以及全屏模式的切换等。DXGI的目的是沟通核心模式驱动和系统硬件。一个应用程序可以直接调用DXGI接口,也可以通过D3D接口间接使用DXGI的功能。
一些我们后面初始化D3D用到的接口:
IDXGIFactory
主要用来创建IDXGISwapChain
还有枚举显示适配器。
Display adapters是抽象的图形功能。一般的一个Display adapters是一个物理硬件(e.g. 显卡),也可也软件模拟出来。我们可以枚举出系统上所有的Display adapters,像这样:
void D3DApp::LogAdapters()
{
UINT i = 0;
IDXGIAdapter* adapter = nullptr;
std::vector<IDXGIAdapter*> adapterList;
while(mdxgiFactory->EnumAdapters(i, &adapter) != DXGI_ERROR_NOT_FOUND)
{
DXGI_ADAPTER_DESC desc;
adapter->GetDesc(&desc);
std::wstring text = L”***Adapter: “;
text += desc.Description;
text += L”\n”;
OutputDebugString(text.c_str());
adapterList.push_back(adapter);
++i;
}
for(size_t i = 0; i < adapterList.size(); ++i)
{
LogAdapterOutputs(adapterList[i]);
ReleaseCom(adapterList[i]);
}
}
我们可以获取到一些硬件支持相关的信息,比如支持的多重采样等级等等。
Residency
在D3D12中,应用程序确定某个资源是否应该留在GPU内存中。基本思想是,最小化GPU内存的开销,因为没办法驻留所有的资源在GPU内存中。但是因为这样的操作是存在开销的,所以当我们把资源从GPU中驱逐出去时,这个资源应该有一段时间不再被使用。
初始的,当一个资源被创建就使他驻留,当他销毁时便被驱逐出GPU了内存,但我们也可以通过以下的方法自主的管理:
HRESULT ID3D12Device::MakeResident(UINT NumObjects,ID3D12Pageable *const *ppObjects);
HRESULT ID3D12Device::Evict(UINT NumObjects,ID3D12Pageable *const *ppObjects);
对于这两个方法,第二个参数是一个ID3D12Pageable
资源的数组。第一个参数是数组的个数。
命令队列和命令列表(The Command Queue and Command Lists)
CPU把 命令投递到命令队列中,GPU从队列中一条一条取出来执行。
我们不想看到的是GPU满载CPU空载,或者CPU空载GPU满载。在性能考虑上,我们尽量让两个硬件都在处理事情,而不是把时间花在等待同步上。
在D3D12中,命令队列用接口:ID3D12CommandQueue
。我们需要填写(fill out)一个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)));
//The IID_PPV_ARGS helper macro is defined as:
#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)),IID_PPV_ARGS_Helper(ppType)
ExecuteCommandLists
:把命令列表中的命令添加到命令队列中。
ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
当我们记录完所有的命令后,我们需要调用ID3D12GraphicsCommandList::Close
,才能再调用ID3D12CommandQueue::ExecuteCommandLists
.
ThrowIfFailed(mCommandList->Close());
ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
一个command list相关的是一块内存区域被叫做ID3D12CommandAllocator
,就像命令被记录在命令列表一样,实际上他们被储存在相关的Allocator里。当调用ID3D12CommandQueue::ExecuteCommandLists
时,命令队列会取到Allocator的引用。
HRESULT ID3D12Device::CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE type,
REFIID riid,
void **ppCommandAllocator);
HRESULT ID3D12Device::CreateCommandList(
UINT nodeMask,
D3D12_COMMAND_LIST_TYPE type,
ID3D12CommandAllocator *pCommandAllocator,
ID3D12PipelineState *pInitialState,
REFIID riid,
void **ppCommandList);
两个type
都需要使用相同的类型。当关联他们在一起时。类型
D3D12_COMMAND_LIST_TYPE_DIRECT
:储存一个命令列表被GPU直接执行。
D3D12_COMMAND_LIST_TYPE_BUNDLE
:指定命令列表表示一个bundle。 在构建命令列表中有一些CPU开销,因此Direct3D 12提供了一个优化,允许我们将一系列命令记录到所谓的bundle中。 在记录了捆绑包之后,驱动程序将预处理命令,以优化它们在渲染过程中的执行。 因此,应在初始化时记录捆绑包。 如果分析显示构建特定命令列表正在花费大量时间,则应将软件包的使用视为优化。 Direct3D 12绘图API已经非常高效,所以您不需要经常使用软件包,如果能够显示其性能提升,您应该只使用它们; 也就是说,默认情况下不要使用它们。 我们在这本书中不用捆绑包; 有关更多详细信息,请参阅DirectX 12文档。
我们可以创建多个命令列表关联同一个allocator,但是不能同时记录命令。换句话说,所有的命令列表必须关闭除了我们需要记录命令的那个列表。因此,所有的统一命令列表的命令都会是连续的。但需要注意的是,当我们created或者reset一个命令列表时,他会是处于open状态的。所以当我们连续的创建两个关联相同allocator的命令列表时,会获得一个错误。
在我们调用ID3D12CommandQueue :: ExecuteCommandList(C)之后,可以通过调用ID3D12CommandList :: Reset方法来重新使用C的内部存储器来记录一组新的命令。 该方法的参数与ID3D12Device :: CreateCommandList中的匹配参数相同。
HRESULT ID3D12CommandList::Reset( ID3D12CommandAllocator *pAllocator,
ID3D12PipelineState *pInitialState);
该方法使命令列表的状态恢复跟刚创建一样,但可以重用已经申请了的内部内存。但他不会影响到命令队列中已有的命令。因为命令队列中的内存已经有了相关的命令备份。这有点像std::vector::clear。
CPU/GPU Synchronization
由于CPU/GPU在并行运行,所以这里有同步的问题。
假定我们有资源R储存了一些几何位置信息,我们要把他画出来。此外,假定CPU更新R来保存位置p1,然后添加一个绘制命令C来引用R去绘制几何体到位置P1。但增加命令到命令队列并不会阻塞CPU,所以CPU继续运行。当CPU又更新了R资源中位置信息到P2的时候,GPU执行C命令,就与我们的期望冲突了。
一种解决方案为使CPU等待直到GPU执行到特定的位置以后。我们称这种方式为flushing the command queue
。接口ID3D12Fence
代表了fence,用来同步GPU/CPU。
我们在某一个指令之后插入GPU Fence加一的的指令。然后去查询GPU的Fence的值,若果已经加一,则证明我们插入的这条指令已经完成。也就意味着插入这条指令之前的指令已经完成了。由此进行CPU/GPU的同步过程。
Resource Transitions
为了完成一个普通的渲染指令,一般情况是GPU写一个资源R在某一步,然后我们在后续步骤去读这个资源R。但是,这里会出现一个问题(resource hazard
),当我们读取这个资源但是GPU还没有写完的时候。为了解决这个问题,D3D给资源关联了一个状态标识。资源被置为初始状态当他被创建时,然后由应用程序告诉D3D任何的状态转换。这使得GPU能够进行任何需要做的工作来进行转换并防止资源危害。例如:如果我们写一个资源,假设是一个纹理,我们将设置他的状态为render target state
,当我们需要读这个纹理时,我们将改变他的状态到shader resource state
。通过这个信息,GPU能通过一些步骤防止出现上述问题。例如:等待所有的写操作完成后,再去读某个资源。
mCommandList->ResourceBarrier(1,
&CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET)
);
例程中这段代码的意思是,将表示我们在屏幕上显示的图像的纹理从presentation state
转换到render target state
。 观察资源障碍被添加到命令列表中。 您可以将资源障碍转换视为本身的指令,指示GPU正在转移资源的状态,以便在执行后续命令时可以采取必要措施来防止资源危害。
Multithreading with Commands
多线程命令列表需要注意的地方:
- Command List不是线程安全的(free-threaded)。多线程共享他并调用他的函数可能不会正确。所以一般的我们都给每一个线程分配一个command list。
- Command Allocators不是线程安全的。所以一般的同上,都会给每一个线程分配一个command allocators。
- Command Queue是线程安全的。所以多线程可以同时存取队列并调用他的函数。特别的,每个线程可以同时提交他们各自生成的命令列表到一个线程Queue。
- 出于性能原因,应用程序必须指定最大的command lists数量当我们初始化时。