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

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