Qt使用OpenGL進行多線程離屏渲染

qt技術開發老jie 發佈 2024-04-28T09:11:05.505600+00:00

基於Qt Widgets的Qt程序,控制項的刷新默認狀況下都是在UI線程中依次進行的,換言之,各個控制項的QWidget::paintEvent方法會在UI線程中串行地被調用。

基於Qt WidgetsQT程序,控制項的刷新默認狀況下都是在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

概述

有如下主要的類或方法:

  1. GLWidget

這個類在UI線程中使用,繼承了QOpenGLWidget,負責將渲染線程渲染結果繪製到屏幕上。

  1. Renderer

這個類在渲染線程中使用,負責將三角形渲染到離屏framebuffer中。

  1. RenderThread

渲染線程管理類,負責初始化渲染線程OpenGLcontext

  1. TextureBuffer

紋理緩存類,負責將Renderer渲染好的圖像緩存到紋理中,供UI線程繪製使用。

  1. RenderThread::run

渲染線程的例程,負責調用Renderer的方法渲染圖像,在Renderer渲染好一幀圖像後將圖像保存在TextureBuffer中。

context

OpenGL須要context來保存狀態,context雖然能夠跨線程使用,但沒法在多個線程中同時使用,在任意時刻,只能綁定在一個線程中。所以咱們須要為渲染線程建立一個獨立的context

數據共享

UI線程如何訪問渲染線程的渲染結果。有兩種思路:

  1. 將渲染結果讀進內存,生成QImage,再傳給UI線程。這種方式的優勢是實現簡單。缺點則是性能可能差一些,把顯存讀進內存是一個開銷比較大的操做。
  2. 將渲染結果保存到紋理中,UI線程綁定紋理繪製到屏幕上。這種方式的優勢是性能較方法1好。缺點是為了讓兩個線程可以共享紋理,須要作一些配置。

在此,咱們選擇的是方法2。

初始化渲染線程

了解到上面的這些信息後,咱們來看一下如何初始化渲染線程。

因為須要UI線程可以和渲染線程共享數據,須要調用QOpenGLContext::setShareContext來設置,而這個方法又須要在QOpenGLContext::create方法前調用。UI線程contextQOpenGLContext::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線程contextmainSurface的關聯,這是為了可以使UI線程的context和渲染線程的context設置共享關係。待渲染線程初始化完成後,再將UI線程contextmainSurface進行關聯。而後設置一個連結用於接收渲染線程的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將渲染線程渲染結果保存到紋理中,在QtOpenGL調用都須要經過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 莬廢領取,先碼住不迷路~】

關鍵字: