| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the Qt Charts module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:GPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 3 or (at your option) any later version |
| ** approved by the KDE Free Qt Foundation. The licenses are as published by |
| ** the Free Software Foundation and appearing in the file LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "declarativeopenglrendernode_p.h" |
| |
| #include <QtGui/QOpenGLContext> |
| #include <QtGui/QOpenGLFunctions> |
| #include <QtGui/QOpenGLFramebufferObjectFormat> |
| #include <QtGui/QOpenGLFramebufferObject> |
| #include <QOpenGLShaderProgram> |
| #include <QtGui/QOpenGLBuffer> |
| |
| //#define QDEBUG_TRACE_GL_FPS |
| #ifdef QDEBUG_TRACE_GL_FPS |
| # include <QElapsedTimer> |
| #endif |
| |
| QT_CHARTS_BEGIN_NAMESPACE |
| |
| // This node draws the xy series data on a transparent background using OpenGL. |
| // It is used as a child node of the chart node. |
| DeclarativeOpenGLRenderNode::DeclarativeOpenGLRenderNode(QQuickWindow *window) : |
| QObject(), |
| m_texture(nullptr), |
| m_imageNode(nullptr), |
| m_window(window), |
| m_textureOptions(QQuickWindow::TextureHasAlphaChannel), |
| m_textureSize(1, 1), |
| m_recreateFbo(false), |
| m_fbo(nullptr), |
| m_resolvedFbo(nullptr), |
| m_selectionFbo(nullptr), |
| m_program(nullptr), |
| m_shaderAttribLoc(-1), |
| m_colorUniformLoc(-1), |
| m_minUniformLoc(-1), |
| m_deltaUniformLoc(-1), |
| m_pointSizeUniformLoc(-1), |
| m_renderNeeded(true), |
| m_antialiasing(false), |
| m_selectionRenderNeeded(true), |
| m_mousePressed(false), |
| m_lastPressSeries(nullptr), |
| m_lastHoverSeries(nullptr) |
| { |
| initializeOpenGLFunctions(); |
| |
| connect(m_window, &QQuickWindow::beforeRendering, |
| this, &DeclarativeOpenGLRenderNode::render); |
| } |
| |
| DeclarativeOpenGLRenderNode::~DeclarativeOpenGLRenderNode() |
| { |
| cleanXYSeriesResources(0); |
| |
| delete m_texture; |
| delete m_fbo; |
| delete m_resolvedFbo; |
| delete m_selectionFbo; |
| delete m_program; |
| |
| qDeleteAll(m_mouseEvents); |
| } |
| |
| static const char *vertexSourceCore = |
| "#version 150\n" |
| "in vec2 points;\n" |
| "uniform vec2 min;\n" |
| "uniform vec2 delta;\n" |
| "uniform float pointSize;\n" |
| "uniform mat4 matrix;\n" |
| "void main() {\n" |
| " vec2 normalPoint = vec2(-1, -1) + ((points - min) / delta);\n" |
| " gl_Position = matrix * vec4(normalPoint, 0, 1);\n" |
| " gl_PointSize = pointSize;\n" |
| "}"; |
| static const char *fragmentSourceCore = |
| "#version 150\n" |
| "uniform vec3 color;\n" |
| "out vec4 fragColor;\n" |
| "void main() {\n" |
| " fragColor = vec4(color,1);\n" |
| "}\n"; |
| |
| static const char *vertexSource = |
| "attribute highp vec2 points;\n" |
| "uniform highp vec2 min;\n" |
| "uniform highp vec2 delta;\n" |
| "uniform highp float pointSize;\n" |
| "uniform highp mat4 matrix;\n" |
| "void main() {\n" |
| " vec2 normalPoint = vec2(-1, -1) + ((points - min) / delta);\n" |
| " gl_Position = matrix * vec4(normalPoint, 0, 1);\n" |
| " gl_PointSize = pointSize;\n" |
| "}"; |
| static const char *fragmentSource = |
| "uniform highp vec3 color;\n" |
| "void main() {\n" |
| " gl_FragColor = vec4(color,1);\n" |
| "}\n"; |
| |
| // Must be called on render thread and in context |
| void DeclarativeOpenGLRenderNode::initGL() |
| { |
| recreateFBO(); |
| |
| m_program = new QOpenGLShaderProgram; |
| if (QOpenGLContext::currentContext()->format().profile() == QSurfaceFormat::CoreProfile) { |
| m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexSourceCore); |
| m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentSourceCore); |
| } else { |
| m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexSource); |
| m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentSource); |
| } |
| m_program->bindAttributeLocation("points", 0); |
| m_program->link(); |
| |
| m_program->bind(); |
| m_colorUniformLoc = m_program->uniformLocation("color"); |
| m_minUniformLoc = m_program->uniformLocation("min"); |
| m_deltaUniformLoc = m_program->uniformLocation("delta"); |
| m_pointSizeUniformLoc = m_program->uniformLocation("pointSize"); |
| m_matrixUniformLoc = m_program->uniformLocation("matrix"); |
| |
| // Create a vertex array object. In OpenGL ES 2.0 and OpenGL 2.x |
| // implementations this is optional and support may not be present |
| // at all. Nonetheless the below code works in all cases and makes |
| // sure there is a VAO when one is needed. |
| m_vao.create(); |
| QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); |
| |
| #if !defined(QT_OPENGL_ES_2) |
| if (!QOpenGLContext::currentContext()->isOpenGLES()) { |
| // Make it possible to change point primitive size and use textures with them in |
| // the shaders. These are implicitly enabled in ES2. |
| // Qt Quick doesn't change these flags, so it should be safe to just enable them |
| // at initialization. |
| glEnable(GL_PROGRAM_POINT_SIZE); |
| } |
| #endif |
| |
| m_program->release(); |
| } |
| |
| void DeclarativeOpenGLRenderNode::recreateFBO() |
| { |
| QOpenGLFramebufferObjectFormat fboFormat; |
| fboFormat.setAttachment(QOpenGLFramebufferObject::NoAttachment); |
| |
| int samples = 0; |
| QOpenGLContext *context = QOpenGLContext::currentContext(); |
| |
| if (m_antialiasing && (!context->isOpenGLES() || context->format().majorVersion() >= 3)) |
| samples = 4; |
| fboFormat.setSamples(samples); |
| |
| delete m_fbo; |
| delete m_resolvedFbo; |
| delete m_selectionFbo; |
| m_resolvedFbo = nullptr; |
| |
| m_fbo = new QOpenGLFramebufferObject(m_textureSize, fboFormat); |
| if (samples > 0) |
| m_resolvedFbo = new QOpenGLFramebufferObject(m_textureSize); |
| m_selectionFbo = new QOpenGLFramebufferObject(m_textureSize); |
| |
| delete m_texture; |
| uint textureId = m_resolvedFbo ? m_resolvedFbo->texture() : m_fbo->texture(); |
| m_texture = m_window->createTextureFromId(textureId, m_textureSize, m_textureOptions); |
| if (!m_imageNode) { |
| m_imageNode = m_window->createImageNode(); |
| m_imageNode->setFiltering(QSGTexture::Linear); |
| m_imageNode->setTextureCoordinatesTransform(QSGImageNode::MirrorVertically); |
| m_imageNode->setFlag(OwnedByParent); |
| if (!m_rect.isEmpty()) |
| m_imageNode->setRect(m_rect); |
| appendChildNode(m_imageNode); |
| } |
| m_imageNode->setTexture(m_texture); |
| |
| m_recreateFbo = false; |
| } |
| |
| // Must be called on render thread and in context |
| void DeclarativeOpenGLRenderNode::setTextureSize(const QSize &size) |
| { |
| m_textureSize = size; |
| m_recreateFbo = true; |
| m_renderNeeded = true; |
| m_selectionRenderNeeded = true; |
| } |
| |
| // Must be called on render thread while gui thread is blocked, and in context |
| void DeclarativeOpenGLRenderNode::setSeriesData(bool mapDirty, const GLXYDataMap &dataMap) |
| { |
| bool dirty = false; |
| if (mapDirty) { |
| // Series have changed, recreate map, but utilize old data where feasible |
| GLXYDataMap oldMap = m_xyDataMap; |
| m_xyDataMap.clear(); |
| |
| for (auto i = dataMap.begin(), end = dataMap.end(); i != end; ++i) { |
| GLXYSeriesData *data = oldMap.take(i.key()); |
| const GLXYSeriesData *newData = i.value(); |
| if (!data || newData->dirty) { |
| data = new GLXYSeriesData; |
| *data = *newData; |
| } |
| m_xyDataMap.insert(i.key(), data); |
| } |
| // Delete remaining old data |
| for (auto i = oldMap.begin(), end = oldMap.end(); i != end; ++i) { |
| delete i.value(); |
| cleanXYSeriesResources(i.key()); |
| } |
| dirty = true; |
| } else { |
| // Series have not changed, so just copy dirty data over |
| for (auto i = dataMap.begin(), end = dataMap.end(); i != end; ++i) { |
| const GLXYSeriesData *newData = i.value(); |
| if (i.value()->dirty) { |
| dirty = true; |
| GLXYSeriesData *data = m_xyDataMap.value(i.key()); |
| if (data) |
| *data = *newData; |
| } |
| } |
| } |
| if (dirty) { |
| markDirty(DirtyMaterial); |
| m_renderNeeded = true; |
| m_selectionRenderNeeded = true; |
| } |
| } |
| |
| void DeclarativeOpenGLRenderNode::setRect(const QRectF &rect) |
| { |
| m_rect = rect; |
| |
| if (m_imageNode) |
| m_imageNode->setRect(rect); |
| } |
| |
| void DeclarativeOpenGLRenderNode::setAntialiasing(bool enable) |
| { |
| if (m_antialiasing != enable) { |
| m_antialiasing = enable; |
| m_recreateFbo = true; |
| m_renderNeeded = true; |
| } |
| } |
| |
| void DeclarativeOpenGLRenderNode::addMouseEvents(const QVector<QMouseEvent *> &events) |
| { |
| if (events.size()) { |
| m_mouseEvents.append(events); |
| markDirty(DirtyMaterial); |
| } |
| } |
| |
| void DeclarativeOpenGLRenderNode::takeMouseEventResponses(QVector<MouseEventResponse> &responses) |
| { |
| responses.append(m_mouseEventResponses); |
| m_mouseEventResponses.clear(); |
| } |
| |
| void DeclarativeOpenGLRenderNode::renderGL(bool selection) |
| { |
| glClearColor(0, 0, 0, 0); |
| |
| QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); |
| m_program->bind(); |
| |
| glClear(GL_COLOR_BUFFER_BIT); |
| glEnableVertexAttribArray(0); |
| |
| glViewport(0, 0, m_textureSize.width(), m_textureSize.height()); |
| |
| int counter = 0; |
| for (auto i = m_xyDataMap.begin(), end = m_xyDataMap.end(); i != end; ++i) { |
| QOpenGLBuffer *vbo = m_seriesBufferMap.value(i.key()); |
| GLXYSeriesData *data = i.value(); |
| |
| if (data->visible) { |
| if (selection) { |
| m_selectionVector[counter] = i.key(); |
| m_program->setUniformValue(m_colorUniformLoc, QVector3D((counter & 0xff) / 255.0f, |
| ((counter & 0xff00) >> 8) / 255.0f, |
| ((counter & 0xff0000) >> 16) / 255.0f)); |
| counter++; |
| } else { |
| m_program->setUniformValue(m_colorUniformLoc, data->color); |
| } |
| m_program->setUniformValue(m_minUniformLoc, data->min); |
| m_program->setUniformValue(m_deltaUniformLoc, data->delta); |
| m_program->setUniformValue(m_matrixUniformLoc, data->matrix); |
| |
| if (!vbo) { |
| vbo = new QOpenGLBuffer; |
| m_seriesBufferMap.insert(i.key(), vbo); |
| vbo->create(); |
| } |
| vbo->bind(); |
| if (data->dirty) { |
| vbo->allocate(data->array.constData(), data->array.count() * sizeof(GLfloat)); |
| data->dirty = false; |
| } |
| |
| glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0); |
| if (data->type == QAbstractSeries::SeriesTypeLine) { |
| glLineWidth(data->width); |
| glDrawArrays(GL_LINE_STRIP, 0, data->array.size() / 2); |
| } else { // Scatter |
| m_program->setUniformValue(m_pointSizeUniformLoc, data->width); |
| glDrawArrays(GL_POINTS, 0, data->array.size() / 2); |
| } |
| vbo->release(); |
| } |
| } |
| } |
| |
| void DeclarativeOpenGLRenderNode::renderSelection() |
| { |
| m_selectionFbo->bind(); |
| |
| m_selectionVector.resize(m_xyDataMap.size()); |
| |
| renderGL(true); |
| |
| m_selectionRenderNeeded = false; |
| } |
| |
| void DeclarativeOpenGLRenderNode::renderVisual() |
| { |
| m_fbo->bind(); |
| |
| renderGL(false); |
| |
| if (m_resolvedFbo) { |
| QRect rect(QPoint(0, 0), m_fbo->size()); |
| QOpenGLFramebufferObject::blitFramebuffer(m_resolvedFbo, rect, m_fbo, rect); |
| } |
| |
| markDirty(DirtyMaterial); |
| |
| #ifdef QDEBUG_TRACE_GL_FPS |
| static QElapsedTimer stopWatch; |
| static int frameCount = -1; |
| if (frameCount == -1) { |
| stopWatch.start(); |
| frameCount = 0; |
| } |
| frameCount++; |
| int elapsed = stopWatch.elapsed(); |
| if (elapsed >= 1000) { |
| elapsed = stopWatch.restart(); |
| qreal fps = qreal(0.1 * int(10000.0 * (qreal(frameCount) / qreal(elapsed)))); |
| qDebug() << "FPS:" << fps; |
| frameCount = 0; |
| } |
| #endif |
| } |
| |
| // Must be called on render thread as response to beforeRendering signal |
| void DeclarativeOpenGLRenderNode::render() |
| { |
| if (m_renderNeeded) { |
| if (m_xyDataMap.size()) { |
| if (!m_program) |
| initGL(); |
| if (m_recreateFbo) |
| recreateFBO(); |
| renderVisual(); |
| } else { |
| if (m_imageNode && m_imageNode->rect() != QRectF()) { |
| glClearColor(0, 0, 0, 0); |
| m_fbo->bind(); |
| glClear(GL_COLOR_BUFFER_BIT); |
| |
| // If last series was removed, zero out the node rect |
| setRect(QRectF()); |
| } |
| } |
| m_renderNeeded = false; |
| } |
| handleMouseEvents(); |
| m_window->resetOpenGLState(); |
| } |
| |
| void DeclarativeOpenGLRenderNode::cleanXYSeriesResources(const QXYSeries *series) |
| { |
| if (series) { |
| delete m_seriesBufferMap.take(series); |
| delete m_xyDataMap.take(series); |
| } else { |
| foreach (QOpenGLBuffer *buffer, m_seriesBufferMap.values()) |
| delete buffer; |
| m_seriesBufferMap.clear(); |
| foreach (GLXYSeriesData *data, m_xyDataMap.values()) |
| delete data; |
| m_xyDataMap.clear(); |
| } |
| } |
| |
| void DeclarativeOpenGLRenderNode::handleMouseEvents() |
| { |
| if (m_mouseEvents.size()) { |
| if (m_xyDataMap.size()) { |
| if (m_selectionRenderNeeded) |
| renderSelection(); |
| } |
| Q_FOREACH (QMouseEvent *event, m_mouseEvents) { |
| const QXYSeries *series = findSeriesAtEvent(event); |
| switch (event->type()) { |
| case QEvent::MouseMove: { |
| if (series != m_lastHoverSeries) { |
| if (m_lastHoverSeries) { |
| m_mouseEventResponses.append( |
| MouseEventResponse(MouseEventResponse::HoverLeave, |
| event->pos(), m_lastHoverSeries)); |
| } |
| if (series) { |
| m_mouseEventResponses.append( |
| MouseEventResponse(MouseEventResponse::HoverEnter, |
| event->pos(), series)); |
| } |
| m_lastHoverSeries = series; |
| } |
| break; |
| } |
| case QEvent::MouseButtonPress: { |
| if (series) { |
| m_mousePressed = true; |
| m_mousePressPos = event->pos(); |
| m_lastPressSeries = series; |
| m_mouseEventResponses.append( |
| MouseEventResponse(MouseEventResponse::Pressed, |
| event->pos(), series)); |
| } |
| break; |
| } |
| case QEvent::MouseButtonRelease: { |
| m_mouseEventResponses.append( |
| MouseEventResponse(MouseEventResponse::Released, |
| m_mousePressPos, m_lastPressSeries)); |
| if (m_mousePressed) { |
| m_mouseEventResponses.append( |
| MouseEventResponse(MouseEventResponse::Clicked, |
| m_mousePressPos, m_lastPressSeries)); |
| } |
| if (m_lastHoverSeries == m_lastPressSeries && m_lastHoverSeries != series) { |
| if (m_lastHoverSeries) { |
| m_mouseEventResponses.append( |
| MouseEventResponse(MouseEventResponse::HoverLeave, |
| event->pos(), m_lastHoverSeries)); |
| } |
| m_lastHoverSeries = nullptr; |
| } |
| m_lastPressSeries = nullptr; |
| m_mousePressed = false; |
| break; |
| } |
| case QEvent::MouseButtonDblClick: { |
| if (series) { |
| m_mouseEventResponses.append( |
| MouseEventResponse(MouseEventResponse::DoubleClicked, |
| event->pos(), series)); |
| } |
| break; |
| } |
| default: |
| break; |
| } |
| } |
| |
| qDeleteAll(m_mouseEvents); |
| m_mouseEvents.clear(); |
| } |
| } |
| |
| const QXYSeries *DeclarativeOpenGLRenderNode::findSeriesAtEvent(QMouseEvent *event) |
| { |
| const QXYSeries *series = nullptr; |
| int index = -1; |
| |
| if (m_xyDataMap.size()) { |
| m_selectionFbo->bind(); |
| |
| GLubyte pixel[4] = {0, 0, 0, 0}; |
| glReadPixels(event->pos().x(), m_textureSize.height() - event->pos().y(), |
| 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, |
| (void *)pixel); |
| if (pixel[3] == 0xff) |
| index = pixel[0] + (pixel[1] << 8) + (pixel[2] << 16); |
| } |
| |
| if (index >= 0 && index < m_selectionVector.size()) |
| series = m_selectionVector.at(index); |
| |
| return series; |
| } |
| |
| QT_CHARTS_END_NAMESPACE |