Avoid direct GL calls in Quick
[qt:qtdeclarative.git] / src / quick / items / context2d / qquickcontext2dtexture.cpp
1 /****************************************************************************
2 **
3 ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
4 ** Contact: http://www.qt-project.org/legal
5 **
6 ** This file is part of the QtQuick module 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 Digia.  For licensing terms and
14 ** conditions see http://qt.digia.com/licensing.  For further information
15 ** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL included in the
21 ** packaging of this file.  Please review the following information to
22 ** ensure the GNU Lesser General Public License version 2.1 requirements
23 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
24 **
25 ** In addition, as a special exception, Digia gives you certain additional
26 ** rights.  These rights are described in the Digia Qt LGPL Exception
27 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
28 **
29 ** GNU General Public License Usage
30 ** Alternatively, this file may be used under the terms of the GNU
31 ** General Public License version 3.0 as published by the Free Software
32 ** Foundation and appearing in the file LICENSE.GPL included in the
33 ** packaging of this file.  Please review the following information to
34 ** ensure the GNU General Public License version 3.0 requirements will be
35 ** met: http://www.gnu.org/copyleft/gpl.html.
36 **
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41
42 #include "qquickcontext2dtexture_p.h"
43 #include "qquickcontext2dtile_p.h"
44 #include "qquickcanvasitem_p.h"
45 #include <private/qquickitem_p.h>
46 #include <QtQuick/private/qsgtexture_p.h>
47 #include "qquickcontext2dcommandbuffer_p.h"
48 #include <QOpenGLPaintDevice>
49
50 #include <QOpenGLFramebufferObject>
51 #include <QOpenGLFramebufferObjectFormat>
52 #include <QtCore/QThread>
53 #include <QtGui/QGuiApplication>
54
55 QT_BEGIN_NAMESPACE
56
57 #define QT_MINIMUM_FBO_SIZE 64
58
59 static inline int qt_next_power_of_two(int v)
60 {
61     v--;
62     v |= v >> 1;
63     v |= v >> 2;
64     v |= v >> 4;
65     v |= v >> 8;
66     v |= v >> 16;
67     ++v;
68     return v;
69 }
70
71 struct GLAcquireContext {
72     GLAcquireContext(QOpenGLContext *c, QSurface *s):ctx(c) {
73         if (ctx) {
74             Q_ASSERT(s);
75             if (!ctx->isValid())
76                 ctx->create();
77
78             if (!ctx->isValid())
79                 qWarning() << "Unable to create GL context";
80             else if (!ctx->makeCurrent(s))
81                 qWarning() << "Can't make current GL context";
82         }
83     }
84     ~GLAcquireContext() {
85         if (ctx)
86             ctx->doneCurrent();
87     }
88     QOpenGLContext *ctx;
89 };
90
91 QQuickContext2DTexture::QQuickContext2DTexture()
92     : m_context(0)
93     , m_item(0)
94     , m_canvasWindowChanged(false)
95     , m_dirtyTexture(false)
96     , m_smooth(true)
97     , m_antialiasing(false)
98     , m_tiledCanvas(false)
99     , m_painting(false)
100 {
101 }
102
103 QQuickContext2DTexture::~QQuickContext2DTexture()
104 {
105    clearTiles();
106 }
107
108 void QQuickContext2DTexture::markDirtyTexture()
109 {
110     if (m_onCustomThread)
111         m_mutex.lock();
112     m_dirtyTexture = true;
113     emit textureChanged();
114     if (m_onCustomThread)
115         m_mutex.unlock();
116 }
117
118 bool QQuickContext2DTexture::setCanvasSize(const QSize &size)
119 {
120     if (m_canvasSize != size) {
121         m_canvasSize = size;
122         return true;
123     }
124     return false;
125 }
126
127 bool QQuickContext2DTexture::setTileSize(const QSize &size)
128 {
129     if (m_tileSize != size) {
130         m_tileSize = size;
131         return true;
132     }
133     return false;
134 }
135
136 void QQuickContext2DTexture::setSmooth(bool smooth)
137 {
138     m_smooth = smooth;
139 }
140
141 void QQuickContext2DTexture::setAntialiasing(bool antialiasing)
142 {
143     m_antialiasing = antialiasing;
144 }
145
146 void QQuickContext2DTexture::setItem(QQuickCanvasItem* item)
147 {
148     m_item = item;
149     m_context = (QQuickContext2D*)item->rawContext(); // FIXME
150     m_state = m_context->state;
151 }
152
153 bool QQuickContext2DTexture::setCanvasWindow(const QRect& r)
154 {
155     if (m_canvasWindow != r) {
156         m_canvasWindow = r;
157         m_canvasWindowChanged = true;
158         return true;
159     }
160     return false;
161 }
162
163 bool QQuickContext2DTexture::setDirtyRect(const QRect &r)
164 {
165     bool doDirty = false;
166     if (m_tiledCanvas) {
167         foreach (QQuickContext2DTile* t, m_tiles) {
168             bool dirty = t->rect().intersected(r).isValid();
169             t->markDirty(dirty);
170             if (dirty)
171                 doDirty = true;
172         }
173     } else {
174         doDirty = m_canvasWindow.intersected(r).isValid();
175     }
176     return doDirty;
177 }
178
179 void QQuickContext2DTexture::canvasChanged(const QSize& canvasSize, const QSize& tileSize, const QRect& canvasWindow, const QRect& dirtyRect, bool smooth, bool antialiasing)
180 {
181     QSize ts = tileSize;
182     if (ts.width() > canvasSize.width())
183         ts.setWidth(canvasSize.width());
184
185     if (ts.height() > canvasSize.height())
186         ts.setHeight(canvasSize.height());
187
188     setCanvasSize(canvasSize);
189     setTileSize(ts);
190     setCanvasWindow(canvasWindow);
191
192     if (canvasSize == canvasWindow.size()) {
193         m_tiledCanvas = false;
194     } else {
195         m_tiledCanvas = true;
196     }
197
198     if (dirtyRect.isValid())
199         setDirtyRect(dirtyRect);
200
201     setSmooth(smooth);
202     setAntialiasing(antialiasing);
203 }
204
205 void QQuickContext2DTexture::paintWithoutTiles(QQuickContext2DCommandBuffer *ccb)
206 {
207     if (!ccb || ccb->isEmpty())
208         return;
209
210     QPaintDevice* device = beginPainting();
211     if (!device) {
212         endPainting();
213         return;
214     }
215
216     QPainter p;
217     p.begin(device);
218     if (m_antialiasing)
219         p.setRenderHints(QPainter::Antialiasing | QPainter::HighQualityAntialiasing | QPainter::TextAntialiasing, true);
220     else
221         p.setRenderHints(QPainter::Antialiasing | QPainter::HighQualityAntialiasing | QPainter::TextAntialiasing, false);
222
223     if (m_smooth)
224         p.setRenderHint(QPainter::SmoothPixmapTransform, true);
225     else
226         p.setRenderHint(QPainter::SmoothPixmapTransform, false);
227
228     p.setCompositionMode(QPainter::CompositionMode_SourceOver);
229
230     ccb->replay(&p, m_state);
231     endPainting();
232     markDirtyTexture();
233 }
234
235 bool QQuickContext2DTexture::canvasDestroyed()
236 {
237     return m_item == 0;
238 }
239
240 void QQuickContext2DTexture::paint(QQuickContext2DCommandBuffer *ccb)
241 {
242     if (canvasDestroyed()) {
243         delete ccb;
244         return;
245     }
246
247     GLAcquireContext currentContext(m_context->glContext(), m_context->surface());
248
249     if (!m_tiledCanvas) {
250         paintWithoutTiles(ccb);
251         delete ccb;
252         return;
253     }
254
255     QRect tiledRegion = createTiles(m_canvasWindow.intersected(QRect(QPoint(0, 0), m_canvasSize)));
256     if (!tiledRegion.isEmpty()) {
257         QRect dirtyRect;
258         foreach (QQuickContext2DTile* tile, m_tiles) {
259             if (tile->dirty()) {
260                 if (dirtyRect.isEmpty())
261                     dirtyRect = tile->rect();
262                 else
263                     dirtyRect |= tile->rect();
264             }
265         }
266
267         if (beginPainting()) {
268             QQuickContext2D::State oldState = m_state;
269             foreach (QQuickContext2DTile* tile, m_tiles) {
270                 if (tile->dirty()) {
271                     ccb->replay(tile->createPainter(m_smooth, m_antialiasing), oldState);
272                     tile->drawFinished();
273                     tile->markDirty(false);
274                 }
275                 compositeTile(tile);
276             }
277             endPainting();
278             m_state = oldState;
279             markDirtyTexture();
280         }
281     }
282     delete ccb;
283 }
284
285 QRect QQuickContext2DTexture::tiledRect(const QRectF& window, const QSize& tileSize)
286 {
287     if (window.isEmpty())
288         return QRect();
289
290     const int tw = tileSize.width();
291     const int th = tileSize.height();
292     const int h1 = window.left() / tw;
293     const int v1 = window.top() / th;
294
295     const int htiles = ((window.right() - h1 * tw) + tw - 1)/tw;
296     const int vtiles = ((window.bottom() - v1 * th) + th - 1)/th;
297
298     return QRect(h1 * tw, v1 * th, htiles * tw, vtiles * th);
299 }
300
301 QRect QQuickContext2DTexture::createTiles(const QRect& window)
302 {
303     QList<QQuickContext2DTile*> oldTiles = m_tiles;
304     m_tiles.clear();
305
306     if (window.isEmpty()) {
307         return QRect();
308     }
309
310     QRect r = tiledRect(window, adjustedTileSize(m_tileSize));
311
312     const int tw = m_tileSize.width();
313     const int th = m_tileSize.height();
314     const int h1 = window.left() / tw;
315     const int v1 = window.top() / th;
316
317
318     const int htiles = r.width() / tw;
319     const int vtiles = r.height() / th;
320
321     for (int yy = 0; yy < vtiles; ++yy) {
322         for (int xx = 0; xx < htiles; ++xx) {
323             int ht = xx + h1;
324             int vt = yy + v1;
325
326             QQuickContext2DTile* tile = 0;
327
328             QPoint pos(ht * tw, vt * th);
329             QRect rect(pos, m_tileSize);
330
331             for (int i = 0; i < oldTiles.size(); i++) {
332                 if (oldTiles[i]->rect() == rect) {
333                     tile = oldTiles.takeAt(i);
334                     break;
335                 }
336             }
337
338             if (!tile)
339                 tile = createTile();
340
341             tile->setRect(rect);
342             m_tiles.append(tile);
343         }
344     }
345
346     qDeleteAll(oldTiles);
347
348     return r;
349 }
350
351 void QQuickContext2DTexture::clearTiles()
352 {
353     qDeleteAll(m_tiles);
354     m_tiles.clear();
355 }
356
357 QSize QQuickContext2DTexture::adjustedTileSize(const QSize &ts)
358 {
359     return ts;
360 }
361
362 bool QQuickContext2DTexture::event(QEvent *e)
363 {
364     if ((int) e->type() == QEvent::User + 1) {
365         PaintEvent *pe = static_cast<PaintEvent *>(e);
366         paint(pe->buffer);
367         return true;
368     } else if ((int) e->type() == QEvent::User + 2) {
369         CanvasChangeEvent *ce = static_cast<CanvasChangeEvent *>(e);
370         canvasChanged(ce->canvasSize, ce->tileSize, ce->canvasWindow, ce->dirtyRect, ce->smooth, ce->antialiasing);
371         return true;
372     }
373     return QObject::event(e);
374 }
375
376 static inline QSize npotAdjustedSize(const QSize &size)
377 {
378     static bool checked = false;
379     static bool npotSupported = false;
380
381     if (!checked) {
382         npotSupported = QOpenGLContext::currentContext()->functions()->hasOpenGLFeature(QOpenGLFunctions::NPOTTextures);
383         checked = true;
384     }
385
386     if (npotSupported) {
387         return QSize(qMax(QT_MINIMUM_FBO_SIZE, size.width()),
388                      qMax(QT_MINIMUM_FBO_SIZE, size.height()));
389     }
390
391     return QSize(qMax(QT_MINIMUM_FBO_SIZE, qt_next_power_of_two(size.width())),
392                        qMax(QT_MINIMUM_FBO_SIZE, qt_next_power_of_two(size.height())));
393 }
394
395 QQuickContext2DFBOTexture::QQuickContext2DFBOTexture()
396     : QQuickContext2DTexture()
397     , m_fbo(0)
398     , m_multisampledFbo(0)
399     , m_paint_device(0)
400 {
401     m_displayTextures[0] = 0;
402     m_displayTextures[1] = 0;
403     m_displayTexture = -1;
404 }
405
406 QQuickContext2DFBOTexture::~QQuickContext2DFBOTexture()
407 {
408     if (m_multisampledFbo)
409         m_multisampledFbo->release();
410     else if (m_fbo)
411         m_fbo->release();
412
413     delete m_fbo;
414     delete m_multisampledFbo;
415     delete m_paint_device;
416
417     QOpenGLContext::currentContext()->functions()->glDeleteTextures(2, m_displayTextures);
418 }
419
420 QSGTexture *QQuickContext2DFBOTexture::textureForNextFrame(QSGTexture *lastTexture)
421 {
422     QSGPlainTexture *texture = static_cast<QSGPlainTexture *>(lastTexture);
423
424     if (m_onCustomThread)
425         m_mutex.lock();
426
427     if (m_fbo) {
428         if (!texture) {
429             texture = new QSGPlainTexture();
430             texture->setHasAlphaChannel(true);
431             texture->setOwnsTexture(false);
432             m_dirtyTexture = true;
433         }
434
435         if (m_dirtyTexture) {
436             if (!m_context->glContext()) {
437                 // on a rendering thread, use the fbo directly...
438                 texture->setTextureId(m_fbo->texture());
439             } else {
440                 // on GUI or custom thread, use display textures...
441                 m_displayTexture = m_displayTexture == 0 ? 1 : 0;
442                 texture->setTextureId(m_displayTextures[m_displayTexture]);
443             }
444             texture->setTextureSize(m_fbo->size());
445             m_dirtyTexture = false;
446         }
447
448     }
449
450     if (m_onCustomThread) {
451         m_condition.wakeOne();
452         m_mutex.unlock();
453     }
454
455     return texture;
456 }
457
458 QSize QQuickContext2DFBOTexture::adjustedTileSize(const QSize &ts)
459 {
460     return npotAdjustedSize(ts);
461 }
462
463 QRectF QQuickContext2DFBOTexture::normalizedTextureSubRect() const
464 {
465     return QRectF(0
466                 , 0
467                 , qreal(m_canvasWindow.width()) / m_fboSize.width()
468                 , qreal(m_canvasWindow.height()) / m_fboSize.height());
469 }
470
471 QQuickContext2DTile* QQuickContext2DFBOTexture::createTile() const
472 {
473     return new QQuickContext2DFBOTile();
474 }
475
476 bool QQuickContext2DFBOTexture::doMultisampling() const
477 {
478     static bool extensionsChecked = false;
479     static bool multisamplingSupported = false;
480
481     if (!extensionsChecked) {
482         const QSet<QByteArray> extensions = m_context->glContext()->extensions();
483         multisamplingSupported = extensions.contains(QByteArrayLiteral("GL_EXT_framebuffer_multisample"))
484             && extensions.contains(QByteArrayLiteral("GL_EXT_framebuffer_blit"));
485         extensionsChecked = true;
486     }
487
488     return multisamplingSupported  && m_antialiasing;
489 }
490
491 void QQuickContext2DFBOTexture::grabImage(const QRectF& rf)
492 {
493     Q_ASSERT(rf.isValid());
494     if (!m_fbo) {
495         m_context->setGrabbedImage(QImage());
496         return;
497     }
498
499     QImage grabbed;
500     {
501         GLAcquireContext ctx(m_context->glContext(), m_context->surface());
502         grabbed = m_fbo->toImage().mirrored().copy(rf.toRect());
503     }
504
505     m_context->setGrabbedImage(grabbed);
506 }
507
508 void QQuickContext2DFBOTexture::compositeTile(QQuickContext2DTile* tile)
509 {
510     QQuickContext2DFBOTile* t = static_cast<QQuickContext2DFBOTile*>(tile);
511     QRect target = t->rect().intersected(m_canvasWindow);
512     if (target.isValid()) {
513         QRect source = target;
514
515         source.moveTo(source.topLeft() - t->rect().topLeft());
516         target.moveTo(target.topLeft() - m_canvasWindow.topLeft());
517
518         QOpenGLFramebufferObject::blitFramebuffer(m_fbo, target, t->fbo(), source);
519     }
520 }
521
522 QQuickCanvasItem::RenderTarget QQuickContext2DFBOTexture::renderTarget() const
523 {
524     return QQuickCanvasItem::FramebufferObject;
525 }
526
527 QPaintDevice* QQuickContext2DFBOTexture::beginPainting()
528 {
529     QQuickContext2DTexture::beginPainting();
530
531     if (m_canvasWindow.size().isEmpty()) {
532         delete m_fbo;
533         delete m_multisampledFbo;
534         delete m_paint_device;
535         m_fbo = 0;
536         m_multisampledFbo = 0;
537         m_paint_device = 0;
538         return 0;
539     } else if (!m_fbo || m_canvasWindowChanged) {
540         delete m_fbo;
541         delete m_multisampledFbo;
542         delete m_paint_device;
543         m_paint_device = 0;
544
545         m_fboSize = npotAdjustedSize(m_canvasWindow.size());
546         m_canvasWindowChanged = false;
547
548         if (doMultisampling()) {
549             {
550                 QOpenGLFramebufferObjectFormat format;
551                 format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
552                 format.setSamples(8);
553                 m_multisampledFbo = new QOpenGLFramebufferObject(m_fboSize, format);
554             }
555             {
556                 QOpenGLFramebufferObjectFormat format;
557                 format.setAttachment(QOpenGLFramebufferObject::NoAttachment);
558                 m_fbo = new QOpenGLFramebufferObject(m_fboSize, format);
559             }
560         } else {
561             QOpenGLFramebufferObjectFormat format;
562             format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
563             m_fbo = new QOpenGLFramebufferObject(m_fboSize, format);
564         }
565     }
566
567     if (doMultisampling())
568         m_multisampledFbo->bind();
569     else
570         m_fbo->bind();
571
572     if (!m_paint_device) {
573         QOpenGLPaintDevice *gl_device = new QOpenGLPaintDevice(m_fbo->size());
574         gl_device->setPaintFlipped(true);
575         gl_device->setSize(m_fbo->size());
576         m_paint_device = gl_device;
577     }
578
579     return m_paint_device;
580 }
581
582 void QQuickContext2DFBOTexture::endPainting()
583 {
584     QQuickContext2DTexture::endPainting();
585     if (m_multisampledFbo)
586         QOpenGLFramebufferObject::blitFramebuffer(m_fbo, m_multisampledFbo);
587
588     if (m_context->glContext()) {
589         /* When rendering happens on the render thread, the fbo's texture is
590          * used directly for display. If we are on the GUI thread or a
591          * dedicated Canvas render thread, we need to decouple the FBO from
592          * the texture we are displaying in the SG rendering thread to avoid
593          * stalls and read/write issues in the GL pipeline as the FBO's texture
594          * could then potentially be used in different threads.
595          *
596          * We could have gotten away with only one display texture, but this
597          * would have implied that beginPainting would have to wait for SG
598          * to release that texture.
599          */
600
601         if (m_onCustomThread)
602             m_mutex.lock();
603
604         QOpenGLFunctions *funcs = QOpenGLContext::currentContext()->functions();
605         if (m_displayTextures[0] == 0) {
606             m_displayTexture = 1;
607             funcs->glGenTextures(2, m_displayTextures);
608         }
609
610         m_fbo->bind();
611         GLuint target = m_displayTexture == 0 ? 1 : 0;
612         funcs->glBindTexture(GL_TEXTURE_2D, m_displayTextures[target]);
613         funcs->glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, m_fbo->width(), m_fbo->height(), 0);
614
615         if (m_onCustomThread)
616             m_mutex.unlock();
617     }
618
619     m_fbo->bindDefault();
620 }
621
622 QQuickContext2DImageTexture::QQuickContext2DImageTexture()
623     : QQuickContext2DTexture()
624 {
625 }
626
627 QQuickContext2DImageTexture::~QQuickContext2DImageTexture()
628 {
629 }
630
631 QQuickCanvasItem::RenderTarget QQuickContext2DImageTexture::renderTarget() const
632 {
633     return QQuickCanvasItem::Image;
634 }
635
636 QQuickContext2DTile* QQuickContext2DImageTexture::createTile() const
637 {
638     return new QQuickContext2DImageTile();
639 }
640
641 void QQuickContext2DImageTexture::grabImage(const QRectF& rf)
642 {
643     Q_ASSERT(rf.isValid());
644     Q_ASSERT(m_context);
645     QImage grabbed = m_displayImage.copy(rf.toRect());
646     m_context->setGrabbedImage(grabbed);
647 }
648
649 QSGTexture *QQuickContext2DImageTexture::textureForNextFrame(QSGTexture *last)
650 {
651     QSGPlainTexture *texture = static_cast<QSGPlainTexture *>(last);
652
653     if (m_onCustomThread)
654         m_mutex.lock();
655
656     if (!texture) {
657         texture = new QSGPlainTexture();
658         texture->setHasAlphaChannel(true);
659         m_dirtyTexture = true;
660     }
661     if (m_dirtyTexture) {
662         texture->setImage(m_displayImage);
663         m_dirtyTexture = false;
664     }
665
666     if (m_onCustomThread)
667         m_mutex.unlock();
668
669     return texture;
670 }
671
672 QPaintDevice* QQuickContext2DImageTexture::beginPainting()
673 {
674     QQuickContext2DTexture::beginPainting();
675
676     if (m_canvasWindow.size().isEmpty())
677         return 0;
678
679     if (m_canvasWindowChanged) {
680         m_image = QImage(m_canvasWindow.size(), QImage::Format_ARGB32_Premultiplied);
681         m_image.fill(0x00000000);
682         m_canvasWindowChanged = false;
683     }
684
685     return &m_image;
686 }
687
688 void QQuickContext2DImageTexture::endPainting()
689 {
690     QQuickContext2DTexture::endPainting();
691     if (m_onCustomThread)
692         m_mutex.lock();
693     m_displayImage = m_image;
694     if (m_onCustomThread)
695         m_mutex.unlock();
696 }
697
698 void QQuickContext2DImageTexture::compositeTile(QQuickContext2DTile* tile)
699 {
700     Q_ASSERT(!tile->dirty());
701     QQuickContext2DImageTile* t = static_cast<QQuickContext2DImageTile*>(tile);
702     QRect target = t->rect().intersected(m_canvasWindow);
703     if (target.isValid()) {
704         QRect source = target;
705         source.moveTo(source.topLeft() - t->rect().topLeft());
706         target.moveTo(target.topLeft() - m_canvasWindow.topLeft());
707
708         m_painter.begin(&m_image);
709         m_painter.setCompositionMode(QPainter::CompositionMode_Source);
710         m_painter.drawImage(target, t->image(), source);
711         m_painter.end();
712     }
713 }
714
715 QT_END_NAMESPACE