1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "declarativeopenglrendernode_p.h"
5
6#include <QtGui/QOpenGLContext>
7#include <QtGui/QOpenGLFunctions>
8#include <QtOpenGL/QOpenGLFramebufferObjectFormat>
9#include <QtOpenGL/QOpenGLFramebufferObject>
10#include <QOpenGLShaderProgram>
11#include <QtOpenGL/QOpenGLBuffer>
12#include <QQuickOpenGLUtils>
13
14//#define QDEBUG_TRACE_GL_FPS
15#ifdef QDEBUG_TRACE_GL_FPS
16# include <QElapsedTimer>
17#endif
18
19QT_BEGIN_NAMESPACE
20
21// This node draws the xy series data on a transparent background using OpenGL.
22// It is used as a child node of the chart node.
23DeclarativeOpenGLRenderNode::DeclarativeOpenGLRenderNode(QQuickWindow *window) :
24 QObject(),
25 m_texture(nullptr),
26 m_imageNode(nullptr),
27 m_window(window),
28 m_textureOptions(QQuickWindow::TextureHasAlphaChannel),
29 m_textureSize(1, 1),
30 m_recreateFbo(false),
31 m_fbo(nullptr),
32 m_resolvedFbo(nullptr),
33 m_selectionFbo(nullptr),
34 m_program(nullptr),
35 m_shaderAttribLoc(-1),
36 m_colorUniformLoc(-1),
37 m_minUniformLoc(-1),
38 m_deltaUniformLoc(-1),
39 m_pointSizeUniformLoc(-1),
40 m_renderNeeded(true),
41 m_antialiasing(false),
42 m_selectionRenderNeeded(true),
43 m_mousePressed(false),
44 m_lastPressSeries(nullptr),
45 m_lastHoverSeries(nullptr)
46{
47 initializeOpenGLFunctions();
48
49 connect(sender: m_window, signal: &QQuickWindow::beforeRendering,
50 context: this, slot: &DeclarativeOpenGLRenderNode::render);
51}
52
53DeclarativeOpenGLRenderNode::~DeclarativeOpenGLRenderNode()
54{
55 cleanXYSeriesResources(series: 0);
56
57 delete m_texture;
58 delete m_fbo;
59 delete m_resolvedFbo;
60 delete m_selectionFbo;
61 delete m_program;
62
63 qDeleteAll(c: m_mouseEvents);
64}
65
66static const char *vertexSourceCore =
67 "#version 150\n"
68 "in vec2 points;\n"
69 "uniform vec2 min;\n"
70 "uniform vec2 delta;\n"
71 "uniform float pointSize;\n"
72 "uniform mat4 matrix;\n"
73 "void main() {\n"
74 " vec2 normalPoint = vec2(-1, -1) + ((points - min) / delta);\n"
75 " gl_Position = matrix * vec4(normalPoint, 0, 1);\n"
76 " gl_PointSize = pointSize;\n"
77 "}";
78static const char *fragmentSourceCore =
79 "#version 150\n"
80 "uniform vec3 color;\n"
81 "out vec4 fragColor;\n"
82 "void main() {\n"
83 " fragColor = vec4(color,1);\n"
84 "}\n";
85
86static const char *vertexSource =
87 "attribute highp vec2 points;\n"
88 "uniform highp vec2 min;\n"
89 "uniform highp vec2 delta;\n"
90 "uniform highp float pointSize;\n"
91 "uniform highp mat4 matrix;\n"
92 "void main() {\n"
93 " vec2 normalPoint = vec2(-1, -1) + ((points - min) / delta);\n"
94 " gl_Position = matrix * vec4(normalPoint, 0, 1);\n"
95 " gl_PointSize = pointSize;\n"
96 "}";
97static const char *fragmentSource =
98 "uniform highp vec3 color;\n"
99 "void main() {\n"
100 " gl_FragColor = vec4(color,1);\n"
101 "}\n";
102
103// Must be called on render thread and in context
104void DeclarativeOpenGLRenderNode::initGL()
105{
106 recreateFBO();
107
108 m_program = new QOpenGLShaderProgram;
109 if (QOpenGLContext::currentContext()->format().profile() == QSurfaceFormat::CoreProfile) {
110 m_program->addShaderFromSourceCode(type: QOpenGLShader::Vertex, source: vertexSourceCore);
111 m_program->addShaderFromSourceCode(type: QOpenGLShader::Fragment, source: fragmentSourceCore);
112 } else {
113 m_program->addShaderFromSourceCode(type: QOpenGLShader::Vertex, source: vertexSource);
114 m_program->addShaderFromSourceCode(type: QOpenGLShader::Fragment, source: fragmentSource);
115 }
116 m_program->bindAttributeLocation(name: "points", location: 0);
117 m_program->link();
118
119 m_program->bind();
120 m_colorUniformLoc = m_program->uniformLocation(name: "color");
121 m_minUniformLoc = m_program->uniformLocation(name: "min");
122 m_deltaUniformLoc = m_program->uniformLocation(name: "delta");
123 m_pointSizeUniformLoc = m_program->uniformLocation(name: "pointSize");
124 m_matrixUniformLoc = m_program->uniformLocation(name: "matrix");
125
126 // Create a vertex array object. In OpenGL ES 2.0 and OpenGL 2.x
127 // implementations this is optional and support may not be present
128 // at all. Nonetheless the below code works in all cases and makes
129 // sure there is a VAO when one is needed.
130 m_vao.create();
131 QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao);
132
133#if !QT_CONFIG(opengles2)
134 if (!QOpenGLContext::currentContext()->isOpenGLES()) {
135 // Make it possible to change point primitive size and use textures with them in
136 // the shaders. These are implicitly enabled in ES2.
137 // Qt Quick doesn't change these flags, so it should be safe to just enable them
138 // at initialization.
139 glEnable(GL_PROGRAM_POINT_SIZE);
140 }
141#endif
142
143 m_program->release();
144}
145
146void DeclarativeOpenGLRenderNode::recreateFBO()
147{
148 QOpenGLFramebufferObjectFormat fboFormat;
149 fboFormat.setAttachment(QOpenGLFramebufferObject::NoAttachment);
150
151 int samples = 0;
152 QOpenGLContext *context = QOpenGLContext::currentContext();
153
154 if (m_antialiasing && (!context->isOpenGLES() || context->format().majorVersion() >= 3))
155 samples = 4;
156 fboFormat.setSamples(samples);
157
158 delete m_fbo;
159 delete m_resolvedFbo;
160 delete m_selectionFbo;
161 m_resolvedFbo = nullptr;
162
163 m_fbo = new QOpenGLFramebufferObject(m_textureSize, fboFormat);
164 if (samples > 0)
165 m_resolvedFbo = new QOpenGLFramebufferObject(m_textureSize);
166 m_selectionFbo = new QOpenGLFramebufferObject(m_textureSize);
167
168 delete m_texture;
169 uint textureId = m_resolvedFbo ? m_resolvedFbo->texture() : m_fbo->texture();
170 m_texture = QNativeInterface::QSGOpenGLTexture::fromNative(textureId, window: m_window, size: m_textureSize, options: m_textureOptions);
171
172 if (!m_imageNode) {
173 m_imageNode = m_window->createImageNode();
174 m_imageNode->setFiltering(QSGTexture::Linear);
175 m_imageNode->setTextureCoordinatesTransform(QSGImageNode::MirrorVertically);
176 m_imageNode->setFlag(OwnedByParent);
177 if (!m_rect.isEmpty())
178 m_imageNode->setRect(m_rect);
179 appendChildNode(node: m_imageNode);
180 }
181 m_imageNode->setTexture(m_texture);
182
183 m_recreateFbo = false;
184}
185
186// Must be called on render thread and in context
187void DeclarativeOpenGLRenderNode::setTextureSize(const QSize &size)
188{
189 m_textureSize = size;
190 m_recreateFbo = true;
191 m_renderNeeded = true;
192 m_selectionRenderNeeded = true;
193}
194
195// Must be called on render thread while gui thread is blocked, and in context
196void DeclarativeOpenGLRenderNode::setSeriesData(bool mapDirty, const GLXYDataMap &dataMap)
197{
198 bool dirty = false;
199 if (mapDirty) {
200 // Series have changed, recreate map, but utilize old data where feasible
201 GLXYDataMap oldMap = m_xyDataMap;
202 m_xyDataMap.clear();
203
204 for (auto i = dataMap.begin(), end = dataMap.end(); i != end; ++i) {
205 GLXYSeriesData *data = oldMap.take(key: i.key());
206 const GLXYSeriesData *newData = i.value();
207 if (!data || newData->dirty) {
208 if (!data)
209 data = new GLXYSeriesData;
210 *data = *newData;
211 }
212 m_xyDataMap.insert(key: i.key(), value: data);
213 }
214 // Delete remaining old data
215 for (auto i = oldMap.begin(), end = oldMap.end(); i != end; ++i) {
216 delete i.value();
217 cleanXYSeriesResources(series: i.key());
218 }
219 dirty = true;
220 } else {
221 // Series have not changed, so just copy dirty data over
222 for (auto i = dataMap.begin(), end = dataMap.end(); i != end; ++i) {
223 const GLXYSeriesData *newData = i.value();
224 if (i.value()->dirty) {
225 dirty = true;
226 GLXYSeriesData *data = m_xyDataMap.value(key: i.key());
227 if (data)
228 *data = *newData;
229 }
230 }
231 }
232 if (dirty) {
233 markDirty(bits: DirtyMaterial);
234 m_renderNeeded = true;
235 m_selectionRenderNeeded = true;
236 }
237}
238
239void DeclarativeOpenGLRenderNode::setRect(const QRectF &rect)
240{
241 m_rect = rect;
242
243 if (m_imageNode)
244 m_imageNode->setRect(rect);
245}
246
247void DeclarativeOpenGLRenderNode::setAntialiasing(bool enable)
248{
249 if (m_antialiasing != enable) {
250 m_antialiasing = enable;
251 m_recreateFbo = true;
252 m_renderNeeded = true;
253 }
254}
255
256void DeclarativeOpenGLRenderNode::addMouseEvents(const QList<QMouseEvent *> &events)
257{
258 if (events.size()) {
259 m_mouseEvents.append(l: events);
260 markDirty(bits: DirtyMaterial);
261 }
262}
263
264void DeclarativeOpenGLRenderNode::takeMouseEventResponses(QList<MouseEventResponse> &responses)
265{
266 responses.append(l: m_mouseEventResponses);
267 m_mouseEventResponses.clear();
268}
269
270void DeclarativeOpenGLRenderNode::renderGL(bool selection)
271{
272 glClearColor(red: 0, green: 0, blue: 0, alpha: 0);
273
274 QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao);
275 m_program->bind();
276
277 glClear(GL_COLOR_BUFFER_BIT);
278 glEnableVertexAttribArray(index: 0);
279
280 glViewport(x: 0, y: 0, width: m_textureSize.width(), height: m_textureSize.height());
281
282 int counter = 0;
283 for (auto i = m_xyDataMap.begin(), end = m_xyDataMap.end(); i != end; ++i) {
284 QOpenGLBuffer *vbo = m_seriesBufferMap.value(key: i.key());
285 GLXYSeriesData *data = i.value();
286
287 if (data->visible) {
288 if (selection) {
289 m_selectionList[counter] = i.key();
290 m_program->setUniformValue(location: m_colorUniformLoc, value: QVector3D((counter & 0xff) / 255.0f,
291 ((counter & 0xff00) >> 8) / 255.0f,
292 ((counter & 0xff0000) >> 16) / 255.0f));
293 counter++;
294 } else {
295 m_program->setUniformValue(location: m_colorUniformLoc, value: data->color);
296 }
297 m_program->setUniformValue(location: m_minUniformLoc, value: data->min);
298 m_program->setUniformValue(location: m_deltaUniformLoc, value: data->delta);
299 m_program->setUniformValue(location: m_matrixUniformLoc, value: data->matrix);
300
301 if (!vbo) {
302 vbo = new QOpenGLBuffer;
303 m_seriesBufferMap.insert(key: i.key(), value: vbo);
304 vbo->create();
305 }
306 vbo->bind();
307 if (data->dirty) {
308 vbo->allocate(data: data->array.constData(), count: int(data->array.size() * sizeof(GLfloat)));
309 data->dirty = false;
310 }
311
312 glVertexAttribPointer(indx: 0, size: 2, GL_FLOAT, GL_FALSE, stride: 0, ptr: 0);
313 if (data->type == QAbstractSeries::SeriesTypeLine) {
314 glLineWidth(width: data->width);
315 glDrawArrays(GL_LINE_STRIP, first: 0, count: data->array.size() / 2);
316 } else { // Scatter
317 m_program->setUniformValue(location: m_pointSizeUniformLoc, value: data->width);
318 glDrawArrays(GL_POINTS, first: 0, count: data->array.size() / 2);
319 }
320 vbo->release();
321 }
322 }
323}
324
325void DeclarativeOpenGLRenderNode::renderSelection()
326{
327 m_selectionFbo->bind();
328
329 m_selectionList.resize(size: m_xyDataMap.size());
330
331 renderGL(selection: true);
332
333 m_selectionRenderNeeded = false;
334}
335
336void DeclarativeOpenGLRenderNode::renderVisual()
337{
338 m_fbo->bind();
339
340 renderGL(selection: false);
341
342 if (m_resolvedFbo) {
343 QRect rect(QPoint(0, 0), m_fbo->size());
344 QOpenGLFramebufferObject::blitFramebuffer(target: m_resolvedFbo, targetRect: rect, source: m_fbo, sourceRect: rect);
345 }
346
347 markDirty(bits: DirtyMaterial);
348
349#ifdef QDEBUG_TRACE_GL_FPS
350 static QElapsedTimer stopWatch;
351 static int frameCount = -1;
352 if (frameCount == -1) {
353 stopWatch.start();
354 frameCount = 0;
355 }
356 frameCount++;
357 int elapsed = stopWatch.elapsed();
358 if (elapsed >= 1000) {
359 elapsed = stopWatch.restart();
360 qreal fps = qreal(0.1 * int(10000.0 * (qreal(frameCount) / qreal(elapsed))));
361 qDebug() << "FPS:" << fps;
362 frameCount = 0;
363 }
364#endif
365}
366
367// Must be called on render thread as response to beforeRendering signal
368void DeclarativeOpenGLRenderNode::render()
369{
370 // Reset blend function, etc. derived from the previous frame.
371 QQuickOpenGLUtils::resetOpenGLState();
372 if (m_renderNeeded) {
373 if (m_xyDataMap.size()) {
374 if (!m_program)
375 initGL();
376 if (m_recreateFbo)
377 recreateFBO();
378 renderVisual();
379 } else {
380 if (m_imageNode && m_imageNode->rect() != QRectF()) {
381 glClearColor(red: 0, green: 0, blue: 0, alpha: 0);
382 m_fbo->bind();
383 glClear(GL_COLOR_BUFFER_BIT);
384
385 // If last series was removed, zero out the node rect
386 setRect(QRectF());
387 }
388 }
389 m_renderNeeded = false;
390 }
391 handleMouseEvents();
392 QQuickOpenGLUtils::resetOpenGLState();
393}
394
395void DeclarativeOpenGLRenderNode::cleanXYSeriesResources(const QXYSeries *series)
396{
397 if (series) {
398 delete m_seriesBufferMap.take(key: series);
399 delete m_xyDataMap.take(key: series);
400 } else {
401 foreach (QOpenGLBuffer *buffer, m_seriesBufferMap.values())
402 delete buffer;
403 m_seriesBufferMap.clear();
404 foreach (GLXYSeriesData *data, m_xyDataMap.values())
405 delete data;
406 m_xyDataMap.clear();
407 }
408}
409
410void DeclarativeOpenGLRenderNode::handleMouseEvents()
411{
412 if (m_mouseEvents.size()) {
413 if (m_xyDataMap.size()) {
414 if (m_selectionRenderNeeded)
415 renderSelection();
416 }
417 Q_FOREACH (QMouseEvent *event, m_mouseEvents) {
418 const QXYSeries *series = findSeriesAtEvent(event);
419 switch (event->type()) {
420 case QEvent::MouseMove: {
421 if (series != m_lastHoverSeries) {
422 if (m_lastHoverSeries) {
423 m_mouseEventResponses.append(
424 t: MouseEventResponse(MouseEventResponse::HoverLeave,
425 event->pos(), m_lastHoverSeries));
426 }
427 if (series) {
428 m_mouseEventResponses.append(
429 t: MouseEventResponse(MouseEventResponse::HoverEnter,
430 event->pos(), series));
431 }
432 m_lastHoverSeries = series;
433 }
434 break;
435 }
436 case QEvent::MouseButtonPress: {
437 if (series) {
438 m_mousePressed = true;
439 m_mousePressPos = event->pos();
440 m_lastPressSeries = series;
441 m_mouseEventResponses.append(
442 t: MouseEventResponse(MouseEventResponse::Pressed,
443 event->pos(), series));
444 }
445 break;
446 }
447 case QEvent::MouseButtonRelease: {
448 m_mouseEventResponses.append(
449 t: MouseEventResponse(MouseEventResponse::Released,
450 m_mousePressPos, m_lastPressSeries));
451 if (m_mousePressed) {
452 m_mouseEventResponses.append(
453 t: MouseEventResponse(MouseEventResponse::Clicked,
454 m_mousePressPos, m_lastPressSeries));
455 }
456 if (m_lastHoverSeries == m_lastPressSeries && m_lastHoverSeries != series) {
457 if (m_lastHoverSeries) {
458 m_mouseEventResponses.append(
459 t: MouseEventResponse(MouseEventResponse::HoverLeave,
460 event->pos(), m_lastHoverSeries));
461 }
462 m_lastHoverSeries = nullptr;
463 }
464 m_lastPressSeries = nullptr;
465 m_mousePressed = false;
466 break;
467 }
468 case QEvent::MouseButtonDblClick: {
469 if (series) {
470 m_mouseEventResponses.append(
471 t: MouseEventResponse(MouseEventResponse::DoubleClicked,
472 event->pos(), series));
473 }
474 break;
475 }
476 default:
477 break;
478 }
479 }
480
481 qDeleteAll(c: m_mouseEvents);
482 m_mouseEvents.clear();
483 }
484}
485
486const QXYSeries *DeclarativeOpenGLRenderNode::findSeriesAtEvent(QMouseEvent *event)
487{
488 const QXYSeries *series = nullptr;
489 int index = -1;
490
491 if (m_xyDataMap.size()) {
492 m_selectionFbo->bind();
493
494 GLubyte pixel[4] = {0, 0, 0, 0};
495 glReadPixels(x: event->pos().x(), y: m_textureSize.height() - event->pos().y(),
496 width: 1, height: 1, GL_RGBA, GL_UNSIGNED_BYTE,
497 pixels: (void *)pixel);
498 if (pixel[3] == 0xff)
499 index = pixel[0] + (pixel[1] << 8) + (pixel[2] << 16);
500 }
501
502 if (index >= 0 && index < m_selectionList.size())
503 series = m_selectionList.at(i: index);
504
505 return series;
506}
507
508QT_END_NAMESPACE
509

source code of qtcharts/src/chartsqml2/declarativeopenglrendernode.cpp