diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index dba2ed3234..ddef3396a2 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -77,6 +77,7 @@ #include <NetworkAccessManager.h> #include <NetworkingConstants.h> #include <ObjectMotionState.h> +#include <OffscreenGlCanvas.h> #include <OctalCode.h> #include <OctreeSceneStats.h> #include <udt/PacketHeaders.h> diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 80f5ce2b69..6f18cac127 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -46,12 +46,7 @@ ApplicationOverlay::ApplicationOverlay() // then release it back to the UI for re-use auto offscreenUi = DependencyManager::get<OffscreenUi>(); connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [&](GLuint textureId) { - auto offscreenUi = DependencyManager::get<OffscreenUi>(); - offscreenUi->lockTexture(textureId); - std::swap(_uiTexture, textureId); - if (textureId) { - offscreenUi->releaseTexture(textureId); - } + _uiTexture = textureId; }); } diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 5a08abbb22..ce5b389333 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -43,20 +43,12 @@ RenderableWebEntityItem::~RenderableWebEntityItem() { if (_webSurface) { _webSurface->pause(); _webSurface->disconnect(_connection); - // After the disconnect, ensure that we have the latest texture by acquiring the - // lock used when updating the _texture value - _textureLock.lock(); - _textureLock.unlock(); // The lifetime of the QML surface MUST be managed by the main thread // Additionally, we MUST use local variables copied by value, rather than // member variables, since they would implicitly refer to a this that // is no longer valid auto webSurface = _webSurface; - auto texture = _texture; - AbstractViewStateInterface::instance()->postLambdaEvent([webSurface, texture] { - if (texture) { - webSurface->releaseTexture(texture); - } + AbstractViewStateInterface::instance()->postLambdaEvent([webSurface] { webSurface->deleteLater(); }); } @@ -74,23 +66,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) { _webSurface->resume(); _webSurface->getRootItem()->setProperty("url", _sourceUrl); _connection = QObject::connect(_webSurface, &OffscreenQmlSurface::textureUpdated, [&](GLuint textureId) { - _webSurface->lockTexture(textureId); - assert(!glGetError()); - // TODO change to atomic<GLuint>? - withLock(_textureLock, [&] { - std::swap(_texture, textureId); - }); - if (textureId) { - _webSurface->releaseTexture(textureId); - } - if (_texture) { - _webSurface->makeCurrent(); - glBindTexture(GL_TEXTURE_2D, _texture); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glBindTexture(GL_TEXTURE_2D, 0); - _webSurface->doneCurrent(); - } + _texture = textureId; }); auto forwardMouseEvent = [=](const RayToEntityIntersectionResult& intersection, const QMouseEvent* event, unsigned int deviceId) { @@ -145,6 +121,19 @@ void RenderableWebEntityItem::render(RenderArgs* args) { point += 0.5f; point.y = 1.0f - point.y; point *= getDimensions() * METERS_TO_INCHES * DPI; + + if (event->button() == Qt::MouseButton::LeftButton) { + if (event->type() == QEvent::MouseButtonPress) { + this->_pressed = true; + this->_lastMove = ivec2((int)point.x, (int)point.y); + } else if (event->type() == QEvent::MouseButtonRelease) { + this->_pressed = false; + } + } + if (event->type() == QEvent::MouseMove) { + this->_lastMove = ivec2((int)point.x, (int)point.y); + } + // Forward the mouse event. QMouseEvent mappedEvent(event->type(), QPoint((int)point.x, (int)point.y), @@ -158,6 +147,16 @@ void RenderableWebEntityItem::render(RenderArgs* args) { QObject::connect(renderer, &EntityTreeRenderer::mousePressOnEntity, forwardMouseEvent); QObject::connect(renderer, &EntityTreeRenderer::mouseReleaseOnEntity, forwardMouseEvent); QObject::connect(renderer, &EntityTreeRenderer::mouseMoveOnEntity, forwardMouseEvent); + QObject::connect(renderer, &EntityTreeRenderer::hoverLeaveEntity, [=](const EntityItemID& entityItemID, const MouseEvent& event) { + if (this->_pressed && this->getID() == entityItemID) { + // If the user mouses off the entity while the button is down, simulate a mouse release + QMouseEvent mappedEvent(QEvent::MouseButtonRelease, + QPoint(_lastMove.x, _lastMove.y), + Qt::MouseButton::LeftButton, + Qt::MouseButtons(), Qt::KeyboardModifiers()); + QCoreApplication::sendEvent(_webSurface->getWindow(), &mappedEvent); + } + }); } glm::vec2 dims = glm::vec2(getDimensions()); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index 47e847a166..ee9c2531f1 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -34,7 +34,8 @@ private: QMetaObject::Connection _connection; uint32_t _texture{ 0 }; ivec2 _lastPress{ INT_MIN }; - QMutex _textureLock; + bool _pressed{ false }; + ivec2 _lastMove{ INT_MIN }; }; diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt index 973e8b562a..0ea71e54e3 100644 --- a/libraries/render-utils/CMakeLists.txt +++ b/libraries/render-utils/CMakeLists.txt @@ -32,4 +32,12 @@ if (WIN32) endif() endif (WIN32) +add_dependency_external_projects(boostconfig) +find_package(BoostConfig REQUIRED) +target_include_directories(${TARGET_NAME} PUBLIC ${BOOSTCONFIG_INCLUDE_DIRS}) + +add_dependency_external_projects(oglplus) +find_package(OGLPLUS REQUIRED) +target_include_directories(${TARGET_NAME} PUBLIC ${OGLPLUS_INCLUDE_DIRS}) + link_hifi_libraries(animation fbx shared gpu model render environment) diff --git a/libraries/render-utils/src/GLEscrow.cpp b/libraries/render-utils/src/GLEscrow.cpp new file mode 100644 index 0000000000..253af35d92 --- /dev/null +++ b/libraries/render-utils/src/GLEscrow.cpp @@ -0,0 +1,9 @@ +// +// Created by Bradley Austin Davis on 2015/08/06. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "GLEscrow.h" diff --git a/libraries/render-utils/src/GLEscrow.h b/libraries/render-utils/src/GLEscrow.h new file mode 100644 index 0000000000..54b124ae3c --- /dev/null +++ b/libraries/render-utils/src/GLEscrow.h @@ -0,0 +1,222 @@ +// +// Created by Bradley Austin Davis on 2015/08/06. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#ifndef hifi_GLEscrow_h +#define hifi_GLEscrow_h + +#include <utility> +#include <algorithm> +#include <deque> +#include <forward_list> +#include <functional> +#include <GL/glew.h> +#include <mutex> + +#include <SharedUtil.h> +#include <NumericalConstants.h> + +// The GLEscrow class provides a simple mechanism for producer GL contexts to provide +// content to a consumer where the consumer is assumed to be connected to a display and +// therefore must never be blocked. +// +// So we need to accomplish a few things. +// +// First the producer context needs to be able to supply content to the primary thread +// in such a way that the consumer only gets it when it's actually valid for reading +// (meaning that the async writing operations have been completed) +// +// Second, the client thread should be able to release the resource when it's finished +// using it (but again the reading of the resource is likely asyncronous) +// +// Finally, blocking operations need to be minimal, and any potentially blocking operations +// that can't be avoided need to be pushed to the submission context to avoid impacting +// the framerate of the consumer +// +// This class acts as a kind of border guard and holding pen between the two contexts +// to hold resources which the CPU is no longer using, but which might still be +// in use by the GPU. Fence sync objects are used to moderate the actual release of +// resources in either direction. +template < + typename T, + // Only accept numeric types + typename = typename std::enable_if<std::is_arithmetic<T>::value, T>::type +> +class GLEscrow { +public: + + struct Item { + T _value; + GLsync _sync; + uint64_t _created; + + Item(T value, GLsync sync) : + _value(value), _sync(sync), _created(usecTimestampNow()) + { + } + + uint64_t age() { + return usecTimestampNow() - _created; + } + + bool signaled() { + auto result = glClientWaitSync(_sync, 0, 0); + if (GL_TIMEOUT_EXPIRED != result && GL_WAIT_FAILED != result) { + return true; + } + if (age() > (USECS_PER_SECOND / 2)) { + qWarning() << "Long unsignaled sync"; + } + return false; + } + }; + + using Mutex = std::mutex; + using Lock = std::unique_lock<Mutex>; + using Recycler = std::function<void(T t)>; + // deque gives us random access, double ended push & pop and size, all in constant time + using Deque = std::deque<Item>; + using List = std::forward_list<Item>; + + void setRecycler(Recycler recycler) { + _recycler = recycler; + } + + // Submit a new resource from the producer context + // returns the number of prior submissions that were + // never consumed before becoming available. + // producers should self-limit if they start producing more + // work than is being consumed; + size_t submit(T t, GLsync writeSync = 0) { + if (!writeSync) { + // FIXME should the release and submit actually force the creation of a fence? + writeSync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + } + + { + Lock lock(_mutex); + _submits.push_back(Item(t, writeSync)); + } + + return cleanTrash(); + } + + // Returns the next available resource provided by the submitter, + // or if none is available (which could mean either the submission + // list is empty or that the first item on the list isn't yet signaled + T fetch() { + T result{0}; + // On the one hand using try_lock() reduces the chance of blocking the consumer thread, + // but if the produce thread is going fast enough, it could effectively + // starve the consumer out of ever actually getting resources. + if (_mutex.try_lock()) { + if (signaled(_submits, 0)) { + result = _submits.at(0)._value; + _submits.pop_front(); + } + _mutex.unlock(); + } + return result; + } + + // If fetch returns a non-zero value, it's the responsibility of the + // client to release it at some point + void release(T t, GLsync readSync = 0) { + if (!readSync) { + // FIXME should the release and submit actually force the creation of a fence? + readSync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + } + + Lock lock(_mutex); + _releases.push_back(Item(t, readSync)); + } + +private: + size_t cleanTrash() { + size_t wastedWork{ 0 }; + List trash; + { + // We only ever need one ready item available in the list, so if the + // second item is signaled (implying the first is as well, remove the first + // item. Iterate until the SECOND item in the list is not in the ready state + // The signaled function takes care of checking against the deque size + while (signaled(_submits, 1)) { + pop(_submits); + ++wastedWork; + } + + // Stuff in the release queue can be cleared out as soon as it's signaled + while (signaled(_releases, 0)) { + pop(_releases); + } + + trash.swap(_trash); + } + + // FIXME maybe doing a timing on the deleters and warn if it's taking excessive time? + // although we are out of the lock, so it shouldn't be blocking anything + std::for_each(trash.begin(), trash.end(), [&](typename List::const_reference item) { + if (item._value) { + _recycler(item._value); + } + if (item._sync) { + glDeleteSync(item._sync); + } + }); + return wastedWork; + } + + // May be called on any thread, but must be inside a locked section + void pop(Deque& deque) { + auto& item = deque.front(); + _trash.push_front(item); + deque.pop_front(); + } + + // May be called on any thread, but must be inside a locked section + bool signaled(Deque& deque, size_t i) { + if (i >= deque.size()) { + return false; + } + + auto& item = deque.at(i); + // If there's no sync object, either it's not required or it's already been found to be signaled + if (!item._sync) { + return true; + } + + // Check the sync value using a zero timeout to ensure we don't block + // This is critically important as this is the only GL function we'll call + // inside the locked sections, so it cannot have any latency + if (item.signaled()) { + // if the sync is signaled, queue it for deletion + _trash.push_front(Item(0, item._sync)); + // And change the stored value to 0 so we don't check it again + item._sync = 0; + return true; + } + + return false; + } + + Mutex _mutex; + Recycler _recycler; + // Items coming from the submission / writer context + Deque _submits; + // Items coming from the client context. + Deque _releases; + // Items which are no longer in use. + List _trash; +}; + +using GLTextureEscrow = GLEscrow<GLuint>; + +#endif + diff --git a/libraries/render-utils/src/OffscreenQmlSurface.cpp b/libraries/render-utils/src/OffscreenQmlSurface.cpp index d5c495b414..78edb8e899 100644 --- a/libraries/render-utils/src/OffscreenQmlSurface.cpp +++ b/libraries/render-utils/src/OffscreenQmlSurface.cpp @@ -6,21 +6,34 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "OffscreenQmlSurface.h" +#include "OglplusHelpers.h" -#include <QOpenGLFramebufferObject> -#include <QOpenGLDebugLogger> -#include <QGLWidget> +#include <QWidget> #include <QtQml> #include <QQmlEngine> #include <QQmlComponent> #include <QQuickItem> #include <QQuickWindow> #include <QQuickRenderControl> +#include <QWaitCondition> +#include <QMutex> -#include "FboCache.h" #include <PerfStat.h> #include <NumericalConstants.h> +#include "GLEscrow.h" +#include "OffscreenGlCanvas.h" +#include "AbstractViewStateInterface.h" + +// FIXME move to threaded rendering with Qt 5.5 +// #define QML_THREADED + +// Time between receiving a request to render the offscreen UI actually triggering +// the render. Could possibly be increased depending on the framerate we expect to +// achieve. +// This has the effect of capping the framerate at 200 +static const int MIN_TIMER_MS = 5; + class QMyQuickRenderControl : public QQuickRenderControl { protected: QWindow* renderWindow(QPoint* offset) Q_DECL_OVERRIDE{ @@ -35,119 +48,324 @@ protected: private: QWindow* _renderWindow{ nullptr }; + friend class OffscreenQmlRenderer; friend class OffscreenQmlSurface; }; -#include "AbstractViewStateInterface.h" + Q_DECLARE_LOGGING_CATEGORY(offscreenFocus) Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus") -// Time between receiving a request to render the offscreen UI actually triggering -// the render. Could possibly be increased depending on the framerate we expect to -// achieve. -static const int MAX_QML_FRAMERATE = 10; -static const int MIN_RENDER_INTERVAL_US = USECS_PER_SECOND / MAX_QML_FRAMERATE; -static const int MIN_TIMER_MS = 5; +#ifdef QML_THREADED +static const QEvent::Type INIT = QEvent::Type(QEvent::User + 1); +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); +static const QEvent::Type UPDATE = QEvent::Type(QEvent::User + 5); +#endif + +class OffscreenQmlRenderer : public OffscreenGlCanvas { + friend class OffscreenQmlSurface; +public: + + OffscreenQmlRenderer(OffscreenQmlSurface* surface, QOpenGLContext* shareContext) : _surface(surface) { + OffscreenGlCanvas::create(shareContext); +#ifdef QML_THREADED + // Qt 5.5 + // _renderControl->prepareThread(_renderThread); + _context->moveToThread(&_thread); + moveToThread(&_thread); + _thread.setObjectName("QML Thread"); + _thread.start(); + post(INIT); +#else + init(); +#endif + } + +#ifdef QML_THREADED + 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)); + } + +#endif + +private: + + 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); + } -OffscreenQmlSurface::OffscreenQmlSurface() : - _renderControl(new QMyQuickRenderControl), _fboCache(new FboCache) { + + void init() { + _renderControl = new QMyQuickRenderControl(); + connect(_renderControl, &QQuickRenderControl::renderRequested, _surface, &OffscreenQmlSurface::requestRender); + connect(_renderControl, &QQuickRenderControl::sceneChanged, _surface, &OffscreenQmlSurface::requestUpdate); + + // 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::WindowFlags>(Qt::WA_TranslucentBackground)); + +#ifdef QML_THREADED + // However, because we want to use synchronous events with the quickwindow, we need to move it back to the main + // thread after it's created. + _quickWindow->moveToThread(qApp->thread()); +#endif + + 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(); + +#ifdef QML_THREADED + _context->moveToThread(QCoreApplication::instance()->thread()); + _cond.wakeOne(); +#endif + } + + void resize(const QSize& newSize) { + // 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(); + } else { + pixelRatio = AbstractViewStateInterface::instance()->getDevicePixelRatio(); + } + + 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; + } + + Q_ASSERT(toGlm(_quickWindow->geometry().size()) == _size); + //Q_ASSERT(toGlm(_quickWindow->geometry().size()) == _textures._size); + + _renderControl->sync(); +#ifdef QML_THREADED + _cond.wakeOne(); + lock->unlock(); +#endif + + + using namespace oglplus; + + _quickWindow->setRenderTarget(GetName(*_fbo), QSize(_size.x, _size.y)); + + TexturePtr texture = _textures.getNextTexture(); + _fbo->Bind(Framebuffer::Target::Draw); + _fbo->AttachTexture(Framebuffer::Target::Draw, FramebufferAttachment::Color, *texture, 0); + _fbo->Complete(Framebuffer::Target::Draw); + //Context::Clear().ColorBuffer(); + { + _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(); + } + + void aboutToQuit() { +#ifdef QML_THREADED + QMutexLocker lock(&_quitMutex); + _quit = true; +#endif + } + + void stop() { +#ifdef QML_THREADED + QMutexLocker lock(&_quitMutex); + post(STOP); + _cond.wait(&_mutex); +#else + cleanup(); +#endif + } + + bool allowNewFrame(uint8_t fps) { + auto minRenderInterval = USECS_PER_SECOND / fps; + auto lastInterval = usecTimestampNow() - _lastRenderTime; + return (lastInterval > minRenderInterval); + } + + OffscreenQmlSurface* _surface{ nullptr }; + QQuickWindow* _quickWindow{ nullptr }; + QMyQuickRenderControl* _renderControl{ nullptr }; + +#ifdef QML_THREADED + QThread _thread; + QMutex _mutex; + QWaitCondition _cond; + QMutex _quitMutex; +#endif + + bool _quit; + FramebufferPtr _fbo; + RenderbufferPtr _depthStencil; + uvec2 _size{ 1920, 1080 }; + uint64_t _lastRenderTime{ 0 }; + TextureRecycler _textures; + GLTextureEscrow _escrow; +}; + +OffscreenQmlSurface::OffscreenQmlSurface() { } OffscreenQmlSurface::~OffscreenQmlSurface() { - // Make sure the context is current while doing cleanup. Note that we use the - // offscreen surface here because passing 'this' at this point is not safe: the - // underlying platform window may already be destroyed. To avoid all the trouble, use - // another surface that is valid for sure. - makeCurrent(); - - // Delete the render control first since it will free the scenegraph resources. - // Destroy the QQuickWindow only afterwards. - delete _renderControl; + _renderer->stop(); + delete _renderer; delete _qmlComponent; - delete _quickWindow; delete _qmlEngine; - - doneCurrent(); - delete _fboCache; } void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { - OffscreenGlCanvas::create(shareContext); + _renderer = new OffscreenQmlRenderer(this, shareContext); - makeCurrent(); - - // 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); - _quickWindow = new QQuickWindow(_renderControl); - _quickWindow->setColor(QColor(255, 255, 255, 0)); - _quickWindow->setFlags(_quickWindow->flags() | static_cast<Qt::WindowFlags>(Qt::WA_TranslucentBackground)); // Create a QML engine. _qmlEngine = new QQmlEngine; if (!_qmlEngine->incubationController()) { - _qmlEngine->setIncubationController(_quickWindow->incubationController()); + _qmlEngine->setIncubationController(_renderer->_quickWindow->incubationController()); } // When Quick says there is a need to render, we will not render immediately. Instead, // a timer with a small interval is used to get better performance. - _updateTimer.setSingleShot(true); + _updateTimer.setInterval(MIN_TIMER_MS); connect(&_updateTimer, &QTimer::timeout, this, &OffscreenQmlSurface::updateQuick); - - // Now hook up the signals. For simplicy we don't differentiate between - // renderRequested (only render is needed, no sync) and sceneChanged (polish and sync - // is needed too). - connect(_renderControl, &QQuickRenderControl::renderRequested, this, &OffscreenQmlSurface::requestRender); - connect(_renderControl, &QQuickRenderControl::sceneChanged, this, &OffscreenQmlSurface::requestUpdate); - -#ifdef DEBUG - connect(_quickWindow, &QQuickWindow::focusObjectChanged, [this]{ - qCDebug(offscreenFocus) << "New focus item " << _quickWindow->focusObject(); - }); - connect(_quickWindow, &QQuickWindow::activeFocusItemChanged, [this] { - qCDebug(offscreenFocus) << "New active focus item " << _quickWindow->activeFocusItem(); - }); -#endif + _updateTimer.start(); _qmlComponent = new QQmlComponent(_qmlEngine); - // Initialize the render control and our OpenGL resources. - makeCurrent(); - _renderControl->initialize(_context); } void OffscreenQmlSurface::resize(const QSize& newSize) { - // Qt bug in 5.4 forces this check of pixel ratio, - // even though we're rendering offscreen. - qreal pixelRatio = 1.0; +#ifdef QML_THREADED + QMutexLocker _locker(&(_renderer->_mutex)); +#endif + if (!_renderer || !_renderer->_quickWindow) { + QSize currentSize = _renderer->_quickWindow->geometry().size(); + if (newSize == currentSize) { + return; + } + } + _qmlEngine->rootContext()->setContextProperty("surfaceSize", newSize); - if (_renderControl && _renderControl->_renderWindow) { - pixelRatio = _renderControl->_renderWindow->devicePixelRatio(); - } else { - pixelRatio = AbstractViewStateInterface::instance()->getDevicePixelRatio(); - } - QSize newOffscreenSize = newSize * pixelRatio; - if (newOffscreenSize == _fboCache->getSize()) { - return; - } - // Clear out any fbos with the old size - makeCurrent(); - qDebug() << "Offscreen UI resizing to " << newSize.width() << "x" << newSize.height() << " with pixel ratio " << pixelRatio; - _fboCache->setSize(newSize * pixelRatio); - - if (_quickWindow) { - _quickWindow->setGeometry(QRect(QPoint(), newSize)); - _quickWindow->contentItem()->setSize(newSize); - } - - // Update our members if (_rootItem) { _rootItem->setSize(newSize); } - doneCurrent(); +#ifdef QML_THREADED + _renderer->post(RESIZE); +#else + _renderer->resize(newSize); +#endif } QQuickItem* OffscreenQmlSurface::getRootItem() { @@ -173,20 +391,11 @@ QObject* OffscreenQmlSurface::load(const QUrl& qmlSource, std::function<void(QQm void OffscreenQmlSurface::requestUpdate() { _polish = true; - requestRender(); + _render = true; } void OffscreenQmlSurface::requestRender() { - if (!_updateTimer.isActive()) { - auto now = usecTimestampNow(); - auto lastInterval = now - _lastRenderTime; - if (lastInterval > MIN_RENDER_INTERVAL_US) { - _updateTimer.setInterval(MIN_TIMER_MS); - } else { - _updateTimer.setInterval((MIN_RENDER_INTERVAL_US - lastInterval) / USECS_PER_MSEC); - } - _updateTimer.start(); - } + _render = true; } QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QObject*)> f) { @@ -240,54 +449,38 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QOb } // The root item is ready. Associate it with the window. _rootItem = newItem; - _rootItem->setParentItem(_quickWindow->contentItem()); - _rootItem->setSize(_quickWindow->renderTargetSize()); + _rootItem->setParentItem(_renderer->_quickWindow->contentItem()); + _rootItem->setSize(_renderer->_quickWindow->renderTargetSize()); return _rootItem; } - void OffscreenQmlSurface::updateQuick() { - PerformanceTimer perfTimer("qmlUpdate"); - if (_paused) { - return; - } - - if (!makeCurrent()) { + if (!_renderer || !_renderer->allowNewFrame(_maxFps)) { return; } - // Polish, synchronize and render the next frame (into our fbo). In this example - // everything happens on the same thread and therefore all three steps are performed - // in succession from here. In a threaded setup the render() call would happen on a - // separate thread. if (_polish) { - _renderControl->polishItems(); - _renderControl->sync(); + _renderer->_renderControl->polishItems(); _polish = false; } - QOpenGLFramebufferObject* fbo = _fboCache->getReadyFbo(); + if (_render) { +#ifdef QML_THREADED + _renderer->post(RENDER); +#else + _renderer->render(nullptr); +#endif + _render = false; + } - _quickWindow->setRenderTarget(fbo); - fbo->bind(); - - glClearColor(0, 0, 0, 1); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - _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(); - - _quickWindow->resetOpenGLState(); - - QOpenGLFramebufferObject::bindDefault(); - _lastRenderTime = usecTimestampNow(); - // Force completion of all the operations before we emit the texture as being ready for use - glFinish(); - - emit textureUpdated(fbo->texture()); + GLuint newTexture = _renderer->_escrow.fetch(); + if (newTexture) { + if (_currentTexture) { + _renderer->_escrow.release(_currentTexture); + } + _currentTexture = newTexture; + emit textureUpdated(_currentTexture); + } } QPointF OffscreenQmlSurface::mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject) { @@ -299,7 +492,7 @@ QPointF OffscreenQmlSurface::mapWindowToUi(const QPointF& sourcePosition, QObjec } vec2 offscreenPosition = toGlm(sourcePosition); offscreenPosition /= sourceSize; - offscreenPosition *= vec2(toGlm(_quickWindow->size())); + offscreenPosition *= vec2(toGlm(_renderer->_quickWindow->size())); return QPointF(offscreenPosition.x, offscreenPosition.y); } @@ -309,7 +502,7 @@ QPointF OffscreenQmlSurface::mapWindowToUi(const QPointF& sourcePosition, QObjec // bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* event) { - if (_quickWindow == originalDestination) { + if (_renderer->_quickWindow == originalDestination) { return false; } // Only intercept events while we're in an active state @@ -321,7 +514,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even // Don't intercept our own events, or we enter an infinite recursion QObject* recurseTest = originalDestination; while (recurseTest) { - Q_ASSERT(recurseTest != _rootItem && recurseTest != _quickWindow); + Q_ASSERT(recurseTest != _rootItem && recurseTest != _renderer->_quickWindow); recurseTest = recurseTest->parent(); } #endif @@ -330,7 +523,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even switch (event->type()) { case QEvent::Resize: { QResizeEvent* resizeEvent = static_cast<QResizeEvent*>(event); - QGLWidget* widget = dynamic_cast<QGLWidget*>(originalDestination); + QWidget* widget = dynamic_cast<QWidget*>(originalDestination); if (widget) { this->resize(resizeEvent->size()); } @@ -340,7 +533,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even case QEvent::KeyPress: case QEvent::KeyRelease: { event->ignore(); - if (QCoreApplication::sendEvent(_quickWindow, event)) { + if (QCoreApplication::sendEvent(_renderer->_quickWindow, event)) { return event->isAccepted(); } break; @@ -353,7 +546,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even wheelEvent->delta(), wheelEvent->buttons(), wheelEvent->modifiers(), wheelEvent->orientation()); mappedEvent.ignore(); - if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) { + if (QCoreApplication::sendEvent(_renderer->_quickWindow, &mappedEvent)) { return mappedEvent.isAccepted(); } break; @@ -376,7 +569,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even _qmlEngine->rootContext()->setContextProperty("lastMousePosition", transformedPos); } mappedEvent.ignore(); - if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) { + if (QCoreApplication::sendEvent(_renderer->_quickWindow, &mappedEvent)) { return mappedEvent.isAccepted(); } break; @@ -389,14 +582,6 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even return false; } -void OffscreenQmlSurface::lockTexture(int texture) { - _fboCache->lockTexture(texture); -} - -void OffscreenQmlSurface::releaseTexture(int texture) { - _fboCache->releaseTexture(texture); -} - void OffscreenQmlSurface::pause() { _paused = true; } @@ -411,13 +596,13 @@ bool OffscreenQmlSurface::isPaused() const { } void OffscreenQmlSurface::setProxyWindow(QWindow* window) { - _renderControl->_renderWindow = window; + _renderer->_renderControl->_renderWindow = window; } QQuickWindow* OffscreenQmlSurface::getWindow() { - return _quickWindow; + return _renderer->_quickWindow; } QSize OffscreenQmlSurface::size() const { - return _quickWindow->geometry().size(); + return _renderer->_quickWindow->geometry().size(); } diff --git a/libraries/render-utils/src/OffscreenQmlSurface.h b/libraries/render-utils/src/OffscreenQmlSurface.h index 6eb886044b..1a1827b63f 100644 --- a/libraries/render-utils/src/OffscreenQmlSurface.h +++ b/libraries/render-utils/src/OffscreenQmlSurface.h @@ -17,18 +17,18 @@ #include <GLMHelpers.h> #include <ThreadHelpers.h> -#include "OffscreenGlCanvas.h" - class QWindow; class QMyQuickRenderControl; +class QOpenGLContext; class QQmlEngine; class QQmlContext; class QQmlComponent; class QQuickWindow; class QQuickItem; -class FboCache; -class OffscreenQmlSurface : public OffscreenGlCanvas { +class OffscreenQmlRenderer; + +class OffscreenQmlSurface : public QObject { Q_OBJECT public: @@ -45,6 +45,7 @@ public: return load(QUrl(qmlSourceFile), f); } + void setMaxFps(uint8_t maxFps) { _maxFps = maxFps; } // Optional values for event handling void setProxyWindow(QWindow* window); void setMouseTranslator(MouseTranslator mouseTranslator) { @@ -67,8 +68,6 @@ signals: public slots: void requestUpdate(); void requestRender(); - void lockTexture(int texture); - void releaseTexture(int texture); private: QObject* finishQmlLoad(std::function<void(QQmlContext*, QObject*)> f); @@ -77,20 +76,20 @@ private: private slots: void updateQuick(); -protected: - QQuickWindow* _quickWindow{ nullptr }; - private: - QMyQuickRenderControl* _renderControl{ nullptr }; + friend class OffscreenQmlRenderer; + OffscreenQmlRenderer* _renderer{ nullptr }; QQmlEngine* _qmlEngine{ nullptr }; QQmlComponent* _qmlComponent{ nullptr }; QQuickItem* _rootItem{ nullptr }; QTimer _updateTimer; - FboCache* _fboCache; - quint64 _lastRenderTime{ 0 }; + uint32_t _currentTexture{ 0 }; + bool _render{ false }; bool _polish{ true }; bool _paused{ true }; + uint8_t _maxFps{ 60 }; MouseTranslator _mouseTranslator{ [](const QPointF& p) { return p; } }; + }; #endif diff --git a/libraries/render-utils/src/OglplusHelpers.cpp b/libraries/render-utils/src/OglplusHelpers.cpp index 7d4a2f18bf..86769436e6 100644 --- a/libraries/render-utils/src/OglplusHelpers.cpp +++ b/libraries/render-utils/src/OglplusHelpers.cpp @@ -7,6 +7,7 @@ // #include "OglplusHelpers.h" #include <QSharedPointer> +#include <set> using namespace oglplus; using namespace oglplus::shapes; @@ -317,3 +318,73 @@ ShapeWrapperPtr loadSphereSection(ProgramPtr program, float fov, float aspect, i new shapes::ShapeWrapper({ "Position", "TexCoord" }, SphereSection(fov, aspect, slices, stacks), *program) ); } + +void TextureRecycler::setSize(const uvec2& size) { + if (size == _size) { + return; + } + _size = size; + while (!_readyTextures.empty()) { + _readyTextures.pop(); + } + std::set<Map::key_type> toDelete; + std::for_each(_allTextures.begin(), _allTextures.end(), [&](Map::const_reference item) { + if (!item.second._active && item.second._size != _size) { + toDelete.insert(item.first); + } + }); + std::for_each(toDelete.begin(), toDelete.end(), [&](Map::key_type key) { + _allTextures.erase(key); + }); +} + +void TextureRecycler::clear() { + while (!_readyTextures.empty()) { + _readyTextures.pop(); + } + _allTextures.clear(); +} + +TexturePtr TextureRecycler::getNextTexture() { + using namespace oglplus; + if (_readyTextures.empty()) { + TexturePtr newTexture(new Texture()); + Context::Bound(oglplus::Texture::Target::_2D, *newTexture) + .MinFilter(TextureMinFilter::Linear) + .MagFilter(TextureMagFilter::Linear) + .WrapS(TextureWrap::ClampToEdge) + .WrapT(TextureWrap::ClampToEdge) + .Image2D( + 0, PixelDataInternalFormat::RGBA8, + _size.x, _size.y, + 0, PixelDataFormat::RGB, PixelDataType::UnsignedByte, nullptr + ); + GLuint texId = GetName(*newTexture); + _allTextures[texId] = TexInfo{ newTexture, _size }; + _readyTextures.push(newTexture); + } + + TexturePtr result = _readyTextures.front(); + _readyTextures.pop(); + + GLuint texId = GetName(*result); + auto& item = _allTextures[texId]; + item._active = true; + + return result; +} + +void TextureRecycler::recycleTexture(GLuint texture) { + Q_ASSERT(_allTextures.count(texture)); + auto& item = _allTextures[texture]; + Q_ASSERT(item._active); + item._active = false; + if (item._size != _size) { + // Buh-bye + _allTextures.erase(texture); + return; + } + + _readyTextures.push(item._tex); +} + diff --git a/libraries/render-utils/src/OglplusHelpers.h b/libraries/render-utils/src/OglplusHelpers.h index 569e0be7a3..99232b97cb 100644 --- a/libraries/render-utils/src/OglplusHelpers.h +++ b/libraries/render-utils/src/OglplusHelpers.h @@ -10,6 +10,10 @@ // FIXME support oglplus on all platforms // For now it's a convenient helper for Windows +#include <queue> +#include <map> + + #include <QtGlobal> #include "GLMHelpers.h" @@ -33,6 +37,8 @@ #include "NumericalConstants.h" using FramebufferPtr = std::shared_ptr<oglplus::Framebuffer>; +using RenderbufferPtr = std::shared_ptr<oglplus::Renderbuffer>; +using TexturePtr = std::shared_ptr<oglplus::Texture>; using ShapeWrapperPtr = std::shared_ptr<oglplus::shapes::ShapeWrapper>; using BufferPtr = std::shared_ptr<oglplus::Buffer>; using VertexArrayPtr = std::shared_ptr<oglplus::VertexArray>; @@ -151,3 +157,29 @@ protected: }; using BasicFramebufferWrapperPtr = std::shared_ptr<BasicFramebufferWrapper>; + +class TextureRecycler { +public: + void setSize(const uvec2& size); + void clear(); + TexturePtr getNextTexture(); + void recycleTexture(GLuint texture); + +private: + + struct TexInfo { + TexturePtr _tex; + uvec2 _size; + bool _active{ false }; + + TexInfo() {} + TexInfo(TexturePtr tex, const uvec2& size) : _tex(tex), _size(size) {} + }; + + using Map = std::map<GLuint, TexInfo>; + using Queue = std::queue<TexturePtr>; + + Map _allTextures; + Queue _readyTextures; + uvec2 _size{ 1920, 1080 }; +}; diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index d581f12473..a1f00ab5ad 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -38,8 +38,8 @@ public: // so I think it's OK for the time being. bool OffscreenUi::shouldSwallowShortcut(QEvent* event) { Q_ASSERT(event->type() == QEvent::ShortcutOverride); - QObject* focusObject = _quickWindow->focusObject(); - if (focusObject != _quickWindow && focusObject != getRootItem()) { + QObject* focusObject = getWindow()->focusObject(); + if (focusObject != getWindow() && focusObject != getRootItem()) { //qDebug() << "Swallowed shortcut " << static_cast<QKeyEvent*>(event)->key(); event->accept(); return true; diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp index 3fe0f4c11d..dab22999d1 100644 --- a/tests/ui/src/main.cpp +++ b/tests/ui/src/main.cpp @@ -186,13 +186,7 @@ public: auto offscreenUi = DependencyManager::get<OffscreenUi>(); offscreenUi->create(_context); connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) { - offscreenUi->lockTexture(textureId); - assert(!glGetError()); - GLuint oldTexture = testQmlTexture; testQmlTexture = textureId; - if (oldTexture) { - offscreenUi->releaseTexture(oldTexture); - } }); makeCurrent();