From f987da4c43eb96431a7b8d61521bf826fb0c5df0 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 18 Mar 2016 18:10:45 -0700 Subject: [PATCH] Move qml rendering to defined thread --- libraries/gl/src/gl/OffscreenQmlSurface.cpp | 509 ++++++++++---------- libraries/gl/src/gl/OffscreenQmlSurface.h | 4 +- 2 files changed, 260 insertions(+), 253 deletions(-) diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 6a780d74e2..c1203ad776 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -47,11 +47,10 @@ protected: private: QWindow* _renderWindow{ nullptr }; - friend class OffscreenQmlRenderer; + friend class OffscreenQmlRenderThread; friend class OffscreenQmlSurface; }; - Q_DECLARE_LOGGING_CATEGORY(offscreenFocus) Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus") @@ -60,264 +59,274 @@ static const QEvent::Type RENDER = QEvent::Type(QEvent::User + 2); static const QEvent::Type RESIZE = QEvent::Type(QEvent::User + 3); static const QEvent::Type STOP = QEvent::Type(QEvent::User + 4); -class OffscreenQmlRenderer : public OffscreenGLCanvas { - friend class OffscreenQmlSurface; +class OffscreenQmlRenderThread : public QThread { public: + OffscreenQmlRenderThread(OffscreenQmlSurface* surface, QOpenGLContext* shareContext); - OffscreenQmlRenderer(OffscreenQmlSurface* surface, QOpenGLContext* shareContext) : _surface(surface) { - OffscreenGLCanvas::create(shareContext); + virtual void run() override; + virtual bool event(QEvent *e) override; - _renderControl = new QMyQuickRenderControl(); +protected: + class Queue : public QQueue { + public: + void add(QEvent::Type type); + QEvent* take(); - // Create a QQuickWindow that is associated with out render control. Note that this - // window never gets created or shown, meaning that it will never get an underlying - // native (platform) window. - QQuickWindow::setDefaultAlphaBuffer(true); - // Weirdness... QQuickWindow NEEDS to be created on the rendering thread, or it will refuse to render - // because it retains an internal 'context' object that retains the thread it was created on, - // regardless of whether you later move it to another thread. - _quickWindow = new QQuickWindow(_renderControl); - _quickWindow->setColor(QColor(255, 255, 255, 0)); - _quickWindow->setFlags(_quickWindow->flags() | static_cast(Qt::WA_TranslucentBackground)); - - // Qt 5.5 - _renderControl->prepareThread(&_thread); - getContextObject()->moveToThread(&_thread); - moveToThread(&_thread); - _thread.setObjectName("QML Thread"); - _thread.start(); - post(INIT); - } - - bool event(QEvent *e) { - switch (int(e->type())) { - case INIT: - { - QMutexLocker lock(&_mutex); - init(); - } - return true; - case RENDER: - { - QMutexLocker lock(&_mutex); - render(&lock); - } - return true; - case RESIZE: - { - QMutexLocker lock(&_mutex); - resize(); - } - return true; - case STOP: - { - QMutexLocker lock(&_mutex); - cleanup(); - } - return true; - default: - return QObject::event(e); - } - } - - void post(const QEvent::Type& type) { - QCoreApplication::postEvent(this, new QEvent(type)); - } + private: + QMutex _mutex; + QWaitCondition _waitCondition; + bool _isWaiting{ false }; + }; + friend class OffscreenQmlSurface; + Queue _queue; + QMutex _mutex; + QWaitCondition _waitCondition; private: + // Event-driven methods + void init(); + void render(); + void resize(); + void cleanup(); - void setupFbo() { - using namespace oglplus; - _textures.setSize(_size); - _depthStencil.reset(new Renderbuffer()); - Context::Bound(Renderbuffer::Target::Renderbuffer, *_depthStencil) - .Storage( - PixelDataInternalFormat::DepthComponent, - _size.x, _size.y); - - _fbo.reset(new Framebuffer()); - _fbo->Bind(Framebuffer::Target::Draw); - _fbo->AttachRenderbuffer(Framebuffer::Target::Draw, - FramebufferAttachment::Depth, *_depthStencil); - DefaultFramebuffer().Bind(Framebuffer::Target::Draw); - } - - - - void init() { - connect(_renderControl, &QQuickRenderControl::renderRequested, _surface, &OffscreenQmlSurface::requestRender); - connect(_renderControl, &QQuickRenderControl::sceneChanged, _surface, &OffscreenQmlSurface::requestUpdate); - - if (!makeCurrent()) { - qWarning("Failed to make context current on render thread"); - return; - } - _renderControl->initialize(_context); - setupFbo(); - _escrow.setRecycler([this](GLuint texture){ - _textures.recycleTexture(texture); - }); - doneCurrent(); - } - - void cleanup() { - if (!makeCurrent()) { - qFatal("Failed to make context current on render thread"); - return; - } - _renderControl->invalidate(); - - _fbo.reset(); - _depthStencil.reset(); - _textures.clear(); - - doneCurrent(); - - getContextObject()->moveToThread(QCoreApplication::instance()->thread()); - _thread.quit(); - _cond.wakeOne(); - } - - void resize() { - // Update our members - if (_quickWindow) { - _quickWindow->setGeometry(QRect(QPoint(), _newSize)); - _quickWindow->contentItem()->setSize(_newSize); - } - - // Qt bug in 5.4 forces this check of pixel ratio, - // even though we're rendering offscreen. - qreal pixelRatio = 1.0; - if (_renderControl && _renderControl->_renderWindow) { - pixelRatio = _renderControl->_renderWindow->devicePixelRatio(); - } - - uvec2 newOffscreenSize = toGlm(_newSize * pixelRatio); - _textures.setSize(newOffscreenSize); - if (newOffscreenSize == _size) { - return; - } - _size = newOffscreenSize; - - // Clear out any fbos with the old size - if (!makeCurrent()) { - qWarning("Failed to make context current on render thread"); - return; - } - - qDebug() << "Offscreen UI resizing to " << _newSize.width() << "x" << _newSize.height() << " with pixel ratio " << pixelRatio; - setupFbo(); - doneCurrent(); - } - - void render(QMutexLocker *lock) { - if (_surface->_paused) { - return; - } - - if (!makeCurrent()) { - qWarning("Failed to make context current on render thread"); - return; - } - - _renderControl->sync(); - _cond.wakeOne(); - lock->unlock(); - - using namespace oglplus; - - _quickWindow->setRenderTarget(GetName(*_fbo), QSize(_size.x, _size.y)); - - try { - PROFILE_RANGE("qml_render") - TexturePtr texture = _textures.getNextTexture(); - _fbo->Bind(Framebuffer::Target::Draw); - _fbo->AttachTexture(Framebuffer::Target::Draw, FramebufferAttachment::Color, *texture, 0); - _fbo->Complete(Framebuffer::Target::Draw); - { - _renderControl->render(); - // FIXME The web browsers seem to be leaving GL in an error state. - // Need a debug context with sync logging to figure out why. - // for now just clear the errors - glGetError(); - } - // FIXME probably unecessary - DefaultFramebuffer().Bind(Framebuffer::Target::Draw); - _quickWindow->resetOpenGLState(); - _escrow.submit(GetName(*texture)); - _lastRenderTime = usecTimestampNow(); - } catch (std::runtime_error& error) { - qWarning() << "Failed to render QML " << error.what(); - } - } - - void aboutToQuit() { - QMutexLocker lock(&_quitMutex); - _quit = true; - } - - static const uint64_t MAX_SHUTDOWN_WAIT_SECS = 2; - void stop() { - if (_thread.isRunning()) { - qDebug() << "Stopping QML render thread " << _thread.currentThreadId(); - { - QMutexLocker lock(&_mutex); - post(STOP); - } - auto start = usecTimestampNow(); - auto now = usecTimestampNow(); - bool shutdownClean = false; - while (now - start < (MAX_SHUTDOWN_WAIT_SECS * USECS_PER_SECOND)) { - QMutexLocker lock(&_mutex); - if (_cond.wait(&_mutex, MSECS_PER_SECOND)) { - shutdownClean = true; - break; - } - now = usecTimestampNow(); - } - - if (!shutdownClean) { - qWarning() << "Failed to shut down the QML render thread"; - } - - } else { - qDebug() << "QML render thread already completed"; - } - } - - bool allowNewFrame(uint8_t fps) { - auto minRenderInterval = USECS_PER_SECOND / fps; - auto lastInterval = usecTimestampNow() - _lastRenderTime; - return (lastInterval > minRenderInterval); - } + // Helper methods + void setupFbo(); + bool allowNewFrame(uint8_t fps); + // Rendering members + OffscreenGLCanvas _canvas; OffscreenQmlSurface* _surface{ nullptr }; QQuickWindow* _quickWindow{ nullptr }; QMyQuickRenderControl* _renderControl{ nullptr }; - - QThread _thread; - QMutex _mutex; - QWaitCondition _cond; - QMutex _quitMutex; - - QSize _newSize; - bool _quit; FramebufferPtr _fbo; RenderbufferPtr _depthStencil; - uvec2 _size{ 1920, 1080 }; - uint64_t _lastRenderTime{ 0 }; TextureRecycler _textures; GLTextureEscrow _escrow; + + uint64_t _lastRenderTime{ 0 }; + uvec2 _size{ 1920, 1080 }; + QSize _newSize; + bool _quit{ false }; }; +void OffscreenQmlRenderThread::Queue::add(QEvent::Type type) { + QMutexLocker locker(&_mutex); + enqueue(new QEvent(type)); + if (_isWaiting) { + _waitCondition.wakeOne(); + } +} + +QEvent* OffscreenQmlRenderThread::Queue::take() { + QMutexLocker locker(&_mutex); + if (size() == 0) { + _isWaiting = true; + _waitCondition.wait(&_mutex); + _isWaiting = false; + } + QEvent* e = dequeue(); + return e; +} + +OffscreenQmlRenderThread::OffscreenQmlRenderThread(OffscreenQmlSurface* surface, QOpenGLContext* shareContext) : _surface(surface) { + _canvas.create(shareContext); + _renderControl = new QMyQuickRenderControl(); + QQuickWindow::setDefaultAlphaBuffer(true); + // Create a QQuickWindow that is associated with our render control. + // This window never gets created or shown, meaning that it will never get an underlying native (platform) window. + // NOTE: Must be created on the main thread so that OffscreenQmlSurface can send it events + // NOTE: Must be created on the rendering thread or it will refuse to render, + // so we wait until after its ctor to move object/context to this thread. + _quickWindow = new QQuickWindow(_renderControl); + _quickWindow->setColor(QColor(255, 255, 255, 0)); + _quickWindow->setFlags(_quickWindow->flags() | static_cast(Qt::WA_TranslucentBackground)); + + // We can prepare, but we must wait to start() the thread until after the ctor + _renderControl->prepareThread(this); + _canvas.getContextObject()->moveToThread(this); + moveToThread(this); + + _queue.add(INIT); +} + +void OffscreenQmlRenderThread::run() { + while (!_quit) { + QEvent* e = _queue.take(); + event(e); + delete e; + } +} + +bool OffscreenQmlRenderThread::event(QEvent *e) { + switch (int(e->type())) { + case INIT: + init(); + return true; + case RENDER: + render(); + return true; + case RESIZE: + resize(); + return true; + case STOP: + cleanup(); + return true; + default: + return QObject::event(e); + } +} + +void OffscreenQmlRenderThread::setupFbo() { + using namespace oglplus; + _textures.setSize(_size); + _depthStencil.reset(new Renderbuffer()); + Context::Bound(Renderbuffer::Target::Renderbuffer, *_depthStencil) + .Storage( + PixelDataInternalFormat::DepthComponent, + _size.x, _size.y); + + _fbo.reset(new Framebuffer()); + _fbo->Bind(Framebuffer::Target::Draw); + _fbo->AttachRenderbuffer(Framebuffer::Target::Draw, + FramebufferAttachment::Depth, *_depthStencil); + DefaultFramebuffer().Bind(Framebuffer::Target::Draw); +} + +void OffscreenQmlRenderThread::init() { + connect(_renderControl, &QQuickRenderControl::renderRequested, _surface, &OffscreenQmlSurface::requestRender); + connect(_renderControl, &QQuickRenderControl::sceneChanged, _surface, &OffscreenQmlSurface::requestUpdate); + + if (!_canvas.makeCurrent()) { + qWarning("Failed to make context current on render thread"); + return; + } + _renderControl->initialize(_canvas.getContext()); + setupFbo(); + _escrow.setRecycler([this](GLuint texture){ + _textures.recycleTexture(texture); + }); + _canvas.doneCurrent(); +} + +void OffscreenQmlRenderThread::cleanup() { + if (!_canvas.makeCurrent()) { + qFatal("Failed to make context current on render thread"); + return; + } + _renderControl->invalidate(); + + _fbo.reset(); + _depthStencil.reset(); + _textures.clear(); + + _canvas.doneCurrent(); + + _canvas.getContextObject()->moveToThread(QCoreApplication::instance()->thread()); + + _quit = true; +} + +void OffscreenQmlRenderThread::resize() { + // Lock _newSize changes + QMutexLocker locker(&_mutex); + + // Update our members + if (_quickWindow) { + _quickWindow->setGeometry(QRect(QPoint(), _newSize)); + _quickWindow->contentItem()->setSize(_newSize); + } + + // Qt bug in 5.4 forces this check of pixel ratio, + // even though we're rendering offscreen. + qreal pixelRatio = 1.0; + if (_renderControl && _renderControl->_renderWindow) { + pixelRatio = _renderControl->_renderWindow->devicePixelRatio(); + } + + uvec2 newOffscreenSize = toGlm(_newSize * pixelRatio); + _textures.setSize(newOffscreenSize); + if (newOffscreenSize == _size) { + return; + } + _size = newOffscreenSize; + + // Clear out any fbos with the old size + if (!_canvas.makeCurrent()) { + qWarning("Failed to make context current on render thread"); + return; + } + + qDebug() << "Offscreen UI resizing to " << _newSize.width() << "x" << _newSize.height() << " with pixel ratio " << pixelRatio; + + locker.unlock(); + + setupFbo(); + _canvas.doneCurrent(); +} + +void OffscreenQmlRenderThread::render() { + if (_surface->_paused) { + return; + } + + if (!_canvas.makeCurrent()) { + qWarning("Failed to make context current on render thread"); + return; + } + + QMutexLocker locker(&_mutex); + _renderControl->sync(); + _waitCondition.wakeOne(); + locker.unlock(); + + using namespace oglplus; + + _quickWindow->setRenderTarget(GetName(*_fbo), QSize(_size.x, _size.y)); + + try { + PROFILE_RANGE("qml_render") + TexturePtr texture = _textures.getNextTexture(); + _fbo->Bind(Framebuffer::Target::Draw); + _fbo->AttachTexture(Framebuffer::Target::Draw, FramebufferAttachment::Color, *texture, 0); + _fbo->Complete(Framebuffer::Target::Draw); + { + _renderControl->render(); + // FIXME The web browsers seem to be leaving GL in an error state. + // Need a debug context with sync logging to figure out why. + // for now just clear the errors + glGetError(); + } + // FIXME probably unecessary + DefaultFramebuffer().Bind(Framebuffer::Target::Draw); + _quickWindow->resetOpenGLState(); + _escrow.submit(GetName(*texture)); + _lastRenderTime = usecTimestampNow(); + } catch (std::runtime_error& error) { + qWarning() << "Failed to render QML " << error.what(); + } +} + +bool OffscreenQmlRenderThread::allowNewFrame(uint8_t fps) { + auto minRenderInterval = USECS_PER_SECOND / fps; + auto lastInterval = usecTimestampNow() - _lastRenderTime; + return (lastInterval > minRenderInterval); +} + OffscreenQmlSurface::OffscreenQmlSurface() { } +static const uint64_t MAX_SHUTDOWN_WAIT_SECS = 2; OffscreenQmlSurface::~OffscreenQmlSurface() { QObject::disconnect(&_updateTimer); QObject::disconnect(qApp); - _renderer->stop(); + + qDebug() << "Stopping QML render thread " << _renderer->currentThreadId(); + _renderer->_queue.add(STOP); + if (!_renderer->wait(MAX_SHUTDOWN_WAIT_SECS * USECS_PER_SECOND)) { + qWarning() << "Failed to shut down the QML render thread"; + } + delete _rootItem; delete _renderer; delete _qmlComponent; @@ -326,15 +335,16 @@ OffscreenQmlSurface::~OffscreenQmlSurface() { void OffscreenQmlSurface::onAboutToQuit() { QObject::disconnect(&_updateTimer); - // Disconnecting the update timer is insufficient, since the renderer - // may attempting to render already, so we need to explicitly tell the renderer - // to stop - _renderer->aboutToQuit(); } void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { - _renderer = new OffscreenQmlRenderer(this, shareContext); + _renderer = new OffscreenQmlRenderThread(this, shareContext); + _renderer->moveToThread(_renderer); + _renderer->setObjectName("QML Renderer Thread"); + _renderer->start(); + _renderer->_renderControl->_renderWindow = _proxyWindow; + // Create a QML engine. _qmlEngine = new QQmlEngine; if (!_qmlEngine->incubationController()) { @@ -383,11 +393,11 @@ void OffscreenQmlSurface::resize(const QSize& newSize_) { } { - QMutexLocker _locker(&(_renderer->_mutex)); + QMutexLocker locker(&(_renderer->_mutex)); _renderer->_newSize = newSize; } - _renderer->post(RESIZE); + _renderer->_queue.add(RESIZE); } QQuickItem* OffscreenQmlSurface::getRootItem() { @@ -487,14 +497,11 @@ void OffscreenQmlSurface::updateQuick() { } if (_render) { - QMutexLocker lock(&(_renderer->_mutex)); - _renderer->post(RENDER); - while (!_renderer->_cond.wait(&(_renderer->_mutex), 100)) { - if (_renderer->_quit) { - return; - } - qApp->processEvents(); - } + // Lock the GUI size while syncing + QMutexLocker locker(&(_renderer->_mutex)); + _renderer->_queue.add(RENDER); + _renderer->_waitCondition.wait(&(_renderer->_mutex)); + _render = false; } diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index 9142e7f2ef..f30a9754ff 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -84,8 +84,8 @@ private slots: void updateQuick(); private: - friend class OffscreenQmlRenderer; - OffscreenQmlRenderer* _renderer{ nullptr }; + friend class OffscreenQmlRenderThread; + OffscreenQmlRenderThread* _renderer{ nullptr }; QQmlEngine* _qmlEngine{ nullptr }; QQmlComponent* _qmlComponent{ nullptr }; QQuickItem* _rootItem{ nullptr };