iOS: Harden logic for when it's safe to use the graphics hardware

To fix QTBUG-52493 we tied the exposed state of a window to the
application being in the foreground. This has the result of a
visible flash of black between hiding the launch screen and showing
the first frame of the application, as the application is still
waiting for UIApplicationStateActive to begin rendering, which
happens after iOS hides the launch screen.

According to the iOS OpenGL ES Programming Guide, it should be safe
to render GL in UIApplicationStateInactive as well, and even in
UIApplicationStateBackground, as long as the rendering finishes
before the UIApplicationDidEnterBackgroundNotification returns.

To ensure that we catch any bugs in this area, checks have been
added that verify that no rendering happens while in the background
state.

Task-number: QTBUG-63229
Task-number: QTBUG-52493
Task-number: QTBUG-55205
Change-Id: Ib42bedbeddd7479ab0fb5e5b7de9f5805658e111
Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
bb10
Tor Arne Vestbø 2017-11-20 12:04:50 +01:00
parent 933497bace
commit a73de7ce2d
4 changed files with 62 additions and 11 deletions

View File

@ -937,7 +937,9 @@ GLuint QOpenGLContext::defaultFramebufferObject() const
/*!
Makes the context current in the current thread, against the given
\a surface. Returns \c true if successful.
\a surface. Returns \c true if successful; otherwise returns \c false.
The latter may happen if the surface is not exposed, or the graphics
hardware is not available due to e.g. the application being suspended.
If \a surface is 0 this is equivalent to calling doneCurrent().

View File

@ -87,6 +87,7 @@ private:
bool isComplete;
};
static bool verifyGraphicsHardwareAvailability();
static void deleteBuffers(const FramebufferObject &framebufferObject);
FramebufferObject &backingFramebufferObjectFor(QPlatformSurface *) const;

View File

@ -38,10 +38,13 @@
****************************************************************************/
#include "qioscontext.h"
#include "qiosintegration.h"
#include "qioswindow.h"
#include <dlfcn.h>
#include <QtGui/QGuiApplication>
#include <QtGui/QOpenGLContext>
#import <OpenGLES/EAGL.h>
@ -136,6 +139,9 @@ 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
@ -214,18 +220,12 @@ void QIOSContext::swapBuffers(QPlatformSurface *surface)
{
Q_ASSERT_IS_GL_SURFACE(surface);
if (!verifyGraphicsHardwareAvailability())
return;
if (surface->surface()->surfaceClass() == QSurface::Offscreen)
return; // Nothing to do
// When using threaded rendering, the render-thread may not have picked up
// yet on the fact that a window is no longer exposed, and will try to swap
// a non-exposed window. This may in some cases result in crashes, e.g. when
// iOS is suspending an application, so we have an extra guard here.
if (!static_cast<QIOSWindow *>(surface)->isExposed()) {
qCDebug(lcQpaGLContext, "Detected swapBuffers on a non-exposed window, skipping flush");
return;
}
FramebufferObject &framebufferObject = backingFramebufferObjectFor(surface);
Q_ASSERT_X(framebufferObject.isComplete, "QIOSContext", "swapBuffers on incomplete FBO");
@ -287,6 +287,54 @@ bool QIOSContext::needsRenderbufferResize(QPlatformSurface *surface) const
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 delegates 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 state) {
if (applicationBackgrounded && state != Qt::ApplicationSuspended) {
qCDebug(lcQpaGLContext) << "app no longer backgrounded, rendering enabled";
applicationBackgrounded = false;
}
});
connect(applicationState, &QIOSApplicationState::applicationStateDidChange, [](Qt::ApplicationState state) {
if (state != 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) {
static const char warning[] = "OpenGL ES calls are not allowed while an application is backgrounded";
Q_ASSERT_X(!applicationBackgrounded, "QIOSContext", warning);
qCWarning(lcQpaGLContext, warning);
}
return !applicationBackgrounded;
}
void QIOSContext::windowDestroyed(QObject *object)
{
QIOSWindow *window = static_cast<QIOSWindow *>(object);

View File

@ -225,7 +225,7 @@ void QIOSWindow::applyGeometry(const QRect &rect)
bool QIOSWindow::isExposed() const
{
return qApp->applicationState() >= Qt::ApplicationActive
return qApp->applicationState() != Qt::ApplicationSuspended
&& window()->isVisible() && !window()->geometry().isEmpty();
}