目录
1、前言及本章内容提要
2、初步理解和使用根签名
3、使用WIC库加载图片
4、D3D12内存管理导论——上传堆和默认堆
4.1、D3D12中创建资源的三种方式
4.1.1、提交方式(CreateCommittedResource)
4.1.2、定位方式(CreatePlacedResource)
4.1.3、保留方式(CreateReservedResource)
4.2、D3D12中堆的类型(默认堆、上传堆等)
4.3、资源屏障(Resource Barrier)
5、创建默认堆资源和上传堆资源并上传纹理数据
5.1、创建默认堆上的2D纹理
5.2、创建上传堆上的资源
5.3、复制纹理图片数据到上传堆
5.4、调用复制命令并放置围栏
6、描述符和描述符堆
7、本章全部代码
距第一篇教程推出至今,不知不觉又过了将近一个月时间了,其实并不是我没有努力的继续准备教程,而是在调用WIC加载纹理的过程中遇到了点挫折,折腾了我好长时间,期间经过了愉快的国庆假期,带着家人游历了一下祖国的壮丽河山,换了换脑筋,安静的读了些资料,问题居然迎刃而解。
通过对上一篇教程的反馈来看,很多网友反映最大的问题就是对D3D12中的新概念——根签名非常的陌生和不理解,因此在本篇教程中我将详细讲解这个概念,并在后续的教程中不断加深大家对这个比较核心的概念的理解。
另外本章的例子中为了循序渐进的学习要求,我特意安排了加载一副纹理并显示的代码。其中用到了WIC(Windows Imaging Component)库,之后我也会详细介绍该库的用法,因为后续的很多示例使用纹理,都需要用到这个库。当然如果你已经升级到了Windows 10系统的话,这个库是系统默认自带的,调用不会有什么问题。
之所以使用这个库是因为这个库支持的图片格式类型比较多,因此使用它来加载纹理是不错的选择。当然如果你熟悉其他的图片库,如:CxImage等,使用它们也可以。这里我只是为了方便而使用它。另外因为大量的DX12示例中都用到了这个库,所以我就讲解下这个库的用法,方便大家阅读理解那些DX12以及DXR中的示例代码。
另外这一讲中我还将重点再介绍一下资源屏障原理及实际中使用时的方法。虽然上一讲中针对渲染目标资源我们已经用到了资源屏障,但是上一讲我有意淡化了对它的讲解,毕竟一开始就灌输很多概念会让人吃不消,所以这一章中我们着重再来讲解一下它。另外资源屏障是D3D12中引入的GPU线程间同步的重要核心概念之一,掌握它也是掌握D3D12核心基础概念之一。
本章教程我们的目标是渲染一个矩形并加载一个纹理显示其上。教程中用到的渲染矩形的方法可能需要大家理解并记忆,因为之后自己在封装引擎的UI部分时既可以此例的方法为基础蓝本。
本章主要目标:绘制一个带纹理的矩形,效果图如下
主要内容:
理解和掌握根签名
WIC库的基本使用方法
初步理解显存管理和加载纹理
讲到根签名,我们应当想到的类似概念其实就是函数签名,也就是C/C++中常说的函数声明。为什么这样说呢?
其实在概念上,我推荐你将整个GPU的渲染管线理解为一个大的函数执行体,其主要代码就是那些运行于GPU上的Shader程序,而根签名则是说明了这个渲染管线可以传入什么参数。更直白的说因为GPU渲染需要数据(比如:纹理、网格、索引、世界变换矩阵、动画矩阵调色板、采样器等等),而这些数据必须是GPU能够理解的专有格式,这些数据始终是CPU负责加载并传递给GPU的,此时就需要提前告诉GPU要传递那些数据,以及传递到什么地方,传递多少个等信息,而这些信息最终就被封装在了根签名里。实际也就是说根签名也是CPU向GPU传递渲染数据的一个契约。整体上看也就是CPU代码调用GPU渲染管线“函数”进行渲染,因此要传递参数给渲染管线,参数的格式就是根签名定义的。
那么为什么需要根签名这么一个结构化的描述呢?这是因为无论什么数据,在存储中都是线性化的按字节依次排列在存储(内存及显存)中的,如果不加额外的描述信息来说明,GPU甚至CPU本身根本无法分辨那块存储器中存储的是什么数据,即没有类型说明的数据,这类似C语言中的VOID*指针指向的一块数据,如果不额外说明,根本不知道里面到底存了些什么。所以根签名从根本上为这些存储的数据描述清楚了基本类型信息和位置信息。当数据按照指定的方式传递到指定的位置后,GPU就可以按照根签名中的约定访问这些数据了。而进一步的详细的类型信息则是由各种描述符来详细描述了。
或者换种说法,根签名描述清楚了渲染管线或者说Shader编译后的执行代码需要的各种资源以什么样的方式传入以及如何在内存、显存中布局,当然主要指定的是GPU上对应的寄存器。另一种等价的说法是说如何将这些数据绑定到渲染管线上,实质是说的一回事情,只是角度不同。
因此每一个执行不同渲染功能的渲染管线“函数”之间就需要不同的根签名来描述它的资源存储及传递情况。或者你可以理解为每个不同的渲染管线“大函数”都需要不同的对应的根签名来描述其“参数”。也就是我们定义了不同的函数,就需要配之以不同的函数声明。
以上是从宏观概念上具体去理解根签名的含义。具体的讲,根签名实际是描述了常量(类似默认参数)、常量缓冲区(CBV)、资源(SRV,纹理)、无序访问缓冲(UAV,随机读写缓冲)、采样器(Sample)等的寄存器(Register)存储规划的一个结构体。同时它还描述了每种资源针对每个阶段Shader的可见性。
并且常量(默认参数),根描述符、静态采样器等可以在根签名中直接被赋值,这有点像函数的默认参数一样。当然这些默认的“参数”在根签名中被理解为静态的,也就是他们被固化在这个根签名中,要动态修改他们所指资源位置及寄存器等信息是不行的。
当然像代码函数一样,根签名也需要编译一下才能被GPU正确理解。代码中调用API编译根签名的方法,在上一讲中已经有了介绍。另一种编译根签名的方法是使用HLSLI脚本的方法,我不打算介绍,因为它要借助独立的编译器和编译环境,对于引擎封装以及灵活性要求来说这都不是很方便的方法。同Shader的编译一样,我都推荐大家使用调用API的方法,因为这些API可以很方便的由你封装进工具或者再封装为脚本的API,甚至内置在引擎中,灵活性是很高的。所以我们也就主要学会API的调用方法即可。
截止目前,D3D12中支持的根签名已经升级到1.1版本。主要扩展了用于针对驱动级优化的一些标志。在这篇教程的示例中,我们将演示如何使用高版本并向下兼容的方法来编译根签名。
因为本章示例中需要显示一个带纹理的矩形,因此除了准备顶点缓冲之外,我们还要准备纹理资源及对应的资源描述符、纹理采样器等。因此本例中根签名就需要稍微复杂一点。代码如下:
//11、创建根描述符
D3D12_FEATURE_DATA_ROOT_SIGNATURE stFeatureData = {};
// 检测是否支持V1.1版本的根签名
stFeatureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_1;
if (FAILED(pID3DDevice->CheckFeatureSupport(D3D12_FEATURE_ROOT_SIGNATURE, &stFeatureData, sizeof(stFeatureData))))
{
stFeatureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_0;
}
// 在GPU上执行SetGraphicsRootDescriptorTable后,我们不修改命令列表中的SRV,因此我们可以使用默认Rang行为:
// D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC_WHILE_SET_AT_EXECUTE
CD3DX12_DESCRIPTOR_RANGE1 stDSPRanges[1];
stDSPRanges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC_WHILE_SET_AT_EXECUTE);
CD3DX12_ROOT_PARAMETER1 stRootParameters[1];
stRootParameters[0].InitAsDescriptorTable(1, &stDSPRanges[0], D3D12_SHADER_VISIBILITY_PIXEL);
D3D12_STATIC_SAMPLER_DESC stSamplerDesc = {};
stSamplerDesc.Filter = D3D12_FILTER_MIN_MAG_MIP_POINT;
stSamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
stSamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
stSamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
stSamplerDesc.MipLODBias = 0;
stSamplerDesc.MaxAnisotropy = 0;
stSamplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_NEVER;
stSamplerDesc.BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK;
stSamplerDesc.MinLOD = 0.0f;
stSamplerDesc.MaxLOD = D3D12_FLOAT32_MAX;
stSamplerDesc.ShaderRegister = 0;
stSamplerDesc.RegisterSpace = 0;
stSamplerDesc.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC stRootSignatureDesc;
stRootSignatureDesc.Init_1_1(_countof(stRootParameters), stRootParameters
, 1, &stSamplerDesc
, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
ComPtr pISignatureBlob;
ComPtr pIErrorBlob;
GRS_THROW_IF_FAILED(D3DX12SerializeVersionedRootSignature(&stRootSignatureDesc
, stFeatureData.HighestVersion
, &pISignatureBlob
, &pIErrorBlob));
GRS_THROW_IF_FAILED(pID3DDevice->CreateRootSignature(0
, pISignatureBlob->GetBufferPointer()
, pISignatureBlob->GetBufferSize()
, IID_PPV_ARGS(&pIRootSignature)));
上面的代码中,首先我们调用了一个极其重要的API:ID3D12Device::CheckFeatureSupport这个函数是我们检测GPU能力以及驱动支持特性的不二方法,以后我们还会多次讲到并用到这个方法,你可以理解为这是古老的GetCaps函数的高级版,只是它能查询的特性更高级,特性项更少而已,而从D3D11起D3D规范已经要求所有支持D3D特性的显卡及驱动一定要支持几乎所有的基本特性,否则就认为是不支持D3D11的,所以调用它来查询的往往是更高级的一些特性,这些特性在一般的应用中很少用到而已。在这里我们使用该函数来检测目前DX12的环境是否支持V1.1版本的根签名。
接下来的CD3DX12_DESCRIPTOR_RANGE1和CD3DX12_ROOT_PARAMETER1两个结构体联合定义了我们的渲染管线需要传入一个SRV的资源。同样的这两个工具类来自D3Dx12.h。这里建议你将所有在D3D12.h中关于根签名的枚举定义、结构体定义等复制到一个单独的文件中,仔细阅读理解一下。
其实在D3D中不论渲染管线需要的资源多么复杂,总体上资源就分为两大类:一类是纹理和对应的采样器,也就是图片;另一类是数据,比如每一帧都需要传入的世界变换矩阵、骨骼动画的矩阵调色板等;我们这里使用的SRV(Shader Resource View)实际就是来描述需要传入一个纹理数据的,本质上就是图片数据。
紧接着一个D3D12_STATIC_SAMPLER_DESC结构体对象定义了一个静态的采样器对象,相当于一个默认参数。这是一种快捷的静态的固化的采样器参数的定义方式,它的特点就是访问速度快,但缺点就是如果我们要变换采样器就需要重新定义一个新的采样器,并且创建一个新的根签名对象。后面的教程中我们还将讲解动态形式的采样器的定义、创建和使用的方法。
这里需要注意的是最后总体组装一个CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC结构体时,静态采样器数组(也就是说我们可以定义多个静态采样器,只是此例中我们只使用了一个)是需要单独作为参数指定的。
此例中我们的渲染管线最终只有一个描述符表和一个静态采样器参数,这从Init_1_1函数(1_1的意思是V1.1)的参数就可以看出来,同时我们的渲染管线还接受原始图元结构数据输入,这通过最后一个D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT参数值来指定即可。
最终我们使用了一个D3DX12.h库中的D3DX12SerializeVersionedRootSignature工具函数对根签名进行了编译(其实理解为序列化更合适),将其转换成了GPU能够理解的二进制形式,然后我们利用这个二进制数据创建了根签名对象。建议有能力的网友去阅读下这个工具函数的源代码,理解下它封装的思路。其实它的代码就是枯燥无味的判断和结构体赋值,最终都是调用对应版本的序列化函数对根签名结构体进行编译。
那么最终如何来理解这个根签名如何规划了GPU需要的数据呢?一图胜千言:
图中的箭头明确的指出了C++代码中指定的寄存器序号和Shader脚本中的寄存器序号的关系。当然Shader中register语义文法中的字母都有其特定类型含义,这里的t即表示Texture寄存器类型,s表示采样器寄存器类型,其他的还有b表示常量缓冲区,u表示无序访问存储区等。而在C++代码中就需要针对不同类型的数据定义不同的结构体加以区别描述,而这就是根签名的核心职责。当然同类数据有多个时就是约定两边使用相同的寄存器序号即可。
如果在根签名中定义了参数,而没有实际传递对应类型的数据话,就会引起一个GPU访问违例的异常,这很好理解,就好像我们在C/C++代码中访问了一个并没有分配内存的指针一样。反过来,如果Shader中只使用了几个数据,而我们传递了多余的数据时,不会引起什么问题,实际效果就是我们传递了数据,而GPU完全无视那些多余的数据而已。
当然上面只是说明了作为“参数”描述时,声明中的“形参”在“函数体”Shader中被引用的情况,那么实际传递参数时又是什么样的对应关系呢?让我们来看看下图:
从上面我们就可以看到在具体的渲染循环中,我们就是使用SetGraphicsRootDescriptorTable方法设置对应根签名参数索引的描述符表的首地址。而描述符则又是描述具体资源的内存位置、类型、大小等信息的。所以从这里也可以看出根签名中参数的类型有点像C/C++代码中的指针的指针“TypeName **”类型,从右边看第一重指针是描述符指向被描述的资源,第二重指针就是根签名参数指向具体的描述符堆的首地址,也就是我们上一讲中说的描述符堆目前实际就是描述符数组。也就是说根签名的中的参数实际描述的是一个“指针数组”,而数组中每个具体的指针元素,也就是描述符,又指向具体的资源数据(纹理、缓冲等)。
这样我们就可以这样来理解对GPU渲染管线“函数”调用的完整过程了:如果我们需要向GPU传递资源数据,那么首先就需要将数据加载到GPU可访问的存储中(内存或显存),其次需要创建一个“指针数组”也就是描述符堆(描述符数组),再次我们需要为每个资源数据利用描述符堆中的一个元素创建一个“指针”也就是描述符,最后我们调用SetGraphicsRootDescriptorTable告诉渲染管线,具体每个根签名中的参数对应哪个“指针数组”也就是描述符堆,当然给它首地址就行了,因为GPU确切的知道每个“指针数组”元素也就是描述符的内存大小,这可以通过GetDescriptorHandleIncrementSize方法来得到。
至此我想你应该完全明白根签名的意义,以及实际的用法了。剩下的就是反复的利用这一模式,定义和实践不同的渲染管线,查看渲染的效果了。
本章伊始,我提到了为了准备例子代码我遇到了点小挫折,折腾了我一段时间,其实并不是D3D12本身让我受挫,而是这一小节我们将要学习的WIC接口让我深深的受了一次挫。
WIC接口,全称就是Windows Image Component(Windows图像组件)。第一次撞见这个库是在看DX12的示例代码的时候,但对这个库我一直很无知,对它的历史我也了解不多,网上资料也很有限,都是入门级的材料,没多少深入的探讨和应用,更没有整体性的介绍。我猜这个库可能是用来补充GDI+的,或者是为了弥补Windows系统自身对图片支持能力不足而产生的。最终按照微软的风格,WIC妥妥的被封装成了古老的COM形式。当然好处就是现在的开发语言环境基本都支持这个东东。
初识WIC我的第一印象就是:哇!Windows居然可以有这么简单的库可以支持这么丰富的图片格式,再也不用使用慢速的CxImage库了,欧耶!但是当我遇到挫折并折腾了好久才搞定之后,我想说:靠!用得着这么复杂的接口体系和概念框架封装吗?调用简直就是体力活!
其实从客观上讲WIC库的功能不仅仅局限于解析和解码各种图片,它还提供了格式转换以及编码压缩图片的能力。所以它自身是一个功能比较齐全和丰富的图片处理库,当然按照COM封装的要求的话它的接口过于丰富和复杂也在所难免了。幸运的是在我们调用它加载纹理的过程中,只会使用到它的一小部分功能,还好这些不需要太多的其他基础知识,只是要求你要对COM有基本的了解和掌握即可。
在此例中我们主要用到这个库的两大功能一个是加载和解码图片;另一个就是将图片转换为D3D12可以加载和识别的格式。
要调用WIC库,就要包含其头文件#include
struct WICTranslate
{
GUID wic;
DXGI_FORMAT format;
};
static WICTranslate g_WICFormats[] =
{//WIC格式与DXGI像素格式的对应表,该表中的格式为被支持的格式
{ GUID_WICPixelFormat128bppRGBAFloat, DXGI_FORMAT_R32G32B32A32_FLOAT },
{ GUID_WICPixelFormat64bppRGBAHalf, DXGI_FORMAT_R16G16B16A16_FLOAT },
{ GUID_WICPixelFormat64bppRGBA, DXGI_FORMAT_R16G16B16A16_UNORM },
{ GUID_WICPixelFormat32bppRGBA, DXGI_FORMAT_R8G8B8A8_UNORM },
{ GUID_WICPixelFormat32bppBGRA, DXGI_FORMAT_B8G8R8A8_UNORM }, // DXGI 1.1
{ GUID_WICPixelFormat32bppBGR, DXGI_FORMAT_B8G8R8X8_UNORM }, // DXGI 1.1
{ GUID_WICPixelFormat32bppRGBA1010102XR, DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM }, // DXGI 1.1
{ GUID_WICPixelFormat32bppRGBA1010102, DXGI_FORMAT_R10G10B10A2_UNORM },
{ GUID_WICPixelFormat16bppBGRA5551, DXGI_FORMAT_B5G5R5A1_UNORM },
{ GUID_WICPixelFormat16bppBGR565, DXGI_FORMAT_B5G6R5_UNORM },
{ GUID_WICPixelFormat32bppGrayFloat, DXGI_FORMAT_R32_FLOAT },
{ GUID_WICPixelFormat16bppGrayHalf, DXGI_FORMAT_R16_FLOAT },
{ GUID_WICPixelFormat16bppGray, DXGI_FORMAT_R16_UNORM },
{ GUID_WICPixelFormat8bppGray, DXGI_FORMAT_R8_UNORM },
{ GUID_WICPixelFormat8bppAlpha, DXGI_FORMAT_A8_UNORM },
};
// WIC 像素格式转换表.
struct WICConvert
{
GUID source;
GUID target;
};
static WICConvert g_WICConvert[] =
{
// 目标格式一定是最接近的被支持的格式
{ GUID_WICPixelFormatBlackWhite, GUID_WICPixelFormat8bppGray }, // DXGI_FORMAT_R8_UNORM
{ GUID_WICPixelFormat1bppIndexed, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat2bppIndexed, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat4bppIndexed, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat8bppIndexed, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat2bppGray, GUID_WICPixelFormat8bppGray }, // DXGI_FORMAT_R8_UNORM
{ GUID_WICPixelFormat4bppGray, GUID_WICPixelFormat8bppGray }, // DXGI_FORMAT_R8_UNORM
{ GUID_WICPixelFormat16bppGrayFixedPoint, GUID_WICPixelFormat16bppGrayHalf }, // DXGI_FORMAT_R16_FLOAT
{ GUID_WICPixelFormat32bppGrayFixedPoint, GUID_WICPixelFormat32bppGrayFloat }, // DXGI_FORMAT_R32_FLOAT
{ GUID_WICPixelFormat16bppBGR555, GUID_WICPixelFormat16bppBGRA5551 }, // DXGI_FORMAT_B5G5R5A1_UNORM
{ GUID_WICPixelFormat32bppBGR101010, GUID_WICPixelFormat32bppRGBA1010102 }, // DXGI_FORMAT_R10G10B10A2_UNORM
{ GUID_WICPixelFormat24bppBGR, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat24bppRGB, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat32bppPBGRA, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat32bppPRGBA, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat48bppRGB, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat48bppBGR, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat64bppBGRA, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat64bppPRGBA, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat64bppPBGRA, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat48bppRGBFixedPoint, GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
{ GUID_WICPixelFormat48bppBGRFixedPoint, GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
{ GUID_WICPixelFormat64bppRGBAFixedPoint, GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
{ GUID_WICPixelFormat64bppBGRAFixedPoint, GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
{ GUID_WICPixelFormat64bppRGBFixedPoint, GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
{ GUID_WICPixelFormat48bppRGBHalf, GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
{ GUID_WICPixelFormat64bppRGBHalf, GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
{ GUID_WICPixelFormat128bppPRGBAFloat, GUID_WICPixelFormat128bppRGBAFloat }, // DXGI_FORMAT_R32G32B32A32_FLOAT
{ GUID_WICPixelFormat128bppRGBFloat, GUID_WICPixelFormat128bppRGBAFloat }, // DXGI_FORMAT_R32G32B32A32_FLOAT
{ GUID_WICPixelFormat128bppRGBAFixedPoint, GUID_WICPixelFormat128bppRGBAFloat }, // DXGI_FORMAT_R32G32B32A32_FLOAT
{ GUID_WICPixelFormat128bppRGBFixedPoint, GUID_WICPixelFormat128bppRGBAFloat }, // DXGI_FORMAT_R32G32B32A32_FLOAT
{ GUID_WICPixelFormat32bppRGBE, GUID_WICPixelFormat128bppRGBAFloat }, // DXGI_FORMAT_R32G32B32A32_FLOAT
{ GUID_WICPixelFormat32bppCMYK, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat64bppCMYK, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat40bppCMYKAlpha, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat80bppCMYKAlpha, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat32bppRGB, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat64bppRGB, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat64bppPRGBAHalf, GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
};
bool GetTargetPixelFormat(const GUID* pSourceFormat, GUID* pTargetFormat)
{//查表确定兼容的最接近格式是哪个
*pTargetFormat = *pSourceFormat;
for (size_t i = 0; i < _countof(g_WICConvert); ++i)
{
if (InlineIsEqualGUID(g_WICConvert[i].source, *pSourceFormat))
{
*pTargetFormat = g_WICConvert[i].target;
return true;
}
}
return false;
}
DXGI_FORMAT GetDXGIFormatFromPixelFormat(const GUID* pPixelFormat)
{//查表确定最终对应的DXGI格式是哪一个
for (size_t i = 0; i < _countof(g_WICFormats); ++i)
{
if (InlineIsEqualGUID(g_WICFormats[i].wic, *pPixelFormat))
{
return g_WICFormats[i].format;
}
}
return DXGI_FORMAT_UNKNOWN;
}
这段代码本身很好理解,其实它就是做了两个匹配,一个是WIC格式自身之间的匹配,另一个就是WIC和DXGI图片格式之间的一个匹配。这里需要注意的就是WIC中图片格式也被定义为一个UUID值,实际就是一个128位的随机整数值,所以比较的时候需要用到UUID的工具函数。这个没什么神奇的地方。转换的规则在表中已经显示的很清楚了,大家通过读表中数据和格式名就理解了。
接着就是使用纯正COM调用的形式从文件载入图片数据,然后检查并转换格式,同时获取一些图片信息,比如:DXGI格式、长、宽、像素位宽等。代码如下:
// 16、使用WIC创建并加载一个2D纹理
//使用纯COM方式创建WIC类厂对象,也是调用WIC第一步要做的事情
GRS_THROW_IF_FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pIWICFactory)));
//使用WIC类厂对象接口加载纹理图片,并得到一个WIC解码器对象接口,图片信息就在这个接口代表的对象中了
WCHAR* pszTexcuteFileName = _T("D:\\Projects_2018_08\\D3D12 Tutorials\\2-D3D12WICTexture\\Texture\\bear.jpg");
GRS_THROW_IF_FAILED(pIWICFactory->CreateDecoderFromFilename(
pszTexcuteFileName, // 文件名
NULL, // 不指定解码器,使用默认
GENERIC_READ, // 访问权限
WICDecodeMetadataCacheOnDemand, // 若需要就缓冲数据
&pIWICDecoder // 解码器对象
));
// 获取第一帧图片(因为GIF等格式文件可能会有多帧图片,其他的格式一般只有一帧图片)
// 实际解析出来的往往是位图格式数据
GRS_THROW_IF_FAILED(pIWICDecoder->GetFrame(0, &pIWICFrame));
WICPixelFormatGUID wpf = {};
//获取WIC图片格式
GRS_THROW_IF_FAILED(pIWICFrame->GetPixelFormat(&wpf));
GUID tgFormat = {};
//通过第一道转换之后获取DXGI的等价格式
if ( GetTargetPixelFormat(&wpf, &tgFormat) )
{
stTextureFormat = GetDXGIFormatFromPixelFormat(&tgFormat);
}
if (DXGI_FORMAT_UNKNOWN == stTextureFormat)
{// 不支持的图片格式 目前退出了事
// 一般 在实际的引擎当中都会提供纹理格式转换工具,
// 图片都需要提前转换好,所以不会出现不支持的现象
throw CGRSCOMException(S_FALSE);
}
// 定义一个位图格式的图片数据对象接口
ComPtrpIBMP;
if ( !InlineIsEqualGUID(wpf, tgFormat) )
{// 这个判断很重要,如果原WIC格式不是直接能转换为DXGI格式的图片时
// 我们需要做的就是转换图片格式为能够直接对应DXGI格式的形式
//创建图片格式转换器
ComPtr pIConverter;
GRS_THROW_IF_FAILED(pIWICFactory->CreateFormatConverter(&pIConverter));
//初始化一个图片转换器,实际也就是将图片数据进行了格式转换
GRS_THROW_IF_FAILED(pIConverter->Initialize(
pIWICFrame.Get(), // 输入原图片数据
tgFormat, // 指定待转换的目标格式
WICBitmapDitherTypeNone, // 指定位图是否有调色板,现代都是真彩位图,不用调色板,所以为None
NULL, // 指定调色板指针
0.f, // 指定Alpha阀值
WICBitmapPaletteTypeCustom // 调色板类型,实际没有使用,所以指定为Custom
));
// 调用QueryInterface方法获得对象的位图数据源接口
GRS_THROW_IF_FAILED(pIConverter.As(&pIBMP));
}
else
{
//图片数据格式不需要转换,直接获取其位图数据源接口
GRS_THROW_IF_FAILED(pIWICFrame.As(&pIBMP));
}
//获得图片大小(单位:像素)
GRS_THROW_IF_FAILED(pIBMP->GetSize(&nTextureW, &nTextureH));
//获取图片像素的位大小的BPP(Bits Per Pixel)信息,用以计算图片行数据的真实大小(单位:字节)
ComPtr pIWICmntinfo;
GRS_THROW_IF_FAILED(pIWICFactory->CreateComponentInfo(tgFormat, pIWICmntinfo.GetAddressOf()));
WICComponentType type;
GRS_THROW_IF_FAILED(pIWICmntinfo->GetComponentType(&type));
if (type != WICPixelFormat)
{
throw CGRSCOMException(S_FALSE);
}
ComPtr pIWICPixelinfo;
GRS_THROW_IF_FAILED(pIWICmntinfo.As(&pIWICPixelinfo));
// 到这里终于可以得到BPP了,这也是我看的比较吐血的地方,为了BPP居然饶了这么多环节
GRS_THROW_IF_FAILED(pIWICPixelinfo->GetBitsPerPixel(&nBPP));
// 计算图片实际的行大小(单位:字节),这里使用了一个上取整除法即(A+B-1)/B ,
// 这曾经被传说是微软的面试题,希望你已经对它了如指掌
UINT nPicRowPitch = (uint64_t(nTextureW) * uint64_t(nBPP) + 7u) / 8u;
上面代码中注释已经比较清楚,关键点就在我们获取了图片的原始格式并获得等价格式之后,有一个判断,如果二者不同,那么我们就需要创建一个转换器,然后将原始格式的图片转换为兼容DXGI格式的图片,当然这就是让我困惑了好久的问题。因为开初我简单的以为我获得了DXGI格式的信息,就可以使用CopyPixel方法将数据读取出来,并复制到上传堆中,然而我却被那两个工具函数给简单的蒙蔽了,其实这两个工具函数只是告诉我们图片应该可以转换为哪种兼容DXGI格式,但并没有转换图片数据本身,因此我直接复制出来的图片数据依然是原始格式的,直接复制它最终渲染就会显示失真。因此当发现原始的WIC格式和目标WIC格式不同时,我们需要的就是利用WIC的图片格式转换功能转换图片格式到目标格式,然后再CopyPixel获得图片数据最终复制到上传堆中。
拿到了已经转换为DXGI兼容格式的图片数据之后,按照在根签名介绍中的描述,我们接着要做的就是将数据传输到显存中作为渲染管线的纹理数据。根据我们前面描述的根签名的理解,那么这时我们首先需要做的就是将数据传输到GPU可以访问的存储(内存或显存)中。而这个过程在之前的D3D接口中就是创建一个Resource对象接口,接着Map之后,再memcpy图片数据进Resource中即可,或者可以直接在创建对应的资源时指定初始化的数据。而在D3D12中这个工作有了本质上的不同。
在我之前的系列文章中已经提过,D3D12中的一个重要概念就是引入了完全的显存管理机制。明确的标志就是引入了“资源堆”的概念。当然这并不是说之前的D3D接口中就没有显存管理的概念了,而是说之前的接口中关于显存管理其实都封装在创建Resource接口方法的背后了,比如传统的D3D9中我们就使用创建资源时的标志来指定使用的是默认缓冲、动态缓冲、或者回写缓冲等,对应的文档中也只是简单的说默认缓冲对GPU有最高的访问性能,动态缓冲是CPU只写GPU只读权限,性能略差,而回写缓冲是CPU只读GPU只写,性能更差,主要用于流输出等。而在D3D12中,这些缓冲的概念都被“堆”缓冲的概念替换了,同时D3D12中暴露了更底层的控制方法。
首先在D3D中,GPU渲染需要的数据资源被分为两大类:一类是数据缓冲,另一类是纹理。对于数据缓冲来说,实际就是一维的某种类型数据的数组,比如:顶点缓冲、索引缓冲、常量缓冲等;而纹理则稍微复杂一些,从数据索引维度上来说可以分为1D纹理、2D纹理、和3D纹理,但其实本质上他们在存储中也是按照线性方式按行来存储的,就像C/C++中的多维数组一样,本质上也是线性存储,只是我们可以用多个索引来访问。从功能上来说纹理又分为普通纹理、渲染目标、以及深度缓冲和蜡板缓冲等,其中渲染目标的纹理存储一般是由DXGI的交换链来分配的,这只是说我们渲染的图形是为了最终在显示器上显示才这样做的,也只是个默认的做法,实际在一些高级的渲染场合渲染目标可能就是我们提前准备好的一个纹理而已,也就是常说的渲染到纹理的方法,这个将在后续的教程中再详细介绍。这里这样讲的目的是不想让大家在学习的过程中思路被禁锢,总以为渲染目标就只能从交换链来创建。
在之前的D3D版本中,这些资源都是统一以不同的对象接口来表达的,比如:ID3D11Texture2D、ID3D11Buffer等,但他们基本都是从ID3D11Resource这个统一的接口派生的,也就是说D3D12之前的版本中区分不同的资源使用了相对复杂的接口派生机制。但这种派生除了有概念表达上的区分意义之外,实际上也并没有更多实质性的意义。同时因为不同类型的资源使用了不同的接口来表达,这必然在代码编写上带来很多额外的问题,虽然我们可以使用基类型接口的指针来统一管理它们,但在具体使用时因为派生接口又会有不同的方法,而使得动态接口类型转换成为了一项非常具有挑战和风险的工作。或者更直白的说其实这种接口派生方式的封装都是一种很无聊的“过度设计”。或者更直白的说这种设计使得D3D接口中过多的考虑了“引擎”应该考虑和封装的事情。
所谓过犹不及,所以在D3D12中,就取消了这些通过复杂的接口派生类型来区分不同类型资源的接口设计,所有的资源统一使用一个接口ID3D12Resource来表达,而区分每种不同的Resource就是从创建它们的描述结构以及对应的创建函数,还有就是可以直接通过获取资源的描述信息来获知它们具体的类型。这样对于具体类型资源的封装设计就完全的变成了引擎设计或其它使用的他们的程序设计需要考虑的事情了,这样才真正体现了D3D12接口“低级”的真正含义。
另外更重要的就是在D3D12中加入了堆(Heap)的概念来表达Resource具体放在那里。也就是说要在D3D12中创建资源,一般就需要先创建堆,并明确在创建具体资源时指定放在哪个堆上。
总的来说,在D3D12中创建资源的方式主要有三种:
该方式是通过调用ID3D12Device::CreateCommittedResource方法来创建资源。这个方法其实主要还是为了兼容旧的存储管理方式而添加的,D3D12的文档中也不推荐使用这个方法。
使用它时,被创建的资源是被放在系统默认创建的堆上的,这种堆在代码中是看不到的,因此也被称为隐式堆,所以对其存储所能做的控制就比较局限了。当然调用时只需要通过堆属性参数来指定资源被放到那个默认堆上即可。因此调用它就不用额外自己去创建堆了。
也因为这个方法的这个特性,所以它具有调用上的方便性,很多简单的例子代码中都是使用这个方法来创建资源。我们当前的例子中也暂时使用这个方法,主要是为了示例代码的简洁性,以及方便大家阅读和理解其他的一些示例代码。
后续的教程中我们会专门讨论其他两种高级的方法。目前我们主要还是停留在概念建立阶段,所以还不宜太“天马行空”的灌输过多的内容,所以先掌握这个简洁的方法,并能够快速动手自己做一些小例子才是正确的学习方法。
定位方式就是D3D12中新增的比较正统和创建资源方式了。要创建定位方式的资源,就要首先显示的调用ID3D12Device::CreateHeap来创建一个至少能容纳资源大小的堆,实质目前应该理解为申请了一块GPU可访问的存储(显存或共享内存)。然后再在此堆上调用ID3D12Device::CreatePlacedResource方法具体来创建资源。
如果你理解之前的提交方法创建资源的话,看到这里一定会有一个疑问,使用系统默认堆不是挺好?干嘛要自己费力创建个额外的堆,再来创建资源?其实这里的繁琐手续,要理解的话就需要你了解内存池的概念了。在一般的C/C++系统中,为了管理大量尺寸差不多且频繁分配和释放的对象时,我们往往采取的策略就是预先分配一大块内存,然后在其上“分配”和“释放”对象,这里的分配和释放其实就是一个指针的转换和标记下某块内存为空闲状态而已,其内部根本没有复杂耗时的内存分配和释放的真实底层调用。而换来的是性能上的极大提升。其核心理念就是“重用”内存。那么在D3D12中自己创建堆的目的跟这个内存池的目的非常类似,也就是要能够重用,即可以在这个堆上重复创建不同的资源,并且自己控制堆的生命期,从而提高性能。而使用默认堆,或者之前的D3D接口是无法做到这一点的,因为隐式堆在资源释放时就自动释放了。
更进一步理解,在D3D中分配或释放资源需要的往往是在显存或者共享内存式的显存上,对它的管理比一般的纯CPU的内存管理要复杂的多,因为这个存储的管理需要协调CPU和GPU,其额外耗费的存储管理调用成本是比一般的内存管理还要高的,或者直白的说它的性能耗费是很大的。所以在D3D12中,就干脆把这块管理工作的接口都暴露出来,让程序自己来管理,通过类似内存池的方式,从根本上提高性能。这也是D3D12较之前D3D接口核心改进扩展的主要方面之一。
那么关于定位式资源创建方法暂时理解到这里就可以了,本例教程中我们先不介绍。后续的教程中我会专门讲解它,那时大家再深入的学习和掌握它,毕竟学习是一个需要不断重复的过程。
保留方式创建作为更高级的方法,就需要你对虚拟内存管理有所了解。说白了保留方式创建就是显存的虚拟管理。这个方式就很类似Windows系统中的VirtualAllloc系列函数族所提供的功能了。也就是说在分配时我们并不是直接保留显存或共享内存,而只是保留虚拟的地址空间,在需要的时候再一段段的真实分配显存或虚拟内存。
那么为什么需要这样的能力呢?其实现代的3D场景渲染中,随着显示分辨率的不断提高,以及画质细腻度的提高,通常纹理资源的尺寸和分辨率都是非常大的。甚至在使用D3D12之前的一些引擎中,为了显存管理和利用的高效性,都会要求将很多很小的纹理拼装成一个巨大的纹理,然后一次加载,供多个不同的渲染对象来使用。这是一种典型的空间换时间的性能优化的方法。
如果再加上为纹理设置不同的Mip等级,那么纹理的尺寸都是非常巨大的。同时它就会占用非常大的显存空间,所以现代显卡的一个重要特性就是都配置了动辄十几个G的显存,同时还会去共享一些系统中富裕的内存作为显存使用。但是虽然资源存储的大小问题解决了,但是实质上,这些巨大的纹理,并不一定在每帧场景中都被用到。比如不同人物角色的不同皮肤常常被拼装在一个巨大的纹理中,但实质在一个场景中可能只会显示一个角色的一套皮肤而已,过多的存储实质上都是被浪费了。但为了性能,我们又不能总是按需来加载不同的皮肤,那样额外的显存分配释放管理的成本就会造成性能上的严重下降。(之前文章中讲过的Draw Call性能问题的解决方案跟这个很类似,也是积攒很多小的网格对象的Draw Call之后拼装成多个实例的较大网格数据之后再一次调用Draw Call进行绘制。我很佩服想到这些性能优化方法的游戏程序员,为了性能真的很拼!)
那么有没有折中的方法来轻松达到即可以一次保留大的存储以提高性能,又可以按需提交来节约显存呢?这看似鱼和熊掌的问题,在D3D12中就通过资源的保留创建方式优雅高效的解决了。
或者直白的说,比如我们现在要加载一个1G大小的纹理,普通的方法就是我们要真实的分配和占用1G的显存或虚拟内存。如果再加上传递数据的中间缓冲,那么可能需要占用至少2G的存储。而使用保留方式我们就只需要先保留1G的地址空间,然后再按照场景渲染时的需要,分段来为某段地址空间分配真实的显存或共享内存,比如只分配其中某段256M的数据,这样真实占用的存储就只有256M了。这样在不断的渲染过程中,就会不断的为还没有真实分配存储的地址空间分配存储,直到所有的资源都按需加载进存储。当然如果某段地址空间中的资源在整个过程中都没有用到,那么就不会分配真实的存储,也就不会造成浪费。同时因为堆管理被独立了出来,那么这个保留方式的堆,也可以被反复重用。这样我们就做到了空间和性能优化上双赢的结果。
当然现代的硬件其地址空间是非常巨大的(CPU上是64bits的地址空间,即2^64这么多),保留地址空间本身,不会造成多大的浪费,就好像我们预留手机号一样,我们可以一次预留比如1000个号码,而实际上并不需要真实的购买1000台手机。
保留方式创建资源,我们也将放在后续的教程中来讲解,本讲中不在详解了。这里要求能够理解其基本原理及思路即可。
在D3D12中,因为CPU和GPU访问同一块存储的方式不同,以及堆具体所在存储位置的不同,比如堆可以在显存中也可以在二者都可以访问的共享内存中,所以D3D12的堆还被细分为四种基本类型:1、默认堆;2、上传堆;3、回读堆;4、自定义堆。D3D12中使用一个枚举值来标识和区分这些类型:
typedef
enum D3D12_HEAP_TYPE
{
D3D12_HEAP_TYPE_DEFAULT = 1,
D3D12_HEAP_TYPE_UPLOAD = 2,
D3D12_HEAP_TYPE_READBACK = 3,
D3D12_HEAP_TYPE_CUSTOM = 4
} D3D12_HEAP_TYPE;
其中默认堆就对应之前D3D中的创建缓冲时指定D3Dxx_USAGE参数为Default时的情形,直白的说就是这块缓冲只是供GPU访问的,CPU不做任何访问。通常它就驻留在显存中。因此默认堆中的数据是CPU无法直接访问的,因此向它直接传输数据就成为不可能的事情。也就是说它是只面向GPU的数据,因此从GPU的视角来看的话这就是自热而然的事情,故名默认堆。这样它就具备了GPU完全独占访问的权限,所以GPU在访问它时有最高的性能。通常我们将一些不易变的数据比如纹理之类的都放在这类堆中。
当然由于GPU自身不可能加载数据,所以怎样向默认堆中传输数据呢?这就要用到上传堆来做中介了。因此对于上传堆,顾名思义主要就是用来向默认堆上传数据用的。上传堆对于CPU来说是只写的访问权限,而对于GPU来说是只读的访问权限,因为CPU和GPU都要访问它,所以一般它都会被放在二者都能访问的共享内存中,所以对于CPU和GPU来说都不是独占访问的,因此GPU访问上传堆中的数据是有性能损失的。所以通常对于它里面的一些不易变的数据,我们就使用GPU上的复制引擎(回忆上一篇教程中关于现代显示适配器描述的内容)将数据复制到默认堆里去。但是对于一些几乎每个渲染周期或每帧都会变动的数据,比如:世界变换矩阵、动画矩阵调色板等,我们通常就直接放在上传堆里面了,此时如果每帧都在堆之间复制它们反而会损失性能(具体原因在资源屏障节中讲解)。
因为上传堆是映射在CPU和GUP都能访问的共享内存中,因此使用CPU向它里面复制数据就相对简单的多,也就是使用我们传统的Map、memcpy、Unmap大法(memcpy大法好!)。又因为堆的生命周期现在是由我们的程序完全控制的,所以对于一些经常要我们Map-memcpy-Unmap复制的数据,比如:每帧都变化的世界变换矩阵等,在D3D12程序中就干脆在一开始就Map一次,之后在每次反复memcpy新数据即可,程序退出前再调用Unmap并销毁堆即可。这样也可以提高不少的性能。在这里再里强调一次,D3D12接口相较于之前的D3D接口最核心的改进就是为了提高性能!
由此可以看出实质上要将数据从内存中彻底传递到默认堆中,至少需要两个Copy操作,一次在从内存到上传堆的Map-memcpy-Unmap中,一次在从上传堆到默认堆中。第一次Copy由CPU完成,而第二次Copy动作则由GPU上的复制引擎完成。并且根据上一讲的内容,第二个Copy动作就需要用到命令列表和命令队列了。
第三种回读堆则是用于GPU写入然后CPU读取类型数据的。通常用于多趟渲染中的流输出数据等,当然有时也用于读取离屏渲染画面数据的时候。从其用途就可以知道对于CPU来说这种堆中的数据是只读的,而对于GPU来说通常是只写的,并且往往需要向GPU标识其为UAV(Unorder Access View 无序访问视图)形式的数据。当然也因为CPU和GPU都要访问这块数据,所以它也是驻留在共享内存中的。所以GPU访问这种类型的堆数据时性能也是有所损失的。
最后一种自定义堆类型就为我们提供了自由组合CPU和GPU访问方式的可能,同时我们还可以指定它在共享内存中,还是在显存中。因此它可以实现更多更丰富的组合访问形式。未来的教程中我们看情况会不会用到,如果用到我们在详细讲解。如果用不到讲不到大家也不用着急,因为前三种堆基本上就可以解决80%-90%的问题了。
如果看懂了上一小节说的从上传堆复制数据到默认堆的原理以及原因之后,那么让我们再来思考一个问题:图形命令引擎如何知道复制引擎已经将数据从上传堆复制进了默认堆中呢?具体的比如我们渲染需要用到一副纹理,尺寸可能有些大,复制过程需要耗费一些时间,但此时可能图形命令引擎已经开始执行Draw Call命令了,之后在使用纹理时,数据还没有复制完怎么办?这也就是我在之前系列文章中说到的“脏读”问题。
这时我们就要用到D3D12中的资源屏障这个同步对象了。它的基本设计思路就是针对每种资源的不同访问状态的转换来实现的。具体的比如在我们一开始创建一个默认堆上的资源时我们指定其访问权限为可作为复制目标(D3D12_RESOURCE_STATE_COPY_DEST),此时图形命令引擎中的命令就不能访问这块资源数据,或者说它会进入一个“等待”状态,等待其有权限访问。而复制引擎看到这个访问权限标志时就可以直接写入数据,这里再次说明因为复制引擎本质上也是在GPU中的,所以它访问上传堆和默认堆都是没有问题。最终我们发现虽然理论上复制引擎和图形引擎是独立的,并且可以完全并行运行,但是在真正需要协作的时候,就需要二者有一定的串行管线,即复制引擎工作完成后图形引擎才能继续执行。这也就是之前说对于一些经常需要变动的数据我们就不再强制放到默认堆里去的原因,主要就是为了避免这个复制动作造成强制的串行执行关系,而导致性能上的损失。(要深度理解这个性能损失的原因,可能需要你对并行计算的一些理论有一些更深入的知识,比如说阿姆达尔定理等的相关知识。)
那么在复制引擎复制完成时,我们就放置一个资源屏障的权限转换同步对象,要求将权限变为图形引擎可以访问的权限标志,在本例中我们指定的是D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,也就是Pixel Shader程序可以访问的标志。具体示例代码如下:
//向命令队列发出从上传堆复制纹理数据到默认堆的命令
CD3DX12_TEXTURE_COPY_LOCATION Dst(pITexcute.Get(), 0);
CD3DX12_TEXTURE_COPY_LOCATION Src(pITextureUpload.Get(), stTxtLayouts);
pICommandList->CopyTextureRegion(&Dst, 0, 0, 0, &Src, nullptr);
//设置一个资源屏障,同步并确认复制操作完成
//直接使用结构体然后调用的形式
D3D12_RESOURCE_BARRIER stResBar = {};
stResBar.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
stResBar.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
stResBar.Transition.pResource = pITexcute.Get();
stResBar.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST;
stResBar.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
stResBar.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
pICommandList->ResourceBarrier(1, &stResBar);
上面代码中pICommandList接口是一个代表直接命令列表对象的接口,所以它能执行所有引擎可以执行的命令。我们可以看到,我们发出了一个复制命令,然后设置了一个资源屏障,并且指明了权限转换的先后状态标志。
这里要注意理解的一个概念就是,虽然命令列表中只是记录了命令,最终只能到命令对象上才能真正执行,并且命令队列最终执行命令与CPU执行是异步的,但是命令列表最终在GPU各引擎上的执行顺序依然是串行顺序的,所以ResourceBarrier命令真正执行完毕后,其实就表示前面的CopyTextureRegion命令已经完成了。这样ResourceBarrier命令就像一道屏障一样,完成了复制和使用之间的隔离,所以命名为资源屏障就实至名归了。
有了上面一大堆的理论基础的准备之后,假如你都明白了,那么接下来就让我们看看真实的代码中需要怎样的调用。
首先,我们需要创建一个提交方式的默认堆纹理资源:
D3D12_RESOURCE_DESC stTextureDesc = {};
stTextureDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
stTextureDesc.MipLevels = 1;
stTextureDesc.Format = stTextureFormat; //DXGI_FORMAT_R8G8B8A8_UNORM;
stTextureDesc.Width = nTextureW;
stTextureDesc.Height = nTextureH;
stTextureDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
stTextureDesc.DepthOrArraySize = 1;
stTextureDesc.SampleDesc.Count = 1;
stTextureDesc.SampleDesc.Quality = 0;
//创建默认堆上的资源,类型是Texture2D,GPU对默认堆资源的访问速度是最快的
//因为纹理资源一般是不易变的资源,所以我们通常使用上传堆复制到默认堆中
//在传统的D3D11及以前的D3D接口中,这些过程都被封装了,我们只能指定创建时的类型为默认堆
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT)
, D3D12_HEAP_FLAG_NONE
, &stTextureDesc //可以使用CD3DX12_RESOURCE_DESC::Tex2D来简化结构体的初始化
, D3D12_RESOURCE_STATE_COPY_DEST
, nullptr
, IID_PPV_ARGS(&pITexcute)));
//获取上传堆资源缓冲的大小,这个尺寸通常大于实际图片的尺寸
const UINT64 n64UploadBufferSize = GetRequiredIntermediateSize(pITexcute.Get(), 0, 1);
上面代码中的主体风格还是初始化结构体然后调用函数的形式。结构体D3D12_RESOURCE_DESC是描述资源类型信息的,如前所述,因为D3D12中已经不再使用派生接口方式来具体区分是纹理还是缓冲了,所以在这个结构体中,我们就要说清楚我们创建的是一个2D纹理,然后只有1个Mip等级,同时制定它的DXGI格式以及图片的宽和高等信息。Flags字段暂时设置为D3D12_RESOURCE_FLAG_NONE。因为我们的纹理也不使用MSAA,所以SampleDesc就要像这里这样指定,以表示关闭MSAA特性。DepthOrArraySize字段则在加载复杂的纹理数组时才用到,所以这里我们就指定1即可,表示一维纹理数组,实质也就是只有一副简单的图片的意思。
接着我们就调用CreateCommittedResource通过系统隐式堆的方式在默认堆上创建这个纹理。当然第一个参数我们依旧是使用了D3Dx12.h中的工具类做了简化处理。需要特别注意的就是我们在创建时就指定了D3D12_RESOURCE_STATE_COPY_DEST权限标志,这就表示之后的命令列表中的命令在访问时,只能是复制引擎的对应的复制命令才能访问它,并且把它作为复制目标。而如果你直接调用其它的3D图形命令来访问它,就有可能引起一个访问违例的异常。
上段代码的最后又使用一个D3Dx12.h中的工具方法GetRequiredIntermediateSize来获取了整个这个纹理资源的大小。其内部实质用到了一个重要的D3D12的方法GetCopyableFootprints。
这里需要补充说明的就是,在D3D12中或者说根据现代GPU访问存储的边界对齐要求,纹理的行大小必须是256字节边界对齐,而整个纹理的大小又必须是512字节大小边界对齐。比如在此例中使用了一副700*700像素大小的图片,每个像素有RGBA格式各8位共32位大小,也就是每像素32/8=4字节大小,如果直接计算行大小的话是700*4=2800字节大小,但如果要256字节边界对齐的话,就变成了(int)256*((700*4+(256-1))/256)=2816字节。
这里用到了一个号称是微软面试题的上取整算法:(A+B-1)/B的公式,比如:5/2=2.5直接取整就是2,这是下取整的结果,如果使用公式就变为(5+2-1)/2 = 3即上取整的结果。希望你看明白并牢记了这个公式,因为很多关于内存管理边界对齐的计算都需要用到这个公式。当然如果你敏而好学,一定想知道个为啥的话,那么想一下余数不能大于除数的定理,以及最小的非零余数为1,自己推导一下就明白了。
那么整个纹理数据边界对齐的大小就是:
(int)512 * ((2816 * 700 + (512-1))/512) = 1971200字节
注意:在调试中GetRequiredIntermediateSize返回的整个纹理尺寸大小为1971184字节与我这里的计算值差16字节,正在查找原因,后续我会跟评说明。也请知道原因的网友不吝赐教,先谢了!
默认堆创建好了,但是实际上里面什么也没有,如前所述我们还需要创建一个中介——上传堆来向默认堆上的纹理上传数据,示例代码如下:
// 创建用于上传纹理的资源,注意其类型是Buffer
// 上传堆对于GPU访问来说性能是很差的,
// 所以对于几乎不变的数据尤其像纹理都是
// 通过它来上传至GPU访问更高效的默认堆中
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(n64UploadBufferSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&pITextureUpload)));
上面的代码中依然使用了D3Dx12.h中的工具类,初始化了一个隐式上传堆属性,然后初始化了一个Buffer类型的资源描述结构体。并且我们为这个资源设置了GPU访问初始化状态为D3D12_RESOURCE_STATE_GENERIC_READ也就是只读属性。这样GPU上的任何类型的引擎其实都可以直接从这个资源中读取数据。
这里需要强调的一个问题是,对于上传堆来说,其类型都必须是Buffer类型。这是因为如前所述,上传堆中放置的实质上是CPU和GPU都能访问的资源,而对于CPU来说,它其实不像GPU那么细致的区分每种资源的类型,也就是说无论纹理还是其他类型数据,它都认为是一段缓冲数据而已。所以为了迁就CPU的“粗犷”,那么上传堆无论是要放数据还是纹理,我们就都创建为Buffer(缓冲)类型。同时也因为这种粗犷,我们只需要指定一个大型的属性给这个资源即可。因为这里我们要利用这个上传堆中的资源来向默认堆上的纹理资源传递数据,所以我们指定的大小必须要大于纹理本身的大小。在此例中我们使用了前面从默认堆上的纹理资源获取的大小尺寸来指定了缓冲的大小,这是因为这个大小必定是大于等于我们图片尺寸的大小的,因为它被要求是向上边界对齐的。
有了上传堆,那么我们就可以进行前面理论介绍部分的第一个Copy动作了,代码如下:
//按照资源缓冲大小来分配实际图片数据存储的内存大小
void* pbPicData = ::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, n64UploadBufferSize);
if (nullptr == pbPicData)
{
throw CGRSCOMException(HRESULT_FROM_WIN32(GetLastError()));
}
//从图片中读取出数据
GRS_THROW_IF_FAILED(pIBMP->CopyPixels(nullptr
, nPicRowPitch
, static_cast(nPicRowPitch * nTextureH) //注意这里才是图片数据真实的大小,这个值通常小于缓冲的大小
, reinterpret_cast(pbPicData)));
//获取向上传堆拷贝纹理数据的一些纹理转换尺寸信息
//对于复杂的DDS纹理这是非常必要的过程
UINT64 n64RequiredSize = 0u;
UINT nNumSubresources = 1u; //我们只有一副图片,即子资源个数为1
D3D12_PLACED_SUBRESOURCE_FOOTPRINT stTxtLayouts = {};
UINT64 n64TextureRowSizes = 0u;
UINT nTextureRowNum = 0u;
D3D12_RESOURCE_DESC stDestDesc = pITexcute->GetDesc();
pID3DDevice->GetCopyableFootprints(&stDestDesc
, 0
, nNumSubresources
, 0
, &stTxtLayouts
, &nTextureRowNum
, &n64TextureRowSizes
, &n64RequiredSize);
//因为上传堆实际就是CPU传递数据到GPU的中介
//所以我们可以使用熟悉的Map方法将它先映射到CPU内存地址中
//然后我们按行将数据复制到上传堆中
//需要注意的是之所以按行拷贝是因为GPU资源的行大小
//与实际图片的行大小是有差异的,二者的内存边界对齐要求是不一样的
BYTE* pData = nullptr;
GRS_THROW_IF_FAILED(pITextureUpload->Map(0, NULL, reinterpret_cast(&pData)));
BYTE* pDestSlice = reinterpret_cast(pData) + stTxtLayouts.Offset;
const BYTE* pSrcSlice = reinterpret_cast(pbPicData);
for (UINT y = 0; y < nTextureRowNum; ++y)
{
memcpy(pDestSlice + static_cast(stTxtLayouts.Footprint.RowPitch) * y
, pSrcSlice + static_cast(nPicRowPitch) * y
, nPicRowPitch );
}
//取消映射 对于易变的数据如每帧的变换矩阵等数据,可以撒懒不用Unmap了,
//让它常驻内存,以提高整体性能,因为每次Map和Unmap是很耗时的操作
//因为现在起码都是64位系统和应用了,地址空间是足够的,被长期占用不会影响什么
pITextureUpload->Unmap(0, NULL);
//释放图片数据,做一个干净的程序员
::HeapFree(::GetProcessHeap(), 0, pbPicData);
代码中的注释已经描述的比较清楚了。这里需要再次强调的就是因为我们上传的是一副纹理图片,它属于不易变的数据,所以我们复制完数据之后,就Unmap了事了。因为使用的是隐式堆,这个上传堆的重用性还不能显现出来,之后的教程示例中我们再做详细的介绍。
这里重点要大家掌握的就是那个按行memcpy图片数据的循环,注意上传堆中的行大小与实际图片数据中的行大小是有差异的,因此计算两个指针的行偏移时使用的是不同的行大小尺寸,而实际复制数据的大小就是真实图片的行大小。因为我们的图片使用的是简单的RGBA格式,所以复制可以按行进行,对于其他复杂格式的纹理数据的复制,就需要按照实际的数据情况区别对待了。
另外一个需要注意的地方就是我们再一次显式的调用了GetCopyableFootprints方法来得到资源中详细的尺寸信息。这个方法几乎是我们复制纹理时必须要调用的方法,主要用它来得到目标纹理数据的真实尺寸信息。因为目标纹理如我们前面所描述的主要都是存储在默认堆上的,而CPU是无法直接访问它的,所以我们就需要这个方法作为桥梁让我们获知最终存储在默认堆中的纹理的详细尺寸信息,以方便我们准备好上传堆中的数据。而上传堆因为都统一为了缓冲格式,被认为是一维存放的数据的,所以是没法获知这些详细的尺寸信息的。
纹理图片数据加载到上传堆之后,我们要做的的就是进行第二个Copy动作了,并且设置资源屏障,保证复制数据动作在GPU的复制引擎上完全执行结束。代码如下:
//向命令队列发出从上传堆复制纹理数据到默认堆的命令
CD3DX12_TEXTURE_COPY_LOCATION Dst(pITexcute.Get(), 0);
CD3DX12_TEXTURE_COPY_LOCATION Src(pITextureUpload.Get(), stTxtLayouts);
pICommandList->CopyTextureRegion(&Dst, 0, 0, 0, &Src, nullptr);
//设置一个资源屏障,同步并确认复制操作完成
//直接使用结构体然后调用的形式
D3D12_RESOURCE_BARRIER stResBar = {};
stResBar.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
stResBar.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
stResBar.Transition.pResource = pITexcute.Get();
stResBar.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST;
stResBar.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
stResBar.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
pICommandList->ResourceBarrier(1, &stResBar);
上面的代码我们在之前的理论介绍中已经介绍过了,需要额外补充说明的就是这里我们调用了CopyTextureRegion这个命令来复制纹理数据,实质上还有CopyBufferRegion、CopyResource、CopyTiles等复制引擎的复制命令。而最常用的就是CopyTextureRegion和CopyBufferRegion,前者主要用于纹理的复制,而后者如其名字所示主要用于缓冲的复制。这些方法在之后的教程中我们都会有更详细的介绍。目前了解本例中的用法即可。
之后我们就像下面这样先执行以下这个命令列表中的复制命令和资源屏障,做第一次同步,代码如下:
// 执行命令列表并等待纹理资源上传完成,这一步是必须的
GRS_THROW_IF_FAILED(pICommandList->Close());
ID3D12CommandList* ppCommandLists[] = { pICommandList.Get() };
pICommandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
//---------------------------------------------------------------------------------------------
// 17、创建一个同步对象——围栏,用于等待渲染完成,因为现在Draw Call是异步的了
GRS_THROW_IF_FAILED(pID3DDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&pIFence)));
n64FenceValue = 1;
//---------------------------------------------------------------------------------------------
// 18、创建一个Event同步对象,用于等待围栏事件通知
hFenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (hFenceEvent == nullptr)
{
GRS_THROW_IF_FAILED(HRESULT_FROM_WIN32(GetLastError()));
}
//---------------------------------------------------------------------------------------------
// 19、等待纹理资源正式复制完成先
const UINT64 fence = n64FenceValue;
GRS_THROW_IF_FAILED(pICommandQueue->Signal(pIFence.Get(), fence));
n64FenceValue++;
//---------------------------------------------------------------------------------------------
// 看命令有没有真正执行到围栏标记的这里,没有就利用事件去等待,注意使用的是命令队列对象的指针
if (pIFence->GetCompletedValue() < fence)
{
GRS_THROW_IF_FAILED(pIFence->SetEventOnCompletion(fence, hFenceEvent));
WaitForSingleObject(hFenceEvent, INFINITE);
}
上面的代码其实就是执行一个命令列表,然后使用围栏同步CPU和GPU的执行。到WaitForSingleObject返回时,我们就可以确定从上传堆复制纹理数据到默认堆的操作已经完全执行完了。也就是纹理数据已经可以使用了,并且已经变成了D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE访问权限,即我们的Pixel Shader程序中就可以访问这个纹理了。当然在D3D12中,几乎所有的Shader阶段中都可以访问纹理,这里只是一个示例,将纹理用于了Pixel Shader中而已。
按照之前根签名的描述,那么加载完资源之后,我们需要做的就是准备好资源描述符了。前一讲中我们以及简单介绍过资源描述符了。在这里我们在补充一些内容,因为我始终认为学习的过程就是一个不断重复加深的螺旋式上升的过程。
实质上按照本讲中的概念来说,我们可以将资源描述符理解为一个指向实际资源的一次指针,而资源描述符堆则可以理解为描述符指针的数组。这样我们就从代码的角度深入的理解了资源描述符的本质。
其实除了起到“指针”的作用,资源描述符还起到详细描述资源类型信息的作用,比如被描述的资源是一个纹理,还是一块纯缓冲数据,又或者资源是渲染目标还是深度缓冲等等。
在本例中,因为我们使用了一个纹理,所以就需要为这个纹理创建描述符和描述符堆,具体代码如下:
//10、创建SRV堆 (Shader Resource View Heap)
D3D12_DESCRIPTOR_HEAP_DESC stSRVHeapDesc = {};
stSRVHeapDesc.NumDescriptors = 1;
stSRVHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
stSRVHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
GRS_THROW_IF_FAILED(pID3DDevice->CreateDescriptorHeap(&stSRVHeapDesc, IID_PPV_ARGS(&pISRVHeap)));
//......
// 最终创建SRV描述符
D3D12_SHADER_RESOURCE_VIEW_DESC stSRVDesc = {};
stSRVDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
stSRVDesc.Format = stTextureDesc.Format;
stSRVDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
stSRVDesc.Texture2D.MipLevels = 1;
pID3DDevice->CreateShaderResourceView(pITexcute.Get(), &stSRVDesc, pISRVHeap->GetCPUDescriptorHandleForHeapStart());
这里我们创建的资源描述符堆使用的是D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV,表示这个堆上可以放置CBV、SRV或UAV。之所以资源描述符也需要这样的堆式创建,其目的依然很简单就是为了描述符堆的“重用”,我们可以简单的释放具体资源描述符,而不用释放描述符堆,通过重用描述符堆,从而提升性能。这与我们使用资源堆的目的相一致。同时也带来了与资源管理在编码框架上的一致性。
最后我们将所有代码粘贴如下,依然希望大家自己动手创建项目,复制代码,自己修改编译调试运行例子,以加深印象。
#include
#define WIN32_LEAN_AND_MEAN // 从 Windows 头中排除极少使用的资料
#include
#include
//添加WTL支持 方便使用COM
#include
using namespace Microsoft;
using namespace Microsoft::WRL;
#include
#include
using namespace DirectX;
//for d3d12
#include
#include
//linker
#pragma comment(lib, "dxguid.lib")
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3d12.lib")
#pragma comment(lib, "d3dcompiler.lib")
#if defined(_DEBUG)
#include
#endif
//for WIC
#include
#include "..\WindowsCommons\d3dx12.h"
#define GRS_WND_CLASS_NAME _T("Game Window Class")
#define GRS_WND_TITLE _T("DirectX12 Texture Sample")
#define GRS_THROW_IF_FAILED(hr) if (FAILED(hr)){ throw CGRSCOMException(hr); }
class CGRSCOMException
{
public:
CGRSCOMException(HRESULT hr) : m_hrError(hr)
{
}
HRESULT Error() const
{
return m_hrError;
}
private:
const HRESULT m_hrError;
};
struct WICTranslate
{
GUID wic;
DXGI_FORMAT format;
};
static WICTranslate g_WICFormats[] =
{//WIC格式与DXGI像素格式的对应表,该表中的格式为被支持的格式
{ GUID_WICPixelFormat128bppRGBAFloat, DXGI_FORMAT_R32G32B32A32_FLOAT },
{ GUID_WICPixelFormat64bppRGBAHalf, DXGI_FORMAT_R16G16B16A16_FLOAT },
{ GUID_WICPixelFormat64bppRGBA, DXGI_FORMAT_R16G16B16A16_UNORM },
{ GUID_WICPixelFormat32bppRGBA, DXGI_FORMAT_R8G8B8A8_UNORM },
{ GUID_WICPixelFormat32bppBGRA, DXGI_FORMAT_B8G8R8A8_UNORM }, // DXGI 1.1
{ GUID_WICPixelFormat32bppBGR, DXGI_FORMAT_B8G8R8X8_UNORM }, // DXGI 1.1
{ GUID_WICPixelFormat32bppRGBA1010102XR, DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM }, // DXGI 1.1
{ GUID_WICPixelFormat32bppRGBA1010102, DXGI_FORMAT_R10G10B10A2_UNORM },
{ GUID_WICPixelFormat16bppBGRA5551, DXGI_FORMAT_B5G5R5A1_UNORM },
{ GUID_WICPixelFormat16bppBGR565, DXGI_FORMAT_B5G6R5_UNORM },
{ GUID_WICPixelFormat32bppGrayFloat, DXGI_FORMAT_R32_FLOAT },
{ GUID_WICPixelFormat16bppGrayHalf, DXGI_FORMAT_R16_FLOAT },
{ GUID_WICPixelFormat16bppGray, DXGI_FORMAT_R16_UNORM },
{ GUID_WICPixelFormat8bppGray, DXGI_FORMAT_R8_UNORM },
{ GUID_WICPixelFormat8bppAlpha, DXGI_FORMAT_A8_UNORM },
};
// WIC 像素格式转换表.
struct WICConvert
{
GUID source;
GUID target;
};
static WICConvert g_WICConvert[] =
{
// 目标格式一定是最接近的被支持的格式
{ GUID_WICPixelFormatBlackWhite, GUID_WICPixelFormat8bppGray }, // DXGI_FORMAT_R8_UNORM
{ GUID_WICPixelFormat1bppIndexed, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat2bppIndexed, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat4bppIndexed, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat8bppIndexed, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat2bppGray, GUID_WICPixelFormat8bppGray }, // DXGI_FORMAT_R8_UNORM
{ GUID_WICPixelFormat4bppGray, GUID_WICPixelFormat8bppGray }, // DXGI_FORMAT_R8_UNORM
{ GUID_WICPixelFormat16bppGrayFixedPoint, GUID_WICPixelFormat16bppGrayHalf }, // DXGI_FORMAT_R16_FLOAT
{ GUID_WICPixelFormat32bppGrayFixedPoint, GUID_WICPixelFormat32bppGrayFloat }, // DXGI_FORMAT_R32_FLOAT
{ GUID_WICPixelFormat16bppBGR555, GUID_WICPixelFormat16bppBGRA5551 }, // DXGI_FORMAT_B5G5R5A1_UNORM
{ GUID_WICPixelFormat32bppBGR101010, GUID_WICPixelFormat32bppRGBA1010102 }, // DXGI_FORMAT_R10G10B10A2_UNORM
{ GUID_WICPixelFormat24bppBGR, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat24bppRGB, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat32bppPBGRA, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat32bppPRGBA, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat48bppRGB, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat48bppBGR, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat64bppBGRA, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat64bppPRGBA, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat64bppPBGRA, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat48bppRGBFixedPoint, GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
{ GUID_WICPixelFormat48bppBGRFixedPoint, GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
{ GUID_WICPixelFormat64bppRGBAFixedPoint, GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
{ GUID_WICPixelFormat64bppBGRAFixedPoint, GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
{ GUID_WICPixelFormat64bppRGBFixedPoint, GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
{ GUID_WICPixelFormat48bppRGBHalf, GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
{ GUID_WICPixelFormat64bppRGBHalf, GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
{ GUID_WICPixelFormat128bppPRGBAFloat, GUID_WICPixelFormat128bppRGBAFloat }, // DXGI_FORMAT_R32G32B32A32_FLOAT
{ GUID_WICPixelFormat128bppRGBFloat, GUID_WICPixelFormat128bppRGBAFloat }, // DXGI_FORMAT_R32G32B32A32_FLOAT
{ GUID_WICPixelFormat128bppRGBAFixedPoint, GUID_WICPixelFormat128bppRGBAFloat }, // DXGI_FORMAT_R32G32B32A32_FLOAT
{ GUID_WICPixelFormat128bppRGBFixedPoint, GUID_WICPixelFormat128bppRGBAFloat }, // DXGI_FORMAT_R32G32B32A32_FLOAT
{ GUID_WICPixelFormat32bppRGBE, GUID_WICPixelFormat128bppRGBAFloat }, // DXGI_FORMAT_R32G32B32A32_FLOAT
{ GUID_WICPixelFormat32bppCMYK, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat64bppCMYK, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat40bppCMYKAlpha, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat80bppCMYKAlpha, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat32bppRGB, GUID_WICPixelFormat32bppRGBA }, // DXGI_FORMAT_R8G8B8A8_UNORM
{ GUID_WICPixelFormat64bppRGB, GUID_WICPixelFormat64bppRGBA }, // DXGI_FORMAT_R16G16B16A16_UNORM
{ GUID_WICPixelFormat64bppPRGBAHalf, GUID_WICPixelFormat64bppRGBAHalf }, // DXGI_FORMAT_R16G16B16A16_FLOAT
};
bool GetTargetPixelFormat(const GUID* pSourceFormat, GUID* pTargetFormat)
{//查表确定兼容的最接近格式是哪个
*pTargetFormat = *pSourceFormat;
for (size_t i = 0; i < _countof(g_WICConvert); ++i)
{
if (InlineIsEqualGUID(g_WICConvert[i].source, *pSourceFormat))
{
*pTargetFormat = g_WICConvert[i].target;
return true;
}
}
return false;
}
DXGI_FORMAT GetDXGIFormatFromPixelFormat(const GUID* pPixelFormat)
{//查表确定最终对应的DXGI格式是哪一个
for (size_t i = 0; i < _countof(g_WICFormats); ++i)
{
if (InlineIsEqualGUID(g_WICFormats[i].wic, *pPixelFormat))
{
return g_WICFormats[i].format;
}
}
return DXGI_FORMAT_UNKNOWN;
}
struct GRS_VERTEX
{
XMFLOAT3 m_vPos; //Position
XMFLOAT2 m_vTxc; //Texcoord
};
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
{
::CoInitialize(nullptr); //for WIC & COM
const UINT nFrameBackBufCount = 3u;
int iWidth = 1024;
int iHeight = 768;
UINT nFrameIndex = 0;
UINT nFrame = 0;
UINT nDXGIFactoryFlags = 0U;
UINT nRTVDescriptorSize = 0U;
HWND hWnd = nullptr;
MSG msg = {};
float fAspectRatio = 3.0f;
D3D12_VERTEX_BUFFER_VIEW stVertexBufferView = {};
UINT64 n64FenceValue = 0ui64;
HANDLE hFenceEvent = nullptr;
UINT nTextureW = 0u;
UINT nTextureH = 0u;
UINT nBPP = 0u;
DXGI_FORMAT stTextureFormat = DXGI_FORMAT_UNKNOWN;
CD3DX12_VIEWPORT stViewPort(0.0f, 0.0f, static_cast(iWidth), static_cast(iHeight));
CD3DX12_RECT stScissorRect(0, 0, static_cast(iWidth), static_cast(iHeight));
ComPtr pIDXGIFactory5;
ComPtr pIAdapter;
ComPtr pID3DDevice;
ComPtr pICommandQueue;
ComPtr pISwapChain1;
ComPtr pISwapChain3;
ComPtr pIRTVHeap;
ComPtr pISRVHeap;
ComPtr pIARenderTargets[nFrameBackBufCount];
ComPtr pITexcute;
ComPtr pICommandAllocator;
ComPtr pIRootSignature;
ComPtr pIPipelineState;
ComPtr pICommandList;
ComPtr pIVertexBuffer;
ComPtr pIFence;
ComPtr pIWICFactory;
ComPtr pIWICDecoder;
ComPtr pIWICFrame;
try
{
//---------------------------------------------------------------------------------------------
// 1、创建窗口
WNDCLASSEX wcex = {};
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_GLOBALCLASS;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)GetStockObject(NULL_BRUSH); //防止无聊的背景重绘
wcex.lpszClassName = GRS_WND_CLASS_NAME;
RegisterClassEx(&wcex);
DWORD dwWndStyle = WS_OVERLAPPED | WS_SYSMENU;
RECT rtWnd = { 0, 0, iWidth, iHeight };
AdjustWindowRect(&rtWnd, dwWndStyle, FALSE);
hWnd = CreateWindowW(GRS_WND_CLASS_NAME
, GRS_WND_TITLE
, dwWndStyle
, CW_USEDEFAULT
, 0
, rtWnd.right - rtWnd.left
, rtWnd.bottom - rtWnd.top
, nullptr
, nullptr
, hInstance
, nullptr);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
//---------------------------------------------------------------------------------------------
#if defined(_DEBUG)
{//打开显示子系统的调试支持
ComPtr debugController;
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
{
debugController->EnableDebugLayer();
// 打开附加的调试支持
nDXGIFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG;
}
}
#endif
//---------------------------------------------------------------------------------------------
//2、创建DXGI Factory对象
GRS_THROW_IF_FAILED(CreateDXGIFactory2(nDXGIFactoryFlags, IID_PPV_ARGS(&pIDXGIFactory5)));
// 关闭ALT+ENTER键切换全屏的功能,因为我们没有实现OnSize处理,所以先关闭
GRS_THROW_IF_FAILED(pIDXGIFactory5->MakeWindowAssociation(hWnd, DXGI_MWA_NO_ALT_ENTER));
//---------------------------------------------------------------------------------------------
//3、枚举适配器,并选择合适的适配器来创建3D设备对象
for (UINT adapterIndex = 1; DXGI_ERROR_NOT_FOUND != pIDXGIFactory5->EnumAdapters1(adapterIndex, &pIAdapter); ++adapterIndex)
{
DXGI_ADAPTER_DESC1 desc = {};
pIAdapter->GetDesc1(&desc);
if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
{//跳过软件虚拟适配器设备
continue;
}
//检查适配器对D3D支持的兼容级别,这里直接要求支持12.1的能力,注意返回接口的那个参数被置为了nullptr,这样
//就不会实际创建一个设备了,也不用我们啰嗦的再调用release来释放接口。这也是一个重要的技巧,请记住!
if (SUCCEEDED(D3D12CreateDevice(pIAdapter.Get(), D3D_FEATURE_LEVEL_12_1, _uuidof(ID3D12Device), nullptr)))
{
break;
}
}
//---------------------------------------------------------------------------------------------
//4、创建D3D12.1的设备
GRS_THROW_IF_FAILED(D3D12CreateDevice(pIAdapter.Get(), D3D_FEATURE_LEVEL_12_1, IID_PPV_ARGS(&pID3DDevice)));
//---------------------------------------------------------------------------------------------
//5、创建直接命令队列
D3D12_COMMAND_QUEUE_DESC stQueueDesc = {};
stQueueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandQueue(&stQueueDesc, IID_PPV_ARGS(&pICommandQueue)));
//---------------------------------------------------------------------------------------------
//6、创建交换链
DXGI_SWAP_CHAIN_DESC1 stSwapChainDesc = {};
stSwapChainDesc.BufferCount = nFrameBackBufCount;
stSwapChainDesc.Width = iWidth;
stSwapChainDesc.Height = iHeight;
stSwapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
stSwapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
stSwapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
stSwapChainDesc.SampleDesc.Count = 1;
GRS_THROW_IF_FAILED(pIDXGIFactory5->CreateSwapChainForHwnd(
pICommandQueue.Get(), // Swap chain needs the queue so that it can force a flush on it.
hWnd,
&stSwapChainDesc,
nullptr,
nullptr,
&pISwapChain1
));
//---------------------------------------------------------------------------------------------
//7、得到当前后缓冲区的序号,也就是下一个将要呈送显示的缓冲区的序号
//注意此处使用了高版本的SwapChain接口的函数
GRS_THROW_IF_FAILED(pISwapChain1.As(&pISwapChain3));
nFrameIndex = pISwapChain3->GetCurrentBackBufferIndex();
//---------------------------------------------------------------------------------------------
//8、创建RTV(渲染目标视图)描述符堆(这里堆的含义应当理解为数组或者固定大小元素的固定大小显存池)
D3D12_DESCRIPTOR_HEAP_DESC stRTVHeapDesc = {};
stRTVHeapDesc.NumDescriptors = nFrameBackBufCount;
stRTVHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
stRTVHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
GRS_THROW_IF_FAILED(pID3DDevice->CreateDescriptorHeap(&stRTVHeapDesc, IID_PPV_ARGS(&pIRTVHeap)));
//得到每个描述符元素的大小
nRTVDescriptorSize = pID3DDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
//---------------------------------------------------------------------------------------------
//9、创建RTV的描述符
CD3DX12_CPU_DESCRIPTOR_HANDLE stRTVHandle(pIRTVHeap->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < nFrameBackBufCount; i++)
{
GRS_THROW_IF_FAILED(pISwapChain3->GetBuffer(i, IID_PPV_ARGS(&pIARenderTargets[i])));
pID3DDevice->CreateRenderTargetView(pIARenderTargets[i].Get(), nullptr, stRTVHandle);
stRTVHandle.Offset(1, nRTVDescriptorSize);
}
//---------------------------------------------------------------------------------------------
//10、创建SRV堆 (Shader Resource View Heap)
D3D12_DESCRIPTOR_HEAP_DESC stSRVHeapDesc = {};
stSRVHeapDesc.NumDescriptors = 1;
stSRVHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
stSRVHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
GRS_THROW_IF_FAILED(pID3DDevice->CreateDescriptorHeap(&stSRVHeapDesc, IID_PPV_ARGS(&pISRVHeap)));
//---------------------------------------------------------------------------------------------
//11、创建根描述符
D3D12_FEATURE_DATA_ROOT_SIGNATURE stFeatureData = {};
// 检测是否支持V1.1版本的根签名
stFeatureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_1;
if (FAILED(pID3DDevice->CheckFeatureSupport(D3D12_FEATURE_ROOT_SIGNATURE, &stFeatureData, sizeof(stFeatureData))))
{
stFeatureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_0;
}
// 在GPU上执行SetGraphicsRootDescriptorTable后,我们不修改命令列表中的SRV,因此我们可以使用默认Rang行为:
// D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC_WHILE_SET_AT_EXECUTE
CD3DX12_DESCRIPTOR_RANGE1 stDSPRanges[1];
stDSPRanges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC_WHILE_SET_AT_EXECUTE);
CD3DX12_ROOT_PARAMETER1 stRootParameters[1];
stRootParameters[0].InitAsDescriptorTable(1, &stDSPRanges[0], D3D12_SHADER_VISIBILITY_PIXEL);
D3D12_STATIC_SAMPLER_DESC stSamplerDesc = {};
stSamplerDesc.Filter = D3D12_FILTER_MIN_MAG_MIP_POINT;
stSamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
stSamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
stSamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
stSamplerDesc.MipLODBias = 0;
stSamplerDesc.MaxAnisotropy = 0;
stSamplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_NEVER;
stSamplerDesc.BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK;
stSamplerDesc.MinLOD = 0.0f;
stSamplerDesc.MaxLOD = D3D12_FLOAT32_MAX;
stSamplerDesc.ShaderRegister = 0;
stSamplerDesc.RegisterSpace = 0;
stSamplerDesc.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC stRootSignatureDesc;
stRootSignatureDesc.Init_1_1(_countof(stRootParameters), stRootParameters
, 1, &stSamplerDesc
, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
ComPtr pISignatureBlob;
ComPtr pIErrorBlob;
GRS_THROW_IF_FAILED(D3DX12SerializeVersionedRootSignature(&stRootSignatureDesc
, stFeatureData.HighestVersion
, &pISignatureBlob
, &pIErrorBlob));
GRS_THROW_IF_FAILED(pID3DDevice->CreateRootSignature(0
, pISignatureBlob->GetBufferPointer()
, pISignatureBlob->GetBufferSize()
, IID_PPV_ARGS(&pIRootSignature)));
//---------------------------------------------------------------------------------------------
// 12、编译Shader创建渲染管线状态对象
ComPtr pIBlobVertexShader;
ComPtr pIBlobPixelShader;
#if defined(_DEBUG)
// Enable better shader debugging with the graphics debugging tools.
UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#else
UINT compileFlags = 0;
#endif
TCHAR pszShaderFileName[] = _T("D:\\Projects_2018_08\\D3D12 Tutorials\\2-D3D12WICTexture\\Shader\\Texture.hlsl");
GRS_THROW_IF_FAILED(D3DCompileFromFile(pszShaderFileName, nullptr, nullptr
, "VSMain", "vs_5_0", compileFlags, 0, &pIBlobVertexShader, nullptr));
GRS_THROW_IF_FAILED(D3DCompileFromFile(pszShaderFileName, nullptr, nullptr
, "PSMain", "ps_5_0", compileFlags, 0, &pIBlobPixelShader, nullptr));
// Define the vertex input layout.
D3D12_INPUT_ELEMENT_DESC stInputElementDescs[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};
// 创建 graphics pipeline state object (PSO)对象
D3D12_GRAPHICS_PIPELINE_STATE_DESC stPSODesc = {};
stPSODesc.InputLayout = { stInputElementDescs, _countof(stInputElementDescs) };
stPSODesc.pRootSignature = pIRootSignature.Get();
stPSODesc.VS = CD3DX12_SHADER_BYTECODE(pIBlobVertexShader.Get());
stPSODesc.PS = CD3DX12_SHADER_BYTECODE(pIBlobPixelShader.Get());
stPSODesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
stPSODesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
stPSODesc.DepthStencilState.DepthEnable = FALSE;
stPSODesc.DepthStencilState.StencilEnable = FALSE;
stPSODesc.SampleMask = UINT_MAX;
stPSODesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
stPSODesc.NumRenderTargets = 1;
stPSODesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
stPSODesc.SampleDesc.Count = 1;
GRS_THROW_IF_FAILED(pID3DDevice->CreateGraphicsPipelineState(&stPSODesc
, IID_PPV_ARGS(&pIPipelineState)));
//---------------------------------------------------------------------------------------------
// 13、创建命令列表分配器
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT
, IID_PPV_ARGS(&pICommandAllocator)));
//---------------------------------------------------------------------------------------------
// 14、创建图形命令列表
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT
, pICommandAllocator.Get(), pIPipelineState.Get(), IID_PPV_ARGS(&pICommandList)));
//---------------------------------------------------------------------------------------------
// 15、创建顶点缓冲
// 定义正方形的3D数据结构
GRS_VERTEX stTriangleVertices[] =
{
{ { -0.25f* fAspectRatio, -0.25f * fAspectRatio, 0.0f}, { 0.0f, 1.0f } }, // Bottom left.
{ { -0.25f* fAspectRatio, 0.25f * fAspectRatio, 0.0f}, { 0.0f, 0.0f } }, // Top left.
{ { 0.25f* fAspectRatio, -0.25f * fAspectRatio, 0.0f }, { 1.0f, 1.0f } }, // Bottom right.
{ { 0.25f* fAspectRatio, 0.25f * fAspectRatio, 0.0f}, { 1.0f, 0.0f } }, // Top right.
};
const UINT nVertexBufferSize = sizeof(stTriangleVertices);
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(nVertexBufferSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&pIVertexBuffer)));
UINT8* pVertexDataBegin = nullptr;
CD3DX12_RANGE stReadRange(0, 0); // We do not intend to read from this resource on the CPU.
GRS_THROW_IF_FAILED(pIVertexBuffer->Map(0, &stReadRange, reinterpret_cast(&pVertexDataBegin)));
memcpy(pVertexDataBegin, stTriangleVertices, sizeof(stTriangleVertices));
pIVertexBuffer->Unmap(0, nullptr);
stVertexBufferView.BufferLocation = pIVertexBuffer->GetGPUVirtualAddress();
stVertexBufferView.StrideInBytes = sizeof(GRS_VERTEX);
stVertexBufferView.SizeInBytes = nVertexBufferSize;
//---------------------------------------------------------------------------------------------
// 16、使用WIC创建并加载一个2D纹理
//使用纯COM方式创建WIC类厂对象,也是调用WIC第一步要做的事情
GRS_THROW_IF_FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pIWICFactory)));
//使用WIC类厂对象接口加载纹理图片,并得到一个WIC解码器对象接口,图片信息就在这个接口代表的对象中了
WCHAR* pszTexcuteFileName = _T("D:\\Projects_2018_08\\D3D12 Tutorials\\2-D3D12WICTexture\\Texture\\bear.jpg");
GRS_THROW_IF_FAILED(pIWICFactory->CreateDecoderFromFilename(
pszTexcuteFileName, // 文件名
NULL, // 不指定解码器,使用默认
GENERIC_READ, // 访问权限
WICDecodeMetadataCacheOnDemand, // 若需要就缓冲数据
&pIWICDecoder // 解码器对象
));
// 获取第一帧图片(因为GIF等格式文件可能会有多帧图片,其他的格式一般只有一帧图片)
// 实际解析出来的往往是位图格式数据
GRS_THROW_IF_FAILED(pIWICDecoder->GetFrame(0, &pIWICFrame));
WICPixelFormatGUID wpf = {};
//获取WIC图片格式
GRS_THROW_IF_FAILED(pIWICFrame->GetPixelFormat(&wpf));
GUID tgFormat = {};
//通过第一道转换之后获取DXGI的等价格式
if ( GetTargetPixelFormat(&wpf, &tgFormat) )
{
stTextureFormat = GetDXGIFormatFromPixelFormat(&tgFormat);
}
if (DXGI_FORMAT_UNKNOWN == stTextureFormat)
{// 不支持的图片格式 目前退出了事
// 一般 在实际的引擎当中都会提供纹理格式转换工具,
// 图片都需要提前转换好,所以不会出现不支持的现象
throw CGRSCOMException(S_FALSE);
}
// 定义一个位图格式的图片数据对象接口
ComPtrpIBMP;
if ( !InlineIsEqualGUID(wpf, tgFormat) )
{// 这个判断很重要,如果原WIC格式不是直接能转换为DXGI格式的图片时
// 我们需要做的就是转换图片格式为能够直接对应DXGI格式的形式
//创建图片格式转换器
ComPtr pIConverter;
GRS_THROW_IF_FAILED(pIWICFactory->CreateFormatConverter(&pIConverter));
//初始化一个图片转换器,实际也就是将图片数据进行了格式转换
GRS_THROW_IF_FAILED(pIConverter->Initialize(
pIWICFrame.Get(), // 输入原图片数据
tgFormat, // 指定待转换的目标格式
WICBitmapDitherTypeNone, // 指定位图是否有调色板,现代都是真彩位图,不用调色板,所以为None
NULL, // 指定调色板指针
0.f, // 指定Alpha阀值
WICBitmapPaletteTypeCustom // 调色板类型,实际没有使用,所以指定为Custom
));
// 调用QueryInterface方法获得对象的位图数据源接口
GRS_THROW_IF_FAILED(pIConverter.As(&pIBMP));
}
else
{
//图片数据格式不需要转换,直接获取其位图数据源接口
GRS_THROW_IF_FAILED(pIWICFrame.As(&pIBMP));
}
//获得图片大小(单位:像素)
GRS_THROW_IF_FAILED(pIBMP->GetSize(&nTextureW, &nTextureH));
//获取图片像素的位大小的BPP(Bits Per Pixel)信息,用以计算图片行数据的真实大小(单位:字节)
ComPtr pIWICmntinfo;
GRS_THROW_IF_FAILED(pIWICFactory->CreateComponentInfo(tgFormat, pIWICmntinfo.GetAddressOf()));
WICComponentType type;
GRS_THROW_IF_FAILED(pIWICmntinfo->GetComponentType(&type));
if (type != WICPixelFormat)
{
throw CGRSCOMException(S_FALSE);
}
ComPtr pIWICPixelinfo;
GRS_THROW_IF_FAILED(pIWICmntinfo.As(&pIWICPixelinfo));
// 到这里终于可以得到BPP了,这也是我看的比较吐血的地方,为了BPP居然饶了这么多环节
GRS_THROW_IF_FAILED(pIWICPixelinfo->GetBitsPerPixel(&nBPP));
// 计算图片实际的行大小(单位:字节),这里使用了一个上取整除法即(A+B-1)/B ,
// 这曾经被传说是微软的面试题,希望你已经对它了如指掌
UINT nPicRowPitch = (uint64_t(nTextureW) * uint64_t(nBPP) + 7u) / 8u;
//---------------------------------------------------------------------------------------------
ComPtr pITextureUpload;
D3D12_RESOURCE_DESC stTextureDesc = {};
stTextureDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
stTextureDesc.MipLevels = 1;
stTextureDesc.Format = stTextureFormat; //DXGI_FORMAT_R8G8B8A8_UNORM;
stTextureDesc.Width = nTextureW;
stTextureDesc.Height = nTextureH;
stTextureDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
stTextureDesc.DepthOrArraySize = 1;
stTextureDesc.SampleDesc.Count = 1;
stTextureDesc.SampleDesc.Quality = 0;
//创建默认堆上的资源,类型是Texture2D,GPU对默认堆资源的访问速度是最快的
//因为纹理资源一般是不易变的资源,所以我们通常使用上传堆复制到默认堆中
//在传统的D3D11及以前的D3D接口中,这些过程都被封装了,我们只能指定创建时的类型为默认堆
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT)
, D3D12_HEAP_FLAG_NONE
, &stTextureDesc //可以使用CD3DX12_RESOURCE_DESC::Tex2D来简化结构体的初始化
, D3D12_RESOURCE_STATE_COPY_DEST
, nullptr
, IID_PPV_ARGS(&pITexcute)));
//获取上传堆资源缓冲的大小,这个尺寸通常大于实际图片的尺寸
const UINT64 n64UploadBufferSize = GetRequiredIntermediateSize(pITexcute.Get(), 0, 1);
// 创建用于上传纹理的资源,注意其类型是Buffer
// 上传堆对于GPU访问来说性能是很差的,
// 所以对于几乎不变的数据尤其像纹理都是
// 通过它来上传至GPU访问更高效的默认堆中
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(n64UploadBufferSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&pITextureUpload)));
//按照资源缓冲大小来分配实际图片数据存储的内存大小
void* pbPicData = ::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, n64UploadBufferSize);
if (nullptr == pbPicData)
{
throw CGRSCOMException(HRESULT_FROM_WIN32(GetLastError()));
}
//从图片中读取出数据
GRS_THROW_IF_FAILED(pIBMP->CopyPixels(nullptr
, nPicRowPitch
, static_cast(nPicRowPitch * nTextureH) //注意这里才是图片数据真实的大小,这个值通常小于缓冲的大小
, reinterpret_cast(pbPicData)));
//{//下面这段代码来自DX12的示例,直接通过填充缓冲绘制了一个黑白方格的纹理
// //还原这段代码,然后注释上面的CopyPixels调用可以看到黑白方格纹理的效果
// const UINT rowPitch = nPicRowPitch; //nTextureW * 4; //static_cast(n64UploadBufferSize / nTextureH);
// const UINT cellPitch = rowPitch >> 3; // The width of a cell in the checkboard texture.
// const UINT cellHeight = nTextureW >> 3; // The height of a cell in the checkerboard texture.
// const UINT textureSize = static_cast(n64UploadBufferSize);
// UINT nTexturePixelSize = static_cast(n64UploadBufferSize / nTextureH / nTextureW);
// UINT8* pData = reinterpret_cast(pbPicData);
// for (UINT n = 0; n < textureSize; n += nTexturePixelSize)
// {
// UINT x = n % rowPitch;
// UINT y = n / rowPitch;
// UINT i = x / cellPitch;
// UINT j = y / cellHeight;
// if (i % 2 == j % 2)
// {
// pData[n] = 0x00; // R
// pData[n + 1] = 0x00; // G
// pData[n + 2] = 0x00; // B
// pData[n + 3] = 0xff; // A
// }
// else
// {
// pData[n] = 0xff; // R
// pData[n + 1] = 0xff; // G
// pData[n + 2] = 0xff; // B
// pData[n + 3] = 0xff; // A
// }
// }
//}
//获取向上传堆拷贝纹理数据的一些纹理转换尺寸信息
//对于复杂的DDS纹理这是非常必要的过程
UINT64 n64RequiredSize = 0u;
UINT nNumSubresources = 1u; //我们只有一副图片,即子资源个数为1
D3D12_PLACED_SUBRESOURCE_FOOTPRINT stTxtLayouts = {};
UINT64 n64TextureRowSizes = 0u;
UINT nTextureRowNum = 0u;
D3D12_RESOURCE_DESC stDestDesc = pITexcute->GetDesc();
pID3DDevice->GetCopyableFootprints(&stDestDesc
, 0
, nNumSubresources
, 0
, &stTxtLayouts
, &nTextureRowNum
, &n64TextureRowSizes
, &n64RequiredSize);
//因为上传堆实际就是CPU传递数据到GPU的中介
//所以我们可以使用熟悉的Map方法将它先映射到CPU内存地址中
//然后我们按行将数据复制到上传堆中
//需要注意的是之所以按行拷贝是因为GPU资源的行大小
//与实际图片的行大小是有差异的,二者的内存边界对齐要求是不一样的
BYTE* pData = nullptr;
GRS_THROW_IF_FAILED(pITextureUpload->Map(0, NULL, reinterpret_cast(&pData)));
BYTE* pDestSlice = reinterpret_cast(pData) + stTxtLayouts.Offset;
const BYTE* pSrcSlice = reinterpret_cast(pbPicData);
for (UINT y = 0; y < nTextureRowNum; ++y)
{
memcpy(pDestSlice + static_cast(stTxtLayouts.Footprint.RowPitch) * y
, pSrcSlice + static_cast(nPicRowPitch) * y
, nPicRowPitch );
}
//取消映射 对于易变的数据如每帧的变换矩阵等数据,可以撒懒不用Unmap了,
//让它常驻内存,以提高整体性能,因为每次Map和Unmap是很耗时的操作
//因为现在起码都是64位系统和应用了,地址空间是足够的,被长期占用不会影响什么
pITextureUpload->Unmap(0, NULL);
//释放图片数据,做一个干净的程序员
::HeapFree(::GetProcessHeap(), 0, pbPicData);
//向命令队列发出从上传堆复制纹理数据到默认堆的命令
CD3DX12_TEXTURE_COPY_LOCATION Dst(pITexcute.Get(), 0);
CD3DX12_TEXTURE_COPY_LOCATION Src(pITextureUpload.Get(), stTxtLayouts);
pICommandList->CopyTextureRegion(&Dst, 0, 0, 0, &Src, nullptr);
//设置一个资源屏障,同步并确认复制操作完成
//直接使用结构体然后调用的形式
D3D12_RESOURCE_BARRIER stResBar = {};
stResBar.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
stResBar.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
stResBar.Transition.pResource = pITexcute.Get();
stResBar.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST;
stResBar.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
stResBar.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
pICommandList->ResourceBarrier(1, &stResBar );
//或者使用D3DX12库中的工具类调用的等价形式,下面的方式更简洁一些
//pICommandList->ResourceBarrier(1
// , &CD3DX12_RESOURCE_BARRIER::Transition(pITexcute.Get()
// , D3D12_RESOURCE_STATE_COPY_DEST
// , D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE)
//);
//---------------------------------------------------------------------------------------------
// 最终创建SRV描述符
D3D12_SHADER_RESOURCE_VIEW_DESC stSRVDesc = {};
stSRVDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
stSRVDesc.Format = stTextureDesc.Format;
stSRVDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
stSRVDesc.Texture2D.MipLevels = 1;
pID3DDevice->CreateShaderResourceView(pITexcute.Get(), &stSRVDesc, pISRVHeap->GetCPUDescriptorHandleForHeapStart());
//---------------------------------------------------------------------------------------------
// 执行命令列表并等待纹理资源上传完成,这一步是必须的
GRS_THROW_IF_FAILED(pICommandList->Close());
ID3D12CommandList* ppCommandLists[] = { pICommandList.Get() };
pICommandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
//---------------------------------------------------------------------------------------------
// 17、创建一个同步对象——围栏,用于等待渲染完成,因为现在Draw Call是异步的了
GRS_THROW_IF_FAILED(pID3DDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&pIFence)));
n64FenceValue = 1;
//---------------------------------------------------------------------------------------------
// 18、创建一个Event同步对象,用于等待围栏事件通知
hFenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (hFenceEvent == nullptr)
{
GRS_THROW_IF_FAILED(HRESULT_FROM_WIN32(GetLastError()));
}
//---------------------------------------------------------------------------------------------
// 19、等待纹理资源正式复制完成先
const UINT64 fence = n64FenceValue;
GRS_THROW_IF_FAILED(pICommandQueue->Signal(pIFence.Get(), fence));
n64FenceValue++;
//---------------------------------------------------------------------------------------------
// 看命令有没有真正执行到围栏标记的这里,没有就利用事件去等待,注意使用的是命令队列对象的指针
if (pIFence->GetCompletedValue() < fence)
{
GRS_THROW_IF_FAILED(pIFence->SetEventOnCompletion(fence, hFenceEvent));
WaitForSingleObject(hFenceEvent, INFINITE);
}
//---------------------------------------------------------------------------------------------
//命令分配器先Reset一下,刚才已经执行过了一个复制纹理的命令
GRS_THROW_IF_FAILED(pICommandAllocator->Reset());
//Reset命令列表,并重新指定命令分配器和PSO对象
GRS_THROW_IF_FAILED(pICommandList->Reset(pICommandAllocator.Get(), pIPipelineState.Get()));
//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
//20、创建定时器对象,以便于创建高效的消息循环
HANDLE phWait = CreateWaitableTimer(NULL, FALSE, NULL);
LARGE_INTEGER liDueTime = {};
liDueTime.QuadPart = -1i64;//1秒后开始计时
SetWaitableTimer(phWait, &liDueTime, 1, NULL, NULL, 0);//40ms的周期
//---------------------------------------------------------------------------------------------
//21、开始消息循环,并在其中不断渲染
DWORD dwRet = 0;
BOOL bExit = FALSE;
while (!bExit)
{
dwRet = ::MsgWaitForMultipleObjects(1, &phWait, FALSE, INFINITE, QS_ALLINPUT);
switch (dwRet - WAIT_OBJECT_0)
{
case 0:
case WAIT_TIMEOUT:
{//计时器时间到
//GRS_TRACE(_T("开始第%u帧渲染{Frame Index = %u}:\n"),nFrame,nFrameIndex);
//开始记录命令
//---------------------------------------------------------------------------------------------
pICommandList->SetGraphicsRootSignature(pIRootSignature.Get());
ID3D12DescriptorHeap* ppHeaps[] = { pISRVHeap.Get() };
pICommandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
pICommandList->SetGraphicsRootDescriptorTable(0, pISRVHeap->GetGPUDescriptorHandleForHeapStart());
pICommandList->RSSetViewports(1, &stViewPort);
pICommandList->RSSetScissorRects(1, &stScissorRect);
//---------------------------------------------------------------------------------------------
// 通过资源屏障判定后缓冲已经切换完毕可以开始渲染了
pICommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(pIARenderTargets[nFrameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));
CD3DX12_CPU_DESCRIPTOR_HANDLE stRTVHandle(pIRTVHeap->GetCPUDescriptorHandleForHeapStart(), nFrameIndex, nRTVDescriptorSize);
//设置渲染目标
pICommandList->OMSetRenderTargets(1, &stRTVHandle, FALSE, nullptr);
// 继续记录命令,并真正开始新一帧的渲染
const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
pICommandList->ClearRenderTargetView(stRTVHandle, clearColor, 0, nullptr);
pICommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
pICommandList->IASetVertexBuffers(0, 1, &stVertexBufferView);
//---------------------------------------------------------------------------------------------
//Draw Call!!!
pICommandList->DrawInstanced(_countof(stTriangleVertices), 1, 0, 0);
//---------------------------------------------------------------------------------------------
//又一个资源屏障,用于确定渲染已经结束可以提交画面去显示了
pICommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(pIARenderTargets[nFrameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
//关闭命令列表,可以去执行了
GRS_THROW_IF_FAILED(pICommandList->Close());
//---------------------------------------------------------------------------------------------
//执行命令列表
ID3D12CommandList* ppCommandLists[] = { pICommandList.Get() };
pICommandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
//---------------------------------------------------------------------------------------------
//提交画面
GRS_THROW_IF_FAILED(pISwapChain3->Present(1, 0));
//---------------------------------------------------------------------------------------------
//开始同步GPU与CPU的执行,先记录围栏标记值
const UINT64 fence = n64FenceValue;
GRS_THROW_IF_FAILED(pICommandQueue->Signal(pIFence.Get(), fence));
n64FenceValue++;
//---------------------------------------------------------------------------------------------
// 看命令有没有真正执行到围栏标记的这里,没有就利用事件去等待,注意使用的是命令队列对象的指针
if (pIFence->GetCompletedValue() < fence)
{
GRS_THROW_IF_FAILED(pIFence->SetEventOnCompletion(fence, hFenceEvent));
WaitForSingleObject(hFenceEvent, INFINITE);
}
//执行到这里说明一个命令队列完整的执行完了,在这里就代表我们的一帧已经渲染完了,接着准备执行下一帧渲染
//---------------------------------------------------------------------------------------------
//获取新的后缓冲序号,因为Present真正完成时后缓冲的序号就更新了
nFrameIndex = pISwapChain3->GetCurrentBackBufferIndex();
//---------------------------------------------------------------------------------------------
//命令分配器先Reset一下
GRS_THROW_IF_FAILED(pICommandAllocator->Reset());
//Reset命令列表,并重新指定命令分配器和PSO对象
GRS_THROW_IF_FAILED(pICommandList->Reset(pICommandAllocator.Get(), pIPipelineState.Get()));
//GRS_TRACE(_T("第%u帧渲染结束.\n"), nFrame++);
}
break;
case 1:
{//处理消息
while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (WM_QUIT != msg.message)
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
else
{
bExit = TRUE;
}
}
}
break;
default:
break;
}
}
//::CoUninitialize();
}
catch (CGRSCOMException& e)
{//发生了COM异常
e;
}
return 0;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
Texture.hlsl
struct PSInput
{
float4 position : SV_POSITION;
float2 uv : TEXCOORD;
};
Texture2D g_texture : register(t0);
SamplerState g_sampler : register(s0);
PSInput VSMain(float4 position : POSITION, float2 uv : TEXCOORD)
{
PSInput result;
result.position = position;
result.uv = uv;
return result;
}
float4 PSMain(PSInput input) : SV_TARGET
{
return g_texture.Sample(g_sampler, input.uv);
}