// // Application.cpp // interface/src // // Created by Andrzej Kapolka on 5/10/13. // Copyright 2013 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 "Application.h" #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 #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 #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 #include #include #include #include #include "AudioClient.h" #include "audio/AudioScope.h" #include "avatar/AvatarManager.h" #include "CrashHandler.h" #include "devices/DdeFaceTracker.h" #include "devices/EyeTracker.h" #include "devices/Faceshift.h" #include "devices/Leapmotion.h" #include "DiscoverabilityManager.h" #include "GLCanvas.h" #include "InterfaceActionFactory.h" #include "InterfaceLogging.h" #include "LODManager.h" #include "ModelPackager.h" #include "scripting/AccountScriptingInterface.h" #include "scripting/AssetMappingsScriptingInterface.h" #include "scripting/AudioDeviceScriptingInterface.h" #include "scripting/ClipboardScriptingInterface.h" #include "scripting/DesktopScriptingInterface.h" #include "scripting/GlobalServicesScriptingInterface.h" #include "scripting/HMDScriptingInterface.h" #include "scripting/LocationScriptingInterface.h" #include "scripting/MenuScriptingInterface.h" #include "scripting/SettingsScriptingInterface.h" #include "scripting/WebWindowClass.h" #include "scripting/WindowScriptingInterface.h" #include "scripting/ControllerScriptingInterface.h" #include "scripting/ToolbarScriptingInterface.h" #include "scripting/RatesScriptingInterface.h" #if defined(Q_OS_MAC) || defined(Q_OS_WIN) #include "SpeechRecognizer.h" #endif #include "Stars.h" #include "ui/AddressBarDialog.h" #include "ui/AvatarInputs.h" #include "ui/DialogsManager.h" #include "ui/LoginDialog.h" #include "ui/overlays/Cube3DOverlay.h" #include "ui/Snapshot.h" #include "ui/StandAloneJSConsole.h" #include "ui/Stats.h" #include "ui/UpdateDialog.h" #include "Util.h" #include "InterfaceParentFinder.h" #include "FrameTimingsScriptingInterface.h" #include #include // On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU // FIXME seems to be broken. #if defined(Q_OS_WIN) extern "C" { _declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001; } #endif using namespace std; static QTimer locationUpdateTimer; static QTimer identityPacketTimer; static QTimer pingTimer; static const int MAX_CONCURRENT_RESOURCE_DOWNLOADS = 16; // For processing on QThreadPool, target 2 less than the ideal number of threads, leaving // 2 logical cores available for time sensitive tasks. static const int MIN_PROCESSING_THREAD_POOL_SIZE = 2; static const int PROCESSING_THREAD_POOL_SIZE = std::max(MIN_PROCESSING_THREAD_POOL_SIZE, QThread::idealThreadCount() - 2); static const QString SNAPSHOT_EXTENSION = ".jpg"; static const QString SVO_EXTENSION = ".svo"; static const QString SVO_JSON_EXTENSION = ".svo.json"; static const QString JSON_EXTENSION = ".json"; static const QString JS_EXTENSION = ".js"; static const QString FST_EXTENSION = ".fst"; static const QString FBX_EXTENSION = ".fbx"; static const QString OBJ_EXTENSION = ".obj"; static const QString AVA_JSON_EXTENSION = ".ava.json"; static const int MIRROR_VIEW_TOP_PADDING = 5; static const int MIRROR_VIEW_LEFT_PADDING = 10; static const int MIRROR_VIEW_WIDTH = 265; static const int MIRROR_VIEW_HEIGHT = 215; static const float MIRROR_FULLSCREEN_DISTANCE = 0.389f; static const float MIRROR_REARVIEW_DISTANCE = 0.722f; static const float MIRROR_REARVIEW_BODY_DISTANCE = 2.56f; static const float MIRROR_FIELD_OF_VIEW = 30.0f; static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND; static const QString INFO_HELP_PATH = "html/interface-welcome.html"; static const QString INFO_EDIT_ENTITIES_PATH = "html/edit-commands.html"; static const unsigned int THROTTLED_SIM_FRAMERATE = 15; static const int THROTTLED_SIM_FRAME_PERIOD_MS = MSECS_PER_SECOND / THROTTLED_SIM_FRAMERATE; static const uint32_t INVALID_FRAME = UINT32_MAX; static const float PHYSICS_READY_RANGE = 3.0f; // how far from avatar to check for entities that aren't ready for simulation static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); Setting::Handle maxOctreePacketsPerSecond("maxOctreePPS", DEFAULT_MAX_OCTREE_PPS); static const QString MARKETPLACE_CDN_HOSTNAME = "mpassets.highfidelity.com"; const QHash Application::_acceptedExtensions { { SNAPSHOT_EXTENSION, &Application::acceptSnapshot }, { SVO_EXTENSION, &Application::importSVOFromURL }, { SVO_JSON_EXTENSION, &Application::importSVOFromURL }, { AVA_JSON_EXTENSION, &Application::askToWearAvatarAttachmentUrl }, { JSON_EXTENSION, &Application::importJSONFromURL }, { JS_EXTENSION, &Application::askToLoadScript }, { FST_EXTENSION, &Application::askToSetAvatarUrl } }; class DeadlockWatchdogThread : public QThread { public: static const unsigned long HEARTBEAT_UPDATE_INTERVAL_SECS = 1; static const unsigned long MAX_HEARTBEAT_AGE_USECS = 30 * USECS_PER_SECOND; static const int WARNING_ELAPSED_HEARTBEAT = 500 * USECS_PER_MSEC; // warn if elapsed heartbeat average is large static const int HEARTBEAT_SAMPLES = 100000; // ~5 seconds worth of samples // Set the heartbeat on launch DeadlockWatchdogThread() { setObjectName("Deadlock Watchdog"); // Give the heartbeat an initial value _heartbeat = usecTimestampNow(); connect(qApp, &QCoreApplication::aboutToQuit, [this] { _quit = true; }); } static void updateHeartbeat() { auto now = usecTimestampNow(); auto elapsed = now - _heartbeat; _movingAverage.addSample(elapsed); _heartbeat = now; } static void deadlockDetectionCrash() { uint32_t* crashTrigger = nullptr; *crashTrigger = 0xDEAD10CC; } void run() override { while (!_quit) { QThread::sleep(HEARTBEAT_UPDATE_INTERVAL_SECS); uint64_t lastHeartbeat = _heartbeat; // sample atomic _heartbeat, because we could context switch away and have it updated on us uint64_t now = usecTimestampNow(); auto lastHeartbeatAge = (now > lastHeartbeat) ? now - lastHeartbeat : 0; auto elapsedMovingAverage = _movingAverage.getAverage(); if (elapsedMovingAverage > _maxElapsedAverage) { qDebug() << "DEADLOCK WATCHDOG WARNING:" << "lastHeartbeatAge:" << lastHeartbeatAge << "elapsedMovingAverage:" << elapsedMovingAverage << "maxElapsed:" << _maxElapsed << "PREVIOUS maxElapsedAverage:" << _maxElapsedAverage << "NEW maxElapsedAverage:" << elapsedMovingAverage << "** NEW MAX ELAPSED AVERAGE **" << "samples:" << _movingAverage.getSamples(); _maxElapsedAverage = elapsedMovingAverage; } if (lastHeartbeatAge > _maxElapsed) { qDebug() << "DEADLOCK WATCHDOG WARNING:" << "lastHeartbeatAge:" << lastHeartbeatAge << "elapsedMovingAverage:" << elapsedMovingAverage << "PREVIOUS maxElapsed:" << _maxElapsed << "NEW maxElapsed:" << lastHeartbeatAge << "** NEW MAX ELAPSED **" << "maxElapsedAverage:" << _maxElapsedAverage << "samples:" << _movingAverage.getSamples(); _maxElapsed = lastHeartbeatAge; } if (elapsedMovingAverage > WARNING_ELAPSED_HEARTBEAT) { qDebug() << "DEADLOCK WATCHDOG WARNING:" << "lastHeartbeatAge:" << lastHeartbeatAge << "elapsedMovingAverage:" << elapsedMovingAverage << "** OVER EXPECTED VALUE **" << "maxElapsed:" << _maxElapsed << "maxElapsedAverage:" << _maxElapsedAverage << "samples:" << _movingAverage.getSamples(); } if (lastHeartbeatAge > MAX_HEARTBEAT_AGE_USECS) { qDebug() << "DEADLOCK DETECTED -- " << "lastHeartbeatAge:" << lastHeartbeatAge << "[ lastHeartbeat :" << lastHeartbeat << "now:" << now << " ]" << "elapsedMovingAverage:" << elapsedMovingAverage << "maxElapsed:" << _maxElapsed << "maxElapsedAverage:" << _maxElapsedAverage << "samples:" << _movingAverage.getSamples(); // Don't actually crash in debug builds, in case this apparent deadlock is simply from // the developer actively debugging code #ifdef NDEBUG deadlockDetectionCrash(); #endif } } } static std::atomic _heartbeat; static std::atomic _maxElapsed; static std::atomic _maxElapsedAverage; static ThreadSafeMovingAverage _movingAverage; bool _quit { false }; }; std::atomic DeadlockWatchdogThread::_heartbeat; std::atomic DeadlockWatchdogThread::_maxElapsed; std::atomic DeadlockWatchdogThread::_maxElapsedAverage; ThreadSafeMovingAverage DeadlockWatchdogThread::_movingAverage; #ifdef Q_OS_WIN class MyNativeEventFilter : public QAbstractNativeEventFilter { public: static MyNativeEventFilter& getInstance() { static MyNativeEventFilter staticInstance; return staticInstance; } bool nativeEventFilter(const QByteArray &eventType, void* msg, long* result) Q_DECL_OVERRIDE { if (eventType == "windows_generic_MSG") { MSG* message = (MSG*)msg; if (message->message == UWM_IDENTIFY_INSTANCES) { *result = UWM_IDENTIFY_INSTANCES; return true; } if (message->message == UWM_SHOW_APPLICATION) { MainWindow* applicationWindow = qApp->getWindow(); if (applicationWindow->isMinimized()) { applicationWindow->showNormal(); // Restores to windowed or maximized state appropriately. } qApp->setActiveWindow(applicationWindow); // Flashes the taskbar icon if not focus. return true; } if (message->message == WM_COPYDATA) { COPYDATASTRUCT* pcds = (COPYDATASTRUCT*)(message->lParam); QUrl url = QUrl((const char*)(pcds->lpData)); if (url.isValid() && url.scheme() == HIFI_URL_SCHEME) { DependencyManager::get()->handleLookupString(url.toString()); return true; } } } return false; } }; #endif class LambdaEvent : public QEvent { std::function _fun; public: LambdaEvent(const std::function & fun) : QEvent(static_cast(Application::Lambda)), _fun(fun) { } LambdaEvent(std::function && fun) : QEvent(static_cast(Application::Lambda)), _fun(fun) { } void call() const { _fun(); } }; void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { QString logMessage = LogHandler::getInstance().printMessage((LogMsgType) type, context, message); if (!logMessage.isEmpty()) { #ifdef Q_OS_WIN OutputDebugStringA(logMessage.toLocal8Bit().constData()); OutputDebugStringA("\n"); #endif qApp->getLogger()->addMessage(qPrintable(logMessage + "\n")); } } static const QString STATE_IN_HMD = "InHMD"; static const QString STATE_SNAP_TURN = "SnapTurn"; static const QString STATE_GROUNDED = "Grounded"; static const QString STATE_NAV_FOCUSED = "NavigationFocused"; bool setupEssentials(int& argc, char** argv) { unsigned int listenPort = 0; // bind to an ephemeral port by default const char** constArgv = const_cast(argv); const char* portStr = getCmdOption(argc, constArgv, "--listenPort"); if (portStr) { listenPort = atoi(portStr); } // Set build version QCoreApplication::setApplicationVersion(BuildInfo::VERSION); Setting::preInit(); static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset"; bool suppressPrompt = cmdOptionExists(argc, const_cast(argv), SUPPRESS_SETTINGS_RESET); bool previousSessionCrashed = CrashHandler::checkForResetSettings(suppressPrompt); CrashHandler::writeRunningMarkerFiler(); qAddPostRoutine(CrashHandler::deleteRunningMarkerFile); DependencyManager::registerInheritance(); DependencyManager::registerInheritance(); DependencyManager::registerInheritance(); DependencyManager::registerInheritance(); Setting::init(); // Set dependencies DependencyManager::set(std::bind(&Application::getUserAgent, qApp)); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(NodeType::Agent, listenPort); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(true); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); #if defined(Q_OS_MAC) || defined(Q_OS_WIN) DependencyManager::set(); #endif DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_SNAP_TURN, STATE_GROUNDED, STATE_NAV_FOCUSED } }); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(true, qApp, qApp); DependencyManager::set(); return previousSessionCrashed; } // FIXME move to header, or better yet, design some kind of UI manager // to take care of highlighting keyboard focused items, rather than // continuing to overburden Application.cpp Cube3DOverlay* _keyboardFocusHighlight{ nullptr }; int _keyboardFocusHighlightID{ -1 }; // 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 // from a QOpenGLContext. // // So instead we create a new offscreen context to share with the QGLWidget, // and manually set THAT to be the shared context for the Chromium helper OffscreenGLCanvas* _chromiumShareContext { nullptr }; Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); Setting::Handle sessionRunTime{ "sessionRunTime", 0 }; Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : QApplication(argc, argv), _window(new MainWindow(desktop())), _sessionRunTimer(startupTimer), _previousSessionCrashed(setupEssentials(argc, argv)), _undoStackScriptingInterface(&_undoStack), _entitySimulation(new PhysicalEntitySimulation()), _physicsEngine(new PhysicsEngine(Vectors::ZERO)), _entityClipboardRenderer(false, this, this), _entityClipboard(new EntityTree()), _lastQueriedTime(usecTimestampNow()), _mirrorViewRect(QRect(MIRROR_VIEW_LEFT_PADDING, MIRROR_VIEW_TOP_PADDING, MIRROR_VIEW_WIDTH, MIRROR_VIEW_HEIGHT)), _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION), _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES), _scaleMirror(1.0f), _rotateMirror(0.0f), _raiseMirror(0.0f), _enableProcessOctreeThread(true), _lastNackTime(usecTimestampNow()), _lastSendDownstreamAudioStats(usecTimestampNow()), _aboutToQuit(false), _notifiedPacketVersionMismatchThisDomain(false), _maxOctreePPS(maxOctreePacketsPerSecond.get()), _lastFaceTrackerUpdate(0) { PluginContainer* pluginContainer = dynamic_cast(this); // set the container for any plugins that care PluginManager::getInstance()->setContainer(pluginContainer); QThreadPool::globalInstance()->setMaxThreadCount(PROCESSING_THREAD_POOL_SIZE); thread()->setPriority(QThread::HighPriority); thread()->setObjectName("Main Thread"); setInstance(this); auto controllerScriptingInterface = DependencyManager::get().data(); _controllerScriptingInterface = dynamic_cast(controllerScriptingInterface); _entityClipboard->createRootElement(); #ifdef Q_OS_WIN installNativeEventFilter(&MyNativeEventFilter::getInstance()); #endif _logger = new FileLogger(this); // After setting organization name in order to get correct directory qInstallMessageHandler(messageHandler); QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "styles/Inconsolata.otf"); _window->setWindowTitle("Interface"); Model::setAbstractViewStateInterface(this); // The model class will sometimes need to know view state details from us auto nodeList = DependencyManager::get(); // Set up a watchdog thread to intentionally crash the application on deadlocks _deadlockWatchdogThread = new DeadlockWatchdogThread(); _deadlockWatchdogThread->start(); qCDebug(interfaceapp) << "[VERSION] Build sequence:" << qPrintable(applicationVersion()); _bookmarks = new Bookmarks(); // Before setting up the menu // start the nodeThread so its event loop is running QThread* nodeThread = new QThread(this); nodeThread->setObjectName("NodeList Thread"); nodeThread->start(); // make sure the node thread is given highest priority nodeThread->setPriority(QThread::TimeCriticalPriority); // setup a timer for domain-server check ins QTimer* domainCheckInTimer = new QTimer(nodeList.data()); connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn); domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); // put the NodeList and datagram processing on the node thread nodeList->moveToThread(nodeThread); // put the audio processing on a separate thread QThread* audioThread = new QThread(); audioThread->setObjectName("Audio Thread"); auto audioIO = DependencyManager::get(); audioIO->setPositionGetter([]{ auto avatarManager = DependencyManager::get(); auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; return myAvatar ? myAvatar->getPositionForAudio() : Vectors::ZERO; }); audioIO->setOrientationGetter([]{ auto avatarManager = DependencyManager::get(); auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; return myAvatar ? myAvatar->getOrientationForAudio() : Quaternions::IDENTITY; }); audioIO->moveToThread(audioThread); recording::Frame::registerFrameHandler(AudioConstants::getAudioFrameName(), [=](recording::Frame::ConstPointer frame) { audioIO->handleRecordedAudioInput(frame->data); }); connect(audioIO.data(), &AudioClient::inputReceived, [](const QByteArray& audio){ static auto recorder = DependencyManager::get(); if (recorder->isRecording()) { static const recording::FrameType AUDIO_FRAME_TYPE = recording::Frame::registerFrameType(AudioConstants::getAudioFrameName()); recorder->recordFrame(AUDIO_FRAME_TYPE, audio); } }); auto& audioScriptingInterface = AudioScriptingInterface::getInstance(); connect(audioThread, &QThread::started, audioIO.data(), &AudioClient::start); connect(audioIO.data(), &AudioClient::destroyed, audioThread, &QThread::quit); connect(audioThread, &QThread::finished, audioThread, &QThread::deleteLater); connect(audioIO.data(), &AudioClient::muteToggled, this, &Application::audioMuteToggled); connect(audioIO.data(), &AudioClient::mutedByMixer, &audioScriptingInterface, &AudioScriptingInterface::mutedByMixer); connect(audioIO.data(), &AudioClient::receivedFirstPacket, &audioScriptingInterface, &AudioScriptingInterface::receivedFirstPacket); connect(audioIO.data(), &AudioClient::disconnected, &audioScriptingInterface, &AudioScriptingInterface::disconnected); connect(audioIO.data(), &AudioClient::muteEnvironmentRequested, [](glm::vec3 position, float radius) { auto audioClient = DependencyManager::get(); auto myAvatarPosition = DependencyManager::get()->getMyAvatar()->getPosition(); float distance = glm::distance(myAvatarPosition, position); bool shouldMute = !audioClient->isMuted() && (distance < radius); if (shouldMute) { audioClient->toggleMute(); AudioScriptingInterface::getInstance().environmentMuted(); } }); audioThread->start(); ResourceManager::init(); // Make sure we don't time out during slow operations at startup updateHeartbeat(); // Setup MessagesClient auto messagesClient = DependencyManager::get(); QThread* messagesThread = new QThread; messagesThread->setObjectName("Messages Client Thread"); messagesClient->moveToThread(messagesThread); connect(messagesThread, &QThread::started, messagesClient.data(), &MessagesClient::init); messagesThread->start(); const DomainHandler& domainHandler = nodeList->getDomainHandler(); connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&))); connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain())); connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &Application::domainConnectionRefused); // update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * MSECS_PER_SECOND; auto discoverabilityManager = DependencyManager::get(); connect(&locationUpdateTimer, &QTimer::timeout, discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); connect(&locationUpdateTimer, &QTimer::timeout, DependencyManager::get().data(), &AddressManager::storeCurrentAddress); locationUpdateTimer.start(DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS); // if we get a domain change, immediately attempt update location in metaverse server connect(&nodeList->getDomainHandler(), &DomainHandler::connectedToDomain, discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); // send a location update immediately discoverabilityManager->updateLocation(); connect(nodeList.data(), &NodeList::nodeAdded, this, &Application::nodeAdded); connect(nodeList.data(), &NodeList::nodeKilled, this, &Application::nodeKilled); connect(nodeList.data(), &NodeList::nodeActivated, this, &Application::nodeActivated); connect(nodeList.data(), &NodeList::uuidChanged, getMyAvatar(), &MyAvatar::setSessionUUID); connect(nodeList.data(), &NodeList::uuidChanged, this, &Application::setSessionUUID); connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); // you might think we could just do this in NodeList but we only want this connection for Interface connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, nodeList.data(), &NodeList::reset); // connect to appropriate slots on AccountManager auto accountManager = DependencyManager::get(); auto dialogsManager = DependencyManager::get(); connect(accountManager.data(), &AccountManager::authRequired, dialogsManager.data(), &DialogsManager::showLoginDialog); connect(accountManager.data(), &AccountManager::usernameChanged, this, &Application::updateWindowTitle); // set the account manager's root URL and trigger a login request if we don't have the access token accountManager->setIsAgent(true); accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL); auto addressManager = DependencyManager::get(); // use our MyAvatar position and quat for address manager path addressManager->setPositionGetter([this]{ return getMyAvatar()->getPosition(); }); addressManager->setOrientationGetter([this]{ return getMyAvatar()->getOrientation(); }); connect(addressManager.data(), &AddressManager::hostChanged, this, &Application::updateWindowTitle); connect(this, &QCoreApplication::aboutToQuit, addressManager.data(), &AddressManager::storeCurrentAddress); // Save avatar location immediately after a teleport. connect(getMyAvatar(), &MyAvatar::positionGoneTo, DependencyManager::get().data(), &AddressManager::storeCurrentAddress); auto scriptEngines = DependencyManager::get().data(); scriptEngines->registerScriptInitializer([this](ScriptEngine* engine){ registerScriptEngineWithApplicationServices(engine); }); connect(scriptEngines, &ScriptEngines::scriptCountChanged, scriptEngines, [this] { auto scriptEngines = DependencyManager::get(); if (scriptEngines->getRunningScripts().isEmpty()) { getMyAvatar()->clearScriptableSettings(); } }, Qt::QueuedConnection); connect(scriptEngines, &ScriptEngines::scriptsReloading, scriptEngines, [this] { getEntities()->reloadEntityScripts(); }, Qt::QueuedConnection); connect(scriptEngines, &ScriptEngines::scriptLoadError, scriptEngines, [](const QString& filename, const QString& error){ OffscreenUi::warning(nullptr, "Error Loading Script", filename + " failed to load."); }, Qt::QueuedConnection); #ifdef _WIN32 WSADATA WsaData; int wsaresult = WSAStartup(MAKEWORD(2, 2), &WsaData); #endif // tell the NodeList instance who to tell the domain server we care about nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer); // connect to the packet sent signal of the _entityEditSender connect(&_entityEditSender, &EntityEditPacketSender::packetSent, this, &Application::packetSent); // send the identity packet for our avatar each second to our avatar mixer connect(&identityPacketTimer, &QTimer::timeout, getMyAvatar(), &MyAvatar::sendIdentityPacket); identityPacketTimer.start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); const char** constArgv = const_cast(argv); QString concurrentDownloadsStr = getCmdOption(argc, constArgv, "--concurrent-downloads"); bool success; int concurrentDownloads = concurrentDownloadsStr.toInt(&success); if (!success) { concurrentDownloads = MAX_CONCURRENT_RESOURCE_DOWNLOADS; } ResourceCache::setRequestLimit(concurrentDownloads); _glWidget = new GLCanvas(); getApplicationCompositor().setRenderingWidget(_glWidget); _window->setCentralWidget(_glWidget); _window->restoreGeometry(); _window->setVisible(true); _glWidget->setFocusPolicy(Qt::StrongFocus); _glWidget->setFocus(); #ifdef Q_OS_MAC auto cursorTarget = _window; // OSX doesn't seem to provide for hiding the cursor only on the GL widget #else // On windows and linux, hiding the top level cursor also means it's invisible when hovering over the // window menu, which is a pain, so only hide it for the GL surface auto cursorTarget = _glWidget; #endif cursorTarget->setCursor(Qt::BlankCursor); // enable mouse tracking; otherwise, we only get drag events _glWidget->setMouseTracking(true); _glWidget->makeCurrent(); _glWidget->initializeGL(); _chromiumShareContext = new OffscreenGLCanvas(); _chromiumShareContext->create(_glWidget->context()->contextHandle()); _chromiumShareContext->makeCurrent(); qt_gl_set_global_share_context(_chromiumShareContext->getContext()); _offscreenContext = new OffscreenGLCanvas(); _offscreenContext->create(_glWidget->context()->contextHandle()); _offscreenContext->makeCurrent(); initializeGL(); _offscreenContext->makeCurrent(); // Make sure we don't time out during slow operations at startup updateHeartbeat(); // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. // The value will be 0 if the user blew away settings this session, which is both a feature and a bug. auto gpuIdent = GPUIdent::getInstance(); auto glContextData = getGLContextData(); QJsonObject properties = { { "version", applicationVersion() }, { "previousSessionCrashed", _previousSessionCrashed }, { "previousSessionRuntime", sessionRunTime.get() }, { "cpu_architecture", QSysInfo::currentCpuArchitecture() }, { "kernel_type", QSysInfo::kernelType() }, { "kernel_version", QSysInfo::kernelVersion() }, { "os_type", QSysInfo::productType() }, { "os_version", QSysInfo::productVersion() }, { "gpu_name", gpuIdent->getName() }, { "gpu_driver", gpuIdent->getDriver() }, { "gpu_memory", static_cast(gpuIdent->getMemory()) }, { "gl_version_int", glVersionToInteger(glContextData.value("version").toString()) }, { "gl_version", glContextData["version"] }, { "gl_vender", glContextData["vendor"] }, { "gl_sl_version", glContextData["slVersion"] }, { "gl_renderer", glContextData["renderer"] } }; auto macVersion = QSysInfo::macVersion(); if (macVersion != QSysInfo::MV_None) { properties["os_osx_version"] = QSysInfo::macVersion(); } auto windowsVersion = QSysInfo::windowsVersion(); if (windowsVersion != QSysInfo::WV_None) { properties["os_win_version"] = QSysInfo::windowsVersion(); } UserActivityLogger::getInstance().logAction("launch", properties); // Tell our entity edit sender about our known jurisdictions _entityEditSender.setServerJurisdictions(&_entityServerJurisdictions); _entityEditSender.setMyAvatar(getMyAvatar()); // For now we're going to set the PPS for outbound packets to be super high, this is // probably not the right long term solution. But for now, we're going to do this to // allow you to move an entity around in your hand _entityEditSender.setPacketsPerSecond(3000); // super high!! _overlays.init(); // do this before scripts load // Make sure we don't time out during slow operations at startup updateHeartbeat(); connect(this, SIGNAL(aboutToQuit()), this, SLOT(aboutToQuit())); // hook up bandwidth estimator QSharedPointer bandwidthRecorder = DependencyManager::get(); connect(nodeList.data(), &LimitedNodeList::dataSent, bandwidthRecorder.data(), &BandwidthRecorder::updateOutboundData); connect(&nodeList->getPacketReceiver(), &PacketReceiver::dataReceived, bandwidthRecorder.data(), &BandwidthRecorder::updateInboundData); // FIXME -- I'm a little concerned about this. connect(getMyAvatar()->getSkeletonModel().get(), &SkeletonModel::skeletonLoaded, this, &Application::checkSkeleton, Qt::QueuedConnection); // Setup the userInputMapper with the actions auto userInputMapper = DependencyManager::get(); connect(userInputMapper.data(), &UserInputMapper::actionEvent, [this](int action, float state) { using namespace controller; auto offscreenUi = DependencyManager::get(); if (offscreenUi->navigationFocused()) { auto actionEnum = static_cast(action); int key = Qt::Key_unknown; static int lastKey = Qt::Key_unknown; bool navAxis = false; switch (actionEnum) { case Action::UI_NAV_VERTICAL: navAxis = true; if (state > 0.0f) { key = Qt::Key_Up; } else if (state < 0.0f) { key = Qt::Key_Down; } break; case Action::UI_NAV_LATERAL: navAxis = true; if (state > 0.0f) { key = Qt::Key_Right; } else if (state < 0.0f) { key = Qt::Key_Left; } break; case Action::UI_NAV_GROUP: navAxis = true; if (state > 0.0f) { key = Qt::Key_Tab; } else if (state < 0.0f) { key = Qt::Key_Backtab; } break; case Action::UI_NAV_BACK: key = Qt::Key_Escape; break; case Action::UI_NAV_SELECT: key = Qt::Key_Return; break; default: break; } if (navAxis) { if (lastKey != Qt::Key_unknown) { QKeyEvent event(QEvent::KeyRelease, lastKey, Qt::NoModifier); sendEvent(offscreenUi->getWindow(), &event); lastKey = Qt::Key_unknown; } if (key != Qt::Key_unknown) { QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); sendEvent(offscreenUi->getWindow(), &event); lastKey = key; } } else if (key != Qt::Key_unknown) { if (state) { QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); sendEvent(offscreenUi->getWindow(), &event); } else { QKeyEvent event(QEvent::KeyRelease, key, Qt::NoModifier); sendEvent(offscreenUi->getWindow(), &event); } return; } } if (action == controller::toInt(controller::Action::RETICLE_CLICK)) { auto reticlePos = getApplicationCompositor().getReticlePosition(); QPoint localPos(reticlePos.x, reticlePos.y); // both hmd and desktop already handle this in our coordinates. if (state) { QMouseEvent mousePress(QEvent::MouseButtonPress, localPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); sendEvent(_glWidget, &mousePress); _reticleClickPressed = true; } else { QMouseEvent mouseRelease(QEvent::MouseButtonRelease, localPos, Qt::LeftButton, Qt::NoButton, Qt::NoModifier); sendEvent(_glWidget, &mouseRelease); _reticleClickPressed = false; } return; // nothing else to do } if (state) { if (action == controller::toInt(controller::Action::TOGGLE_MUTE)) { DependencyManager::get()->toggleMute(); } else if (action == controller::toInt(controller::Action::CYCLE_CAMERA)) { cycleCamera(); } else if (action == controller::toInt(controller::Action::UI_NAV_SELECT)) { if (!offscreenUi->navigationFocused()) { toggleMenuUnderReticle(); } } else if (action == controller::toInt(controller::Action::CONTEXT_MENU)) { toggleMenuUnderReticle(); } else if (action == controller::toInt(controller::Action::RETICLE_X)) { auto oldPos = getApplicationCompositor().getReticlePosition(); getApplicationCompositor().setReticlePosition({ oldPos.x + state, oldPos.y }); } else if (action == controller::toInt(controller::Action::RETICLE_Y)) { auto oldPos = getApplicationCompositor().getReticlePosition(); getApplicationCompositor().setReticlePosition({ oldPos.x, oldPos.y + state }); } else if (action == controller::toInt(controller::Action::TOGGLE_OVERLAY)) { toggleOverlays(); } } }); _applicationStateDevice = userInputMapper->getStateDevice(); _applicationStateDevice->setInputVariant(STATE_IN_HMD, []() -> float { return qApp->isHMDMode() ? 1 : 0; }); _applicationStateDevice->setInputVariant(STATE_SNAP_TURN, []() -> float { return qApp->getMyAvatar()->getSnapTurn() ? 1 : 0; }); _applicationStateDevice->setInputVariant(STATE_GROUNDED, []() -> float { return qApp->getMyAvatar()->getCharacterController()->onGround() ? 1 : 0; }); _applicationStateDevice->setInputVariant(STATE_NAV_FOCUSED, []() -> float { return DependencyManager::get()->navigationFocused() ? 1 : 0; }); // Setup the _keyboardMouseDevice, _touchscreenDevice and the user input mapper with the default bindings userInputMapper->registerDevice(_keyboardMouseDevice->getInputDevice()); // if the _touchscreenDevice is not supported it will not be registered if (_touchscreenDevice) { userInputMapper->registerDevice(_touchscreenDevice->getInputDevice()); } // force the model the look at the correct directory (weird order of operations issue) scriptEngines->setScriptsLocation(scriptEngines->getScriptsLocation()); // do this as late as possible so that all required subsystems are initialized scriptEngines->loadScripts(); // Make sure we don't time out during slow operations at startup updateHeartbeat(); loadSettings(); // Now that we've loaded the menu and thus switched to the previous display plugin // we can unlock the desktop repositioning code, since all the positions will be // relative to the desktop size for this plugin auto offscreenUi = DependencyManager::get(); offscreenUi->getDesktop()->setProperty("repositionLocked", false); // Make sure we don't time out during slow operations at startup updateHeartbeat(); int SAVE_SETTINGS_INTERVAL = 10 * MSECS_PER_SECOND; // Let's save every seconds for now connect(&_settingsTimer, &QTimer::timeout, this, &Application::saveSettings); connect(&_settingsThread, SIGNAL(started()), &_settingsTimer, SLOT(start())); connect(&_settingsThread, SIGNAL(finished()), &_settingsTimer, SLOT(stop())); _settingsTimer.moveToThread(&_settingsThread); _settingsTimer.setSingleShot(false); _settingsTimer.setInterval(SAVE_SETTINGS_INTERVAL); _settingsThread.start(); if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) { getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); // So that camera doesn't auto-switch to third person. } else if (Menu::getInstance()->isOptionChecked(MenuOption::IndependentMode)) { Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); cameraMenuChanged(); } else if (Menu::getInstance()->isOptionChecked(MenuOption::CameraEntityMode)) { Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); cameraMenuChanged(); } // set the local loopback interface for local sounds from audio scripts AudioScriptingInterface::getInstance().setLocalAudioInterface(audioIO.data()); this->installEventFilter(this); // initialize our face trackers after loading the menu settings auto faceshiftTracker = DependencyManager::get(); faceshiftTracker->init(); connect(faceshiftTracker.data(), &FaceTracker::muteToggled, this, &Application::faceTrackerMuteToggled); #ifdef HAVE_DDE auto ddeTracker = DependencyManager::get(); ddeTracker->init(); connect(ddeTracker.data(), &FaceTracker::muteToggled, this, &Application::faceTrackerMuteToggled); #endif #ifdef HAVE_IVIEWHMD auto eyeTracker = DependencyManager::get(); eyeTracker->init(); setActiveEyeTracker(); #endif auto applicationUpdater = DependencyManager::get(); connect(applicationUpdater.data(), &AutoUpdater::newVersionIsAvailable, dialogsManager.data(), &DialogsManager::showUpdateDialog); applicationUpdater->checkForUpdate(); // Now that menu is initialized we can sync myAvatar with it's state. getMyAvatar()->updateMotionBehaviorFromMenu(); // FIXME spacemouse code still needs cleanup #if 0 // the 3Dconnexion device wants to be initialized after a window is displayed. SpacemouseManager::getInstance().init(); #endif // If the user clicks an an entity, we will check that it's an unlocked web entity, and if so, set the focus to it auto entityScriptingInterface = DependencyManager::get(); connect(entityScriptingInterface.data(), &EntityScriptingInterface::clickDownOnEntity, [this, entityScriptingInterface](const EntityItemID& entityItemID, const MouseEvent& event) { if (_keyboardFocusedItem != entityItemID) { _keyboardFocusedItem = UNKNOWN_ENTITY_ID; auto properties = entityScriptingInterface->getEntityProperties(entityItemID); if (EntityTypes::Web == properties.getType() && !properties.getLocked() && properties.getVisible()) { auto entity = entityScriptingInterface->getEntityTree()->findEntityByID(entityItemID); RenderableWebEntityItem* webEntity = dynamic_cast(entity.get()); if (webEntity) { webEntity->setProxyWindow(_window->windowHandle()); if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->pluginFocusOutEvent(); } _keyboardFocusedItem = entityItemID; _lastAcceptedKeyPress = usecTimestampNow(); if (_keyboardFocusHighlightID < 0 || !getOverlays().isAddedOverlay(_keyboardFocusHighlightID)) { _keyboardFocusHighlight = new Cube3DOverlay(); _keyboardFocusHighlight->setAlpha(1.0f); _keyboardFocusHighlight->setBorderSize(1.0f); _keyboardFocusHighlight->setColor({ 0xFF, 0xEF, 0x00 }); _keyboardFocusHighlight->setIsSolid(false); _keyboardFocusHighlight->setPulseMin(0.5); _keyboardFocusHighlight->setPulseMax(1.0); _keyboardFocusHighlight->setColorPulse(1.0); _keyboardFocusHighlight->setIgnoreRayIntersection(true); _keyboardFocusHighlight->setDrawInFront(true); } _keyboardFocusHighlight->setRotation(webEntity->getRotation()); _keyboardFocusHighlight->setPosition(webEntity->getPosition()); _keyboardFocusHighlight->setDimensions(webEntity->getDimensions() * 1.05f); _keyboardFocusHighlight->setVisible(true); _keyboardFocusHighlightID = getOverlays().addOverlay(_keyboardFocusHighlight); } } if (_keyboardFocusedItem == UNKNOWN_ENTITY_ID && _keyboardFocusHighlight) { _keyboardFocusHighlight->setVisible(false); } } }); connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, [=](const EntityItemID& entityItemID) { if (entityItemID == _keyboardFocusedItem) { _keyboardFocusedItem = UNKNOWN_ENTITY_ID; if (_keyboardFocusHighlight) { _keyboardFocusHighlight->setVisible(false); } } }); // If the user clicks somewhere where there is NO entity at all, we will release focus connect(getEntities(), &EntityTreeRenderer::mousePressOffEntity, [=](const RayToEntityIntersectionResult& entityItemID, const QMouseEvent* event) { _keyboardFocusedItem = UNKNOWN_ENTITY_ID; if (_keyboardFocusHighlight) { _keyboardFocusHighlight->setVisible(false); } }); connect(this, &Application::aboutToQuit, [=]() { _keyboardFocusedItem = UNKNOWN_ENTITY_ID; if (_keyboardFocusHighlight) { _keyboardFocusHighlight->setVisible(false); } }); // Add periodic checks to send user activity data static int CHECK_NEARBY_AVATARS_INTERVAL_MS = 10000; static int SEND_STATS_INTERVAL_MS = 10000; static int NEARBY_AVATAR_RADIUS_METERS = 10; // Periodically send fps as a user activity event QTimer* sendStatsTimer = new QTimer(this); sendStatsTimer->setInterval(SEND_STATS_INTERVAL_MS); connect(sendStatsTimer, &QTimer::timeout, this, [this]() { QJsonObject properties = {}; MemoryInfo memInfo; if (getMemoryInfo(memInfo)) { properties["system_memory_total"] = static_cast(memInfo.totalMemoryBytes); properties["system_memory_used"] = static_cast(memInfo.usedMemoryBytes); properties["process_memory_used"] = static_cast(memInfo.processUsedMemoryBytes); } auto displayPlugin = qApp->getActiveDisplayPlugin(); properties["fps"] = _frameCounter.rate(); properties["present_rate"] = displayPlugin->presentRate(); properties["new_frame_present_rate"] = displayPlugin->newFramePresentRate(); properties["dropped_frame_rate"] = displayPlugin->droppedFrameRate(); properties["sim_rate"] = getAverageSimsPerSecond(); properties["avatar_sim_rate"] = getAvatarSimrate(); auto bandwidthRecorder = DependencyManager::get(); properties["packet_rate_in"] = bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond(); properties["packet_rate_out"] = bandwidthRecorder->getCachedTotalAverageOutputPacketsPerSecond(); properties["kbps_in"] = bandwidthRecorder->getCachedTotalAverageInputKilobitsPerSecond(); properties["kbps_out"] = bandwidthRecorder->getCachedTotalAverageOutputKilobitsPerSecond(); auto nodeList = DependencyManager::get(); SharedNodePointer entityServerNode = nodeList->soloNodeOfType(NodeType::EntityServer); SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer); SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer); SharedNodePointer assetServerNode = nodeList->soloNodeOfType(NodeType::AssetServer); SharedNodePointer messagesMixerNode = nodeList->soloNodeOfType(NodeType::MessagesMixer); properties["entity_ping"] = entityServerNode ? entityServerNode->getPingMs() : -1; properties["audio_ping"] = audioMixerNode ? audioMixerNode->getPingMs() : -1; properties["avatar_ping"] = avatarMixerNode ? avatarMixerNode->getPingMs() : -1; properties["asset_ping"] = assetServerNode ? assetServerNode->getPingMs() : -1; properties["messages_ping"] = messagesMixerNode ? messagesMixerNode->getPingMs() : -1; auto loadingRequests = ResourceCache::getLoadingRequests(); properties["active_downloads"] = loadingRequests.size(); properties["pending_downloads"] = ResourceCache::getPendingRequestCount(); properties["throttled"] = _displayPlugin ? _displayPlugin->isThrottled() : false; UserActivityLogger::getInstance().logAction("stats", properties); }); sendStatsTimer->start(); // Periodically check for count of nearby avatars static int lastCountOfNearbyAvatars = -1; QTimer* checkNearbyAvatarsTimer = new QTimer(this); checkNearbyAvatarsTimer->setInterval(CHECK_NEARBY_AVATARS_INTERVAL_MS); connect(checkNearbyAvatarsTimer, &QTimer::timeout, this, [this]() { auto avatarManager = DependencyManager::get(); int nearbyAvatars = avatarManager->numberOfAvatarsInRange(avatarManager->getMyAvatar()->getPosition(), NEARBY_AVATAR_RADIUS_METERS) - 1; if (nearbyAvatars != lastCountOfNearbyAvatars) { lastCountOfNearbyAvatars = nearbyAvatars; UserActivityLogger::getInstance().logAction("nearby_avatars", { { "count", nearbyAvatars } }); } }); checkNearbyAvatarsTimer->start(); // Track user activity event when we receive a mute packet auto onMutedByMixer = []() { UserActivityLogger::getInstance().logAction("received_mute_packet"); }; connect(DependencyManager::get().data(), &AudioClient::mutedByMixer, this, onMutedByMixer); // Track when the address bar is opened auto onAddressBarToggled = [this]() { // Record time UserActivityLogger::getInstance().logAction("opened_address_bar", { { "uptime_ms", _sessionRunTimer.elapsed() } }); }; connect(DependencyManager::get().data(), &DialogsManager::addressBarToggled, this, onAddressBarToggled); // Make sure we don't time out during slow operations at startup updateHeartbeat(); OctreeEditPacketSender* packetSender = entityScriptingInterface->getPacketSender(); EntityEditPacketSender* entityPacketSender = static_cast(packetSender); entityPacketSender->setMyAvatar(getMyAvatar()); connect(this, &Application::applicationStateChanged, this, &Application::activeChanged); qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0); // After all of the constructor is completed, then set firstRun to false. Setting::Handle firstRun{ Settings::firstRun, true }; firstRun.set(false); } void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCode) { switch (static_cast(reasonCode)) { case DomainHandler::ConnectionRefusedReason::ProtocolMismatch: case DomainHandler::ConnectionRefusedReason::TooManyUsers: case DomainHandler::ConnectionRefusedReason::Unknown: { QString message = "Unable to connect to the location you are visiting.\n"; message += reasonMessage; OffscreenUi::warning("", message); break; } default: // nothing to do. break; } } QString Application::getUserAgent() { if (QThread::currentThread() != thread()) { QString userAgent; QMetaObject::invokeMethod(this, "getUserAgent", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QString, userAgent)); return userAgent; } QString userAgent = "Mozilla/5.0 (HighFidelityInterface/" + BuildInfo::VERSION + "; " + QSysInfo::productType() + " " + QSysInfo::productVersion() + ")"; auto formatPluginName = [](QString name) -> QString { return name.trimmed().replace(" ", "-"); }; // For each plugin, add to userAgent auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); for (auto& dp : displayPlugins) { if (dp->isActive() && dp->isHmd()) { userAgent += " " + formatPluginName(dp->getName()); } } auto inputPlugins= PluginManager::getInstance()->getInputPlugins(); for (auto& ip : inputPlugins) { if (ip->isActive()) { userAgent += " " + formatPluginName(ip->getName()); } } // for codecs, we include all of them, even if not active auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); for (auto& cp : codecPlugins) { userAgent += " " + formatPluginName(cp->getName()); } return userAgent; } void Application::toggleMenuUnderReticle() const { // In HMD, if the menu is near the mouse but not under it, the reticle can be at a significantly // different depth. When you focus on the menu, the cursor can appear to your crossed eyes as both // on the menu and off. // Even in 2D, it is arguable whether the user would want the menu to be to the side. const float X_LEFT_SHIFT = 50.0; auto offscreenUi = DependencyManager::get(); auto reticlePosition = getApplicationCompositor().getReticlePosition(); offscreenUi->toggleMenu(QPoint(reticlePosition.x - X_LEFT_SHIFT, reticlePosition.y)); } void Application::checkChangeCursor() { QMutexLocker locker(&_changeCursorLock); if (_cursorNeedsChanging) { #ifdef Q_OS_MAC auto cursorTarget = _window; // OSX doesn't seem to provide for hiding the cursor only on the GL widget #else // On windows and linux, hiding the top level cursor also means it's invisible when hovering over the // window menu, which is a pain, so only hide it for the GL surface auto cursorTarget = _glWidget; #endif cursorTarget->setCursor(_desiredCursor); _cursorNeedsChanging = false; } } void Application::showCursor(const QCursor& cursor) { QMutexLocker locker(&_changeCursorLock); _desiredCursor = cursor; _cursorNeedsChanging = true; } void Application::updateHeartbeat() const { static_cast(_deadlockWatchdogThread)->updateHeartbeat(); } void Application::aboutToQuit() { emit beforeAboutToQuit(); foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { if (inputPlugin->isActive()) { inputPlugin->deactivate(); } } getActiveDisplayPlugin()->deactivate(); // Hide Running Scripts dialog so that it gets destroyed in an orderly manner; prevents warnings at shutdown. DependencyManager::get()->hide("RunningScripts"); _aboutToQuit = true; cleanupBeforeQuit(); } void Application::cleanupBeforeQuit() { // add a logline indicating if QTWEBENGINE_REMOTE_DEBUGGING is set or not QString webengineRemoteDebugging = QProcessEnvironment::systemEnvironment().value("QTWEBENGINE_REMOTE_DEBUGGING", "false"); qCDebug(interfaceapp) << "QTWEBENGINE_REMOTE_DEBUGGING =" << webengineRemoteDebugging; // Stop third party processes so that they're not left running in the event of a subsequent shutdown crash. #ifdef HAVE_DDE DependencyManager::get()->setEnabled(false); #endif #ifdef HAVE_IVIEWHMD DependencyManager::get()->setEnabled(false, true); #endif AnimDebugDraw::getInstance().shutdown(); // FIXME: once we move to shared pointer for the INputDevice we shoud remove this naked delete: _applicationStateDevice.reset(); if (_keyboardFocusHighlightID > 0) { getOverlays().deleteOverlay(_keyboardFocusHighlightID); _keyboardFocusHighlightID = -1; } _keyboardFocusHighlight = nullptr; auto nodeList = DependencyManager::get(); // send the domain a disconnect packet, force stoppage of domain-server check-ins nodeList->getDomainHandler().disconnect(); nodeList->setIsShuttingDown(true); // tell the packet receiver we're shutting down, so it can drop packets nodeList->getPacketReceiver().setShouldDropPackets(true); getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts // Clear any queued processing (I/O, FBX/OBJ/Texture parsing) QThreadPool::globalInstance()->clear(); DependencyManager::get()->saveScripts(); DependencyManager::get()->shutdownScripting(); // stop all currently running global scripts DependencyManager::destroy(); // Cleanup all overlays after the scripts, as scripts might add more _overlays.cleanupAllOverlays(); // first stop all timers directly or by invokeMethod // depending on what thread they run in locationUpdateTimer.stop(); identityPacketTimer.stop(); pingTimer.stop(); QMetaObject::invokeMethod(&_settingsTimer, "stop", Qt::BlockingQueuedConnection); // save state _settingsThread.quit(); saveSettings(); _window->saveGeometry(); // stop the AudioClient QMetaObject::invokeMethod(DependencyManager::get().data(), "stop", Qt::BlockingQueuedConnection); // destroy the AudioClient so it and its thread have a chance to go down safely DependencyManager::destroy(); // destroy the AudioInjectorManager so it and its thread have a chance to go down safely // this will also stop any ongoing network injectors DependencyManager::destroy(); // Destroy third party processes after scripts have finished using them. #ifdef HAVE_DDE DependencyManager::destroy(); #endif #ifdef HAVE_IVIEWHMD DependencyManager::destroy(); #endif DependencyManager::destroy(); } Application::~Application() { _entityClipboard->eraseAllOctreeElements(); _entityClipboard.reset(); EntityTreePointer tree = getEntities()->getTree(); tree->setSimulation(nullptr); _octreeProcessor.terminate(); _entityEditSender.terminate(); _physicsEngine->setCharacterController(nullptr); // remove avatars from physics engine DependencyManager::get()->clearAllAvatars(); VectorOfMotionStates motionStates; DependencyManager::get()->getObjectsToRemoveFromPhysics(motionStates); _physicsEngine->removeObjects(motionStates); DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); ResourceManager::cleanup(); QThread* nodeThread = DependencyManager::get()->thread(); // remove the NodeList from the DependencyManager DependencyManager::destroy(); // ask the node thread to quit and wait until it is done nodeThread->quit(); nodeThread->wait(); Leapmotion::destroy(); #if 0 ConnexionClient::getInstance().destroy(); #endif // The window takes ownership of the menu, so this has the side effect of destroying it. _window->setMenuBar(nullptr); _window->deleteLater(); qInstallMessageHandler(nullptr); // NOTE: Do this as late as possible so we continue to get our log messages } void Application::initializeGL() { qCDebug(interfaceapp) << "Created Display Window."; // initialize glut for shape drawing; Qt apparently initializes it on OS X if (_isGLInitialized) { return; } else { _isGLInitialized = true; } gpu::Context::init(); _gpuContext = std::make_shared(); // The gpu context can make child contexts for transfers, so // we need to restore primary rendering context _offscreenContext->makeCurrent(); initDisplay(); qCDebug(interfaceapp, "Initialized Display."); // Set up the render engine render::CullFunctor cullFunctor = LODManager::shouldRender; _renderEngine->addJob("RenderShadowTask", cullFunctor); _renderEngine->addJob("RenderDeferredTask", cullFunctor); _renderEngine->load(); _renderEngine->registerScene(_main3DScene); // TODO: Load a cached config file // 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 initializeUi(); qCDebug(interfaceapp, "Initialized Offscreen UI."); _offscreenContext->makeCurrent(); // call Menu getInstance static method to set up the menu // Needs to happen AFTER the QML UI initialization _window->setMenuBar(Menu::getInstance()); init(); qCDebug(interfaceapp, "init() complete."); // create thread for parsing of octree data independent of the main network and rendering threads _octreeProcessor.initialize(_enableProcessOctreeThread); connect(&_octreeProcessor, &OctreePacketProcessor::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); _entityEditSender.initialize(_enableProcessOctreeThread); _idleLoopStdev.reset(); // update before the first render update(0); } FrameTimingsScriptingInterface _frameTimingsScriptingInterface; extern void setupPreferences(); void Application::initializeUi() { AddressBarDialog::registerType(); ErrorDialog::registerType(); LoginDialog::registerType(); Tooltip::registerType(); UpdateDialog::registerType(); qmlRegisterType("Hifi", 1, 0, "Preference"); auto offscreenUi = DependencyManager::get(); offscreenUi->create(_offscreenContext->getContext()); auto rootContext = offscreenUi->getRootContext(); offscreenUi->setProxyWindow(_window->windowHandle()); offscreenUi->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); // OffscreenUi is a subclass of OffscreenQmlSurface specifically designed to // support the window management and scripting proxies for VR use offscreenUi->createDesktop(QString("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 = rootContext->engine(); connect(engine, &QQmlEngine::quit, [] { qApp->quit(); }); setupPreferences(); // For some reason there is already an "Application" object in the QML context, // though I can't find it. Hence, "ApplicationInterface" rootContext->setContextProperty("ApplicationInterface", this); rootContext->setContextProperty("Audio", &AudioScriptingInterface::getInstance()); rootContext->setContextProperty("Controller", DependencyManager::get().data()); rootContext->setContextProperty("Entities", DependencyManager::get().data()); rootContext->setContextProperty("MyAvatar", getMyAvatar()); rootContext->setContextProperty("Messages", DependencyManager::get().data()); rootContext->setContextProperty("Recording", DependencyManager::get().data()); rootContext->setContextProperty("Preferences", DependencyManager::get().data()); rootContext->setContextProperty("AddressManager", DependencyManager::get().data()); rootContext->setContextProperty("FrameTimings", &_frameTimingsScriptingInterface); rootContext->setContextProperty("Rates", new RatesScriptingInterface(this)); rootContext->setContextProperty("TREE_SCALE", TREE_SCALE); rootContext->setContextProperty("Quat", new Quat()); rootContext->setContextProperty("Vec3", new Vec3()); rootContext->setContextProperty("Uuid", new ScriptUUID()); rootContext->setContextProperty("Assets", new AssetMappingsScriptingInterface()); rootContext->setContextProperty("AvatarList", DependencyManager::get().data()); rootContext->setContextProperty("Camera", &_myCamera); #if defined(Q_OS_MAC) || defined(Q_OS_WIN) rootContext->setContextProperty("SpeechRecognizer", DependencyManager::get().data()); #endif rootContext->setContextProperty("Overlays", &_overlays); rootContext->setContextProperty("Window", DependencyManager::get().data()); rootContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); rootContext->setContextProperty("Stats", Stats::getInstance()); rootContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); rootContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); rootContext->setContextProperty("AudioDevice", AudioDeviceScriptingInterface::getInstance()); // Caches rootContext->setContextProperty("AnimationCache", DependencyManager::get().data()); rootContext->setContextProperty("TextureCache", DependencyManager::get().data()); rootContext->setContextProperty("ModelCache", DependencyManager::get().data()); rootContext->setContextProperty("SoundCache", DependencyManager::get().data()); rootContext->setContextProperty("Account", AccountScriptingInterface::getInstance()); rootContext->setContextProperty("DialogsManager", _dialogsManagerScriptingInterface); rootContext->setContextProperty("GlobalServices", GlobalServicesScriptingInterface::getInstance()); rootContext->setContextProperty("FaceTracker", DependencyManager::get().data()); rootContext->setContextProperty("AvatarManager", DependencyManager::get().data()); rootContext->setContextProperty("UndoStack", &_undoStackScriptingInterface); rootContext->setContextProperty("LODManager", DependencyManager::get().data()); rootContext->setContextProperty("Paths", DependencyManager::get().data()); rootContext->setContextProperty("HMD", DependencyManager::get().data()); rootContext->setContextProperty("Scene", DependencyManager::get().data()); rootContext->setContextProperty("Render", _renderEngine->getConfiguration().get()); rootContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); rootContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); _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, [=] { if (isHMDMode()) { showCursor(compositorHelper->getAllowMouseCapture() ? Qt::BlankCursor : Qt::ArrowCursor); } }); } void Application::paintGL() { // Some plugins process message events, allowing paintGL to be called reentrantly. if (_inPaint || _aboutToQuit) { return; } _inPaint = true; Finally clearFlag([this] { _inPaint = false; }); _frameCount++; _frameCounter.increment(); auto lastPaintBegin = usecTimestampNow(); PROFILE_RANGE_EX(__FUNCTION__, 0xff0000ff, (uint64_t)_frameCount); PerformanceTimer perfTimer("paintGL"); if (nullptr == _displayPlugin) { return; } auto displayPlugin = getActiveDisplayPlugin(); // FIXME not needed anymore? _offscreenContext->makeCurrent(); // If a display plugin loses it's underlying support, it // needs to be able to signal us to not use it if (!displayPlugin->beginFrameRender(_frameCount)) { _inPaint = false; updateDisplayMode(); return; } // update the avatar with a fresh HMD pose getMyAvatar()->updateFromHMDSensorMatrix(getHMDSensorPose()); auto lodManager = DependencyManager::get(); { QMutexLocker viewLocker(&_viewMutex); _viewFrustum.calculate(); } RenderArgs renderArgs(_gpuContext, getEntities(), lodManager->getOctreeSizeScale(), lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE, RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); { QMutexLocker viewLocker(&_viewMutex); renderArgs.setViewFrustum(_viewFrustum); } PerformanceWarning::setSuppressShortTimings(Menu::getInstance()->isOptionChecked(MenuOption::SuppressShortTimings)); bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::paintGL()"); resizeGL(); // Before anything else, let's sync up the gpuContext with the true glcontext used in case anything happened { PerformanceTimer perfTimer("syncCache"); renderArgs._context->syncCache(); } auto framebufferCache = DependencyManager::get(); // Final framebuffer that will be handled to the display-plugin auto finalFramebuffer = framebufferCache->getFramebuffer(); _gpuContext->beginFrame(finalFramebuffer, getHMDSensorPose()); // Reset the gpu::Context Stages // Back to the default framebuffer; gpu::doInBatch(_gpuContext, [&](gpu::Batch& batch) { batch.resetStages(); }); auto inputs = AvatarInputs::getInstance(); if (inputs->mirrorVisible()) { PerformanceTimer perfTimer("Mirror"); renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; renderArgs._blitFramebuffer = DependencyManager::get()->getSelfieFramebuffer(); _mirrorViewRect.moveTo(inputs->x(), inputs->y()); renderRearViewMirror(&renderArgs, _mirrorViewRect, inputs->mirrorZoomed()); renderArgs._blitFramebuffer.reset(); renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; } { PerformanceTimer perfTimer("renderOverlay"); // NOTE: There is no batch associated with this renderArgs // the ApplicationOverlay class assumes it's viewport is setup to be the device size QSize size = getDeviceSize(); renderArgs._viewport = glm::ivec4(0, 0, size.width(), size.height()); _applicationOverlay.renderOverlay(&renderArgs); } glm::vec3 boomOffset; { PerformanceTimer perfTimer("CameraUpdates"); auto myAvatar = getMyAvatar(); boomOffset = myAvatar->getScale() * myAvatar->getBoomLength() * -IDENTITY_FRONT; if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON || _myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, myAvatar->getBoomLength() <= MyAvatar::ZOOM_MIN); Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !(myAvatar->getBoomLength() <= MyAvatar::ZOOM_MIN)); cameraMenuChanged(); } // The render mode is default or mirror if the camera is in mirror mode, assigned further below renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; // Always use the default eye position, not the actual head eye position. // Using the latter will cause the camera to wobble with idle animations, // or with changes from the face tracker if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { if (isHMDMode()) { mat4 camMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); _myCamera.setPosition(extractTranslation(camMat)); _myCamera.setOrientation(glm::quat_cast(camMat)); } else { _myCamera.setPosition(myAvatar->getDefaultEyePosition()); _myCamera.setOrientation(myAvatar->getHead()->getCameraOrientation()); } } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { if (isHMDMode()) { auto hmdWorldMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); _myCamera.setOrientation(glm::normalize(glm::quat_cast(hmdWorldMat))); _myCamera.setPosition(extractTranslation(hmdWorldMat) + myAvatar->getOrientation() * boomOffset); } else { _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); if (Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { _myCamera.setPosition(myAvatar->getDefaultEyePosition() + _myCamera.getOrientation() * boomOffset); } else { _myCamera.setPosition(myAvatar->getDefaultEyePosition() + myAvatar->getOrientation() * boomOffset); } } } else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { if (isHMDMode()) { auto mirrorBodyOrientation = myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f)); glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); // Mirror HMD yaw and roll glm::vec3 mirrorHmdEulers = glm::eulerAngles(hmdRotation); mirrorHmdEulers.y = -mirrorHmdEulers.y; mirrorHmdEulers.z = -mirrorHmdEulers.z; glm::quat mirrorHmdRotation = glm::quat(mirrorHmdEulers); glm::quat worldMirrorRotation = mirrorBodyOrientation * mirrorHmdRotation; _myCamera.setOrientation(worldMirrorRotation); glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); // Mirror HMD lateral offsets hmdOffset.x = -hmdOffset.x; _myCamera.setPosition(myAvatar->getDefaultEyePosition() + glm::vec3(0, _raiseMirror * myAvatar->getUniformScale(), 0) + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror + mirrorBodyOrientation * hmdOffset); } else { _myCamera.setOrientation(myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))); _myCamera.setPosition(myAvatar->getDefaultEyePosition() + glm::vec3(0, _raiseMirror * myAvatar->getUniformScale(), 0) + (myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror); } renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; } else if (_myCamera.getMode() == CAMERA_MODE_ENTITY) { EntityItemPointer cameraEntity = _myCamera.getCameraEntityPointer(); if (cameraEntity != nullptr) { if (isHMDMode()) { glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); _myCamera.setOrientation(cameraEntity->getRotation() * hmdRotation); glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); _myCamera.setPosition(cameraEntity->getPosition() + (hmdRotation * hmdOffset)); } else { _myCamera.setOrientation(cameraEntity->getRotation()); _myCamera.setPosition(cameraEntity->getPosition()); } } } // Update camera position if (!isHMDMode()) { _myCamera.update(1.0f / _frameCounter.rate()); } } getApplicationCompositor().setFrameInfo(_frameCount, _myCamera.getTransform()); // Primary rendering pass const QSize size = framebufferCache->getFrameBufferSize(); { PROFILE_RANGE(__FUNCTION__ "/mainRender"); PerformanceTimer perfTimer("mainRender"); renderArgs._boomOffset = boomOffset; // Viewport is assigned to the size of the framebuffer renderArgs._viewport = ivec4(0, 0, size.width(), size.height()); if (displayPlugin->isStereo()) { // Stereo modes will typically have a larger projection matrix overall, // so we ask for the 'mono' projection matrix, which for stereo and HMD // plugins will imply the combined projection for both eyes. // // This is properly implemented for the Oculus plugins, but for OpenVR // and Stereo displays I'm not sure how to get / calculate it, so we're // just relying on the left FOV in each case and hoping that the // overall culling margin of error doesn't cause popping in the // right eye. There are FIXMEs in the relevant plugins _myCamera.setProjection(displayPlugin->getCullingProjection(_myCamera.getProjection())); renderArgs._context->enableStereo(true); mat4 eyeOffsets[2]; mat4 eyeProjections[2]; auto baseProjection = renderArgs.getViewFrustum().getProjection(); auto hmdInterface = DependencyManager::get(); float IPDScale = hmdInterface->getIPDScale(); mat4 headPose = displayPlugin->getHeadPose(); // FIXME we probably don't need to set the projection matrix every frame, // only when the display plugin changes (or in non-HMD modes when the user // changes the FOV manually, which right now I don't think they can. for_each_eye([&](Eye eye) { // For providing the stereo eye views, the HMD head pose has already been // applied to the avatar, so we need to get the difference between the head // pose applied to the avatar and the per eye pose, and use THAT as // the per-eye stereo matrix adjustment. mat4 eyeToHead = displayPlugin->getEyeToHeadTransform(eye); // Grab the translation vec3 eyeOffset = glm::vec3(eyeToHead[3]); // Apply IPD scaling mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * IPDScale); eyeOffsets[eye] = eyeOffsetTransform; // Tell the plugin what pose we're using to render. In this case we're just using the // unmodified head pose because the only plugin that cares (the Oculus plugin) uses it // for rotational timewarp. If we move to support positonal timewarp, we need to // ensure this contains the full pose composed with the eye offsets. displayPlugin->setEyeRenderPose(_frameCount, eye, headPose * glm::inverse(eyeOffsetTransform)); eyeProjections[eye] = displayPlugin->getEyeProjection(eye, baseProjection); }); renderArgs._context->setStereoProjections(eyeProjections); renderArgs._context->setStereoViews(eyeOffsets); } renderArgs._blitFramebuffer = finalFramebuffer; displaySide(&renderArgs, _myCamera); renderArgs._blitFramebuffer.reset(); renderArgs._context->enableStereo(false); } _gpuContext->endFrame(); gpu::TexturePointer overlayTexture = _applicationOverlay.acquireOverlay(); if (overlayTexture) { displayPlugin->submitOverlayTexture(overlayTexture); } // deliver final composited scene to the display plugin { PROFILE_RANGE(__FUNCTION__ "/pluginOutput"); PerformanceTimer perfTimer("pluginOutput"); auto finalTexture = finalFramebuffer->getRenderBuffer(0); Q_ASSERT(!_lockedFramebufferMap.contains(finalTexture)); _lockedFramebufferMap[finalTexture] = finalFramebuffer; Q_ASSERT(isCurrentContext(_offscreenContext->getContext())); { PROFILE_RANGE(__FUNCTION__ "/pluginSubmitScene"); PerformanceTimer perfTimer("pluginSubmitScene"); displayPlugin->submitSceneTexture(_frameCount, finalTexture); } Q_ASSERT(isCurrentContext(_offscreenContext->getContext())); } { Stats::getInstance()->setRenderDetails(renderArgs._details); } uint64_t lastPaintDuration = usecTimestampNow() - lastPaintBegin; _frameTimingsScriptingInterface.addValue(lastPaintDuration); } void Application::runTests() { runTimingTests(); runUnitTests(); } void Application::audioMuteToggled() const { QAction* muteAction = Menu::getInstance()->getActionForOption(MenuOption::MuteAudio); Q_CHECK_PTR(muteAction); muteAction->setChecked(DependencyManager::get()->isMuted()); } void Application::faceTrackerMuteToggled() { QAction* muteAction = Menu::getInstance()->getActionForOption(MenuOption::MuteFaceTracking); Q_CHECK_PTR(muteAction); bool isMuted = getSelectedFaceTracker()->isMuted(); muteAction->setChecked(isMuted); getSelectedFaceTracker()->setEnabled(!isMuted); Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setEnabled(!isMuted); } void Application::setFieldOfView(float fov) { if (fov != _fieldOfView.get()) { _fieldOfView.set(fov); resizeGL(); } } void Application::aboutApp() { InfoView::show(INFO_HELP_PATH); } void Application::showHelp() { InfoView::show(INFO_EDIT_ENTITIES_PATH); } void Application::resizeEvent(QResizeEvent* event) { resizeGL(); } void Application::resizeGL() { PROFILE_RANGE(__FUNCTION__); if (nullptr == _displayPlugin) { return; } auto displayPlugin = getActiveDisplayPlugin(); // Set the desired FBO texture size. If it hasn't changed, this does nothing. // Otherwise, it must rebuild the FBOs uvec2 framebufferSize = displayPlugin->getRecommendedRenderSize(); uvec2 renderSize = uvec2(vec2(framebufferSize) * getRenderResolutionScale()); if (_renderResolution != renderSize) { _renderResolution = renderSize; DependencyManager::get()->setFrameBufferSize(fromGlm(renderSize)); } // FIXME the aspect ratio for stereo displays is incorrect based on this. float aspectRatio = displayPlugin->getRecommendedAspectRatio(); _myCamera.setProjection(glm::perspective(glm::radians(_fieldOfView.get()), aspectRatio, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); // Possible change in aspect ratio { QMutexLocker viewLocker(&_viewMutex); loadViewFrustum(_myCamera, _viewFrustum); } auto offscreenUi = DependencyManager::get(); auto uiSize = displayPlugin->getRecommendedUiSize(); // Bit of a hack since there's no device pixel ratio change event I can find. static qreal lastDevicePixelRatio = 0; qreal devicePixelRatio = _window->devicePixelRatio(); if (offscreenUi->size() != fromGlm(uiSize) || devicePixelRatio != lastDevicePixelRatio) { qDebug() << "Device pixel ratio changed, triggering resize"; offscreenUi->resize(fromGlm(uiSize), true); _offscreenContext->makeCurrent(); lastDevicePixelRatio = devicePixelRatio; } } bool Application::importJSONFromURL(const QString& urlString) { // we only load files that terminate in just .json (not .svo.json and not .ava.json) // if they come from the High Fidelity Marketplace Assets CDN QUrl jsonURL { urlString }; if (jsonURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { emit svoImportRequested(urlString); return true; } else { return false; } } bool Application::importSVOFromURL(const QString& urlString) { emit svoImportRequested(urlString); return true; } bool Application::event(QEvent* event) { if (!Menu::getInstance()) { return false; } // Presentation/painting logic // TODO: Decouple presentation and painting loops static bool isPaintingThrottled = false; if ((int)event->type() == (int)Present) { if (isPaintingThrottled) { // If painting (triggered by presentation) is hogging the main thread, // repost as low priority to avoid hanging the GUI. // This has the effect of allowing presentation to exceed the paint budget by X times and // only dropping every (1/X) frames, instead of every ceil(X) frames // (e.g. at a 60FPS target, painting for 17us would fall to 58.82FPS instead of 30FPS). removePostedEvents(this, Present); postEvent(this, new QEvent(static_cast(Present)), Qt::LowEventPriority); isPaintingThrottled = false; return true; } float nsecsElapsed = (float)_lastTimeUpdated.nsecsElapsed(); if (shouldPaint(nsecsElapsed)) { _lastTimeUpdated.start(); idle(nsecsElapsed); postEvent(this, new QEvent(static_cast(Paint)), Qt::HighEventPriority); } isPaintingThrottled = true; return true; } else if ((int)event->type() == (int)Paint) { // NOTE: This must be updated as close to painting as possible, // or AvatarInputs will mysteriously move to the bottom-right AvatarInputs::getInstance()->update(); paintGL(); isPaintingThrottled = false; return true; } if ((int)event->type() == (int)Lambda) { static_cast(event)->call(); return true; } if (!_keyboardFocusedItem.isInvalidID()) { switch (event->type()) { case QEvent::KeyPress: case QEvent::KeyRelease: { auto entityScriptingInterface = DependencyManager::get(); auto entity = entityScriptingInterface->getEntityTree()->findEntityByID(_keyboardFocusedItem); RenderableWebEntityItem* webEntity = dynamic_cast(entity.get()); if (webEntity && webEntity->getEventHandler()) { event->setAccepted(false); QCoreApplication::sendEvent(webEntity->getEventHandler(), event); if (event->isAccepted()) { _lastAcceptedKeyPress = usecTimestampNow(); return true; } } break; } default: break; } } switch (event->type()) { case QEvent::MouseMove: mouseMoveEvent(static_cast(event)); return true; case QEvent::MouseButtonPress: mousePressEvent(static_cast(event)); return true; case QEvent::MouseButtonDblClick: mouseDoublePressEvent(static_cast(event)); return true; case QEvent::MouseButtonRelease: mouseReleaseEvent(static_cast(event)); return true; case QEvent::KeyPress: keyPressEvent(static_cast(event)); return true; case QEvent::KeyRelease: keyReleaseEvent(static_cast(event)); return true; case QEvent::FocusOut: focusOutEvent(static_cast(event)); return true; case QEvent::TouchBegin: touchBeginEvent(static_cast(event)); event->accept(); return true; case QEvent::TouchEnd: touchEndEvent(static_cast(event)); return true; case QEvent::TouchUpdate: touchUpdateEvent(static_cast(event)); return true; case QEvent::Gesture: touchGestureEvent((QGestureEvent*)event); return true; case QEvent::Wheel: wheelEvent(static_cast(event)); return true; case QEvent::Drop: dropEvent(static_cast(event)); return true; default: break; } // handle custom URL if (event->type() == QEvent::FileOpen) { QFileOpenEvent* fileEvent = static_cast(event); QUrl url = fileEvent->url(); if (!url.isEmpty()) { QString urlString = url.toString(); if (canAcceptURL(urlString)) { return acceptURL(urlString); } } return false; } if (HFActionEvent::types().contains(event->type())) { _controllerScriptingInterface->handleMetaEvent(static_cast(event)); } return QApplication::event(event); } bool Application::eventFilter(QObject* object, QEvent* event) { if (event->type() == QEvent::Leave) { getApplicationCompositor().handleLeaveEvent(); } if (event->type() == QEvent::ShortcutOverride) { if (DependencyManager::get()->shouldSwallowShortcut(event)) { event->accept(); return true; } // Filter out captured keys before they're used for shortcut actions. if (_controllerScriptingInterface->isKeyCaptured(static_cast(event))) { event->accept(); return true; } } return false; } static bool _altPressed{ false }; void Application::keyPressEvent(QKeyEvent* event) { _altPressed = event->key() == Qt::Key_Alt; _keysPressed.insert(event->key()); _controllerScriptingInterface->emitKeyPressEvent(event); // send events to any registered scripts // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isKeyCaptured(event)) { return; } if (hasFocus()) { if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->keyPressEvent(event); } bool isShifted = event->modifiers().testFlag(Qt::ShiftModifier); bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); bool isOption = event->modifiers().testFlag(Qt::AltModifier); switch (event->key()) { case Qt::Key_Enter: case Qt::Key_Return: if (isOption) { if (_window->isFullScreen()) { unsetFullscreen(); } else { setFullscreen(nullptr); } } else { Menu::getInstance()->triggerOption(MenuOption::AddressBar); } break; case Qt::Key_1: case Qt::Key_2: case Qt::Key_3: case Qt::Key_4: case Qt::Key_5: case Qt::Key_6: case Qt::Key_7: if (isMeta || isOption) { unsigned int index = static_cast(event->key() - Qt::Key_1); auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); if (index < displayPlugins.size()) { auto targetPlugin = displayPlugins.at(index); QString targetName = targetPlugin->getName(); auto menu = Menu::getInstance(); QAction* action = menu->getActionForOption(targetName); if (action && !action->isChecked()) { action->trigger(); } } } break; case Qt::Key_X: if (isShifted && isMeta) { auto offscreenUi = DependencyManager::get(); offscreenUi->togglePinned(); //offscreenUi->getRootContext()->engine()->clearComponentCache(); //OffscreenUi::information("Debugging", "Component cache cleared"); // placeholder for dialogs being converted to QML. } break; case Qt::Key_Y: if (isShifted && isMeta) { getActiveDisplayPlugin()->cycleDebugOutput(); } break; case Qt::Key_B: if (isMeta) { auto offscreenUi = DependencyManager::get(); offscreenUi->load("Browser.qml"); } break; case Qt::Key_L: if (isShifted && isMeta) { Menu::getInstance()->triggerOption(MenuOption::Log); } else if (isMeta) { Menu::getInstance()->triggerOption(MenuOption::AddressBar); } else if (isShifted) { Menu::getInstance()->triggerOption(MenuOption::LodTools); } break; case Qt::Key_F: { _physicsEngine->dumpNextStats(); break; } case Qt::Key_Asterisk: Menu::getInstance()->triggerOption(MenuOption::Stars); break; case Qt::Key_S: if (isShifted && isMeta && !isOption) { Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); } else if (isOption && !isShifted && !isMeta) { Menu::getInstance()->triggerOption(MenuOption::ScriptEditor); } else if (!isOption && !isShifted && isMeta) { takeSnapshot(); } break; case Qt::Key_Apostrophe: { if (isMeta) { auto cursor = Cursor::Manager::instance().getCursor(); auto curIcon = cursor->getIcon(); if (curIcon == Cursor::Icon::DEFAULT) { cursor->setIcon(Cursor::Icon::LINK); } else { cursor->setIcon(Cursor::Icon::DEFAULT); } } else { resetSensors(true); } break; } case Qt::Key_Backslash: Menu::getInstance()->triggerOption(MenuOption::Chat); break; case Qt::Key_Up: if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { if (!isShifted) { _scaleMirror *= 0.95f; } else { _raiseMirror += 0.05f; } } break; case Qt::Key_Down: if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { if (!isShifted) { _scaleMirror *= 1.05f; } else { _raiseMirror -= 0.05f; } } break; case Qt::Key_Left: if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { _rotateMirror += PI / 20.0f; } break; case Qt::Key_Right: if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { _rotateMirror -= PI / 20.0f; } break; #if 0 case Qt::Key_I: if (isShifted) { _myCamera.setEyeOffsetOrientation(glm::normalize( glm::quat(glm::vec3(0.002f, 0, 0)) * _myCamera.getEyeOffsetOrientation())); } else { _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(0, 0.001, 0)); } updateProjectionMatrix(); break; case Qt::Key_K: if (isShifted) { _myCamera.setEyeOffsetOrientation(glm::normalize( glm::quat(glm::vec3(-0.002f, 0, 0)) * _myCamera.getEyeOffsetOrientation())); } else { _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(0, -0.001, 0)); } updateProjectionMatrix(); break; case Qt::Key_J: if (isShifted) { QMutexLocker viewLocker(&_viewMutex); _viewFrustum.setFocalLength(_viewFrustum.getFocalLength() - 0.1f); } else { _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(-0.001, 0, 0)); } updateProjectionMatrix(); break; case Qt::Key_M: if (isShifted) { QMutexLocker viewLocker(&_viewMutex); _viewFrustum.setFocalLength(_viewFrustum.getFocalLength() + 0.1f); } else { _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(0.001, 0, 0)); } updateProjectionMatrix(); break; case Qt::Key_U: if (isShifted) { _myCamera.setEyeOffsetOrientation(glm::normalize( glm::quat(glm::vec3(0, 0, -0.002f)) * _myCamera.getEyeOffsetOrientation())); } else { _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(0, 0, -0.001)); } updateProjectionMatrix(); break; case Qt::Key_Y: if (isShifted) { _myCamera.setEyeOffsetOrientation(glm::normalize( glm::quat(glm::vec3(0, 0, 0.002f)) * _myCamera.getEyeOffsetOrientation())); } else { _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(0, 0, 0.001)); } updateProjectionMatrix(); break; #endif case Qt::Key_H: if (isShifted) { Menu::getInstance()->triggerOption(MenuOption::MiniMirror); } else { // whenever switching to/from full screen mirror from the keyboard, remember // the state you were in before full screen mirror, and return to that. auto previousMode = _myCamera.getMode(); if (previousMode != CAMERA_MODE_MIRROR) { switch (previousMode) { case CAMERA_MODE_FIRST_PERSON: _returnFromFullScreenMirrorTo = MenuOption::FirstPerson; break; case CAMERA_MODE_THIRD_PERSON: _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; break; // FIXME - it's not clear that these modes make sense to return to... case CAMERA_MODE_INDEPENDENT: _returnFromFullScreenMirrorTo = MenuOption::IndependentMode; break; case CAMERA_MODE_ENTITY: _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode; break; default: _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; break; } } bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror); Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked); if (isMirrorChecked) { // if we got here without coming in from a non-Full Screen mirror case, then our // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old // behavior of returning to ThirdPerson if (_returnFromFullScreenMirrorTo.isEmpty()) { _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; } Menu::getInstance()->setIsOptionChecked(_returnFromFullScreenMirrorTo, true); } cameraMenuChanged(); } break; case Qt::Key_P: { bool isFirstPersonChecked = Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson); Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, !isFirstPersonChecked); Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, isFirstPersonChecked); cameraMenuChanged(); break; } case Qt::Key_Slash: Menu::getInstance()->triggerOption(MenuOption::Stats); break; case Qt::Key_Plus: { if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { auto& cursorManager = Cursor::Manager::instance(); cursorManager.setScale(cursorManager.getScale() * 1.1f); } else { getMyAvatar()->increaseSize(); } break; } case Qt::Key_Minus: { if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { auto& cursorManager = Cursor::Manager::instance(); cursorManager.setScale(cursorManager.getScale() / 1.1f); } else { getMyAvatar()->decreaseSize(); } break; } case Qt::Key_Equal: getMyAvatar()->resetSize(); break; case Qt::Key_Space: { if (!event->isAutoRepeat()) { // FIXME -- I don't think we've tested the HFActionEvent in a while... this looks possibly dubious // this starts an HFActionEvent HFActionEvent startActionEvent(HFActionEvent::startType(), computePickRay(getMouse().x, getMouse().y)); sendEvent(this, &startActionEvent); } break; } case Qt::Key_Escape: { getActiveDisplayPlugin()->abandonCalibration(); if (!event->isAutoRepeat()) { // this starts the HFCancelEvent HFBackEvent startBackEvent(HFBackEvent::startType()); sendEvent(this, &startBackEvent); } break; } default: event->ignore(); break; } } } void Application::keyReleaseEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Alt && _altPressed && hasFocus()) { toggleMenuUnderReticle(); } _keysPressed.remove(event->key()); _controllerScriptingInterface->emitKeyReleaseEvent(event); // send events to any registered scripts // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isKeyCaptured(event)) { return; } if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->keyReleaseEvent(event); } switch (event->key()) { case Qt::Key_Space: { if (!event->isAutoRepeat()) { // FIXME -- I don't think we've tested the HFActionEvent in a while... this looks possibly dubious // this ends the HFActionEvent HFActionEvent endActionEvent(HFActionEvent::endType(), computePickRay(getMouse().x, getMouse().y)); sendEvent(this, &endActionEvent); } break; } case Qt::Key_Escape: { if (!event->isAutoRepeat()) { // this ends the HFCancelEvent HFBackEvent endBackEvent(HFBackEvent::endType()); sendEvent(this, &endBackEvent); } break; } default: event->ignore(); break; } } void Application::focusOutEvent(QFocusEvent* event) { auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); foreach(auto inputPlugin, inputPlugins) { if (inputPlugin->isActive()) { inputPlugin->pluginFocusOutEvent(); } } // FIXME spacemouse code still needs cleanup #if 0 //SpacemouseDevice::getInstance().focusOutEvent(); //SpacemouseManager::getInstance().getDevice()->focusOutEvent(); SpacemouseManager::getInstance().ManagerFocusOutEvent(); #endif // synthesize events for keys currently pressed, since we may not get their release events foreach (int key, _keysPressed) { QKeyEvent keyEvent(QEvent::KeyRelease, key, Qt::NoModifier); keyReleaseEvent(&keyEvent); } _keysPressed.clear(); } void Application::maybeToggleMenuVisible(QMouseEvent* event) const { #ifndef Q_OS_MAC // If in full screen, and our main windows menu bar is hidden, and we're close to the top of the QMainWindow // then show the menubar. if (_window->isFullScreen()) { QMenuBar* menuBar = _window->menuBar(); if (menuBar) { static const int MENU_TOGGLE_AREA = 10; if (!menuBar->isVisible()) { if (event->pos().y() <= MENU_TOGGLE_AREA) { menuBar->setVisible(true); } } else { if (event->pos().y() > MENU_TOGGLE_AREA) { menuBar->setVisible(false); } } } } #endif } void Application::mouseMoveEvent(QMouseEvent* event) { PROFILE_RANGE(__FUNCTION__); if (_aboutToQuit) { return; } maybeToggleMenuVisible(event); auto& compositor = getApplicationCompositor(); // if this is a real mouse event, and we're in HMD mode, then we should use it to move the // compositor reticle // handleRealMouseMoveEvent() will return true, if we shouldn't process the event further if (!compositor.fakeEventActive() && compositor.handleRealMouseMoveEvent()) { return; // bail } auto offscreenUi = DependencyManager::get(); auto eventPosition = compositor.getMouseEventPosition(event); QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition, _glWidget); auto button = event->button(); auto buttons = event->buttons(); // Determine if the ReticleClick Action is 1 and if so, fake include the LeftMouseButton if (_reticleClickPressed) { if (button == Qt::NoButton) { button = Qt::LeftButton; } buttons |= Qt::LeftButton; } QMouseEvent mappedEvent(event->type(), transformedPos, event->screenPos(), button, buttons, event->modifiers()); getEntities()->mouseMoveEvent(&mappedEvent); _controllerScriptingInterface->emitMouseMoveEvent(&mappedEvent); // send events to any registered scripts // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isMouseCaptured()) { return; } if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->mouseMoveEvent(event); } } void Application::mousePressEvent(QMouseEvent* event) { // Inhibit the menu if the user is using alt-mouse dragging _altPressed = false; auto offscreenUi = DependencyManager::get(); // If we get a mouse press event it means it wasn't consumed by the offscreen UI, // hence, we should defocus all of the offscreen UI windows, in order to allow // keyboard shortcuts not to be swallowed by them. In particular, WebEngineViews // will consume all keyboard events. offscreenUi->unfocusWindows(); auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition, _glWidget); QMouseEvent mappedEvent(event->type(), transformedPos, event->screenPos(), event->button(), event->buttons(), event->modifiers()); if (!_aboutToQuit) { getEntities()->mousePressEvent(&mappedEvent); } _controllerScriptingInterface->emitMousePressEvent(&mappedEvent); // send events to any registered scripts // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isMouseCaptured()) { return; } if (hasFocus()) { if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->mousePressEvent(event); } if (event->button() == Qt::LeftButton) { // nobody handled this - make it an action event on the _window object HFActionEvent actionEvent(HFActionEvent::startType(), computePickRay(mappedEvent.x(), mappedEvent.y())); sendEvent(this, &actionEvent); } } } void Application::mouseDoublePressEvent(QMouseEvent* event) const { // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isMouseCaptured()) { return; } _controllerScriptingInterface->emitMouseDoublePressEvent(event); } void Application::mouseReleaseEvent(QMouseEvent* event) { auto offscreenUi = DependencyManager::get(); auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition, _glWidget); QMouseEvent mappedEvent(event->type(), transformedPos, event->screenPos(), event->button(), event->buttons(), event->modifiers()); if (!_aboutToQuit) { getEntities()->mouseReleaseEvent(&mappedEvent); } _controllerScriptingInterface->emitMouseReleaseEvent(&mappedEvent); // send events to any registered scripts // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isMouseCaptured()) { return; } if (hasFocus()) { if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->mouseReleaseEvent(event); } if (event->button() == Qt::LeftButton) { // fire an action end event HFActionEvent actionEvent(HFActionEvent::endType(), computePickRay(mappedEvent.x(), mappedEvent.y())); sendEvent(this, &actionEvent); } } } void Application::touchUpdateEvent(QTouchEvent* event) { _altPressed = false; if (event->type() == QEvent::TouchUpdate) { TouchEvent thisEvent(*event, _lastTouchEvent); _controllerScriptingInterface->emitTouchUpdateEvent(thisEvent); // send events to any registered scripts _lastTouchEvent = thisEvent; } // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isTouchCaptured()) { return; } if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->touchUpdateEvent(event); } if (_touchscreenDevice && _touchscreenDevice->isActive()) { _touchscreenDevice->touchUpdateEvent(event); } } void Application::touchBeginEvent(QTouchEvent* event) { _altPressed = false; TouchEvent thisEvent(*event); // on touch begin, we don't compare to last event _controllerScriptingInterface->emitTouchBeginEvent(thisEvent); // send events to any registered scripts _lastTouchEvent = thisEvent; // and we reset our last event to this event before we call our update touchUpdateEvent(event); // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isTouchCaptured()) { return; } if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->touchBeginEvent(event); } if (_touchscreenDevice && _touchscreenDevice->isActive()) { _touchscreenDevice->touchBeginEvent(event); } } void Application::touchEndEvent(QTouchEvent* event) { _altPressed = false; TouchEvent thisEvent(*event, _lastTouchEvent); _controllerScriptingInterface->emitTouchEndEvent(thisEvent); // send events to any registered scripts _lastTouchEvent = thisEvent; // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isTouchCaptured()) { return; } if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->touchEndEvent(event); } if (_touchscreenDevice && _touchscreenDevice->isActive()) { _touchscreenDevice->touchEndEvent(event); } // put any application specific touch behavior below here.. } void Application::touchGestureEvent(QGestureEvent* event) { if (_touchscreenDevice && _touchscreenDevice->isActive()) { _touchscreenDevice->touchGestureEvent(event); } } void Application::wheelEvent(QWheelEvent* event) const { _altPressed = false; _controllerScriptingInterface->emitWheelEvent(event); // send events to any registered scripts // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isWheelCaptured()) { return; } if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->wheelEvent(event); } } void Application::dropEvent(QDropEvent *event) { const QMimeData* mimeData = event->mimeData(); for (auto& url : mimeData->urls()) { QString urlString = url.toString(); if (acceptURL(urlString, true)) { event->acceptProposedAction(); } } } void Application::dragEnterEvent(QDragEnterEvent* event) { event->acceptProposedAction(); } bool Application::acceptSnapshot(const QString& urlString) { QUrl url(urlString); QString snapshotPath = url.toLocalFile(); SnapshotMetaData* snapshotData = Snapshot::parseSnapshotData(snapshotPath); if (snapshotData) { if (!snapshotData->getURL().toString().isEmpty()) { DependencyManager::get()->handleLookupString(snapshotData->getURL().toString()); } } else { OffscreenUi::warning("", "No location details were found in the file\n" + snapshotPath + "\nTry dragging in an authentic Hifi snapshot."); } return true; } static uint32_t _renderedFrameIndex { INVALID_FRAME }; bool Application::shouldPaint(float nsecsElapsed) { if (_aboutToQuit) { return false; } auto displayPlugin = getActiveDisplayPlugin(); #ifdef DEBUG_PAINT_DELAY static uint64_t paintDelaySamples{ 0 }; static uint64_t paintDelayUsecs{ 0 }; paintDelayUsecs += displayPlugin->getPaintDelayUsecs(); static const int PAINT_DELAY_THROTTLE = 1000; if (++paintDelaySamples % PAINT_DELAY_THROTTLE == 0) { qCDebug(interfaceapp).nospace() << "Paint delay (" << paintDelaySamples << " samples): " << (float)paintDelaySamples / paintDelayUsecs << "us"; } #endif float msecondsSinceLastUpdate = nsecsElapsed / NSECS_PER_USEC / USECS_PER_MSEC; // Throttle if requested if (displayPlugin->isThrottled() && (msecondsSinceLastUpdate < THROTTLED_SIM_FRAME_PERIOD_MS)) { return false; } // Sync up the _renderedFrameIndex _renderedFrameIndex = displayPlugin->presentCount(); return true; } void Application::idle(float nsecsElapsed) { // Update the deadlock watchdog updateHeartbeat(); auto offscreenUi = DependencyManager::get(); // These tasks need to be done on our first idle, because we don't want the showing of // overlay subwindows to do a showDesktop() until after the first time through static bool firstIdle = true; if (firstIdle) { firstIdle = false; connect(offscreenUi.data(), &OffscreenUi::showDesktop, this, &Application::showDesktop); } PROFILE_RANGE(__FUNCTION__); float secondsSinceLastUpdate = nsecsElapsed / NSECS_PER_MSEC / MSECS_PER_SECOND; // If the offscreen Ui has something active that is NOT the root, then assume it has keyboard focus. if (_keyboardDeviceHasFocus && offscreenUi && offscreenUi->getWindow()->activeFocusItem() != offscreenUi->getRootItem()) { _keyboardMouseDevice->pluginFocusOutEvent(); _keyboardDeviceHasFocus = false; } else if (offscreenUi && offscreenUi->getWindow()->activeFocusItem() == offscreenUi->getRootItem()) { _keyboardDeviceHasFocus = true; } checkChangeCursor(); Stats::getInstance()->updateStats(); _simCounter.increment(); PerformanceTimer perfTimer("idle"); // Drop focus from _keyboardFocusedItem if no keyboard messages for 30 seconds if (!_keyboardFocusedItem.isInvalidID()) { const quint64 LOSE_FOCUS_AFTER_ELAPSED_TIME = 30 * USECS_PER_SECOND; // if idle for 30 seconds, drop focus quint64 elapsedSinceAcceptedKeyPress = usecTimestampNow() - _lastAcceptedKeyPress; if (elapsedSinceAcceptedKeyPress > LOSE_FOCUS_AFTER_ELAPSED_TIME) { _keyboardFocusedItem = UNKNOWN_ENTITY_ID; } } // Normally we check PipelineWarnings, but since idle will often take more than 10ms we only show these idle timing // details if we're in ExtraDebugging mode. However, the ::update() and its subcomponents will show their timing // details normally. bool showWarnings = getLogger()->extraDebugging(); PerformanceWarning warn(showWarnings, "idle()"); { PerformanceTimer perfTimer("update"); PerformanceWarning warn(showWarnings, "Application::idle()... update()"); static const float BIGGEST_DELTA_TIME_SECS = 0.25f; update(glm::clamp(secondsSinceLastUpdate, 0.0f, BIGGEST_DELTA_TIME_SECS)); } { PerformanceTimer perfTimer("pluginIdle"); PerformanceWarning warn(showWarnings, "Application::idle()... pluginIdle()"); getActiveDisplayPlugin()->idle(); auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); foreach(auto inputPlugin, inputPlugins) { if (inputPlugin->isActive()) { inputPlugin->idle(); } } } { PerformanceTimer perfTimer("rest"); PerformanceWarning warn(showWarnings, "Application::idle()... rest of it"); _idleLoopStdev.addValue(secondsSinceLastUpdate); // Record standard deviation and reset counter if needed const int STDEV_SAMPLES = 500; if (_idleLoopStdev.getSamples() > STDEV_SAMPLES) { _idleLoopMeasuredJitter = _idleLoopStdev.getStDev(); _idleLoopStdev.reset(); } } _overlayConductor.update(secondsSinceLastUpdate); } void Application::setLowVelocityFilter(bool lowVelocityFilter) { controller::InputDevice::setLowVelocityFilter(lowVelocityFilter); } ivec2 Application::getMouse() const { return getApplicationCompositor().getReticlePosition(); } FaceTracker* Application::getActiveFaceTracker() { auto faceshift = DependencyManager::get(); auto dde = DependencyManager::get(); return (dde->isActive() ? static_cast(dde.data()) : (faceshift->isActive() ? static_cast(faceshift.data()) : nullptr)); } FaceTracker* Application::getSelectedFaceTracker() { FaceTracker* faceTracker = nullptr; #ifdef HAVE_FACESHIFT if (Menu::getInstance()->isOptionChecked(MenuOption::Faceshift)) { faceTracker = DependencyManager::get().data(); } #endif #ifdef HAVE_DDE if (Menu::getInstance()->isOptionChecked(MenuOption::UseCamera)) { faceTracker = DependencyManager::get().data(); } #endif return faceTracker; } void Application::setActiveFaceTracker() const { #if defined(HAVE_FACESHIFT) || defined(HAVE_DDE) bool isMuted = Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking); #endif #ifdef HAVE_FACESHIFT auto faceshiftTracker = DependencyManager::get(); faceshiftTracker->setIsMuted(isMuted); faceshiftTracker->setEnabled(Menu::getInstance()->isOptionChecked(MenuOption::Faceshift) && !isMuted); #endif #ifdef HAVE_DDE bool isUsingDDE = Menu::getInstance()->isOptionChecked(MenuOption::UseCamera); Menu::getInstance()->getActionForOption(MenuOption::BinaryEyelidControl)->setVisible(isUsingDDE); Menu::getInstance()->getActionForOption(MenuOption::CoupleEyelids)->setVisible(isUsingDDE); Menu::getInstance()->getActionForOption(MenuOption::UseAudioForMouth)->setVisible(isUsingDDE); Menu::getInstance()->getActionForOption(MenuOption::VelocityFilter)->setVisible(isUsingDDE); Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setVisible(isUsingDDE); auto ddeTracker = DependencyManager::get(); ddeTracker->setIsMuted(isMuted); ddeTracker->setEnabled(isUsingDDE && !isMuted); #endif } #ifdef HAVE_IVIEWHMD void Application::setActiveEyeTracker() { auto eyeTracker = DependencyManager::get(); if (!eyeTracker->isInitialized()) { return; } bool isEyeTracking = Menu::getInstance()->isOptionChecked(MenuOption::SMIEyeTracking); bool isSimulating = Menu::getInstance()->isOptionChecked(MenuOption::SimulateEyeTracking); eyeTracker->setEnabled(isEyeTracking, isSimulating); Menu::getInstance()->getActionForOption(MenuOption::OnePointCalibration)->setEnabled(isEyeTracking && !isSimulating); Menu::getInstance()->getActionForOption(MenuOption::ThreePointCalibration)->setEnabled(isEyeTracking && !isSimulating); Menu::getInstance()->getActionForOption(MenuOption::FivePointCalibration)->setEnabled(isEyeTracking && !isSimulating); } void Application::calibrateEyeTracker1Point() { DependencyManager::get()->calibrate(1); } void Application::calibrateEyeTracker3Points() { DependencyManager::get()->calibrate(3); } void Application::calibrateEyeTracker5Points() { DependencyManager::get()->calibrate(5); } #endif bool Application::exportEntities(const QString& filename, const QVector& entityIDs, const glm::vec3* givenOffset) { QHash entities; auto entityTree = getEntities()->getTree(); auto exportTree = std::make_shared(); exportTree->createRootElement(); glm::vec3 root(TREE_SCALE, TREE_SCALE, TREE_SCALE); bool success = true; entityTree->withReadLock([&] { for (auto entityID : entityIDs) { // Gather entities and properties. auto entityItem = entityTree->findEntityByEntityItemID(entityID); if (!entityItem) { qCWarning(interfaceapp) << "Skipping export of" << entityID << "that is not in scene."; continue; } if (!givenOffset) { EntityItemID parentID = entityItem->getParentID(); if (parentID.isInvalidID() || !entityIDs.contains(parentID) || !entityTree->findEntityByEntityItemID(parentID)) { auto position = entityItem->getPosition(); // If parent wasn't selected, we want absolute position, which isn't in properties. root.x = glm::min(root.x, position.x); root.y = glm::min(root.y, position.y); root.z = glm::min(root.z, position.z); } } entities[entityID] = entityItem; } if (entities.size() == 0) { success = false; return; } if (givenOffset) { root = *givenOffset; } for (EntityItemPointer& entityDatum : entities) { auto properties = entityDatum->getProperties(); EntityItemID parentID = properties.getParentID(); if (parentID.isInvalidID()) { properties.setPosition(properties.getPosition() - root); } else if (!entities.contains(parentID)) { entityDatum->globalizeProperties(properties, "Parent %3 of %2 %1 is not selected for export.", -root); } // else valid parent -- don't offset exportTree->addEntity(entityDatum->getEntityItemID(), properties); } }); if (success) { success = exportTree->writeToJSONFile(filename.toLocal8Bit().constData()); // restore the main window's active state _window->activateWindow(); } return success; } bool Application::exportEntities(const QString& filename, float x, float y, float z, float scale) { glm::vec3 center(x, y, z); glm::vec3 minCorner = center - vec3(scale); float cubeSize = scale * 2; AACube boundingCube(minCorner, cubeSize); QVector entities; QVector ids; auto entityTree = getEntities()->getTree(); entityTree->withReadLock([&] { entityTree->findEntities(boundingCube, entities); foreach(EntityItemPointer entity, entities) { ids << entity->getEntityItemID(); } }); return exportEntities(filename, ids, ¢er); } void Application::loadSettings() { sessionRunTime.set(0); // Just clean living. We're about to saveSettings, which will update value. DependencyManager::get()->loadSettings(); DependencyManager::get()->loadSettings(); // DONT CHECK IN //DependencyManager::get()->setAutomaticLODAdjust(false); Menu::getInstance()->loadSettings(); // If there is a preferred plugin, we probably messed it up with the menu settings, so fix it. auto pluginManager = PluginManager::getInstance(); auto plugins = pluginManager->getPreferredDisplayPlugins(); for (auto plugin : plugins) { auto menu = Menu::getInstance(); if (auto action = menu->getActionForOption(plugin->getName())) { action->setChecked(true); action->trigger(); // Find and activated highest priority plugin, bail for the rest break; } } auto inputs = pluginManager->getInputPlugins(); for (auto plugin : inputs) { if (!plugin->isActive()) { plugin->activate(); } } getMyAvatar()->loadData(); _settingsLoaded = true; } void Application::saveSettings() const { sessionRunTime.set(_sessionRunTimer.elapsed() / MSECS_PER_SECOND); DependencyManager::get()->saveSettings(); DependencyManager::get()->saveSettings(); Menu::getInstance()->saveSettings(); getMyAvatar()->saveData(); PluginManager::getInstance()->saveSettings(); } bool Application::importEntities(const QString& urlOrFilename) { bool success = false; _entityClipboard->withWriteLock([&] { _entityClipboard->eraseAllOctreeElements(); success = _entityClipboard->readFromURL(urlOrFilename); if (success) { _entityClipboard->reaverageOctreeElements(); } }); return success; } QVector Application::pasteEntities(float x, float y, float z) { return _entityClipboard->sendEntities(&_entityEditSender, getEntities()->getTree(), x, y, z); } void Application::initDisplay() { } void Application::init() { // Make sure Login state is up to date DependencyManager::get()->toggleLoginDialog(); DependencyManager::get()->init(); DependencyManager::get()->init(); _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); _mirrorCamera.setMode(CAMERA_MODE_MIRROR); _timerStart.start(); _lastTimeUpdated.start(); // when --url in command line, teleport to location const QString HIFI_URL_COMMAND_LINE_KEY = "--url"; int urlIndex = arguments().indexOf(HIFI_URL_COMMAND_LINE_KEY); QString addressLookupString; if (urlIndex != -1) { addressLookupString = arguments().value(urlIndex + 1); } Setting::Handle firstRun { Settings::firstRun, true }; if (addressLookupString.isEmpty() && firstRun.get()) { qDebug() << "First run and no URL passed... attempting to go to Home or Entry..."; DependencyManager::get()->ifLocalSandboxRunningElse([](){ qDebug() << "Home sandbox appears to be running, going to Home."; DependencyManager::get()->goToLocalSandbox(); }, [](){ qDebug() << "Home sandbox does not appear to be running, going to Entry."; DependencyManager::get()->goToEntry(); }); } else { qDebug() << "Not first run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("previous location") : addressLookupString); DependencyManager::get()->loadSettings(addressLookupString); } qCDebug(interfaceapp) << "Loaded settings"; Leapmotion::init(); // fire off an immediate domain-server check in now that settings are loaded DependencyManager::get()->sendDomainServerCheckIn(); getEntities()->init(); { QMutexLocker viewLocker(&_viewMutex); getEntities()->setViewFrustum(_viewFrustum); } getEntities()->setEntityLoadingPriorityFunction([this](const EntityItem& item) { auto dims = item.getDimensions(); auto maxSize = glm::max(dims.x, dims.y, dims.z); if (maxSize <= 0.0f) { return 0.0f; } auto distance = glm::distance(getMyAvatar()->getPosition(), item.getPosition()); return atan2(maxSize, distance); }); ObjectMotionState::setShapeManager(&_shapeManager); _physicsEngine->init(); EntityTreePointer tree = getEntities()->getTree(); _entitySimulation->init(tree, _physicsEngine, &_entityEditSender); tree->setSimulation(_entitySimulation); auto entityScriptingInterface = DependencyManager::get(); // connect the _entityCollisionSystem to our EntityTreeRenderer since that's what handles running entity scripts connect(_entitySimulation.get(), &EntitySimulation::entityCollisionWithEntity, getEntities(), &EntityTreeRenderer::entityCollisionWithEntity); // connect the _entities (EntityTreeRenderer) to our script engine's EntityScriptingInterface for firing // of events related clicking, hovering over, and entering entities getEntities()->connectSignalsToSlots(entityScriptingInterface.data()); _entityClipboardRenderer.init(); { QMutexLocker viewLocker(&_viewMutex); _entityClipboardRenderer.setViewFrustum(_viewFrustum); } _entityClipboardRenderer.setTree(_entityClipboard); // Make sure any new sounds are loaded as soon as know about them. connect(tree.get(), &EntityTree::newCollisionSoundURL, this, [this](QUrl newURL, EntityItemID id) { EntityTreePointer tree = getEntities()->getTree(); if (auto entity = tree->findEntityByEntityItemID(id)) { auto sound = DependencyManager::get()->getSound(newURL); entity->setCollisionSound(sound); } }, Qt::QueuedConnection); connect(getMyAvatar(), &MyAvatar::newCollisionSoundURL, this, [this](QUrl newURL) { if (auto avatar = getMyAvatar()) { auto sound = DependencyManager::get()->getSound(newURL); avatar->setCollisionSound(sound); } }, Qt::QueuedConnection); } void Application::updateLOD() const { PerformanceTimer perfTimer("LOD"); // adjust it unless we were asked to disable this feature, or if we're currently in throttleRendering mode if (!isThrottleRendering()) { DependencyManager::get()->autoAdjustLOD(_frameCounter.rate()); } else { DependencyManager::get()->resetLODAdjust(); } } void Application::pushPostUpdateLambda(void* key, std::function func) { std::unique_lock guard(_postUpdateLambdasLock); _postUpdateLambdas[key] = func; } // Called during Application::update immediately before AvatarManager::updateMyAvatar, updating my data that is then sent to everyone. // (Maybe this code should be moved there?) // The principal result is to call updateLookAtTargetAvatar() and then setLookAtPosition(). // Note that it is called BEFORE we update position or joints based on sensors, etc. void Application::updateMyAvatarLookAtPosition() { PerformanceTimer perfTimer("lookAt"); bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateMyAvatarLookAtPosition()"); auto myAvatar = getMyAvatar(); myAvatar->updateLookAtTargetAvatar(); FaceTracker* faceTracker = getActiveFaceTracker(); auto eyeTracker = DependencyManager::get(); bool isLookingAtSomeone = false; bool isHMD = qApp->isHMDMode(); glm::vec3 lookAtSpot; if (eyeTracker->isTracking() && (isHMD || eyeTracker->isSimulating())) { // Look at the point that the user is looking at. glm::vec3 lookAtPosition = eyeTracker->getLookAtPosition(); if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { lookAtPosition.x = -lookAtPosition.x; } if (isHMD) { glm::mat4 headPose = getActiveDisplayPlugin()->getHeadPose(); glm::quat hmdRotation = glm::quat_cast(headPose); lookAtSpot = _myCamera.getPosition() + myAvatar->getOrientation() * (hmdRotation * lookAtPosition); } else { lookAtSpot = myAvatar->getHead()->getEyePosition() + (myAvatar->getHead()->getFinalOrientationInWorldFrame() * lookAtPosition); } } else { AvatarSharedPointer lookingAt = myAvatar->getLookAtTargetAvatar().lock(); if (lookingAt && myAvatar != lookingAt.get()) { // If I am looking at someone else, look directly at one of their eyes isLookingAtSomeone = true; auto lookingAtHead = static_pointer_cast(lookingAt)->getHead(); const float MAXIMUM_FACE_ANGLE = 65.0f * RADIANS_PER_DEGREE; glm::vec3 lookingAtFaceOrientation = lookingAtHead->getFinalOrientationInWorldFrame() * IDENTITY_FRONT; glm::vec3 fromLookingAtToMe = glm::normalize(myAvatar->getHead()->getEyePosition() - lookingAtHead->getEyePosition()); float faceAngle = glm::angle(lookingAtFaceOrientation, fromLookingAtToMe); if (faceAngle < MAXIMUM_FACE_ANGLE) { // Randomly look back and forth between look targets eyeContactTarget target = Menu::getInstance()->isOptionChecked(MenuOption::FixGaze) ? LEFT_EYE : myAvatar->getEyeContactTarget(); switch (target) { case LEFT_EYE: lookAtSpot = lookingAtHead->getLeftEyePosition(); break; case RIGHT_EYE: lookAtSpot = lookingAtHead->getRightEyePosition(); break; case MOUTH: lookAtSpot = lookingAtHead->getMouthPosition(); break; } } else { // Just look at their head (mid point between eyes) lookAtSpot = lookingAtHead->getEyePosition(); } } else { // I am not looking at anyone else, so just look forward if (isHMD) { glm::mat4 worldHMDMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); lookAtSpot = transformPoint(worldHMDMat, glm::vec3(0.0f, 0.0f, -TREE_SCALE)); } else { lookAtSpot = myAvatar->getHead()->getEyePosition() + (myAvatar->getHead()->getFinalOrientationInWorldFrame() * glm::vec3(0.0f, 0.0f, -TREE_SCALE)); } } // Deflect the eyes a bit to match the detected gaze from the face tracker if active. if (faceTracker && !faceTracker->isMuted()) { float eyePitch = faceTracker->getEstimatedEyePitch(); float eyeYaw = faceTracker->getEstimatedEyeYaw(); const float GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT = 0.1f; glm::vec3 origin = myAvatar->getHead()->getEyePosition(); float deflection = faceTracker->getEyeDeflection(); if (isLookingAtSomeone) { deflection *= GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT; } lookAtSpot = origin + _myCamera.getOrientation() * glm::quat(glm::radians(glm::vec3( eyePitch * deflection, eyeYaw * deflection, 0.0f))) * glm::inverse(_myCamera.getOrientation()) * (lookAtSpot - origin); } } myAvatar->getHead()->setLookAtPosition(lookAtSpot); } void Application::updateThreads(float deltaTime) { PerformanceTimer perfTimer("updateThreads"); bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateThreads()"); // parse voxel packets if (!_enableProcessOctreeThread) { _octreeProcessor.threadRoutine(); _entityEditSender.threadRoutine(); } } void Application::toggleOverlays() { auto menu = Menu::getInstance(); menu->setIsOptionChecked(MenuOption::Overlays, menu->isOptionChecked(MenuOption::Overlays)); } void Application::setOverlaysVisible(bool visible) { auto menu = Menu::getInstance(); menu->setIsOptionChecked(MenuOption::Overlays, true); } void Application::centerUI() { _overlayConductor.centerUI(); } void Application::cycleCamera() { auto menu = Menu::getInstance(); if (menu->isOptionChecked(MenuOption::FullscreenMirror)) { menu->setIsOptionChecked(MenuOption::FullscreenMirror, false); menu->setIsOptionChecked(MenuOption::FirstPerson, true); } else if (menu->isOptionChecked(MenuOption::FirstPerson)) { menu->setIsOptionChecked(MenuOption::FirstPerson, false); menu->setIsOptionChecked(MenuOption::ThirdPerson, true); } else if (menu->isOptionChecked(MenuOption::ThirdPerson)) { menu->setIsOptionChecked(MenuOption::ThirdPerson, false); menu->setIsOptionChecked(MenuOption::FullscreenMirror, true); } else if (menu->isOptionChecked(MenuOption::IndependentMode) || menu->isOptionChecked(MenuOption::CameraEntityMode)) { // do nothing if in independent or camera entity modes return; } cameraMenuChanged(); // handle the menu change } void Application::cameraMenuChanged() { if (Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { if (_myCamera.getMode() != CAMERA_MODE_MIRROR) { _myCamera.setMode(CAMERA_MODE_MIRROR); } } else if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) { if (_myCamera.getMode() != CAMERA_MODE_FIRST_PERSON) { _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); } } else if (Menu::getInstance()->isOptionChecked(MenuOption::ThirdPerson)) { if (_myCamera.getMode() != CAMERA_MODE_THIRD_PERSON) { _myCamera.setMode(CAMERA_MODE_THIRD_PERSON); if (getMyAvatar()->getBoomLength() == MyAvatar::ZOOM_MIN) { getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT); } } } else if (Menu::getInstance()->isOptionChecked(MenuOption::IndependentMode)) { if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { _myCamera.setMode(CAMERA_MODE_INDEPENDENT); } } else if (Menu::getInstance()->isOptionChecked(MenuOption::CameraEntityMode)) { if (_myCamera.getMode() != CAMERA_MODE_ENTITY) { _myCamera.setMode(CAMERA_MODE_ENTITY); } } } void Application::resetPhysicsReadyInformation() { // we've changed domains or cleared out caches or something. we no longer know enough about the // collision information of nearby entities to make running bullet be safe. _fullSceneReceivedCounter = 0; _fullSceneCounterAtLastPhysicsCheck = 0; _nearbyEntitiesCountAtLastPhysicsCheck = 0; _nearbyEntitiesStabilityCount = 0; _physicsEnabled = false; } void Application::reloadResourceCaches() { resetPhysicsReadyInformation(); { QMutexLocker viewLocker(&_viewMutex); _viewFrustum.setPosition(glm::vec3(0.0f, 0.0f, TREE_SCALE)); _viewFrustum.setOrientation(glm::quat()); } // Clear entities out of view frustum queryOctree(NodeType::EntityServer, PacketType::EntityQuery, _entityServerJurisdictions); DependencyManager::get()->clearCache(); DependencyManager::get()->refreshAll(); DependencyManager::get()->refreshAll(); DependencyManager::get()->refreshAll(); DependencyManager::get()->refreshAll(); DependencyManager::get()->reset(); // Force redownload of .fst models getMyAvatar()->resetFullAvatarURL(); } void Application::rotationModeChanged() const { if (!Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { getMyAvatar()->setHeadPitch(0); } } void Application::updateDialogs(float deltaTime) const { PerformanceTimer perfTimer("updateDialogs"); bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateDialogs()"); auto dialogsManager = DependencyManager::get(); // Update audio stats dialog, if any AudioStatsDialog* audioStatsDialog = dialogsManager->getAudioStatsDialog(); if(audioStatsDialog) { audioStatsDialog->update(); } // Update bandwidth dialog, if any BandwidthDialog* bandwidthDialog = dialogsManager->getBandwidthDialog(); if (bandwidthDialog) { bandwidthDialog->update(); } QPointer octreeStatsDialog = dialogsManager->getOctreeStatsDialog(); if (octreeStatsDialog) { octreeStatsDialog->update(); } } void Application::update(float deltaTime) { PROFILE_RANGE_EX(__FUNCTION__, 0xffff0000, (uint64_t)_frameCount + 1); bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::update()"); updateLOD(); if (!_physicsEnabled) { // we haven't yet enabled physics. we wait until we think we have all the collision information // for nearby entities before starting bullet up. quint64 now = usecTimestampNow(); bool timeout = false; const int PHYSICS_CHECK_TIMEOUT = 2 * USECS_PER_SECOND; if (_lastPhysicsCheckTime > 0 && now - _lastPhysicsCheckTime > PHYSICS_CHECK_TIMEOUT) { timeout = true; } if (timeout || _fullSceneReceivedCounter > _fullSceneCounterAtLastPhysicsCheck) { // we've received a new full-scene octree stats packet, or it's been long enough to try again anyway _lastPhysicsCheckTime = now; _fullSceneCounterAtLastPhysicsCheck = _fullSceneReceivedCounter; // process octree stats packets are sent in between full sends of a scene (this isn't currently true). // We keep physics disabled until we've received a full scene and everything near the avatar in that // scene is ready to compute its collision shape. if (nearbyEntitiesAreReadyForPhysics()) { _physicsEnabled = true; getMyAvatar()->updateMotionBehaviorFromMenu(); } else { auto characterController = getMyAvatar()->getCharacterController(); if (characterController) { // if we have a character controller, disable it here so the avatar doesn't get stuck due to // a non-loading collision hull. characterController->setEnabled(false); } } } } { PerformanceTimer perfTimer("devices"); DeviceTracker::updateAll(); FaceTracker* tracker = getSelectedFaceTracker(); if (tracker && Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking) != tracker->isMuted()) { tracker->toggleMute(); } tracker = getActiveFaceTracker(); if (tracker && !tracker->isMuted()) { tracker->update(deltaTime); // Auto-mute microphone after losing face tracking? if (tracker->isTracking()) { _lastFaceTrackerUpdate = usecTimestampNow(); } else { const quint64 MUTE_MICROPHONE_AFTER_USECS = 5000000; //5 secs Menu* menu = Menu::getInstance(); if (menu->isOptionChecked(MenuOption::AutoMuteAudio) && !menu->isOptionChecked(MenuOption::MuteAudio)) { if (_lastFaceTrackerUpdate > 0 && ((usecTimestampNow() - _lastFaceTrackerUpdate) > MUTE_MICROPHONE_AFTER_USECS)) { menu->triggerOption(MenuOption::MuteAudio); _lastFaceTrackerUpdate = 0; } } else { _lastFaceTrackerUpdate = 0; } } } else { _lastFaceTrackerUpdate = 0; } } auto myAvatar = getMyAvatar(); auto userInputMapper = DependencyManager::get(); controller::InputCalibrationData calibrationData = { myAvatar->getSensorToWorldMatrix(), createMatFromQuatAndPos(myAvatar->getOrientation(), myAvatar->getPosition()), myAvatar->getHMDSensorMatrix() }; InputPluginPointer keyboardMousePlugin; for (auto inputPlugin : PluginManager::getInstance()->getInputPlugins()) { if (inputPlugin->getName() == KeyboardMouseDevice::NAME) { keyboardMousePlugin = inputPlugin; } else if (inputPlugin->isActive()) { inputPlugin->pluginUpdate(deltaTime, calibrationData); } } userInputMapper->update(deltaTime); if (keyboardMousePlugin && keyboardMousePlugin->isActive()) { keyboardMousePlugin->pluginUpdate(deltaTime, calibrationData); } _controllerScriptingInterface->updateInputControllers(); // Transfer the user inputs to the driveKeys // FIXME can we drop drive keys and just have the avatar read the action states directly? myAvatar->clearDriveKeys(); if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { if (!_controllerScriptingInterface->areActionsCaptured()) { myAvatar->setDriveKeys(TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); myAvatar->setDriveKeys(TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); myAvatar->setDriveKeys(TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); if (deltaTime > FLT_EPSILON) { myAvatar->setDriveKeys(PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); myAvatar->setDriveKeys(YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); myAvatar->setDriveKeys(STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); } } myAvatar->setDriveKeys(ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); } controller::Pose leftHandPose = userInputMapper->getPoseState(controller::Action::LEFT_HAND); controller::Pose rightHandPose = userInputMapper->getPoseState(controller::Action::RIGHT_HAND); auto myAvatarMatrix = createMatFromQuatAndPos(myAvatar->getOrientation(), myAvatar->getPosition()); auto worldToSensorMatrix = glm::inverse(myAvatar->getSensorToWorldMatrix()); auto avatarToSensorMatrix = worldToSensorMatrix * myAvatarMatrix; myAvatar->setHandControllerPosesInSensorFrame(leftHandPose.transform(avatarToSensorMatrix), rightHandPose.transform(avatarToSensorMatrix)); updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... updateDialogs(deltaTime); // update various stats dialogs if present QSharedPointer avatarManager = DependencyManager::get(); if (_physicsEnabled) { PROFILE_RANGE_EX("Physics", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); PerformanceTimer perfTimer("physics"); { PROFILE_RANGE_EX("UpdateStats", 0xffffff00, (uint64_t)getActiveDisplayPlugin()->presentCount()); PerformanceTimer perfTimer("updateStates)"); static VectorOfMotionStates motionStates; _entitySimulation->getObjectsToRemoveFromPhysics(motionStates); _physicsEngine->removeObjects(motionStates); _entitySimulation->deleteObjectsRemovedFromPhysics(); getEntities()->getTree()->withReadLock([&] { _entitySimulation->getObjectsToAddToPhysics(motionStates); _physicsEngine->addObjects(motionStates); }); getEntities()->getTree()->withReadLock([&] { _entitySimulation->getObjectsToChange(motionStates); VectorOfMotionStates stillNeedChange = _physicsEngine->changeObjects(motionStates); _entitySimulation->setObjectsToChange(stillNeedChange); }); _entitySimulation->applyActionChanges(); avatarManager->getObjectsToRemoveFromPhysics(motionStates); _physicsEngine->removeObjects(motionStates); avatarManager->getObjectsToAddToPhysics(motionStates); _physicsEngine->addObjects(motionStates); avatarManager->getObjectsToChange(motionStates); _physicsEngine->changeObjects(motionStates); myAvatar->prepareForPhysicsSimulation(); _physicsEngine->forEachAction([&](EntityActionPointer action) { action->prepareForPhysicsSimulation(); }); } { PROFILE_RANGE_EX("StepSimulation", 0xffff8000, (uint64_t)getActiveDisplayPlugin()->presentCount()); PerformanceTimer perfTimer("stepSimulation"); getEntities()->getTree()->withWriteLock([&] { _physicsEngine->stepSimulation(); }); } { PROFILE_RANGE_EX("HarvestChanges", 0xffffff00, (uint64_t)getActiveDisplayPlugin()->presentCount()); PerformanceTimer perfTimer("harvestChanges"); if (_physicsEngine->hasOutgoingChanges()) { getEntities()->getTree()->withWriteLock([&] { PerformanceTimer perfTimer("handleOutgoingChanges"); const VectorOfMotionStates& outgoingChanges = _physicsEngine->getOutgoingChanges(); _entitySimulation->handleOutgoingChanges(outgoingChanges); avatarManager->handleOutgoingChanges(outgoingChanges); }); auto collisionEvents = _physicsEngine->getCollisionEvents(); avatarManager->handleCollisionEvents(collisionEvents); _physicsEngine->dumpStatsIfNecessary(); if (!_aboutToQuit) { PerformanceTimer perfTimer("entities"); // Collision events (and their scripts) must not be handled when we're locked, above. (That would risk // deadlock.) _entitySimulation->handleCollisionEvents(collisionEvents); // NOTE: the getEntities()->update() call below will wait for lock // and will simulate entity motion (the EntityTree has been given an EntitySimulation). getEntities()->update(); // update the models... } myAvatar->harvestResultsFromPhysicsSimulation(deltaTime); } } } // AvatarManager update { PerformanceTimer perfTimer("AvatarManger"); _avatarSimCounter.increment(); { PROFILE_RANGE_EX("OtherAvatars", 0xffff00ff, (uint64_t)getActiveDisplayPlugin()->presentCount()); avatarManager->updateOtherAvatars(deltaTime); } qApp->updateMyAvatarLookAtPosition(); { PROFILE_RANGE_EX("MyAvatar", 0xffff00ff, (uint64_t)getActiveDisplayPlugin()->presentCount()); avatarManager->updateMyAvatar(deltaTime); } } { PROFILE_RANGE_EX("Overlays", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); PerformanceTimer perfTimer("overlays"); _overlays.update(deltaTime); } // Update _viewFrustum with latest camera and view frustum data... // NOTE: we get this from the view frustum, to make it simpler, since the // loadViewFrumstum() method will get the correct details from the camera // We could optimize this to not actually load the viewFrustum, since we don't // actually need to calculate the view frustum planes to send these details // to the server. { QMutexLocker viewLocker(&_viewMutex); loadViewFrustum(_myCamera, _viewFrustum); } quint64 now = usecTimestampNow(); // Update my voxel servers with my current voxel query... { PROFILE_RANGE_EX("QueryOctree", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); QMutexLocker viewLocker(&_viewMutex); PerformanceTimer perfTimer("queryOctree"); quint64 sinceLastQuery = now - _lastQueriedTime; const quint64 TOO_LONG_SINCE_LAST_QUERY = 3 * USECS_PER_SECOND; bool queryIsDue = sinceLastQuery > TOO_LONG_SINCE_LAST_QUERY; bool viewIsDifferentEnough = !_lastQueriedViewFrustum.isVerySimilar(_viewFrustum); // if it's been a while since our last query or the view has significantly changed then send a query, otherwise suppress it if (queryIsDue || viewIsDifferentEnough) { _lastQueriedTime = now; if (DependencyManager::get()->shouldRenderEntities()) { queryOctree(NodeType::EntityServer, PacketType::EntityQuery, _entityServerJurisdictions); } _lastQueriedViewFrustum = _viewFrustum; } } // sent nack packets containing missing sequence numbers of received packets from nodes { quint64 sinceLastNack = now - _lastNackTime; const quint64 TOO_LONG_SINCE_LAST_NACK = 1 * USECS_PER_SECOND; if (sinceLastNack > TOO_LONG_SINCE_LAST_NACK) { _lastNackTime = now; sendNackPackets(); } } // send packet containing downstream audio stats to the AudioMixer { quint64 sinceLastNack = now - _lastSendDownstreamAudioStats; if (sinceLastNack > TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS) { _lastSendDownstreamAudioStats = now; QMetaObject::invokeMethod(DependencyManager::get().data(), "sendDownstreamAudioStatsPacket", Qt::QueuedConnection); } } avatarManager->postUpdate(deltaTime); { PROFILE_RANGE_EX("PreRenderLambdas", 0xffff0000, (uint64_t)0); std::unique_lock guard(_postUpdateLambdasLock); for (auto& iter : _postUpdateLambdas) { iter.second(); } _postUpdateLambdas.clear(); } AnimDebugDraw::getInstance().update(); } int Application::sendNackPackets() { // iterates through all nodes in NodeList auto nodeList = DependencyManager::get(); int packetsSent = 0; nodeList->eachNode([&](const SharedNodePointer& node){ if (node->getActiveSocket() && node->getType() == NodeType::EntityServer) { auto nackPacketList = NLPacketList::create(PacketType::OctreeDataNack); QUuid nodeUUID = node->getUUID(); // if there are octree packets from this node that are waiting to be processed, // don't send a NACK since the missing packets may be among those waiting packets. if (_octreeProcessor.hasPacketsToProcessFrom(nodeUUID)) { return; } QSet missingSequenceNumbers; _octreeServerSceneStats.withReadLock([&] { // retrieve octree scene stats of this node if (_octreeServerSceneStats.find(nodeUUID) == _octreeServerSceneStats.end()) { return; } // get sequence number stats of node, prune its missing set, and make a copy of the missing set SequenceNumberStats& sequenceNumberStats = _octreeServerSceneStats[nodeUUID].getIncomingOctreeSequenceNumberStats(); sequenceNumberStats.pruneMissingSet(); missingSequenceNumbers = sequenceNumberStats.getMissingSet(); }); // construct nack packet(s) for this node foreach(const OCTREE_PACKET_SEQUENCE& missingNumber, missingSequenceNumbers) { nackPacketList->writePrimitive(missingNumber); } if (nackPacketList->getNumPackets()) { packetsSent += (int)nackPacketList->getNumPackets(); // send the packet list nodeList->sendPacketList(std::move(nackPacketList), *node); } } }); return packetsSent; } void Application::queryOctree(NodeType_t serverType, PacketType packetType, NodeToJurisdictionMap& jurisdictions, bool forceResend) { if (!_settingsLoaded) { return; // bail early if settings are not loaded } //qCDebug(interfaceapp) << ">>> inside... queryOctree()... _viewFrustum.getFieldOfView()=" << _viewFrustum.getFieldOfView(); bool wantExtraDebugging = getLogger()->extraDebugging(); ViewFrustum viewFrustum; copyViewFrustum(viewFrustum); _octreeQuery.setCameraPosition(viewFrustum.getPosition()); _octreeQuery.setCameraOrientation(viewFrustum.getOrientation()); _octreeQuery.setCameraFov(viewFrustum.getFieldOfView()); _octreeQuery.setCameraAspectRatio(viewFrustum.getAspectRatio()); _octreeQuery.setCameraNearClip(viewFrustum.getNearClip()); _octreeQuery.setCameraFarClip(viewFrustum.getFarClip()); _octreeQuery.setCameraEyeOffsetPosition(glm::vec3()); _octreeQuery.setCameraCenterRadius(viewFrustum.getCenterRadius()); auto lodManager = DependencyManager::get(); _octreeQuery.setOctreeSizeScale(lodManager->getOctreeSizeScale()); _octreeQuery.setBoundaryLevelAdjust(lodManager->getBoundaryLevelAdjust()); // Iterate all of the nodes, and get a count of how many octree servers we have... int totalServers = 0; int inViewServers = 0; int unknownJurisdictionServers = 0; auto nodeList = DependencyManager::get(); nodeList->eachNode([&](const SharedNodePointer& node) { // only send to the NodeTypes that are serverType if (node->getActiveSocket() && node->getType() == serverType) { totalServers++; // get the server bounds for this server QUuid nodeUUID = node->getUUID(); // if we haven't heard from this voxel server, go ahead and send it a query, so we // can get the jurisdiction... if (jurisdictions.find(nodeUUID) == jurisdictions.end()) { unknownJurisdictionServers++; } else { const JurisdictionMap& map = (jurisdictions)[nodeUUID]; auto rootCode = map.getRootOctalCode(); if (rootCode) { VoxelPositionSize rootDetails; voxelDetailsForCode(rootCode.get(), rootDetails); AACube serverBounds(glm::vec3(rootDetails.x * TREE_SCALE, rootDetails.y * TREE_SCALE, rootDetails.z * TREE_SCALE) - glm::vec3(HALF_TREE_SCALE), rootDetails.s * TREE_SCALE); if (viewFrustum.cubeIntersectsKeyhole(serverBounds)) { inViewServers++; } } } } }); if (wantExtraDebugging) { qCDebug(interfaceapp, "Servers: total %d, in view %d, unknown jurisdiction %d", totalServers, inViewServers, unknownJurisdictionServers); } int perServerPPS = 0; const int SMALL_BUDGET = 10; int perUnknownServer = SMALL_BUDGET; int totalPPS = getMaxOctreePacketsPerSecond(); // determine PPS based on number of servers if (inViewServers >= 1) { // set our preferred PPS to be exactly evenly divided among all of the voxel servers... and allocate 1 PPS // for each unknown jurisdiction server perServerPPS = (totalPPS / inViewServers) - (unknownJurisdictionServers * perUnknownServer); } else { if (unknownJurisdictionServers > 0) { perUnknownServer = (totalPPS / unknownJurisdictionServers); } } if (wantExtraDebugging) { qCDebug(interfaceapp, "perServerPPS: %d perUnknownServer: %d", perServerPPS, perUnknownServer); } auto queryPacket = NLPacket::create(packetType); nodeList->eachNode([&](const SharedNodePointer& node) { // only send to the NodeTypes that are serverType if (node->getActiveSocket() && node->getType() == serverType) { // get the server bounds for this server QUuid nodeUUID = node->getUUID(); bool inView = false; bool unknownView = false; // if we haven't heard from this voxel server, go ahead and send it a query, so we // can get the jurisdiction... if (jurisdictions.find(nodeUUID) == jurisdictions.end()) { unknownView = true; // assume it's in view if (wantExtraDebugging) { qCDebug(interfaceapp) << "no known jurisdiction for node " << *node << ", assume it's visible."; } } else { const JurisdictionMap& map = (jurisdictions)[nodeUUID]; auto rootCode = map.getRootOctalCode(); if (rootCode) { VoxelPositionSize rootDetails; voxelDetailsForCode(rootCode.get(), rootDetails); AACube serverBounds(glm::vec3(rootDetails.x * TREE_SCALE, rootDetails.y * TREE_SCALE, rootDetails.z * TREE_SCALE) - glm::vec3(HALF_TREE_SCALE), rootDetails.s * TREE_SCALE); inView = viewFrustum.cubeIntersectsKeyhole(serverBounds); } else if (wantExtraDebugging) { qCDebug(interfaceapp) << "Jurisdiction without RootCode for node " << *node << ". That's unusual!"; } } if (inView) { _octreeQuery.setMaxQueryPacketsPerSecond(perServerPPS); } else if (unknownView) { if (wantExtraDebugging) { qCDebug(interfaceapp) << "no known jurisdiction for node " << *node << ", give it budget of " << perUnknownServer << " to send us jurisdiction."; } // set the query's position/orientation to be degenerate in a manner that will get the scene quickly // If there's only one server, then don't do this, and just let the normal voxel query pass through // as expected... this way, we will actually get a valid scene if there is one to be seen if (totalServers > 1) { _octreeQuery.setCameraPosition(glm::vec3(-0.1,-0.1,-0.1)); const glm::quat OFF_IN_NEGATIVE_SPACE = glm::quat(-0.5, 0, -0.5, 1.0); _octreeQuery.setCameraOrientation(OFF_IN_NEGATIVE_SPACE); _octreeQuery.setCameraNearClip(0.1f); _octreeQuery.setCameraFarClip(0.1f); if (wantExtraDebugging) { qCDebug(interfaceapp) << "Using 'minimal' camera position for node" << *node; } } else { if (wantExtraDebugging) { qCDebug(interfaceapp) << "Using regular camera position for node" << *node; } } _octreeQuery.setMaxQueryPacketsPerSecond(perUnknownServer); } else { _octreeQuery.setMaxQueryPacketsPerSecond(0); } // if asked to forceResend, then set the query's position/orientation to be degenerate in a manner // that will cause our next query to be guarenteed to be different and the server will resend to us if (forceResend) { _octreeQuery.setCameraPosition(glm::vec3(-0.1, -0.1, -0.1)); const glm::quat OFF_IN_NEGATIVE_SPACE = glm::quat(-0.5, 0, -0.5, 1.0); _octreeQuery.setCameraOrientation(OFF_IN_NEGATIVE_SPACE); _octreeQuery.setCameraNearClip(0.1f); _octreeQuery.setCameraFarClip(0.1f); } // encode the query data int packetSize = _octreeQuery.getBroadcastData(reinterpret_cast(queryPacket->getPayload())); queryPacket->setPayloadSize(packetSize); // make sure we still have an active socket nodeList->sendUnreliablePacket(*queryPacket, *node); } }); } bool Application::isHMDMode() const { return getActiveDisplayPlugin()->isHmd(); } float Application::getTargetFrameRate() const { return getActiveDisplayPlugin()->getTargetFrameRate(); } QRect Application::getDesirableApplicationGeometry() const { QRect applicationGeometry = getWindow()->geometry(); // If our parent window is on the HMD, then don't use its geometry, instead use // the "main screen" geometry. HMDToolsDialog* hmdTools = DependencyManager::get()->getHMDToolsDialog(); if (hmdTools && hmdTools->hasHMDScreen()) { QScreen* hmdScreen = hmdTools->getHMDScreen(); QWindow* appWindow = getWindow()->windowHandle(); QScreen* appScreen = appWindow->screen(); // if our app's screen is the hmd screen, we don't want to place the // running scripts widget on it. So we need to pick a better screen. // we will use the screen for the HMDTools since it's a guaranteed // better screen. if (appScreen == hmdScreen) { QScreen* betterScreen = hmdTools->windowHandle()->screen(); applicationGeometry = betterScreen->geometry(); } } return applicationGeometry; } ///////////////////////////////////////////////////////////////////////////////////// // loadViewFrustum() // // Description: this will load the view frustum bounds for EITHER the head // or the "myCamera". // void Application::loadViewFrustum(Camera& camera, ViewFrustum& viewFrustum) { PerformanceTimer perfTimer("loadViewFrustum"); PROFILE_RANGE(__FUNCTION__); // We will use these below, from either the camera or head vectors calculated above viewFrustum.setProjection(camera.getProjection()); // Set the viewFrustum up with the correct position and orientation of the camera viewFrustum.setPosition(camera.getPosition()); viewFrustum.setOrientation(camera.getOrientation()); // Ask the ViewFrustum class to calculate our corners viewFrustum.calculate(); } glm::vec3 Application::getSunDirection() const { // Sun direction is in fact just the location of the sun relative to the origin auto skyStage = DependencyManager::get()->getSkyStage(); return skyStage->getSunLight()->getDirection(); } // FIXME, preprocessor guard this check to occur only in DEBUG builds static QThread * activeRenderingThread = nullptr; PickRay Application::computePickRay(float x, float y) const { vec2 pickPoint { x, y }; PickRay result; if (isHMDMode()) { getApplicationCompositor().computeHmdPickRay(pickPoint, result.origin, result.direction); } else { pickPoint /= getCanvasSize(); QMutexLocker viewLocker(&_viewMutex); _viewFrustum.computePickRay(pickPoint.x, pickPoint.y, result.origin, result.direction); } return result; } MyAvatar* Application::getMyAvatar() const { return DependencyManager::get()->getMyAvatar(); } glm::vec3 Application::getAvatarPosition() const { return getMyAvatar()->getPosition(); } void Application::copyViewFrustum(ViewFrustum& viewOut) const { QMutexLocker viewLocker(&_viewMutex); viewOut = _viewFrustum; } void Application::copyDisplayViewFrustum(ViewFrustum& viewOut) const { QMutexLocker viewLocker(&_viewMutex); viewOut = _displayViewFrustum; } void Application::copyShadowViewFrustum(ViewFrustum& viewOut) const { QMutexLocker viewLocker(&_viewMutex); viewOut = _shadowViewFrustum; } // WorldBox Render Data & rendering functions class WorldBoxRenderData { public: typedef render::Payload Payload; typedef Payload::DataPointer Pointer; int _val = 0; static render::ItemID _item; // unique WorldBoxRenderData }; render::ItemID WorldBoxRenderData::_item { render::Item::INVALID_ITEM_ID }; namespace render { template <> const ItemKey payloadGetKey(const WorldBoxRenderData::Pointer& stuff) { return ItemKey::Builder::opaqueShape(); } template <> const Item::Bound payloadGetBound(const WorldBoxRenderData::Pointer& stuff) { return Item::Bound(); } template <> void payloadRender(const WorldBoxRenderData::Pointer& stuff, RenderArgs* args) { if (args->_renderMode != RenderArgs::MIRROR_RENDER_MODE && Menu::getInstance()->isOptionChecked(MenuOption::WorldAxes)) { PerformanceTimer perfTimer("worldBox"); auto& batch = *args->_batch; DependencyManager::get()->bindSimpleProgram(batch); renderWorldBox(batch); } } } // Background Render Data & rendering functions class BackgroundRenderData { public: typedef render::Payload Payload; typedef Payload::DataPointer Pointer; Stars _stars; static render::ItemID _item; // unique WorldBoxRenderData }; render::ItemID BackgroundRenderData::_item = 0; namespace render { template <> const ItemKey payloadGetKey(const BackgroundRenderData::Pointer& stuff) { return ItemKey::Builder::background(); } template <> const Item::Bound payloadGetBound(const BackgroundRenderData::Pointer& stuff) { return Item::Bound(); } template <> void payloadRender(const BackgroundRenderData::Pointer& background, RenderArgs* args) { Q_ASSERT(args->_batch); gpu::Batch& batch = *args->_batch; // Background rendering decision auto skyStage = DependencyManager::get()->getSkyStage(); auto backgroundMode = skyStage->getBackgroundMode(); switch (backgroundMode) { case model::SunSkyStage::SKY_BOX: { auto skybox = skyStage->getSkybox(); if (skybox) { PerformanceTimer perfTimer("skybox"); skybox->render(batch, args->getViewFrustum()); break; } } // Fall through: if no skybox is available, render the SKY_DOME case model::SunSkyStage::SKY_DOME: { if (Menu::getInstance()->isOptionChecked(MenuOption::Stars)) { PerformanceTimer perfTimer("stars"); PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::payloadRender() ... My god, it's full of stars..."); // should be the first rendering pass - w/o depth buffer / lighting static const float alpha = 1.0f; background->_stars.render(args, alpha); } } break; case model::SunSkyStage::NO_BACKGROUND: default: // this line intentionally left blank break; } } } void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool selfAvatarOnly) { // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. // Then we can move this logic into the Avatar::simulate call. auto myAvatar = getMyAvatar(); myAvatar->preDisplaySide(renderArgs); activeRenderingThread = QThread::currentThread(); PROFILE_RANGE(__FUNCTION__); PerformanceTimer perfTimer("display"); PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide()"); // load the view frustum { QMutexLocker viewLocker(&_viewMutex); loadViewFrustum(theCamera, _displayViewFrustum); } // TODO fix shadows and make them use the GPU library // The pending changes collecting the changes here render::PendingChanges pendingChanges; // FIXME: Move this out of here!, Background / skybox should be driven by the enityt content just like the other entities // Background rendering decision if (!render::Item::isValidID(BackgroundRenderData::_item)) { auto backgroundRenderData = make_shared(); auto backgroundRenderPayload = make_shared(backgroundRenderData); BackgroundRenderData::_item = _main3DScene->allocateID(); pendingChanges.resetItem(BackgroundRenderData::_item, backgroundRenderPayload); } // Assuming nothing get's rendered through that if (!selfAvatarOnly) { if (DependencyManager::get()->shouldRenderEntities()) { // render models... PerformanceTimer perfTimer("entities"); PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide() ... entities..."); RenderArgs::DebugFlags renderDebugFlags = RenderArgs::RENDER_DEBUG_NONE; if (Menu::getInstance()->isOptionChecked(MenuOption::PhysicsShowHulls)) { renderDebugFlags = static_cast(renderDebugFlags | static_cast(RenderArgs::RENDER_DEBUG_HULLS)); } renderArgs->_debugFlags = renderDebugFlags; //ViveControllerManager::getInstance().updateRendering(renderArgs, _main3DScene, pendingChanges); } } // FIXME: Move this out of here!, WorldBox should be driven by the entity content just like the other entities // Make sure the WorldBox is in the scene if (!render::Item::isValidID(WorldBoxRenderData::_item)) { auto worldBoxRenderData = make_shared(); auto worldBoxRenderPayload = make_shared(worldBoxRenderData); WorldBoxRenderData::_item = _main3DScene->allocateID(); pendingChanges.resetItem(WorldBoxRenderData::_item, worldBoxRenderPayload); } else { pendingChanges.updateItem(WorldBoxRenderData::_item, [](WorldBoxRenderData& payload) { payload._val++; }); } // Setup the current Zone Entity lighting { auto stage = DependencyManager::get()->getSkyStage(); DependencyManager::get()->setGlobalLight(stage->getSunLight()); } { PerformanceTimer perfTimer("SceneProcessPendingChanges"); _main3DScene->enqueuePendingChanges(pendingChanges); _main3DScene->processPendingChangesQueue(); } // For now every frame pass the renderContext { PerformanceTimer perfTimer("EngineRun"); { QMutexLocker viewLocker(&_viewMutex); renderArgs->setViewFrustum(_displayViewFrustum); } _renderEngine->getRenderContext()->args = renderArgs; // Before the deferred pass, let's try to use the render engine _renderEngine->run(); } activeRenderingThread = nullptr; } void Application::renderRearViewMirror(RenderArgs* renderArgs, const QRect& region, bool isZoomed) { auto originalViewport = renderArgs->_viewport; // Grab current viewport to reset it at the end float aspect = (float)region.width() / region.height(); float fov = MIRROR_FIELD_OF_VIEW; auto myAvatar = getMyAvatar(); // bool eyeRelativeCamera = false; if (!isZoomed) { _mirrorCamera.setPosition(myAvatar->getChestPosition() + myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_BODY_DISTANCE * myAvatar->getScale()); } else { // HEAD zoom level // FIXME note that the positioning of the camera relative to the avatar can suffer limited // precision as the user's position moves further away from the origin. Thus at // /1e7,1e7,1e7 (well outside the buildable volume) the mirror camera veers and sways // wildly as you rotate your avatar because the floating point values are becoming // larger, squeezing out the available digits of precision you have available at the // human scale for camera positioning. // Previously there was a hack to correct this using the mechanism of repositioning // the avatar at the origin of the world for the purposes of rendering the mirror, // but it resulted in failing to render the avatar's head model in the mirror view // when in first person mode. Presumably this was because of some missed culling logic // that was not accounted for in the hack. // This was removed in commit 71e59cfa88c6563749594e25494102fe01db38e9 but could be further // investigated in order to adapt the technique while fixing the head rendering issue, // but the complexity of the hack suggests that a better approach _mirrorCamera.setPosition(myAvatar->getDefaultEyePosition() + myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_DISTANCE * myAvatar->getScale()); } _mirrorCamera.setProjection(glm::perspective(glm::radians(fov), aspect, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); _mirrorCamera.setOrientation(myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI, 0.0f))); // set the bounds of rear mirror view // the region is in device independent coordinates; must convert to device float ratio = (float)QApplication::desktop()->windowHandle()->devicePixelRatio() * getRenderResolutionScale(); int width = region.width() * ratio; int height = region.height() * ratio; gpu::Vec4i viewport = gpu::Vec4i(0, 0, width, height); renderArgs->_viewport = viewport; // render rear mirror view displaySide(renderArgs, _mirrorCamera, true); renderArgs->_viewport = originalViewport; } void Application::resetSensors(bool andReload) { DependencyManager::get()->reset(); DependencyManager::get()->reset(); DependencyManager::get()->reset(); getActiveDisplayPlugin()->resetSensors(); _overlayConductor.centerUI(); getMyAvatar()->reset(andReload); QMetaObject::invokeMethod(DependencyManager::get().data(), "reset", Qt::QueuedConnection); } void Application::updateWindowTitle() const { QString buildVersion = " (build " + applicationVersion() + ")"; auto nodeList = DependencyManager::get(); QString connectionStatus = nodeList->getDomainHandler().isConnected() ? "" : " (NOT CONNECTED) "; QString username = DependencyManager::get()->getAccountInfo().getUsername(); QString currentPlaceName = DependencyManager::get()->getHost(); if (currentPlaceName.isEmpty()) { currentPlaceName = nodeList->getDomainHandler().getHostname(); } QString title = QString() + (!username.isEmpty() ? username + " @ " : QString()) + currentPlaceName + connectionStatus + buildVersion; #ifndef WIN32 // crashes with vs2013/win32 qCDebug(interfaceapp, "Application title set to: %s", title.toStdString().c_str()); #endif _window->setWindowTitle(title); } void Application::clearDomainOctreeDetails() { // if we're about to quit, we really don't need to do any of these things... if (_aboutToQuit) { return; } qCDebug(interfaceapp) << "Clearing domain octree details..."; resetPhysicsReadyInformation(); getMyAvatar()->setAvatarEntityDataChanged(true); // to recreate worn entities // reset our node to stats and node to jurisdiction maps... since these must be changing... _entityServerJurisdictions.withWriteLock([&] { _entityServerJurisdictions.clear(); }); _octreeServerSceneStats.withWriteLock([&] { _octreeServerSceneStats.clear(); }); // reset the model renderer getEntities()->clear(); auto skyStage = DependencyManager::get()->getSkyStage(); skyStage->setBackgroundMode(model::SunSkyStage::SKY_DOME); _recentlyClearedDomain = true; } void Application::domainChanged(const QString& domainHostname) { updateWindowTitle(); // disable physics until we have enough information about our new location to not cause craziness. resetPhysicsReadyInformation(); } void Application::resettingDomain() { _notifiedPacketVersionMismatchThisDomain = false; } void Application::nodeAdded(SharedNodePointer node) const { if (node->getType() == NodeType::AvatarMixer) { // new avatar mixer, send off our identity packet right away getMyAvatar()->sendIdentityPacket(); } } void Application::nodeActivated(SharedNodePointer node) { if (node->getType() == NodeType::AssetServer) { // asset server just connected - check if we have the asset browser showing auto offscreenUi = DependencyManager::get(); auto assetDialog = offscreenUi->getRootItem()->findChild("AssetServer"); if (assetDialog) { auto nodeList = DependencyManager::get(); if (nodeList->getThisNodeCanWriteAssets()) { // call reload on the shown asset browser dialog to get the mappings (if permissions allow) QMetaObject::invokeMethod(assetDialog, "reload"); } else { // we switched to an Asset Server that we can't modify, hide the Asset Browser assetDialog->setVisible(false); } } } // If we get a new EntityServer activated, do a "forceRedraw" query. This will send a degenerate // query so that the server will think our next non-degenerate query is "different enough" to send // us a full scene if (_recentlyClearedDomain && node->getType() == NodeType::EntityServer) { _recentlyClearedDomain = false; if (DependencyManager::get()->shouldRenderEntities()) { queryOctree(NodeType::EntityServer, PacketType::EntityQuery, _entityServerJurisdictions, true); } } if (node->getType() == NodeType::AudioMixer) { DependencyManager::get()->negotiateAudioFormat(); } } void Application::nodeKilled(SharedNodePointer node) { // These are here because connecting NodeList::nodeKilled to OctreePacketProcessor::nodeKilled doesn't work: // OctreePacketProcessor::nodeKilled is not being called when NodeList::nodeKilled is emitted. // This may have to do with GenericThread::threadRoutine() blocking the QThread event loop _octreeProcessor.nodeKilled(node); _entityEditSender.nodeKilled(node); if (node->getType() == NodeType::AudioMixer) { QMetaObject::invokeMethod(DependencyManager::get().data(), "audioMixerKilled"); } else if (node->getType() == NodeType::EntityServer) { QUuid nodeUUID = node->getUUID(); // see if this is the first we've heard of this node... _entityServerJurisdictions.withReadLock([&] { if (_entityServerJurisdictions.find(nodeUUID) == _entityServerJurisdictions.end()) { return; } auto rootCode = _entityServerJurisdictions[nodeUUID].getRootOctalCode(); VoxelPositionSize rootDetails; voxelDetailsForCode(rootCode.get(), rootDetails); qCDebug(interfaceapp, "model server going away...... v[%f, %f, %f, %f]", (double)rootDetails.x, (double)rootDetails.y, (double)rootDetails.z, (double)rootDetails.s); }); // If the model server is going away, remove it from our jurisdiction map so we don't send voxels to a dead server _entityServerJurisdictions.withWriteLock([&] { _entityServerJurisdictions.erase(_entityServerJurisdictions.find(nodeUUID)); }); // also clean up scene stats for that server _octreeServerSceneStats.withWriteLock([&] { if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) { _octreeServerSceneStats.erase(nodeUUID); } }); } else if (node->getType() == NodeType::AvatarMixer) { // our avatar mixer has gone away - clear the hash of avatars DependencyManager::get()->clearOtherAvatars(); } else if (node->getType() == NodeType::AssetServer) { // asset server going away - check if we have the asset browser showing auto offscreenUi = DependencyManager::get(); auto assetDialog = offscreenUi->getRootItem()->findChild("AssetServer"); if (assetDialog) { // call reload on the shown asset browser dialog QMetaObject::invokeMethod(assetDialog, "clear"); } } } void Application::trackIncomingOctreePacket(ReceivedMessage& message, SharedNodePointer sendingNode, bool wasStatsPacket) { // Attempt to identify the sender from its address. if (sendingNode) { const QUuid& nodeUUID = sendingNode->getUUID(); // now that we know the node ID, let's add these stats to the stats for that node... _octreeServerSceneStats.withWriteLock([&] { if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) { OctreeSceneStats& stats = _octreeServerSceneStats[nodeUUID]; stats.trackIncomingOctreePacket(message, wasStatsPacket, sendingNode->getClockSkewUsec()); } }); } } bool Application::nearbyEntitiesAreReadyForPhysics() { // this is used to avoid the following scenario: // A table has some items sitting on top of it. The items are at rest, meaning they aren't active in bullet. // Someone logs in close to the table. They receive information about the items on the table before they // receive information about the table. The items are very close to the avatar's capsule, so they become // activated in bullet. This causes them to fall to the floor, because the table's shape isn't yet in bullet. EntityTreePointer entityTree = getEntities()->getTree(); if (!entityTree) { return false; } QVector entities; entityTree->withReadLock([&] { AABox box(getMyAvatar()->getPosition() - glm::vec3(PHYSICS_READY_RANGE), glm::vec3(2 * PHYSICS_READY_RANGE)); entityTree->findEntities(box, entities); }); // For reasons I haven't found, we don't necessarily have the full scene when we receive a stats packet. Apply // a heuristic to try to decide when we actually know about all of the nearby entities. uint32_t nearbyCount = entities.size(); if (nearbyCount == _nearbyEntitiesCountAtLastPhysicsCheck) { _nearbyEntitiesStabilityCount++; } else { _nearbyEntitiesStabilityCount = 0; } _nearbyEntitiesCountAtLastPhysicsCheck = nearbyCount; const uint32_t MINIMUM_NEARBY_ENTITIES_STABILITY_COUNT = 3; if (_nearbyEntitiesStabilityCount >= MINIMUM_NEARBY_ENTITIES_STABILITY_COUNT) { // We've seen the same number of nearby entities for several stats packets in a row. assume we've got all // the local entities. foreach (EntityItemPointer entity, entities) { if (entity->shouldBePhysical() && !entity->isReadyToComputeShape()) { static QString repeatedMessage = LogHandler::getInstance().addRepeatedMessageRegex("Physics disabled until entity loads: .*"); qCDebug(interfaceapp) << "Physics disabled until entity loads: " << entity->getID() << entity->getName(); return false; } } return true; } return false; } int Application::processOctreeStats(ReceivedMessage& message, SharedNodePointer sendingNode) { // But, also identify the sender, and keep track of the contained jurisdiction root for this server // parse the incoming stats datas stick it in a temporary object for now, while we // determine which server it belongs to int statsMessageLength = 0; const QUuid& nodeUUID = sendingNode->getUUID(); // now that we know the node ID, let's add these stats to the stats for that node... _octreeServerSceneStats.withWriteLock([&] { OctreeSceneStats& octreeStats = _octreeServerSceneStats[nodeUUID]; statsMessageLength = octreeStats.unpackFromPacket(message); if (octreeStats.isFullScene()) { _fullSceneReceivedCounter++; } // see if this is the first we've heard of this node... NodeToJurisdictionMap* jurisdiction = nullptr; QString serverType; if (sendingNode->getType() == NodeType::EntityServer) { jurisdiction = &_entityServerJurisdictions; serverType = "Entity"; } bool found = false; jurisdiction->withReadLock([&] { if (jurisdiction->find(nodeUUID) != jurisdiction->end()) { found = true; return; } VoxelPositionSize rootDetails; voxelDetailsForCode(octreeStats.getJurisdictionRoot().get(), rootDetails); qCDebug(interfaceapp, "stats from new %s server... [%f, %f, %f, %f]", qPrintable(serverType), (double)rootDetails.x, (double)rootDetails.y, (double)rootDetails.z, (double)rootDetails.s); }); if (!found) { // store jurisdiction details for later use // This is bit of fiddling is because JurisdictionMap assumes it is the owner of the values used to construct it // but OctreeSceneStats thinks it's just returning a reference to its contents. So we need to make a copy of the // details from the OctreeSceneStats to construct the JurisdictionMap JurisdictionMap jurisdictionMap; jurisdictionMap.copyContents(octreeStats.getJurisdictionRoot(), octreeStats.getJurisdictionEndNodes()); jurisdiction->withWriteLock([&] { (*jurisdiction)[nodeUUID] = jurisdictionMap; }); } }); return statsMessageLength; } void Application::packetSent(quint64 length) { } void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scriptEngine) { scriptEngine->setEmitScriptUpdatesFunction([this]() { SharedNodePointer entityServerNode = DependencyManager::get()->soloNodeOfType(NodeType::EntityServer); return !entityServerNode || isPhysicsEnabled(); }); // setup the packet senders and jurisdiction listeners of the script engine's scripting interfaces so // we can use the same ones from the application. auto entityScriptingInterface = DependencyManager::get(); entityScriptingInterface->setPacketSender(&_entityEditSender); entityScriptingInterface->setEntityTree(getEntities()->getTree()); // AvatarManager has some custom types AvatarManager::registerMetaTypes(scriptEngine); scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this)); // hook our avatar and avatar hash map object into this script engine scriptEngine->registerGlobalObject("MyAvatar", getMyAvatar()); qScriptRegisterMetaType(scriptEngine, audioListenModeToScriptValue, audioListenModeFromScriptValue); scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Camera", &_myCamera); #if defined(Q_OS_MAC) || defined(Q_OS_WIN) scriptEngine->registerGlobalObject("SpeechRecognizer", DependencyManager::get().data()); #endif ClipboardScriptingInterface* clipboardScriptable = new ClipboardScriptingInterface(); scriptEngine->registerGlobalObject("Clipboard", clipboardScriptable); connect(scriptEngine, &ScriptEngine::finished, clipboardScriptable, &ClipboardScriptingInterface::deleteLater); scriptEngine->registerGlobalObject("Overlays", &_overlays); qScriptRegisterMetaType(scriptEngine, OverlayPropertyResultToScriptValue, OverlayPropertyResultFromScriptValue); qScriptRegisterMetaType(scriptEngine, RayToOverlayIntersectionResultToScriptValue, RayToOverlayIntersectionResultFromScriptValue); scriptEngine->registerGlobalObject("OffscreenFlags", DependencyManager::get()->getFlags()); scriptEngine->registerGlobalObject("Desktop", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Toolbars", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Window", DependencyManager::get().data()); qScriptRegisterMetaType(scriptEngine, CustomPromptResultToScriptValue, CustomPromptResultFromScriptValue); scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, LocationScriptingInterface::locationSetter, "Window"); // register `location` on the global object. scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, LocationScriptingInterface::locationSetter); scriptEngine->registerFunction("WebWindow", WebWindowClass::constructor, 1); scriptEngine->registerFunction("OverlayWebWindow", QmlWebWindowClass::constructor); scriptEngine->registerFunction("OverlayWindow", QmlWindowClass::constructor); scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("Stats", Stats::getInstance()); scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("AudioDevice", AudioDeviceScriptingInterface::getInstance()); // Caches scriptEngine->registerGlobalObject("AnimationCache", DependencyManager::get().data()); scriptEngine->registerGlobalObject("TextureCache", DependencyManager::get().data()); scriptEngine->registerGlobalObject("ModelCache", DependencyManager::get().data()); scriptEngine->registerGlobalObject("SoundCache", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Account", AccountScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("DialogsManager", _dialogsManagerScriptingInterface); scriptEngine->registerGlobalObject("GlobalServices", GlobalServicesScriptingInterface::getInstance()); qScriptRegisterMetaType(scriptEngine, DownloadInfoResultToScriptValue, DownloadInfoResultFromScriptValue); scriptEngine->registerGlobalObject("FaceTracker", DependencyManager::get().data()); scriptEngine->registerGlobalObject("AvatarManager", DependencyManager::get().data()); scriptEngine->registerGlobalObject("UndoStack", &_undoStackScriptingInterface); scriptEngine->registerGlobalObject("LODManager", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Paths", DependencyManager::get().data()); scriptEngine->registerGlobalObject("HMD", DependencyManager::get().data()); scriptEngine->registerFunction("HMD", "getHUDLookAtPosition2D", HMDScriptingInterface::getHUDLookAtPosition2D, 0); scriptEngine->registerFunction("HMD", "getHUDLookAtPosition3D", HMDScriptingInterface::getHUDLookAtPosition3D, 0); scriptEngine->registerGlobalObject("Scene", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Render", _renderEngine->getConfiguration().get()); scriptEngine->registerGlobalObject("ScriptDiscoveryService", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Reticle", getApplicationCompositor().getReticleInterface()); scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Users", DependencyManager::get().data()); } bool Application::canAcceptURL(const QString& urlString) const { QUrl url(urlString); if (urlString.startsWith(HIFI_URL_SCHEME)) { return true; } QHashIterator i(_acceptedExtensions); QString lowerPath = url.path().toLower(); while (i.hasNext()) { i.next(); if (lowerPath.endsWith(i.key(), Qt::CaseInsensitive)) { return true; } } return false; } bool Application::acceptURL(const QString& urlString, bool defaultUpload) { if (urlString.startsWith(HIFI_URL_SCHEME)) { // this is a hifi URL - have the AddressManager handle it QMetaObject::invokeMethod(DependencyManager::get().data(), "handleLookupString", Qt::AutoConnection, Q_ARG(const QString&, urlString)); return true; } QUrl url(urlString); QHashIterator i(_acceptedExtensions); QString lowerPath = url.path().toLower(); while (i.hasNext()) { i.next(); if (lowerPath.endsWith(i.key(), Qt::CaseInsensitive)) { AcceptURLMethod method = i.value(); return (this->*method)(urlString); } } if (defaultUpload) { toggleAssetServerWidget(urlString); } return defaultUpload; } void Application::setSessionUUID(const QUuid& sessionUUID) const { Physics::setSessionUUID(sessionUUID); } bool Application::askToSetAvatarUrl(const QString& url) { QUrl realUrl(url); if (realUrl.isLocalFile()) { OffscreenUi::warning("", "You can not use local files for avatar components."); return false; } // Download the FST file, to attempt to determine its model type QVariantHash fstMapping = FSTReader::downloadMapping(url); FSTReader::ModelType modelType = FSTReader::predictModelType(fstMapping); QString modelName = fstMapping["name"].toString(); QString modelLicense = fstMapping["license"].toString(); bool agreeToLicence = true; // assume true if (!modelLicense.isEmpty()) { // word wrap the licence text to fit in a reasonable shaped message box. const int MAX_CHARACTERS_PER_LINE = 90; modelLicense = simpleWordWrap(modelLicense, MAX_CHARACTERS_PER_LINE); agreeToLicence = QMessageBox::Yes == OffscreenUi::question("Avatar Usage License", modelLicense + "\nDo you argee to these terms?", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); } bool ok = false; if (!agreeToLicence) { qCDebug(interfaceapp) << "Declined to agree to avatar license: " << url; } else { switch (modelType) { case FSTReader::HEAD_AND_BODY_MODEL: ok = QMessageBox::Ok == OffscreenUi::question("Set Avatar", "Would you like to use '" + modelName + "' for your avatar?", QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok); break; default: OffscreenUi::warning("", modelName + "Does not support a head and body as required."); break; } } if (ok) { getMyAvatar()->useFullAvatarURL(url, modelName); emit fullAvatarURLChanged(url, modelName); } else { qCDebug(interfaceapp) << "Declined to use the avatar: " << url; } return true; } bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { QMessageBox::StandardButton reply; QString shortName = scriptFilenameOrURL; QUrl scriptURL { scriptFilenameOrURL }; if (scriptURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { shortName = shortName.mid(shortName.lastIndexOf('/') + 1); } QString message = "Would you like to run this script:\n" + shortName; reply = OffscreenUi::question(getWindow(), "Run Script", message, QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::Yes) { qCDebug(interfaceapp) << "Chose to run the script: " << scriptFilenameOrURL; DependencyManager::get()->loadScript(scriptFilenameOrURL); } else { qCDebug(interfaceapp) << "Declined to run the script: " << scriptFilenameOrURL; } return true; } bool Application::askToWearAvatarAttachmentUrl(const QString& url) { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkRequest networkRequest = QNetworkRequest(url); networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); QNetworkReply* reply = networkAccessManager.get(networkRequest); int requestNumber = ++_avatarAttachmentRequest; connect(reply, &QNetworkReply::finished, [this, reply, url, requestNumber]() { if (requestNumber != _avatarAttachmentRequest) { // this request has been superseded by another more recent request reply->deleteLater(); return; } QNetworkReply::NetworkError networkError = reply->error(); if (networkError == QNetworkReply::NoError) { // download success QByteArray contents = reply->readAll(); QJsonParseError jsonError; auto doc = QJsonDocument::fromJson(contents, &jsonError); if (jsonError.error == QJsonParseError::NoError) { auto jsonObject = doc.object(); // retrieve optional name field from JSON QString name = tr("Unnamed Attachment"); auto nameValue = jsonObject.value("name"); if (nameValue.isString()) { name = nameValue.toString(); } // display confirmation dialog if (displayAvatarAttachmentConfirmationDialog(name)) { // add attachment to avatar auto myAvatar = getMyAvatar(); assert(myAvatar); auto attachmentDataVec = myAvatar->getAttachmentData(); AttachmentData attachmentData; attachmentData.fromJson(jsonObject); attachmentDataVec.push_back(attachmentData); myAvatar->setAttachmentData(attachmentDataVec); } else { qCDebug(interfaceapp) << "User declined to wear the avatar attachment: " << url; } } else { // json parse error auto avatarAttachmentParseErrorString = tr("Error parsing attachment JSON from url: \"%1\""); displayAvatarAttachmentWarning(avatarAttachmentParseErrorString.arg(url)); } } else { // download failure auto avatarAttachmentDownloadErrorString = tr("Error downloading attachment JSON from url: \"%1\""); displayAvatarAttachmentWarning(avatarAttachmentDownloadErrorString.arg(url)); } reply->deleteLater(); }); return true; } void Application::displayAvatarAttachmentWarning(const QString& message) const { auto avatarAttachmentWarningTitle = tr("Avatar Attachment Failure"); OffscreenUi::warning(avatarAttachmentWarningTitle, message); } bool Application::displayAvatarAttachmentConfirmationDialog(const QString& name) const { auto avatarAttachmentConfirmationTitle = tr("Avatar Attachment Confirmation"); auto avatarAttachmentConfirmationMessage = tr("Would you like to wear '%1' on your avatar?").arg(name); auto reply = OffscreenUi::question(avatarAttachmentConfirmationTitle, avatarAttachmentConfirmationMessage, QMessageBox::Ok | QMessageBox::Cancel); if (QMessageBox::Ok == reply) { return true; } else { return false; } } void Application::toggleRunningScriptsWidget() const { static const QUrl url("hifi/dialogs/RunningScripts.qml"); DependencyManager::get()->show(url, "RunningScripts"); //if (_runningScriptsWidget->isVisible()) { // if (_runningScriptsWidget->hasFocus()) { // _runningScriptsWidget->hide(); // } else { // _runningScriptsWidget->raise(); // setActiveWindow(_runningScriptsWidget); // _runningScriptsWidget->setFocus(); // } //} else { // _runningScriptsWidget->show(); // _runningScriptsWidget->setFocus(); //} } void Application::toggleAssetServerWidget(QString filePath) { if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { return; } static const QUrl url { "AssetServer.qml" }; auto startUpload = [=](QQmlContext* context, QObject* newObject){ if (!filePath.isEmpty()) { emit uploadRequest(filePath); } }; DependencyManager::get()->show(url, "AssetServer", startUpload); startUpload(nullptr, nullptr); } void Application::packageModel() { ModelPackager::package(); } void Application::openUrl(const QUrl& url) const { if (!url.isEmpty()) { if (url.scheme() == HIFI_URL_SCHEME) { DependencyManager::get()->handleLookupString(url.toString()); } else { // address manager did not handle - ask QDesktopServices to handle QDesktopServices::openUrl(url); } } } void Application::loadDialog() { auto scriptEngines = DependencyManager::get(); QString fileNameString = OffscreenUi::getOpenFileName( _glWidget, tr("Open Script"), getPreviousScriptLocation(), tr("JavaScript Files (*.js)")); if (!fileNameString.isEmpty() && QFile(fileNameString).exists()) { setPreviousScriptLocation(QFileInfo(fileNameString).absolutePath()); DependencyManager::get()->loadScript(fileNameString, true, false, false, true); // Don't load from cache } } QString Application::getPreviousScriptLocation() { QString result = _previousScriptLocation.get(); return result; } void Application::setPreviousScriptLocation(const QString& location) { _previousScriptLocation.set(location); } void Application::loadScriptURLDialog() const { auto newScript = OffscreenUi::getText(nullptr, "Open and Run Script", "Script URL"); if (!newScript.isEmpty()) { DependencyManager::get()->loadScript(newScript); } } void Application::toggleLogDialog() { if (! _logDialog) { _logDialog = new LogDialog(_glWidget, getLogger()); } if (_logDialog->isVisible()) { _logDialog->hide(); } else { _logDialog->show(); } } void Application::takeSnapshot() { QMediaPlayer* player = new QMediaPlayer(); QFileInfo inf = QFileInfo(PathUtils::resourcesPath() + "sounds/snap.wav"); player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot()); emit DependencyManager::get()->snapshotTaken(path); } float Application::getRenderResolutionScale() const { if (Menu::getInstance()->isOptionChecked(MenuOption::RenderResolutionOne)) { return 1.0f; } else if (Menu::getInstance()->isOptionChecked(MenuOption::RenderResolutionTwoThird)) { return 0.666f; } else if (Menu::getInstance()->isOptionChecked(MenuOption::RenderResolutionHalf)) { return 0.5f; } else if (Menu::getInstance()->isOptionChecked(MenuOption::RenderResolutionThird)) { return 0.333f; } else if (Menu::getInstance()->isOptionChecked(MenuOption::RenderResolutionQuarter)) { return 0.25f; } else { return 1.0f; } } void Application::notifyPacketVersionMismatch() { if (!_notifiedPacketVersionMismatchThisDomain) { _notifiedPacketVersionMismatchThisDomain = true; QString message = "The location you are visiting is running an incompatible server version.\n"; message += "Content may not display properly."; OffscreenUi::warning("", message); } } void Application::checkSkeleton() const { if (getMyAvatar()->getSkeletonModel()->isActive() && !getMyAvatar()->getSkeletonModel()->hasSkeleton()) { qCDebug(interfaceapp) << "MyAvatar model has no skeleton"; QString message = "Your selected avatar body has no skeleton.\n\nThe default body will be loaded..."; OffscreenUi::warning("", message); getMyAvatar()->useFullAvatarURL(AvatarData::defaultFullAvatarModelUrl(), DEFAULT_FULL_AVATAR_MODEL_NAME); } else { _physicsEngine->setCharacterController(getMyAvatar()->getCharacterController()); } } void Application::activeChanged(Qt::ApplicationState state) { switch (state) { case Qt::ApplicationActive: _isForeground = true; break; case Qt::ApplicationSuspended: case Qt::ApplicationHidden: case Qt::ApplicationInactive: default: _isForeground = false; break; } } void Application::postLambdaEvent(std::function f) { if (this->thread() == QThread::currentThread()) { f(); } else { QCoreApplication::postEvent(this, new LambdaEvent(f)); } } void Application::initPlugins(const QStringList& arguments) { QCommandLineOption display("display", "Preferred displays", "displays"); QCommandLineOption disableDisplays("disable-displays", "Displays to disable", "displays"); QCommandLineOption disableInputs("disable-inputs", "Inputs to disable", "inputs"); QCommandLineParser parser; parser.addOption(display); parser.addOption(disableDisplays); parser.addOption(disableInputs); parser.parse(arguments); if (parser.isSet(display)) { auto preferredDisplays = parser.value(display).split(',', QString::SkipEmptyParts); qInfo() << "Setting prefered display plugins:" << preferredDisplays; PluginManager::getInstance()->setPreferredDisplayPlugins(preferredDisplays); } if (parser.isSet(disableDisplays)) { auto disabledDisplays = parser.value(disableDisplays).split(',', QString::SkipEmptyParts); qInfo() << "Disabling following display plugins:" << disabledDisplays; PluginManager::getInstance()->disableDisplays(disabledDisplays); } if (parser.isSet(disableInputs)) { auto disabledInputs = parser.value(disableInputs).split(',', QString::SkipEmptyParts); qInfo() << "Disabling following input plugins:" << disabledInputs; PluginManager::getInstance()->disableInputs(disabledInputs); } } void Application::shutdownPlugins() { } glm::uvec2 Application::getCanvasSize() const { return glm::uvec2(_glWidget->width(), _glWidget->height()); } QRect Application::getRenderingGeometry() const { auto geometry = _glWidget->geometry(); auto topLeft = geometry.topLeft(); auto topLeftScreen = _glWidget->mapToGlobal(topLeft); geometry.moveTopLeft(topLeftScreen); return geometry; } glm::uvec2 Application::getUiSize() const { static const uint MIN_SIZE = 1; glm::uvec2 result(MIN_SIZE); if (_displayPlugin) { result = getActiveDisplayPlugin()->getRecommendedUiSize(); } return result; } QRect Application::getRecommendedOverlayRect() const { auto uiSize = getUiSize(); QRect result(0, 0, uiSize.x, uiSize.y); if (_displayPlugin) { result = getActiveDisplayPlugin()->getRecommendedOverlayRect(); } return result; } QSize Application::getDeviceSize() const { static const int MIN_SIZE = 1; QSize result(MIN_SIZE, MIN_SIZE); if (_displayPlugin) { result = fromGlm(getActiveDisplayPlugin()->getRecommendedRenderSize()); } return result; } bool Application::isThrottleRendering() const { if (_displayPlugin) { return getActiveDisplayPlugin()->isThrottled(); } return false; } bool Application::hasFocus() const { if (_displayPlugin) { return getActiveDisplayPlugin()->hasFocus(); } return (QApplication::activeWindow() != nullptr); } glm::vec2 Application::getViewportDimensions() const { return toGlm(getDeviceSize()); } void Application::setMaxOctreePacketsPerSecond(int maxOctreePPS) { if (maxOctreePPS != _maxOctreePPS) { _maxOctreePPS = maxOctreePPS; maxOctreePacketsPerSecond.set(_maxOctreePPS); } } int Application::getMaxOctreePacketsPerSecond() const { return _maxOctreePPS; } qreal Application::getDevicePixelRatio() { return (_window && _window->windowHandle()) ? _window->windowHandle()->devicePixelRatio() : 1.0; } DisplayPluginPointer Application::getActiveDisplayPlugin() const { if (QThread::currentThread() != thread()) { std::unique_lock lock(_displayPluginLock); return _displayPlugin; } if (!_displayPlugin) { const_cast(this)->updateDisplayMode(); Q_ASSERT(_displayPlugin); } return _displayPlugin; } static void addDisplayPluginToMenu(DisplayPluginPointer displayPlugin, bool active = false) { auto menu = Menu::getInstance(); QString name = displayPlugin->getName(); auto grouping = displayPlugin->getGrouping(); QString groupingMenu { "" }; Q_ASSERT(!menu->menuItemExists(MenuOption::OutputMenu, name)); // assign the meny grouping based on plugin grouping switch (grouping) { case Plugin::ADVANCED: groupingMenu = "Advanced"; break; case Plugin::DEVELOPER: groupingMenu = "Developer"; break; default: groupingMenu = "Standard"; break; } static QActionGroup* displayPluginGroup = nullptr; if (!displayPluginGroup) { displayPluginGroup = new QActionGroup(menu); displayPluginGroup->setExclusive(true); } auto parent = menu->getMenu(MenuOption::OutputMenu); auto action = menu->addActionToQMenuAndActionHash(parent, name, 0, qApp, SLOT(updateDisplayMode()), QAction::NoRole, Menu::UNSPECIFIED_POSITION, groupingMenu); action->setCheckable(true); action->setChecked(active); displayPluginGroup->addAction(action); Q_ASSERT(menu->menuItemExists(MenuOption::OutputMenu, name)); } void Application::updateDisplayMode() { // Unsafe to call this method from anything but the main thread if (QThread::currentThread() != thread()) { qFatal("Attempted to switch display plugins from a non-main thread"); } // Some plugins *cough* Oculus *cough* process message events from inside their // display function, and we don't want to change the display plugin underneath // the paintGL call, so we need to guard against that // The current oculus runtime doesn't do this anymore if (_inPaint) { qFatal("Attempted to switch display plugins while in painting"); } auto menu = Menu::getInstance(); auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); static std::once_flag once; std::call_once(once, [&] { bool first = true; // first sort the plugins into groupings: standard, advanced, developer DisplayPluginList standard; DisplayPluginList advanced; DisplayPluginList developer; foreach(auto displayPlugin, displayPlugins) { auto grouping = displayPlugin->getGrouping(); switch (grouping) { case Plugin::ADVANCED: advanced.push_back(displayPlugin); break; case Plugin::DEVELOPER: developer.push_back(displayPlugin); break; default: standard.push_back(displayPlugin); break; } } // concatenate the groupings into a single list in the order: standard, advanced, developer standard.insert(std::end(standard), std::begin(advanced), std::end(advanced)); standard.insert(std::end(standard), std::begin(developer), std::end(developer)); foreach(auto displayPlugin, standard) { addDisplayPluginToMenu(displayPlugin, first); auto displayPluginName = displayPlugin->getName(); QObject::connect(displayPlugin.get(), &DisplayPlugin::recommendedFramebufferSizeChanged, [this](const QSize & size) { resizeGL(); }); first = false; } // after all plugins have been added to the menu, add a separator to the menu auto menu = Menu::getInstance(); auto parent = menu->getMenu(MenuOption::OutputMenu); parent->addSeparator(); }); // Default to the first item on the list, in case none of the menu items match DisplayPluginPointer newDisplayPlugin = displayPlugins.at(0); foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { QString name = displayPlugin->getName(); QAction* action = menu->getActionForOption(name); // Menu might have been removed if the display plugin lost if (!action) { continue; } if (action->isChecked()) { newDisplayPlugin = displayPlugin; break; } } if (newDisplayPlugin == _displayPlugin) { return; } UserActivityLogger::getInstance().logAction("changed_display_mode", { { "previous_display_mode", _displayPlugin ? _displayPlugin->getName() : "" }, { "display_mode", newDisplayPlugin ? newDisplayPlugin->getName() : "" } }); auto offscreenUi = DependencyManager::get(); // Make the switch atomic from the perspective of other threads { std::unique_lock lock(_displayPluginLock); auto oldDisplayPlugin = _displayPlugin; if (_displayPlugin) { _displayPlugin->deactivate(); } // FIXME probably excessive and useless context switching _offscreenContext->makeCurrent(); bool active = newDisplayPlugin->activate(); if (!active) { // If the new plugin fails to activate, fallback to last display qWarning() << "Failed to activate display: " << newDisplayPlugin->getName(); newDisplayPlugin = oldDisplayPlugin; if (newDisplayPlugin) { qWarning() << "Falling back to last display: " << newDisplayPlugin->getName(); active = newDisplayPlugin->activate(); } // If there is no last display, or // If the last display fails to activate, fallback to desktop if (!active) { newDisplayPlugin = displayPlugins.at(0); qWarning() << "Falling back to display: " << newDisplayPlugin->getName(); active = newDisplayPlugin->activate(); } if (!active) { qFatal("Failed to activate fallback plugin"); } } _offscreenContext->makeCurrent(); offscreenUi->resize(fromGlm(newDisplayPlugin->getRecommendedUiSize())); _offscreenContext->makeCurrent(); getApplicationCompositor().setDisplayPlugin(newDisplayPlugin); _displayPlugin = newDisplayPlugin; } emit activeDisplayPluginChanged(); // reset the avatar, to set head and hand palms back to a reasonable default pose. getMyAvatar()->reset(false); Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); } mat4 Application::getEyeProjection(int eye) const { QMutexLocker viewLocker(&_viewMutex); if (isHMDMode()) { return getActiveDisplayPlugin()->getEyeProjection((Eye)eye, _viewFrustum.getProjection()); } return _viewFrustum.getProjection(); } mat4 Application::getEyeOffset(int eye) const { // FIXME invert? return getActiveDisplayPlugin()->getEyeToHeadTransform((Eye)eye); } mat4 Application::getHMDSensorPose() const { if (isHMDMode()) { return getActiveDisplayPlugin()->getHeadPose(); } return mat4(); } void Application::deadlockApplication() { qCDebug(interfaceapp) << "Intentionally deadlocked Interface"; // Using a loop that will *technically* eventually exit (in ~600 billion years) // to avoid compiler warnings about a loop that will never exit for (uint64_t i = 1; i != 0; ++i) { QThread::sleep(1); } } void Application::setActiveDisplayPlugin(const QString& pluginName) { auto menu = Menu::getInstance(); foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { QString name = displayPlugin->getName(); QAction* action = menu->getActionForOption(name); if (pluginName == name) { action->setChecked(true); } } updateDisplayMode(); } void Application::handleLocalServerConnection() const { auto server = qobject_cast(sender()); qDebug() << "Got connection on local server from additional instance - waiting for parameters"; auto socket = server->nextPendingConnection(); connect(socket, &QLocalSocket::readyRead, this, &Application::readArgumentsFromLocalSocket); qApp->getWindow()->raise(); qApp->getWindow()->activateWindow(); } void Application::readArgumentsFromLocalSocket() const { auto socket = qobject_cast(sender()); auto message = socket->readAll(); socket->deleteLater(); qDebug() << "Read from connection: " << message; // If we received a message, try to open it as a URL if (message.length() > 0) { qApp->openUrl(QString::fromUtf8(message)); } } void Application::showDesktop() { Menu::getInstance()->setIsOptionChecked(MenuOption::Overlays, true); } CompositorHelper& Application::getApplicationCompositor() const { return *DependencyManager::get(); } // virtual functions required for PluginContainer ui::Menu* Application::getPrimaryMenu() { auto appMenu = _window->menuBar(); auto uiMenu = dynamic_cast(appMenu); return uiMenu; } void Application::showDisplayPluginsTools(bool show) { DependencyManager::get()->hmdTools(show); } GLWidget* Application::getPrimaryWidget() { return _glWidget; } MainWindow* Application::getPrimaryWindow() { return getWindow(); } QOpenGLContext* Application::getPrimaryContext() { return _glWidget->context()->contextHandle(); } bool Application::makeRenderingContextCurrent() { return _offscreenContext->makeCurrent(); } void Application::releaseSceneTexture(const gpu::TexturePointer& texture) { Q_ASSERT(QThread::currentThread() == thread()); auto& framebufferMap = _lockedFramebufferMap; Q_ASSERT(framebufferMap.contains(texture)); auto framebufferPointer = framebufferMap[texture]; framebufferMap.remove(texture); auto framebufferCache = DependencyManager::get(); framebufferCache->releaseFramebuffer(framebufferPointer); } void Application::releaseOverlayTexture(const gpu::TexturePointer& texture) { _applicationOverlay.releaseOverlay(texture); } bool Application::isForeground() const { return _isForeground && !_window->isMinimized(); }