/**************************************************************************** ** ** 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 #include #include #import #import #import QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(lcQpaGLContext, "qt.qpa.glcontext"); QIOSContext::QIOSContext(QOpenGLContext *context) : QPlatformOpenGLContext() , m_sharedContext(static_cast(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(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(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(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(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(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