Revision control

++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses
++
++ #include "declarativeopenglrendernode_p.h"
++
++ #include <QtGui/QOpenGLContext>
++ #include <QtGui/QOpenGLFunctions>
++ #include <QtOpenGL/QOpenGLFramebufferObjectFormat>
++ #include <QtOpenGL/QOpenGLFramebufferObject>
++ #include <QOpenGLShaderProgram>
++ #include <QtOpenGL/QOpenGLBuffer>
++ #include <QQuickOpenGLUtils>
++
++ //#define QDEBUG_TRACE_GL_FPS
++ #ifdef QDEBUG_TRACE_GL_FPS
++ # include <QElapsedTimer>
++ #endif
++
++ QT_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 !QT_CONFIG(opengles2)
++ 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 = QNativeInterface::QSGOpenGLTexture::fromNative(textureId, m_window, 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) {
++ if (!data)
++ 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 QList<QMouseEvent *> &events)
++ {
++ if (events.size()) {
++ m_mouseEvents.append(events);
++ markDirty(DirtyMaterial);
++ }
++ }
++
++ void DeclarativeOpenGLRenderNode::takeMouseEventResponses(QList<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_selectionList[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(), int(data->array.size() * 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_selectionList.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()
++ {
++ // Reset blend function, etc. derived from the previous frame.
++ QQuickOpenGLUtils::resetOpenGLState();
++ 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();
++ QQuickOpenGLUtils::resetOpenGLState();
++ }
++
++ void DeclarativeOpenGLRenderNode::cleanXYSeriesResources(const QXYSeries *series)
++ {
++ if (series) {
++ delete m_seriesBufferMap.take(series);
++ delete m_xyDataMap.take(series);
++ } else {
++ for (QOpenGLBuffer *buffer : std::as_const(m_seriesBufferMap))
++ delete buffer;
++ m_seriesBufferMap.clear();
++ for (GLXYSeriesData *data : std::as_const(m_xyDataMap))
++ delete data;
++ m_xyDataMap.clear();
++ }
++ }
++
++ void DeclarativeOpenGLRenderNode::handleMouseEvents()
++ {
++ if (m_mouseEvents.size()) {
++ if (m_xyDataMap.size()) {
++ if (m_selectionRenderNeeded)
++ renderSelection();
++ }
++ for (const QMouseEvent *event : std::as_const(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(const 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_selectionList.size())
++ series = m_selectionList.at(index);
++
++ return series;
++ }
++
++ QT_END_NAMESPACE