1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #ifndef QT_NO_OPENGL |
5 | |
6 | #include "private/glwidget_p.h" |
7 | #include "private/glxyseriesdata_p.h" |
8 | #include "private/qabstractseries_p.h" |
9 | #include <QtOpenGL/QOpenGLShaderProgram> |
10 | #include <QtGui/QOpenGLContext> |
11 | #include <QtOpenGL/QOpenGLBuffer> |
12 | |
13 | //#define QDEBUG_TRACE_GL_FPS |
14 | #ifdef QDEBUG_TRACE_GL_FPS |
15 | # include <QElapsedTimer> |
16 | #endif |
17 | |
18 | QT_BEGIN_NAMESPACE |
19 | |
20 | GLWidget::GLWidget(GLXYSeriesDataManager *xyDataManager, QChart *chart, |
21 | QGraphicsView *parent) |
22 | : QOpenGLWidget(parent->viewport()), |
23 | m_program(nullptr), |
24 | m_shaderAttribLoc(-1), |
25 | m_colorUniformLoc(-1), |
26 | m_minUniformLoc(-1), |
27 | m_deltaUniformLoc(-1), |
28 | m_pointSizeUniformLoc(-1), |
29 | m_xyDataManager(xyDataManager), |
30 | m_antiAlias(parent->renderHints().testFlag(flag: QPainter::Antialiasing)), |
31 | m_view(parent), |
32 | m_selectionFbo(nullptr), |
33 | m_chart(chart), |
34 | m_recreateSelectionFbo(true), |
35 | m_selectionRenderNeeded(true), |
36 | m_mousePressed(false), |
37 | m_lastPressSeries(nullptr), |
38 | m_lastHoverSeries(nullptr) |
39 | { |
40 | setAttribute(Qt::WA_TranslucentBackground); |
41 | setAttribute(Qt::WA_AlwaysStackOnTop); |
42 | |
43 | QSurfaceFormat surfaceFormat; |
44 | surfaceFormat.setDepthBufferSize(0); |
45 | surfaceFormat.setStencilBufferSize(0); |
46 | surfaceFormat.setRedBufferSize(8); |
47 | surfaceFormat.setGreenBufferSize(8); |
48 | surfaceFormat.setBlueBufferSize(8); |
49 | surfaceFormat.setAlphaBufferSize(8); |
50 | surfaceFormat.setSwapBehavior(QSurfaceFormat::DoubleBuffer); |
51 | surfaceFormat.setRenderableType(QSurfaceFormat::DefaultRenderableType); |
52 | surfaceFormat.setSamples(m_antiAlias ? 4 : 0); |
53 | setFormat(surfaceFormat); |
54 | |
55 | connect(sender: xyDataManager, signal: &GLXYSeriesDataManager::seriesRemoved, |
56 | context: this, slot: &GLWidget::cleanXYSeriesResources); |
57 | |
58 | setMouseTracking(true); |
59 | } |
60 | |
61 | GLWidget::~GLWidget() |
62 | { |
63 | cleanup(); |
64 | } |
65 | |
66 | void GLWidget::cleanup() |
67 | { |
68 | makeCurrent(); |
69 | |
70 | delete m_program; |
71 | m_program = 0; |
72 | |
73 | foreach (QOpenGLBuffer *buffer, m_seriesBufferMap.values()) |
74 | delete buffer; |
75 | m_seriesBufferMap.clear(); |
76 | |
77 | doneCurrent(); |
78 | } |
79 | |
80 | void GLWidget::cleanXYSeriesResources(const QXYSeries *series) |
81 | { |
82 | makeCurrent(); |
83 | if (series) { |
84 | delete m_seriesBufferMap.take(key: series); |
85 | } else { |
86 | // Null series means all series were removed |
87 | foreach (QOpenGLBuffer *buffer, m_seriesBufferMap.values()) |
88 | delete buffer; |
89 | m_seriesBufferMap.clear(); |
90 | } |
91 | doneCurrent(); |
92 | } |
93 | |
94 | static const char *vertexSource = |
95 | "attribute highp vec2 points;\n" |
96 | "uniform highp vec2 min;\n" |
97 | "uniform highp vec2 delta;\n" |
98 | "uniform highp float pointSize;\n" |
99 | "uniform highp mat4 matrix;\n" |
100 | "void main() {\n" |
101 | " vec2 normalPoint = vec2(-1, -1) + ((points - min) / delta);\n" |
102 | " gl_Position = matrix * vec4(normalPoint, 0, 1);\n" |
103 | " gl_PointSize = pointSize;\n" |
104 | "}" ; |
105 | static const char *fragmentSource = |
106 | "uniform highp vec3 color;\n" |
107 | "void main() {\n" |
108 | " gl_FragColor = vec4(color,1);\n" |
109 | "}\n" ; |
110 | |
111 | void GLWidget::initializeGL() |
112 | { |
113 | connect(sender: context(), signal: &QOpenGLContext::aboutToBeDestroyed, context: this, slot: &GLWidget::cleanup); |
114 | |
115 | initializeOpenGLFunctions(); |
116 | glClearColor(red: 0, green: 0, blue: 0, alpha: 0); |
117 | |
118 | m_program = new QOpenGLShaderProgram; |
119 | m_program->addShaderFromSourceCode(type: QOpenGLShader::Vertex, source: vertexSource); |
120 | m_program->addShaderFromSourceCode(type: QOpenGLShader::Fragment, source: fragmentSource); |
121 | m_program->bindAttributeLocation(name: "points" , location: 0); |
122 | m_program->link(); |
123 | |
124 | m_program->bind(); |
125 | m_colorUniformLoc = m_program->uniformLocation(name: "color" ); |
126 | m_minUniformLoc = m_program->uniformLocation(name: "min" ); |
127 | m_deltaUniformLoc = m_program->uniformLocation(name: "delta" ); |
128 | m_pointSizeUniformLoc = m_program->uniformLocation(name: "pointSize" ); |
129 | m_matrixUniformLoc = m_program->uniformLocation(name: "matrix" ); |
130 | |
131 | |
132 | // Create a vertex array object. In OpenGL ES 2.0 and OpenGL 2.x |
133 | // implementations this is optional and support may not be present |
134 | // at all. Nonetheless the below code works in all cases and makes |
135 | // sure there is a VAO when one is needed. |
136 | m_vao.create(); |
137 | QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); |
138 | |
139 | glEnableVertexAttribArray(index: 0); |
140 | |
141 | glDisable(GL_DEPTH_TEST); |
142 | glDisable(GL_STENCIL_TEST); |
143 | |
144 | #if !QT_CONFIG(opengles2) |
145 | if (!QOpenGLContext::currentContext()->isOpenGLES()) { |
146 | // Make it possible to change point primitive size and use textures with them in |
147 | // the shaders. These are implicitly enabled in ES2. |
148 | glEnable(GL_PROGRAM_POINT_SIZE); |
149 | } |
150 | #endif |
151 | |
152 | m_program->release(); |
153 | } |
154 | |
155 | void GLWidget::paintGL() |
156 | { |
157 | render(selection: false); |
158 | |
159 | #ifdef QDEBUG_TRACE_GL_FPS |
160 | static QElapsedTimer stopWatch; |
161 | static int frameCount = -1; |
162 | if (frameCount == -1) { |
163 | stopWatch.start(); |
164 | frameCount = 0; |
165 | } |
166 | frameCount++; |
167 | int elapsed = stopWatch.elapsed(); |
168 | if (elapsed >= 1000) { |
169 | elapsed = stopWatch.restart(); |
170 | qreal fps = qreal(0.1 * int(10000.0 * (qreal(frameCount) / qreal(elapsed)))); |
171 | qDebug() << "FPS:" << fps; |
172 | frameCount = 0; |
173 | } |
174 | #endif |
175 | } |
176 | |
177 | void GLWidget::resizeGL(int w, int h) |
178 | { |
179 | m_fboSize.setWidth(w); |
180 | m_fboSize.setHeight(h); |
181 | m_recreateSelectionFbo = true; |
182 | m_selectionRenderNeeded = true; |
183 | } |
184 | |
185 | void GLWidget::mouseDoubleClickEvent(QMouseEvent *event) |
186 | { |
187 | QXYSeries *series = findSeriesAtEvent(event); |
188 | if (series) |
189 | emit series->doubleClicked(point: series->d_ptr->domain()->calculateDomainPoint(point: event->pos())); |
190 | } |
191 | |
192 | void GLWidget::mouseMoveEvent(QMouseEvent *event) |
193 | { |
194 | if (m_view->hasMouseTracking() && !event->buttons()) { |
195 | QXYSeries *series = findSeriesAtEvent(event); |
196 | if (series != m_lastHoverSeries) { |
197 | if (m_lastHoverSeries) { |
198 | if (chartSeries(cSeries: m_lastHoverSeries)) { |
199 | emit m_lastHoverSeries->hovered( |
200 | point: m_lastHoverSeries->d_ptr->domain()->calculateDomainPoint( |
201 | point: event->pos()), state: false); |
202 | } |
203 | } |
204 | if (series) { |
205 | emit series->hovered( |
206 | point: series->d_ptr->domain()->calculateDomainPoint(point: event->pos()), state: true); |
207 | } |
208 | m_lastHoverSeries = series; |
209 | } |
210 | } else { |
211 | event->ignore(); |
212 | } |
213 | } |
214 | |
215 | void GLWidget::mousePressEvent(QMouseEvent *event) |
216 | { |
217 | QXYSeries *series = findSeriesAtEvent(event); |
218 | if (series) { |
219 | m_mousePressed = true; |
220 | m_mousePressPos = event->pos(); |
221 | m_lastPressSeries = series; |
222 | emit series->pressed(point: series->d_ptr->domain()->calculateDomainPoint(point: event->pos())); |
223 | } |
224 | } |
225 | |
226 | void GLWidget::mouseReleaseEvent(QMouseEvent *event) |
227 | { |
228 | if (chartSeries(cSeries: m_lastPressSeries)) { |
229 | emit m_lastPressSeries->released( |
230 | point: m_lastPressSeries->d_ptr->domain()->calculateDomainPoint(point: m_mousePressPos)); |
231 | if (m_mousePressed) { |
232 | emit m_lastPressSeries->clicked( |
233 | point: m_lastPressSeries->d_ptr->domain()->calculateDomainPoint(point: m_mousePressPos)); |
234 | } |
235 | if (m_lastHoverSeries == m_lastPressSeries |
236 | && m_lastHoverSeries != findSeriesAtEvent(event)) { |
237 | if (chartSeries(cSeries: m_lastHoverSeries)) { |
238 | emit m_lastHoverSeries->hovered( |
239 | point: m_lastHoverSeries->d_ptr->domain()->calculateDomainPoint( |
240 | point: event->pos()), state: false); |
241 | } |
242 | m_lastHoverSeries = nullptr; |
243 | } |
244 | m_lastPressSeries = nullptr; |
245 | m_mousePressed = false; |
246 | } else { |
247 | event->ignore(); |
248 | } |
249 | } |
250 | |
251 | QXYSeries *GLWidget::findSeriesAtEvent(QMouseEvent *event) |
252 | { |
253 | QXYSeries *series = nullptr; |
254 | int index = -1; |
255 | |
256 | if (m_xyDataManager->dataMap().size()) { |
257 | makeCurrent(); |
258 | |
259 | if (m_recreateSelectionFbo) |
260 | recreateSelectionFbo(); |
261 | |
262 | m_selectionFbo->bind(); |
263 | |
264 | if (m_selectionRenderNeeded) { |
265 | m_selectionList.resize(size: m_xyDataManager->dataMap().size()); |
266 | render(selection: true); |
267 | m_selectionRenderNeeded = false; |
268 | } |
269 | |
270 | GLubyte pixel[4] = {0, 0, 0, 0}; |
271 | glReadPixels(x: event->pos().x(), y: m_fboSize.height() - event->pos().y(), |
272 | width: 1, height: 1, GL_RGBA, GL_UNSIGNED_BYTE, |
273 | pixels: (void *)pixel); |
274 | if (pixel[3] == 0xff) |
275 | index = pixel[0] + (pixel[1] << 8) + (pixel[2] << 16); |
276 | |
277 | glBindFramebuffer(GL_FRAMEBUFFER, framebuffer: defaultFramebufferObject()); |
278 | |
279 | doneCurrent(); |
280 | } |
281 | |
282 | if (index >= 0) { |
283 | const QXYSeries *cSeries = nullptr; |
284 | if (index < m_selectionList.size()) |
285 | cSeries = m_selectionList.at(i: index); |
286 | |
287 | series = chartSeries(cSeries); |
288 | } |
289 | if (series) |
290 | event->accept(); |
291 | else |
292 | event->ignore(); |
293 | |
294 | return series; |
295 | } |
296 | |
297 | void GLWidget::render(bool selection) |
298 | { |
299 | glClear(GL_COLOR_BUFFER_BIT); |
300 | |
301 | QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); |
302 | m_program->bind(); |
303 | |
304 | int counter = 0; |
305 | const auto &dataMap = m_xyDataManager->dataMap(); |
306 | for (auto i = dataMap.begin(), end = dataMap.end(); i != end; ++i) { |
307 | QOpenGLBuffer *vbo = m_seriesBufferMap.value(key: i.key()); |
308 | GLXYSeriesData *data = i.value(); |
309 | |
310 | if (data->visible) { |
311 | if (selection) { |
312 | m_selectionList[counter] = i.key(); |
313 | m_program->setUniformValue(location: m_colorUniformLoc, value: QVector3D((counter & 0xff) / 255.0f, |
314 | ((counter & 0xff00) >> 8) / 255.0f, |
315 | ((counter & 0xff0000) >> 16) / 255.0f)); |
316 | counter++; |
317 | } else { |
318 | m_program->setUniformValue(location: m_colorUniformLoc, value: data->color); |
319 | } |
320 | m_program->setUniformValue(location: m_minUniformLoc, value: data->min); |
321 | m_program->setUniformValue(location: m_deltaUniformLoc, value: data->delta); |
322 | m_program->setUniformValue(location: m_matrixUniformLoc, value: data->matrix); |
323 | bool dirty = data->dirty; |
324 | if (!vbo) { |
325 | vbo = new QOpenGLBuffer; |
326 | m_seriesBufferMap.insert(key: i.key(), value: vbo); |
327 | vbo->create(); |
328 | dirty = true; |
329 | } |
330 | vbo->bind(); |
331 | if (dirty) { |
332 | vbo->allocate(data: data->array.constData(), count: int(data->array.size() * sizeof(GLfloat))); |
333 | dirty = false; |
334 | m_selectionRenderNeeded = true; |
335 | } |
336 | |
337 | glVertexAttribPointer(indx: 0, size: 2, GL_FLOAT, GL_FALSE, stride: 0, ptr: 0); |
338 | if (data->type == QAbstractSeries::SeriesTypeLine) { |
339 | glLineWidth(width: data->width); |
340 | glDrawArrays(GL_LINE_STRIP, first: 0, count: data->array.size() / 2); |
341 | } else { // Scatter |
342 | m_program->setUniformValue(location: m_pointSizeUniformLoc, value: data->width); |
343 | glDrawArrays(GL_POINTS, first: 0, count: data->array.size() / 2); |
344 | } |
345 | vbo->release(); |
346 | } |
347 | } |
348 | m_program->release(); |
349 | } |
350 | |
351 | void GLWidget::recreateSelectionFbo() |
352 | { |
353 | QOpenGLFramebufferObjectFormat fboFormat; |
354 | fboFormat.setAttachment(QOpenGLFramebufferObject::NoAttachment); |
355 | |
356 | delete m_selectionFbo; |
357 | |
358 | const QSize deviceSize = m_fboSize * devicePixelRatio(); |
359 | m_selectionFbo = new QOpenGLFramebufferObject(deviceSize, fboFormat); |
360 | m_recreateSelectionFbo = false; |
361 | m_selectionRenderNeeded = true; |
362 | } |
363 | |
364 | // This function makes sure the series we are dealing with has not been removed from the |
365 | // chart since we stored the pointer. |
366 | QXYSeries *GLWidget::chartSeries(const QXYSeries *cSeries) |
367 | { |
368 | QXYSeries *series = nullptr; |
369 | if (cSeries) { |
370 | Q_FOREACH (QAbstractSeries *chartSeries, m_chart->series()) { |
371 | if (cSeries == chartSeries) { |
372 | series = qobject_cast<QXYSeries *>(object: chartSeries); |
373 | break; |
374 | } |
375 | } |
376 | } |
377 | return series; |
378 | } |
379 | |
380 | bool GLWidget::needsReset() const |
381 | { |
382 | return m_view->renderHints().testFlag(flag: QPainter::Antialiasing) != m_antiAlias; |
383 | } |
384 | |
385 | QT_END_NAMESPACE |
386 | |
387 | #endif |
388 | |