mirror of
https://github.com/overte-org/overte.git
synced 2025-09-01 15:17:49 +02:00
2531 lines
108 KiB
C++
2531 lines
108 KiB
C++
//
|
|
// Application.cpp
|
|
// interface/src
|
|
//
|
|
// Created by Andrzej Kapolka on 5/10/13.
|
|
// Copyright 2013 High Fidelity, Inc.
|
|
// Copyright 2020 Vircadia contributors.
|
|
// Copyright 2022-2023 Overte e.V.
|
|
//
|
|
// Distributed under the Apache License, Version 2.0.
|
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
|
|
#include "Application.h"
|
|
|
|
#include <QDesktopWidget>
|
|
#include <QDesktopServices>
|
|
#include <QtGui/QClipboard>
|
|
#include <QtNetwork/QLocalSocket>
|
|
#include <QtNetwork/QLocalServer>
|
|
#include <QtQuick/QQuickWindow>
|
|
#include <QWidget>
|
|
|
|
#include <AccountManager.h>
|
|
#include <AddressManager.h>
|
|
#include <AnimationCacheScriptingInterface.h>
|
|
#include <AnimDebugDraw.h>
|
|
#include <AvatarBookmarks.h>
|
|
#include <audio/AudioScope.h>
|
|
#include <avatar/AvatarManager.h>
|
|
#include <avatar/AvatarPackager.h>
|
|
#include <avatar/GrabManager.h>
|
|
#include <BuildInfo.h>
|
|
#include <controllers/ScriptingInterface.h>
|
|
#include <controllers/UserInputMapper.h>
|
|
#include <CrashHelpers.h>
|
|
#include <DebugDraw.h>
|
|
#include <DesktopPreviewProvider.h>
|
|
#include <display-plugins/CompositorHelper.h>
|
|
#include <display-plugins/DisplayPlugin.h>
|
|
#include <DomainAccountManager.h>
|
|
#include <EntityScriptServerLogClient.h>
|
|
#include <FramebufferCache.h>
|
|
#include <GPUIdent.h>
|
|
#include <graphics-scripting/GraphicsScriptingInterface.h>
|
|
#include <hfm/ModelFormatRegistry.h>
|
|
#include <input-plugins/InputPlugin.h>
|
|
#include <input-plugins/KeyboardMouseDevice.h>
|
|
#include <LocationScriptingInterface.h>
|
|
#include <LogHandler.h>
|
|
#include <MainWindow.h>
|
|
#include <MessagesClient.h>
|
|
#include <material-networking/TextureCacheScriptingInterface.h>
|
|
#include <model-networking/ModelCacheScriptingInterface.h>
|
|
#include <networking/CloseEventSender.h>
|
|
#include <OffscreenUi.h>
|
|
#include <PickManager.h>
|
|
#include <plugins/OculusPlatformPlugin.h>
|
|
#include <plugins/PluginManager.h>
|
|
#include <plugins/PluginUtils.h>
|
|
#include <plugins/SteamClientPlugin.h>
|
|
#include <Preferences.h>
|
|
#include <procedural/MaterialCacheScriptingInterface.h>
|
|
#include <QmlFragmentClass.h>
|
|
#include <QmlWebWindowClass.h>
|
|
#include <QmlWindowClass.h>
|
|
#include <raypick/PickScriptingInterface.h>
|
|
#include <raypick/PointerScriptingInterface.h>
|
|
#include <raypick/RayPickScriptingInterface.h>
|
|
#include <recording/RecordingScriptingInterface.h>
|
|
#include <render/EngineStats.h>
|
|
#include <ResourceScriptingInterface.h>
|
|
#include <SandboxUtils.h>
|
|
#include <SceneScriptingInterface.h>
|
|
#include <ScriptEngines.h>
|
|
#include <scripting/AccountServicesScriptingInterface.h>
|
|
#include <scripting/Audio.h>
|
|
#include <scripting/ClipboardScriptingInterface.h>
|
|
#include <scripting/ControllerScriptingInterface.h>
|
|
#include <scripting/DesktopScriptingInterface.h>
|
|
#include <scripting/HMDScriptingInterface.h>
|
|
#include <scripting/KeyboardScriptingInterface.h>
|
|
#include <scripting/MenuScriptingInterface.h>
|
|
#include <scripting/PerformanceScriptingInterface.h>
|
|
#include <scripting/PlatformInfoScriptingInterface.h>
|
|
#include <scripting/RatesScriptingInterface.h>
|
|
#include <scripting/RenderScriptingInterface.h>
|
|
#include <scripting/SelectionScriptingInterface.h>
|
|
#include <scripting/SettingsScriptingInterface.h>
|
|
#include <scripting/TestScriptingInterface.h>
|
|
#include <scripting/WindowScriptingInterface.h>
|
|
#ifndef Q_OS_ANDROID
|
|
#include <shared/FileLogger.h>
|
|
#endif
|
|
#include <shared/GlobalAppProperties.h>
|
|
#include <shared/PlatformHelper.h>
|
|
#include <shared/QtHelpers.h>
|
|
#include <SoundCacheScriptingInterface.h>
|
|
#include <StatTracker.h>
|
|
#include <ui/AvatarInputs.h>
|
|
#include <ui/AnimStats.h>
|
|
#include <ui/TabletScriptingInterface.h>
|
|
#include <ui/Keyboard.h>
|
|
#include <ui/OctreeStatsProvider.h>
|
|
#include <ui/OffscreenQmlSurfaceCache.h>
|
|
#include <ui/Snapshot.h>
|
|
#include <ui/SnapshotAnimated.h>
|
|
#include <ui/StandAloneJSConsole.h>
|
|
#include <ui/Stats.h>
|
|
#include <ui/ToolbarScriptingInterface.h>
|
|
#include <UserActivityLogger.h>
|
|
#include <UserActivityLoggerScriptingInterface.h>
|
|
#include <UsersScriptingInterface.h>
|
|
|
|
#include "AboutUtil.h"
|
|
#include "ApplicationEventHandler.h"
|
|
#include "AudioClient.h"
|
|
#include "DeadlockWatchdog.h"
|
|
#include "GLCanvas.h"
|
|
#include "LocationBookmarks.h"
|
|
#include "LODManager.h"
|
|
#include "Menu.h"
|
|
#include "ResourceRequestObserver.h"
|
|
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
|
|
#include "SpeechRecognizer.h"
|
|
#endif
|
|
#include "Util.h"
|
|
|
|
#if defined(Q_OS_WIN)
|
|
#include "WindowsSystemInfo.h"
|
|
|
|
// On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU
|
|
// FIXME seems to be broken.
|
|
extern "C" {
|
|
_declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001;
|
|
}
|
|
#endif
|
|
|
|
#if defined(Q_OS_MAC)
|
|
// On Mac OS, disable App Nap to prevent audio glitches while running in the background
|
|
#include "AppNapDisabler.h"
|
|
static AppNapDisabler appNapDisabler; // disabled, while in scope
|
|
#endif
|
|
|
|
#if defined(Q_OS_ANDROID)
|
|
#include <android/log.h>
|
|
#endif
|
|
|
|
// For processing on QThreadPool, we target a number of threads after reserving some
|
|
// based on how many are being consumed by the application and the display plugin. However,
|
|
// we will never drop below the 'min' value
|
|
static const int MIN_PROCESSING_THREAD_POOL_SIZE = 2;
|
|
static const int ENTITY_SERVER_CONNECTION_TIMEOUT = 5000;
|
|
|
|
const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 60.0f;
|
|
const float DEFAULT_DESKTOP_TABLET_SCALE_PERCENT = 75.0f;
|
|
const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true;
|
|
const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false;
|
|
const bool DEFAULT_PREFER_STYLUS_OVER_LASER = false;
|
|
const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = false;
|
|
const bool DEFAULT_MOUSE_CAPTURE_VR = false;
|
|
const bool DEFAULT_SHOW_GRAPHICS_ICON = true;
|
|
const bool DEFAULT_MINI_TABLET_ENABLED = false;
|
|
const bool DEFAULT_AWAY_STATE_WHEN_FOCUS_LOST_IN_VR_ENABLED = true;
|
|
|
|
static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND;
|
|
|
|
static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
|
|
static const QString KEEP_ME_LOGGED_IN_SETTING_NAME = "keepMeLoggedIn";
|
|
const QString DEFAULT_CURSOR_NAME = "SYSTEM";
|
|
|
|
Setting::Handle<int> sessionRunTime { "sessionRunTime", 0 };
|
|
|
|
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_ANDROID
|
|
const char * local=logMessage.toStdString().c_str();
|
|
switch (type) {
|
|
case QtDebugMsg:
|
|
__android_log_write(ANDROID_LOG_DEBUG,"Interface",local);
|
|
break;
|
|
case QtInfoMsg:
|
|
__android_log_write(ANDROID_LOG_INFO,"Interface",local);
|
|
break;
|
|
case QtWarningMsg:
|
|
__android_log_write(ANDROID_LOG_WARN,"Interface",local);
|
|
break;
|
|
case QtCriticalMsg:
|
|
__android_log_write(ANDROID_LOG_ERROR,"Interface",local);
|
|
break;
|
|
case QtFatalMsg:
|
|
default:
|
|
__android_log_write(ANDROID_LOG_FATAL,"Interface",local);
|
|
abort();
|
|
}
|
|
#else
|
|
qApp->getLogger()->addMessage(qPrintable(logMessage));
|
|
#endif
|
|
}
|
|
}
|
|
|
|
Application::Application(
|
|
int& argc, char** argv,
|
|
const QCommandLineParser& parser,
|
|
QElapsedTimer& startupTimer
|
|
) :
|
|
QApplication(argc, argv),
|
|
_window(new MainWindow(desktop())),
|
|
// Menu needs to be initialized before other initializers. Otherwise deadlock happens on qApp->getWindow()->menuBar().
|
|
_isMenuInitialized(initMenu()),
|
|
#ifndef Q_OS_ANDROID
|
|
_logger(new FileLogger(this)),
|
|
#endif
|
|
_sessionRunTimer(startupTimer),
|
|
_lastNackTime(usecTimestampNow()),
|
|
_lastSendDownstreamAudioStats(usecTimestampNow()),
|
|
_firstRun(Settings::firstRun, true),
|
|
_previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION),
|
|
// UI
|
|
_hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT),
|
|
_desktopTabletScale("desktopTabletScale", DEFAULT_DESKTOP_TABLET_SCALE_PERCENT),
|
|
_desktopTabletBecomesToolbarSetting("desktopTabletBecomesToolbar", DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR),
|
|
_hmdTabletBecomesToolbarSetting("hmdTabletBecomesToolbar", DEFAULT_HMD_TABLET_BECOMES_TOOLBAR),
|
|
_preferStylusOverLaserSetting("preferStylusOverLaser", DEFAULT_PREFER_STYLUS_OVER_LASER),
|
|
_preferAvatarFingerOverStylusSetting("preferAvatarFingerOverStylus", DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS),
|
|
_defaultMouseCaptureVR("defaultMouseCaptureVR", DEFAULT_MOUSE_CAPTURE_VR),
|
|
_showGraphicsIconSetting("showGraphicsIcon", DEFAULT_SHOW_GRAPHICS_ICON),
|
|
_constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true),
|
|
_awayStateWhenFocusLostInVREnabled("awayStateWhenFocusLostInVREnabled", DEFAULT_AWAY_STATE_WHEN_FOCUS_LOST_IN_VR_ENABLED),
|
|
_preferredCursor("preferredCursor", DEFAULT_CURSOR_NAME),
|
|
_darkTheme("darkTheme", true),
|
|
_miniTabletEnabledSetting("miniTabletEnabled", DEFAULT_MINI_TABLET_ENABLED),
|
|
// Entities
|
|
_maxOctreePacketsPerSecond("maxOctreePPS", DEFAULT_MAX_OCTREE_PPS),
|
|
_maxOctreePPS(_maxOctreePacketsPerSecond.get()),
|
|
// Camera
|
|
_fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES),
|
|
_cameraClippingEnabled("cameraClippingEnabled", false)
|
|
{
|
|
setProperty(hifi::properties::CRASHED, _previousSessionCrashed);
|
|
|
|
LogHandler::getInstance().moveToThread(thread());
|
|
LogHandler::getInstance().setupRepeatedMessageFlusher();
|
|
qInstallMessageHandler(messageHandler);
|
|
|
|
DependencyManager::set<PathUtils>();
|
|
}
|
|
|
|
Application::~Application() {
|
|
// remove avatars from physics engine
|
|
if (auto avatarManager = DependencyManager::get<AvatarManager>()) {
|
|
// AvatarManager may not yet exist in case of an early exit
|
|
|
|
avatarManager->clearOtherAvatars();
|
|
auto myCharacterController = getMyAvatar()->getCharacterController();
|
|
myCharacterController->clearDetailedMotionStates();
|
|
|
|
PhysicsEngine::Transaction transaction;
|
|
avatarManager->buildPhysicsTransaction(transaction);
|
|
_physicsEngine->processTransaction(transaction);
|
|
avatarManager->handleProcessedPhysicsTransaction(transaction);
|
|
avatarManager->deleteAllAvatars();
|
|
}
|
|
|
|
if (_physicsEngine) {
|
|
_physicsEngine->setCharacterController(nullptr);
|
|
}
|
|
|
|
// the _shapeManager should have zero references
|
|
_shapeManager.collectGarbage();
|
|
assert(_shapeManager.getNumShapes() == 0);
|
|
|
|
if (_graphicsEngine) {
|
|
// shutdown graphics engine
|
|
_graphicsEngine->shutdown();
|
|
}
|
|
|
|
_gameWorkload.shutdown();
|
|
|
|
DependencyManager::destroy<Preferences>();
|
|
PlatformHelper::shutdown();
|
|
|
|
if (_entityClipboard) {
|
|
_entityClipboard->eraseAllOctreeElements();
|
|
_entityClipboard.reset();
|
|
}
|
|
|
|
if (_octreeProcessor) {
|
|
_octreeProcessor->terminate();
|
|
}
|
|
|
|
if (_entityEditSender) {
|
|
_entityEditSender->terminate();
|
|
}
|
|
|
|
if (auto pluginManager = PluginManager::getInstance()) {
|
|
if (auto steamClient = pluginManager->getSteamClientPlugin()) {
|
|
steamClient->shutdown();
|
|
}
|
|
|
|
if (auto oculusPlatform = pluginManager->getOculusPlatformPlugin()) {
|
|
oculusPlatform->shutdown();
|
|
}
|
|
}
|
|
|
|
DependencyManager::destroy<PluginManager>();
|
|
|
|
DependencyManager::destroy<CompositorHelper>(); // must be destroyed before the FramebufferCache
|
|
|
|
DependencyManager::destroy<SoundCacheScriptingInterface>();
|
|
|
|
DependencyManager::destroy<AudioInjectorManager>();
|
|
DependencyManager::destroy<AvatarManager>();
|
|
DependencyManager::destroy<AnimationCacheScriptingInterface>();
|
|
DependencyManager::destroy<AnimationCache>();
|
|
DependencyManager::destroy<FramebufferCache>();
|
|
DependencyManager::destroy<MaterialCacheScriptingInterface>();
|
|
DependencyManager::destroy<MaterialCache>();
|
|
DependencyManager::destroy<TextureCacheScriptingInterface>();
|
|
DependencyManager::destroy<TextureCache>();
|
|
DependencyManager::destroy<ModelCacheScriptingInterface>();
|
|
DependencyManager::destroy<ModelCache>();
|
|
DependencyManager::destroy<ModelFormatRegistry>();
|
|
DependencyManager::destroy<ScriptCache>();
|
|
DependencyManager::destroy<SoundCacheScriptingInterface>();
|
|
DependencyManager::destroy<SoundCache>();
|
|
DependencyManager::destroy<OctreeStatsProvider>();
|
|
DependencyManager::destroy<GeometryCache>();
|
|
|
|
if (auto resourceManager = DependencyManager::get<ResourceManager>()) {
|
|
resourceManager->cleanup();
|
|
}
|
|
|
|
// remove the NodeList from the DependencyManager
|
|
DependencyManager::destroy<NodeList>();
|
|
|
|
#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();
|
|
|
|
// make sure that the quit event has finished sending before we take the application down
|
|
if (auto closeEventSender = DependencyManager::get<CloseEventSender>()) {
|
|
while (!closeEventSender->hasFinishedQuitEvent() && !closeEventSender->hasTimedOutQuitEvent()) {
|
|
// sleep a little so we're not spinning at 100%
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
}
|
|
// quit the thread used by the closure event sender
|
|
closeEventSender->thread()->quit();
|
|
}
|
|
|
|
// Can't log to file past this point, FileLogger about to be deleted
|
|
qInstallMessageHandler(LogHandler::verboseMessageHandler);
|
|
|
|
#ifdef Q_OS_MAC
|
|
// 26 Feb 2021 - Tried re-enabling this call but OSX still crashes on exit.
|
|
//
|
|
// 10/16/2019 - Disabling this call. This causes known crashes (A), and it is not
|
|
// fully understood whether it might cause other unknown crashes (B).
|
|
//
|
|
// (A) Although we try to shutdown the ScriptEngine threads in onAboutToQuit, there is
|
|
// currently no guarantee that they have stopped. Waiting on them to stop has so far appeared to
|
|
// never return on Mac, causing the application to hang on shutdown. Because ScriptEngines
|
|
// may still be running, they may end up receiving events that are triggered from this processEvents call,
|
|
// and then try to access resources that are no longer available at this point in time.
|
|
// If the ScriptEngine threads were fully destroyed before getting here, this would
|
|
// not be an issue.
|
|
//
|
|
// (B) It seems likely that a bunch of potential event handlers are dependent on Application
|
|
// and other common dependencies to be available and not destroyed or in the middle of being
|
|
// destroyed.
|
|
|
|
|
|
// Clear the event queue before application is totally destructed.
|
|
// This will drain the messasge queue of pending "deleteLaters" queued up
|
|
// during shutdown of the script engines.
|
|
// We do this here because there is a possiblty that [NSApplication terminate:]
|
|
// will be called during processEvents which will invoke all static destructors.
|
|
// We want to postpone this utill the last possible moment.
|
|
//QCoreApplication::processEvents();
|
|
#endif
|
|
}
|
|
|
|
bool Application::isServerlessMode() const {
|
|
auto tree = getEntities()->getTree();
|
|
if (tree) {
|
|
return tree->isServerlessMode();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
QString Application::getUserAgent() {
|
|
if (QThread::currentThread() != thread()) {
|
|
QString userAgent;
|
|
|
|
BLOCKING_INVOKE_METHOD(this, "getUserAgent", Q_RETURN_ARG(QString, userAgent));
|
|
|
|
return userAgent;
|
|
}
|
|
|
|
QString userAgent = NetworkingConstants::OVERTE_USER_AGENT + "/" + BuildInfo::VERSION + "; "
|
|
+ QSysInfo::productType() + " " + QSysInfo::productVersion() + ")";
|
|
|
|
auto formatPluginName = [](QString name) -> QString { return name.trimmed().replace(" ", "-"); };
|
|
|
|
// For each plugin, add to userAgent
|
|
const auto& displayPlugins = PluginManager::getInstance()->getDisplayPlugins();
|
|
for (const auto& dp : displayPlugins) {
|
|
if (dp->isActive() && dp->isHmd()) {
|
|
userAgent += " " + formatPluginName(dp->getName());
|
|
}
|
|
}
|
|
const auto& inputPlugins = PluginManager::getInstance()->getInputPlugins();
|
|
for (const auto& ip : inputPlugins) {
|
|
if (ip->isActive()) {
|
|
userAgent += " " + formatPluginName(ip->getName());
|
|
}
|
|
}
|
|
// for codecs, we include all of them, even if not active
|
|
const auto& codecPlugins = PluginManager::getInstance()->getCodecPlugins();
|
|
for (const auto& cp : codecPlugins) {
|
|
userAgent += " " + formatPluginName(cp->getName());
|
|
}
|
|
|
|
return userAgent;
|
|
}
|
|
|
|
static const QString CONTENT_SET_NAME_QUERY_PARAM = "name";
|
|
void Application::replaceDomainContent(const QString& url, const QString& itemName) {
|
|
qCDebug(interfaceapp) << "Attempting to replace domain content";
|
|
QUrl msgUrl(url);
|
|
QUrlQuery urlQuery(msgUrl.query());
|
|
urlQuery.addQueryItem(CONTENT_SET_NAME_QUERY_PARAM, itemName);
|
|
msgUrl.setQuery(urlQuery.query(QUrl::QUrl::FullyEncoded));
|
|
QByteArray urlData(msgUrl.toString(QUrl::QUrl::FullyEncoded).toUtf8());
|
|
auto limitedNodeList = DependencyManager::get<NodeList>();
|
|
const auto& domainHandler = limitedNodeList->getDomainHandler();
|
|
|
|
auto octreeFilePacket = NLPacket::create(PacketType::DomainContentReplacementFromUrl, urlData.size(), true);
|
|
octreeFilePacket->write(urlData);
|
|
limitedNodeList->sendPacket(std::move(octreeFilePacket), domainHandler.getSockAddr());
|
|
|
|
auto addressManager = DependencyManager::get<AddressManager>();
|
|
addressManager->handleLookupString(DOMAIN_SPAWNING_POINT);
|
|
QString newHomeAddress = addressManager->getHost() + DOMAIN_SPAWNING_POINT;
|
|
qCDebug(interfaceapp) << "Setting new home bookmark to: " << newHomeAddress;
|
|
DependencyManager::get<LocationBookmarks>()->setHomeLocationToAddress(newHomeAddress);
|
|
}
|
|
|
|
void Application::openDirectory(const QString& path) {
|
|
if (QThread::currentThread() != thread()) {
|
|
QMetaObject::invokeMethod(this, "openDirectory", Q_ARG(const QString&, path));
|
|
return;
|
|
}
|
|
|
|
QString dirPath = path;
|
|
#if defined(Q_OS_WIN)
|
|
const QString FILE_SCHEME = "file:///";
|
|
#else
|
|
const QString FILE_SCHEME = "file://";
|
|
#endif
|
|
if (dirPath.startsWith(FILE_SCHEME)) {
|
|
dirPath.remove(0, FILE_SCHEME.length());
|
|
}
|
|
QFileInfo fileInfo(dirPath);
|
|
if (fileInfo.isDir()) {
|
|
auto scheme = QUrl(path).scheme();
|
|
QDesktopServices::unsetUrlHandler(scheme);
|
|
QDesktopServices::openUrl(path);
|
|
QDesktopServices::setUrlHandler(scheme, this, "showUrlHandler");
|
|
}
|
|
}
|
|
|
|
void Application::forceLoginWithTokens(const QString& tokens) {
|
|
DependencyManager::get<AccountManager>()->setAccessTokens(tokens);
|
|
Setting::Handle<bool>(KEEP_ME_LOGGED_IN_SETTING_NAME, true).set(true);
|
|
}
|
|
|
|
void Application::setConfigFileURL(const QString& fileUrl) {
|
|
DependencyManager::get<AccountManager>()->setConfigFileURL(fileUrl);
|
|
}
|
|
|
|
void Application::loadAvatarScripts(const QVector<QString>& urls) {
|
|
auto scriptEngines = DependencyManager::get<ScriptEngines>();
|
|
auto runningScripts = scriptEngines->getRunningScripts();
|
|
for (auto url : urls) {
|
|
int index = runningScripts.indexOf(url);
|
|
if (index < 0) {
|
|
auto scriptEnginePointer = scriptEngines->loadScript(url, false);
|
|
if (scriptEnginePointer) {
|
|
scriptEnginePointer->setType(ScriptManager::Type::AVATAR);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Application::unloadAvatarScripts() {
|
|
auto scriptEngines = DependencyManager::get<ScriptEngines>();
|
|
auto urls = scriptEngines->getRunningScripts();
|
|
for (auto url : urls) {
|
|
auto scriptEngine = scriptEngines->getScriptEngine(url);
|
|
if (scriptEngine->getType() == ScriptManager::Type::AVATAR) {
|
|
scriptEngines->stopScript(url, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Application::copyToClipboard(const QString& text) {
|
|
if (QThread::currentThread() != qApp->thread()) {
|
|
QMetaObject::invokeMethod(this, "copyToClipboard");
|
|
return;
|
|
}
|
|
|
|
// assume that the address is being copied because the user wants a shareable address
|
|
QApplication::clipboard()->setText(text);
|
|
}
|
|
|
|
void Application::registerScriptEngineWithApplicationServices(ScriptManagerPointer& scriptManager) {
|
|
auto scriptEngine = scriptManager->engine();
|
|
scriptManager->setEmitScriptUpdatesFunction([this]() {
|
|
SharedNodePointer entityServerNode = DependencyManager::get<NodeList>()->soloNodeOfType(NodeType::EntityServer);
|
|
return !entityServerNode || isPhysicsEnabled();
|
|
});
|
|
|
|
// setup the packet sender of the script engine's scripting interfaces so
|
|
// we can use the same ones from the application.
|
|
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
|
entityScriptingInterface->setPacketSender(_entityEditSender.get());
|
|
entityScriptingInterface->setEntityTree(getEntities()->getTree());
|
|
|
|
if (property(hifi::properties::TEST).isValid()) {
|
|
scriptEngine->registerGlobalObject("Test", TestScriptingInterface::getInstance());
|
|
}
|
|
|
|
scriptEngine->registerGlobalObject("PlatformInfo", PlatformInfoScriptingInterface::getInstance());
|
|
scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this));
|
|
|
|
scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get<AvatarManager>().data());
|
|
|
|
scriptEngine->registerGlobalObject("Camera", &_myCamera);
|
|
|
|
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
|
|
scriptEngine->registerGlobalObject("SpeechRecognizer", DependencyManager::get<SpeechRecognizer>().data());
|
|
#endif
|
|
|
|
ClipboardScriptingInterface* clipboardScriptable = new ClipboardScriptingInterface();
|
|
scriptEngine->registerGlobalObject("Clipboard", clipboardScriptable);
|
|
connect(scriptManager.get(), &ScriptManager::finished, clipboardScriptable, &ClipboardScriptingInterface::deleteLater);
|
|
|
|
scriptEngine->registerGlobalObject("Overlays", &_overlays);
|
|
|
|
bool clientScript = scriptManager->isClientScript();
|
|
|
|
#if !defined(DISABLE_QML)
|
|
scriptEngine->registerGlobalObject("OffscreenFlags", getOffscreenUI()->getFlags());
|
|
if (clientScript) {
|
|
scriptEngine->registerGlobalObject("Desktop", DependencyManager::get<DesktopScriptingInterface>().data());
|
|
} else {
|
|
auto desktopScriptingInterface = new DesktopScriptingInterface(nullptr, true);
|
|
scriptEngine->registerGlobalObject("Desktop", desktopScriptingInterface);
|
|
if (QThread::currentThread() != thread()) {
|
|
desktopScriptingInterface->moveToThread(thread());
|
|
}
|
|
}
|
|
#endif
|
|
|
|
scriptEngine->registerGlobalObject("Toolbars", DependencyManager::get<ToolbarScriptingInterface>().data());
|
|
|
|
scriptEngine->registerGlobalObject("Tablet", DependencyManager::get<TabletScriptingInterface>().data());
|
|
// FIXME remove these deprecated names for the tablet scripting interface
|
|
scriptEngine->registerGlobalObject("tabletInterface", DependencyManager::get<TabletScriptingInterface>().data());
|
|
|
|
auto toolbarScriptingInterface = DependencyManager::get<ToolbarScriptingInterface>().data();
|
|
DependencyManager::get<TabletScriptingInterface>().data()->setToolbarScriptingInterface(toolbarScriptingInterface);
|
|
|
|
scriptEngine->registerGlobalObject("Window", DependencyManager::get<WindowScriptingInterface>().data());
|
|
scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter,
|
|
LocationScriptingInterface::locationSetter, "Window");
|
|
// register `location` on the global object.
|
|
scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter,
|
|
LocationScriptingInterface::locationSetter);
|
|
|
|
scriptEngine->registerFunction("OverlayWindow", clientScript ? QmlWindowClass::constructor : QmlWindowClass::restricted_constructor);
|
|
#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML)
|
|
scriptEngine->registerFunction("OverlayWebWindow", clientScript ? QmlWebWindowClass::constructor : QmlWebWindowClass::restricted_constructor);
|
|
#endif
|
|
scriptEngine->registerFunction("QmlFragment", clientScript ? QmlFragmentClass::constructor : QmlFragmentClass::restricted_constructor);
|
|
|
|
scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance());
|
|
scriptEngine->registerGlobalObject("DesktopPreviewProvider", DependencyManager::get<DesktopPreviewProvider>().data());
|
|
#if !defined(DISABLE_QML)
|
|
scriptEngine->registerGlobalObject("Stats", Stats::getInstance());
|
|
#endif
|
|
scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance());
|
|
scriptEngine->registerGlobalObject("Snapshot", DependencyManager::get<Snapshot>().data());
|
|
scriptEngine->registerGlobalObject("AudioStats", DependencyManager::get<AudioClient>()->getStats().data());
|
|
scriptEngine->registerGlobalObject("AudioScope", DependencyManager::get<AudioScope>().data());
|
|
scriptEngine->registerGlobalObject("AvatarBookmarks", DependencyManager::get<AvatarBookmarks>().data());
|
|
scriptEngine->registerGlobalObject("LocationBookmarks", DependencyManager::get<LocationBookmarks>().data());
|
|
|
|
scriptEngine->registerGlobalObject("RayPick", DependencyManager::get<RayPickScriptingInterface>().data());
|
|
scriptEngine->registerGlobalObject("Picks", DependencyManager::get<PickScriptingInterface>().data());
|
|
scriptEngine->registerGlobalObject("Pointers", DependencyManager::get<PointerScriptingInterface>().data());
|
|
|
|
// Caches
|
|
scriptEngine->registerGlobalObject("AnimationCache", DependencyManager::get<AnimationCacheScriptingInterface>().data());
|
|
scriptEngine->registerGlobalObject("TextureCache", DependencyManager::get<TextureCacheScriptingInterface>().data());
|
|
scriptEngine->registerGlobalObject("MaterialCache", DependencyManager::get<MaterialCacheScriptingInterface>().data());
|
|
scriptEngine->registerGlobalObject("ModelCache", DependencyManager::get<ModelCacheScriptingInterface>().data());
|
|
scriptEngine->registerGlobalObject("SoundCache", DependencyManager::get<SoundCacheScriptingInterface>().data());
|
|
|
|
scriptEngine->registerGlobalObject("DialogsManager", _dialogsManagerScriptingInterface);
|
|
|
|
scriptEngine->registerGlobalObject("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED
|
|
scriptEngine->registerGlobalObject("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED
|
|
scriptEngine->registerGlobalObject("AccountServices", AccountServicesScriptingInterface::getInstance());
|
|
|
|
scriptEngine->registerGlobalObject("AvatarManager", DependencyManager::get<AvatarManager>().data());
|
|
|
|
scriptEngine->registerGlobalObject("LODManager", DependencyManager::get<LODManager>().data());
|
|
|
|
scriptEngine->registerGlobalObject("Keyboard", DependencyManager::get<KeyboardScriptingInterface>().data());
|
|
scriptEngine->registerGlobalObject("Performance", new PerformanceScriptingInterface());
|
|
|
|
scriptEngine->registerGlobalObject("Paths", DependencyManager::get<PathUtils>().data());
|
|
|
|
scriptEngine->registerGlobalObject("HMD", DependencyManager::get<HMDScriptingInterface>().data());
|
|
scriptEngine->registerFunction("HMD", "getHUDLookAtPosition2D", HMDScriptingInterface::getHUDLookAtPosition2D, 0);
|
|
scriptEngine->registerFunction("HMD", "getHUDLookAtPosition3D", HMDScriptingInterface::getHUDLookAtPosition3D, 0);
|
|
|
|
scriptEngine->registerGlobalObject("Scene", DependencyManager::get<SceneScriptingInterface>().data());
|
|
scriptEngine->registerGlobalObject("Render", RenderScriptingInterface::getInstance());
|
|
scriptEngine->registerGlobalObject("Workload", _gameWorkload._engine->getConfiguration().get());
|
|
|
|
scriptEngine->registerGlobalObject("Graphics", DependencyManager::get<GraphicsScriptingInterface>().data());
|
|
|
|
scriptEngine->registerGlobalObject("ScriptDiscoveryService", DependencyManager::get<ScriptEngines>().data());
|
|
scriptEngine->registerGlobalObject("Reticle", getApplicationCompositor().getReticleInterface());
|
|
|
|
scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get<UserActivityLoggerScriptingInterface>().data());
|
|
scriptEngine->registerGlobalObject("Users", DependencyManager::get<UsersScriptingInterface>().data());
|
|
|
|
if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) {
|
|
scriptEngine->registerGlobalObject("Steam", new SteamScriptingInterface(scriptManager.get(), steamClient.get()));
|
|
}
|
|
auto scriptingInterface = DependencyManager::get<controller::ScriptingInterface>();
|
|
scriptEngine->registerGlobalObject("Controller", scriptingInterface.data());
|
|
|
|
{
|
|
auto connection = std::make_shared<QMetaObject::Connection>();
|
|
*connection = scriptManager->connect(scriptManager.get(), &ScriptManager::scriptEnding, [this, scriptManager, connection]() {
|
|
// Request removal of controller routes with callbacks to a given script engine
|
|
auto userInputMapper = DependencyManager::get<UserInputMapper>();
|
|
// scheduleScriptEndpointCleanup will have the last instance of shared pointer to script manager
|
|
// so script manager will get deleted as soon as cleanup is done
|
|
userInputMapper->scheduleScriptEndpointCleanup(scriptManager);
|
|
QObject::disconnect(*connection);
|
|
});
|
|
}
|
|
|
|
UserInputMapper::registerControllerTypes(scriptEngine.get());
|
|
|
|
auto recordingInterface = DependencyManager::get<RecordingScriptingInterface>();
|
|
scriptEngine->registerGlobalObject("Recording", recordingInterface.data());
|
|
|
|
auto entityScriptServerLog = DependencyManager::get<EntityScriptServerLogClient>();
|
|
scriptEngine->registerGlobalObject("EntityScriptServerLog", entityScriptServerLog.data());
|
|
scriptEngine->registerGlobalObject("AvatarInputs", AvatarInputs::getInstance());
|
|
scriptEngine->registerGlobalObject("Selection", DependencyManager::get<SelectionScriptingInterface>().data());
|
|
scriptEngine->registerGlobalObject("AddressManager", DependencyManager::get<AddressManager>().data());
|
|
scriptEngine->registerGlobalObject("About", AboutUtil::getInstance());
|
|
scriptEngine->registerGlobalObject("HifiAbout", AboutUtil::getInstance()); // Deprecated.
|
|
scriptEngine->registerGlobalObject("ResourceRequestObserver", DependencyManager::get<ResourceRequestObserver>().data());
|
|
|
|
// connect this script engines printedMessage signal to the global ScriptEngines these various messages
|
|
auto scriptEngines = DependencyManager::get<ScriptEngines>().data();
|
|
connect(scriptManager.get(), &ScriptManager::printedMessage, scriptEngines, &ScriptEngines::onPrintedMessage);
|
|
connect(scriptManager.get(), &ScriptManager::errorMessage, scriptEngines, &ScriptEngines::onErrorMessage);
|
|
connect(scriptManager.get(), &ScriptManager::warningMessage, scriptEngines, &ScriptEngines::onWarningMessage);
|
|
connect(scriptManager.get(), &ScriptManager::infoMessage, scriptEngines, &ScriptEngines::onInfoMessage);
|
|
connect(scriptManager.get(), &ScriptManager::clearDebugWindow, scriptEngines, &ScriptEngines::onClearDebugWindow);
|
|
}
|
|
|
|
// Snapshots
|
|
void Application::addSnapshotOperator(const SnapshotOperator& snapshotOperator) {
|
|
std::lock_guard<std::mutex> lock(_snapshotMutex);
|
|
_snapshotOperators.push(snapshotOperator);
|
|
_hasPrimarySnapshot = _hasPrimarySnapshot || std::get<2>(snapshotOperator);
|
|
}
|
|
|
|
bool Application::takeSnapshotOperators(std::queue<SnapshotOperator>& snapshotOperators) {
|
|
std::lock_guard<std::mutex> lock(_snapshotMutex);
|
|
bool hasPrimarySnapshot = _hasPrimarySnapshot;
|
|
_hasPrimarySnapshot = false;
|
|
_snapshotOperators.swap(snapshotOperators);
|
|
return hasPrimarySnapshot;
|
|
}
|
|
|
|
void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio, const QString& filename) {
|
|
addSnapshotOperator(std::make_tuple([notify, includeAnimated, aspectRatio, filename](const QImage& snapshot) {
|
|
qApp->postLambdaEvent([snapshot, notify, includeAnimated, aspectRatio, filename] {
|
|
QString path = DependencyManager::get<Snapshot>()->saveSnapshot(snapshot, filename, TestScriptingInterface::getInstance()->getTestResultsLocation());
|
|
|
|
// If we're not doing an animated snapshot as well...
|
|
if (!includeAnimated) {
|
|
if (!path.isEmpty()) {
|
|
// Tell the dependency manager that the capture of the still snapshot has taken place.
|
|
emit DependencyManager::get<WindowScriptingInterface>()->stillSnapshotTaken(path, notify);
|
|
}
|
|
} else if (!SnapshotAnimated::isAlreadyTakingSnapshotAnimated()) {
|
|
// Get an animated GIF snapshot and save it
|
|
SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, DependencyManager::get<WindowScriptingInterface>());
|
|
}
|
|
});
|
|
}, aspectRatio, true));
|
|
}
|
|
|
|
void Application::takeSecondaryCameraSnapshot(const bool& notify, const QString& filename) {
|
|
addSnapshotOperator(std::make_tuple([notify, filename](const QImage& snapshot) {
|
|
qApp->postLambdaEvent([snapshot, notify, filename] {
|
|
QString snapshotPath = DependencyManager::get<Snapshot>()->saveSnapshot(snapshot, filename, TestScriptingInterface::getInstance()->getTestResultsLocation());
|
|
|
|
emit DependencyManager::get<WindowScriptingInterface>()->stillSnapshotTaken(snapshotPath, notify);
|
|
});
|
|
}, 0.0f, false));
|
|
}
|
|
|
|
void Application::takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const bool& notify, const QString& filename) {
|
|
postLambdaEvent([notify, filename, cubemapOutputFormat, cameraPosition] {
|
|
DependencyManager::get<Snapshot>()->save360Snapshot(cameraPosition, cubemapOutputFormat, notify, filename);
|
|
});
|
|
}
|
|
|
|
void Application::shareSnapshot(const QString& path, const QUrl& href) {
|
|
postLambdaEvent([path, href] {
|
|
// not much to do here, everything is done in snapshot code...
|
|
DependencyManager::get<Snapshot>()->uploadSnapshot(path, href);
|
|
});
|
|
}
|
|
|
|
#if defined(Q_OS_ANDROID)
|
|
void Application::beforeEnterBackground() {
|
|
auto nodeList = DependencyManager::get<NodeList>();
|
|
nodeList->setSendDomainServerCheckInEnabled(false);
|
|
nodeList->reset("Entering background", true);
|
|
clearDomainOctreeDetails();
|
|
}
|
|
|
|
void Application::enterBackground() {
|
|
QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(),
|
|
"stop", Qt::BlockingQueuedConnection);
|
|
// Quest only supports one plugin which can't be deactivated currently
|
|
#if !defined(ANDROID_APP_QUEST_INTERFACE)
|
|
if (getActiveDisplayPlugin()->isActive()) {
|
|
getActiveDisplayPlugin()->deactivate();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void Application::enterForeground() {
|
|
QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(),
|
|
"start", Qt::BlockingQueuedConnection);
|
|
// Quest only supports one plugin which can't be deactivated currently
|
|
#if !defined(ANDROID_APP_QUEST_INTERFACE)
|
|
if (!getActiveDisplayPlugin() || getActiveDisplayPlugin()->isActive() || !getActiveDisplayPlugin()->activate()) {
|
|
qWarning() << "Could not re-activate display plugin";
|
|
}
|
|
#endif
|
|
auto nodeList = DependencyManager::get<NodeList>();
|
|
nodeList->setSendDomainServerCheckInEnabled(true);
|
|
}
|
|
|
|
void Application::toggleAwayMode(){
|
|
QKeyEvent event = QKeyEvent (QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier);
|
|
QCoreApplication::sendEvent (this, &event);
|
|
}
|
|
#endif
|
|
|
|
// FIXME? perhaps two, one for the main thread and one for the offscreen UI rendering thread?
|
|
static const int UI_RESERVED_THREADS = 1;
|
|
// Windows won't let you have all the cores
|
|
static const int OS_RESERVED_THREADS = 1;
|
|
void Application::updateThreadPoolCount() const {
|
|
auto reservedThreads = UI_RESERVED_THREADS + OS_RESERVED_THREADS + _displayPlugin->getRequiredThreadCount();
|
|
auto availableThreads = QThread::idealThreadCount() - reservedThreads;
|
|
auto threadPoolSize = std::max(MIN_PROCESSING_THREAD_POOL_SIZE, availableThreads);
|
|
qCDebug(interfaceapp) << "Ideal Thread Count " << QThread::idealThreadCount();
|
|
qCDebug(interfaceapp) << "Reserved threads " << reservedThreads;
|
|
qCDebug(interfaceapp) << "Setting thread pool size to " << threadPoolSize;
|
|
QThreadPool::globalInstance()->setMaxThreadCount(threadPoolSize);
|
|
}
|
|
|
|
void Application::gotoTutorial() {
|
|
const QString TUTORIAL_ADDRESS = "file:///~/serverless/tutorial.json";
|
|
DependencyManager::get<AddressManager>()->handleLookupString(TUTORIAL_ADDRESS);
|
|
}
|
|
|
|
void Application::goToErrorDomainURL(QUrl errorDomainURL) {
|
|
// disable physics until we have enough information about our new location to not cause craziness.
|
|
setIsServerlessMode(errorDomainURL.scheme() != URL_SCHEME_OVERTE);
|
|
if (isServerlessMode()) {
|
|
loadErrorDomain(errorDomainURL);
|
|
}
|
|
updateWindowTitle();
|
|
}
|
|
|
|
void Application::handleLocalServerConnection() const {
|
|
auto server = qobject_cast<QLocalServer*>(sender());
|
|
Q_ASSERT(server != nullptr);
|
|
|
|
qCDebug(interfaceapp) << "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<QLocalSocket*>(sender());
|
|
Q_ASSERT(socket != nullptr);
|
|
|
|
auto message = socket->readAll();
|
|
socket->deleteLater();
|
|
|
|
qCDebug(interfaceapp) << "Read from connection: " << message;
|
|
|
|
// If we received a message, try to open it as a URL
|
|
if (message.length() > 0) {
|
|
DependencyManager::get<AddressManager>()->handleLookupString(QString::fromUtf8(message));
|
|
}
|
|
}
|
|
|
|
void Application::showUrlHandler(const QUrl& url) {
|
|
if (QThread::currentThread() != thread()) {
|
|
QMetaObject::invokeMethod(this, "showUrlHandler", Q_ARG(const QUrl&, url));
|
|
return;
|
|
}
|
|
|
|
ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Confirm openUrl", "Do you recognize this path or code and want to open or execute it: " + url.toDisplayString());
|
|
QObject::connect(dlg, &ModalDialogListener::response, this, [=](QVariant answer) {
|
|
QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr);
|
|
if (QMessageBox::Yes == static_cast<QMessageBox::StandardButton>(answer.toInt())) {
|
|
// Unset the handler, open the URL, and the reset the handler
|
|
QDesktopServices::unsetUrlHandler(url.scheme());
|
|
QDesktopServices::openUrl(url);
|
|
QDesktopServices::setUrlHandler(url.scheme(), this, "showUrlHandler");
|
|
}
|
|
});
|
|
}
|
|
|
|
void Application::hmdVisibleChanged(bool visible) {
|
|
// TODO
|
|
// calling start and stop will change audio input and ouput to default audio devices.
|
|
// we need to add a pause/unpause functionality to AudioClient for this to work properly
|
|
#if 0
|
|
if (visible) {
|
|
QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(), "start", Qt::QueuedConnection);
|
|
} else {
|
|
QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(), "stop", Qt::QueuedConnection);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void Application::reloadResourceCaches() {
|
|
resetPhysicsReadyInformation();
|
|
|
|
// Query the octree to refresh everything in view
|
|
_queryExpiry = SteadyClock::now();
|
|
_octreeQuery.incrementConnectionID();
|
|
|
|
queryOctree(NodeType::EntityServer, PacketType::EntityQuery);
|
|
|
|
getMyAvatar()->prepareAvatarEntityDataForReload();
|
|
// Clear the entities and their renderables
|
|
getEntities()->clear();
|
|
|
|
DependencyManager::get<AssetClient>()->clearCache();
|
|
//It's already cleared in reloadAllScripts so I'm not sure this is necessary.
|
|
//DependencyManager::get<ScriptCache>()->clearCache();
|
|
|
|
// Clear all the resource caches
|
|
DependencyManager::get<ResourceCacheSharedItems>()->clear();
|
|
DependencyManager::get<AnimationCache>()->refreshAll();
|
|
DependencyManager::get<SoundCache>()->refreshAll();
|
|
DependencyManager::get<MaterialCache>()->refreshAll();
|
|
DependencyManager::get<ModelCache>()->refreshAll();
|
|
ShaderCache::instance().refreshAll();
|
|
DependencyManager::get<TextureCache>()->refreshAll();
|
|
DependencyManager::get<recording::ClipCache>()->refreshAll();
|
|
|
|
DependencyManager::get<NodeList>()->reset("Reloading resources"); // Force redownload of .fst models
|
|
|
|
DependencyManager::get<ScriptEngines>()->reloadAllScripts();
|
|
getOffscreenUI()->clearCache();
|
|
|
|
DependencyManager::get<Keyboard>()->createKeyboard();
|
|
|
|
getMyAvatar()->resetFullAvatarURL();
|
|
}
|
|
|
|
void Application::updateHeartbeat() const {
|
|
DeadlockWatchdogThread::updateHeartbeat();
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// cause main thread to be unresponsive for 35 seconds
|
|
void Application::unresponsiveApplication() {
|
|
// to avoid compiler warnings about a loop that will never exit
|
|
uint64_t start = usecTimestampNow();
|
|
uint64_t UNRESPONSIVE_FOR_SECONDS = 35;
|
|
uint64_t UNRESPONSIVE_FOR_USECS = UNRESPONSIVE_FOR_SECONDS * USECS_PER_SECOND;
|
|
qCDebug(interfaceapp) << "Intentionally cause Interface to be unresponsive for " << UNRESPONSIVE_FOR_SECONDS << " seconds";
|
|
while (usecTimestampNow() - start < UNRESPONSIVE_FOR_USECS) {
|
|
QThread::sleep(1);
|
|
}
|
|
}
|
|
|
|
// used to test "shutdown" crash annotation.
|
|
void Application::crashOnShutdown() {
|
|
qDebug() << "crashOnShutdown(), ON PURPOSE!";
|
|
_crashOnShutdown = true;
|
|
quit();
|
|
}
|
|
|
|
void Application::rotationModeChanged() const {
|
|
if (!Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) {
|
|
getMyAvatar()->setHeadPitch(0);
|
|
}
|
|
}
|
|
|
|
void Application::setIsServerlessMode(bool serverlessDomain) {
|
|
DependencyManager::get<NodeList>()->setSendDomainServerCheckInEnabled(!serverlessDomain);
|
|
auto tree = getEntities()->getTree();
|
|
if (tree) {
|
|
tree->setIsServerlessMode(serverlessDomain);
|
|
_waitForServerlessToBeSet = false;
|
|
}
|
|
}
|
|
|
|
std::map<QString, QString> Application::prepareServerlessDomainContents(QUrl domainURL, QByteArray data) {
|
|
QUuid serverlessSessionID = QUuid::createUuid();
|
|
getMyAvatar()->setSessionUUID(serverlessSessionID);
|
|
auto nodeList = DependencyManager::get<NodeList>();
|
|
nodeList->setSessionUUID(serverlessSessionID);
|
|
|
|
// there is no domain-server to tell us our permissions, so enable all
|
|
NodePermissions permissions;
|
|
permissions.setAll(true);
|
|
nodeList->setPermissions(permissions);
|
|
|
|
// FIXME: Lock the main tree and import directly into it.
|
|
EntityTreePointer tmpTree(std::make_shared<EntityTree>());
|
|
tmpTree->setIsServerlessMode(true);
|
|
tmpTree->createRootElement();
|
|
auto myAvatar = getMyAvatar();
|
|
tmpTree->setMyAvatar(myAvatar);
|
|
bool success = tmpTree->readFromByteArray(domainURL.toString(), data);
|
|
if (success) {
|
|
tmpTree->reaverageOctreeElements();
|
|
tmpTree->sendEntities(_entityEditSender.get(), getEntities()->getTree(), "domain", 0, 0, 0);
|
|
}
|
|
std::map<QString, QString> namedPaths = tmpTree->getNamedPaths();
|
|
|
|
// we must manually eraseAllOctreeElements(false) else the tmpTree will mem-leak
|
|
tmpTree->eraseAllOctreeElements(false);
|
|
|
|
return namedPaths;
|
|
}
|
|
|
|
void Application::loadServerlessDomain(QUrl domainURL) {
|
|
if (QThread::currentThread() != thread()) {
|
|
QMetaObject::invokeMethod(this, "loadServerlessDomain", Q_ARG(QUrl, domainURL));
|
|
return;
|
|
}
|
|
|
|
if (domainURL.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
QString trimmedUrl = domainURL.toString().trimmed();
|
|
bool DEFAULT_IS_OBSERVABLE = true;
|
|
const qint64 DEFAULT_CALLER_ID = -1;
|
|
auto request = DependencyManager::get<ResourceManager>()->createResourceRequest(
|
|
this, trimmedUrl, DEFAULT_IS_OBSERVABLE, DEFAULT_CALLER_ID, "Application::loadServerlessDomain");
|
|
|
|
if (!request) {
|
|
return;
|
|
}
|
|
|
|
connect(request, &ResourceRequest::finished, this, [=]() {
|
|
if (request->getResult() == ResourceRequest::Success) {
|
|
auto namedPaths = prepareServerlessDomainContents(domainURL, request->getData());
|
|
auto nodeList = DependencyManager::get<NodeList>();
|
|
nodeList->getDomainHandler().connectedToServerless(namedPaths);
|
|
_octreeProcessor->getFullSceneReceivedCounter()++;
|
|
}
|
|
request->deleteLater();
|
|
});
|
|
request->send();
|
|
}
|
|
|
|
void Application::loadErrorDomain(QUrl domainURL) {
|
|
if (QThread::currentThread() != thread()) {
|
|
QMetaObject::invokeMethod(this, "loadErrorDomain", Q_ARG(QUrl, domainURL));
|
|
return;
|
|
}
|
|
|
|
loadServerlessDomain(domainURL);
|
|
}
|
|
|
|
void Application::setIsInterstitialMode(bool interstitialMode) {
|
|
bool enableInterstitial = DependencyManager::get<NodeList>()->getDomainHandler().getInterstitialModeEnabled();
|
|
if (enableInterstitial) {
|
|
if (_interstitialMode != interstitialMode) {
|
|
_interstitialMode = interstitialMode;
|
|
emit interstitialModeChanged(_interstitialMode);
|
|
|
|
DependencyManager::get<AudioClient>()->setAudioPaused(_interstitialMode);
|
|
DependencyManager::get<AvatarManager>()->setMyAvatarDataPacketsPaused(_interstitialMode);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Application::updateVerboseLogging() {
|
|
auto menu = Menu::getInstance();
|
|
if (!menu) {
|
|
return;
|
|
}
|
|
bool enable = menu->isOptionChecked(MenuOption::VerboseLogging);
|
|
|
|
QString rules =
|
|
"hifi.*.info=%1\n"
|
|
"hifi.audio-stream.debug=false\n"
|
|
"hifi.audio-stream.info=false";
|
|
rules = rules.arg(enable ? "true" : "false");
|
|
QLoggingCategory::setFilterRules(rules);
|
|
}
|
|
|
|
static const QString CACHEBUST_SCRIPT_REQUIRE_SETTING_NAME = "cachebustScriptRequire";
|
|
void Application::setCachebustRequire() {
|
|
auto menu = Menu::getInstance();
|
|
if (!menu) {
|
|
return;
|
|
}
|
|
bool enable = menu->isOptionChecked(MenuOption::CachebustRequire);
|
|
|
|
Setting::Handle<bool>{ CACHEBUST_SCRIPT_REQUIRE_SETTING_NAME, false }.set(enable);
|
|
}
|
|
|
|
QString Application::getGraphicsCardType() {
|
|
return GPUIdent::getInstance()->getName();
|
|
}
|
|
|
|
bool Application::gpuTextureMemSizeStable() {
|
|
auto renderConfig = qApp->getRenderEngine()->getConfiguration();
|
|
auto renderStats = renderConfig->getConfig<render::EngineStats>("Stats");
|
|
|
|
qint64 textureResourceGPUMemSize = renderStats->textureResourceGPUMemSize;
|
|
qint64 texturePopulatedGPUMemSize = renderStats->textureResourcePopulatedGPUMemSize;
|
|
qint64 textureTransferSize = renderStats->texturePendingGPUTransferSize;
|
|
|
|
if (_gpuTextureMemSizeAtLastCheck == textureResourceGPUMemSize) {
|
|
_gpuTextureMemSizeStabilityCount++;
|
|
} else {
|
|
_gpuTextureMemSizeStabilityCount = 0;
|
|
}
|
|
_gpuTextureMemSizeAtLastCheck = textureResourceGPUMemSize;
|
|
|
|
if (_gpuTextureMemSizeStabilityCount >= _minimumGPUTextureMemSizeStabilityCount) {
|
|
return (textureResourceGPUMemSize == texturePopulatedGPUMemSize) && (textureTransferSize == 0);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Application::runTests() {
|
|
runTimingTests();
|
|
runUnitTests();
|
|
}
|
|
|
|
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.
|
|
_octreeProcessor->getFullSceneReceivedCounter() = 0;
|
|
_fullSceneCounterAtLastPhysicsCheck = 0;
|
|
_gpuTextureMemSizeStabilityCount = 0;
|
|
_gpuTextureMemSizeAtLastCheck = 0;
|
|
_physicsEnabled = false;
|
|
_octreeProcessor->stopSafeLanding();
|
|
}
|
|
|
|
static const QString ACTIVE_DISPLAY_PLUGIN_SETTING_NAME = "activeDisplayPlugin";
|
|
void Application::onAboutToQuit() {
|
|
auto &ch = CrashHandler::getInstance();
|
|
ch.setAnnotation("shutdown", "1");
|
|
|
|
// quickly save AvatarEntityData before the EntityTree is dismantled
|
|
getMyAvatar()->saveAvatarEntityDataToSettings();
|
|
|
|
emit beforeAboutToQuit();
|
|
|
|
if (getLoginDialogPoppedUp() && _firstRun.get()) {
|
|
_firstRun.set(false);
|
|
}
|
|
|
|
for(const auto& inputPlugin : PluginManager::getInstance()->getInputPlugins()) {
|
|
if (inputPlugin->isActive()) {
|
|
inputPlugin->deactivate();
|
|
}
|
|
}
|
|
|
|
// The active display plugin needs to be loaded before the menu system is active,
|
|
// so its persisted explicitly here
|
|
Setting::Handle<QString>{ ACTIVE_DISPLAY_PLUGIN_SETTING_NAME }.set(getActiveDisplayPlugin()->getName());
|
|
|
|
getActiveDisplayPlugin()->deactivate();
|
|
if (_autoSwitchDisplayModeSupportedHMDPlugin
|
|
&& _autoSwitchDisplayModeSupportedHMDPlugin->isSessionActive()) {
|
|
_autoSwitchDisplayModeSupportedHMDPlugin->endSession();
|
|
}
|
|
// use the CloseEventSender via a QThread to send an event that says the user asked for the app to close
|
|
DependencyManager::get<CloseEventSender>()->startThread();
|
|
|
|
// Hide Running Scripts dialog so that it gets destroyed in an orderly manner; prevents warnings at shutdown.
|
|
#if !defined(DISABLE_QML)
|
|
getOffscreenUI()->hide("RunningScripts");
|
|
#endif
|
|
|
|
_aboutToQuit = true;
|
|
|
|
cleanupBeforeQuit();
|
|
|
|
if (_crashOnShutdown) {
|
|
// triggered by crash menu
|
|
crash::nullDeref();
|
|
}
|
|
|
|
getRefreshRateManager().setRefreshRateRegime(RefreshRateManager::RefreshRateRegime::SHUTDOWN);
|
|
}
|
|
|
|
void Application::loadSettings(const QCommandLineParser& parser) {
|
|
|
|
sessionRunTime.set(0); // Just clean living. We're about to saveSettings, which will update value.
|
|
DependencyManager::get<AudioClient>()->loadSettings();
|
|
DependencyManager::get<LODManager>()->loadSettings();
|
|
|
|
auto menu = Menu::getInstance();
|
|
menu->loadSettings();
|
|
|
|
// override the menu option show overlays to always be true on startup
|
|
menu->setIsOptionChecked(MenuOption::Overlays, true);
|
|
|
|
// 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();
|
|
if (plugins.size() > 0) {
|
|
for (auto plugin : plugins) {
|
|
if (auto action = menu->getActionForOption(plugin->getName())) {
|
|
action->setChecked(true);
|
|
action->trigger();
|
|
// Find and activated highest priority plugin, bail for the rest
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool isFirstPerson = false;
|
|
if (parser.isSet("no-launcher")) {
|
|
const auto& displayPlugins = pluginManager->getDisplayPlugins();
|
|
for (const auto& plugin : displayPlugins) {
|
|
if (!plugin->isHmd()) {
|
|
if (auto action = menu->getActionForOption(plugin->getName())) {
|
|
action->setChecked(true);
|
|
action->trigger();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
isFirstPerson = (qApp->isHMDMode());
|
|
} else {
|
|
if (_firstRun.get()) {
|
|
// If this is our first run, and no preferred devices were set, default to
|
|
// an HMD device if available.
|
|
const auto& displayPlugins = pluginManager->getDisplayPlugins();
|
|
for (const auto& plugin : displayPlugins) {
|
|
if (plugin->isHmd()) {
|
|
if (auto action = menu->getActionForOption(plugin->getName())) {
|
|
action->setChecked(true);
|
|
action->trigger();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
isFirstPerson = (qApp->isHMDMode());
|
|
} else {
|
|
// if this is not the first run, the camera will be initialized differently depending on user settings
|
|
if (qApp->isHMDMode()) {
|
|
// if the HMD is active, use first-person camera, unless the appropriate setting is checked
|
|
isFirstPerson = menu->isOptionChecked(MenuOption::FirstPersonHMD);
|
|
} else {
|
|
// if HMD is not active, only use first person if the menu option is checked
|
|
isFirstPerson = menu->isOptionChecked(MenuOption::FirstPersonLookAt);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load settings of the RenderScritpingInterface
|
|
// Do that explicitely before being used
|
|
RenderScriptingInterface::getInstance()->loadSettings();
|
|
|
|
// Setup the PerformanceManager which will enforce the several settings to match the Preset
|
|
// On the first run, the Preset is evaluated from the
|
|
getPerformanceManager().setupPerformancePresetSettings(_firstRun.get());
|
|
|
|
// finish initializing the camera, based on everything we checked above. Third person camera will be used if no settings
|
|
// dictated that we should be in first person
|
|
Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPersonLookAt, isFirstPerson);
|
|
Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !isFirstPerson);
|
|
_myCamera.setMode((isFirstPerson) ? CAMERA_MODE_FIRST_PERSON_LOOK_AT : CAMERA_MODE_LOOK_AT);
|
|
cameraMenuChanged();
|
|
|
|
const auto& inputs = pluginManager->getInputPlugins();
|
|
for (const auto& plugin : inputs) {
|
|
if (!plugin->isActive()) {
|
|
plugin->activate();
|
|
}
|
|
}
|
|
|
|
QSharedPointer<scripting::Audio> audioScriptingInterface = qSharedPointerDynamicCast<scripting::Audio>(DependencyManager::get<AudioScriptingInterface>());
|
|
if (audioScriptingInterface) {
|
|
audioScriptingInterface->loadData();
|
|
}
|
|
|
|
getMyAvatar()->loadData();
|
|
|
|
auto bucketEnum = QMetaEnum::fromType<ExternalResource::Bucket>();
|
|
auto externalResource = ExternalResource::getInstance();
|
|
|
|
for (int i = 0; i < bucketEnum.keyCount(); i++) {
|
|
const char* keyName = bucketEnum.key(i);
|
|
QString setting("ExternalResource/");
|
|
setting += keyName;
|
|
auto bucket = static_cast<ExternalResource::Bucket>(bucketEnum.keyToValue(keyName));
|
|
Setting::Handle<QString> url(setting, externalResource->getBase(bucket));
|
|
externalResource->setBase(bucket, url.get());
|
|
}
|
|
|
|
// the setter function isn't called, so update the theme colors now
|
|
updateThemeColors();
|
|
|
|
_settingsLoaded = true;
|
|
}
|
|
|
|
void Application::saveSettings() const {
|
|
sessionRunTime.set(_sessionRunTimer.elapsed() / MSECS_PER_SECOND);
|
|
DependencyManager::get<AudioClient>()->saveSettings();
|
|
DependencyManager::get<LODManager>()->saveSettings();
|
|
|
|
QSharedPointer<scripting::Audio> audioScriptingInterface = qSharedPointerDynamicCast<scripting::Audio>(DependencyManager::get<AudioScriptingInterface>());
|
|
if (audioScriptingInterface) {
|
|
audioScriptingInterface->saveData();
|
|
}
|
|
|
|
Menu::getInstance()->saveSettings();
|
|
getMyAvatar()->saveData();
|
|
PluginManager::getInstance()->saveSettings();
|
|
|
|
// Don't save external resource paths until such time as there's UI to select or set alternatives. Otherwise new default
|
|
// values won't be used unless Interface.json entries are manually remove or Interface.json is deleted.
|
|
/*
|
|
auto bucketEnum = QMetaEnum::fromType<ExternalResource::Bucket>();
|
|
auto externalResource = ExternalResource::getInstance();
|
|
|
|
for (int i = 0; i < bucketEnum.keyCount(); i++) {
|
|
const char* keyName = bucketEnum.key(i);
|
|
QString setting("ExternalResource/");
|
|
setting += keyName;
|
|
auto bucket = static_cast<ExternalResource::Bucket>(bucketEnum.keyToValue(keyName));
|
|
Setting::Handle<QString> url(setting, externalResource->getBase(bucket));
|
|
url.set(externalResource->getBase(bucket));
|
|
}
|
|
*/
|
|
}
|
|
|
|
// This is currently not used, but could be invoked if the user wants to go to the place embedded in an
|
|
// Interface-taken snapshot. (It was developed for drag and drop, before we had asset-server loading or in-world browsers.)
|
|
bool Application::acceptSnapshot(const QString& urlString) {
|
|
QUrl url(urlString);
|
|
QString snapshotPath = url.toLocalFile();
|
|
|
|
SnapshotMetaData* snapshotData = DependencyManager::get<Snapshot>()->parseSnapshotData(snapshotPath);
|
|
if (snapshotData) {
|
|
if (!snapshotData->getURL().toString().isEmpty()) {
|
|
DependencyManager::get<AddressManager>()->handleLookupString(snapshotData->getURL().toString());
|
|
}
|
|
} else {
|
|
OffscreenUi::asyncWarning("", "No location details were found in the file\n" +
|
|
snapshotPath + "\nTry dragging in an authentic Hifi snapshot.");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Application::setSessionUUID(const QUuid& sessionUUID) const {
|
|
Physics::setSessionUUID(sessionUUID);
|
|
}
|
|
|
|
void Application::domainURLChanged(QUrl domainURL) {
|
|
// disable physics until we have enough information about our new location to not cause craziness.
|
|
setIsServerlessMode(domainURL.scheme() != URL_SCHEME_OVERTE);
|
|
if (isServerlessMode()) {
|
|
loadServerlessDomain(domainURL);
|
|
}
|
|
updateWindowTitle();
|
|
}
|
|
|
|
void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) {
|
|
DomainHandler::ConnectionRefusedReason reasonCode = static_cast<DomainHandler::ConnectionRefusedReason>(reasonCodeInt);
|
|
|
|
if (reasonCode == DomainHandler::ConnectionRefusedReason::TooManyUsers && !extraInfo.isEmpty()) {
|
|
DependencyManager::get<AddressManager>()->handleLookupString(extraInfo);
|
|
return;
|
|
}
|
|
|
|
switch (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::asyncWarning("", message);
|
|
getMyAvatar()->setWorldVelocity(glm::vec3(0.0f));
|
|
break;
|
|
}
|
|
default:
|
|
// nothing to do.
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Application::updateWindowTitle() const {
|
|
auto nodeList = DependencyManager::get<NodeList>();
|
|
auto accountManager = DependencyManager::get<AccountManager>();
|
|
auto domainAccountManager = DependencyManager::get<DomainAccountManager>();
|
|
auto isInErrorState = nodeList->getDomainHandler().isInErrorState();
|
|
bool isMetaverseLoggedIn = accountManager->isLoggedIn();
|
|
bool hasDomainLogIn = domainAccountManager->hasLogIn();
|
|
bool isDomainLoggedIn = domainAccountManager->isLoggedIn();
|
|
QString authedDomainName = domainAccountManager->getAuthedDomainName();
|
|
|
|
QString buildVersion = " - Overte - " +
|
|
(BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable ? QString("Version") : QString("Build")) +
|
|
" " + applicationVersion();
|
|
|
|
QString connectionStatus = isInErrorState ? " (ERROR CONNECTING)"
|
|
: nodeList->getDomainHandler().isConnected() ? ""
|
|
: " (NOT CONNECTED)";
|
|
|
|
QString metaverseUsername = accountManager->getAccountInfo().getUsername();
|
|
QString domainUsername = domainAccountManager->getUsername();
|
|
|
|
auto& ch = CrashHandler::getInstance();
|
|
ch.setAnnotation("sentry[user][username]", metaverseUsername.toStdString());
|
|
|
|
QString currentPlaceName;
|
|
if (isServerlessMode()) {
|
|
if (isInErrorState) {
|
|
currentPlaceName = "Serverless: " + nodeList->getDomainHandler().getErrorDomainURL().toString();
|
|
} else {
|
|
currentPlaceName = "Serverless: " + DependencyManager::get<AddressManager>()->getDomainURL().toString();
|
|
}
|
|
} else {
|
|
currentPlaceName = DependencyManager::get<AddressManager>()->getDomainURL().host();
|
|
if (currentPlaceName.isEmpty()) {
|
|
currentPlaceName = nodeList->getDomainHandler().getHostname();
|
|
}
|
|
}
|
|
|
|
QString metaverseDetails;
|
|
if (isMetaverseLoggedIn) {
|
|
metaverseDetails = " (Directory Services: Connected to " + MetaverseAPI::getCurrentMetaverseServerURL().toString() +
|
|
" as " + metaverseUsername + ")";
|
|
} else {
|
|
metaverseDetails = " (Directory Services: Not Logged In)";
|
|
}
|
|
|
|
QString domainDetails;
|
|
if (hasDomainLogIn) {
|
|
if (currentPlaceName == authedDomainName && isDomainLoggedIn) {
|
|
domainDetails = " (Domain: Logged in as " + domainUsername + ")";
|
|
} else {
|
|
domainDetails = " (Domain: Not Logged In)";
|
|
}
|
|
} else {
|
|
domainDetails = "";
|
|
}
|
|
|
|
QString title = currentPlaceName + connectionStatus + metaverseDetails + domainDetails + buildVersion;
|
|
|
|
#ifndef WIN32
|
|
// crashes with vs2013/win32
|
|
qCDebug(interfaceapp, "Application title set to: %s", title.toStdString().c_str());
|
|
#endif
|
|
_window->setWindowTitle(title);
|
|
|
|
// updateTitleWindow gets called whenever there's a change regarding the domain, so rather
|
|
// than placing this within domainURLChanged, it's placed here to cover the other potential cases.
|
|
DependencyManager::get<MessagesClient>()->sendLocalMessage("Toolbar-DomainChanged", "");
|
|
}
|
|
|
|
void Application::nodeAdded(SharedNodePointer node) {
|
|
if (node->getType() == NodeType::EntityServer) {
|
|
if (_failedToConnectToEntityServer && !_entityServerConnectionTimer.isActive()) {
|
|
_octreeProcessor->stopSafeLanding();
|
|
_failedToConnectToEntityServer = false;
|
|
} else if (_entityServerConnectionTimer.isActive()) {
|
|
_entityServerConnectionTimer.stop();
|
|
}
|
|
_octreeProcessor->startSafeLanding();
|
|
_entityServerConnectionTimer.setInterval(ENTITY_SERVER_CONNECTION_TIMEOUT);
|
|
_entityServerConnectionTimer.start();
|
|
}
|
|
}
|
|
|
|
void Application::nodeActivated(SharedNodePointer node) {
|
|
if (node->getType() == NodeType::AssetServer) {
|
|
// asset server just connected - check if we have the asset browser showing
|
|
|
|
#if !defined(DISABLE_QML)
|
|
auto offscreenUi = getOffscreenUI();
|
|
if (offscreenUi) {
|
|
auto nodeList = DependencyManager::get<NodeList>();
|
|
|
|
if (nodeList->getThisNodeCanWriteAssets()) {
|
|
// call reload on the shown asset browser dialog to get the mappings (if permissions allow)
|
|
auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild<QQuickItem*>("AssetServer") : nullptr;
|
|
if (assetDialog) {
|
|
QMetaObject::invokeMethod(assetDialog, "reload");
|
|
}
|
|
} else {
|
|
// we switched to an Asset Server that we can't modify, hide the Asset Browser
|
|
offscreenUi->hide("AssetServer");
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// If we get a new EntityServer activated, reset lastQueried time
|
|
// so we will do a proper query during update
|
|
if (node->getType() == NodeType::EntityServer) {
|
|
_queryExpiry = SteadyClock::now();
|
|
_octreeQuery.incrementConnectionID();
|
|
|
|
if (!_failedToConnectToEntityServer) {
|
|
_entityServerConnectionTimer.stop();
|
|
}
|
|
}
|
|
|
|
if (node->getType() == NodeType::AudioMixer && !isInterstitialMode()) {
|
|
DependencyManager::get<AudioClient>()->negotiateAudioFormat();
|
|
}
|
|
|
|
if (node->getType() == NodeType::AvatarMixer) {
|
|
_queryExpiry = SteadyClock::now();
|
|
|
|
// new avatar mixer, send off our identity packet on next update loop
|
|
// Reset skeletonModelUrl if the last server modified our choice.
|
|
// Override the avatar url (but not model name) here too.
|
|
if (_avatarOverrideUrl.isValid()) {
|
|
getMyAvatar()->useFullAvatarURL(_avatarOverrideUrl);
|
|
}
|
|
|
|
if (getMyAvatar()->getFullAvatarURLFromPreferences() != getMyAvatar()->getSkeletonModelURL()) {
|
|
getMyAvatar()->resetFullAvatarURL();
|
|
}
|
|
getMyAvatar()->markIdentityDataChanged();
|
|
getMyAvatar()->resetLastSent();
|
|
|
|
if (!isInterstitialMode()) {
|
|
// transmit a "sendAll" packet to the AvatarMixer we just connected to.
|
|
getMyAvatar()->sendAvatarDataPacket(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
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<AudioClient>().data(), "audioMixerKilled");
|
|
} else if (node->getType() == NodeType::EntityServer) {
|
|
// we lost an entity server, clear all of the domain octree details
|
|
clearDomainOctreeDetails(false);
|
|
} else if (node->getType() == NodeType::AssetServer) {
|
|
// asset server going away - check if we have the asset browser showing
|
|
|
|
#if !defined(DISABLE_QML)
|
|
auto offscreenUi = getOffscreenUI();
|
|
auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild<QQuickItem*>("AssetServer") : nullptr;
|
|
|
|
if (assetDialog) {
|
|
// call reload on the shown asset browser dialog
|
|
QMetaObject::invokeMethod(assetDialog, "clear");
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
void Application::handleSandboxStatus(QNetworkReply* reply) {
|
|
PROFILE_RANGE(render, __FUNCTION__);
|
|
|
|
bool sandboxIsRunning = SandboxUtils::readStatus(reply->readAll());
|
|
|
|
enum HandControllerType {
|
|
Vive,
|
|
Oculus
|
|
};
|
|
static const std::map<HandControllerType, int> MIN_CONTENT_VERSION = {
|
|
{ Vive, 1 },
|
|
{ Oculus, 27 }
|
|
};
|
|
|
|
// Get sandbox content set version
|
|
auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/";
|
|
auto contentVersionPath = acDirPath + "content-version.txt";
|
|
qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version";
|
|
int contentVersion = 0;
|
|
QFile contentVersionFile(contentVersionPath);
|
|
if (contentVersionFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
QString line = contentVersionFile.readAll();
|
|
contentVersion = line.toInt(); // returns 0 if conversion fails
|
|
}
|
|
|
|
// Get controller availability
|
|
#ifdef ANDROID_APP_QUEST_INTERFACE
|
|
bool hasHandControllers = true;
|
|
#else
|
|
bool hasHandControllers = false;
|
|
if (PluginUtils::isViveControllerAvailable() || PluginUtils::isOculusTouchControllerAvailable()) {
|
|
hasHandControllers = true;
|
|
}
|
|
#endif
|
|
|
|
// Check HMD use (may be technically available without being in use)
|
|
bool hasHMD = PluginUtils::isHMDAvailable();
|
|
bool isUsingHMD = _displayPlugin->isHmd();
|
|
bool isUsingHMDAndHandControllers = hasHMD && hasHandControllers && isUsingHMD;
|
|
|
|
qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMDAndHandControllers;
|
|
|
|
QString addressLookupString;
|
|
|
|
// when --url in command line, teleport to location
|
|
if (!_urlParam.isEmpty()) { // Not sure if format supported by isValid().
|
|
if (_urlParam.scheme() == URL_SCHEME_OVERTEAPP) {
|
|
Setting::Handle<QVariant>("startUpApp").set(_urlParam.path());
|
|
} else {
|
|
addressLookupString = _urlParam.toString();
|
|
}
|
|
}
|
|
|
|
static const QString SENT_TO_PREVIOUS_LOCATION = "previous_location";
|
|
static const QString SENT_TO_ENTRY = "entry";
|
|
|
|
QString sentTo;
|
|
|
|
// If this is a first run we short-circuit the address passed in
|
|
if (_firstRun.get()) {
|
|
if (!BuildInfo::PRELOADED_STARTUP_LOCATION.isEmpty()) {
|
|
DependencyManager::get<LocationBookmarks>()->setHomeLocationToAddress(NetworkingConstants::DEFAULT_OVERTE_ADDRESS);
|
|
Menu::getInstance()->triggerOption(MenuOption::HomeLocation);
|
|
}
|
|
|
|
if (!_overrideEntry) {
|
|
DependencyManager::get<AddressManager>()->goToEntry();
|
|
sentTo = SENT_TO_ENTRY;
|
|
} else {
|
|
DependencyManager::get<AddressManager>()->loadSettings(addressLookupString);
|
|
sentTo = SENT_TO_PREVIOUS_LOCATION;
|
|
}
|
|
_firstRun.set(false);
|
|
} else {
|
|
QString goingTo = "";
|
|
if (addressLookupString.isEmpty()) {
|
|
if (Menu::getInstance()->isOptionChecked(MenuOption::HomeLocation)) {
|
|
auto locationBookmarks = DependencyManager::get<LocationBookmarks>();
|
|
addressLookupString = locationBookmarks->addressForBookmark(LocationBookmarks::HOME_BOOKMARK);
|
|
goingTo = "home location";
|
|
} else {
|
|
goingTo = "previous location";
|
|
}
|
|
}
|
|
qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(!goingTo.isEmpty() ? goingTo : addressLookupString);
|
|
DependencyManager::get<AddressManager>()->loadSettings(addressLookupString);
|
|
sentTo = SENT_TO_PREVIOUS_LOCATION;
|
|
}
|
|
|
|
UserActivityLogger::getInstance().logAction("startup_sent_to", {
|
|
{ "sent_to", sentTo },
|
|
{ "sandbox_is_running", sandboxIsRunning },
|
|
{ "has_hmd", hasHMD },
|
|
{ "has_hand_controllers", hasHandControllers },
|
|
{ "is_using_hmd", isUsingHMD },
|
|
{ "is_using_hmd_and_hand_controllers", isUsingHMDAndHandControllers },
|
|
{ "content_version", contentVersion }
|
|
});
|
|
|
|
_connectionMonitor.init();
|
|
}
|
|
|
|
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;
|
|
|
|
DependencyManager::prepareToExit();
|
|
|
|
if (tracing::enabled()) {
|
|
auto tracer = DependencyManager::get<tracing::Tracer>();
|
|
tracer->stopTracing();
|
|
auto outputFile = property(hifi::properties::TRACING).toString();
|
|
tracer->serialize(outputFile);
|
|
}
|
|
|
|
// Stop third party processes so that they're not left running in the event of a subsequent shutdown crash.
|
|
AnimDebugDraw::getInstance().shutdown();
|
|
|
|
// FIXME: once we move to shared pointer for the INputDevice we shoud remove this naked delete:
|
|
_applicationStateDevice.reset();
|
|
|
|
{
|
|
if (_keyboardFocusHighlightID != UNKNOWN_ENTITY_ID) {
|
|
DependencyManager::get<EntityScriptingInterface>()->deleteEntity(_keyboardFocusHighlightID);
|
|
_keyboardFocusHighlightID = UNKNOWN_ENTITY_ID;
|
|
}
|
|
}
|
|
|
|
{
|
|
auto nodeList = DependencyManager::get<NodeList>();
|
|
|
|
// send the domain a disconnect packet, force stoppage of domain-server check-ins
|
|
nodeList->getDomainHandler().disconnect("Quitting");
|
|
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();
|
|
QThreadPool::globalInstance()->waitForDone();
|
|
|
|
DependencyManager::destroy<RecordingScriptingInterface>();
|
|
|
|
// FIXME: Something is still holding on to the ScriptEnginePointers contained in ScriptEngines, and they hold backpointers to ScriptEngines,
|
|
// so this doesn't shut down properly
|
|
DependencyManager::get<ScriptEngines>()->shutdownScripting(); // stop all currently running global scripts
|
|
// These classes hold ScriptEnginePointers, so they must be destroyed before ScriptEngines
|
|
// Must be done after shutdownScripting in case any scripts try to access these things
|
|
{
|
|
DependencyManager::destroy<StandAloneJSConsole>();
|
|
EntityTreePointer tree = getEntities()->getTree();
|
|
tree->setSimulation(nullptr);
|
|
DependencyManager::destroy<EntityTreeRenderer>();
|
|
}
|
|
DependencyManager::destroy<ScriptEngines>();
|
|
|
|
bool keepMeLoggedIn = Setting::Handle<bool>(KEEP_ME_LOGGED_IN_SETTING_NAME, false).get();
|
|
if (!keepMeLoggedIn) {
|
|
DependencyManager::get<AccountManager>()->removeAccountFromFile();
|
|
}
|
|
// ####### TODO
|
|
|
|
_displayPlugin.reset();
|
|
PluginManager::getInstance()->shutdown();
|
|
|
|
// 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();
|
|
_window->saveGeometry();
|
|
|
|
// stop QML
|
|
DependencyManager::destroy<TabletScriptingInterface>();
|
|
DependencyManager::destroy<ToolbarScriptingInterface>();
|
|
DependencyManager::destroy<OffscreenUi>();
|
|
|
|
DependencyManager::destroy<OffscreenQmlSurfaceCache>();
|
|
|
|
// destroy Audio so it and its threads have a chance to go down safely
|
|
// this must happen after QML, as there are unexplained audio crashes originating in qtwebengine
|
|
AudioInjector::setLocalAudioInterface(nullptr);
|
|
DependencyManager::destroy<AudioClient>();
|
|
DependencyManager::destroy<AudioScriptingInterface>();
|
|
|
|
// The PointerManager must be destroyed before the PickManager because when a Pointer is deleted,
|
|
// it accesses the PickManager to delete its associated Pick
|
|
DependencyManager::destroy<PointerManager>();
|
|
DependencyManager::destroy<PickManager>();
|
|
DependencyManager::destroy<KeyboardScriptingInterface>();
|
|
DependencyManager::destroy<Keyboard>();
|
|
DependencyManager::destroy<AvatarPackager>();
|
|
|
|
qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete";
|
|
}
|
|
|
|
|
|
static const float FOCUS_HIGHLIGHT_EXPANSION_FACTOR = 1.05f;
|
|
void Application::idle() {
|
|
PerformanceTimer perfTimer("idle");
|
|
|
|
#if !defined(DISABLE_QML)
|
|
auto offscreenUi = getOffscreenUI();
|
|
|
|
// 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);
|
|
}
|
|
#endif
|
|
|
|
#ifdef Q_OS_WIN
|
|
{
|
|
// If tracing is enabled then monitor the CPU in a separate thread
|
|
static std::once_flag once;
|
|
std::call_once(once, [&] {
|
|
if (trace_app().isDebugEnabled()) {
|
|
QThread* cpuMonitorThread = new QThread(qApp);
|
|
cpuMonitorThread->setObjectName("cpuMonitorThread");
|
|
QObject::connect(cpuMonitorThread, &QThread::started, [this] { setupCpuMonitorThread(); });
|
|
QObject::connect(qApp, &QCoreApplication::aboutToQuit, cpuMonitorThread, &QThread::quit);
|
|
cpuMonitorThread->start();
|
|
}
|
|
});
|
|
}
|
|
#endif
|
|
|
|
auto displayPlugin = getActiveDisplayPlugin();
|
|
#if !defined(DISABLE_QML)
|
|
if (displayPlugin) {
|
|
auto uiSize = displayPlugin->getRecommendedUiSize();
|
|
// Bit of a hack since there's no device pixel ratio change event I can find.
|
|
if (offscreenUi->size() != fromGlm(uiSize)) {
|
|
qCDebug(interfaceapp) << "Device pixel ratio changed, triggering resize to " << uiSize;
|
|
offscreenUi->resize(fromGlm(uiSize));
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (displayPlugin) {
|
|
PROFILE_COUNTER_IF_CHANGED(app, "present", float, displayPlugin->presentRate());
|
|
}
|
|
PROFILE_COUNTER_IF_CHANGED(app, "renderLoopRate", float, getRenderLoopRate());
|
|
PROFILE_COUNTER_IF_CHANGED(app, "currentDownloads", uint32_t, ResourceCache::getLoadingRequests().length());
|
|
PROFILE_COUNTER_IF_CHANGED(app, "pendingDownloads", uint32_t, ResourceCache::getPendingRequestCount());
|
|
PROFILE_COUNTER_IF_CHANGED(app, "currentProcessing", int, DependencyManager::get<StatTracker>()->getStat("Processing").toInt());
|
|
PROFILE_COUNTER_IF_CHANGED(app, "pendingProcessing", int, DependencyManager::get<StatTracker>()->getStat("PendingProcessing").toInt());
|
|
auto renderConfig = _graphicsEngine->getRenderEngine()->getConfiguration();
|
|
PROFILE_COUNTER_IF_CHANGED(render, "gpuTime", float, (float)_graphicsEngine->getGPUContext()->getFrameTimerGPUAverage());
|
|
|
|
PROFILE_RANGE(app, __FUNCTION__);
|
|
|
|
if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) {
|
|
steamClient->runCallbacks();
|
|
}
|
|
|
|
if (auto oculusPlugin = PluginManager::getInstance()->getOculusPlatformPlugin()) {
|
|
oculusPlugin->handleOVREvents();
|
|
}
|
|
|
|
float secondsSinceLastUpdate = (float)_lastTimeUpdated.nsecsElapsed() / NSECS_PER_MSEC / MSECS_PER_SECOND;
|
|
_lastTimeUpdated.start();
|
|
|
|
#if !defined(DISABLE_QML)
|
|
// If the offscreen Ui has something active that is NOT the root, then assume it has keyboard focus.
|
|
if (offscreenUi && offscreenUi->getWindow()) {
|
|
auto activeFocusItem = offscreenUi->getWindow()->activeFocusItem();
|
|
if (_keyboardDeviceHasFocus && (activeFocusItem != NULL && activeFocusItem != offscreenUi->getRootItem())) {
|
|
_keyboardMouseDevice->pluginFocusOutEvent();
|
|
_keyboardDeviceHasFocus = false;
|
|
synthesizeKeyReleasEvents();
|
|
} else if (activeFocusItem == offscreenUi->getRootItem()) {
|
|
_keyboardDeviceHasFocus = true;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
checkChangeCursor();
|
|
|
|
#if !defined(DISABLE_QML)
|
|
auto stats = Stats::getInstance();
|
|
if (stats) {
|
|
stats->updateStats();
|
|
}
|
|
auto animStats = AnimStats::getInstance();
|
|
if (animStats) {
|
|
animStats->updateStats();
|
|
}
|
|
#endif
|
|
|
|
// 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.
|
|
#ifdef Q_OS_ANDROID
|
|
bool showWarnings = false;
|
|
#else
|
|
bool showWarnings = getLogger()->extraDebugging();
|
|
#endif
|
|
PerformanceWarning warn(showWarnings, "idle()");
|
|
|
|
{
|
|
_gameWorkload.updateViews(_viewFrustum, getMyAvatar()->getHeadPosition());
|
|
_gameWorkload._engine->run();
|
|
}
|
|
{
|
|
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));
|
|
}
|
|
|
|
{ // Update keyboard focus highlight
|
|
if (!_keyboardFocusedEntity.get().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) {
|
|
setKeyboardFocusEntity(UNKNOWN_ENTITY_ID);
|
|
} else {
|
|
if (auto entity = getEntities()->getTree()->findEntityByID(_keyboardFocusedEntity.get())) {
|
|
EntityItemProperties properties;
|
|
properties.setPosition(entity->getWorldPosition());
|
|
properties.setRotation(entity->getWorldOrientation());
|
|
properties.setDimensions(entity->getScaledDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR);
|
|
DependencyManager::get<EntityScriptingInterface>()->editEntity(_keyboardFocusHighlightID, properties);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
if (_keyboardFocusWaitingOnRenderable && getEntities()->renderableForEntityId(_keyboardFocusedEntity.get())) {
|
|
QUuid entityId = _keyboardFocusedEntity.get();
|
|
setKeyboardFocusEntity(UNKNOWN_ENTITY_ID);
|
|
_keyboardFocusWaitingOnRenderable = false;
|
|
setKeyboardFocusEntity(entityId);
|
|
}
|
|
}
|
|
|
|
{
|
|
PerformanceTimer perfTimer("pluginIdle");
|
|
PerformanceWarning warn(showWarnings, "Application::idle()... pluginIdle()");
|
|
getActiveDisplayPlugin()->idle();
|
|
const auto& inputPlugins = PluginManager::getInstance()->getInputPlugins();
|
|
for(const auto& inputPlugin : inputPlugins) {
|
|
if (inputPlugin->isActive()) {
|
|
inputPlugin->idle();
|
|
}
|
|
}
|
|
}
|
|
|
|
_overlayConductor.update(secondsSinceLastUpdate);
|
|
|
|
_gameLoopCounter.increment();
|
|
|
|
{
|
|
static std::once_flag once;
|
|
std::call_once(once, [] {
|
|
const QString& bookmarksError = DependencyManager::get<AvatarBookmarks>()->getBookmarkError();
|
|
if (!bookmarksError.isEmpty()) {
|
|
OffscreenUi::asyncWarning("Avatar Bookmarks Error", "JSON parse error: " + bookmarksError, QMessageBox::Ok, QMessageBox::Ok);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void Application::update(float deltaTime) {
|
|
PROFILE_RANGE_EX(app, __FUNCTION__, 0xffff0000, (uint64_t)_graphicsEngine->_renderFrameCount + 1);
|
|
|
|
if (_aboutToQuit) {
|
|
return;
|
|
}
|
|
|
|
if (!_physicsEnabled) {
|
|
if (!_domainLoadingInProgress) {
|
|
PROFILE_ASYNC_BEGIN(app, "Scene Loading", "");
|
|
_domainLoadingInProgress = true;
|
|
}
|
|
|
|
// we haven't yet enabled physics. we wait until we think we have all the collision information
|
|
// for nearby entities before starting bullet up.
|
|
if (isServerlessMode() && !_waitForServerlessToBeSet) {
|
|
tryToEnablePhysics();
|
|
} else if (_failedToConnectToEntityServer) {
|
|
if (_octreeProcessor->safeLandingIsActive()) {
|
|
_octreeProcessor->stopSafeLanding();
|
|
}
|
|
} else {
|
|
_octreeProcessor->updateSafeLanding();
|
|
if (_octreeProcessor->safeLandingIsComplete()) {
|
|
tryToEnablePhysics();
|
|
}
|
|
}
|
|
} else if (_domainLoadingInProgress) {
|
|
_domainLoadingInProgress = false;
|
|
PROFILE_ASYNC_END(app, "Scene Loading", "");
|
|
}
|
|
|
|
if (shouldCaptureMouse()) {
|
|
QPoint point = _glWidget->mapToGlobal(_glWidget->geometry().center());
|
|
if (QCursor::pos() != point) {
|
|
_mouseCaptureTarget = point;
|
|
_ignoreMouseMove = true;
|
|
if (_captureMouse) {
|
|
_keyboardMouseDevice->updateMousePositionForCapture(QCursor::pos(), _mouseCaptureTarget);
|
|
}
|
|
QCursor::setPos(point);
|
|
}
|
|
}
|
|
|
|
auto myAvatar = getMyAvatar();
|
|
{
|
|
PerformanceTimer perfTimer("devices");
|
|
auto userInputMapper = DependencyManager::get<UserInputMapper>();
|
|
|
|
controller::HmdAvatarAlignmentType hmdAvatarAlignmentType;
|
|
if (myAvatar->getHmdAvatarAlignmentType() == "eyes") {
|
|
hmdAvatarAlignmentType = controller::HmdAvatarAlignmentType::Eyes;
|
|
} else {
|
|
hmdAvatarAlignmentType = controller::HmdAvatarAlignmentType::Head;
|
|
}
|
|
|
|
controller::InputCalibrationData calibrationData = {
|
|
myAvatar->getSensorToWorldMatrix(),
|
|
createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition()),
|
|
myAvatar->getHMDSensorMatrix(),
|
|
myAvatar->getCenterEyeCalibrationMat(),
|
|
myAvatar->getHeadCalibrationMat(),
|
|
myAvatar->getSpine2CalibrationMat(),
|
|
myAvatar->getHipsCalibrationMat(),
|
|
myAvatar->getLeftFootCalibrationMat(),
|
|
myAvatar->getRightFootCalibrationMat(),
|
|
myAvatar->getRightArmCalibrationMat(),
|
|
myAvatar->getLeftArmCalibrationMat(),
|
|
myAvatar->getRightHandCalibrationMat(),
|
|
myAvatar->getLeftHandCalibrationMat(),
|
|
hmdAvatarAlignmentType
|
|
};
|
|
|
|
InputPluginPointer keyboardMousePlugin;
|
|
for(const auto& inputPlugin : PluginManager::getInstance()->getInputPlugins()) {
|
|
if (inputPlugin->getName() == KeyboardMouseDevice::NAME) {
|
|
keyboardMousePlugin = inputPlugin;
|
|
} else if (inputPlugin->isActive()) {
|
|
inputPlugin->pluginUpdate(deltaTime, calibrationData);
|
|
}
|
|
}
|
|
|
|
userInputMapper->setInputCalibrationData(calibrationData);
|
|
userInputMapper->update(deltaTime);
|
|
|
|
if (keyboardMousePlugin && keyboardMousePlugin->isActive()) {
|
|
keyboardMousePlugin->pluginUpdate(deltaTime, calibrationData);
|
|
}
|
|
// 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 && !isInterstitialMode()) {
|
|
if (!_controllerScriptingInterface->areActionsCaptured() && _myCamera.getMode() != CAMERA_MODE_MIRROR) {
|
|
myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z));
|
|
myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y));
|
|
myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X));
|
|
if (deltaTime > FLT_EPSILON && userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z) == 0.0f) {
|
|
myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH));
|
|
myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW));
|
|
myAvatar->setDriveKey(MyAvatar::DELTA_PITCH, -_myCamera.getSensitivity() * userInputMapper->getActionState(controller::Action::DELTA_PITCH));
|
|
myAvatar->setDriveKey(MyAvatar::DELTA_YAW, -_myCamera.getSensitivity() * userInputMapper->getActionState(controller::Action::DELTA_YAW));
|
|
myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW));
|
|
}
|
|
}
|
|
myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z));
|
|
}
|
|
|
|
myAvatar->setSprintMode((bool)userInputMapper->getActionState(controller::Action::SPRINT));
|
|
static const std::vector<controller::Action> avatarControllerActions = {
|
|
controller::Action::LEFT_HAND,
|
|
controller::Action::RIGHT_HAND,
|
|
controller::Action::LEFT_FOOT,
|
|
controller::Action::RIGHT_FOOT,
|
|
controller::Action::HIPS,
|
|
controller::Action::SPINE2,
|
|
controller::Action::HEAD,
|
|
controller::Action::LEFT_HAND_THUMB1,
|
|
controller::Action::LEFT_HAND_THUMB2,
|
|
controller::Action::LEFT_HAND_THUMB3,
|
|
controller::Action::LEFT_HAND_THUMB4,
|
|
controller::Action::LEFT_HAND_INDEX1,
|
|
controller::Action::LEFT_HAND_INDEX2,
|
|
controller::Action::LEFT_HAND_INDEX3,
|
|
controller::Action::LEFT_HAND_INDEX4,
|
|
controller::Action::LEFT_HAND_MIDDLE1,
|
|
controller::Action::LEFT_HAND_MIDDLE2,
|
|
controller::Action::LEFT_HAND_MIDDLE3,
|
|
controller::Action::LEFT_HAND_MIDDLE4,
|
|
controller::Action::LEFT_HAND_RING1,
|
|
controller::Action::LEFT_HAND_RING2,
|
|
controller::Action::LEFT_HAND_RING3,
|
|
controller::Action::LEFT_HAND_RING4,
|
|
controller::Action::LEFT_HAND_PINKY1,
|
|
controller::Action::LEFT_HAND_PINKY2,
|
|
controller::Action::LEFT_HAND_PINKY3,
|
|
controller::Action::LEFT_HAND_PINKY4,
|
|
controller::Action::RIGHT_HAND_THUMB1,
|
|
controller::Action::RIGHT_HAND_THUMB2,
|
|
controller::Action::RIGHT_HAND_THUMB3,
|
|
controller::Action::RIGHT_HAND_THUMB4,
|
|
controller::Action::RIGHT_HAND_INDEX1,
|
|
controller::Action::RIGHT_HAND_INDEX2,
|
|
controller::Action::RIGHT_HAND_INDEX3,
|
|
controller::Action::RIGHT_HAND_INDEX4,
|
|
controller::Action::RIGHT_HAND_MIDDLE1,
|
|
controller::Action::RIGHT_HAND_MIDDLE2,
|
|
controller::Action::RIGHT_HAND_MIDDLE3,
|
|
controller::Action::RIGHT_HAND_MIDDLE4,
|
|
controller::Action::RIGHT_HAND_RING1,
|
|
controller::Action::RIGHT_HAND_RING2,
|
|
controller::Action::RIGHT_HAND_RING3,
|
|
controller::Action::RIGHT_HAND_RING4,
|
|
controller::Action::RIGHT_HAND_PINKY1,
|
|
controller::Action::RIGHT_HAND_PINKY2,
|
|
controller::Action::RIGHT_HAND_PINKY3,
|
|
controller::Action::RIGHT_HAND_PINKY4,
|
|
controller::Action::LEFT_ARM,
|
|
controller::Action::RIGHT_ARM,
|
|
controller::Action::LEFT_SHOULDER,
|
|
controller::Action::RIGHT_SHOULDER,
|
|
controller::Action::LEFT_FORE_ARM,
|
|
controller::Action::RIGHT_FORE_ARM,
|
|
controller::Action::LEFT_LEG,
|
|
controller::Action::RIGHT_LEG,
|
|
controller::Action::LEFT_UP_LEG,
|
|
controller::Action::RIGHT_UP_LEG,
|
|
controller::Action::LEFT_TOE_BASE,
|
|
controller::Action::RIGHT_TOE_BASE,
|
|
controller::Action::LEFT_EYE,
|
|
controller::Action::RIGHT_EYE
|
|
|
|
};
|
|
|
|
// copy controller poses from userInputMapper to myAvatar.
|
|
glm::mat4 myAvatarMatrix = createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition());
|
|
glm::mat4 worldToSensorMatrix = glm::inverse(myAvatar->getSensorToWorldMatrix());
|
|
glm::mat4 avatarToSensorMatrix = worldToSensorMatrix * myAvatarMatrix;
|
|
for (auto& action : avatarControllerActions) {
|
|
controller::Pose pose = userInputMapper->getPoseState(action);
|
|
myAvatar->setControllerPoseInSensorFrame(action, pose.transform(avatarToSensorMatrix));
|
|
}
|
|
|
|
static const std::vector<QString> trackedObjectStringLiterals = {
|
|
QStringLiteral("_TrackedObject00"), QStringLiteral("_TrackedObject01"), QStringLiteral("_TrackedObject02"), QStringLiteral("_TrackedObject03"),
|
|
QStringLiteral("_TrackedObject04"), QStringLiteral("_TrackedObject05"), QStringLiteral("_TrackedObject06"), QStringLiteral("_TrackedObject07"),
|
|
QStringLiteral("_TrackedObject08"), QStringLiteral("_TrackedObject09"), QStringLiteral("_TrackedObject10"), QStringLiteral("_TrackedObject11"),
|
|
QStringLiteral("_TrackedObject12"), QStringLiteral("_TrackedObject13"), QStringLiteral("_TrackedObject14"), QStringLiteral("_TrackedObject15")
|
|
};
|
|
|
|
// Controlled by the Developer > Avatar > Show Tracked Objects menu.
|
|
if (_showTrackedObjects) {
|
|
static const std::vector<controller::Action> trackedObjectActions = {
|
|
controller::Action::TRACKED_OBJECT_00, controller::Action::TRACKED_OBJECT_01, controller::Action::TRACKED_OBJECT_02, controller::Action::TRACKED_OBJECT_03,
|
|
controller::Action::TRACKED_OBJECT_04, controller::Action::TRACKED_OBJECT_05, controller::Action::TRACKED_OBJECT_06, controller::Action::TRACKED_OBJECT_07,
|
|
controller::Action::TRACKED_OBJECT_08, controller::Action::TRACKED_OBJECT_09, controller::Action::TRACKED_OBJECT_10, controller::Action::TRACKED_OBJECT_11,
|
|
controller::Action::TRACKED_OBJECT_12, controller::Action::TRACKED_OBJECT_13, controller::Action::TRACKED_OBJECT_14, controller::Action::TRACKED_OBJECT_15
|
|
};
|
|
|
|
int i = 0;
|
|
glm::vec4 BLUE(0.0f, 0.0f, 1.0f, 1.0f);
|
|
for (auto& action : trackedObjectActions) {
|
|
controller::Pose pose = userInputMapper->getPoseState(action);
|
|
if (pose.valid) {
|
|
glm::vec3 pos = transformPoint(myAvatarMatrix, pose.translation);
|
|
glm::quat rot = glmExtractRotation(myAvatarMatrix) * pose.rotation;
|
|
DebugDraw::getInstance().addMarker(trackedObjectStringLiterals[i], rot, pos, BLUE);
|
|
} else {
|
|
DebugDraw::getInstance().removeMarker(trackedObjectStringLiterals[i]);
|
|
}
|
|
i++;
|
|
}
|
|
} else if (_prevShowTrackedObjects) {
|
|
for (auto& key : trackedObjectStringLiterals) {
|
|
DebugDraw::getInstance().removeMarker(key);
|
|
}
|
|
}
|
|
_prevShowTrackedObjects = _showTrackedObjects;
|
|
}
|
|
|
|
updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process...
|
|
updateDialogs(deltaTime); // update various stats dialogs if present
|
|
|
|
auto grabManager = DependencyManager::get<GrabManager>();
|
|
grabManager->simulateGrabs();
|
|
|
|
// TODO: break these out into distinct perfTimers when they prove interesting
|
|
{
|
|
PROFILE_RANGE(app, "PickManager");
|
|
PerformanceTimer perfTimer("pickManager");
|
|
DependencyManager::get<PickManager>()->update();
|
|
}
|
|
|
|
{
|
|
PROFILE_RANGE(app, "PointerManager");
|
|
PerformanceTimer perfTimer("pointerManager");
|
|
DependencyManager::get<PointerManager>()->update();
|
|
}
|
|
|
|
QSharedPointer<AvatarManager> avatarManager = DependencyManager::get<AvatarManager>();
|
|
|
|
{
|
|
PROFILE_RANGE(simulation_physics, "Simulation");
|
|
PerformanceTimer perfTimer("simulation");
|
|
|
|
getEntities()->preUpdate();
|
|
_entitySimulation->removeDeadEntities();
|
|
|
|
auto t0 = std::chrono::high_resolution_clock::now();
|
|
auto t1 = t0;
|
|
{
|
|
PROFILE_RANGE(simulation_physics, "PrePhysics");
|
|
PerformanceTimer perfTimer("prePhysics)");
|
|
{
|
|
PROFILE_RANGE(simulation_physics, "Entities");
|
|
PhysicsEngine::Transaction transaction;
|
|
_entitySimulation->buildPhysicsTransaction(transaction);
|
|
_physicsEngine->processTransaction(transaction);
|
|
_entitySimulation->handleProcessedPhysicsTransaction(transaction);
|
|
}
|
|
|
|
t1 = std::chrono::high_resolution_clock::now();
|
|
|
|
{
|
|
PROFILE_RANGE(simulation_physics, "Avatars");
|
|
PhysicsEngine::Transaction transaction;
|
|
avatarManager->buildPhysicsTransaction(transaction);
|
|
_physicsEngine->processTransaction(transaction);
|
|
avatarManager->handleProcessedPhysicsTransaction(transaction);
|
|
|
|
myAvatar->prepareForPhysicsSimulation();
|
|
myAvatar->getCharacterController()->preSimulation();
|
|
}
|
|
}
|
|
|
|
if (_physicsEnabled) {
|
|
{
|
|
PROFILE_RANGE(simulation_physics, "PrepareActions");
|
|
_entitySimulation->applyDynamicChanges();
|
|
_physicsEngine->forEachDynamic([&](EntityDynamicPointer dynamic) {
|
|
dynamic->prepareForPhysicsSimulation();
|
|
});
|
|
}
|
|
auto t2 = std::chrono::high_resolution_clock::now();
|
|
{
|
|
PROFILE_RANGE(simulation_physics, "StepPhysics");
|
|
PerformanceTimer perfTimer("stepPhysics");
|
|
getEntities()->getTree()->withWriteLock([&] {
|
|
_physicsEngine->stepSimulation();
|
|
});
|
|
}
|
|
auto t3 = std::chrono::high_resolution_clock::now();
|
|
{
|
|
if (_physicsEngine->hasOutgoingChanges()) {
|
|
{
|
|
PROFILE_RANGE(simulation_physics, "PostPhysics");
|
|
PerformanceTimer perfTimer("postPhysics");
|
|
// grab the collision events BEFORE handleChangedMotionStates() because at this point
|
|
// we have a better idea of which objects we own or should own.
|
|
auto& collisionEvents = _physicsEngine->getCollisionEvents();
|
|
|
|
getEntities()->getTree()->withWriteLock([&] {
|
|
PROFILE_RANGE(simulation_physics, "HandleChanges");
|
|
PerformanceTimer perfTimer("handleChanges");
|
|
|
|
const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates();
|
|
_entitySimulation->handleChangedMotionStates(outgoingChanges);
|
|
avatarManager->handleChangedMotionStates(outgoingChanges);
|
|
|
|
const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates();
|
|
_entitySimulation->handleDeactivatedMotionStates(deactivations);
|
|
});
|
|
|
|
// handleCollisionEvents() AFTER handleChangedMotionStates()
|
|
{
|
|
PROFILE_RANGE(simulation_physics, "CollisionEvents");
|
|
avatarManager->handleCollisionEvents(collisionEvents);
|
|
// Collision events (and their scripts) must not be handled when we're locked, above. (That would risk
|
|
// deadlock.)
|
|
_entitySimulation->handleCollisionEvents(collisionEvents);
|
|
}
|
|
|
|
{
|
|
PROFILE_RANGE(simulation_physics, "MyAvatar");
|
|
myAvatar->getCharacterController()->postSimulation();
|
|
myAvatar->harvestResultsFromPhysicsSimulation(deltaTime);
|
|
}
|
|
|
|
if (PerformanceTimer::isActive() &&
|
|
Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) &&
|
|
Menu::getInstance()->isOptionChecked(MenuOption::ExpandPhysicsTiming)) {
|
|
_physicsEngine->harvestPerformanceStats();
|
|
}
|
|
// NOTE: the PhysicsEngine stats are written to stdout NOT to Qt log framework
|
|
_physicsEngine->dumpStatsIfNecessary();
|
|
}
|
|
auto t4 = std::chrono::high_resolution_clock::now();
|
|
|
|
// NOTE: the getEntities()->update() call below will wait for lock
|
|
// and will provide non-physical entity motion
|
|
getEntities()->update(true); // update the models...
|
|
|
|
auto t5 = std::chrono::high_resolution_clock::now();
|
|
|
|
workload::Timings timings(6);
|
|
timings[0] = t1 - t0; // prePhysics entities
|
|
timings[1] = t2 - t1; // prePhysics avatars
|
|
timings[2] = t3 - t2; // stepPhysics
|
|
timings[3] = t4 - t3; // postPhysics
|
|
timings[4] = t5 - t4; // non-physical kinematics
|
|
timings[5] = workload::Timing_ns((int32_t)(NSECS_PER_SECOND * deltaTime)); // game loop duration
|
|
_gameWorkload.updateSimulationTimings(timings);
|
|
}
|
|
}
|
|
} else {
|
|
// update the rendering without any simulation
|
|
getEntities()->update(false);
|
|
}
|
|
// remove recently dead avatarEntities
|
|
SetOfEntities deadAvatarEntities;
|
|
_entitySimulation->takeDeadAvatarEntities(deadAvatarEntities);
|
|
avatarManager->removeDeadAvatarEntities(deadAvatarEntities);
|
|
}
|
|
|
|
// AvatarManager update
|
|
{
|
|
{
|
|
PROFILE_RANGE(simulation, "OtherAvatars");
|
|
PerformanceTimer perfTimer("otherAvatars");
|
|
avatarManager->updateOtherAvatars(deltaTime);
|
|
}
|
|
|
|
{
|
|
PROFILE_RANGE(simulation, "MyAvatar");
|
|
PerformanceTimer perfTimer("MyAvatar");
|
|
qApp->updateMyAvatarLookAtPosition(deltaTime);
|
|
avatarManager->updateMyAvatar(deltaTime);
|
|
}
|
|
}
|
|
|
|
bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings);
|
|
PerformanceWarning warn(showWarnings, "Application::update()");
|
|
|
|
updateLOD(deltaTime);
|
|
|
|
if (!_loginDialogID.isNull()) {
|
|
_loginStateManager.update(getMyAvatar()->getDominantHand(), _loginDialogID);
|
|
updateLoginDialogPosition();
|
|
}
|
|
|
|
{
|
|
PROFILE_RANGE_EX(app, "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);
|
|
_myCamera.loadViewFrustum(_viewFrustum);
|
|
|
|
_conicalViews.clear();
|
|
_conicalViews.push_back(_viewFrustum);
|
|
// TODO: Fix this by modeling the way the secondary camera works on how the main camera works
|
|
// ie. Use a camera object stored in the game logic and informs the Engine on where the secondary
|
|
// camera should be.
|
|
updateSecondaryCameraViewFrustum();
|
|
}
|
|
|
|
quint64 now = usecTimestampNow();
|
|
|
|
// Update my voxel servers with my current voxel query...
|
|
{
|
|
PROFILE_RANGE_EX(app, "QueryOctree", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount());
|
|
PerformanceTimer perfTimer("queryOctree");
|
|
QMutexLocker viewLocker(&_viewMutex);
|
|
|
|
bool viewIsDifferentEnough = false;
|
|
if (_conicalViews.size() == _lastQueriedViews.size()) {
|
|
for (size_t i = 0; i < _conicalViews.size(); ++i) {
|
|
if (!_conicalViews[i].isVerySimilar(_lastQueriedViews[i])) {
|
|
viewIsDifferentEnough = true;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
viewIsDifferentEnough = true;
|
|
}
|
|
|
|
|
|
// if it's been a while since our last query or the view has significantly changed then send a query, otherwise suppress it
|
|
static const std::chrono::seconds MIN_PERIOD_BETWEEN_QUERIES { 3 };
|
|
auto now = SteadyClock::now();
|
|
if (now > _queryExpiry || viewIsDifferentEnough) {
|
|
if (DependencyManager::get<SceneScriptingInterface>()->shouldRenderEntities()) {
|
|
queryOctree(NodeType::EntityServer, PacketType::EntityQuery);
|
|
}
|
|
queryAvatars();
|
|
|
|
_lastQueriedViews = _conicalViews;
|
|
_queryExpiry = now + MIN_PERIOD_BETWEEN_QUERIES;
|
|
}
|
|
}
|
|
|
|
// 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 && !isInterstitialMode()) {
|
|
_lastSendDownstreamAudioStats = now;
|
|
|
|
QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(), "sendDownstreamAudioStatsPacket", Qt::QueuedConnection);
|
|
}
|
|
}
|
|
|
|
{
|
|
PerformanceTimer perfTimer("avatarManager/postUpdate");
|
|
avatarManager->postUpdate(deltaTime, getMain3DScene());
|
|
}
|
|
|
|
{
|
|
PROFILE_RANGE_EX(app, "PostUpdateLambdas", 0xffff0000, (uint64_t)0);
|
|
PerformanceTimer perfTimer("postUpdateLambdas");
|
|
std::unique_lock<std::mutex> guard(_postUpdateLambdasLock);
|
|
for (auto& iter : _postUpdateLambdas) {
|
|
iter.second();
|
|
}
|
|
_postUpdateLambdas.clear();
|
|
}
|
|
|
|
|
|
updateRenderArgs(deltaTime);
|
|
|
|
{
|
|
PerformanceTimer perfTimer("AnimDebugDraw");
|
|
AnimDebugDraw::getInstance().update();
|
|
}
|
|
|
|
{ // Game loop is done, mark the end of the frame for the scene transactions and the render loop to take over
|
|
PerformanceTimer perfTimer("enqueueFrame");
|
|
getMain3DScene()->enqueueFrame();
|
|
}
|
|
|
|
// If the display plugin is inactive then the frames won't be processed so process them here.
|
|
if (!getActiveDisplayPlugin()->isActive()) {
|
|
getMain3DScene()->processTransactionQueue();
|
|
}
|
|
|
|
// decide if the sensorToWorldMatrix is changing in a way that warrents squeezing the edges of the view down
|
|
if (getActiveDisplayPlugin()->isHmd()) {
|
|
PerformanceTimer perfTimer("squeezeVision");
|
|
_visionSqueeze.updateVisionSqueeze(myAvatar->getSensorToWorldMatrix(), deltaTime);
|
|
}
|
|
}
|
|
|
|
|
|
void Application::updateLOD(float deltaTime) const {
|
|
PerformanceTimer perfTimer("LOD");
|
|
// adjust it unless we were asked to disable this feature, or if we're currently in throttleRendering mode
|
|
if (!isThrottleRendering()) {
|
|
float presentTime = getActiveDisplayPlugin()->getAveragePresentTime();
|
|
float engineRunTime = (float)(_graphicsEngine->getRenderEngine()->getConfiguration().get()->getCPURunTime());
|
|
float gpuTime = getGPUContext()->getFrameTimerGPUAverage();
|
|
float batchTime = getGPUContext()->getFrameTimerBatchAverage();
|
|
auto lodManager = DependencyManager::get<LODManager>();
|
|
lodManager->setRenderTimes(presentTime, engineRunTime, batchTime, gpuTime);
|
|
lodManager->autoAdjustLOD(deltaTime);
|
|
} else {
|
|
DependencyManager::get<LODManager>()->resetLODAdjust();
|
|
}
|
|
}
|
|
|
|
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::userKickConfirmation(const QUuid& nodeID, unsigned int banFlags) {
|
|
auto avatarHashMap = DependencyManager::get<AvatarHashMap>();
|
|
auto avatar = avatarHashMap->getAvatarBySessionID(nodeID);
|
|
|
|
QString userName;
|
|
|
|
if (avatar) {
|
|
userName = avatar->getSessionDisplayName();
|
|
} else {
|
|
userName = nodeID.toString();
|
|
}
|
|
|
|
QString kickMessage = "Do you wish to kick " + userName + " from your domain";
|
|
ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Kick User", kickMessage,
|
|
QMessageBox::Yes | QMessageBox::No);
|
|
|
|
if (dlg->getDialogItem()) {
|
|
|
|
QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) {
|
|
QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr);
|
|
|
|
bool yes = (static_cast<QMessageBox::StandardButton>(answer.toInt()) == QMessageBox::Yes);
|
|
// ask the NodeList to kick the user with the given session ID
|
|
|
|
if (yes) {
|
|
DependencyManager::get<NodeList>()->kickNodeBySessionID(nodeID, banFlags);
|
|
}
|
|
|
|
DependencyManager::get<UsersScriptingInterface>()->setWaitForKickResponse(false);
|
|
});
|
|
DependencyManager::get<UsersScriptingInterface>()->setWaitForKickResponse(true);
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<MyAvatar> Application::getMyAvatar() const {
|
|
return DependencyManager::get<AvatarManager>()->getMyAvatar();
|
|
}
|
|
|
|
void Application::checkSkeleton() const {
|
|
if (getMyAvatar()->getSkeletonModel()->isLoaded() && !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::asyncWarning("", message);
|
|
|
|
getMyAvatar()->useFullAvatarURL(AvatarData::defaultFullAvatarModelUrl(), DEFAULT_FULL_AVATAR_MODEL_NAME);
|
|
} else {
|
|
_physicsEngine->setCharacterController(getMyAvatar()->getCharacterController());
|
|
}
|
|
}
|
|
|
|
void Application::queryAvatars() {
|
|
if (!isInterstitialMode()) {
|
|
auto avatarPacket = NLPacket::create(PacketType::AvatarQuery);
|
|
auto destinationBuffer = reinterpret_cast<unsigned char*>(avatarPacket->getPayload());
|
|
unsigned char* bufferStart = destinationBuffer;
|
|
|
|
uint8_t numFrustums = (uint8_t)_conicalViews.size();
|
|
memcpy(destinationBuffer, &numFrustums, sizeof(numFrustums));
|
|
destinationBuffer += sizeof(numFrustums);
|
|
|
|
for (const auto& view : _conicalViews) {
|
|
destinationBuffer += view.serialize(destinationBuffer);
|
|
}
|
|
|
|
avatarPacket->setPayloadSize(destinationBuffer - bufferStart);
|
|
|
|
DependencyManager::get<NodeList>()->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer);
|
|
}
|
|
}
|
|
|
|
void Application::tryToEnablePhysics() {
|
|
bool enableInterstitial = DependencyManager::get<NodeList>()->getDomainHandler().getInterstitialModeEnabled();
|
|
|
|
if (gpuTextureMemSizeStable() || !enableInterstitial) {
|
|
_fullSceneCounterAtLastPhysicsCheck = _octreeProcessor->getFullSceneReceivedCounter();
|
|
_lastQueriedViews.clear(); // Force new view.
|
|
|
|
// 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.
|
|
auto myAvatar = getMyAvatar();
|
|
if (myAvatar->isReadyForPhysics()) {
|
|
myAvatar->getCharacterController()->setPhysicsEngine(_physicsEngine);
|
|
_octreeProcessor->resetSafeLanding();
|
|
_physicsEnabled = true;
|
|
setIsInterstitialMode(false);
|
|
myAvatar->updateMotionBehaviorFromMenu();
|
|
}
|
|
}
|
|
}
|