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