基于 Qt Widgets
的 Qt
程序,控件的刷新默认状况下都是在UI线程中依次进行的,换言之,各个控件的 QWidget::paintEvent
方法会在UI线程中串行地被调用。若是某个控件的 paintEvent
很是耗时(等待数据时间+CPU处理时间+GPU渲染时间),会致使刷新帧率降低,界面的响应速度变慢。git
假如这个 paintEvent
耗时的控件没有使用 OpenGL
渲染,彻底使用CPU渲染。这种状况处理起来比较简单,只须要另外开一个线程用CPU往 QImage
里面渲染,当主线程调用到这个控件的 paintEvent
时,再把渲染好的 QImage
画出来就能够了,单纯绘制一个 QImage
仍是很快的。github
若是这个 paintEvent
耗时的控件使用了 OpenGL
渲染,状况会复杂一些,由于想要把 OpenGL
渲染过程搬到另一个线程中并非直接把 OpenGL
调用从UI线程搬到渲染线程就能够的,是须要作一些准备工做的。另外,UI线程如何使用渲染线程的渲染结果也是一个须要思考的问题。web
以绘制一个迭代了15次的Sierpinski三角形为例,它总共有3^15=14348907个三角形,在个人MX150显卡上绘制一次须要30ms左右的时间。所以若是我在UI线程渲染这些顶点的话,UI线程的刷新帧率就会掉到30帧左右。如今咱们来看一下如何在另外一个线程中渲染这些三角形。demo仓库地址:https://github.com/im-red/offscreen_render缓存
软硬件环境
CPU:Intel® Core™ i5-8250U CPU @ 1.60GHz 多线程
GPU:NVIDIA GeForce MX150(Driver:388.19)svg
OS:Microsoft Windows 10 Home 10.0.18362 性能
Compiler:MSVC 2017 测试
Optimization flag:O2 this
Qt version:5.12.1 spa
OpenGL version:4.6.0
概述
有如下主要的类或方法:
GLWidget
这个类在UI线程中使用,继承了 QOpenGLWidget
,负责将渲染线程渲染结果绘制到屏幕上。
Renderer
这个类在渲染线程中使用,负责将三角形渲染到离屏 framebuffer
中。
RenderThread
渲染线程管理类,负责初始化渲染线程 OpenGL
的 context
。
TextureBuffer
纹理缓存类,负责将 Renderer
渲染好的图像缓存到纹理中,供UI线程绘制使用。
RenderThread::run
渲染线程的例程,负责调用 Renderer
的方法渲染图像,在 Renderer
渲染好一帧图像后将图像保存在 TextureBuffer
中。
context
OpenGL
须要 context
来保存状态, context
虽然能够跨线程使用,但没法在多个线程中同时使用,在任意时刻,只能绑定在一个线程中。所以咱们须要为渲染线程建立一个独立的 context
。
数据共享
UI线程如何访问渲染线程的渲染结果。有两种思路:
- 将渲染结果读进内存,生成
QImage
,再传给UI线程。这种方式的优势是实现简单。缺点则是性能可能差一些,把显存读进内存是一个开销比较大的操做。 - 将渲染结果保存到纹理中,UI线程绑定纹理绘制到屏幕上。这种方式的优势是性能较方法1好。缺点是为了让两个线程可以共享纹理,须要作一些配置。
在此,咱们选择的是方法2。
初始化渲染线程
了解到上面的这些信息后,咱们来看一下如何初始化渲染线程。
因为须要UI线程可以和渲染线程共享数据,须要调用 QOpenGLContext::setShareContext
来设置,而这个方法又须要 在QOpenGLContext::create
方法前调用。UI线程 context
的 QOpenGLContext::create
方法调用咱们是没法掌握的,所以须要渲染线程 context
来调用 QOpenGLContext::setShareContext
。因为调用时须要确保UI线程 context
已经初始化,所以在 GLWidget::initializeGL
中初始化渲染线程比较好,相关代码以下:
void GLWidget::initializeGL()
{
initRenderThread();
...
}
...
void GLWidget::initRenderThread()
{
auto context = QOpenGLContext::currentContext();
auto mainSurface = context->surface();
auto renderSurface = new QOffscreenSurface(nullptr, this);
renderSurface->setFormat(context->format());
renderSurface->create();
context->doneCurrent();
m_thread = new RenderThread(renderSurface, context, this);
context->makeCurrent(mainSurface);
connect(m_thread, &RenderThread::imageReady, this, [this](){
update();
}, Qt::QueuedConnection);
m_thread->start();
}
...
RenderThread::RenderThread(QSurface *surface, QOpenGLContext *mainContext, QObject *parent)
: QThread(parent)
, m_running(true)
, m_width(100)
, m_height(100)
, m_mainContext(mainContext)
, m_surface(surface)
{
m_renderContext = new QOpenGLContext;
m_renderContext->setFormat(m_mainContext->format());
m_renderContext->setShareContext(m_mainContext);
m_renderContext->create();
m_renderContext->moveToThread(this);
}
...
在 GLWidget::initRenderThread
中,咱们首先得到UI线程的 context
,以及其关联的 mainSurface
。而后为渲染线程建立了一个 QOffscreenSurface
,将其格式设置为与UI线程 context
相同。而后调用 doneCurrent
取消UI线程 context
与 mainSurface
的关联,这是为了可以使UI线程的 context
和渲染线程的 context
设置共享关系。待渲染线程初始化完成后,再将UI线程 context
与 mainSurface
进行关联。而后设置一个链接用于接收渲染线程的 imageReady
信号。最后启动渲染线程开始渲染。
在 RenderThread::RenderThread
中,首先初始化渲染线程的 context
,因为 RenderThread::RenderThread
是在UI线程中调用的,还要调用 moveToThread
将其移到渲染线程中。
渲染线程例程
undefined
渲染线程开始渲染时,首先绑定 context
和初始化 TextureBuffer
。而后在循环中重复执行渲染-保存纹理的循环
离屏渲染
其初始化在 Renderer::init
中进行,渲染在 Renderer::render
中进行,各种 OpenGL
基础教程中都有对离屏渲染的相关介绍和分析,此处再也不赘述。
保存纹理
undefined
在 RenderThread::run
中调用 TextureBuffer::updateTexture
将使用 glCopyTexImage2D
将渲染线程渲染结果保存到纹理中,在 Qt
中 OpenGL
调用都须要经过 QOpenGLFunction
对象,所以将渲染线程的 QOpenGLContext
对象传进来,能够得到其默认的 QOpenGLFunction
对象。
因为咱们只使用了一个纹理来缓存图像,若是渲染线程渲染得比较快的话,有些帧就会来不及渲染被丢弃。固然你也能够改程序阻塞渲染线程避免被阻塞。
绘制纹理
void GLWidget::paintGL()
{
Timer t("GLWidget::paintGL");
glEnable(GL_TEXTURE_2D);
m_program->bind();
glBindVertexArray(m_vao);
if (TextureBuffer::instance()->ready())
{
TextureBuffer::instance()->drawTexture(QOpenGLContext::currentContext(), sizeof(vertices) / sizeof(float) / 4);
}
glBindVertexArray(0);
m_program->release();
glDisable(GL_TEXTURE_2D);
FpsCounter::instance()->frame(FpsCounter::Display);
}
...
在 GLWidget::paintGL
中调用 TextureBuffer::drawTexture
来绘制缓存的纹理。
性能
上面所作的这一切,可以提升性能吗?很遗憾,答案是”不必定”。就这个demo而言,渲染过程几乎彻底不须要等待数据和CPU处理(除了初始化时须要CPU计算),不断使用GPU进行渲染,这致使GPU占用率几乎达到了100%,成为了一个瓶颈。当主线程进行 OpenGL
调用时,极可能会由于正在处理渲染线程的 OpenGL
调用而被阻塞,致使帧率降低。使用NVIDIA Nsights Graphics实测结果以下:
第一幅图是刚打开程序时的帧率,基本稳定在60帧,第二幅图是运行一段时间后的帧率,时常跌到30帧。就平均帧率而言,性能较单线程渲染仍是有提高的。至于为何运行一段时间后帧率会降低,猜测是GPU温度升高被降频致使的,使用GPU-Z观察GPU时钟频率能够验证这一猜测。
若是渲染过程当中等待数据和CPU处理时间占了必定的比重的话,多线程离屏渲染就有优点了。不过在这种状况下,单把等待数据和CPU处理的代码移到独立线程也许是个不错的选择。具体采用哪一种方案仍是要根据实际测试效果来决定。
Original: https://www.cnblogs.com/zhoug2020/p/16393734.html
Author: 莫水千流
Title: Qt使用OpenGL进行多线程离屏渲染
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/540169/
转载文章受原作者版权保护。转载请注明原作者出处!