| /**************************************************************************** |
| ** |
| ** 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 "qioscontext.h" |
| |
| #include "qiosintegration.h" |
| #include "qioswindow.h" |
| |
| #include <dlfcn.h> |
| |
| #include <QtGui/QGuiApplication> |
| #include <QtGui/QOpenGLContext> |
| |
| #import <OpenGLES/EAGL.h> |
| #import <OpenGLES/ES2/glext.h> |
| #import <QuartzCore/CAEAGLLayer.h> |
| |
| QT_BEGIN_NAMESPACE |
| |
| Q_LOGGING_CATEGORY(lcQpaGLContext, "qt.qpa.glcontext"); |
| |
| QIOSContext::QIOSContext(QOpenGLContext *context) |
| : QPlatformOpenGLContext() |
| , m_sharedContext(static_cast<QIOSContext *>(context->shareHandle())) |
| , m_eaglContext(0) |
| , m_format(context->format()) |
| { |
| m_format.setRenderableType(QSurfaceFormat::OpenGLES); |
| |
| EAGLSharegroup *shareGroup = m_sharedContext ? [m_sharedContext->m_eaglContext sharegroup] : nil; |
| const int preferredVersion = m_format.majorVersion() == 1 ? kEAGLRenderingAPIOpenGLES1 : kEAGLRenderingAPIOpenGLES3; |
| for (int version = preferredVersion; !m_eaglContext && version >= m_format.majorVersion(); --version) |
| m_eaglContext = [[EAGLContext alloc] initWithAPI:EAGLRenderingAPI(version) sharegroup:shareGroup]; |
| |
| if (m_eaglContext != nil) { |
| EAGLContext *originalContext = [EAGLContext currentContext]; |
| [EAGLContext setCurrentContext:m_eaglContext]; |
| const GLubyte *s = glGetString(GL_VERSION); |
| if (s) { |
| QByteArray version = QByteArray(reinterpret_cast<const char *>(s)); |
| int major, minor; |
| if (QPlatformOpenGLContext::parseOpenGLVersion(version, major, minor)) { |
| m_format.setMajorVersion(major); |
| m_format.setMinorVersion(minor); |
| } |
| } |
| [EAGLContext setCurrentContext:originalContext]; |
| } |
| |
| // iOS internally double-buffers its rendering using copy instead of flipping, |
| // so technically we could report that we are single-buffered so that clients |
| // could take advantage of the unchanged buffer, but this means clients (and Qt) |
| // will also assume that swapBufferes() is not needed, which is _not_ the case. |
| m_format.setSwapBehavior(QSurfaceFormat::DoubleBuffer); |
| |
| qCDebug(lcQpaGLContext) << "created context with format" << m_format << "shared with" << m_sharedContext; |
| } |
| |
| QIOSContext::~QIOSContext() |
| { |
| [EAGLContext setCurrentContext:m_eaglContext]; |
| |
| foreach (const FramebufferObject &framebufferObject, m_framebufferObjects) |
| deleteBuffers(framebufferObject); |
| |
| [EAGLContext setCurrentContext:nil]; |
| [m_eaglContext release]; |
| } |
| |
| void QIOSContext::deleteBuffers(const FramebufferObject &framebufferObject) |
| { |
| if (framebufferObject.handle) |
| glDeleteFramebuffers(1, &framebufferObject.handle); |
| if (framebufferObject.colorRenderbuffer) |
| glDeleteRenderbuffers(1, &framebufferObject.colorRenderbuffer); |
| if (framebufferObject.depthRenderbuffer) |
| glDeleteRenderbuffers(1, &framebufferObject.depthRenderbuffer); |
| } |
| |
| QSurfaceFormat QIOSContext::format() const |
| { |
| return m_format; |
| } |
| |
| #define QT_IOS_GL_STATUS_CASE(val) case val: return QLatin1String(#val) |
| |
| static QString fboStatusString(GLenum status) |
| { |
| switch (status) { |
| QT_IOS_GL_STATUS_CASE(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT); |
| QT_IOS_GL_STATUS_CASE(GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS); |
| QT_IOS_GL_STATUS_CASE(GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT); |
| QT_IOS_GL_STATUS_CASE(GL_FRAMEBUFFER_UNSUPPORTED); |
| default: |
| return QString(QStringLiteral("unknown status: %x")).arg(status); |
| } |
| } |
| |
| #define Q_ASSERT_IS_GL_SURFACE(surface) \ |
| Q_ASSERT(surface && (surface->surface()->surfaceType() & (QSurface::OpenGLSurface | QSurface::RasterGLSurface))) |
| |
| bool QIOSContext::makeCurrent(QPlatformSurface *surface) |
| { |
| Q_ASSERT_IS_GL_SURFACE(surface); |
| |
| if (!verifyGraphicsHardwareAvailability()) |
| return false; |
| |
| [EAGLContext setCurrentContext:m_eaglContext]; |
| |
| // For offscreen surfaces we don't prepare a default FBO |
| if (surface->surface()->surfaceClass() == QSurface::Offscreen) |
| return true; |
| |
| Q_ASSERT(surface->surface()->surfaceClass() == QSurface::Window); |
| FramebufferObject &framebufferObject = backingFramebufferObjectFor(surface); |
| |
| if (!framebufferObject.handle) { |
| // Set up an FBO for the window if it hasn't been created yet |
| glGenFramebuffers(1, &framebufferObject.handle); |
| glBindFramebuffer(GL_FRAMEBUFFER, framebufferObject.handle); |
| |
| glGenRenderbuffers(1, &framebufferObject.colorRenderbuffer); |
| glBindRenderbuffer(GL_RENDERBUFFER, framebufferObject.colorRenderbuffer); |
| glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, |
| framebufferObject.colorRenderbuffer); |
| |
| if (m_format.depthBufferSize() > 0 || m_format.stencilBufferSize() > 0) { |
| glGenRenderbuffers(1, &framebufferObject.depthRenderbuffer); |
| glBindRenderbuffer(GL_RENDERBUFFER, framebufferObject.depthRenderbuffer); |
| glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, |
| framebufferObject.depthRenderbuffer); |
| |
| if (m_format.stencilBufferSize() > 0) |
| glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, |
| framebufferObject.depthRenderbuffer); |
| } |
| } else { |
| glBindFramebuffer(GL_FRAMEBUFFER, framebufferObject.handle); |
| } |
| |
| if (needsRenderbufferResize(surface)) { |
| // Ensure that the FBO's buffers match the size of the layer |
| CAEAGLLayer *layer = static_cast<QIOSWindow *>(surface)->eaglLayer(); |
| qCDebug(lcQpaGLContext, "Reallocating renderbuffer storage - current: %dx%d, layer: %gx%g", |
| framebufferObject.renderbufferWidth, framebufferObject.renderbufferHeight, |
| layer.frame.size.width * layer.contentsScale, layer.frame.size.height * layer.contentsScale); |
| |
| glBindRenderbuffer(GL_RENDERBUFFER, framebufferObject.colorRenderbuffer); |
| [m_eaglContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer]; |
| |
| glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &framebufferObject.renderbufferWidth); |
| glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &framebufferObject.renderbufferHeight); |
| |
| if (framebufferObject.depthRenderbuffer) { |
| glBindRenderbuffer(GL_RENDERBUFFER, framebufferObject.depthRenderbuffer); |
| |
| // FIXME: Support more fine grained control over depth/stencil buffer sizes |
| if (m_format.stencilBufferSize() > 0) |
| glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8_OES, |
| framebufferObject.renderbufferWidth, framebufferObject.renderbufferHeight); |
| else |
| glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, |
| framebufferObject.renderbufferWidth, framebufferObject.renderbufferHeight); |
| } |
| |
| framebufferObject.isComplete = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE; |
| |
| if (!framebufferObject.isComplete) { |
| qCWarning(lcQpaGLContext, "QIOSContext failed to make complete framebuffer object (%s)", |
| qPrintable(fboStatusString(glCheckFramebufferStatus(GL_FRAMEBUFFER)))); |
| } |
| } |
| |
| return framebufferObject.isComplete; |
| } |
| |
| void QIOSContext::doneCurrent() |
| { |
| [EAGLContext setCurrentContext:nil]; |
| } |
| |
| void QIOSContext::swapBuffers(QPlatformSurface *surface) |
| { |
| Q_ASSERT_IS_GL_SURFACE(surface); |
| |
| if (!verifyGraphicsHardwareAvailability()) |
| return; |
| |
| if (surface->surface()->surfaceClass() == QSurface::Offscreen) |
| return; // Nothing to do |
| |
| FramebufferObject &framebufferObject = backingFramebufferObjectFor(surface); |
| Q_ASSERT_X(framebufferObject.isComplete, "QIOSContext", "swapBuffers on incomplete FBO"); |
| |
| if (needsRenderbufferResize(surface)) { |
| qCWarning(lcQpaGLContext, "CAEAGLLayer was resized between makeCurrent and swapBuffers, skipping flush"); |
| return; |
| } |
| |
| [EAGLContext setCurrentContext:m_eaglContext]; |
| glBindRenderbuffer(GL_RENDERBUFFER, framebufferObject.colorRenderbuffer); |
| [m_eaglContext presentRenderbuffer:GL_RENDERBUFFER]; |
| } |
| |
| QIOSContext::FramebufferObject &QIOSContext::backingFramebufferObjectFor(QPlatformSurface *surface) const |
| { |
| // We keep track of default-FBOs in the root context of a share-group. This assumes |
| // that the contexts form a tree, where leaf nodes are always destroyed before their |
| // parents. If that assumption (based on the current implementation) doesn't hold we |
| // should probably use QOpenGLMultiGroupSharedResource to track the shared default-FBOs. |
| if (m_sharedContext) |
| return m_sharedContext->backingFramebufferObjectFor(surface); |
| |
| if (!m_framebufferObjects.contains(surface)) { |
| // We're about to create a new FBO, make sure it's cleaned up as well |
| connect(static_cast<QIOSWindow *>(surface), SIGNAL(destroyed(QObject*)), this, SLOT(windowDestroyed(QObject*))); |
| } |
| |
| return m_framebufferObjects[surface]; |
| } |
| |
| GLuint QIOSContext::defaultFramebufferObject(QPlatformSurface *surface) const |
| { |
| if (surface->surface()->surfaceClass() == QSurface::Offscreen) { |
| // Binding and rendering to the zero-FBO on iOS seems to be |
| // no-ops, so we can safely return 0 here, even if it's not |
| // really a valid FBO on iOS. |
| return 0; |
| } |
| |
| FramebufferObject &framebufferObject = backingFramebufferObjectFor(surface); |
| Q_ASSERT_X(framebufferObject.handle, "QIOSContext", "can't resolve default FBO before makeCurrent"); |
| |
| return framebufferObject.handle; |
| } |
| |
| bool QIOSContext::needsRenderbufferResize(QPlatformSurface *surface) const |
| { |
| Q_ASSERT(surface->surface()->surfaceClass() == QSurface::Window); |
| |
| FramebufferObject &framebufferObject = backingFramebufferObjectFor(surface); |
| CAEAGLLayer *layer = static_cast<QIOSWindow *>(surface)->eaglLayer(); |
| |
| if (framebufferObject.renderbufferWidth != (layer.frame.size.width * layer.contentsScale)) |
| return true; |
| |
| if (framebufferObject.renderbufferHeight != (layer.frame.size.height * layer.contentsScale)) |
| return true; |
| |
| return false; |
| } |
| |
| bool QIOSContext::verifyGraphicsHardwareAvailability() |
| { |
| // Per the iOS OpenGL ES Programming Guide, background apps may not execute commands on the |
| // graphics hardware. Specifically: "In your app delegate’s applicationDidEnterBackground: |
| // method, your app may want to delete some of its OpenGL ES objects to make memory and |
| // resources available to the foreground app. Call the glFinish function to ensure that |
| // the resources are removed immediately. After your app exits its applicationDidEnterBackground: |
| // method, it must not make any new OpenGL ES calls. If it makes an OpenGL ES call, it is |
| // terminated by iOS.". |
| static bool applicationBackgrounded = QGuiApplication::applicationState() == Qt::ApplicationSuspended; |
| |
| static dispatch_once_t onceToken = 0; |
| dispatch_once(&onceToken, ^{ |
| QIOSApplicationState *applicationState = &QIOSIntegration::instance()->applicationState; |
| connect(applicationState, &QIOSApplicationState::applicationStateWillChange, |
| [](Qt::ApplicationState oldState, Qt::ApplicationState newState) { |
| Q_UNUSED(oldState); |
| if (applicationBackgrounded && newState != Qt::ApplicationSuspended) { |
| qCDebug(lcQpaGLContext) << "app no longer backgrounded, rendering enabled"; |
| applicationBackgrounded = false; |
| } |
| } |
| ); |
| connect(applicationState, &QIOSApplicationState::applicationStateDidChange, |
| [](Qt::ApplicationState oldState, Qt::ApplicationState newState) { |
| Q_UNUSED(oldState); |
| if (newState != Qt::ApplicationSuspended) |
| return; |
| |
| qCDebug(lcQpaGLContext) << "app backgrounded, rendering disabled"; |
| applicationBackgrounded = true; |
| |
| // By the time we receive this signal the application has moved into |
| // Qt::ApplactionStateSuspended, and all windows have been obscured, |
| // which should stop all rendering. If there's still an active GL context, |
| // we follow Apple's advice and call glFinish before making it inactive. |
| if (QOpenGLContext *currentContext = QOpenGLContext::currentContext()) { |
| qCWarning(lcQpaGLContext) << "explicitly glFinishing and deactivating" << currentContext; |
| glFinish(); |
| currentContext->doneCurrent(); |
| } |
| } |
| ); |
| }); |
| |
| if (applicationBackgrounded) |
| qCWarning(lcQpaGLContext, "OpenGL ES calls are not allowed while an application is backgrounded"); |
| |
| return !applicationBackgrounded; |
| } |
| |
| void QIOSContext::windowDestroyed(QObject *object) |
| { |
| QIOSWindow *window = static_cast<QIOSWindow *>(object); |
| if (!m_framebufferObjects.contains(window)) |
| return; |
| |
| qCDebug(lcQpaGLContext) << object << "destroyed, deleting corresponding FBO"; |
| |
| EAGLContext *originalContext = [EAGLContext currentContext]; |
| [EAGLContext setCurrentContext:m_eaglContext]; |
| deleteBuffers(m_framebufferObjects[window]); |
| m_framebufferObjects.remove(window); |
| [EAGLContext setCurrentContext:originalContext]; |
| } |
| |
| QFunctionPointer QIOSContext::getProcAddress(const char *functionName) |
| { |
| return QFunctionPointer(dlsym(RTLD_DEFAULT, functionName)); |
| } |
| |
| bool QIOSContext::isValid() const |
| { |
| return m_eaglContext; |
| } |
| |
| bool QIOSContext::isSharing() const |
| { |
| return m_sharedContext; |
| } |
| |
| #include "moc_qioscontext.cpp" |
| |
| QT_END_NAMESPACE |