| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the plugins of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "qeglfscursor_p.h" |
| #include "qeglfsintegration_p.h" |
| #include "qeglfsscreen_p.h" |
| #include "qeglfscontext_p.h" |
| |
| #include <qpa/qwindowsysteminterface.h> |
| #include <QtGui/QOpenGLContext> |
| #include <QtCore/QFile> |
| #include <QtCore/QJsonDocument> |
| #include <QtCore/QJsonArray> |
| #include <QtCore/QJsonObject> |
| |
| #include <QtGui/private/qguiapplication_p.h> |
| #include <QtGui/private/qopenglvertexarrayobject_p.h> |
| |
| #ifndef GL_VERTEX_ARRAY_BINDING |
| #define GL_VERTEX_ARRAY_BINDING 0x85B5 |
| #endif |
| |
| QT_BEGIN_NAMESPACE |
| |
| QEglFSCursor::QEglFSCursor(QPlatformScreen *screen) |
| : m_visible(true), |
| m_screen(static_cast<QEglFSScreen *>(screen)), |
| m_activeScreen(nullptr), |
| m_deviceListener(0), |
| m_updateRequested(false) |
| { |
| QByteArray hideCursorVal = qgetenv("QT_QPA_EGLFS_HIDECURSOR"); |
| if (!hideCursorVal.isEmpty()) |
| m_visible = hideCursorVal.toInt() == 0; |
| if (!m_visible) |
| return; |
| |
| int rotation = qEnvironmentVariableIntValue("QT_QPA_EGLFS_ROTATION"); |
| if (rotation) |
| m_rotationMatrix.rotate(rotation, 0, 0, 1); |
| |
| // Try to load the cursor atlas. If this fails, m_visible is set to false and |
| // paintOnScreen() and setCurrentCursor() become no-ops. |
| initCursorAtlas(); |
| |
| // initialize the cursor |
| #ifndef QT_NO_CURSOR |
| QCursor cursor(Qt::ArrowCursor); |
| setCurrentCursor(&cursor); |
| #endif |
| |
| m_deviceListener = new QEglFSCursorDeviceListener(this); |
| connect(QGuiApplicationPrivate::inputDeviceManager(), &QInputDeviceManager::deviceListChanged, |
| m_deviceListener, &QEglFSCursorDeviceListener::onDeviceListChanged); |
| updateMouseStatus(); |
| } |
| |
| QEglFSCursor::~QEglFSCursor() |
| { |
| resetResources(); |
| delete m_deviceListener; |
| } |
| |
| void QEglFSCursor::updateMouseStatus() |
| { |
| m_visible = m_deviceListener->hasMouse(); |
| } |
| |
| bool QEglFSCursorDeviceListener::hasMouse() const |
| { |
| return QGuiApplicationPrivate::inputDeviceManager()->deviceCount(QInputDeviceManager::DeviceTypePointer) > 0; |
| } |
| |
| void QEglFSCursorDeviceListener::onDeviceListChanged(QInputDeviceManager::DeviceType type) |
| { |
| if (type == QInputDeviceManager::DeviceTypePointer) |
| m_cursor->updateMouseStatus(); |
| } |
| |
| void QEglFSCursor::resetResources() |
| { |
| m_cursor.customCursorPending = !m_cursor.customCursorImage.isNull(); |
| } |
| |
| void QEglFSCursor::createShaderPrograms() |
| { |
| static const char *textureVertexProgram = |
| "attribute highp vec2 vertexCoordEntry;\n" |
| "attribute highp vec2 textureCoordEntry;\n" |
| "varying highp vec2 textureCoord;\n" |
| "uniform highp mat4 mat;\n" |
| "void main() {\n" |
| " textureCoord = textureCoordEntry;\n" |
| " gl_Position = mat * vec4(vertexCoordEntry, 1.0, 1.0);\n" |
| "}\n"; |
| |
| static const char *textureFragmentProgram = |
| "uniform sampler2D texture;\n" |
| "varying highp vec2 textureCoord;\n" |
| "void main() {\n" |
| " gl_FragColor = texture2D(texture, textureCoord).bgra;\n" |
| "}\n"; |
| |
| QEglFSCursorData &gfx = static_cast<QEglFSContext*>(QOpenGLContext::currentContext()->handle())->cursorData; |
| gfx.program.reset(new QOpenGLShaderProgram); |
| gfx.program->addCacheableShaderFromSourceCode(QOpenGLShader::Vertex, textureVertexProgram); |
| gfx.program->addCacheableShaderFromSourceCode(QOpenGLShader::Fragment, textureFragmentProgram); |
| gfx.program->bindAttributeLocation("vertexCoordEntry", 0); |
| gfx.program->bindAttributeLocation("textureCoordEntry", 1); |
| gfx.program->link(); |
| |
| gfx.textureEntry = gfx.program->uniformLocation("texture"); |
| gfx.matEntry = gfx.program->uniformLocation("mat"); |
| } |
| |
| void QEglFSCursor::createCursorTexture(uint *texture, const QImage &image) |
| { |
| if (!*texture) |
| glGenTextures(1, texture); |
| glBindTexture(GL_TEXTURE_2D, *texture); |
| glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
| glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
| glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); |
| glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); |
| |
| glTexImage2D(GL_TEXTURE_2D, 0 /* level */, GL_RGBA, image.width(), image.height(), 0 /* border */, |
| GL_RGBA, GL_UNSIGNED_BYTE, image.constBits()); |
| } |
| |
| void QEglFSCursor::initCursorAtlas() |
| { |
| static QByteArray json = qgetenv("QT_QPA_EGLFS_CURSOR"); |
| if (json.isEmpty()) |
| json = ":/cursor.json"; |
| |
| QFile file(QString::fromUtf8(json)); |
| if (!file.open(QFile::ReadOnly)) { |
| m_visible = false; |
| return; |
| } |
| |
| QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); |
| QJsonObject object = doc.object(); |
| |
| QString atlas = object.value(QLatin1String("image")).toString(); |
| Q_ASSERT(!atlas.isEmpty()); |
| |
| const int cursorsPerRow = object.value(QLatin1String("cursorsPerRow")).toDouble(); |
| Q_ASSERT(cursorsPerRow); |
| m_cursorAtlas.cursorsPerRow = cursorsPerRow; |
| |
| const QJsonArray hotSpots = object.value(QLatin1String("hotSpots")).toArray(); |
| Q_ASSERT(hotSpots.count() == Qt::LastCursor + 1); |
| for (int i = 0; i < hotSpots.count(); i++) { |
| QPoint hotSpot(hotSpots[i].toArray()[0].toDouble(), hotSpots[i].toArray()[1].toDouble()); |
| m_cursorAtlas.hotSpots << hotSpot; |
| } |
| |
| QImage image = QImage(atlas).convertToFormat(QImage::Format_ARGB32_Premultiplied); |
| m_cursorAtlas.cursorWidth = image.width() / m_cursorAtlas.cursorsPerRow; |
| m_cursorAtlas.cursorHeight = image.height() / ((Qt::LastCursor + cursorsPerRow) / cursorsPerRow); |
| m_cursorAtlas.width = image.width(); |
| m_cursorAtlas.height = image.height(); |
| m_cursorAtlas.image = image; |
| } |
| |
| #ifndef QT_NO_CURSOR |
| void QEglFSCursor::changeCursor(QCursor *cursor, QWindow *window) |
| { |
| Q_UNUSED(window); |
| const QRect oldCursorRect = cursorRect(); |
| if (setCurrentCursor(cursor)) |
| update(oldCursorRect | cursorRect(), false); |
| } |
| |
| bool QEglFSCursor::setCurrentCursor(QCursor *cursor) |
| { |
| if (!m_visible) |
| return false; |
| |
| const Qt::CursorShape newShape = cursor ? cursor->shape() : Qt::ArrowCursor; |
| if (m_cursor.shape == newShape && newShape != Qt::BitmapCursor) |
| return false; |
| |
| if (m_cursor.shape == Qt::BitmapCursor) { |
| m_cursor.customCursorImage = QImage(); |
| m_cursor.customCursorPending = false; |
| } |
| m_cursor.shape = newShape; |
| if (newShape != Qt::BitmapCursor) { // standard cursor |
| const float ws = (float)m_cursorAtlas.cursorWidth / m_cursorAtlas.width, |
| hs = (float)m_cursorAtlas.cursorHeight / m_cursorAtlas.height; |
| m_cursor.textureRect = QRectF(ws * (m_cursor.shape % m_cursorAtlas.cursorsPerRow), |
| hs * (m_cursor.shape / m_cursorAtlas.cursorsPerRow), |
| ws, hs); |
| m_cursor.hotSpot = m_cursorAtlas.hotSpots[m_cursor.shape]; |
| m_cursor.useCustomCursor = false; |
| m_cursor.size = QSize(m_cursorAtlas.cursorWidth, m_cursorAtlas.cursorHeight); |
| } else { |
| QImage image = cursor->pixmap().toImage(); |
| m_cursor.textureRect = QRectF(0, 0, 1, 1); |
| m_cursor.hotSpot = cursor->hotSpot(); |
| m_cursor.useCustomCursor = false; // will get updated in the next render() |
| m_cursor.size = image.size(); |
| m_cursor.customCursorImage = image; |
| m_cursor.customCursorPending = true; |
| m_cursor.customCursorKey = m_cursor.customCursorImage.cacheKey(); |
| } |
| |
| return true; |
| } |
| #endif |
| |
| class CursorUpdateEvent : public QEvent |
| { |
| public: |
| CursorUpdateEvent(const QPoint &pos, const QRect &rect, bool allScreens) |
| : QEvent(QEvent::Type(QEvent::User + 1)), |
| m_pos(pos), |
| m_rect(rect), |
| m_allScreens(allScreens) |
| { } |
| QPoint pos() const { return m_pos; } |
| QRegion rect() const { return m_rect; } |
| bool allScreens() const { return m_allScreens; } |
| |
| private: |
| QPoint m_pos; |
| QRect m_rect; |
| bool m_allScreens; |
| }; |
| |
| bool QEglFSCursor::event(QEvent *e) |
| { |
| if (e->type() == QEvent::User + 1) { |
| CursorUpdateEvent *ev = static_cast<CursorUpdateEvent *>(e); |
| m_updateRequested = false; |
| if (!ev->allScreens()) { |
| QWindow *w = m_screen->topLevelAt(ev->pos()); // works for the entire virtual desktop, no need to loop |
| if (w) { |
| QWindowSystemInterface::handleExposeEvent(w, ev->rect()); |
| QWindowSystemInterface::flushWindowSystemEvents(QEventLoop::ExcludeUserInputEvents); |
| } |
| } else { |
| for (QWindow *w : qGuiApp->topLevelWindows()) |
| QWindowSystemInterface::handleExposeEvent(w, w->geometry()); |
| QWindowSystemInterface::flushWindowSystemEvents(QEventLoop::ExcludeUserInputEvents); |
| } |
| return true; |
| } |
| return QPlatformCursor::event(e); |
| } |
| |
| void QEglFSCursor::update(const QRect &rect, bool allScreens) |
| { |
| if (!m_updateRequested) { |
| // Must not flush the window system events directly from here since we are likely to |
| // be a called directly from QGuiApplication's processMouseEvents. Flushing events |
| // could cause reentering by dispatching more queued mouse events. |
| m_updateRequested = true; |
| QCoreApplication::postEvent(this, new CursorUpdateEvent(m_cursor.pos, rect, allScreens)); |
| } |
| } |
| |
| QRect QEglFSCursor::cursorRect() const |
| { |
| return QRect(m_cursor.pos - m_cursor.hotSpot, m_cursor.size); |
| } |
| |
| QPoint QEglFSCursor::pos() const |
| { |
| return m_cursor.pos; |
| } |
| |
| void QEglFSCursor::setPos(const QPoint &pos) |
| { |
| QGuiApplicationPrivate::inputDeviceManager()->setCursorPos(pos); |
| const QRect oldCursorRect = cursorRect(); |
| m_cursor.pos = pos; |
| update(oldCursorRect | cursorRect(), false); |
| for (QPlatformScreen *screen : m_screen->virtualSiblings()) |
| static_cast<QEglFSScreen *>(screen)->handleCursorMove(m_cursor.pos); |
| } |
| |
| void QEglFSCursor::pointerEvent(const QMouseEvent &event) |
| { |
| if (event.type() != QEvent::MouseMove) |
| return; |
| const QRect oldCursorRect = cursorRect(); |
| m_cursor.pos = event.screenPos().toPoint(); |
| update(oldCursorRect | cursorRect(), false); |
| for (QPlatformScreen *screen : m_screen->virtualSiblings()) |
| static_cast<QEglFSScreen *>(screen)->handleCursorMove(m_cursor.pos); |
| } |
| |
| void QEglFSCursor::paintOnScreen() |
| { |
| if (!m_visible) |
| return; |
| |
| // cr must be a QRectF, otherwise cr.right() and bottom() would be off by |
| // one in the calculations below. |
| QRectF cr = cursorRect(); // hotspot included |
| |
| // Support virtual desktop too. Backends with multi-screen support (e.g. all |
| // variants of KMS/DRM) will enable this by default. In this case all |
| // screens are siblings of each other. When not enabled, the sibling list |
| // only contains m_screen itself. |
| for (QPlatformScreen *screen : m_screen->virtualSiblings()) { |
| if (screen->geometry().contains(cr.topLeft().toPoint() + m_cursor.hotSpot) |
| && QOpenGLContext::currentContext()->screen() == screen->screen()) |
| { |
| cr.translate(-screen->geometry().topLeft()); |
| const QSize screenSize = screen->geometry().size(); |
| const GLfloat x1 = 2 * (cr.left() / GLfloat(screenSize.width())) - 1; |
| const GLfloat x2 = 2 * (cr.right() / GLfloat(screenSize.width())) - 1; |
| const GLfloat y1 = 1 - (cr.top() / GLfloat(screenSize.height())) * 2; |
| const GLfloat y2 = 1 - (cr.bottom() / GLfloat(screenSize.height())) * 2; |
| QRectF r(QPointF(x1, y1), QPointF(x2, y2)); |
| |
| draw(r); |
| |
| if (screen != m_activeScreen) { |
| m_activeScreen = screen; |
| // Do not want a leftover cursor on the screen the cursor just left. |
| update(cursorRect(), true); |
| } |
| |
| break; |
| } |
| } |
| } |
| |
| // In order to prevent breaking code doing custom OpenGL rendering while |
| // expecting the state in the context unchanged, save and restore all the state |
| // we touch. The exception is Qt Quick where the scenegraph is known to be able |
| // to deal with the changes we make. |
| struct StateSaver |
| { |
| StateSaver() { |
| f = QOpenGLContext::currentContext()->functions(); |
| vaoHelper = new QOpenGLVertexArrayObjectHelper(QOpenGLContext::currentContext()); |
| |
| static bool windowsChecked = false; |
| static bool shouldSave = true; |
| if (!windowsChecked) { |
| windowsChecked = true; |
| QWindowList windows = QGuiApplication::allWindows(); |
| if (!windows.isEmpty() && windows[0]->inherits("QQuickWindow")) |
| shouldSave = false; |
| } |
| saved = shouldSave; |
| if (!shouldSave) |
| return; |
| |
| f->glGetIntegerv(GL_CURRENT_PROGRAM, &program); |
| f->glGetIntegerv(GL_TEXTURE_BINDING_2D, &texture); |
| f->glGetIntegerv(GL_ACTIVE_TEXTURE, &activeTexture); |
| f->glGetIntegerv(GL_FRONT_FACE, &frontFace); |
| cull = f->glIsEnabled(GL_CULL_FACE); |
| depthTest = f->glIsEnabled(GL_DEPTH_TEST); |
| blend = f->glIsEnabled(GL_BLEND); |
| f->glGetIntegerv(GL_BLEND_SRC_RGB, blendFunc); |
| f->glGetIntegerv(GL_BLEND_SRC_ALPHA, blendFunc + 1); |
| f->glGetIntegerv(GL_BLEND_DST_RGB, blendFunc + 2); |
| f->glGetIntegerv(GL_BLEND_DST_ALPHA, blendFunc + 3); |
| f->glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &arrayBuf); |
| if (vaoHelper->isValid()) |
| f->glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &vao); |
| else |
| vao = 0; |
| for (int i = 0; i < 2; ++i) { |
| f->glGetVertexAttribiv(i, GL_VERTEX_ATTRIB_ARRAY_ENABLED, &va[i].enabled); |
| f->glGetVertexAttribiv(i, GL_VERTEX_ATTRIB_ARRAY_SIZE, &va[i].size); |
| f->glGetVertexAttribiv(i, GL_VERTEX_ATTRIB_ARRAY_TYPE, &va[i].type); |
| f->glGetVertexAttribiv(i, GL_VERTEX_ATTRIB_ARRAY_NORMALIZED, &va[i].normalized); |
| f->glGetVertexAttribiv(i, GL_VERTEX_ATTRIB_ARRAY_STRIDE, &va[i].stride); |
| f->glGetVertexAttribiv(i, GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING, &va[i].buffer); |
| f->glGetVertexAttribPointerv(i, GL_VERTEX_ATTRIB_ARRAY_POINTER, &va[i].pointer); |
| } |
| } |
| ~StateSaver() { |
| if (saved) { |
| f->glUseProgram(program); |
| f->glBindTexture(GL_TEXTURE_2D, texture); |
| f->glActiveTexture(activeTexture); |
| f->glFrontFace(frontFace); |
| if (cull) |
| f->glEnable(GL_CULL_FACE); |
| else |
| f->glDisable(GL_CULL_FACE); |
| if (depthTest) |
| f->glEnable(GL_DEPTH_TEST); |
| else |
| f->glDisable(GL_DEPTH_TEST); |
| if (blend) |
| f->glEnable(GL_BLEND); |
| else |
| f->glDisable(GL_BLEND); |
| f->glBlendFuncSeparate(blendFunc[0], blendFunc[1], blendFunc[2], blendFunc[3]); |
| f->glBindBuffer(GL_ARRAY_BUFFER, arrayBuf); |
| if (vaoHelper->isValid()) |
| vaoHelper->glBindVertexArray(vao); |
| for (int i = 0; i < 2; ++i) { |
| if (va[i].enabled) |
| f->glEnableVertexAttribArray(i); |
| else |
| f->glDisableVertexAttribArray(i); |
| f->glBindBuffer(GL_ARRAY_BUFFER, va[i].buffer); |
| f->glVertexAttribPointer(i, va[i].size, va[i].type, va[i].normalized, va[i].stride, va[i].pointer); |
| } |
| } |
| delete vaoHelper; |
| } |
| QOpenGLFunctions *f; |
| QOpenGLVertexArrayObjectHelper *vaoHelper; |
| bool saved; |
| GLint program; |
| GLint texture; |
| GLint activeTexture; |
| GLint frontFace; |
| bool cull; |
| bool depthTest; |
| bool blend; |
| GLint blendFunc[4]; |
| GLint vao; |
| GLint arrayBuf; |
| struct { GLint enabled, type, size, normalized, stride, buffer; GLvoid *pointer; } va[2]; |
| }; |
| |
| void QEglFSCursor::draw(const QRectF &r) |
| { |
| StateSaver stateSaver; |
| |
| QEglFSCursorData &gfx = static_cast<QEglFSContext*>(QOpenGLContext::currentContext()->handle())->cursorData; |
| if (!gfx.program) { |
| // one time initialization |
| initializeOpenGLFunctions(); |
| |
| createShaderPrograms(); |
| |
| if (!gfx.atlasTexture) { |
| createCursorTexture(&gfx.atlasTexture, m_cursorAtlas.image); |
| |
| if (m_cursor.shape != Qt::BitmapCursor) |
| m_cursor.useCustomCursor = false; |
| } |
| } |
| |
| if (m_cursor.shape == Qt::BitmapCursor && (m_cursor.customCursorPending || m_cursor.customCursorKey != gfx.customCursorKey)) { |
| // upload the custom cursor |
| createCursorTexture(&gfx.customCursorTexture, m_cursor.customCursorImage); |
| m_cursor.useCustomCursor = true; |
| m_cursor.customCursorPending = false; |
| gfx.customCursorKey = m_cursor.customCursorKey; |
| } |
| |
| GLuint cursorTexture = !m_cursor.useCustomCursor ? gfx.atlasTexture : gfx.customCursorTexture; |
| Q_ASSERT(cursorTexture); |
| |
| gfx.program->bind(); |
| |
| const GLfloat x1 = r.left(); |
| const GLfloat x2 = r.right(); |
| const GLfloat y1 = r.top(); |
| const GLfloat y2 = r.bottom(); |
| const GLfloat cursorCoordinates[] = { |
| x1, y2, |
| x2, y2, |
| x1, y1, |
| x2, y1 |
| }; |
| |
| const GLfloat s1 = m_cursor.textureRect.left(); |
| const GLfloat s2 = m_cursor.textureRect.right(); |
| const GLfloat t1 = m_cursor.textureRect.top(); |
| const GLfloat t2 = m_cursor.textureRect.bottom(); |
| const GLfloat textureCoordinates[] = { |
| s1, t2, |
| s2, t2, |
| s1, t1, |
| s2, t1 |
| }; |
| |
| glActiveTexture(GL_TEXTURE0); |
| glBindTexture(GL_TEXTURE_2D, cursorTexture); |
| |
| if (stateSaver.vaoHelper->isValid()) |
| stateSaver.vaoHelper->glBindVertexArray(0); |
| |
| glBindBuffer(GL_ARRAY_BUFFER, 0); |
| |
| gfx.program->enableAttributeArray(0); |
| gfx.program->enableAttributeArray(1); |
| gfx.program->setAttributeArray(0, cursorCoordinates, 2); |
| gfx.program->setAttributeArray(1, textureCoordinates, 2); |
| |
| gfx.program->setUniformValue(gfx.textureEntry, 0); |
| gfx.program->setUniformValue(gfx.matEntry, m_rotationMatrix); |
| |
| glDisable(GL_CULL_FACE); |
| glFrontFace(GL_CCW); |
| glEnable(GL_BLEND); |
| glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); |
| glDisable(GL_DEPTH_TEST); // disable depth testing to make sure cursor is always on top |
| |
| glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); |
| |
| gfx.program->disableAttributeArray(0); |
| gfx.program->disableAttributeArray(1); |
| gfx.program->release(); |
| } |
| |
| QT_END_NAMESPACE |