Direct 3D 12编程指南

978-7-115-45025-8
作者: 张羽乔
译者:
编辑: 张爽

图书目录:

详情

本书介绍Direct 3D 12的各个方面知识,包括开始前的准备工作,如何创建DX 12项目,编程后的步骤,以及关于多线程、命令队列,资源结构,计算流水线等。目前比较前沿的应用例子有shadow map,各种post process,如HDR,景深等,书中都会有讲解。重点介绍特定于Direct 3D 12的知识,减少计算机图形学中通用知识的介绍,因为读者完全可以在其他的书中得到这些方面的知识。

图书摘要

版权信息

书名:Direct 3D 12编程指南

ISBN:978-7-115-45025-8

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 编  著 张羽乔    

  责任编辑 张 涛    

  责任编辑 张 爽

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


本书系统介绍了Direct3D 12各方面的知识,包括开始前的准备工作,如何创建DirectX 12项目,编程后的步骤,以及关于多线程、命令队列、资源结构、图形流水线、计算流水线和GPU内部传参等内容,最后讲解了一个基于Direct3D 12实现的字体引擎。本书重点介绍Direct3D 12的知识,而且减少对计算机图形学中通用知识的介绍,因为读者完全可以在其他的书中得到这些知识。

本书的适用对象为面向Windows平台的3D开发人员。


Direct3D 12是Windows 10新推出的3D图形库API。本书将介绍Direct3D 12相关的各个方面的知识,主要内容如下。

第1章主要介绍开始前的准备工作,包括如何创建Direct3D 12项目和COM简介等内容。

第2章开始学习简单的Direct3D 12编程内容,包括设备、命令队列、交换链,以及渲染等内容。

第3章主要介绍多线程的相关内容,包括命令队列、命令分配器和命令列表等内容。

第4章主要介绍资源的结构、创建,以及CPU、GPU访问资源等内容。

第5章在第4章的基础上,更加深入地介绍图形流水线的相关内容。

第6章介绍计算流水线状态、启动等内容。

第7章简单介绍GPU内部传参的知识。

第8章结合本书所讲解的全部内容,完成一个基于Direct3D 12实现的字体引擎。

本书适合于希望使用Direct3D 12进行编程的读者,并且本书假定读者已经掌握了C/C++编程语言的相关知识。本书并不要求读者掌握Win32 And COM API或之前版本的Direct3D的相关知识,但是已经掌握了这些知识的读者可以更轻松地阅读本书。

由于作者水平有限,本书中难免存在错漏之处。如果读者发现了本书中的错误,请反馈到作者的邮箱D3D12CoreProgram@163.com或本书编辑的邮箱zhangshuang@ptpress.com.cn,作者将尽力对其进行更正。

在完成C++语法的学习后,应当进一步学习C++的相关算法。与读者学习C++的过程类似,学习完本书中的Direct3D 12语法后,读者可继续学习一些相关的算法。

如果读者需要学习渲染流水线的相关算法,那么《Real-Time Rendering》(ISBN:9781568814247)和《Physically Based Rendering》(ISBN:9780123750792)是两本不错的参考书。

如果读者需要学习计算流水线的相关算法,可以参考人民邮电出版社的《OpenCL实战》(ISBN:9787115347343)一书。此外,Bullet是一个基于OpenCL的开源物理引擎,AMD也发布了很多基于OpenCL的开源项目,NVIDIA也发布了很多基于OpenCL的示例程序,这些都可供读者自行参考学习。


本章介绍在开始Direct3D 12编程前需要做的准备工作。

下面介绍如何配置开发环境并创建一个贯穿本书使用的DirectX 12项目。

读者可以从相应的官方网站上获取Windows 10和Visual Studio 2015的免费试用版。

在安装Visual Studio 2015时,应当确保安装了Visual C++和Windows 10 SDK这两个功能,如图1-1所示。

图1-1 配置Visual Studio 2015功能

安装完成后,打开Visual Studio 2015,新建一个Win32空项目。

在菜单栏中选择“文件”→“新建”→“项目”,如图1-2所示。

图1-2 新建项目

在“Visual C++”→“Win32”中选择“Win32项目”,如图1-3所示。

图1-3 Win32项目

在“附加选项”中勾选“空项目”,如图1-4所示。

图1-4 空项目

在解决方案管理器中右击“DX12”项目,选择“属性”,如图1-5所示。

图1-5 选择“属性”

选择所有配置和所有平台,在“配置属性”→“常规”中,将“目标平台”版本设置为“10.0.10586.0”,以使用Windows 10 SDK而不是默认的Windows 8.1 SDK,如图1-6所示。

图1-6 Windows 10 SDK

在“解决方案管理器”中右击“DX12”项目,在下拉菜单栏中选择“添加”→“新建项”,如图1-7所示。

图1-7 添加“新建项”

在“Visual C++”→“代码”中选择“C++文件(.cpp)”,并将名称设置为“main.cpp”,如图1-8所示。

图1-8 C++文件(.cpp)

在main.cpp中添加以下代码,用于创建一个Win32窗口并创建一个渲染线程。代码中已添加了注释以帮助读者理解,如果读者希望更深入地了解相关知识,可以参阅《Windows程序设计》(ISBN:9787302227397)和《Windows 核心编程》(ISBN:9787302184003)。

#include <sdkddkver.h>//表明当前使用的SDK的版本(即Windows 10 SDK)
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <process.h> //含有下文中用于创建线程的_beginthreadex的定义

//_beginthreadex的参数类型与Windows数据类型不匹配,定义了以下BeginThread辅助函数
inline HANDLE __stdcall BeginThread(
     LPSECURITY_ATTRIBUTES lpThreadAttributes,
     SIZE_T dwStackSize,
     LPTHREAD_START_ROUTINE lpStartAddress,
     LPVOID lpParameter,
     DWORD dwCreationFlags,
     LPDWORD lpThreadId
)
{
     return reinterpret_cast<HANDLE>(_beginthreadex(static_cast<void *>(lpThreadAttributes), 
     dwStackSize,
     reinterpret_cast<unsigned(__stdcall *) (void *)>(lpStartAddress), 
     lpParameter, 
     dwCreationFlags, 
     reinterpret_cast<unsigned *>(lpThreadId)));
}

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);//窗口过程的声明
DWORD WINAPI RenderThreadMain(LPVOID);//渲染线程的入口点函数的声明

int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int nCmdShow)//进程中主线程的入口点函数
{
     {
          //以下函数用于注册窗口类
          WNDCLASSEXW wcex;//用于描述窗口类的结构体
          wcex.cbSize = sizeof(WNDCLASSEX);
          wcex.style = CS_OWNDC;//缓存窗口的图形设备环境的状态,以提升性能
          wcex.lpfnWndProc = WndProc;//指定窗口过程
          wcex.cbClsExtra = 0;
          wcex.cbWndExtra = 0;
          wcex.hInstance = hInstance;//窗口类所在的模块
          wcex.hIcon = LoadIconW(hInstance, IDI_APPLICATION);//使用系统默认的图标
          wcex.hCursor = LoadCursorW(NULL, IDC_ARROW);//使用系统默认的鼠标指针
          wcex.hbrBackground = NULL;//指定空画刷,阻止GDI对窗口进行渲染,以提升性能
          wcex.lpszMenuName = NULL;//没有菜单栏
          wcex.lpszClassName = L"{640CB8AD-56CD-4328-B4D0-2A9DAA951494}";//窗口类名
          wcex.hIconSm = LoadIconW(hInstance, IDI_APPLICATION);//使用系统默认的图标
          RegisterClassExW(&wcex);//注册窗口类

          RECT windowrect;
          windowrect.left = 0;
          windowrect.top = 0;
          windowrect.right = 800;//窗口的客户区的宽度
          windowrect.bottom = 600; //窗口的客户区的高度
          AdjustWindowRect(&windowrect, WS_POPUP | WS_VISIBLE, FALSE);//根据窗口的客户区大小计算窗口的大小

          //以下函数用于新建窗口
          HWND hWnd = CreateWindowExW(0, //不使用扩展的窗口风格
          L"{640CB8AD-56CD-4328-B4D0-2A9DAA951494}", //上文中注册的窗口类名
          L"《Direct3D 12核心编程》", //窗口实例的名字
          WS_POPUP | WS_VISIBLE, //没有标题栏的窗口风格,并且显示
          GetSystemMetrics(SM_CXSCREEN) / 2 - windowrect.right / 2, //窗口左上角的横坐标,GetSystemMetrics(SM_CXSCREEN)用于获取屏幕的宽度
          GetSystemMetrics(SM_CYSCREEN) / 2 - windowrect.bottom / 2, //窗口左上角的纵坐标,GetSystemMetrics(SM_CXSCREEN)用于获取屏幕的高度
          windowrect.right, //窗口的宽度
          windowrect.bottom, //窗口的高度
          NULL, //没有父窗口
          NULL,//使用窗口类中指定的菜单栏
          hInstance,//上文中注册的窗口类所在的模块
          NULL//自定义参数为NULL
          );

          //以下代码用于新建线程
          BeginThread(
          NULL,//使用默认的安全属性
          0,//使用默认的线程栈大小
          RenderThreadMain,//指向渲染线程的入口点函数
          static_cast<void *>(hWnd), //传入窗口句柄供渲染线程使用
          0,//没有创建标志
          NULL//对新建线程的线程ID不感兴趣
          );
     }

     //进入线程的消息循环(又称作消息泵)
     MSG msg;
     while(GetMessageW(&msg,NULL,0,0))//在Windows中,每个线程都有一个消息队列,窗口的消息会被发送到创建该窗口的线程的消息队列中,GetMessageW用于从消息队列中获取消息,当消息队列为空时(即队列中没有任何消息),GetMessageW会导致主调线程等待,直到消息队列不再为空(即有新的消息被加入到队列中)
          DispatchMessageW(&msg);//内部会使用GetWindowLongPtrW获取窗口所对应的窗口过程,并调用该窗口过程
     return (int)msg.wParam;//收到WM_QUIT时消息循环会终止,按照惯例,消息的wParam成员表示线程的执行结果
}

//窗口过程的定义
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam,LPARAM lParam)
{
     switch(message)
     {
     case WM_DESTROY://窗口销毁时收到该消息
             PostQuitMessage(0);//会将WM_QUIT消息添加到主调线程的消息队列中,结束上文中的消息循环
     break;
     default:
          return DefWindowProcW(hWnd, message, wParam, lParam);//调用Windows默认的窗口过程进行处理
     }
     return 0;
}

用同样的方式创建rendermain.cpp,并添加以下代码。

#include <sdkddkver.h>
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <dxgi.h>
#include <d3d12.h>

DWORD WINAPI RenderThreadMain(LPVOID lpThreadParameter)
{
     HWND hWnd=static_cast<HWND>(lpThreadParameter);//即我们在main.cpp中传入的窗口句柄
     //在后文中会添加代码
     return 0U;//表示成功返回
}

在“解决方案管理器”中右击“DX12”项目,选择“属性”,如图1-9所示。

图1-9 选择“属性”

在“配置”中选择“所有配置”和“平台”中选择“所有平台”,在“配置属性”→“链接器”→“输入”中,在“附加依赖项”中加入“dxgi.lib”和“d3d12.lib”,如图1-10所示。

图1-10 附加依赖项

选择工具栏中的“本地Windows调试器”生成并调试我们的程序,如图1-11所示。

图1-11 本地Windows调试器

可以在任务栏中看到我们创建的窗口,由于没有向窗口表面写入任何数据,因此窗口表面中没有任何内容可以显示,如图1-12所示。在接下来的章节中,我们将介绍如何用Direct3D 12渲染到窗口表面(严格意义上,Direct3D 12并不直接与本地窗口系统交互,Direct3D 12先渲染到DXGI交换链缓冲,然后DXGI再将交换链缓冲中的数据呈现到窗口表面)。

调试完毕,可以右击任务栏中的窗口图标,再单击关闭窗口,结束本次调试,如图1-13所示。

图1-12 创建的窗口

图1-13 结束程序运行的方式

由于DXGI和Direct3D都是基于COM构建的,因此在开始Direct3D 12编程前,简要介绍在接下来的编程中会用到的COM的相关知识,如果读者想要更深入地了解COM,可以参阅《COM本质论》(ISBN:9787508306117)。

COM是为了构建分布式系统而设计的,可以认为是CORBA在Windows平台上的实现。

然而DXGI和Direct3D不需要用到构建分布式系统的相关功能,因此,DXGI和Direct3D并没有与COM运行时层交互。一个很有力的证据就是,在调用DXGI和Direct3D的API时,并不要求主调线程事先调用CoInitialize(Ex)进入到某个COM套间中。

除了构建分布式系统,COM的另外一个功能是使对象的接口和实现彻底分离,这也是COM(Component Object Model,组件对象模型)的名字的由来。

读者不妨回忆一下C++中抽象类的相关知识。例如我们将A定义为抽象类,B和C同时继承自A(即B和C同时实现了接口A),那么B和C的使用者就可以用A的指针统一访问B和C的对象,从而使B和C的使用者与B和C的实现分离。

在C++中,B和C的使用者需要调用B和C的构造函数来创建B和C的对象,从而使B和C的使用者必须包含B和C的定义。也就是说,如果B和C的定义发生了任何改变,那么所有与调用B和C的构造函数相关的代码将全部受到影响,即B和C的使用者并没有在真正意义上与B和C的实现分离。

COM解决方案是由B和C的实现者,而不是B和C的使用者负责B和C的对象的创建过程。B和C的使用者通过一个实现者事先约定的C风格全局函数来创建B和C的对象(在DXGI中是CreateDXGIFactory,在Direct3D 12中是D3D12CreateDevice,见后文),从而使B和C的使用者不再需要包含B和C的定义。如果B和C的定义发生任何改变,只要抽象类A不发生变化,B和C的使用者就不会受到任何影响。

然而COM并没有规定B和C的实现者在C风格全局函数中创建对象的方式,并不一定是new,因此B和C的使用者并不一定可以用delete来销毁对象。实现者需要事先约定某个函数,在其中定义了与对象的创建方法相兼容的销毁方式,供B和C的使用者调用。

为了统一,COM对此进行了规定:所有的抽象类A都继承自IUnknown接口,B和C的使用者通过IUnknown接口的Release方法来销毁对象。

关于COM的知识暂时就介绍这么多,如果在接下来的章节中需要用到关于COM的其他知识,那么会在相应的章节中进行介绍。

本章我们创建了一个DirectX 12项目,该项目会贯穿本书使用。并且,由于Direct3D 12是基于COM架构的,因此,简单介绍了COM的相关知识。


在前一章中我们已经完成了开始前的准备工作,本章我们正式开始Direct3D 12编程。在开始Direct3D 12编程前,请再次确认你已经链接了dxgi.lib和d3d12.lib库(见1.1.6节)。

在RenderThreadMain中加入以下代码以启用调试层,调试层会进行额外的错误检查,并且会在必要时用OutputDebugStringA输出调试信息,大大提高了在开发中发现并排除bug的效率。

#if defined(_DEBUG)
{
     ID3D12Debug *pD3D12Debug;
     if(SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&pD3D12Debug))))
     {
          pD3D12Debug->EnableDebugLayer();
     }
     pD3D12Debug->Release();
}
#endif

值得注意的是,在本书编写时,调试层可能有bug。如果读者无法正常运行本书后文中的示例程序,那么可以尝试不启用调试层。

正如1.2.2节中所述,Direct3D 12事先约定了全局的C风格的函数D3D12CreateDevice用于创建设备对象,该函数的原型如下。

HRESULT  WINAPI D3D12CreateDevice(
        IDXGIAdapter *pAdapter,                  //[In,opt] 
        D3D_FEATURE_LEVEL MinimumFeatureLevel,   //[In] 
        REFIID riid,                             //[In] 
        void** ppvObject                         //[Out] 
        );

(1)返回值HRESULT

用于表示函数调用成功或者失败,使用带参数的宏SUCCEEDED(hr)或FAILED(hr)来判断。

SUCCEEDED宏的值当且仅当HRESULT的值表示函数调用成功时非0,FAILED宏的值当且仅当HRESULT的值表示函数调用失败时非0。

(2)pAdapter

要求传入一个DXGI适配器对象,表明设备对象所对应的适配器((显示)适配器即平时所说的“显卡”)。在下文中,我们会介绍如何与DXGI交互以创建DXGI适配器对象。

(3)MinimumFeatureLevel

指定应用程序可以接受的最小的功能级。

功能级由适配器决定,表明了适配器所支持的功能,会在各个方面有所体现,在后文中涉及相关的功能时会进行介绍。

该函数会按照12.1 12.0 ... MinimumFeatureLevel(我们指定的值)的顺序进行测试,一旦成功,即创建相应功能级的设备。

MSDN上的Direct3D官方文档指出:在Direct3D 12中,指定的MinimumFeatureLevel的值不得小于11.0(即D3D_FEATURE_LEVEL_11_0)。

(4)riid和ppvObject

在COM中很多创建对象的API中都会看到以上两个参数,这是由于一个对象完全可能支持多个接口。例如1.2.2节中的B和C可能同时继承自抽象类A和抽象类D,对象的使用者在创建对象时需要通过某种方式指定自己期望得到的接口的类型。

为了统一,COM对此进行了规定:

用一个128位的整数来唯一标识一个接口,称作IID(Interface ID,接口ID)。

创建对象时在riid中传入IID表明期望得到的接口类型,如果对象支持相应类型的接口,那么会在ppvObject输出相应类型的接口的指针。同时,COM还定义了辅助宏IID_PPV_ARGS(ppType)以提供便利。

我们只需要事先定义一个接口指针变量,在ppType中传入该接口指针变量的地址,IID_PPV_ARGS中会自动展开成以上两个参数。例如,事先定义ID3D12Device *pDevice;。

在逻辑上,IID_PPV_ARGS(&pDevice)会自动展开成IID_ID3D12Device,reinterprect_cast <void **>(&pDevice)。MSDN上的Direct3D官方文档指出:设备对象支持ID3D12Device接口。

DXGI

DXGI和Direct3D的关系类似于GLX和OpenGL的关系,从Direct3D10开始,由DXGI负责与本地的窗口系统交互,Direct3D在DXGI的基础上构建。

(1)DXGI类厂

正如1.1.2节中所述,DXGI事先约定了全局的C风格的函数CreateDXGIFactory,用于创建DXGI类厂对象。该函数的原型如下。

HRESULT WINAPI CreateDXGIFactory(
            REFIID riid,//[In] 
            void **ppvObject//[Out] 
            );

riid和ppvObject:MSDN上的Direct3D官方文档指出:DXGI类厂对象支持IDXGIFactory接口,同样地,我们也可以使用上文中提到的辅助宏IID_PPV_ARGS。

(2)枚举适配器

IDXGIFactory接口的EnumAdapters方法用于枚举所有的适配器,并创建表示指定的适配器的DXGI适配器对象。该方法的原型如下。

HRESULT STDMETHODCALLTYPE EnumAdapters(
                       UINT AdapterId,//[In]
                       IDXGIAdapter **ppAdapter//[Out]
                       )

MSDN上的Direct3D官方文档指出:适配器索引(即AdapterId)为0的适配器为主适配器,一般都优先使用该适配器,这种做法同样适用于双显卡的环境。如果用户设置默认图形处理器为“独显”,那么主适配器就是“独显”。

如果传入的适配器索引越界,那么该方法会返回DXGI_ERROR_NOT_FOUND,可以用SUCCEEDED或FAILED宏进行判断。

以上我们完成了创建设备对象,不妨在RenderThreadMain中再加入以下代码。

IDXGIFactory *pDXGIFactory;
CreateDXGIFactory(IID_PPV_ARGS(&pDXGIFactory));

ID3D12Device *pD3D12Device = NULL;
{
    IDXGIAdapter *pDXGIAdapter;
    //遍历所有的适配器进行尝试,优先尝试主适配器
    for (UINT i = 0U; SUCCEEDED(pDXGIFactory->EnumAdapters(i, &pDXGIAdapter)); ++i)
    {
         if (SUCCEEDED(D3D12CreateDevice(pDXGIAdapter, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&pD3D12Device))))
         {
                pDXGIAdapter->Release();//实际上,在完成创建设备对象以后,DXGI适配器对象就可以释放
                break;
         }
         pDXGIAdapter->Release();
    }
}

在此只是对命令队列进行简单的介绍,本书会在第3章中对命令队列进行详细介绍。

显示适配器(上文中提到,即平时所说的“显卡”)可以抽象为由GPU(Graphic Process Unit,图形处理单元)和显示内存(即平时所说的“显存”)两部分组成。为了方便理解,在行为上,GPU可以和CPU类比,而显示内存可以和系统内存类似。

在此,我们需要明确“内存”这个术语的含义,在涉及与显示适配器交互的领域,内存既可以指显示内存,也可以指系统内存。

我们往往误认为“内存”就是指系统内存,而“显存”才指显示内存,这是错误的。一个很有力的证据就是,运行C:\Windows\System32\dxdiag.exe(DirectX诊断工具)。在该程序中,系统选项卡中的“内存”指系统内存,而显示或者呈现选项卡中的“内存”指显示内存。

就像任何C/C++代码都一定在某个CPU线程上执行一样,任何GPU命令也都一定在某个GPU线程上执行。

命令队列实际上类似于Windows中的消息队列,唯一对应于一个GPU线程。该GPU线程不断的从命令队列中取出命令并执行,类似于具有消息泵(见1.1.4节)的CPU线程不断地从消息队列中取出窗口消息,并调用窗口过程进行处理。

为了行文简洁,有时也称命令队列执行某个命令,实际上是指,该命令队列所对应的GPU线程从命令队列中取出该命令并执行。

可以用ID3D12Device接口的CreateCommandQueue方法来创建命令队列,该方法的原型如下。

HRESULT STDMETHODCALLTYPE CreateCommandQueue(
                       const D3D12_COMMAND_QUEUE_DESC *pDesc,//[In]
                       REFIID riid,//[In] 
                       void **ppCommandQueue//[Out] 
                       );

1.pDesc

应用程序需要填充一个D3D12_COMMAND_QUEUE_DESC来描述所要创建的命令队列的属性,该结构体的定义如下。

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

(1)Type

命令队列的类型,Direct3D 12中有3种命令队列。

值得注意的是,IDXGISwapChain::Present命令(见2.1.4节)在图形引擎上执行,因此必须在直接命令队列上执行。

实际上,D3D12_COMMAND_LIST_TYPE 中还有第4个枚举值D3D12_COMMAND_LIST_TYPE_BUNDLE,但是它并不用于创建命令队列,而是在ID3D12Device::CreateCommandAllocator或ID3D12Device::CreateCommandList(见2.2.3节)中用于创建捆绑包。

本书会在第3章对复制引擎、计算引擎、图形引擎和捆绑包进行详细的介绍。

(2)Priority

命令队列唯一对应的GPU线程的优先级,在行为上可以类比CPU线程的优先级,优先级高的GPU线程在调度时可以得到更多的GPU时间,共两种取值。

(3)Flags

标志,一般都指定D3D12_COMMAND_QUEUE_FLAG_NONE。

(4)NodeMask

命令队列唯一对应的GPU线程的相关性,支持多GPU节点适配器。在行为上可以类比CPU线程的相关性,用于指定GPU线程可以在哪一个GPU节点上执行。

NodeMask是一个位集合,其中每一个二进制位对应于适配器中一个GPU节点,按照从低位到高位的顺序对适配器中的GPU节点进行编号。

用ID3D12Device接口的GetNodeCount方法可以得到适配器中的GPU节点的总个数。

目前,一般适配器都只有1个GPU节点。因此,一般都简单地在NodeMask中传入0X1,表示在第1个GPU节点上执行。本书不对与多GPU节点适配器相关的知识进行任何介绍。

2.riid和ppvObject

MSDN上的Direct3D官方文档指出:命令队列对象支持ID3D12CommandQueue接口,可以使用辅助宏IID_PPV_ARGS。

以上我们完成了创建命令队列,不妨在RenderThreadMain中再加入以下代码。

ID3D12CommandQueue *pDirectCommandQueue;    
{
  D3D12_COMMAND_QUEUE_DESC cqdc;
  cqdc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;//在此我们创建一个直接命令队列,因为IDXGISwapChain::Present(见2.1.4节)只能在直接命令队列上执行
  cqdc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL;
  cqdc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
  cqdc.NodeMask = 0X1;
  pD3D12Device->CreateCommandQueue(&cqdc, IID_PPV_ARGS(&pDirectCommandQueue));
}

在1.1.7节中,我们在调试程序时发现,窗口表面没有任何内容可以显示,用Direct3D 12对窗口表面进行绘制的方式就是借助于交换链。

可以用IDXGIFactory接口的CreateSwapChain用于创建交换链对象,该方法的原型如下。

HRESULT STDMETHODCALLTYPE CreateSwapChain( 
                     IUnknown *pCommandQueue,//[In]
                     DXGI_SWAP_CHAIN_DESC *pDesc,//[In]
                     IDXGISwapChain **ppSwapChain//[Out]
                     )

1.pCommandQueue

将交换链与某个命令队列关联,所创建的交换链的IDXGISwapChain::Present命令(见2.1.5节)会隐式地在该命令队列上执行。

正如2.1.3节中所述,IDXGISwapChain::Present命令在图形引擎上执行,因此传入的必须是直接命令队列。

2.pDesc

应用程序需要填充一个DXGI_SWAP_CHAIN_DESC来描述所要创建的交换链缓冲的属性。该结构体的定义如下。

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

(1)OutputWindow

输出窗口,执行IDXGISwapChain::Present命令时,交换链缓冲中的内容会呈现到该窗口。

(2)Windowed

TRUE表示窗口模式,FALSE表示全屏模式。

(3)DXGI_SWAP_EFFECT

交换效果,表示交换链缓冲中的内容呈现到输出窗口的方式。

在Direct3D 12中,位块传输(BITBLT(BIT BLock Transfer))模型(DXGI_SWAP_EFFECT_DISCARD或DXGI_SWAP_EFFECT_SEQUENTIAL)已经被弃用,只能使用翻转(FLIP)模型(DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL或DXGI_SWAP_EFFECT_FLIP_DISCARD)。

其中,DXGI_SWAP_EFFECT_FLIP_DISCARD更高效,它不保留已呈现到输出窗口的交换链缓冲中的内容。

(4)BufferCount

交换链中交换链缓冲的个数,交换链可以看作是一个由交换链缓冲构成的循环队列。MSDN上的Direct3D官方文档指出:对于翻转模型,该值至少为2。

(5)BufferUsage

在创建交换链时,应当设置为DXGI_USAGE_BACK_BUFFER|DXGI_USAGE_RENDER_TARGET_OUTPUT。

(6)BufferDesc

描述交换链中每个交换链缓冲的属性,DXGI_MODE_DESC的定义如下。

struct DXGI_MODE_DESC
{
     UINT Width;
     UINT Height;
     DXGI_RATIONAL RefreshRate; 
     DXGI_FORMAT Format; 
     DXGI_MODE_SCANLINE_ORDER ScanlineOrdering; 
     DXGI_MODE_SCALING Scaling; 
};

交换链缓冲的大小,一般设置为和输出窗口的大小一致。MSDN上的Direct3D官方文档指出:如果Width和Height都设置为0,那么DXGI将使用输出窗口的宽度和高度。

如果交互链缓冲的大小与输出窗口不同,那么在将交换链缓冲中的内容呈现到输出窗口时,会进行缩放,显然这样会造成失真。

刷新率,一般设置为60/1。

交换链缓冲的像素的格式。正如2.1.2节中所述,Direct3D 12中,适配器的功能级至少为11.0。MSDN上的Direct3D官方文档指出:功能级11.0的适配器一定支持DXGI_FORMAT_R8G8B8A8_UNORM格式,一般都使用该格式。

一般都设置为DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED。

一般都设置为DXGI_MODE_SCALING_UNSPECIFIED。

(7)SampleDesc

描述交换链缓冲的MSAA属性。本书会在第4章对MMSA(Multiple Sampling Anti-Aliasing,多重采样反走样)进行详细的介绍,DXGI_SAMPLE_DESC的定义如下。

struct DXGI_SAMPLE_DESC
{
     UINT Count;
     UINT Quality;
};

MSDN上的Direct3D官方文档指出:翻转模型不支持MSAA,因此必须将Count设置为1,Quality设置为0,表示禁用MSAA。

值得注意的是,按照C/C++中的惯例,我们很容易误认为将结构体中的成员全设置为0表示禁用,然而Count应当设置为1。如果设置为0,函数调用会失败。

如果要使用MSAA,应当创建一个启用MSAA的2D纹理数组。先用Direct3D 12渲染到该2D纹理数组中的表面,再用ID3D12GraphicsCommandList::ResolveSubresource对启用MSAA的2D纹理数组进行解析,并将解析结果写入到交换链缓冲中。本书会在第4章对此进行详细的介绍。

(8)Flags

一般都设置为0,不使用任何标志。

3.ppSwapChain

输出所创建的交换链对象所支持的IDXGISwapChain接口。

以上我们完成了创建交换链对象,不妨在RenderThreadMain中再加入以下代码。

IDXGISwapChain *pDXGISwapChain;
{
     DXGI_SWAP_CHAIN_DESC scdc;
     scdc.BufferDesc.Width = 0U;
     scdc.BufferDesc.Height = 0U;
     scdc.BufferDesc.RefreshRate.Numerator = 60U;
     scdc.BufferDesc.RefreshRate.Denominator = 1U;
     scdc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
     scdc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
     scdc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
     scdc.SampleDesc.Count = 1U;//注意禁用MSAA的设置方式
     scdc.SampleDesc.Quality = 0U;
     scdc.BufferUsage = DXGI_USAGE_SHADER_INPUT;
     scdc.BufferCount = 2;
     scdc.OutputWindow = hWnd;//即我们在main.cpp中传入的窗口句柄
     scdc.Windowed = TRUE;//设置为窗口模式更加友好
     scdc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;//读者也可以使用  
DXGI_SWAP_EFFECT_FLIP_DISCARD
     scdc.Flags = 0U;
     pDXGIFactory->CreateSwapChain(pDirectCommandQueue, &scdc, &pDXGISwapChain);
}
pDXGIFactory->Release();//实际上,在完成创建交换链对象以后,DXGI类厂对象就可以释放

正如2.1.4节中所述,交换链可以看作是一个由交换链缓冲构成的循环队列,交换链内部维护着一个索引值,称为当前后台缓冲索引。在交换链被创建时,当前后台缓冲索引为0。

当命令队列执行IDXGISwapChain::Present时,索引值为当前后台缓冲索引的交换链缓冲中的内容会被呈现到窗口表面,随后,当前后台缓冲索引加1(如果超过了交换链中缓冲的总个数,那么会回到0)。

不妨在RenderThreadMain中再加入以下代码。

pDXGISwapChain->Present(0, 0);

再次调试我们的程序,可以看到交换链中的内容呈现在窗口表面,如图2-1所示。

由于我们没有向交换链缓冲中写入任何数据,因此交换链缓冲中的数据全都是未定义的值。然而实验表明交换链缓冲中的数据全为0,因此,呈现到窗口表面中全部显示为黑色(RGBA(0,0,0,0)表示黑色)。在下一节中,我们将介绍如何用Direct3D 12渲染到交换链缓冲(即写入交换链缓冲)。

图2-1 呈现以后的运行结果

用Direct3D 12渲染到交换链缓冲前,我们还需要进行一些必要的准备步骤。

正如2.1.3节中所述,Direct3D 12中的命令队列唯一对应于一个GPU线程。该GPU线程不断从命令队列中获取命令并执行。

可以类比消息队列唯一对应于一个具有消息泵(见1.1.4节)的CPU线程。该CPU线程不断地从消息队列中取出窗口消息,并调用窗口过程进行处理。

命令队列中的命令一共有6种(可以类比为窗口过程只处理6种窗口消息),其中5种分别对应于ID3D12CommandQueue接口的5个方法ExecuteCommandLists、Signal、Wait、UpdateTileMapping和CopyTileMappings。第6种是IDXGISwapChain::Present,正如2.1.4节所述,该命令在创建交换链时所指定的命令队列上执行。

与渲染到交换链缓冲有关的命令是ExecuteCommandLists(执行命令列表中的命令),Direct3D 12渲染到交换链缓冲的方式只有两种。

(1)归零:执行命令列表中的ClearRenderTargetView命令,不涉及图形流水线,将渲染目标视图中每个像素统一设置为某个指定的值。

(2)绘制:执行命令列表中的DrawInstanced或DrawIndexedInstanced命令,会启动一个图形流水线,将一系列图元绘制到渲染目标视图中,需要事先执行一些其它的命令来设置图形流水线的相关状态。

由此可见,无论是以上哪种方式,都涉及到渲染目标视图。因此,接下来介绍渲染目标视图。

在此只是对渲染目标视图进行简单的介绍,本书会在第4章详细介绍资源中的视图。

1.创建描述符堆

Direct3D 12中视图的含义与数据库中的视图并没有多大的差别,Direct3D 12并不直接访问资源,而是在资源之上建立视图,从而保证Direct3D 12和资源之间存在着某种安全的距离。

在Direct3D 12中,渲染目标视图、深度模板视图、常量缓冲视图、着色器资源视图、无序访问视图和采样器状态被统称为描述符。在Direct3D 12中创建描述符,需要先创建描述符堆以分配内存。描述符堆的生命期与内存的分配和释放一致,可以用ID3D12Device接口的CreateDescriptorHeap方法来创建描述符堆。该方法的原型如下。

HRESULT STDMETHODCALLTYPE CreateDescriptorHeap(
                   const D3D12_DESCRIPTOR_HEAP_DESC *pDescriptorHeapDesc,//[In] 
                   REFIID riid,//[In] 
                   void **ppvObject//[Out] 
                   );

(1)pDescriptorHeapDesc

应用程序需要填充一个D3D12_DESCRIPTOR_HEAP_DESC结构体,来描述所要创建的描述符堆的属性。该结构体的定义如下。

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

描述符堆的类型,表示允许在描述符堆中存放的描述符的类型,共4种。

D3D12_DESCRIPTOR_HEAP_TYPE_RTV:渲染目标视图。

D3D12_DESCRIPTOR_HEAP_TYPE_DSV:深度模板视图。

D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV:常量缓冲视图(CBV)、着色器资源视图是(SRV)和无序访问视图(UAV)。

D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER:采样器状态。

在此设置为D3D12_DESCRIPTOR_HEAP_TYPE_RTV表示渲染目标视图。

描述符堆中可以容纳的描述符的个数。描述符堆在逻辑上可以看作是一个由描述符构成的一维数组,在此设置为1。

着色器是否可见,本书会在第4章中对描述符堆和描述符进行详细的介绍。在此暂且设置为D3D12_DESCRIPTOR_HEAP_FLAG_NONE。

正如2.1.3节中所述,支持多GPU节点适配器,一般都传入0X1表示第1个GPU节点。

(2)riid和ppvObject

MSDN上的Direct3D官方文档指出:描述符堆对象支持ID3D12DescriptorHeap接口可以使用辅助宏IID_PPV_ARGS。

2.获取交换链缓冲

在完成了创建描述符堆之后,我们便可以在描述符堆中创建渲染目标视图了。

显然,在创建渲染目标视图之前,需要指定渲染目标视图的底层的资源(就像数据库中需要指定视图的底层的表一样)。正如2.1.5节中所述,交换链被创建时,当前后台缓冲索引为0。因此,在此我们获取交换链中索引值为0的缓冲,并在该缓冲上建立渲染目标视图。

可以用IDXGISwapChain接口的GetBuffer方法访问交换链中的缓冲,该方法的原型如下。

HRESULT STDMETHODCALLTYPE GetBuffer(
                     UINT BufferId,//[In]
                     REFIID riid,//[In]
                     void **ppvObject//[Out]
                     );

MSDN上的Direct3D官方文档指出:交换链缓冲支持ID3D12Resource接口。

3.创建渲染目标视图

在获取交换链中的缓冲的指针之后,我们即可在该缓冲之上创建渲染目标视图了。在Direct3D 12中,创建描述符的过程并不分配内存,只是在描述符堆中定位构造描述符(类似C++中的定位new运算符,并不分配内存而只是将数据写入内存)。

Direct3D 12的设计初衷是让应用程序重复使用描述符堆中的内存以提升性能,可以用ID3D12Device接口的CreateRenderTargetView方法构造描述符。该方法的原型如下。

void STDMETHODCALLTYPE CreateRenderTargetView(
                    ID3D12Resource *pResource,//[In,opt] 
                    const D3D12_RENDER_TARGET_VIEW_DESC *pDesc,//[In,opt] 
                    D3D12_CPU_DESCRIPTOR_HANDLE DestDescriptor//[In] 
                    )

(1)pResource

渲染目标视图的底层的资源,在此传入之前得到的交换链缓冲的指针。

(2)pDesc

一般传入NULL,表示创建一个“默认”的渲染目标视图,“默认”的语义是指视图尽可能地接近底层的资源。

(3)DestDescriptor

CPU描述符句柄,用于指定描述符堆中的位置,本书会在第4章中对描述符堆和描述符进行详细的介绍。

在此暂且设置为用ID3D12DescriptorHeap接口的GetCPUDescriptorHandleForHeapStart方法获取的CPU描述符句柄,表示描述符堆中的第一个描述符。

以上我们完成了创建渲染目标视图,不妨在RenderThreadMain中再加入以下代码(当然是在pDXGISwapChain->Present(0, 0)之前)。

    ID3D12DescriptorHeap *pRTVHeap;
    {
        D3D12_DESCRIPTOR_HEAP_DESC RTVHeapDesc    
    ={D3D12_DESCRIPTOR_HEAP_TYPE_RTV ,1,D3D12_DESCRIPTOR_HEAP_FLAG_NONE,0X1 };
        pD3D12Device->CreateDescriptorHeap(&RTVHeapDesc, IID_PPV_ARGS(&pRTVHeap));
    }

    ID3D12Resource *pFrameBuffer;
    pDXGISwapChain->GetBuffer(0, IID_PPV_ARGS(&pFrameBuffer ));

    pD3D12Device->CreateRenderTargetView(pFrameBuffer, NULL, pRTVHeap->GetCPUDescriptorHandleForHeapStart());

正如2.2.1节中所述,在Direct3D 12中进行渲染,需要将ExecuteCommandList命令添加到命令队列中,执行命令列表中的命令。

在此,只是简单介绍命令分配器和命令列表,本书会在第3章进行详细介绍。

1.创建命令分配器

在创建命令列表前,需要先创建一个命令分配器。在创建命令列表时需要指定一个关联的命令分配器。当我们调用ID3D12GraphicsCommandList接口的相关方法向命令列表中添加命令时,命令列表会在其所关联的命令分配器中为新添加的命令分配内存。

可以用ID3D12Device接口的CreateCommandAllocator方法来创建命令分配器,该方法的原型如下。

HRESULT STDMETHODCALLTYPE CreateCommandAllocator(
               D3D12_COMMAND_LIST_TYPE type,//[In]
               REFIID riid,//[In] 
               void **ppObject//[Out] 
               )

(1)type

指定命令分配器的类型,共4种。

在此,我们创建一个直接命令分配器,用于创建直接命令列表。

(2)riid和ppObject

MSDN上的Direct3D官方文档指出:命令分配器支持ID3D12CommandSignature接口,可以使用辅助宏IID_PPV_ARGS。

2.创建命令列表

有了命令分配器之后,我们可以创建命令列表了。命令列表用ID3D12Device接口的CreateCommandList方法创建,该方法的原型如下。

HRESULT STDMETHODCALLTYPE CreateCommandList( 
        UINT nodeMask,//[In] 
        D3D12_COMMAND_LIST_TYPE type,//[In] 
        ID3D12CommandAllocator *pCommandAllocator,//[In] 
        ID3D12PipelineState *pInitialState,//[In] 
        REFIID riid,//[In] 
        void **ppvObject//[Out] 
        );

(1)pCommandAllocator

即上文中所创建的命令分配器。

(2)nodeMask

支持多GPU节点适配器,一般都传入0X1表示第1个GPU节点。

(3)type

命令列表的类型,共4种:复制、计算、直接和捆绑包。在此,我们指定为直接。

(4)pInitialState

与图形流水线有关,见2.4.2节。

(5)riid和ppvObject

MSDN上的Direct3D官方文档指出:命令列表对象支持ID3D12GraphicsCommandList接口,可以使用辅助宏IID_PPV_ARGS。

值得注意的是,ID3D12GraphicsCommandList接口继承自ID3D12CommandList接口,很容易让人误认为:ID3D12GraphicsCommandList接口是专门用于表示可以在图形引擎上执行的直接命令列表的(本书会在第3章中对复制引擎、计算引擎、图形引擎和捆绑包进行详细的介绍)。但是,实际上任何一种类型(复制、计算、直接和捆绑包)的命令列表都支持ID3D12GraphicsCommandList接口。可能是由于在Direct3D 12的设计之初,打算用不同类型的接口来表示不同类型的命令列表,但是在后来的开发过程中又不打算这么做了,因此产生了这种令人困惑的接口命名。

以上我们完成了命令分配器和命令列表的创建,不妨在RenderThreadMain中再加入以下代码。

ID3D12CommandAllocator *pDirectCommandAllocator;
pD3D12Device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&pDirectCommandAllocator));

ID3D12GraphicsCommandList *pDirectCommandList;
pD3D12Device->CreateCommandList(0X1, D3D12_COMMAND_LIST_TYPE_DIRECT, pDirectCommandAllocator, NULL, IID_PPV_ARGS(&pDirectCommandList));

在此,只是对转换资源屏障进行简单的介绍,本书会在第3章中对转换资源屏障进行详细介绍。

读者很容易联想到,以归零方式渲染,先在命令列表中添加ClearRenderTargetView命令,再在命令队列中添加ExecuteCommandList命令,执行该命令列表中的命令。这个想法基本上是正确的,但是在Direct3D 12中,还涉及子资源的权限的问题。

在Direct3D 12中,所有的直接命令队列和计算命令队列被分为一类,称为图形/计算类;所有的复制命令队列被分为一类,称为复制类。上述的图形/计算类和复制类都被称为命令队列类,命令队列类有且仅有图形/计算类和复制类两个实例。

任何一种类型的资源都可以按照某种标准看作是一个子资源的序列(本书会在第4章对该过程进行详细的介绍),每个子资源都关联了两份权限,分别对应于图形/计算类和复制类。一般而言,子资源对图形/计算类的权限在CPU线程执行直接命令队列,或计算命令队列中的命令访问子资源时得到体现。子资源对复制类的权限在CPU线程执行复制命令队列中的命令访问子资源时得到体现,当然也有例外,例如CPU线程访问子资源时的权限要求(因为CPU线程并不与任何命令队列相对应)。

当GPU线程执行直接命令队列的ExecuteCommandLists命令执行命令列表中的ClearRender TargetView命令时,作为渲染目标的子资源(即传入的渲染目标视图的底层的子资源),对图形/计算类的权限必须有可作为渲染目标(D3D12_RESOURCE_STATE_RENDER_TARGET)。

1.添加资源屏障命令

可以用ID3D12GraphicsCommandList接口的ResourceBarrier方法在命令列表中添加ResourceBarrier命令,来转换子资源对该命令所在的命令列表、命令队列、所属的类的权限(值得注意的是,子资源关联了两份权限,在此,我们特别强调了所转换的权限对应于哪一个命令队列类),该方法的原型如下。

    void STDMETHODCALLTYPE ResourceBarrier(
UINT NumBarriers,//[In]
const D3D12_RESOURCE_BARRIER *pBarriers//[In,size_is(NumBarriers)] 
    );

该方法要求传入D3D12_RESOURCE_BARRIER结构体描述资源屏障,该结构体的定义如下。

struct D3D12_RESOURCE_BARRIER
{
     D3D12_RESOURCE_BARRIER_TYPE Type;
     D3D12_RESOURCE_BARRIER_FLAGS Flags;
     union 
     {
          D3D12_RESOURCE_TRANSITION_BARRIER Transition; 
          D3D12_RESOURCE_ALIASING_BARRIER Aliasing;
          D3D12_RESOURCE_UAV_BARRIER UAV;
     };
};

(1)Type

表示资源屏障的类型。显然,在此应当设置为转换资源屏障(D3D12_RESOURCE_BARRIER_TYPE_TRANSITION)。

(2)Flags

与分离资源屏障有关,本书会在第3章中进行介绍。在此暂且设置为不使用分离资源屏障(D3D12_RESOURCE_BARRIER_FLAG_NONE)。

(3)Transition

根据Type的不同,使用联合体中的不同成员。显然,在此应当使用Transition成员,该成员又是一个D3D12_RESOURCE_TRANSITION_BARRIER结构体,该结构体的定义如下。

struct D3D12_RESOURCE_TRANSITION_BARRIER
{
     ID3D12Resource *pResource; 
     UINT Subresource; 
     D3D12_RESOURCE_STATES StateBefore; 
     D3D12_RESOURCE_STATES StateAfter; 
};

表示需要转换权限的子资源所在的资源。显然,在此应当传入之前,用IDXGISwapChain:: GetBuffer获取的指向交换链中索引值为0的缓冲的指针。

表示子资源索引,本书会在第4章中进行介绍。可以设置为D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,表示转换中的所有子资源。在此暂且设置为0。

表示需要转换权限的子资源的权限在转换前的值(即当前的值)。如果设置的值与实际中的值不符,那么可能会导致GPU崩溃。也就是说,在Direct3D 12中,应用程序有义务跟踪子资源的权限在当前的值。

MSDN上的Direct3D官方文档指出:在交换链被创建时,其中所有的交换链缓冲中的所有的子资源对图形/计算类的权限都是公共(D3D12_RESOURCE_STATE_COMMON)。

表示需要转换权限的子资源的权限在转换后的值。显然,在此应当设置为可作为渲染目标(D3D12_RESOURCE_STATE_RENDER_TARGET)。

2.添加归零命令

在命令列表中添加了转换资源屏障后,我们即可以用ID3D12GraphicsCommandList接口的ClearRenderTargetView方法在命令列表中添加ClearRenderTargetView命令了,该方法的原型如下。

void STDMETHODCALLTYPE ClearRenderTargetView( 
                         D3D12_CPU_DESCRIPTOR_HANDLE RenderTargetView,//[In] 
                         const FLOAT ColorRGBA[ 4 ],//[In] 
                         UINT NumRects,//[In] 
                         const D3D12_RECT *pRects//[In,size_is(NumRects),opt] 
                         )

(1)RenderTargetView

CPU描述符句柄,表示描述符在描述符堆中的位置,本书会在第4章中对描述符堆和描述符进行详细的介绍。在此,暂且设置为用ID3D12DescriptorHeap接口的GetCPUDescriptor HandleForHeapStart方法获取的CPU描述符句柄,即之前在描述符堆中创建的渲染目标视图的位置。

(2)ColorRGBA

表示用于归零的颜色。读者可以设置为任意自己喜欢的颜色,例如设置为(1.0f,0.0f,1.0f,1.0f)表示紫色。

(3)NumRects和pRects

表示在渲染目标视图中的归零的矩形区域,如果不传入任何矩形区域,那么表示归零整个渲染目标视图。

在2.2.1节中,我们提到了IDXGISwapChain::Present也是一个命令队列中的命令,添加到创建交换链时所关联的命令队列中。MSDN上的Direct3D官方文档指出:GPU线程在执行命令队列中的IDXGISwapChain::Present命令时,交换链缓冲中的所有子资源对图形/计算类的权限必须是公共。

可以在命令列表的ClearRenderTargetView命令之后再次,添加转换资源屏障,将交换链缓冲中的所有子资源相对于命令队列所属的类的权限转换为公共(D3D12_RESOURCE_STATE_COMMON),这样GPU线程在之后就可以顺利地执行IDXGISwapChain::Present命令了。

可以用命令队列对象暴露的ID3D12CommandQueue接口的ExecuteCommandLists方法,向命令队列中添加ExecuteCommandLists命令执行命令列表,该方法的原型如下。

void STDMETHODCALLTYPE ExecuteCommandLists( 
            UINT NumCommandLists,//[In]
ID3D12CommandList *const *ppCommandLists)//[In,size_is(NumCommandLists)]
            );

值得注意的是,在此可能需要用reinterpret_cast将ID3D12GraphicsCommandList **类型转换成ID3D12CommandList **类型,正如2.2.3节中所述,这可能是Direct3D 12设计过程中的缺陷。

命令列表是不允许被CPU线程和GPU线程之间并发访问的。为此,命令列表被分为记录和关闭两种状态。只有当命令列表处于记录状态时,才允许向其中添加命令;只有当命令列表处于关闭状态时,才允许将其作为ID3D12CommandQueue::ExecuteCommandLists的实参。命令列表在创建时处于记录状态;可以用命令列表对象暴露的ID3D12GraphicsCommandList接口的Close方法,将命令列表从记录状态转换为关闭状态;但是没有任何途径可以将命令列表从关闭状态转换为记录状态。

这保证了在GPU线程执行命令队列中的ExecuteCommandLists命令访问命令列表中的命令时,命令列表一定处于关闭状态,不会有CPU线程同时向命令列表中添加命令,即CPU线程和GPU线程之间不会并发访问命令列表。

以上我们即完成了以归零方式渲染到交换链缓冲,不妨在RenderThreadMain中再加入以下代码。

     //只有当命令列表处于记录状态时才允许向其中添加命令
     D3D12_RESOURCE_BARRIER CommonToRendertarget = { D3D12_RESOURCE_BARRIER_TYPE_TRANSITION ,D3D12_RESOURCE_BARRIER_FLAG_NONE,{ 
pFrameBuffer,0,D3D12_RESOURCE_STATE_COMMON ,D3D12_RESOURCE_STATE_RENDER_TARGET } };
     pDirectCommandList->ResourceBarrier(1, &CommonToRendertarget);

     float rgbacolor[4] = { 1.0f,0.0f,1.0f,1.0f };//紫色
     pDirectCommandList->ClearRenderTargetView(pRTVHeap->GetCPUDescriptorHandleFor
HeapStart(), rgbacolor, 0, NULL);

     D3D12_RESOURCE_BARRIER RendertargetToCommon = { D3D12_RESOURCE_BARRIER_TYPE_TRANSITION ,D3D12_RESOURCE_BARRIER_FLAG_NONE,{ 
pFrameBuffer,0,D3D12_RESOURCE_STATE_RENDER_TARGET ,D3D12_RESOURCE_STATE_COMMON } };
     pDirectCommandList->ResourceBarrier(1, &RendertargetToCommon);
     //只有当命令列表处于关闭状态时才允许执行
     pDirectCommandList->Close();
     pDirectCommandQueue->ExecuteCommandLists(1, reinterpret_cast<ID3D12CommandList **>(&pDirectCommandList));//需要reinterpret_cast可能是Direct3D 12设计时的缺陷

     pDXGISwapChain->Present(0, 0);//之前的呈现命令

再次调试我们的程序,可以看到交换链缓冲被归零为紫色,如图2-2所示。

图2-2 以归零方式渲染到交换链缓冲后的运行结果

正如在2.1.5节中所述,还有一种渲染到交换链缓冲的方式是执行命令列表中的DrawInstanced或DrawIndexedInstanced命令,启动一个图形流水线并绘制到渲染目标视图中。

图2-3是Direct3D 12中完整的图形流水线的示意图,显然,图形流水线的体系结构风格是软件工程中的数据流风格中的管道/过滤器风格。图形流水线有很多可配置的状态,在命令列表中的DrawInstanced或DrawIndexedInstanced命令被执行时,图形流水线的状态会对命令的行为产生影响。

图2-3 Direct3D 12中完整的图形流水线

一种简单的设计思路是在DrawInstanced或DrawIndexedInstanced命令的参数表中传入图形流水线的各个状态。但是,读者通过图2-3可以发现,图形流水线涉及的状态是极其庞大的,这样的设计将导致每次向命令列表中添加DrawInstanced或DrawIndexedInstanced命令时都需要传入几十个参数。

实际上,命令列表每次执行时,GPU线程会为其维护一个状态集合(该集合仅在GPU线程执行该命令列表中的命令时有效,当GPU线程执行下一个命令列表中的命令时,又会维护一个新的集合)。其中包含图形流水线的各个状态,可以在命令列表中添加相关的命令设置状态集合中的相关状态(但是,应用程序应当尽可能地减少状态的改变以提高性能),GPU线程执行命令列表中DrawInstanced或DrawIndexedInstanced命令时,会使用集合中相关的状态,而不需要在每次向命令列表中添加相关命令时传入几十个参数。

如果突然介绍整个图形流水线中的所有状态,读者可能会感到难以接受。因此,本章只选取图形流水线中的一部分进行介绍,如图2-4所示。本书会在第5章中对图形流水线的其余部分进行详细的介绍。

图2-4 图形流水线中本章介绍的部分

1.输入装配(Input Assembler)阶段

首先观察ID3D12GraphicsCommandList接口的DrawInstanced和DrawIndexedInstanced方法的参数表,如下。

void STDMETHODCALLTYPE DrawInstanced( 
     UINT VertexCountPerInstance,//[In]
     UINT InstanceCount,//[In]
     UINT StartVertexLocation,//[In]
     UINT StartInstanceLocation//[In]
     );
void STDMETHODCALLTYPE DrawIndexedInstanced( 
     UINT IndexCountPerInstance,//[In]
     UINT InstanceCount,//[In]
     UINT StartIndexLocation,//[In]
     INT BaseVertexLocation,//[In]
     UINT StartInstanceLocation//[In]
     )

其中,StartVertexLocation、BaseVertexLocation和StartInstanceLocation都与顶点缓冲有关。我们通过在根签名中不设置ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT标志(见2.4.2节)禁用了输入布局从而禁用了顶点缓冲,因此忽略这3个参数。本书会在第4章中对顶点缓冲进行详细的介绍。

(1)产生顶点信息

输入装配阶段的一个功能是产生顶点信息。

DrawInstanced在不使用索引缓冲的情况下进行绘制,会使输入装配阶段产生VertexCountPer Instance*InstanceCount个顶点,并为每个顶点附加SV_INSTANCEID和SV_VERTEXID这两个成员。各个顶点中的SV_INSTANCEID和SV_VERTEXID的值的定义,用伪代码描述如下。

//目前每个顶点中有以下两个成员
Vertex_IA_OUT
{
     uint SV_INSTANCEID;
     uint SV_VERTEXID;
};
Vertex_IA_OUT IAOutputVertexArray[VertexCountPerInstance*InstanceCount];//输入装配阶段产生了VertexCountPerInstance* InstanceCount个顶点
//输入装配阶段为每个顶点附加了SV_INSTANCEID和SV_VERTEXID这两个成员,并按照以下算法对这两个成员赋值
for(i=0;i<InstanceCount;++i)
     for(vi=0;vi<VertexCountPerInstance;++vi)
     {
          IAOutputVertexArray [VertexCountPerInstance*i+vi]. SV_INSTANCEID=i;
          IAOutputVertexArray [VertexCountPerInstance*i+vi]. SV_VERTEXID=vi;
     }

DrawIndexedInstanced使用索引缓冲进行绘制,本书会在第4章中对索引缓冲进行详细的介绍。在此,我们只需要将索引缓冲看作是一个由索引构成的一维数组。

DrawIndexedInstanced同样会使输入装配阶段产生VertexCountPerInstance*InstanceCount个顶点,并为每个顶点附加SV_INSTANCEID和SV_VERTEXID这两个成员,只不过Vertex CountPer Instance需要计算得出。这是因为DrawIndexedInstanced还涉及一个概念——重启动条带(读者可以参阅本节下文中的直线条带和三角形条带中的相关内容)。重启动条带的大致含义是指,当索引缓冲中遇到uint(-1)(有UINT16(-1)(即0XFFFF)和UINT32(-1)(即0XFFFFFFFF)两种选择,可以在D3D12_GRAPHICS_PIPELINE_STATE_DESC结构体(见2.4.2节)的IBStripCutValue成员中设置)时,会中断当前条带,重新启动一个新的条带。相关的过程用伪代码描述如下。

uint IAInputIndexArray [ ];//相对于DrawInstanced 而言,DrawIndexedInstanced需要额外地引入这个表示索引缓冲的数组
Vertex_IA_OUT IAOutputVertexArray[VertexCountPerInstance*InstanceCount];//即DrawInstanced中的IAOutputVertexArray,只不过VertexCountPerInstance需要计算得出
list<uint> RestartStripIndexArray;//用于重启动条带(在本节下文中的直线条带和三角形条带中都会用到),一开始为空,每个条带中最后1个顶点的位置会被添加到这个列表中,我们用push_back方法对将数据添加到列表的过程进行抽象

//计算VertexCountPerInstance
VertexCountPerInstance=0;
for(i=0;i<IndexCountPerInstance;++i)
      if(IAInputIndexArray [StartIndexLocation+i]!=uint(-1))
            ++ VertexCountPerInstance;
//在确定了VertexCountPerInstance的值之后,就可以产生个数为VertexCountPerInstance*InstanceCount的顶点了

//在DrawIndexedInstanced中,SV_VERTEXID的含义就是索引值
for(i=0;i<InstanceCount;++i)
{
      uint vi=0;
      for(iid=0;iid<IndexCountPerInstance;++iid)
      {
            if(IAInputIndexArray [StartIndexLocation+iid]!=uint(-1))
       {
            IAOutputVertexArray [VertexCountPerInstance*i+vi]. SV_INSTANCEID=i;
            IAOutputVertexArray [VertexCountPerInstance*i+vi]. SV_VERTEXID= 
       IAInputIndexArray [StartIndexLocation+iid];
            ++vi;
       }
      }
}

//产生RestartStripIndexArray
uint vi=0;
uint iid=0;
while(IAInputIndexArray [StartIndexLocation+iid]==uint(-1)) //索引缓冲中第1个开始的多个连续的uint(-1)会被忽略
      ++iid;
while(iid<IndexCountPerInstance)
{
      if(IAInputIndexArray [StartIndexLocation+iid]!=uint(-1))
      {
            ++ vi;
      }
      else
      {
            if(RestartStripIndexArray.back()!=vi)//索引缓冲中多个连续的uint(-1)等效于1个uint(-1)
                    RestartStripIndexArray.push_back(vi);//旧条带中最后1个顶点的位置被添加到了RestartStripIndexArray中,RestartStripIndexArray在不同的实例(即SV_INSTANCEID)间共享
      }
      ++iid;
}
If(RestartStripIndexArray.size()==0)//没有使用重启动条带
      RestartStripIndexArray.push_back(uint(-1));//第1个条带中的最后1个顶点的位置“无穷大”

(2)产生图元信息

输入装配阶段的另一个功能是产生图元信息。

输入装配阶会为每个图元附加图元关联的各个顶点在IAOutputVertexArray中的索引和SV_PrimitiveID这两个成员。

枚举D3D_PRIMITIVE_TOPOLOGY中定义了Direct3D 12中所有的图元拓扑类型,在Direct3D 12中可以用ID3D12GraphicsCommandList接口的IASetPrimitiveTopology方法设置图形流水线的输入装配阶段的图元拓扑类型。

Direct3D 12中的图元类型可以分为3类:点,直线和三角形(在此我们不讨论面片和带邻接信息的图元类型。本书会在第5章中对此进行介绍),每种类型的图元又可能分为多种拓扑类型。

点图元只有一种拓扑类型,即点列表(D3D_PRIMITIVE_TOPOLOGY_POINTLIST)点是最简单的图元类型,在伪代码中,我们用以下结构体描述点图元。

Point
{
    uint index;//每个点图元对应于1个顶点,该值表示顶点在IAOutputVertexArray中的索引
    uint SV_PRIMITIVEID;//输入装配阶段为每个图元附加了一个SV_PRIMITIVEID
}

所谓拓扑,即确定图元关联的各个顶点在IAOutputVertexArray中的索引的过程。对于点列表的拓扑过程用伪代码的描述如下,如图2-5所示。

图2-5 点列表图元拓扑类型的形象描述

list<Point> IAOutputPrimitiveArray;//表示产生的图元信息,一开始为空,我们用push_back方法对将数据添加到列表的过程进行抽象

for(i=0;i<InstanceCount;++i)
{
     uint currentPrimitiveID=0;//对于每一个实例(即i的值),SV_PRIMITIVEID会重新开始计算
     for(int vi=0;vi<VertexCountPerInstance;++vi)
     {
          Point primitiveToAppend;
          primitiveToAppend.index=VertexCountPerInstance *i+vi;
          primitiveToAppend.SV_PRIMITIVEID=currentPrimitiveID;
          IAOutputPrimitiveArray.push_back(primitiveToAppend);//产生一个图元
          ++currentPrimitiveID;
     }
}

直线图元分为两种拓扑类型:直线列表(D3D_PRIMITIVE_TOPOLOGY_LINELIST)和直线条带(D3D_PRIMITIVE_TOPOLOGY_LINESTRIP)。相对于点而言,直线更为复杂。在伪代码中,我们用以下结构体描述直线图元。

Line
{
     uint Index[2] ;//每个直线图元对应于2个顶点,该值表示顶点在IAOutputVertexArray中的索引
     uint SV_PRIMITIVEID;
}

对于直线列表的拓扑过程用伪代码的描述如下,如图2-6所示。

图2-6 直线列表图元拓扑类型的形象描述

for(i=0;i<InstanceCount;++i)
{
     uint currentPrimitiveID=0;
for(int vi=0;(vi+1)<VertexCountPerInstance;vi=vi+2)//如果VertexCountPerInstance不能被2整除,那么对于每一个实例(即i的值),最后1个顶点会由于不够组成1个图元(组成一个图元需要有2个顶点)而被丢弃
     {
          Line primitiveToAppend;
          primitiveToAppend.index[0]=VertexCountPerInstance*i+vi;
          primitiveToAppend.index[1]=VertexCountPerInstance*i+vi+1;
          primitiveToAppend.SV_PRIMITIVEID= currentPrimitiveID;
          IAOutputPrimitiveArray.push_back(primitiveToAppend);
          ++currentPrimitiveID;
     }
}

对于直线条带的拓扑过程用伪代码的描述如下,如图2-7所示。

图2-7 直线条带图元拓扑类型的形象描述

for(i=0;i<InstanceCount;++i)
{
     uint currentPrimitiveID=0;
     uint currentCurrentStartStripID=0;//见上文DrawIndexedInstanced中对于重启动条带的介绍
     uint vi=0;
     while((vi+1)<VertexCountPerInstance)
     {
          Line primitiveToAppend;
          if(vi>=RestartStripIndexArray[currentCurrentStartStripID]) //只剩1个顶点,不够组成1个图元
          {
               vi=RestartStripIndexArray[currentCurrentStartStripID]+1;
               ++ currentCurrentStartStripID;
          }
          else if((vi+1)>= RestartStripIndexArray [currentCurrentStartStripID])//当前条带中的最后1个图元
          {
               primitiveToAppend.index[0]= VertexCountPerInstance *i+vi;
               primitiveToAppend.index[1]= VertexCountPerInstance *i+vi+1;
               primitiveToAppend.SV_PRIMITIVEID= currentPrimitiveID;
               IAOutputPrimitiveArray.push_back(primitiveToAppend);
               ++currentPrimitiveID;
               vi= RestartStripIndexArray[currentCurrentStartStripID]+1;
               ++ currentCurrentStartStripID;
          }
          else
          {    primitiveToAppend.index[0]= VertexCountPerInstance *i+vi;
               primitiveToAppend.index[1]= VertexCountPerInstance *i+vi+1;
               primitiveToAppend.SV_PRIMITIVEID= currentPrimitiveID;
               IAOutputPrimitiveArray.push_back(primitiveToAppend);
               ++currentPrimitiveID;
               ++vi;
          }
     }
}

三角形图元分为两种拓扑类型:三角形列表(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST)和三角形条带(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP)。角形是最复杂的图元,在伪代码中,我们用以下结构体描述三角形图元。

Triangle
{
     uint Index[3];//每个三角形图元对应于3个顶点,该值表示顶点在IAOutputVertexArray中的索引
     uint SV_PRIMITIVEID;
}

值得注意的是,三角形图元中index数组中所对应的顶点的顺序称作三角形图元的环绕方向(Winding Direction),光栅化阶段(见2.4.1节)在处理不同环绕方向的三角形图元时可能有不同的行为。

对于三角形列表的拓扑过程用伪代码的描述如下,如图2-8所示。

图2-8 三角形列表图元拓扑类型的形象描述

for(i=0;i<InstanceCount;++i)
{
     uint currentPrimitiveID=0;
     for(vi=0;(vi+2)<VertexCountPerInstance;vi=vi+3) //如果VertexCountPerInstance不能被3整除,那么对于每一个实例(即i的值),最后的1个或2个顶点会由于不够组成一个图元(组成一个图元需要有3个顶点)而被丢弃
     {
          Triangle primitiveToAppend;
          primitiveToAppend.index[0]=VertexCountPerInstance*i+vi;
          primitiveToAppend.index[1]=VertexCountPerInstance*i+vi+1;
          primitiveToAppend.index[2]=VertexCountPerInstance*i+vi+2;
          primitiveToAppend.SV_PRIMITIVEID=currentPrimitiveID;
          IAOutputPrimitiveArray.push_back(primitiveToAppend);
          ++currentPrimitiveID;
     }
}

对于三角形条带的拓扑过程用伪代码的描述如下,如图2-9所示。

图2-9 三角形条带图元拓扑类型的形象描述

for(i=0;i<InstanceCount;++i)
{
     uint currentPrimitiveID=0;
     uint currentCurrentStartStripID=0;//见上文DrawIndexedInstanced中对于重启动条带的介绍
     uint vi=0;
     while((vi+2)<VertexCountPerInstance)
     {
          uint isOdd=0;
          Triangle primitiveToAppend;
          if((vi+1)>=RestartstripIndexArray[currentCurrentStartStripID]) //只剩1个或2个顶点,“不够”组成1个图元
          {
               vi=RestartStripIndexArray[currentCurrentStartStripID]+1;
               ++ currentCurrentStartStripID;
               isOdd=0;
          }
          else if((vi+2)>=RestartStripIndexArray[currentCurrentStartStripID])//当前条带中的最后1个图元
          {
               if(isOdd%2==1)
               {
                    primitiveToAppend.index[0]= VertexCountPerInstance *i+vi;
                    primitiveToAppend.index[1]= VertexCountPerInstance *i+vi+1;
                    primitiveToAppend.index[2]= VertexCountPerInstance *i+vi+2;
               }
               else//值得注意的是,对于拓扑形成的每一个条带,第偶数个图元的顶点的顺序与IAOutputVertexArray中的顺序相反
               {
                    primitiveToAppend.index[0]= VertexCountPerInstance *i+vi;
                    primitiveToAppend.index[1]= VertexCountPerInstance *i+vi+2;
                    primitiveToAppend.index[2]= VertexCountPerInstance *i+vi+1;
               }

               primitiveToAppend.SV_PRIMITIVEID= currentPrimitiveID;
               IAOutputPrimitiveArray.push_back(primitiveToAppend);
               ++currentPrimitiveID;
               isOdd=0;//重置奇偶性
               vi=RestartStripIndexArray[currentCurrentStartStripID]+1;
               ++ currentCurrentStartStripID;
          }
          else
          {
               if(isOdd%2==1)
               {
                    primitiveToAppend.index[0]= VertexCountPerInstance *i+vi;
                    primitiveToAppend.index[1]= VertexCountPerInstance *i+vi+1;
                    primitiveToAppend.index[2]= VertexCountPerInstance *i+vi+2;
               }
               else
               {
                    primitiveToAppend.index[0]= VertexCountPerInstance *i+vi;
                    primitiveToAppend.index[1]= VertexCountPerInstance *i+vi+2;
                    primitiveToAppend.index[2]= VertexCountPerInstance *i+vi+1;
               }

               primitiveToAppend.SV_PRIMITIVEID= currentPrimitiveID;
               IAOutputPrimitiveArray.push_back(primitiveToAppend);
               ++currentPrimitiveID;
               ++vi;
          }
     }
}

实际上,读者可以发现,如果对于直线条带每隔2个顶点插入1个uint(-1)索引,就相当于是直线列表。同样地,对于三角形条带,如果每隔3个顶点插入1个uint(-1)索引就相当是三角形列表。因此,列表拓扑类型实际上可以看作是对条带拓扑类型的封装,也就是说,列表拓扑类型可以用条带拓扑类型来实现。

并且,读者还可以发现,输入装配阶段的输入中需要指明图元拓扑类型(见ID3D12GraphicsCommandList::IASetPrimitiveTopology中的参数),而输入装配阶段的输出中只具有图元类型的信息,而不再具有拓扑类型的信息。由此可见,无论是列表拓扑类型,还是条带拓扑类型,在后续的图形流水线中的行为应当是相同的。

综上所述,我们可以认为,输入装配阶段主要有两个功能:分别是产生表示顶点信息的Vertex_IA_OUT IAOutputVertexArray[ ]和表示图元信息的list<PrimitiveType> IAOutputPrimitiveArray。

2.顶点着色器(Vertex Shader)阶段

图形流水线针对IAOutputVertexArray中的每一个顶点执行一次顶点着色器,顶点着色器是一个用户自定义的程序(在2.4.2节中会介绍如何用HLSL(High Level Shader Langugue,高级着色器语言)编写着色器程序)。每次执行顶点着色器时,图形流水线会将顶点所对应的Vertex_IA_OUT输入到顶点着色器。顶点着色器在每次执行后会输出一个表示顶点的结构体,为了方便讨论,我们将该结构体记作Vertex_VS_OUT。Direct3D 12规定,Vertex_VS_OUT中必须包含SV_POSITION成员。该成员会在接下来的光栅化阶段中被用于产生顶点的坐标,当然顶点着色器完全可以在输出的结构体中附加其他的自定义的成员。

在对所有的顶点都执行了一次顶点着色器后,图形流水线得到了这么一个数组Vertex_VS_OUT VSOutputVertexArray[ ](数组的大小与IAOutputVertexArray 相同)。随后图形流水线将输入装配阶段产生的list<PrimitiveType> IAOutputPrimitiveArray和顶点着色器阶段产生的Vertex_VS_OUT VSOutputVertexArray[ ]输入到光栅化阶段。

细心的读者应该已经注意到,输入装配阶段产生的IAOutputPrimitiveArray直接被图形流水线传入光栅化阶段,不经过顶点着色器阶段,对顶点着色器而言是不可见的。

3.光栅化(Rasterizer)阶段

总体上,光栅化阶段的功能是将输入的矢量图转换成像素图并输出。光栅化阶段是图形流水线中最能够体现出硬件加速的阶段,输出到光栅化阶段的数据和从光栅化阶段输出的数据可能是不在一个数量级上的。因此,Direct3D 12又被称作基于光栅化的系统。

在Direct3D 12中,与光栅化有关的结构体有:D3D12_GRAPHICS_PIPELINE_STATE_DESC中的RasterizerState成员的类型D3D12_RASTERIZER_DESC结构体;ID3D12GraphicsCommand List::RSSetViewports中传入的D3D12_VIEWPORT结构体;ID3D12GraphicsCommandList::RSSet ScissorRects中传入的D3D12_RECT结构体。这3个结构体的定义如下。

struct D3D12_RASTERIZER_DESC
{
     D3D12_FILL_MODE FillMode;
     D3D12_CULL_MODE CullMode;
     BOOL FrontCounterClockwise;
     INT DepthBias;
     FLOAT DepthBiasClamp;
     FLOAT SlopeScaledDepthBias;
     BOOL DepthClipEnable;
     BOOL MultisampleEnable;
     BOOL AntialiasedLineEnable;
     UINT ForcedSampleCount;
     D3D12_CONSERVATIVE_RASTERIZATION_MODE ConservativeRaster;
}
struct D3D12_VIEWPORT
{
     FLOAT TopLeftX;
     FLOAT TopLeftY;
     FLOAT Width;
     FLOAT Height;
     FLOAT MinDepth;
     FLOAT MaxDepth;
}
struct D3D12_RECT
{
     LONG    left;
     LONG    top;
     LONG    right;
     LONG    bottom;
}

接下来,我们结合这3个结构体对光栅化阶段进行介绍。

(1)产生顶点坐标

光栅化阶段会根据VSOutputVertexArray中的每个顶点的SV_POSITION成员为每个顶点产生1个顶点坐标。在伪代码中,我们用以下结构体来描述来顶点坐标。

RS_Coord
{
    int x;
    int y;
    float z;
};

在求得每个顶点的坐标后,光栅化阶段得到了这么一个数组RS_Coord RSInnerPositionArray[ ](数组的大小与VSOutputVertexArray相同)。

首先光栅化阶段会对VSOutputVertexArray中的每个顶点的SV_POSITION成员进行剪辑。

SV_POSITION是一个4分量的浮点型向量,在伪代码中,我们用以下结构体来描述。

{
    float x;
    float y;
    float z;
    float w;
}SV_POSITION;

光栅化阶段会保证SV_POSITION的各个分量满足:

w>0
-w<=x<=w
-w<=y<=w

如果启用了深度剪辑(即D3D12_RASTERIZER_DESC结构体中的DepthClipEnable成员为TRUE),那么光栅化阶段还会保证。

0<=z<=w

从而光栅化阶段保证了x/w和y/w在[-1.0f,1.0f]内,如果启用了深度剪辑,那么光栅化阶段还将保证z/w在[0.0f,1.0f]内。

随后光栅化阶段会对SV_POSITION中的坐标进行视口变换,得到一个视口坐标系中的坐标。SV_POSITION的几何意义是表示一个在归一化(Uniform)坐标系中的坐标(x/w,y/w,z/w),如图2-10所示。

图2-10 归一化坐标系

视口坐标的x和y分量实际上就是作为渲染目标的表面(详见第4章)中的位置,z分量与深度测试有关(详见第5章)。

读者可以发现,视口坐标系中的x和y分量是整型,而z分量是浮点型,如图2-11所示。

图2-11 视口坐标系

在伪代码中,我们用以下结构体来描述来视口坐标。

RS_VP_Coord
{
    int x;
    int y;
    float z;
};

视口变换的过程与D3D12_VIEWPORT结构体有关。

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

Direct3D 12对MinDepth和MaxDepth的取值进行了限制,规定MinDepth和MaxDepth必须都在[0.0f,1.0f]内。

在ID3D12GraphicsCommandList::RSSetViewports中可以传入1个D3D12_VIEWPORT数组,设置图形流水线的光栅化阶段的视口数组。可以在几何着色器中设置SV_ViewportArrayIndex的值,选择在光栅化阶段使用哪个D3D12_VIEWPORT。如果没有启用几何着色器,那么光栅化阶段将使用传入的D3D12_VIEWPORT数组中的第0个D3D12_VIEWPORT。

我们将光栅化阶段使用的D3D12_VIEWPORT记作viewport,那么视口变换的过程可以用伪代码描述如下,如图2-12所示。

图2-12 视口变换

Vertex_VS_OUT in;//伪代码输入
RS_VP_Coord out;//伪代码输出
out.x=viewport.TopLeftX+viewport.Width*(1.0f+in.SV_POSITION.x/ in.SV_POSITION.w)*0.5f;
out.y=viewport.TopLeftY+viewport.Height*(1.0f-in.SV_POSITION.y/ in.SV_POSITION.w)*0.5f;
out.z=viewport.MinDepth+(viewport.MaxDepth-Viewport.MinDepth) *(in.SV_POSITION.z/ in.SV_POSITION.w)

读者可以发现,视口坐标系和归一化坐标系中的y轴的方向正好相反。根据上文所述,在Direct3D 12中MinDepth和MaxDepth一定在[0.0f,1.0f]内。因此,如果启用了深度剪辑(即0<=z<=w),那么视口坐标的z分量一定在[0.0f,1.0f]内。

深度偏移只有在两种情况下才会进行:图元类型是三角形;图元类型是直线,且光栅化阶段的填充模式为线框模式(D3D12_RASTERIZER_DESC结构体的FillMode成员为D3D12_FILL_MODE_WIREFRAME,下文中还会讨论该成员)。

如果不进行深度偏移,那么顶点坐标RS_Coord就是视口坐标RS_VP_Coord。

如果进行深度偏移,那么顶点坐标RS_Coord的x和y分量就是视口坐标RS_VP_Coord的x和y分量,顶点坐标RS_Coord的z分量为视口坐标RS_VP_Coord的z分量加上Bias,深度偏移即计算的得到Bias的过程。

深度偏移的过程与D3D12_RASTERIZER_DESC中的DepthBias、SlopeScaledDepthBias和DepthBiasClamp成员有关,可以用伪代码描述如下。

Bias = FloatUnit*DepthBias + max(DepthSlopeX,DepthSlopeY)*SlopeScaledDepthBias;
if(DepthBiasClamp > 0)
     Bias = min(DepthBiasClamp, Bias)
else if(DepthBiasClamp < 0)
     Bias = max(DepthBiasClamp, Bias)

FloatUnit是计算机可识别的、两个不同的浮点数之间的最小的间隔。计算机不可能表示连续的实数区间,只可能用离散的值进行模拟,FloatUnit即是这些离散的值之间的间隔。

DepthSlopeX和DepthSlopeY分别是X方向和Y方向的深度斜率。根据上文所述,只有直线(必须线框模式)或三角形图元可以进行深度偏移。通过简单的数学证明可知,这两种类型的图元在视口坐标系中在平面y=0或平面x=0上的投影都是一条直线,该直线的斜率被定义为x方向或y方向的深度斜率。

正如上文所述,如果启用了深度剪辑,那么视口坐标的z分量一定在[0.0f,1.0f]内;但是如果进行了深度偏移,那么显然顶点坐标的z分量并不一定在[0.0f,1.0f]内。

(2)插值

光栅化阶段对IAOutputPrimitiveArray中的每个图元进行插值(Interpolation)转换为一个像素图。在插值过程中只会用到顶点坐标中的x和y分量,可以认为插值得到的像素图的几何意义是顶点坐标表示的几何体在平面xOy上的正交投影。

在伪代码中,我们用以下结构来描述像素图。

Primitive_RS_OUT
{
     list<{Vertex_VS_OUT;RS_Coord;}> pixelArrayInPrimitive;//不确定个像素,显然像素图不再区分图元类型
     uint SV_PRIMITIVEID;
}

插值的过程中与D3D12_RECT结构体有关。

struct D3D12_RECT
{
     LONG    left;
     LONG    top;
     LONG    right;
     LONG    bottom;
}

ID3D12GraphicsCommandList:: RSSetScissorRects中可以传入1个D3D12_RECT数组,设置图形流水线的光栅化阶段的剪裁矩形数组。可以通过在几何着色器中设置SV_ViewportArrayIndex的值,来选择在光栅化阶段使用哪个D3D12_RECT。如果没有启用几何着色器,那么光栅化阶段将使用D3D12_RECT数组中的第0个D3D12_RECT。

D3D12_RECT定义了渲染目标视图坐标系中一个矩形区域,称为剪裁(Scissor)区域,如图2-13所示。

图2-13 剪裁区域

光栅化阶段将IAOutputPrimitiveArray中的图元转换为像素图时,会丢弃不在剪裁区域内的像素点。

接下来,我们对光栅化阶段根据每个PrimitiveType产生Primitive_RS_OUT的方式进行介绍。显然Primitive_RS_OUT中的SV_PRIMITIVEID即为各个PrimiviteType中的SV_PRIMITIVEID。至于Primitive_RS_OUT中的pixelArrayInPrimitive,则随着图元类型的不同而不同。接下来,我们对此进行介绍。

Point
{
     uint index;
     uint SV_PRIMITIVEID;
}

点是最简单的图元类型,光栅化阶段根据Point中Index,确定顶点在VSOutputVertexArray中的Vertex_VS_OUT和在RSInnerPositionArray中的RS_Coord。光栅化阶段根据RS_Coord的x和y分量确定顶点在渲染目标视图中的位置。

只有顶点对应的位置在剪裁区域内的情况下才产生像素。该顶点的Vertex_VS_OUT和RS_Coord即为像素的Vertex_VS_OUT和RS_Coord,添加到Primitive_RS_OUT的pixelArrayIn Primitive中。

Line
{
     uint Index[2] ;
     uint SV_PRIMITIVEID;
}

相对于点而言,直线要复杂得多。光栅化阶段根据Line中的2个Index,确定这2个顶点在VSOutputVertexArray中的Vertex_VS_OUT和在RSInnerPositionArray中的RS_Coord,为了方便讨论,我们将这两个点记作A和B。

光栅化阶段根据A和B的RS_Coord中的x和y分量确定直线在渲染目标视图中的位置。光栅化阶段对直线覆盖的每一个像素点P进行测试,如果P不在剪裁区域内,那么不进行任何操作。只有当P在剪裁区域内的情况下,才会进行接下来的操作,产生该像素点。

根据高中数学中向量的相关知识可知,对于直线覆盖的像素点P,一定有OP=xOA+yOB,其中x+y=1,并且x和y有唯一解,如图2-14所示。

图2-14 直线的插值

光栅化阶段将解出x和y,并计算x*A. Vertex_VS_OUT+y*B. Vertex_VS_OUT和x*A.RS_Coord+ y*B.RS_Coord得到像素的Vertex_VS_OUT和RS_Coord值,添加到Primitive_RS_OUT中的pixelArrayInPrimitive中。

上述表达式中涉及到对结构体进行数乘和加法运算,显然这只是一种简便的记法。因为Vertex_VS_OUT中的成员并不确定,读者应该很容易理解,实际上就是对Vertex_VS_OUT中的各个成员进行数乘和加法运算。

根据OP=xOA+yOB,求解x和y,并根据x和y的值,计算x*A. Vertex_VS_OUT+y*B. Vertex_VS_OUT和x*A.RS_Coord+y*B.RS_Coord的过程称为插值。可以理解为,在已有的A和B两个点之间“插”入了好多个新的点P,因此被称作“插”值。

直线图元实际上并没有“填充模式”的概念,但是D3D12_RASTERIZER_DESC的FillMode成员是有意义的,这决定了光栅化阶段是否会进行深度偏移(见2.4.1节)。

Triangle
{
     uint Index[3] ;
     uint SV_PRIMITIVEID;
}

三角形图元的插值过程中还与以下D3D12_RASTERIZER结构体的以下成员有关:正背面(Front Back)、剔除模式(Cull Mode)和填充模式(Fill Mode)。

正背面通过D3D12_RASTERIZER_DESC的FrontCounterClockwise成员设置,共分为2种:TRUE 环绕方向逆时针表示正面;FALSE环绕方向顺时针表示正面。

一般OpenGL程序员习惯用逆时针表示正面,而Direct3D程序员恰好相反,然而这并没有硬性的规定。

关于三角形图元的环绕方向的定义已经在2.4.1节输入装配阶段中介绍。

剔除模式通过D3D12_RASTERIZER_DESC中的CullMode成员设置,共分为3种:不剔除(D3D12_CULL_MODE_NONE);剔除正面(D3D12_CULL_MODE_FRONT);剔除背面(D3D12_CULL_MODE_BACK)。

填充模式通过D3D12_RASTERIZER_DESC中的FillMode成员设置,共分为2种:线框模式(D3D12_FILL_MODE_WIREFRAME):只产生三角形边界所覆盖的像素;实心模式(D3D12_FILL_MODE_SOLID):产生三角形边界和内部所覆盖的像素。

光栅化阶段根据Triangle中的3个Index,确定这3个顶点在VSOutputVertexArray中对应的Vertex_VS_OUT和在RSInnerPositionArray中对应的RS_Coord,为了方便讨论,我们将这3个点记作A、B和C。

光栅化阶段根据这3个点的RS_Coord的x和y分量,确定三角形在渲染目标视图中的位置。

光栅化阶段根据三角形的环绕方向,确定三角形是正面还是背面,并根据剔除模式:如果三角形被剔除,那么直接跳过后续的步骤,不产生相应的Primitive_RS_OUT;如果三角形没有被剔除,那么光栅化阶段根据光栅化阶段的剪裁矩形和填充模式确定需要产生的像素。

同样地,根据高中数学知识可知,对于产生的像素点P,都有OP=xOA+yOB+zOC,其中x+y+z=1,并且x、y和z有唯一解,如图2-15所示。

图2-15 三角形的插值

光栅化阶段将解出x、y和z,并计算x*A. Vertex_VS_OUT+y*B. Vertex_VS_OUT+z*C. Vertex_VS_OUT和 x*A.RS_Coord+y*B.RS_Coord+z*C.RS_Coord,得到像素的Vertex_VS_OUT和RS_Coord的值,添加到Primitive_RS_OUT中的pixelArray InPrimitive列表中。

对每个图元都插值后,光栅化阶段得到了这么一个数组Primitive_RS_OUT RSOutputPrimitive Array[ ](数组的大小与RSOutputPrimitive Array相同)。

4.像素着色器(Pixel Shader)阶段

图形流水线对RSOutputPrimitiveArray中的每一个Primitive_RS_OUT中的每一个像素中执行一次像素着色器

像素着色器是一个用户自定义的程序(在2.4.2节中会介绍如何用HLSL编写着色器程序)。每次执行像素着色器时,图形流水线会将像素的Vertex_VS_OUT和像素所在的Primitive_RS_OUT中的SV_PRIMITIVEID输入到像素着色器。像素着色器在每次执行后会输出一个结构体,为了方便讨论,我们将该结构体记作Pixel_PS_OUT。

像素的RS_Coord不会被输入到像素着色器,但是像素着色器可以改变RS_Coord中的z成员。在每次执行像素着色器后,图形流水线会查看Pixel_PS_OUT中是否有SV_DEPTH成员。如果有,那么像素的RS_Coord的z成员会被设置为Pixel_PS_OUT中的SV_DEPTH成员的值。

在对所有的像素都执行了一次像素着色器后,图形流水线得到了这样一个数组{Pixel_PS_OUT;RS_Coord} PSOutputPixelArray[ ](数组的大小为所有Primitive_RS_OUT中的pixelArrayIn Primitive中的像素的个数之和),该数组被传入到输出混合阶段。

5.输出混合(Output Merger)阶段

在此,我们只对输出混合阶段进行简单的介绍,本书会在第5章中对输出混合阶段进行详细的介绍。

可以使用ID3D12GraphicsCommandList接口的OMSetRenderTarget 方法设置渲染目标视图,图形流水线中至多有8个渲染目标视图。输出混合阶段会检查PSOutputPixelArray中的每个像素的Pixel_PS_OUT中是否有SV_TARGET0、SV_TARGET1、……、SV_TARGET7成员(分别对应于该8个渲染目标视图)。如果Pixel_PS_OUT中有成员SV_TARGETn(n的取值范围从0到7),那么输出混合阶段会将该成员SV_TARGETn的值写入到第n+1个渲染目标视图中该像素的RS_Coord的x和y分量确定的位置。

正如2.4.1节中所述,GPU线程会为命令列表的每次执行维护一个状态集合,可以在命令列表中添加相关的命令设置集合中的相关状态,GPU线程执行命令列表中DrawInstanced或DrawIndexedInstanced命令时,会使用状态集合中的相关状态。

在上一节中,我们已经详细介绍了图形流水线中本章会用到的各个状态。接下来,我们介绍用具体的API设置图形流水线的相关状态,并执行DrawInstanced命令绘制一个三角形,在本章我们只会用到ID3D12GraphicsCommandList接口中的以下方法。

(1)零碎的API

(2)设置根签名

SetGraphicsRootSignature控制是否启用IA阶段的输入布局和是否启用流输出阶段(见2.4.1节中的图2-4)。

(3)设置图形流水线状态对象

SetPipelineState 包括对图形流水线中的大多数状态的设置。

1.零碎的API

(1)IASetPrimitiveTopology用于设置输入装配阶段的图元拓扑类类型(见2.4.1节)。

pDirectCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);//设置图元拓扑类型为三角形条带

(2)RSSetViewports用于设置光栅化阶段的视口数组(见2.4.1节中的图2-12)。

D3D12_VIEWPORT vp = { 0.0f,0.0f,800.0f,600.0f,0.0f,1.0f };//正如2.4.1.3.1节中所述,Direct3D 12规定,MinDepth和MaxDepth必须都在[0.0f,1.0f]内
pDirectCommandList->RSSetViewports(1, &vp);//将视口变换的效果设置为将整个归一化坐标系变换到整个渲染目标视图

(3)RSSetScissorRects用于设置光栅化阶段的剪裁矩形数组(见2.4.1节中的图2-13)。

D3D12_RECT sr = { 0,0,800,600 };
pDirectCommandList->RSSetScissorRects(1, &sr);//将剪裁区域设置为整个渲染目标视图

(4)OMSetRenderTargets的原型如下。

   void STDMETHODCALLTYPE OMSetRenderTargets( 
        UINT NumRenderTargetDescriptors,//[In]
        const D3D12_CPU_DESCRIPTOR_HANDLE *pRenderTargetDescriptors,//[In,opt]
        BOOL RTsSingleHandleToDescriptorRange,//[In]
        const D3D12_CPU_DESCRIPTOR_HANDLE *pDepthStencilDescriptor//[In,opt]
   )
NumRenderTargetDescriptors

指定的渲染目标视图的数量,在Direct3D 12中最多同时指定8个渲染目标视图(见2.4.1节)。

读者观察D3D12_DESCRIPTOR_HEAP_DESC结构体(见2.2.2节)可以发现,其中有一个 NumDescriptors成员可以设置,也就是说,在创建描述符堆时,可以一次性创建多个在内存中连续的描述符。

RtsSingleHandleToDescriptorRange为True,表示在pRenderTargetDescriptors中传入的是在内存中连续的多个描述符的首地址,显然只需要传入一个地址即可。

RtsSingleHandleToDescriptorRange为False表示在pRenderTargetDescriptors中为每个描述符传入一个地址,显然需要传入NumRenderTargetDescriptors个地址。

用于指定深度模板缓冲,在第5章会介绍,暂且传入NULL,代码如下。

pDirectCommandList->OMSetRenderTargets(1, &pRTVHeap->GetCPUDescriptorHandleForHeapStart(),FALSE,NULL);//设置渲染目标

2.根签名

在此只对根签名进行简单的介绍,本书在第4章中会对根签名进行详细的介绍。

在介绍根签名之前,首先介绍HLSL(High Level Shader Language,高级着色器语言)。实际上,在上文中介绍顶点着色器阶段和像素着色器阶段时,我们已经提到了HLSL。HLSL是编写GPU程序的语言,HLSL的语法与C/C++极其相似,因此本书中不会有专门的章节对HLSL的语法进行介绍。

就像C/C++可以编写CPU上执行的程序一样,HLSL可以编写在GPU上执行的程序,顶点着色器和像素着色器实际上就是在GPU上执行的程序。与C/C++不同的是,HLSL编译生成的是字节码(就是Java中的字节码的含义),而不是在GPU上执行的本地代码。这是因为在不同GPU上执行的本地代码之间的差异很大,Direct3D 12会在运行时将字节码解析成可以在特定的GPU上执行的本地代码。

在2.1.2节中指出,Direct3D 12要求设备的功能级至少为11.0。MSDN上的Direct3D官方文档指出:功能级为11.0的设备在Direct3D 12运行环境下至少支持着色器模型5.1,着色器模型的版本可以认为就是指HLSL的版本。

在着色器模型5.1中,引入了一种新的着色器类型——根签名对象,目前根签名对象的最新版本为1.0,即(rootsig_1_0)。

我们可以用两种方式生成根签名对象的字节码:一种方式是用D3D12_ROOT_SIGNATURE_DESC描述根签名对象,并调用D3D12SerializeRootSignature函数序列化生成根签名对象的字节码;另一种方式是用HLSL描述根签名对象,并编译生成根签名对象的字节码。无论使用哪一种方式得到的根签名对象的字节码都是等效的。

通过观察D3D12_ROOT_SIGNATURE_DESC结构体的定义,我们可知,根签名对象的逻辑结构可以用UML描述,如图2-16所示。

图2-16 根签名对象的逻辑结构

在此,我们暂且不需要用到任何根形参或静态采样器,只需要用到根标志。

接下来我们对如何用HLSL编译生成根签名对象的字节码进行介绍:首先创建一个头文件“GRS.hlsli”,在其中添加根签名对象的定义的代码。右击“项目”中的“头文件”,在下拉菜单栏中选“择添加”→“新建项”,如图2-17所示。

图2-17 添加新建项

在“Visual C++”→“HLSL”中选择“HLSL标头文件(.hlsli)”,并将名称设置为“GRS.hlsli”,如图2-18所示。

图2-18 HLSL标头文件(.hlsli)

在GRS.hlsli中以“#define 根签名对象名 字符串常量”的形式定义根签名对象,其中#define的语法和C/C++中#define的语法相同,可以使用“\”将字符串常量分开写成多行。

字符串常量用于描述根签名对象的各个成员,大致的语法格式为“类型名(属性表)”,成员之间用逗号隔开,并且在字符串常量中不允许有制表符(即’\t’),否则会被视为错误。

在此,我们只需要用到根标志,在GRS.hlsli中添加的HLSL代码如下。

#define GRS "RootFlags(\
DENY_VERTEX_SHADER_ROOT_ACCESS\
|DENY_PIXEL_SHADER_ROOT_ACCESS\
|DENY_DOMAIN_SHADER_ROOT_ACCESS\
|DENY_HULL_SHADER_ROOT_ACCESS\
|DENY_GEOMETRY_SHADER_ROOT_ACCESS\
)"

RootFlags为根标志,对应于D3D12_ROOT_SIGNATURE_DESC结构体的Flags成员。在此,根标志实际上起到了两个作用:不指定ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT标志禁用输入布局从而禁用顶点缓冲(见2.4.1节);不指定ALLOW_STREAM_OUTPUT禁用流输出阶段(见2.4.1节中的图2-4)。

同时RootFlags中还指定了一系列DENY标志,MSDN上的Direct3D官方文档指出:将根签名的权限设置为“只允许真正需要访问它的着色器访问”可以提高性能。由于目前根签名中没有任何根形参或静态采样器,显然没有任何着色器需要访问根签名,因此全部禁用。

刚刚我们添加了一个头文件GRS.hlsli,接下来添加一个源文件GRS.hlsl,并编译生成根签名对象的字节码。右击“项目”中的“源文件”,在下拉菜单栏中选择“添加”→“新建项”,如图2-19所示。

图2-19 添加新建项

在“Visual C++”→“HLSL”中选择“HLSL标头文件(.hlsli)”,并将名称设置为“GRS.hlsl”,如图2-20所示。

图2-20 HLSL标头文件(.hlsli)

右击“GRS.hlsl”,在下拉菜单栏中选择“属性”,如图2-21所示。

选择“配置”中的“所有配置”和“平台”中的“所有平台”,确保项类型是HLSL编译器,如图2-22所示。

图2-21 项属性

图2-22 项类型

选择“配置”中的“所有配置”和“平台”中的“所有平台”,在“HLSL编译器”→“常规”中,将“入口点名称”设置为之前在#define中定义的根签名对象名“GRS”,“着色器类型”设置为“生成根签名对象”,“着色器模型”设置为“/rootsig_1_0”,如图2-23所示。

图2-23 着色器模型

选择“配置”中的“所有配置”和“平台”中的“所有平台”,在“HLSL编译器”→“输出文件”中,将“对象文件名”设置为“$(Local DebuggerWorkingDirectory)%(Filename).cso”,如图2-24所示。

图2-24 对象文件名

在GRS.hlsl中添加以下HLSL代码。

#include "GRS.hlsli"

接下来我们就可以编译生成根签名对象的字节码了,右击“GRS.hlsl”,在下拉菜单栏中选择“编译”,如图2-25所示。

图2-25 编译

输出编译成功的消息,如图2-26所示。

图2-26 编译成功消息

在编译生成根签名对象的字节码后,我们就可以在Direct3D 12中创建根签名了,Direct3D 12会在运行时将字节码解析成可以在特定的GPU上执行的本地代码。首先将字节码加载到系统内存中,为了方便,我使用了内存映射文件。如果读者想要更深入地了解内存映射文件,可以参阅《Windows 核心编程》(ISBN:9787302184003)。具体的代码如下。

HANDLE hGRSFile = CreateFileW(L"GRS.cso", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
LARGE_INTEGER szGRSFile;
GetFileSizeEx(hGRSFile, &szGRSFile);
HANDLE hGRSSection = CreateFileMappingW(hGRSFile, NULL, PAGE_READONLY, 0, szGRSFile.LowPart, NULL);
void *pGRSFile = MapViewOfFile(hGRSSection, FILE_MAP_READ, 0, 0, szGRSFile.LowPart);

随后,用ID3D12Device接口的CreateRootSignature方法创建根签名,该方法会将根签名的字节码解析成可以在特定的GPU上执行的本地代码,该方法的原型如下。

HRESULT STDMETHODCALLTYPE CreateRootSignature( 
                       UINT nodeMask,//[In] 
                       const void *pBlobWithRootSignature,//[In,size_is(blobLengthInBytes) 
                       SIZE_T blobLengthInBytes,//[In] 
                       REFIID riid,//[In] 
                       void **ppvRootSignature//[In] 
                       )

(1)nodeMask

支持多GPU节点适配器,一般都传入0X1表示第1个GPU节点。

(2)pBlobWithRootSignature

系统内存中的根签名的字节码的首地址。

(3)blobLengthInBytes

系统内存中的根签名的字节码的大小。

(4)riid和ppvRootSignature

MSDN上的Direct3D官方文档指出:根签名对象支持ID3D12RootSignature接口,可以使用IID_PPV_ARGS宏。

综上,代码如下。

ID3D12RootSignature *pGRS;
{
    //加载根签名字节码到内存中
    HANDLE hGRSFile = CreateFileW(L"GRS.cso", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    LARGE_INTEGER szGRSFile;
    GetFileSizeEx(hGRSFile, &szGRSFile);
    HANDLE hGRSSection = CreateFileMappingW(hGRSFile, NULL, PAGE_READONLY, 0, szGRSFile.LowPart, NULL);
    void *pGRSFile = MapViewOfFile(hGRSSection, FILE_MAP_READ, 0, 0, szGRSFile.LowPart);

    //创建根签名对象
    pD3D12Device->CreateRootSignature(0X1, pGRSFile, szGRSFile.LowPart, IID_PPV_ARGS(&pGRS));

    //在完成了根签名对象的创建之后,相关的字节码的内存就可以释放
    UnmapViewOfFile(pGRSFile);
    CloseHandle(hGRSSection);
    CloseHandle(hGRSFile);
}

在完成了根签名对象的创建后,即可以使用SetGraphicsRootSignature设置图形流水线的根签名。

pDirectCommandList->SetGraphicsRootSignature(pGRS);

正如2.4.1节中所述,GPU线程会为命令列表的每次执行维护一个状态集合。实际上,与根签名有关的状态有两个:图形流水线的根签名(SetGraphicsRootSignature)和计算流水线的根签名(SetComputeRootSignature)。本书会在第6章对计算流水线进行介绍。

3.图形流水线状态对象

Direct3D 12中大多数的图形流水线状态都通过图形流水线状态对象设置,可以用ID3D12Device接口的CreateGraphicsPipelineState方法创建图形流水线状态对象,该方法的原型如下。

HRESULT STDMETHODCALLTYPE CreateGraphicsPipelineState( 
                       const D3D12_GRAPHICS_PIPELINE_STATE_DESC *pDesc,//[In] 
                       REFIID riid,//[In] 
                       void **ppvObject//[Out] 
                       )

(1)pDesc

应用程序需要填充一个D3D12_GRAPHICS_PIPELINE_STATE_DESC来描述所要创建的图形流水线状态对象的属性,该结构体的定义如下。

struct D3D12_GRAPHICS_PIPELINE_STATE_DESC
{
    ID3D12RootSignature *pRootSignature;
    D3D12_SHADER_BYTECODE VS; 
    D3D12_SHADER_BYTECODE PS; 
    D3D12_SHADER_BYTECODE DS; 
    D3D12_SHADER_BYTECODE HS; 
    D3D12_SHADER_BYTECODE GS; 
    D3D12_STREAM_OUTPUT_DESC StreamOutput;
    D3D12_BLEND_DESC BlendState; 
    UINT SampleMask; 
    D3D12_RASTERIZER_DESC RasterizerState; 
    D3D12_DEPTH_STENCIL_DESC DepthStencilState;
    D3D12_INPUT_LAYOUT_DESC InputLayout;
    D3D12_INDEX_BUFFER_STRIP_CUT_VALUE IBStripCutValue;
    D3D12_PRIMITIVE_TOPOLOGY_TYPE PrimitiveTopologyType;
    UINT NumRenderTargets; 
    DXGI_FORMAT RTVFormats[ 8 ]; 
    DXGI_FORMAT DSVFormat; 
    DXGI_SAMPLE_DESC SampleDesc;
    UINT NodeMask; 
    D3D12_CACHED_PIPELINE_STATE CachedPSO; 
    D3D12_PIPELINE_STATE_FLAGS Flags; 
}

正如2.4.2节中所述,图形流水线的根签名通过SetGraphicsRootSignature设置,此处的pRootSignature仅用于有效性检查(所谓的有效性检查是指检查与结构体中的其他成员的一 致性)。

指定图元类型(不包括拓扑类型),根据上文,输入装配阶段的图元拓扑类型通过IASetPrimitiveTopology设置,这里的PrimitiveTopologyType仅用于有效性检查,但一般情况下都设置为与IASetPrimitiveTopology中设置的值一致。

IBStripCutValue

设置输入装配阶段的表示重启动条带的索引(见2.4.1节)。

D3D12_INDEX_BUFFER_STRIP_CUT_VALUE_DISABLED表示禁用重启动条带。

D3D12_INDEX_BUFFER_STRIP_CUT_VALUE_0xFFFF表示重启动条带的索引是UINT16(-1)。

D3D12_INDEX_BUFFER_STRIP_CUT_VALUE_0xFFFFFFFF表示重启动条带的索引是UINT32(-1)。

根据上文,由于根签名中没有设置ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT标志,因此输入装配阶段的输入布局被禁用,但是有效性检查是苛刻的,在这种情况下,必须将InputLayout的NumElements设置为0,pInputElementDescs设置为NULL。

表示着色器的字节码,是一个D3D12_SHADER_BYTECODE结构体,该结构体的pShader Bytecode成员表示系统内存中的着色器的字节码的首地址,BytecodeLength成员表示系统内存中的着色器字节码的大小。

本章禁用HS(Hull Shader,外壳着色器)、DS(Domain Shader,域着色器)和GS(Geometry Shader,几何着色器)(见2.4.1节中的图2-3),因此将pShaderByteCode设置为NULL,BytecodeLength设置为0。

根据上文,由于根签名中没有设置ALLOW_STREAM_OUTPUT标志,因此流输出阶段被禁用,但是有效性检查是苛刻的,在这种情况下,必须将StreamOutput的值全部设置为0,在C/C++中可以用“StreamOutput={};”方便地完成这个操作。

光栅化阶段的相关状态,D3D12_RASTERIZER_DESC的定义如下。

struct D3D12_RASTERIZER_DESC
{
    D3D12_FILL_MODE FillMode; 
    D3D12_CULL_MODE CullMode;
    BOOL FrontCounterClockwise; 
    INT DepthBias; 
    FLOAT DepthBiasClamp; 
    FLOAT SlopeScaledDepthBias; 
    BOOL DepthClipEnable; 
    BOOL MultisampleEnable;
    BOOL AntialiasedLineEnable;
    UINT ForcedSampleCount; 
    D3D12_CONSERVATIVE_RASTERIZATION_MODE ConservativeRaster; 
}

DepthClipEnable:深度裁剪,见2.4.1节。

DepthBias、DepthBiasClamp和SlopeScaledDepthBias:深度偏移,见2.4.1节。

FrontCounterClockwise:三角形图元的正背面,见2.4.1节。

CullMode:三角形图元的剔除模式,见2.4.1节。

FillMode;直线图元是否深度偏移和三角形图元的填充模式,见2.4.1节。

MultisampleEnable,AntialiasedLineEnable和ForcedSampleCount:与多重采样有关,暂且设为FALSE,FALSE和0表示禁用。

ConservativeRaster:Direct3D 12保守光栅化模式,暂且设为D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF表示禁用。

根据上文,输出混合阶段的渲染目标视图和深度模板视图通过OMSetRenderTargets进行设置。这里的NumRenderTargets、RTVFormats和DSVFormat仅用于有效性检查,但一般情况下都设置为与OMSetRenderTargets中设置的值一致。但是有效性检查是苛刻的,对于没有用到的渲染目标视图或深度模板视图的格式必须设置为DXGI_FORMAT_UNKNOWN。

同样地,这也只是用于有效性检查,例如用交换链缓冲作为渲染目标的情形、多重采样已经在创建交换链时,由传入的DXGI_SWAP_CHAIN_DESC结构体中的SampleDesc成员进行设置。

但一般情况下都设置为与实际时的情形相同,这里将SampleDesc的Count成员设置为1,Quality成员设置为0表示禁用多重采样。

与MSAA(Multi Sample Anti Aliasing,多重采样反走样,见2.1.4节)有关,在此暂且设置为0XFFFFFF。

在此暂且将DepthEnable和StencilEnable成员设置为FALSE,表示禁用深度测试和模板测试。同样地,有效性检查是苛刻的,必须将DepthStencilState中的其他的值全部设置为0,在C/C++中可以用“DepthStencilState={};”方便地完成这个操作。

在此暂且将AlphaToCoverageEnable和IndependentBlendEnable设置为FALSE;将Render Target[0]的BlendEnable和LogicOpEnable设置为FALSE,表示禁用融合操作和逻辑操作,但是有效性检查是苛刻的,在这种情况下,必须将BlendState中的其他的值全部设置为0,在C/C++中可以用“BlendState={};”方便地完成这个操作;将RenderTargetWriteMask设置为D3D12_COLOR_WRITE_ENABLE_ALL表示根据SV_TARGETn(见2.4.1节)的值写入到渲染目标时,所有的分量都会被写入,值得注意的是,RenderTargetWriteMask始终生效,与融合是否被禁用无关。

支持多GPU节点适配器,一般传入0X1表示使用适配器中的第1个GPU节点。

缓存的图形流水线状态对象,暂且将pCachedBlob设置为NULL,CachedBlobSizeInBytes设置为0,表示不使用缓存的图形流水线状态对象。

一般设置为D3D12_PIPELINE_STATE_FLAG_NONE。

(2)riid和ppvObject

MSDN上的Direct3D官方文档指出:图形流水线状态对象支持ID3D12PipelineState接口,可以使用IID_PPV_ARGS宏。

4.顶点着色器和像素着色器

接下来介绍用HLSL编译生成顶点着色器和像素着色器的字节码的过程。

(1)顶点着色器

首先介绍如何用HLSL编译生成顶点着色器,右击项目中源文件,在下拉菜单栏中选择“添加”→“新建项”,如图2-27所示。

图2-27 添加新建项

在“Visual C++”→“HLSL”中选择“顶点着色器文件(.hlsl)”,并将名称设置为“VS.hlsl”,如图2-28所示。

图2-28 顶点着色器文件(.hlsl)

右击“VS.hlsl”,在下拉菜单栏中选择“属性”,如图2-29所示。

选择“配置”中的“所有配置”和“平台”中的“所有平台”,在“HLSL编译器”→“常规”中,将“着色器模型”设置为“5.1”,如图2-30所示。

图2-29 项属性

图2-30 着色器模型

选择“配置”中的“所有配置”和“平台”中的“所有平台”,在“HLSL编译器”→“输出文件”中,将“对象文件名”设置为“$(Local DebuggerWorkingDirectory)%(Filename).cso”,如图2-31所示。

图2-31 对象文件名

删去VS.hlsl中自动生成的代码并添加如下代码。

#include"GRS.hlsli"//根签名对象

struct Vertex_IA_OUT//见2.4.1节
{
    uint vid:SV_VERTEXID;
    uint iid:SV_INSTANCEID;
};

struct Vertex_VS_OUT//见2.4.1节
{
    float4 pos:SV_POSITION;
    float4 color:UserDefine0;//冒号后面的称为语义,用于指定变量的含义
                             //其中有SV_前缀的一般都是系统预定义的,具有标准的含义(即2.4.1节中所介绍的各种含义),类似C/C++中的关键字
                             //UserDefine0表明是应用程序自定义的变量
                             //其中UserDefine称为语义名,类似C/C++中的标识符,可以随意定义,但不能与系统预定义的SV_前缀的语义名重名
                             //0称为语义索引,当语义索引为0的情况下可以省略
                             //根据2.4.1节,这个结构体会被传入到像素着色器中
                             //因此像素着色器中的语义名必须与此相同,这样才可以正常链接
                             //也就是说顶点着色器和像素着色器的源代码中的相应结构体中的变量名可以不同,但语义名必须相同
};

[RootSignature(GRS)]//为顶点着色器指定根签名
Vertex_VS_OUT main(Vertex_IA_OUT vertex)//正如2.4.1节中所述,图形流水线会对IAOutputVertexArray中的每一个顶点执行一次着色器
{
    //在后文中会调用DrawInstanced(3,1,0,0)进行绘制,因此我们用顶点着色器为不同的顶点生成不同的Vertex_VS_OUT
    Vertex_VS_OUT rtval;
    rtval.pos = float4(0.0f, 0.0f, 0.0f, 0.0f);
    rtval.color = float4(0.0f, 0.0f, 0.0f, 0.0f);
    if (vertex.iid == 0)
    {
          if (vertex.vid == 0)
          {
                rtval.pos = float4(0.0f, 0.5f, 0.5f, 1.0f);
                rtval.color = float4(1.0f, 0.0f, 0.0f, 1.0f);//红色
          }
          else if (vertex.vid == 1)
          {
                rtval.pos = float4(0.5f, -0.5f, 0.5f, 1.0f);
                rtval.color = float4(0.0f, 1.0f, 0.0f, 1.0f);//绿色
          }
          else if (vertex.vid == 2)
          {
                rtval.pos = float4(-0.5f, -0.5f, 0.5f, 1.0f);
                rtval.color = float4(0.0f, 0.0f, 1.0f, 1.0f);//蓝色
          }
    }
    return rtval;
}

接下来就可以编译生成顶点着色器的字节码了。

右击“VS.hlsl”,在下拉菜单栏中选择“编译”,如图2-32所示。

图2-32 编译

输出编译成功的消息,如图2-33所示。

图2-33 编译成功消息

与根签名中一样,使用内存映射文件将顶点着色器字节码加载到系统内存中,相关的代码如下。

HANDLE hVSFile = CreateFileW(L"VS.cso", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
LARGE_INTEGER szVSFile;
GetFileSizeEx(hVSFile, &szVSFile);
HANDLE hVSSection = CreateFileMappingW(hVSFile, NULL, PAGE_READONLY, 0, szVSFile.LowPart, NULL);
void *pVSFile = MapViewOfFile(hVSSection, FILE_MAP_READ, 0, 0, szVSFile.LowPart);

(2)像素着色器

首先介绍如何用HLSL编译生成像素着色器,具体的操作方式与顶点着色器的情形类似,只不过在“Visual C++”→“HLSL”中选择“像素着色器文件(.hlsl)”,并将“名称”设置为“PS.hlsl”,如图2-34所示。

图2-34 像素着色器文件(.hlsl)

同样地,选择“配置”中的“所有配置”和“平台”中的“所有平台”,在“HLSL编译器”→“常规”中,将“着色器模型”设置为“5.1”,在“HLSL编译器”→“输出文件”中,将“对象文件名”设置为“$(LocalDebuggerWorkingDirectory)%(Filename).cso”。

删去PS.hlsl中自动生成的代码并添加如下代码。

#include"GRS.hlsli"

struct Vertex_VS_OUT//应当与顶点着色器中的定义一致,见2.4.1节
{
    float4 pos:SV_POSITION;
    float4 color:UserDefine0;//经过了光栅化阶段的插值,见2.4.1节
};

struct Pixel_PS_OUT
{
    float4 color:SV_TARGET0;//表示写入到第1个渲染目标视图,见2.4.1节
};

[RootSignature(GRS)]//为像素着色器指定根签名
Pixel_PS_OUT main(Vertex_VS_OUT pixel//,uint pid:SV_PRIMITIVEID)
                                        )//根据2.4.1节,图形流水线还会输入SV_PRIMITIVEID,实际上这个值是可选的,如果像素着色器不使用这个值,那么图形流水线也不会将该值输入到像素着色器,实际上顶点着色器中的SV_VERTEXID和SV_INSTANCEID也是如此
{
    Pixel_PS_OUT rtval;
    rtval.color = pixel.color;//输入到像素着色器中的 Vertex_VS_OUT中的color值是经过光栅化阶段插值的到的,见2.4.1节
    return rtval;
}

接下来就可以编译生成像素着色器的字节码了,右击“PS.hlsl”,在下拉菜单栏中选择“编译”,如图2-35所示。

图2-35 编译

输出编译成功的消息,如图2-36所示。

图2-36 编译成功消息

同样地,使用内存映射文件将顶点着色器字节码加载到系统内存中,相关的代码如下。

HANDLE hPSFile = CreateFileW(L"PS.cso", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
LARGE_INTEGER szPSFile;
GetFileSizeEx(hPSFile, &szPSFile);
HANDLE hPSSection = CreateFileMappingW(hPSFile, NULL, PAGE_READONLY, 0, szPSFile.LowPart, NULL);
void *pPSFile = MapViewOfFile(hPSSection, FILE_MAP_READ, 0, 0, szPSFile.LowPart);

综上,代码如下。

ID3D12PipelineState *pGraphicPipelineState;
{
    HANDLE hVSFile = CreateFileW(L"VS.cso", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    LARGE_INTEGER szVSFile;
    GetFileSizeEx(hVSFile, &szVSFile);
    HANDLE hVSSection = CreateFileMappingW(hVSFile, NULL, PAGE_READONLY, 0, szVSFile.LowPart, NULL);
    void *pVSFile = MapViewOfFile(hVSSection, FILE_MAP_READ, 0, 0, szVSFile.LowPart);

    HANDLE hPSFile = CreateFileW(L"PS.cso", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    LARGE_INTEGER szPSFile;
    GetFileSizeEx(hPSFile, &szPSFile);
    HANDLE hPSSection = CreateFileMappingW(hPSFile, NULL, PAGE_READONLY, 0, szPSFile.LowPart, NULL);
    void *pPSFile = MapViewOfFile(hPSSection, FILE_MAP_READ, 0, 0, szPSFile.LowPart);

    D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc;
    psoDesc.pRootSignature = pRS;

    //在根签名没有指定ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT标志的情况下,InputLayout必须为空
    psoDesc.InputLayout.NumElements = 0;
    psoDesc.InputLayout.pInputElementDescs = NULL;

    //禁用重启动条带
    psoDesc.IBStripCutValue = D3D12_INDEX_BUFFER_STRIP_CUT_VALUE_DISABLED;
    //用于有效性检查,一般和IASetPrimitiveTopology中设置的值一致
    psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;

    //顶点着色器和像素着色器
    psoDesc.VS.pShaderBytecode = pVSFile;
    psoDesc.VS.BytecodeLength = szVSFile.LowPart;
    psoDesc.PS.pShaderBytecode = pPSFile;
    psoDesc.PS.BytecodeLength = szPSFile.LowPart;
    //以下着色器暂时不使用
    psoDesc.HS.pShaderBytecode = NULL;
    psoDesc.HS.BytecodeLength = 0;
    psoDesc.DS.pShaderBytecode = NULL;
    psoDesc.DS.BytecodeLength = 0;
    psoDesc.GS.pShaderBytecode = NULL;
    psoDesc.GS.BytecodeLength = 0;

    //在根签名没有指定ALLOW_STREAM_OUTPUT标志的情况下,StreamOutputr必须全部赋值为0
    psoDesc.StreamOutput = {};

    //光栅化阶段
    psoDesc.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
    psoDesc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
    psoDesc.RasterizerState.FrontCounterClockwise = FALSE;
    psoDesc.RasterizerState.DepthBias = 0;
    psoDesc.RasterizerState.DepthBiasClamp = 0.0f;
    psoDesc.RasterizerState.SlopeScaledDepthBias = 0.0f;
    psoDesc.RasterizerState.DepthClipEnable = FALSE;
    psoDesc.RasterizerState.MultisampleEnable = FALSE;
    psoDesc.RasterizerState.AntialiasedLineEnable = FALSE;
    psoDesc.RasterizerState.ForcedSampleCount = 0U;
    psoDesc.RasterizerState.ConservativeRaster = D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF;

    //有效性检查
    psoDesc.NumRenderTargets = 1;
    psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
    psoDesc.RTVFormats[1] = DXGI_FORMAT_UNKNOWN;//没有用到的渲染目标,要求全部设置为DXGI_FORMAT_UNKNOWN
    psoDesc.RTVFormats[2] = DXGI_FORMAT_UNKNOWN;
    psoDesc.RTVFormats[3] = DXGI_FORMAT_UNKNOWN;
    psoDesc.RTVFormats[4] = DXGI_FORMAT_UNKNOWN;
    psoDesc.RTVFormats[5] = DXGI_FORMAT_UNKNOWN;
    psoDesc.RTVFormats[6] = DXGI_FORMAT_UNKNOWN;
    psoDesc.RTVFormats[7] = DXGI_FORMAT_UNKNOWN;

    psoDesc.DSVFormat = DXGI_FORMAT_UNKNOWN;//在没有用到深度模板缓冲的情况下,要求设置为DXGI_FORMAT_UNKNOWN

    //多重采样状态,与渲染目标视图中一致
    psoDesc.SampleDesc.Count = 1;
    psoDesc.SampleDesc.Quality = 0;

    //采样掩码
    psoDesc.SampleMask = 0XFFFFFFFF;

    //禁用深度测试和模板测试
    psoDesc.DepthStencilState = {};//有效性检查要求其他成员全部赋值为0
    psoDesc.DepthStencilState.DepthEnable = FALSE;
    psoDesc.DepthStencilState.StencilEnable = FALSE;

    //禁用融合操作
    psoDesc.BlendState = D3D12_BLEND_DESC{};//有效性检查要求其他成员全部赋值为0
    psoDesc.BlendState.AlphaToCoverageEnable = FALSE;
    psoDesc.BlendState.IndependentBlendEnable = FALSE;
    psoDesc.BlendState.RenderTarget[0].BlendEnable = FALSE;
    psoDesc.BlendState.RenderTarget[0].LogicOpEnable = FALSE;
    psoDesc.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;

    psoDesc.NodeMask = 0X1;

    psoDesc.CachedPSO.pCachedBlob = NULL;
    psoDesc.CachedPSO.CachedBlobSizeInBytes = 0;

    psoDesc.Flags = D3D12_PIPELINE_STATE_FLAG_NONE;

    //创建图形流水线状态对象
    pD3D12Device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pGraphicPipelineState));

    //在完成了图形流水线状态对象的创建之后,相关的字节码的内存就可以释放
    UnmapViewOfFile(pVSFile);
    CloseHandle(hVSSection);
    CloseHandle(hVSFile);

    UnmapViewOfFile(pPSFile);
    CloseHandle(hPSSection);
    CloseHandle(hPSFile);
}

在完成了图形流水线状态对象的创建后,设置图形流水线的图形流水线状态对象的方法有以下两种。

5.小结

以上我们已经完成了对图形流水线的各个状态的设置,与以归零方式渲染到交换链缓冲(见2.3.1节)时一样,以绘制方式渲染到交换链缓冲时,同样涉及到子资源的权限的问题。

在GPU线程直接命令队列上执行DrawInstance或DrawIndexedInstanced命令时,作为渲染目标的子资源(即用OMSetRenderTargets设置的渲染目标视图的底层的子资源)对图形/计算类的权限必须可作为渲染目标(D3D12_RESOURCE_STATE_RENDER_TARGET)。

可以用ID3D12GraphicsCommandList接口的ResourceBarrier方法,在命令列表中添加ResourceBarrier命令来转换子资源对该命令所在的命令列表、所在的命令队列、所属的类的权限,本书已经在2.3.1节中对该方法的原型进行了详尽的介绍,在此不再赘述。

同样地,在GPU线程执行命令队列中IDXGISwapChain::Present命令之前,需要再次执行转换资源屏障,将资源相对图形/计算类的权限转换为公共(D3D12_RESOURCE_STATE_COMMON)。

在此,我们将之前介绍的所有代码进行整合。综上所述,rendermain.cpp中的代码如下。

#include <sdkddkver.h>
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <dxgi.h>
#include <d3d12.h>

DWORD WINAPI RenderThreadMain(LPVOID lpThreadParameter)
{

     HWND hWnd = static_cast<HWND>(lpThreadParameter);//即我们在main.cpp中传入的窗口句柄

//启用调试层
#if defined(_DEBUG)
     {
          ID3D12Debug *pD3D12Debug;
          if(SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&pD3D12Debug))))
          {
               pD3D12Debug->EnableDebugLayer();
          }
          pD3D12Debug->Release();
     }
#endif

//创建设备
     IDXGIFactory *pDXGIFactory;
     CreateDXGIFactory(IID_PPV_ARGS(&pDXGIFactory));

     ID3D12Device *pD3D12Device = NULL;
     {
          IDXGIAdapter *pDXGIAdapter;
          //遍历所有的适配器进行尝试,优先尝试主适配器
          for (UINT i = 0U; SUCCEEDED(pDXGIFactory->EnumAdapters(i, &pDXGIAdapter)); ++i)
          {
               if (SUCCEEDED(D3D12CreateDevice(pDXGIAdapter, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&pD3D12Device))))
               {
                    pDXGIAdapter->Release();//实际上,在完成创建设备对象以后,DXGI适配器对象就可以释放
                    break;
               }
               pDXGIAdapter->Release();
          }
     }

//创建命令队列
     ID3D12CommandQueue *pDirectCommandQueue;    
     {
       D3D12_COMMAND_QUEUE_DESC cqdc;
       cqdc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;//创建一个直接命令队列,因为IDXGISwapChain::Present只能在直接命令队列上执行
       cqdc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL;
       cqdc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
       cqdc.NodeMask = 0X1;
       pD3D12Device->CreateCommandQueue(&cqdc, IID_PPV_ARGS(&pDirectCommandQueue));
     }

//创建交换链
     IDXGISwapChain *pDXGISwapChain;
     {
       DXGI_SWAP_CHAIN_DESC scdc;
       scdc.BufferDesc.Width = 0U;
       scdc.BufferDesc.Height = 0U;
       scdc.BufferDesc.RefreshRate.Numerator = 60U;
       scdc.BufferDesc.RefreshRate.Denominator = 1U;
       scdc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
       scdc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
       scdc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
       scdc.SampleDesc.Count = 1U;//注意 多重采样的设置方式
       scdc.SampleDesc.Quality = 0U;
       scdc.BufferUsage = DXGI_USAGE_BACK_BUFFER|DXGI_USAGE_RENDER_TARGET_OUTPUT;
       scdc.BufferCount = 2;
       scdc.OutputWindow = hWnd;//即我们在main.cpp中传入的窗口句柄
       scdc.Windowed = TRUE;//设置为窗口模式更加友好
       scdc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;//读者也可以使用  
     DXGI_SWAP_EFFECT_FLIP_DISCARD
       scdc.Flags = 0U;
       pDXGIFactory->CreateSwapChain(pDirectCommandQueue, &scdc, &pDXGISwapChain);
     }
     pDXGIFactory->Release();//实际上,在完成创建交换链对象以后,DXGI类厂对象就可以释放

//创建渲染目标视图
     ID3D12Resource *pFrameBuffer;
     pDXGISwapChain->GetBuffer(0, IID_PPV_ARGS(&pFrameBuffer ));

     ID3D12DescriptorHeap *pRTVHeap;
     {
       D3D12_DESCRIPTOR_HEAP_DESC RTVHeapDesc    
     ={ D3D12_DESCRIPTOR_HEAP_TYPE_RTV ,1,D3D12_DESCRIPTOR_HEAP_FLAG_NONE,0X1 };
       pD3D12Device->CreateDescriptorHeap(&RTVHeapDesc, IID_PPV_ARGS(&pRTVHeap));    
     }
     pD3D12Device->CreateRenderTargetView(pFrameBuffer, NULL, pRTVHeap->GetCPUDescriptorHandleForHeapStart());

//创建根签名
     ID3D12RootSignature *pGRS;
     {
       //加载根签名字节码到内存中
       HANDLE hGRSFile = CreateFileW(L"GRS.cso", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
       LARGE_INTEGER szGRSFile;
       GetFileSizeEx(hGRSFile, &szGRSFile);
       HANDLE hGRSSection = CreateFileMappingW(hGRSFile, NULL, PAGE_READONLY, 0, szGRSFile.LowPart, NULL);
       void *pGRSFile = MapViewOfFile(hGRSSection, FILE_MAP_READ, 0, 0, szGRSFile.LowPart);

       //创建根签名对象
       pD3D12Device->CreateRootSignature(0X1, pGRSFile, szGRSFile.LowPart, IID_PPV_ ARGS(&pGRS));

       //在完成了根签名对象的创建之后,相关的字节码的内存就可以释放
       UnmapViewOfFile(pGRSFile);
       CloseHandle(hGRSSection);
       CloseHandle(hGRSFile);
     }

//创建图形流水线状态对象
     ID3D12PipelineState *pGraphicPipelineState;
     {
       HANDLE hVSFile = CreateFileW(L"VS.cso", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
       LARGE_INTEGER szVSFile;
       GetFileSizeEx(hVSFile, &szVSFile);
       HANDLE hVSSection = CreateFileMappingW(hVSFile, NULL, PAGE_READONLY, 0, szVSFile.LowPart, NULL);
       void *pVSFile = MapViewOfFile(hVSSection, FILE_MAP_READ, 0, 0, szVSFile.LowPart);

       HANDLE hPSFile = CreateFileW(L"PS.cso", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
       LARGE_INTEGER szPSFile;
       GetFileSizeEx(hPSFile, &szPSFile);
       HANDLE hPSSection = CreateFileMappingW(hPSFile, NULL, PAGE_READONLY, 0, szPSFile.LowPart, NULL);
       void *pPSFile = MapViewOfFile(hPSSection, FILE_MAP_READ, 0, 0, szPSFile.LowPart);

       D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc;
       psoDesc.pRootSignature = pGRS;

       //在根签名没有指定ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT标志的情况下,InputLayout必须为空
       psoDesc.InputLayout.NumElements = 0;
       psoDesc.InputLayout.pInputElementDescs = NULL;

       //禁用重启动条带
       psoDesc.IBStripCutValue = D3D12_INDEX_BUFFER_STRIP_CUT_VALUE_DISABLED;
       //用于有效性检查,一般和IASetPrimitiveTopology中设置的值一致
       psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;

       //顶点着色器和像素着色器
       psoDesc.VS.pShaderBytecode = pVSFile;
       psoDesc.VS.BytecodeLength = szVSFile.LowPart;
       psoDesc.PS.pShaderBytecode = pPSFile;
       psoDesc.PS.BytecodeLength = szPSFile.LowPart;
       //以下着色器暂时不使用
       psoDesc.HS.pShaderBytecode = NULL;
       psoDesc.HS.BytecodeLength = 0;
       psoDesc.DS.pShaderBytecode = NULL;
       psoDesc.DS.BytecodeLength = 0;
       psoDesc.GS.pShaderBytecode = NULL;
       psoDesc.GS.BytecodeLength = 0;

       //在根签名没有指定ALLOW_STREAM_OUTPUT标志的情况下,StreamOutputr必须全部赋值为0
       psoDesc.StreamOutput = {};

       //光栅化阶段
       psoDesc.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
       psoDesc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
       psoDesc.RasterizerState.FrontCounterClockwise = FALSE;
       psoDesc.RasterizerState.DepthBias = 0;
       psoDesc.RasterizerState.DepthBiasClamp = 0.0f;
       psoDesc.RasterizerState.SlopeScaledDepthBias = 0.0f;
       psoDesc.RasterizerState.DepthClipEnable = FALSE;
       psoDesc.RasterizerState.MultisampleEnable = FALSE;
       psoDesc.RasterizerState.AntialiasedLineEnable = FALSE;
       psoDesc.RasterizerState.ForcedSampleCount = 0U;
       psoDesc.RasterizerState.ConservativeRaster = D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF;

       //有效性检查
       psoDesc.NumRenderTargets = 1;
       psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
       psoDesc.RTVFormats[1] = DXGI_FORMAT_UNKNOWN;//没有用到的渲染目标,要求全部设置为DXGI_FORMAT_UNKNOWN
       psoDesc.RTVFormats[2] = DXGI_FORMAT_UNKNOWN;
       psoDesc.RTVFormats[3] = DXGI_FORMAT_UNKNOWN;
       psoDesc.RTVFormats[4] = DXGI_FORMAT_UNKNOWN;
       psoDesc.RTVFormats[5] = DXGI_FORMAT_UNKNOWN;
       psoDesc.RTVFormats[6] = DXGI_FORMAT_UNKNOWN;
       psoDesc.RTVFormats[7] = DXGI_FORMAT_UNKNOWN;

       psoDesc.DSVFormat = DXGI_FORMAT_UNKNOWN;//在没有用到深度模板缓冲的情况下,要求设置为DXGI_FORMAT_UNKNOWN

       //多重采样状态,与渲染目标视图中一致
       psoDesc.SampleDesc.Count = 1;
       psoDesc.SampleDesc.Quality = 0;

       //采样掩码
       psoDesc.SampleMask = 0XFFFFFFFF;

       //禁用深度测试和模板测试
       psoDesc.DepthStencilState = {};//有效性检查要求其他成员全部赋值为0
       psoDesc.DepthStencilState.DepthEnable = FALSE;
       psoDesc.DepthStencilState.StencilEnable = FALSE;

       //禁用融合操作
       psoDesc.BlendState = D3D12_BLEND_DESC{};//有效性检查要求其他成员全部赋值为0
       psoDesc.BlendState.AlphaToCoverageEnable = FALSE;
       psoDesc.BlendState.IndependentBlendEnable = FALSE;
       psoDesc.BlendState.RenderTarget[0].BlendEnable = FALSE;
       psoDesc.BlendState.RenderTarget[0].LogicOpEnable = FALSE;
       psoDesc.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;

       psoDesc.NodeMask = 0X1;

       psoDesc.CachedPSO.pCachedBlob = NULL;
       psoDesc.CachedPSO.CachedBlobSizeInBytes = 0;

       psoDesc.Flags = D3D12_PIPELINE_STATE_FLAG_NONE;

       //创建图形流水线状态对象
       pD3D12Device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pGraphicPipelineState));

       //在完成了图形流水线状态对象的创建之后,相关的字节码的内存就可以释放
       UnmapViewOfFile(pVSFile);
       CloseHandle(hVSSection);
       CloseHandle(hVSFile);

       UnmapViewOfFile(pPSFile);
       CloseHandle(hPSSection);
       CloseHandle(hPSFile);
     }

//创建命令分配器和命令列表
     ID3D12CommandAllocator *pDirectCommandAllocator;
     pD3D12Device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, 
     IID_PPV_ARGS(&pDirectCommandAllocator));

     ID3D12GraphicsCommandList *pDirectCommandList;
     pD3D12Device->CreateCommandList(0X1, D3D12_COMMAND_LIST_TYPE_DIRECT, 
     pDirectCommandAllocator, NULL, IID_PPV_ARGS(&pDirectCommandList));

//零碎的API
     pDirectCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);//设置图元拓扑类型为三角形条带
     D3D12_VIEWPORT vp = { 0.0f,0.0f,800.0f,600.0f,0.0f,1.0f };//正如2.4.1节中所述,Direct3D 12规定,MinDepth和MaxDepth必须都在[0.0f,1.0f]内
     pDirectCommandList->RSSetViewports(1, &vp);//将视口变换的效果设置为将整个归一化坐标系变换到整个整个渲染目标视图
     D3D12_RECT sr = { 0,0,800,600 };
     pDirectCommandList->RSSetScissorRects(1, &sr);//将剪裁区域设置为整个渲染目标视图
     pDirectCommandList->OMSetRenderTargets(1, &pRTVHeap->GetCPUDescriptorHandleFor HeapStart(),FALSE,NULL);//设置渲染目标

//设置根签名
     pDirectCommandList->SetGraphicsRootSignature(pGRS);

//设置图形流水线状态对象
     pDirectCommandList->SetPipelineState(pGraphicPipelineState);

//资源权限
     D3D12_RESOURCE_BARRIER CommonToRendertarget = { D3D12_RESOURCE_BARRIER_TYPE_TRANSITION ,D3D12_RESOURCE_BARRIER_FLAG_NONE,{ 
pFrameBuffer,0,D3D12_RESOURCE_STATE_COMMON ,D3D12_RESOURCE_STATE_RENDER_TARGET } };
     pDirectCommandList->ResourceBarrier(1, &CommonToRendertarget);

//绘制
     pDirectCommandList->DrawInstanced(3, 1, 0, 0);

     D3D12_RESOURCE_BARRIER RendertargetToCommon = { D3D12_RESOURCE_BARRIER_TYPE_TRANSITION ,D3D12_RESOURCE_BARRIER_FLAG_NONE,{ 
pFrameBuffer,0,D3D12_RESOURCE_STATE_RENDER_TARGET ,D3D12_RESOURCE_STATE_COMMON } };
     pDirectCommandList->ResourceBarrier(1, &RendertargetToCommon);

//执行命令列表
     pDirectCommandList->Close();
     pDirectCommandQueue->ExecuteCommandLists(1, reinterpret_cast<ID3D12CommandList **>(&pDirectCommandList));

//呈现
     pDXGISwapChain->Present(0, 0);

     return 0U;
}

再次调试程序,可以看到以下三角形,如图2-37所示。读者可以根据实验的结果体会光栅化阶段的插值(见2.4.1节)。

图2-37 绘制三角形

本章介绍了用Direct3D 12渲染到窗口表面的两种方式——归零和绘制,并分别给出了一个示例程序。尽管示例程序非常简单,但还是涉及到了较多的概念。本章只对这些概念进行了简单的介绍,本书会在后面章节中对这些概念进行详细的介绍。


相关图书

Python面向对象编程:构建游戏和GUI
Python面向对象编程:构建游戏和GUI
精通游戏测试(第3版)
精通游戏测试(第3版)
罗布乐思开发官方指南 从入门到实践
罗布乐思开发官方指南 从入门到实践
游戏引擎原理与实践 卷2 高级技术
游戏引擎原理与实践 卷2 高级技术
游戏数值设计
游戏数值设计
游戏引擎原理与实践 卷1 基础框架
游戏引擎原理与实践 卷1 基础框架

相关文章

相关课程