21.1基于任务队列的多线程渲染


文档摘要

## 21.1 基于任务队列的多线程渲染 上一节测试了GLFW对多线程的支持,证实了在单独子线程调用OpenGL API渲染是可行的。 那么这一节就来对其进行细化,将渲染三角形这一目标,拆分多个子任务,主线程以命令的形式与渲染线程进行通信。 任务有两种,阻塞性任务和非阻塞性任务。 任务可以拆分为任务命令和任务参数。 案例一 你在蹲坑,发现没有纸了,打电话叫你老婆拿纸,这是一个任务。 任务命令:拿纸 任务参数:无 你是主线程,发出命令。 老婆是渲染线程,接受命令,执行,给你结果。 这是一个阻塞式任务,你(主线程)必须等老婆(渲染线程)执行任务(拿纸),返回结果(给你纸)。 就如下面图中的 命令。 案例二 你在外面闲逛,你老婆打电话过来叫你去买一斤西瓜,这是一个任务。

21.1 基于任务队列的多线程渲染

CLion项目文件位于 samples\multithread_render\engine_render_queue

上一节测试了GLFW对多线程的支持,证实了在单独子线程调用OpenGL API渲染是可行的。

那么这一节就来对其进行细化,将渲染三角形这一目标,拆分多个子任务,主线程以命令的形式与渲染线程进行通信。

任务有两种,阻塞性任务和非阻塞性任务。

任务可以拆分为任务命令和任务参数。

  • 案例一

    你在蹲坑,发现没有纸了,打电话叫你老婆拿纸,这是一个任务。

    任务命令:拿纸

    任务参数:无

    你是主线程,发出命令。

    老婆是渲染线程,接受命令,执行,给你结果。

    这是一个阻塞式任务,你(主线程)必须等老婆(渲染线程)执行任务(拿纸),返回结果(给你纸)。

    就如下面图中的编译Shader命令。



  • 案例二

    你在外面闲逛,你老婆打电话过来叫你去买一斤西瓜,这是一个任务。

    任务命令:买东西

    任务参数有2个:物品:西瓜 重量:一斤

    老婆是主线程,发出命令并带参数。

    你是渲染线程,接受命令,解析参数,执行。

    买西瓜是比较复杂的操作,要去多个店里询问,比价,拿西瓜,称重,最后付款,才算完成了买西瓜这个任务。

    老婆(主线程)不管这些步骤,她只要发命令,然后等结果。

    这是一个非阻塞任务,老婆不会一直等你买瓜回去,她可能在打王者荣耀。

    等你买好瓜到家,老婆又发出命令,让你去切瓜。

    主线程发出渲染命令后,也不会等渲染结果,而是转身就去执行其他逻辑。

    就如案例一图中的绘制命令。

  • 案例三

    你老婆在蹲坑,没有纸了,打电话叫你拿纸,叫你买一斤西瓜,然后再去拿两个顺丰快递,然后再去取1000块钱。

    第一个是阻塞任务,然后几个都是非阻塞任务。

    命令太多,参数太多,人到中年,记忆力衰退,你记不住了。

    你需要一个任务队列来保存这些任务,然后从任务队列里面取任务来做。

    就如同下图,编译shader是阻塞性任务,完成之后才能去做绘制这个非阻塞任务。



那么案例三是我们所需要的,来看下如何实现。

1. 渲染任务

非阻塞性任务由 任务命令 任务参数 组成。

阻塞性任务由 任务命令 任务参数 回传结果 组成。

任务命令

//file:render_command.h line:9 /// 渲染命令 enum RenderCommand { NONE, COMPILE_SHADER,//编译着色器 CREATE_VAO,//创建缓冲区 DRAW_ARRAY,//绘制 END_FRAME,//帧结束 };

针对每一个操作,创建一个命令。

并不是对每个OpenGL API创建一个命令,不用搞这么细,毕竟每个命令都对应一个任务,任务太多也会造成性能损耗。

渲染一个三角形,简单分三步:
1.编译着色器
2.创建缓冲区
3.绘制

对每一步创建一个命令任务即可。

当需要渲染的任务全部发送之后,还需要一个发出特殊任务END_FRAME来标志这一帧结束,这样在渲染线程中,收到这个特殊任务后,就可以去交换缓冲区了。

任务参数&回传结果

//file:render_task_type.h line:11 /// 渲染任务基类 class RenderTaskBase{ public: RenderTaskBase(){} virtual ~RenderTaskBase(){} public: RenderCommand render_command_;//渲染命令 bool need_return_result_ = false;//是否需要回传结果 bool return_result_set_ = false;//是否设置好了回传结果 }; /// 需要回传结果的阻塞性任务 class RenderTaskNeedReturnResult: public RenderTaskBase{ public: RenderTaskNeedReturnResult(){ render_command_=RenderCommand::NONE; need_return_result_=true; } ~RenderTaskNeedReturnResult(){} /// 等待任务在渲染线程执行完毕,并设置回传结果。主线程拿到结果后才能执行下一步代码。 virtual void Wait(){ while(return_result_set_==false){ std::this_thread::sleep_for(std::chrono::microseconds(1)); } } };

对于阻塞性任务,在主线程中将参数need_return_result_设置为true

渲染线程中处理这个任务后,要设置回传结果,然后设置return_result_set_=true,标记任务完成。

主线程则调用Wait(),等待这个任务执行完毕才继续下一步。

本小节实例中有4个任务:

  1. RenderTaskCompileShader (编译着色器任务)
  2. RenderTaskCreateVAO (创建VAO)
  3. RenderTaskDrawArray (绘制任务)
  4. RenderTaskEndFrame (特殊任务:帧结束标志)

任务结构如下:

//file:render_task_type.h line:40 /// 编译着色器任务 class RenderTaskCompileShader: public RenderTaskNeedReturnResult{ public: RenderTaskCompileShader(){ render_command_=RenderCommand::COMPILE_SHADER; } ~RenderTaskCompileShader(){} public: const char* vertex_shader_source_= nullptr; const char* fragment_shader_source_= nullptr; public: GLuint result_shader_program_id_=0;//存储编译Shader结果的程序ID }; /// 创建VAO class RenderTaskCreateVAO: public RenderTaskNeedReturnResult{ public: RenderTaskCreateVAO(){ render_command_=RenderCommand::CREATE_VAO; } ~RenderTaskCreateVAO(){} public: GLuint shader_program_id_=0;//着色器程序ID const void* positions_=nullptr;//顶点位置 GLsizei positions_stride_=0;//顶点数据大小 const void* colors_=nullptr;//顶点颜色 GLsizei colors_stride_=0;//颜色数据大小 public: GLuint result_vao_=0;//回传创建好的VAO }; /// 绘制任务 class RenderTaskDrawArray: public RenderTaskBase { public: RenderTaskDrawArray(){ render_command_=RenderCommand::DRAW_ARRAY; } ~RenderTaskDrawArray(){} public: GLuint shader_program_id_=0;//着色器程序ID GLuint vao_=0; }; /// 特殊任务:帧结束标志,渲染线程收到这个任务后,刷新缓冲区,设置帧结束。 class RenderTaskEndFrame: public RenderTaskNeedReturnResult { public: RenderTaskEndFrame(){ render_command_=RenderCommand::END_FRAME; } ~RenderTaskEndFrame(){} };

RenderTaskCompileShader (编译着色器任务),是阻塞性任务。

在渲染线程编译着色器之后,需要将编译Shader结果的ProgramID设置到 result_program_id_,主线程拿到它之后,才能进入渲染。

RenderTaskCreateVAO (创建VAO),是阻塞性任务。

在渲染线程创建好VAO之后,将VAO设置到result_vao_,主线程拿到它之后,才能进入渲染。

RenderTaskDrawArray (绘制任务),是非阻塞性的。

绘制一个三角形也没有什么好返回的,主线程源源不断的发出任务,渲染线程不断取出,然后执行即可。

RenderTaskEndFrame (特殊任务:帧结束标志),也是阻塞性任务。

主线程发出一堆渲染任务后,需要等待渲染线程渲染完毕,并且调用glfwSwapBuffers交换缓冲区,然后才能往下走执行其他逻辑代码。

那主线程是如何知道渲染线程渲染完毕呢,没办法知道!

主线程和渲染线程通信是通过阻塞性任务的回传结果,如果在主线程中能确定某个任务是最后一个任务,就可以在这个任务上加一个标志,表示渲染结束。渲染线程执行这个任务后,交换缓冲区,并且设置标志。主线程判断标志后,就可以继续下一步了。

普通的渲染任务是没办法确定是否最后一个任务的,所以只能创建一个特殊任务,在主线程发出所有渲染命令后,再发出这个命令。

2. 任务队列

我们口头上叫多线程渲染,但是其实只是在另外一个线程进行渲染,这个模型中只有2个线程,主线程发出命令,渲染线程接受命令做出处理,所以这其实是一个单生产者单消费者的模型。

这种模型在游戏里最常见的应用就是逻辑线程与网络线程的交互,一般就是用RingBuffer来做。

我这里找了一个开源的实现,Github地址:https://github.com/rigtorp/SPSCQueue,简单封装了一下。

//file:render_task_queue.h #include <spscqueue/include/rigtorp/SPSCQueue.h> class RenderTaskBase; /// 定义一个渲染任务队列 class RenderTaskQueue { public: /// 添加任务到队列 /// \param render_task static void Push(RenderTaskBase* render_task){ render_task_queue_.push(render_task); } /// 队列中是否没有了任务 /// \return static bool Empty(){ return render_task_queue_.empty(); } /// 获取队列中第一个任务 /// \return static RenderTaskBase* Front(){ return *(render_task_queue_.front()); } /// 弹出队列中第一个任务 static void Pop(){ render_task_queue_.pop(); } /// 获取队列中的任务数量 static size_t Size(){ return render_task_queue_.size(); } private: static rigtorp::SPSCQueue<RenderTaskBase*> render_task_queue_;//渲染任务队列 };

主线程创建任务,把指针扔到队列尾。

渲染线程从队列头取任务。

3. 任务生产者

生产者负责创建任务,把指针扔到队列尾。

//file:render_task_producer.cpp #include <glm/glm.hpp> #include "render_task_producer.h" #include "render_task_type.h" #include "render_task_queue.h" /// 发出阻塞型任务:编译Shader /// \param vertex_shader_source 顶点shader源码 /// \param fragment_shader_source 片段shader源码 /// \param result_shader_program_id 回传的Shader程序ID void RenderTaskProducer::ProduceRenderTaskCompileShader(const char* vertex_shader_source,const char* fragment_shader_source,GLuint& result_shader_program_id){ RenderTaskCompileShader* render_task_compile_shader=new RenderTaskCompileShader(); render_task_compile_shader->vertex_shader_source_=vertex_shader_source; render_task_compile_shader->fragment_shader_source_=fragment_shader_source; render_task_compile_shader->need_return_result_=true;//需要返回结果 RenderTaskQueue::Push(render_task_compile_shader); //等待编译Shader任务结束并设置回传结果 render_task_compile_shader->Wait(); result_shader_program_id=render_task_compile_shader->result_shader_program_id_; delete render_task_compile_shader;//需要等待结果的渲染任务,需要在获取结果后删除。 } /// 发出任务:创建VAO /// \param shader_program_id 使用的Shader程序ID /// \param positions 绘制的顶点位置 /// \param positions_stride 顶点位置数组的stride /// \param colors 绘制的顶点颜色 /// \param colors_stride 顶点颜色数组的stride /// \param result_vao 回传的VAO void RenderTaskProducer::ProduceRenderTaskCreateVAO(GLuint shader_program_id, const void *positions, GLsizei positions_stride, const void *colors, GLsizei colors_stride, GLuint& result_vao) { RenderTaskCreateVAO* render_task_create_vao=new RenderTaskCreateVAO(); render_task_create_vao->shader_program_id_=shader_program_id; render_task_create_vao->positions_=positions; render_task_create_vao->positions_stride_=positions_stride; render_task_create_vao->colors_=colors; render_task_create_vao->colors_stride_=colors_stride; RenderTaskQueue::Push(render_task_create_vao); //等待任务结束并设置回传结果 render_task_create_vao->Wait(); result_vao=render_task_create_vao->result_vao_; delete render_task_create_vao; } /// 发出任务:绘制 /// \param shader_program_id 使用的Shader程序ID /// \param vao void RenderTaskProducer::ProduceRenderTaskDrawArray(GLuint shader_program_id, GLuint vao){ RenderTaskDrawArray* render_task_draw_array=new RenderTaskDrawArray(); render_task_draw_array->shader_program_id_=shader_program_id; render_task_draw_array->vao_=vao; RenderTaskQueue::Push(render_task_draw_array);//普通非阻塞型任务,交由RenderTaskConsumer使用后删除。 } void RenderTaskProducer::ProduceRenderTaskEndFrame() { RenderTaskEndFrame* render_task_frame_end=new RenderTaskEndFrame(); RenderTaskQueue::Push(render_task_frame_end); //等待渲染结束任务,说明渲染线程渲染完了这一帧所有的东西。 render_task_frame_end->Wait(); delete render_task_frame_end;//需要等待结果的任务,需要在获取结果后删除。 }

主线程中只负责初始化GLFW,然后发出命令。

//file:main.cpp line:17 GLuint program_id_=0; GLuint vao_=0; int main(void) { //设置错误回调 glfwSetErrorCallback(error_callback); if (!glfwInit()) exit(EXIT_FAILURE); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); #endif //创建窗口 GLFWwindow* window = glfwCreateWindow(960, 640, "Simple example", NULL, NULL); if (!window) { glfwTerminate(); exit(EXIT_FAILURE); } //初始化渲染任务消费者(独立线程) RenderTaskConsumer::Init(window); //编译Shader任务 RenderTaskProducer::ProduceRenderTaskCompileShader(vertex_shader_text,fragment_shader_text,program_id_); //创建缓冲区任务 RenderTaskProducer::ProduceRenderTaskCreateVAO(program_id_, kPositions, sizeof(glm::vec3), kColors, sizeof(glm::vec4), vao_); //主线程 渲染循环逻辑 while (!glfwWindowShouldClose(window)) { Render(); //发出特殊任务:渲染结束 RenderTaskProducer::ProduceRenderTaskEndFrame(); //非渲染相关的API,例如处理系统事件,就放到主线程中。 glfwPollEvents(); } RenderTaskConsumer::Exit(); glfwDestroyWindow(window); glfwTerminate(); exit(EXIT_SUCCESS); } void Render(){ //绘制任务 RenderTaskProducer::ProduceRenderTaskDrawArray(program_id_,vao_); }

4. 任务消费者

主线程中初始化RenderTaskConsumer后,在RenderTaskConsumer中就创建了渲染线程。

//file:render_task_consumer.cpp line:19 void RenderTaskConsumer::Init(GLFWwindow *window) { window_ = window; render_thread_ = std::thread(&RenderTaskConsumer::ProcessTask); render_thread_.detach(); } void RenderTaskConsumer::Exit() { if (render_thread_.joinable()) { render_thread_.join();//等待渲染线程结束 } }

渲染线程苏醒后,一直从队列中取任务执行。

//file:render_task_consumer.cpp line:147 void RenderTaskConsumer::ProcessTask() { //渲染相关的API调用需要放到渲染线程中。 glfwMakeContextCurrent(window_); gladLoadGL(glfwGetProcAddress); glfwSwapInterval(1); while (!glfwWindowShouldClose(window_)) { float ratio; int width, height; glm::mat4 model,view, projection, mvp; //获取画面宽高 glfwGetFramebufferSize(window_, &width, &height); ratio = width / (float) height; glViewport(0, 0, width, height); glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glClearColor(49.f/255,77.f/255,121.f/255,1.f); view = glm::lookAt(glm::vec3(0, 0, 10), glm::vec3(0, 0,0), glm::vec3(0, 1, 0)); projection=glm::perspective(glm::radians(60.f),ratio,1.f,1000.f); while(true){ if(RenderTaskQueue::Empty()){//渲染线程一直等待主线程发出任务。没有了任务Sleep 1微秒。 std::this_thread::sleep_for(std::chrono::microseconds(1)); continue; } RenderTaskBase* render_task = RenderTaskQueue::Front(); RenderCommand render_command=render_task->render_command_; switch (render_command) {//根据主线程发来的命令,做不同的处理 case RenderCommand::NONE:break; case RenderCommand::COMPILE_SHADER:{ CompileShader(render_task); break; } case RenderCommand::CREATE_VAO:{ CreateVAO(render_task); break; } case RenderCommand::DRAW_ARRAY:{ DrawArray(render_task, projection, view); break; } case RenderCommand::END_FRAME:{ EndFrame(render_task); break; } } RenderTaskQueue::Pop(); //如果这个任务不需要返回参数,那么用完就删掉。 if(render_task->need_return_result_==false){ delete render_task; } //如果是帧结束任务,就交换缓冲区。 if(render_command==RenderCommand::END_FRAME){ break; } } std::cout<<"task in queue:"<<RenderTaskQueue::Size()<<std::endl; } }

CompileShader(编译Shader) CreateVAO(创建VAO) DrawArray(绘制) 这三个再熟悉不过了。

EndFrame (特殊任务:帧结束标志)用来标记渲染线程渲染一帧结束,代码如下:

//file:render_task_consumer.cpp line:120 /// 结束一帧 /// \param task_base void RenderTaskConsumer::EndFrame(RenderTaskBase* task_base) { RenderTaskEndFrame *task = dynamic_cast<RenderTaskEndFrame *>(task_base); glfwSwapBuffers(window_); task->return_result_set_=true; }

5. 线程同步

主线程发出任务,渲染线程从队列取任务,那么主线程是一定比渲染线程快的。

下一帧的逻辑,可能是需要对上一帧渲染结果做后处理,那么主线程就一定要等待渲染线程这一帧结束。

如果渲染任务特别重,那么渲染线程会比主线程慢很多,主线程就需要一直等待。

这就是在Unity中常见到的Gfx.WaitForPresent,CPU正在等待GPU渲染这一帧。

我们这里是用RenderTaskEndFrame (特殊任务:帧结束标志)来作为帧结束标志的。

主线程发出这个任务后,就开始等待这个任务完成。

渲染线程将RenderTaskEndFrame.return_result_set_设置为true后,主线程才知道渲染线程完成了这一帧的渲染,才继续往下走。

发出任务,到判断任务完成的这一段时间,就是Unity中的Gfx.WaitForPresent

6. 测试

运行项目测试,结果正常。


发布者: 作者: 转发
评论区 (0)
U