1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the plugins of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "qioscontext.h"
41
42#include "qiosintegration.h"
43#include "qioswindow.h"
44
45#include <dlfcn.h>
46
47#include <QtGui/QGuiApplication>
48#include <QtGui/QOpenGLContext>
49
50#import <OpenGLES/EAGL.h>
51#import <OpenGLES/ES2/glext.h>
52#import <QuartzCore/CAEAGLLayer.h>
53
54QT_BEGIN_NAMESPACE
55
56Q_LOGGING_CATEGORY(lcQpaGLContext, "qt.qpa.glcontext");
57
58QIOSContext::QIOSContext(QOpenGLContext *context)
59    : QPlatformOpenGLContext()
60    , m_sharedContext(static_cast<QIOSContext *>(context->shareHandle()))
61    , m_eaglContext(0)
62    , m_format(context->format())
63{
64    m_format.setRenderableType(QSurfaceFormat::OpenGLES);
65
66    EAGLSharegroup *shareGroup = m_sharedContext ? [m_sharedContext->m_eaglContext sharegroup] : nil;
67    const int preferredVersion = m_format.majorVersion() == 1 ? kEAGLRenderingAPIOpenGLES1 : kEAGLRenderingAPIOpenGLES3;
68    for (int version = preferredVersion; !m_eaglContext && version >= m_format.majorVersion(); --version)
69        m_eaglContext = [[EAGLContext alloc] initWithAPI:EAGLRenderingAPI(version) sharegroup:shareGroup];
70
71    if (m_eaglContext != nil) {
72        EAGLContext *originalContext = [EAGLContext currentContext];
73        [EAGLContext setCurrentContext:m_eaglContext];
74        const GLubyte *s = glGetString(GL_VERSION);
75        if (s) {
76            QByteArray version = QByteArray(reinterpret_cast<const char *>(s));
77            int major, minor;
78            if (QPlatformOpenGLContext::parseOpenGLVersion(version, major, minor)) {
79                m_format.setMajorVersion(major);
80                m_format.setMinorVersion(minor);
81            }
82        }
83        [EAGLContext setCurrentContext:originalContext];
84    }
85
86    // iOS internally double-buffers its rendering using copy instead of flipping,
87    // so technically we could report that we are single-buffered so that clients
88    // could take advantage of the unchanged buffer, but this means clients (and Qt)
89    // will also assume that swapBufferes() is not needed, which is _not_ the case.
90    m_format.setSwapBehavior(QSurfaceFormat::DoubleBuffer);
91
92    qCDebug(lcQpaGLContext) << "created context with format" << m_format << "shared with" << m_sharedContext;
93}
94
95QIOSContext::~QIOSContext()
96{
97    [EAGLContext setCurrentContext:m_eaglContext];
98
99    foreach (const FramebufferObject &framebufferObject, m_framebufferObjects)
100        deleteBuffers(framebufferObject);
101
102    [EAGLContext setCurrentContext:nil];
103    [m_eaglContext release];
104}
105
106void QIOSContext::deleteBuffers(const FramebufferObject &framebufferObject)
107{
108    if (framebufferObject.handle)
109        glDeleteFramebuffers(1, &framebufferObject.handle);
110    if (framebufferObject.colorRenderbuffer)
111        glDeleteRenderbuffers(1, &framebufferObject.colorRenderbuffer);
112    if (framebufferObject.depthRenderbuffer)
113        glDeleteRenderbuffers(1, &framebufferObject.depthRenderbuffer);
114}
115
116QSurfaceFormat QIOSContext::format() const
117{
118    return m_format;
119}
120
121#define QT_IOS_GL_STATUS_CASE(val) case val: return QLatin1String(#val)
122
123static QString fboStatusString(GLenum status)
124{
125    switch (status) {
126        QT_IOS_GL_STATUS_CASE(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT);
127        QT_IOS_GL_STATUS_CASE(GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS);
128        QT_IOS_GL_STATUS_CASE(GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT);
129        QT_IOS_GL_STATUS_CASE(GL_FRAMEBUFFER_UNSUPPORTED);
130    default:
131        return QString(QStringLiteral("unknown status: %x")).arg(status);
132    }
133}
134
135#define Q_ASSERT_IS_GL_SURFACE(surface) \
136    Q_ASSERT(surface && (surface->surface()->surfaceType() & (QSurface::OpenGLSurface | QSurface::RasterGLSurface)))
137
138bool QIOSContext::makeCurrent(QPlatformSurface *surface)
139{
140    Q_ASSERT_IS_GL_SURFACE(surface);
141
142    if (!verifyGraphicsHardwareAvailability())
143        return false;
144
145    [EAGLContext setCurrentContext:m_eaglContext];
146
147    // For offscreen surfaces we don't prepare a default FBO
148    if (surface->surface()->surfaceClass() == QSurface::Offscreen)
149        return true;
150
151    Q_ASSERT(surface->surface()->surfaceClass() == QSurface::Window);
152    FramebufferObject &framebufferObject = backingFramebufferObjectFor(surface);
153
154    if (!framebufferObject.handle) {
155        // Set up an FBO for the window if it hasn't been created yet
156        glGenFramebuffers(1, &framebufferObject.handle);
157        glBindFramebuffer(GL_FRAMEBUFFER, framebufferObject.handle);
158
159        glGenRenderbuffers(1, &framebufferObject.colorRenderbuffer);
160        glBindRenderbuffer(GL_RENDERBUFFER, framebufferObject.colorRenderbuffer);
161        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
162            framebufferObject.colorRenderbuffer);
163
164        if (m_format.depthBufferSize() > 0 || m_format.stencilBufferSize() > 0) {
165            glGenRenderbuffers(1, &framebufferObject.depthRenderbuffer);
166            glBindRenderbuffer(GL_RENDERBUFFER, framebufferObject.depthRenderbuffer);
167            glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER,
168                framebufferObject.depthRenderbuffer);
169
170            if (m_format.stencilBufferSize() > 0)
171                glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER,
172                    framebufferObject.depthRenderbuffer);
173        }
174    } else {
175        glBindFramebuffer(GL_FRAMEBUFFER, framebufferObject.handle);
176    }
177
178    if (needsRenderbufferResize(surface)) {
179        // Ensure that the FBO's buffers match the size of the layer
180        CAEAGLLayer *layer = static_cast<QIOSWindow *>(surface)->eaglLayer();
181        qCDebug(lcQpaGLContext, "Reallocating renderbuffer storage - current: %dx%d, layer: %gx%g",
182            framebufferObject.renderbufferWidth, framebufferObject.renderbufferHeight,
183            layer.frame.size.width * layer.contentsScale, layer.frame.size.height * layer.contentsScale);
184
185        glBindRenderbuffer(GL_RENDERBUFFER, framebufferObject.colorRenderbuffer);
186        [m_eaglContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];
187
188        glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &framebufferObject.renderbufferWidth);
189        glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &framebufferObject.renderbufferHeight);
190
191        if (framebufferObject.depthRenderbuffer) {
192            glBindRenderbuffer(GL_RENDERBUFFER, framebufferObject.depthRenderbuffer);
193
194            // FIXME: Support more fine grained control over depth/stencil buffer sizes
195            if (m_format.stencilBufferSize() > 0)
196                glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8_OES,
197                    framebufferObject.renderbufferWidth, framebufferObject.renderbufferHeight);
198            else
199                glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16,
200                    framebufferObject.renderbufferWidth, framebufferObject.renderbufferHeight);
201        }
202
203        framebufferObject.isComplete = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
204
205        if (!framebufferObject.isComplete) {
206            qCWarning(lcQpaGLContext, "QIOSContext failed to make complete framebuffer object (%s)",
207                qPrintable(fboStatusString(glCheckFramebufferStatus(GL_FRAMEBUFFER))));
208        }
209    }
210
211    return framebufferObject.isComplete;
212}
213
214void QIOSContext::doneCurrent()
215{
216    [EAGLContext setCurrentContext:nil];
217}
218
219void QIOSContext::swapBuffers(QPlatformSurface *surface)
220{
221    Q_ASSERT_IS_GL_SURFACE(surface);
222
223    if (!verifyGraphicsHardwareAvailability())
224        return;
225
226    if (surface->surface()->surfaceClass() == QSurface::Offscreen)
227        return; // Nothing to do
228
229    FramebufferObject &framebufferObject = backingFramebufferObjectFor(surface);
230    Q_ASSERT_X(framebufferObject.isComplete, "QIOSContext", "swapBuffers on incomplete FBO");
231
232    if (needsRenderbufferResize(surface)) {
233        qCWarning(lcQpaGLContext, "CAEAGLLayer was resized between makeCurrent and swapBuffers, skipping flush");
234        return;
235    }
236
237    [EAGLContext setCurrentContext:m_eaglContext];
238    glBindRenderbuffer(GL_RENDERBUFFER, framebufferObject.colorRenderbuffer);
239    [m_eaglContext presentRenderbuffer:GL_RENDERBUFFER];
240}
241
242QIOSContext::FramebufferObject &QIOSContext::backingFramebufferObjectFor(QPlatformSurface *surface) const
243{
244    // We keep track of default-FBOs in the root context of a share-group. This assumes
245    // that the contexts form a tree, where leaf nodes are always destroyed before their
246    // parents. If that assumption (based on the current implementation) doesn't hold we
247    // should probably use QOpenGLMultiGroupSharedResource to track the shared default-FBOs.
248    if (m_sharedContext)
249        return m_sharedContext->backingFramebufferObjectFor(surface);
250
251    if (!m_framebufferObjects.contains(surface)) {
252        // We're about to create a new FBO, make sure it's cleaned up as well
253        connect(static_cast<QIOSWindow *>(surface), SIGNAL(destroyed(QObject*)), this, SLOT(windowDestroyed(QObject*)));
254    }
255
256    return m_framebufferObjects[surface];
257}
258
259GLuint QIOSContext::defaultFramebufferObject(QPlatformSurface *surface) const
260{
261    if (surface->surface()->surfaceClass() == QSurface::Offscreen) {
262        // Binding and rendering to the zero-FBO on iOS seems to be
263        // no-ops, so we can safely return 0 here, even if it's not
264        // really a valid FBO on iOS.
265        return 0;
266    }
267
268    FramebufferObject &framebufferObject = backingFramebufferObjectFor(surface);
269    Q_ASSERT_X(framebufferObject.handle, "QIOSContext", "can't resolve default FBO before makeCurrent");
270
271    return framebufferObject.handle;
272}
273
274bool QIOSContext::needsRenderbufferResize(QPlatformSurface *surface) const
275{
276    Q_ASSERT(surface->surface()->surfaceClass() == QSurface::Window);
277
278    FramebufferObject &framebufferObject = backingFramebufferObjectFor(surface);
279    CAEAGLLayer *layer = static_cast<QIOSWindow *>(surface)->eaglLayer();
280
281    if (framebufferObject.renderbufferWidth != (layer.frame.size.width * layer.contentsScale))
282        return true;
283
284    if (framebufferObject.renderbufferHeight != (layer.frame.size.height * layer.contentsScale))
285        return true;
286
287    return false;
288}
289
290bool QIOSContext::verifyGraphicsHardwareAvailability()
291{
292    // Per the iOS OpenGL ES Programming Guide, background apps may not execute commands on the
293    // graphics hardware. Specifically: "In your app delegate’s applicationDidEnterBackground:
294    // method, your app may want to delete some of its OpenGL ES objects to make memory and
295    // resources available to the foreground app. Call the glFinish function to ensure that
296    // the resources are removed immediately. After your app exits its applicationDidEnterBackground:
297    // method, it must not make any new OpenGL ES calls. If it makes an OpenGL ES call, it is
298    // terminated by iOS.".
299    static bool applicationBackgrounded = QGuiApplication::applicationState() == Qt::ApplicationSuspended;
300
301    static dispatch_once_t onceToken = 0;
302    dispatch_once(&onceToken, ^{
303        QIOSApplicationState *applicationState = &QIOSIntegration::instance()->applicationState;
304        connect(applicationState, &QIOSApplicationState::applicationStateWillChange,
305            [](Qt::ApplicationState oldState, Qt::ApplicationState newState) {
306                Q_UNUSED(oldState);
307                if (applicationBackgrounded && newState != Qt::ApplicationSuspended) {
308                    qCDebug(lcQpaGLContext) << "app no longer backgrounded, rendering enabled";
309                    applicationBackgrounded = false;
310                }
311            }
312        );
313        connect(applicationState, &QIOSApplicationState::applicationStateDidChange,
314            [](Qt::ApplicationState oldState, Qt::ApplicationState newState) {
315                Q_UNUSED(oldState);
316                if (newState != Qt::ApplicationSuspended)
317                    return;
318
319                qCDebug(lcQpaGLContext) << "app backgrounded, rendering disabled";
320                applicationBackgrounded = true;
321
322                // By the time we receive this signal the application has moved into
323                // Qt::ApplactionStateSuspended, and all windows have been obscured,
324                // which should stop all rendering. If there's still an active GL context,
325                // we follow Apple's advice and call glFinish before making it inactive.
326                if (QOpenGLContext *currentContext = QOpenGLContext::currentContext()) {
327                    qCWarning(lcQpaGLContext) << "explicitly glFinishing and deactivating" << currentContext;
328                    glFinish();
329                    currentContext->doneCurrent();
330                }
331            }
332        );
333    });
334
335    if (applicationBackgrounded)
336        qCWarning(lcQpaGLContext, "OpenGL ES calls are not allowed while an application is backgrounded");
337
338    return !applicationBackgrounded;
339}
340
341void QIOSContext::windowDestroyed(QObject *object)
342{
343    QIOSWindow *window = static_cast<QIOSWindow *>(object);
344    if (!m_framebufferObjects.contains(window))
345        return;
346
347    qCDebug(lcQpaGLContext) << object << "destroyed, deleting corresponding FBO";
348
349    EAGLContext *originalContext = [EAGLContext currentContext];
350    [EAGLContext setCurrentContext:m_eaglContext];
351    deleteBuffers(m_framebufferObjects[window]);
352    m_framebufferObjects.remove(window);
353    [EAGLContext setCurrentContext:originalContext];
354}
355
356QFunctionPointer QIOSContext::getProcAddress(const char *functionName)
357{
358    return QFunctionPointer(dlsym(RTLD_DEFAULT, functionName));
359}
360
361bool QIOSContext::isValid() const
362{
363    return m_eaglContext;
364}
365
366bool QIOSContext::isSharing() const
367{
368    return m_sharedContext;
369}
370
371#include "moc_qioscontext.cpp"
372
373QT_END_NAMESPACE
374