From 79a856530b6986ca6d6d7485b2e6cec810c3b7fe 2015-09-25 06:44:35 From: Miikka Heikkinen Date: 2015-09-25 06:44:35 Subject: [PATCH] Accelerating lineseries with OpenGL Added support for QAbstractSeries::useOpenGL property. When true, the series in question is drawn on a separate offscreen buffer using OpenGL and then superimposed on the chart. Currently this property is only supported for line and scatter series. Change-Id: I174fec541f9f3c23464270c1fe08f824af16a0fb Reviewed-by: Titta Heikkala Reviewed-by: Tomi Korpipää --- diff --git a/examples/charts/audio/xyseriesiodevice.cpp b/examples/charts/audio/xyseriesiodevice.cpp index ef9789c..4b7d2f5 100644 --- a/examples/charts/audio/xyseriesiodevice.cpp +++ b/examples/charts/audio/xyseriesiodevice.cpp @@ -35,12 +35,12 @@ qint64 XYSeriesIODevice::readData(char * data, qint64 maxSize) qint64 XYSeriesIODevice::writeData(const char * data, qint64 maxSize) { qint64 range = 2000; - QList oldPoints = m_series->points(); - QList points; + QVector oldPoints = m_series->pointsVector(); + QVector points; int resolution = 4; if (oldPoints.count() < range) { - points = m_series->points(); + points = m_series->pointsVector(); } else { for (int i = maxSize/resolution; i < oldPoints.count(); i++) points.append(QPointF(i - maxSize/resolution, oldPoints.at(i).y())); diff --git a/examples/charts/charts.pro b/examples/charts/charts.pro index a104fcd..3f463e4 100644 --- a/examples/charts/charts.pro +++ b/examples/charts/charts.pro @@ -50,7 +50,13 @@ qtHaveModule(quick) { qtHaveModule(multimedia) { SUBDIRS += audio } else { - message("QtMultimedia library not available. Some examples are disabled") + message("QtMultimedia library not available. Some examples are disabled.") +} + +contains(QT_CONFIG, opengl) { + SUBDIRS += openglseries +} else { + message("OpenGL not available. Some examples are disabled.") } !linux-arm*: { diff --git a/examples/charts/chartthemes/themewidget.cpp b/examples/charts/chartthemes/themewidget.cpp index 797356b..a78ab9b 100644 --- a/examples/charts/chartthemes/themewidget.cpp +++ b/examples/charts/chartthemes/themewidget.cpp @@ -189,7 +189,7 @@ QChart *ThemeWidget::createAreaChart() const for (int j(0); j < m_dataTable[i].count(); j++) { Data data = m_dataTable[i].at(j); if (lowerSeries) { - const QList& points = lowerSeries->points(); + const QVector& points = lowerSeries->pointsVector(); upperSeries->append(QPointF(j, points[i].y() + data.first.y())); } else { upperSeries->append(QPointF(j, data.first.y())); diff --git a/examples/charts/openglseries/datasource.cpp b/examples/charts/openglseries/datasource.cpp new file mode 100644 index 0000000..64b4540 --- /dev/null +++ b/examples/charts/openglseries/datasource.cpp @@ -0,0 +1,118 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd +** All rights reserved. +** For any questions to The Qt Company, please use contact form at http://qt.io +** +** This file is part of the Qt Charts module. +** +** Licensees holding valid commercial license for Qt may use this file in +** accordance with the Qt License Agreement provided with the Software +** or, alternatively, in accordance with the terms contained in a written +** agreement between you and The Qt Company. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.io +** +****************************************************************************/ + +#include "datasource.h" +#include + +QT_CHARTS_USE_NAMESPACE + +DataSource::DataSource(QObject *parent) : + QObject(parent), + m_index(-1) +{ + generateData(0, 0, 0); +} + +void DataSource::update(QAbstractSeries *series, int seriesIndex) +{ + if (series) { + QXYSeries *xySeries = static_cast(series); + const QVector > &seriesData = m_data.at(seriesIndex); + if (seriesIndex == 0) + m_index++; + if (m_index > seriesData.count() - 1) + m_index = 0; + + QVector points = seriesData.at(m_index); + // Use replace instead of clear + append, it's optimized for performance + xySeries->replace(points); + } +} + +void DataSource::handleSceneChanged() +{ + m_dataUpdater.start(); +} + +void DataSource::updateAllSeries() +{ + static int frameCount = 0; + static QString labelText = QStringLiteral("FPS: %1"); + + for (int i = 0; i < m_seriesList.size(); i++) + update(m_seriesList[i], i); + + frameCount++; + int elapsed = m_fpsTimer.elapsed(); + if (elapsed >= 1000) { + elapsed = m_fpsTimer.restart(); + qreal fps = qreal(0.1 * int(10000.0 * (qreal(frameCount) / qreal(elapsed)))); + m_fpsLabel->setText(labelText.arg(QString::number(fps, 'f', 1))); + frameCount = 0; + } +} + +void DataSource::startUpdates(const QList &seriesList, QLabel *fpsLabel) +{ + m_seriesList = seriesList; + m_fpsLabel = fpsLabel; + + m_dataUpdater.setInterval(0); + m_dataUpdater.setSingleShot(true); + QObject::connect(&m_dataUpdater, &QTimer::timeout, + this, &DataSource::updateAllSeries); + + m_fpsTimer.start(); + updateAllSeries(); +} + +void DataSource::generateData(int seriesCount, int rowCount, int colCount) +{ + // Remove previous data + foreach (QVector > seriesData, m_data) { + foreach (QVector row, seriesData) + row.clear(); + } + + m_data.clear(); + + qreal xAdjustment = 20.0 / (colCount * rowCount); + qreal yMultiplier = 3.0 / qreal(seriesCount); + + // Append the new data depending on the type + for (int k(0); k < seriesCount; k++) { + QVector > seriesData; + qreal height = qreal(k) * (10.0 / qreal(seriesCount)) + 0.3; + for (int i(0); i < rowCount; i++) { + QVector points; + points.reserve(colCount); + for (int j(0); j < colCount; j++) { + qreal x(0); + qreal y(0); + // data with sin + random component + y = height + (yMultiplier * qSin(3.14159265358979 / 50 * j) + + (yMultiplier * (qreal) rand() / (qreal) RAND_MAX)); + // 0.000001 added to make values logaxis compatible + x = 0.000001 + 20.0 * (qreal(j) / qreal(colCount)) + (xAdjustment * qreal(i)); + points.append(QPointF(x, y)); + } + seriesData.append(points); + } + m_data.append(seriesData); + } +} diff --git a/examples/charts/openglseries/datasource.h b/examples/charts/openglseries/datasource.h new file mode 100644 index 0000000..295d93a --- /dev/null +++ b/examples/charts/openglseries/datasource.h @@ -0,0 +1,53 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd +** All rights reserved. +** For any questions to The Qt Company, please use contact form at http://qt.io +** +** This file is part of the Qt Charts module. +** +** Licensees holding valid commercial license for Qt may use this file in +** accordance with the Qt License Agreement provided with the Software +** or, alternatively, in accordance with the terms contained in a written +** agreement between you and The Qt Company. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.io +** +****************************************************************************/ + +#ifndef DATASOURCE_H +#define DATASOURCE_H + +#include +#include +#include +#include +#include + +QT_CHARTS_USE_NAMESPACE + +class DataSource : public QObject +{ + Q_OBJECT +public: + explicit DataSource(QObject *parent = 0); + + void startUpdates(const QList &seriesList, QLabel *fpsLabel); + +public slots: + void generateData(int seriesCount, int rowCount, int colCount); + void update(QAbstractSeries *series, int seriesIndex); + void handleSceneChanged(); + void updateAllSeries(); + +private: + QVector > > m_data; + int m_index; + QList m_seriesList; + QLabel *m_fpsLabel; + QElapsedTimer m_fpsTimer; + QTimer m_dataUpdater; +}; + +#endif // DATASOURCE_H diff --git a/examples/charts/openglseries/main.cpp b/examples/charts/openglseries/main.cpp new file mode 100644 index 0000000..7b654a5 --- /dev/null +++ b/examples/charts/openglseries/main.cpp @@ -0,0 +1,167 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd +** All rights reserved. +** For any questions to The Qt Company, please use contact form at http://qt.io +** +** This file is part of the Qt Charts module. +** +** Licensees holding valid commercial license for Qt may use this file in +** accordance with the Qt License Agreement provided with the Software +** or, alternatively, in accordance with the terms contained in a written +** agreement between you and The Qt Company. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.io +** +****************************************************************************/ + +#include "datasource.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// Uncomment to use logarithmic axes instead of regular value axes +//#define USE_LOG_AXIS + +// Uncomment to use regular series instead of OpenGL accelerated series +//#define DONT_USE_GL_SERIES + +// Uncomment to add a simple regular series (thick line) and a matching OpenGL series (thinner line) +// to verify the series have same visible geometry. +//#define ADD_SIMPLE_SERIES + +QT_CHARTS_USE_NAMESPACE + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + QStringList colors; + colors << "red" << "blue" << "green" << "black"; + + QChart *chart = new QChart(); + chart->legend()->hide(); + +#ifdef USE_LOG_AXIS + QLogValueAxis *axisX = new QLogValueAxis; + QLogValueAxis *axisY = new QLogValueAxis; +#else + QValueAxis *axisX = new QValueAxis; + QValueAxis *axisY = new QValueAxis; +#endif + + chart->addAxis(axisX, Qt::AlignBottom); + chart->addAxis(axisY, Qt::AlignLeft); + + const int seriesCount = 10; +#ifdef DONT_USE_GL_SERIES + const int pointCount = 100; + chart->setTitle("Unaccelerated Series"); +#else + const int pointCount = 10000; + chart->setTitle("OpenGL Accelerated Series"); +#endif + + QList seriesList; + for (int i = 0; i < seriesCount; i++) { + QXYSeries *series = 0; + int colorIndex = i % colors.size(); + if (i % 2) { + series = new QScatterSeries; + QScatterSeries *scatter = static_cast(series); + scatter->setColor(QColor(colors.at(colorIndex))); + scatter->setMarkerSize(qreal(colorIndex + 2) / 2.0); + // Scatter pen doesn't have affect in OpenGL drawing, but if you disable OpenGL drawing + // this makes the marker border visible and gives comparable marker size to OpenGL + // scatter points. + scatter->setPen(QPen("black")); + } else { + series = new QLineSeries; + series->setPen(QPen(QBrush(QColor(colors.at(colorIndex))), + qreal(colorIndex + 2) / 2.0)); + } + seriesList.append(series); +#ifdef DONT_USE_GL_SERIES + series->setUseOpenGL(false); +#else + //![1] + series->setUseOpenGL(true); + //![1] +#endif + chart->addSeries(series); + series->attachAxis(axisX); + series->attachAxis(axisY); + } + + if (axisX->type() == QAbstractAxis::AxisTypeLogValue) + axisX->setRange(0.1, 20.0); + else + axisX->setRange(0, 20.0); + + if (axisY->type() == QAbstractAxis::AxisTypeLogValue) + axisY->setRange(0.1, 10.0); + else + axisY->setRange(0, 10.0); + +#ifdef ADD_SIMPLE_SERIES + QLineSeries *simpleRasterSeries = new QLineSeries; + *simpleRasterSeries << QPointF(0.001, 0.001) + << QPointF(2.5, 8.0) + << QPointF(5.0, 4.0) + << QPointF(7.5, 9.0) + << QPointF(10.0, 0.001) + << QPointF(12.5, 2.0) + << QPointF(15.0, 1.0) + << QPointF(17.5, 6.0) + << QPointF(20.0, 10.0); + simpleRasterSeries->setUseOpenGL(false); + simpleRasterSeries->setPen(QPen(QBrush("magenta"), 8)); + chart->addSeries(simpleRasterSeries); + simpleRasterSeries->attachAxis(axisX); + simpleRasterSeries->attachAxis(axisY); + + QLineSeries *simpleGLSeries = new QLineSeries; + simpleGLSeries->setUseOpenGL(true); + simpleGLSeries->setPen(QPen(QBrush("black"), 2)); + simpleGLSeries->replace(simpleRasterSeries->points()); + chart->addSeries(simpleGLSeries); + simpleGLSeries->attachAxis(axisX); + simpleGLSeries->attachAxis(axisY); +#endif + + QChartView *chartView = new QChartView(chart); + + QMainWindow window; + window.setCentralWidget(chartView); + window.resize(600, 400); + window.show(); + + DataSource dataSource; + dataSource.generateData(seriesCount, 10, pointCount); + + QLabel *fpsLabel = new QLabel(&window); + QLabel *countLabel = new QLabel(&window); + QString countText = QStringLiteral("Total point count: %1"); + countLabel->setText(countText.arg(pointCount * seriesCount)); + countLabel->resize(window.width(), countLabel->height()); + fpsLabel->move(10,2); + fpsLabel->raise(); + fpsLabel->show(); + countLabel->move(10, 14); + fpsLabel->raise(); + countLabel->show(); + + // We can get more than one changed event per frame, so do async update. + // This also allows the application to be responsive. + QObject::connect(chart->scene(), &QGraphicsScene::changed, + &dataSource, &DataSource::handleSceneChanged); + + dataSource.startUpdates(seriesList, fpsLabel); + + return a.exec(); +} diff --git a/examples/charts/openglseries/openglseries.pro b/examples/charts/openglseries/openglseries.pro new file mode 100644 index 0000000..0ae1159 --- /dev/null +++ b/examples/charts/openglseries/openglseries.pro @@ -0,0 +1,9 @@ +!include( ../examples.pri ) { + error( "Couldn't find the examples.pri file!" ) +} + +TARGET = openglseries +SOURCES += main.cpp \ + datasource.cpp +HEADERS += datasource.h + diff --git a/examples/charts/qmloscilloscope/qml/qmloscilloscope/ControlPanel.qml b/examples/charts/qmloscilloscope/qml/qmloscilloscope/ControlPanel.qml index 478a2cc..ccf53fa 100644 --- a/examples/charts/qmloscilloscope/qml/qmloscilloscope/ControlPanel.qml +++ b/examples/charts/qmloscilloscope/qml/qmloscilloscope/ControlPanel.qml @@ -16,10 +16,11 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.1 import QtQuick.Layouts 1.0 ColumnLayout { + property alias openGLButton: openGLButton spacing: 8 Layout.fillHeight: true signal animationsEnabled(bool enabled) @@ -27,6 +28,7 @@ ColumnLayout { signal refreshRateChanged(variant rate); signal signalSourceChanged(string source, int signalCount, int sampleCount); signal antialiasingEnabled(bool enabled) + signal openGlChanged(bool enabled) Text { text: "Scope" @@ -35,6 +37,14 @@ ColumnLayout { } MultiButton { + id: openGLButton + text: "OpenGL: " + items: ["false", "true"] + currentSelection: 0 + onSelectionChanged: openGlChanged(currentSelection == 1); + } + + MultiButton { text: "Graph: " items: ["line", "spline", "scatter"] currentSelection: 0 diff --git a/examples/charts/qmloscilloscope/qml/qmloscilloscope/ScopeView.qml b/examples/charts/qmloscilloscope/qml/qmloscilloscope/ScopeView.qml index e422d05..443e515 100644 --- a/examples/charts/qmloscilloscope/qml/qmloscilloscope/ScopeView.qml +++ b/examples/charts/qmloscilloscope/qml/qmloscilloscope/ScopeView.qml @@ -17,13 +17,18 @@ ****************************************************************************/ import QtQuick 2.0 -import QtCharts 2.0 +import QtCharts 2.1 //![1] ChartView { id: chartView animationOptions: ChartView.NoAnimation theme: ChartView.ChartThemeDark + property bool openGL: false + onOpenGLChanged: { + series("signal 1").useOpenGL = openGL; + series("signal 2").useOpenGL = openGL; + } ValueAxis { id: axisY1 @@ -40,7 +45,7 @@ ChartView { ValueAxis { id: axisX min: 0 - max: 1000 + max: 1024 } LineSeries { @@ -48,12 +53,14 @@ ChartView { name: "signal 1" axisX: axisX axisY: axisY1 + useOpenGL: chartView.openGL } LineSeries { id: lineSeries2 name: "signal 2" axisX: axisX axisYRight: axisY2 + useOpenGL: chartView.openGL } //![1] @@ -78,18 +85,28 @@ ChartView { // but the series have their own y-axes to make it possible to control the y-offset // of the "signal sources". if (type == "line") { - chartView.createSeries(ChartView.SeriesTypeLine, "signal 1", axisX, axisY1); - chartView.createSeries(ChartView.SeriesTypeLine, "signal 2", axisX, axisY2); + var series1 = chartView.createSeries(ChartView.SeriesTypeLine, "signal 1", + axisX, axisY1); + series1.useOpenGL = chartView.openGL + + var series2 = chartView.createSeries(ChartView.SeriesTypeLine, "signal 2", + axisX, axisY2); + series2.useOpenGL = chartView.openGL } else if (type == "spline") { chartView.createSeries(ChartView.SeriesTypeSpline, "signal 1", axisX, axisY1); chartView.createSeries(ChartView.SeriesTypeSpline, "signal 2", axisX, axisY2); } else { - var series1 = chartView.createSeries(ChartView.SeriesTypeScatter, "signal 1", axisX, axisY1); + var series1 = chartView.createSeries(ChartView.SeriesTypeScatter, "signal 1", + axisX, axisY1); series1.markerSize = 3; series1.borderColor = "transparent"; - var series2 = chartView.createSeries(ChartView.SeriesTypeScatter, "signal 2", axisX, axisY2); + series1.useOpenGL = chartView.openGL + + var series2 = chartView.createSeries(ChartView.SeriesTypeScatter, "signal 2", + axisX, axisY2); series2.markerSize = 3; series2.borderColor = "transparent"; + series2.useOpenGL = chartView.openGL } } diff --git a/examples/charts/qmloscilloscope/qml/qmloscilloscope/main.qml b/examples/charts/qmloscilloscope/qml/qmloscilloscope/main.qml index 0ff40c2..5340177 100644 --- a/examples/charts/qmloscilloscope/qml/qmloscilloscope/main.qml +++ b/examples/charts/qmloscilloscope/qml/qmloscilloscope/main.qml @@ -21,8 +21,8 @@ import QtQuick 2.0 //![1] Rectangle { id: main - width: 400 - height: 300 + width: 600 + height: 400 color: "#404040" ControlPanel { @@ -39,11 +39,22 @@ Rectangle { dataSource.generateData(0, signalCount, sampleCount); else dataSource.generateData(1, signalCount, sampleCount); + scopeView.axisX().max = sampleCount; } onAnimationsEnabled: scopeView.setAnimations(enabled); - onSeriesTypeChanged: scopeView.changeSeriesType(type); + onSeriesTypeChanged: { + scopeView.changeSeriesType(type); + if (type === "spline") { + controlPanel.openGLButton.currentSelection = 0; + controlPanel.openGLButton.enabled = false; + scopeView.openGL = false; + } else { + controlPanel.openGLButton.enabled = true; + } + } onRefreshRateChanged: scopeView.changeRefreshRate(rate); onAntialiasingEnabled: scopeView.antialiasing = enabled; + onOpenGlChanged: scopeView.openGL = enabled; } //![2] @@ -56,4 +67,5 @@ Rectangle { height: main.height } //![2] + } diff --git a/src/charts/areachart/qareaseries.cpp b/src/charts/areachart/qareaseries.cpp index a33db92..c6dd9f4 100644 --- a/src/charts/areachart/qareaseries.cpp +++ b/src/charts/areachart/qareaseries.cpp @@ -345,7 +345,7 @@ QT_CHARTS_BEGIN_NAMESPACE \sa pointLabelsVisible */ /*! - \fn void QAreaSeries::pointLabelsClippintChanged(bool clipping) + \fn void QAreaSeries::pointLabelsClippingChanged(bool clipping) The clipping of the data point labels is changed to \a clipping. */ /*! @@ -395,8 +395,10 @@ QAbstractSeries::SeriesType QAreaSeries::type() const void QAreaSeries::setUpperSeries(QLineSeries *series) { Q_D(QAreaSeries); - if (d->m_upperSeries != series) + if (d->m_upperSeries != series) { + series->d_ptr->setBlockOpenGL(true); d->m_upperSeries = series; + } } QLineSeries *QAreaSeries::upperSeries() const @@ -411,6 +413,7 @@ QLineSeries *QAreaSeries::upperSeries() const void QAreaSeries::setLowerSeries(QLineSeries *series) { Q_D(QAreaSeries); + series->d_ptr->setBlockOpenGL(true); d->m_lowerSeries = series; } @@ -624,7 +627,7 @@ void QAreaSeriesPrivate::initializeDomain() QLineSeries *lowerSeries = q->lowerSeries(); if (upperSeries) { - const QList& points = upperSeries->points(); + const QVector &points = upperSeries->pointsVector(); for (int i = 0; i < points.count(); i++) { qreal x = points[i].x(); @@ -636,8 +639,7 @@ void QAreaSeriesPrivate::initializeDomain() } } if (lowerSeries) { - - const QList& points = lowerSeries->points(); + const QVector &points = lowerSeries->pointsVector(); for (int i = 0; i < points.count(); i++) { qreal x = points[i].x(); diff --git a/src/charts/axis/qabstractaxis.cpp b/src/charts/axis/qabstractaxis.cpp index e286ba5..1bbe63c 100644 --- a/src/charts/axis/qabstractaxis.cpp +++ b/src/charts/axis/qabstractaxis.cpp @@ -883,7 +883,7 @@ bool QAbstractAxis::isVisible() const } /*! - Sets axis, shades, labels and grid lines to be visible. + Sets axis, shades, labels and grid lines visibility to \a visible. */ void QAbstractAxis::setVisible(bool visible) { diff --git a/src/charts/barchart/qbarset.cpp b/src/charts/barchart/qbarset.cpp index 4eb1cdb..0ff859d 100644 --- a/src/charts/barchart/qbarset.cpp +++ b/src/charts/barchart/qbarset.cpp @@ -546,7 +546,7 @@ QColor QBarSet::borderColor() } /*! - Sets the color of pen for this bar set. + Sets the \a color of pen for this bar set. */ void QBarSet::setBorderColor(QColor color) { @@ -567,7 +567,7 @@ QColor QBarSet::labelColor() } /*! - Sets the color of labels for this bar set. + Sets the \a color of labels for this bar set. */ void QBarSet::setLabelColor(QColor color) { diff --git a/src/charts/chartdataset.cpp b/src/charts/chartdataset.cpp index 35361ff..1da28b9 100644 --- a/src/charts/chartdataset.cpp +++ b/src/charts/chartdataset.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #ifndef QT_ON_ARM #include @@ -47,7 +48,8 @@ QT_CHARTS_BEGIN_NAMESPACE ChartDataSet::ChartDataSet(QChart *chart) : QObject(chart), - m_chart(chart) + m_chart(chart), + m_glXYSeriesDataManager(new GLXYSeriesDataManager(this)) { } @@ -77,6 +79,8 @@ void ChartDataSet::addSeries(QAbstractSeries *series) qWarning() << QObject::tr("Can not add series. Series type is not supported by a polar chart."); return; } + // Disable OpenGL for series in polar charts + series->setUseOpenGL(false); series->d_ptr->setDomain(new XYPolarDomain()); // Set the correct domain for upper and lower series too if (series->type() == QAbstractSeries::SeriesTypeArea) { @@ -157,6 +161,10 @@ void ChartDataSet::removeSeries(QAbstractSeries *series) series->d_ptr->setDomain(new XYDomain()); series->setParent(0); series->d_ptr->m_chart = 0; + + QXYSeries *xySeries = qobject_cast(series); + if (xySeries) + m_glXYSeriesDataManager->removeSeries(xySeries); } /* diff --git a/src/charts/chartdataset_p.h b/src/charts/chartdataset_p.h index 3fd71a7..2cc8c2d 100644 --- a/src/charts/chartdataset_p.h +++ b/src/charts/chartdataset_p.h @@ -37,6 +37,7 @@ QT_CHARTS_BEGIN_NAMESPACE class QAbstractAxis; class ChartPresenter; +class GLXYSeriesDataManager; class QT_CHARTS_AUTOTEST_EXPORT ChartDataSet : public QObject { @@ -67,6 +68,8 @@ public: QPointF mapToValue(const QPointF &position, QAbstractSeries *series = 0); QPointF mapToPosition(const QPointF &value, QAbstractSeries *series = 0); + GLXYSeriesDataManager *glXYSeriesDataManager() { return m_glXYSeriesDataManager; } + Q_SIGNALS: void axisAdded(QAbstractAxis* axis); void axisRemoved(QAbstractAxis* axis); @@ -85,6 +88,7 @@ private: QList m_seriesList; QList m_axisList; QChart* m_chart; + GLXYSeriesDataManager *m_glXYSeriesDataManager; }; QT_CHARTS_END_NAMESPACE diff --git a/src/charts/chartelement.cpp b/src/charts/chartelement.cpp index 5ccf8d4..9e540cf 100644 --- a/src/charts/chartelement.cpp +++ b/src/charts/chartelement.cpp @@ -19,13 +19,15 @@ #include #include #include +#include QT_CHARTS_BEGIN_NAMESPACE ChartElement::ChartElement(QGraphicsItem* item): QGraphicsObject(item), m_presenter(0), - m_themeManager(0) + m_themeManager(0), + m_dataSet(0) { } @@ -50,4 +52,14 @@ ChartThemeManager* ChartElement::themeManager() const return m_themeManager; } +void ChartElement::setDataSet(ChartDataSet *dataSet) +{ + m_dataSet = dataSet; +} + +ChartDataSet *ChartElement::dataSet() const +{ + return m_dataSet; +} + QT_CHARTS_END_NAMESPACE diff --git a/src/charts/chartelement_p.h b/src/charts/chartelement_p.h index 60b2552..72b55e7 100644 --- a/src/charts/chartelement_p.h +++ b/src/charts/chartelement_p.h @@ -40,6 +40,7 @@ class ChartPresenter; class ChartAnimation; class ChartThemeManager; class AbstractDomain; +class ChartDataSet; class ChartElement: public QGraphicsObject { @@ -52,10 +53,13 @@ public: ChartPresenter *presenter() const; virtual void setThemeManager(ChartThemeManager *manager); ChartThemeManager* themeManager() const; + virtual void setDataSet(ChartDataSet *dataSet); + ChartDataSet *dataSet() const; private: ChartPresenter *m_presenter; ChartThemeManager *m_themeManager; + ChartDataSet *m_dataSet; }; QT_CHARTS_END_NAMESPACE diff --git a/src/charts/chartpresenter.cpp b/src/charts/chartpresenter.cpp index df66c01..6fc3503 100644 --- a/src/charts/chartpresenter.cpp +++ b/src/charts/chartpresenter.cpp @@ -46,6 +46,10 @@ ChartPresenter::ChartPresenter(QChart *chart, QChart::ChartType type) m_plotAreaBackground(0), m_title(0), m_localizeNumbers(false) +#ifndef QT_NO_OPENGL + , m_glWidget(0) + , m_glUseWidget(true) +#endif { if (type == QChart::ChartTypeCartesian) m_layout = new CartesianChartLayout(this); @@ -56,7 +60,9 @@ ChartPresenter::ChartPresenter(QChart *chart, QChart::ChartType type) ChartPresenter::~ChartPresenter() { - +#ifndef QT_NO_OPENGL + delete m_glWidget.data(); +#endif } void ChartPresenter::setGeometry(const QRectF rect) @@ -67,6 +73,10 @@ void ChartPresenter::setGeometry(const QRectF rect) chart->domain()->setSize(rect.size()); chart->setPos(rect.topLeft()); } +#ifndef QT_NO_OPENGL + if (!m_glWidget.isNull()) + m_glWidget->setGeometry(m_rect.toRect()); +#endif emit plotAreaChanged(m_rect); } } @@ -108,6 +118,7 @@ void ChartPresenter::handleSeriesAdded(QAbstractSeries *series) ChartItem *chart = series->d_ptr->chartItem(); chart->setPresenter(this); chart->setThemeManager(m_chart->d_ptr->m_themeManager); + chart->setDataSet(m_chart->d_ptr->m_dataset); chart->domain()->setSize(m_rect.size()); chart->setPos(m_rect.topLeft()); chart->handleDomainUpdated(); //this could be moved to intializeGraphics when animator is refactored @@ -531,6 +542,28 @@ QString ChartPresenter::numberToString(int value) return QString::number(value); } +void ChartPresenter::ensureGLWidget() +{ +#ifndef QT_NO_OPENGL + // GLWidget pointer is wrapped in QPointer as its parent is not in our control, and therefore + // can potentially get deleted unexpectedly. + if (m_glWidget.isNull() && m_glUseWidget && m_chart->scene()) { + QObject *parent = m_chart->scene()->parent(); + while (parent) { + QWidget *parentWidget = qobject_cast(parent); + if (parentWidget) { + m_glWidget = new GLWidget(m_chart->d_ptr->m_dataset->glXYSeriesDataManager(), + parentWidget); + m_glWidget->setGeometry(m_rect.toRect()); + m_glWidget->show(); + break; + } + parent = parent->parent(); + } + } +#endif +} + #include "moc_chartpresenter_p.cpp" QT_CHARTS_END_NAMESPACE diff --git a/src/charts/chartpresenter_p.h b/src/charts/chartpresenter_p.h index bf9d5a9..8dadbbc 100644 --- a/src/charts/chartpresenter_p.h +++ b/src/charts/chartpresenter_p.h @@ -30,9 +30,11 @@ #include #include //because of QChart::ChartThemeId +#include #include #include #include +#include QT_CHARTS_BEGIN_NAMESPACE @@ -160,6 +162,9 @@ public: QString numberToString(double value, char f = 'g', int prec = 6); QString numberToString(int value); + void ensureGLWidget(); + void glSetUseWidget(bool enable) { m_glUseWidget = enable; } + private: void createBackgroundItem(); void createPlotAreaBackgroundItem(); @@ -192,6 +197,10 @@ private: QRectF m_rect; bool m_localizeNumbers; QLocale m_locale; +#ifndef QT_NO_OPENGL + QPointer m_glWidget; +#endif + bool m_glUseWidget; }; QT_CHARTS_END_NAMESPACE diff --git a/src/charts/charts.pro b/src/charts/charts.pro index a89b1dd..2ec6ce0 100644 --- a/src/charts/charts.pro +++ b/src/charts/charts.pro @@ -32,6 +32,9 @@ SOURCES += \ $$PWD/scroller.cpp \ $$PWD/charttitle.cpp \ $$PWD/qpolarchart.cpp + +contains(QT_CONFIG, opengl): SOURCES += $$PWD/glwidget.cpp + PRIVATE_HEADERS += \ $$PWD/chartdataset_p.h \ $$PWD/chartitem_p.h \ @@ -46,6 +49,9 @@ PRIVATE_HEADERS += \ $$PWD/qabstractseries_p.h \ $$PWD/charttitle_p.h \ $$PWD/charthelpers_p.h + +contains(QT_CONFIG, opengl): PRIVATE_HEADERS += $$PWD/glwidget_p.h + PUBLIC_HEADERS += \ $$PWD/qchart.h \ $$PWD/qchartglobal.h \ diff --git a/src/charts/doc/images/examples_openglseries.png b/src/charts/doc/images/examples_openglseries.png new file mode 100644 index 0000000000000000000000000000000000000000..56719a975796895211ac734f331e995d05edad4b GIT binary patch literal 65565 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�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 JT<89?{{fTq1YZCE literal 0 Hc$@ calculateGeometryPoints(const QList &vector) const = 0; + virtual QVector calculateGeometryPoints(const QVector &vector) const = 0; virtual bool attachAxis(QAbstractAxis *axis); virtual bool detachAxis(QAbstractAxis *axis); diff --git a/src/charts/domain/logxlogydomain.cpp b/src/charts/domain/logxlogydomain.cpp index 292b259..665a13f 100644 --- a/src/charts/domain/logxlogydomain.cpp +++ b/src/charts/domain/logxlogydomain.cpp @@ -162,7 +162,7 @@ QPointF LogXLogYDomain::calculateGeometryPoint(const QPointF &point, bool &ok) c return QPointF(x, y); } -QVector LogXLogYDomain::calculateGeometryPoints(const QList &vector) const +QVector LogXLogYDomain::calculateGeometryPoints(const QVector &vector) const { const qreal deltaX = m_size.width() / qAbs(m_logRightX - m_logLeftX); const qreal deltaY = m_size.height() / qAbs(m_logRightY - m_logLeftY); diff --git a/src/charts/domain/logxlogydomain_p.h b/src/charts/domain/logxlogydomain_p.h index ed8fb26..11bc601 100644 --- a/src/charts/domain/logxlogydomain_p.h +++ b/src/charts/domain/logxlogydomain_p.h @@ -54,7 +54,7 @@ public: QPointF calculateGeometryPoint(const QPointF &point, bool &ok) const; QPointF calculateDomainPoint(const QPointF &point) const; - QVector calculateGeometryPoints(const QList &vector) const; + QVector calculateGeometryPoints(const QVector &vector) const; bool attachAxis(QAbstractAxis *axis); bool detachAxis(QAbstractAxis *axis); diff --git a/src/charts/domain/logxydomain.cpp b/src/charts/domain/logxydomain.cpp index 00e8b1c..1c30607 100644 --- a/src/charts/domain/logxydomain.cpp +++ b/src/charts/domain/logxydomain.cpp @@ -146,7 +146,7 @@ QPointF LogXYDomain::calculateGeometryPoint(const QPointF &point, bool &ok) cons return QPointF(x, y); } -QVector LogXYDomain::calculateGeometryPoints(const QList &vector) const +QVector LogXYDomain::calculateGeometryPoints(const QVector &vector) const { const qreal deltaX = m_size.width() / (m_logRightX - m_logLeftX); const qreal deltaY = m_size.height() / (m_maxY - m_minY); diff --git a/src/charts/domain/logxydomain_p.h b/src/charts/domain/logxydomain_p.h index 72c89b1..5ff8b0c 100644 --- a/src/charts/domain/logxydomain_p.h +++ b/src/charts/domain/logxydomain_p.h @@ -54,7 +54,7 @@ public: QPointF calculateGeometryPoint(const QPointF &point, bool &ok) const; QPointF calculateDomainPoint(const QPointF &point) const; - QVector calculateGeometryPoints(const QList &vector) const; + QVector calculateGeometryPoints(const QVector &vector) const; bool attachAxis(QAbstractAxis *axis); bool detachAxis(QAbstractAxis *axis); diff --git a/src/charts/domain/polardomain.cpp b/src/charts/domain/polardomain.cpp index 4681f6c..55fedf0 100644 --- a/src/charts/domain/polardomain.cpp +++ b/src/charts/domain/polardomain.cpp @@ -53,7 +53,7 @@ QPointF PolarDomain::calculateGeometryPoint(const QPointF &point, bool &ok) cons } } -QVector PolarDomain::calculateGeometryPoints(const QList &vector) const +QVector PolarDomain::calculateGeometryPoints(const QVector &vector) const { QVector result; result.resize(vector.count()); diff --git a/src/charts/domain/polardomain_p.h b/src/charts/domain/polardomain_p.h index 6ed5b85..e179a8a 100644 --- a/src/charts/domain/polardomain_p.h +++ b/src/charts/domain/polardomain_p.h @@ -43,7 +43,7 @@ public: void setSize(const QSizeF &size); QPointF calculateGeometryPoint(const QPointF &point, bool &ok) const; - QVector calculateGeometryPoints(const QList &vector) const; + QVector calculateGeometryPoints(const QVector &vector) const; virtual qreal toAngularCoordinate(qreal value, bool &ok) const = 0; virtual qreal toRadialCoordinate(qreal value, bool &ok) const = 0; diff --git a/src/charts/domain/xlogydomain.cpp b/src/charts/domain/xlogydomain.cpp index bc9d367..ca4986d 100644 --- a/src/charts/domain/xlogydomain.cpp +++ b/src/charts/domain/xlogydomain.cpp @@ -146,7 +146,7 @@ QPointF XLogYDomain::calculateGeometryPoint(const QPointF &point, bool &ok) cons return QPointF(x, y); } -QVector XLogYDomain::calculateGeometryPoints(const QList &vector) const +QVector XLogYDomain::calculateGeometryPoints(const QVector &vector) const { const qreal deltaX = m_size.width() / (m_maxX - m_minX); const qreal deltaY = m_size.height() / qAbs(m_logRightY - m_logLeftY); diff --git a/src/charts/domain/xlogydomain_p.h b/src/charts/domain/xlogydomain_p.h index 5dbd2a4..c49f7bb 100644 --- a/src/charts/domain/xlogydomain_p.h +++ b/src/charts/domain/xlogydomain_p.h @@ -54,7 +54,7 @@ public: QPointF calculateGeometryPoint(const QPointF &point, bool &ok) const; QPointF calculateDomainPoint(const QPointF &point) const; - QVector calculateGeometryPoints(const QList &vector) const; + QVector calculateGeometryPoints(const QVector &vector) const; bool attachAxis(QAbstractAxis *axis); bool detachAxis(QAbstractAxis *axis); diff --git a/src/charts/domain/xydomain.cpp b/src/charts/domain/xydomain.cpp index cad9dce..da20590 100644 --- a/src/charts/domain/xydomain.cpp +++ b/src/charts/domain/xydomain.cpp @@ -144,7 +144,7 @@ QPointF XYDomain::calculateGeometryPoint(const QPointF &point, bool &ok) const return QPointF(x, y); } -QVector XYDomain::calculateGeometryPoints(const QList &vector) const +QVector XYDomain::calculateGeometryPoints(const QVector &vector) const { const qreal deltaX = m_size.width() / (m_maxX - m_minX); const qreal deltaY = m_size.height() / (m_maxY - m_minY); diff --git a/src/charts/domain/xydomain_p.h b/src/charts/domain/xydomain_p.h index 735fc0f..3ba8e1f 100644 --- a/src/charts/domain/xydomain_p.h +++ b/src/charts/domain/xydomain_p.h @@ -54,7 +54,7 @@ public: QPointF calculateGeometryPoint(const QPointF &point, bool &ok) const; QPointF calculateDomainPoint(const QPointF &point) const; - QVector calculateGeometryPoints(const QList &vector) const; + QVector calculateGeometryPoints(const QVector &vector) const; }; QT_CHARTS_END_NAMESPACE diff --git a/src/charts/glwidget.cpp b/src/charts/glwidget.cpp new file mode 100644 index 0000000..a447048 --- /dev/null +++ b/src/charts/glwidget.cpp @@ -0,0 +1,222 @@ +/**************************************************************************** + ** + ** Copyright (C) 2015 The Qt Company Ltd + ** All rights reserved. + ** For any questions to The Qt Company, please use contact form at http://qt.io + ** + ** This file is part of the Qt Charts module. + ** + ** Licensees holding valid commercial license for Qt may use this file in + ** accordance with the Qt License Agreement provided with the Software + ** or, alternatively, in accordance with the terms contained in a written + ** agreement between you and The Qt Company. + ** + ** If you have questions regarding the use of this file, please use + ** contact form at http://qt.io + ** + ****************************************************************************/ + +#ifndef QT_NO_OPENGL + +#include "private/glwidget_p.h" +#include "private/glxyseriesdata_p.h" +#include +#include +#include + +//#define QDEBUG_TRACE_GL_FPS +#ifdef QDEBUG_TRACE_GL_FPS +# include +#endif + +QT_CHARTS_BEGIN_NAMESPACE + +GLWidget::GLWidget(GLXYSeriesDataManager *xyDataManager, QWidget *parent) + : QOpenGLWidget(parent), + m_program(0), + m_shaderAttribLoc(-1), + m_colorUniformLoc(-1), + m_minUniformLoc(-1), + m_deltaUniformLoc(-1), + m_pointSizeUniformLoc(-1), + m_xyDataManager(xyDataManager) +{ + setAttribute(Qt::WA_TranslucentBackground); + setAttribute(Qt::WA_AlwaysStackOnTop); + setAttribute(Qt::WA_TransparentForMouseEvents); + + QSurfaceFormat surfaceFormat; + surfaceFormat.setDepthBufferSize(0); + surfaceFormat.setStencilBufferSize(0); + surfaceFormat.setRedBufferSize(8); + surfaceFormat.setGreenBufferSize(8); + surfaceFormat.setBlueBufferSize(8); + surfaceFormat.setAlphaBufferSize(8); + surfaceFormat.setSwapBehavior(QSurfaceFormat::DoubleBuffer); + surfaceFormat.setRenderableType(QSurfaceFormat::DefaultRenderableType); + setFormat(surfaceFormat); + + connect(xyDataManager, &GLXYSeriesDataManager::seriesRemoved, + this, &GLWidget::cleanXYSeriesResources); +} + +GLWidget::~GLWidget() +{ + cleanup(); +} + +void GLWidget::cleanup() +{ + makeCurrent(); + + delete m_program; + m_program = 0; + + foreach (QOpenGLBuffer *buffer, m_seriesBufferMap.values()) + delete buffer; + m_seriesBufferMap.clear(); + + doneCurrent(); +} + +void GLWidget::cleanXYSeriesResources(const QXYSeries *series) +{ + makeCurrent(); + if (series) { + delete m_seriesBufferMap.take(series); + } else { + // Null series means all series were removed + foreach (QOpenGLBuffer *buffer, m_seriesBufferMap.values()) + delete buffer; + m_seriesBufferMap.clear(); + } + doneCurrent(); +} + +static const char *vertexSource = + "attribute highp vec2 points;\n" + "uniform highp vec2 min;\n" + "uniform highp vec2 delta;\n" + "uniform highp float pointSize;\n" + "void main() {\n" + " vec2 normalPoint = vec2(-1, -1) + ((points - min) / delta);\n" + " gl_Position = 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"; + +void GLWidget::initializeGL() +{ + connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, &GLWidget::cleanup); + + initializeOpenGLFunctions(); + glClearColor(0, 0, 0, 0); + + m_program = new QOpenGLShaderProgram; + 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"); + + + // 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); + + glEnableVertexAttribArray(0); + + glDisable(GL_DEPTH_TEST); + glDisable(GL_STENCIL_TEST); + +#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. + glEnable(GL_PROGRAM_POINT_SIZE); + } +#endif + + m_program->release(); +} + +void GLWidget::paintGL() +{ + glClear(GL_COLOR_BUFFER_BIT); + + QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); + m_program->bind(); + + GLXYDataMapIterator i(m_xyDataManager->dataMap()); + while (i.hasNext()) { + i.next(); + QOpenGLBuffer *vbo = m_seriesBufferMap.value(i.key()); + GLXYSeriesData *data = i.value(); + + m_program->setUniformValue(m_colorUniformLoc, data->color); + m_program->setUniformValue(m_minUniformLoc, data->min); + m_program->setUniformValue(m_deltaUniformLoc, data->delta); + + 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(); + } + +#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 + + m_program->release(); +} + +void GLWidget::resizeGL(int w, int h) +{ + Q_UNUSED(w) + Q_UNUSED(h) +} + +QT_CHARTS_END_NAMESPACE + +#endif diff --git a/src/charts/glwidget_p.h b/src/charts/glwidget_p.h new file mode 100644 index 0000000..209c634 --- /dev/null +++ b/src/charts/glwidget_p.h @@ -0,0 +1,80 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd +** All rights reserved. +** For any questions to The Qt Company, please use contact form at http://qt.io +** +** This file is part of the Qt Charts module. +** +** Licensees holding valid commercial license for Qt may use this file in +** accordance with the Qt License Agreement provided with the Software +** or, alternatively, in accordance with the terms contained in a written +** agreement between you and The Qt Company. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.io +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the Qt Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef GLWIDGET_H +#define GLWIDGET_H + +#ifndef QT_NO_OPENGL + +#include +#include +#include +#include +#include +#include + +QT_FORWARD_DECLARE_CLASS(QOpenGLShaderProgram) + +class QOpenGLBuffer; + +QT_CHARTS_BEGIN_NAMESPACE + +class GLXYSeriesDataManager; + +class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions +{ + Q_OBJECT + +public: + GLWidget(GLXYSeriesDataManager *xyDataManager, QWidget *parent = 0); + ~GLWidget(); + +public Q_SLOTS: + void cleanup(); + void cleanXYSeriesResources(const QXYSeries *series); + +protected: + void initializeGL() Q_DECL_OVERRIDE; + void paintGL() Q_DECL_OVERRIDE; + void resizeGL(int width, int height) Q_DECL_OVERRIDE; + +private: + QOpenGLShaderProgram *m_program; + int m_shaderAttribLoc; + int m_colorUniformLoc; + int m_minUniformLoc; + int m_deltaUniformLoc; + int m_pointSizeUniformLoc; + QOpenGLVertexArrayObject m_vao; + + QHash m_seriesBufferMap; + GLXYSeriesDataManager *m_xyDataManager; +}; + +QT_CHARTS_END_NAMESPACE +#endif +#endif diff --git a/src/charts/linechart/linechartitem.cpp b/src/charts/linechart/linechartitem.cpp index 84d9e1e..f0b0a57 100644 --- a/src/charts/linechart/linechartitem.cpp +++ b/src/charts/linechart/linechartitem.cpp @@ -70,6 +70,17 @@ QPainterPath LineChartItem::shape() const void LineChartItem::updateGeometry() { + static const QRectF dummyRect = QRectF(0.0, 0.0, 0.001, 0.001); + if (m_series->useOpenGL()) { + // Fake a miniscule region, so we trigger changed signal. + if (m_rect.width() != dummyRect.width()) { + prepareGeometryChange(); + m_rect = dummyRect; + } + update(); + return; + } + // Store the points to a local variable so that the old line gets properly cleared // when animation starts. m_linePoints = geometryPoints(); @@ -345,6 +356,9 @@ void LineChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opt Q_UNUSED(widget) Q_UNUSED(option) + if (m_series->useOpenGL()) + return; + QRectF clipRect = QRectF(QPointF(0, 0), domain()->size()); painter->save(); diff --git a/src/charts/qabstractseries.cpp b/src/charts/qabstractseries.cpp index cb3c7c5..8ce0c9d 100644 --- a/src/charts/qabstractseries.cpp +++ b/src/charts/qabstractseries.cpp @@ -135,6 +135,69 @@ QT_CHARTS_BEGIN_NAMESPACE */ /*! + \property QAbstractSeries::useOpenGL + \brief Specifies whether or not the series drawing is accelerated with OpenGL. + + Drawing series with OpenGL is supported only for QLineSeries and QScatterSeries. + Line series used as edge series for a QAreaSeries cannot use OpenGL acceleration. + When a chart contains any series that are drawn with OpenGL, a transparent QOpenGLWidget + is created on top of the chart plot area. Specified series are not drawn on the underlying + QGraphicsView, but are instead drawn on the created QOpenGLWidget. + + Performance gained from using OpenGL to accelerate series drawing depends on the underlying + hardware, but in most cases it is significant. For example, on a standard desktop computer, + enabling OpenGL acceleration for a series typically allows rendering at least hundred times + more points without reduction on the frame rate. + Chart size also has less effect on the frame rate. + + The OpenGL acceleration of series drawing is meant for use cases that need fast drawing of + large numbers of points. It is optimized for efficiency, and therefore the series using + it lack support for some features available to non-accelerated series. + + There are the following restrictions imposed on charts and series when using OpenGL + acceleration: + + \list + \li Series animations are not supported for accelerated series. + \li Antialiasing is not supported for accelerated series. + \li Pen styles and marker shapes are ignored for accelerated series. + Only solid lines and plain scatter dots are supported. + The scatter dots may be circular or rectangular, depending on the underlying graphics + hardware and drivers. + \li Polar charts are not supported for accelerated series. + \li Since the accelerated series are drawn on top of the entire graphics view, they get drawn + on top of any other graphics items that you may have on top chart in the same scene. + \li To enable QOpenGLWidget to be partially transparent, it needs to be stacked on top of + all other widgets. This means you cannot have other widgets partially covering the + chart. + \endlist + + The default value is \c{false}. +*/ +/*! + \qmlproperty bool AbstractSeries::useOpenGL + Specifies whether or not the series is drawn with OpenGL. + + Drawing series with OpenGL is supported only for LineSeries and ScatterSeries. + + For more details, see QAbstractSeries::useOpenGL documentation. QML applications have similar + restrictions as those listed in QAbstractSeries::useOpenGL documentation, + except there is no restriction about covering the ChartView partially with other + items due to different rendering mechanism. + + The default value is \c{false}. +*/ + +/*! + \fn void QAbstractSeries::useOpenGLChanged() + Emitted when the useOpenGL property value changes. +*/ +/*! + \qmlsignal AbstractSeries::onUseOpenGLChanged() + Emitted when the useOpenGL property value changes. +*/ + +/*! \internal \brief Constructs QAbstractSeries object with \a parent. */ @@ -192,6 +255,28 @@ void QAbstractSeries::setOpacity(qreal opacity) } } +void QAbstractSeries::setUseOpenGL(bool enable) +{ +#ifdef QT_NO_OPENGL + Q_UNUSED(enable) +#else + bool polarChart = d_ptr->m_chart && d_ptr->m_chart->chartType() == QChart::ChartTypePolar; + bool supportedSeries = (type() == SeriesTypeLine || type() == SeriesTypeScatter); + if ((!enable || !d_ptr->m_blockOpenGL) + && supportedSeries + && enable != d_ptr->m_useOpenGL + && (!enable || !polarChart)) { + d_ptr->m_useOpenGL = enable; + emit useOpenGLChanged(); + } +#endif +} + +bool QAbstractSeries::useOpenGL() const +{ + return d_ptr->m_useOpenGL; +} + /*! \brief Returns the chart where series belongs to. @@ -274,7 +359,9 @@ QAbstractSeriesPrivate::QAbstractSeriesPrivate(QAbstractSeries *q) m_item(0), m_domain(new XYDomain()), m_visible(true), - m_opacity(1.0) + m_opacity(1.0), + m_useOpenGL(false), + m_blockOpenGL(false) { } @@ -354,6 +441,15 @@ bool QAbstractSeriesPrivate::reverseYAxis() return reverseYAxis; } +// This function can be used to explicitly block OpenGL use from some otherwise supported series, +// such as the line series used as edge series of an area series. +void QAbstractSeriesPrivate::setBlockOpenGL(bool enable) +{ + m_blockOpenGL = enable; + if (enable) + q_ptr->setUseOpenGL(false); +} + #include "moc_qabstractseries.cpp" #include "moc_qabstractseries_p.cpp" diff --git a/src/charts/qabstractseries.h b/src/charts/qabstractseries.h index c861068..3e34591 100644 --- a/src/charts/qabstractseries.h +++ b/src/charts/qabstractseries.h @@ -36,6 +36,7 @@ class QT_CHARTS_EXPORT QAbstractSeries : public QObject Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged) Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity NOTIFY opacityChanged) Q_PROPERTY(SeriesType type READ type) + Q_PROPERTY(bool useOpenGL READ useOpenGL WRITE setUseOpenGL NOTIFY useOpenGLChanged) Q_ENUMS(SeriesType) public: @@ -67,6 +68,8 @@ public: bool isVisible() const; qreal opacity() const; void setOpacity(qreal opacity); + void setUseOpenGL(bool enable = true); + bool useOpenGL() const; QChart *chart() const; @@ -81,6 +84,7 @@ Q_SIGNALS: void nameChanged(); void visibleChanged(); void opacityChanged(); + void useOpenGLChanged(); protected: QScopedPointer d_ptr; @@ -89,6 +93,7 @@ protected: friend class ChartThemeManager; friend class QLegendPrivate; friend class DeclarativeChart; + friend class QAreaSeries; }; QT_CHARTS_END_NAMESPACE diff --git a/src/charts/qabstractseries_p.h b/src/charts/qabstractseries_p.h index 7e481f4..7120c0d 100644 --- a/src/charts/qabstractseries_p.h +++ b/src/charts/qabstractseries_p.h @@ -81,6 +81,8 @@ public: bool reverseXAxis(); bool reverseYAxis(); + void setBlockOpenGL(bool enable); + Q_SIGNALS: void countChanged(); @@ -96,6 +98,8 @@ private: bool m_visible; qreal m_opacity; ChartPresenter *m_presenter; + bool m_useOpenGL; + bool m_blockOpenGL; friend class QAbstractSeries; friend class ChartDataSet; diff --git a/src/charts/scatterchart/scatterchartitem.cpp b/src/charts/scatterchart/scatterchartitem.cpp index ac0611e..e8e4fe4 100644 --- a/src/charts/scatterchart/scatterchartitem.cpp +++ b/src/charts/scatterchart/scatterchartitem.cpp @@ -130,6 +130,18 @@ void ScatterChartItem::markerDoubleClicked(QGraphicsItem *marker) void ScatterChartItem::updateGeometry() { + static const QRectF dummyRect = QRectF(0.0, 0.0, 0.001, 0.001); + if (m_series->useOpenGL()) { + if (m_items.childItems().count()) + deletePoints(m_items.childItems().count()); + // Fake a miniscule region, so we trigger changed signal. + if (m_rect.width() != dummyRect.width()) { + prepareGeometryChange(); + m_rect = dummyRect; + } + update(); + return; + } const QVector& points = geometryPoints(); @@ -199,6 +211,9 @@ void ScatterChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * Q_UNUSED(option) Q_UNUSED(widget) + if (m_series->useOpenGL()) + return; + QRectF clipRect = QRectF(QPointF(0, 0), domain()->size()); painter->save(); diff --git a/src/charts/xychart/glxyseriesdata.cpp b/src/charts/xychart/glxyseriesdata.cpp new file mode 100644 index 0000000..2dafafd --- /dev/null +++ b/src/charts/xychart/glxyseriesdata.cpp @@ -0,0 +1,147 @@ +/**************************************************************************** + ** + ** Copyright (C) 2015 The Qt Company Ltd + ** All rights reserved. + ** For any questions to The Qt Company, please use contact form at http://qt.io + ** + ** This file is part of the Qt Charts module. + ** + ** Licensees holding valid commercial license for Qt may use this file in + ** accordance with the Qt License Agreement provided with the Software + ** or, alternatively, in accordance with the terms contained in a written + ** agreement between you and The Qt Company. + ** + ** If you have questions regarding the use of this file, please use + ** contact form at http://qt.io + ** + ****************************************************************************/ + +#include "private/glxyseriesdata_p.h" +#include "private/abstractdomain_p.h" +#include + +QT_CHARTS_BEGIN_NAMESPACE + +GLXYSeriesDataManager::GLXYSeriesDataManager(QObject *parent) + : QObject(parent), + m_mapDirty(false) +{ +} + +GLXYSeriesDataManager::~GLXYSeriesDataManager() +{ + cleanup(); +} + +void GLXYSeriesDataManager::setPoints(QXYSeries *series, const AbstractDomain *domain) +{ + GLXYSeriesData *data = m_seriesDataMap.value(series); + if (!data) { + data = new GLXYSeriesData; + data->type = series->type(); + QColor sc; + if (data->type == QAbstractSeries::SeriesTypeScatter) { + QScatterSeries *scatter = static_cast(series); + data->width = float(scatter->markerSize()); + sc = scatter->color(); // Scatter overwrites color property + } else { + data->width = float(series->pen().widthF()); + sc = series->color(); + } + data->color = QVector3D(float(sc.redF()), float(sc.greenF()), float(sc.blueF())); + connect(series, &QXYSeries::penChanged, this, + &GLXYSeriesDataManager::handleSeriesPenChange); + connect(series, &QXYSeries::useOpenGLChanged, this, + &GLXYSeriesDataManager::handleSeriesOpenGLChange); + m_seriesDataMap.insert(series, data); + m_mapDirty = true; + } + QVector &array = data->array; + + bool logAxis = false; + foreach (QAbstractAxis* axis, series->attachedAxes()) { + if (axis->type() == QAbstractAxis::AxisTypeLogValue) { + logAxis = true; + break; + } + } + + int count = series->count(); + int index = 0; + array.resize(count * 2); + if (logAxis) { + // Use domain to resolve geometry points. Not as fast as shaders, but simpler that way + QVector geometryPoints = domain->calculateGeometryPoints(series->pointsVector()); + const float height = domain->size().height(); + if (geometryPoints.size()) { + for (int i = 0; i < count; i++) { + const QPointF &point = geometryPoints.at(i); + array[index++] = float(point.x()); + array[index++] = float(height - point.y()); + } + } else { + // If there are invalid log values, geometry points generation fails + for (int i = 0; i < count; i++) { + array[index++] = 0.0f; + array[index++] = 0.0f; + } + } + data->min = QVector2D(0, 0); + data->delta = QVector2D(domain->size().width() / 2.0f, domain->size().height() / 2.0f); + } else { + // Regular value axes, so we can do the math easily on shaders. + QVector seriesPoints = series->pointsVector(); + for (int i = 0; i < count; i++) { + const QPointF &point = seriesPoints.at(i); + array[index++] = float(point.x()); + array[index++] = float(point.y()); + } + data->min = QVector2D(domain->minX(), domain->minY()); + data->delta = QVector2D((domain->maxX() - domain->minX()) / 2.0f, + (domain->maxY() - domain->minY()) / 2.0f); + } + data->dirty = true; +} + +void GLXYSeriesDataManager::removeSeries(const QXYSeries *series) +{ + GLXYSeriesData *data = m_seriesDataMap.take(series); + if (data) { + disconnect(series, 0, this, 0); + delete data; + emit seriesRemoved(series); + m_mapDirty = true; + } +} + +void GLXYSeriesDataManager::cleanup() +{ + foreach (GLXYSeriesData *data, m_seriesDataMap.values()) + delete data; + m_seriesDataMap.clear(); + m_mapDirty = true; + // Signal all series removal by using zero as parameter + emit seriesRemoved(0); +} + +void GLXYSeriesDataManager::handleSeriesPenChange() +{ + QXYSeries *series = qobject_cast(sender()); + if (series) { + GLXYSeriesData *data = m_seriesDataMap.value(series); + if (data) { + QColor sc = series->color(); + data->color = QVector3D(float(sc.redF()), float(sc.greenF()), float(sc.blueF())); + data->width = float(series->pen().widthF()); + } + } +} + +void GLXYSeriesDataManager::handleSeriesOpenGLChange() +{ + QXYSeries *series = qobject_cast(sender()); + if (!series->useOpenGL()) + removeSeries(series); +} + +QT_CHARTS_END_NAMESPACE diff --git a/src/charts/xychart/glxyseriesdata_p.h b/src/charts/xychart/glxyseriesdata_p.h new file mode 100644 index 0000000..0f59eff --- /dev/null +++ b/src/charts/xychart/glxyseriesdata_p.h @@ -0,0 +1,91 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd +** All rights reserved. +** For any questions to The Qt Company, please use contact form at http://qt.io +** +** This file is part of the Qt Charts module. +** +** Licensees holding valid commercial license for Qt may use this file in +** accordance with the Qt License Agreement provided with the Software +** or, alternatively, in accordance with the terms contained in a written +** agreement between you and The Qt Company. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.io +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the Qt Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef GLXYSERIESDATA_H +#define GLXYSERIESDATA_H + +#include +#include +#include +#include +#include + +QT_CHARTS_BEGIN_NAMESPACE + +class AbstractDomain; + +struct GLXYSeriesData { + QVector array; + bool dirty; + QVector3D color; + float width; + QAbstractSeries::SeriesType type; + QVector2D min; + QVector2D delta; +}; + +typedef QMap GLXYDataMap; +typedef QMapIterator GLXYDataMapIterator; + +class GLXYSeriesDataManager : public QObject +{ + Q_OBJECT + +public: + GLXYSeriesDataManager(QObject *parent = 0); + ~GLXYSeriesDataManager(); + + void setPoints(QXYSeries *series, const AbstractDomain *domain); + + void removeSeries(const QXYSeries *series); + + GLXYDataMap &dataMap() { return m_seriesDataMap; } + + // These functions are needed by qml side, so they must be inline + bool mapDirty() const { return m_mapDirty; } + void clearAllDirty() { + m_mapDirty = false; + foreach (GLXYSeriesData *data, m_seriesDataMap.values()) + data->dirty = false; + } + +public Q_SLOTS: + void cleanup(); + void handleSeriesPenChange(); + void handleSeriesOpenGLChange(); + +Q_SIGNALS: + void seriesRemoved(const QXYSeries *series); + +private: + GLXYDataMap m_seriesDataMap; + bool m_mapDirty; +}; + +QT_CHARTS_END_NAMESPACE + +#endif diff --git a/src/charts/xychart/qxyseries.cpp b/src/charts/xychart/qxyseries.cpp index a88c5f8..681781e 100644 --- a/src/charts/xychart/qxyseries.cpp +++ b/src/charts/xychart/qxyseries.cpp @@ -238,7 +238,7 @@ QT_CHARTS_BEGIN_NAMESPACE \sa pointLabelsVisible */ /*! - \fn void QXYSeries::pointLabelsClippintChanged(bool clipping) + \fn void QXYSeries::pointLabelsClippingChanged(bool clipping) The clipping of the data point labels is changed to \a clipping. */ /*! @@ -398,6 +398,11 @@ QT_CHARTS_BEGIN_NAMESPACE */ /*! + \fn void QXYSeries::penChanged(const QPen &pen) + \brief Signal is emitted when the line pen has changed to \a pen. +*/ + +/*! \fn void QXYSeriesPrivate::updated() \brief \internal */ @@ -539,7 +544,7 @@ void QXYSeries::replace(int index, const QPointF &newPoint) \note This is much faster than replacing data points one by one, or first clearing all data, and then appending the new data. Emits QXYSeries::pointsReplaced() when the points have been replaced. However, note that using the overload that takes - \c{QVector} as parameter is slightly faster than using this overload. + \c{QVector} as parameter is faster than using this overload. \sa pointsReplaced() */ void QXYSeries::replace(QList points) @@ -634,7 +639,8 @@ void QXYSeries::clear() } /*! - Returns list of points in the series. + Returns the points in the series as a list. + Use QXYSeries::pointsVector() for better performance. */ QList QXYSeries::points() const { @@ -643,6 +649,16 @@ QList QXYSeries::points() const } /*! + Returns the points in the series as a vector. + This is more efficient that calling QXYSeries::points(); +*/ +QVector QXYSeries::pointsVector() const +{ + Q_D(const QXYSeries); + return d->m_points; +} + +/*! Returns point at \a index in internal points vector. */ const QPointF &QXYSeries::at(int index) const @@ -675,6 +691,7 @@ void QXYSeries::setPen(const QPen &pen) emit d->updated(); if (emitColorChanged) emit colorChanged(pen.color()); + emit penChanged(pen); } } @@ -864,7 +881,7 @@ void QXYSeriesPrivate::initializeDomain() Q_Q(QXYSeries); - const QList& points = q->points(); + const QVector &points = q->pointsVector(); if (!points.isEmpty()) { minX = points[0].x(); diff --git a/src/charts/xychart/qxyseries.h b/src/charts/xychart/qxyseries.h index 6a211fb..25f52ea 100644 --- a/src/charts/xychart/qxyseries.h +++ b/src/charts/xychart/qxyseries.h @@ -65,6 +65,7 @@ public: int count() const; QList points() const; + QVector pointsVector() const; const QPointF &at(int index) const; QXYSeries &operator << (const QPointF &point); @@ -117,6 +118,7 @@ Q_SIGNALS: void pointLabelsColorChanged(const QColor &color); void pointLabelsClippingChanged(bool clipping); void pointsRemoved(int index, int count); + void penChanged(const QPen &pen); private: Q_DECLARE_PRIVATE(QXYSeries) diff --git a/src/charts/xychart/xychart.cpp b/src/charts/xychart/xychart.cpp index 16dd707..e7d3fdd 100644 --- a/src/charts/xychart/xychart.cpp +++ b/src/charts/xychart/xychart.cpp @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include #include #include @@ -106,6 +108,13 @@ void XYChart::updateChart(QVector &oldPoints, QVector &newPoin } } +void XYChart::updateGlChart() +{ + presenter()->ensureGLWidget(); + dataSet()->glXYSeriesDataManager()->setPoints(m_series, domain()); + updateGeometry(); +} + //handlers void XYChart::handlePointAdded(int index) @@ -113,20 +122,23 @@ void XYChart::handlePointAdded(int index) Q_ASSERT(index < m_series->count()); Q_ASSERT(index >= 0); - QVector points; - - if (m_dirty || m_points.isEmpty()) { - points = domain()->calculateGeometryPoints(m_series->points()); + if (m_series->useOpenGL()) { + updateGlChart(); } else { - points = m_points; - QPointF point = domain()->calculateGeometryPoint(m_series->points()[index], m_validData); - if (!m_validData) - m_points.clear(); - else - points.insert(index, point); + QVector points; + if (m_dirty || m_points.isEmpty()) { + points = domain()->calculateGeometryPoints(m_series->pointsVector()); + } else { + points = m_points; + QPointF point = domain()->calculateGeometryPoint(m_series->pointsVector().at(index), + m_validData); + if (!m_validData) + m_points.clear(); + else + points.insert(index, point); + } + updateChart(m_points, points, index); } - - updateChart(m_points, points, index); } void XYChart::handlePointRemoved(int index) @@ -134,16 +146,18 @@ void XYChart::handlePointRemoved(int index) Q_ASSERT(index <= m_series->count()); Q_ASSERT(index >= 0); - QVector points; - - if (m_dirty || m_points.isEmpty()) { - points = domain()->calculateGeometryPoints(m_series->points()); + if (m_series->useOpenGL()) { + updateGlChart(); } else { - points = m_points; - points.remove(index); + QVector points; + if (m_dirty || m_points.isEmpty()) { + points = domain()->calculateGeometryPoints(m_series->pointsVector()); + } else { + points = m_points; + points.remove(index); + } + updateChart(m_points, points, index); } - - updateChart(m_points, points, index); } void XYChart::handlePointsRemoved(int index, int count) @@ -151,16 +165,18 @@ void XYChart::handlePointsRemoved(int index, int count) Q_ASSERT(index <= m_series->count()); Q_ASSERT(index >= 0); - QVector points; - - if (m_dirty || m_points.isEmpty()) { - points = domain()->calculateGeometryPoints(m_series->points()); + if (m_series->useOpenGL()) { + updateGlChart(); } else { - points = m_points; - points.remove(index, count); + QVector points; + if (m_dirty || m_points.isEmpty()) { + points = domain()->calculateGeometryPoints(m_series->pointsVector()); + } else { + points = m_points; + points.remove(index, count); + } + updateChart(m_points, points, index); } - - updateChart(m_points, points, index); } void XYChart::handlePointReplaced(int index) @@ -168,34 +184,45 @@ void XYChart::handlePointReplaced(int index) Q_ASSERT(index < m_series->count()); Q_ASSERT(index >= 0); - QVector points; - - if (m_dirty || m_points.isEmpty()) { - points = domain()->calculateGeometryPoints(m_series->points()); + if (m_series->useOpenGL()) { + updateGlChart(); } else { - QPointF point = domain()->calculateGeometryPoint(m_series->points()[index], m_validData); - if (!m_validData) - m_points.clear(); - points = m_points; - if (m_validData) - points.replace(index, point); + QVector points; + if (m_dirty || m_points.isEmpty()) { + points = domain()->calculateGeometryPoints(m_series->pointsVector()); + } else { + QPointF point = domain()->calculateGeometryPoint(m_series->pointsVector().at(index), + m_validData); + if (!m_validData) + m_points.clear(); + points = m_points; + if (m_validData) + points.replace(index, point); + } + updateChart(m_points, points, index); } - - updateChart(m_points, points, index); } void XYChart::handlePointsReplaced() { - // All the points were replaced -> recalculate - QVector points = domain()->calculateGeometryPoints(m_series->points()); - updateChart(m_points, points, -1); + if (m_series->useOpenGL()) { + updateGlChart(); + } else { + // All the points were replaced -> recalculate + QVector points = domain()->calculateGeometryPoints(m_series->pointsVector()); + updateChart(m_points, points, -1); + } } void XYChart::handleDomainUpdated() { - if (isEmpty()) return; - QVector points = domain()->calculateGeometryPoints(m_series->points()); - updateChart(m_points, points); + if (m_series->useOpenGL()) { + updateGlChart(); + } else { + if (isEmpty()) return; + QVector points = domain()->calculateGeometryPoints(m_series->pointsVector()); + updateChart(m_points, points); + } } bool XYChart::isEmpty() diff --git a/src/charts/xychart/xychart.pri b/src/charts/xychart/xychart.pri index e7af66e..1c5efb9 100644 --- a/src/charts/xychart/xychart.pri +++ b/src/charts/xychart/xychart.pri @@ -6,12 +6,14 @@ SOURCES += \ $$PWD/qxyseries.cpp \ $$PWD/qxymodelmapper.cpp \ $$PWD/qvxymodelmapper.cpp \ - $$PWD/qhxymodelmapper.cpp + $$PWD/qhxymodelmapper.cpp \ + $$PWD/glxyseriesdata.cpp PRIVATE_HEADERS += \ $$PWD/xychart_p.h \ $$PWD/qxyseries_p.h \ - $$PWD/qxymodelmapper_p.h + $$PWD/qxymodelmapper_p.h \ + $$PWD/glxyseriesdata_p.h PUBLIC_HEADERS += \ $$PWD/qxyseries.h \ diff --git a/src/charts/xychart/xychart_p.h b/src/charts/xychart/xychart_p.h index f45d9c6..e72dcd3 100644 --- a/src/charts/xychart/xychart_p.h +++ b/src/charts/xychart/xychart_p.h @@ -76,6 +76,7 @@ Q_SIGNALS: protected: virtual void updateChart(QVector &oldPoints, QVector &newPoints, int index = -1); + virtual void updateGlChart(); private: inline bool isEmpty(); diff --git a/src/chartsqml2/chartsqml2.pro b/src/chartsqml2/chartsqml2.pro index cc544d3..4dd8295 100644 --- a/src/chartsqml2/chartsqml2.pro +++ b/src/chartsqml2/chartsqml2.pro @@ -33,7 +33,9 @@ SOURCES += \ declarativemargins.cpp \ declarativeaxes.cpp \ declarativepolarchart.cpp \ - declarativeboxplotseries.cpp + declarativeboxplotseries.cpp \ + declarativechartnode.cpp \ + declarativerendernode.cpp HEADERS += \ declarativechart.h \ @@ -49,7 +51,9 @@ HEADERS += \ declarativemargins.h \ declarativeaxes.h \ declarativepolarchart.h \ - declarativeboxplotseries.h + declarativeboxplotseries.h \ + declarativechartnode.h \ + declarativerendernode.h OTHER_FILES = qmldir diff --git a/src/chartsqml2/declarativechart.cpp b/src/chartsqml2/declarativechart.cpp index a8910f4..c028b3d 100644 --- a/src/chartsqml2/declarativechart.cpp +++ b/src/chartsqml2/declarativechart.cpp @@ -25,6 +25,8 @@ #include "declarativesplineseries.h" #include "declarativeboxplotseries.h" #include "declarativescatterseries.h" +#include "declarativechartnode.h" +#include "declarativerendernode.h" #include #include #include @@ -34,6 +36,7 @@ #include #include "declarativeaxes.h" #include +#include #include #ifndef QT_ON_ARM @@ -88,6 +91,7 @@ QT_CHARTS_BEGIN_NAMESPACE /*! \qmlproperty easing ChartView::animationEasingCurve The easing curve of the animation for the chart. +*/ /*! \qmlproperty Font ChartView::titleFont @@ -312,34 +316,42 @@ QT_CHARTS_BEGIN_NAMESPACE */ DeclarativeChart::DeclarativeChart(QQuickItem *parent) - : QQuickPaintedItem(parent) + : QQuickItem(parent) { initChart(QChart::ChartTypeCartesian); } DeclarativeChart::DeclarativeChart(QChart::ChartType type, QQuickItem *parent) - : QQuickPaintedItem(parent) + : QQuickItem(parent) { initChart(type); } void DeclarativeChart::initChart(QChart::ChartType type) { - m_currentSceneImage = 0; + m_sceneImage = 0; + m_sceneImageDirty = false; m_guiThreadId = QThread::currentThreadId(); m_paintThreadId = 0; m_updatePending = false; + setFlag(ItemHasContents, true); + if (type == QChart::ChartTypePolar) m_chart = new QPolarChart(); else m_chart = new QChart(); + m_chart->d_ptr->m_presenter->glSetUseWidget(false); + m_glXYDataManager = m_chart->d_ptr->m_dataset->glXYSeriesDataManager(); + m_scene = new QGraphicsScene(this); m_scene->addItem(m_chart); setAntialiasing(QQuickItem::antialiasing()); - connect(m_scene, SIGNAL(changed(QList)), this, SLOT(sceneChanged(QList))); + connect(m_scene, &QGraphicsScene::changed, this, &DeclarativeChart::sceneChanged); + connect(this, &DeclarativeChart::needRender, this, &DeclarativeChart::renderScene, + Qt::QueuedConnection); connect(this, SIGNAL(antialiasingChanged(bool)), this, SLOT(handleAntialiasingChanged(bool))); setAcceptedMouseButtons(Qt::AllButtons); @@ -377,10 +389,7 @@ void DeclarativeChart::changeMargins(int top, int bottom, int left, int right) DeclarativeChart::~DeclarativeChart() { delete m_chart; - m_sceneImageLock.lock(); - delete m_currentSceneImage; - m_currentSceneImage = 0; - m_sceneImageLock.unlock(); + delete m_sceneImage; } void DeclarativeChart::childEvent(QChildEvent *event) @@ -493,19 +502,79 @@ void DeclarativeChart::geometryChanged(const QRectF &newGeometry, const QRectF & QQuickItem::geometryChanged(newGeometry, oldGeometry); } -void DeclarativeChart::sceneChanged(QList region) +QSGNode *DeclarativeChart::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *) { - Q_UNUSED(region); + DeclarativeChartNode *node = static_cast(oldNode); - if (m_guiThreadId == m_paintThreadId) { - // Rendering in gui thread, no need for shenannigans, just update - update(); - } else { - // Multi-threaded rendering, need to ensure scene is actually rendered in gui thread - if (!m_updatePending) { + if (!node) { + node = new DeclarativeChartNode(window()); + connect(window(), &QQuickWindow::beforeRendering, + node->glRenderNode(), &DeclarativeRenderNode::render); + } + + const QRectF &bRect = boundingRect(); + + // Update GL data + if (m_glXYDataManager->dataMap().size() || m_glXYDataManager->mapDirty()) { + const QRectF &plotArea = m_chart->plotArea(); + const QSizeF &chartAreaSize = m_chart->size(); + + // We can't use chart's plot area directly, as graphicscene has some internal minimum size + const qreal normalizedX = plotArea.x() / chartAreaSize.width(); + const qreal normalizedY = plotArea.y() / chartAreaSize.height(); + const qreal normalizedWidth = plotArea.width() / chartAreaSize.width(); + const qreal normalizedHeight = plotArea.height() / chartAreaSize.height(); + + QRectF adjustedPlotArea(normalizedX * bRect.width(), + normalizedY * bRect.height(), + normalizedWidth * bRect.width(), + normalizedHeight * bRect.height()); + + const QSize &adjustedPlotSize = adjustedPlotArea.size().toSize(); + if (adjustedPlotSize != node->glRenderNode()->textureSize()) + node->glRenderNode()->setTextureSize(adjustedPlotSize); + + node->glRenderNode()->setRect(adjustedPlotArea); + node->glRenderNode()->setSeriesData(m_glXYDataManager->mapDirty(), + m_glXYDataManager->dataMap()); + + // Clear dirty flags from original xy data + m_glXYDataManager->clearAllDirty(); + } + + // Copy chart (if dirty) to chart node + if (m_sceneImageDirty) { + node->createTextureFromImage(*m_sceneImage); + m_sceneImageDirty = false; + } + + node->setRect(bRect); + + return node; +} + +void DeclarativeChart::sceneChanged(QList region) +{ + const int count = region.size(); + const qreal limitSize = 0.01; + if (count && !m_updatePending) { + qreal totalSize = 0.0; + for (int i = 0; i < count; i++) { + const QRectF ® = region.at(i); + totalSize += (reg.height() * reg.width()); + if (totalSize >= limitSize) + break; + } + // Ignore region updates that change less than small fraction of a pixel, as there is + // little point regenerating the image in these cases. These are typically cases + // where OpenGL series are drawn to otherwise static chart. + if (totalSize >= limitSize) { m_updatePending = true; // Do async render to avoid some unnecessary renders. - QTimer::singleShot(0, this, SLOT(renderScene())); + emit needRender(); + } else { + // We do want to call update to trigger possible gl series updates. + update(); } } } @@ -513,45 +582,22 @@ void DeclarativeChart::sceneChanged(QList region) void DeclarativeChart::renderScene() { m_updatePending = false; - m_sceneImageLock.lock(); - delete m_currentSceneImage; - m_currentSceneImage = new QImage(m_chart->size().toSize(), QImage::Format_ARGB32); - m_currentSceneImage->fill(Qt::transparent); - QPainter painter(m_currentSceneImage); - if (antialiasing()) - painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform); - QRect renderRect(QPoint(0, 0), m_chart->size().toSize()); - m_scene->render(&painter, renderRect, renderRect); - m_sceneImageLock.unlock(); - - update(); -} - -void DeclarativeChart::paint(QPainter *painter) -{ - if (!m_paintThreadId) { - m_paintThreadId = QThread::currentThreadId(); - if (m_guiThreadId == m_paintThreadId) { - // No need for scene image in single threaded rendering, so delete - // the one that got made by default before the rendering type was - // detected. - delete m_currentSceneImage; - m_currentSceneImage = 0; - } + m_sceneImageDirty = true; + QSize chartSize = m_chart->size().toSize(); + if (!m_sceneImage || chartSize != m_sceneImage->size()) { + delete m_sceneImage; + m_sceneImage = new QImage(chartSize, QImage::Format_ARGB32); + m_sceneImage->fill(Qt::transparent); } - if (m_guiThreadId == m_paintThreadId) { - QRectF renderRect(QPointF(0, 0), m_chart->size()); - m_scene->render(painter, renderRect, renderRect); - } else { - m_sceneImageLock.lock(); - if (m_currentSceneImage) { - QRect imageRect(QPoint(0, 0), m_currentSceneImage->size()); - QRect itemRect(QPoint(0, 0), QSize(width(), height())); - painter->drawImage(itemRect, *m_currentSceneImage, imageRect); - } - m_sceneImageLock.unlock(); + QPainter painter(m_sceneImage); + if (antialiasing()) { + painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing + | QPainter::SmoothPixmapTransform); } + QRect renderRect(QPoint(0, 0), chartSize); + m_scene->render(&painter, renderRect, renderRect); + update(); } void DeclarativeChart::mousePressEvent(QMouseEvent *event) diff --git a/src/chartsqml2/declarativechart.h b/src/chartsqml2/declarativechart.h index 80505d4..c5d06b8 100644 --- a/src/chartsqml2/declarativechart.h +++ b/src/chartsqml2/declarativechart.h @@ -19,11 +19,11 @@ #ifndef DECLARATIVECHART_H #define DECLARATIVECHART_H +#include + #include #include -#include #include -#include #include #include @@ -34,7 +34,7 @@ class DeclarativeMargins; class Domain; class DeclarativeAxes; -class DeclarativeChart : public QQuickPaintedItem +class DeclarativeChart : public QQuickItem { Q_OBJECT Q_PROPERTY(Theme theme READ theme WRITE setTheme) @@ -102,7 +102,7 @@ public: // From parent classes void childEvent(QChildEvent *event); void componentComplete(); void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry); - void paint(QPainter *painter); + QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *); protected: void mousePressEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); @@ -199,6 +199,7 @@ Q_SIGNALS: Q_REVISION(4) void localeChanged(); Q_REVISION(5) void animationDurationChanged(int msecs); Q_REVISION(5) void animationEasingCurveChanged(QEasingCurve curve); + void needRender(); private Q_SLOTS: void changeMargins(int top, int bottom, int left, int right); @@ -227,12 +228,13 @@ private: QPoint m_lastMouseMoveScreenPoint; Qt::MouseButton m_mousePressButton; Qt::MouseButtons m_mousePressButtons; - QMutex m_sceneImageLock; - QImage *m_currentSceneImage; + QImage *m_sceneImage; + bool m_sceneImageDirty; bool m_updatePending; Qt::HANDLE m_paintThreadId; Qt::HANDLE m_guiThreadId; DeclarativeMargins *m_margins; + GLXYSeriesDataManager *m_glXYDataManager; }; QT_CHARTS_END_NAMESPACE diff --git a/src/chartsqml2/declarativechartnode.cpp b/src/chartsqml2/declarativechartnode.cpp new file mode 100644 index 0000000..4e32344 --- /dev/null +++ b/src/chartsqml2/declarativechartnode.cpp @@ -0,0 +1,78 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd +** All rights reserved. +** For any questions to The Qt Company, please use contact form at http://qt.io +** +** This file is part of the Qt Charts module. +** +** Licensees holding valid commercial license for Qt may use this file in +** accordance with the Qt License Agreement provided with the Software +** or, alternatively, in accordance with the terms contained in a written +** agreement between you and The Qt Company. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.io +** +****************************************************************************/ + +#include "declarativechartnode.h" +#include "declarativerendernode.h" +#include +#include +#include +#include +#include +#include + +QT_CHARTS_BEGIN_NAMESPACE + +// This node handles displaying of the chart itself +DeclarativeChartNode::DeclarativeChartNode(QQuickWindow *window) : + QSGSimpleTextureNode(), + m_texture(0), + m_window(window), + m_textureOptions(0), + m_textureSize(1, 1), + m_glRenderNode(0) +{ + initializeOpenGLFunctions(); + + // Our texture node must have a texture, so use a default one pixel texture + GLuint defaultTexture = 0; + glGenTextures(1, &defaultTexture); + glBindTexture(GL_TEXTURE_2D, defaultTexture); + uchar buf[4] = { 0, 0, 0, 0 }; + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &buf); + + QQuickWindow::CreateTextureOptions defaultTextureOptions = QQuickWindow::CreateTextureOptions( + QQuickWindow::TextureHasAlphaChannel | QQuickWindow::TextureOwnsGLTexture); + m_texture = m_window->createTextureFromId(defaultTexture, QSize(1, 1), defaultTextureOptions); + + setTexture(m_texture); + setFiltering(QSGTexture::Linear); + + // Create child node for rendering GL graphics + m_glRenderNode = new DeclarativeRenderNode(m_window); + m_glRenderNode->setFlag(OwnedByParent); + appendChildNode(m_glRenderNode); + m_glRenderNode->setRect(0, 0, 0, 0); // Hide child node by default +} + +DeclarativeChartNode::~DeclarativeChartNode() +{ + delete m_texture; +} + +// Must be called on render thread and in context +void DeclarativeChartNode::createTextureFromImage(const QImage &chartImage) +{ + if (chartImage.size() != m_textureSize) + m_textureSize = chartImage.size(); + + delete m_texture; + m_texture = m_window->createTextureFromImage(chartImage, m_textureOptions); + setTexture(m_texture); +} + +QT_CHARTS_END_NAMESPACE diff --git a/src/chartsqml2/declarativechartnode.h b/src/chartsqml2/declarativechartnode.h new file mode 100644 index 0000000..46936c8 --- /dev/null +++ b/src/chartsqml2/declarativechartnode.h @@ -0,0 +1,50 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd +** All rights reserved. +** For any questions to The Qt Company, please use contact form at http://qt.io +** +** This file is part of the Qt Charts module. +** +** Licensees holding valid commercial license for Qt may use this file in +** accordance with the Qt License Agreement provided with the Software +** or, alternatively, in accordance with the terms contained in a written +** agreement between you and The Qt Company. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.io +** +****************************************************************************/ + +#ifndef DECLARATIVECHARTNODE_P_H +#define DECLARATIVECHARTNODE_P_H + +#include +#include +#include +#include + +QT_CHARTS_BEGIN_NAMESPACE + +class DeclarativeRenderNode; + +class DeclarativeChartNode : public QSGSimpleTextureNode, QOpenGLFunctions +{ +public: + DeclarativeChartNode(QQuickWindow *window); + ~DeclarativeChartNode(); + + void createTextureFromImage(const QImage &chartImage); + DeclarativeRenderNode *glRenderNode() const { return m_glRenderNode; } + +private: + QSGTexture *m_texture; + QQuickWindow *m_window; + QQuickWindow::CreateTextureOptions m_textureOptions; + QSize m_textureSize; + DeclarativeRenderNode *m_glRenderNode; +}; + +QT_CHARTS_END_NAMESPACE + +#endif // DECLARATIVECHARTNODE_P_H diff --git a/src/chartsqml2/declarativerendernode.cpp b/src/chartsqml2/declarativerendernode.cpp new file mode 100644 index 0000000..b271dcc --- /dev/null +++ b/src/chartsqml2/declarativerendernode.cpp @@ -0,0 +1,317 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd +** All rights reserved. +** For any questions to The Qt Company, please use contact form at http://qt.io +** +** This file is part of the Qt Charts module. +** +** Licensees holding valid commercial license for Qt may use this file in +** accordance with the Qt License Agreement provided with the Software +** or, alternatively, in accordance with the terms contained in a written +** agreement between you and The Qt Company. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.io +** +****************************************************************************/ + +#include "declarativerendernode.h" + +#include +#include +#include +#include +#include +#include + +//#define QDEBUG_TRACE_GL_FPS +#ifdef QDEBUG_TRACE_GL_FPS +# include +#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. +DeclarativeRenderNode::DeclarativeRenderNode(QQuickWindow *window) : + QObject(), + QSGSimpleTextureNode(), + m_texture(0), + m_window(window), + m_textureOptions(QQuickWindow::TextureHasAlphaChannel), + m_textureSize(1, 1), + m_recreateFbo(false), + m_fbo(0), + m_program(0), + m_shaderAttribLoc(-1), + m_colorUniformLoc(-1), + m_minUniformLoc(-1), + m_deltaUniformLoc(-1), + m_pointSizeUniformLoc(-1), + m_renderNeeded(true) +{ + initializeOpenGLFunctions(); + + // Our texture node must have a texture, so use a default one pixel texture + GLuint defaultTexture = 0; + glGenTextures(1, &defaultTexture); + glBindTexture(GL_TEXTURE_2D, defaultTexture); + uchar buf[4] = { 0, 0, 0, 0 }; + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &buf); + + QQuickWindow::CreateTextureOptions defaultTextureOptions = QQuickWindow::CreateTextureOptions( + QQuickWindow::TextureHasAlphaChannel | QQuickWindow::TextureOwnsGLTexture); + m_texture = m_window->createTextureFromId(defaultTexture, QSize(1, 1), defaultTextureOptions); + + setTexture(m_texture); + setFiltering(QSGTexture::Linear); + setTextureCoordinatesTransform(QSGSimpleTextureNode::MirrorVertically); +} + +DeclarativeRenderNode::~DeclarativeRenderNode() +{ + delete m_texture; + delete m_fbo; + + delete m_program; + m_program = 0; + + cleanXYSeriesResources(0); +} + +static const char *vertexSource = + "attribute highp vec2 points;\n" + "uniform highp vec2 min;\n" + "uniform highp vec2 delta;\n" + "uniform highp float pointSize;\n" + "void main() {\n" + " vec2 normalPoint = vec2(-1, -1) + ((points - min) / delta);\n" + " gl_Position = 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 DeclarativeRenderNode::initGL() +{ + recreateFBO(); + + m_program = new QOpenGLShaderProgram; + 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"); + + // 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 DeclarativeRenderNode::recreateFBO() +{ + QOpenGLFramebufferObjectFormat fboFormat; + fboFormat.setAttachment(QOpenGLFramebufferObject::NoAttachment); + delete m_fbo; + m_fbo = new QOpenGLFramebufferObject(m_textureSize.width(), + m_textureSize.height(), + fboFormat); + + delete m_texture; + m_texture = m_window->createTextureFromId(m_fbo->texture(), m_textureSize, m_textureOptions); + setTexture(m_texture); + + m_recreateFbo = false; +} + +// Must be called on render thread and in context +void DeclarativeRenderNode::setTextureSize(const QSize &size) +{ + m_textureSize = size; + m_recreateFbo = true; + m_renderNeeded = true; +} + +// Must be called on render thread while gui thread is blocked, and in context +void DeclarativeRenderNode::setSeriesData(bool mapDirty, const GLXYDataMap &dataMap) +{ + if (mapDirty) { + // Series have changed, recreate map, but utilize old data where feasible + GLXYDataMap oldMap = m_xyDataMap; + m_xyDataMap.clear(); + + GLXYDataMapIterator i(dataMap); + while (i.hasNext()) { + i.next(); + GLXYSeriesData *data = oldMap.take(i.key()); + const GLXYSeriesData *newData = i.value(); + if (!data || newData->dirty) { + data = new GLXYSeriesData; + data->array = newData->array; + data->color = newData->color; + data->dirty = newData->dirty; + data->width = newData->width; + data->type = newData->type; + data->min = newData->min; + data->delta = newData->delta; + } + m_xyDataMap.insert(i.key(), data); + } + // Delete remaining old data + i = oldMap; + while (i.hasNext()) { + i.next(); + delete i.value(); + cleanXYSeriesResources(i.key()); + } + } else { + // Series have not changed, so just copy dirty data over + GLXYDataMapIterator i(dataMap); + while (i.hasNext()) { + i.next(); + const GLXYSeriesData *newData = i.value(); + if (i.value()->dirty) { + GLXYSeriesData *data = m_xyDataMap.value(i.key()); + if (data) { + data->array = newData->array; + data->color = newData->color; + data->dirty = newData->dirty; + data->width = newData->width; + data->type = newData->type; + data->min = newData->min; + data->delta = newData->delta; + } + } + } + } + markDirty(DirtyMaterial); + m_renderNeeded = true; +} + +void DeclarativeRenderNode::renderGL() +{ + glClearColor(0, 0, 0, 0); + + QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); + m_program->bind(); + m_fbo->bind(); + + glClear(GL_COLOR_BUFFER_BIT); + glEnableVertexAttribArray(0); + + glViewport(0, 0, m_textureSize.width(), m_textureSize.height()); + + GLXYDataMapIterator i(m_xyDataMap); + while (i.hasNext()) { + i.next(); + QOpenGLBuffer *vbo = m_seriesBufferMap.value(i.key()); + GLXYSeriesData *data = i.value(); + + m_program->setUniformValue(m_colorUniformLoc, data->color); + m_program->setUniformValue(m_minUniformLoc, data->min); + m_program->setUniformValue(m_deltaUniformLoc, data->delta); + + 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(); + } + +#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 + + markDirty(DirtyMaterial); + m_window->resetOpenGLState(); +} + +// Must be called on render thread as response to beforeRendering signal +void DeclarativeRenderNode::render() +{ + if (m_renderNeeded) { + if (m_xyDataMap.size()) { + if (!m_program) + initGL(); + if (m_recreateFbo) + recreateFBO(); + renderGL(); + } else { + if (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; + } +} + +void DeclarativeRenderNode::cleanXYSeriesResources(const QXYSeries *series) +{ + if (series) { + delete m_seriesBufferMap.take(series); + } else { + foreach (QOpenGLBuffer *buffer, m_seriesBufferMap.values()) + delete buffer; + m_seriesBufferMap.clear(); + } +} + +QT_CHARTS_END_NAMESPACE diff --git a/src/chartsqml2/declarativerendernode.h b/src/chartsqml2/declarativerendernode.h new file mode 100644 index 0000000..f00f5c4 --- /dev/null +++ b/src/chartsqml2/declarativerendernode.h @@ -0,0 +1,74 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd +** All rights reserved. +** For any questions to The Qt Company, please use contact form at http://qt.io +** +** This file is part of the Qt Charts module. +** +** Licensees holding valid commercial license for Qt may use this file in +** accordance with the Qt License Agreement provided with the Software +** or, alternatively, in accordance with the terms contained in a written +** agreement between you and The Qt Company. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.io +** +****************************************************************************/ + +#ifndef DECLARATIVERENDERNODE_P_H +#define DECLARATIVERENDERNODE_P_H + +#include +#include +#include +#include +#include +#include + +class QOpenGLFramebufferObject; +class QOpenGLBuffer; + +QT_CHARTS_BEGIN_NAMESPACE + +class DeclarativeRenderNode : public QObject, public QSGSimpleTextureNode, QOpenGLFunctions +{ + Q_OBJECT +public: + DeclarativeRenderNode(QQuickWindow *window); + ~DeclarativeRenderNode(); + + void initGL(); + QSize textureSize() const { return m_textureSize; } + void setTextureSize(const QSize &size); + void setSeriesData(bool mapDirty, const GLXYDataMap &dataMap); + +public Q_SLOTS: + void render(); + +private: + void renderGL(); + void recreateFBO(); + void cleanXYSeriesResources(const QXYSeries *series); + + QSGTexture *m_texture; + QQuickWindow *m_window; + QQuickWindow::CreateTextureOptions m_textureOptions; + QSize m_textureSize; + bool m_recreateFbo; + GLXYDataMap m_xyDataMap; + QOpenGLFramebufferObject *m_fbo; + QOpenGLShaderProgram *m_program; + int m_shaderAttribLoc; + int m_colorUniformLoc; + int m_minUniformLoc; + int m_deltaUniformLoc; + int m_pointSizeUniformLoc; + QOpenGLVertexArrayObject m_vao; + QHash m_seriesBufferMap; + bool m_renderNeeded; +}; + +QT_CHARTS_END_NAMESPACE + +#endif // DECLARATIVERENDERNODE_P_H diff --git a/tests/auto/qlineseries/tst_qlineseries.cpp b/tests/auto/qlineseries/tst_qlineseries.cpp index e1df6c7..1b7a57b 100644 --- a/tests/auto/qlineseries/tst_qlineseries.cpp +++ b/tests/auto/qlineseries/tst_qlineseries.cpp @@ -19,8 +19,8 @@ #include "../qxyseries/tst_qxyseries.h" #include - Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(QVector) class tst_QLineSeries : public tst_QXYSeries { @@ -76,6 +76,7 @@ void tst_QLineSeries::qlineseries() QCOMPARE(series.count(),0); QCOMPARE(series.brush(), QBrush()); QCOMPARE(series.points(), QList()); + QCOMPARE(series.pointsVector(), QVector()); QCOMPARE(series.pen(), QPen()); QCOMPARE(series.pointsVisible(), false); QCOMPARE(series.pointLabelsVisible(), false); diff --git a/tests/auto/qscatterseries/tst_qscatterseries.cpp b/tests/auto/qscatterseries/tst_qscatterseries.cpp index 588a654..eedb4c3 100644 --- a/tests/auto/qscatterseries/tst_qscatterseries.cpp +++ b/tests/auto/qscatterseries/tst_qscatterseries.cpp @@ -20,6 +20,7 @@ #include Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(QVector) class tst_QScatterSeries : public tst_QXYSeries { @@ -75,6 +76,7 @@ void tst_QScatterSeries::qscatterseries() QCOMPARE(series.count(),0); QCOMPARE(series.brush(), QBrush()); QCOMPARE(series.points(), QList()); + QCOMPARE(series.pointsVector(), QVector()); QCOMPARE(series.pen(), QPen()); QCOMPARE(series.pointsVisible(), false); diff --git a/tests/auto/qsplineseries/tst_qsplineseries.cpp b/tests/auto/qsplineseries/tst_qsplineseries.cpp index 53ddf01..4f02308 100644 --- a/tests/auto/qsplineseries/tst_qsplineseries.cpp +++ b/tests/auto/qsplineseries/tst_qsplineseries.cpp @@ -20,6 +20,7 @@ #include Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(QVector) class tst_QSplineSeries : public tst_QXYSeries { @@ -73,6 +74,7 @@ void tst_QSplineSeries::qsplineseries() QCOMPARE(series.count(),0); QCOMPARE(series.brush(), QBrush()); QCOMPARE(series.points(), QList()); + QCOMPARE(series.pointsVector(), QVector()); QCOMPARE(series.pen(), QPen()); QCOMPARE(series.pointsVisible(), false); diff --git a/tests/auto/qxyseries/tst_qxyseries.cpp b/tests/auto/qxyseries/tst_qxyseries.cpp index bb82741..762c7a0 100644 --- a/tests/auto/qxyseries/tst_qxyseries.cpp +++ b/tests/auto/qxyseries/tst_qxyseries.cpp @@ -198,6 +198,7 @@ void tst_QXYSeries::append_raw() TRY_COMPARE(spy0.count(), 0); TRY_COMPARE(addedSpy.count(), points.count()); QCOMPARE(m_series->points(), points); + QCOMPARE(m_series->pointsVector(), points.toVector()); // Process events between appends foreach (const QPointF &point, otherPoints) {