现代的图形API都具备对多线程渲染友好的特性,所谓的多线程并不是指GPU端的多线程图像渲染,而是指在CPU提交DrawCall时所做的一系列工作可以并行化,也就是说多线程渲染其实是在CPU端提升程序的性能。
在使用D3D 11或者OpenGL的时候,每次提交DrawCall之前,都需要将相关的状态进行更新,将需要用的资源进行绑定,在提交DrawCall时,还要进行相关的参数检查等工作,这些看上去耗费的时间并没有太大的影响,而如果场景的几何体、材质种类非常多,用到的Shader数量比较多,每一帧的Pass比较多,就会导致有大量的DrawCall产生。那么每次在CPU端进行的这些操作的费时就很有可能会成为瓶颈。一种很自然的优化策略,就是将所有的CPU端的这些操作并行处理,即多线程地进行状态修改、参数检查等工作。但是传统的图形API对此并不友好,不管是D3D11还是OpenGL,它们都具有一个Context的概念,这个Context负责进行资源的绑定、状态修改、DrawCall调用,这样的模式对多线程十分不友好,如果想要实现多线程地提交,理论上是可以完成,但是非常麻烦,而且需要用到很多复杂同步原语,导致整体性能未必能达到理想的效果。
而现代的图形API则进行了一些模式上的更新,使得对多线程的支持更加友好。在Vulkan中的设计则主要体现在Queue和CommandBuffer上。在前几篇中有提到过,Vulkan中所有需要GPU执行的命令,只能通过CommandBuffer来完成,这些命令并不只包括DrawCall,对计算的调用,内存的操作,都需要用到CommandBuffer。而渲染所需要的所有状态(Shader和DescriptorSet等),都需要在CommandBuffer中进行绑定。每一个CommandBuffer,都有它独立的这些状态,在使用任意一个CommandBuffer时,都不可能避免这些操作,这与传统API中,如果不改变一个状态的话那么它将一直保持不变很不一样。而Queue则是在Vulkan中唯一一个可以向GPU提交命令的通道,而不是通过绑定在一个单一线程上的Context来完成。可以向Queue提交任务,而如果需要等待Queue中的某个任务结束的话,就需要手动的进行同步控制。用到上一篇所介绍的同步机制。
因此在Vulkan中的一种简单的多线程模式为:每个线程在每一帧都负责设置好自己的CommandBuffer,等待所有的线程将自己的CommandBuffer都设置好后,再将所有的CommandBuffer全部提交给Queue。
本文需要渲染的场景为:
这个场景由非常多的飞碟构成,观察到每个飞碟中间部分的颜色都不相同,也就是在渲染每一个飞碟时,都需要对渲染的状态进行更新。并且每个飞碟的位置在每一帧都需要进行更新。
下面就介绍这种多线程模式是如何具体实现的:
首先需要手动实现一下Thread:
class Thread
{
private:
bool destroying = false;
std::thread worker;
std::queue<std::function<void()>> jobQueue;
std::mutex queueMutex;
std::condition_variable condition;
// Loop through all remaining jobs
void queueLoop()
{
while (true)
{
std::function<void()> job;
{
std::unique_lock<std::mutex> lock(queueMutex);
condition.wait(lock, [this] {
return !jobQueue.empty() || destroying; });
if (destroying)
{
break;
}
job = jobQueue.front();
}
job();
{
std::lock_guard<std::mutex> lock(queueMutex);
jobQueue.pop();
condition.notify_one();
}
}
}
public:
Thread()
{
worker = std::thread(&Thread::queueLoop, this);
}
~Thread()
{
if (worker.joinable())
{
wait();
queueMutex.lock();
destroying = true;
condition.notify_one();
queueMutex.unlock();
worker.join();
}
}
// Add a new job to the thread's queue
void addJob(std::function<void()> function)
{
std::lock_guard<std::mutex> lock(queueMutex);
jobQueue.push(std::move(function));
condition.notify_one();
}
// Wait until all work items have been finished
void wait()
{
std::unique_lock<std::mutex> lock(queueMutex);
condition.wait(lock, [this]() {
return jobQueue.empty(); });
}
}