基於Qt Widgets的QT程序,控制項的刷新默認狀況下都是在UI線程中依次進行的,換言之,各個控制項的QWidget::paintEvent方法會在UI線程中串行地被調用。若是某個控制項的paintEvent很是耗時(等待數據時間+CPU處理時間+GPU渲染時間),會致使刷新幀率降低,界面的響應速度變慢。
假如這個paintEvent耗時的控制項沒有使用OpenGL渲染,徹底使用CPU渲染。這種狀況處理起來比較簡單,只須要另外開一個線程用CPU往QImage裡面渲染,當主線程調用到這個控制項的paintEvent時,再把渲染好的QImage畫出來就能夠了,單純繪製一個QImage仍是很快的。
若是這個paintEvent耗時的控制項使用了OpenGL渲染,狀況會複雜一些,由於想要把OpenGL渲染過程搬到另一個線程中並非直接把OpenGL調用從UI線程搬到渲染線程就能夠的,是須要作一些準備工做的。另外,UI線程如何使用渲染線程的渲染結果也是一個須要思考的問題。
以繪製一個疊代了15次的Sierpinski三角形為例,它總共有3^15=14348907個三角形,在個人MX150顯卡上繪製一次需要30ms左右的時間。所以若是我在UI線程渲染這些頂點的話,UI線程的刷新幀率就會掉到30幀左右。如今咱們來看一下如何在另外一個線程中渲染這些三角形。
軟硬體環境
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:O2this
Qt version:5.12.1spa
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將其移到渲染線程中。
【領QT開發教程學習資料,進Qt開發交流君羊:546183882 莬廢領取,先碼住不迷路~】
渲染線程例程
// called in render thread
void RenderThread::run()
{
m_renderContext->makeCurrent(m_surface);
TextureBuffer::instance()->createTexture(m_renderContext);
Renderer renderer;
while (m_running)
{
int width = 0;
int height = 0;
{
QMutexLocker lock(&m_mutex);
width = m_width;
height = m_height;
}
renderer.render(width, height);
TextureBuffer::instance()->updateTexture(m_renderContext, width, height);
emit imageReady();
FpsCounter::instance()->frame(FpsCounter::Render);
}
TextureBuffer::instance()->deleteTexture(m_renderContext);
}
渲染線程開始渲染時,首先綁定context和初始化TextureBuffer。而後在循環中重複執行渲染-保存紋理的循環
離屏渲染
其初始化在Renderer::init中進行,渲染在Renderer::render中進行,各種OpenGL基礎教程中都有對離屏渲染的相關介紹和分析,此處再也不贅述。
保存紋理
// called in render thread
void TextureBuffer::updateTexture(QOpenGLContext *context, int width, int height)
{
Timer t("ImageBuffer::updateTexture");
QMutexLocker lock(&m_mutex);
auto f = context->Functions();
f->glActiveTexture(GL_TEXTURE0);
f->glBindTexture(GL_TEXTURE_2D, m_texture);
f->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
f->glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, width, height, 0);
f->glBindTexture(GL_TEXTURE_2D, 0);
f->glFinish();
}
在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);
}
...
// called in main thread
void TextureBuffer::drawTexture(QOpenGLContext *context, int vertextCount)
{
Timer t("ImageBuffer::drawTexture");
QMutexLocker lock(&m_mutex);
auto f = context->functions();
f->glBindTexture(GL_TEXTURE_2D, m_texture);
f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
f->glActiveTexture(GL_TEXTURE0);
f->glDrawArrays(GL_TRIANGLES, 0, vertextCount);
f->glBindTexture(GL_TEXTURE_2D, 0);
//f->glFinish();
}
在GLWidget::paintGL中調用TextureBuffer::drawTexture來繪製緩存的紋理。
性能
上面所做的這一切,可以提升性能嗎?很遺憾,答案是「不必定」。就這個demo而言,渲染過程幾乎徹底不須要等待數據和CPU處理(除了初始化時須要CPU計算),不斷使用GPU進行渲染,這致使GPU占用率幾乎達到了100%,成為了一個瓶頸。當主線程進行OpenGL調用時,極可能會由於正在處理渲染線程的OpenGL調用而被阻塞,致使幀率降低。使用NVIDIA Nsights Graphics實測結果以下:
第一幅圖是剛打開程序時的幀率,基本穩定在60幀,第二幅圖是運行一段時間後的幀率,時常跌到30幀。就平均幀率而言,性能較單線程渲染仍是有提高的。至於為何運行一段時間後幀率會降低,猜測是GPU溫度升高被降頻致使的,使用GPU-Z觀察GPU時鐘頻率能夠驗證這一猜測。
若是渲染過程當中等待數據和CPU處理時間占了必定的比重的話,多線程離屏渲染就有優點了。不過在這種狀況下,單把等待數據和CPU處理的代碼移到獨立線程也許是個不錯的選擇。具體採用哪一種方案仍是要根據實際測試效果來決定。
【領QT開發教程學習資料,進Qt開發交流君羊:546183882 莬廢領取,先碼住不迷路~】