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 | |
21 | QT_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. |
25 | DeclarativeOpenGLRenderNode::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 | |
55 | DeclarativeOpenGLRenderNode::~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 | |
68 | static 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 | "}" ; |
80 | static 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 | |
88 | static 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 | "}" ; |
99 | static 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 |
106 | void 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 | |
148 | void 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 |
189 | void 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 |
198 | void 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 | |
241 | void DeclarativeOpenGLRenderNode::setRect(const QRectF &rect) |
242 | { |
243 | m_rect = rect; |
244 | |
245 | if (m_imageNode) |
246 | m_imageNode->setRect(rect); |
247 | } |
248 | |
249 | void 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 | |
258 | void DeclarativeOpenGLRenderNode::addMouseEvents(const QList<QMouseEvent *> &events) |
259 | { |
260 | if (events.size()) { |
261 | m_mouseEvents.append(l: events); |
262 | markDirty(bits: DirtyMaterial); |
263 | } |
264 | } |
265 | |
266 | void DeclarativeOpenGLRenderNode::takeMouseEventResponses(QList<MouseEventResponse> &responses) |
267 | { |
268 | responses.append(l: m_mouseEventResponses); |
269 | m_mouseEventResponses.clear(); |
270 | } |
271 | |
272 | void 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 | |
327 | void 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 | |
338 | void 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 |
370 | void 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 | |
397 | void 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 | |
412 | void 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 | |
488 | const 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 | |
510 | QT_END_NAMESPACE |
511 | |