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

news2024/11/25 3:03:00

基于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

概述

有如下主要的类或方法:

  1. GLWidget

这个类在UI线程中使用,继承了QOpenGLWidget,负责将渲染线程渲染结果绘制到屏幕上。

  1. Renderer

这个类在渲染线程中使用,负责将三角形渲染到离屏framebuffer中。

  1. RenderThread

渲染线程管理类,负责初始化渲染线程OpenGL的context。

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

渲染线程例程

// 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处理的代码移到独立线程也许是个不错的选择。具体采用哪一种方案仍是要根据实际测试效果来决定。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/385397.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

BI不是报表,千万不要混淆

商业智能BI作为商业世界的新宠儿,在市场上实现了高速增长并获得了各领域企业的口碑赞誉。 很多企业把商业智能BI做成了纯报表,二维表格的数据展现形式,也有一些简单的图表可视化。但是这些简单的商业智能BI可视化报表基本上只服务到了一线的…

【JAVA程序设计】【C00110】基于SSM(非maven)的车辆维修管理系统

基于SSM(非maven)的车辆维修管理系统项目简介项目获取开发环境项目技术运行截图项目简介 基于ssm框架非maven开发的车辆维修管理系统共分为三个角色:管理员、用户 管理员角色包含以下功能: 查看用户、添加用户、查看车辆信息、故…

FPGA纯verilog实现图像视频旋转 串口指令控制旋转角度 提供工程源码和技术支持

目录1、前言2、理论基础3、设计思路和框架图像输入和采集图像旋转处理图像缓存图像输出4、vivado工程详解5、上板调试验证6、福利:工程代码的获取1、前言 图像旋转是一种常用的图像处理技术,其基本原理就是指图像以某一点为中心旋转一定的角度&#xff…

函数的极限

目录 函数极限的定义: 数列的极限和函数极限 定理1: 自变量趋向有限制时,函数的极限 左右极限: 定理: 需要分左右极限求极限的三种问题: 例题: 例2: 极限性质: 保号…

WebRTC标准与框架解读(1)

1、如果让我来设计webrtc框架我在分析源码的时候,都喜欢做这样一件事情:如果让我来设计它,我会怎么做?大家可以紧跟我的思路,分析一下WebRTC为什么如此设计。为了对整个框架有有一个全面的了解,我们首先要做…

外包测试3年,离职后成功入职华为,拿到offer的那天我泪目了....

一提及外包测试,大部分人的第一印象就是:工作强度大,技术含量低,没有归属感!外包工作三年总体感受就是这份工作缺乏归属感,心里总有一种落差,进步空间不大,接触不到核心技术&#xf…

IO详解(文件,流对象,一些练习)

目录 文件 文件概念 文件的路径 路径有俩种表示风格 文件类型 如何区分文本文件还是二进制文件? java对文件的操作 File类中的一些方法 流对象 流对象的简单概念 java标准库的流对象 1.字节流,(操作二进制数据的) 2.字符流 (操作文本数据的) 流对象最核心的四个…

Android Framework-进程间通信——Binder

我们知道,同一个程序中的两个函数之间能直接调用的根本原因是处于相同的内存空间中。 比如有以下两个函数A和B: /*Simple.c*/ void A() { B(); } void B() { }因为是在一个内存空间中,虚拟地址的映射规则完全一致,所以函数A和B之…

【JAVA程序设计】【C00111】基于SSM的网上图书商城管理系统——有文档

基于SSM的网上图书商城管理系统——有文档项目简介项目获取开发环境项目技术运行截图项目简介 基于ssm框架开发的网上在线图书售卖商城项目,本项目分为三种权限:系统管理员、卖家、买家 管理员角色包含以下功能: 用户信息管理、权限管理、订…

XSS跨站脚本

XSS跨站脚本XSS简介XSS验证XSS危害XSS简介 XSS被称为跨站脚本攻击(Cross-site scripting),由于和CSS(Cascading Style Sheets)重名,所以改为XSS。XSS主要基于javascript语言完成恶意的攻击行为,因为javascript可以非常灵活的操作html、css和…

day57-day58【代码随想录】二刷数组

文章目录前言一、螺旋矩阵||(力扣59)二、螺旋矩阵(力扣54)三、顺时针打印矩阵(剑指 Offer29)四、在排序数组中查找元素的第一个和最后一个位置(力扣34)【二分查找】五、有多少小于当…

自动驾驶决策概况

文章目录1. 第一章行为决策在自动驾驶系统架构中的位置2. 行为决策算法的种类2.1 基于规则的决策算法2.1.1 决策树2.1.2 有限状态机(FSM)2.1.3 基于本体论(Ontologies-based)2.2 基于统计的决策算法2.2.1 贝叶斯网络(B…

智慧制硅厂 Web SCADA 生产线

我国目前是全球最大的工业硅生产国、消费国和贸易国,且未来该产业的主要增量也将来源于我国。绿色低碳发展已成为全球大趋势和国际社会的共识,随着我国“双碳”目标的推进,光伏产业链快速发展,在光伏装机需求的带动下,…

flink兼容性验证

flink介绍:https://blog.csdn.net/weixin_43563705/article/details/107604693 一、安装启动 安装flink及其依赖 yum install java-1.8.0-openjdk curl tar mkdir -p /usr/local/flink wget https://mirrors.aliyun.com/apache/flink/flink-1.16.1/flink-1.16.1-bi…

如何校招进BAT做产品经理

嗨,很高兴,以文字的形式和你见面。在校招中,我拿到了百度、京东、爱奇艺、新浪和去哪儿的产品经理校招offer,其中百度是special offer。在找实习的过程中,也拿到了爱奇艺、微信电影票、搜狐畅游、艺龙等公司的产品经理…

Ac4GlcNAz,98924-81-3,N-乙酰葡糖胺叠氮基,可以进行糖化学修饰

Ac4GlcNAz反应特点:四乙酰氨基叠氮氨基葡萄糖(Ac4GlcNAz,98924-81-3)为糖缀合物可视化提供非放射性替代物。西安凯新生物科技有限公司供应的解释道它是细胞通透性、细胞内处理和结合的,而不是其天然单糖对乙酰氨基葡萄…

CEC2017:斑马优化算法(Zebra Optimization Algorithm,ZOA)求解cec2017(提供MATLAB代码)

一、斑马优化算法 斑马优化算法(Zebra Optimization Algorithm,ZOA)Eva Trojovsk等人于2022年提出,其模拟斑马的觅食和对捕食者攻击的防御行为。 斑马因身上有起保护作用的斑纹而得名。没有任何动物比斑马的皮毛更与众不同。斑…

SpringCloud的基本使用

文章目录一.单体架构和分布式架构的区别1.单体架构2.分布式架构二.微服务1.微服务的架构特征2.微服务技术对比3.企业需求4.微服务的远程定义5.提供者与消费者三.Eureka注册中心1.eureka的作用2.搭建EurekaServer3.Eureka注册3.服务拉取四.Ribbon负载均衡1.负载均衡流程2.负载均…

总被程序员坑?你需要了解API接口

编辑导读:程序员是公司里的技术岗,也是产品经理最密切的合作伙伴。但是,程序员能看懂产品经理的工作,产品经理却不一定能明白程序员的工作,因此也常常被无良程序员坑。本文就从API接口的维度,浅析API的概念…

十七、本地方法接口的理解

什么是本地方法? 1.简单来讲,一个Ntive method 就是一个Java调用非Java代码的接口.一个Native Method 是这样一个Java方法:该方法的实现由非Java语言实现,比如C,这个特征并非Java所特有,很多其他的编程语言都由这一机制,比如在C中…