| 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 | |