diff --git a/android/app/CMakeLists.txt b/android/app/CMakeLists.txt index 4c50cd8609..0ef1427306 100644 --- a/android/app/CMakeLists.txt +++ b/android/app/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME native-lib) setup_hifi_library() -link_hifi_libraries(shared networking gl gpu image fbx render-utils physics entities octree ${PLATFORM_GL_BACKEND}) +link_hifi_libraries(shared networking gl gpu qml image fbx render-utils physics entities octree ${PLATFORM_GL_BACKEND}) target_opengl() target_bullet() diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index d80bc846f0..728f1467e0 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -2,7 +2,7 @@ set(TARGET_NAME interface) project(${TARGET_NAME}) file(GLOB_RECURSE QML_SRC resources/qml/*.qml resources/qml/*.js) -add_custom_target(qml SOURCES ${QML_SRC}) +add_custom_target(qmls SOURCES ${QML_SRC}) GroupSources("resources/qml") function(JOIN VALUES GLUE OUTPUT) @@ -208,7 +208,7 @@ link_hifi_libraries( pointers recording fbx networking model-networking entities avatars trackers audio audio-client animation script-engine physics - render-utils entities-renderer avatars-renderer ui auto-updater midi + render-utils entities-renderer avatars-renderer ui qml auto-updater midi controllers plugins image trackers ui-plugins display-plugins input-plugins ${PLATFORM_GL_BACKEND} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1a65c5e8d7..337bcbcf35 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -803,6 +803,8 @@ std::shared_ptr _keyboardFocusHighlight{ nullptr }; OverlayID _keyboardFocusHighlightID{ UNKNOWN_OVERLAY_ID }; +OffscreenGLCanvas* _qmlShareContext { nullptr }; + // FIXME hack access to the internal share context for the Chromium helper // Normally we'd want to use QWebEngine::initialize(), but we can't because // our primary context is a QGLWidget, which can't easily be initialized to share @@ -2265,18 +2267,37 @@ void Application::initializeGL() { _isGLInitialized = true; } + // Build a shared canvas / context for the Chromium processes _glWidget->makeCurrent(); #if !defined(DISABLE_QML) + // Chromium rendering uses some GL functions that prevent nSight from capturing + // frames, so we only create the shared context if nsight is NOT active. if (!nsightActive()) { _chromiumShareContext = new OffscreenGLCanvas(); _chromiumShareContext->setObjectName("ChromiumShareContext"); _chromiumShareContext->create(_glWidget->qglContext()); _chromiumShareContext->makeCurrent(); + if (!_chromiumShareContext->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make chromium shared context current"); + } qt_gl_set_global_share_context(_chromiumShareContext->getContext()); + } else { + qCWarning(interfaceapp) << "nSIGHT detected, disabling chrome rendering"; } #endif + // Build a shared canvas / context for the QML rendering + _glWidget->makeCurrent(); + _qmlShareContext = new OffscreenGLCanvas(); + _qmlShareContext->setObjectName("QmlShareContext"); + _qmlShareContext->create(_glWidget->qglContext()); + if (!_qmlShareContext->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make QML shared context current"); + } + OffscreenQmlSurface::setSharedContext(_qmlShareContext->getContext()); + _qmlShareContext->doneCurrent(); + _glWidget->makeCurrent(); gpu::Context::init(); qApp->setProperty(hifi::properties::gl::MAKE_PROGRAM_CALLBACK, @@ -2312,20 +2333,23 @@ void Application::initializeGL() { if (!_offscreenContext->makeCurrent()) { qFatal("Unable to make offscreen context current"); } + _offscreenContext->doneCurrent(); + _renderEventHandler = new RenderEventHandler(_glWidget->qglContext()); // The UI can't be created until the primary OpenGL // context is created, because it needs to share // texture resources // Needs to happen AFTER the render engine initialization to access its configuration + if (!_offscreenContext->makeCurrent()) { + qFatal("Unable to make offscreen context current"); + } + initializeUi(); qCDebug(interfaceapp, "Initialized Offscreen UI."); - _glWidget->makeCurrent(); - - - // call Menu getInstance static method to set up the menu - // Needs to happen AFTER the QML UI initialization - _window->setMenuBar(Menu::getInstance()); + if (!_offscreenContext->makeCurrent()) { + qFatal("Unable to make offscreen context current"); + } init(); qCDebug(interfaceapp, "init() complete."); @@ -2336,7 +2360,6 @@ void Application::initializeGL() { _idleLoopStdev.reset(); - _renderEventHandler = new RenderEventHandler(_glWidget->qglContext()); // Restore the primary GL content for the main thread if (!_offscreenContext->makeCurrent()) { @@ -2402,29 +2425,70 @@ void Application::initializeUi() { auto tabletScriptingInterface = DependencyManager::get(); tabletScriptingInterface->getTablet(SYSTEM_TABLET); } - auto offscreenUi = DependencyManager::get(); - DeadlockWatchdogThread::pause(); - offscreenUi->create(); - DeadlockWatchdogThread::resume(); - auto surfaceContext = offscreenUi->getSurfaceContext(); + auto offscreenUi = DependencyManager::get(); + connect(offscreenUi.data(), &hifi::qml::OffscreenSurface::rootContextCreated, + this, &Application::onDesktopRootContextCreated); + connect(offscreenUi.data(), &hifi::qml::OffscreenSurface::rootItemCreated, + this, &Application::onDesktopRootItemCreated); offscreenUi->setProxyWindow(_window->windowHandle()); // OffscreenUi is a subclass of OffscreenQmlSurface specifically designed to // support the window management and scripting proxies for VR use - offscreenUi->createDesktop(PathUtils::qmlUrl("hifi/Desktop.qml")); + DeadlockWatchdogThread::withPause([&] { + offscreenUi->createDesktop(PathUtils::qmlUrl("hifi/Desktop.qml")); + }); // FIXME either expose so that dialogs can set this themselves or // do better detection in the offscreen UI of what has focus offscreenUi->setNavigationFocused(false); - auto engine = surfaceContext->engine(); - connect(engine, &QQmlEngine::quit, [] { - qApp->quit(); - }); - - setupPreferences(); + _glWidget->installEventFilter(offscreenUi.data()); + offscreenUi->setMouseTranslator([=](const QPointF& pt) { + QPointF result = pt; + auto displayPlugin = getActiveDisplayPlugin(); + if (displayPlugin->isHmd()) { + getApplicationCompositor().handleRealMouseMoveEvent(false); + auto resultVec = getApplicationCompositor().getReticlePosition(); + result = QPointF(resultVec.x, resultVec.y); + } + return result.toPoint(); + }); + offscreenUi->resume(); + connect(_window, &MainWindow::windowGeometryChanged, [this](const QRect& r){ + resizeGL(); + }); + + // This will set up the input plugins UI + _activeInputPlugins.clear(); + foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { + if (KeyboardMouseDevice::NAME == inputPlugin->getName()) { + _keyboardMouseDevice = std::dynamic_pointer_cast(inputPlugin); + } + if (TouchscreenDevice::NAME == inputPlugin->getName()) { + _touchscreenDevice = std::dynamic_pointer_cast(inputPlugin); + } + } + + auto compositorHelper = DependencyManager::get(); + connect(compositorHelper.data(), &CompositorHelper::allowMouseCaptureChanged, this, [=] { + if (isHMDMode()) { + showCursor(compositorHelper->getAllowMouseCapture() ? + Cursor::Manager::lookupIcon(_preferredCursor.get()) : + Cursor::Icon::SYSTEM); + } + }); + + // Pre-create a couple of Web3D overlays to speed up tablet UI + auto offscreenSurfaceCache = DependencyManager::get(); + offscreenSurfaceCache->reserve(TabletScriptingInterface::QML, 1); + offscreenSurfaceCache->reserve(Web3DOverlay::QML, 2); +} + + +void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { + auto engine = surfaceContext->engine(); // in Qt 5.10.0 there is already an "Audio" object in the QML context // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); @@ -2506,48 +2570,12 @@ void Application::initializeUi() { surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); } - - _glWidget->installEventFilter(offscreenUi.data()); - offscreenUi->setMouseTranslator([=](const QPointF& pt) { - QPointF result = pt; - auto displayPlugin = getActiveDisplayPlugin(); - if (displayPlugin->isHmd()) { - getApplicationCompositor().handleRealMouseMoveEvent(false); - auto resultVec = getApplicationCompositor().getReticlePosition(); - result = QPointF(resultVec.x, resultVec.y); - } - return result.toPoint(); - }); - offscreenUi->resume(); - connect(_window, &MainWindow::windowGeometryChanged, [this](const QRect& r){ - resizeGL(); - }); - - // This will set up the input plugins UI - _activeInputPlugins.clear(); - foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { - if (KeyboardMouseDevice::NAME == inputPlugin->getName()) { - _keyboardMouseDevice = std::dynamic_pointer_cast(inputPlugin); - } - if (TouchscreenDevice::NAME == inputPlugin->getName()) { - _touchscreenDevice = std::dynamic_pointer_cast(inputPlugin); - } - } _window->setMenuBar(new Menu()); +} - auto compositorHelper = DependencyManager::get(); - connect(compositorHelper.data(), &CompositorHelper::allowMouseCaptureChanged, this, [=] { - if (isHMDMode()) { - showCursor(compositorHelper->getAllowMouseCapture() ? - Cursor::Manager::lookupIcon(_preferredCursor.get()) : - Cursor::Icon::SYSTEM); - } - }); - - // Pre-create a couple of Web3D overlays to speed up tablet UI - auto offscreenSurfaceCache = DependencyManager::get(); - offscreenSurfaceCache->reserve(TabletScriptingInterface::QML, 1); - offscreenSurfaceCache->reserve(Web3DOverlay::QML, 2); +void Application::onDesktopRootItemCreated(QQuickItem* rootItem) { + Stats::show(); + AvatarInputs::show(); } void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) { @@ -2786,6 +2814,8 @@ void Application::resizeGL() { QMutexLocker viewLocker(&_viewMutex); _myCamera.loadViewFrustum(_viewFrustum); } + + DependencyManager::get()->resize(fromGlm(displayPlugin->getRecommendedUiSize())); } void Application::handleSandboxStatus(QNetworkReply* reply) { @@ -3994,7 +4024,7 @@ void Application::idle() { // Bit of a hack since there's no device pixel ratio change event I can find. if (offscreenUi->size() != fromGlm(uiSize)) { qCDebug(interfaceapp) << "Device pixel ratio changed, triggering resize to " << uiSize; - offscreenUi->resize(fromGlm(uiSize), true); + offscreenUi->resize(fromGlm(uiSize)); _offscreenContext->makeCurrent(); } } @@ -7326,9 +7356,7 @@ void Application::updateDisplayMode() { action->setChecked(true); } - _offscreenContext->makeCurrent(); offscreenUi->resize(fromGlm(newDisplayPlugin->getRecommendedUiSize())); - _offscreenContext->makeCurrent(); getApplicationCompositor().setDisplayPlugin(newDisplayPlugin); _displayPlugin = newDisplayPlugin; connect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent, Qt::DirectConnection); diff --git a/interface/src/Application.h b/interface/src/Application.h index 33f784a07f..7821369b4a 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -391,6 +391,8 @@ public slots: void setPreferredCursor(const QString& cursor); private slots: + void onDesktopRootItemCreated(QQuickItem* qmlContext); + void onDesktopRootContextCreated(QQmlContext* qmlContext); void showDesktop(); void clearDomainOctreeDetails(); void clearDomainAvatars(); diff --git a/interface/src/scripting/LimitlessConnection.cpp b/interface/src/scripting/LimitlessConnection.cpp index b9f4eacd4b..f504136489 100644 --- a/interface/src/scripting/LimitlessConnection.cpp +++ b/interface/src/scripting/LimitlessConnection.cpp @@ -1,8 +1,10 @@ +#include "LimitlessConnection.h" + +#include #include #include #include #include -#include "LimitlessConnection.h" #include "LimitlessVoiceRecognitionScriptingInterface.h" LimitlessConnection::LimitlessConnection() : diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index ccac6d7207..fe79bec6ef 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -25,7 +25,6 @@ Setting::Handle showAudioToolsSetting { QStringList { "AvatarInputs", "sho AvatarInputs* AvatarInputs::getInstance() { if (!INSTANCE) { AvatarInputs::registerType(); - AvatarInputs::show(); Q_ASSERT(INSTANCE); } return INSTANCE; diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index c157898e74..80f57bfe0e 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -48,7 +48,6 @@ QString getTextureMemoryPressureModeString(); Stats* Stats::getInstance() { if (!INSTANCE) { Stats::registerType(); - Stats::show(); Q_ASSERT(INSTANCE); } return INSTANCE; diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 4f96e70aa9..d1a2ff3943 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -134,7 +134,11 @@ void Web3DOverlay::destroyWebSurface() { QObject::disconnect(this, &Web3DOverlay::scriptEventReceived, _webSurface.data(), &OffscreenQmlSurface::emitScriptEvent); QObject::disconnect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, &Web3DOverlay::webEventReceived); - DependencyManager::get()->release(QML, _webSurface); + auto offscreenCache = DependencyManager::get(); + // FIXME prevents crash on shutdown, but we shoudln't have to do this check + if (offscreenCache) { + offscreenCache->release(QML, _webSurface); + } _webSurface.reset(); } diff --git a/libraries/entities-renderer/CMakeLists.txt b/libraries/entities-renderer/CMakeLists.txt index 4b94757dec..4eaa16a107 100644 --- a/libraries/entities-renderer/CMakeLists.txt +++ b/libraries/entities-renderer/CMakeLists.txt @@ -1,7 +1,7 @@ set(TARGET_NAME entities-renderer) AUTOSCRIBE_SHADER_LIB(gpu graphics procedural render render-utils) setup_hifi_library(Network Script) -link_hifi_libraries(shared gpu procedural graphics model-networking script-engine render render-utils image ui pointers) +link_hifi_libraries(shared gpu procedural graphics model-networking script-engine render render-utils image qml ui pointers) include_hifi_library_headers(networking) include_hifi_library_headers(gl) include_hifi_library_headers(ktx) diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 3789cca69a..f7aaa43b7d 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -221,19 +221,17 @@ bool WebEntityRenderer::buildWebSurface(const TypedEntityPointer& entity) { }); }; - { - // FIXME use the surface cache instead of explicit creation - _webSurface = QSharedPointer(new OffscreenQmlSurface(), deleter); - _webSurface->create(); - } - + // FIXME use the surface cache instead of explicit creation + _webSurface = QSharedPointer(new OffscreenQmlSurface(), deleter); // FIXME, the max FPS could be better managed by being dynamic (based on the number of current surfaces // and the current rendering load) _webSurface->setMaxFps(DEFAULT_MAX_FPS); - // FIXME - Keyboard HMD only: Possibly add "HMDinfo" object to context for WebView.qml. - _webSurface->getSurfaceContext()->setContextProperty("desktop", QVariant()); - // Let us interact with the keyboard - _webSurface->getSurfaceContext()->setContextProperty("tabletInterface", DependencyManager::get().data()); + QObject::connect(_webSurface.data(), &OffscreenQmlSurface::rootContextCreated, [this](QQmlContext* surfaceContext) { + // FIXME - Keyboard HMD only: Possibly add "HMDinfo" object to context for WebView.qml. + surfaceContext->setContextProperty("desktop", QVariant()); + // Let us interact with the keyboard + surfaceContext->setContextProperty("tabletInterface", DependencyManager::get().data()); + }); _fadeStartTime = usecTimestampNow(); loadSourceURL(); _webSurface->resume(); diff --git a/libraries/gl/src/gl/GLHelpers.cpp b/libraries/gl/src/gl/GLHelpers.cpp index d5e3bb5a67..e3f85afa78 100644 --- a/libraries/gl/src/gl/GLHelpers.cpp +++ b/libraries/gl/src/gl/GLHelpers.cpp @@ -81,6 +81,10 @@ bool isRenderThread() { return QThread::currentThread() == RENDER_THREAD; } +#if defined(Q_OS_ANDROID) +#define USE_GLES 1 +#endif + namespace gl { void withSavedContext(const std::function& f) { // Save the original GL context, because creating a QML surface will create a new context @@ -91,4 +95,47 @@ namespace gl { savedContext->makeCurrent(savedSurface); } } + + bool checkGLError(const char* name) { + GLenum error = glGetError(); + if (!error) { + return false; + } + switch (error) { + case GL_INVALID_ENUM: + qCWarning(glLogging) << "GLBackend" << name << ": An unacceptable value is specified for an enumerated argument.The offending command is ignored and has no other side effect than to set the error flag."; + break; + case GL_INVALID_VALUE: + qCWarning(glLogging) << "GLBackend" << name << ": A numeric argument is out of range.The offending command is ignored and has no other side effect than to set the error flag"; + break; + case GL_INVALID_OPERATION: + qCWarning(glLogging) << "GLBackend" << name << ": The specified operation is not allowed in the current state.The offending command is ignored and has no other side effect than to set the error flag.."; + break; + case GL_INVALID_FRAMEBUFFER_OPERATION: + qCWarning(glLogging) << "GLBackend" << name << ": The framebuffer object is not complete.The offending command is ignored and has no other side effect than to set the error flag."; + break; + case GL_OUT_OF_MEMORY: + qCWarning(glLogging) << "GLBackend" << name << ": There is not enough memory left to execute the command.The state of the GL is undefined, except for the state of the error flags, after this error is recorded."; + break; +#if !defined(USE_GLES) + case GL_STACK_UNDERFLOW: + qCWarning(glLogging) << "GLBackend" << name << ": An attempt has been made to perform an operation that would cause an internal stack to underflow."; + break; + case GL_STACK_OVERFLOW: + qCWarning(glLogging) << "GLBackend" << name << ": An attempt has been made to perform an operation that would cause an internal stack to overflow."; + break; +#endif + } + return true; + } + + + bool checkGLErrorDebug(const char* name) { +#ifdef DEBUG + return checkGLError(name); +#else + Q_UNUSED(name); + return false; +#endif + } } diff --git a/libraries/gl/src/gl/GLHelpers.h b/libraries/gl/src/gl/GLHelpers.h index 581bb09360..dcbf5a9e77 100644 --- a/libraries/gl/src/gl/GLHelpers.h +++ b/libraries/gl/src/gl/GLHelpers.h @@ -13,6 +13,8 @@ #include #include +#include "GLLogging.h" + // 16 bits of depth precision #define DEFAULT_GL_DEPTH_BUFFER_BITS 16 // 8 bits of stencil buffer (typically you really only need 1 bit for functionality @@ -43,6 +45,13 @@ bool isRenderThread(); namespace gl { void withSavedContext(const std::function& f); -} + + bool checkGLError(const char* name); + + bool checkGLErrorDebug(const char* name); + +} // namespace gl + +#define CHECK_GL_ERROR() ::gl::checkGLErrorDebug(__FUNCTION__) #endif diff --git a/libraries/gpu-gl/src/gpu/gl/GLShared.cpp b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp index 5a015cd550..dc1c8036b0 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShared.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp @@ -8,12 +8,13 @@ #include "GLShared.h" #include +#include #include +#include #include #include -#include Q_LOGGING_CATEGORY(gpugllogging, "hifi.gpu.gl") Q_LOGGING_CATEGORY(trace_render_gpu_gl, "trace.render.gpu.gl") @@ -21,47 +22,6 @@ Q_LOGGING_CATEGORY(trace_render_gpu_gl_detail, "trace.render.gpu.gl.detail") namespace gpu { namespace gl { -bool checkGLError(const char* name) { - GLenum error = glGetError(); - if (!error) { - return false; - } else { - switch (error) { - case GL_INVALID_ENUM: - qCWarning(gpugllogging) << "GLBackend::" << name << ": An unacceptable value is specified for an enumerated argument.The offending command is ignored and has no other side effect than to set the error flag."; - break; - case GL_INVALID_VALUE: - qCWarning(gpugllogging) << "GLBackend" << name << ": A numeric argument is out of range.The offending command is ignored and has no other side effect than to set the error flag"; - break; - case GL_INVALID_OPERATION: - qCWarning(gpugllogging) << "GLBackend" << name << ": The specified operation is not allowed in the current state.The offending command is ignored and has no other side effect than to set the error flag.."; - break; - case GL_INVALID_FRAMEBUFFER_OPERATION: - qCWarning(gpugllogging) << "GLBackend" << name << ": The framebuffer object is not complete.The offending command is ignored and has no other side effect than to set the error flag."; - break; - case GL_OUT_OF_MEMORY: - qCWarning(gpugllogging) << "GLBackend" << name << ": There is not enough memory left to execute the command.The state of the GL is undefined, except for the state of the error flags, after this error is recorded."; - break; - case GL_STACK_UNDERFLOW: - qCWarning(gpugllogging) << "GLBackend" << name << ": An attempt has been made to perform an operation that would cause an internal stack to underflow."; - break; - case GL_STACK_OVERFLOW: - qCWarning(gpugllogging) << "GLBackend" << name << ": An attempt has been made to perform an operation that would cause an internal stack to overflow."; - break; - } - return true; - } -} - -bool checkGLErrorDebug(const char* name) { -#ifdef DEBUG - return checkGLError(name); -#else - Q_UNUSED(name); - return false; -#endif -} - gpu::Size getFreeDedicatedMemory() { Size result { 0 }; static bool nvidiaMemorySupported { true }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLShared.h b/libraries/gpu-gl/src/gpu/gl/GLShared.h index 6c2948a736..ccdf0a5c41 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShared.h +++ b/libraries/gpu-gl/src/gpu/gl/GLShared.h @@ -9,6 +9,7 @@ #define hifi_gpu_GLShared_h #include +#include #include #include #include @@ -114,9 +115,6 @@ static const GLenum ELEMENT_TYPE_TO_GL[gpu::NUM_TYPES] = { GL_INT_2_10_10_10_REV, }; -bool checkGLError(const char* name = nullptr); -bool checkGLErrorDebug(const char* name = nullptr); - class GLBackend; template @@ -141,11 +139,8 @@ class GLShader; class GLTexture; struct ShaderObject; - } } // namespace gpu::gl -#define CHECK_GL_ERROR() gpu::gl::checkGLErrorDebug(__FUNCTION__) - #endif diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp index 925e83bb77..2834a8463c 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp @@ -413,7 +413,7 @@ void copyCompressedTexGPUMem(const gpu::Texture& texture, GLenum texTarget, GLui sourceMip._size = (GLint)faceTargets.size() * sourceMip._faceSize; sourceMip._offset = bufferOffset; bufferOffset += sourceMip._size; - gpu::gl::checkGLError(); + ::gl::checkGLError(__FUNCTION__); } (void)CHECK_GL_ERROR(); @@ -458,7 +458,7 @@ void copyCompressedTexGPUMem(const gpu::Texture& texture, GLenum texTarget, GLui #endif glCompressedTexSubImage2D(faceTargets[f], destLevel, 0, 0, sourceMip._width, sourceMip._height, internalFormat, sourceMip._faceSize, BUFFER_OFFSET(sourceMip._offset + f * sourceMip._faceSize)); - gpu::gl::checkGLError(); + ::gl::checkGLError(__FUNCTION__); } } diff --git a/libraries/gpu-gles/src/gpu/gl/GLShared.h b/libraries/gpu-gles/src/gpu/gl/GLShared.h index dcce896a37..1341dd16fa 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLShared.h +++ b/libraries/gpu-gles/src/gpu/gl/GLShared.h @@ -9,11 +9,14 @@ #define hifi_gpu_GLShared_h #include +#include + #include #include #include #include + Q_DECLARE_LOGGING_CATEGORY(gpugllogging) Q_DECLARE_LOGGING_CATEGORY(trace_render_gpu_gl) Q_DECLARE_LOGGING_CATEGORY(trace_render_gpu_gl_detail) @@ -143,8 +146,6 @@ struct ShaderObject; } } // namespace gpu::gl -#define CHECK_GL_ERROR() gpu::gl::checkGLErrorDebug(__FUNCTION__) - #endif diff --git a/libraries/qml/CMakeLists.txt b/libraries/qml/CMakeLists.txt new file mode 100644 index 0000000000..f8ed7acdb1 --- /dev/null +++ b/libraries/qml/CMakeLists.txt @@ -0,0 +1,6 @@ +set(TARGET_NAME qml) +setup_hifi_library(Multimedia Network Qml Quick WebChannel WebSockets ${PLATFORM_QT_COMPONENTS}) +link_hifi_libraries(shared networking gl) + +# Required for some low level GL interaction in the OffscreenQMLSurface +target_opengl() diff --git a/libraries/qml/src/qml/Logging.cpp b/libraries/qml/src/qml/Logging.cpp new file mode 100644 index 0000000000..c809cab944 --- /dev/null +++ b/libraries/qml/src/qml/Logging.cpp @@ -0,0 +1,11 @@ +// +// Created by Bradley Austin Davis 2016/03/01 +// 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 "Logging.h" + +Q_LOGGING_CATEGORY(qmlLogging, "hifi.qml") diff --git a/libraries/qml/src/qml/Logging.h b/libraries/qml/src/qml/Logging.h new file mode 100644 index 0000000000..487566701d --- /dev/null +++ b/libraries/qml/src/qml/Logging.h @@ -0,0 +1,16 @@ +// +// Created by Bradley Austin Davis 2018/01/04 +// Copyright 2013-2018 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 +// + +#ifndef hifi_Controllers_Logging_h +#define hifi_Controllers_Logging_h + +#include + +Q_DECLARE_LOGGING_CATEGORY(qmlLogging) + +#endif diff --git a/libraries/qml/src/qml/OffscreenSurface.cpp b/libraries/qml/src/qml/OffscreenSurface.cpp new file mode 100644 index 0000000000..87fc8a3025 --- /dev/null +++ b/libraries/qml/src/qml/OffscreenSurface.cpp @@ -0,0 +1,355 @@ +// +// Created by Bradley Austin Davis on 2015-05-13 +// 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 "OffscreenSurface.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "Logging.h" +#include "impl/SharedObject.h" +#include "impl/TextureCache.h" + +using namespace hifi::qml; +using namespace hifi::qml::impl; + +static uvec2 clampSize(const uvec2& size, uint32_t maxDimension) { + return glm::clamp(size, glm::uvec2(1), glm::uvec2(maxDimension)); +} + +static QSize clampSize(const QSize& qsize, uint32_t maxDimension) { + return fromGlm(clampSize(toGlm(qsize), maxDimension)); +} + +const QmlContextObjectCallback OffscreenSurface::DEFAULT_CONTEXT_CALLBACK = [](QQmlContext*, QQuickItem*) {}; + +void OffscreenSurface::initializeEngine(QQmlEngine* engine) { +} + +using namespace hifi::qml::impl; + +size_t OffscreenSurface::getUsedTextureMemory() { + return SharedObject::getTextureCache().getUsedTextureMemory(); +} + +void OffscreenSurface::setSharedContext(QOpenGLContext* sharedContext) { + SharedObject::setSharedContext(sharedContext); +} + +std::function OffscreenSurface::getDiscardLambda() { + return [](uint32_t texture, void* fence) { + SharedObject::getTextureCache().releaseTexture({ texture, static_cast(fence) }); + }; +} + +OffscreenSurface::OffscreenSurface() + : _sharedObject(new impl::SharedObject()) { +} + +OffscreenSurface::~OffscreenSurface() { + disconnect(qApp); + _sharedObject->destroy(); +} + +bool OffscreenSurface::fetchTexture(TextureAndFence& textureAndFence) { + if (!_sharedObject) { + return false; + } + hifi::qml::impl::TextureAndFence typedTextureAndFence; + bool result = _sharedObject->fetchTexture(typedTextureAndFence); + textureAndFence = typedTextureAndFence; + return result; +} + +void OffscreenSurface::resize(const QSize& newSize_) { + const uint32_t MAX_OFFSCREEN_DIMENSION = 4096; + _sharedObject->setSize(clampSize(newSize_, MAX_OFFSCREEN_DIMENSION)); +} + +QQuickItem* OffscreenSurface::getRootItem() { + return _sharedObject->getRootItem(); +} + +void OffscreenSurface::clearCache() { + _sharedObject->getContext()->engine()->clearComponentCache(); +} + +QPointF OffscreenSurface::mapToVirtualScreen(const QPointF& originalPoint) { + return _mouseTranslator(originalPoint); +} + +/////////////////////////////////////////////////////// +// +// Event handling customization +// + +bool OffscreenSurface::filterEnabled(QObject* originalDestination, QEvent* event) const { + if (!_sharedObject || _sharedObject->getWindow() == originalDestination) { + return false; + } + // Only intercept events while we're in an active state + if (_sharedObject->isPaused()) { + return false; + } + return true; +} + +bool OffscreenSurface::eventFilter(QObject* originalDestination, QEvent* event) { + if (!filterEnabled(originalDestination, event)) { + return false; + } +#ifdef DEBUG + // Don't intercept our own events, or we enter an infinite recursion + { + auto rootItem = _sharedObject->getRootItem(); + auto quickWindow = _sharedObject->getWindow(); + QObject* recurseTest = originalDestination; + while (recurseTest) { + Q_ASSERT(recurseTest != rootItem && recurseTest != quickWindow); + recurseTest = recurseTest->parent(); + } + } +#endif + + switch (event->type()) { + case QEvent::KeyPress: + case QEvent::KeyRelease: { + event->ignore(); + if (QCoreApplication::sendEvent(_sharedObject->getWindow(), event)) { + return event->isAccepted(); + } + break; + } + + case QEvent::Wheel: { + QWheelEvent* wheelEvent = static_cast(event); + QPointF transformedPos = mapToVirtualScreen(wheelEvent->pos()); + QWheelEvent mappedEvent(transformedPos, wheelEvent->delta(), wheelEvent->buttons(), wheelEvent->modifiers(), + wheelEvent->orientation()); + mappedEvent.ignore(); + if (QCoreApplication::sendEvent(_sharedObject->getWindow(), &mappedEvent)) { + return mappedEvent.isAccepted(); + } + break; + } + case QEvent::MouseMove: { + QMouseEvent* mouseEvent = static_cast(event); + QPointF transformedPos = mapToVirtualScreen(mouseEvent->localPos()); + QMouseEvent mappedEvent(mouseEvent->type(), transformedPos, mouseEvent->screenPos(), mouseEvent->button(), + mouseEvent->buttons(), mouseEvent->modifiers()); + if (event->type() == QEvent::MouseMove) { + // TODO - this line necessary for the QML Tooltop to work (which is not currently being used), but it causes interface to crash on launch on a fresh install + // need to investigate into why this crash is happening. + //_qmlContext->setContextProperty("lastMousePosition", transformedPos); + } + mappedEvent.ignore(); + if (QCoreApplication::sendEvent(_sharedObject->getWindow(), &mappedEvent)) { + return mappedEvent.isAccepted(); + } + break; + } + default: + break; + } + + return false; +} + +void OffscreenSurface::pause() { + _sharedObject->pause(); +} + +void OffscreenSurface::resume() { + _sharedObject->resume(); +} + +bool OffscreenSurface::isPaused() const { + return _sharedObject->isPaused(); +} + +void OffscreenSurface::setProxyWindow(QWindow* window) { + _sharedObject->setProxyWindow(window); +} + +QObject* OffscreenSurface::getEventHandler() { + return getWindow(); +} + +QQuickWindow* OffscreenSurface::getWindow() { + return _sharedObject->getWindow(); +} + +QSize OffscreenSurface::size() const { + return _sharedObject->getSize(); +} + +QQmlContext* OffscreenSurface::getSurfaceContext() { + return _sharedObject->getContext(); +} + +void OffscreenSurface::setMaxFps(uint8_t maxFps) { + _sharedObject->setMaxFps(maxFps); +} + +void OffscreenSurface::load(const QUrl& qmlSource, QQuickItem* parent, const QJSValue& callback) { + loadInternal(qmlSource, false, parent, [callback](QQmlContext* context, QQuickItem* newItem) { + QJSValue(callback).call(QJSValueList() << context->engine()->newQObject(newItem)); + }); +} + +void OffscreenSurface::load(const QUrl& qmlSource, bool createNewContext, const QmlContextObjectCallback& callback) { + loadInternal(qmlSource, createNewContext, nullptr, callback); +} + +void OffscreenSurface::loadInNewContext(const QUrl& qmlSource, const QmlContextObjectCallback& callback) { + load(qmlSource, true, callback); +} + +void OffscreenSurface::load(const QUrl& qmlSource, const QmlContextObjectCallback& callback) { + load(qmlSource, false, callback); +} + +void OffscreenSurface::load(const QString& qmlSourceFile, const QmlContextObjectCallback& callback) { + return load(QUrl(qmlSourceFile), callback); +} + +void OffscreenSurface::loadInternal(const QUrl& qmlSource, + bool createNewContext, + QQuickItem* parent, + const QmlContextObjectCallback& callback) { + if (QThread::currentThread() != thread()) { + qFatal("Called load on a non-surface thread"); + } + // Synchronous loading may take a while; restart the deadlock timer + QMetaObject::invokeMethod(qApp, "updateHeartbeat", Qt::DirectConnection); + + if (!getRootItem()) { + _sharedObject->create(this); + } + + QUrl finalQmlSource = qmlSource; + if ((qmlSource.isRelative() && !qmlSource.isEmpty()) || qmlSource.scheme() == QLatin1String("file")) { + finalQmlSource = getSurfaceContext()->resolvedUrl(qmlSource); + } + + if (!getRootItem()) { + _sharedObject->setObjectName(finalQmlSource.toString()); + } + + auto targetContext = contextForUrl(finalQmlSource, parent, createNewContext); + auto qmlComponent = new QQmlComponent(getSurfaceContext()->engine(), finalQmlSource, QQmlComponent::PreferSynchronous); + if (qmlComponent->isLoading()) { + connect(qmlComponent, &QQmlComponent::statusChanged, this, + [=](QQmlComponent::Status) { finishQmlLoad(qmlComponent, targetContext, parent, callback); }); + return; + } + + finishQmlLoad(qmlComponent, targetContext, parent, callback); +} + +void OffscreenSurface::finishQmlLoad(QQmlComponent* qmlComponent, + QQmlContext* qmlContext, + QQuickItem* parent, + const QmlContextObjectCallback& callback) { + disconnect(qmlComponent, &QQmlComponent::statusChanged, this, 0); + if (qmlComponent->isError()) { + for (const auto& error : qmlComponent->errors()) { + qCWarning(qmlLogging) << error.url() << error.line() << error; + } + qmlComponent->deleteLater(); + return; + } + + QObject* newObject = qmlComponent->beginCreate(qmlContext); + if (qmlComponent->isError()) { + for (const auto& error : qmlComponent->errors()) { + qCWarning(qmlLogging) << error.url() << error.line() << error; + } + if (!getRootItem()) { + qFatal("Unable to finish loading QML root"); + } + qmlComponent->deleteLater(); + return; + } + + if (!newObject) { + if (!getRootItem()) { + qFatal("Could not load object as root item"); + return; + } + qCWarning(qmlLogging) << "Unable to load QML item"; + return; + } + + qmlContext->engine()->setObjectOwnership(this, QQmlEngine::CppOwnership); + + // 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); + } + + bool rootCreated = getRootItem() != nullptr; + + // Make sure we will call callback for this codepath + // Call this before qmlComponent->completeCreate() otherwise ghost window appears + // If we already have a root, just set a couple of flags and the ancestry + if (rootCreated) { + callback(qmlContext, newItem); + if (!parent) { + parent = getRootItem(); + } + // Allow child windows to be destroyed from JS + QQmlEngine::setObjectOwnership(newObject, QQmlEngine::JavaScriptOwnership); + newObject->setParent(parent); + newItem->setParentItem(parent); + } else { + // The root item is ready. Associate it with the window. + _sharedObject->setRootItem(newItem); + } + + qmlComponent->completeCreate(); + qmlComponent->deleteLater(); + + onItemCreated(qmlContext, newItem); + connect(newItem, SIGNAL(sendToScript(QVariant)), this, SIGNAL(fromQml(QVariant))); + + if (!rootCreated) { + onRootCreated(); + emit rootItemCreated(newItem); + // Call this callback after rootitem is set, otherwise VrMenu wont work + callback(qmlContext, newItem); + } +} + +QQmlContext* OffscreenSurface::contextForUrl(const QUrl& qmlSource, QQuickItem* parent, bool forceNewContext) { + QQmlContext* targetContext = parent ? QQmlEngine::contextForObject(parent) : getSurfaceContext(); + if (!targetContext) { + targetContext = getSurfaceContext(); + } + + if (getRootItem() && forceNewContext) { + targetContext = new QQmlContext(targetContext, targetContext->engine()); + } + + return targetContext; +} diff --git a/libraries/qml/src/qml/OffscreenSurface.h b/libraries/qml/src/qml/OffscreenSurface.h new file mode 100644 index 0000000000..555f2ee6a4 --- /dev/null +++ b/libraries/qml/src/qml/OffscreenSurface.h @@ -0,0 +1,127 @@ +// +// 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_qml_OffscreenSurface_h +#define hifi_qml_OffscreenSurface_h + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +class QWindow; +class QOpenGLContext; +class QQmlContext; +class QQmlEngine; +class QQmlComponent; +class QQuickWindow; +class QQuickItem; +class OffscreenQmlSharedObject; + +namespace hifi { namespace qml { + +namespace impl { +class SharedObject; +} + +using QmlContextObjectCallback = ::std::function; + +class OffscreenSurface : public QObject { + Q_OBJECT + +public: + static const QmlContextObjectCallback DEFAULT_CONTEXT_CALLBACK; + + using TextureAndFence = std::pair; + using MouseTranslator = std::function; + + static void setSharedContext(QOpenGLContext* context); + + OffscreenSurface(); + virtual ~OffscreenSurface(); + + QSize size() const; + virtual void resize(const QSize& size); + void clearCache(); + + void setMaxFps(uint8_t maxFps); + // Optional values for event handling + void setProxyWindow(QWindow* window); + void setMouseTranslator(const MouseTranslator& mouseTranslator) { _mouseTranslator = mouseTranslator; } + + void pause(); + void resume(); + bool isPaused() const; + + QQuickItem* getRootItem(); + QQuickWindow* getWindow(); + QObject* getEventHandler(); + QQmlContext* getSurfaceContext(); + + // Checks to see if a new texture is available. If one is, the function returns true and + // textureAndFence will be populated with the texture ID and a fence which will be signalled + // when the texture is safe to read. + // Returns false if no new texture is available + bool fetchTexture(TextureAndFence& textureAndFence); + + static std::function getDiscardLambda(); + static size_t getUsedTextureMemory(); + QPointF mapToVirtualScreen(const QPointF& originalPoint); + + // For use from QML/JS + Q_INVOKABLE void load(const QUrl& qmlSource, QQuickItem* parent, const QJSValue& callback); + + // For use from C++ + Q_INVOKABLE void load(const QUrl& qmlSource, const QmlContextObjectCallback& callback = DEFAULT_CONTEXT_CALLBACK); + Q_INVOKABLE void load(const QUrl& qmlSource, bool createNewContext, const QmlContextObjectCallback& callback = DEFAULT_CONTEXT_CALLBACK); + Q_INVOKABLE void load(const QString& qmlSourceFile, const QmlContextObjectCallback& callback = DEFAULT_CONTEXT_CALLBACK); + Q_INVOKABLE void loadInNewContext(const QUrl& qmlSource, const QmlContextObjectCallback& callback = DEFAULT_CONTEXT_CALLBACK); + +public slots: + virtual void onFocusObjectChanged(QObject* newFocus) {} + +signals: + void rootContextCreated(QQmlContext* rootContext); + void rootItemCreated(QQuickItem* rootContext); + +protected: + bool eventFilter(QObject* originalDestination, QEvent* event) override; + bool filterEnabled(QObject* originalDestination, QEvent* event) const; + + virtual void initializeEngine(QQmlEngine* engine); + virtual void loadInternal(const QUrl& qmlSource, + bool createNewContext, + QQuickItem* parent, + const QmlContextObjectCallback& callback) final; + virtual void finishQmlLoad(QQmlComponent* qmlComponent, + QQmlContext* qmlContext, + QQuickItem* parent, + const QmlContextObjectCallback& onQmlLoadedCallback) final; + + virtual void onRootCreated() {} + virtual void onItemCreated(QQmlContext* context, QQuickItem* newItem) {} + virtual void onRootContextCreated(QQmlContext* qmlContext) {} + + virtual QQmlContext* contextForUrl(const QUrl& qmlSource, QQuickItem* parent, bool forceNewContext); +private: + MouseTranslator _mouseTranslator{ [](const QPointF& p) { return p.toPoint(); } }; + friend class hifi::qml::impl::SharedObject; + impl::SharedObject* const _sharedObject; +}; + +}} // namespace hifi::qml + +#endif diff --git a/libraries/qml/src/qml/impl/Profiling.cpp b/libraries/qml/src/qml/impl/Profiling.cpp new file mode 100644 index 0000000000..488b45b082 --- /dev/null +++ b/libraries/qml/src/qml/impl/Profiling.cpp @@ -0,0 +1,12 @@ +// +// Created by Bradley Austin Davis on 2018-01-04 +// Copyright 2013-2018 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 "Profiling.h" + +Q_LOGGING_CATEGORY(trace_render_qml, "trace.render.qml") +Q_LOGGING_CATEGORY(trace_render_qml_gl, "trace.render.qml.gl") diff --git a/libraries/qml/src/qml/impl/Profiling.h b/libraries/qml/src/qml/impl/Profiling.h new file mode 100644 index 0000000000..c1f3decedc --- /dev/null +++ b/libraries/qml/src/qml/impl/Profiling.h @@ -0,0 +1,13 @@ +// +// Created by Bradley Austin Davis on 2018-01-04 +// Copyright 2013-2018 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 + +#include + +Q_DECLARE_LOGGING_CATEGORY(trace_render_qml) +Q_DECLARE_LOGGING_CATEGORY(trace_render_qml_gl) diff --git a/libraries/qml/src/qml/impl/RenderControl.cpp b/libraries/qml/src/qml/impl/RenderControl.cpp new file mode 100644 index 0000000000..885d719df5 --- /dev/null +++ b/libraries/qml/src/qml/impl/RenderControl.cpp @@ -0,0 +1,29 @@ +// +// Created by Bradley Austin Davis on 2018-01-04 +// Copyright 2013-2018 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 "RenderControl.h" + +using namespace hifi::qml::impl; + +RenderControl::RenderControl(QObject* parent) : QQuickRenderControl(parent) { + +} + +void RenderControl::setRenderWindow(QWindow* renderWindow) { + _renderWindow = renderWindow; +} + +QWindow* RenderControl::renderWindow(QPoint* offset) { + if (nullptr == _renderWindow) { + return QQuickRenderControl::renderWindow(offset); + } + if (nullptr != offset) { + offset->rx() = offset->ry() = 0; + } + return _renderWindow; +} + diff --git a/libraries/qml/src/qml/impl/RenderControl.h b/libraries/qml/src/qml/impl/RenderControl.h new file mode 100644 index 0000000000..e63196b6ee --- /dev/null +++ b/libraries/qml/src/qml/impl/RenderControl.h @@ -0,0 +1,26 @@ +// +// Created by Bradley Austin Davis on 2018-01-04 +// Copyright 2013-2018 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 + +#include + +namespace hifi { namespace qml { namespace impl { + +class RenderControl : public QQuickRenderControl { +public: + RenderControl(QObject* parent = Q_NULLPTR); + void setRenderWindow(QWindow* renderWindow); + +protected: + QWindow* renderWindow(QPoint* offset) override; + +private: + QWindow* _renderWindow{ nullptr }; +}; + +}}} // namespace hifi::qml::impl diff --git a/libraries/qml/src/qml/impl/RenderEventHandler.cpp b/libraries/qml/src/qml/impl/RenderEventHandler.cpp new file mode 100644 index 0000000000..cce1b68da9 --- /dev/null +++ b/libraries/qml/src/qml/impl/RenderEventHandler.cpp @@ -0,0 +1,169 @@ +// +// Created by Bradley Austin Davis on 2018-01-04 +// Copyright 2013-2018 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 "RenderEventHandler.h" + +#include +#include + +#include + +#include +#include "Profiling.h" +#include "SharedObject.h" +#include "TextureCache.h" +#include "RenderControl.h" +#include "../Logging.h" + +using namespace hifi::qml::impl; + +bool RenderEventHandler::event(QEvent* e) { + switch (static_cast(e->type())) { + case OffscreenEvent::Render: + onRender(); + return true; + + case OffscreenEvent::Initialize: + onInitalize(); + return true; + + case OffscreenEvent::Quit: + onQuit(); + return true; + + default: + break; + } + return QObject::event(e); +} + +RenderEventHandler::RenderEventHandler(SharedObject* shared, QThread* targetThread) + : _shared(shared) { + // Create the GL canvas in the same thread as the share canvas + if (!_canvas.create(SharedObject::getSharedContext())) { + qFatal("Unable to create new offscreen GL context"); + } + + moveToThread(targetThread); + _canvas.moveToThreadWithContext(targetThread); +} + +void RenderEventHandler::onInitalize() { + if (_shared->isQuit()) { + return; + } + + if (!_canvas.makeCurrent()) { + qFatal("Unable to make QML rendering context current on render thread"); + } + _shared->initializeRenderControl(_canvas.getContext()); +} + +void RenderEventHandler::resize() { + PROFILE_RANGE(render_qml_gl, __FUNCTION__); + auto targetSize = _shared->getSize(); + if (_currentSize != targetSize) { + auto& offscreenTextures = SharedObject::getTextureCache(); + // Release hold on the textures of the old size + if (_currentSize != QSize()) { + _shared->releaseTextureAndFence(); + offscreenTextures.releaseSize(_currentSize); + } + + _currentSize = targetSize; + + // Acquire the new texture size + if (_currentSize != QSize()) { + qCDebug(qmlLogging) << "Upating offscreen textures to " << _currentSize.width() << " x " << _currentSize.height(); + offscreenTextures.acquireSize(_currentSize); + if (_depthStencil) { + glDeleteRenderbuffers(1, &_depthStencil); + _depthStencil = 0; + } + glGenRenderbuffers(1, &_depthStencil); + glBindRenderbuffer(GL_RENDERBUFFER, _depthStencil); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, _currentSize.width(), _currentSize.height()); + if (!_fbo) { + glGenFramebuffers(1, &_fbo); + } + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); + glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthStencil); + glViewport(0, 0, _currentSize.width(), _currentSize.height()); + glScissor(0, 0, _currentSize.width(), _currentSize.height()); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + } + } +} + +void RenderEventHandler::onRender() { + if (_shared->isQuit()) { + return; + } + + if (_canvas.getContext() != QOpenGLContextWrapper::currentContext()) { + qFatal("QML rendering context not current on render thread"); + } + + PROFILE_RANGE(render_qml_gl, __FUNCTION__); + + if (!_shared->preRender()) { + return; + } + + resize(); + + { + PROFILE_RANGE(render_qml_gl, "render"); + GLuint texture = SharedObject::getTextureCache().acquireTexture(_currentSize); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); + glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0); + if (nsightActive()) { + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + } else { + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + _shared->_quickWindow->setRenderTarget(_fbo, _currentSize); + _shared->_renderControl->render(); + } + _shared->_lastRenderTime = usecTimestampNow(); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glBindTexture(GL_TEXTURE_2D, texture); + glGenerateMipmap(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, 0); + auto fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + _shared->updateTextureAndFence({ texture, fence }); + // Fence will be used in another thread / context, so a flush is required + _shared->_quickWindow->resetOpenGLState(); + } +} + +void RenderEventHandler::onQuit() { + if (_canvas.getContext() != QOpenGLContextWrapper::currentContext()) { + qFatal("QML rendering context not current on render thread"); + } + + if (_depthStencil) { + glDeleteRenderbuffers(1, &_depthStencil); + _depthStencil = 0; + } + + if (_fbo) { + glDeleteFramebuffers(1, &_fbo); + _fbo = 0; + } + + _shared->shutdownRendering(_canvas, _currentSize); + // Release the reference to the shared object. This will allow it to + // be destroyed (should happen on it's own thread). + _shared->deleteLater(); + + deleteLater(); + + QThread::currentThread()->quit(); +} diff --git a/libraries/qml/src/qml/impl/RenderEventHandler.h b/libraries/qml/src/qml/impl/RenderEventHandler.h new file mode 100644 index 0000000000..d1e079cc85 --- /dev/null +++ b/libraries/qml/src/qml/impl/RenderEventHandler.h @@ -0,0 +1,56 @@ +// +// Created by Bradley Austin Davis on 2018-01-04 +// Copyright 2013-2018 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 + +#include +#include +#include + +#include +#include + +namespace hifi { namespace qml { namespace impl { + +class SharedObject; + +class OffscreenEvent : public QEvent { +public: + enum Type { + Initialize = QEvent::User + 1, + Render, + Quit + }; + + OffscreenEvent(Type type) : QEvent(static_cast(type)) {} +}; + +/* The render event handler lives on the QML rendering thread for a given surface + * (each surface has a dedicated rendering thread) and handles events of type + * OffscreenEvent to do one time initialization or destruction, and to actually + * perform the render. + */ +class RenderEventHandler : public QObject { +public: + RenderEventHandler(SharedObject* shared, QThread* targetThread); + +private: + bool event(QEvent* e) override; + void onInitalize(); + void resize(); + void onRender(); + void onQuit(); + + SharedObject* const _shared; + OffscreenGLCanvas _canvas; + QSize _currentSize; + + uint32_t _fbo{ 0 }; + uint32_t _depthStencil{ 0 }; +}; + +}}} // namespace hifi::qml::impl diff --git a/libraries/qml/src/qml/impl/SharedObject.cpp b/libraries/qml/src/qml/impl/SharedObject.cpp new file mode 100644 index 0000000000..f2af5bd036 --- /dev/null +++ b/libraries/qml/src/qml/impl/SharedObject.cpp @@ -0,0 +1,448 @@ +// +// Created by Bradley Austin Davis on 2018-01-04 +// Copyright 2013-2018 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 "SharedObject.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "../OffscreenSurface.h" +#include "../Logging.h" + +#include "Profiling.h" +#include "RenderControl.h" +#include "RenderEventHandler.h" +#include "TextureCache.h" + +// 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; + +using namespace hifi::qml; +using namespace hifi::qml::impl; + +TextureCache offscreenTextures; + +TextureCache& SharedObject::getTextureCache() { + return offscreenTextures; +} + +#define OFFSCREEN_QML_SHARED_CONTEXT_PROPERTY "com.highfidelity.qml.gl.sharedContext" +void SharedObject::setSharedContext(QOpenGLContext* sharedContext) { + qApp->setProperty(OFFSCREEN_QML_SHARED_CONTEXT_PROPERTY, QVariant::fromValue(sharedContext)); + if (QOpenGLContextWrapper::currentContext() != sharedContext) { + qFatal("The shared context must be the current context when setting"); + } +} + +QOpenGLContext* SharedObject::getSharedContext() { + return static_cast(qApp->property(OFFSCREEN_QML_SHARED_CONTEXT_PROPERTY).value()); +} + +SharedObject::SharedObject() { + // Create render control + _renderControl = new RenderControl(); + + // 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. + QQuickWindow::setDefaultAlphaBuffer(true); + _quickWindow = new QQuickWindow(_renderControl); + _quickWindow->setColor(QColor(255, 255, 255, 0)); + _quickWindow->setClearBeforeRendering(true); + + QObject::connect(qApp, &QCoreApplication::aboutToQuit, this, &SharedObject::onAboutToQuit); +} + +SharedObject::~SharedObject() { + if (_quickWindow) { + _quickWindow->destroy(); + _quickWindow = nullptr; + } + + if (_renderControl) { + _renderControl->deleteLater(); + _renderControl = nullptr; + } + + if (_renderThread) { + _renderThread->quit(); + _renderThread->deleteLater(); + } + + if (_rootItem) { + _rootItem->deleteLater(); + _rootItem = nullptr; + } + + releaseEngine(_qmlContext->engine()); +} + +void SharedObject::create(OffscreenSurface* surface) { + if (_rootItem) { + qFatal("QML surface root item already set"); + } + + QObject::connect(_quickWindow, &QQuickWindow::focusObjectChanged, surface, &OffscreenSurface::onFocusObjectChanged); + + // Create a QML engine. + auto qmlEngine = acquireEngine(surface); + _qmlContext = new QQmlContext(qmlEngine->rootContext(), qmlEngine); + surface->onRootContextCreated(_qmlContext); + emit surface->rootContextCreated(_qmlContext); + + if (!qmlEngine->incubationController()) { + qmlEngine->setIncubationController(_quickWindow->incubationController()); + } + _qmlContext->setContextProperty("offscreenWindow", QVariant::fromValue(_quickWindow)); +} + +void SharedObject::setRootItem(QQuickItem* rootItem) { + _rootItem = rootItem; + _rootItem->setSize(_quickWindow->size()); + + // Create the render thread + _renderThread = new QThread(); + _renderThread->setObjectName(objectName()); + _renderThread->start(); + + // Create event handler for the render thread + _renderObject = new RenderEventHandler(this, _renderThread); + QCoreApplication::postEvent(this, new OffscreenEvent(OffscreenEvent::Initialize)); + + QObject::connect(_renderControl, &QQuickRenderControl::renderRequested, this, &SharedObject::requestRender); + QObject::connect(_renderControl, &QQuickRenderControl::sceneChanged, this, &SharedObject::requestRenderSync); +} + +void SharedObject::destroy() { + if (_quit) { + return; + } + + if (!_rootItem) { + deleteLater(); + return; + } + + + _paused = true; + if (_renderTimer) { + QObject::disconnect(_renderTimer); + _renderTimer->deleteLater(); + } + + QObject::disconnect(_renderControl); + QObject::disconnect(qApp); + + QMutexLocker lock(&_mutex); + _quit = true; + QCoreApplication::postEvent(_renderObject, new OffscreenEvent(OffscreenEvent::Quit)); +} + + +#define SINGLE_QML_ENGINE 0 + + +#if SINGLE_QML_ENGINE +static QQmlEngine* globalEngine{ nullptr }; +static size_t globalEngineRefCount{ 0 }; +#endif + +QQmlEngine* SharedObject::acquireEngine(OffscreenSurface* surface) { + Q_ASSERT(QThread::currentThread() == qApp->thread()); + + QQmlEngine* result = nullptr; + if (QThread::currentThread() != qApp->thread()) { + qCWarning(qmlLogging) << "Cannot acquire QML engine on any thread but the main thread"; + } + +#if SINGLE_QML_ENGINE + if (!globalEngine) { + Q_ASSERT(0 == globalEngineRefCount); + globalEngine = new QQmlEngine(); + surface->initializeQmlEngine(result); + ++globalEngineRefCount; + } + result = globalEngine; +#else + result = new QQmlEngine(); + surface->initializeEngine(result); +#endif + + return result; +} + +void SharedObject::releaseEngine(QQmlEngine* engine) { + Q_ASSERT(QThread::currentThread() == qApp->thread()); +#if SINGLE_QML_ENGINE + Q_ASSERT(0 != globalEngineRefCount); + if (0 == --globalEngineRefCount) { + globalEngine->deleteLater(); + globalEngine = nullptr; + } +#else + engine->deleteLater(); +#endif +} + +bool SharedObject::event(QEvent* e) { + switch (static_cast(e->type())) { + case OffscreenEvent::Initialize: + onInitialize(); + return true; + + case OffscreenEvent::Render: + onRender(); + return true; + + default: + break; + } + return QObject::event(e); +} + +// Called by the render event handler, from the render thread +void SharedObject::initializeRenderControl(QOpenGLContext* context) { + if (context->shareContext() != getSharedContext()) { + qFatal("QML rendering context has no share context"); + } + + if (!nsightActive()) { + _renderControl->initialize(context); + } +} + +void SharedObject::releaseTextureAndFence() { + QMutexLocker lock(&_mutex); + // If the most recent texture was unused, we can directly recycle it + if (_latestTextureAndFence.first) { + offscreenTextures.releaseTexture(_latestTextureAndFence); + _latestTextureAndFence = TextureAndFence{ 0, 0 }; + } +} + +void SharedObject::setRenderTarget(uint32_t fbo, const QSize& size) { + _quickWindow->setRenderTarget(fbo, size); +} + +QSize SharedObject::getSize() const { + QMutexLocker locker(&_mutex); + return _size; +} + +void SharedObject::setSize(const QSize& size) { + if (getSize() == size) { + return; + } + + { + QMutexLocker locker(&_mutex); + _size = size; + } + + qCDebug(qmlLogging) << "Offscreen UI resizing to " << size.width() << "x" << size.height(); + _quickWindow->setGeometry(QRect(QPoint(), size)); + _quickWindow->contentItem()->setSize(size); + + if (_rootItem) { + _qmlContext->setContextProperty("surfaceSize", size); + _rootItem->setSize(size); + } + + requestRenderSync(); +} + +bool SharedObject::preRender() { + QMutexLocker lock(&_mutex); + if (_paused) { + if (_syncRequested) { + wake(); + } + return false; + } + + if (_syncRequested) { + bool syncResult = true; + if (!nsightActive()) { + PROFILE_RANGE(render_qml_gl, "sync") + syncResult = _renderControl->sync(); + } + wake(); + if (!syncResult) { + return false; + } + _syncRequested = false; + } + + return true; +} + +void SharedObject::shutdownRendering(OffscreenGLCanvas& canvas, const QSize& size) { + QMutexLocker locker(&_mutex); + if (size != QSize(0, 0)) { + offscreenTextures.releaseSize(size); + } + _renderControl->invalidate(); + canvas.doneCurrent(); + wake(); +} + +bool SharedObject::isQuit() { + QMutexLocker locker(&_mutex); + return _quit; +} + +void SharedObject::requestRender() { + // Don't queue multiple renders + if (_renderRequested) { + return; + } + _renderRequested = true; +} + +void SharedObject::requestRenderSync() { + if (_quit) { + return; + } + + { + QMutexLocker lock(&_mutex); + _syncRequested = true; + } + + requestRender(); +} + +bool SharedObject::fetchTexture(TextureAndFence& textureAndFence) { + QMutexLocker locker(&_mutex); + if (0 == _latestTextureAndFence.first) { + return false; + } + textureAndFence = { 0, 0 }; + std::swap(textureAndFence, _latestTextureAndFence); + return true; +} + +void SharedObject::setProxyWindow(QWindow* window) { + _proxyWindow = window; + _renderControl->setRenderWindow(window); +} + +void SharedObject::wait() { + _cond.wait(&_mutex); +} + +void SharedObject::wake() { + _cond.wakeOne(); +} + +void SharedObject::onInitialize() { + // Associate root item with the window. + _rootItem->setParentItem(_quickWindow->contentItem()); + _renderControl->prepareThread(_renderThread); + + // Set up the render thread + QCoreApplication::postEvent(_renderObject, new OffscreenEvent(OffscreenEvent::Initialize)); + + requestRender(); + + // Set up timer to trigger renders + _renderTimer = new QTimer(this); + QObject::connect(_renderTimer, &QTimer::timeout, this, &SharedObject::onTimer); + + _renderTimer->setTimerType(Qt::PreciseTimer); + _renderTimer->setInterval(MIN_TIMER_MS); // 5ms, Qt::PreciseTimer required + _renderTimer->start(); +} + +void SharedObject::onRender() { + PROFILE_RANGE(render_qml, __FUNCTION__); + if (_quit) { + return; + } + + QMutexLocker lock(&_mutex); + if (_syncRequested) { + lock.unlock(); + _renderControl->polishItems(); + lock.relock(); + QCoreApplication::postEvent(_renderObject, new OffscreenEvent(OffscreenEvent::Render)); + // sync and render request, main and render threads must be synchronized + wait(); + } else { + QCoreApplication::postEvent(_renderObject, new OffscreenEvent(OffscreenEvent::Render)); + } + _renderRequested = false; +} + +void SharedObject::onTimer() { + offscreenTextures.report(); + if (!_renderRequested) { + return; + } + + { + QMutexLocker locker(&_mutex); + // Don't queue more than one frame at a time + if (0 != _latestTextureAndFence.first) { + return; + } + } + + { + auto minRenderInterval = USECS_PER_SECOND / _maxFps; + auto lastInterval = usecTimestampNow() - _lastRenderTime; + // Don't exceed the framerate limit + if (lastInterval < minRenderInterval) { + return; + } + } + + QCoreApplication::postEvent(this, new OffscreenEvent(OffscreenEvent::Render)); +} + +void SharedObject::onAboutToQuit() { + destroy(); +} + +void SharedObject::updateTextureAndFence(const TextureAndFence& newTextureAndFence) { + QMutexLocker locker(&_mutex); + // If the most recent texture was unused, we can directly recycle it + if (_latestTextureAndFence.first) { + offscreenTextures.releaseTexture(_latestTextureAndFence); + _latestTextureAndFence = { 0, 0 }; + } + + _latestTextureAndFence = newTextureAndFence; +} + +void SharedObject::pause() { + _paused = true; +} + +void SharedObject::resume() { + _paused = false; + requestRender(); +} + +bool SharedObject::isPaused() const { + return _paused; +} diff --git a/libraries/qml/src/qml/impl/SharedObject.h b/libraries/qml/src/qml/impl/SharedObject.h new file mode 100644 index 0000000000..76dde611fc --- /dev/null +++ b/libraries/qml/src/qml/impl/SharedObject.h @@ -0,0 +1,119 @@ +// +// Created by Bradley Austin Davis on 2018-01-04 +// Copyright 2013-2018 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 + +#include +#include +#include +#include +#include +#include + +#include "TextureCache.h" + + +class QWindow; +class QTimer; +class QQuickWindow; +class QQuickItem; +class QOpenGLContext; +class QQmlEngine; +class QQmlContext; +class OffscreenGLCanvas; + +namespace hifi { namespace qml { + +class OffscreenSurface; + +namespace impl { + +class RenderControl; +class RenderEventHandler; + +class SharedObject : public QObject { + Q_OBJECT + + friend class RenderEventHandler; + +public: + static void setSharedContext(QOpenGLContext* context); + static QOpenGLContext* getSharedContext(); + static TextureCache& getTextureCache(); + + SharedObject(); + virtual ~SharedObject(); + + void create(OffscreenSurface* surface); + void setRootItem(QQuickItem* rootItem); + void destroy(); + bool isQuit(); + + QSize getSize() const; + void setSize(const QSize& size); + void setMaxFps(uint8_t maxFps) { _maxFps = maxFps; } + + QQuickWindow* getWindow() { return _quickWindow; } + QQuickItem* getRootItem() { return _rootItem; } + QQmlContext* getContext() { return _qmlContext; } + void setProxyWindow(QWindow* window); + + void pause(); + void resume(); + bool isPaused() const; + bool fetchTexture(TextureAndFence& textureAndFence); + + +private: + bool event(QEvent* e) override; + + bool preRender(); + void shutdownRendering(OffscreenGLCanvas& canvas, const QSize& size); + // Called by the render event handler, from the render thread + void initializeRenderControl(QOpenGLContext* context); + void releaseTextureAndFence(); + void setRenderTarget(uint32_t fbo, const QSize& size); + + QQmlEngine* acquireEngine(OffscreenSurface* surface); + void releaseEngine(QQmlEngine* engine); + + void requestRender(); + void requestRenderSync(); + void wait(); + void wake(); + void onInitialize(); + void onRender(); + void onTimer(); + void onAboutToQuit(); + void updateTextureAndFence(const TextureAndFence& newTextureAndFence); + + // Texture management + TextureAndFence _latestTextureAndFence{ 0, 0 }; + RenderControl* _renderControl{ nullptr }; + RenderEventHandler* _renderObject{ nullptr }; + QQuickWindow* _quickWindow{ nullptr }; + QWindow* _proxyWindow{ nullptr }; + QQuickItem* _item{ nullptr }; + QQuickItem* _rootItem{ nullptr }; + QQmlContext* _qmlContext{ nullptr }; + QTimer* _renderTimer{ nullptr }; + QThread* _renderThread{ nullptr }; + QWaitCondition _cond; + mutable QMutex _mutex; + + uint64_t _lastRenderTime{ 0 }; + QSize _size{ 100, 100 }; + uint8_t _maxFps{ 60 }; + + bool _renderRequested{ false }; + bool _syncRequested{ false }; + bool _quit{ false }; + bool _paused{ false }; +}; + +} // namespace impl +}} // namespace hifi::qml diff --git a/libraries/qml/src/qml/impl/TextureCache.cpp b/libraries/qml/src/qml/impl/TextureCache.cpp new file mode 100644 index 0000000000..c649a36594 --- /dev/null +++ b/libraries/qml/src/qml/impl/TextureCache.cpp @@ -0,0 +1,152 @@ +#include "TextureCache.h" + +#include + +#include + +#include +#include + +#include "Profiling.h" + +using namespace hifi::qml::impl; + +#if defined(Q_OS_ANDROID) +#define USE_GLES 1 +#endif + +uint64_t uvec2ToUint64(const QSize& size) { + uint64_t result = size.width(); + result <<= 32; + result |= size.height(); + return result; +} + +void TextureCache::acquireSize(const QSize& size) { + auto sizeKey = uvec2ToUint64(size); + Lock lock(_mutex); + auto& textureSet = _textures[sizeKey]; + ++textureSet.clientCount; +} + +void TextureCache::releaseSize(const QSize& size) { + auto sizeKey = uvec2ToUint64(size); + ValueList texturesToDelete; + { + Lock lock(_mutex); + assert(_textures.count(sizeKey)); + auto& textureSet = _textures[sizeKey]; + if (0 == --textureSet.clientCount) { + texturesToDelete.swap(textureSet.returnedTextures); + _textures.erase(sizeKey); + } + } + for (const auto& textureAndFence : texturesToDelete) { + destroy(textureAndFence); + } +} + +uint32_t TextureCache::acquireTexture(const QSize& size) { + Lock lock(_mutex); + recycle(); + + + ++_activeTextureCount; + auto sizeKey = uvec2ToUint64(size); + assert(_textures.count(sizeKey)); + auto& textureSet = _textures[sizeKey]; + if (!textureSet.returnedTextures.empty()) { + auto textureAndFence = textureSet.returnedTextures.front(); + textureSet.returnedTextures.pop_front(); + glWaitSync((GLsync)textureAndFence.second, 0, GL_TIMEOUT_IGNORED); + glDeleteSync((GLsync)textureAndFence.second); + return textureAndFence.first; + } + return createTexture(size); +} + +void TextureCache::releaseTexture(const Value& textureAndFence) { + --_activeTextureCount; + Lock lock(_mutex); + _returnedTextures.push_back(textureAndFence); +} + +void TextureCache::report() { + if (randFloat() < 0.01f) { + PROFILE_COUNTER(render_qml_gl, "offscreenTextures", + { + { "total", QVariant::fromValue(_allTextureCount.load()) }, + { "active", QVariant::fromValue(_activeTextureCount.load()) }, + }); + PROFILE_COUNTER(render_qml_gl, "offscreenTextureMemory", { { "value", QVariant::fromValue(_totalTextureUsage) } }); + } +} + +size_t TextureCache::getUsedTextureMemory() { + return _totalTextureUsage; +} + +size_t TextureCache::getMemoryForSize(const QSize& size) { + // Base size + mips + return static_cast(((size.width() * size.height()) << 2) * 1.33f); +} + +void TextureCache::destroyTexture(uint32_t texture) { + --_allTextureCount; + auto size = _textureSizes[texture]; + assert(getMemoryForSize(size) <= _totalTextureUsage); + _totalTextureUsage -= getMemoryForSize(size); + _textureSizes.erase(texture); + // FIXME prevents crash on shutdown, but we should migrate to a global functions object owned by the shared context. + glDeleteTextures(1, &texture); +} + +void TextureCache::destroy(const Value& textureAndFence) { + const auto& fence = textureAndFence.second; + // FIXME prevents crash on shutdown, but we should migrate to a global functions object owned by the shared context. + glWaitSync((GLsync)fence, 0, GL_TIMEOUT_IGNORED); + glDeleteSync((GLsync)fence); + destroyTexture(textureAndFence.first); +} + +uint32_t TextureCache::createTexture(const QSize& size) { + // Need a new texture + uint32_t newTexture; + glGenTextures(1, &newTexture); + ++_allTextureCount; + _textureSizes[newTexture] = size; + _totalTextureUsage += getMemoryForSize(size); + glBindTexture(GL_TEXTURE_2D, newTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f); +#if !defined(USE_GLES) + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -0.2f); +#endif + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, size.width(), size.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, 0); + return newTexture; +} + +void TextureCache::recycle() { + // First handle any global returns + ValueList returnedTextures; + returnedTextures.swap(_returnedTextures); + + for (auto textureAndFence : returnedTextures) { + GLuint texture = textureAndFence.first; + QSize size = _textureSizes[texture]; + auto sizeKey = uvec2ToUint64(size); + // Textures can be returned after all surfaces of the given size have been destroyed, + // in which case we just destroy the texture + if (!_textures.count(sizeKey)) { + destroy(textureAndFence); + continue; + } + _textures[sizeKey].returnedTextures.push_back(textureAndFence); + } +} diff --git a/libraries/qml/src/qml/impl/TextureCache.h b/libraries/qml/src/qml/impl/TextureCache.h new file mode 100644 index 0000000000..572e1cadea --- /dev/null +++ b/libraries/qml/src/qml/impl/TextureCache.h @@ -0,0 +1,75 @@ +// +// Created by Bradley Austin Davis on 2018-01-04 +// Copyright 2013-2018 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_QmlTextureCache_h +#define hifi_QmlTextureCache_h + +#include +#include +#include +#include +#include +#include + +#include + +namespace hifi { namespace qml { namespace impl { + +class TextureAndFence : public std::pair { + using Parent = std::pair; +public: + TextureAndFence() : Parent(0, 0) {} + TextureAndFence(uint32_t texture, void* sync) : Parent(texture, sync) {}; +}; + + +class TextureCache { +public: + using Value = TextureAndFence; + using ValueList = std::list; + using Size = uint64_t; + + struct TextureSet { + Size textureSize; + // The number of surfaces with this size + size_t clientCount{ 0 }; + ValueList returnedTextures; + }; + + + void releaseSize(const QSize& size); + void acquireSize(const QSize& size); + uint32_t acquireTexture(const QSize& size); + void releaseTexture(const Value& textureAndFence); + + // For debugging + void report(); + size_t getUsedTextureMemory(); +private: + static size_t getMemoryForSize(const QSize& size); + + uint32_t createTexture(const QSize& size); + void destroyTexture(uint32_t texture); + + void destroy(const Value& textureAndFence); + void recycle(); + + using Mutex = std::mutex; + using Lock = std::unique_lock; + std::atomic _allTextureCount; + std::atomic _activeTextureCount; + std::unordered_map _textures; + std::unordered_map _textureSizes; + Mutex _mutex; + std::list _returnedTextures; + size_t _totalTextureUsage{ 0 }; +}; + +}}} // namespace hifi::qml::impl + +#endif diff --git a/libraries/shared/src/shared/MiniPromises.h b/libraries/shared/src/shared/MiniPromises.h index 5983f135b7..30b57ad7b8 100644 --- a/libraries/shared/src/shared/MiniPromises.h +++ b/libraries/shared/src/shared/MiniPromises.h @@ -103,7 +103,7 @@ public: return self(); } Promise fail(ErrorFunction errorOnly) { - return fail([this, errorOnly](QString error, QVariantMap result) { + return fail([errorOnly](QString error, QVariantMap result) { errorOnly(error); }); } @@ -122,7 +122,7 @@ public: } Promise then(SuccessFunction successOnly) { - return then([this, successOnly](QString error, QVariantMap result) { + return then([successOnly](QString error, QVariantMap result) { successOnly(result); }); } diff --git a/libraries/ui/CMakeLists.txt b/libraries/ui/CMakeLists.txt index d16377587c..cbafc00f82 100644 --- a/libraries/ui/CMakeLists.txt +++ b/libraries/ui/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME ui) -setup_hifi_library(Widgets Multimedia Network Qml Quick Script WebChannel WebSockets XmlPatterns ${PLATFORM_QT_COMPONENTS}) -link_hifi_libraries(shared networking gl audio audio-client plugins pointers) +setup_hifi_library(OpenGL Multimedia Network Qml Quick Script WebChannel WebSockets XmlPatterns ${PLATFORM_QT_COMPONENTS}) +link_hifi_libraries(shared networking qml gl audio audio-client plugins pointers) include_hifi_library_headers(controllers) # Required for some low level GL interaction in the OffscreenQMLSurface diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 221f5013bf..0e3c15b965 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -127,13 +127,17 @@ void OffscreenUi::removeModalDialog(QObject* modal) { } } -void OffscreenUi::create() { - OffscreenQmlSurface::create(); - auto myContext = getSurfaceContext(); +void OffscreenUi::onRootContextCreated(QQmlContext* qmlContext) { + OffscreenQmlSurface::onRootContextCreated(qmlContext); + qmlContext->setContextProperty("OffscreenUi", this); + qmlContext->setContextProperty("offscreenFlags", offscreenFlags = new OffscreenFlags()); + qmlContext->setContextProperty("fileDialogHelper", new FileDialogHelper()); +#ifdef DEBUG + qmlContext->setContextProperty("DebugQML", QVariant(true)); +#else + qmlContext->setContextProperty("DebugQML", QVariant(false)); +#endif - myContext->setContextProperty("OffscreenUi", this); - myContext->setContextProperty("offscreenFlags", offscreenFlags = new OffscreenFlags()); - myContext->setContextProperty("fileDialogHelper", new FileDialogHelper()); } void OffscreenUi::show(const QUrl& url, const QString& name, std::function f) { @@ -656,12 +660,6 @@ void OffscreenUi::createDesktop(const QUrl& url) { return; } -#ifdef DEBUG - getSurfaceContext()->setContextProperty("DebugQML", QVariant(true)); -#else - getSurfaceContext()->setContextProperty("DebugQML", QVariant(false)); -#endif - load(url, [=](QQmlContext* context, QObject* newObject) { Q_UNUSED(context) _desktop = static_cast(newObject); diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index ab3a209820..e507333840 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -57,7 +57,6 @@ class OffscreenUi : public OffscreenQmlSurface, public Dependency { friend class VrMenu; public: OffscreenUi(); - virtual void create() override; void createDesktop(const QUrl& url); void show(const QUrl& url, const QString& name, std::function f = [](QQmlContext*, QObject*) {}); void hide(const QString& name); @@ -253,6 +252,9 @@ private slots: void hoverEndEvent(const PointerEvent& event); void handlePointerEvent(const PointerEvent& event); +protected: + void onRootContextCreated(QQmlContext* qmlContext) override; + private: QString fileDialog(const QVariantMap& properties); ModalDialogListener *fileDialogAsync(const QVariantMap &properties); diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index 6ff0a7d416..ea34f3de76 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -7,13 +7,13 @@ // #include "OffscreenQmlSurface.h" -#include - #include #include #include +#include +#include #include #include #include @@ -41,11 +41,11 @@ #include #include #include +#include #include #include #include -#include #include #include "ImageProvider.h" @@ -57,16 +57,12 @@ #include "ToolbarScriptingInterface.h" #include "Logging.h" -Q_LOGGING_CATEGORY(trace_render_qml, "trace.render.qml") -Q_LOGGING_CATEGORY(trace_render_qml_gl, "trace.render.qml.gl") -Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus") - +namespace hifi { namespace qml { namespace offscreen { class OffscreenQmlWhitelist : public Dependency, private ReadWriteLockable { SINGLETON_DEPENDENCY public: - void addWhitelistContextHandler(const std::initializer_list& urls, const QmlContextCallback& callback) { withWriteLock([&] { for (auto url : urls) { @@ -90,241 +86,32 @@ public: } private: - QHash> _callbacks; }; QSharedPointer getQmlWhitelist() { static std::once_flag once; - std::call_once(once, [&] { - DependencyManager::set(); - }); + std::call_once(once, [&] { DependencyManager::set(); }); return DependencyManager::get(); } - -void OffscreenQmlSurface::addWhitelistContextHandler(const std::initializer_list& urls, const QmlContextCallback& callback) { - getQmlWhitelist()->addWhitelistContextHandler(urls, callback); -} - - -QmlContextObjectCallback OffscreenQmlSurface::DEFAULT_CONTEXT_CALLBACK = [](QQmlContext*, QQuickItem*) {}; - -struct TextureSet { - // The number of surfaces with this size - size_t count { 0 }; - std::list returnedTextures; -}; - -uint64_t uvec2ToUint64(const uvec2& v) { - uint64_t result = v.x; - result <<= 32; - result |= v.y; - return result; -} - // Class to handle changing QML audio output device using another thread class AudioHandler : public QObject, QRunnable { Q_OBJECT public: - AudioHandler(QSharedPointer surface, const QString& deviceName, QObject* parent = nullptr) : QObject(parent) { - _newTargetDevice = deviceName; - _surface = surface; - setAutoDelete(true); - if (deviceName.size() > 0) { - QThreadPool::globalInstance()->start(this); - } - } - virtual ~AudioHandler() { - qDebug() << "Audio Handler Destroyed"; - } - void run() override { - if (!_surface.isNull() && _surface->getRootItem() && !_surface->getCleaned()) { - for (auto player : _surface->getRootItem()->findChildren()) { - auto mediaState = player->state(); - QMediaService *svc = player->service(); - if (nullptr == svc) { - return; - } - QAudioOutputSelectorControl *out = qobject_cast - (svc->requestControl(QAudioOutputSelectorControl_iid)); - if (nullptr == out) { - return; - } - QString deviceOuput; - auto outputs = out->availableOutputs(); - for (int i = 0; i < outputs.size(); i++) { - QString output = outputs[i]; - QString description = out->outputDescription(output); - if (description == _newTargetDevice) { - deviceOuput = output; - break; - } - } - out->setActiveOutput(deviceOuput); - svc->releaseControl(out); - // if multimedia was paused, it will start playing automatically after changing audio device - // this will reset it back to a paused state - if (mediaState == QMediaPlayer::State::PausedState) { - player->pause(); - } - else if (mediaState == QMediaPlayer::State::StoppedState) { - player->stop(); - } - } - } - qDebug() << "QML Audio changed to " << _newTargetDevice; - } + AudioHandler(OffscreenQmlSurface* surface, const QString& deviceName, QObject* parent = nullptr); + + virtual ~AudioHandler() { qDebug() << "Audio Handler Destroyed"; } + + void run() override; private: QString _newTargetDevice; QSharedPointer _surface; + std::vector _players; }; -class OffscreenTextures { -public: - GLuint getNextTexture(const uvec2& size) { - assert(QThread::currentThread() == qApp->thread()); - - recycle(); - - ++_activeTextureCount; - auto sizeKey = uvec2ToUint64(size); - assert(_textures.count(sizeKey)); - auto& textureSet = _textures[sizeKey]; - if (!textureSet.returnedTextures.empty()) { - auto textureAndFence = textureSet.returnedTextures.front(); - textureSet.returnedTextures.pop_front(); - waitOnFence(static_cast(textureAndFence.second)); - return textureAndFence.first; - } - - return createTexture(size); - } - - void releaseSize(const uvec2& size) { - assert(QThread::currentThread() == qApp->thread()); - auto sizeKey = uvec2ToUint64(size); - assert(_textures.count(sizeKey)); - auto& textureSet = _textures[sizeKey]; - if (0 == --textureSet.count) { - for (const auto& textureAndFence : textureSet.returnedTextures) { - destroy(textureAndFence); - } - _textures.erase(sizeKey); - } - } - - void acquireSize(const uvec2& size) { - assert(QThread::currentThread() == qApp->thread()); - auto sizeKey = uvec2ToUint64(size); - auto& textureSet = _textures[sizeKey]; - ++textureSet.count; - } - - // May be called on any thread - void releaseTexture(const OffscreenQmlSurface::TextureAndFence & textureAndFence) { - --_activeTextureCount; - Lock lock(_mutex); - _returnedTextures.push_back(textureAndFence); - } - - void report() { - if (randFloat() < 0.01f) { - PROFILE_COUNTER(render_qml_gl, "offscreenTextures", { - { "total", QVariant::fromValue(_allTextureCount.load()) }, - { "active", QVariant::fromValue(_activeTextureCount.load()) }, - }); - PROFILE_COUNTER(render_qml_gl, "offscreenTextureMemory", { - { "value", QVariant::fromValue(_totalTextureUsage) } - }); - } - } - - size_t getUsedTextureMemory() { return _totalTextureUsage; } -private: - static void waitOnFence(GLsync fence) { - glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); - glDeleteSync(fence); - } - - static size_t getMemoryForSize(const uvec2& size) { - // Base size + mips - return static_cast(((size.x * size.y) << 2) * 1.33f); - } - - void destroyTexture(GLuint texture) { - --_allTextureCount; - auto size = _textureSizes[texture]; - assert(getMemoryForSize(size) <= _totalTextureUsage); - _totalTextureUsage -= getMemoryForSize(size); - _textureSizes.erase(texture); - glDeleteTextures(1, &texture); - } - - void destroy(const OffscreenQmlSurface::TextureAndFence& textureAndFence) { - waitOnFence(static_cast(textureAndFence.second)); - destroyTexture(textureAndFence.first); - } - - GLuint createTexture(const uvec2& size) { - // Need a new texture - uint32_t newTexture; - glGenTextures(1, &newTexture); - ++_allTextureCount; - _textureSizes[newTexture] = size; - _totalTextureUsage += getMemoryForSize(size); - glBindTexture(GL_TEXTURE_2D, newTexture); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f); -#if !defined(USE_GLES) - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -0.2f); -#endif - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0); - return newTexture; - } - - void recycle() { - assert(QThread::currentThread() == qApp->thread()); - // First handle any global returns - std::list returnedTextures; - { - Lock lock(_mutex); - returnedTextures.swap(_returnedTextures); - } - - for (auto textureAndFence : returnedTextures) { - GLuint texture = textureAndFence.first; - uvec2 size = _textureSizes[texture]; - auto sizeKey = uvec2ToUint64(size); - // Textures can be returned after all surfaces of the given size have been destroyed, - // in which case we just destroy the texture - if (!_textures.count(sizeKey)) { - destroy(textureAndFence); - continue; - } - _textures[sizeKey].returnedTextures.push_back(textureAndFence); - } - } - - using Mutex = std::mutex; - using Lock = std::unique_lock; - std::atomic _allTextureCount; - std::atomic _activeTextureCount; - std::unordered_map _textures; - std::unordered_map _textureSizes; - Mutex _mutex; - std::list _returnedTextures; - size_t _totalTextureUsage { 0 }; -} offscreenTextures; - class UrlHandler : public QObject { Q_OBJECT public: @@ -339,57 +126,22 @@ 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. -// 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{ - if (nullptr == _renderWindow) { - return QQuickRenderControl::renderWindow(offset); - } - if (nullptr != offset) { - offset->rx() = offset->ry() = 0; - } - return _renderWindow; - } - -private: - QWindow* _renderWindow{ nullptr }; - friend class OffscreenQmlRenderThread; - friend class OffscreenQmlSurface; -}; - -size_t OffscreenQmlSurface::getUsedTextureMemory() { - return offscreenTextures.getUsedTextureMemory(); -} - -class QmlNetworkAccessManager : public NetworkAccessManager { -public: - friend class QmlNetworkAccessManagerFactory; -protected: - QmlNetworkAccessManager(QObject* parent) : NetworkAccessManager(parent) { } -}; - class QmlNetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory { public: - QNetworkAccessManager* create(QObject* parent) override; + class QmlNetworkAccessManager : public NetworkAccessManager { + public: + QmlNetworkAccessManager(QObject* parent) + : NetworkAccessManager(parent){}; + }; + QNetworkAccessManager* create(QObject* parent) override { return new QmlNetworkAccessManager(parent); } }; -QNetworkAccessManager* QmlNetworkAccessManagerFactory::create(QObject* parent) { - return new QmlNetworkAccessManager(parent); -} - QString getEventBridgeJavascript() { // FIXME: Refactor with similar code in RenderableWebEntityItem QString javaScriptToInject; QFile webChannelFile(":qtwebchannel/qwebchannel.js"); QFile createGlobalEventBridgeFile(PathUtils::resourcesPath() + "/html/createGlobalEventBridge.js"); - if (webChannelFile.open(QFile::ReadOnly | QFile::Text) && - createGlobalEventBridgeFile.open(QFile::ReadOnly | QFile::Text)) { + if (webChannelFile.open(QFile::ReadOnly | QFile::Text) && createGlobalEventBridgeFile.open(QFile::ReadOnly | QFile::Text)) { QString webChannelStr = QTextStream(&webChannelFile).readAll(); QString createGlobalEventBridgeStr = QTextStream(&createGlobalEventBridgeFile).readAll(); javaScriptToInject = webChannelStr + createGlobalEventBridgeStr; @@ -404,28 +156,82 @@ class EventBridgeWrapper : public QObject { Q_PROPERTY(QObject* eventBridge READ getEventBridge CONSTANT); public: - EventBridgeWrapper(QObject* eventBridge, QObject* parent = nullptr) : QObject(parent), _eventBridge(eventBridge) { - } + EventBridgeWrapper(QObject* eventBridge, QObject* parent = nullptr) + : QObject(parent) + , _eventBridge(eventBridge) {} - QObject* getEventBridge() { - return _eventBridge; - } + QObject* getEventBridge() { return _eventBridge; } private: QObject* _eventBridge; }; +}}} // namespace hifi::qml::offscreen +using namespace hifi::qml::offscreen; -#define SINGLE_QML_ENGINE 0 +AudioHandler::AudioHandler(OffscreenQmlSurface* surface, const QString& deviceName, QObject* parent) + : QObject(parent) { + setAutoDelete(true); + _newTargetDevice = deviceName; + auto rootItem = surface->getRootItem(); + if (rootItem) { + for (auto player : rootItem->findChildren()) { + _players.push_back(player); + } + } -#if SINGLE_QML_ENGINE -static QQmlEngine* globalEngine{ nullptr }; -static size_t globalEngineRefCount{ 0 }; -#endif + if (!_newTargetDevice.isEmpty() && !_players.empty()) { + QThreadPool::globalInstance()->start(this); + } else { + deleteLater(); + } +} -void initializeQmlEngine(QQmlEngine* engine, QQuickWindow* window) { - new QQmlFileSelector(engine, window); +void AudioHandler::run() { + for (auto player : _players) { + auto mediaState = player->state(); + QMediaService* svc = player->service(); + if (nullptr == svc) { + continue; + } + QAudioOutputSelectorControl* out = + qobject_cast(svc->requestControl(QAudioOutputSelectorControl_iid)); + if (nullptr == out) { + continue; + } + QString deviceOuput; + auto outputs = out->availableOutputs(); + for (int i = 0; i < outputs.size(); i++) { + QString output = outputs[i]; + QString description = out->outputDescription(output); + if (description == _newTargetDevice) { + deviceOuput = output; + break; + } + } + out->setActiveOutput(deviceOuput); + svc->releaseControl(out); + // if multimedia was paused, it will start playing automatically after changing audio device + // this will reset it back to a paused state + if (mediaState == QMediaPlayer::State::PausedState) { + player->pause(); + } else if (mediaState == QMediaPlayer::State::StoppedState) { + player->stop(); + } + } + qDebug() << "QML Audio changed to " << _newTargetDevice; +} + +void OffscreenQmlSurface::initializeEngine(QQmlEngine* engine) { + Parent::initializeEngine(engine); + + static std::once_flag once; + std::call_once(once, [] { + qRegisterMetaType(); + qRegisterMetaType(); + qmlRegisterType("Hifi", 1, 0, "SoundEffect"); + }); // register the pixmap image provider (used only for security image, for now) engine->addImageProvider(ImageProvider::PROVIDER_NAME, new ImageProvider()); @@ -438,13 +244,10 @@ void initializeQmlEngine(QQmlEngine* engine, QQuickWindow* window) { qDebug() << path; } - if (!engine->incubationController()) { - engine->setIncubationController(window->incubationController()); - } auto rootContext = engine->rootContext(); rootContext->setContextProperty("GL", ::getGLContextData()); rootContext->setContextProperty("urlHandler", new UrlHandler()); - rootContext->setContextProperty("resourceDirectoryUrl", QUrl(PathUtils::resourcesUrl())); + rootContext->setContextProperty("resourceDirectoryUrl", QUrl::fromLocalFile(PathUtils::resourcesPath())); rootContext->setContextProperty("pathToFonts", "../../"); rootContext->setContextProperty("ApplicationInterface", qApp); auto javaScriptToInject = getEventBridgeJavascript(); @@ -456,414 +259,27 @@ void initializeQmlEngine(QQmlEngine* engine, QQuickWindow* window) { rootContext->setContextProperty("HFWebEngineProfile", new HFWebEngineProfile(rootContext)); #endif rootContext->setContextProperty("Paths", DependencyManager::get().data()); - static std::once_flag once; - std::call_once(once, [&] { - qRegisterMetaType(); - qRegisterMetaType(); - }); rootContext->setContextProperty("Tablet", DependencyManager::get().data()); rootContext->setContextProperty("Toolbars", DependencyManager::get().data()); - TabletProxy* tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); + TabletProxy* tablet = + DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); engine->setObjectOwnership(tablet, QQmlEngine::CppOwnership); } -QQmlEngine* acquireEngine(QQuickWindow* window) { - Q_ASSERT(QThread::currentThread() == qApp->thread()); - - QQmlEngine* result = nullptr; - if (QThread::currentThread() != qApp->thread()) { - qCWarning(uiLogging) << "Cannot acquire QML engine on any thread but the main thread"; - } - static std::once_flag once; - std::call_once(once, [] { - qmlRegisterType("Hifi", 1, 0, "SoundEffect"); - }); - - -#if SINGLE_QML_ENGINE - if (!globalEngine) { - Q_ASSERT(0 == globalEngineRefCount); - globalEngine = new QQmlEngine(); - initializeQmlEngine(result, window); - ++globalEngineRefCount; - } - result = globalEngine; -#else - result = new QQmlEngine(); - initializeQmlEngine(result, window); -#endif - - return result; +void OffscreenQmlSurface::addWhitelistContextHandler(const std::initializer_list& urls, + const QmlContextCallback& callback) { + getQmlWhitelist()->addWhitelistContextHandler(urls, callback); } -void releaseEngine(QQmlEngine* engine) { - Q_ASSERT(QThread::currentThread() == qApp->thread()); -#if SINGLE_QML_ENGINE - Q_ASSERT(0 != globalEngineRefCount); - if (0 == --globalEngineRefCount) { - globalEngine->deleteLater(); - globalEngine = nullptr; - } -#else - engine->deleteLater(); -#endif -} - -#define OFFSCREEN_QML_SHARED_CONTEXT_PROPERTY "com.highfidelity.qml.gl.sharedContext" -void OffscreenQmlSurface::setSharedContext(QOpenGLContext* sharedContext) { - qApp->setProperty(OFFSCREEN_QML_SHARED_CONTEXT_PROPERTY, QVariant::fromValue(sharedContext)); -} - -QOpenGLContext* OffscreenQmlSurface::getSharedContext() { - return static_cast(qApp->property(OFFSCREEN_QML_SHARED_CONTEXT_PROPERTY).value()); -} - -void OffscreenQmlSurface::cleanup() { - _isCleaned = true; -#if !defined(DISABLE_QML) - _canvas->makeCurrent(); - - _renderControl->invalidate(); - delete _renderControl; // and invalidate - - if (_depthStencil) { - glDeleteRenderbuffers(1, &_depthStencil); - _depthStencil = 0; - } - if (_fbo) { - glDeleteFramebuffers(1, &_fbo); - _fbo = 0; - } - - offscreenTextures.releaseSize(_size); - - _canvas->doneCurrent(); -#endif -} - -void OffscreenQmlSurface::render() { -#if !defined(DISABLE_QML) - - if (nsightActive()) { - return; - } - if (_paused) { - return; - } - - PROFILE_RANGE(render_qml_gl, __FUNCTION__) - _canvas->makeCurrent(); - - _renderControl->sync(); - _quickWindow->setRenderTarget(_fbo, QSize(_size.x, _size.y)); - - GLuint texture = offscreenTextures.getNextTexture(_size); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); - glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - _renderControl->render(); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - glBindTexture(GL_TEXTURE_2D, texture); - glGenerateMipmap(GL_TEXTURE_2D); - glBindTexture(GL_TEXTURE_2D, 0); - - - { - // If the most recent texture was unused, we can directly recycle it - auto fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - // Fence will be used in another thread / context, so a flush is required - glFlush(); - - { - Lock lock(_latestTextureAndFenceMutex); - if (_latestTextureAndFence.first) { - offscreenTextures.releaseTexture(_latestTextureAndFence); - _latestTextureAndFence = { 0, 0 }; - } - _latestTextureAndFence = { texture, fence}; - } - } - - _quickWindow->resetOpenGLState(); - _lastRenderTime = usecTimestampNow(); - _canvas->doneCurrent(); -#endif -} - -bool OffscreenQmlSurface::fetchTexture(TextureAndFence& textureAndFence) { - textureAndFence = { 0, 0 }; - - // Lock free early check - if (0 == _latestTextureAndFence.first) { - return false; - } - - // Ensure writes to the latest texture are complete before before returning it for reading - { - Lock lock(_latestTextureAndFenceMutex); - // Double check inside the lock - if (0 == _latestTextureAndFence.first) { - return false; - } - textureAndFence = _latestTextureAndFence; - _latestTextureAndFence = { 0, 0 }; - } - return true; -} - -std::function OffscreenQmlSurface::getDiscardLambda() { - return [](uint32_t texture, void* fence) { - offscreenTextures.releaseTexture({ texture, static_cast(fence) }); - }; -} - -bool OffscreenQmlSurface::allowNewFrame(uint8_t fps) { - // If we already have a pending texture, don't render another one - // i.e. don't render faster than the consumer context, since it wastes - // GPU cycles on producing output that will never be seen - if (0 != _latestTextureAndFence.first) { - return false; - } - - auto minRenderInterval = USECS_PER_SECOND / fps; - auto lastInterval = usecTimestampNow() - _lastRenderTime; - return (lastInterval > minRenderInterval); -} - -OffscreenQmlSurface::OffscreenQmlSurface() { -} - -OffscreenQmlSurface::~OffscreenQmlSurface() { - QObject::disconnect(&_updateTimer); - disconnectAudioOutputTimer(); - QObject::disconnect(qApp); - - cleanup(); - auto engine = _qmlContext->engine(); -#if !defined(DISABLE_QML) - _canvas->deleteLater(); -#endif - _rootItem->deleteLater(); - _quickWindow->deleteLater(); - releaseEngine(engine); -} - -void OffscreenQmlSurface::onAboutToQuit() { - _paused = true; - QObject::disconnect(&_updateTimer); - disconnectAudioOutputTimer(); - -} - -void OffscreenQmlSurface::disconnectAudioOutputTimer() { -#if !defined(Q_OS_ANDROID) - if (_audioOutputUpdateTimer.isActive()) { - _audioOutputUpdateTimer.stop(); - } - QObject::disconnect(&_audioOutputUpdateTimer); -#endif -} - -void OffscreenQmlSurface::create() { - qCDebug(uiLogging) << "Building QML surface"; - -#if !defined(DISABLE_QML) - _renderControl = new QMyQuickRenderControl(); - connect(_renderControl, &QQuickRenderControl::renderRequested, this, [this] { _render = true; }); - connect(_renderControl, &QQuickRenderControl::sceneChanged, this, [this] { _render = _polish = true; }); - - 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->setClearBeforeRendering(false); - - _renderControl->_renderWindow = _proxyWindow; - _canvas = new OffscreenGLCanvas(); - if (!_canvas->create(getSharedContext())) { - qFatal("Failed to create OffscreenGLCanvas"); - return; - }; - - // acquireEngine interrogates the GL context, so we need to have the context current here - if (!_canvas->makeCurrent()) { - qFatal("Failed to make context current for QML Renderer"); - return; - } -#else - _quickWindow = new QQuickWindow(); -#endif - - - connect(_quickWindow, &QQuickWindow::focusObjectChanged, this, &OffscreenQmlSurface::onFocusObjectChanged); - - - // Create a QML engine. - auto qmlEngine = acquireEngine(_quickWindow); - - _qmlContext = new QQmlContext(qmlEngine->rootContext()); - _qmlContext->setBaseUrl(QUrl{ PathUtils::qmlBaseUrl() }); - _qmlContext->setContextProperty("offscreenWindow", QVariant::fromValue(getWindow())); - _qmlContext->setContextProperty("eventBridge", this); - _qmlContext->setContextProperty("webEntity", this); - _qmlContext->setContextProperty("QmlSurface", this); - +void OffscreenQmlSurface::onRootContextCreated(QQmlContext* qmlContext) { + OffscreenSurface::onRootContextCreated(qmlContext); + qmlContext->setBaseUrl(PathUtils::qmlBaseUrl()); + qmlContext->setContextProperty("eventBridge", this); + qmlContext->setContextProperty("webEntity", this); + qmlContext->setContextProperty("QmlSurface", this); // FIXME Compatibility mechanism for existing HTML and JS that uses eventBridgeWrapper // Find a way to flag older scripts using this mechanism and wanr that this is deprecated - _qmlContext->setContextProperty("eventBridgeWrapper", new EventBridgeWrapper(this, _qmlContext)); - -#if !defined(DISABLE_QML) - _renderControl->initialize(_canvas->getContext()); -#endif - -#if !defined(Q_OS_ANDROID) - // Connect with the audio client and listen for audio device changes - auto audioIO = DependencyManager::get(); - connect(audioIO.data(), &AudioClient::deviceChanged, this, [&](QAudio::Mode mode, const QAudioDeviceInfo& device) { - if (mode == QAudio::Mode::AudioOutput) { - QMetaObject::invokeMethod(this, "changeAudioOutputDevice", Qt::QueuedConnection, Q_ARG(QString, device.deviceName())); - } - }); - - // Setup the update of the QML media components with the current audio output device - QObject::connect(&_audioOutputUpdateTimer, &QTimer::timeout, this, [this]() { - if (_currentAudioOutputDevice.size() > 0) { - new AudioHandler(sharedFromThis(), _currentAudioOutputDevice); - } - }); - int waitForAudioQmlMs = 200; - _audioOutputUpdateTimer.setInterval(waitForAudioQmlMs); - _audioOutputUpdateTimer.setSingleShot(true); -#endif - - // 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. - QObject::connect(&_updateTimer, &QTimer::timeout, this, &OffscreenQmlSurface::updateQuick); - QObject::connect(qApp, &QCoreApplication::aboutToQuit, this, &OffscreenQmlSurface::onAboutToQuit); - _updateTimer.setTimerType(Qt::PreciseTimer); - _updateTimer.setInterval(MIN_TIMER_MS); // 5ms, Qt::PreciseTimer required - _updateTimer.start(); -} - -void OffscreenQmlSurface::changeAudioOutputDevice(const QString& deviceName, bool isHtmlUpdate) { -#if !defined(Q_OS_ANDROID) - _currentAudioOutputDevice = deviceName; - if (_rootItem != nullptr && !isHtmlUpdate) { - QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::QueuedConnection); - } - emit audioOutputDeviceChanged(deviceName); -#endif -} - -void OffscreenQmlSurface::forceHtmlAudioOutputDeviceUpdate() { -#if !defined(Q_OS_ANDROID) - if (_currentAudioOutputDevice.size() > 0) { - QMetaObject::invokeMethod(this, "changeAudioOutputDevice", Qt::QueuedConnection, - Q_ARG(QString, _currentAudioOutputDevice), Q_ARG(bool, true)); - } -#endif -} - -void OffscreenQmlSurface::forceQmlAudioOutputDeviceUpdate() { -#if !defined(Q_OS_ANDROID) - if (QThread::currentThread() != qApp->thread()) { - QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::QueuedConnection); - } else { - if (_audioOutputUpdateTimer.isActive()) { - _audioOutputUpdateTimer.stop(); - } - _audioOutputUpdateTimer.start(); - } -#endif -} - -static uvec2 clampSize(const uvec2& size, uint32_t maxDimension) { - return glm::clamp(size, glm::uvec2(1), glm::uvec2(maxDimension)); -} - -static QSize clampSize(const QSize& qsize, uint32_t maxDimension) { - return fromGlm(clampSize(toGlm(qsize), maxDimension)); -} - -void OffscreenQmlSurface::resize(const QSize& newSize_, bool forceResize) { - - if (!_quickWindow) { - return; - } - - const uint32_t MAX_OFFSCREEN_DIMENSION = 4096; - const QSize newSize = clampSize(newSize_, MAX_OFFSCREEN_DIMENSION); - if (!forceResize && newSize == _quickWindow->geometry().size()) { - return; - } - - _qmlContext->setContextProperty("surfaceSize", newSize); - - if (_rootItem) { - _rootItem->setSize(newSize); - } - - // Update our members - _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. - uvec2 newOffscreenSize = toGlm(newSize); - if (newOffscreenSize == _size) { - return; - } - -#if !defined(DISABLE_QML) - qCDebug(uiLogging) << "Offscreen UI resizing to " << newSize.width() << "x" << newSize.height(); - gl::withSavedContext([&] { - _canvas->makeCurrent(); - - // Release hold on the textures of the old size - if (uvec2() != _size) { - { - Lock lock(_latestTextureAndFenceMutex); - // If the most recent texture was unused, we can directly recycle it - if (_latestTextureAndFence.first) { - offscreenTextures.releaseTexture(_latestTextureAndFence); - _latestTextureAndFence = { 0, 0 }; - } - } - offscreenTextures.releaseSize(_size); - } - - _size = newOffscreenSize; - - // Acquire the new texture size - if (uvec2() != _size) { - offscreenTextures.acquireSize(_size); - if (_depthStencil) { - glDeleteRenderbuffers(1, &_depthStencil); - _depthStencil = 0; - } - glGenRenderbuffers(1, &_depthStencil); - glBindRenderbuffer(GL_RENDERBUFFER, _depthStencil); - glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, _size.x, _size.y); - if (!_fbo) { - glGenFramebuffers(1, &_fbo); - } - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); - glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthStencil); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - } - - _canvas->doneCurrent(); - }); -#endif -} - -QQuickItem* OffscreenQmlSurface::getRootItem() { - return _rootItem; + qmlContext->setContextProperty("eventBridgeWrapper", new EventBridgeWrapper(this, qmlContext)); } QQmlContext* OffscreenQmlSurface::contextForUrl(const QUrl& qmlSource, QQuickItem* parent, bool forceNewContext) { @@ -872,13 +288,7 @@ QQmlContext* OffscreenQmlSurface::contextForUrl(const QUrl& qmlSource, QQuickIte // If we have whitelisted content, we must load a new context forceNewContext |= !callbacks.empty(); - QQmlContext* targetContext = parent ? QQmlEngine::contextForObject(parent) : _qmlContext; - if (!targetContext) { - targetContext = _qmlContext; - } - if (_rootItem && forceNewContext) { - targetContext = new QQmlContext(targetContext); - } + QQmlContext* targetContext = Parent::contextForUrl(qmlSource, parent, forceNewContext); for (const auto& callback : callbacks) { callback(targetContext); @@ -887,171 +297,48 @@ QQmlContext* OffscreenQmlSurface::contextForUrl(const QUrl& qmlSource, QQuickIte return targetContext; } -void OffscreenQmlSurface::load(const QUrl& qmlSource, QQuickItem* parent, const QJSValue& callback) { - loadInternal(qmlSource, false, parent, [callback](QQmlContext* context, QQuickItem* newItem) { - QJSValue(callback).call(QJSValueList() << context->engine()->newQObject(newItem)); - }); -} - -void OffscreenQmlSurface::load(const QUrl& qmlSource, bool createNewContext, const QmlContextObjectCallback& onQmlLoadedCallback) { - loadInternal(qmlSource, createNewContext, nullptr, onQmlLoadedCallback); -} - -void OffscreenQmlSurface::loadInternal(const QUrl& qmlSource, bool createNewContext, QQuickItem* parent, const QmlContextObjectCallback& onQmlLoadedCallback) { - if (QThread::currentThread() != thread()) { - qCWarning(uiLogging) << "Called load on a non-surface thread"; - } - // Synchronous loading may take a while; restart the deadlock timer - QMetaObject::invokeMethod(qApp, "updateHeartbeat", Qt::DirectConnection); - - QUrl finalQmlSource = qmlSource; - if ((qmlSource.isRelative() && !qmlSource.isEmpty()) || qmlSource.scheme() == QLatin1String("file")) { - finalQmlSource = _qmlContext->resolvedUrl(qmlSource); - } - - auto targetContext = contextForUrl(finalQmlSource, parent, createNewContext); - auto qmlComponent = new QQmlComponent(_qmlContext->engine(), finalQmlSource, QQmlComponent::PreferSynchronous); - if (qmlComponent->isLoading()) { - connect(qmlComponent, &QQmlComponent::statusChanged, this, [=](QQmlComponent::Status) { - finishQmlLoad(qmlComponent, targetContext, parent, onQmlLoadedCallback); - }); - return; - } - - finishQmlLoad(qmlComponent, targetContext, parent, onQmlLoadedCallback); -} - -void OffscreenQmlSurface::loadInNewContext(const QUrl& qmlSource, const QmlContextObjectCallback& onQmlLoadedCallback) { - load(qmlSource, true, onQmlLoadedCallback); -} - -void OffscreenQmlSurface::load(const QUrl& qmlSource, const QmlContextObjectCallback& onQmlLoadedCallback) { - load(qmlSource, false, onQmlLoadedCallback); -} - -void OffscreenQmlSurface::load(const QString& qmlSourceFile, const QmlContextObjectCallback& onQmlLoadedCallback) { - return load(QUrl(qmlSourceFile), onQmlLoadedCallback); -} - -void OffscreenQmlSurface::clearCache() { - _qmlContext->engine()->clearComponentCache(); -} - - -void OffscreenQmlSurface::finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext* qmlContext, QQuickItem* parent, const QmlContextObjectCallback& callback) { - disconnect(qmlComponent, &QQmlComponent::statusChanged, this, 0); - if (qmlComponent->isError()) { - for (const auto& error : qmlComponent->errors()) { - qCWarning(uiLogging) << error.url() << error.line() << error; - } - qmlComponent->deleteLater(); - return; - } - - QObject* newObject = qmlComponent->beginCreate(qmlContext); - if (qmlComponent->isError()) { - for (const auto& error : qmlComponent->errors()) { - qCWarning(uiLogging) << error.url() << error.line() << error; - } - if (!_rootItem) { - qFatal("Unable to finish loading QML root"); - } - qmlComponent->deleteLater(); - return; - } - - if (!newObject) { - if (!_rootItem) { - qFatal("Could not load object as root item"); - return; - } - qCWarning(uiLogging) << "Unable to load QML item"; - return; - } - +void OffscreenQmlSurface::onItemCreated(QQmlContext* qmlContext, QQuickItem* newItem) { QObject* eventBridge = qmlContext->contextProperty("eventBridge").value(); - if (qmlContext != _qmlContext && eventBridge && eventBridge != this) { + if (qmlContext != getSurfaceContext() && eventBridge && eventBridge != this) { // FIXME Compatibility mechanism for existing HTML and JS that uses eventBridgeWrapper // Find a way to flag older scripts using this mechanism and wanr that this is deprecated qmlContext->setContextProperty("eventBridgeWrapper", new EventBridgeWrapper(eventBridge, qmlContext)); } - qmlContext->engine()->setObjectOwnership(this, QQmlEngine::CppOwnership); - - // 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); - } - - - // Make sure we will call callback for this codepath - // Call this before qmlComponent->completeCreate() otherwise ghost window appears - // If we already have a root, just set a couple of flags and the ancestry - if (_rootItem) { - callback(qmlContext, newItem); - - if (!parent) { - parent = _rootItem; - } - // Allow child windows to be destroyed from JS - QQmlEngine::setObjectOwnership(newObject, QQmlEngine::JavaScriptOwnership); - newObject->setParent(parent); - newItem->setParentItem(parent); - } - - qmlComponent->completeCreate(); - qmlComponent->deleteLater(); - - if (_rootItem) { - QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::QueuedConnection); - return; - } - connect(newItem, SIGNAL(sendToScript(QVariant)), this, SIGNAL(fromQml(QVariant))); +} - // The root item is ready. Associate it with the window. - _rootItem = newItem; - _rootItem->setParentItem(_quickWindow->contentItem()); - _rootItem->setSize(_quickWindow->renderTargetSize()); +void OffscreenQmlSurface::onRootCreated() { + getSurfaceContext()->setContextProperty("offscreenWindow", QVariant::fromValue(getWindow())); - if (_rootItem->objectName() == "tabletRoot") { - _qmlContext->setContextProperty("tabletRoot", QVariant::fromValue(_rootItem)); + // Connect with the audio client and listen for audio device changes + auto audioIO = DependencyManager::get(); + connect(audioIO.data(), &AudioClient::deviceChanged, this, [&](QAudio::Mode mode, const QAudioDeviceInfo& device) { + if (mode == QAudio::Mode::AudioOutput) { + QMetaObject::invokeMethod(this, "changeAudioOutputDevice", Qt::QueuedConnection, Q_ARG(QString, device.deviceName())); + } + }); + +#if !defined(Q_OS_ANDROID) + // Setup the update of the QML media components with the current audio output device + QObject::connect(&_audioOutputUpdateTimer, &QTimer::timeout, this, [this]() { + if (_currentAudioOutputDevice.size() > 0) { + new AudioHandler(this, _currentAudioOutputDevice); + } + }); + int waitForAudioQmlMs = 200; + _audioOutputUpdateTimer.setInterval(waitForAudioQmlMs); + _audioOutputUpdateTimer.setSingleShot(true); +#endif + + if (getRootItem()->objectName() == "tabletRoot") { + getSurfaceContext()->setContextProperty("tabletRoot", QVariant::fromValue(getRootItem())); auto tabletScriptingInterface = DependencyManager::get(); tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", this); QObject* tablet = tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"); - _qmlContext->engine()->setObjectOwnership(tablet, QQmlEngine::CppOwnership); + getSurfaceContext()->engine()->setObjectOwnership(tablet, QQmlEngine::CppOwnership); } QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::QueuedConnection); - // Call this callback after rootitem is set, otherwise VrMenu wont work - callback(qmlContext, newItem); -} - -void OffscreenQmlSurface::updateQuick() { - offscreenTextures.report(); - // If we're - // a) not set up - // b) already rendering a frame - // c) rendering too fast - // then skip this - if (!allowNewFrame(_maxFps)) { - return; - } - - if (_polish) { - PROFILE_RANGE(render_qml, "OffscreenQML polish") -#if !defined(DISABLE_QML) - _renderControl->polishItems(); -#endif - _polish = false; - } - - if (_render) { - render(); - _render = false; - } } QPointF OffscreenQmlSurface::mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject) { @@ -1063,106 +350,37 @@ QPointF OffscreenQmlSurface::mapWindowToUi(const QPointF& sourcePosition, QObjec } vec2 offscreenPosition = toGlm(sourcePosition); offscreenPosition /= sourceSize; - offscreenPosition *= vec2(toGlm(_quickWindow->size())); + offscreenPosition *= vec2(toGlm(getWindow()->size())); return QPointF(offscreenPosition.x, offscreenPosition.y); } -QPointF OffscreenQmlSurface::mapToVirtualScreen(const QPointF& originalPoint) { - return _mouseTranslator(originalPoint); -} - /////////////////////////////////////////////////////// // // Event handling customization // -bool OffscreenQmlSurface::filterEnabled(QObject* originalDestination, QEvent* event) const { - if (_quickWindow == originalDestination) { - return false; - } - // Only intercept events while we're in an active state - if (_paused) { - return false; - } - return true; -} - bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* event) { if (!filterEnabled(originalDestination, event)) { 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); - QWidget* widget = static_cast(originalDestination); - if (widget) { - this->resize(resizeEvent->size()); - } - break; + if (event->type() == QEvent::Resize) { + QResizeEvent* resizeEvent = static_cast(event); + QWidget* widget = static_cast(originalDestination); + if (widget) { + this->resize(resizeEvent->size()); } - - 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); - QPointF transformedPos = mapToVirtualScreen(wheelEvent->pos()); - QWheelEvent mappedEvent( - transformedPos, - wheelEvent->delta(), wheelEvent->buttons(), - wheelEvent->modifiers(), wheelEvent->orientation()); - mappedEvent.ignore(); - if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) { - return mappedEvent.isAccepted(); - } - break; - } -#if defined(Q_OS_ANDROID) - case QEvent::TouchBegin: - case QEvent::TouchUpdate: - case QEvent::TouchEnd: { - QTouchEvent *originalEvent = static_cast(event); - QTouchEvent *fakeEvent = new QTouchEvent(*originalEvent); - auto newTouchPoints = fakeEvent->touchPoints(); - for (size_t i = 0; i < newTouchPoints.size(); ++i) { - const auto &originalPoint = originalEvent->touchPoints()[i]; - auto &newPoint = newTouchPoints[i]; - newPoint.setPos(originalPoint.pos()); - } - fakeEvent->setTouchPoints(newTouchPoints); - if (QCoreApplication::sendEvent(_quickWindow, fakeEvent)) { - qInfo() << __FUNCTION__ << "sent fake touch event:" << fakeEvent->type() - << "_quickWindow handled it... accepted:" << fakeEvent->isAccepted(); - return false; //event->isAccepted(); - } - break; - } -#endif - default: - break; } - return false; + return Parent::eventFilter(originalDestination, event); } unsigned int OffscreenQmlSurface::deviceIdByTouchPoint(qreal x, qreal y) { - auto mapped = _rootItem->mapFromGlobal(QPoint(x, y)); + if (!getRootItem()) { + return PointerEvent::INVALID_POINTER_ID; + } + auto mapped = getRootItem()->mapFromGlobal(QPoint(x, y)); for (auto pair : _activeTouchPoints) { if (mapped.x() == (int)pair.second.touchPoint.pos().x() && mapped.y() == (int)pair.second.touchPoint.pos().y()) { return pair.first; @@ -1207,7 +425,7 @@ void OffscreenQmlSurface::hoverEndEvent(const PointerEvent& event, class QTouchD bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QTouchDevice& device, bool release) { // Ignore mouse interaction if we're paused - if (_paused || !_quickWindow) { + if (!getRootItem() || isPaused()) { return false; } @@ -1226,7 +444,8 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QT // - this was a hover end event and the mouse wasn't pressed // - this was a release event and we aren't still hovering auto touchPoint = _activeTouchPoints.find(event.getID()); - bool removeTouchPoint = release || (touchPoint != _activeTouchPoints.end() && !touchPoint->second.hovering && state == Qt::TouchPointReleased); + bool removeTouchPoint = + release || (touchPoint != _activeTouchPoints.end() && !touchPoint->second.hovering && state == Qt::TouchPointReleased); QEvent::Type touchType = QEvent::TouchUpdate; if (_activeTouchPoints.empty()) { // If the first active touch point is being created, send a begin @@ -1259,9 +478,9 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QT touchPoints.push_back(entry.second.touchPoint); } - touchEvent.setWindow(_quickWindow); touchEvent.setDevice(&device); - touchEvent.setTarget(_rootItem); + touchEvent.setWindow(getWindow()); + touchEvent.setTarget(getRootItem()); touchEvent.setTouchPoints(touchPoints); touchEvent.setTouchPointStates(touchPointStates); touchEvent.setTimestamp((ulong)QDateTime::currentMSecsSinceEpoch()); @@ -1285,25 +504,26 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QT bool eventsAccepted = true; if (event.getType() == PointerEvent::Move) { - QMouseEvent mouseEvent(QEvent::MouseMove, windowPoint, windowPoint, windowPoint, button, buttons, event.getKeyboardModifiers()); + QMouseEvent mouseEvent(QEvent::MouseMove, windowPoint, windowPoint, windowPoint, button, buttons, + event.getKeyboardModifiers()); // TODO - this line necessary for the QML Tooltop to work (which is not currently being used), but it causes interface to crash on launch on a fresh install // need to investigate into why this crash is happening. //_qmlContext->setContextProperty("lastMousePosition", windowPoint); mouseEvent.ignore(); - if (QCoreApplication::sendEvent(_quickWindow, &mouseEvent)) { + if (QCoreApplication::sendEvent(getWindow(), &mouseEvent)) { eventSent = true; eventsAccepted &= mouseEvent.isAccepted(); } } if (touchType == QEvent::TouchBegin) { - _touchBeginAccepted = QCoreApplication::sendEvent(_quickWindow, &touchEvent); + _touchBeginAccepted = QCoreApplication::sendEvent(getWindow(), &touchEvent); if (_touchBeginAccepted) { eventSent = true; eventsAccepted &= touchEvent.isAccepted(); } } else if (_touchBeginAccepted) { - if (QCoreApplication::sendEvent(_quickWindow, &touchEvent)) { + if (QCoreApplication::sendEvent(getWindow(), &touchEvent)) { eventSent = true; eventsAccepted &= touchEvent.isAccepted(); } @@ -1316,49 +536,10 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QT return eventSent && eventsAccepted; } -void OffscreenQmlSurface::pause() { - _paused = true; -} - -void OffscreenQmlSurface::resume() { - _paused = false; - _render = true; - - if (getRootItem()) { - getRootItem()->setProperty("eventBridge", QVariant::fromValue(this)); +void OffscreenQmlSurface::focusDestroyed(QObject* obj) { + if (_currentFocusItem) { + disconnect(_currentFocusItem, &QObject::destroyed, this, &OffscreenQmlSurface::focusDestroyed); } -} - -bool OffscreenQmlSurface::isPaused() const { - return _paused; -} - -void OffscreenQmlSurface::setProxyWindow(QWindow* window) { - _proxyWindow = window; -#if !defined(DISABLE_QML) - if (_renderControl) { - _renderControl->_renderWindow = window; - } -#endif -} - -QObject* OffscreenQmlSurface::getEventHandler() { - return getWindow(); -} - -QQuickWindow* OffscreenQmlSurface::getWindow() { - return _quickWindow; -} - -QSize OffscreenQmlSurface::size() const { - return _quickWindow->geometry().size(); -} - -QQmlContext* OffscreenQmlSurface::getSurfaceContext() { - return _qmlContext; -} - -void OffscreenQmlSurface::focusDestroyed(QObject *obj) { _currentFocusItem = nullptr; } @@ -1454,8 +635,7 @@ void OffscreenQmlSurface::synthesizeKeyPress(QString key, QObject* targetOverrid } void OffscreenQmlSurface::lowerKeyboard() { - - QSignalBlocker blocker(_quickWindow); + QSignalBlocker blocker(getWindow()); if (_currentFocusItem) { _currentFocusItem->setFocus(false); @@ -1464,7 +644,8 @@ void OffscreenQmlSurface::lowerKeyboard() { } void OffscreenQmlSurface::setKeyboardRaised(QObject* object, bool raised, bool numeric, bool passwordField) { - qCDebug(uiLogging) << "setKeyboardRaised: " << object << ", raised: " << raised << ", numeric: " << numeric << ", password: " << passwordField; + qCDebug(uiLogging) << "setKeyboardRaised: " << object << ", raised: " << raised << ", numeric: " << numeric + << ", password: " << passwordField; if (!object) { return; @@ -1488,7 +669,6 @@ void OffscreenQmlSurface::setKeyboardRaised(QObject* object, bool raised, bool n numeric = numeric || QString(item->metaObject()->className()).left(7) == "SpinBox"; if (item->property("keyboardRaised").isValid()) { - // FIXME - HMD only: Possibly set value of "keyboardEnabled" per isHMDMode() for use in WebView.qml. if (item->property("punctuationMode").isValid()) { item->setProperty("punctuationMode", QVariant(numeric)); @@ -1544,11 +724,42 @@ void OffscreenQmlSurface::emitWebEvent(const QVariant& message) { void OffscreenQmlSurface::sendToQml(const QVariant& message) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "emitQmlEvent", Qt::QueuedConnection, Q_ARG(QVariant, message)); - } else if (_rootItem) { + } else if (getRootItem()) { // call fromScript method on qml root - QMetaObject::invokeMethod(_rootItem, "fromScript", Qt::QueuedConnection, Q_ARG(QVariant, message)); + QMetaObject::invokeMethod(getRootItem(), "fromScript", Qt::QueuedConnection, Q_ARG(QVariant, message)); } } +void OffscreenQmlSurface::changeAudioOutputDevice(const QString& deviceName, bool isHtmlUpdate) { +#if !defined(Q_OS_ANDROID) + _currentAudioOutputDevice = deviceName; + if (getRootItem() && !isHtmlUpdate) { + QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::QueuedConnection); + } + emit audioOutputDeviceChanged(deviceName); +#endif +} + +void OffscreenQmlSurface::forceHtmlAudioOutputDeviceUpdate() { +#if !defined(Q_OS_ANDROID) + if (_currentAudioOutputDevice.size() > 0) { + QMetaObject::invokeMethod(this, "changeAudioOutputDevice", Qt::QueuedConnection, + Q_ARG(QString, _currentAudioOutputDevice), Q_ARG(bool, true)); + } +#endif +} + +void OffscreenQmlSurface::forceQmlAudioOutputDeviceUpdate() { +#if !defined(Q_OS_ANDROID) + if (QThread::currentThread() != qApp->thread()) { + QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::QueuedConnection); + } else { + if (_audioOutputUpdateTimer.isActive()) { + _audioOutputUpdateTimer.stop(); + } + _audioOutputUpdateTimer.start(); + } +#endif +} #include "OffscreenQmlSurface.moc" diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.h b/libraries/ui/src/ui/OffscreenQmlSurface.h index 08c7ca6bf8..9fa86f12a3 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.h +++ b/libraries/ui/src/ui/OffscreenQmlSurface.h @@ -9,200 +9,79 @@ #ifndef hifi_OffscreenQmlSurface_h #define hifi_OffscreenQmlSurface_h -#include -#include -#include -#include - -#include -#include -#include - -#include -#include +#include +#include #include #include "PointerEvent.h" -class QWindow; -class QMyQuickRenderControl; -class OffscreenGLCanvas; -class QOpenGLContext; -class QQmlEngine; -class QQmlContext; -class QQmlComponent; -class QQuickWindow; -class QQuickItem; -class QJSValue; - -// GPU resources are typically buffered for one copy being used by the renderer, -// one copy in flight, and one copy being used by the receiver -#define GPU_RESOURCE_BUFFER_SIZE 3 - using QmlContextCallback = std::function; -using QmlContextObjectCallback = std::function; -class OffscreenQmlSurface : public QObject, public QEnableSharedFromThis { +class OffscreenQmlSurface : public hifi::qml::OffscreenSurface { + using Parent = hifi::qml::OffscreenSurface; Q_OBJECT Q_PROPERTY(bool focusText READ isFocusText NOTIFY focusTextChanged) public: - static void setSharedContext(QOpenGLContext* context); - - static QmlContextObjectCallback DEFAULT_CONTEXT_CALLBACK; + static void addWhitelistContextHandler(const std::initializer_list& urls, const QmlContextCallback& callback); static void addWhitelistContextHandler(const QUrl& url, const QmlContextCallback& callback) { addWhitelistContextHandler({ { url } }, callback); }; - OffscreenQmlSurface(); - virtual ~OffscreenQmlSurface(); - - using MouseTranslator = std::function; - - virtual void create(); - void resize(const QSize& size, bool forceResize = false); - QSize size() const; - - // Usable from QML code as QmlSurface.load(url, parent, function(newItem){ ... }) - Q_INVOKABLE void load(const QUrl& qmlSource, QQuickItem* parent, const QJSValue& callback); - - // For C++ use - Q_INVOKABLE void load(const QUrl& qmlSource, bool createNewContext, const QmlContextObjectCallback& onQmlLoadedCallback = DEFAULT_CONTEXT_CALLBACK); - Q_INVOKABLE void loadInNewContext(const QUrl& qmlSource, const QmlContextObjectCallback& onQmlLoadedCallback = DEFAULT_CONTEXT_CALLBACK); - Q_INVOKABLE void load(const QUrl& qmlSource, const QmlContextObjectCallback& onQmlLoadedCallback = DEFAULT_CONTEXT_CALLBACK); - Q_INVOKABLE void load(const QString& qmlSourceFile, const QmlContextObjectCallback& onQmlLoadedCallback = DEFAULT_CONTEXT_CALLBACK); - - void clearCache(); - void setMaxFps(uint8_t maxFps) { _maxFps = maxFps; } - // Optional values for event handling - void setProxyWindow(QWindow* window); - void setMouseTranslator(MouseTranslator mouseTranslator) { - _mouseTranslator = mouseTranslator; - } - bool isFocusText() const { return _focusText; } - void pause(); - void resume(); - bool isPaused() const; bool getCleaned() { return _isCleaned; } - QQuickItem* getRootItem(); - QQuickWindow* getWindow(); - QObject* getEventHandler(); - QQmlContext* getSurfaceContext(); - - QPointF mapToVirtualScreen(const QPointF& originalPoint); bool eventFilter(QObject* originalDestination, QEvent* event) override; - void setKeyboardRaised(QObject* object, bool raised, bool numeric = false, bool passwordField = false); Q_INVOKABLE void synthesizeKeyPress(QString key, QObject* targetOverride = nullptr); Q_INVOKABLE void lowerKeyboard(); - - using TextureAndFence = std::pair; - // Checks to see if a new texture is available. If one is, the function returns true and - // textureAndFence will be populated with the texture ID and a fence which will be signalled - // when the texture is safe to read. - // Returns false if no new texture is available - bool fetchTexture(TextureAndFence& textureAndFence); - - static std::function getDiscardLambda(); - static size_t getUsedTextureMemory(); - PointerEvent::EventType choosePointerEventType(QEvent::Type type); - unsigned int deviceIdByTouchPoint(qreal x, qreal y); signals: void focusObjectChanged(QObject* newFocus); void focusTextChanged(bool focusText); - -public slots: - void onAboutToQuit(); - void focusDestroyed(QObject *obj); - - // audio output device -public slots: - void changeAudioOutputDevice(const QString& deviceName, bool isHtmlUpdate = false); - void forceHtmlAudioOutputDeviceUpdate(); - void forceQmlAudioOutputDeviceUpdate(); - -signals: void audioOutputDeviceChanged(const QString& deviceName); - // event bridge -public slots: - void emitScriptEvent(const QVariant& scriptMessage); - void emitWebEvent(const QVariant& webMessage); -signals: - void scriptEventReceived(const QVariant& message); + // web event bridge void webEventReceived(const QVariant& message); - + // script event bridge + void scriptEventReceived(const QVariant& message); // qml event bridge + void fromQml(const QVariant& message); + public slots: + void focusDestroyed(QObject *obj); + // script event bridge + void emitScriptEvent(const QVariant& scriptMessage); + // web event bridge + void emitWebEvent(const QVariant& webMessage); + // qml event bridge void sendToQml(const QVariant& message); -signals: - void fromQml(QVariant message); protected: - bool filterEnabled(QObject* originalDestination, QEvent* event) const; void setFocusText(bool newFocusText); + void initializeEngine(QQmlEngine* engine) override; + void onRootContextCreated(QQmlContext* qmlContext) override; private: - static QOpenGLContext* getSharedContext(); - - QQmlContext* contextForUrl(const QUrl& url, QQuickItem* parent, bool forceNewContext = false); - void loadInternal(const QUrl& qmlSource, bool createNewContext, QQuickItem* parent, const QmlContextObjectCallback& onQmlLoadedCallback); - void finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext* qmlContext, QQuickItem* parent, const QmlContextObjectCallback& onQmlLoadedCallback); + void onRootCreated() override; + void onItemCreated(QQmlContext* qmlContext, QQuickItem* newItem) override; + QQmlContext* contextForUrl(const QUrl& url, QQuickItem* parent, bool forceNewContext = false) override; QPointF mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject); - bool allowNewFrame(uint8_t fps); - void render(); - void cleanup(); - void disconnectAudioOutputTimer(); private slots: - void updateQuick(); - void onFocusObjectChanged(QObject* newFocus); + void onFocusObjectChanged(QObject* newFocus) override; public slots: void hoverBeginEvent(const PointerEvent& event, class QTouchDevice& device); void hoverEndEvent(const PointerEvent& event, class QTouchDevice& device); bool handlePointerEvent(const PointerEvent& event, class QTouchDevice& device, bool release = false); + void changeAudioOutputDevice(const QString& deviceName, bool isHtmlUpdate = false); + void forceHtmlAudioOutputDeviceUpdate(); + void forceQmlAudioOutputDeviceUpdate(); private: - using Mutex = std::mutex; - using Lock = std::unique_lock; - - QQuickWindow* _quickWindow { nullptr }; - QQmlContext* _qmlContext { nullptr }; - QQuickItem* _rootItem { nullptr }; - -#if !defined(DISABLE_QML) - QMyQuickRenderControl* _renderControl{ nullptr }; - OffscreenGLCanvas* _canvas { nullptr }; -#endif - - QTimer _updateTimer; - uint32_t _fbo { 0 }; - uint32_t _depthStencil { 0 }; - uint64_t _lastRenderTime { 0 }; - uvec2 _size; - -#if !defined(Q_OS_ANDROID) - QTimer _audioOutputUpdateTimer; - QString _currentAudioOutputDevice; -#endif - - // Texture management - Mutex _latestTextureAndFenceMutex; - TextureAndFence _latestTextureAndFence { 0, 0 }; - - bool _render { false }; - bool _polish { true }; - bool _paused { true }; bool _focusText { false }; bool _isCleaned{ false }; - uint8_t _maxFps { 60 }; - MouseTranslator _mouseTranslator { [](const QPointF& p) { return p.toPoint(); } }; - QWindow* _proxyWindow { nullptr }; - QQuickItem* _currentFocusItem { nullptr }; struct TouchState { @@ -214,6 +93,9 @@ private: bool _pressed; bool _touchBeginAccepted { false }; std::map _activeTouchPoints; + + QString _currentAudioOutputDevice; + QTimer _audioOutputUpdateTimer; }; #endif diff --git a/libraries/ui/src/ui/OffscreenQmlSurfaceCache.cpp b/libraries/ui/src/ui/OffscreenQmlSurfaceCache.cpp index 9b6b031dd9..7efa36624b 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurfaceCache.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurfaceCache.cpp @@ -44,7 +44,6 @@ void OffscreenQmlSurfaceCache::release(const QString& rootSource, const QSharedP QSharedPointer OffscreenQmlSurfaceCache::buildSurface(const QString& rootSource) { auto surface = QSharedPointer(new OffscreenQmlSurface()); - surface->create(); surface->load(rootSource); surface->resize(QSize(100, 100)); return surface; diff --git a/plugins/hifiNeuron/CMakeLists.txt b/plugins/hifiNeuron/CMakeLists.txt index dd78b52240..7d2616e4d6 100644 --- a/plugins/hifiNeuron/CMakeLists.txt +++ b/plugins/hifiNeuron/CMakeLists.txt @@ -10,7 +10,7 @@ if (APPLE OR WIN32) set(TARGET_NAME hifiNeuron) setup_hifi_plugin(Qml) - link_hifi_libraries(shared controllers ui plugins input-plugins) + link_hifi_libraries(shared controllers qml ui plugins input-plugins) target_neuron() endif() diff --git a/plugins/oculus/CMakeLists.txt b/plugins/oculus/CMakeLists.txt index 59986d103e..2a011a349a 100644 --- a/plugins/oculus/CMakeLists.txt +++ b/plugins/oculus/CMakeLists.txt @@ -19,7 +19,7 @@ if (WIN32 AND (NOT USE_GLES)) set(TARGET_NAME oculus) setup_hifi_plugin(Multimedia) link_hifi_libraries( - shared gl gpu controllers ui + shared gl gpu gpu-gl controllers ui qml plugins ui-plugins display-plugins input-plugins audio-client networking render-utils ${PLATFORM_GL_BACKEND} diff --git a/plugins/openvr/CMakeLists.txt b/plugins/openvr/CMakeLists.txt index 487a005068..a6e90aa5a6 100644 --- a/plugins/openvr/CMakeLists.txt +++ b/plugins/openvr/CMakeLists.txt @@ -11,7 +11,7 @@ if (WIN32 AND (NOT USE_GLES)) add_definitions(-DGLEW_STATIC) set(TARGET_NAME openvr) setup_hifi_plugin(Gui Qml Multimedia) - link_hifi_libraries(shared gl networking controllers ui + link_hifi_libraries(shared gl qml networking controllers ui plugins display-plugins ui-plugins input-plugins script-engine audio-client render-utils graphics gpu render model-networking fbx ktx image procedural ${PLATFORM_GL_BACKEND}) diff --git a/tests/qml/CMakeLists.txt b/tests/qml/CMakeLists.txt new file mode 100644 index 0000000000..f56b8c4533 --- /dev/null +++ b/tests/qml/CMakeLists.txt @@ -0,0 +1,13 @@ +set(TARGET_NAME qml-test) +setup_hifi_project(Quick Qml Gui OpenGL) +setup_memory_debugger() +set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") +link_hifi_libraries(shared networking gl qml) + +if (WIN32) + set(EXTRA_DEPLOY_OPTIONS "--qmldir \"${PROJECT_SOURCE_DIR}/qml\"") + + package_libraries_for_deployment() +endif() + +target_nsight() diff --git a/tests/qml/qml/main.qml b/tests/qml/qml/main.qml new file mode 100644 index 0000000000..3b37543393 --- /dev/null +++ b/tests/qml/qml/main.qml @@ -0,0 +1,100 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.2 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.1 +import QtQuick.Controls 1.2 +import "qml/UI.js" as UI +import "qml" + +Item { + width: 640 + height: 480 + + Rectangle { + width: 5 + height: 5 + color: "red" + ColorAnimation on color { loops: Animation.Infinite; from: "red"; to: "yellow"; duration: 1000 } + } + + TabView { + id: tabView + + anchors.fill: parent + anchors.margins: UI.margin + tabPosition: UI.tabPosition + + Layout.minimumWidth: 360 + Layout.minimumHeight: 360 + Layout.preferredWidth: 480 + Layout.preferredHeight: 640 + + Tab { + title: "Buttons" + ButtonPage { + enabled: enabler.checked + } + } + Tab { + title: "Progress" + ProgressPage { + enabled: enabler.checked + } + } + Tab { + title: "Input" + InputPage { + enabled: enabler.checked + } + } + } +} diff --git a/tests/qml/qml/qml/+android/UI.js b/tests/qml/qml/qml/+android/UI.js new file mode 100644 index 0000000000..be7ddfa967 --- /dev/null +++ b/tests/qml/qml/qml/+android/UI.js @@ -0,0 +1,55 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +.pragma library + +var margin = 0 +var tabPosition = Qt.BottomEdge +var label = "Gallery" diff --git a/tests/qml/qml/qml/+ios/UI.js b/tests/qml/qml/qml/+ios/UI.js new file mode 100644 index 0000000000..be7ddfa967 --- /dev/null +++ b/tests/qml/qml/qml/+ios/UI.js @@ -0,0 +1,55 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +.pragma library + +var margin = 0 +var tabPosition = Qt.BottomEdge +var label = "Gallery" diff --git a/tests/qml/qml/qml/+osx/UI.js b/tests/qml/qml/qml/+osx/UI.js new file mode 100644 index 0000000000..28b13b64aa --- /dev/null +++ b/tests/qml/qml/qml/+osx/UI.js @@ -0,0 +1,55 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +.pragma library + +var margin = 12 +var tabPosition = Qt.TopEdge +var label = "" diff --git a/tests/qml/qml/qml/ButtonPage.qml b/tests/qml/qml/qml/ButtonPage.qml new file mode 100644 index 0000000000..8eb9c1a730 --- /dev/null +++ b/tests/qml/qml/qml/ButtonPage.qml @@ -0,0 +1,140 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.2 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 1.2 + +ScrollView { + id: page + implicitWidth: 640 + implicitHeight: 200 + + horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff + + Item { + id: content + + width: Math.max(page.viewport.width, grid.implicitWidth + 2 * grid.rowSpacing) + height: Math.max(page.viewport.height, grid.implicitHeight + 2 * grid.columnSpacing) + + GridLayout { + id: grid + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: grid.rowSpacing + anchors.rightMargin: grid.rowSpacing + anchors.topMargin: grid.columnSpacing + + columns: page.width < page.height ? 1 : 2 + + GroupBox { + title: "Button" + Layout.fillWidth: true + Layout.columnSpan: grid.columns + RowLayout { + anchors.fill: parent + Button { text: "OK"; isDefault: true } + Button { text: "Cancel" } + Item { Layout.fillWidth: true } + Button { + text: "Attach" + menu: Menu { + MenuItem { text: "Image" } + MenuItem { text: "Document" } + } + } + } + } + + GroupBox { + title: "CheckBox" + Layout.fillWidth: true + ColumnLayout { + anchors.fill: parent + CheckBox { text: "E-mail"; checked: true } + CheckBox { text: "Calendar"; checked: true } + CheckBox { text: "Contacts" } + } + } + + GroupBox { + title: "RadioButton" + Layout.fillWidth: true + ColumnLayout { + anchors.fill: parent + ExclusiveGroup { id: radioGroup } + RadioButton { text: "Portrait"; exclusiveGroup: radioGroup } + RadioButton { text: "Landscape"; exclusiveGroup: radioGroup } + RadioButton { text: "Automatic"; exclusiveGroup: radioGroup; checked: true } + } + } + + GroupBox { + title: "Switch" + Layout.fillWidth: true + Layout.columnSpan: grid.columns + ColumnLayout { + anchors.fill: parent + RowLayout { + Label { text: "Wi-Fi"; Layout.fillWidth: true } + Switch { checked: true } + } + RowLayout { + Label { text: "Bluetooth"; Layout.fillWidth: true } + Switch { checked: false } + } + } + } + } + } +} diff --git a/tests/qml/qml/qml/InputPage.qml b/tests/qml/qml/qml/InputPage.qml new file mode 100644 index 0000000000..78ef0a64d6 --- /dev/null +++ b/tests/qml/qml/qml/InputPage.qml @@ -0,0 +1,126 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.2 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 1.2 + +ScrollView { + id: page + implicitWidth: 640 + implicitHeight: 400 + + horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff + + Item { + id: content + + width: Math.max(page.viewport.width, column.implicitWidth + 2 * column.spacing) + height: Math.max(page.viewport.height, column.implicitHeight + 2 * column.spacing) + + ColumnLayout { + id: column + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: column.spacing + + GroupBox { + title: "TextField" + Layout.fillWidth: true + ColumnLayout { + anchors.fill: parent + TextField { placeholderText: "..."; Layout.fillWidth: true; z: 1 } + TextField { placeholderText: "Password"; echoMode: TextInput.Password; Layout.fillWidth: true } + } + } + + GroupBox { + title: "ComboBox" + Layout.fillWidth: true + ColumnLayout { + anchors.fill: parent + ComboBox { + model: Qt.fontFamilies() + Layout.fillWidth: true + } + ComboBox { + editable: true + model: ListModel { + id: listModel + ListElement { text: "Apple" } + ListElement { text: "Banana" } + ListElement { text: "Coconut" } + ListElement { text: "Orange" } + } + onAccepted: { + if (find(currentText) === -1) { + listModel.append({text: editText}) + currentIndex = find(editText) + } + } + Layout.fillWidth: true + } + } + } + + GroupBox { + title: "SpinBox" + Layout.fillWidth: true + ColumnLayout { + anchors.fill: parent + SpinBox { value: 99; Layout.fillWidth: true; z: 1 } + SpinBox { decimals: 2; Layout.fillWidth: true } + } + } + } + } +} diff --git a/tests/qml/qml/qml/ProgressPage.qml b/tests/qml/qml/qml/ProgressPage.qml new file mode 100644 index 0000000000..5123d101ff --- /dev/null +++ b/tests/qml/qml/qml/ProgressPage.qml @@ -0,0 +1,102 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.2 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 1.2 + +ScrollView { + id: page + implicitWidth: 640 + implicitHeight: 400 + + horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff + + Item { + id: content + + width: Math.max(page.viewport.width, column.implicitWidth + 2 * column.spacing) + height: Math.max(page.viewport.height, column.implicitHeight + 2 * column.spacing) + + ColumnLayout { + id: column + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: column.spacing + + GroupBox { + title: "ProgressBar" + Layout.fillWidth: true + ColumnLayout { + anchors.fill: parent + ProgressBar { indeterminate: true; Layout.fillWidth: true } + ProgressBar { value: slider.value; Layout.fillWidth: true } + } + } + + GroupBox { + title: "Slider" + Layout.fillWidth: true + ColumnLayout { + anchors.fill: parent + Slider { id: slider; value: 0.5; Layout.fillWidth: true } + } + } + + GroupBox { + title: "BusyIndicator" + Layout.fillWidth: true + BusyIndicator { running: true } + } + } + } +} diff --git a/tests/qml/qml/qml/UI.js b/tests/qml/qml/qml/UI.js new file mode 100644 index 0000000000..3899ced027 --- /dev/null +++ b/tests/qml/qml/qml/UI.js @@ -0,0 +1,55 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +.pragma library + +var margin = 2 +var tabPosition = Qt.TopEdge +var label = "" diff --git a/tests/qml/src/main.cpp b/tests/qml/src/main.cpp new file mode 100644 index 0000000000..219efa5996 --- /dev/null +++ b/tests/qml/src/main.cpp @@ -0,0 +1,198 @@ +// +// main.cpp +// tests/gpu-test/src +// +// 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 +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + + +class OffscreenQmlSurface : public hifi::qml::OffscreenSurface { + +}; + +class TestWindow : public QWindow { + +public: + TestWindow(); + + +private: + using TextureAndFence = hifi::qml::OffscreenSurface::TextureAndFence; + QOpenGLContext _glContext; + OffscreenGLCanvas _sharedContext; + OffscreenQmlSurface _offscreenQml; + QOpenGLFunctions_4_5_Core _glf; + uint32_t _currentTexture{ 0 }; + GLsync _readFence{ 0 }; + std::function _discardLamdba; + QSize _size; + GLuint _fbo{ 0 }; + const QSize _qmlSize{ 640, 480 }; + bool _aboutToQuit{ false }; + void initGl(); + void resizeWindow(const QSize& size); + void draw(); + void resizeEvent(QResizeEvent* ev) override; +}; + +TestWindow::TestWindow() { + setSurfaceType(QSurface::OpenGLSurface); + + QSurfaceFormat format; + format.setDepthBufferSize(24); + format.setStencilBufferSize(8); + format.setVersion(4, 5); + format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); + format.setOption(QSurfaceFormat::DebugContext); + QSurfaceFormat::setDefaultFormat(format); + setFormat(format); + + show(); + + resize(QSize(800, 600)); + + auto timer = new QTimer(this); + timer->setTimerType(Qt::PreciseTimer); + timer->setInterval(5); + connect(timer, &QTimer::timeout, [&] { draw(); }); + timer->start(); + + connect(qApp, &QCoreApplication::aboutToQuit, [this, timer] { + timer->stop(); + _aboutToQuit = true; + }); + +} + +void TestWindow::initGl() { + _glContext.setFormat(format()); + if (!_glContext.create() || !_glContext.makeCurrent(this)) { + qFatal("Unable to intialize Window GL context"); + } + + _glf.initializeOpenGLFunctions(); + _glf.glCreateFramebuffers(1, &_fbo); + + if (!_sharedContext.create(&_glContext) || !_sharedContext.makeCurrent()) { + qFatal("Unable to intialize Shared GL context"); + } + hifi::qml::OffscreenSurface::setSharedContext(_sharedContext.getContext()); + _discardLamdba = _offscreenQml.getDiscardLambda(); + _offscreenQml.resize({ 640, 480 }); + _offscreenQml.load(QUrl::fromLocalFile("C:/Users/bdavi/Git/hifi/tests/qml/qml/main.qml")); +} + +void TestWindow::resizeWindow(const QSize& size) { + _size = size; +} + +void TestWindow::draw() { + if (_aboutToQuit) { + return; + } + + // Attempting to draw before we're visible and have a valid size will + // produce GL errors. + if (!isVisible() || _size.width() <= 0 || _size.height() <= 0) { + return; + } + + static std::once_flag once; + std::call_once(once, [&] { initGl(); }); + + if (!_glContext.makeCurrent(this)) { + return; + } + + _glf.glClearColor(1, 0, 0, 1); + _glf.glClear(GL_COLOR_BUFFER_BIT); + + TextureAndFence newTextureAndFence; + if (_offscreenQml.fetchTexture(newTextureAndFence)) { + if (_currentTexture) { + _discardLamdba(_currentTexture, _readFence); + _readFence = 0; + } + + _currentTexture = newTextureAndFence.first; + _glf.glWaitSync((GLsync)newTextureAndFence.second, 0, GL_TIMEOUT_IGNORED); + _glf.glNamedFramebufferTexture(_fbo, GL_COLOR_ATTACHMENT0, _currentTexture, 0); + } + + auto diff = _size - _qmlSize; + diff /= 2; + auto qmlExtent = diff + _qmlSize; + + if (_currentTexture) { + _glf.glBlitNamedFramebuffer(_fbo, 0, + 0, 0, _qmlSize.width() - 1, _qmlSize.height() - 1, + diff.width(), diff.height(), qmlExtent.width() - 1, qmlExtent.height() - 2, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + } + + if (_readFence) { + _glf.glDeleteSync(_readFence); + } + _readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + _glf.glFlush(); + + _glContext.swapBuffers(this); +} + +void TestWindow::resizeEvent(QResizeEvent* ev) { + resizeWindow(ev->size()); +} + +void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { + if (!message.isEmpty()) { +#ifdef Q_OS_WIN + OutputDebugStringA(message.toLocal8Bit().constData()); + OutputDebugStringA("\n"); +#endif + } +} + +int main(int argc, char** argv) { + QGuiApplication app(argc, argv); + qInstallMessageHandler(messageHandler); + TestWindow window; + app.exec(); + return 0; +} + diff --git a/tests/render-perf/CMakeLists.txt b/tests/render-perf/CMakeLists.txt index d377b7616d..57ae7dace7 100644 --- a/tests/render-perf/CMakeLists.txt +++ b/tests/render-perf/CMakeLists.txt @@ -14,11 +14,11 @@ set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries link_hifi_libraries( shared networking animation - ktx image octree gl gpu + ktx image octree gl gpu gpu-gl render render-utils graphics fbx model-networking entities entities-renderer audio avatars script-engine - physics procedural midi ui + physics procedural midi qml ui ${PLATFORM_GL_BACKEND} )