DirectX12(D3D12)基础教程(六)——多线程渲染
目录 1、前言2、为什么要多线程渲染3、多线程3.1、什么是线程3.2、进程的主线程3.3、线程的入口函数3.4、创建线程3.5、CreateThread示例3.6、C/C++创建线程函数(VC版)3.7、_beginthreadex示例3.8、线程退出3.9、线程的暂停(挂起)和恢复3.10、线程的暂停式创建3.11、线程栈3...
目录
1、前言
时间总是过得很快,不知不觉,2018年也就结束了,本来我想在2018年底前,将这个基本系列的教程赶紧结束掉,但实际我遇到的问题超出了我的想象,从而未能如愿。
于是我一如既往的先试图放松下自己的大脑,并且尽力调整自己的状态,我相信硬怼是不能解决问题的。期间除了忙于工作外,又与家人及两个孩子度过了愉快的元旦假期。并且在几位良师益友的建议下,阅读完了余秋雨先生的《文化苦旅(新版)》,心灵受到了深深的震撼,灵魂又得到了一次前所未有的升华,在此先向老先生致以崇高的敬意!他让我重新明白了什么是文明,什么是文化,受益良多。
近两个月以来,期间除了编写示例代码遇到了一些意外的挫折外,还发生了很多其他的事情,就不一一诉说了。本质上也许与我们的教程无关,但那却是实实在在的生活。因为生活使我看问题更加立体和敏锐,也使我开心愉快,所有才会有更高的激情与勇气去面对任何复杂困难的挑战。
最终D3D12多线程示例程序终于成功的完成了。现在看来所谓的困难只怪自己没有好好看D3D12多线程渲染的官方示例代码。其实所谓的问题只是个稍微开下脑洞的问题,后面我会详细介绍思路和方法,防止大家也在这里因为思路问题而卡壳。
OK,闲言碎语不多讲,言归正传,本章我们的目的就是使用多线程渲染,绘制和渲染一个稍微复杂点的场景,并且明白多线程渲染的关键点。当然我们的目的依然是通过简洁的C风格代码,去学习和掌握技术的关键点。本章示例运行后的效果如下:
2、为什么要多线程渲染
1月8日起,为期三天的2019 CES(国际消费类电子产品展览会International Consumer Electronics Show,简称CES)在美国拉斯维加斯正在如火如荼的召开。会上几大硬件巨头Intel、AMD、Nvidia等,都纷纷献上了新的芯片和其他相关软硬件产品。尤其AMD更是展示了即将上市的ZEN2架构的多款多核CPU产品。
如今大到服务器CPU,再到手机CPU、以致到小小的儿童智能玩具上的CPU,基本都是多核架构了。这导致现在如果一款软件说自己不是支持多线程架构的,都不好意思跟别人打招呼了。
当然透过现象看本质,我们必须要思考清楚的问题就是为什么我们需要多线程的支持,更进一步对我们的3D应用程序来说,为什么需要多线程渲染的支持?
首先这主要基于这样一个基本需求目标——那就是各种实时3D交互应用(引擎、游戏、教学、仿真等)对性能都有极高的要求。甚至可以这样说所有3D软件编程的一切都是为了追求极致的性能。而追求性能的终极目标就是让3D场景以假乱真,从而全面释放人类在现实中所有无法简单实现的梦想、幻想和理想。这点建议你去感受下那些让你从感官甚至到灵魂都深深受到震撼的3D电影、3D游戏等就明白了。基于此种目的,实时光线追踪渲染也就应运而生。
其次,就目前的CPU技术发展水平来说,想要再去提高单个线程核心的执行效能已经很困难了。或者说投入的各种成本和实质能够得到的单核效能提升已经严重不成比例了(想象一下对数曲线,我就不画图了,懒。)。此时将多个单CPU核心拼接或扩展为多个在一个die上的多核CPU,甚至使用超线程技术就成为了另一条高效提升CPU执行效能的有效技术路线。而这就为我们编写真正可以并行执行的多线程程序提供了物理基础。
又次,在CPU不断进化性能的同时,现代的GPU效能也得到了突飞猛进的发展。现代的GPU从诞生之刻起就是并行计算架构的,因为GPU面临的问题就是需要用类似的指令来处理大量类似的数据(SIMD),以至于现在对并行计算要求很高的AI核心训练计算主要依托于GPU核心来进行。但再强悍的GPU碍于其架构的特殊要求,并不能直接独立工作,总是需要CPU来管理和配合GPU计算的执行。在我们的渲染执行中如此、在时下火爆的AI计算中也是如此。因此为了管理和配合好GPU的高效执行,就需要CPU也变得更加强悍。否则因为CPU自身效能低下而无法分配足够的任务或数据给GPU,导致GPU闲置。甚至在传统的单核串行渲染框架下,CPU和GPU成了交替工作的低效工作模式,最终造成严重的资源浪费。结果就是无法简单的实现更好的渲染效果,3D渲染画质提升就遇到了很多的瓶颈。而使用多线程,并充分利用多核CPU的优势,则可以简单高效的提高分配给GPU任务或数据的效能,从而使GPU高效的运行完成渲染、计算等任务。同时也可以实现CPU和GPU都高效并行运行的工作模式,最终可以充分榨取CPU和GPU两边的性能,使我们可以实现更加真实和更高质量的渲染画质。
再次,就3D场景的渲染本身来说,由于模型数据及相关算法的天然并行性,所以很多计算处理都是可以并行执行的。而GPU很早就充分利用了这个特性,比如常见的顶点处理、像素处理本质上就是可以并行处理的。通常现代GPU上都会有上千个流处理器(可以理解为简化的小CPU核心),可以同时并行处理每个模型的不同顶点或光栅化之后的片元上的不同像素。再往前追溯在一个场景中不同物体的物理变换计算、AI计算等其实也都可以并行处理,而这些计算中有一些已经可以被GPU处理,而另一些相对复杂的就可以通过多个CPU线程来并行处理。
综上,我们就可以简单的理解成:为了极致的3D渲染画面效果,我们就需要极致的性能支持,而极致的性能就需要我们进行多线程渲染。
最后我们可以补充一个不太恰当的比喻来形象的思考这样的情形,以方便大家理解。我们将一个CPU核心想象成一匹马,而将GPU想象成一辆车,虽然二者都可以完成运输(喻指对数据的计算)的功能,但是我们可以发现,如果只是一匹马来运输,它是驮不了太多东西的,而车一次就可以运输很多东西,但是它必须要马来拉才能发挥效能(喻指GPU自身功能受限,需要CPU配合),最简单的组合就是一匹马拉一架车,我们可以发现哪怕最简单的组合都可以一次运输很多货物。而现代的GPU可以想象为一架很大的车,一匹马几乎拉不动它了,这时就需要很多匹马(喻指多线程)来同时拉才能发挥最大效能。这时它们就组成了多匹马拉一辆大车的情形,可以一次运输几乎几倍于最简单的单匹马车的货物(喻指多线程+GPU渲染)。
3、多线程
3.1、什么是线程
在Windows中进程是装入内存并执行的程序(一般以一个exe为代表,但不只有exe,还有其他的DLL等)。每个进程都有独立的虚拟地址空间。进程由代码,数据和该进程中的线程可用的其他系统资源,如:文件、同步对象等等元素组成。
虽然每个进程都由单线程(即主线程)开始,但是在主线程中可以建立另外的执行线程。
在Windows中,进程是不活动的,或者说它不是被用来执行的,它其实本质就是虚拟地址空间加各种资源、线程等的载体或容器。
一般进程中一定包含至少一个线程来代表它的执行。
在Windows平台上,最终可以利用CPU执行代码的最小实体就是线程。首先从内核角度看,线程是一个内核对象,系统用它来存储一些关于线程的统计信息等(比如运行时间)。
其次从编程的角度看,线程是一堆寄存器状态以及线程栈的一个结构体对象,本质上可以理解为一个函数调用器,其寄存器状态用于控制CPU执行,栈用于存储局部变量和函数调用参数及函数返回地址等。
最后需要知道的就是线程还可以带有几个队列(简单的理解为异步函数调用队列):
消息队列(GUI线程系统内部会创建)
APC队列(调用APC函数时会创建)
(注意:这些队列在线程创建时并不存在)
最终线程就是执行体,可以理解为一个可以独立执行函数的函数调用器,也可以理解为是CPU的软件封装或CPU驱动程序。类似于D3D12中的命令队列对象(Command Queue)。
3.2、进程的主线程
我们知道每个Windows进程都有一个入口函数,可以简单的想成我们VC++程序中的WinMain函数。从本质上讲进程的入口函数就是主线程的入口函数。主线程是进程内第一个可执行的线程实体,可以用它来创建别的线程。
一般来讲主线程退出后,进程也会退出。因为VC++嵌入入口(也就是wWinMainCRTStartup等VC++运行时入口函数,它内部会调用你实现的WinMain或Main等函数,以便符合C/C++语言的标准),在主线程返回后会调用ExitProcess终止其它线程的执行,所以最终主线程也是进程退出的最后一个线程。当使用自定义的入口时,这个行为就要在自定义入口即自定义主线程中自行来维护。但这个不是Windows自身进程的特性,是我们使用的VC++运行时库特意设置的特性。
可以使用/ENTRY链接开关来自定义入口函数,如果使用了自定义入口,如果不做控制,那么进程将在最后一个线程退出后才退出,这是Win32本身的进程退出行为。
注意:以上关于主线程退出特性很多资料没有说清楚,容易产生混淆。我们特意区分了Windows自身进程特性和VC++扩展的入口函数的特性。
3.3、线程的入口函数
在Windows中,必须为每个线程指定一个线程执行代码的开始地址。在VC++中,这个起始地址就是在程序中定义的一个函数的名字,实际上传递的是函数指针,就是一段函数代码开始地址。默认的线程函数必须具有如下原型:
DWORD WINAPI ThreadProc(LPVOID lpParameter);
3.4、创建线程
调用API:CreateThread就可以创建一个新线程,其原型如下:
HANDLE WINAPI CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
其中LPSECURITY_ATTRIBUTES lpThreadAttributes安全属性参数指定的是创建的新线程内核对象的安全属性,一般传入nullptr即可。
dwStackSize用于指定线程初始时的栈大小,通常传入0即可,此时系统会使用一个合适的大小。
lpStartAddress就是新线程入口函数的地址,也即我们的线程函数的指针。
lpParameter就是传入线程函数的参数,这个参数完全由调用者使用,系统只是简单的将这个参数传给线程函数,并不做别的任何处理
dwCreationFlags指出创建线程的方式,如果是0,表示线程一被创建就立即被执行,如果是CREATE_SUSPENDED,表示线程一被创建先暂停,并不立即执行。推荐使用暂停方式创建线程,然后再启动的方式。
在XP以上的系统中,dwCreationFlags参数还可以位或一个STACK_SIZE_PARAM_IS_A_RESERVATION 用于指出设置的dwStackSize其实只是为线程栈保留的虚拟地址空间的大小,并不需要实际提交这么多的物理页面,如果没有指定这个标志位,那么dwStackSize也是实际提交内存的大小值。
lpThreadId则用于返回线程的唯一标识符ID。
3.5、CreateThread示例
#include <windows.h>
#include <tchar.h>
#include <strsafe.h>
#define GRS_USEPRINTF() TCHAR pBuf[1024] = {}
#define GRS_PRINTF(...) \
StringCchPrintf(pBuf,1024,__VA_ARGS__);\
WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE),pBuf,lstrlen(pBuf),NULL,NULL);
#define GRS_ALLOC(sz) HeapAlloc(GetProcessHeap(),0,sz)
#define GRS_CALLOC(sz) HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sz)
#define GRS_SAFEFREE(p) if(NULL != p){HeapFree(GetProcessHeap(),0,p);p=NULL;}
#define MAX_THREADS 10 //最大线程数
DWORD WINAPI MyThreadFunction( LPVOID lpParam );
void ErrorHandler(LPTSTR lpszFunction);
//自定义线程数据
typedef struct MyData
{
int val1;
int val2;
} MYDATA, *PMYDATA;
int _tmain()
{
PMYDATA pDataArray[MAX_THREADS];
DWORD dwThreadIdArray[MAX_THREADS];
HANDLE hThreadArray[MAX_THREADS];
// 创建线程循环
for( int i=0; i<MAX_THREADS; i++ )
{
pDataArray[i] = (PMYDATA)GRS_CALLOC(sizeof(MYDATA));
pDataArray[i]->val1 = i;
pDataArray[i]->val2 = i+100;
hThreadArray[i] = CreateThread(NULL,0,MyThreadFunction,pDataArray[i],0,&dwThreadIdArray[i]);
if (hThreadArray[i] == NULL)
{
ErrorHandler(_T("CreateThread"));
ExitProcess(3);
}
}
WaitForMultipleObjects(MAX_THREADS, hThreadArray, TRUE, INFINITE);
for(int i=0; i<MAX_THREADS; i++)
{
CloseHandle(hThreadArray[i]);
GRS_SAFEFREE(pDataArray[i]);
}
_tsystem(_T("PAUSE"));
return 0;
}
DWORD WINAPI MyThreadFunction( LPVOID lpParam )
{//线程函数
GRS_USEPRINTF();
PMYDATA pDataArray = (PMYDATA)lpParam;
GRS_PRINTF(_T("Parameters = %d, %d\n"),pDataArray->val1, pDataArray->val2);
return 0;
}
void ErrorHandler(LPTSTR lpszFunction)
{
GRS_USEPRINTF();
LPVOID lpMsgBuf;
DWORD dw = GetLastError();
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
dw,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR) &lpMsgBuf,
0, NULL );
GRS_PRINTF(_T("%s failed with error %d: %s"),lpszFunction, dw, lpMsgBuf);
LocalFree(lpMsgBuf);
}
3.6、C/C++创建线程函数(VC版)
由于历史原因,标准C/C++库在开始时并没有针对多线程做考虑(比如使用了一些全局变量的strstr函数等),在多线程环境中使用这些函数就会有潜在的风险。
在VC++中,对标准库做了多线程安全扩展,要利用这些扩展就需要使用VC++扩展库提供的创建线程函数_beginthread, _beginthreadex等函数来替代Windows的原始API——CreateThread。因为这些创建函数在内部考虑了多线程安全初始化C/C++标准库,使用这些替代函数创建的线程调用C/C++时将是多线程安全的(比如:利用TLS等特性改进全局变量)。
如果在线程中只使用API的话,那么不存在什么问题。但是若需要在线程中调用C/C++库函数的话都建议使用扩展的创建函数替代品,此时建议使用_beginthreadex。
3.7、_beginthreadex示例
#include <windows.h>
#include <process.h>
#include <tchar.h>
#include <strsafe.h>
#define GRS_USEPRINTF() TCHAR pBuf[1024] = {}
#define GRS_PRINTF(...) \
StringCchPrintf(pBuf,1024,__VA_ARGS__);\
WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE),pBuf,lstrlen(pBuf),NULL,NULL);
#define GRS_ALLOC(sz) HeapAlloc(GetProcessHeap(),0,sz)
#define GRS_CALLOC(sz) HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sz)
#define GRS_SAFEFREE(p) if(NULL != p){HeapFree(GetProcessHeap(),0,p);p=NULL;}
#define MAX_THREADS 10 //最大线程数
UINT WINAPI MyThreadFunction(LPVOID lpParam);
void ErrorHandler(LPCTSTR lpszFunction);
//自定义线程数据
typedef struct MyData
{
int val1;
int val2;
} MYDATA, *PMYDATA;
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
{
::AllocConsole();
PMYDATA pDataArray[MAX_THREADS];
UINT nThreadIdArray[MAX_THREADS];
HANDLE hThreadArray[MAX_THREADS];
// 创建线程循环
for (int i = 0; i < MAX_THREADS; i++)
{
pDataArray[i] = (PMYDATA)GRS_CALLOC(sizeof(MYDATA));
pDataArray[i]->val1 = i;
pDataArray[i]->val2 = i + 100;
hThreadArray[i] = (HANDLE)_beginthreadex(NULL, 0, MyThreadFunction, pDataArray[i], 0, &nThreadIdArray[i]);
if (hThreadArray[i] == NULL)
{
ErrorHandler(_T("_beginthreadex"));
ExitProcess(3);
}
}
WaitForMultipleObjects(MAX_THREADS, hThreadArray, TRUE, INFINITE);
for (int i = 0; i < MAX_THREADS; i++)
{
CloseHandle(hThreadArray[i]);
GRS_SAFEFREE(pDataArray[i]);
}
_tsystem(_T("PAUSE"));
::FreeConsole();
return 0;
}
UINT WINAPI MyThreadFunction(LPVOID lpParam)
{//线程函数
GRS_USEPRINTF();
PMYDATA pDataArray = (PMYDATA)lpParam;
GRS_PRINTF(_T("Parameters = %d, %d\n"), pDataArray->val1, pDataArray->val2);
return 0;
}
void ErrorHandler(LPCTSTR lpszFunction)
{
GRS_USEPRINTF();
LPVOID lpMsgBuf;
DWORD dw = GetLastError();
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
dw,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf,
0, NULL);
GRS_PRINTF(_T("%s failed with error %d: %s"), lpszFunction, dw, lpMsgBuf);
LocalFree(lpMsgBuf);
}
3.8、线程退出
当下列事件之一发生时线程终止运行:
- 线程调用ExitThread;
- 该线程函数返回,即主线程隐含调用ExitProcess或其它线程调用ExitThread。一般我们推荐使用线程函数自然返回的方式来退出线程;
- ExitProcess显式或隐含的由进程的任何线程调用;
- 用线程的句柄调用TerminateThread;
- 用线程的进程句柄调用TerminateProcess;
当线程终止时,线程对象的状态为有信号状态,释放所有一直在该线程句柄上等待该线程终止信号的其它线程,该线程终止状态从STILL_ACTIVE改变为线程的退出码。调用GetExitCodeThread可以得到一个退出线程的退出代码(由ExitThread、ExitProcess、TerminateThread指定的值或线程函数的返回值)。
调用TerminateThread终止线程时,将不会通知DLL的DLLMain函数某个线程退出,这可能导致一些资源无法释放等问题。这不是被推荐的退出线程的方法,不到万不得已一般不要使用。
3.9、线程的暂停(挂起)和恢复
在Windows上,提供了一些方法可以控制线程的执行状态,也就是影响系统的线程调度过程。SuspendThread和ResumeThread就是用来控制线程暂停和恢复运行的方法。这两个方法入口参数为线程的句柄。
SuspendThreand用来暂停一个线程的执行,线程暂停时,不会被调度执行。ResumeThread用来恢复一个暂停线程的执行。
注意一个暂停的线程无法调用这个方法来恢复自己,因为暂停线程不可能被执行。
暂停线程总是立即被暂停,而不管被暂停的线程执行到了那个指令。在线程内核对象内部,存储了一个暂停计数的值,每调用一次SuspendThread方法该值就加1,调用ResumeThread一次该值就减一,当该值为0时,就表示该线程可以被调度执行了,但不会被立即执行,所以多次被Suspend的线程不能期望调用一次Resume方法就恢复。
另外线程函数内部可以通过调用Sleep或SleepEx方法自行暂停一定时间后自动恢复执行,但是这个暂停时间对于系统调度程序来说只是个参考值调度程序不能保证精确的暂停指定的时长。通常暂停时间会长于指定的暂停时长。同时在Windows上我们也不建议使用Sleep或SleepEx方法来控制线程的暂定时长或计时等任务。精确定时应当使用我们之前教程例子中多次用到的Timer内核对象,并配合Wait函数。
3.10、线程的暂停式创建
在调用CreateThreand创建新线程时,可以明确指定以暂停方式创建线程,这是个有用的特性。如果没有指定暂停方式执行的话,创建线程的行为将比较难以控制,有可能调度程序在CreateThread返回前就开始执行新线程,也有可能在返回之后才去调度执行新线程,此时这样的行为很难预期。基于上面的原因,一般推荐在创建新线程时都以暂停标志创建,直到CreateThread返回,并做了一些必要的处理之后,再使用ResumeThread方法恢复新建的线程使其有机会被执行。本章例子中我们就使用这种方法来创建线程。
3.11、线程栈
Windows的线程栈是一个严格的栈数据结构,它不但符合C/C++语言对函数调用栈的要求,而且还符合其它很多语言对函数调用栈的功能要求。Windows栈本身依托于CPU的栈机制,在机器码层面,甚至可以认为其实就是CPU的栈机制在工作,Windows几乎没有任何封装。Windows栈也是基于虚拟内存管理的,因此在一些情况下可以认为Windows栈的大小就是用户空间的大小。在新的VC++运行时库中加入了严格的栈完整性检测机制,默认情况下此属性都是打开的,这有利于检测栈溢出错误(编译调用_RTC_CheckStackVars)。通过严格的C运行期库检测机制,Windows栈比原本的CPU栈要健壮的多,虽然机器码层面二者没什么区别。
3.12、线程的同步执行
很多情况下,多个线程都会访问同一个系统资源(可能是同一进程的也有可能是不同进程的),如果对这些访问行为不加以控制,可能会引起一些不必要的麻烦。比如:一个线程销毁了一个堆,而另一个线程因为保存有该堆句柄的副本(不是复制得来的),又去这个堆上分配内存,虽然堆本身可能是严格串行化的,但此时必然会发生问题。此类问题通常被称为“并发问题”(严格应该叫做并行冲突问题,也叫多线程竞争问题)。此时就有必要对多线程访问共享资源的行为加以控制,这种控制通常称为多线程同步(也就是让它们以严格串行化的方法访问资源)。多线程同步还被用于保证相互依赖的代码(非严格串行化的方法)能以正确的顺序执行(变为严格串行化的方法)关于同步后面将详细介绍。
3.13、线程可警告状态及真实含义
通过另一些方法会让线程“暂停”(非SuspendThread方法,比如SleepEx,Wait函数族等)并可以进入一种称之为Altertable的状态(该词有很多种翻译,最常见的叫可警告状态,本教程中就用这个翻译词)。这种状态本质上其实是告诉系统调度程序:当前线程的函数调用栈以及对应的寄存器状态可以直接被用来调用别的一些函数,一般这些函数都被称为异步函数。通常系统会用此线程环境,此时将线程理解为就是个函数调用器,来调用一些其它的回调函数,比如:IO完成通知,线程的异步调用队列等。
对此我们可以形象的将线程理解为就像我们自己的车辆临时由私家车变成了滴滴专车,当然系统在借用之前会备份线程的环境块(栈和寄存器状态)。这样借用线程的好处就是,系统不必为一些回调函数大费周折的去创建一些新的专用线程,既可以达到节约系统资源的目的,还可以充分发挥系统性能(因为线程太多,调度、轮询等系统级算法也会耗费CPU时间)。通常异步函数最好能够很快的执行完,不要做太耗时的操作,不然真正的线程函数需要执行时,会无法立即执行。这相当于理解为私家车的借用时间不要太久,否则车主要用车的时候就会发生额外的冲突。
3.14、线程消息队列
线程的消息队列是一个基于数据结构(即Windows消息结构)的队列。当调用任何一个与消息队列相关的函数时,系统在内部为线程创建消息队列(也即没有专门的创建消息队列的函数)。
一般Post函数族发出的消息都会进入对应线程的消息队列。
线程一般使用同步版GetMessage的方式从消息队列中取出消息并进行处理(需要打造成循环形式取消息,称为消息循环),一般该函数在没有消息在消息队列时,就使线程进入等待状态而放弃CPU时间片。而在3D渲染循环中我们往往使用异步版的PeekMessage函数。
消息队列不是严格的队列式操作,有些消息系统会向后排(如:WM_PAINT、WM_TIMER),有些消息会优先插入队列。甚至有些消息通过Send函数族,绕过消息队列,直接调用窗口的窗口过程,并传递消息参数。
通过调用WaitForInputIdle函数可以等待到一个进程没有输入消息时返回。
3.15、强制切换线程(SwitchToThread)
通过调用SwitchToThread方法可以强制内核任务调度程序将下一个时间片分配给可能急需执行的线程。但是这个方法有时候并不能真正做到让当前的线程主动放弃下一个执行机会,尤其多核系统上
因为有时候可能系统调度程序会判断出调用SwitchToThread的线程其实是最想得到下一个时间片的线程。
此时更好的替代方法是用一个非0值调用Sleep函数来切换线程,这个方法有时比SwitchToThread有效。注意:多核系统上因内核设计的原因,这种影响可能都不会生效(即有可能不按照预期的行为来切换)。
3.16、什么时候不使用多线程
至于在什么时候使用线程,可以找到n多的资料来解答这个问题,而且很多应用确实已经这样去利用多线程来工作了。
而这一章我则反过来告诉你什么时候不要使用线程,实际上也就是提供一个线程使用的注意事项说明,以防止一些刚学多线程编程的程序员编出表面上是多线程的“串行多线程程序”(即每个线程因为同步按顺序访问某资源等情形实际变成了一个个线程轮着执行的情形):
- 当一个算法本身是严格串行化的时候,也即计算的每一步都严重依赖前一个操作步骤的结果时,这种算法一般比较难改为并行化的,因此也不适合应用于多线程(例外情况是,针对不同的初始参数,可以利用多线程运行同一个算法来得到各自的结果);
- 当有多个功能任务也具有比较严格的先后逻辑关系时,不宜采用多线程,此时,若使用多线程,并且试图让每个线程处理不同的任务时,为了照顾先后的逻辑顺序,则必须要使用线程同步方法严格控制几个线程的执行顺序,最终其实跟用一个线程来顺序执行这些功能逻辑几乎没什么区别,而且多线程加同步控制还降低了效率(这是很多初学者应用多线程时常犯的错误);
- 还有一种特殊的情况,比如一个服务器需要处理成千上万个客户端链接,并处理不同的请求时,这种情况下应当优先考虑使用线程池,而不是简单的多线程(很多程序员喜欢为每个客户端链接创建一个线程,殊不知这样不但资源浪费严重、而且性能十分低下);
4、线程同步和等待
4.1、多线程竞争资源
在Windows上同一进程中的所有线程都在一个进程空间中。此时这些线程将共享进程中的资源,比如:虚拟地址空间、虚拟内存、堆、进程对象、其它内核对象等。当线程不加控制随机访问这些资源时就会引起一些不可预知,并且很难追踪的问题。这种情况我们就称之为出现了多线程竞争资源的问题。
4.2、多线程同步
为了避免线程竞争,必须使用一些强力控制线程执行顺序逻辑的方法来严格控制对公共资源的访问。这些方法就被称为线程同步方法,于此对应的,刚才我们说的借用线程当作函数调用器的方法就被称为异步方法。
从本质上说,通过同步控制我们可以让多个线程近乎以一种串行的方式来访问资源,从而使访问的顺序,访问的权限等都可以由程序进行有效控制。
4.3、Windows线程同步模型
在Windows系统中,线程通过一些API可以主动放弃被执行的机会,直到某个事件或时间点到达时再去执行。这些行为直接被Windows内核线程调度程序支持。多个线程可以通过这样的方式,以一种预先安排好的顺序或逻辑执行。从而可以达到以安全的方式共享使用公共资源的目的。
这些特殊的事件和时间点,通过内核对象的方式提供给线程使用。
而线程则通过调用被称为等待函数(Wait函数族)的一组API来释放CPU时间片进入一种"等待状态"。
当内核对象表示的事件或时间点到达时,内核调度程序会立即为等待该事件或时间点的线程分配时间片使其立即得到执行,即调用了等待函数的线程被“唤醒”。
等待某个事件或时间点的线程执行方式,可以被理解为是被动方式执行的。被动方式执行可以极大的节约宝贵的CPU资源,线程只在需要执行的时候才占用CPU。
在Windows上最普遍的被动执行方式就是"消息循环"。一般的窗口应用都是基于被称为Windows消息的事件的驱动而执行,没有消息的时候消息线程就不执行。这种线程被动式执行方式优雅的替代了传统低效的轮询式执行方式。
因为被动方式的线程只在真正需要的时候才占用CPU时间片,否则只是呆在内存中的一段数据而已。这也是Windows系统能够执行多任务的基础条件之一。
可以实验利用真正的死循环方式编制测试线程,然后运行几个该进程的实例,即会发现系统性能有所下降。当然现在的Windows系统早已经对这种死循环的程序也做了很好的调度控制,因此靠死循环已经不太可能让Windows死机了。
4.4、等待函数
Windows平台提供了一组能使线程阻塞其自身执行的等待函数,这些函数能让线程放弃CPU时间片,自行暂停进入等待状态。这组函数直到由等待函数的参数指定的一组条件满足后才返回。
等待函数在等待条件满足之前,将使线程立即进入有效等待状态,线程不会被调度,也就几乎不耗费CPU时间。
等待时可以设定一个等待时间值,当时间到达还未等到需要的条件时,就超时返回。通过传递INFINITE值可以让线程一直等待直到条件达到。
一般等待函数族API均以Wait开头,或含有Wait单词,比如:
WaitForSingleObject、
WaitForMultipleObject、
MsgWaitForMultipleObject、
WaitForDebugEvent等
例外:EnterCriticalSection
还有GetMessage函数也可以被认为使线程进入等待状态。类似的还有GetQueuedCompletionStatus。
4.5、等待函数详解
分类 | 含义 | 函数 | 备注 |
single-object | 一次等待一个对象(调用线程进入等待状态) | SignalObjectAndWait | |
multiple-object | 一次等待多个对象(调用线程进入等待状态) | WaitForMultipleObjects | MsgWaitForMultipleObjects 在等待对象的同时还等待消息,UI多线程程序推荐使用 |
alertable | 等待单个对象或多个对象的同时使线程进入"等待+可警告状态" | MsgWaitForMultipleObjectsEx | |
registered | 注册一个等待回调函数,当等待成功(或失败)时,由线程池中线程负责调用该回调函数(调用线程立即返回继续执行) | RegisterWaitForSingleObject |
4.6、同步对象和状态
同步对象是那些其句柄可以在等待函数中指定的协调多线程执行的对象(一般是内核对象)。
同步对象状态要么是"有信号的"可以使等待函数返回;要么是无信号的,此时禁止等待函数返回。
因为同步对象的句柄可复制、可继承,可打开,所以使得不但是多线程可以共享访问这些对象,而且多进程也可以共享访问这些对象并拥有同一对象各自不同的句柄值。这才使得任意进程间的任意线程间的同步控制成为可能。
4.7、可以被等待的对象
对象名称 | 中文意义 | 有信号状态 |
Change notification | 文件、目录变更通知 | 被监视的文件或文件夹发生变更 |
Console input | 控制台输入 | 标准控制台接收到输入 |
Event | 事件 | 调用SetEvent方法置为有信号状态 |
Memory resource notification | 内存资源通知 | 监视系统可用内存高于或低于某个临界值时 |
Mutex | 互斥 | 另一个线程调用ReleaseMutex |
Process | 进程 | 进程退出时 |
Semaphore | 信标量 | 另一线程调用ReleaseSemaphore |
Thread | 线程 | 线程退出时 |
Waitable timer | 计时器 | 设定的时间周期到达时 |
4.8、等待函数成功返回时的副作用
一些等待函数在等到对象变成有信号状态时返回。这时候有些对象的状态又会被等待函数还原为无信号的状态,这就被称为"成功等待的副作用"。
Windows系统中实际比较倾向于一种一个信号唤醒一个等待线程的方式来工作。所以当多个线程等待同一个对象的时候,哪个线程被唤醒又将是随机的行为。此时使用同步控制实际可能并没有达到预期的效果。这时候为了更好的控制线程的协作关系,就需要重新设计同步的方式,比如:为每对线程间分配独立的同步对象,实现线程集合级的协作。
4.9、Event用法
1、使用CreateEvent创建一个Event对象取得句柄
2、传递给需要等待的线程,并由其调用等待函数族等待这个Event
3、调用SetEvent唤醒等待该Event的线程
4、最后使用CloseHandle关闭这个Event
4.10、Event示例
#include <tchar.h>
#include <windows.h>
#include <strsafe.h>
#define GRS_ALLOC(sz) HeapAlloc(GetProcessHeap(),0,sz)
#define GRS_CALLOC(sz) HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sz)
#define GRS_SAFEFREE(p) if(NULL != p){HeapFree(GetProcessHeap(),0,p);p=NULL;}
#define GRS_USEPRINTF() TCHAR pBuf[1024] = {}
#define GRS_PRINTF(...) \
StringCchPrintf(pBuf,1024,__VA_ARGS__);\
WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE),pBuf,lstrlen(pBuf),NULL,NULL);
#define THREADCOUNT 4
HANDLE ghWriteEvent = NULL;
HANDLE ghThreads[THREADCOUNT] = {};
DWORD WINAPI ThreadProc(LPVOID);
void CreateEventsAndThreads(void)
{
int i = 0;
DWORD dwThreadID = 0;
//创建成手动重置事件对象状态可以使所有线程都退出
//如果改为自动,那么就只有一个线程有机会等到该事件,程序出现问题
//这就是成功等待的副作用(像药物副作用一样)
ghWriteEvent = CreateEvent(NULL,TRUE,FALSE,_T("WriteEvent"));
for(i = 0; i < THREADCOUNT; i++)
{
ghThreads[i] = CreateThread(NULL,0,ThreadProc,NULL,0,&dwThreadID);
}
}
void WriteToBuffer(VOID)
{
GRS_USEPRINTF();
GRS_PRINTF(_T("Main thread writing to the shared buffer...\n"));
if (! SetEvent(ghWriteEvent) )
{
GRS_PRINTF(_T("SetEvent failed (%d)\n"), GetLastError());
return;
}
}
void CloseEvents()
{
CloseHandle(ghWriteEvent);
}
void _tmain()
{
GRS_USEPRINTF();
DWORD dwWaitResult = 0;
CreateEventsAndThreads();
WriteToBuffer();
GRS_PRINTF(_T("Main thread waiting for threads to exit...\n"));
dwWaitResult = WaitForMultipleObjects(THREADCOUNT,ghThreads,TRUE,INFINITE);
switch (dwWaitResult)
{
case WAIT_OBJECT_0:
GRS_PRINTF(_T("All threads ended, cleaning up for application exit...\n"));
break;
default:
GRS_PRINTF(_T("WaitForMultipleObjects failed (%d)\n"), GetLastError());
return;
}
CloseEvents();
_tsystem(_T("PAUSE"));
}
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
GRS_USEPRINTF();
DWORD dwWaitResult = 0;
GRS_PRINTF(_T("Thread %d waiting for write event...\n"), GetCurrentThreadId());
dwWaitResult = WaitForSingleObject(ghWriteEvent,INFINITE);
switch (dwWaitResult)
{
case WAIT_OBJECT_0:
GRS_PRINTF(_T("Thread %d reading from buffer\n"),GetCurrentThreadId());
break;
default:
GRS_PRINTF(_T("Wait error (%d)\n"), GetLastError());
return 0;
}
GRS_PRINTF(_T("Thread %d exiting\n"), GetCurrentThreadId());
return 1;
}
4.11、并行数据结构简介
本章中介绍的很多方法,除了互锁函数族之外的所有其他方法基本都是基于等待-操作-释放这样的阻塞模型来工作的。从这种模型本身就可以发现,其实这些方法有些时候会很低效。因为有等待这种过程,所以多CPU时实际无法利用多CPU的优势,并且在使用不当的时候还会引发死锁问题。
基于此,一种被称为Lock-Free的算法被提出,类似的还有"Wait-Free"算法,这方面都是比较前沿活跃的领域。它的设计目标之一就是抛弃前面所说的这种基于等待式的同步方式,而变成"无锁式并发"。Lock Free算法能够确保所有线程中至少有一个能够继续执行,从而免疫了死锁等问题。
Lock Free算法需要对应的原子操作加以支持,比如CAS(compare-and-swap)及其变种(互锁函数家族中的InterlockedCompareExchange等函数,即比较前两个值若相等就把第三个值赋给第一个变量)。这些原子操作中很多都直接被现代多核CPU支持,所以具有极高的性能。Windows中直接将其封装为前述的互锁函数家族。甚至在GPU上的Shader程序中也有可供调用的互锁函数族,方便我们编写Lock-Free版的基于GPU的算法,比如:并行排序算法、并行聚合算法等。
可以利用这些原子锁函数编写一些多线程安全的高效的数据结构出来。例如:Lock-Free的Stack等。
5、D3D12多线程渲染的支持和限制
5.1、D3D12多线程渲染基本框架
其实在之前的我的很多其他博文中,我都有说过D3D12就是为多线程渲染而生。本章教程中我也啰嗦了很多关于多线程的基础知识,目的就是为了让大家使用起多线程来毫无障碍。那么对于D3D12来说如何具体实现多线程渲染呢?
基本的思路就是为不同的线程分派不同的Command List,注意不是分派Command Queue,它仍然在主线程中,且直接命令队列全局一般只有一个,代表GPU本身。当然这是在单GPU系统中的情形。然后在不同的CPU线程中使用这些不同的Command List通过记录命令进行渲染,最后Close命令列表,并传递至主线程中使用Command Queue来执行,最终完成全局的渲染。当然Command Queue也不一定非要放在主线程中,我们完全可以重新创建一个只管渲染的线程,让它来管理Command Queue,此时为了区别我们可以称这个线程为渲染主线程。
本章主要多线程渲染示例的框架图如下:
本章我们要实现的场景中有一个球体、一个正方体和一个平面,球体和正方体就放在平面上。这三个物体我们就是使用不同的三个线程来渲染。因为线程执行的随机性,这时你可能担心的是物体前后遮挡顺序如何保障呢?其实这种担心是完全多余的,首先,每个线程的每个Command List可能是以不同顺序先后被记录完成的,但我们知道记录并不会引起真正的渲染,我们最终是按照顺序排队成一个Command List数组一次性提交执行的;其次,场景中物体的先后遮挡顺序可以使用Z-Buffer算法保证其正确性,因为我们是在进行3D渲染,Z坐标本身保证了每一个物体的不同顶点直接的前后次序,所以也不用过多考虑,这是3D渲染的本质属性。
5.2、D3D12多线程渲染的限制
在前一小节框架的描述中,大家应该已经注意到我在图中用红色字体标出的那个过程,也就是在子线程的Command List中,也必须重新设置一遍RTV和DSV,并且要求与主线程中的Command List保持一致,否则就会报错。或者可以说多个Command List完成同一个场景的同一帧渲染时,必须设置相同的RTV和DSV。这跟在使用捆绑包时重新设置与Command List一致的描述符堆指针数组一致。
但一般只要一个线程的Command List执行Clear操作,除非有特殊渲染需要,因为线程运行的随机性,很可能因为某个中间的Command List调用了Clear操作将之前的Command List完成的渲染内容给Clear掉了。
另外我要补充说明的就是,因为D3D12整体都是围绕多线程渲染设计的,所以关于资源属性状态的变换都需要程序自身来控制的,正如我们之前的教程中讲解的要使用Resource Barrier(资源屏障)来做GPU内部操作之间的同步控制。最常见的就是我们需要在画面渲染完毕并Present之前,们当前的渲染目标通过资源屏障再次变换为D3D12_RESOURCE_STATE_PRESENT状态。通常我们会在主线程的Command List中线性的记录如下:
pICmdList->ResourceBarrier(1
, &CD3DX12_RESOURCE_BARRIER::Transition(
g_pIARenderTargets[nCurrentFrameIndex].Get()
, D3D12_RESOURCE_STATE_PRESENT
, D3D12_RESOURCE_STATE_RENDER_TARGET)
);
//偏移描述符指针到指定帧缓冲视图位置
CD3DX12_CPU_DESCRIPTOR_HANDLE stRTVHandle(g_pIRTVHeap->GetCPUDescriptorHandleForHeapStart()
, nCurrentFrameIndex, g_nRTVDescriptorSize);
CD3DX12_CPU_DESCRIPTOR_HANDLE dsvHandle(g_pIDSVHeap->GetCPUDescriptorHandleForHeapStart());
//设置渲染目标
pICmdList->OMSetRenderTargets(1, &stRTVHandle, FALSE, &dsvHandle);
pICmdList->RSSetViewports(1, &g_stViewPort);
pICmdList->RSSetScissorRects(1, &g_stScissorRect);
const float clearColor[] = { 0.0f, 0.1f, 0.0f, 1.0f };
pICmdList->ClearRenderTargetView(stRTVHandle, clearColor, 0, nullptr);
pICmdList->ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
......//使用pICmdList记录渲染命令
pICmdList ->ResourceBarrier(1
, &CD3DX12_RESOURCE_BARRIER::Transition(
g_pIARenderTargets[nCurrentFrameIndex].Get()
, D3D12_RESOURCE_STATE_RENDER_TARGET
, D3D12_RESOURCE_STATE_PRESENT));
//关闭命令列表,可以去执行了
GRS_THROW_IF_FAILED(pICmdList->Close());
arCmdList.RemoveAll();
//执行命令列表
arCmdList.Add(pICmdList.Get());
pIMainCmdQueue->ExecuteCommandLists(static_cast<UINT>(arCmdList.GetCount()), arCmdList.GetData());
如果我们只使用一个Command List,并在单线程中像上面这样记录渲染命令是毫无问题的,因为所有的渲染命令逻辑顺序都因为单线程线性运行(串行)而有先后顺序的保证。第一个资源屏障保证了渲染目标在开始用于渲染时,正确的将状态同步到了可以作为渲染目标的状态,而最后一个作用于渲染目标上的资源屏障保证了所有的绘制操作都同步完成了,可以提交画面了。
但当多线程渲染使用多个Command List时,情形就变成了下面这样:
pICmdList->ResourceBarrier(1
, &CD3DX12_RESOURCE_BARRIER::Transition(
g_pIARenderTargets[nCurrentFrameIndex].Get()
, D3D12_RESOURCE_STATE_PRESENT
, D3D12_RESOURCE_STATE_RENDER_TARGET)
);
//偏移描述符指针到指定帧缓冲视图位置
CD3DX12_CPU_DESCRIPTOR_HANDLE stRTVHandle(g_pIRTVHeap->GetCPUDescriptorHandleForHeapStart()
, nCurrentFrameIndex, g_nRTVDescriptorSize);
CD3DX12_CPU_DESCRIPTOR_HANDLE dsvHandle(g_pIDSVHeap->GetCPUDescriptorHandleForHeapStart());
//设置渲染目标
pICmdList->OMSetRenderTargets(1, &stRTVHandle, FALSE, &dsvHandle);
pICmdList->RSSetViewports(1, &g_stViewPort);
pICmdList->RSSetScissorRects(1, &g_stScissorRect);
const float clearColor[] = { 0.0f, 0.1f, 0.0f, 1.0f };
pICmdList->ClearRenderTargetView(stRTVHandle, clearColor, 0, nullptr);
pICmdList->ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
//通知各线程开始渲染
for (int i = 0; i < g_nMaxThread; i++)
{
g_stThreadParams[i].nCurrentFrameIndex = nCurrentFrameIndex;
SetEvent(g_stThreadParams[i].hRunEvent);
}
//等待各线程渲染完,并进行渲染后处理
dwRet = WaitForMultipleObjects(static_cast<DWORD>(arHWaited.GetCount())
,arHWaited.GetData(), TRUE, INFINITE);
dwRet -= WAIT_OBJECT_0;
if (0 == dwRet)
{
pICmdList->ResourceBarrier(1
, &CD3DX12_RESOURCE_BARRIER::Transition(
g_pIARenderTargets[nCurrentFrameIndex].Get()
, D3D12_RESOURCE_STATE_RENDER_TARGET
, D3D12_RESOURCE_STATE_PRESENT));
//关闭命令列表,可以去执行了
GRS_THROW_IF_FAILED(pICmdList->Close());
GRS_THROW_IF_FAILED(pICmdListPost->Close());
arCmdList.RemoveAll();
//执行命令列表(注意命令列表排队组合的方式)
arCmdList.Add(pICmdList.Get());
arCmdList.Add(g_stThreadParams[g_nThdSphere].pICmdList);
arCmdList.Add(g_stThreadParams[g_nThdCube].pICmdList);
arCmdList.Add(g_stThreadParams[g_nThdPlane].pICmdList);
pIMainCmdQueue->ExecuteCommandLists(static_cast<UINT>(arCmdList.GetCount())
, arCmdList.GetData());
//提交画面
GRS_THROW_IF_FAILED(pISwapChain3->Present(1, 0));
......//等待GPU完成渲染和提交
}
这里请仔细观察并分析命令列表添加进数组的顺序,其中arCmdlist是一个简单的ATL模版类CAtlArray<ID3D12CommandList*>的实例,类似于STL中的vector。因为pICmdList作为第一个Command List添加进了数组,而Command Queue将忠实按照你指定的数组顺序依次执行里面的渲染命令,因此在最终ExecuteCommandLists的时候,pICmdList最后记录的ResourceBarrier(D3D12_RESOURCE_STATE_RENDER_TARGET-> D3D12_RESOURCE_STATE_PRESENT)同步命令,将在后面几个其他线程记录的Command List之前执行。而通常其他线程记录的渲染命令依然是一些普通的绘制命令,它们显然需要当前渲染目标资源是Render Target状态,此时错误就发生了。
而这个问题就是我开始说的那个只需要开开脑洞就解决的问题,这时我们需要的就是使用另一个命令列表记录最后一个资源屏障命令,并添加到命令列表数组的末尾来提交执行即可,因此我们将代码修改成下面这样,请大家注意里面关于Command List的微妙变化:
//渲染前处理
{
pICmdListPre->ResourceBarrier(1
, &CD3DX12_RESOURCE_BARRIER::Transition(
g_pIARenderTargets[nCurrentFrameIndex].Get()
, D3D12_RESOURCE_STATE_PRESENT
, D3D12_RESOURCE_STATE_RENDER_TARGET)
);
//偏移描述符指针到指定帧缓冲视图位置
CD3DX12_CPU_DESCRIPTOR_HANDLE stRTVHandle(g_pIRTVHeap->GetCPUDescriptorHandleForHeapStart()
, nCurrentFrameIndex, g_nRTVDescriptorSize);
CD3DX12_CPU_DESCRIPTOR_HANDLE dsvHandle(g_pIDSVHeap->GetCPUDescriptorHandleForHeapStart());
//设置渲染目标
pICmdListPre->OMSetRenderTargets(1, &stRTVHandle, FALSE, &dsvHandle);
pICmdListPre->RSSetViewports(1, &g_stViewPort);
pICmdListPre->RSSetScissorRects(1, &g_stScissorRect);
const float clearColor[] = { 0.0f, 0.1f, 0.0f, 1.0f };
pICmdListPre->ClearRenderTargetView(stRTVHandle, clearColor, 0, nullptr);
pICmdListPre->ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH
, 1.0f, 0, 0, nullptr);
}
//通知各线程开始渲染
{
for (int i = 0; i < g_nMaxThread; i++)
{
g_stThreadParams[i].nCurrentFrameIndex = nCurrentFrameIndex;
SetEvent(g_stThreadParams[i].hRunEvent);
}
}
//等待各线程渲染完,并进行渲染后处理
{
dwRet = WaitForMultipleObjects(static_cast<DWORD>(arHWaited.GetCount()), arHWaited.GetData(), TRUE, INFINITE);
dwRet -= WAIT_OBJECT_0;
if (0 == dwRet)
{
pICmdListPost->ResourceBarrier(1
, &CD3DX12_RESOURCE_BARRIER::Transition(
g_pIARenderTargets[nCurrentFrameIndex].Get()
, D3D12_RESOURCE_STATE_RENDER_TARGET
, D3D12_RESOURCE_STATE_PRESENT));
//关闭命令列表,可以去执行了
GRS_THROW_IF_FAILED(pICmdListPre->Close());
GRS_THROW_IF_FAILED(pICmdListPost->Close());
arCmdList.RemoveAll();
//执行命令列表(注意命令列表排队组合的方式)
arCmdList.Add(pICmdListPre.Get());
arCmdList.Add(g_stThreadParams[g_nThdSphere].pICmdList);
arCmdList.Add(g_stThreadParams[g_nThdCube].pICmdList);
arCmdList.Add(g_stThreadParams[g_nThdPlane].pICmdList);
arCmdList.Add(pICmdListPost.Get());
pIMainCmdQueue->ExecuteCommandLists(static_cast<UINT>(arCmdList.GetCount()), arCmdList.GetData());
//提交画面
GRS_THROW_IF_FAILED(pISwapChain3->Present(1, 0));
......//等待GPU完成渲染和提交
}
......
}
上面的代码中我们使用了一个额外的称为Post Command List(后处理命令列表)的命令列表记录了最后那个资源屏障的命令,并将它排队至命令列表数组的最后,这样它就会在最后所有的渲染都结束之后在最后执行了。
此处的所谓“脑洞”,就是将命令列表按照渲染逻辑进行必要的拆分,使用多个命令列表记录不同阶段的渲染命令。而这也就是D3D12自带示例代码中所使用的“技巧”,显然微软的这个例子由于过度的封装,并没有让我明显的发现这个“技巧”,从而耽误了一些功夫。而在我的示例教程中我就尽力为大家讲清楚这个技巧。
这个技巧不仅用于像我们现在这个示例的多线程渲染中,其实只要你要用到多个Command List的情形,几乎都需要用到这个拆分命令列表的“技巧”,那么就请你牢牢的记住它。因为后面一些教程示例中,我们还将继续用到。那时我不会再啰嗦的讲解这个方法了。
5.3、完整代码1:
#include <SDKDDKVer.h>
#define WIN32_LEAN_AND_MEAN // 从 Windows 头中排除极少使用的资料
#include <windows.h>
#include <tchar.h>
#include <fstream> //for ifstream
using namespace std;
#include <wrl.h> //添加WTL支持 方便使用COM
using namespace Microsoft;
using namespace Microsoft::WRL;
#include <atlcoll.h> //for atl array
#include <strsafe.h> //for StringCchxxxxx function
#include <dxgi1_6.h>
#include <d3d12.h> //for d3d12
#include <d3dcompiler.h>
#pragma comment(lib, "dxguid.lib")
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3d12.lib")
#pragma comment(lib, "d3dcompiler.lib")
#if defined(_DEBUG)
#include <dxgidebug.h>
#endif
#include <DirectXMath.h>
#include "..\WindowsCommons\d3dx12.h"
#include "..\WindowsCommons\DDSTextureLoader12.h"
using namespace DirectX;
#define GRS_WND_CLASS_NAME _T("Game Window Class")
#define GRS_WND_TITLE _T("DirectX12 MultiThread Sample")
#define GRS_THROW_IF_FAILED(hr) if (FAILED(hr)){ throw CGRSCOMException(hr); }
//新定义的宏用于上取整除法
#define GRS_UPPER_DIV(A,B) ((UINT)(((A)+((B)-1))/(B)))
//更简洁的向上边界对齐算法 内存管理中常用 请记住
#define GRS_UPPER(A,B) ((UINT)(((A)+((B)-1))&~(B - 1)))
//------------------------------------------------------------------------------------------------------------
// 为了调试加入下面的内联函数和宏定义,为每个接口对象设置名称,方便查看调试输出
#if defined(_DEBUG)
inline void GRS_SetD3D12DebugName(ID3D12Object* pObject, LPCWSTR name)
{
pObject->SetName(name);
}
inline void GRS_SetD3D12DebugNameIndexed(ID3D12Object* pObject, LPCWSTR name, UINT index)
{
WCHAR _DebugName[MAX_PATH] = {};
if ( SUCCEEDED( StringCchPrintfW(_DebugName, _countof(_DebugName), L"%s[%u]", name, index) ) )
{
pObject->SetName(_DebugName);
}
}
#else
inline void GRS_SetD3D12DebugName(ID3D12Object*, LPCWSTR)
{
}
inline void GRS_SetD3D12DebugNameIndexed(ID3D12Object*, LPCWSTR, UINT)
{
}
#endif
#define GRS_SET_D3D12_DEBUGNAME(x) GRS_SetD3D12DebugName(x, L#x)
#define GRS_SET_D3D12_DEBUGNAME_INDEXED(x, n) GRS_SetD3D12DebugNameIndexed(x[n], L#x, n)
#define GRS_SET_D3D12_DEBUGNAME_COMPTR(x) GRS_SetD3D12DebugName(x.Get(), L#x)
#define GRS_SET_D3D12_DEBUGNAME_INDEXED_COMPTR(x, n) GRS_SetD3D12DebugNameIndexed(x[n].Get(), L#x, n)
#if defined(_DEBUG)
inline void GRS_SetDXGIDebugName(IDXGIObject* pObject, LPCWSTR name)
{
size_t szLen = 0;
StringCchLengthW(name, 50, &szLen);
pObject->SetPrivateData(WKPDID_D3DDebugObjectName, static_cast<UINT>(szLen - 1), name);
}
inline void GRS_SetDXGIDebugNameIndexed(IDXGIObject* pObject, LPCWSTR name, UINT index)
{
size_t szLen = 0;
WCHAR _DebugName[MAX_PATH] = {};
if (SUCCEEDED(StringCchPrintfW(_DebugName, _countof(_DebugName), L"%s[%u]", name, index)))
{
StringCchLengthW(_DebugName, _countof(_DebugName), &szLen);
pObject->SetPrivateData(WKPDID_D3DDebugObjectName, static_cast<UINT>(szLen), _DebugName);
}
}
#else
inline void GRS_SetDXGIDebugName(ID3D12Object*, LPCWSTR)
{
}
inline void GRS_SetDXGIDebugNameIndexed(ID3D12Object*, LPCWSTR, UINT)
{
}
#endif
#define GRS_SET_DXGI_DEBUGNAME(x) GRS_SetDXGIDebugName(x, L#x)
#define GRS_SET_DXGI_DEBUGNAME_INDEXED(x, n) GRS_SetDXGIDebugNameIndexed(x[n], L#x, n)
#define GRS_SET_DXGI_DEBUGNAME_COMPTR(x) GRS_SetDXGIDebugName(x.Get(), L#x)
#define GRS_SET_DXGI_DEBUGNAME_INDEXED_COMPTR(x, n) GRS_SetDXGIDebugNameIndexed(x[n].Get(), L#x, n)
//------------------------------------------------------------------------------------------------------------
class CGRSCOMException
{
public:
CGRSCOMException(HRESULT hr) : m_hrError(hr)
{
}
HRESULT Error() const
{
return m_hrError;
}
private:
const HRESULT m_hrError;
};
// 顶点结构
struct ST_GRS_VERTEX
{
XMFLOAT4 m_vPos; //Position
XMFLOAT2 m_vTex; //Texcoord
XMFLOAT3 m_vNor; //Normal
};
// 常量缓冲区
struct ST_GRS_MVP
{
XMFLOAT4X4 m_MVP; //经典的Model-view-projection(MVP)矩阵.
};
// 渲染子线程参数
struct ST_GRS_THREAD_PARAMS
{
DWORD dwThisThreadID;
HANDLE hThisThread;
DWORD dwMainThreadID;
HANDLE hMainThread;
HANDLE hRunEvent;
HANDLE hEventRenderOver;
UINT nCurrentFrameIndex;
XMFLOAT4 v4ModelPos;
const TCHAR* pszDDSFile;
const CHAR* pszMeshFile;
ID3D12Device4* pID3DDevice;
ID3D12CommandAllocator* pICmdAlloc;
ID3D12GraphicsCommandList* pICmdList;
ID3D12RootSignature* pIRS;
ID3D12PipelineState* pIPSO;
};
UINT __stdcall RenderThread(void* pParam);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
BOOL LoadMeshVertex(const CHAR*pszMeshFileName, UINT&nVertexCnt, ST_GRS_VERTEX*&ppVertex, UINT*&ppIndices);
int g_iWndWidth = 1024;
int g_iWndHeight = 768;
CD3DX12_VIEWPORT g_stViewPort(0.0f, 0.0f, static_cast<float>(g_iWndWidth), static_cast<float>(g_iWndHeight));
CD3DX12_RECT g_stScissorRect(0, 0, static_cast<LONG>(g_iWndWidth), static_cast<LONG>(g_iWndHeight));
//初始的默认摄像机的位置
XMFLOAT3 g_f3EyePos = XMFLOAT3(0.0f, 5.0f, -10.0f); //眼睛位置
XMFLOAT3 g_f3LockAt = XMFLOAT3(0.0f, 0.0f, 0.0f); //眼睛所盯的位置
XMFLOAT3 g_f3HeapUp = XMFLOAT3(0.0f, 1.0f, 0.0f); //头部正上方位置
float g_fYaw = 0.0f; // 绕正Z轴的旋转量.
float g_fPitch = 0.0f; // 绕XZ平面的旋转量
double g_fPalstance = 10.0f * XM_PI / 180.0f; //物体旋转的角速度,单位:弧度/秒
XMFLOAT4X4 g_mxWorld = {}; //World Matrix
XMFLOAT4X4 g_mxVP = {}; //View Projection Matrix
// 全局线程参数
const UINT g_nMaxThread = 3;
const UINT g_nThdSphere = 0;
const UINT g_nThdCube = 1;
const UINT g_nThdPlane = 2;
ST_GRS_THREAD_PARAMS g_stThreadParams[g_nMaxThread] = {};
const UINT g_nFrameBackBufCount = 3u;
UINT g_nRTVDescriptorSize = 0U;
ComPtr<ID3D12Resource> g_pIARenderTargets[g_nFrameBackBufCount];
ComPtr<ID3D12DescriptorHeap> g_pIRTVHeap;
ComPtr<ID3D12DescriptorHeap> g_pIDSVHeap; //深度缓冲描述符堆
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
{
::CoInitialize(nullptr); //for WIC & COM
HWND hWnd = nullptr;
MSG msg = {};
UINT nDXGIFactoryFlags = 0U;
ComPtr<IDXGIFactory5> pIDXGIFactory5;
ComPtr<IDXGIAdapter1> pIAdapter;
ComPtr<ID3D12Device4> pID3DDevice;
ComPtr<ID3D12CommandQueue> pIMainCmdQueue;
ComPtr<IDXGISwapChain1> pISwapChain1;
ComPtr<IDXGISwapChain3> pISwapChain3;
ComPtr<ID3D12Resource> pIDepthStencilBuffer; //深度蜡板缓冲区
UINT nCurrentFrameIndex = 0;
DXGI_FORMAT emRTFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
DXGI_FORMAT emDSFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;
ComPtr<ID3D12Fence> pIFence;
UINT64 n64FenceValue = 1ui64;
HANDLE hFenceEvent = nullptr;
ComPtr<ID3D12CommandAllocator> pICmdAllocPre;
ComPtr<ID3D12GraphicsCommandList> pICmdListPre;
ComPtr<ID3D12CommandAllocator> pICmdAllocPost;
ComPtr<ID3D12GraphicsCommandList> pICmdListPost;
CAtlArray<HANDLE> arHWaited;
CAtlArray<HANDLE> arHSubThread;
ComPtr<ID3DBlob> pIVSSphere;
ComPtr<ID3DBlob> pIPSSphere;
ComPtr<ID3D12RootSignature> pIRootSignature;
ComPtr<ID3D12PipelineState> pIPSOSphere;
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, g_iWndWidth, g_iWndHeight };
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)
{
throw CGRSCOMException(HRESULT_FROM_WIN32(GetLastError()));
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
}
//2、打开显示子系统的调试支持
{
#if defined(_DEBUG)
ComPtr<ID3D12Debug> debugController;
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
{
debugController->EnableDebugLayer();
// 打开附加的调试支持
nDXGIFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG;
}
#endif
}
//3、创建DXGI Factory对象
{
GRS_THROW_IF_FAILED(CreateDXGIFactory2(nDXGIFactoryFlags, IID_PPV_ARGS(&pIDXGIFactory5)));
GRS_SET_DXGI_DEBUGNAME_COMPTR(pIDXGIFactory5);
// 关闭ALT+ENTER键切换全屏的功能,因为我们没有实现OnSize处理,所以先关闭
GRS_THROW_IF_FAILED(pIDXGIFactory5->MakeWindowAssociation(hWnd, DXGI_MWA_NO_ALT_ENTER));
}
//4、枚举适配器创建设备
{//选择NUMA架构的独显来创建3D设备对象,暂时先不支持集显了,当然你可以修改这些行为
D3D12_FEATURE_DATA_ARCHITECTURE stArchitecture = {};
for (UINT nAdapterIndex = 0; DXGI_ERROR_NOT_FOUND != pIDXGIFactory5->EnumAdapters1(nAdapterIndex, &pIAdapter); ++ nAdapterIndex)
{
DXGI_ADAPTER_DESC1 stAdapterDesc = {};
pIAdapter->GetDesc1(&stAdapterDesc);
if (stAdapterDesc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
{//跳过软件虚拟适配器设备
continue;
}
GRS_THROW_IF_FAILED(D3D12CreateDevice(pIAdapter.Get(), D3D_FEATURE_LEVEL_12_1, IID_PPV_ARGS(&pID3DDevice)));
GRS_THROW_IF_FAILED(pID3DDevice->CheckFeatureSupport(D3D12_FEATURE_ARCHITECTURE
, &stArchitecture, sizeof(D3D12_FEATURE_DATA_ARCHITECTURE)));
if (!stArchitecture.UMA)
{
break;
}
pID3DDevice.Reset();
}
//---------------------------------------------------------------------------------------------
if (nullptr == pID3DDevice.Get())
{// 可怜的机器上居然没有独显 还是先退出了事
throw CGRSCOMException(E_FAIL);
}
GRS_SET_D3D12_DEBUGNAME_COMPTR(pID3DDevice);
}
//5、创建直接命令队列
{
D3D12_COMMAND_QUEUE_DESC stQueueDesc = {};
stQueueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandQueue(&stQueueDesc, IID_PPV_ARGS(&pIMainCmdQueue)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIMainCmdQueue);
}
//6、创建交换链
{
DXGI_SWAP_CHAIN_DESC1 stSwapChainDesc = {};
stSwapChainDesc.BufferCount = g_nFrameBackBufCount;
stSwapChainDesc.Width = g_iWndWidth;
stSwapChainDesc.Height = g_iWndHeight;
stSwapChainDesc.Format = emRTFormat;
stSwapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
stSwapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
stSwapChainDesc.SampleDesc.Count = 1;
GRS_THROW_IF_FAILED(pIDXGIFactory5->CreateSwapChainForHwnd(
pIMainCmdQueue.Get(), // Swap chain needs the queue so that it can force a flush on it.
hWnd,
&stSwapChainDesc,
nullptr,
nullptr,
&pISwapChain1
));
GRS_SET_DXGI_DEBUGNAME_COMPTR(pISwapChain1);
//注意此处使用了高版本的SwapChain接口的函数
GRS_THROW_IF_FAILED(pISwapChain1.As(&pISwapChain3));
GRS_SET_DXGI_DEBUGNAME_COMPTR(pISwapChain3);
// 获取当前第一个供绘制的后缓冲序号
nCurrentFrameIndex = pISwapChain3->GetCurrentBackBufferIndex();
//创建RTV(渲染目标视图)描述符堆(这里堆的含义应当理解为数组或者固定大小元素的固定大小显存池)
D3D12_DESCRIPTOR_HEAP_DESC stRTVHeapDesc = {};
stRTVHeapDesc.NumDescriptors = g_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(&g_pIRTVHeap)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(g_pIRTVHeap);
//得到每个描述符元素的大小
g_nRTVDescriptorSize = pID3DDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
//---------------------------------------------------------------------------------------------
CD3DX12_CPU_DESCRIPTOR_HANDLE stRTVHandle(g_pIRTVHeap->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < g_nFrameBackBufCount; i++)
{//这个循环暴漏了描述符堆实际上是个数组的本质
GRS_THROW_IF_FAILED(pISwapChain3->GetBuffer(i, IID_PPV_ARGS(&g_pIARenderTargets[i])));
GRS_SET_D3D12_DEBUGNAME_INDEXED_COMPTR(g_pIARenderTargets, i);
pID3DDevice->CreateRenderTargetView(g_pIARenderTargets[i].Get(), nullptr, stRTVHandle);
stRTVHandle.Offset(1, g_nRTVDescriptorSize);
}
}
//7、创建深度缓冲及深度缓冲描述符堆
{
D3D12_CLEAR_VALUE stDepthOptimizedClearValue = {};
stDepthOptimizedClearValue.Format = emDSFormat;
stDepthOptimizedClearValue.DepthStencil.Depth = 1.0f;
stDepthOptimizedClearValue.DepthStencil.Stencil = 0;
//使用隐式默认堆创建一个深度蜡板缓冲区,
//因为基本上深度缓冲区会一直被使用,重用的意义不大,所以直接使用隐式堆,图方便
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT)
, D3D12_HEAP_FLAG_NONE
, &CD3DX12_RESOURCE_DESC::Tex2D(emDSFormat
, g_iWndWidth
, g_iWndHeight
, 1
, 0
, 1
, 0
, D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL)
, D3D12_RESOURCE_STATE_DEPTH_WRITE
, &stDepthOptimizedClearValue
, IID_PPV_ARGS(&pIDepthStencilBuffer)
));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIDepthStencilBuffer);
D3D12_DEPTH_STENCIL_VIEW_DESC stDepthStencilDesc = {};
stDepthStencilDesc.Format = emDSFormat;
stDepthStencilDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
stDepthStencilDesc.Flags = D3D12_DSV_FLAG_NONE;
D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = {};
dsvHeapDesc.NumDescriptors = 1;
dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
GRS_THROW_IF_FAILED(pID3DDevice->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&g_pIDSVHeap)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(g_pIDSVHeap);
pID3DDevice->CreateDepthStencilView(pIDepthStencilBuffer.Get()
, &stDepthStencilDesc
, g_pIDSVHeap->GetCPUDescriptorHandleForHeapStart());
}
//8、创建直接命令列表
{
// 预处理命令列表
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT
, IID_PPV_ARGS(&pICmdAllocPre)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pICmdAllocPre);
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT
, pICmdAllocPre.Get(), nullptr, IID_PPV_ARGS(&pICmdListPre)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pICmdListPre);
//后处理命令列表
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT
, IID_PPV_ARGS(&pICmdAllocPost)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pICmdAllocPost);
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT
, pICmdAllocPost.Get(), nullptr, IID_PPV_ARGS(&pICmdListPost)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pICmdListPost);
}
//9、创建根签名
{//这个例子中,所有物体使用相同的根签名,因为渲染过程中需要的参数是一样的
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;
}
CD3DX12_DESCRIPTOR_RANGE1 stDSPRanges[3];
stDSPRanges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0);
stDSPRanges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0);
stDSPRanges[2].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 1, 0);
CD3DX12_ROOT_PARAMETER1 stRootParameters[3];
stRootParameters[0].InitAsDescriptorTable(1, &stDSPRanges[0], D3D12_SHADER_VISIBILITY_ALL); //CBV是所有Shader可见
stRootParameters[1].InitAsDescriptorTable(1, &stDSPRanges[1], D3D12_SHADER_VISIBILITY_PIXEL);//SRV仅PS可见
stRootParameters[2].InitAsDescriptorTable(1, &stDSPRanges[2], D3D12_SHADER_VISIBILITY_PIXEL);//SAMPLE仅PS可见
CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC stRootSignatureDesc;
stRootSignatureDesc.Init_1_1(_countof(stRootParameters)
, stRootParameters
, 0
, nullptr
, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
ComPtr<ID3DBlob> pISignatureBlob;
ComPtr<ID3DBlob> 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)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIRootSignature);
}
//10、编译Shader创建渲染管线状态对象
{
#if defined(_DEBUG)
// Enable better shader debugging with the graphics debugging tools.
UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#else
UINT compileFlags = 0;
#endif
//编译为行矩阵形式
compileFlags |= D3DCOMPILE_PACK_MATRIX_ROW_MAJOR;
TCHAR pszShaderFileName[] = _T("D:\\Projects_2018_08\\D3D12 Tutorials\\6-MultiThread\\Shader\\TextureCube.hlsl");
GRS_THROW_IF_FAILED(D3DCompileFromFile(pszShaderFileName, nullptr, nullptr
, "VSMain", "vs_5_0", compileFlags, 0, &pIVSSphere, nullptr));
GRS_THROW_IF_FAILED(D3DCompileFromFile(pszShaderFileName, nullptr, nullptr
, "PSMain", "ps_5_0", compileFlags, 0, &pIPSSphere, nullptr));
// 我们多添加了一个法线的定义,但目前Shader中我们并没有使用
D3D12_INPUT_ELEMENT_DESC stIALayoutSphere[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 16, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};
// 创建 graphics pipeline state object (PSO)对象
D3D12_GRAPHICS_PIPELINE_STATE_DESC stPSODesc = {};
stPSODesc.InputLayout = { stIALayoutSphere, _countof(stIALayoutSphere) };
stPSODesc.pRootSignature = pIRootSignature.Get();
stPSODesc.VS = CD3DX12_SHADER_BYTECODE(pIVSSphere.Get());
stPSODesc.PS = CD3DX12_SHADER_BYTECODE(pIPSSphere.Get());
stPSODesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
stPSODesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
stPSODesc.SampleMask = UINT_MAX;
stPSODesc.SampleDesc.Count = 1;
stPSODesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
stPSODesc.NumRenderTargets = 1;
stPSODesc.RTVFormats[0] = emRTFormat;
stPSODesc.DSVFormat = emDSFormat;
stPSODesc.DepthStencilState.StencilEnable = FALSE;
stPSODesc.DepthStencilState.DepthEnable = TRUE; //打开深度缓冲
stPSODesc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;//启用深度缓存写入功能
stPSODesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS; //深度测试函数(该值为普通的深度测试,即较小值写入)
GRS_THROW_IF_FAILED(pID3DDevice->CreateGraphicsPipelineState(&stPSODesc
, IID_PPV_ARGS(&pIPSOSphere)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIPSOSphere);
}
//11、准备参数并启动多个渲染线程
{
// 球体个性参数
g_stThreadParams[g_nThdSphere].pszDDSFile = _T("D:\\Projects_2018_08\\D3D12 Tutorials\\6-MultiThread\\Mesh\\sphere.dds");
g_stThreadParams[g_nThdSphere].pszMeshFile = "D:\\Projects_2018_08\\D3D12 Tutorials\\6-MultiThread\\Mesh\\sphere.txt";
g_stThreadParams[g_nThdSphere].v4ModelPos = XMFLOAT4(2.0f, 2.0f, 0.0f, 1.0f);
// 立方体个性参数
g_stThreadParams[g_nThdCube].pszDDSFile = _T("D:\\Projects_2018_08\\D3D12 Tutorials\\6-MultiThread\\Mesh\\Cube.dds");
g_stThreadParams[g_nThdCube].pszMeshFile = "D:\\Projects_2018_08\\D3D12 Tutorials\\6-MultiThread\\Mesh\\Cube.txt";
g_stThreadParams[g_nThdCube].v4ModelPos = XMFLOAT4(-2.0f, 2.0f, 0.0f, 1.0f);
// 平板个性参数
g_stThreadParams[g_nThdPlane].pszDDSFile = _T("D:\\Projects_2018_08\\D3D12 Tutorials\\6-MultiThread\\Mesh\\Plane.dds");
g_stThreadParams[g_nThdPlane].pszMeshFile = "D:\\Projects_2018_08\\D3D12 Tutorials\\6-MultiThread\\Mesh\\Plane.txt";
g_stThreadParams[g_nThdPlane].v4ModelPos = XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f);
// 物体的共性参数,也就是各线程的共性参数
for (int i = 0; i < g_nMaxThread; i++)
{
//创建每个线程需要的命令列表和复制命令队列
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT
, IID_PPV_ARGS(&g_stThreadParams[i].pICmdAlloc)));
GRS_SetD3D12DebugNameIndexed(g_stThreadParams[i].pICmdAlloc, _T("pIThreadCmdAlloc"), i);
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT
, g_stThreadParams[i].pICmdAlloc, nullptr, IID_PPV_ARGS(&g_stThreadParams[i].pICmdList)));
GRS_SetD3D12DebugNameIndexed(g_stThreadParams[i].pICmdList, _T("pIThreadCmdList"), i);
g_stThreadParams[i].dwMainThreadID = ::GetCurrentThreadId();
g_stThreadParams[i].hMainThread = ::GetCurrentThread();
g_stThreadParams[i].hRunEvent = ::CreateEvent(nullptr, FALSE, FALSE, nullptr);
g_stThreadParams[i].hEventRenderOver= ::CreateEvent(nullptr, FALSE, FALSE, nullptr);
g_stThreadParams[i].pID3DDevice = pID3DDevice.Get();
g_stThreadParams[i].pIRS = pIRootSignature.Get();
g_stThreadParams[i].pIPSO = pIPSOSphere.Get();
arHWaited.Add(g_stThreadParams[i].hEventRenderOver); //添加到被等待队列里
//以暂停方式创建线程
g_stThreadParams[i].hThisThread = (HANDLE)_beginthreadex(nullptr,
0, RenderThread, (void*)&g_stThreadParams[i],
CREATE_SUSPENDED, (UINT*)&g_stThreadParams[i].dwThisThreadID);
//然后判断线程创建是否成功
if (nullptr == g_stThreadParams[i].hThisThread
|| reinterpret_cast<HANDLE>(-1) == g_stThreadParams[i].hThisThread)
{
throw CGRSCOMException(HRESULT_FROM_WIN32(GetLastError()));
}
arHSubThread.Add(g_stThreadParams[i].hThisThread);
}
//逐一启动线程
for (int i = 0; i < g_nMaxThread; i++)
{
::ResumeThread(g_stThreadParams[i].hThisThread);
}
}
//12、创建围栏对象
{
GRS_THROW_IF_FAILED(pID3DDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&pIFence)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIFence);
//创建一个Event同步对象,用于等待围栏事件通知
hFenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (hFenceEvent == nullptr)
{
GRS_THROW_IF_FAILED(HRESULT_FROM_WIN32(GetLastError()));
}
}
DWORD dwRet = 0;
CAtlArray<ID3D12CommandList*> arCmdList;
UINT64 n64fence = 0;
//13、等待子线程第一次设置同步信号 ,表示要执行第二个COPY命令,完成资源上传至默认堆
{
dwRet = WaitForMultipleObjects(static_cast<DWORD>(arHWaited.GetCount()), arHWaited.GetData(), TRUE, INFINITE);
dwRet -= WAIT_OBJECT_0;
if (0 == dwRet)
{
arCmdList.RemoveAll();
//执行命令列表
arCmdList.Add(g_stThreadParams[g_nThdSphere].pICmdList);
arCmdList.Add(g_stThreadParams[g_nThdCube].pICmdList);
arCmdList.Add(g_stThreadParams[g_nThdPlane].pICmdList);
pIMainCmdQueue->ExecuteCommandLists(static_cast<UINT>(arCmdList.GetCount()), arCmdList.GetData());
//---------------------------------------------------------------------------------------------
//开始同步GPU与CPU的执行,先记录围栏标记值
n64fence = n64FenceValue;
GRS_THROW_IF_FAILED(pIMainCmdQueue->Signal(pIFence.Get(), n64fence));
n64FenceValue++;
if (pIFence->GetCompletedValue() < n64fence)
{
GRS_THROW_IF_FAILED(pIFence->SetEventOnCompletion(n64fence, hFenceEvent));
WaitForSingleObject(hFenceEvent, INFINITE);
}
}
else
{
GRS_THROW_IF_FAILED(HRESULT_FROM_WIN32(GetLastError()));
}
}
//---------------------------------------------------------------------------------------------
BOOL bExit = FALSE;
HANDLE phWait = CreateWaitableTimer(NULL, FALSE, NULL);
LARGE_INTEGER liDueTime = {};
liDueTime.QuadPart = -1i64;//1秒后开始计时
SetWaitableTimer(phWait, &liDueTime, 1, NULL, NULL, 0);//40ms的周期
ULONGLONG n64tmFrameStart = ::GetTickCount64();
ULONGLONG n64tmCurrent = n64tmFrameStart;
//计算旋转角度需要的变量
double dModelRotationYAngle = 0.0f;
//14、开始消息循环,并在其中不断渲染
while (!bExit)
{//注意这里我们调整了消息循环,将等待时间设置为0,同时将定时性的渲染,改成了每次循环都渲染
//特别注意这次等待与之前不同
//主线程进入等待
dwRet = ::MsgWaitForMultipleObjects(1, &phWait, FALSE, 0, QS_ALLINPUT);
switch (dwRet - WAIT_OBJECT_0)
{
case 0:
{
}
break;
case WAIT_TIMEOUT:
{//计时器时间到
}
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;
}
//计算全局的Matrix
{
//关于时间的基本运算都放在了主线程中
//真实的引擎或程序中建议时间值也作为一个每帧更新的参数从主线程获取并传给各子线程
n64tmCurrent = ::GetTickCount();
//计算旋转的角度:旋转角度(弧度) = 时间(秒) * 角速度(弧度/秒)
//下面这句代码相当于经典游戏消息循环中的OnUpdate函数中需要做的事情
dModelRotationYAngle += ((n64tmCurrent - n64tmFrameStart) / 1000.0f) * g_fPalstance;
n64tmFrameStart = n64tmCurrent;
//旋转角度是2PI周期的倍数,去掉周期数,只留下相对0弧度开始的小于2PI的弧度即可
if (dModelRotationYAngle > XM_2PI)
{
dModelRotationYAngle = fmod(dModelRotationYAngle, XM_2PI);
}
//计算 World 矩阵 这里是个旋转矩阵
XMStoreFloat4x4(&g_mxWorld, XMMatrixRotationY(static_cast<float>(dModelRotationYAngle)));
//计算 视矩阵 view * 裁剪矩阵 projection
XMStoreFloat4x4( &g_mxVP
,XMMatrixMultiply(XMMatrixLookAtLH(XMLoadFloat3(&g_f3EyePos)
, XMLoadFloat3(&g_f3LockAt)
, XMLoadFloat3(&g_f3HeapUp))
, XMMatrixPerspectiveFovLH(XM_PIDIV4
, (FLOAT)g_iWndWidth / (FLOAT)g_iWndHeight, 0.1f, 1000.0f)) );
}
//渲染前处理
{
pICmdListPre->ResourceBarrier(1
, &CD3DX12_RESOURCE_BARRIER::Transition(
g_pIARenderTargets[nCurrentFrameIndex].Get()
, D3D12_RESOURCE_STATE_PRESENT
, D3D12_RESOURCE_STATE_RENDER_TARGET)
);
//偏移描述符指针到指定帧缓冲视图位置
CD3DX12_CPU_DESCRIPTOR_HANDLE stRTVHandle(g_pIRTVHeap->GetCPUDescriptorHandleForHeapStart()
, nCurrentFrameIndex, g_nRTVDescriptorSize);
CD3DX12_CPU_DESCRIPTOR_HANDLE dsvHandle(g_pIDSVHeap->GetCPUDescriptorHandleForHeapStart());
//设置渲染目标
pICmdListPre->OMSetRenderTargets(1, &stRTVHandle, FALSE, &dsvHandle);
pICmdListPre->RSSetViewports(1, &g_stViewPort);
pICmdListPre->RSSetScissorRects(1, &g_stScissorRect);
const float clearColor[] = { 0.0f, 0.1f, 0.0f, 1.0f };
pICmdListPre->ClearRenderTargetView(stRTVHandle, clearColor, 0, nullptr);
pICmdListPre->ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
}
//通知各线程开始渲染
{
for (int i = 0; i < g_nMaxThread; i++)
{
g_stThreadParams[i].nCurrentFrameIndex = nCurrentFrameIndex;
SetEvent(g_stThreadParams[i].hRunEvent);
}
}
//等待各线程渲染完,并进行渲染后处理
{
dwRet = WaitForMultipleObjects(static_cast<DWORD>(arHWaited.GetCount()), arHWaited.GetData(), TRUE, INFINITE);
dwRet -= WAIT_OBJECT_0;
if (0 == dwRet)
{
pICmdListPost->ResourceBarrier(1
, &CD3DX12_RESOURCE_BARRIER::Transition(
g_pIARenderTargets[nCurrentFrameIndex].Get()
, D3D12_RESOURCE_STATE_RENDER_TARGET
, D3D12_RESOURCE_STATE_PRESENT));
//关闭命令列表,可以去执行了
GRS_THROW_IF_FAILED(pICmdListPre->Close());
GRS_THROW_IF_FAILED(pICmdListPost->Close());
arCmdList.RemoveAll();
//执行命令列表(注意命令列表排队组合的方式)
arCmdList.Add(pICmdListPre.Get());
arCmdList.Add(g_stThreadParams[g_nThdSphere].pICmdList);
arCmdList.Add(g_stThreadParams[g_nThdCube].pICmdList);
arCmdList.Add(g_stThreadParams[g_nThdPlane].pICmdList);
arCmdList.Add(pICmdListPost.Get());
pIMainCmdQueue->ExecuteCommandLists(static_cast<UINT>(arCmdList.GetCount()), arCmdList.GetData());
//提交画面
GRS_THROW_IF_FAILED(pISwapChain3->Present(1, 0));
//开始同步GPU与CPU的执行,先记录围栏标记值
n64fence = n64FenceValue;
GRS_THROW_IF_FAILED(pIMainCmdQueue->Signal(pIFence.Get(), n64fence));
n64FenceValue++;
if (pIFence->GetCompletedValue() < n64fence)
{
GRS_THROW_IF_FAILED(pIFence->SetEventOnCompletion(n64fence, hFenceEvent));
WaitForSingleObject(hFenceEvent, INFINITE);
}
nCurrentFrameIndex = pISwapChain3->GetCurrentBackBufferIndex();
GRS_THROW_IF_FAILED(pICmdAllocPre->Reset());
GRS_THROW_IF_FAILED(pICmdListPre->Reset(pICmdAllocPre.Get(), pIPSOSphere.Get()));
GRS_THROW_IF_FAILED(pICmdAllocPost->Reset());
GRS_THROW_IF_FAILED(pICmdListPost->Reset(pICmdAllocPost.Get(), pIPSOSphere.Get()));
//==========================================================================================================
}
else
{
GRS_THROW_IF_FAILED(HRESULT_FROM_WIN32(GetLastError()));
}
}
//---------------------------------------------------------------------------------------------
//检测一下线程的活动情况,如果有线程已经退出了,就退出循环
dwRet = WaitForMultipleObjects(static_cast<DWORD>(arHSubThread.GetCount()), arHSubThread.GetData(), FALSE, 0);
dwRet -= WAIT_OBJECT_0;
if ( dwRet >= 0 && dwRet < g_nMaxThread )
{
bExit = TRUE;
}
}
}
catch (CGRSCOMException& e)
{//发生了COM异常
e;
}
try
{
// 通知子线程退出
for (int i = 0; i < g_nMaxThread; i++)
{
::PostThreadMessage(g_stThreadParams[i].dwThisThreadID, WM_QUIT, 0, 0);
}
// 等待所有子线程退出
DWORD dwRet = WaitForMultipleObjects(static_cast<DWORD>(arHSubThread.GetCount()), arHSubThread.GetData(), TRUE, INFINITE);
// 清理所有子线程资源
for (int i = 0; i < g_nMaxThread; i++)
{
::CloseHandle(g_stThreadParams[i].hThisThread);
::CloseHandle(g_stThreadParams[i].hEventRenderOver);
g_stThreadParams[i].pICmdList->Release();
g_stThreadParams[i].pICmdAlloc->Release();
}
//::CoUninitialize();
}
catch (CGRSCOMException& e)
{//发生了COM异常
e;
}
::CoUninitialize();
return 0;
}
UINT __stdcall RenderThread(void* pParam)
{
ST_GRS_THREAD_PARAMS* pThdPms = static_cast<ST_GRS_THREAD_PARAMS*>(pParam);
try
{
if ( nullptr == pThdPms )
{//参数异常,抛异常终止线程
throw CGRSCOMException(E_INVALIDARG);
}
SIZE_T szMVPBuf = GRS_UPPER(sizeof(ST_GRS_MVP), 256);
ComPtr<ID3D12Resource> pITexture;
ComPtr<ID3D12Resource> pITextureUpload;
ComPtr<ID3D12Resource> pIVB;
ComPtr<ID3D12Resource> pIIB;
ComPtr<ID3D12Resource> pICBWVP;
ComPtr<ID3D12DescriptorHeap> pISRVCBVHp;
ComPtr<ID3D12DescriptorHeap> pISampleHp;
ST_GRS_MVP* pMVPBufModule = nullptr;
D3D12_VERTEX_BUFFER_VIEW stVBV = {};
D3D12_INDEX_BUFFER_VIEW stIBV = {};
UINT nIndexCnt = 0;
XMMATRIX mxPosModule = XMMatrixTranslationFromVector(XMLoadFloat4(&pThdPms->v4ModelPos)); //当前渲染物体的位置
// Mesh Value
ST_GRS_VERTEX* pstVertices = nullptr;
UINT* pnIndices = nullptr;
UINT nVertexCnt = 0;
// DDS Value
std::unique_ptr<uint8_t[]> pbDDSData;
std::vector<D3D12_SUBRESOURCE_DATA> stArSubResources;
DDS_ALPHA_MODE emAlphaMode = DDS_ALPHA_MODE_UNKNOWN;
bool bIsCube = false;
//1、加载DDS纹理
{
GRS_THROW_IF_FAILED(LoadDDSTextureFromFile(pThdPms->pID3DDevice, pThdPms->pszDDSFile, pITexture.GetAddressOf()
, pbDDSData, stArSubResources, SIZE_MAX, &emAlphaMode, &bIsCube));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pITexture);
UINT64 n64szUpSphere = GetRequiredIntermediateSize(pITexture.Get(), 0, static_cast<UINT>(stArSubResources.size()));
D3D12_RESOURCE_DESC stTXDesc = pITexture->GetDesc();
GRS_THROW_IF_FAILED(pThdPms->pID3DDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD)
, D3D12_HEAP_FLAG_NONE
, &CD3DX12_RESOURCE_DESC::Buffer(n64szUpSphere)
, D3D12_RESOURCE_STATE_GENERIC_READ
, nullptr
, IID_PPV_ARGS(&pITextureUpload)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pITextureUpload);
UpdateSubresources(pThdPms->pICmdList
, pITexture.Get()
, pITextureUpload.Get()
, 0
, 0
, static_cast<UINT>(stArSubResources.size())
, stArSubResources.data());
//同步
pThdPms->pICmdList->ResourceBarrier(1
, &CD3DX12_RESOURCE_BARRIER::Transition(pITexture.Get()
, D3D12_RESOURCE_STATE_COPY_DEST
, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE));
D3D12_DESCRIPTOR_HEAP_DESC stSRVCBVHPDesc = {};
stSRVCBVHPDesc.NumDescriptors = 2; // 1 CBV + 1 SRV
stSRVCBVHPDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
stSRVCBVHPDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
GRS_THROW_IF_FAILED(pThdPms->pID3DDevice->CreateDescriptorHeap(&stSRVCBVHPDesc, IID_PPV_ARGS(&pISRVCBVHp)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pISRVCBVHp);
//创建SRV
D3D12_SHADER_RESOURCE_VIEW_DESC stSRVDesc = {};
stSRVDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
stSRVDesc.Format = stTXDesc.Format;
stSRVDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
stSRVDesc.Texture2D.MipLevels = 1;
CD3DX12_CPU_DESCRIPTOR_HANDLE stCbvSrvHandle(pISRVCBVHp->GetCPUDescriptorHandleForHeapStart()
, 1, pThdPms->pID3DDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV));
pThdPms->pID3DDevice->CreateShaderResourceView(pITexture.Get()
, &stSRVDesc
, stCbvSrvHandle);
}
//2、创建Sample
{
D3D12_DESCRIPTOR_HEAP_DESC stSamplerHeapDesc = {};
stSamplerHeapDesc.NumDescriptors = 1;
stSamplerHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER;
stSamplerHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
GRS_THROW_IF_FAILED(pThdPms->pID3DDevice->CreateDescriptorHeap(&stSamplerHeapDesc, IID_PPV_ARGS(&pISampleHp)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pISampleHp);
D3D12_SAMPLER_DESC stSamplerDesc = {};
stSamplerDesc.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR;
stSamplerDesc.MinLOD = 0;
stSamplerDesc.MaxLOD = D3D12_FLOAT32_MAX;
stSamplerDesc.MipLODBias = 0.0f;
stSamplerDesc.MaxAnisotropy = 1;
stSamplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS;
stSamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
stSamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
stSamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
pThdPms->pID3DDevice->CreateSampler(&stSamplerDesc, pISampleHp->GetCPUDescriptorHandleForHeapStart());
}
//3、加载网格数据
{
LoadMeshVertex(pThdPms->pszMeshFile, nVertexCnt, pstVertices, pnIndices);
nIndexCnt = nVertexCnt;
//创建 Vertex Buffer 仅使用Upload隐式堆
GRS_THROW_IF_FAILED(pThdPms->pID3DDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD)
, D3D12_HEAP_FLAG_NONE
, &CD3DX12_RESOURCE_DESC::Buffer(nVertexCnt * sizeof(ST_GRS_VERTEX))
, D3D12_RESOURCE_STATE_GENERIC_READ
, nullptr
, IID_PPV_ARGS(&pIVB)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIVB);
//使用map-memcpy-unmap大法将数据传至顶点缓冲对象
UINT8* pVertexDataBegin = nullptr;
CD3DX12_RANGE stReadRange(0, 0); // We do not intend to read from this resource on the CPU.
GRS_THROW_IF_FAILED(pIVB->Map(0, &stReadRange, reinterpret_cast<void**>(&pVertexDataBegin)));
memcpy(pVertexDataBegin, pstVertices, nVertexCnt * sizeof(ST_GRS_VERTEX));
pIVB->Unmap(0, nullptr);
//创建 Index Buffer 仅使用Upload隐式堆
GRS_THROW_IF_FAILED(pThdPms->pID3DDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD)
, D3D12_HEAP_FLAG_NONE
, &CD3DX12_RESOURCE_DESC::Buffer(nIndexCnt * sizeof(UINT))
, D3D12_RESOURCE_STATE_GENERIC_READ
, nullptr
, IID_PPV_ARGS(&pIIB)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIIB);
UINT8* pIndexDataBegin = nullptr;
GRS_THROW_IF_FAILED(pIIB->Map(0, &stReadRange, reinterpret_cast<void**>(&pIndexDataBegin)));
memcpy(pIndexDataBegin, pnIndices, nIndexCnt * sizeof(UINT));
pIIB->Unmap(0, nullptr);
//创建Vertex Buffer View
stVBV.BufferLocation = pIVB->GetGPUVirtualAddress();
stVBV.StrideInBytes = sizeof(ST_GRS_VERTEX);
stVBV.SizeInBytes = nVertexCnt * sizeof(ST_GRS_VERTEX);
//创建Index Buffer View
stIBV.BufferLocation = pIIB->GetGPUVirtualAddress();
stIBV.Format = DXGI_FORMAT_R32_UINT;
stIBV.SizeInBytes = nIndexCnt * sizeof(UINT);
::HeapFree(::GetProcessHeap(), 0, pstVertices);
::HeapFree(::GetProcessHeap(), 0, pnIndices);
}
//4、创建常量缓冲
{
GRS_THROW_IF_FAILED(pThdPms->pID3DDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD)
, D3D12_HEAP_FLAG_NONE
, &CD3DX12_RESOURCE_DESC::Buffer(szMVPBuf) //注意缓冲尺寸设置为256边界对齐大小
, D3D12_RESOURCE_STATE_GENERIC_READ
, nullptr
, IID_PPV_ARGS(&pICBWVP)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pICBWVP);
// Map 之后就不再Unmap了 直接复制数据进去 这样每帧都不用map-copy-unmap浪费时间了
GRS_THROW_IF_FAILED(pICBWVP->Map(0, nullptr, reinterpret_cast<void**>(&pMVPBufModule)));
// 创建CBV
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
cbvDesc.BufferLocation = pICBWVP->GetGPUVirtualAddress();
cbvDesc.SizeInBytes = static_cast<UINT>(szMVPBuf);
CD3DX12_CPU_DESCRIPTOR_HANDLE stCbvSrvHandle(pISRVCBVHp->GetCPUDescriptorHandleForHeapStart());
pThdPms->pID3DDevice->CreateConstantBufferView(&cbvDesc, stCbvSrvHandle);
}
//5、设置事件对象 通知并切回主线程 完成资源的第二个Copy命令
{
GRS_THROW_IF_FAILED(pThdPms->pICmdList->Close());
//第一次通知主线程本线程加载资源完毕
::SetEvent(pThdPms->hEventRenderOver); // 设置信号,通知主线程本线程资源加载完毕
}
DWORD dwRet = 0;
BOOL bQuit = FALSE;
MSG msg = {};
//6、渲染循环
while (!bQuit)
{
// 等待主线程通知开始渲染,同时仅接收主线程Post过来的消息,目前就是为了等待WM_QUIT消息
dwRet = ::MsgWaitForMultipleObjects(1, &pThdPms->hRunEvent, FALSE, INFINITE, QS_ALLPOSTMESSAGE);
switch ( dwRet - WAIT_OBJECT_0 )
{
case 0:
{
//命令分配器先Reset一下,刚才已经执行过了一个复制纹理的命令
GRS_THROW_IF_FAILED(pThdPms->pICmdAlloc->Reset());
//Reset命令列表,并重新指定命令分配器和PSO对象
GRS_THROW_IF_FAILED(pThdPms->pICmdList->Reset(pThdPms->pICmdAlloc, pThdPms->pIPSO));
// 准备MWVP矩阵
{
// Module * World
XMMATRIX xmMWVP = XMMatrixMultiply( mxPosModule,XMLoadFloat4x4(&g_mxWorld) );
// (Module * World) * View * Projection
xmMWVP = XMMatrixMultiply(xmMWVP, XMLoadFloat4x4(&g_mxVP));
XMStoreFloat4x4(&pMVPBufModule->m_MVP, xmMWVP);
}
//---------------------------------------------------------------------------------------------
//设置对应的渲染目标和视裁剪框(这是渲染子线程必须要做的步骤,基本也就是所谓多线程渲染的核心秘密所在了)
{
CD3DX12_CPU_DESCRIPTOR_HANDLE stRTVHandle(g_pIRTVHeap->GetCPUDescriptorHandleForHeapStart()
, pThdPms->nCurrentFrameIndex
, g_nRTVDescriptorSize);
CD3DX12_CPU_DESCRIPTOR_HANDLE dsvHandle(g_pIDSVHeap->GetCPUDescriptorHandleForHeapStart());
//设置渲染目标
pThdPms->pICmdList->OMSetRenderTargets(1, &stRTVHandle, FALSE, &dsvHandle);
pThdPms->pICmdList->RSSetViewports(1, &g_stViewPort);
pThdPms->pICmdList->RSSetScissorRects(1, &g_stScissorRect);
}
//---------------------------------------------------------------------------------------------
//渲染(实质就是记录渲染命令列表)
{
pThdPms->pICmdList->SetGraphicsRootSignature(pThdPms->pIRS);
pThdPms->pICmdList->SetPipelineState(pThdPms->pIPSO);
ID3D12DescriptorHeap* ppHeapsSphere[] = { pISRVCBVHp.Get(),pISampleHp.Get() };
pThdPms->pICmdList->SetDescriptorHeaps(_countof(ppHeapsSphere), ppHeapsSphere);
CD3DX12_GPU_DESCRIPTOR_HANDLE stGPUCBVHandleSphere(pISRVCBVHp->GetGPUDescriptorHandleForHeapStart());
//设置CBV
pThdPms->pICmdList->SetGraphicsRootDescriptorTable(0, stGPUCBVHandleSphere);
stGPUCBVHandleSphere.Offset(1, pThdPms->pID3DDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV));
//设置SRV
pThdPms->pICmdList->SetGraphicsRootDescriptorTable(1, stGPUCBVHandleSphere);
//设置Sample
pThdPms->pICmdList->SetGraphicsRootDescriptorTable(2, pISampleHp->GetGPUDescriptorHandleForHeapStart());
//注意我们使用的渲染手法是三角形列表,也就是通常的Mesh网格
pThdPms->pICmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
pThdPms->pICmdList->IASetVertexBuffers(0, 1, &stVBV);
pThdPms->pICmdList->IASetIndexBuffer(&stIBV);
//Draw Call!!!
pThdPms->pICmdList->DrawIndexedInstanced(nIndexCnt, 1, 0, 0, 0);
}
//完成渲染(即关闭命令列表,并设置同步对象通知主线程开始执行)
{
GRS_THROW_IF_FAILED(pThdPms->pICmdList->Close());
::SetEvent(pThdPms->hEventRenderOver); // 设置信号,通知主线程本线程渲染完毕
}
}
break;
case 1:
{//处理消息
while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{//这里只可能是别的线程发过来的消息,用于更复杂的场景
if (WM_QUIT != msg.message)
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
else
{
bQuit = TRUE;
}
}
}
break;
case WAIT_TIMEOUT:
break;
default:
break;
}
}
}
catch (CGRSCOMException&)
{
}
return 0;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_KEYDOWN:
{
USHORT n16KeyCode = (wParam & 0xFF);
if (VK_SPACE == n16KeyCode)
{//按空格键切换不同的采样器看效果,以明白每种采样器具体的含义
//UINT g_nCurrentSamplerNO = 0; //当前使用的采样器索引
//UINT g_nSampleMaxCnt = 5; //创建五个典型的采样器
//++g_nCurrentSamplerNO;
//g_nCurrentSamplerNO %= g_nSampleMaxCnt;
//=================================================================================================
//重新设置球体的捆绑包
//pICmdListSphere->Reset(pICmdAllocSphere.Get(), pIPSOSphere.Get());
//pICmdListSphere->SetGraphicsRootSignature(pIRootSignature.Get());
//pICmdListSphere->SetPipelineState(pIPSOSphere.Get());
//ID3D12DescriptorHeap* ppHeapsSphere[] = { pISRVCBVHp.Get(),pISampleHp.Get() };
//pICmdListSphere->SetDescriptorHeaps(_countof(ppHeapsSphere), ppHeapsSphere);
设置SRV
//pICmdListSphere->SetGraphicsRootDescriptorTable(0, pISRVCBVHp->GetGPUDescriptorHandleForHeapStart());
//CD3DX12_GPU_DESCRIPTOR_HANDLE stGPUCBVHandleSphere(pISRVCBVHp->GetGPUDescriptorHandleForHeapStart()
// , 1
// , pID3DDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV));
设置CBV
//pICmdListSphere->SetGraphicsRootDescriptorTable(1, stGPUCBVHandleSphere);
//CD3DX12_GPU_DESCRIPTOR_HANDLE hGPUSamplerSphere(pISampleHp->GetGPUDescriptorHandleForHeapStart()
// , g_nCurrentSamplerNO
// , nSamplerDescriptorSize);
设置Sample
//pICmdListSphere->SetGraphicsRootDescriptorTable(2, hGPUSamplerSphere);
注意我们使用的渲染手法是三角形列表,也就是通常的Mesh网格
//pICmdListSphere->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
//pICmdListSphere->IASetVertexBuffers(0, 1, &stVBV);
//pICmdListSphere->IASetIndexBuffer(&stIBV);
Draw Call!!!
//pICmdListSphere->DrawIndexedInstanced(nIndexCnt, 1, 0, 0, 0);
//pICmdListSphere->Close();
//=================================================================================================
}
if (VK_ADD == n16KeyCode || VK_OEM_PLUS == n16KeyCode)
{
//double g_fPalstance = 10.0f * XM_PI / 180.0f; //物体旋转的角速度,单位:弧度/秒
g_fPalstance += 10 * XM_PI / 180.0f;
if (g_fPalstance > XM_PI)
{
g_fPalstance = XM_PI;
}
//XMMatrixOrthographicOffCenterLH()
}
if (VK_SUBTRACT == n16KeyCode || VK_OEM_MINUS == n16KeyCode)
{
g_fPalstance -= 10 * XM_PI / 180.0f;
if (g_fPalstance < 0.0f)
{
g_fPalstance = XM_PI / 180.0f;
}
}
//根据用户输入变换
//XMVECTOR g_f3EyePos = XMVectorSet(0.0f, 5.0f, -10.0f, 0.0f); //眼睛位置
//XMVECTOR g_f3LockAt = XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f); //眼睛所盯的位置
//XMVECTOR g_f3HeapUp = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f); //头部正上方位置
XMFLOAT3 move(0, 0, 0);
float fMoveSpeed = 2.0f;
float fTurnSpeed = XM_PIDIV2 * 0.005f;
if ('w' == n16KeyCode || 'W' == n16KeyCode)
{
move.z -= 1.0f;
}
if ('s' == n16KeyCode || 'S' == n16KeyCode)
{
move.z += 1.0f;
}
if ('d' == n16KeyCode || 'D' == n16KeyCode)
{
move.x += 1.0f;
}
if ('a' == n16KeyCode || 'A' == n16KeyCode)
{
move.x -= 1.0f;
}
if (fabs(move.x) > 0.1f && fabs(move.z) > 0.1f)
{
XMVECTOR vector = XMVector3Normalize(XMLoadFloat3(&move));
move.x = XMVectorGetX(vector);
move.z = XMVectorGetZ(vector);
}
if (VK_UP == n16KeyCode)
{
g_fPitch += fTurnSpeed;
}
if (VK_DOWN == n16KeyCode)
{
g_fPitch -= fTurnSpeed;
}
if (VK_RIGHT == n16KeyCode)
{
g_fYaw -= fTurnSpeed;
}
if (VK_LEFT == n16KeyCode)
{
g_fYaw += fTurnSpeed;
}
// Prevent looking too far up or down.
g_fPitch = min(g_fPitch, XM_PIDIV4);
g_fPitch = max(-XM_PIDIV4, g_fPitch);
// Move the camera in model space.
float x = move.x * -cosf(g_fYaw) - move.z * sinf(g_fYaw);
float z = move.x * sinf(g_fYaw) - move.z * cosf(g_fYaw);
g_f3EyePos.x += x * fMoveSpeed;
g_f3EyePos.z += z * fMoveSpeed;
// Determine the look direction.
float r = cosf(g_fPitch);
g_f3LockAt.x = r * sinf(g_fYaw);
g_f3LockAt.y = sinf(g_fPitch);
g_f3LockAt.z = r * cosf(g_fYaw);
if (VK_TAB == n16KeyCode)
{//按Tab键还原摄像机位置
g_f3EyePos = XMFLOAT3(0.0f, 0.0f, -10.0f); //眼睛位置
g_f3LockAt = XMFLOAT3(0.0f, 0.0f, 0.0f); //眼睛所盯的位置
g_f3HeapUp = XMFLOAT3(0.0f, 1.0f, 0.0f); //头部正上方位置
}
}
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
BOOL LoadMeshVertex(const CHAR*pszMeshFileName, UINT&nVertexCnt, ST_GRS_VERTEX*&ppVertex, UINT*&ppIndices)
{
ifstream fin;
char input;
BOOL bRet = TRUE;
try
{
fin.open(pszMeshFileName);
if (fin.fail())
{
throw CGRSCOMException(E_FAIL);
}
fin.get(input);
while (input != ':')
{
fin.get(input);
}
fin >> nVertexCnt;
fin.get(input);
while (input != ':')
{
fin.get(input);
}
fin.get(input);
fin.get(input);
ppVertex = (ST_GRS_VERTEX*)HeapAlloc(::GetProcessHeap()
, HEAP_ZERO_MEMORY
, nVertexCnt * sizeof(ST_GRS_VERTEX));
ppIndices = (UINT*)HeapAlloc(::GetProcessHeap()
, HEAP_ZERO_MEMORY
, nVertexCnt * sizeof(UINT));
for (UINT i = 0; i < nVertexCnt; i++)
{
fin >> ppVertex[i].m_vPos.x >> ppVertex[i].m_vPos.y >> ppVertex[i].m_vPos.z;
ppVertex[i].m_vPos.w = 1.0f;
fin >> ppVertex[i].m_vTex.x >> ppVertex[i].m_vTex.y;
fin >> ppVertex[i].m_vNor.x >> ppVertex[i].m_vNor.y >> ppVertex[i].m_vNor.z;
ppIndices[i] = i;
}
}
catch (CGRSCOMException& e)
{
e;
bRet = FALSE;
}
return bRet;
}
5.4、充分利用MsgWait函数的特性
如果你仔细阅读了上面的完整示例代码之后,那么我想你一定会对使用了那么多Wait函数而感到一些不安,虽然它表面上让我们可以线性的理解和编写所谓多线程渲染的程序,但是这些Wait是不是会降低性能?或者甚至使我们本来认为应该是并行执行的程序,变成了可笑的“串行多线程程序”呢?那么有没有可能将它们与我们在消息循环中使用的MsgWait函数合体呢?而这一点我也在之前的教程中暗示过了,我们使用MsgWait函数的目的就是为了多线程框架而准备的。那么最终如何将它们合并到MsgWait函数中呢?
答案就是使用状态机。在我们的示例中,我们将各线程加载完资源并完成第一个COPY,准备执行第二个COPY的过程称之为状态0;而当第二个COPY完成之后也即我们等到了GPU执行完成的状态称之为状态1,在此状态中我们重新获取当前帧的序号,并开始新一帧的渲染,同时通知各线程开始渲染,然后进入状态2,在状态2中表示我们已经成功等到各线程完成了渲染,此时开始进行渲染后处理,收集并执行各线程记录的命令列表,并执行Present命令,最后进入等待重回状态1。最终渲染循环主要就在状态1和状态2之间完成。
整个过程的状态迁移图如下所示:
最终我们合并多个Wait函数后的消息循环框架变成如下情形:
详细代码请看后面完整代码列表。
5.5、完整代码2(充分利用MsgWait函数版)
#include <SDKDDKVer.h>
#define WIN32_LEAN_AND_MEAN // 从 Windows 头中排除极少使用的资料
#include <windows.h>
#include <tchar.h>
#include <fstream> //for ifstream
using namespace std;
#include <wrl.h> //添加WTL支持 方便使用COM
using namespace Microsoft;
using namespace Microsoft::WRL;
#include <atlcoll.h> //for atl array
#include <strsafe.h> //for StringCchxxxxx function
#include <dxgi1_6.h>
#include <d3d12.h> //for d3d12
#include <d3dcompiler.h>
#pragma comment(lib, "dxguid.lib")
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3d12.lib")
#pragma comment(lib, "d3dcompiler.lib")
#if defined(_DEBUG)
#include <dxgidebug.h>
#endif
#include <DirectXMath.h>
#include "..\WindowsCommons\d3dx12.h"
#include "..\WindowsCommons\DDSTextureLoader12.h"
using namespace DirectX;
#define GRS_WND_CLASS_NAME _T("Game Window Class")
#define GRS_WND_TITLE _T("DirectX12 MultiThread Sample")
#define GRS_THROW_IF_FAILED(hr) if (FAILED(hr)){ throw CGRSCOMException(hr); }
//新定义的宏用于上取整除法
#define GRS_UPPER_DIV(A,B) ((UINT)(((A)+((B)-1))/(B)))
//更简洁的向上边界对齐算法 内存管理中常用 请记住
#define GRS_UPPER(A,B) ((UINT)(((A)+((B)-1))&~(B - 1)))
//------------------------------------------------------------------------------------------------------------
// 为了调试加入下面的内联函数和宏定义,为每个接口对象设置名称,方便查看调试输出
#if defined(_DEBUG)
inline void GRS_SetD3D12DebugName(ID3D12Object* pObject, LPCWSTR name)
{
pObject->SetName(name);
}
inline void GRS_SetD3D12DebugNameIndexed(ID3D12Object* pObject, LPCWSTR name, UINT index)
{
WCHAR _DebugName[MAX_PATH] = {};
if (SUCCEEDED(StringCchPrintfW(_DebugName, _countof(_DebugName), L"%s[%u]", name, index)))
{
pObject->SetName(_DebugName);
}
}
#else
inline void GRS_SetD3D12DebugName(ID3D12Object*, LPCWSTR)
{
}
inline void GRS_SetD3D12DebugNameIndexed(ID3D12Object*, LPCWSTR, UINT)
{
}
#endif
#define GRS_SET_D3D12_DEBUGNAME(x) GRS_SetD3D12DebugName(x, L#x)
#define GRS_SET_D3D12_DEBUGNAME_INDEXED(x, n) GRS_SetD3D12DebugNameIndexed(x[n], L#x, n)
#define GRS_SET_D3D12_DEBUGNAME_COMPTR(x) GRS_SetD3D12DebugName(x.Get(), L#x)
#define GRS_SET_D3D12_DEBUGNAME_INDEXED_COMPTR(x, n) GRS_SetD3D12DebugNameIndexed(x[n].Get(), L#x, n)
#if defined(_DEBUG)
inline void GRS_SetDXGIDebugName(IDXGIObject* pObject, LPCWSTR name)
{
size_t szLen = 0;
StringCchLengthW(name, 50, &szLen);
pObject->SetPrivateData(WKPDID_D3DDebugObjectName, static_cast<UINT>(szLen - 1), name);
}
inline void GRS_SetDXGIDebugNameIndexed(IDXGIObject* pObject, LPCWSTR name, UINT index)
{
size_t szLen = 0;
WCHAR _DebugName[MAX_PATH] = {};
if (SUCCEEDED(StringCchPrintfW(_DebugName, _countof(_DebugName), L"%s[%u]", name, index)))
{
StringCchLengthW(_DebugName, _countof(_DebugName), &szLen);
pObject->SetPrivateData(WKPDID_D3DDebugObjectName, static_cast<UINT>(szLen), _DebugName);
}
}
#else
inline void GRS_SetDXGIDebugName(ID3D12Object*, LPCWSTR)
{
}
inline void GRS_SetDXGIDebugNameIndexed(ID3D12Object*, LPCWSTR, UINT)
{
}
#endif
#define GRS_SET_DXGI_DEBUGNAME(x) GRS_SetDXGIDebugName(x, L#x)
#define GRS_SET_DXGI_DEBUGNAME_INDEXED(x, n) GRS_SetDXGIDebugNameIndexed(x[n], L#x, n)
#define GRS_SET_DXGI_DEBUGNAME_COMPTR(x) GRS_SetDXGIDebugName(x.Get(), L#x)
#define GRS_SET_DXGI_DEBUGNAME_INDEXED_COMPTR(x, n) GRS_SetDXGIDebugNameIndexed(x[n].Get(), L#x, n)
//------------------------------------------------------------------------------------------------------------
class CGRSCOMException
{
public:
CGRSCOMException(HRESULT hr) : m_hrError(hr)
{
}
HRESULT Error() const
{
return m_hrError;
}
private:
const HRESULT m_hrError;
};
// 顶点结构
struct ST_GRS_VERTEX
{
XMFLOAT4 m_vPos; //Position
XMFLOAT2 m_vTex; //Texcoord
XMFLOAT3 m_vNor; //Normal
};
// 常量缓冲区
struct ST_GRS_MVP
{
XMFLOAT4X4 m_MVP; //经典的Model-view-projection(MVP)矩阵.
};
// 渲染子线程参数
struct ST_GRS_THREAD_PARAMS
{
UINT nIndex; //序号
DWORD dwThisThreadID;
HANDLE hThisThread;
DWORD dwMainThreadID;
HANDLE hMainThread;
HANDLE hRunEvent;
HANDLE hEventRenderOver;
UINT nCurrentFrameIndex;//当前渲染后缓冲序号
ULONGLONG nStartTime; //当前帧开始时间
ULONGLONG nCurrentTime; //当前时间
XMFLOAT4 v4ModelPos;
const TCHAR* pszDDSFile;
const CHAR* pszMeshFile;
ID3D12Device4* pID3DDevice;
ID3D12CommandAllocator* pICmdAlloc;
ID3D12GraphicsCommandList* pICmdList;
ID3D12RootSignature* pIRS;
ID3D12PipelineState* pIPSO;
};
UINT __stdcall RenderThread(void* pParam);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
BOOL LoadMeshVertex(const CHAR*pszMeshFileName, UINT&nVertexCnt, ST_GRS_VERTEX*&ppVertex, UINT*&ppIndices);
int g_iWndWidth = 1024;
int g_iWndHeight = 768;
CD3DX12_VIEWPORT g_stViewPort(0.0f, 0.0f, static_cast<float>(g_iWndWidth), static_cast<float>(g_iWndHeight));
CD3DX12_RECT g_stScissorRect(0, 0, static_cast<LONG>(g_iWndWidth), static_cast<LONG>(g_iWndHeight));
//初始的默认摄像机的位置
XMFLOAT3 g_f3EyePos = XMFLOAT3(0.0f, 5.0f, -10.0f); //眼睛位置
XMFLOAT3 g_f3LockAt = XMFLOAT3(0.0f, 0.0f, 0.0f); //眼睛所盯的位置
XMFLOAT3 g_f3HeapUp = XMFLOAT3(0.0f, 1.0f, 0.0f); //头部正上方位置
float g_fYaw = 0.0f; // 绕正Z轴的旋转量.
float g_fPitch = 0.0f; // 绕XZ平面的旋转量
double g_fPalstance = 10.0f * XM_PI / 180.0f; //物体旋转的角速度,单位:弧度/秒
XMFLOAT4X4 g_mxWorld = {}; //World Matrix
XMFLOAT4X4 g_mxVP = {}; //View Projection Matrix
// 全局线程参数
const UINT g_nMaxThread = 3;
const UINT g_nThdSphere = 0;
const UINT g_nThdCube = 1;
const UINT g_nThdPlane = 2;
ST_GRS_THREAD_PARAMS g_stThreadParams[g_nMaxThread] = {};
const UINT g_nFrameBackBufCount = 3u;
UINT g_nRTVDescriptorSize = 0U;
ComPtr<ID3D12Resource> g_pIARenderTargets[g_nFrameBackBufCount];
ComPtr<ID3D12DescriptorHeap> g_pIRTVHeap;
ComPtr<ID3D12DescriptorHeap> g_pIDSVHeap; //深度缓冲描述符堆
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
{
::CoInitialize(nullptr); //for WIC & COM
HWND hWnd = nullptr;
MSG msg = {};
UINT nDXGIFactoryFlags = 0U;
ComPtr<IDXGIFactory5> pIDXGIFactory5;
ComPtr<IDXGIAdapter1> pIAdapter;
ComPtr<ID3D12Device4> pID3DDevice;
ComPtr<ID3D12CommandQueue> pIMainCmdQueue;
ComPtr<IDXGISwapChain1> pISwapChain1;
ComPtr<IDXGISwapChain3> pISwapChain3;
ComPtr<ID3D12Resource> pIDepthStencilBuffer; //深度蜡板缓冲区
UINT nCurrentFrameIndex = 0;
DXGI_FORMAT emRTFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
DXGI_FORMAT emDSFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;
ComPtr<ID3D12Fence> pIFence;
UINT64 n64FenceValue = 1ui64;
HANDLE hFenceEvent = nullptr;
ComPtr<ID3D12CommandAllocator> pICmdAllocPre;
ComPtr<ID3D12GraphicsCommandList> pICmdListPre;
ComPtr<ID3D12CommandAllocator> pICmdAllocPost;
ComPtr<ID3D12GraphicsCommandList> pICmdListPost;
CAtlArray<HANDLE> arHWaited;
CAtlArray<HANDLE> arHSubThread;
ComPtr<ID3DBlob> pIVSSphere;
ComPtr<ID3DBlob> pIPSSphere;
ComPtr<ID3D12RootSignature> pIRootSignature;
ComPtr<ID3D12PipelineState> pIPSOSphere;
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, g_iWndWidth, g_iWndHeight };
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)
{
throw CGRSCOMException(HRESULT_FROM_WIN32(GetLastError()));
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
}
//2、打开显示子系统的调试支持
{
#if defined(_DEBUG)
ComPtr<ID3D12Debug> debugController;
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
{
debugController->EnableDebugLayer();
// 打开附加的调试支持
nDXGIFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG;
}
#endif
}
//3、创建DXGI Factory对象
{
GRS_THROW_IF_FAILED(CreateDXGIFactory2(nDXGIFactoryFlags, IID_PPV_ARGS(&pIDXGIFactory5)));
GRS_SET_DXGI_DEBUGNAME_COMPTR(pIDXGIFactory5);
// 关闭ALT+ENTER键切换全屏的功能,因为我们没有实现OnSize处理,所以先关闭
GRS_THROW_IF_FAILED(pIDXGIFactory5->MakeWindowAssociation(hWnd, DXGI_MWA_NO_ALT_ENTER));
}
//4、枚举适配器创建设备
{//选择NUMA架构的独显来创建3D设备对象,暂时先不支持集显了,当然你可以修改这些行为
D3D12_FEATURE_DATA_ARCHITECTURE stArchitecture = {};
for (UINT nAdapterIndex = 0; DXGI_ERROR_NOT_FOUND != pIDXGIFactory5->EnumAdapters1(nAdapterIndex, &pIAdapter); ++nAdapterIndex)
{
DXGI_ADAPTER_DESC1 stAdapterDesc = {};
pIAdapter->GetDesc1(&stAdapterDesc);
if (stAdapterDesc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
{//跳过软件虚拟适配器设备
continue;
}
GRS_THROW_IF_FAILED(D3D12CreateDevice(pIAdapter.Get(), D3D_FEATURE_LEVEL_12_1, IID_PPV_ARGS(&pID3DDevice)));
GRS_THROW_IF_FAILED(pID3DDevice->CheckFeatureSupport(D3D12_FEATURE_ARCHITECTURE
, &stArchitecture, sizeof(D3D12_FEATURE_DATA_ARCHITECTURE)));
if (!stArchitecture.UMA)
{
break;
}
pID3DDevice.Reset();
}
//---------------------------------------------------------------------------------------------
if (nullptr == pID3DDevice.Get())
{// 可怜的机器上居然没有独显 还是先退出了事
throw CGRSCOMException(E_FAIL);
}
GRS_SET_D3D12_DEBUGNAME_COMPTR(pID3DDevice);
}
//5、创建直接命令队列
{
D3D12_COMMAND_QUEUE_DESC stQueueDesc = {};
stQueueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandQueue(&stQueueDesc, IID_PPV_ARGS(&pIMainCmdQueue)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIMainCmdQueue);
}
//6、创建交换链
{
DXGI_SWAP_CHAIN_DESC1 stSwapChainDesc = {};
stSwapChainDesc.BufferCount = g_nFrameBackBufCount;
stSwapChainDesc.Width = g_iWndWidth;
stSwapChainDesc.Height = g_iWndHeight;
stSwapChainDesc.Format = emRTFormat;
stSwapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
stSwapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
stSwapChainDesc.SampleDesc.Count = 1;
GRS_THROW_IF_FAILED(pIDXGIFactory5->CreateSwapChainForHwnd(
pIMainCmdQueue.Get(), // Swap chain needs the queue so that it can force a flush on it.
hWnd,
&stSwapChainDesc,
nullptr,
nullptr,
&pISwapChain1
));
GRS_SET_DXGI_DEBUGNAME_COMPTR(pISwapChain1);
//注意此处使用了高版本的SwapChain接口的函数
GRS_THROW_IF_FAILED(pISwapChain1.As(&pISwapChain3));
GRS_SET_DXGI_DEBUGNAME_COMPTR(pISwapChain3);
// 获取当前第一个供绘制的后缓冲序号
nCurrentFrameIndex = pISwapChain3->GetCurrentBackBufferIndex();
//创建RTV(渲染目标视图)描述符堆(这里堆的含义应当理解为数组或者固定大小元素的固定大小显存池)
D3D12_DESCRIPTOR_HEAP_DESC stRTVHeapDesc = {};
stRTVHeapDesc.NumDescriptors = g_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(&g_pIRTVHeap)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(g_pIRTVHeap);
//得到每个描述符元素的大小
g_nRTVDescriptorSize = pID3DDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
//---------------------------------------------------------------------------------------------
CD3DX12_CPU_DESCRIPTOR_HANDLE stRTVHandle(g_pIRTVHeap->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < g_nFrameBackBufCount; i++)
{//这个循环暴漏了描述符堆实际上是个数组的本质
GRS_THROW_IF_FAILED(pISwapChain3->GetBuffer(i, IID_PPV_ARGS(&g_pIARenderTargets[i])));
GRS_SET_D3D12_DEBUGNAME_INDEXED_COMPTR(g_pIARenderTargets, i);
pID3DDevice->CreateRenderTargetView(g_pIARenderTargets[i].Get(), nullptr, stRTVHandle);
stRTVHandle.Offset(1, g_nRTVDescriptorSize);
}
}
//7、创建深度缓冲及深度缓冲描述符堆
{
D3D12_CLEAR_VALUE stDepthOptimizedClearValue = {};
stDepthOptimizedClearValue.Format = emDSFormat;
stDepthOptimizedClearValue.DepthStencil.Depth = 1.0f;
stDepthOptimizedClearValue.DepthStencil.Stencil = 0;
//使用隐式默认堆创建一个深度蜡板缓冲区,
//因为基本上深度缓冲区会一直被使用,重用的意义不大,所以直接使用隐式堆,图方便
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT)
, D3D12_HEAP_FLAG_NONE
, &CD3DX12_RESOURCE_DESC::Tex2D(emDSFormat
, g_iWndWidth
, g_iWndHeight
, 1
, 0
, 1
, 0
, D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL)
, D3D12_RESOURCE_STATE_DEPTH_WRITE
, &stDepthOptimizedClearValue
, IID_PPV_ARGS(&pIDepthStencilBuffer)
));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIDepthStencilBuffer);
D3D12_DEPTH_STENCIL_VIEW_DESC stDepthStencilDesc = {};
stDepthStencilDesc.Format = emDSFormat;
stDepthStencilDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
stDepthStencilDesc.Flags = D3D12_DSV_FLAG_NONE;
D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = {};
dsvHeapDesc.NumDescriptors = 1;
dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
GRS_THROW_IF_FAILED(pID3DDevice->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&g_pIDSVHeap)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(g_pIDSVHeap);
pID3DDevice->CreateDepthStencilView(pIDepthStencilBuffer.Get()
, &stDepthStencilDesc
, g_pIDSVHeap->GetCPUDescriptorHandleForHeapStart());
}
//8、创建直接命令列表
{
// 预处理命令列表
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT
, IID_PPV_ARGS(&pICmdAllocPre)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pICmdAllocPre);
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT
, pICmdAllocPre.Get(), nullptr, IID_PPV_ARGS(&pICmdListPre)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pICmdListPre);
//后处理命令列表
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT
, IID_PPV_ARGS(&pICmdAllocPost)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pICmdAllocPost);
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT
, pICmdAllocPost.Get(), nullptr, IID_PPV_ARGS(&pICmdListPost)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pICmdListPost);
}
//9、创建根签名
{//这个例子中,所有物体使用相同的根签名,因为渲染过程中需要的参数是一样的
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;
}
CD3DX12_DESCRIPTOR_RANGE1 stDSPRanges[3];
stDSPRanges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0);
stDSPRanges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0);
stDSPRanges[2].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 1, 0);
CD3DX12_ROOT_PARAMETER1 stRootParameters[3];
stRootParameters[0].InitAsDescriptorTable(1, &stDSPRanges[0], D3D12_SHADER_VISIBILITY_ALL); //CBV是所有Shader可见
stRootParameters[1].InitAsDescriptorTable(1, &stDSPRanges[1], D3D12_SHADER_VISIBILITY_PIXEL);//SRV仅PS可见
stRootParameters[2].InitAsDescriptorTable(1, &stDSPRanges[2], D3D12_SHADER_VISIBILITY_PIXEL);//SAMPLE仅PS可见
CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC stRootSignatureDesc;
stRootSignatureDesc.Init_1_1(_countof(stRootParameters)
, stRootParameters
, 0
, nullptr
, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
ComPtr<ID3DBlob> pISignatureBlob;
ComPtr<ID3DBlob> 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)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIRootSignature);
}
//10、编译Shader创建渲染管线状态对象
{
#if defined(_DEBUG)
// Enable better shader debugging with the graphics debugging tools.
UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#else
UINT compileFlags = 0;
#endif
//编译为行矩阵形式
compileFlags |= D3DCOMPILE_PACK_MATRIX_ROW_MAJOR;
TCHAR pszShaderFileName[] = _T("D:\\Projects_2018_08\\D3D12 Tutorials\\6-MultiThread\\Shader\\TextureCube.hlsl");
GRS_THROW_IF_FAILED(D3DCompileFromFile(pszShaderFileName, nullptr, nullptr
, "VSMain", "vs_5_0", compileFlags, 0, &pIVSSphere, nullptr));
GRS_THROW_IF_FAILED(D3DCompileFromFile(pszShaderFileName, nullptr, nullptr
, "PSMain", "ps_5_0", compileFlags, 0, &pIPSSphere, nullptr));
// 我们多添加了一个法线的定义,但目前Shader中我们并没有使用
D3D12_INPUT_ELEMENT_DESC stIALayoutSphere[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 16, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};
// 创建 graphics pipeline state object (PSO)对象
D3D12_GRAPHICS_PIPELINE_STATE_DESC stPSODesc = {};
stPSODesc.InputLayout = { stIALayoutSphere, _countof(stIALayoutSphere) };
stPSODesc.pRootSignature = pIRootSignature.Get();
stPSODesc.VS = CD3DX12_SHADER_BYTECODE(pIVSSphere.Get());
stPSODesc.PS = CD3DX12_SHADER_BYTECODE(pIPSSphere.Get());
stPSODesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
stPSODesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
stPSODesc.SampleMask = UINT_MAX;
stPSODesc.SampleDesc.Count = 1;
stPSODesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
stPSODesc.NumRenderTargets = 1;
stPSODesc.RTVFormats[0] = emRTFormat;
stPSODesc.DSVFormat = emDSFormat;
stPSODesc.DepthStencilState.StencilEnable = FALSE;
stPSODesc.DepthStencilState.DepthEnable = TRUE; //打开深度缓冲
stPSODesc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;//启用深度缓存写入功能
stPSODesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS; //深度测试函数(该值为普通的深度测试,即较小值写入)
GRS_THROW_IF_FAILED(pID3DDevice->CreateGraphicsPipelineState(&stPSODesc
, IID_PPV_ARGS(&pIPSOSphere)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIPSOSphere);
}
//11、准备参数并启动多个渲染线程
{
// 球体个性参数
g_stThreadParams[g_nThdSphere].pszDDSFile = _T("D:\\Projects_2018_08\\D3D12 Tutorials\\6-MultiThread\\Mesh\\sphere.dds");
g_stThreadParams[g_nThdSphere].pszMeshFile = "D:\\Projects_2018_08\\D3D12 Tutorials\\6-MultiThread\\Mesh\\sphere.txt";
g_stThreadParams[g_nThdSphere].v4ModelPos = XMFLOAT4(2.0f, 2.0f, 0.0f, 1.0f);
// 立方体个性参数
g_stThreadParams[g_nThdCube].pszDDSFile = _T("D:\\Projects_2018_08\\D3D12 Tutorials\\6-MultiThread\\Mesh\\Cube.dds");
g_stThreadParams[g_nThdCube].pszMeshFile = "D:\\Projects_2018_08\\D3D12 Tutorials\\6-MultiThread\\Mesh\\Cube.txt";
g_stThreadParams[g_nThdCube].v4ModelPos = XMFLOAT4(-2.0f, 2.0f, 0.0f, 1.0f);
// 平板个性参数
g_stThreadParams[g_nThdPlane].pszDDSFile = _T("D:\\Projects_2018_08\\D3D12 Tutorials\\6-MultiThread\\Mesh\\Plane.dds");
g_stThreadParams[g_nThdPlane].pszMeshFile = "D:\\Projects_2018_08\\D3D12 Tutorials\\6-MultiThread\\Mesh\\Plane.txt";
g_stThreadParams[g_nThdPlane].v4ModelPos = XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f);
// 物体的共性参数,也就是各线程的共性参数
for (int i = 0; i < g_nMaxThread; i++)
{
g_stThreadParams[i].nIndex = i; //记录序号
//创建每个线程需要的命令列表和复制命令队列
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT
, IID_PPV_ARGS(&g_stThreadParams[i].pICmdAlloc)));
GRS_SetD3D12DebugNameIndexed(g_stThreadParams[i].pICmdAlloc, _T("pIThreadCmdAlloc"), i);
GRS_THROW_IF_FAILED(pID3DDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT
, g_stThreadParams[i].pICmdAlloc, nullptr, IID_PPV_ARGS(&g_stThreadParams[i].pICmdList)));
GRS_SetD3D12DebugNameIndexed(g_stThreadParams[i].pICmdList, _T("pIThreadCmdList"), i);
g_stThreadParams[i].dwMainThreadID = ::GetCurrentThreadId();
g_stThreadParams[i].hMainThread = ::GetCurrentThread();
g_stThreadParams[i].hRunEvent = ::CreateEvent(nullptr, FALSE, FALSE, nullptr);
g_stThreadParams[i].hEventRenderOver = ::CreateEvent(nullptr, FALSE, FALSE, nullptr);
g_stThreadParams[i].pID3DDevice = pID3DDevice.Get();
g_stThreadParams[i].pIRS = pIRootSignature.Get();
g_stThreadParams[i].pIPSO = pIPSOSphere.Get();
arHWaited.Add(g_stThreadParams[i].hEventRenderOver); //添加到被等待队列里
//以暂停方式创建线程
g_stThreadParams[i].hThisThread = (HANDLE)_beginthreadex(nullptr,
0, RenderThread, (void*)&g_stThreadParams[i],
CREATE_SUSPENDED, (UINT*)&g_stThreadParams[i].dwThisThreadID);
//然后判断线程创建是否成功
if (nullptr == g_stThreadParams[i].hThisThread
|| reinterpret_cast<HANDLE>(-1) == g_stThreadParams[i].hThisThread)
{
throw CGRSCOMException(HRESULT_FROM_WIN32(GetLastError()));
}
arHSubThread.Add(g_stThreadParams[i].hThisThread);
}
//逐一启动线程
for (int i = 0; i < g_nMaxThread; i++)
{
::ResumeThread(g_stThreadParams[i].hThisThread);
}
}
//12、创建围栏对象
{
GRS_THROW_IF_FAILED(pID3DDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&pIFence)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIFence);
//创建一个Event同步对象,用于等待围栏事件通知
hFenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (hFenceEvent == nullptr)
{
GRS_THROW_IF_FAILED(HRESULT_FROM_WIN32(GetLastError()));
}
}
UINT nStates = 0; //初识状态为0
DWORD dwRet = 0;
CAtlArray<ID3D12CommandList*> arCmdList;
UINT64 n64fence = 0;
//---------------------------------------------------------------------------------------------
BOOL bExit = FALSE;
ULONGLONG n64tmFrameStart = ::GetTickCount64();
ULONGLONG n64tmCurrent = n64tmFrameStart;
//计算旋转角度需要的变量
double dModelRotationYAngle = 0.0f;
//起始的时候关闭一下两个命令列表,因为我们在开始渲染的时候都需要先reset它们,为了防止报错故先Close
GRS_THROW_IF_FAILED(pICmdListPre->Close());
GRS_THROW_IF_FAILED(pICmdListPost->Close());
//13、开始消息循环,并在其中不断渲染
while (!bExit)
{//注意这里我们调整了消息循环,将等待时间设置为0,同时将定时性的渲染,改成了每次循环都渲染
//特别注意这次等待与之前不同
//主线程进入等待
dwRet = ::MsgWaitForMultipleObjects(static_cast<DWORD>(arHWaited.GetCount()), arHWaited.GetData(), TRUE, 10, QS_ALLINPUT);
if (0 == (dwRet - WAIT_OBJECT_0))
{
switch (nStates)
{
case 0://状态0,表示等到各子线程加载资源完毕,此时执行一次命令列表完成各子线程要求的资源上传的第二个Copy命令
{
arCmdList.RemoveAll();
//执行命令列表
arCmdList.Add(g_stThreadParams[g_nThdSphere].pICmdList);
arCmdList.Add(g_stThreadParams[g_nThdCube].pICmdList);
arCmdList.Add(g_stThreadParams[g_nThdPlane].pICmdList);
pIMainCmdQueue->ExecuteCommandLists(static_cast<UINT>(arCmdList.GetCount()), arCmdList.GetData());
//---------------------------------------------------------------------------------------------
//开始同步GPU与CPU的执行,先记录围栏标记值
n64fence = n64FenceValue;
GRS_THROW_IF_FAILED(pIMainCmdQueue->Signal(pIFence.Get(), n64fence));
n64FenceValue++;
GRS_THROW_IF_FAILED(pIFence->SetEventOnCompletion(n64fence, hFenceEvent));
nStates = 1;
arHWaited.RemoveAll();
arHWaited.Add(hFenceEvent);
}
break;
case 1:// 状态1 等到命令队列执行结束(也即CPU等到GPU执行结束),开始新一轮渲染
{
GRS_THROW_IF_FAILED(pICmdAllocPre->Reset());
GRS_THROW_IF_FAILED(pICmdListPre->Reset(pICmdAllocPre.Get(), pIPSOSphere.Get()));
GRS_THROW_IF_FAILED(pICmdAllocPost->Reset());
GRS_THROW_IF_FAILED(pICmdListPost->Reset(pICmdAllocPost.Get(), pIPSOSphere.Get()));
nCurrentFrameIndex = pISwapChain3->GetCurrentBackBufferIndex();
//计算全局的Matrix
{
//关于时间的基本运算都放在了主线程中
//真实的引擎或程序中建议时间值也作为一个每帧更新的参数从主线程获取并传给各子线程
n64tmCurrent = ::GetTickCount();
//计算旋转的角度:旋转角度(弧度) = 时间(秒) * 角速度(弧度/秒)
//下面这句代码相当于经典游戏消息循环中的OnUpdate函数中需要做的事情
dModelRotationYAngle += ((n64tmCurrent - n64tmFrameStart) / 1000.0f) * g_fPalstance;
//旋转角度是2PI周期的倍数,去掉周期数,只留下相对0弧度开始的小于2PI的弧度即可
if (dModelRotationYAngle > XM_2PI)
{
dModelRotationYAngle = fmod(dModelRotationYAngle, XM_2PI);
}
//计算 World 矩阵 这里是个旋转矩阵
XMStoreFloat4x4(&g_mxWorld, XMMatrixRotationY(static_cast<float>(dModelRotationYAngle)));
//计算 视矩阵 view * 裁剪矩阵 projection
XMStoreFloat4x4(&g_mxVP
, XMMatrixMultiply(XMMatrixLookAtLH(XMLoadFloat3(&g_f3EyePos)
, XMLoadFloat3(&g_f3LockAt)
, XMLoadFloat3(&g_f3HeapUp))
, XMMatrixPerspectiveFovLH(XM_PIDIV4
, (FLOAT)g_iWndWidth / (FLOAT)g_iWndHeight, 0.1f, 1000.0f)));
}
//渲染前处理
{
pICmdListPre->ResourceBarrier(1
, &CD3DX12_RESOURCE_BARRIER::Transition(
g_pIARenderTargets[nCurrentFrameIndex].Get()
, D3D12_RESOURCE_STATE_PRESENT
, D3D12_RESOURCE_STATE_RENDER_TARGET)
);
//偏移描述符指针到指定帧缓冲视图位置
CD3DX12_CPU_DESCRIPTOR_HANDLE stRTVHandle(g_pIRTVHeap->GetCPUDescriptorHandleForHeapStart()
, nCurrentFrameIndex, g_nRTVDescriptorSize);
CD3DX12_CPU_DESCRIPTOR_HANDLE dsvHandle(g_pIDSVHeap->GetCPUDescriptorHandleForHeapStart());
//设置渲染目标
pICmdListPre->OMSetRenderTargets(1, &stRTVHandle, FALSE, &dsvHandle);
pICmdListPre->RSSetViewports(1, &g_stViewPort);
pICmdListPre->RSSetScissorRects(1, &g_stScissorRect);
const float clearColor[] = { 0.0f, 0.1f, 0.0f, 1.0f };
pICmdListPre->ClearRenderTargetView(stRTVHandle, clearColor, 0, nullptr);
pICmdListPre->ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
}
nStates = 2;
arHWaited.RemoveAll();
arHWaited.Add(g_stThreadParams[g_nThdSphere].hEventRenderOver);
arHWaited.Add(g_stThreadParams[g_nThdCube].hEventRenderOver);
arHWaited.Add(g_stThreadParams[g_nThdPlane].hEventRenderOver);
//通知各线程开始渲染
for (int i = 0; i < g_nMaxThread; i++)
{
g_stThreadParams[i].nCurrentFrameIndex = nCurrentFrameIndex;
g_stThreadParams[i].nStartTime = n64tmFrameStart;
g_stThreadParams[i].nCurrentTime = n64tmCurrent;
SetEvent(g_stThreadParams[i].hRunEvent);
}
}
break;
case 2:// 状态2 表示所有的渲染命令列表都记录完成了,开始后处理和执行命令列表
{
pICmdListPost->ResourceBarrier(1
, &CD3DX12_RESOURCE_BARRIER::Transition(
g_pIARenderTargets[nCurrentFrameIndex].Get()
, D3D12_RESOURCE_STATE_RENDER_TARGET
, D3D12_RESOURCE_STATE_PRESENT));
//关闭命令列表,可以去执行了
GRS_THROW_IF_FAILED(pICmdListPre->Close());
GRS_THROW_IF_FAILED(pICmdListPost->Close());
arCmdList.RemoveAll();
//执行命令列表(注意命令列表排队组合的方式)
arCmdList.Add(pICmdListPre.Get());
arCmdList.Add(g_stThreadParams[g_nThdSphere].pICmdList);
arCmdList.Add(g_stThreadParams[g_nThdCube].pICmdList);
arCmdList.Add(g_stThreadParams[g_nThdPlane].pICmdList);
arCmdList.Add(pICmdListPost.Get());
pIMainCmdQueue->ExecuteCommandLists(static_cast<UINT>(arCmdList.GetCount()), arCmdList.GetData());
//提交画面
GRS_THROW_IF_FAILED(pISwapChain3->Present(1, 0));
//开始同步GPU与CPU的执行,先记录围栏标记值
n64fence = n64FenceValue;
GRS_THROW_IF_FAILED(pIMainCmdQueue->Signal(pIFence.Get(), n64fence));
n64FenceValue++;
GRS_THROW_IF_FAILED(pIFence->SetEventOnCompletion(n64fence, hFenceEvent));
nStates = 1;
arHWaited.RemoveAll();
arHWaited.Add(hFenceEvent);
//更新下帧开始时间为上一帧开始时间
n64tmFrameStart = n64tmCurrent;
}
break;
default:// 不可能的情况,但为了避免讨厌的编译警告或意外情况保留这个default
{
bExit = TRUE;
}
break;
}
}
else //为了能正常处理消息故直接else,因为上面的渲染循环会抢占线程资源,
{//处理消息
while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (WM_QUIT != msg.message)
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
else
{
bExit = TRUE;
}
}
}
//---------------------------------------------------------------------------------------------
//检测一下线程的活动情况,如果有线程已经退出了,就退出循环
dwRet = WaitForMultipleObjects(static_cast<DWORD>(arHSubThread.GetCount()), arHSubThread.GetData(), FALSE, 0);
dwRet -= WAIT_OBJECT_0;
if (dwRet >= 0 && dwRet < g_nMaxThread)
{
bExit = TRUE;
}
}
}
catch (CGRSCOMException& e)
{//发生了COM异常
e;
}
try
{
// 通知子线程退出
for (int i = 0; i < g_nMaxThread; i++)
{
::PostThreadMessage(g_stThreadParams[i].dwThisThreadID, WM_QUIT, 0, 0);
}
// 等待所有子线程退出
DWORD dwRet = WaitForMultipleObjects(static_cast<DWORD>(arHSubThread.GetCount()), arHSubThread.GetData(), TRUE, INFINITE);
// 清理所有子线程资源
for (int i = 0; i < g_nMaxThread; i++)
{
::CloseHandle(g_stThreadParams[i].hThisThread);
::CloseHandle(g_stThreadParams[i].hEventRenderOver);
g_stThreadParams[i].pICmdList->Release();
g_stThreadParams[i].pICmdAlloc->Release();
}
//::CoUninitialize();
}
catch (CGRSCOMException& e)
{//发生了COM异常
e;
}
::CoUninitialize();
return 0;
}
UINT __stdcall RenderThread(void* pParam)
{
ST_GRS_THREAD_PARAMS* pThdPms = static_cast<ST_GRS_THREAD_PARAMS*>(pParam);
try
{
if (nullptr == pThdPms)
{//参数异常,抛异常终止线程
throw CGRSCOMException(E_INVALIDARG);
}
SIZE_T szMVPBuf = GRS_UPPER(sizeof(ST_GRS_MVP), 256);
ComPtr<ID3D12Resource> pITexture;
ComPtr<ID3D12Resource> pITextureUpload;
ComPtr<ID3D12Resource> pIVB;
ComPtr<ID3D12Resource> pIIB;
ComPtr<ID3D12Resource> pICBWVP;
ComPtr<ID3D12DescriptorHeap> pISRVCBVHp;
ComPtr<ID3D12DescriptorHeap> pISampleHp;
ST_GRS_MVP* pMVPBufModule = nullptr;
D3D12_VERTEX_BUFFER_VIEW stVBV = {};
D3D12_INDEX_BUFFER_VIEW stIBV = {};
UINT nIndexCnt = 0;
XMMATRIX mxPosModule = XMMatrixTranslationFromVector(XMLoadFloat4(&pThdPms->v4ModelPos)); //当前渲染物体的位置
// Mesh Value
ST_GRS_VERTEX* pstVertices = nullptr;
UINT* pnIndices = nullptr;
UINT nVertexCnt = 0;
// DDS Value
std::unique_ptr<uint8_t[]> pbDDSData;
std::vector<D3D12_SUBRESOURCE_DATA> stArSubResources;
DDS_ALPHA_MODE emAlphaMode = DDS_ALPHA_MODE_UNKNOWN;
bool bIsCube = false;
//1、加载DDS纹理
{
GRS_THROW_IF_FAILED(LoadDDSTextureFromFile(pThdPms->pID3DDevice, pThdPms->pszDDSFile, pITexture.GetAddressOf()
, pbDDSData, stArSubResources, SIZE_MAX, &emAlphaMode, &bIsCube));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pITexture);
UINT64 n64szUpSphere = GetRequiredIntermediateSize(pITexture.Get(), 0, static_cast<UINT>(stArSubResources.size()));
D3D12_RESOURCE_DESC stTXDesc = pITexture->GetDesc();
GRS_THROW_IF_FAILED(pThdPms->pID3DDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD)
, D3D12_HEAP_FLAG_NONE
, &CD3DX12_RESOURCE_DESC::Buffer(n64szUpSphere)
, D3D12_RESOURCE_STATE_GENERIC_READ
, nullptr
, IID_PPV_ARGS(&pITextureUpload)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pITextureUpload);
UpdateSubresources(pThdPms->pICmdList
, pITexture.Get()
, pITextureUpload.Get()
, 0
, 0
, static_cast<UINT>(stArSubResources.size())
, stArSubResources.data());
//同步
pThdPms->pICmdList->ResourceBarrier(1
, &CD3DX12_RESOURCE_BARRIER::Transition(pITexture.Get()
, D3D12_RESOURCE_STATE_COPY_DEST
, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE));
D3D12_DESCRIPTOR_HEAP_DESC stSRVCBVHPDesc = {};
stSRVCBVHPDesc.NumDescriptors = 2; // 1 CBV + 1 SRV
stSRVCBVHPDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
stSRVCBVHPDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
GRS_THROW_IF_FAILED(pThdPms->pID3DDevice->CreateDescriptorHeap(&stSRVCBVHPDesc, IID_PPV_ARGS(&pISRVCBVHp)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pISRVCBVHp);
//创建SRV
D3D12_SHADER_RESOURCE_VIEW_DESC stSRVDesc = {};
stSRVDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
stSRVDesc.Format = stTXDesc.Format;
stSRVDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
stSRVDesc.Texture2D.MipLevels = 1;
CD3DX12_CPU_DESCRIPTOR_HANDLE stCbvSrvHandle(pISRVCBVHp->GetCPUDescriptorHandleForHeapStart()
, 1, pThdPms->pID3DDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV));
pThdPms->pID3DDevice->CreateShaderResourceView(pITexture.Get()
, &stSRVDesc
, stCbvSrvHandle);
}
//2、创建Sample
{
D3D12_DESCRIPTOR_HEAP_DESC stSamplerHeapDesc = {};
stSamplerHeapDesc.NumDescriptors = 1;
stSamplerHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER;
stSamplerHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
GRS_THROW_IF_FAILED(pThdPms->pID3DDevice->CreateDescriptorHeap(&stSamplerHeapDesc, IID_PPV_ARGS(&pISampleHp)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pISampleHp);
D3D12_SAMPLER_DESC stSamplerDesc = {};
stSamplerDesc.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR;
stSamplerDesc.MinLOD = 0;
stSamplerDesc.MaxLOD = D3D12_FLOAT32_MAX;
stSamplerDesc.MipLODBias = 0.0f;
stSamplerDesc.MaxAnisotropy = 1;
stSamplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS;
stSamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
stSamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
stSamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
pThdPms->pID3DDevice->CreateSampler(&stSamplerDesc, pISampleHp->GetCPUDescriptorHandleForHeapStart());
}
//3、加载网格数据
{
LoadMeshVertex(pThdPms->pszMeshFile, nVertexCnt, pstVertices, pnIndices);
nIndexCnt = nVertexCnt;
//创建 Vertex Buffer 仅使用Upload隐式堆
GRS_THROW_IF_FAILED(pThdPms->pID3DDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD)
, D3D12_HEAP_FLAG_NONE
, &CD3DX12_RESOURCE_DESC::Buffer(nVertexCnt * sizeof(ST_GRS_VERTEX))
, D3D12_RESOURCE_STATE_GENERIC_READ
, nullptr
, IID_PPV_ARGS(&pIVB)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIVB);
//使用map-memcpy-unmap大法将数据传至顶点缓冲对象
UINT8* pVertexDataBegin = nullptr;
CD3DX12_RANGE stReadRange(0, 0); // We do not intend to read from this resource on the CPU.
GRS_THROW_IF_FAILED(pIVB->Map(0, &stReadRange, reinterpret_cast<void**>(&pVertexDataBegin)));
memcpy(pVertexDataBegin, pstVertices, nVertexCnt * sizeof(ST_GRS_VERTEX));
pIVB->Unmap(0, nullptr);
//创建 Index Buffer 仅使用Upload隐式堆
GRS_THROW_IF_FAILED(pThdPms->pID3DDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD)
, D3D12_HEAP_FLAG_NONE
, &CD3DX12_RESOURCE_DESC::Buffer(nIndexCnt * sizeof(UINT))
, D3D12_RESOURCE_STATE_GENERIC_READ
, nullptr
, IID_PPV_ARGS(&pIIB)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIIB);
UINT8* pIndexDataBegin = nullptr;
GRS_THROW_IF_FAILED(pIIB->Map(0, &stReadRange, reinterpret_cast<void**>(&pIndexDataBegin)));
memcpy(pIndexDataBegin, pnIndices, nIndexCnt * sizeof(UINT));
pIIB->Unmap(0, nullptr);
//创建Vertex Buffer View
stVBV.BufferLocation = pIVB->GetGPUVirtualAddress();
stVBV.StrideInBytes = sizeof(ST_GRS_VERTEX);
stVBV.SizeInBytes = nVertexCnt * sizeof(ST_GRS_VERTEX);
//创建Index Buffer View
stIBV.BufferLocation = pIIB->GetGPUVirtualAddress();
stIBV.Format = DXGI_FORMAT_R32_UINT;
stIBV.SizeInBytes = nIndexCnt * sizeof(UINT);
::HeapFree(::GetProcessHeap(), 0, pstVertices);
::HeapFree(::GetProcessHeap(), 0, pnIndices);
}
//4、创建常量缓冲
{
GRS_THROW_IF_FAILED(pThdPms->pID3DDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD)
, D3D12_HEAP_FLAG_NONE
, &CD3DX12_RESOURCE_DESC::Buffer(szMVPBuf) //注意缓冲尺寸设置为256边界对齐大小
, D3D12_RESOURCE_STATE_GENERIC_READ
, nullptr
, IID_PPV_ARGS(&pICBWVP)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pICBWVP);
// Map 之后就不再Unmap了 直接复制数据进去 这样每帧都不用map-copy-unmap浪费时间了
GRS_THROW_IF_FAILED(pICBWVP->Map(0, nullptr, reinterpret_cast<void**>(&pMVPBufModule)));
// 创建CBV
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
cbvDesc.BufferLocation = pICBWVP->GetGPUVirtualAddress();
cbvDesc.SizeInBytes = static_cast<UINT>(szMVPBuf);
CD3DX12_CPU_DESCRIPTOR_HANDLE stCbvSrvHandle(pISRVCBVHp->GetCPUDescriptorHandleForHeapStart());
pThdPms->pID3DDevice->CreateConstantBufferView(&cbvDesc, stCbvSrvHandle);
}
//5、设置事件对象 通知并切回主线程 完成资源的第二个Copy命令
{
GRS_THROW_IF_FAILED(pThdPms->pICmdList->Close());
//第一次通知主线程本线程加载资源完毕
::SetEvent(pThdPms->hEventRenderOver); // 设置信号,通知主线程本线程资源加载完毕
}
// 仅用于球体跳动物理特效的变量
float fJmpSpeed = 0.003f; //跳动速度
float fUp = 1.0f;
float fRawYPos = pThdPms->v4ModelPos.y;
DWORD dwRet = 0;
BOOL bQuit = FALSE;
MSG msg = {};
//6、渲染循环
while (!bQuit)
{
// 等待主线程通知开始渲染,同时仅接收主线程Post过来的消息,目前就是为了等待WM_QUIT消息
dwRet = ::MsgWaitForMultipleObjects(1, &pThdPms->hRunEvent, FALSE, INFINITE, QS_ALLPOSTMESSAGE);
switch (dwRet - WAIT_OBJECT_0)
{
case 0:
{
//命令分配器先Reset一下,刚才已经执行过了一个复制纹理的命令
GRS_THROW_IF_FAILED(pThdPms->pICmdAlloc->Reset());
//Reset命令列表,并重新指定命令分配器和PSO对象
GRS_THROW_IF_FAILED(pThdPms->pICmdList->Reset(pThdPms->pICmdAlloc, pThdPms->pIPSO));
// 准备MWVP矩阵
{
//==============================================================================
//使用主线程更新的统一帧时间更新下物体的物理状态等,这里仅演示球体上下跳动的样子
//启发式的向大家说明利用多线程渲染时,物理变换可以在不同的线程中,并且在不同的CPU上并行的执行
if ( g_nThdSphere == pThdPms->nIndex )
{
if ( pThdPms->v4ModelPos.y >= 2.0f * fRawYPos )
{
fUp = -1.0f;
pThdPms->v4ModelPos.y = 2.0f * fRawYPos;
}
if ( pThdPms->v4ModelPos.y <= fRawYPos )
{
fUp = 1.0f;
pThdPms->v4ModelPos.y = fRawYPos;
}
pThdPms->v4ModelPos.y += fUp * fJmpSpeed * static_cast<float>(pThdPms->nCurrentTime - pThdPms->nStartTime);
mxPosModule = XMMatrixTranslationFromVector(XMLoadFloat4(&pThdPms->v4ModelPos));
}
//==============================================================================
// Module * World
XMMATRIX xmMWVP = XMMatrixMultiply(mxPosModule, XMLoadFloat4x4(&g_mxWorld));
// (Module * World) * View * Projection
xmMWVP = XMMatrixMultiply(xmMWVP, XMLoadFloat4x4(&g_mxVP));
XMStoreFloat4x4(&pMVPBufModule->m_MVP, xmMWVP);
}
//---------------------------------------------------------------------------------------------
//设置对应的渲染目标和视裁剪框(这是渲染子线程必须要做的步骤,基本也就是所谓多线程渲染的核心秘密所在了)
{
CD3DX12_CPU_DESCRIPTOR_HANDLE stRTVHandle(g_pIRTVHeap->GetCPUDescriptorHandleForHeapStart()
, pThdPms->nCurrentFrameIndex
, g_nRTVDescriptorSize);
CD3DX12_CPU_DESCRIPTOR_HANDLE dsvHandle(g_pIDSVHeap->GetCPUDescriptorHandleForHeapStart());
//设置渲染目标
pThdPms->pICmdList->OMSetRenderTargets(1, &stRTVHandle, FALSE, &dsvHandle);
pThdPms->pICmdList->RSSetViewports(1, &g_stViewPort);
pThdPms->pICmdList->RSSetScissorRects(1, &g_stScissorRect);
}
//---------------------------------------------------------------------------------------------
//渲染(实质就是记录渲染命令列表)
{
pThdPms->pICmdList->SetGraphicsRootSignature(pThdPms->pIRS);
pThdPms->pICmdList->SetPipelineState(pThdPms->pIPSO);
ID3D12DescriptorHeap* ppHeapsSphere[] = { pISRVCBVHp.Get(),pISampleHp.Get() };
pThdPms->pICmdList->SetDescriptorHeaps(_countof(ppHeapsSphere), ppHeapsSphere);
CD3DX12_GPU_DESCRIPTOR_HANDLE stGPUCBVHandleSphere(pISRVCBVHp->GetGPUDescriptorHandleForHeapStart());
//设置CBV
pThdPms->pICmdList->SetGraphicsRootDescriptorTable(0, stGPUCBVHandleSphere);
stGPUCBVHandleSphere.Offset(1, pThdPms->pID3DDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV));
//设置SRV
pThdPms->pICmdList->SetGraphicsRootDescriptorTable(1, stGPUCBVHandleSphere);
//设置Sample
pThdPms->pICmdList->SetGraphicsRootDescriptorTable(2, pISampleHp->GetGPUDescriptorHandleForHeapStart());
//注意我们使用的渲染手法是三角形列表,也就是通常的Mesh网格
pThdPms->pICmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
pThdPms->pICmdList->IASetVertexBuffers(0, 1, &stVBV);
pThdPms->pICmdList->IASetIndexBuffer(&stIBV);
//Draw Call!!!
pThdPms->pICmdList->DrawIndexedInstanced(nIndexCnt, 1, 0, 0, 0);
}
//完成渲染(即关闭命令列表,并设置同步对象通知主线程开始执行)
{
GRS_THROW_IF_FAILED(pThdPms->pICmdList->Close());
::SetEvent(pThdPms->hEventRenderOver); // 设置信号,通知主线程本线程渲染完毕
}
}
break;
case 1:
{//处理消息
while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{//这里只可能是别的线程发过来的消息,用于更复杂的场景
if (WM_QUIT != msg.message)
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
else
{
bQuit = TRUE;
}
}
}
break;
case WAIT_TIMEOUT:
break;
default:
break;
}
}
}
catch (CGRSCOMException&)
{
}
return 0;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_KEYDOWN:
{
USHORT n16KeyCode = (wParam & 0xFF);
if (VK_SPACE == n16KeyCode)
{//按空格键切换不同的采样器看效果,以明白每种采样器具体的含义
//UINT g_nCurrentSamplerNO = 0; //当前使用的采样器索引
//UINT g_nSampleMaxCnt = 5; //创建五个典型的采样器
//++g_nCurrentSamplerNO;
//g_nCurrentSamplerNO %= g_nSampleMaxCnt;
//=================================================================================================
//重新设置球体的捆绑包
//pICmdListSphere->Reset(pICmdAllocSphere.Get(), pIPSOSphere.Get());
//pICmdListSphere->SetGraphicsRootSignature(pIRootSignature.Get());
//pICmdListSphere->SetPipelineState(pIPSOSphere.Get());
//ID3D12DescriptorHeap* ppHeapsSphere[] = { pISRVCBVHp.Get(),pISampleHp.Get() };
//pICmdListSphere->SetDescriptorHeaps(_countof(ppHeapsSphere), ppHeapsSphere);
设置SRV
//pICmdListSphere->SetGraphicsRootDescriptorTable(0, pISRVCBVHp->GetGPUDescriptorHandleForHeapStart());
//CD3DX12_GPU_DESCRIPTOR_HANDLE stGPUCBVHandleSphere(pISRVCBVHp->GetGPUDescriptorHandleForHeapStart()
// , 1
// , pID3DDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV));
设置CBV
//pICmdListSphere->SetGraphicsRootDescriptorTable(1, stGPUCBVHandleSphere);
//CD3DX12_GPU_DESCRIPTOR_HANDLE hGPUSamplerSphere(pISampleHp->GetGPUDescriptorHandleForHeapStart()
// , g_nCurrentSamplerNO
// , nSamplerDescriptorSize);
设置Sample
//pICmdListSphere->SetGraphicsRootDescriptorTable(2, hGPUSamplerSphere);
注意我们使用的渲染手法是三角形列表,也就是通常的Mesh网格
//pICmdListSphere->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
//pICmdListSphere->IASetVertexBuffers(0, 1, &stVBV);
//pICmdListSphere->IASetIndexBuffer(&stIBV);
Draw Call!!!
//pICmdListSphere->DrawIndexedInstanced(nIndexCnt, 1, 0, 0, 0);
//pICmdListSphere->Close();
//=================================================================================================
}
if (VK_ADD == n16KeyCode || VK_OEM_PLUS == n16KeyCode)
{
//double g_fPalstance = 10.0f * XM_PI / 180.0f; //物体旋转的角速度,单位:弧度/秒
g_fPalstance += 10 * XM_PI / 180.0f;
if (g_fPalstance > XM_PI)
{
g_fPalstance = XM_PI;
}
//XMMatrixOrthographicOffCenterLH()
}
if (VK_SUBTRACT == n16KeyCode || VK_OEM_MINUS == n16KeyCode)
{
g_fPalstance -= 10 * XM_PI / 180.0f;
if (g_fPalstance < 0.0f)
{
g_fPalstance = XM_PI / 180.0f;
}
}
//根据用户输入变换
//XMVECTOR g_f3EyePos = XMVectorSet(0.0f, 5.0f, -10.0f, 0.0f); //眼睛位置
//XMVECTOR g_f3LockAt = XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f); //眼睛所盯的位置
//XMVECTOR g_f3HeapUp = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f); //头部正上方位置
XMFLOAT3 move(0, 0, 0);
float fMoveSpeed = 2.0f;
float fTurnSpeed = XM_PIDIV2 * 0.005f;
if ('w' == n16KeyCode || 'W' == n16KeyCode)
{
move.z -= 1.0f;
}
if ('s' == n16KeyCode || 'S' == n16KeyCode)
{
move.z += 1.0f;
}
if ('d' == n16KeyCode || 'D' == n16KeyCode)
{
move.x += 1.0f;
}
if ('a' == n16KeyCode || 'A' == n16KeyCode)
{
move.x -= 1.0f;
}
if (fabs(move.x) > 0.1f && fabs(move.z) > 0.1f)
{
XMVECTOR vector = XMVector3Normalize(XMLoadFloat3(&move));
move.x = XMVectorGetX(vector);
move.z = XMVectorGetZ(vector);
}
if (VK_UP == n16KeyCode)
{
g_fPitch += fTurnSpeed;
}
if (VK_DOWN == n16KeyCode)
{
g_fPitch -= fTurnSpeed;
}
if (VK_RIGHT == n16KeyCode)
{
g_fYaw -= fTurnSpeed;
}
if (VK_LEFT == n16KeyCode)
{
g_fYaw += fTurnSpeed;
}
// Prevent looking too far up or down.
g_fPitch = min(g_fPitch, XM_PIDIV4);
g_fPitch = max(-XM_PIDIV4, g_fPitch);
// Move the camera in model space.
float x = move.x * -cosf(g_fYaw) - move.z * sinf(g_fYaw);
float z = move.x * sinf(g_fYaw) - move.z * cosf(g_fYaw);
g_f3EyePos.x += x * fMoveSpeed;
g_f3EyePos.z += z * fMoveSpeed;
// Determine the look direction.
float r = cosf(g_fPitch);
g_f3LockAt.x = r * sinf(g_fYaw);
g_f3LockAt.y = sinf(g_fPitch);
g_f3LockAt.z = r * cosf(g_fYaw);
if (VK_TAB == n16KeyCode)
{//按Tab键还原摄像机位置
g_f3EyePos = XMFLOAT3(0.0f, 0.0f, -10.0f); //眼睛位置
g_f3LockAt = XMFLOAT3(0.0f, 0.0f, 0.0f); //眼睛所盯的位置
g_f3HeapUp = XMFLOAT3(0.0f, 1.0f, 0.0f); //头部正上方位置
}
}
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
BOOL LoadMeshVertex(const CHAR*pszMeshFileName, UINT&nVertexCnt, ST_GRS_VERTEX*&ppVertex, UINT*&ppIndices)
{
ifstream fin;
char input;
BOOL bRet = TRUE;
try
{
fin.open(pszMeshFileName);
if (fin.fail())
{
throw CGRSCOMException(E_FAIL);
}
fin.get(input);
while (input != ':')
{
fin.get(input);
}
fin >> nVertexCnt;
fin.get(input);
while (input != ':')
{
fin.get(input);
}
fin.get(input);
fin.get(input);
ppVertex = (ST_GRS_VERTEX*)HeapAlloc(::GetProcessHeap()
, HEAP_ZERO_MEMORY
, nVertexCnt * sizeof(ST_GRS_VERTEX));
ppIndices = (UINT*)HeapAlloc(::GetProcessHeap()
, HEAP_ZERO_MEMORY
, nVertexCnt * sizeof(UINT));
for (UINT i = 0; i < nVertexCnt; i++)
{
fin >> ppVertex[i].m_vPos.x >> ppVertex[i].m_vPos.y >> ppVertex[i].m_vPos.z;
ppVertex[i].m_vPos.w = 1.0f;
fin >> ppVertex[i].m_vTex.x >> ppVertex[i].m_vTex.y;
fin >> ppVertex[i].m_vNor.x >> ppVertex[i].m_vNor.y >> ppVertex[i].m_vNor.z;
ppIndices[i] = i;
}
}
catch (CGRSCOMException& e)
{
e;
bRet = FALSE;
}
return bRet;
}
5.6、关于多线程渲染的思考
至此我想大家应该对多线程渲染已经入门了,关键点无非就是使用多个Command List在多个不同的线程中并行的记录命令,最终都放到主线程的Command Queue中去执行。当然还要注意两个细节:
1、就是为所有的Command List设置相同的RTV和DSV;
2、使用多个Command List分别记录开始渲染、渲染后处理的命令,最后按照逻辑顺序添加至数组,再ExecuteCommandLists;
这时我们需要思考的问题就是,如果每个线程仅仅只是使用Command List记录命令,同时我们知道在D3D12的设计中,Command List本身只是简单记录一下命令,并不真正执行,甚至是调用即返回,并不怎么消耗CPU时间,因此其实使用单线程高效记录命令,和使用多线程记录多个不同的命令列表,本质上是不是性能差异不会很大?相反多线程本身是不是只是提高了编程复杂性?
其实从本质上来讲,对于简单的场景渲染来说其实真的是这样的,比如我们例子中,如果你改回单线程渲染然后和多线程渲染进行性能比对时几乎不会看到太明显的差异。
那么说到底多线程渲染的真正价值在哪里呢?或者它怎么体现性能方面的改进呢?
其实,一开始我也是有这样的困惑,后来我发现其实这是因为我们的示例太过简单,让我们没法完整去了解在一个复杂的3D应用中CPU究竟需要干些什么?
为了理解这个问题,在我们改进的使用MsgWait的示例中,我特意安排了一个小的CPU计算过程让场景中的球体可以上下的跳动。这其实就是我想说的,使用多线程渲染,每个线程中都可以更多去进行输入处理,AI计算(注意和时下流行所指的AI不是同一个概念,这里是指游戏AI),物理变换计算,动画预计算等,而这些计算都是很耗CPU资源的,而这些都可以利用多线程并行计算带来一些计算效能的提升,同时当他们都可以与多线程渲染无缝结合的时候,才可以真正的爆发最强悍的性能,最终你也就可以很有逼格的说你的3D应用是使用了多线程渲染技术。当然带来的性能提升就看你能并行到什么程度了。而这一切都得益于多核超线程架构CPU大行其道的今天!
6、本章全部代码链接
因为本章代码比较复杂,牵扯到资源较多,为了方便大家学习,特提供代码下载链接:
提取码:tee3
二维码:
7、本章后记
本章教程中关于线程以及线程同步的内容,大多数来自于差不多10年前我的视频教程中的PPT。想来时间过得真快,彼时我还想继续讲D3D11以及游戏引擎架构和实现的内容,但因为种种原因未能如愿,现在D3D12已经都推出很多年了。而实时光线追踪渲染(Real-Time Raytracing)也已经纳入了D3D12后续的小版本升级中,被称之为DXR,而支持DXR渲染的显卡也已经开始上市进行普及了。后续待RTX相对稳定后,我也会继续讲解DXR,敬请期待!
现在很多技术已经得到了天翻地覆的进化和发展,而有些东西这十多年甚至几十年来基本没有什么变化。我想我最大的感触就是我们不应该惧怕技术变化过快让我们无所适从,相反我们应该从现在开始脚踏实地,一步步的去学习和掌握它们,而总有一天我们终将征服它们,站在技术的巅峰,去实现我们心中梦想的东西。这中间我们唯一要去做的就是坚持!
最早在1984年我第一次接触电脑及电脑游戏(是真的电脑,当时很昂贵的80286PC),中间从1998年开始正式学习C语言编程,至今,我一直梦想自己能够编写游戏。时至今日虽然我依然是个业余游戏程序员,甚至只能说算个业余程序员,因为我基本没编出来一款游戏,甚至连个小游戏都没有编出来,而且现在我也早已不在编码的第一线了,我的工作也早已不是编写代码了。想想也许很遗憾,但中间我学习了很多东西,成长了很多,青春也已逝去。
而现在我依然在坚持,并且这成了我生活中主要的乐趣之一,甚至我会与家人分享我搞定一些困扰我很久问题之后的喜悦,或者于我而言,这就是佛家说的极乐。我想我会一直坚持下去,现在我更多的是将我知道的尽力都分享出来,因为要学和要写的东西实在是太多了,总是感慨时间不够,现在我深深的明白从小受到的那些让我们珍惜时间的教育是真实的,但我似乎明白的太晚了。
或许这也就是朋友说的,是因为我有这样的一种情怀。如果这就是情怀的话,我想这还只是我情怀的一部分,因为我曾试着写过操作系统,当然最终也只写了一个引导区的程序而已。我也曾写过几个简单的小编译器,其中有一个还在我10多年前的一款软件中作为核心组件之一,至今仍然运行着。我也曾写过简单的数据库系统,但这个就没什么成果了。不管怎样这些东西都让我对技术本身有了深刻的认识,技术绝对不是一个简单的重复的搬砖式的工作,他本身需要的是你的智力、耐力、学习力、领悟能力等等方面要有较高的综合水平。当然我认为更需要的是一种情怀和坚持。
无论怎样请尊重技术本身,他本身就是很多人智慧的结晶,他是我们人类之所以成为万物之灵长的重要因素。我想往后余生如有闲暇我将继续在技术的海洋中遨游!越往前走,越难!越难,越往前走!
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)