Qt使用OpenGL进行多线程离屏渲染

基于 Qt WidgetsQt程序,控件的刷新默认状况下都是在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

概述

有如下主要的类或方法:

  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将其移到渲染线程中。

渲染线程例程

undefined

渲染线程开始渲染时,首先绑定 context和初始化 TextureBuffer。而后在循环中重复执行渲染-保存纹理的循环

离屏渲染

其初始化在 Renderer::init中进行,渲染在 Renderer::render中进行,各种 OpenGL基础教程中都有对离屏渲染的相关介绍和分析,此处再也不赘述。

保存纹理

undefined

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);
}
...

GLWidget::paintGL中调用 TextureBuffer::drawTexture来绘制缓存的纹理。

性能

上面所作的这一切,可以提升性能吗?很遗憾,答案是”不必定”。就这个demo而言,渲染过程几乎彻底不须要等待数据和CPU处理(除了初始化时须要CPU计算),不断使用GPU进行渲染,这致使GPU占用率几乎达到了100%,成为了一个瓶颈。当主线程进行 OpenGL调用时,极可能会由于正在处理渲染线程的 OpenGL调用而被阻塞,致使帧率降低。使用NVIDIA Nsights Graphics实测结果以下:

Qt使用OpenGL进行多线程离屏渲染Qt使用OpenGL进行多线程离屏渲染
第一幅图是刚打开程序时的帧率,基本稳定在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/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • windows安装jdk8

    win10系统安装jdk8全过程 一 下载安装文件 jdk的安装与配置是Java学习的第一步,下面记录一下具体过程。首先根据自己系统下载对应版本。下载地址http://www.or…

    Java 2023年5月30日
    066
  • 异常:Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.3.2.RELEASE:repackage (repackage) 解决办法

    异常:Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.3.2.RELEASE:…

    Java 2023年6月9日
    078
  • java通过command调用openssl生成私钥和证书

    在windows环境下进行的测试,前提条件,windows上需要先安装openssl。 配置环境变量,查看版本: import java.io.*; import java.uti…

    Java 2023年5月29日
    070
  • 深入浅出分析 ArrayDeque

    作者:炸鸡可乐原文出处:www.pzblog.cn 一、摘要 在 jdk1.5 中,新增了 Queue 接口,代表一种队列集合的实现,咱们继续来聊聊 java 集合体系中的 Que…

    Java 2023年6月9日
    086
  • Django基础学习之Cookie 和 Sessions 应用

    在Django里面,使用Cookie和Session看起来好像是一样的,使用的方式都是request.COOKIES[XXX]和request.session[XXX],其中XXX…

    Java 2023年5月29日
    048
  • Java调试与定位

    目录: Java程序测试:JVM调试体系, Intellij 调试, Junit自动化 Java程序监控: 常用监控工具, 性能问题 Java典型问题:锁问题, 线程池问题,JVM…

    Java 2023年5月29日
    0103
  • Java正则表达式——matcher.find()的匹配原理

    在Java正则中,matcher.find()通过获取目的子字符串的第一元素和最后一个元素的索引来确定目的字符串,大致方法就是将获取的索引存入在类中定义好的属性groups[]中,…

    Java 2023年6月8日
    069
  • Spring基于注解的AOP的切面优先级

    每一个切面都有一个默认的优先级(默认值为Integer的最大值) @Order(1)通过这个注解设置一个正整数数值,数值越小,优先级越高 @Component @Aspect//&…

    Java 2023年6月16日
    076
  • 多线程_基础

    一.一个Java程序最少开几个线程? 3个:主线程;gc线程;异常处理线程 二.线程的生命周期以及状态? 阻塞的分类: 等待阻塞:执行wait(),需要notify()/notif…

    Java 2023年6月7日
    080
  • 商城秒杀系统总结(Java)

    本文写的较为零散,对没有基础的同学不太友好。 一、秒杀系统项目总结(基础版) classpath 在.properties中时常需要读取资源,定位文件地址时经常用到 classpa…

    Java 2023年6月7日
    0101
  • 1、TCP四元组UDP二元组

    UDP是面向无连接的,对于服务器和客户端来说只有一个socket,只需要确定目的IP和Port(dst ip 和dsp port),把数据都发送给这一个socket,所以采用二元组…

    Java 2023年6月8日
    093
  • springboot整合ehcache

    springboot整合ehcache 工具类实现缓存 1. jar包导入 <dependency>   <groupId>org.springf…

    Java 2023年6月9日
    063
  • 引路蜂地图API:Gis.Location包定义

    本包定义了GPS接收器一个通用接口,并提供对NMEA 2.0数据的解码方法。在Java ME平台上对JSR179 进行了封装. Coordinates 定义地址经纬度坐标。 Loc…

    Java 2023年5月30日
    098
  • fastposter发布1.4.3 跨语言的海报生成器

    fastposter发布1.4.3 跨语言的海报生成器 v1.4.3 增加golang语言支持,优化生成器代码,完善官方文档 昨天喝了点小9️⃣,发版慢了些。 future: 增加…

    Java 2023年6月5日
    084
  • 设计模式——行为型设计模式

    行为型设计模式 针对对象之间的交互 解释器模式 java中用的很。JVM编译的时候就是对我们写的代码进行了解释操作;数据库SQL语句亦是如此 解释器:对语言进行解释,根据不同语义来…

    Java 2023年6月14日
    080
  • 设计模式学习笔记(十七)中介者模式及其应用场景

    中介者(Mediator)模式指定义了一个单独的中介对象,来封装一组对象之间的交互。即将这组对象之间的交互委派给中介对象,从而来避免对象之间的直接交互。比如我们各种设备之间的通信,…

    Java 2023年6月6日
    084
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球