Revision control
Copy as Markdown
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses
#include "declarativeopenglrendernode_p.h"
#include <QtGui/QOpenGLContext>
#include <QtGui/QOpenGLFunctions>
#include <QtOpenGL/QOpenGLFramebufferObjectFormat>
#include <QtOpenGL/QOpenGLFramebufferObject>
#include <QOpenGLShaderProgram>
#include <QtOpenGL/QOpenGLBuffer>
#include <QQuickOpenGLUtils>
//#define QDEBUG_TRACE_GL_FPS
#ifdef QDEBUG_TRACE_GL_FPS
# include <QElapsedTimer>
#endif
QT_BEGIN_NAMESPACE
// This node draws the xy series data on a transparent background using OpenGL.
// It is used as a child node of the chart node.
DeclarativeOpenGLRenderNode::DeclarativeOpenGLRenderNode(QQuickWindow *window) :
QObject(),
m_texture(nullptr),
m_imageNode(nullptr),
m_window(window),
m_textureOptions(QQuickWindow::TextureHasAlphaChannel),
m_textureSize(1, 1),
m_recreateFbo(false),
m_fbo(nullptr),
m_resolvedFbo(nullptr),
m_selectionFbo(nullptr),
m_program(nullptr),
m_shaderAttribLoc(-1),
m_colorUniformLoc(-1),
m_minUniformLoc(-1),
m_deltaUniformLoc(-1),
m_pointSizeUniformLoc(-1),
m_renderNeeded(true),
m_antialiasing(false),
m_selectionRenderNeeded(true),
m_mousePressed(false),
m_lastPressSeries(nullptr),
m_lastHoverSeries(nullptr)
{
initializeOpenGLFunctions();
connect(m_window, &QQuickWindow::beforeRendering,
this, &DeclarativeOpenGLRenderNode::render);
}
DeclarativeOpenGLRenderNode::~DeclarativeOpenGLRenderNode()
{
cleanXYSeriesResources(0);
delete m_texture;
delete m_fbo;
delete m_resolvedFbo;
delete m_selectionFbo;
delete m_program;
qDeleteAll(m_mouseEvents);
}
static const char *vertexSourceCore =
"#version 150\n"
"in vec2 points;\n"
"uniform vec2 min;\n"
"uniform vec2 delta;\n"
"uniform float pointSize;\n"
"uniform mat4 matrix;\n"
"void main() {\n"
" vec2 normalPoint = vec2(-1, -1) + ((points - min) / delta);\n"
" gl_Position = matrix * vec4(normalPoint, 0, 1);\n"
" gl_PointSize = pointSize;\n"
"}";
static const char *fragmentSourceCore =
"#version 150\n"
"uniform vec3 color;\n"
"out vec4 fragColor;\n"
"void main() {\n"
" fragColor = vec4(color,1);\n"
"}\n";
static const char *vertexSource =
"attribute highp vec2 points;\n"
"uniform highp vec2 min;\n"
"uniform highp vec2 delta;\n"
"uniform highp float pointSize;\n"
"uniform highp mat4 matrix;\n"
"void main() {\n"
" vec2 normalPoint = vec2(-1, -1) + ((points - min) / delta);\n"
" gl_Position = matrix * vec4(normalPoint, 0, 1);\n"
" gl_PointSize = pointSize;\n"
"}";
static const char *fragmentSource =
"uniform highp vec3 color;\n"
"void main() {\n"
" gl_FragColor = vec4(color,1);\n"
"}\n";
// Must be called on render thread and in context
void DeclarativeOpenGLRenderNode::initGL()
{
recreateFBO();
m_program = new QOpenGLShaderProgram;
if (QOpenGLContext::currentContext()->format().profile() == QSurfaceFormat::CoreProfile) {
m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexSourceCore);
m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentSourceCore);
} else {
m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexSource);
m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentSource);
}
m_program->bindAttributeLocation("points", 0);
m_program->link();
m_program->bind();
m_colorUniformLoc = m_program->uniformLocation("color");
m_minUniformLoc = m_program->uniformLocation("min");
m_deltaUniformLoc = m_program->uniformLocation("delta");
m_pointSizeUniformLoc = m_program->uniformLocation("pointSize");
m_matrixUniformLoc = m_program->uniformLocation("matrix");
// Create a vertex array object. In OpenGL ES 2.0 and OpenGL 2.x
// implementations this is optional and support may not be present
// at all. Nonetheless the below code works in all cases and makes
// sure there is a VAO when one is needed.
m_vao.create();
QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao);
#if !QT_CONFIG(opengles2)
if (!QOpenGLContext::currentContext()->isOpenGLES()) {
// Make it possible to change point primitive size and use textures with them in
// the shaders. These are implicitly enabled in ES2.
// Qt Quick doesn't change these flags, so it should be safe to just enable them
// at initialization.
glEnable(GL_PROGRAM_POINT_SIZE);
}
#endif
m_program->release();
}
void DeclarativeOpenGLRenderNode::recreateFBO()
{
QOpenGLFramebufferObjectFormat fboFormat;
fboFormat.setAttachment(QOpenGLFramebufferObject::NoAttachment);
int samples = 0;
QOpenGLContext *context = QOpenGLContext::currentContext();
if (m_antialiasing && (!context->isOpenGLES() || context->format().majorVersion() >= 3))
samples = 4;
fboFormat.setSamples(samples);
delete m_fbo;
delete m_resolvedFbo;
delete m_selectionFbo;
m_resolvedFbo = nullptr;
m_fbo = new QOpenGLFramebufferObject(m_textureSize, fboFormat);
if (samples > 0)
m_resolvedFbo = new QOpenGLFramebufferObject(m_textureSize);
m_selectionFbo = new QOpenGLFramebufferObject(m_textureSize);
delete m_texture;
uint textureId = m_resolvedFbo ? m_resolvedFbo->texture() : m_fbo->texture();
m_texture = QNativeInterface::QSGOpenGLTexture::fromNative(textureId, m_window, m_textureSize, m_textureOptions);
if (!m_imageNode) {
m_imageNode = m_window->createImageNode();
m_imageNode->setFiltering(QSGTexture::Linear);
m_imageNode->setTextureCoordinatesTransform(QSGImageNode::MirrorVertically);
m_imageNode->setFlag(OwnedByParent);
if (!m_rect.isEmpty())
m_imageNode->setRect(m_rect);
appendChildNode(m_imageNode);
}
m_imageNode->setTexture(m_texture);
m_recreateFbo = false;
}
// Must be called on render thread and in context
void DeclarativeOpenGLRenderNode::setTextureSize(const QSize &size)
{
m_textureSize = size;
m_recreateFbo = true;
m_renderNeeded = true;
m_selectionRenderNeeded = true;
}
// Must be called on render thread while gui thread is blocked, and in context
void DeclarativeOpenGLRenderNode::setSeriesData(bool mapDirty, const GLXYDataMap &dataMap)
{
bool dirty = false;
if (mapDirty) {
// Series have changed, recreate map, but utilize old data where feasible
GLXYDataMap oldMap = m_xyDataMap;
m_xyDataMap.clear();
for (auto i = dataMap.begin(), end = dataMap.end(); i != end; ++i) {
GLXYSeriesData *data = oldMap.take(i.key());
const GLXYSeriesData *newData = i.value();
if (!data || newData->dirty) {
if (!data)
data = new GLXYSeriesData;
*data = *newData;
}
m_xyDataMap.insert(i.key(), data);
}
// Delete remaining old data
for (auto i = oldMap.begin(), end = oldMap.end(); i != end; ++i) {
delete i.value();
cleanXYSeriesResources(i.key());
}
dirty = true;
} else {
// Series have not changed, so just copy dirty data over
for (auto i = dataMap.begin(), end = dataMap.end(); i != end; ++i) {
const GLXYSeriesData *newData = i.value();
if (i.value()->dirty) {
dirty = true;
GLXYSeriesData *data = m_xyDataMap.value(i.key());
if (data)
*data = *newData;
}
}
}
if (dirty) {
markDirty(DirtyMaterial);
m_renderNeeded = true;
m_selectionRenderNeeded = true;
}
}
void DeclarativeOpenGLRenderNode::setRect(const QRectF &rect)
{
m_rect = rect;
if (m_imageNode)
m_imageNode->setRect(rect);
}
void DeclarativeOpenGLRenderNode::setAntialiasing(bool enable)
{
if (m_antialiasing != enable) {
m_antialiasing = enable;
m_recreateFbo = true;
m_renderNeeded = true;
}
}
void DeclarativeOpenGLRenderNode::addMouseEvents(const QList<QMouseEvent *> &events)
{
if (events.size()) {
m_mouseEvents.append(events);
markDirty(DirtyMaterial);
}
}
void DeclarativeOpenGLRenderNode::takeMouseEventResponses(QList<MouseEventResponse> &responses)
{
responses.append(m_mouseEventResponses);
m_mouseEventResponses.clear();
}
void DeclarativeOpenGLRenderNode::renderGL(bool selection)
{
glClearColor(0, 0, 0, 0);
QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao);
m_program->bind();
glClear(GL_COLOR_BUFFER_BIT);
glEnableVertexAttribArray(0);
glViewport(0, 0, m_textureSize.width(), m_textureSize.height());
int counter = 0;
for (auto i = m_xyDataMap.begin(), end = m_xyDataMap.end(); i != end; ++i) {
QOpenGLBuffer *vbo = m_seriesBufferMap.value(i.key());
GLXYSeriesData *data = i.value();
if (data->visible) {
if (selection) {
m_selectionList[counter] = i.key();
m_program->setUniformValue(m_colorUniformLoc, QVector3D((counter & 0xff) / 255.0f,
((counter & 0xff00) >> 8) / 255.0f,
((counter & 0xff0000) >> 16) / 255.0f));
counter++;
} else {
m_program->setUniformValue(m_colorUniformLoc, data->color);
}
m_program->setUniformValue(m_minUniformLoc, data->min);
m_program->setUniformValue(m_deltaUniformLoc, data->delta);
m_program->setUniformValue(m_matrixUniformLoc, data->matrix);
if (!vbo) {
vbo = new QOpenGLBuffer;
m_seriesBufferMap.insert(i.key(), vbo);
vbo->create();
}
vbo->bind();
if (data->dirty) {
vbo->allocate(data->array.constData(), int(data->array.size() * sizeof(GLfloat)));
data->dirty = false;
}
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
if (data->type == QAbstractSeries::SeriesTypeLine) {
glLineWidth(data->width);
glDrawArrays(GL_LINE_STRIP, 0, data->array.size() / 2);
} else { // Scatter
m_program->setUniformValue(m_pointSizeUniformLoc, data->width);
glDrawArrays(GL_POINTS, 0, data->array.size() / 2);
}
vbo->release();
}
}
}
void DeclarativeOpenGLRenderNode::renderSelection()
{
m_selectionFbo->bind();
m_selectionList.resize(m_xyDataMap.size());
renderGL(true);
m_selectionRenderNeeded = false;
}
void DeclarativeOpenGLRenderNode::renderVisual()
{
m_fbo->bind();
renderGL(false);
if (m_resolvedFbo) {
QRect rect(QPoint(0, 0), m_fbo->size());
QOpenGLFramebufferObject::blitFramebuffer(m_resolvedFbo, rect, m_fbo, rect);
}
markDirty(DirtyMaterial);
#ifdef QDEBUG_TRACE_GL_FPS
static QElapsedTimer stopWatch;
static int frameCount = -1;
if (frameCount == -1) {
stopWatch.start();
frameCount = 0;
}
frameCount++;
int elapsed = stopWatch.elapsed();
if (elapsed >= 1000) {
elapsed = stopWatch.restart();
qreal fps = qreal(0.1 * int(10000.0 * (qreal(frameCount) / qreal(elapsed))));
qDebug() << "FPS:" << fps;
frameCount = 0;
}
#endif
}
// Must be called on render thread as response to beforeRendering signal
void DeclarativeOpenGLRenderNode::render()
{
// Reset blend function, etc. derived from the previous frame.
QQuickOpenGLUtils::resetOpenGLState();
if (m_renderNeeded) {
if (m_xyDataMap.size()) {
if (!m_program)
initGL();
if (m_recreateFbo)
recreateFBO();
renderVisual();
} else {
if (m_imageNode && m_imageNode->rect() != QRectF()) {
glClearColor(0, 0, 0, 0);
m_fbo->bind();
glClear(GL_COLOR_BUFFER_BIT);
// If last series was removed, zero out the node rect
setRect(QRectF());
}
}
m_renderNeeded = false;
}
handleMouseEvents();
QQuickOpenGLUtils::resetOpenGLState();
}
void DeclarativeOpenGLRenderNode::cleanXYSeriesResources(const QXYSeries *series)
{
if (series) {
delete m_seriesBufferMap.take(series);
delete m_xyDataMap.take(series);
} else {
for (QOpenGLBuffer *buffer : std::as_const(m_seriesBufferMap))
delete buffer;
m_seriesBufferMap.clear();
for (GLXYSeriesData *data : std::as_const(m_xyDataMap))
delete data;
m_xyDataMap.clear();
}
}
void DeclarativeOpenGLRenderNode::handleMouseEvents()
{
if (m_mouseEvents.size()) {
if (m_xyDataMap.size()) {
if (m_selectionRenderNeeded)
renderSelection();
}
for (const QMouseEvent *event : std::as_const(m_mouseEvents)) {
const QXYSeries *series = findSeriesAtEvent(event);
switch (event->type()) {
case QEvent::MouseMove: {
if (series != m_lastHoverSeries) {
if (m_lastHoverSeries) {
m_mouseEventResponses.append(
MouseEventResponse(MouseEventResponse::HoverLeave,
event->pos(), m_lastHoverSeries));
}
if (series) {
m_mouseEventResponses.append(
MouseEventResponse(MouseEventResponse::HoverEnter,
event->pos(), series));
}
m_lastHoverSeries = series;
}
break;
}
case QEvent::MouseButtonPress: {
if (series) {
m_mousePressed = true;
m_mousePressPos = event->pos();
m_lastPressSeries = series;
m_mouseEventResponses.append(
MouseEventResponse(MouseEventResponse::Pressed,
event->pos(), series));
}
break;
}
case QEvent::MouseButtonRelease: {
m_mouseEventResponses.append(
MouseEventResponse(MouseEventResponse::Released,
m_mousePressPos, m_lastPressSeries));
if (m_mousePressed) {
m_mouseEventResponses.append(
MouseEventResponse(MouseEventResponse::Clicked,
m_mousePressPos, m_lastPressSeries));
}
if (m_lastHoverSeries == m_lastPressSeries && m_lastHoverSeries != series) {
if (m_lastHoverSeries) {
m_mouseEventResponses.append(
MouseEventResponse(MouseEventResponse::HoverLeave,
event->pos(), m_lastHoverSeries));
}
m_lastHoverSeries = nullptr;
}
m_lastPressSeries = nullptr;
m_mousePressed = false;
break;
}
case QEvent::MouseButtonDblClick: {
if (series) {
m_mouseEventResponses.append(
MouseEventResponse(MouseEventResponse::DoubleClicked,
event->pos(), series));
}
break;
}
default:
break;
}
}
qDeleteAll(m_mouseEvents);
m_mouseEvents.clear();
}
}
const QXYSeries *DeclarativeOpenGLRenderNode::findSeriesAtEvent(const QMouseEvent *event)
{
const QXYSeries *series = nullptr;
int index = -1;
if (m_xyDataMap.size()) {
m_selectionFbo->bind();
GLubyte pixel[4] = {0, 0, 0, 0};
glReadPixels(event->pos().x(), m_textureSize.height() - event->pos().y(),
1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
(void *)pixel);
if (pixel[3] == 0xff)
index = pixel[0] + (pixel[1] << 8) + (pixel[2] << 16);
}
if (index >= 0 && index < m_selectionList.size())
series = m_selectionList.at(index);
return series;
}
QT_END_NAMESPACE