From e9b83e1c028b6a626d3fd1431ed3aca23f63f291 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 12 May 2015 21:15:54 -0700 Subject: [PATCH] Working on web entities --- examples/html/entityProperties.html | 4 +- interface/resources/qml/WebEntity.qml | 45 +++ .../src/RenderableWebEntityItem.cpp | 88 ++++- .../src/RenderableWebEntityItem.h | 15 +- .../entities/src/EntityItemProperties.cpp | 15 +- libraries/entities/src/EntityItemProperties.h | 2 + libraries/entities/src/WebEntityItem.cpp | 12 +- libraries/entities/src/WebEntityItem.h | 4 +- .../render-utils/src/OffscreenQmlSurface.cpp | 372 ++++++++++++++++++ .../render-utils/src/OffscreenQmlSurface.h | 108 +++++ libraries/ui/src/OffscreenUi.cpp | 372 +----------------- libraries/ui/src/OffscreenUi.h | 95 +---- 12 files changed, 655 insertions(+), 477 deletions(-) create mode 100644 interface/resources/qml/WebEntity.qml create mode 100644 libraries/render-utils/src/OffscreenQmlSurface.cpp create mode 100644 libraries/render-utils/src/OffscreenQmlSurface.h diff --git a/examples/html/entityProperties.html b/examples/html/entityProperties.html index 5e92d2861a..ad16302ba4 100644 --- a/examples/html/entityProperties.html +++ b/examples/html/entityProperties.html @@ -665,6 +665,8 @@ elLightExponent.addEventListener('change', createEmitNumberPropertyUpdateFunction('exponent')); elLightCutoff.addEventListener('change', createEmitNumberPropertyUpdateFunction('cutoff')); + elWebSourceURL.addEventListener('change', createEmitTextPropertyUpdateFunction('sourceUrl')); + elModelURL.addEventListener('change', createEmitTextPropertyUpdateFunction('modelURL')); elShapeType.addEventListener('change', createEmitTextPropertyUpdateFunction('shapeType')); elCompoundShapeURL.addEventListener('change', createEmitTextPropertyUpdateFunction('compoundShapeURL')); @@ -1033,7 +1035,7 @@
Source URL
- +
diff --git a/interface/resources/qml/WebEntity.qml b/interface/resources/qml/WebEntity.qml new file mode 100644 index 0000000000..a9d8350244 --- /dev/null +++ b/interface/resources/qml/WebEntity.qml @@ -0,0 +1,45 @@ +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtWebKit 3.0 + +Item { + id: root + implicitHeight: 600 + implicitWidth: 800 + Rectangle { + anchors.margins: 120 + color: "#7f0000ff" + anchors.right: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.top: parent.top + anchors.rightMargin: 10 + anchors.leftMargin: 10 + anchors.bottomMargin: 10 + anchors.topMargin: 10 + } + + Rectangle { + color: "#7Fff0000" + anchors.left: parent.horizontalCenter + anchors.leftMargin: 10 + anchors.top: parent.top + anchors.margins: 120 + anchors.right: parent.right + anchors.topMargin: 10 + anchors.bottomMargin: 10 + anchors.bottom: parent.bottom + anchors.rightMargin: 10 +/* + ScrollView { + id: scrollView + anchors.fill: parent + WebView { + id: webview + objectName: "webview" + anchors.fill: parent + } + } +*/ + } +} diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index d15e3714d9..487a6cfbb4 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -16,16 +16,52 @@ #include #include #include - -#include "GLMHelpers.h" +#include +#include +#include +#include +#include const int FIXED_FONT_POINT_SIZE = 40; +const float DPI = 30.47; +const float METERS_TO_INCHES = 39.3701; EntityItem* RenderableWebEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { return new RenderableWebEntityItem(entityID, properties); } +RenderableWebEntityItem::RenderableWebEntityItem(const EntityItemID& entityItemID, const EntityItemProperties& properties) : + WebEntityItem(entityItemID, properties) { +} + void RenderableWebEntityItem::render(RenderArgs* args) { + QOpenGLContext * currentContext = QOpenGLContext::currentContext(); + QSurface * currentSurface = currentContext->surface(); + if (!_webSurface) { + _webSurface = new OffscreenQmlSurface(); + _webSurface->create(currentContext); + _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); + _webSurface->load("WebEntity.qml"); + _webSurface->resume(); + updateQmlSourceUrl(); + QObject::connect(_webSurface, &OffscreenQmlSurface::textureUpdated, [&](GLuint textureId) { + _webSurface->lockTexture(textureId); + assert(!glGetError()); + std::swap(_texture, textureId); + if (textureId) { + _webSurface->releaseTexture(textureId); + } + }); + } + + glm::vec2 dims = glm::vec2(_dimensions); + dims *= METERS_TO_INCHES * DPI; + // The offscreen surface is idempotent for resizes (bails early + // if it's a no-op), so it's safe to just call resize every frame + // without worrying about excessive overhead. + _webSurface->resize(QSize(dims.x, dims.y)); + currentContext->makeCurrent(currentSurface); + PerformanceTimer perfTimer("RenderableWebEntityItem::render"); assert(getType() == EntityTypes::Web); glm::vec3 position = getPosition(); @@ -42,16 +78,50 @@ void RenderableWebEntityItem::render(RenderArgs* args) { glRotatef(glm::degrees(glm::angle(rotation)), axis.x, axis.y, axis.z); float alpha = 1.0f; //getBackgroundAlpha(); - - glm::vec3 topLeft(-halfDimensions.x, -halfDimensions.y, 0); - glm::vec3 bottomRight(halfDimensions.x, halfDimensions.y, 0); + static const glm::vec2 texMin(0); + static const glm::vec2 texMax(1); + glm::vec2 topLeft(-halfDimensions.x, -halfDimensions.y); + glm::vec2 bottomRight(halfDimensions.x, halfDimensions.y); + if (_texture) { + glEnable(GL_TEXTURE_2D); + 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); + } + // TODO: Determine if we want these entities to have the deferred lighting effect? I think we do, so that the color // used for a sphere, or box have the same look as those used on a text entity. - DependencyManager::get()->bindSimpleProgram(); - DependencyManager::get()->renderQuad(topLeft, bottomRight, glm::vec4(0, 1, 0, alpha)); - DependencyManager::get()->releaseSimpleProgram(); - } + DependencyManager::get()->renderQuad(topLeft, bottomRight, + texMin, texMax, glm::vec4(1)); + + if (_texture) { + glBindTexture(GL_TEXTURE_2D, 0); + glEnable(GL_TEXTURE_2D); + } + } glPopMatrix(); } +void RenderableWebEntityItem::setSourceUrl(const QString& value) { + if (_sourceUrl != value) { + _sourceUrl = value; + // Update the offscreen display + updateQmlSourceUrl(); + } +} + +void RenderableWebEntityItem::updateQmlSourceUrl() { + if (!_webSurface) { + return; + } + auto webView = _webSurface->getRootItem()->findChild("webview"); + if (!webView) { + return; + } + if (!_sourceUrl.isEmpty()) { + webView->setProperty("url", _sourceUrl); + } else { + webView->setProperty("url", QVariant()); + } +} \ No newline at end of file diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index 2d639b85a1..c926314b66 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -11,15 +11,22 @@ #include +class OffscreenQmlSurface; + class RenderableWebEntityItem : public WebEntityItem { public: static EntityItem* factory(const EntityItemID& entityID, const EntityItemProperties& properties); - RenderableWebEntityItem(const EntityItemID& entityItemID, const EntityItemProperties& properties) : - WebEntityItem(entityItemID, properties) - { } - + RenderableWebEntityItem(const EntityItemID& entityItemID, const EntityItemProperties& properties); virtual void render(RenderArgs* args); + + virtual void setSourceUrl(const QString& value); + +private: + void updateQmlSourceUrl(); + + OffscreenQmlSurface* _webSurface{ nullptr }; + uint32_t _texture{ 0 }; }; diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index ff54ef88fe..0d997cd7cb 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -86,6 +86,7 @@ EntityItemProperties::EntityItemProperties() : CONSTRUCT_PROPERTY(keyLightDirection, ZoneEntityItem::DEFAULT_KEYLIGHT_DIRECTION), CONSTRUCT_PROPERTY(name, ENTITY_ITEM_DEFAULT_NAME), CONSTRUCT_PROPERTY(backgroundMode, BACKGROUND_MODE_INHERIT), + CONSTRUCT_PROPERTY(sourceUrl, ""), _id(UNKNOWN_ENTITY_ID), _idSet(false), @@ -328,6 +329,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_KEYLIGHT_AMBIENT_INTENSITY, keyLightAmbientIntensity); CHECK_PROPERTY_CHANGE(PROP_KEYLIGHT_DIRECTION, keyLightDirection); CHECK_PROPERTY_CHANGE(PROP_BACKGROUND_MODE, backgroundMode); + CHECK_PROPERTY_CHANGE(PROP_SOURCE_URL, sourceUrl); changedProperties += _stage.getChangedProperties(); changedProperties += _atmosphere.getChangedProperties(); @@ -409,6 +411,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(keyLightAmbientIntensity); COPY_PROPERTY_TO_QSCRIPTVALUE_VEC3(keyLightDirection); COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(backgroundMode, getBackgroundModeAsString()); + COPY_PROPERTY_TO_QSCRIPTVALUE(sourceUrl); // Sitting properties support if (!skipDefaults) { @@ -510,6 +513,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object) { COPY_PROPERTY_FROM_QSCRIPTVALUE_FLOAT(keyLightAmbientIntensity, setKeyLightAmbientIntensity); COPY_PROPERTY_FROM_QSCRIPTVALUE_VEC3(keyLightDirection, setKeyLightDirection); COPY_PROPERTY_FROM_QSCRITPTVALUE_ENUM(backgroundMode, BackgroundMode); + COPY_PROPERTY_FROM_QSCRIPTVALUE_STRING(sourceUrl, setSourceUrl); _stage.copyFromScriptValue(object, _defaultSettings); _atmosphere.copyFromScriptValue(object, _defaultSettings); @@ -666,7 +670,11 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem APPEND_ENTITY_PROPERTY(PROP_LOCKED, appendValue, properties.getLocked()); APPEND_ENTITY_PROPERTY(PROP_USER_DATA, appendValue, properties.getUserData()); APPEND_ENTITY_PROPERTY(PROP_SIMULATOR_ID, appendValue, properties.getSimulatorID()); - + + if (properties.getType() == EntityTypes::Web) { + APPEND_ENTITY_PROPERTY(PROP_SOURCE_URL, appendValue, properties.getSourceUrl()); + } + if (properties.getType() == EntityTypes::Text) { APPEND_ENTITY_PROPERTY(PROP_TEXT, appendValue, properties.getText()); APPEND_ENTITY_PROPERTY(PROP_LINE_HEIGHT, appendValue, properties.getLineHeight()); @@ -922,6 +930,10 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_STRING_TO_PROPERTIES(PROP_USER_DATA, setUserData); READ_ENTITY_PROPERTY_UUID_TO_PROPERTIES(PROP_SIMULATOR_ID, setSimulatorID); + if (properties.getType() == EntityTypes::Web) { + READ_ENTITY_PROPERTY_STRING_TO_PROPERTIES(PROP_SOURCE_URL, setSourceUrl); + } + if (properties.getType() == EntityTypes::Text) { READ_ENTITY_PROPERTY_STRING_TO_PROPERTIES(PROP_TEXT, setText); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_LINE_HEIGHT, float, setLineHeight); @@ -1073,6 +1085,7 @@ void EntityItemProperties::markAllChanged() { _atmosphere.markAllChanged(); _skybox.markAllChanged(); + _sourceUrlChanged = true; } /// The maximum bounding cube for the entity, independent of it's rotation. diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index ef6ab9089f..46c4c39e10 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -140,6 +140,8 @@ public: DEFINE_PROPERTY_GROUP(Stage, stage, StagePropertyGroup); DEFINE_PROPERTY_GROUP(Atmosphere, atmosphere, AtmospherePropertyGroup); DEFINE_PROPERTY_GROUP(Skybox, skybox, SkyboxPropertyGroup); + DEFINE_PROPERTY_REF(PROP_SOURCE_URL, SourceUrl, sourceUrl, QString); + static QString getBackgroundModeString(BackgroundMode mode); diff --git a/libraries/entities/src/WebEntityItem.cpp b/libraries/entities/src/WebEntityItem.cpp index b7c3efd821..90dd13334d 100644 --- a/libraries/entities/src/WebEntityItem.cpp +++ b/libraries/entities/src/WebEntityItem.cpp @@ -44,7 +44,7 @@ void WebEntityItem::setDimensions(const glm::vec3& value) { EntityItemProperties WebEntityItem::getProperties() const { EntityItemProperties properties = EntityItem::getProperties(); // get the properties from our base class - COPY_ENTITY_PROPERTY_TO_PROPERTIES(modelURL, getSource); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(sourceUrl, getSourceUrl); return properties; } @@ -52,7 +52,7 @@ bool WebEntityItem::setProperties(const EntityItemProperties& properties) { bool somethingChanged = false; somethingChanged = EntityItem::setProperties(properties); // set the properties in our base class - SET_ENTITY_PROPERTY_FROM_PROPERTIES(modelURL, setSourceUrl); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(sourceUrl, setSourceUrl); if (somethingChanged) { bool wantDebug = false; @@ -143,3 +143,11 @@ bool WebEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const g } return intersects; } + +void WebEntityItem::setSourceUrl(const QString& value) { + if (_sourceUrl != value) { + _sourceUrl = value; + } +} + +const QString& WebEntityItem::getSourceUrl() const { return _sourceUrl; } \ No newline at end of file diff --git a/libraries/entities/src/WebEntityItem.h b/libraries/entities/src/WebEntityItem.h index ee8d4cf450..35e98b2092 100644 --- a/libraries/entities/src/WebEntityItem.h +++ b/libraries/entities/src/WebEntityItem.h @@ -49,8 +49,8 @@ public: bool& keepSearching, OctreeElement*& element, float& distance, BoxFace& face, void** intersectedObject, bool precisionPicking) const; - void setSourceUrl(const QString& value) { _sourceUrl = value; } - const QString& getSourceUrl() const { return _sourceUrl; } + virtual void setSourceUrl(const QString& value); + const QString& getSourceUrl() const; protected: QString _sourceUrl; diff --git a/libraries/render-utils/src/OffscreenQmlSurface.cpp b/libraries/render-utils/src/OffscreenQmlSurface.cpp new file mode 100644 index 0000000000..1893c77595 --- /dev/null +++ b/libraries/render-utils/src/OffscreenQmlSurface.cpp @@ -0,0 +1,372 @@ +// +// OffscreenUi.cpp +// interface/src/render-utils +// +// Created by Bradley Austin Davis on 2015-04-04 +// 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 "OffscreenQmlSurface.h" + +#include +#include +#include +#include + +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 SMALL_INTERVAL = 5; + +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; + + delete _qmlComponent; + delete _quickWindow; + delete _qmlEngine; + + doneCurrent(); +} + +void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { + OffscreenGlCanvas::create(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::WA_TranslucentBackground)); + // Create a QML engine. + _qmlEngine = new QQmlEngine; + if (!_qmlEngine->incubationController()) { + _qmlEngine->setIncubationController(_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(SMALL_INTERVAL); + 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 + + _qmlComponent = new QQmlComponent(_qmlEngine); + // Initialize the render control and our OpenGL resources. + makeCurrent(); + _renderControl->initialize(&_context); +} + +void OffscreenQmlSurface::resize(const QSize& newSize) { + + qreal pixelRatio = _renderControl->_renderWindow ? _renderControl->_renderWindow->devicePixelRatio() : 1.0; + 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(); +} + +QQuickItem* OffscreenQmlSurface::getRootItem() { + return _rootItem; +} + +void OffscreenQmlSurface::setBaseUrl(const QUrl& baseUrl) { + _qmlEngine->setBaseUrl(baseUrl); +} + +QObject* OffscreenQmlSurface::load(const QUrl& qmlSource, std::function f) { + _qmlComponent->loadUrl(qmlSource); + if (_qmlComponent->isLoading()) { + connect(_qmlComponent, &QQmlComponent::statusChanged, this, + [this, f](QQmlComponent::Status){ + finishQmlLoad(f); + }); + return nullptr; + } + + return finishQmlLoad(f); +} + +void OffscreenQmlSurface::requestUpdate() { + _polish = true; + if (!_updateTimer.isActive()) { + _updateTimer.start(); + } +} + +void OffscreenQmlSurface::requestRender() { + if (!_updateTimer.isActive()) { + _updateTimer.start(); + } +} + +QObject* OffscreenQmlSurface::finishQmlLoad(std::function f) { + disconnect(_qmlComponent, &QQmlComponent::statusChanged, this, 0); + if (_qmlComponent->isError()) { + QList errorList = _qmlComponent->errors(); + foreach(const QQmlError& error, errorList) { + qWarning() << error.url() << error.line() << error; + } + return nullptr; + } + + QQmlContext* newContext = new QQmlContext(_qmlEngine, qApp); + QObject* newObject = _qmlComponent->beginCreate(newContext); + if (_qmlComponent->isError()) { + QList errorList = _qmlComponent->errors(); + foreach(const QQmlError& error, errorList) + qWarning() << error.url() << error.line() << error; + if (!_rootItem) { + qFatal("Unable to finish loading QML root"); + } + return nullptr; + } + + f(newContext, newObject); + _qmlComponent->completeCreate(); + + + // All quick items should be focusable + QQuickItem* newItem = qobject_cast(newObject); + if (newItem) { + // Make sure we make items focusable (critical for + // supporting keyboard shortcuts) + newItem->setFlag(QQuickItem::ItemIsFocusScope, true); + } + + // If we already have a root, just set a couple of flags and the ancestry + if (_rootItem) { + // Allow child windows to be destroyed from JS + QQmlEngine::setObjectOwnership(newObject, QQmlEngine::JavaScriptOwnership); + newObject->setParent(_rootItem); + if (newItem) { + newItem->setParentItem(_rootItem); + } + return newObject; + } + + if (!newItem) { + qFatal("Could not load object as root item"); + return nullptr; + } + // The root item is ready. Associate it with the window. + _rootItem = newItem; + _rootItem->setParentItem(_quickWindow->contentItem()); + _rootItem->setSize(_quickWindow->renderTargetSize()); + return _rootItem; +} + + +void OffscreenQmlSurface::updateQuick() { + if (_paused) { + return; + } + if (!makeCurrent()) { + 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(); + _polish = false; + } + + QOpenGLFramebufferObject* fbo = _fboCache.getReadyFbo(); + + _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(); +// Q_ASSERT(!glGetError()); + + _quickWindow->resetOpenGLState(); + + QOpenGLFramebufferObject::bindDefault(); + // Force completion of all the operations before we emit the texture as being ready for use + glFinish(); + + emit textureUpdated(fbo->texture()); +} + +QPointF OffscreenQmlSurface::mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject) { + vec2 sourceSize; + if (dynamic_cast(sourceObject)) { + sourceSize = toGlm(((QWidget*)sourceObject)->size()); + } else if (dynamic_cast(sourceObject)) { + sourceSize = toGlm(((QWindow*)sourceObject)->size()); + } + vec2 offscreenPosition = toGlm(sourcePosition); + offscreenPosition /= sourceSize; + offscreenPosition *= vec2(toGlm(_quickWindow->size())); + return QPointF(offscreenPosition.x, offscreenPosition.y); +} + +/////////////////////////////////////////////////////// +// +// Event handling customization +// + +bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* event) { + // Only intercept events while we're in an active state + if (_paused) { + return false; + } + +#ifdef DEBUG + // Don't intercept our own events, or we enter an infinite recursion + QObject* recurseTest = originalDestination; + while (recurseTest) { + Q_ASSERT(recurseTest != _rootItem && recurseTest != _quickWindow); + recurseTest = recurseTest->parent(); + } +#endif + + + switch (event->type()) { + case QEvent::Resize: { + QResizeEvent* resizeEvent = static_cast(event); + QGLWidget* widget = dynamic_cast(originalDestination); + if (widget) { + this->resize(resizeEvent->size()); + } + break; + } + + case QEvent::KeyPress: + case QEvent::KeyRelease: { + event->ignore(); + if (QCoreApplication::sendEvent(_quickWindow, event)) { + return event->isAccepted(); + } + break; + } + + case QEvent::Wheel: { + QWheelEvent* wheelEvent = static_cast(event); + QWheelEvent mappedEvent( + mapWindowToUi(wheelEvent->pos(), originalDestination), + wheelEvent->delta(), wheelEvent->buttons(), + wheelEvent->modifiers(), wheelEvent->orientation()); + mappedEvent.ignore(); + if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) { + return mappedEvent.isAccepted(); + } + break; + } + + // Fall through + case QEvent::MouseButtonDblClick: + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + case QEvent::MouseMove: { + QMouseEvent* mouseEvent = static_cast(event); + QPointF originalPos = mouseEvent->localPos(); + QPointF transformedPos = _mouseTranslator(originalPos); + transformedPos = mapWindowToUi(transformedPos, originalDestination); + QMouseEvent mappedEvent(mouseEvent->type(), + transformedPos, + mouseEvent->screenPos(), mouseEvent->button(), + mouseEvent->buttons(), mouseEvent->modifiers()); + if (event->type() == QEvent::MouseMove) { + _qmlEngine->rootContext()->setContextProperty("lastMousePosition", transformedPos); + } + mappedEvent.ignore(); + if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) { + return mappedEvent.isAccepted(); + } + break; + } + + default: + break; + } + + return false; +} + +void OffscreenQmlSurface::lockTexture(int texture) { + _fboCache.lockTexture(texture); +} + +void OffscreenQmlSurface::releaseTexture(int texture) { + _fboCache.releaseTexture(texture); +} + +void OffscreenQmlSurface::pause() { + _paused = true; +} + +void OffscreenQmlSurface::resume() { + _paused = false; + requestRender(); +} + +bool OffscreenQmlSurface::isPaused() const { + return _paused; +} + +void OffscreenQmlSurface::setProxyWindow(QWindow* window) { + _renderControl->_renderWindow = window; +} + diff --git a/libraries/render-utils/src/OffscreenQmlSurface.h b/libraries/render-utils/src/OffscreenQmlSurface.h new file mode 100644 index 0000000000..dcb3e1e17e --- /dev/null +++ b/libraries/render-utils/src/OffscreenQmlSurface.h @@ -0,0 +1,108 @@ +// +// Created by Bradley Austin Davis on 2015-04-04 +// 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_OffscreenQmlSurface_h +#define hifi_OffscreenQmlSurface_h + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "OffscreenGlCanvas.h" +#include "FboCache.h" + +class OffscreenQmlSurface : public OffscreenGlCanvas { + Q_OBJECT +protected: + class QMyQuickRenderControl : public QQuickRenderControl { + protected: + QWindow* renderWindow(QPoint* offset) Q_DECL_OVERRIDE{ + if (nullptr == _renderWindow) { + return QQuickRenderControl::renderWindow(offset); + } + if (nullptr != offset) { + offset->rx() = offset->ry() = 0; + } + return _renderWindow; + } + + private: + QWindow* _renderWindow{ nullptr }; + friend class OffscreenQmlSurface; + }; +public: + OffscreenQmlSurface(); + virtual ~OffscreenQmlSurface(); + + using MouseTranslator = std::function; + + void create(QOpenGLContext* context); + void resize(const QSize& size); + QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); + QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { + return load(QUrl(qmlSourceFile), f); + } + + // Optional values for event handling + void setProxyWindow(QWindow* window); + void setMouseTranslator(MouseTranslator mouseTranslator) { + _mouseTranslator = mouseTranslator; + } + + void pause(); + void resume(); + bool isPaused() const; + + void setBaseUrl(const QUrl& baseUrl); + QQuickItem* getRootItem(); + + virtual bool eventFilter(QObject* originalDestination, QEvent* event); + +signals: + void textureUpdated(GLuint texture); + +public slots: + void requestUpdate(); + void requestRender(); + void lockTexture(int texture); + void releaseTexture(int texture); + +private: + QObject* finishQmlLoad(std::function f); + QPointF mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject); + +private slots: + void updateQuick(); + +protected: + QQuickWindow* _quickWindow{ nullptr }; + +private: + QMyQuickRenderControl* _renderControl{ new QMyQuickRenderControl }; + QQmlEngine* _qmlEngine{ nullptr }; + QQmlComponent* _qmlComponent{ nullptr }; + QQuickItem* _rootItem{ nullptr }; + QTimer _updateTimer; + FboCache _fboCache; + bool _polish{ true }; + bool _paused{ true }; + MouseTranslator _mouseTranslator{ [](const QPointF& p) { return p; } }; +}; + +#endif diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index b133d1e488..449657ca04 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -16,252 +16,18 @@ #include "MessageDialog.h" -Q_DECLARE_LOGGING_CATEGORY(offscreenFocus) -Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus") +class OffscreenUiRoot : public QQuickItem { + Q_OBJECT +public: -// 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 SMALL_INTERVAL = 5; - -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; - - delete _qmlComponent; - delete _quickWindow; - delete _qmlEngine; - - doneCurrent(); -} - -void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { - OffscreenGlCanvas::create(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::WA_TranslucentBackground)); - // Create a QML engine. - _qmlEngine = new QQmlEngine; - if (!_qmlEngine->incubationController()) { - _qmlEngine->setIncubationController(_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(SMALL_INTERVAL); - 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 - - _qmlComponent = new QQmlComponent(_qmlEngine); - // Initialize the render control and our OpenGL resources. - makeCurrent(); - _renderControl->initialize(&_context); -} - -void OffscreenQmlSurface::resize(const QSize& newSize) { - makeCurrent(); - - qreal pixelRatio = _renderControl->_renderWindow ? _renderControl->_renderWindow->devicePixelRatio() : 1.0; - QSize newOffscreenSize = newSize * pixelRatio; - if (newOffscreenSize == _fboCache.getSize()) { - return; - } - - // Clear out any fbos with the old size - 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); + OffscreenUiRoot(QQuickItem* parent = 0); + Q_INVOKABLE void information(const QString& title, const QString& text); + Q_INVOKABLE void loadChild(const QUrl& url) { + DependencyManager::get()->load(url); } +}; - // Update our members - if (_rootItem) { - _rootItem->setSize(newSize); - } - - doneCurrent(); -} - -QQuickItem* OffscreenQmlSurface::getRootItem() { - return _rootItem; -} - -void OffscreenQmlSurface::setBaseUrl(const QUrl& baseUrl) { - _qmlEngine->setBaseUrl(baseUrl); -} - -QObject* OffscreenQmlSurface::load(const QUrl& qmlSource, std::function f) { - _qmlComponent->loadUrl(qmlSource); - if (_qmlComponent->isLoading()) { - connect(_qmlComponent, &QQmlComponent::statusChanged, this, - [this, f](QQmlComponent::Status){ - finishQmlLoad(f); - }); - return nullptr; - } - - return finishQmlLoad(f); -} - -void OffscreenQmlSurface::requestUpdate() { - _polish = true; - if (!_updateTimer.isActive()) { - _updateTimer.start(); - } -} - -void OffscreenQmlSurface::requestRender() { - if (!_updateTimer.isActive()) { - _updateTimer.start(); - } -} - -QObject* OffscreenQmlSurface::finishQmlLoad(std::function f) { - disconnect(_qmlComponent, &QQmlComponent::statusChanged, this, 0); - if (_qmlComponent->isError()) { - QList errorList = _qmlComponent->errors(); - foreach(const QQmlError& error, errorList) { - qWarning() << error.url() << error.line() << error; - } - return nullptr; - } - - QQmlContext* newContext = new QQmlContext(_qmlEngine, qApp); - QObject* newObject = _qmlComponent->beginCreate(newContext); - if (_qmlComponent->isError()) { - QList errorList = _qmlComponent->errors(); - foreach(const QQmlError& error, errorList) - qWarning() << error.url() << error.line() << error; - if (!_rootItem) { - qFatal("Unable to finish loading QML root"); - } - return nullptr; - } - - f(newContext, newObject); - _qmlComponent->completeCreate(); - - - // All quick items should be focusable - QQuickItem* newItem = qobject_cast(newObject); - if (newItem) { - // Make sure we make items focusable (critical for - // supporting keyboard shortcuts) - newItem->setFlag(QQuickItem::ItemIsFocusScope, true); - } - - // If we already have a root, just set a couple of flags and the ancestry - if (_rootItem) { - // Allow child windows to be destroyed from JS - QQmlEngine::setObjectOwnership(newObject, QQmlEngine::JavaScriptOwnership); - newObject->setParent(_rootItem); - if (newItem) { - newItem->setParentItem(_rootItem); - } - return newObject; - } - - if (!newItem) { - qFatal("Could not load object as root item"); - return nullptr; - } - // The root item is ready. Associate it with the window. - _rootItem = newItem; - _rootItem->setParentItem(_quickWindow->contentItem()); - _rootItem->setSize(_quickWindow->renderTargetSize()); - return _rootItem; -} - - -void OffscreenQmlSurface::updateQuick() { - if (_paused) { - return; - } - if (!makeCurrent()) { - 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(); - _polish = false; - } - - QOpenGLFramebufferObject* fbo = _fboCache.getReadyFbo(); - - _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(); -// Q_ASSERT(!glGetError()); - - _quickWindow->resetOpenGLState(); - - QOpenGLFramebufferObject::bindDefault(); - // Force completion of all the operations before we emit the texture as being ready for use - glFinish(); - - emit textureUpdated(fbo->texture()); -} - -QPointF OffscreenQmlSurface::mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject) { - vec2 sourceSize; - if (dynamic_cast(sourceObject)) { - sourceSize = toGlm(((QWidget*)sourceObject)->size()); - } else if (dynamic_cast(sourceObject)) { - sourceSize = toGlm(((QWindow*)sourceObject)->size()); - } - vec2 offscreenPosition = toGlm(sourcePosition); - offscreenPosition /= sourceSize; - offscreenPosition *= vec2(toGlm(_quickWindow->size())); - return QPointF(offscreenPosition.x, offscreenPosition.y); -} // This hack allows the QML UI to work with keys that are also bound as // shortcuts at the application level. However, it seems as though the @@ -281,128 +47,6 @@ bool OffscreenUi::shouldSwallowShortcut(QEvent* event) { return false; } -/////////////////////////////////////////////////////// -// -// Event handling customization -// - -bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* event) { - // Only intercept events while we're in an active state - if (_paused) { - return false; - } - - -#ifdef DEBUG - // Don't intercept our own events, or we enter an infinite recursion - QObject* recurseTest = originalDestination; - while (recurseTest) { - Q_ASSERT(recurseTest != _rootItem && recurseTest != _quickWindow); - recurseTest = recurseTest->parent(); - } -#endif - - - switch (event->type()) { - case QEvent::Resize: { - QResizeEvent* resizeEvent = static_cast(event); - QGLWidget* widget = dynamic_cast(originalDestination); - if (widget) { - this->resize(resizeEvent->size()); - } - break; - } - - case QEvent::KeyPress: - case QEvent::KeyRelease: { - event->ignore(); - if (QCoreApplication::sendEvent(_quickWindow, event)) { - return event->isAccepted(); - } - break; - } - - case QEvent::Wheel: { - QWheelEvent* wheelEvent = static_cast(event); - QWheelEvent mappedEvent( - mapWindowToUi(wheelEvent->pos(), originalDestination), - wheelEvent->delta(), wheelEvent->buttons(), - wheelEvent->modifiers(), wheelEvent->orientation()); - mappedEvent.ignore(); - if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) { - return mappedEvent.isAccepted(); - } - break; - } - - // Fall through - case QEvent::MouseButtonDblClick: - case QEvent::MouseButtonPress: - case QEvent::MouseButtonRelease: - case QEvent::MouseMove: { - QMouseEvent* mouseEvent = static_cast(event); - QPointF originalPos = mouseEvent->localPos(); - QPointF transformedPos = _mouseTranslator(originalPos); - transformedPos = mapWindowToUi(transformedPos, originalDestination); - QMouseEvent mappedEvent(mouseEvent->type(), - transformedPos, - mouseEvent->screenPos(), mouseEvent->button(), - mouseEvent->buttons(), mouseEvent->modifiers()); - if (event->type() == QEvent::MouseMove) { - _qmlEngine->rootContext()->setContextProperty("lastMousePosition", transformedPos); - } - mappedEvent.ignore(); - if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) { - return mappedEvent.isAccepted(); - } - break; - } - - default: - break; - } - - return false; -} - -void OffscreenQmlSurface::lockTexture(int texture) { - _fboCache.lockTexture(texture); -} - -void OffscreenQmlSurface::releaseTexture(int texture) { - _fboCache.releaseTexture(texture); -} - -void OffscreenQmlSurface::pause() { - _paused = true; -} - -void OffscreenQmlSurface::resume() { - _paused = false; - requestRender(); -} - -bool OffscreenQmlSurface::isPaused() const { - return _paused; -} - -void OffscreenQmlSurface::setProxyWindow(QWindow* window) { - _renderControl->_renderWindow = window; -} - - -class OffscreenUiRoot : public QQuickItem { - Q_OBJECT -public: - - OffscreenUiRoot(QQuickItem* parent = 0); - Q_INVOKABLE void information(const QString& title, const QString& text); - Q_INVOKABLE void loadChild(const QUrl& url) { - DependencyManager::get()->load(url); - } -}; - - OffscreenUiRoot::OffscreenUiRoot(QQuickItem* parent) : QQuickItem(parent) { } diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 846cd4bfd6..d3567bbb5e 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -12,25 +12,10 @@ #ifndef hifi_OffscreenUi_h #define hifi_OffscreenUi_h -#include -#include -#include -#include -#include -#include -#include -#include +#include "OffscreenQmlSurface.h" -#include -#include - -#include -#include #include -#include "OffscreenGlCanvas.h" -#include "FboCache.h" -#include #define HIFI_QML_DECL \ private: \ @@ -96,84 +81,6 @@ private: offscreenUi->load(QML, f); \ } -class OffscreenQmlSurface : public OffscreenGlCanvas { - Q_OBJECT -protected: - class QMyQuickRenderControl : public QQuickRenderControl { - protected: - QWindow* renderWindow(QPoint* offset) Q_DECL_OVERRIDE{ - if (nullptr == _renderWindow) { - return QQuickRenderControl::renderWindow(offset); - } - if (nullptr != offset) { - offset->rx() = offset->ry() = 0; - } - return _renderWindow; - } - - private: - QWindow* _renderWindow{ nullptr }; - friend class OffscreenQmlSurface; - }; -public: - OffscreenQmlSurface(); - virtual ~OffscreenQmlSurface(); - - using MouseTranslator = std::function; - - void create(QOpenGLContext* context); - void resize(const QSize& size); - QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); - QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { - return load(QUrl(qmlSourceFile), f); - } - - // Optional values for event handling - void setProxyWindow(QWindow* window); - void setMouseTranslator(MouseTranslator mouseTranslator) { - _mouseTranslator = mouseTranslator; - } - - void pause(); - void resume(); - bool isPaused() const; - - void setBaseUrl(const QUrl& baseUrl); - QQuickItem* getRootItem(); - - virtual bool eventFilter(QObject* originalDestination, QEvent* event); - -signals: - void textureUpdated(GLuint texture); - -public slots: - void requestUpdate(); - void requestRender(); - void lockTexture(int texture); - void releaseTexture(int texture); - -private: - QObject* finishQmlLoad(std::function f); - QPointF mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject); - -private slots: - void updateQuick(); - -protected: - QQuickWindow* _quickWindow{ nullptr }; - -private: - QMyQuickRenderControl* _renderControl{ new QMyQuickRenderControl }; - QQmlEngine* _qmlEngine{ nullptr }; - QQmlComponent* _qmlComponent{ nullptr }; - QQuickItem* _rootItem{ nullptr }; - QTimer _updateTimer; - FboCache _fboCache; - bool _polish{ true }; - bool _paused{ true }; - MouseTranslator _mouseTranslator{ [](const QPointF& p) { return p; } }; -}; - class OffscreenUi : public OffscreenQmlSurface, public Dependency { Q_OBJECT